@devflow-tools/delivery-line 0.18.45 → 0.18.47

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 (38) hide show
  1. package/dist/adapters/integration.d.ts +1 -0
  2. package/dist/adapters/integration.d.ts.map +1 -1
  3. package/dist/adapters/integration.js +1 -1
  4. package/dist/adapters/integration.js.map +1 -1
  5. package/dist/adapters/workflow-execution.d.ts.map +1 -1
  6. package/dist/adapters/workflow-execution.js +9 -8
  7. package/dist/adapters/workflow-execution.js.map +1 -1
  8. package/dist/artifacts/artifact-contract.js +1 -1
  9. package/dist/artifacts/artifact-contract.js.map +1 -1
  10. package/dist/artifacts/git-artifact-store.d.ts +1 -0
  11. package/dist/artifacts/git-artifact-store.d.ts.map +1 -1
  12. package/dist/artifacts/git-artifact-store.js +2 -1
  13. package/dist/artifacts/git-artifact-store.js.map +1 -1
  14. package/dist/domain/types.d.ts +20 -2
  15. package/dist/domain/types.d.ts.map +1 -1
  16. package/dist/domain/types.js.map +1 -1
  17. package/dist/facade/delivery-line-facade.d.ts +4 -1
  18. package/dist/facade/delivery-line-facade.d.ts.map +1 -1
  19. package/dist/facade/delivery-line-facade.js +172 -31
  20. package/dist/facade/delivery-line-facade.js.map +1 -1
  21. package/dist/persistence/memory-store.d.ts +10 -0
  22. package/dist/persistence/memory-store.d.ts.map +1 -1
  23. package/dist/persistence/memory-store.js +9 -0
  24. package/dist/persistence/memory-store.js.map +1 -1
  25. package/dist/persistence/sqlite-store.d.ts +10 -0
  26. package/dist/persistence/sqlite-store.d.ts.map +1 -1
  27. package/dist/persistence/sqlite-store.js +23 -0
  28. package/dist/persistence/sqlite-store.js.map +1 -1
  29. package/dist/ports/index.d.ts +11 -0
  30. package/dist/ports/index.d.ts.map +1 -1
  31. package/dist/runtime/execution-coordinator.d.ts.map +1 -1
  32. package/dist/runtime/execution-coordinator.js +5 -1
  33. package/dist/runtime/execution-coordinator.js.map +1 -1
  34. package/dist/runtime/plan-compiler.d.ts +17 -1
  35. package/dist/runtime/plan-compiler.d.ts.map +1 -1
  36. package/dist/runtime/plan-compiler.js +57 -24
  37. package/dist/runtime/plan-compiler.js.map +1 -1
  38. package/package.json +4 -4
