@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
|
@@ -7,7 +7,10 @@ import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SE
|
|
|
7
7
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
8
8
|
import { writeTask } from './write.js';
|
|
9
9
|
import { allowsManualOverride } from './guard-override.js';
|
|
10
|
-
import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
|
|
10
|
+
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
|
|
11
|
+
import { extractSection, extractSubSection } from './sections.js';
|
|
12
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
13
|
+
import { scanVisibleMarkdown } from './markdown.js';
|
|
11
14
|
const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
|
|
12
15
|
const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
|
|
13
16
|
const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
|
|
@@ -23,15 +26,50 @@ function ledgerReadErrorCode(error) {
|
|
|
23
26
|
return code;
|
|
24
27
|
return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const CLASSIFICATION_TARGET = {
|
|
30
|
+
'scope-or-requirement': 'analysis', design: 'plan', implementation: 'code',
|
|
31
|
+
'human-decision': 'pause', 'insufficient-evidence': 'pause'
|
|
32
|
+
};
|
|
33
|
+
function semanticDigest(value) {
|
|
34
|
+
return createHash('sha256').update(value.normalize('NFKC').replace(/\s+/g, ' ').trim()).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
function normalizedTaskFact(value) {
|
|
37
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
38
|
+
}
|
|
39
|
+
function semanticFindingEvidence(reviewContent, heading, end) {
|
|
40
|
+
const title = heading.text.replace(/^\d+[.、::]\s*/, '').trim();
|
|
41
|
+
return `${title}\n${reviewContent.slice(heading.end, end)}`;
|
|
42
|
+
}
|
|
43
|
+
function findingEvidence(reviewContent, evidence) {
|
|
44
|
+
const anchor = evidence.split('#')[1];
|
|
45
|
+
if (!anchor)
|
|
46
|
+
return null;
|
|
47
|
+
const markdown = scanVisibleMarkdown(reviewContent);
|
|
48
|
+
const explicit = markdown.anchors.find((candidate) => candidate.id === anchor);
|
|
49
|
+
if (explicit) {
|
|
50
|
+
const heading = markdown.headings.find((candidate) => candidate.start > explicit.start);
|
|
51
|
+
const next = heading && markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
52
|
+
const end = next?.start ?? reviewContent.length;
|
|
53
|
+
return heading ? semanticFindingEvidence(reviewContent, heading, end) : reviewContent.slice(explicit.end, end);
|
|
34
54
|
}
|
|
55
|
+
const heading = markdown.headings.find((candidate) => {
|
|
56
|
+
const escapedAnchor = anchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
return candidate.text === anchor || new RegExp(`^${escapedAnchor}(?:\\s|[.、::])`).test(candidate.text);
|
|
58
|
+
});
|
|
59
|
+
if (!heading)
|
|
60
|
+
return null;
|
|
61
|
+
const next = markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
62
|
+
return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
|
|
63
|
+
}
|
|
64
|
+
function taskFactDigest(taskDir, content) {
|
|
65
|
+
const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
|
|
66
|
+
const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
|
|
67
|
+
const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
|
|
68
|
+
return semanticDigest(JSON.stringify({
|
|
69
|
+
taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
|
|
70
|
+
requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
|
|
71
|
+
flow: flow?.status === 'valid' ? flow.decision.semanticDigest : flow?.status ?? 'missing'
|
|
72
|
+
}));
|
|
35
73
|
}
|
|
36
74
|
function nextFindingId(rows, stage) {
|
|
37
75
|
const prefix = PREFIX[stage];
|
|
@@ -89,12 +127,15 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
89
127
|
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
|
|
90
128
|
}
|
|
91
129
|
if (intent.kind === 'rework-intent-upsert') {
|
|
92
|
-
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !
|
|
93
|
-
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity,
|
|
130
|
+
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)) {
|
|
131
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, classification, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
94
132
|
}
|
|
95
133
|
const finding = rows.find((row) => row.id === intent.findingId);
|
|
96
134
|
if (!finding || finding.severity === 'decision')
|
|
97
135
|
return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
136
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
137
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId, intent.findingId);
|
|
138
|
+
}
|
|
98
139
|
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
99
140
|
if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
|
|
100
141
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
@@ -106,12 +147,35 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
106
147
|
const parsed = parseReworkIntentDocument(content);
|
|
107
148
|
if (!parsed.ok)
|
|
108
149
|
return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
109
|
-
const
|
|
150
|
+
const reviewContent = fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact), 'utf8');
|
|
151
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
152
|
+
if (!evidenceBlock)
|
|
153
|
+
return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId, intent.intentId);
|
|
154
|
+
const evidenceDigest = semanticDigest(evidenceBlock);
|
|
155
|
+
const factsDigest = taskFactDigest(resolved.taskDir, content);
|
|
156
|
+
const requestedIntent = {
|
|
110
157
|
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
111
|
-
sourceSha256: intent.sourceSha256, target: intent.
|
|
158
|
+
sourceSha256: intent.sourceSha256, target: CLASSIFICATION_TARGET[intent.classification], classification: intent.classification,
|
|
159
|
+
evidenceDigest, taskFactDigest: factsDigest, status: 'pending',
|
|
112
160
|
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
113
161
|
};
|
|
114
|
-
|
|
162
|
+
if (parsed.intents.some((candidate) => candidate.intentId === intent.intentId)) {
|
|
163
|
+
const replay = upsertReworkIntent(parsed.intents, requestedIntent);
|
|
164
|
+
if (!replay.changed) {
|
|
165
|
+
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const same = parsed.intents.find((candidate) => candidate.findingId === intent.findingId
|
|
169
|
+
&& candidate.evidenceDigest === evidenceDigest && candidate.taskFactDigest === factsDigest
|
|
170
|
+
&& candidate.classification === intent.classification && candidate.target === CLASSIFICATION_TARGET[intent.classification]);
|
|
171
|
+
const classification = same ? 'insufficient-evidence' : intent.classification;
|
|
172
|
+
const nextIntent = {
|
|
173
|
+
...requestedIntent, target: CLASSIFICATION_TARGET[classification], classification
|
|
174
|
+
};
|
|
175
|
+
const now = nextIntent.declaredAt;
|
|
176
|
+
const superseded = parsed.intents.map((candidate) => candidate.status === 'pending'
|
|
177
|
+
? { ...candidate, status: 'superseded', consumedAt: now } : candidate);
|
|
178
|
+
const next = upsertReworkIntent(superseded, nextIntent);
|
|
115
179
|
if (!next.changed)
|
|
116
180
|
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
117
181
|
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 } });
|
|
@@ -123,6 +187,84 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
123
187
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
|
|
124
188
|
}
|
|
125
189
|
}
|
|
190
|
+
if (intent.kind === 'rework-intent-rebuild') {
|
|
191
|
+
const beforeSha256 = createHash('sha256').update(content).digest('hex');
|
|
192
|
+
const supplied = [intent.findingId, intent.sourceArtifact, intent.sourceSha256, intent.classification].filter((value) => value !== undefined).length;
|
|
193
|
+
if (supplied !== 0 && supplied !== 4)
|
|
194
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild binding options must be provided together', resolved.taskId);
|
|
195
|
+
const current = parseReworkIntentDocument(content);
|
|
196
|
+
if (current.ok && current.present) {
|
|
197
|
+
if (supplied !== 0)
|
|
198
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'current rework intent format does not accept rebuild binding options', resolved.taskId);
|
|
199
|
+
return {
|
|
200
|
+
status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: null,
|
|
201
|
+
before: null, after: null, operations: [], error: null,
|
|
202
|
+
rebuild: { mode: 'format-only', beforeSha256, afterSha256: beforeSha256, converted: current.intents.length, newIntentId: null }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const legacy = parseLegacyReworkIntentDocument(content);
|
|
206
|
+
if (!legacy.ok)
|
|
207
|
+
return failed(intent, legacy.code, legacy.message, resolved.taskId);
|
|
208
|
+
const pending = legacy.intents.filter((row) => row.status === 'pending');
|
|
209
|
+
if (pending.length === 0 && supplied !== 0)
|
|
210
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'format-only rebuild does not accept finding or classification options', resolved.taskId);
|
|
211
|
+
if (pending.length > 0 && supplied !== 4)
|
|
212
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'legacy pending intents require finding, review, hash, and classification', resolved.taskId);
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
let next = legacy.intents.map((row) => row.status === 'pending' ? { ...row, status: 'superseded', consumedAt: now } : row);
|
|
215
|
+
let entityId = null;
|
|
216
|
+
if (pending.length > 0) {
|
|
217
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
218
|
+
if (!finding || finding.severity === 'decision' || finding.evidence.split('#')[0] !== intent.sourceArtifact)
|
|
219
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild binding does not match a review finding', resolved.taskId);
|
|
220
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
221
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId);
|
|
222
|
+
}
|
|
223
|
+
const sourceIdentity = parseArtifactName(intent.sourceArtifact);
|
|
224
|
+
const expectedFamily = `review-${finding.stage}`;
|
|
225
|
+
const latestSource = fs.readdirSync(resolved.taskDir)
|
|
226
|
+
.map((name) => parseArtifactName(name))
|
|
227
|
+
.filter((identity) => identity?.family === expectedFamily)
|
|
228
|
+
.sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0];
|
|
229
|
+
if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
|
|
230
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
|
|
231
|
+
}
|
|
232
|
+
const sourcePath = path.join(resolved.taskDir, intent.sourceArtifact);
|
|
233
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(sourcePath)).digest('hex');
|
|
234
|
+
if (actualHash !== intent.sourceSha256)
|
|
235
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact hash does not match', resolved.taskId);
|
|
236
|
+
const max = next.reduce((value, row) => Math.max(value, Number(row.intentId.slice(3))), 0);
|
|
237
|
+
entityId = `RI-${max + 1}`;
|
|
238
|
+
const reviewContent = fs.readFileSync(sourcePath, 'utf8');
|
|
239
|
+
const classification = intent.classification;
|
|
240
|
+
if (!(classification in CLASSIFICATION_TARGET))
|
|
241
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild classification is invalid', resolved.taskId);
|
|
242
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
243
|
+
if (!evidenceBlock)
|
|
244
|
+
return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId);
|
|
245
|
+
next = [...next, {
|
|
246
|
+
intentId: entityId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact, sourceSha256: intent.sourceSha256,
|
|
247
|
+
target: CLASSIFICATION_TARGET[classification], classification,
|
|
248
|
+
evidenceDigest: semanticDigest(evidenceBlock),
|
|
249
|
+
taskFactDigest: taskFactDigest(resolved.taskDir, content), status: 'pending', declaredAt: now, consumedAt: ''
|
|
250
|
+
}];
|
|
251
|
+
}
|
|
252
|
+
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 } });
|
|
253
|
+
if (result.status === 'failed')
|
|
254
|
+
return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
|
|
255
|
+
const afterContent = result.status === 'applied' || result.status === 'no-op' ? fs.readFileSync(resolved.taskMdPath, 'utf8') : content;
|
|
256
|
+
return {
|
|
257
|
+
status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId,
|
|
258
|
+
before: null, after: null, operations: result.operations, error: null,
|
|
259
|
+
rebuild: {
|
|
260
|
+
mode: pending.length > 0 ? 'pending-reclassification' : 'format-only',
|
|
261
|
+
beforeSha256,
|
|
262
|
+
afterSha256: createHash('sha256').update(afterContent).digest('hex'),
|
|
263
|
+
converted: legacy.intents.length,
|
|
264
|
+
newIntentId: entityId
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
126
268
|
let before = null;
|
|
127
269
|
let after;
|
|
128
270
|
if (intent.kind === 'finding-upsert') {
|
|
@@ -199,7 +341,7 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
199
341
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
200
342
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
|
|
201
343
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
202
|
-
if ((
|
|
344
|
+
if ((!sameRoundMinorClose && !allowed.has(intent.status))
|
|
203
345
|
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
204
346
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
205
347
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { extractSection } from './sections.js';
|
|
3
|
+
const PATH_STAGES = {
|
|
4
|
+
streamlined: ['analysis', 'code', 'review-code'],
|
|
5
|
+
standard: ['analysis', 'plan', 'code', 'review-code'],
|
|
6
|
+
full: ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']
|
|
7
|
+
};
|
|
8
|
+
const FIELD_ALIASES = {
|
|
9
|
+
path: ['本任务路径', 'Path'],
|
|
10
|
+
basis: ['判定依据', 'Basis'],
|
|
11
|
+
unmetHigherConditions: ['未满足的更高路径条件', '未满足的较低路径条件', 'Unmet Higher-path Conditions'],
|
|
12
|
+
upgradeTriggers: ['升级触发条件', 'Upgrade Triggers']
|
|
13
|
+
};
|
|
14
|
+
function normalizeText(value) {
|
|
15
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim().replace(/[。.]$/, '');
|
|
16
|
+
}
|
|
17
|
+
function digest(value) {
|
|
18
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
function pathToken(value) {
|
|
21
|
+
const normalized = normalizeText(value).toLowerCase().replace(/[`*_]/g, '');
|
|
22
|
+
if (/^(?:streamlined|精简路径|精简)$/.test(normalized))
|
|
23
|
+
return 'streamlined';
|
|
24
|
+
if (/^(?:standard|标准路径|标准)$/.test(normalized))
|
|
25
|
+
return 'standard';
|
|
26
|
+
if (/^(?:full|完整路径|完整)$/.test(normalized))
|
|
27
|
+
return 'full';
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function fieldValues(body, aliases) {
|
|
31
|
+
const wanted = new Set(aliases);
|
|
32
|
+
const values = [];
|
|
33
|
+
for (const line of body.split(/\r?\n/)) {
|
|
34
|
+
const match = /^\s*[-*]\s+\*\*([^*]+)\*\*[::]\s*(.*?)\s*$/.exec(line);
|
|
35
|
+
if (match && wanted.has(match[1].trim()))
|
|
36
|
+
values.push(match[2].trim());
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
function parseLifecyclePathDecision(analysisContent, sourceArtifact = '', sourceSha256 = '') {
|
|
41
|
+
const body = extractSection(analysisContent, ['流程裁定', 'Flow Decision']);
|
|
42
|
+
if (!body)
|
|
43
|
+
return { status: 'missing', decision: null, message: 'analysis flow decision section is missing' };
|
|
44
|
+
const fields = Object.fromEntries(Object.entries(FIELD_ALIASES).map(([key, aliases]) => [key, fieldValues(body, aliases)]));
|
|
45
|
+
const invalid = Object.entries(fields).find(([, values]) => values.length !== 1);
|
|
46
|
+
if (invalid)
|
|
47
|
+
return { status: 'invalid', decision: null, message: `flow decision field '${invalid[0]}' must appear exactly once` };
|
|
48
|
+
const path = pathToken(fields.path[0]);
|
|
49
|
+
if (!path)
|
|
50
|
+
return { status: 'invalid', decision: null, message: `unknown lifecycle path '${fields.path[0]}'` };
|
|
51
|
+
const basis = normalizeText(fields.basis[0]);
|
|
52
|
+
const unmetHigherConditions = normalizeText(fields.unmetHigherConditions[0]);
|
|
53
|
+
const upgradeTriggers = normalizeText(fields.upgradeTriggers[0]);
|
|
54
|
+
if (!basis || !unmetHigherConditions || !upgradeTriggers)
|
|
55
|
+
return { status: 'invalid', decision: null, message: 'flow decision fields must be non-empty' };
|
|
56
|
+
return {
|
|
57
|
+
status: 'valid',
|
|
58
|
+
message: null,
|
|
59
|
+
decision: {
|
|
60
|
+
path,
|
|
61
|
+
stages: PATH_STAGES[path],
|
|
62
|
+
basis,
|
|
63
|
+
unmetHigherConditions,
|
|
64
|
+
upgradeTriggers,
|
|
65
|
+
semanticDigest: digest({ path, basis, unmetHigherConditions, upgradeTriggers }),
|
|
66
|
+
sourceArtifact,
|
|
67
|
+
sourceSha256
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function pathIncludes(state, stage) {
|
|
72
|
+
return state?.status === 'valid' && state.decision.stages.includes(stage);
|
|
73
|
+
}
|
|
74
|
+
export { PATH_STAGES, parseLifecyclePathDecision, pathIncludes };
|
|
75
|
+
//# sourceMappingURL=lifecycle-path.js.map
|
|
@@ -151,7 +151,7 @@ function actionAndNote(request, restoredFiles = 0) {
|
|
|
151
151
|
if (request.intent === 'close-dependabot')
|
|
152
152
|
return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
153
153
|
if (request.intent === 'recover-started')
|
|
154
|
-
return { action: 'Recover Started', note:
|
|
154
|
+
return { action: 'Recover Started', note: 'automatic recovery' };
|
|
155
155
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
156
156
|
}
|
|
157
157
|
function matchingCompletion(content, request, restoredFiles = 0) {
|
|
@@ -578,6 +578,16 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
578
578
|
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 });
|
|
579
579
|
}
|
|
580
580
|
}
|
|
581
|
+
if (options.prepareOnly && !completed.has('directory-moved')) {
|
|
582
|
+
return {
|
|
583
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
584
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
585
|
+
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
586
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
587
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
588
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
589
|
+
};
|
|
590
|
+
}
|
|
581
591
|
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath))
|
|
582
592
|
completed.add('directory-moved');
|
|
583
593
|
if (!completed.has('directory-moved')) {
|
|
@@ -3,9 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
4
4
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
5
5
|
import { parseArtifactName } from './artifact-name.js';
|
|
6
|
-
import { hasOpenArtifactRound, } from './artifact-lifecycle.js';
|
|
7
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
8
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
9
7
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.js';
|
|
10
8
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
11
9
|
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
@@ -57,21 +55,6 @@ function validateLocalArtifact(content, options) {
|
|
|
57
55
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
58
56
|
if (!decisionDetails.ok)
|
|
59
57
|
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
60
|
-
if (options.taskContent !== undefined) {
|
|
61
|
-
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
62
|
-
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
63
|
-
? expectedQualificationRelations(options.taskContent, options.family)
|
|
64
|
-
: undefined;
|
|
65
|
-
if (expected && !expected.ok)
|
|
66
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
67
|
-
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
68
|
-
family: options.family,
|
|
69
|
-
artifact: options.artifact,
|
|
70
|
-
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
71
|
-
});
|
|
72
|
-
if (!qualification.ok)
|
|
73
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
74
|
-
}
|
|
75
58
|
return {
|
|
76
59
|
ok: diagnostics.length === 0,
|
|
77
60
|
family: options.family,
|
|
@@ -98,10 +81,8 @@ function prepareLocalArtifact(request, candidate) {
|
|
|
98
81
|
const parsed = parseArtifactName(request.artifact);
|
|
99
82
|
if (!parsed || parsed.family !== request.family)
|
|
100
83
|
return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
101
|
-
let taskContent;
|
|
102
84
|
let content;
|
|
103
85
|
try {
|
|
104
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
105
86
|
const artifactPath = path.join(resolved.taskDir, request.artifact);
|
|
106
87
|
if (!fs.statSync(artifactPath).isFile())
|
|
107
88
|
return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
|
|
@@ -114,7 +95,7 @@ function prepareLocalArtifact(request, candidate) {
|
|
|
114
95
|
return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
|
|
115
96
|
}
|
|
116
97
|
const validation = validateLocalArtifact(content, {
|
|
117
|
-
family: request.family, requiredSections: request.requiredSections
|
|
98
|
+
family: request.family, requiredSections: request.requiredSections
|
|
118
99
|
});
|
|
119
100
|
const artifactSha256 = sha256Content(content);
|
|
120
101
|
const result = failedFinalization(request, {
|
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { MANUAL_VALIDATION_ARTIFACT as ARTIFACT, MANUAL_VALIDATION_SHA40 as SHA40, MANUAL_VALIDATION_SHA64 as SHA64, MANUAL_VALIDATION_TASK_ID as TASK_ID, exactKeys, isRecord, validTimestamp, writeJsonAtomic } from './manual-validation-shared.js';
|
|
5
5
|
const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
|
|
6
6
|
const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
|
|
7
|
+
const MANUAL_VALIDATION_RECEIPT_PLACEHOLDER = '[[manual-validation-receipt]]';
|
|
7
8
|
function invalid(message) {
|
|
8
9
|
return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_INVALID', message } };
|
|
9
10
|
}
|
|
@@ -33,12 +34,16 @@ function manualValidationFinalSummaryDigest(body) {
|
|
|
33
34
|
const projection = heading
|
|
34
35
|
? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
|
|
35
36
|
: body;
|
|
36
|
-
const preimage = projection
|
|
37
|
+
const preimage = projection
|
|
38
|
+
.replace(/\n*<!--\s*canonical-pr-change-report\s*-->\s*/gu, '')
|
|
39
|
+
.replace(/<!--\s*manual-validation-receipt:\s*[\s\S]*?-->/gu, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER)
|
|
40
|
+
.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
37
41
|
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
38
42
|
}
|
|
39
43
|
function manualValidationFinalSummaryProjectionMatches(body, receipt) {
|
|
40
44
|
const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
|
|
41
|
-
return body.includes(identity)
|
|
45
|
+
return (body.includes(identity) || body.includes(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER))
|
|
46
|
+
&& manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
|
|
42
47
|
}
|
|
43
48
|
function validateManualValidationReceipt(value, expected) {
|
|
44
49
|
if (!isRecord(value))
|
|
@@ -101,5 +106,5 @@ function readManualValidationReceipt(taskDir, expected) {
|
|
|
101
106
|
}
|
|
102
107
|
return validateManualValidationReceipt(value, expected);
|
|
103
108
|
}
|
|
104
|
-
export { MANUAL_VALIDATION_RECEIPT_SCHEMA, MANUAL_VALIDATION_RECEIPT_VERSION, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, manualValidationReceiptDigest, manualValidationReceiptPath, readManualValidationReceipt, validateManualValidationReceipt, writeManualValidationReceiptAtomic };
|
|
109
|
+
export { MANUAL_VALIDATION_RECEIPT_SCHEMA, MANUAL_VALIDATION_RECEIPT_VERSION, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, manualValidationReceiptDigest, manualValidationReceiptPath, readManualValidationReceipt, validateManualValidationReceipt, writeManualValidationReceiptAtomic };
|
|
105
110
|
//# sourceMappingURL=manual-validation-receipt.js.map
|
|
@@ -22,6 +22,7 @@ import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js'
|
|
|
22
22
|
import { hasOpenLifecycleExecution } from './activity-log.js';
|
|
23
23
|
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
24
24
|
import { normalizeAgentToken } from '../agent-clients/tokens.js';
|
|
25
|
+
import { resolveArtifactContext } from './artifact-lifecycle.js';
|
|
25
26
|
function supportsLifecycleDelegation(client) {
|
|
26
27
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
27
28
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -479,17 +480,13 @@ function validateSavedCurrentDiffBase(repoRoot, metadata, taskDir, reviewedHead)
|
|
|
479
480
|
function routeFromFacts(facts) {
|
|
480
481
|
const recommendation = recommendNext(facts);
|
|
481
482
|
if (!recommendation.action)
|
|
482
|
-
return { completion: true };
|
|
483
|
+
return recommendation.reasonCode === 'LIFECYCLE_REVIEWED' ? { completion: true } : null;
|
|
483
484
|
const action = recommendation.action;
|
|
484
485
|
if (action === 'manual-validation' || action === 'validation-run')
|
|
485
486
|
return null;
|
|
486
|
-
const family = action === 'analysis' ? 'analysis' : action;
|
|
487
|
-
const round = maxArtifactRound([
|
|
488
|
-
...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
|
|
489
|
-
], family) + 1;
|
|
490
487
|
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
491
488
|
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
492
|
-
return { action: command, role, stage: action
|
|
489
|
+
return { action: command, role, stage: action };
|
|
493
490
|
}
|
|
494
491
|
function routeOrchestration(taskRef, options = {}) {
|
|
495
492
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -619,8 +616,19 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
619
616
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
620
617
|
}
|
|
621
618
|
const action = routed.stage;
|
|
619
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
620
|
+
const artifactContext = resolveArtifactContext(taskRef, family, {
|
|
621
|
+
repoRoot: resolved.repoRoot
|
|
622
|
+
});
|
|
623
|
+
if (artifactContext.status !== 'ready' || !artifactContext.selection) {
|
|
624
|
+
return failed(artifactContext.error?.code ?? 'ORCHESTRATION_ARTIFACT_SELECTION_FAILED', artifactContext.error?.message ?? 'cannot determine artifact selection', resolved.taskId);
|
|
625
|
+
}
|
|
626
|
+
if (artifactContext.selection.disposition === 'reuse') {
|
|
627
|
+
return failed('ORCHESTRATION_REUSE_STALLED', `lifecycle still routes to ${action} after reusing ${artifactContext.selection.artifact.name}`, resolved.taskId);
|
|
628
|
+
}
|
|
629
|
+
const selected = artifactContext.selection.artifact;
|
|
622
630
|
const capability = canStart(action, facts.facts, {
|
|
623
|
-
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${
|
|
631
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${selected.round}`,
|
|
624
632
|
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
625
633
|
});
|
|
626
634
|
if (!capability.allowed)
|
|
@@ -628,6 +636,8 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
628
636
|
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
629
637
|
const next = {
|
|
630
638
|
...routed,
|
|
639
|
+
round: selected.round,
|
|
640
|
+
artifact: selected.name,
|
|
631
641
|
requestedModel: policy?.model ?? null,
|
|
632
642
|
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
633
643
|
};
|
|
@@ -1075,6 +1085,14 @@ function recoverPreparedOrchestrationDelegation(taskRef, options = {}) {
|
|
|
1075
1085
|
return failed('ORCHESTRATION_PREPARED_RECOVERY_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1076
1086
|
}
|
|
1077
1087
|
}
|
|
1088
|
+
function recoveryReceiptMatches(receipt, taskId, event) {
|
|
1089
|
+
return receipt.taskId === taskId
|
|
1090
|
+
&& receipt.stage === event.stage
|
|
1091
|
+
&& receipt.round === event.round
|
|
1092
|
+
&& receipt.artifact === event.artifact
|
|
1093
|
+
&& receipt.childId === event.childId
|
|
1094
|
+
&& normalizeAgentToken(event.startedAgent) === normalizeAgentToken(receipt.client);
|
|
1095
|
+
}
|
|
1078
1096
|
function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, options = {}) {
|
|
1079
1097
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1080
1098
|
if (!resolved.ok)
|
|
@@ -1089,12 +1107,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, option
|
|
|
1089
1107
|
const receipt = matches.find((candidate) => candidate.id === event.receiptId);
|
|
1090
1108
|
if (!receipt)
|
|
1091
1109
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
|
|
1092
|
-
if (receipt.taskId
|
|
1093
|
-
|| receipt.stage !== event.stage
|
|
1094
|
-
|| receipt.round !== event.round
|
|
1095
|
-
|| receipt.artifact !== event.artifact
|
|
1096
|
-
|| receipt.childId !== event.childId
|
|
1097
|
-
|| normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)) {
|
|
1110
|
+
if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
|
|
1098
1111
|
return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
|
|
1099
1112
|
}
|
|
1100
1113
|
if (receipt.status === 'aborted' && run.pendingDelegation === null) {
|
|
@@ -1106,6 +1119,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, option
|
|
|
1106
1119
|
if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
|
|
1107
1120
|
return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
|
|
1108
1121
|
}
|
|
1122
|
+
if (run.status !== 'running' || run.pause !== null) {
|
|
1123
|
+
return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'activated recovery cannot replace an unrelated orchestration pause', resolved.taskId);
|
|
1124
|
+
}
|
|
1109
1125
|
const aborted = abortActivatedDelegation(receipt, {
|
|
1110
1126
|
childId: event.childId,
|
|
1111
1127
|
stopRevision: event.stopRevision,
|
|
@@ -12,7 +12,6 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
12
12
|
import { allowsManualOverride } from './guard-override.js';
|
|
13
13
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
14
14
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
15
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
16
15
|
const STAGES = {
|
|
17
16
|
analysis: { family: 'review-analysis' },
|
|
18
17
|
plan: { family: 'review-plan' },
|
|
@@ -66,10 +65,8 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
|
66
65
|
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
67
66
|
if (publicationError)
|
|
68
67
|
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
69
|
-
let taskContent;
|
|
70
68
|
let content;
|
|
71
69
|
try {
|
|
72
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
73
70
|
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
74
71
|
if (!validated.ok)
|
|
75
72
|
return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
@@ -89,22 +86,12 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
|
89
86
|
const semanticDigest = canonicalSemanticDigest(content);
|
|
90
87
|
const detail = inspectDecisionDetailDuplicates(content);
|
|
91
88
|
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
|
|
92
|
-
|
|
93
|
-
if (!expected.ok)
|
|
94
|
-
return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
95
|
-
const qualification = validateQualificationAudit(taskContent, content, {
|
|
96
|
-
family: spec.family,
|
|
97
|
-
artifact: request.artifact,
|
|
98
|
-
expectedUpstreamRelations: expected.relations
|
|
99
|
-
});
|
|
100
|
-
const qualificationError = qualification.ok ? null : qualification;
|
|
101
|
-
if (!detail.ok || !structure.ok || qualificationError) {
|
|
89
|
+
if (!detail.ok || !structure.ok) {
|
|
102
90
|
const diagnostics = [
|
|
103
91
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
104
|
-
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
105
|
-
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
92
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
106
93
|
].join('; ');
|
|
107
|
-
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' :
|
|
94
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : 'REVIEW_ARTIFACT_STRUCTURE_INVALID', diagnostics, resolved.taskId, {
|
|
108
95
|
artifactSha256, semanticDigest,
|
|
109
96
|
});
|
|
110
97
|
}
|