@fitlab-ai/agent-infra 0.11.3 → 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 +1 -1
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- 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-operation-registry.js +1 -0
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/sandbox/control/server.js +26 -143
- 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 +3 -2
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization.js +301 -143
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +10 -0
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/orchestration.js +17 -7
- 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/internal/cli-route-inventory.ts +1 -1
- 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-operation-registry.ts +1 -0
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/sandbox/control/server.ts +21 -146
- 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 +3 -1
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization.ts +269 -111
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +13 -0
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/orchestration.ts +26 -8
- 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/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-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- 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/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 -33
- package/lib/sandbox/control/finalization-response.ts +0 -35
|
@@ -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
|
|
@@ -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, {
|
|
@@ -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
|
};
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const REWORK_INTENT_HEADINGS = ['返工意图', 'Rework Intent'];
|
|
2
2
|
const REWORK_INTENT_COLUMNS = [
|
|
3
|
+
'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
|
|
4
|
+
'classification', 'evidence_digest', 'task_fact_digest', 'status', 'declared_at', 'consumed_at'
|
|
5
|
+
];
|
|
6
|
+
const LEGACY_REWORK_INTENT_COLUMNS = [
|
|
3
7
|
'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
|
|
4
8
|
'status', 'declared_at', 'consumed_at'
|
|
5
9
|
];
|
|
@@ -55,12 +59,16 @@ function parseReworkIntentDocument(content) {
|
|
|
55
59
|
throw new Error(`invalid intent id '${row.intent_id}'`);
|
|
56
60
|
if (!/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id))
|
|
57
61
|
throw new Error(`invalid finding id '${row.finding_id}'`);
|
|
58
|
-
if (!['analysis', 'plan', 'code'].includes(row.target))
|
|
62
|
+
if (!['analysis', 'plan', 'code', 'pause'].includes(row.target))
|
|
59
63
|
throw new Error(`invalid rework target '${row.target}'`);
|
|
64
|
+
if (!['scope-or-requirement', 'design', 'implementation', 'human-decision', 'insufficient-evidence'].includes(row.classification))
|
|
65
|
+
throw new Error(`invalid rework classification '${row.classification}'`);
|
|
60
66
|
if (!['pending', 'consumed', 'superseded'].includes(row.status))
|
|
61
67
|
throw new Error(`invalid rework status '${row.status}'`);
|
|
62
68
|
if (!/^[a-f0-9]{64}$/.test(row.source_sha256))
|
|
63
69
|
throw new Error(`invalid source hash for '${row.intent_id}'`);
|
|
70
|
+
if (!/^[a-f0-9]{64}$/.test(row.evidence_digest) || !/^[a-f0-9]{64}$/.test(row.task_fact_digest))
|
|
71
|
+
throw new Error(`invalid semantic digest for '${row.intent_id}'`);
|
|
64
72
|
if (!row.source_artifact || !row.declared_at)
|
|
65
73
|
throw new Error(`rework intent '${row.intent_id}' has invalid required fields`);
|
|
66
74
|
const terminal = row.status === 'consumed' || row.status === 'superseded';
|
|
@@ -69,6 +77,8 @@ function parseReworkIntentDocument(content) {
|
|
|
69
77
|
return {
|
|
70
78
|
intentId: row.intent_id, findingId: row.finding_id, sourceArtifact: row.source_artifact,
|
|
71
79
|
sourceSha256: row.source_sha256, target: row.target,
|
|
80
|
+
classification: row.classification,
|
|
81
|
+
evidenceDigest: row.evidence_digest, taskFactDigest: row.task_fact_digest,
|
|
72
82
|
status: row.status, declaredAt: row.declared_at, consumedAt: row.consumed_at
|
|
73
83
|
};
|
|
74
84
|
});
|
|
@@ -91,7 +101,10 @@ function renderReworkIntents(intents) {
|
|
|
91
101
|
`| ${REWORK_INTENT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
92
102
|
...intents.map((intent) => `| ${[
|
|
93
103
|
intent.intentId, intent.findingId, intent.sourceArtifact, intent.sourceSha256,
|
|
94
|
-
intent.target, intent.
|
|
104
|
+
intent.target, intent.classification ?? (intent.target === 'analysis' ? 'scope-or-requirement' : intent.target === 'plan' ? 'design' : intent.target === 'code' ? 'implementation' : 'insufficient-evidence'),
|
|
105
|
+
intent.evidenceDigest ?? requireDigest(`${intent.findingId}|${intent.sourceArtifact}|${intent.sourceSha256}`),
|
|
106
|
+
intent.taskFactDigest ?? requireDigest(`current|${intent.findingId}|${intent.target}`),
|
|
107
|
+
intent.status, intent.declaredAt, intent.consumedAt
|
|
95
108
|
].map(escape).join(' | ')} |`)
|
|
96
109
|
].join('\n');
|
|
97
110
|
}
|
|
@@ -99,11 +112,63 @@ function upsertReworkIntent(intents, intent) {
|
|
|
99
112
|
const existing = intents.find((candidate) => candidate.intentId === intent.intentId);
|
|
100
113
|
if (!existing)
|
|
101
114
|
return { changed: true, intents: [...intents, intent] };
|
|
102
|
-
const identity = (value) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target];
|
|
115
|
+
const identity = (value) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target, value.classification, value.evidenceDigest, value.taskFactDigest];
|
|
103
116
|
if (JSON.stringify(identity(existing)) !== JSON.stringify(identity(intent)))
|
|
104
117
|
throw new Error(`intent '${intent.intentId}' identity conflicts with existing state`);
|
|
105
118
|
return { changed: false, intents };
|
|
106
119
|
}
|
|
120
|
+
function consumeHumanDecisionReworkIntents(intents, findingId, consumedAt) {
|
|
121
|
+
let changed = false;
|
|
122
|
+
const next = intents.map((intent) => {
|
|
123
|
+
if (intent.status !== 'pending' || intent.target !== 'pause' || intent.classification !== 'human-decision' || intent.findingId !== findingId)
|
|
124
|
+
return intent;
|
|
125
|
+
changed = true;
|
|
126
|
+
return { ...intent, status: 'consumed', consumedAt };
|
|
127
|
+
});
|
|
128
|
+
return { changed, intents: next };
|
|
129
|
+
}
|
|
130
|
+
function parseLegacyReworkIntentDocument(content) {
|
|
131
|
+
// TODO(compat): Remove this explicit decoder once no task contains the eight-column rework-intent header.
|
|
132
|
+
const body = sectionBody(content);
|
|
133
|
+
if (body === null)
|
|
134
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'legacy rework intent section is missing' };
|
|
135
|
+
const lines = body.split(/\r?\n/).filter((line) => line.trim());
|
|
136
|
+
const header = lines[0] ? cells(lines[0]) : null;
|
|
137
|
+
const separator = lines[1] ? cells(lines[1]) : null;
|
|
138
|
+
if (!header || !separator || header.length !== LEGACY_REWORK_INTENT_COLUMNS.length || !header.every((value, index) => value === LEGACY_REWORK_INTENT_COLUMNS[index])) {
|
|
139
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'rework intent section is not the known legacy table' };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const intents = lines.slice(2).map((line) => {
|
|
143
|
+
const values = cells(line);
|
|
144
|
+
if (!values || values.length !== LEGACY_REWORK_INTENT_COLUMNS.length)
|
|
145
|
+
throw new Error('legacy rework intent row has the wrong column count');
|
|
146
|
+
const row = Object.fromEntries(LEGACY_REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]]));
|
|
147
|
+
if (!/^RI-[1-9]\d*$/.test(row.intent_id) || !/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id))
|
|
148
|
+
throw new Error('legacy rework intent identity is invalid');
|
|
149
|
+
if (!['analysis', 'plan', 'code'].includes(row.target) || !['pending', 'consumed', 'superseded'].includes(row.status))
|
|
150
|
+
throw new Error('legacy rework intent state is invalid');
|
|
151
|
+
if (!/^[a-f0-9]{64}$/.test(row.source_sha256))
|
|
152
|
+
throw new Error('legacy rework intent source hash is invalid');
|
|
153
|
+
const legacyDigest = (value) => requireDigest(value);
|
|
154
|
+
const classification = row.target === 'analysis' ? 'scope-or-requirement' : row.target === 'plan' ? 'design' : 'implementation';
|
|
155
|
+
return {
|
|
156
|
+
intentId: row.intent_id, findingId: row.finding_id, sourceArtifact: row.source_artifact, sourceSha256: row.source_sha256,
|
|
157
|
+
target: row.target, classification,
|
|
158
|
+
evidenceDigest: legacyDigest(`${row.finding_id}|${row.source_artifact}|${row.source_sha256}`),
|
|
159
|
+
taskFactDigest: legacyDigest(`legacy|${row.finding_id}|${row.target}`),
|
|
160
|
+
status: row.status, declaredAt: row.declared_at, consumedAt: row.consumed_at
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
return { ok: true, present: true, intents };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function requireDigest(value) {
|
|
170
|
+
return createHash('sha256').update(value).digest('hex');
|
|
171
|
+
}
|
|
107
172
|
function consumeReworkIntents(intents, target, sourceHashes, consumedAt) {
|
|
108
173
|
let changed = false;
|
|
109
174
|
const next = intents.map((intent) => {
|
|
@@ -134,5 +199,6 @@ function reworkIntentMutation(content, intents) {
|
|
|
134
199
|
body: renderReworkIntents(intents)
|
|
135
200
|
};
|
|
136
201
|
}
|
|
137
|
-
export { REWORK_INTENT_COLUMNS, REWORK_INTENT_HEADINGS, parseReworkIntentDocument, renderReworkIntents, reworkIntentMutation, consumeReworkIntents, supersedeReworkIntents, upsertReworkIntent };
|
|
202
|
+
export { REWORK_INTENT_COLUMNS, REWORK_INTENT_HEADINGS, parseReworkIntentDocument, renderReworkIntents, reworkIntentMutation, consumeReworkIntents, supersedeReworkIntents, upsertReworkIntent, consumeHumanDecisionReworkIntents, parseLegacyReworkIntentDocument };
|
|
203
|
+
import { createHash } from 'node:crypto';
|
|
138
204
|
//# sourceMappingURL=rework-intent.js.map
|