Add gated delivery lifecycle

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 18:48:48 -04:00
1 parent 73a4251054
commit 5a06debfef
13 files changed
+671 -3

No files matched your search

+28
View File
@@ -48,6 +48,9 @@ and the first Claude reference-fleet path:
- a native integration queue with durable authorization and fast-forward main protection;
- domain-owned convergence plans that defer during checks, measurements, pins, dirty work, and Git operations;
- a local bare canonical Git option with honest one-disk/local-remote grading;
- a durable delivery state machine that keeps implemented, verified,
integrated, release-ready, released, deployed, and live-verified distinct,
with per-state authority and provider-capability gates;
- mutation audit events tied to project and Fleet identity.
- isolated candidate version stores with explicit compatibility declarations;
- acceptance-gated atomic activation, crash reconciliation, and exact rollback.
@@ -268,6 +271,31 @@ checks, backlog, worktrees, and policy remain operational. Detach works during
that outage and restores the prior local canonical configuration; it does not
delete a user-owned remote.
Delivery is separate from integration. A clean domain tip starts as
implemented; exact-commit checks gate verification, and a landed integration
record gates integration. Release, deployment, and live verification require
owner authority, explicit approval, and successful adapter receipts. Fresh
installs configure no delivery providers, so TORCH cannot publish or deploy by
accident:
```bash
torch delivery create --area backend --label "API change" \
--evidence "task:TASK-42" --json
torch delivery plan --delivery <id> --to verified --by backend --json
torch delivery transition --delivery <id> --to verified --by backend \
--evidence "check:<receipt-id>" --json
torch delivery configure --release-provider <adapter> \
--deployment-provider <adapter> --dry-run --json
torch delivery configure --release-provider <adapter> \
--deployment-provider <adapter> --yes --json
torch delivery transition --delivery <id> --to released --by owner \
--evidence "artifact:<reference>" --yes --json
```
Adapter names configure policy; a trusted installed adapter must also declare
and implement the requested capability. The stock CLI ships with no release or
deployment provider, so configuration alone cannot trigger an external action.
TORCH source checkouts can stage a self-host candidate without changing the
active version. Candidate build runs the complete local acceptance, lint, and
syntax gates before writing its receipt:
+12
View File
@@ -1217,6 +1217,18 @@ configuration MUST state who may perform each transition. Secrets are resolved
at execution time from approved external stores and never copied into tracked
Fleet state.
Implementation checkpoint (2026-09-27): TORCH now persists this complete state
machine in the local control plane. Creation requires a clean, exact source
worktree tip and evidence; verification requires the configured exact-commit
check receipts; integration requires a landed request for that commit; and
release, deployment, and live verification require separately configured
provider capabilities, state-specific authority, evidence, successful adapter
receipts, and explicit owner approval. Fresh installations configure both
provider slots as `none`, so no external release or deployment can occur by
default. The CLI and read-only Fleet observation surface are implemented and
scenario-tested with a virtual adapter; no live release or deployment was
performed.
## 25. Security and trust boundaries
TORCH MUST:
+4 -2
View File
@@ -15,6 +15,7 @@
"./console": "./src/console/server.mjs",
"./design/brief": "./src/design/brief.mjs",
"./design/architect": "./src/design/architect.mjs",
"./delivery": "./src/delivery/service.mjs",
"./evolution": "./src/evolution/service.mjs",
"./forge": "./src/forge/service.mjs",
"./control-plane": "./src/control-plane/service.mjs",
@@ -66,6 +67,7 @@
"src/convergence/",
"src/control-plane/",
"src/design/",
"src/delivery/",
"src/evolution/",
"src/forge/",
"src/kernel/",
@@ -90,8 +92,8 @@
"torch": "node bin/torch.mjs",
"check": "node --check bin/torch.mjs && node --check src/cli.mjs && node --check src/mcp/server.mjs && node --check src/console/server.mjs && node --check site/app.js && node --check site/console.js",
"site:serve": "node bin/torch.mjs console serve",
"format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix",
"lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet"
"format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix",
"lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.1.0",
@@ -134,6 +134,10 @@ test_integrity_note:
given: a converged domain commit with every required exact-SHA receipt
when: the domain requests integration and the Session Manager authorizes and lands it
then: main fast-forwards only while source, target, receipts, cleanliness, and authority remain valid
- id: SCN-delivery-lifecycle
given: a clean implemented domain commit, exact check receipt, landed integration, and a virtual provider adapter
when: TORCH advances it through verified, integrated, release-ready, released, deployed, and live-verified
then: states cannot be skipped, authority remains separate, high-impact transitions need explicit approval, adapters declare capabilities, receipts persist, and CLI plus observation expose the same state
- id: SCN-safe-convergence
given: a managed domain worktree behind canonical main
when: a check, measurement, pin, dirty state, or Git operation could make convergence unsafe
@@ -253,6 +257,7 @@ test_integrity_note:
- immutable check receipt commit and result
- resource holder, FIFO position, expiry, and cancellation state
- canonical main SHA before and after authorized landing
- durable delivery state, authority blockers, adapter invocation order, and provider receipts
- Git remote configuration and recoverability grade
determinism_controls:
- isolated temporary Git repositories
@@ -295,6 +300,8 @@ test_integrity_note:
- passing receipts detached from the commit actually measured
- unfair or automatically stolen resource leases
- integration based on branch labels, stale checks, or stale main
- integration authority being treated as release or deployment authority
- release, deployment, or live claims without explicit adapter receipts
- a same-disk bare repository mislabeled as off-machine recovery
- a merely passing command being mislabeled as complete candidate acceptance
- candidate staging silently replacing stable or accepted code changing before activation
+25
View File
@@ -0,0 +1,25 @@
import { TorchError } from '../kernel/errors.mjs';
export const DELIVERY_OPERATIONS = Object.freeze(['release', 'deploy', 'verifyLive']);
export function validateDeliveryAdapter(adapter) {
if (!adapter || typeof adapter.name !== 'string' || !adapter.capabilities) {
throw new TorchError('Delivery adapter is missing its name or capability declaration', {
code: 'INVALID_DELIVERY_ADAPTER',
});
}
for (const operation of DELIVERY_OPERATIONS) {
if (!(operation in adapter.capabilities) || typeof adapter[operation] !== 'function') {
throw new TorchError(`Delivery adapter ${adapter.name} does not declare and implement ${operation}`, {
code: 'INVALID_DELIVERY_ADAPTER', details: { operation },
});
}
}
return adapter;
}
export function unsupportedDeliveryCapability(adapterName, operation) {
throw new TorchError(`${adapterName} delivery adapter does not support ${operation}`, {
code: 'DELIVERY_CAPABILITY_UNSUPPORTED', details: { adapter: adapterName, operation },
});
}
+64
View File
@@ -38,6 +38,9 @@ import { ConvergenceService } from './convergence/service.mjs';
import {
attachForge, detachForge, forgeStatus, planForgeAttach, planForgeDetach,
} from './forge/service.mjs';
import {
configureDelivery, DeliveryService, planDeliveryConfiguration,
} from './delivery/service.mjs';
const HELP = `TORCH — portable agent fleet
@@ -103,6 +106,12 @@ Usage:
torch forge attach --remote <name> [--provider <name>] --yes [--json]
torch forge status [--json]
torch forge detach [--dry-run] --yes [--json]
torch delivery list [--state <state>] [--json]
torch delivery get --delivery <id> [--json]
torch delivery create --area <id> [--commit <sha>] --label <text> --evidence <text,...> [--json]
torch delivery plan --delivery <id> --to <state> --by <id|owner> [--json]
torch delivery transition --delivery <id> --to <state> --by <id|owner> --evidence <text,...> [--yes] [--json]
torch delivery configure [--release-provider <name>] [--deployment-provider <name>] [--dry-run] --yes [--json]
torch candidate plan --source <path> [--version <version>] [--json]
torch candidate build --source <path> [--version <version>] --yes [--json]
torch candidate status [--json]
@@ -239,6 +248,7 @@ function runCandidateAcceptance(candidateRoot, spawn) {
['fleet-boundary-evolution', ['SCN-fleet-boundary-evolution']],
['cli-lifecycle-surface', ['SCN-cli-lifecycle-surface']],
['forge-migration', ['SCN-forge-migration']],
['delivery-lifecycle', ['SCN-delivery-lifecycle']],
['review-install-roster', ['SCN-cli-domain-review']],
['branches-worktrees', ['SCN-worktree-bootstrap']],
['runtime-identities', ['SCN-mixed-runtime']],
@@ -1078,6 +1088,60 @@ export async function runCli(argv = process.argv.slice(2), {
}
throw new TorchError(`Unknown forge operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
}
if (command === 'delivery') {
const operation = argv[1] ?? 'list';
if (operation === 'configure') {
const input = {
repositoryRoot: repository.root,
releaseProvider: optionValue(argv, '--release-provider') ?? 'none',
deploymentProvider: optionValue(argv, '--deployment-provider') ?? 'none',
};
if (argv.includes('--dry-run')) {
const plan = planDeliveryConfiguration(input);
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Delivery adapter configuration changes tracked project policy. Review --dry-run, then use --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(configureDelivery(input), { json });
return 0;
}
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
const checks = new CheckService({
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
});
const delivery = new DeliveryService({
repositoryRoot: repository.root, controlPlane: control, checkService: checks,
});
const deliveryId = optionValue(argv, '--delivery');
if (operation === 'list') print({ deliveries: delivery.list({ state: optionValue(argv, '--state') }) }, { json });
else if (operation === 'get') print(delivery.get(deliveryId), { json });
else if (operation === 'create') print(delivery.create({
sourceArea: optionValue(argv, '--area'), commit: optionValue(argv, '--commit') ?? 'HEAD',
label: optionValue(argv, '--label'), evidence: commaList(optionValue(argv, '--evidence')),
}), { json });
else if (operation === 'plan') {
const plan = delivery.planTransition({
deliveryId, targetState: optionValue(argv, '--to'), actor: optionValue(argv, '--by'),
});
print(plan, { json });
return plan.canProceed ? 0 : 1;
} else if (operation === 'transition') {
print(delivery.transition({
deliveryId, targetState: optionValue(argv, '--to'), actor: optionValue(argv, '--by'),
evidence: commaList(optionValue(argv, '--evidence')), approved: argv.includes('--yes'),
}), { json });
} else throw new TorchError(`Unknown delivery operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'uninstall') {
const result = uninstallProject({
repository,
+317
View File
@@ -0,0 +1,317 @@
import { spawnSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { renameSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
import { loadProjectConfig } from '../kernel/config.mjs';
import { readInstallManifest, writeInstallManifest } from '../kernel/install.mjs';
import { fileHash } from '../kernel/files.mjs';
import { validateDeliveryAdapter } from '../adapters/delivery.mjs';
export const DELIVERY_STATES = Object.freeze([
'implemented', 'verified', 'integrated', 'release-ready', 'released', 'deployed', 'live-verified',
]);
const HIGH_IMPACT_STATES = new Set(['released', 'deployed', 'live-verified']);
function text(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} must be a non-empty string`, {
code: 'DELIVERY_INPUT_INVALID', details: { field: name },
});
}
return value.trim();
}
function textList(value, name) {
if (!Array.isArray(value) || value.length === 0
|| value.some((entry) => typeof entry !== 'string' || !entry.trim())) {
throw new TorchError(`${name} must be a non-empty array of strings`, {
code: 'DELIVERY_INPUT_INVALID', details: { field: name },
});
}
return [...new Set(value.map((entry) => entry.trim()))];
}
function git(root, args) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
if (result.status !== 0) throw new TorchError('Delivery Git inspection failed', {
code: 'GIT_COMMAND_FAILED', details: result.stderr?.trim() || null,
});
return result.stdout.trim();
}
function initialize(database) {
database.exec(`
CREATE TABLE IF NOT EXISTS deliveries (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
label TEXT NOT NULL,
source_area TEXT NOT NULL,
commit_sha TEXT NOT NULL,
state TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
evidence_json TEXT NOT NULL,
adapter_receipts_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS delivery_events (
id TEXT PRIMARY KEY,
delivery_id TEXT NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,
from_state TEXT,
to_state TEXT NOT NULL,
actor TEXT NOT NULL,
created_at TEXT NOT NULL,
evidence_json TEXT NOT NULL,
adapter_receipt_json TEXT
);
CREATE INDEX IF NOT EXISTS deliveries_state_updated ON deliveries(state, updated_at, id);
`);
}
function hasTable(database, name) {
return Boolean(database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name));
}
function rowToDelivery(row) {
if (!row) return null;
return {
id: row.id, projectId: row.project_id, label: row.label, sourceArea: row.source_area,
commit: row.commit_sha, state: row.state, createdAt: row.created_at, updatedAt: row.updated_at,
evidence: JSON.parse(row.evidence_json), adapterReceipts: JSON.parse(row.adapter_receipts_json),
};
}
function adapterRequirement(config, targetState) {
if (targetState === 'released') {
return { slot: 'release', operation: 'release', provider: config.delivery.adapters.release.provider };
}
if (targetState === 'deployed') {
return { slot: 'deployment', operation: 'deploy', provider: config.delivery.adapters.deployment.provider };
}
if (targetState === 'live-verified') {
return { slot: 'deployment', operation: 'verifyLive', provider: config.delivery.adapters.deployment.provider };
}
return null;
}
function configurationPaths(repositoryRoot) {
return { config: join(repositoryRoot, '.torch', 'torch.yaml') };
}
function ownedConfiguration(repositoryRoot, manifest) {
const record = manifest.created.find((entry) => entry.path === '.torch/torch.yaml');
return Boolean(record && fileHash(configurationPaths(repositoryRoot).config) === record.sha256);
}
export function planDeliveryConfiguration({
repositoryRoot, releaseProvider = 'none', deploymentProvider = 'none',
} = {}) {
const release = text(releaseProvider, 'releaseProvider');
const deployment = text(deploymentProvider, 'deploymentProvider');
const config = loadProjectConfig(repositoryRoot);
const manifest = readInstallManifest(repositoryRoot);
const blockers = [];
if (!ownedConfiguration(repositoryRoot, manifest)) blockers.push({ code: 'DELIVERY_CONFIGURATION_MODIFIED' });
return {
action: 'configure-delivery-adapters',
current: {
release: config.delivery.adapters.release.provider,
deployment: config.delivery.adapters.deployment.provider,
},
requested: { release, deployment },
blockers, canProceed: blockers.length === 0, mutationPerformed: false,
};
}
export function configureDelivery(input = {}) {
const plan = planDeliveryConfiguration(input);
if (!plan.canProceed) throw new TorchError('Delivery adapter configuration is blocked', {
code: 'DELIVERY_CONFIGURATION_BLOCKED', details: plan.blockers,
});
const config = loadProjectConfig(input.repositoryRoot);
const manifest = readInstallManifest(input.repositoryRoot);
config.delivery.adapters.release.provider = plan.requested.release;
config.delivery.adapters.deployment.provider = plan.requested.deployment;
const path = configurationPaths(input.repositoryRoot).config;
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, {
encoding: 'utf8', mode: 0o600, flag: 'wx',
});
renameSync(temporary, path);
const record = manifest.created.find((entry) => entry.path === '.torch/torch.yaml');
record.sha256 = fileHash(path);
writeInstallManifest(input.repositoryRoot, manifest);
return { ...plan, configuredAt: new Date().toISOString(), mutationPerformed: true };
}
export class DeliveryService {
constructor({
repositoryRoot, controlPlane, checkService = null, adapters = new Map(),
clock = () => new Date(), idFactory = randomUUID,
} = {}) {
if (!controlPlane) throw new TorchError('Delivery lifecycle requires the TORCH control plane', { code: 'CONTROL_PLANE_REQUIRED' });
this.repositoryRoot = repositoryRoot;
this.controlPlane = controlPlane;
this.checkService = checkService;
this.adapters = adapters instanceof Map ? adapters : new Map(Object.entries(adapters));
this.clock = clock;
this.idFactory = idFactory;
this.database = controlPlane.database;
this.config = loadProjectConfig(repositoryRoot);
this.manifest = readInstallManifest(repositoryRoot);
this.worktrees = new Map((this.manifest.external ?? [])
.filter((entry) => entry.type === 'worktree').map((entry) => [entry.area, entry]));
initialize(this.database);
}
list({ state } = {}) {
if (state && !DELIVERY_STATES.includes(state)) {
throw new TorchError(`Unknown delivery state: ${state}`, { code: 'DELIVERY_STATE_INVALID' });
}
const rows = state
? this.database.prepare('SELECT * FROM deliveries WHERE state = ? ORDER BY updated_at, id').all(state)
: this.database.prepare('SELECT * FROM deliveries ORDER BY updated_at, id').all();
return rows.map(rowToDelivery);
}
get(deliveryId) {
const id = text(deliveryId, 'deliveryId');
const delivery = rowToDelivery(this.database.prepare('SELECT * FROM deliveries WHERE id = ?').get(id));
if (!delivery) throw new TorchError(`Unknown delivery: ${id}`, { code: 'DELIVERY_NOT_FOUND' });
return delivery;
}
create({ sourceArea, commit = 'HEAD', label, evidence } = {}) {
const area = this.controlPlane.assertIdentity(sourceArea);
if (area === 'session-manager') {
throw new TorchError('A delivery source must be a development domain', { code: 'DELIVERY_SOURCE_INVALID' });
}
const worktree = this.worktrees.get(area);
if (!worktree) throw new TorchError(`No managed worktree for ${area}`, { code: 'WORKTREE_MISSING' });
const resolvedCommit = git(worktree.path, ['rev-parse', text(commit, 'commit')]);
const head = git(worktree.path, ['rev-parse', 'HEAD']);
const dirty = git(worktree.path, ['status', '--porcelain']).split('\n').filter(Boolean);
if (resolvedCommit !== head || dirty.length) {
throw new TorchError('Delivery implementation must name the clean source worktree HEAD', {
code: 'DELIVERY_SOURCE_UNSAFE', details: { resolvedCommit, head, dirty },
});
}
const now = this.clock().toISOString();
const record = {
id: this.idFactory(), projectId: this.controlPlane.projectId, label: text(label, 'label'),
sourceArea: area, commit: resolvedCommit, state: 'implemented', createdAt: now, updatedAt: now,
evidence: textList(evidence, 'evidence'), adapterReceipts: [],
};
this.database.prepare(`INSERT INTO deliveries (
id, project_id, label, source_area, commit_sha, state, created_at, updated_at,
evidence_json, adapter_receipts_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(record.id, record.projectId, record.label, record.sourceArea, record.commit,
record.state, now, now, JSON.stringify(record.evidence), '[]');
this.database.prepare(`INSERT INTO delivery_events (
id, delivery_id, from_state, to_state, actor, created_at, evidence_json, adapter_receipt_json
) VALUES (?, ?, NULL, 'implemented', ?, ?, ?, NULL)`)
.run(this.idFactory(), record.id, area, now, JSON.stringify(record.evidence));
this.controlPlane.audit({
actorId: area, operation: 'delivery.create', entityType: 'delivery', entityId: record.id,
details: { commit: record.commit, state: record.state },
});
return record;
}
planTransition({ deliveryId, targetState, actor } = {}) {
const delivery = this.get(deliveryId);
const target = text(targetState, 'targetState');
const currentIndex = DELIVERY_STATES.indexOf(delivery.state);
if (DELIVERY_STATES[currentIndex + 1] !== target) {
throw new TorchError(`Delivery must advance from ${delivery.state} to ${DELIVERY_STATES[currentIndex + 1] ?? '<terminal>'}`, {
code: 'DELIVERY_TRANSITION_INVALID', details: { current: delivery.state, target },
});
}
const principal = text(actor, 'actor');
if (principal !== 'owner') this.controlPlane.assertIdentity(principal);
const authorityKey = target.replaceAll('-', '_');
const configuredAuthority = this.config.delivery.authority[authorityKey] ?? [];
const authorizedActors = configuredAuthority.map((entry) => entry === '$source' ? delivery.sourceArea : entry);
const blockers = [];
if (!authorizedActors.includes(principal)) blockers.push({ code: 'DELIVERY_AUTHORITY_REQUIRED', actor: principal, allowed: authorizedActors });
if (target === 'verified') {
const requiredChecks = this.config.integration.required_checks;
if (requiredChecks.length && !this.checkService?.exactPasses({ commit: delivery.commit, requiredChecks })) {
blockers.push({ code: 'DELIVERY_CHECKS_REQUIRED', commit: delivery.commit, checks: requiredChecks });
}
}
if (target === 'integrated') {
const landed = hasTable(this.database, 'integration_requests')
? this.database.prepare(`SELECT id FROM integration_requests
WHERE source_commit = ? AND state = 'landed' LIMIT 1`).get(delivery.commit)
: null;
if (!landed) blockers.push({ code: 'DELIVERY_INTEGRATION_REQUIRED', commit: delivery.commit });
}
const requirement = adapterRequirement(this.config, target);
if (requirement) {
if (requirement.provider === 'none') blockers.push({
code: 'DELIVERY_ADAPTER_NOT_CONFIGURED', slot: requirement.slot, operation: requirement.operation,
});
else {
const adapter = this.adapters.get(requirement.provider) ?? null;
if (!adapter) blockers.push({ code: 'DELIVERY_ADAPTER_MISSING', provider: requirement.provider });
else {
validateDeliveryAdapter(adapter);
if (adapter.capabilities[requirement.operation] !== true) blockers.push({
code: 'DELIVERY_CAPABILITY_UNSUPPORTED', provider: requirement.provider, operation: requirement.operation,
});
}
}
}
return {
action: 'delivery-transition', deliveryId: delivery.id, from: delivery.state, to: target,
actor: principal, authority: authorizedActors, adapter: requirement ? {
slot: requirement.slot, provider: requirement.provider, operation: requirement.operation,
} : null,
requiresApproval: HIGH_IMPACT_STATES.has(target), blockers,
canProceed: blockers.length === 0, mutationPerformed: false,
};
}
transition({ deliveryId, targetState, actor, evidence, approved = false } = {}) {
const plan = this.planTransition({ deliveryId, targetState, actor });
if (!plan.canProceed) throw new TorchError('Delivery transition is blocked', {
code: 'DELIVERY_TRANSITION_BLOCKED', details: plan.blockers,
});
if (plan.requiresApproval && !approved) throw new TorchError('High-impact delivery transition requires explicit owner approval', {
code: 'APPROVAL_REQUIRED', details: { state: plan.to },
});
const evidenceList = textList(evidence, 'evidence');
const delivery = this.get(deliveryId);
let receipt = null;
if (plan.adapter) {
const adapter = this.adapters.get(plan.adapter.provider);
receipt = adapter[plan.adapter.operation]({ delivery, evidence: evidenceList });
if (!receipt || receipt.status !== 'succeeded') throw new TorchError('Delivery adapter did not return a successful receipt', {
code: 'DELIVERY_ADAPTER_FAILED', details: { adapter: plan.adapter, receipt },
});
}
const now = this.clock().toISOString();
const receipts = [...delivery.adapterReceipts, ...(receipt ? [{ ...receipt, operation: plan.adapter.operation }] : [])];
const allEvidence = [...new Set([...delivery.evidence, ...evidenceList])];
const update = this.database.prepare(`UPDATE deliveries SET state = ?, updated_at = ?, evidence_json = ?,
adapter_receipts_json = ? WHERE id = ? AND state = ?`)
.run(plan.to, now, JSON.stringify(allEvidence), JSON.stringify(receipts), delivery.id, delivery.state);
if (update.changes !== 1) throw new TorchError('Delivery state changed concurrently', {
code: 'DELIVERY_STATE_CONFLICT', details: { deliveryId: delivery.id, expected: delivery.state },
});
this.database.prepare(`INSERT INTO delivery_events (
id, delivery_id, from_state, to_state, actor, created_at, evidence_json, adapter_receipt_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run(this.idFactory(), delivery.id, delivery.state, plan.to, plan.actor, now,
JSON.stringify(evidenceList), receipt ? JSON.stringify(receipt) : null);
this.controlPlane.audit({
actorId: plan.actor === 'owner' ? 'session-manager' : plan.actor,
operation: `delivery.${plan.to}`, entityType: 'delivery', entityId: delivery.id,
details: { commit: delivery.commit, approved, adapter: plan.adapter },
});
return this.get(delivery.id);
}
}
+15
View File
@@ -98,6 +98,21 @@ export const projectConfigSchema = z.object({
required_checks: textList,
landing_authority: textList.min(1),
}).strict(),
delivery: z.object({
authority: z.object({
implemented: textList.min(1),
verified: textList.min(1),
integrated: textList.min(1),
release_ready: textList.min(1),
released: textList.min(1),
deployed: textList.min(1),
live_verified: textList.min(1),
}).strict(),
adapters: z.object({
release: z.object({ provider: text }).passthrough(),
deployment: z.object({ provider: text }).passthrough(),
}).strict(),
}).strict(),
session_manager: z.object({
id: z.literal('session-manager'),
runtime: text,
+15
View File
@@ -79,6 +79,21 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes })
required_checks: (proposal.checks ?? []).map((check) => check.id),
landing_authority: ['session-manager'],
},
delivery: {
authority: {
implemented: ['$source'],
verified: ['$source', 'session-manager'],
integrated: ['session-manager'],
release_ready: ['session-manager'],
released: ['owner'],
deployed: ['owner'],
live_verified: ['owner'],
},
adapters: {
release: { provider: 'none' },
deployment: { provider: 'none' },
},
},
session_manager: {
id: 'session-manager', runtime: proposal.session_manager?.runtime ?? 'claude', start_last: true,
branch: proposal.session_manager?.branch ?? null,
+6 -1
View File
@@ -79,6 +79,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
checks: { definitions: config.checks ?? [], receipts: [] },
resources: { definitions: config.resources ?? [], holders: [], waiters: [] },
integration: [],
deliveries: [],
fleetChanges: [],
schedules: config.schedules ?? [],
audit: [],
@@ -122,7 +123,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
}
}
result.checks.receipts = rows(database, 'check_receipts', `
SELECT id, check_id AS checkId, area_id AS areaId, commit_sha AS commit, result, finished_at AS finishedAt
SELECT id, check_id AS checkId, area_id AS areaId, commit_sha AS "commit", result, finished_at AS finishedAt
FROM check_receipts ORDER BY finished_at DESC LIMIT 40
`);
result.resources.holders = rows(database, 'resource_leases', `
@@ -137,6 +138,10 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
SELECT id, source_area AS sourceArea, source_commit AS sourceCommit, target_branch AS targetBranch, state, reason, updated_at AS updatedAt
FROM integration_requests ORDER BY updated_at DESC LIMIT 40
`);
result.deliveries = rows(database, 'deliveries', `
SELECT id, label, source_area AS sourceArea, commit_sha AS "commit", state, updated_at AS updatedAt
FROM deliveries ORDER BY updated_at DESC, id LIMIT 40
`);
result.fleetChanges = rows(database, 'fleet_changes', `
SELECT id, change_type AS changeType, state, proposer,
COALESCE(json_extract(domain_json, '$.id'), json_extract(proposal_json, '$.resultDomains[0].id')) AS domainId,
+1
View File
@@ -16,6 +16,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
'fleet-boundary-evolution',
'cli-lifecycle-surface',
'forge-migration',
'delivery-lifecycle',
'review-install-roster',
'branches-worktrees',
'runtime-identities',
+1
View File
@@ -39,6 +39,7 @@ function releaseCandidate(root, version) {
'SCN-fleet-boundary-evolution',
'SCN-cli-lifecycle-surface',
'SCN-forge-migration',
'SCN-delivery-lifecycle',
'SCN-durable-message', 'SCN-ownership-handoff', 'SCN-worktree-purge-safety',
'SCN-backlog-lifecycle', 'SCN-native-integration', 'SCN-resource-fifo',
'SCN-fleet-fresh-resume', 'SCN-install-doctor-purge',
+176
View File
@@ -0,0 +1,176 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import test from 'node:test';
import { CheckService } from '../../src/checks/service.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import {
configureDelivery, DeliveryService, planDeliveryConfiguration,
} from '../../src/delivery/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { IntegrationService } from '../../src/integration/service.mjs';
import { observeProject } from '../../src/observability/snapshot.mjs';
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-delivery-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'package.json'), `${JSON.stringify({
name: 'delivery-fixture', scripts: { 'verify:pass': 'node -e "process.exit(0)"' },
}, null, 2)}\n`);
writeFileSync(join(root, 'app.js'), 'export const value = 1;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
let repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.checks = proposal.checks.filter((check) => check.id === 'verify-pass');
proposal.review = {
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) };
installProject({ repository, proposal, env, projectId: 'delivery-fixture' });
execFileSync('git', ['-C', root, 'add', '.torch']);
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
repository = inspectRepository(root);
createWorktrees({
repository, parentOverride: join(tmpdir(), `${basename(root)}-worktrees`),
});
execFileSync('git', ['-C', root, 'add', '.torch/install-manifest.json']);
execFileSync('git', ['-C', root, 'commit', '-m', 'record managed worktrees']);
const manifest = JSON.parse(readFileSync(join(root, '.torch', 'install-manifest.json'), 'utf8'));
const worker = proposal.domains[0].id;
const worktree = manifest.external.find((entry) => entry.type === 'worktree' && entry.area === worker);
execFileSync('git', ['-C', worktree.path, 'merge', 'main']);
writeFileSync(join(worktree.path, 'app.js'), 'export const value = 2;\n');
execFileSync('git', ['-C', worktree.path, 'add', 'app.js']);
execFileSync('git', ['-C', worktree.path, 'commit', '-m', 'delivery change']);
return { root, env, worker, worktree };
}
test('SCN-delivery-lifecycle: delivery authority, evidence, integration, and adapters are distinct gates', () => {
const context = fixture();
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
let serial = 0;
const idFactory = () => `delivery-id-${++serial}`;
const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control, idFactory });
let service = new DeliveryService({
repositoryRoot: context.root, controlPlane: control, checkService: checks, idFactory,
});
let delivery = service.create({
sourceArea: context.worker, label: 'Portable release boundary', evidence: ['implementation commit'],
});
assert.equal(delivery.state, 'implemented');
assert.throws(
() => service.planTransition({ deliveryId: delivery.id, targetState: 'integrated', actor: 'session-manager' }),
(error) => error.code === 'DELIVERY_TRANSITION_INVALID',
);
assert.equal(service.planTransition({
deliveryId: delivery.id, targetState: 'verified', actor: context.worker,
}).blockers.some((blocker) => blocker.code === 'DELIVERY_CHECKS_REQUIRED'), true);
const receipt = checks.run({ checkId: 'verify-pass', areaId: context.worker });
delivery = service.transition({
deliveryId: delivery.id, targetState: 'verified', actor: context.worker,
evidence: [`check:${receipt.id}`],
});
assert.equal(delivery.state, 'verified');
assert.equal(service.planTransition({
deliveryId: delivery.id, targetState: 'integrated', actor: 'session-manager',
}).blockers.some((blocker) => blocker.code === 'DELIVERY_INTEGRATION_REQUIRED'), true);
const integration = new IntegrationService({
repositoryRoot: context.root, controlPlane: control, checkService: checks, idFactory,
});
const request = integration.request({ areaId: context.worker, commit: receipt.commit });
integration.authorize({ requestId: request.id, actorId: 'session-manager' });
const landed = integration.land({ requestId: request.id, actorId: 'session-manager' });
delivery = service.transition({
deliveryId: delivery.id, targetState: 'integrated', actor: 'session-manager',
evidence: [`integration:${landed.id}`],
});
delivery = service.transition({
deliveryId: delivery.id, targetState: 'release-ready', actor: 'session-manager',
evidence: ['release checklist complete'],
});
assert.equal(delivery.state, 'release-ready');
assert.equal(service.planTransition({
deliveryId: delivery.id, targetState: 'released', actor: 'owner',
}).blockers[0].code, 'DELIVERY_ADAPTER_NOT_CONFIGURED');
const configurationPlan = planDeliveryConfiguration({
repositoryRoot: context.root, releaseProvider: 'fixture', deploymentProvider: 'fixture',
});
assert.equal(configurationPlan.canProceed, true);
assert.equal(configurationPlan.mutationPerformed, false);
configureDelivery({
repositoryRoot: context.root, releaseProvider: 'fixture', deploymentProvider: 'fixture',
});
const invocations = [];
const adapter = {
name: 'fixture', capabilities: { release: true, deploy: true, verifyLive: true },
release: ({ delivery: item }) => {
invocations.push('release'); return { status: 'succeeded', reference: `release:${item.commit}` };
},
deploy: ({ delivery: item }) => {
invocations.push('deploy'); return { status: 'succeeded', reference: `deploy:${item.commit}` };
},
verifyLive: ({ delivery: item }) => {
invocations.push('verifyLive'); return { status: 'succeeded', reference: `live:${item.commit}` };
},
};
service = new DeliveryService({
repositoryRoot: context.root, controlPlane: control, checkService: checks,
adapters: new Map([['fixture', adapter]]), idFactory,
});
assert.equal(service.planTransition({
deliveryId: delivery.id, targetState: 'released', actor: 'session-manager',
}).blockers[0].code, 'DELIVERY_AUTHORITY_REQUIRED');
assert.throws(
() => service.transition({
deliveryId: delivery.id, targetState: 'released', actor: 'owner', evidence: ['release candidate'],
}),
(error) => error.code === 'APPROVAL_REQUIRED',
);
for (const [state, evidence] of [
['released', 'published artifact'], ['deployed', 'deployment receipt'], ['live-verified', 'live probe'],
]) {
delivery = service.transition({
deliveryId: delivery.id, targetState: state, actor: 'owner', evidence: [evidence], approved: true,
});
}
assert.equal(delivery.state, 'live-verified');
assert.deepEqual(invocations, ['release', 'deploy', 'verifyLive']);
assert.equal(delivery.adapterReceipts.length, 3);
assert.equal(observeProject({ repositoryRoot: context.root, env: context.env }).deliveries[0].state, 'live-verified');
control.close();
const reopened = openControlPlane({ repositoryRoot: context.root, env: context.env });
const durable = new DeliveryService({
repositoryRoot: context.root, controlPlane: reopened, adapters: new Map([['fixture', adapter]]),
}).get(delivery.id);
assert.equal(durable.state, 'live-verified');
assert.equal(durable.adapterReceipts.length, 3);
reopened.close();
const cliCreate = spawnSync(process.execPath, [
CLI, 'delivery', 'create', '--area', context.worker, '--label', 'CLI delivery',
'--evidence', 'clean source tip', '--json',
], { cwd: context.root, env: context.env, encoding: 'utf8' });
assert.equal(cliCreate.status, 0, cliCreate.stderr || cliCreate.stdout);
const cliDelivery = JSON.parse(cliCreate.stdout);
assert.equal(cliDelivery.state, 'implemented');
const cliList = spawnSync(process.execPath, [CLI, 'delivery', 'list', '--json'], {
cwd: context.root, env: context.env, encoding: 'utf8',
});
assert.equal(cliList.status, 0, cliList.stderr || cliList.stdout);
assert.equal(JSON.parse(cliList.stdout).deliveries.some((item) => item.id === cliDelivery.id), true);
});