@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.
- package/dist/adapters/integration.d.ts +1 -0
- package/dist/adapters/integration.d.ts.map +1 -1
- package/dist/adapters/integration.js +1 -1
- package/dist/adapters/integration.js.map +1 -1
- package/dist/adapters/workflow-execution.d.ts.map +1 -1
- package/dist/adapters/workflow-execution.js +9 -8
- package/dist/adapters/workflow-execution.js.map +1 -1
- package/dist/artifacts/artifact-contract.js +1 -1
- package/dist/artifacts/artifact-contract.js.map +1 -1
- package/dist/artifacts/git-artifact-store.d.ts +1 -0
- package/dist/artifacts/git-artifact-store.d.ts.map +1 -1
- package/dist/artifacts/git-artifact-store.js +2 -1
- package/dist/artifacts/git-artifact-store.js.map +1 -1
- package/dist/domain/types.d.ts +20 -2
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/domain/types.js.map +1 -1
- package/dist/facade/delivery-line-facade.d.ts +4 -1
- package/dist/facade/delivery-line-facade.d.ts.map +1 -1
- package/dist/facade/delivery-line-facade.js +172 -31
- package/dist/facade/delivery-line-facade.js.map +1 -1
- package/dist/persistence/memory-store.d.ts +10 -0
- package/dist/persistence/memory-store.d.ts.map +1 -1
- package/dist/persistence/memory-store.js +9 -0
- package/dist/persistence/memory-store.js.map +1 -1
- package/dist/persistence/sqlite-store.d.ts +10 -0
- package/dist/persistence/sqlite-store.d.ts.map +1 -1
- package/dist/persistence/sqlite-store.js +23 -0
- package/dist/persistence/sqlite-store.js.map +1 -1
- package/dist/ports/index.d.ts +11 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/runtime/execution-coordinator.d.ts.map +1 -1
- package/dist/runtime/execution-coordinator.js +5 -1
- package/dist/runtime/execution-coordinator.js.map +1 -1
- package/dist/runtime/plan-compiler.d.ts +17 -1
- package/dist/runtime/plan-compiler.d.ts.map +1 -1
- package/dist/runtime/plan-compiler.js +57 -24
- package/dist/runtime/plan-compiler.js.map +1 -1
- 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};
|
|
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 === '
|
|
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) {
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|