@fitlab-ai/agent-infra 0.11.2 → 0.11.4
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/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +49 -343
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +1 -2
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +37 -38
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +319 -234
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +11 -1
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +29 -13
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +47 -362
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +1 -2
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +41 -48
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +310 -216
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +15 -2
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +56 -27
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/sandbox/control/finalization-response.js +0 -35
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/finalization-response.ts +0 -37
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
|
@@ -15,8 +15,11 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
15
15
|
import { writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
17
|
import { allowsManualOverride } from './guard-override.ts';
|
|
18
|
-
import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
19
|
-
import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
18
|
+
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
19
|
+
import type { ReworkClassification, ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
20
|
+
import { extractSection, extractSubSection } from './sections.ts';
|
|
21
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
22
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
20
23
|
|
|
21
24
|
type ReviewSeverity = 'blocker' | 'major' | 'minor';
|
|
22
25
|
type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
|
|
@@ -27,7 +30,8 @@ type LedgerIntent =
|
|
|
27
30
|
| { kind: 'finding-review'; taskRef: string; id: string; status: ReviewDisposition; evidence: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
28
31
|
| { kind: 'decision-next-id'; taskRef: string }
|
|
29
32
|
| { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
30
|
-
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string;
|
|
33
|
+
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string; classification: ReworkClassification; dryRun?: boolean }
|
|
34
|
+
| { kind: 'rework-intent-rebuild'; taskRef: string; findingId?: string; sourceArtifact?: string; sourceSha256?: string; classification?: ReworkClassification; dryRun?: boolean };
|
|
31
35
|
|
|
32
36
|
type LedgerIntentError = { code: string; message: string };
|
|
33
37
|
type LedgerIntentResult = {
|
|
@@ -39,6 +43,7 @@ type LedgerIntentResult = {
|
|
|
39
43
|
before: LedgerRow | null;
|
|
40
44
|
after: LedgerRow | null;
|
|
41
45
|
operations: readonly TaskOperationSummary[];
|
|
46
|
+
rebuild?: Readonly<{ mode: 'format-only' | 'pending-reclassification'; beforeSha256: string; afterSha256: string; converted: number; newIntentId: string | null }>;
|
|
42
47
|
error: LedgerIntentError | null;
|
|
43
48
|
};
|
|
44
49
|
|
|
@@ -60,12 +65,53 @@ function ledgerReadErrorCode(error: unknown): string {
|
|
|
60
65
|
return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const CLASSIFICATION_TARGET: Record<ReworkClassification, ReworkTarget> = {
|
|
69
|
+
'scope-or-requirement': 'analysis', design: 'plan', implementation: 'code',
|
|
70
|
+
'human-decision': 'pause', 'insufficient-evidence': 'pause'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function semanticDigest(value: string): string {
|
|
74
|
+
return createHash('sha256').update(value.normalize('NFKC').replace(/\s+/g, ' ').trim()).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizedTaskFact(value: string): string {
|
|
78
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function semanticFindingEvidence(reviewContent: string, heading: ReturnType<typeof scanVisibleMarkdown>['headings'][number], end: number): string {
|
|
82
|
+
const title = heading.text.replace(/^\d+[.、::]\s*/, '').trim();
|
|
83
|
+
return `${title}\n${reviewContent.slice(heading.end, end)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findingEvidence(reviewContent: string, evidence: string): string | null {
|
|
87
|
+
const anchor = evidence.split('#')[1];
|
|
88
|
+
if (!anchor) return null;
|
|
89
|
+
const markdown = scanVisibleMarkdown(reviewContent);
|
|
90
|
+
const explicit = markdown.anchors.find((candidate) => candidate.id === anchor);
|
|
91
|
+
if (explicit) {
|
|
92
|
+
const heading = markdown.headings.find((candidate) => candidate.start > explicit.start);
|
|
93
|
+
const next = heading && markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
94
|
+
const end = next?.start ?? reviewContent.length;
|
|
95
|
+
return heading ? semanticFindingEvidence(reviewContent, heading, end) : reviewContent.slice(explicit.end, end);
|
|
96
|
+
}
|
|
97
|
+
const heading = markdown.headings.find((candidate) => {
|
|
98
|
+
const escapedAnchor = anchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
return candidate.text === anchor || new RegExp(`^${escapedAnchor}(?:\\s|[.、::])`).test(candidate.text);
|
|
100
|
+
});
|
|
101
|
+
if (!heading) return null;
|
|
102
|
+
const next = markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
103
|
+
return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function taskFactDigest(taskDir: string, content: string): string {
|
|
107
|
+
const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
|
|
108
|
+
const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
|
|
109
|
+
const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
|
|
110
|
+
return semanticDigest(JSON.stringify({
|
|
111
|
+
taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
|
|
112
|
+
requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
|
|
113
|
+
flow: flow?.status === 'valid' ? flow.decision.semanticDigest : flow?.status ?? 'missing'
|
|
114
|
+
}));
|
|
69
115
|
}
|
|
70
116
|
|
|
71
117
|
function nextFindingId(rows: readonly LedgerRow[], stage: ReviewStage): string {
|
|
@@ -123,11 +169,14 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
123
169
|
}
|
|
124
170
|
|
|
125
171
|
if (intent.kind === 'rework-intent-upsert') {
|
|
126
|
-
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !
|
|
127
|
-
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity,
|
|
172
|
+
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !(intent.classification in CLASSIFICATION_TARGET) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
|
|
173
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, classification, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
128
174
|
}
|
|
129
175
|
const finding = rows.find((row) => row.id === intent.findingId);
|
|
130
176
|
if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
177
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
178
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId, intent.findingId);
|
|
179
|
+
}
|
|
131
180
|
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
132
181
|
if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
|
|
133
182
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
@@ -137,12 +186,34 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
137
186
|
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
|
|
138
187
|
const parsed = parseReworkIntentDocument(content);
|
|
139
188
|
if (!parsed.ok) return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
140
|
-
const
|
|
189
|
+
const reviewContent = fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact), 'utf8');
|
|
190
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
191
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId, intent.intentId);
|
|
192
|
+
const evidenceDigest = semanticDigest(evidenceBlock);
|
|
193
|
+
const factsDigest = taskFactDigest(resolved.taskDir, content);
|
|
194
|
+
const requestedIntent: ReworkIntent = {
|
|
141
195
|
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
142
|
-
sourceSha256: intent.sourceSha256, target: intent.
|
|
196
|
+
sourceSha256: intent.sourceSha256, target: CLASSIFICATION_TARGET[intent.classification], classification: intent.classification,
|
|
197
|
+
evidenceDigest, taskFactDigest: factsDigest, status: 'pending',
|
|
143
198
|
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
144
199
|
};
|
|
145
|
-
|
|
200
|
+
if (parsed.intents.some((candidate) => candidate.intentId === intent.intentId)) {
|
|
201
|
+
const replay = upsertReworkIntent(parsed.intents, requestedIntent);
|
|
202
|
+
if (!replay.changed) {
|
|
203
|
+
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const same = parsed.intents.find((candidate) => candidate.findingId === intent.findingId
|
|
207
|
+
&& candidate.evidenceDigest === evidenceDigest && candidate.taskFactDigest === factsDigest
|
|
208
|
+
&& candidate.classification === intent.classification && candidate.target === CLASSIFICATION_TARGET[intent.classification]);
|
|
209
|
+
const classification: ReworkClassification = same ? 'insufficient-evidence' : intent.classification;
|
|
210
|
+
const nextIntent: ReworkIntent = {
|
|
211
|
+
...requestedIntent, target: CLASSIFICATION_TARGET[classification], classification
|
|
212
|
+
};
|
|
213
|
+
const now = nextIntent.declaredAt;
|
|
214
|
+
const superseded = parsed.intents.map((candidate) => candidate.status === 'pending'
|
|
215
|
+
? { ...candidate, status: 'superseded' as const, consumedAt: now } : candidate);
|
|
216
|
+
const next = upsertReworkIntent(superseded, nextIntent);
|
|
146
217
|
if (!next.changed) return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
147
218
|
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
148
219
|
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
|
|
@@ -152,6 +223,75 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
152
223
|
}
|
|
153
224
|
}
|
|
154
225
|
|
|
226
|
+
if (intent.kind === 'rework-intent-rebuild') {
|
|
227
|
+
const beforeSha256 = createHash('sha256').update(content).digest('hex');
|
|
228
|
+
const supplied = [intent.findingId, intent.sourceArtifact, intent.sourceSha256, intent.classification].filter((value) => value !== undefined).length;
|
|
229
|
+
if (supplied !== 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild binding options must be provided together', resolved.taskId);
|
|
230
|
+
const current = parseReworkIntentDocument(content);
|
|
231
|
+
if (current.ok && current.present) {
|
|
232
|
+
if (supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'current rework intent format does not accept rebuild binding options', resolved.taskId);
|
|
233
|
+
return {
|
|
234
|
+
status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: null,
|
|
235
|
+
before: null, after: null, operations: [], error: null,
|
|
236
|
+
rebuild: { mode: 'format-only', beforeSha256, afterSha256: beforeSha256, converted: current.intents.length, newIntentId: null }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const legacy = parseLegacyReworkIntentDocument(content);
|
|
240
|
+
if (!legacy.ok) return failed(intent, legacy.code, legacy.message, resolved.taskId);
|
|
241
|
+
const pending = legacy.intents.filter((row) => row.status === 'pending');
|
|
242
|
+
if (pending.length === 0 && supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'format-only rebuild does not accept finding or classification options', resolved.taskId);
|
|
243
|
+
if (pending.length > 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'legacy pending intents require finding, review, hash, and classification', resolved.taskId);
|
|
244
|
+
const now = new Date().toISOString();
|
|
245
|
+
let next = legacy.intents.map((row) => row.status === 'pending' ? { ...row, status: 'superseded' as const, consumedAt: now } : row);
|
|
246
|
+
let entityId: string | null = null;
|
|
247
|
+
if (pending.length > 0) {
|
|
248
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
249
|
+
if (!finding || finding.severity === 'decision' || finding.evidence.split('#')[0] !== intent.sourceArtifact) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild binding does not match a review finding', resolved.taskId);
|
|
250
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
251
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId);
|
|
252
|
+
}
|
|
253
|
+
const sourceIdentity = parseArtifactName(intent.sourceArtifact!);
|
|
254
|
+
const expectedFamily = `review-${finding.stage}`;
|
|
255
|
+
const latestSource = fs.readdirSync(resolved.taskDir)
|
|
256
|
+
.map((name) => parseArtifactName(name))
|
|
257
|
+
.filter((identity) => identity?.family === expectedFamily)
|
|
258
|
+
.sort((left, right) => right!.round - left!.round || left!.name.localeCompare(right!.name))[0];
|
|
259
|
+
if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
|
|
260
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
|
|
261
|
+
}
|
|
262
|
+
const sourcePath = path.join(resolved.taskDir, intent.sourceArtifact!);
|
|
263
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(sourcePath)).digest('hex');
|
|
264
|
+
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact hash does not match', resolved.taskId);
|
|
265
|
+
const max = next.reduce((value, row) => Math.max(value, Number(row.intentId.slice(3))), 0);
|
|
266
|
+
entityId = `RI-${max + 1}`;
|
|
267
|
+
const reviewContent = fs.readFileSync(sourcePath, 'utf8');
|
|
268
|
+
const classification = intent.classification!;
|
|
269
|
+
if (!(classification in CLASSIFICATION_TARGET)) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild classification is invalid', resolved.taskId);
|
|
270
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
271
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId);
|
|
272
|
+
next = [...next, {
|
|
273
|
+
intentId: entityId, findingId: intent.findingId!, sourceArtifact: intent.sourceArtifact!, sourceSha256: intent.sourceSha256!,
|
|
274
|
+
target: CLASSIFICATION_TARGET[classification], classification,
|
|
275
|
+
evidenceDigest: semanticDigest(evidenceBlock),
|
|
276
|
+
taskFactDigest: taskFactDigest(resolved.taskDir, content), status: 'pending', declaredAt: now, consumedAt: ''
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
280
|
+
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
|
|
281
|
+
const afterContent = result.status === 'applied' || result.status === 'no-op' ? fs.readFileSync(resolved.taskMdPath, 'utf8') : content;
|
|
282
|
+
return {
|
|
283
|
+
status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId,
|
|
284
|
+
before: null, after: null, operations: result.operations, error: null,
|
|
285
|
+
rebuild: {
|
|
286
|
+
mode: pending.length > 0 ? 'pending-reclassification' : 'format-only',
|
|
287
|
+
beforeSha256,
|
|
288
|
+
afterSha256: createHash('sha256').update(afterContent).digest('hex'),
|
|
289
|
+
converted: legacy.intents.length,
|
|
290
|
+
newIntentId: entityId
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
155
295
|
let before: LedgerRow | null = null;
|
|
156
296
|
let after: LedgerRow;
|
|
157
297
|
if (intent.kind === 'finding-upsert') {
|
|
@@ -213,7 +353,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
213
353
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
214
354
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
|
|
215
355
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
216
|
-
if ((
|
|
356
|
+
if ((!sameRoundMinorClose && !allowed.has(intent.status))
|
|
217
357
|
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
218
358
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
219
359
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { extractSection } from './sections.ts';
|
|
4
|
+
|
|
5
|
+
const PATH_STAGES = {
|
|
6
|
+
streamlined: ['analysis', 'code', 'review-code'],
|
|
7
|
+
standard: ['analysis', 'plan', 'code', 'review-code'],
|
|
8
|
+
full: ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
type LifecyclePath = keyof typeof PATH_STAGES;
|
|
12
|
+
type LifecycleStage = (typeof PATH_STAGES)[LifecyclePath][number];
|
|
13
|
+
type LifecyclePathDecision = Readonly<{
|
|
14
|
+
path: LifecyclePath;
|
|
15
|
+
stages: readonly LifecycleStage[];
|
|
16
|
+
basis: string;
|
|
17
|
+
unmetHigherConditions: string;
|
|
18
|
+
upgradeTriggers: string;
|
|
19
|
+
semanticDigest: string;
|
|
20
|
+
sourceArtifact: string;
|
|
21
|
+
sourceSha256: string;
|
|
22
|
+
}>;
|
|
23
|
+
type LifecyclePathState =
|
|
24
|
+
| Readonly<{ status: 'missing'; decision: null; message: string }>
|
|
25
|
+
| Readonly<{ status: 'invalid'; decision: null; message: string }>
|
|
26
|
+
| Readonly<{ status: 'valid'; decision: LifecyclePathDecision; message: null }>;
|
|
27
|
+
|
|
28
|
+
const FIELD_ALIASES = {
|
|
29
|
+
path: ['本任务路径', 'Path'],
|
|
30
|
+
basis: ['判定依据', 'Basis'],
|
|
31
|
+
unmetHigherConditions: ['未满足的更高路径条件', '未满足的较低路径条件', 'Unmet Higher-path Conditions'],
|
|
32
|
+
upgradeTriggers: ['升级触发条件', 'Upgrade Triggers']
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
function normalizeText(value: string): string {
|
|
36
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim().replace(/[。.]$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function digest(value: unknown): string {
|
|
40
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pathToken(value: string): LifecyclePath | null {
|
|
44
|
+
const normalized = normalizeText(value).toLowerCase().replace(/[`*_]/g, '');
|
|
45
|
+
if (/^(?:streamlined|精简路径|精简)$/.test(normalized)) return 'streamlined';
|
|
46
|
+
if (/^(?:standard|标准路径|标准)$/.test(normalized)) return 'standard';
|
|
47
|
+
if (/^(?:full|完整路径|完整)$/.test(normalized)) return 'full';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fieldValues(body: string, aliases: readonly string[]): string[] {
|
|
52
|
+
const wanted = new Set(aliases);
|
|
53
|
+
const values: string[] = [];
|
|
54
|
+
for (const line of body.split(/\r?\n/)) {
|
|
55
|
+
const match = /^\s*[-*]\s+\*\*([^*]+)\*\*[::]\s*(.*?)\s*$/.exec(line);
|
|
56
|
+
if (match && wanted.has(match[1]!.trim())) values.push(match[2]!.trim());
|
|
57
|
+
}
|
|
58
|
+
return values;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseLifecyclePathDecision(
|
|
62
|
+
analysisContent: string,
|
|
63
|
+
sourceArtifact = '',
|
|
64
|
+
sourceSha256 = ''
|
|
65
|
+
): LifecyclePathState {
|
|
66
|
+
const body = extractSection(analysisContent, ['流程裁定', 'Flow Decision']);
|
|
67
|
+
if (!body) return { status: 'missing', decision: null, message: 'analysis flow decision section is missing' };
|
|
68
|
+
const fields = Object.fromEntries(Object.entries(FIELD_ALIASES).map(([key, aliases]) => [key, fieldValues(body, aliases)])) as Record<keyof typeof FIELD_ALIASES, string[]>;
|
|
69
|
+
const invalid = Object.entries(fields).find(([, values]) => values.length !== 1);
|
|
70
|
+
if (invalid) return { status: 'invalid', decision: null, message: `flow decision field '${invalid[0]}' must appear exactly once` };
|
|
71
|
+
const path = pathToken(fields.path[0]!);
|
|
72
|
+
if (!path) return { status: 'invalid', decision: null, message: `unknown lifecycle path '${fields.path[0]}'` };
|
|
73
|
+
const basis = normalizeText(fields.basis[0]!);
|
|
74
|
+
const unmetHigherConditions = normalizeText(fields.unmetHigherConditions[0]!);
|
|
75
|
+
const upgradeTriggers = normalizeText(fields.upgradeTriggers[0]!);
|
|
76
|
+
if (!basis || !unmetHigherConditions || !upgradeTriggers) return { status: 'invalid', decision: null, message: 'flow decision fields must be non-empty' };
|
|
77
|
+
return {
|
|
78
|
+
status: 'valid',
|
|
79
|
+
message: null,
|
|
80
|
+
decision: {
|
|
81
|
+
path,
|
|
82
|
+
stages: PATH_STAGES[path],
|
|
83
|
+
basis,
|
|
84
|
+
unmetHigherConditions,
|
|
85
|
+
upgradeTriggers,
|
|
86
|
+
semanticDigest: digest({ path, basis, unmetHigherConditions, upgradeTriggers }),
|
|
87
|
+
sourceArtifact,
|
|
88
|
+
sourceSha256
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pathIncludes(state: LifecyclePathState | undefined, stage: LifecycleStage): boolean {
|
|
94
|
+
return state?.status === 'valid' && state.decision.stages.includes(stage);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { PATH_STAGES, parseLifecyclePathDecision, pathIncludes };
|
|
98
|
+
export type { LifecyclePath, LifecyclePathDecision, LifecyclePathState, LifecycleStage };
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -58,7 +58,7 @@ type TaskLifecycleRequest =
|
|
|
58
58
|
| { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
59
59
|
| { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
60
60
|
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
61
|
-
| { taskRef: string; intent: 'recover-started'; agent: string;
|
|
61
|
+
| { taskRef: string; intent: 'recover-started'; agent: string; auto: true; dryRun?: boolean };
|
|
62
62
|
|
|
63
63
|
type HotState = 'active' | 'blocked' | 'completed';
|
|
64
64
|
type SourceState = HotState | 'staging';
|
|
@@ -108,6 +108,9 @@ type TaskLifecycleOptions = {
|
|
|
108
108
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
109
109
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
110
110
|
manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
|
|
111
|
+
/** Write the terminal task document and durable journal, but leave the move
|
|
112
|
+
* and short-id mutation for a separately authorized commit. */
|
|
113
|
+
prepareOnly?: boolean;
|
|
111
114
|
};
|
|
112
115
|
|
|
113
116
|
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
@@ -209,7 +212,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
|
|
|
209
212
|
if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
|
|
210
213
|
if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
211
214
|
if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
212
|
-
if (request.intent === 'recover-started') return { action: 'Recover Started', note:
|
|
215
|
+
if (request.intent === 'recover-started') return { action: 'Recover Started', note: 'automatic recovery' };
|
|
213
216
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
214
217
|
}
|
|
215
218
|
|
|
@@ -574,6 +577,16 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
574
577
|
try { writeJournal(journalPath, journal, io); }
|
|
575
578
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
576
579
|
}
|
|
580
|
+
if (options.prepareOnly && !completed.has('directory-moved')) {
|
|
581
|
+
return {
|
|
582
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
583
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
584
|
+
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
585
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
586
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
587
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
588
|
+
};
|
|
589
|
+
}
|
|
577
590
|
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath)) completed.add('directory-moved');
|
|
578
591
|
if (!completed.has('directory-moved')) {
|
|
579
592
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -4,11 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { inspectDecisionDetailDuplicates } from './decision-details.ts';
|
|
5
5
|
import { scanVisibleMarkdown } from './markdown.ts';
|
|
6
6
|
import { parseArtifactName } from './artifact-name.ts';
|
|
7
|
-
import {
|
|
8
|
-
hasOpenArtifactRound,
|
|
9
|
-
} from './artifact-lifecycle.ts';
|
|
10
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
11
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
12
8
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
|
|
13
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
14
10
|
|
|
@@ -28,8 +24,7 @@ type LocalArtifactDiagnosticCode =
|
|
|
28
24
|
| 'LOCAL_DECISION_DETAIL_DUPLICATE'
|
|
29
25
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
30
26
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
31
|
-
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
32
|
-
| 'LOCAL_QUALIFICATION_AUDIT_INVALID';
|
|
27
|
+
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION';
|
|
33
28
|
|
|
34
29
|
type LocalArtifactDiagnostic = {
|
|
35
30
|
code: LocalArtifactDiagnosticCode;
|
|
@@ -40,8 +35,6 @@ type LocalArtifactDiagnostic = {
|
|
|
40
35
|
type LocalArtifactValidationOptions = {
|
|
41
36
|
family: LocalArtifactFamily;
|
|
42
37
|
requiredSections?: readonly string[];
|
|
43
|
-
taskContent?: string;
|
|
44
|
-
artifact?: string;
|
|
45
38
|
};
|
|
46
39
|
|
|
47
40
|
type LocalArtifactValidationResult = {
|
|
@@ -128,20 +121,6 @@ function validateLocalArtifact(
|
|
|
128
121
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
129
122
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
130
123
|
|
|
131
|
-
if (options.taskContent !== undefined) {
|
|
132
|
-
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
133
|
-
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
134
|
-
? expectedQualificationRelations(options.taskContent, options.family)
|
|
135
|
-
: undefined;
|
|
136
|
-
if (expected && !expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
137
|
-
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
138
|
-
family: options.family,
|
|
139
|
-
artifact: options.artifact,
|
|
140
|
-
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
141
|
-
});
|
|
142
|
-
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
124
|
return {
|
|
146
125
|
ok: diagnostics.length === 0,
|
|
147
126
|
family: options.family,
|
|
@@ -182,10 +161,8 @@ function prepareLocalArtifact(
|
|
|
182
161
|
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
183
162
|
const parsed = parseArtifactName(request.artifact);
|
|
184
163
|
if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
185
|
-
let taskContent: string;
|
|
186
164
|
let content: string;
|
|
187
165
|
try {
|
|
188
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
189
166
|
const artifactPath = path.join(resolved.taskDir, request.artifact);
|
|
190
167
|
if (!fs.statSync(artifactPath).isFile()) return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
|
|
191
168
|
content = candidate ?? fs.readFileSync(artifactPath, 'utf8');
|
|
@@ -195,7 +172,7 @@ function prepareLocalArtifact(
|
|
|
195
172
|
return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
|
|
196
173
|
}
|
|
197
174
|
const validation = validateLocalArtifact(content, {
|
|
198
|
-
family: request.family, requiredSections: request.requiredSections
|
|
175
|
+
family: request.family, requiredSections: request.requiredSections
|
|
199
176
|
});
|
|
200
177
|
const artifactSha256 = sha256Content(content);
|
|
201
178
|
const result = failedFinalization(request, {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
|
|
16
16
|
const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
|
|
17
|
+
const MANUAL_VALIDATION_RECEIPT_PLACEHOLDER = '[[manual-validation-receipt]]';
|
|
17
18
|
|
|
18
19
|
type ManualValidationReceipt = Readonly<{
|
|
19
20
|
schema: typeof MANUAL_VALIDATION_RECEIPT_SCHEMA;
|
|
@@ -73,13 +74,17 @@ function manualValidationFinalSummaryDigest(body: string): string {
|
|
|
73
74
|
const projection = heading
|
|
74
75
|
? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
|
|
75
76
|
: body;
|
|
76
|
-
const preimage = projection
|
|
77
|
+
const preimage = projection
|
|
78
|
+
.replace(/\n*<!--\s*canonical-pr-change-report\s*-->\s*/gu, '')
|
|
79
|
+
.replace(/<!--\s*manual-validation-receipt:\s*[\s\S]*?-->/gu, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER)
|
|
80
|
+
.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
77
81
|
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function manualValidationFinalSummaryProjectionMatches(body: string, receipt: ManualValidationReceipt): boolean {
|
|
81
85
|
const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
|
|
82
|
-
return body.includes(identity)
|
|
86
|
+
return (body.includes(identity) || body.includes(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER))
|
|
87
|
+
&& manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
function validateManualValidationReceipt(value: unknown, expected?: ManualValidationReceiptIdentity): ManualValidationReceiptResult {
|
|
@@ -137,6 +142,7 @@ function readManualValidationReceipt(taskDir: string, expected?: ManualValidatio
|
|
|
137
142
|
export {
|
|
138
143
|
MANUAL_VALIDATION_RECEIPT_SCHEMA,
|
|
139
144
|
MANUAL_VALIDATION_RECEIPT_VERSION,
|
|
145
|
+
MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
|
|
140
146
|
createManualValidationReceipt,
|
|
141
147
|
manualValidationFinalSummaryDigest,
|
|
142
148
|
manualValidationFinalSummaryProjectionMatches,
|
|
@@ -59,6 +59,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
|
59
59
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
60
60
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
61
61
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
62
|
+
import { resolveArtifactContext } from './artifact-lifecycle.ts';
|
|
62
63
|
|
|
63
64
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
64
65
|
type ModelPolicySource = Readonly<{
|
|
@@ -642,18 +643,14 @@ function validateSavedCurrentDiffBase(
|
|
|
642
643
|
return null;
|
|
643
644
|
}
|
|
644
645
|
|
|
645
|
-
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
|
|
646
|
+
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort' | 'round' | 'artifact'> | { completion: true } | null {
|
|
646
647
|
const recommendation = recommendNext(facts);
|
|
647
|
-
if (!recommendation.action) return { completion: true };
|
|
648
|
+
if (!recommendation.action) return recommendation.reasonCode === 'LIFECYCLE_REVIEWED' ? { completion: true } : null;
|
|
648
649
|
const action = recommendation.action;
|
|
649
650
|
if (action === 'manual-validation' || action === 'validation-run') return null;
|
|
650
|
-
const family = action === 'analysis' ? 'analysis' : action;
|
|
651
|
-
const round = maxArtifactRound([
|
|
652
|
-
...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
|
|
653
|
-
], family) + 1;
|
|
654
651
|
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
655
652
|
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
656
|
-
return { action: command, role, stage: action
|
|
653
|
+
return { action: command, role, stage: action };
|
|
657
654
|
}
|
|
658
655
|
|
|
659
656
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -786,14 +783,35 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
786
783
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
787
784
|
}
|
|
788
785
|
const action = routed.stage as LifecycleAction;
|
|
786
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
787
|
+
const artifactContext = resolveArtifactContext(taskRef, family, {
|
|
788
|
+
repoRoot: resolved.repoRoot
|
|
789
|
+
});
|
|
790
|
+
if (artifactContext.status !== 'ready' || !artifactContext.selection) {
|
|
791
|
+
return failed(
|
|
792
|
+
artifactContext.error?.code ?? 'ORCHESTRATION_ARTIFACT_SELECTION_FAILED',
|
|
793
|
+
artifactContext.error?.message ?? 'cannot determine artifact selection',
|
|
794
|
+
resolved.taskId
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (artifactContext.selection.disposition === 'reuse') {
|
|
798
|
+
return failed(
|
|
799
|
+
'ORCHESTRATION_REUSE_STALLED',
|
|
800
|
+
`lifecycle still routes to ${action} after reusing ${artifactContext.selection.artifact.name}`,
|
|
801
|
+
resolved.taskId
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const selected = artifactContext.selection.artifact;
|
|
789
805
|
const capability = canStart(action, facts.facts, {
|
|
790
|
-
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${
|
|
806
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${selected.round}`,
|
|
791
807
|
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
792
808
|
});
|
|
793
809
|
if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
|
|
794
810
|
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
795
811
|
const next: OrchestrationNext = {
|
|
796
812
|
...routed,
|
|
813
|
+
round: selected.round,
|
|
814
|
+
artifact: selected.name,
|
|
797
815
|
requestedModel: policy?.model ?? null,
|
|
798
816
|
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
799
817
|
};
|
|
@@ -1417,19 +1435,34 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1417
1435
|
}
|
|
1418
1436
|
}
|
|
1419
1437
|
|
|
1438
|
+
type ActivatedRecoveryEvent = Readonly<{
|
|
1439
|
+
receiptId: string;
|
|
1440
|
+
stage: DelegationStage;
|
|
1441
|
+
round: number;
|
|
1442
|
+
artifact: string;
|
|
1443
|
+
startedAgent: string;
|
|
1444
|
+
childId: string;
|
|
1445
|
+
stopRevision: number;
|
|
1446
|
+
consumer: string;
|
|
1447
|
+
consumedAt: string;
|
|
1448
|
+
}>;
|
|
1449
|
+
|
|
1450
|
+
function recoveryReceiptMatches(
|
|
1451
|
+
receipt: DelegationReceipt,
|
|
1452
|
+
taskId: string,
|
|
1453
|
+
event: ActivatedRecoveryEvent
|
|
1454
|
+
): boolean {
|
|
1455
|
+
return receipt.taskId === taskId
|
|
1456
|
+
&& receipt.stage === event.stage
|
|
1457
|
+
&& receipt.round === event.round
|
|
1458
|
+
&& receipt.artifact === event.artifact
|
|
1459
|
+
&& receipt.childId === event.childId
|
|
1460
|
+
&& normalizeAgentToken(event.startedAgent) === normalizeAgentToken(receipt.client);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1420
1463
|
function recoverActivatedOrchestrationDelegationUnderLock(
|
|
1421
1464
|
taskRef: string,
|
|
1422
|
-
event:
|
|
1423
|
-
receiptId: string;
|
|
1424
|
-
stage: DelegationStage;
|
|
1425
|
-
round: number;
|
|
1426
|
-
artifact: string;
|
|
1427
|
-
startedAgent: string;
|
|
1428
|
-
childId: string;
|
|
1429
|
-
stopRevision: number;
|
|
1430
|
-
consumer: string;
|
|
1431
|
-
consumedAt: string;
|
|
1432
|
-
}>,
|
|
1465
|
+
event: ActivatedRecoveryEvent,
|
|
1433
1466
|
options: OrchestrationOptions = {}
|
|
1434
1467
|
): OrchestrationResult {
|
|
1435
1468
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -1442,14 +1475,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(
|
|
|
1442
1475
|
];
|
|
1443
1476
|
const receipt = matches.find((candidate) => candidate.id === event.receiptId);
|
|
1444
1477
|
if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
|
|
1445
|
-
if (
|
|
1446
|
-
receipt.taskId !== resolved.taskId
|
|
1447
|
-
|| receipt.stage !== event.stage
|
|
1448
|
-
|| receipt.round !== event.round
|
|
1449
|
-
|| receipt.artifact !== event.artifact
|
|
1450
|
-
|| receipt.childId !== event.childId
|
|
1451
|
-
|| normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
|
|
1452
|
-
) {
|
|
1478
|
+
if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
|
|
1453
1479
|
return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
|
|
1454
1480
|
}
|
|
1455
1481
|
if (receipt.status === 'aborted' && run.pendingDelegation === null) {
|
|
@@ -1461,6 +1487,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(
|
|
|
1461
1487
|
if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
|
|
1462
1488
|
return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
|
|
1463
1489
|
}
|
|
1490
|
+
if (run.status !== 'running' || run.pause !== null) {
|
|
1491
|
+
return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'activated recovery cannot replace an unrelated orchestration pause', resolved.taskId);
|
|
1492
|
+
}
|
|
1464
1493
|
const aborted = abortActivatedDelegation(receipt, {
|
|
1465
1494
|
childId: event.childId,
|
|
1466
1495
|
stopRevision: event.stopRevision,
|