@devflow-tools/delivery-line 0.18.19 → 0.18.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/adapters/delivery-package.d.ts.map +1 -1
  2. package/dist/adapters/delivery-package.js +2 -1
  3. package/dist/adapters/delivery-package.js.map +1 -1
  4. package/dist/adapters/integration.d.ts +7 -0
  5. package/dist/adapters/integration.d.ts.map +1 -1
  6. package/dist/adapters/integration.js +11 -0
  7. package/dist/adapters/integration.js.map +1 -1
  8. package/dist/adapters/plugin-capability.d.ts +14 -0
  9. package/dist/adapters/plugin-capability.d.ts.map +1 -0
  10. package/dist/adapters/plugin-capability.js +61 -0
  11. package/dist/adapters/plugin-capability.js.map +1 -0
  12. package/dist/adapters/verification.d.ts +1 -0
  13. package/dist/adapters/verification.d.ts.map +1 -1
  14. package/dist/adapters/verification.js +60 -2
  15. package/dist/adapters/verification.js.map +1 -1
  16. package/dist/adapters/workflow-execution.d.ts +6 -0
  17. package/dist/adapters/workflow-execution.d.ts.map +1 -1
  18. package/dist/adapters/workflow-execution.js +61 -6
  19. package/dist/adapters/workflow-execution.js.map +1 -1
  20. package/dist/artifacts/artifact-contract.d.ts.map +1 -1
  21. package/dist/artifacts/artifact-contract.js +7 -6
  22. package/dist/artifacts/artifact-contract.js.map +1 -1
  23. package/dist/composition.d.ts +1 -1
  24. package/dist/composition.d.ts.map +1 -1
  25. package/dist/composition.js +18 -1
  26. package/dist/composition.js.map +1 -1
  27. package/dist/domain/plugin-capability.d.ts +33 -0
  28. package/dist/domain/plugin-capability.d.ts.map +1 -1
  29. package/dist/domain/plugin-capability.js +16 -0
  30. package/dist/domain/plugin-capability.js.map +1 -1
  31. package/dist/domain/types.d.ts +96 -2
  32. package/dist/domain/types.d.ts.map +1 -1
  33. package/dist/domain/types.js.map +1 -1
  34. package/dist/facade/delivery-line-facade.d.ts +8 -0
  35. package/dist/facade/delivery-line-facade.d.ts.map +1 -1
  36. package/dist/facade/delivery-line-facade.js +157 -27
  37. package/dist/facade/delivery-line-facade.js.map +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/persistence/memory-store.js +1 -1
  43. package/dist/persistence/memory-store.js.map +1 -1
  44. package/dist/persistence/sqlite-store.js +1 -1
  45. package/dist/persistence/sqlite-store.js.map +1 -1
  46. package/dist/ports/index.d.ts +116 -3
  47. package/dist/ports/index.d.ts.map +1 -1
  48. package/dist/runtime/context-binder.d.ts +15 -0
  49. package/dist/runtime/context-binder.d.ts.map +1 -1
  50. package/dist/runtime/context-binder.js +26 -7
  51. package/dist/runtime/context-binder.js.map +1 -1
  52. package/dist/runtime/execution-coordinator.d.ts +12 -1
  53. package/dist/runtime/execution-coordinator.d.ts.map +1 -1
  54. package/dist/runtime/execution-coordinator.js +204 -46
  55. package/dist/runtime/execution-coordinator.js.map +1 -1
  56. package/dist/runtime/memory-writeback.d.ts +8 -0
  57. package/dist/runtime/memory-writeback.d.ts.map +1 -1
  58. package/dist/runtime/memory-writeback.js +12 -2
  59. package/dist/runtime/memory-writeback.js.map +1 -1
  60. package/dist/runtime/operation-journal.d.ts +1 -1
  61. package/dist/runtime/operation-journal.d.ts.map +1 -1
  62. package/dist/runtime/operation-journal.js +5 -1
  63. package/dist/runtime/operation-journal.js.map +1 -1
  64. package/dist/runtime/plan-compiler.d.ts +4 -0
  65. package/dist/runtime/plan-compiler.d.ts.map +1 -1
  66. package/dist/runtime/plan-compiler.js +15 -0
  67. package/dist/runtime/plan-compiler.js.map +1 -1
  68. package/package.json +3 -3