@@ -53,6 +53,10 @@ function normalizeDecisionOptions(options) {
53
53
  function normalizeDecisionText(value) {
54
54
  return value.replace(/[\u0000-\u001f\u007f\ufffd]+/gu, ' ').replace(/\s+/gu, ' ').trim();
55
55
  }
56
+ function intentFingerprint(input) {
57
+ const tokens = normalizeDecisionText(input.title).toLocaleLowerCase('en-US').split(/[^\p{L}\p{N}]+/u).filter(token => token.length > 1).sort();
58
+ return hash({ projectRoot: input.projectRoot, intent: [...new Set(tokens)] });
59
+ }
56
60
  function optionsForExistingImplementations(paths) {
57
61
  const candidates = paths.slice(0, 3);
58
62
  return [
@@ -60,7 +64,7 @@ function optionsForExistingImplementations(paths) {
60
64
  ...candidates.map((path, index) => ({
61
65
  id: `existing-${index + 1}`,
62
66
  label: `Use existing implementation: ${path}`,
63
- description: `Choose ${path}; the next question selects whether to extend, replace, or retain it.`,
67
+ description: `Choose ${path}; a follow-up asks whether to extend, replace, or retain it.`,
64
68
  })),
65
69
  ...(paths.length > candidates.length ? [{
66
70
  id: 'existing-other',
@@ -69,13 +73,6 @@ function optionsForExistingImplementations(paths) {
69
73
  }] : []),
70
74
  ];
71
75
  }
72
- function optionsForExistingImplementationAction(path) {
73
- return [
74
- { id: 'extend', label: `Extend ${path}`, description: `Reuse ${path} and preserve its existing public behavior while adding the requested capability.` },
75
- { id: 'replace', label: `Replace ${path}`, description: `Replace ${path}; the plan must preserve or explicitly revise every current behavior and caller.` },
76
- { id: 'retain', label: `Retain ${path}`, description: `Keep ${path} unchanged and constrain the new work to a separate integration point.` },
77
- ];
78
- }
79
76
  function nextActionsFor(record) {
80
77
  const nextActions = [];
81
78
  if (record.interaction.status === 'awaiting_user')
@@ -97,7 +94,9 @@ function nextActionsFor(record) {
97
94
  nextActions.push('approve');
98
95
  if (record.blockers?.some(blocker => blocker.code === 'WORKTREE_ADMISSION_REQUIRED'))
99
96
  nextActions.push('admit_worktree');
100
- else if (record.blockers?.some(blocker => blocker.code === 'STALE_STACK_FACTS'))
97
+ else if (record.blockers?.some(blocker => blocker.code === 'REQUIRED_CONTEXT_MISSING'))
98
+ nextActions.push('repair_context');
99
+ else if (record.blockers?.some(blocker => blocker.code === 'STALE_STACK_FACTS' || blocker.code === 'HOST_MODEL_UNAVAILABLE' || blocker.code === 'HOST_AUTHENTICATION_REQUIRED'))
101
100
  nextActions.push('refresh_activation');
102
101
  else if (record.blockers?.some(blocker => blocker.code === 'EXTERNAL_CANDIDATE_REQUIRED'))
103
102
  nextActions.push('import_external_change');
@@ -113,6 +112,13 @@ function nextActionsFor(record) {
113
112
  nextActions.push('cancel');
114
113
  return nextActions;
115
114
  }
115
+ function existingActionOptions(path) {
116
+ return [
117
+ { id: 'extend', label: `Extend ${path}`, description: `Reuse ${path} and preserve its existing public behavior while adding the requested capability.` },
118
+ { id: 'replace', label: `Replace ${path}`, description: `Replace ${path}; preserve or explicitly revise every current behavior and caller.` },
119
+ { id: 'retain', label: `Retain ${path}`, description: `Keep ${path} unchanged and constrain the new work to a separate integration point.` },
120
+ ];
121
+ }
116
122
  function staleTurnError(record, receivedTurn) {
117
123
  const allowedActions = nextActionsFor(record);
118
124
  return new DeliveryLineError('STALE_INTERACTION_TURN', `interaction turn is stale; current turn is ${record.interaction.turn}`, {
@@ -208,6 +214,18 @@ export class DeliveryLineFacade {
208
214
  const projectRoot = this.projectRoot ?? input.projectRoot;
209
215
  const baseRef = this.ports.revision ? await this.ports.revision.resolveRevision({ repositoryPath: projectRoot, revision: input.baseRef }) : input.baseRef;
210
216
  const id = input.caseId ?? randomUUID();
217
+ const fingerprint = intentFingerprint({ projectRoot, title: input.title });
218
+ const duplicateFingerprint = await this.ports.store.findActiveCaseByFingerprint?.({ sessionId: input.sessionId, projectRoot, intentFingerprint: fingerprint });
219
+ if (duplicateFingerprint && !input.caseId) {
220
+ if (input.contextReceipt && duplicateFingerprint.contextReceipt !== input.contextReceipt)
221
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'context receipt is not authoritative');
222
+ if (duplicateFingerprint.baseRef !== baseRef || duplicateFingerprint.targetRef !== input.targetRef)
223
+ throw new DeliveryLineError('IDEMPOTENCY_CONFLICT', 'active intent is bound to a different repository target');
224
+ return this.view(duplicateFingerprint);
225
+ }
226
+ const priorCase = await this.ports.store.findCaseBySessionAndTitle?.({ sessionId: input.sessionId, projectRoot, title: input.title });
227
+ if (priorCase && !input.caseId)
228
+ return this.view(priorCase);
211
229
  if (await this.ports.store.hasIdempotencyKey(input.idempotencyKey)) {
212
230
  const existing = await this.ports.store.findCaseByIdempotencyKey?.(input.idempotencyKey);
213
231
  if (existing) {
@@ -270,13 +288,14 @@ export class DeliveryLineFacade {
270
288
  const admissionBlocked = Boolean(admission && !admission.clean && (worktreeMode === 'auto' || worktreeMode === 'clean'));
271
289
  const record = {
272
290
  id, projectRoot, baseRef, integrationRef: `refs/devflow/delivery/${id}`, targetRef: input.targetRef,
273
- title: input.title, originalRequest: input.title, source: input.source ?? 'human', riskTier, autonomy: input.autonomy ?? 'manual', stage: 'clarifying',
291
+ title: input.title, intentFingerprint: fingerprint, originalRequest: input.title, source: input.source ?? 'human', riskTier, autonomy: input.autonomy ?? 'manual', stage: 'clarifying',
274
292
  budgets: { ...defaults, ...input.budgets }, artifacts: [], tasks: [], approvals: [], decisions: [], evidence: [],
275
293
  approvalStates: {},
276
294
  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?` : unknownRegistry[0]?.question ?? '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?` : unknownRegistry[0]?.question ?? 'What outcome and acceptance criteria should this change satisfy?'], summaryRefs: [] },
277
295
  createdBy: input.actor, sessionId: input.sessionId, executionId: input.executionId, createdAt: now, updatedAt: now, version: 0, contextReceipt, worktreeMode,
278
296
  runtimeActivation: { ...activationBody, snapshotHash: hash(activationBody) },
279
297
  ...(input.existingImplementations?.length ? { existingImplementations: [...input.existingImplementations] } : {}),
298
+ ...(admission?.clean ? { worktreeAdmission: { mode: worktreeMode === 'auto' ? 'clean' : worktreeMode, effectiveWorktreePath: admission.effectiveWorktreePath ?? projectRoot, baseRevision: baseRef, currentRevision: admission.revision, dirtyFiles: [], admissionToken: hash({ projectRoot, baseRef, currentRevision: admission.revision, dirtyFiles: [] }), admittedAt: now } } : {}),
280
299
  ...(contextQuality ? { contextQuality } : {}),
281
300
  ...(stackFacts ? { stackFacts: { ...stackFacts, capturedAt: now } } : {}),
282
301
  taskSemantics,
@@ -305,7 +324,12 @@ export class DeliveryLineFacade {
305
324
  await appendLedgerEvent(this.ports.store, { caseId: id, type: 'case.created', actor: input.actor, idempotencyKey: input.idempotencyKey, payload: { stage: record.stage } });
306
325
  return this.view(record);
307
326
  }
308
- async status(caseId) { return this.view(await this.ports.store.get(caseId)); }
327
+ async status(caseId, options) {
328
+ const view = await this.view(await this.ports.store.get(caseId));
329
+ return options?.sinceVersion !== undefined && options.sinceVersion === view.stateVersion
330
+ ? { ...view, unchanged: true }
331
+ : view;
332
+ }
309
333
  async pendingDecision(caseId) {
310
334
  const record = await this.ports.store.get(caseId);
311
335
  return this.ensurePendingDecision(record);
@@ -404,6 +428,13 @@ export class DeliveryLineFacade {
404
428
  throw new DeliveryLineError('UNAUTHORIZED_ROLE', 'only an agent may propose options; a trusted user must select one');
405
429
  const current = await this.ports.store.get(caseId);
406
430
  const pending = await this.ports.store.getPendingDecision?.(caseId);
431
+ if (!['clarifying', 'designing'].includes(current.stage)) {
432
+ throw new DeliveryLineError('INVALID_TRANSITION', 'options can only be proposed during clarification or design', {
433
+ currentStage: current.stage,
434
+ allowedActions: nextActionsFor(current),
435
+ recoveryAction: 'Refresh delivery_status and follow the returned nextActions; do not reopen a completed decision.',
436
+ });
437
+ }
407
438
  const enrichesInitialDecision = Boolean(pending && pending.status === 'pending' && pending.options.length === 0 && pending.turn === current.interaction.turn && current.stage === 'clarifying');
408
439
  const options = normalizeDecisionOptions(input.options);
409
440
  if (!input.question.trim() || options.length < 2)
@@ -452,6 +483,9 @@ export class DeliveryLineFacade {
452
483
  async answer(caseId, input) {
453
484
  requireIdentity(input);
454
485
  requireTrustedUserDecision(input);
486
+ const selectedExistingIndex = input.optionId?.match(/^existing-(\d+)$/u)?.[1];
487
+ const selectedExistingPath = selectedExistingIndex ? (await this.ports.store.get(caseId)).existingImplementations?.[Number(selectedExistingIndex) - 1] : undefined;
488
+ const followUpOptions = selectedExistingPath ? existingActionOptions(selectedExistingPath) : undefined;
455
489
  return this.mutate(caseId, input, 'interaction.answered', async (record) => {
456
490
  if (!['clarifying', 'designing'].includes(record.stage))
457
491
  throw new DeliveryLineError('INVALID_TRANSITION', 'interaction answers are only accepted during clarification or design', { currentStage: record.stage, allowedStages: ['clarifying', 'designing'] });
@@ -471,6 +505,17 @@ export class DeliveryLineFacade {
471
505
  if (acceptanceCriteria.length === 0)
472
506
  acceptanceCriteria.push(rawAnswer);
473
507
  record.interaction = answerInteraction(record.interaction, input.turn);
508
+ if (selectedExistingPath && followUpOptions) {
509
+ const nextTurn = record.interaction.turn + 1;
510
+ record.interaction = {
511
+ ...record.interaction,
512
+ status: 'awaiting_user',
513
+ turn: nextTurn,
514
+ openQuestions: [`How should ${selectedExistingPath} be handled?`],
515
+ blockingUnknowns: [`How should ${selectedExistingPath} be handled?`],
516
+ };
517
+ return record;
518
+ }
474
519
  if (record.unknownRegistry) {
475
520
  const answeredQuestion = normalizeDecisionText(question);
476
521
  record.unknownRegistry = record.unknownRegistry.map(item => normalizeDecisionText(item.question) === answeredQuestion ? { ...item, blocking: false } : item);
@@ -482,13 +527,31 @@ export class DeliveryLineFacade {
482
527
  const optionsSnapshot = input.optionsSnapshot ?? (input.optionId ? [{ id: input.optionId, label: selectedLabel }] : []);
483
528
  const decisionHash = hash({ questionId, optionId: input.optionId, selectedLabel, rawAnswer, acceptanceCriteria, optionsSnapshot });
484
529
  record.decisions.push({ id: randomUUID(), kind: 'product', questionId, question, options: optionsSnapshot.map(option => option.label), optionsSnapshot, optionId: input.optionId, selectedLabel, rawAnswer, acceptanceCriteria, decisionHash, sessionId: input.sessionId, toolUseId: input.toolUseId, decision: selectedLabel, decidedBy: input.actor, artifactHash: decisionHash });
530
+ if (input.optionId === 'option-reuse')
531
+ record.reuseMode = 'reuse_as_is';
532
+ if (input.optionId === 'option-extend' || input.optionId === 'option-reuse-adjust')
533
+ record.reuseMode = 'extend';
485
534
  if (record.interaction.phase === 'clarification')
486
535
  record.artifacts.push(await this.createArtifact(record, 'intent', renderIntentMarkdown({ title: record.title, originalRequest: record.originalRequest, problem: record.originalRequest ?? record.title, outcome: selectedLabel, answer: rawAnswer, contextQuality: record.contextQuality, affectedSystems: [record.projectRoot], constraints: ['Preserve existing behavior outside the confirmed scope.'], successCriteria: acceptanceCriteria, openQuestions: record.taskSemantics && record.taskSemantics.confidence > 0 ? (record.unknownRegistry?.filter(item => item.blocking).map(item => item.question) ?? []) : record.interaction.blockingUnknowns, unknownRegistry: record.taskSemantics && record.taskSemantics.confidence > 0 ? record.unknownRegistry : undefined, actor: input.actor }), input.actor));
487
536
  await this.createProjectionArtifact(record, 'stack-facts.md', 'Stack facts', 'Project stack evidence bound at case creation.', { baseRef: record.baseRef, targetRef: record.targetRef, projectRoot: record.projectRoot }, input.actor);
488
537
  await this.createProjectionArtifact(record, 'decisions.md', 'Decisions', 'Current-turn decision ledger projection.', { decisionHash, questionId, optionId: input.optionId, selectedLabel }, input.actor);
489
538
  record.interaction = appendInteractionSummary(record.interaction, { id: randomUUID(), phase: record.interaction.phase, turn: input.turn, question, answer: rawAnswer, rawAnswer, acceptanceCriteria, decision: selectedLabel, options: optionsSnapshot.map(option => option.label), contextReceipt: input.contextReceipt, artifactHashes: [decisionHash], actor: input.actor });
490
539
  return record;
491
- });
540
+ }, selectedExistingPath && followUpOptions ? {
541
+ decisionId: `${caseId}:decision:${input.turn}`,
542
+ expectedCaseVersion: (await this.ports.store.getPendingDecision?.(caseId))?.caseVersion ?? 0,
543
+ replacement: record => ({
544
+ id: `${record.id}:decision:${record.interaction.turn}`,
545
+ caseId: record.id,
546
+ kind: 'clarification',
547
+ caseVersion: record.version + 1,
548
+ turn: record.interaction.turn,
549
+ targetDecisionHash: hash({ caseId: record.id, question: record.interaction.openQuestions[0], options: followUpOptions }),
550
+ options: followUpOptions,
551
+ status: 'pending',
552
+ createdAt: Date.now(),
553
+ }),
554
+ } : undefined);
492
555
  }
493
556
  async revise(caseId, input) {
494
557
  requireIdentity(input);
@@ -636,22 +699,47 @@ export class DeliveryLineFacade {
636
699
  throw new DeliveryLineError('INVALID_TRANSITION', 'plan preparation requires a ready plan stage', { currentStage: record.stage, recoveryAction: 'Refresh delivery_status and follow its nextActions.' });
637
700
  if (record.artifacts.some(artifact => artifact.type === 'plan'))
638
701
  throw new DeliveryLineError('IDEMPOTENCY_CONFLICT', 'a plan draft is already prepared; refresh delivery_status and approve the pending plan decision');
702
+ // Validate the model payload before doing retrieval or mutating context.
703
+ // This prevents malformed plans from consuming a retrieval turn and
704
+ // reporting an unrelated context failure as the primary error.
705
+ new PlanCompiler().compile({
706
+ caseId: record.id,
707
+ projectRoot: record.projectRoot,
708
+ baseRevision: record.baseRef,
709
+ planHash: 'preflight-plan-payload',
710
+ contextReceiptHash: input.contextReceipt,
711
+ hostSelectionSnapshotHash: 'preflight-host-selection',
712
+ executionAuthority: 'preflight-plan-authority',
713
+ plan: input.plan,
714
+ });
639
715
  if (this.ports.context) {
640
716
  const planContextQuery = `${record.originalRequest ?? record.title} implementation plan ${(record.existingImplementations ?? []).join(' ')}`.trim();
641
717
  const planContextIdentity = hash(planContextQuery).slice(0, 16);
642
- const plannerReceipt = await this.ports.context.build({
643
- role: 'planner',
644
- ...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
645
- retrieval: {
646
- taskRevision: record.version + 1,
647
- projectRoot: record.projectRoot,
648
- query: planContextQuery,
649
- sessionId: input.sessionId,
650
- executionId: input.executionId,
651
- requestId: `delivery:plan:${record.id}:${record.version}:${planContextIdentity}`,
652
- turnId: `delivery:plan:${record.id}:${record.interaction.turn}:${planContextIdentity}`,
653
- },
654
- });
718
+ let plannerReceipt;
719
+ try {
720
+ plannerReceipt = await this.ports.context.build({
721
+ role: 'planner',
722
+ ...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
723
+ retrieval: {
724
+ taskRevision: record.version + 1,
725
+ projectRoot: record.projectRoot,
726
+ query: planContextQuery,
727
+ sessionId: input.sessionId,
728
+ executionId: input.executionId,
729
+ requestId: `delivery:plan:${record.id}:${record.version}:${planContextIdentity}`,
730
+ turnId: `delivery:plan:${record.id}:${record.interaction.turn}:${planContextIdentity}`,
731
+ },
732
+ });
733
+ }
734
+ catch (error) {
735
+ if ((error instanceof DeliveryLineError && error.code === 'REQUIRED_CONTEXT_MISSING') || (error instanceof Error && /required context evidence|unrelated context evidence/i.test(error.message))) {
736
+ record.stage = 'held';
737
+ const metadata = error instanceof DeliveryLineError ? error.metadata : undefined;
738
+ record.blockers = [{ code: 'REQUIRED_CONTEXT_MISSING', message: error instanceof Error ? error.message : 'required context evidence is missing', responsibleRole: 'context-owner', artifactRefs: [], evidenceRefs: [], recoveryAction: 'Repair the planner query, then call delivery_repair_context before preparing the plan again.', retryable: true, diagnostics: { ...(metadata ?? {}), repairAttempts: Number(metadata?.repairAttempts ?? 0) } }];
739
+ return record;
740
+ }
741
+ throw error;
742
+ }
655
743
  record.contextReceipt = plannerReceipt.receiptHash;
656
744
  if (plannerReceipt.channelStatus && plannerReceipt.gaps && plannerReceipt.attribution)
657
745
  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 } : {}) };
@@ -677,6 +765,37 @@ export class DeliveryLineFacade {
677
765
  return record;
678
766
  });
679
767
  }
768
+ async repairContext(caseId, input) {
769
+ requireIdentity(input);
770
+ if (input.actor.kind !== 'agent')
771
+ throw new DeliveryLineError('UNAUTHORIZED_ROLE', 'only an agent may repair retrieval context');
772
+ return this.mutate(caseId, input, 'context.repaired', async (record) => {
773
+ const blocker = record.blockers?.find(item => item.code === 'REQUIRED_CONTEXT_MISSING');
774
+ if (!blocker)
775
+ throw new DeliveryLineError('INVALID_TRANSITION', 'case is not waiting for context repair', { currentStage: record.stage });
776
+ const attempts = Number(blocker.diagnostics?.repairAttempts ?? 0);
777
+ if (attempts >= 2)
778
+ throw new DeliveryLineError('REQUIRED_CONTEXT_MISSING', 'context repair budget exhausted; a human must provide the missing repository anchor', { repairAttempts: attempts, recoveryAction: 'Provide an exact file, symbol, or repository path, then create a new context request.' });
779
+ if (!this.ports.context)
780
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'context adapter is required for repair');
781
+ const query = `${record.originalRequest ?? record.title} implementation plan exact files symbols current repository anchor repair-${attempts + 1}`.trim();
782
+ try {
783
+ const receipt = await this.ports.context.build({ role: 'planner', retrieval: { taskRevision: record.version + 1, projectRoot: record.projectRoot, query, sessionId: input.sessionId, executionId: input.executionId, requestId: `delivery:context-repair:${record.id}:${attempts + 1}`, turnId: `delivery:context-repair:${record.id}:${record.interaction.turn}:${attempts + 1}` } });
784
+ record.contextReceipt = receipt.receiptHash;
785
+ record.contextQuality = { blocking: receipt.blocking ?? false, ...(receipt.channelAvailability ? { channelAvailability: receipt.channelAvailability } : {}), channelStatus: receipt.channelStatus ?? { code: 'degraded', memory: 'degraded', knowledge: 'degraded' }, gaps: receipt.gaps ?? [], attribution: receipt.attribution ?? { selected: [], consumed: [], verified: [] }, ...(receipt.quality ? { quality: receipt.quality } : {}) };
786
+ record.blockers = undefined;
787
+ record.stage = 'plan_ready';
788
+ return record;
789
+ }
790
+ catch (error) {
791
+ if (error instanceof DeliveryLineError && error.code === 'REQUIRED_CONTEXT_MISSING') {
792
+ record.blockers = [{ ...blocker, diagnostics: { ...(error.metadata ?? {}), repairAttempts: attempts + 1 }, recoveryAction: attempts + 1 >= 2 ? 'Provide an exact repository anchor and create a new context request.' : 'Retry delivery_repair_context once with the current case identity.' }];
793
+ return record;
794
+ }
795
+ throw error;
796
+ }
797
+ });
798
+ }
680
799
  async approve(caseId, input) {
681
800
  requireIdentity(input);
682
801
  requireTrustedUserDecision(input);
@@ -736,8 +855,21 @@ export class DeliveryLineFacade {
736
855
  record.interaction = { ...record.interaction, status: 'awaiting_approval' };
737
856
  return record;
738
857
  }
739
- if (input.stage === 'intent')
740
- record.stage = 'designing';
858
+ if (input.stage === 'intent') {
859
+ if (record.reuseMode === 'reuse_as_is') {
860
+ record.evidence.push({
861
+ id: randomUUID(),
862
+ kind: 'verification',
863
+ hash: hash({ caseId: record.id, mode: 'reuse_as_is', implementations: record.existingImplementations ?? [], contextReceipt: record.contextReceipt }),
864
+ createdAt: Date.now(),
865
+ metadata: { mode: 'reuse_as_is', implementations: record.existingImplementations ?? [], verification: 'existing implementation confirmed by user and context evidence' },
866
+ });
867
+ record.interaction = { ...record.interaction, status: 'active', openQuestions: [], blockingUnknowns: [] };
868
+ record.stage = 'delivered';
869
+ }
870
+ else
871
+ record.stage = 'designing';
872
+ }
741
873
  if (input.stage === 'spec')
742
874
  record.stage = 'plan_ready';
743
875
  if (input.stage === 'plan') {
@@ -923,7 +1055,7 @@ export class DeliveryLineFacade {
923
1055
  if (input.mode === 'clean' && admission && !admission.clean)
924
1056
  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'] } });
925
1057
  record.worktreeMode = input.mode;
926
- record.worktreeAdmission = { mode: input.mode, baseRevision: record.baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [], admissionToken: input.admissionToken, admittedAt: Date.now() };
1058
+ record.worktreeAdmission = { mode: input.mode, effectiveWorktreePath: admission?.effectiveWorktreePath ?? record.projectRoot, baseRevision: record.baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [], admissionToken: input.admissionToken, admittedAt: Date.now() };
927
1059
  record.stage = 'clarifying';
928
1060
  record.blockers = [];
929
1061
  record.interaction.status = 'awaiting_user';
@@ -1126,7 +1258,12 @@ export class DeliveryLineFacade {
1126
1258
  const next = await fn(structuredClone(record));
1127
1259
  next.version = record.version + 1;
1128
1260
  next.updatedAt = Date.now();
1129
- if (pendingDecisionUpdate) {
1261
+ if (pendingDecisionUpdate && input.trustedDecision) {
1262
+ if (!this.ports.store.saveAndConsumePendingDecision)
1263
+ throw new DeliveryLineError('MISSING_PREREQUISITE', 'trusted decision storage transaction is unavailable', { recoveryAction: 'Use a Delivery store with atomic pending-decision consumption.' });
1264
+ await this.ports.store.saveAndConsumePendingDecision(next, record.version, { decisionId: input.trustedDecision.pendingDecisionId, expectedCaseVersion: input.trustedDecision.pendingCaseVersion, nonce: input.trustedDecision.nonce, replacement: pendingDecisionUpdate.replacement(next) });
1265
+ }
1266
+ else if (pendingDecisionUpdate) {
1130
1267
  if (!this.ports.store.saveAndReplacePendingDecision)
1131
1268
  throw new DeliveryLineError('MISSING_PREREQUISITE', 'pending decision storage transaction is unavailable', { recoveryAction: 'Use a Delivery store with atomic pending-decision replacement.' });
1132
1269
  await this.ports.store.saveAndReplacePendingDecision(next, record.version, { decisionId: pendingDecisionUpdate.decisionId, expectedCaseVersion: pendingDecisionUpdate.expectedCaseVersion, replacement: pendingDecisionUpdate.replacement(next) });
@@ -1158,12 +1295,13 @@ export class DeliveryLineFacade {
1158
1295
  return this.ports.artifact.create({
1159
1296
  caseId: record.id,
1160
1297
  repoId: record.projectRoot,
1161
- repositoryPath: record.projectRoot,
1298
+ repositoryPath: record.worktreeAdmission?.effectiveWorktreePath ?? record.projectRoot,
1162
1299
  type,
1163
1300
  path: projection?.artifactPath ?? `${type}.md`,
1164
1301
  content,
1165
1302
  parentBlobHashes: record.artifacts.map(artifact => artifact.blobHash),
1166
1303
  createdBy: actor,
1304
+ ...(record.worktreeAdmission?.dirtyFiles?.length ? { allowedDirtyFiles: record.worktreeAdmission.dirtyFiles } : {}),
1167
1305
  ...(projection ? { projection } : {}),
1168
1306
  version: record.artifacts.filter(item => item.type === type).length + 1,
1169
1307
  });
@@ -1193,7 +1331,10 @@ export class DeliveryLineFacade {
1193
1331
  }));
1194
1332
  const nextActions = nextActionsFor(record);
1195
1333
  const blockers = record.blockers ?? (['held', 'failed', 'needs_changes', 'blocked'].includes(record.stage) ? [this.defaultBlocker(record)] : []);
1196
- 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 } };
1334
+ const snapshotInput = { caseId: record.id, version: record.version, turn: record.interaction.turn, stage: record.stage, pending: pending ? { id: pending.id, targetDecisionHash: pending.targetDecisionHash, caseVersion: pending.caseVersion } : null, blockers, artifacts: record.artifacts.map(item => item.contentHash), tasks: record.tasks.map(task => ({ id: task.id, status: task.status, attempt: task.attempt })) };
1335
+ const snapshotHash = hash(snapshotInput);
1336
+ const cursor = await this.ports.store.latestEventCursor?.(record.id);
1337
+ return { caseId: record.id, stateVersion: record.version, snapshotHash, ...(cursor ? { lastEventId: cursor.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 } };
1197
1338
  }
1198
1339
  async ensurePendingDecision(record) {
1199
1340
  const current = await this.ports.store.getPendingDecision?.(record.id);