@@ -32,9 +32,17 @@ function nextActionsFor(record) {
32
32
  nextActions.push('compare_options');
33
33
  if (['clarifying', 'spec_ready', 'plan_ready', 'reviewed'].includes(record.stage))
34
34
  nextActions.push('approve');
35
- if (['plan_approved', 'executing', 'needs_changes', 'held', 'blocked'].includes(record.stage))
35
+ if (record.blockers?.some(blocker => blocker.code === 'WORKTREE_ADMISSION_REQUIRED'))
36
+ nextActions.push('admit_worktree');
37
+ else if (record.blockers?.some(blocker => blocker.code === 'STALE_STACK_FACTS'))
38
+ nextActions.push('refresh_activation');
39
+ else if (record.blockers?.some(blocker => blocker.code === 'EXTERNAL_CANDIDATE_REQUIRED'))
40
+ nextActions.push('import_external_change');
41
+ else if (['plan_approved', 'executing', 'needs_changes', 'held', 'blocked'].includes(record.stage))
36
42
  nextActions.push('resume');
37
- if (record.stage === 'failed')
43
+ if (record.tasks.some(task => task.status === 'running' || task.status === 'merge_pending') || record.blockers?.some(blocker => blocker.code === 'WORKER_RECONCILIATION_INCONCLUSIVE'))
44
+ nextActions.push('recover_task');
45
+ if (record.stage === 'failed' && (record.blockers?.some(blocker => blocker.retryable) ?? true))
38
46
  nextActions.push('retry_task');
39
47
  if (record.stage === 'release_pending')
40
48
  nextActions.push('export');
@@ -95,17 +103,6 @@ export class DeliveryLineFacade {
95
103
  if (input.caseId)
96
104
  return this.status(id);
97
105
  }
98
- if (this.ports.revision?.inspectWorktree) {
99
- const worktree = await this.ports.revision.inspectWorktree({ repositoryPath: projectRoot, baseRevision: baseRef });
100
- if (worktree.dirtyFiles.length > 0) {
101
- throw new DeliveryLineError('MISSING_PREREQUISITE', 'delivery requires a clean worktree before case creation', {
102
- dirtyFiles: worktree.dirtyFiles,
103
- baseRevision: baseRef,
104
- currentRevision: worktree.currentRevision,
105
- recoveryAction: 'stash or commit existing changes, then create the Delivery case again',
106
- });
107
- }
108
- }
109
106
  let contextReceipt = input.contextReceipt;
110
107
  let contextQuality;
111
108
  if (this.ports.context) {
@@ -125,22 +122,45 @@ export class DeliveryLineFacade {
125
122
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'context receipt is not authoritative');
126
123
  contextReceipt = bound.receiptHash;
127
124
  if (bound.channelStatus && bound.gaps && bound.attribution)
128
- contextQuality = { blocking: bound.blocking ?? false, ...(bound.channelAvailability ? { channelAvailability: bound.channelAvailability } : {}), channelStatus: bound.channelStatus, gaps: bound.gaps, attribution: bound.attribution };
125
+ contextQuality = { blocking: bound.blocking ?? false, ...(bound.channelAvailability ? { channelAvailability: bound.channelAvailability } : {}), channelStatus: bound.channelStatus, gaps: bound.gaps, attribution: bound.attribution, ...('quality' in bound && bound.quality ? { quality: bound.quality } : {}) };
129
126
  }
130
127
  if (!contextReceipt)
131
128
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'context receipt is required when authoritative retrieval is not configured');
132
129
  const now = Date.now();
133
130
  const riskTier = input.riskSignals?.some(signal => /auth|scope|security|database|gateway|release/i.test(signal)) ? 'high' : input.riskTier ?? 'low';
131
+ const worktreeMode = input.worktreeMode ?? 'isolated';
132
+ const admission = await this.ports.worktreeAdmission?.inspect({ projectRoot });
133
+ const stackFacts = await this.ports.stackFacts?.inspect({ projectRoot });
134
+ const runtimeProjection = await this.ports.runtimeActivation?.capture({ projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: baseRef }) ?? {
135
+ host: 'external', provider: 'none', model: 'none', permissions: { mode: 'external-candidate-only' }, probeReceiptHash: 'not-applicable',
136
+ };
137
+ const activationBody = {
138
+ schemaVersion: 'delivery-runtime-activation.v1',
139
+ projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: baseRef,
140
+ ...runtimeProjection,
141
+ contextReceiptHash: contextReceipt,
142
+ stackFactsHash: stackFacts?.fingerprint ?? hash({ projectRoot, baseRef }),
143
+ pluginSelectionHash: hash({ selectedPlugins: stackFacts?.selectedPlugins ?? [], selectedRunners: stackFacts?.selectedRunners ?? [] }),
144
+ createdAt: now,
145
+ };
146
+ const admissionBlocked = Boolean(admission && !admission.clean && (worktreeMode === 'auto' || worktreeMode === 'clean'));
134
147
  const record = {
135
148
  id, projectRoot, baseRef, integrationRef: `refs/devflow/delivery/${id}`, targetRef: input.targetRef,
136
149
  title: input.title, source: input.source ?? 'human', riskTier, autonomy: input.autonomy ?? 'manual', stage: 'clarifying',
137
150
  budgets: { ...defaults, ...input.budgets }, artifacts: [], tasks: [], approvals: [], decisions: [], evidence: [],
138
151
  approvalStates: {},
139
152
  interaction: { phase: 'clarification', status: 'awaiting_user', turn: 1, openQuestions: [input.existingImplementations?.length ? `Existing implementations were detected (${input.existingImplementations.join(', ')}). Which implementation should be extended, replaced, or retained?` : 'What outcome and acceptance criteria should this change satisfy?'], blockingUnknowns: [input.existingImplementations?.length ? `Existing implementations were detected (${input.existingImplementations.join(', ')}). Which implementation should be extended, replaced, or retained?` : 'What outcome and acceptance criteria should this change satisfy?'], summaryRefs: [] },
140
- createdBy: input.actor, sessionId: input.sessionId, executionId: input.executionId, createdAt: now, updatedAt: now, version: 0, contextReceipt,
153
+ createdBy: input.actor, sessionId: input.sessionId, executionId: input.executionId, createdAt: now, updatedAt: now, version: 0, contextReceipt, worktreeMode,
154
+ runtimeActivation: { ...activationBody, snapshotHash: hash(activationBody) },
141
155
  ...(input.existingImplementations?.length ? { existingImplementations: [...input.existingImplementations] } : {}),
142
156
  ...(contextQuality ? { contextQuality } : {}),
157
+ ...(stackFacts ? { stackFacts: { ...stackFacts, capturedAt: now } } : {}),
143
158
  };
159
+ if (admissionBlocked) {
160
+ record.stage = 'held';
161
+ record.interaction.status = 'paused';
162
+ record.blockers = [{ code: 'WORKTREE_ADMISSION_REQUIRED', message: 'The project worktree contains uncommitted changes.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', artifactRefs: [], evidenceRefs: [], recoveryAction: 'Choose isolated, external, or clean admission before continuing.', retryable: true, diagnostics: { dirtyFiles: admission?.dirtyFiles ?? [], actions: ['isolated', 'external', 'clean'], currentRevision: admission?.revision, baseRevision: baseRef, admissionToken: hash({ projectRoot, baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [] }) } }];
163
+ }
144
164
  await this.ports.store.create(record);
145
165
  await appendLedgerEvent(this.ports.store, { caseId: id, type: 'case.created', actor: input.actor, idempotencyKey: input.idempotencyKey, payload: { stage: record.stage } });
146
166
  return this.view(record);
@@ -170,6 +190,8 @@ export class DeliveryLineFacade {
170
190
  async answer(caseId, input) {
171
191
  requireIdentity(input);
172
192
  return this.mutate(caseId, input, 'interaction.answered', async (record) => {
193
+ if (!['clarifying', 'designing'].includes(record.stage))
194
+ throw new DeliveryLineError('INVALID_TRANSITION', 'interaction answers are only accepted during clarification or design', { currentStage: record.stage, allowedStages: ['clarifying', 'designing'] });
173
195
  if (input.turn !== record.interaction.turn)
174
196
  throw staleTurnError(record, input.turn);
175
197
  if (record.interaction.turn >= record.budgets.maxInteractionTurns) {
@@ -200,6 +222,9 @@ export class DeliveryLineFacade {
200
222
  async revise(caseId, input) {
201
223
  requireIdentity(input);
202
224
  return this.mutate(caseId, input, 'interaction.revised', record => {
225
+ const allowedStages = input.phase === 'clarification' ? ['draft', 'clarifying'] : ['designing'];
226
+ if (!allowedStages.includes(record.stage))
227
+ throw new DeliveryLineError('INVALID_TRANSITION', `${input.phase} revisions are not allowed at ${record.stage}`, { currentStage: record.stage, requestedPhase: input.phase, allowedStages });
203
228
  const stage = input.phase === 'clarification' ? 'clarifying' : 'designing';
204
229
  const next = invalidateDownstream(stage, record);
205
230
  next.interaction = { ...next.interaction, phase: input.phase, status: 'awaiting_user', turn: next.interaction.turn + 1, openQuestions: [input.content], blockingUnknowns: [input.content] };
@@ -244,11 +269,11 @@ export class DeliveryLineFacade {
244
269
  });
245
270
  record.contextReceipt = designerReceipt.receiptHash;
246
271
  if (designerReceipt.channelStatus && designerReceipt.gaps && designerReceipt.attribution)
247
- record.contextQuality = { blocking: designerReceipt.blocking ?? false, ...(designerReceipt.channelAvailability ? { channelAvailability: designerReceipt.channelAvailability } : {}), channelStatus: designerReceipt.channelStatus, gaps: designerReceipt.gaps, attribution: designerReceipt.attribution };
272
+ record.contextQuality = { blocking: designerReceipt.blocking ?? false, ...(designerReceipt.channelAvailability ? { channelAvailability: designerReceipt.channelAvailability } : {}), channelStatus: designerReceipt.channelStatus, gaps: designerReceipt.gaps, attribution: designerReceipt.attribution, ...('quality' in designerReceipt && designerReceipt.quality ? { quality: designerReceipt.quality } : {}) };
248
273
  }
249
274
  const decisionHash = hash({ questionId, optionId, selectedLabel, optionsSnapshot });
250
275
  record.decisions.push({ id: randomUUID(), kind: 'technical', questionId, question: 'Which implementation option should Delivery execute?', options: optionsSnapshot.map(option => option.label), optionsSnapshot, optionId, selectedLabel, decisionHash, sessionId: input.sessionId, toolUseId: input.toolUseId, decision: selectedLabel, decidedBy: input.actor, artifactHash: decisionHash });
251
- const stackFactsHash = hash({ projectRoot: record.projectRoot, baseRef: record.baseRef, targetRef: record.targetRef });
276
+ const stackFactsHash = record.stackFacts?.fingerprint ?? record.runtimeActivation?.stackFactsHash ?? record.stackFactsHash ?? hash({ projectRoot: record.projectRoot, baseRef: record.baseRef, targetRef: record.targetRef });
252
277
  const selectedCapabilities = this.capabilities?.manifests.length
253
278
  ? selectPluginCapabilities({
254
279
  detectedStacks: this.capabilities.detectedStacks,
@@ -258,7 +283,9 @@ export class DeliveryLineFacade {
258
283
  : undefined;
259
284
  const pluginManifestHash = selectedCapabilities?.selectedManifestHash ?? hash({ capability: selectedLabel, protocolVersion: 'delivery-capability.v1' });
260
285
  record.stackFactsHash = stackFactsHash;
261
- record.pluginSelection = { selectedManifestHash: pluginManifestHash, stackFactsHash, decisionRefs: [decisionHash], selectionReason: selectedCapabilities?.selectionReason ?? 'selected by current user decision', rejectedAlternatives: [...new Set([...(selectedCapabilities?.rejectedAlternatives ?? []), ...optionsSnapshot.filter(option => option.id !== optionId).map(option => option.id)])], selectedAt: Date.now() };
286
+ const requiredContextChannels = [...new Set(Object.values(selectedCapabilities?.selected ?? {}).flatMap(manifest => manifest.requiredContextChannels))]
287
+ .filter((channel) => ['code', 'memory', 'knowledge'].includes(channel));
288
+ record.pluginSelection = { selectedManifestHash: pluginManifestHash, stackFactsHash, decisionRefs: [decisionHash], selectionReason: selectedCapabilities?.selectionReason ?? 'selected by current user decision', rejectedAlternatives: [...new Set([...(selectedCapabilities?.rejectedAlternatives ?? []), ...optionsSnapshot.filter(option => option.id !== optionId).map(option => option.id)])], ...(requiredContextChannels.length ? { requiredContextChannels } : {}), selectedAt: Date.now() };
262
289
  await this.createProjectionArtifact(record, 'plugin-selection.md', 'Plugin selection', 'Capability providers selected by Delivery from stack facts and the current decision.', record.pluginSelection, input.actor);
263
290
  record.stage = 'spec_ready';
264
291
  record.interaction = { ...record.interaction, phase: 'design', status: 'awaiting_approval', openQuestions: [], blockingUnknowns: [] };
@@ -282,6 +309,7 @@ export class DeliveryLineFacade {
282
309
  if (this.ports.context) {
283
310
  const plannerReceipt = await this.ports.context.build({
284
311
  role: 'planner',
312
+ ...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
285
313
  retrieval: {
286
314
  taskRevision: record.version + 1,
287
315
  projectRoot: record.projectRoot,
@@ -294,7 +322,7 @@ export class DeliveryLineFacade {
294
322
  });
295
323
  record.contextReceipt = plannerReceipt.receiptHash;
296
324
  if (plannerReceipt.channelStatus && plannerReceipt.gaps && plannerReceipt.attribution)
297
- record.contextQuality = { blocking: plannerReceipt.blocking ?? false, ...(plannerReceipt.channelAvailability ? { channelAvailability: plannerReceipt.channelAvailability } : {}), channelStatus: plannerReceipt.channelStatus, gaps: plannerReceipt.gaps, attribution: plannerReceipt.attribution };
325
+ record.contextQuality = { blocking: plannerReceipt.blocking ?? false, ...(plannerReceipt.channelAvailability ? { channelAvailability: plannerReceipt.channelAvailability } : {}), channelStatus: plannerReceipt.channelStatus, gaps: plannerReceipt.gaps, attribution: plannerReceipt.attribution, ...('quality' in plannerReceipt && plannerReceipt.quality ? { quality: plannerReceipt.quality } : {}) };
298
326
  }
299
327
  planContent = renderPlanMarkdown(input.plan);
300
328
  proposedArtifactHash = contentHash(planContent);
@@ -370,6 +398,9 @@ export class DeliveryLineFacade {
370
398
  taskId: envelopes[0]?.taskId ?? '',
371
399
  baseRevision: record.baseRef,
372
400
  verificationEvidenceHashes: [],
401
+ reviewEvidenceHashes: [],
402
+ intentArtifactHash: record.artifacts.find(item => item.type === 'intent')?.contentHash,
403
+ specArtifactHash: record.artifacts.find(item => item.type === 'spec')?.contentHash,
373
404
  };
374
405
  }
375
406
  record.tasks = envelopes.map(envelope => ({
@@ -382,6 +413,7 @@ export class DeliveryLineFacade {
382
413
  dependsOn: envelope.dependencies,
383
414
  allowedFiles: envelope.allowedFiles,
384
415
  expectedChanges: envelope.implementationSteps,
416
+ acceptanceCriteria: envelope.verification.acceptanceCriteria,
385
417
  verificationCommands: envelope.verification.commands,
386
418
  inputArtifacts: envelope.inputArtifacts,
387
419
  planHash: envelope.planHash,
@@ -389,6 +421,7 @@ export class DeliveryLineFacade {
389
421
  hostSelectionSnapshotHash: envelope.hostSelectionSnapshotHash,
390
422
  executionAuthority: envelope.executionAuthority,
391
423
  constraints: envelope.constraints,
424
+ planNodes: envelope.nodes,
392
425
  status: 'pending',
393
426
  evidence: [],
394
427
  }));
@@ -413,7 +446,15 @@ export class DeliveryLineFacade {
413
446
  }
414
447
  if (record.stage === 'plan_approved')
415
448
  record.stage = 'executing';
416
- if (record.stage === 'executing') {
449
+ if (record.stage === 'executing' || (record.stage === 'held' && record.tasks.some(task => task.status === 'running' || task.status === 'merge_pending') && this.ports.operationJournal)) {
450
+ if (record.stackFacts && this.ports.stackFacts) {
451
+ const currentStackFacts = await this.ports.stackFacts.inspect({ projectRoot: record.projectRoot });
452
+ if (currentStackFacts.fingerprint !== record.stackFacts.fingerprint)
453
+ return hold(record, { code: 'STALE_STACK_FACTS', message: 'Project stack facts changed after case creation.', responsibleRole: 'planner', requiredRole: 'planner', recoveryAction: 'Refresh stack facts and re-approve the executable plan.', retryable: false, diagnostics: { previousFingerprint: record.stackFacts.fingerprint, currentFingerprint: currentStackFacts.fingerprint } });
454
+ }
455
+ if (record.worktreeMode === 'external' && !record.evidence.some(item => item.kind === 'tool' && item.metadata.trust === 'external-untrusted' && item.metadata.imported === true)) {
456
+ return hold(record, { code: 'EXTERNAL_CANDIDATE_REQUIRED', message: 'The admitted external workspace must be imported as an untrusted candidate before execution.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', recoveryAction: 'Commit the admitted workspace changes, then call import-change with that candidate revision.', retryable: false, diagnostics: { baseRevision: record.worktreeAdmission?.baseRevision ?? record.baseRef, currentRevision: record.worktreeAdmission?.currentRevision, dirtyFiles: record.worktreeAdmission?.dirtyFiles ?? [] } });
457
+ }
417
458
  const verifier = this.ports.verification;
418
459
  if (!this.ports.execution)
419
460
  return hold(record, { code: 'WORKER_UNAVAILABLE', message: 'No Delivery execution adapter is configured for this case.', responsibleRole: 'execution-owner', recoveryAction: 'Configure a Delivery execution adapter or import an external change.', retryable: false });
@@ -426,6 +467,18 @@ export class DeliveryLineFacade {
426
467
  verify: input => this.ports.verification.verify(input),
427
468
  integrate: input => this.ports.integration.integrate(input),
428
469
  inspectIntegration: async (input) => await this.ports.integration.inspect?.(input) ?? null,
470
+ inspectWorker: this.ports.execution.inspectWorker ? (input => this.ports.execution.inspectWorker(input)) : undefined,
471
+ runCapabilities: record.stackFacts?.selectedRunners?.length && this.ports.capabilities ? async ({ task, execution }) => {
472
+ const receipts = [];
473
+ for (const pluginName of record.stackFacts.selectedRunners) {
474
+ const manifest = await this.ports.capabilities.manifest(pluginName);
475
+ const receipt = await this.ports.capabilities.run({ caseId: record.id, taskId: task.id, pluginName, capability: manifest.capability, manifestHash: manifest.manifestHash, stackFactsFingerprint: record.stackFacts.fingerprint, allowedFiles: task.allowedFiles, payload: { changedFiles: execution.handoff?.changedFiles ?? [], planHash: task.planHash, contextReceiptHash: task.contextReceiptHash, baseRevision: task.baseRevision, fencingToken: task.fencingToken } });
476
+ if (receipt.manifestHash !== manifest.manifestHash)
477
+ throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', `plugin receipt hash mismatch: ${pluginName}`);
478
+ receipts.push({ ...receipt, pluginName, capability: manifest.capability, stackFactsFingerprint: record.stackFacts.fingerprint });
479
+ }
480
+ return receipts;
481
+ } : undefined,
429
482
  }, this.ports.operationJournal).run(record);
430
483
  if (record.tasks.length > 0 && record.tasks.every(task => task.status === 'integrated')) {
431
484
  await this.createProjectionArtifact(record, 'code-evidence.md', 'Code evidence', 'Worker handoffs and changed files bound to the integrated candidate revision.', {
@@ -434,7 +487,7 @@ export class DeliveryLineFacade {
434
487
  }, input.actor);
435
488
  const integrationRevision = record.integrationRevision ?? record.baseRef;
436
489
  try {
437
- const planReceipt = await verifier.verify({ caseId: record.id, integrationRef: record.integrationRef, commit: integrationRevision, planLevel: true, commands: record.tasks.flatMap(task => task.verificationCommands) });
490
+ const planReceipt = await verifier.verify({ caseId: record.id, integrationRef: record.integrationRef, commit: integrationRevision, planLevel: true, commands: record.tasks.flatMap(task => task.verificationCommands), acceptanceCriteria: record.tasks.flatMap(task => task.acceptanceCriteria) });
438
491
  if (!planReceipt.passed) {
439
492
  record.stage = 'failed';
440
493
  return record;
@@ -442,7 +495,7 @@ export class DeliveryLineFacade {
442
495
  record.evidence.push(this.evidence('verification', planReceipt));
443
496
  await this.createProjectionArtifact(record, 'verification.md', 'Verification', 'Verification evidence for the integrated revision.', { commit: planReceipt.commit, passed: planReceipt.passed, evidenceHash: hash(planReceipt) }, input.actor);
444
497
  if (record.lineage) {
445
- record.lineage = { ...record.lineage, candidateRevision: integrationRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
498
+ record.lineage = { ...record.lineage, candidateRevision: integrationRevision, integrationRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
446
499
  }
447
500
  record.stage = 'verified';
448
501
  }
@@ -461,7 +514,10 @@ export class DeliveryLineFacade {
461
514
  if (result.receipts.some(receipt => receipt.artifactHash !== reviewArtifactHash))
462
515
  return hold(record, { code: 'STALE_REVIEW_EVIDENCE', message: 'Review evidence does not match the current integration revision.', responsibleRole: 'review-owner', recoveryAction: 'Run review again against the current integration revision.', retryable: true });
463
516
  else {
464
- record.evidence.push(...result.receipts.map(receipt => this.evidence('review', receipt)));
517
+ const reviewEvidence = result.receipts.map(receipt => this.evidence('review', receipt));
518
+ record.evidence.push(...reviewEvidence);
519
+ if (record.lineage)
520
+ record.lineage = { ...record.lineage, reviewEvidenceHashes: reviewEvidence.map(item => item.hash) };
465
521
  await this.createProjectionArtifact(record, 'review.md', 'Review', 'Independent review evidence for the integrated revision.', { artifactHash: reviewArtifactHash, passed: result.passed, receipts: result.receipts.map(receipt => receipt.id) }, input.actor);
466
522
  if (!result.passed) {
467
523
  record.stage = 'needs_changes';
@@ -480,9 +536,62 @@ export class DeliveryLineFacade {
480
536
  return record;
481
537
  });
482
538
  }
539
+ /** Explicit recovery action used after a process restart or an unknown operation receipt. */
540
+ async recoverTask(caseId, input) {
541
+ return this.resume(caseId, input);
542
+ }
543
+ async admitWorktree(caseId, input) {
544
+ requireIdentity(input);
545
+ return this.mutate(caseId, input, 'worktree.admitted', async (record) => {
546
+ if (record.blockers?.[0]?.code !== 'WORKTREE_ADMISSION_REQUIRED')
547
+ throw new DeliveryLineError('INVALID_TRANSITION', 'case is not awaiting worktree admission');
548
+ const expectedToken = record.blockers[0].diagnostics?.admissionToken;
549
+ if (typeof expectedToken !== 'string' || input.admissionToken !== expectedToken)
550
+ throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', 'worktree admission token is stale');
551
+ const admission = await this.ports.worktreeAdmission?.inspect({ projectRoot: record.projectRoot });
552
+ if (input.mode === 'clean' && admission && !admission.clean)
553
+ return hold(record, { code: 'WORKTREE_ADMISSION_REQUIRED', message: 'The project worktree is still dirty.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', recoveryAction: 'Clean the listed files or choose isolated/external admission.', retryable: true, diagnostics: { dirtyFiles: admission.dirtyFiles, choices: ['isolated', 'external', 'clean'] } });
554
+ record.worktreeMode = input.mode;
555
+ record.worktreeAdmission = { mode: input.mode, baseRevision: record.baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [], admissionToken: input.admissionToken, admittedAt: Date.now() };
556
+ record.stage = 'clarifying';
557
+ record.blockers = [];
558
+ record.interaction.status = 'awaiting_user';
559
+ return record;
560
+ });
561
+ }
562
+ async refreshActivation(caseId, input) {
563
+ requireIdentity(input);
564
+ return this.mutate(caseId, input, 'activation.refreshed', async (record) => {
565
+ if (!this.ports.stackFacts)
566
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'stack facts adapter is required to refresh activation');
567
+ const current = await this.ports.stackFacts.inspect({ projectRoot: record.projectRoot });
568
+ let runtimeActivation = record.runtimeActivation;
569
+ if (this.ports.runtimeActivation) {
570
+ const projection = await this.ports.runtimeActivation.capture({ projectRoot: record.projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: record.baseRef });
571
+ const body = {
572
+ schemaVersion: 'delivery-runtime-activation.v1',
573
+ projectRoot: record.projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: record.baseRef,
574
+ ...projection,
575
+ contextReceiptHash: record.contextReceipt ?? input.contextReceipt,
576
+ stackFactsHash: current.fingerprint,
577
+ pluginSelectionHash: record.pluginSelection ? hash(record.pluginSelection) : hash({ selectedPlugins: current.selectedPlugins, selectedRunners: current.selectedRunners }),
578
+ createdAt: Date.now(),
579
+ };
580
+ runtimeActivation = { ...body, snapshotHash: hash(body) };
581
+ }
582
+ const changed = record.stackFacts?.fingerprint !== current.fingerprint || runtimeActivation?.snapshotHash !== record.runtimeActivation?.snapshotHash;
583
+ let next = changed && ['plan_approved', 'executing', 'held', 'blocked', 'failed', 'needs_changes', 'verified', 'reviewed', 'release_pending'].includes(record.stage)
584
+ ? invalidateDownstream('plan_ready', record) : record;
585
+ next.stackFacts = { ...current, capturedAt: Date.now() };
586
+ next.runtimeActivation = runtimeActivation;
587
+ next.blockers = next.blockers?.filter(blocker => blocker.code !== 'STALE_STACK_FACTS');
588
+ return next;
589
+ });
590
+ }
483
591
  async retryTask(caseId, input) { return this.mutate(caseId, input, 'task.retried', record => { const task = record.tasks.find(item => item.id === input.taskId); if (!task)
484
592
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'task not found'); if (!['failed', 'blocked', 'needs_changes', 'verified'].includes(task.status))
485
- throw new DeliveryLineError('INVALID_TRANSITION', 'task is not retryable'); task.status = 'pending'; task.integratedRevision = undefined; if (['failed', 'blocked', 'needs_changes', 'held'].includes(record.stage))
593
+ throw new DeliveryLineError('INVALID_TRANSITION', 'task is not retryable'); const blocker = record.blockers?.find(item => item.taskId === task.id); if (blocker && !blocker.retryable)
594
+ throw new DeliveryLineError('INVALID_TRANSITION', `task failure is not retryable: ${blocker.code}`); task.status = 'pending'; task.integratedRevision = undefined; record.blockers = record.blockers?.filter(item => item.taskId !== task.id); if (['failed', 'blocked', 'needs_changes', 'held'].includes(record.stage))
486
595
  record.stage = 'executing'; return record; }); }
487
596
  async importExternalChange(caseId, input) {
488
597
  requireIdentity(input);
@@ -512,7 +621,7 @@ export class DeliveryLineFacade {
512
621
  task.evidence = [];
513
622
  if (!this.ports.verification || !this.ports.integration)
514
623
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'verification and integration adapters are required for external import');
515
- const verification = await this.ports.verification.verify({ caseId: record.id, taskId: task.id, projectRoot: record.projectRoot, commit: authority.candidateRevision, commands: task.verificationCommands });
624
+ const verification = await this.ports.verification.verify({ caseId: record.id, taskId: task.id, projectRoot: record.projectRoot, commit: authority.candidateRevision, commands: task.verificationCommands, acceptanceCriteria: task.acceptanceCriteria });
516
625
  if (!verification.passed || verification.commit !== authority.candidateRevision)
517
626
  throw new DeliveryLineError('VERIFICATION_FAILED', 'external candidate verification failed');
518
627
  record.evidence.push(this.evidence('verification', verification));
@@ -525,7 +634,7 @@ export class DeliveryLineFacade {
525
634
  record.evidence.push({ id: randomUUID(), kind: 'merge', hash: hash(integrated), createdAt: Date.now(), metadata: integrated });
526
635
  record.stage = 'verified';
527
636
  if (record.lineage)
528
- record.lineage = { ...record.lineage, candidateRevision: integrated.candidateRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
637
+ record.lineage = { ...record.lineage, candidateRevision: integrated.candidateRevision, integrationRevision: integrated.candidateRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
529
638
  return record;
530
639
  });
531
640
  }
@@ -538,10 +647,29 @@ export class DeliveryLineFacade {
538
647
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'delivery package adapter is required');
539
648
  if (record.tasks.length === 0 || record.tasks.some(task => task.status !== 'integrated' || !task.integratedRevision))
540
649
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'all tasks must be integrated before export');
650
+ if (!['intent', 'spec', 'plan'].every(type => record.artifacts.some(artifact => artifact.type === type)))
651
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'intent, spec, and plan lineage is required before export');
541
652
  if (!record.lineage || (record.tasks[0]?.planHash && record.lineage.planHash !== record.tasks[0].planHash) || record.lineage.baseRevision !== record.baseRef)
542
653
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'execution lineage is incomplete or stale');
543
654
  if (!record.evidence.some(item => item.kind === 'verification') || !record.evidence.some(item => item.kind === 'review'))
544
655
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'verification and review evidence are required before export');
656
+ if (!record.evidence.some(item => item.kind === 'merge' && item.metadata.candidateRevision === record.integrationRevision))
657
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'integration lineage is required before export');
658
+ const intentHash = record.artifacts.find(item => item.type === 'intent')?.contentHash;
659
+ const specHash = record.artifacts.find(item => item.type === 'spec')?.contentHash;
660
+ const currentVerificationHashes = record.evidence.filter(item => item.kind === 'verification').map(item => item.hash);
661
+ const currentReviewHashes = record.evidence.filter(item => item.kind === 'review').map(item => item.hash);
662
+ if (record.lineage.intentArtifactHash !== intentHash || record.lineage.specArtifactHash !== specHash
663
+ || record.lineage.integrationRevision !== record.integrationRevision
664
+ || record.lineage.verificationEvidenceHashes.length === 0
665
+ || record.lineage.verificationEvidenceHashes.some(item => !currentVerificationHashes.includes(item))
666
+ || record.lineage.reviewEvidenceHashes.length === 0
667
+ || record.lineage.reviewEvidenceHashes.some(item => !currentReviewHashes.includes(item))) {
668
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'execution lineage does not contain the current intent, spec, verification, review, and integration evidence');
669
+ }
670
+ for (const pluginName of record.stackFacts?.selectedRunners ?? [])
671
+ if (!record.evidence.some(item => item.kind === 'tool' && item.metadata.pluginName === pluginName && item.metadata.stackFactsFingerprint === record.stackFacts?.fingerprint && item.metadata.verified === true))
672
+ throw new DeliveryLineError('MISSING_PREREQUISITE', `verified plugin runner lineage is missing: ${pluginName}`);
545
673
  const packageInput = { record, path: input.path };
546
674
  const operationId = `package:${record.id}:${input.idempotencyKey}`;
547
675
  const requestHash = hash({ caseId: record.id, path: input.path, integrationRevision: record.integrationRevision, artifacts: record.artifacts.map(item => item.contentHash), evidence: record.evidence.map(item => item.hash) });
@@ -593,6 +721,8 @@ export class DeliveryLineFacade {
593
721
  const packageArtifact = { id: randomUUID(), caseId: record.id, type: 'delivery-package', repoId: record.projectRoot, ref: record.integrationRef, commit: record.integrationRevision ?? record.baseRef, path: exported.path, blobHash: exported.packageHash, parentBlobHashes: record.artifacts.length ? [record.artifacts.at(-1).blobHash] : [], contentHash: exported.packageHash, createdBy: input.actor, createdAt: Date.now() };
594
722
  record.artifacts.push(packageArtifact);
595
723
  record.evidence.push({ id: randomUUID(), kind: 'export', hash: exported.packageHash, createdAt: Date.now(), metadata: { path: exported.path, packageHash: exported.packageHash, integrationRevision: record.integrationRevision, exportBindingHash: hash({ packageHash: exported.packageHash, path: exported.path, integrationRevision: record.integrationRevision }), candidateRevisions: record.tasks.map(task => task.integratedRevision) } });
724
+ if (record.lineage)
725
+ record.lineage = { ...record.lineage, exportEvidenceHash: exported.packageHash };
596
726
  record.stage = 'delivered';
597
727
  return record;
598
728
  });
@@ -651,7 +781,7 @@ export class DeliveryLineFacade {
651
781
  return this.createArtifact(record, 'interaction', content, actor, { artifactPath: path, path: path.replace(/\.md$/u, '.json'), content: `${JSON.stringify(data, null, 2)}\n` });
652
782
  }
653
783
  evidence(kind, value) { return { id: randomUUID(), kind, hash: hash(value), createdAt: Date.now(), metadata: value }; }
654
- view(record) { const pendingDecisions = record.interaction.openQuestions.map((question, index) => ({ id: `${record.id}:question:${record.interaction.turn}:${index}`, kind: 'product', question, options: [], blocking: record.interaction.blockingUnknowns.includes(question), turn: record.interaction.turn })); const nextActions = nextActionsFor(record); const blockers = record.blockers ?? (['held', 'failed', 'needs_changes', 'blocked'].includes(record.stage) ? [this.defaultBlocker(record)] : []); return { caseId: record.id, currentTurn: record.interaction.turn, contextReceipt: record.contextReceipt, ...(record.contextQuality ? { contextQuality: record.contextQuality } : {}), ...(record.sessionId && record.executionId ? { canonicalIdentity: { sessionId: record.sessionId, executionId: record.executionId, actorId: record.createdBy.id } } : {}), stage: record.stage, pendingDecisions, nextActions, blockers, artifactRefs: record.artifacts, evidenceRefs: record.evidence, taskSummaries: record.tasks.map(task => ({ taskId: task.id, status: task.status, attempt: task.attempt })), budget: { interactionsUsed: record.interaction.turn, taskAttemptsUsed: record.tasks.reduce((sum, task) => sum + task.attempt, 0), wallTimeMs: Math.max(0, Date.now() - (record.createdAt ?? record.updatedAt)), toolCalls: 0 } }; }
784
+ view(record) { const pendingDecisions = record.interaction.openQuestions.map((question, index) => ({ id: `${record.id}:question:${record.interaction.turn}:${index}`, kind: 'product', question, options: [], blocking: record.interaction.blockingUnknowns.includes(question), turn: record.interaction.turn })); const nextActions = nextActionsFor(record); const blockers = record.blockers ?? (['held', 'failed', 'needs_changes', 'blocked'].includes(record.stage) ? [this.defaultBlocker(record)] : []); return { caseId: record.id, currentTurn: record.interaction.turn, contextReceipt: record.contextReceipt, ...(record.contextQuality ? { contextQuality: record.contextQuality } : {}), ...(record.sessionId && record.executionId ? { canonicalIdentity: { sessionId: record.sessionId, executionId: record.executionId, actorId: record.createdBy.id } } : {}), ...(record.runtimeActivation ? { runtimeActivationHash: record.runtimeActivation.snapshotHash } : {}), stage: record.stage, pendingDecisions, nextActions, blockers, artifactRefs: record.artifacts, evidenceRefs: record.evidence, taskSummaries: record.tasks.map(task => ({ taskId: task.id, status: task.status, attempt: task.attempt })), budget: { interactionsUsed: record.interaction.turn, taskAttemptsUsed: record.tasks.reduce((sum, task) => sum + task.attempt, 0), wallTimeMs: Math.max(0, Date.now() - (record.createdAt ?? record.updatedAt)), toolCalls: 0 } }; }
655
785
  defaultBlocker(record) {
656
786
  const task = record.tasks.find(item => ['blocked', 'failed', 'needs_changes'].includes(item.status));
657
787
  const code = record.stage === 'held' ? 'HELD' : record.stage.toUpperCase();