@fitlab-ai/agent-infra 0.11.9 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -6
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-finalization.js +14 -23
- package/dist/lib/internal/task-operation-registry.js +2 -3
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +4 -2
- package/dist/lib/sandbox/control/client.js +54 -6
- package/dist/lib/sandbox/control/executor.js +8 -3
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/task/artifact-command.js +11 -1
- package/dist/lib/task/artifact-lifecycle.js +119 -62
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +55 -21
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -4
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +8 -274
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +10 -36
- package/dist/lib/task/frontmatter.js +1 -1
- package/dist/lib/task/ledger-intents.js +4 -7
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +17 -10
- package/dist/lib/task/platform-operation-recovery.js +95 -28
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-finalization.ts +16 -24
- package/lib/internal/task-operation-registry.ts +2 -3
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +4 -2
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/sandbox/control/client.ts +57 -6
- package/lib/sandbox/control/executor.ts +8 -3
- package/lib/sandbox/removal.ts +2 -10
- package/lib/task/artifact-command.ts +10 -1
- package/lib/task/artifact-lifecycle.ts +100 -52
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +47 -24
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -4
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +8 -247
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +10 -35
- package/lib/task/frontmatter.ts +1 -1
- package/lib/task/ledger-intents.ts +4 -7
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +18 -11
- package/lib/task/platform-operation-recovery.ts +90 -31
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- 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 +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -6,19 +6,19 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
6
6
|
import type { ResolveTaskRefErrorCode, TaskWorkspaceState } from './resolve-ref.ts';
|
|
7
7
|
import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
8
8
|
import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
|
|
9
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
9
10
|
import { parseVerdict } from './review-artifacts.ts';
|
|
10
11
|
import { extractSection, findSectionHeading } from './sections.ts';
|
|
11
|
-
import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
12
|
-
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
|
|
12
|
+
import { receiptForOutput, receiptsForOutput, sha256File } from './artifact-receipts.ts';
|
|
13
|
+
import { artifactCompletionOrder, hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.ts';
|
|
13
14
|
import { inspectReviewIdentity } from './review-identity.ts';
|
|
14
|
-
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
15
|
-
import type { InvalidationDocument } from './invalidation.ts';
|
|
16
15
|
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
|
|
17
16
|
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
18
17
|
import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
19
18
|
import { parseReworkIntentDocument } from './rework-intent.ts';
|
|
20
19
|
import { selectArtifactDisposition } from './artifact-selection.ts';
|
|
21
20
|
import type { ArtifactSelection } from './artifact-selection.ts';
|
|
21
|
+
import { executionInputsMatchLatest } from './execution-inputs.ts';
|
|
22
22
|
|
|
23
23
|
const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
|
|
24
24
|
const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
|
|
@@ -29,24 +29,37 @@ const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
|
|
|
29
29
|
|
|
30
30
|
/** Init, repair and finalization share the same authoritative round identity. */
|
|
31
31
|
export function hasOpenArtifactRound(content: string, family: string, round: number): boolean {
|
|
32
|
+
return openArtifactRounds(content, family).includes(round);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openArtifactRounds(content: string, family: string): number[] {
|
|
32
36
|
const activity = locateActivityLog(content);
|
|
33
37
|
const step = ARTIFACT_STEPS[family];
|
|
34
|
-
if (!activity || !step) return
|
|
38
|
+
if (!activity || !step) return [];
|
|
35
39
|
const qualifier = family === 'code'
|
|
36
40
|
? '(?:, (?:fix for review-code(?:-r(?:[2-9]|[1-9]\\d+))?\\.md|decision II-[1-9]\\d*))?'
|
|
37
41
|
: '';
|
|
38
|
-
const expected = new RegExp(`^${step} \\(Round ${
|
|
39
|
-
return startedBackedRows(pairEntries(activity.entries))
|
|
42
|
+
const expected = new RegExp(`^${step} \\(Round ([1-9]\\d*)${qualifier}\\)$`);
|
|
43
|
+
return [...new Set(startedBackedRows(pairEntries(activity.entries))
|
|
44
|
+
.filter((row) => expected.test(row.step) && !row.done)
|
|
45
|
+
.map((row) => Number(expected.exec(row.step)?.[1])))].sort((left, right) => left - right);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function hasCompletedArtifactRound(content: string, family: string, round: number, name: string): boolean {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const event = completionEvent(family);
|
|
50
|
+
return Boolean(event && parseArtifactName(name)?.round === round
|
|
51
|
+
&& artifactCompletionOrder(content, name, event) !== null
|
|
52
|
+
&& hasArtifactCompletionFact(content, name, event));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function completionEvent(family: string): string | null {
|
|
56
|
+
if (family === 'analysis') return 'analyze.completed';
|
|
57
|
+
if (family === 'review-analysis') return 'review-analysis.completed';
|
|
58
|
+
if (family === 'plan') return 'plan.completed';
|
|
59
|
+
if (family === 'review-plan') return 'review-plan.completed';
|
|
60
|
+
if (family === 'code') return 'code.completed';
|
|
61
|
+
if (family === 'review-code') return 'review-code.completed';
|
|
62
|
+
return null;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
type ArtifactIdentity = {
|
|
@@ -60,7 +73,7 @@ type ArtifactIdentity = {
|
|
|
60
73
|
type ArtifactDiagnosticCode =
|
|
61
74
|
| 'NONCANONICAL_NAME' | 'ROUND_OUT_OF_RANGE' | 'MISSING_BASE' | 'ROUND_GAP'
|
|
62
75
|
| 'DUPLICATE_LOGICAL_ROUND' | 'NON_REGULAR_FILE' | 'SYMBOLIC_LINK'
|
|
63
|
-
| 'FILESYSTEM_RACE' | 'CATALOG_CONFLICT' | 'BROKEN_REFERENCE';
|
|
76
|
+
| 'FILESYSTEM_RACE' | 'CATALOG_CONFLICT' | 'BROKEN_REFERENCE' | 'MULTIPLE_OPEN_ROUNDS';
|
|
64
77
|
type ArtifactDiagnostic = {
|
|
65
78
|
code: ArtifactDiagnosticCode;
|
|
66
79
|
family: ArtifactFamily;
|
|
@@ -74,7 +87,7 @@ type ArtifactErrorCode =
|
|
|
74
87
|
| 'LIFECYCLE_PATH_INVALID'
|
|
75
88
|
| 'ARTIFACT_IDENTITY_INVALID' | 'ARTIFACT_NOT_FOUND'
|
|
76
89
|
| 'ARTIFACT_NOT_REGULAR' | 'ARTIFACT_NOT_READABLE' | 'ARTIFACT_VERDICT_INVALID'
|
|
77
|
-
| 'ARTIFACT_MODE_REFUSED'
|
|
90
|
+
| 'ARTIFACT_MODE_REFUSED'
|
|
78
91
|
| 'ARTIFACT_SELECTION_INDETERMINATE';
|
|
79
92
|
|
|
80
93
|
type ArtifactError = { code: ArtifactErrorCode; message: string };
|
|
@@ -87,8 +100,10 @@ type ArtifactInventoryResult = {
|
|
|
87
100
|
taskState: TaskWorkspaceState | null;
|
|
88
101
|
family: ArtifactFamily | string;
|
|
89
102
|
artifacts: readonly ArtifactIdentity[];
|
|
103
|
+
completed: readonly ArtifactIdentity[];
|
|
90
104
|
latest: ArtifactIdentity | null;
|
|
91
105
|
next: { round: number; name: string } | null;
|
|
106
|
+
openRounds: readonly number[];
|
|
92
107
|
reviewedInput: ArtifactIdentity | null;
|
|
93
108
|
diagnostics: readonly ArtifactDiagnostic[];
|
|
94
109
|
error: ArtifactError | null;
|
|
@@ -122,7 +137,7 @@ type InspectOptions = {
|
|
|
122
137
|
const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
|
|
123
138
|
'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
|
|
124
139
|
'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
|
|
125
|
-
'FILESYSTEM_RACE', 'CATALOG_CONFLICT'
|
|
140
|
+
'FILESYSTEM_RACE', 'CATALOG_CONFLICT', 'MULTIPLE_OPEN_ROUNDS'
|
|
126
141
|
]);
|
|
127
142
|
|
|
128
143
|
function familySpec(family: string): ArtifactFamilySpec | null {
|
|
@@ -136,7 +151,7 @@ function escapeRegExp(value: string): string {
|
|
|
136
151
|
function failure(requestRef: string, family: string, error: ArtifactError, extra: Partial<ArtifactInventoryResult> = {}): ArtifactInventoryResult {
|
|
137
152
|
return {
|
|
138
153
|
status: 'failed', changed: false, requestRef, taskId: null, taskDir: null,
|
|
139
|
-
taskState: null, family, artifacts: [], latest: null, next: null,
|
|
154
|
+
taskState: null, family, artifacts: [], completed: [], latest: null, next: null, openRounds: [],
|
|
140
155
|
reviewedInput: null, diagnostics: [], error, ...extra
|
|
141
156
|
};
|
|
142
157
|
}
|
|
@@ -168,18 +183,11 @@ function inspectArtifactDirectory(
|
|
|
168
183
|
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
169
184
|
});
|
|
170
185
|
}
|
|
171
|
-
let
|
|
186
|
+
let taskContent: string;
|
|
172
187
|
try {
|
|
173
|
-
|
|
174
|
-
const parsed = parseInvalidationDocument(taskContent);
|
|
175
|
-
if (!parsed.ok) {
|
|
176
|
-
return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: parsed.message }, {
|
|
177
|
-
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
invalidation = parsed.document;
|
|
188
|
+
taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
181
189
|
} catch (error) {
|
|
182
|
-
return failure(requestRef, family, { code: '
|
|
190
|
+
return failure(requestRef, family, { code: 'ARTIFACT_DIRECTORY_READ_FAILED', message: String(error) }, {
|
|
183
191
|
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
184
192
|
});
|
|
185
193
|
}
|
|
@@ -216,23 +224,34 @@ function inspectArtifactDirectory(
|
|
|
216
224
|
}
|
|
217
225
|
const identity = { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
218
226
|
knownArtifacts.push(identity);
|
|
219
|
-
|
|
227
|
+
artifacts.push(identity);
|
|
220
228
|
} catch (error) {
|
|
221
229
|
diagnostics.push(diagnostic('FILESYSTEM_RACE', family, entry.name, String(error)));
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
232
|
artifacts.sort((left, right) => left.round - right.round);
|
|
225
233
|
knownArtifacts.sort((left, right) => left.round - right.round);
|
|
234
|
+
const openRounds = openArtifactRounds(taskContent, family);
|
|
226
235
|
const knownRounds = new Set(knownArtifacts.map((item) => item.round));
|
|
227
|
-
|
|
228
|
-
const max =
|
|
236
|
+
for (const round of openRounds) knownRounds.add(round);
|
|
237
|
+
const max = Math.max(...knownRounds, 0);
|
|
238
|
+
if (max > 0 && !knownRounds.has(1)) diagnostics.push(diagnostic('MISSING_BASE', family, null, `${family}.md is missing`));
|
|
229
239
|
for (let round = 1; round <= max; round += 1) {
|
|
230
240
|
if (!knownRounds.has(round)) diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
|
|
231
241
|
}
|
|
232
|
-
const
|
|
242
|
+
const event = completionEvent(family);
|
|
243
|
+
const completed = event
|
|
244
|
+
? artifacts.flatMap((artifact) => {
|
|
245
|
+
const order = artifactCompletionOrder(taskContent, artifact.name, event);
|
|
246
|
+
return order !== null && hasArtifactCompletionFact(taskContent, artifact.path, event)
|
|
247
|
+
? [{ artifact, order }] : [];
|
|
248
|
+
}).sort((left, right) => left.order - right.order).map(({ artifact }) => artifact)
|
|
249
|
+
: [];
|
|
250
|
+
if (openRounds.length > 1) diagnostics.push(diagnostic('MULTIPLE_OPEN_ROUNDS', family, null, `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`));
|
|
251
|
+
const latest = completed.at(-1) ?? null;
|
|
233
252
|
let hasReviewCodeReceipt = false;
|
|
234
253
|
if (family === 'review-code' && latest) {
|
|
235
|
-
try { hasReviewCodeReceipt = Boolean(receiptForOutput(
|
|
254
|
+
try { hasReviewCodeReceipt = Boolean(receiptForOutput(taskContent, latest.name)); }
|
|
236
255
|
catch { hasReviewCodeReceipt = false; }
|
|
237
256
|
}
|
|
238
257
|
const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
|
|
@@ -240,9 +259,9 @@ function inspectArtifactDirectory(
|
|
|
240
259
|
: null;
|
|
241
260
|
return {
|
|
242
261
|
status: 'ready', changed: false, requestRef, taskId: identity.taskId ?? null,
|
|
243
|
-
taskDir, taskState: identity.taskState ?? null, family, artifacts, latest,
|
|
262
|
+
taskDir, taskState: identity.taskState ?? null, family, artifacts, completed, latest,
|
|
244
263
|
next: { round: max + 1, name: artifactName(family, max + 1) },
|
|
245
|
-
reviewedInput, diagnostics, error: null
|
|
264
|
+
openRounds, reviewedInput, diagnostics, error: null
|
|
246
265
|
};
|
|
247
266
|
}
|
|
248
267
|
|
|
@@ -262,9 +281,6 @@ function resolveReviewedInput(
|
|
|
262
281
|
const abs = path.join(taskDir, input.name);
|
|
263
282
|
const stat = fs.lstatSync(abs);
|
|
264
283
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
|
|
265
|
-
const invalidation = parseInvalidationDocument(taskContent);
|
|
266
|
-
if (!invalidation.ok) throw new Error(invalidation.message);
|
|
267
|
-
if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name)) throw new Error(`${input.name} is invalidated`);
|
|
268
284
|
const actualSha256 = sha256File(abs);
|
|
269
285
|
if (actualSha256 !== receipt.inputSha256) {
|
|
270
286
|
throw new Error(`${input.name} content digest does not match receipt`);
|
|
@@ -338,10 +354,22 @@ function hasSelectionEvidence(
|
|
|
338
354
|
content: string,
|
|
339
355
|
family: ArtifactFamily,
|
|
340
356
|
options: InspectOptions,
|
|
341
|
-
context: ArtifactContextResult
|
|
357
|
+
context: ArtifactContextResult,
|
|
358
|
+
pathState: LifecyclePathState
|
|
342
359
|
): boolean {
|
|
343
360
|
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256) return true;
|
|
344
361
|
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt') return true;
|
|
362
|
+
if ((family === 'plan' || family === 'code') && context.latest) {
|
|
363
|
+
const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
|
|
364
|
+
const capturedInputs = receiptsForOutput(content, context.latest.name)
|
|
365
|
+
.filter((receipt) => receipt.event === `${family}.completed`)
|
|
366
|
+
.filter((receipt) => {
|
|
367
|
+
try { return sha256File(path.join(context.taskDir!, receipt.input)) === receipt.inputSha256; }
|
|
368
|
+
catch { return false; }
|
|
369
|
+
})
|
|
370
|
+
.map((receipt) => receipt.input);
|
|
371
|
+
if (!executionInputsMatchLatest(family, pathState, latestInputs, capturedInputs)) return true;
|
|
372
|
+
}
|
|
345
373
|
if (family.startsWith('review-')) {
|
|
346
374
|
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
347
375
|
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
@@ -398,16 +426,19 @@ function attachArtifactSelection(
|
|
|
398
426
|
} catch (error) {
|
|
399
427
|
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
400
428
|
}
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
const selectedIdentity =
|
|
429
|
+
const openRounds = openArtifactRounds(taskContent, family);
|
|
430
|
+
if (openRounds.length > 1) return fail('ARTIFACT_TOPOLOGY_CONFLICT', `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`);
|
|
431
|
+
const openRound = openRounds[0] ?? null;
|
|
432
|
+
const selectedIdentity = openRound === null ? context.next : {
|
|
433
|
+
family, round: openRound, name: artifactName(family, openRound),
|
|
434
|
+
path: path.join(context.taskDir, artifactName(family, openRound)), size: 0, mtimeMs: 0
|
|
435
|
+
};
|
|
405
436
|
try {
|
|
406
|
-
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
|
|
437
|
+
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context, pathState);
|
|
407
438
|
const selection = selectArtifactDisposition({
|
|
408
439
|
next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
|
|
409
440
|
latest: context.latest,
|
|
410
|
-
open,
|
|
441
|
+
open: openRound !== null,
|
|
411
442
|
hasChangeEvidence
|
|
412
443
|
});
|
|
413
444
|
return {
|
|
@@ -434,7 +465,8 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
|
|
|
434
465
|
const selected = attachArtifactSelection(code, pathState, options);
|
|
435
466
|
if (selected.status !== 'ready' || !selected.selection) return selected;
|
|
436
467
|
if (selected.selection.disposition === 'reuse') return selected;
|
|
437
|
-
if (selected.codeMode?.mode === 'error'
|
|
468
|
+
if (selected.codeMode?.mode === 'error'
|
|
469
|
+
&& selected.codeMode.message.includes('is not captured by the latest code input receipt')) {
|
|
438
470
|
return {
|
|
439
471
|
...selected,
|
|
440
472
|
status: 'ready',
|
|
@@ -490,23 +522,33 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
|
|
|
490
522
|
const source = inspectTaskArtifacts(taskRef, inputFamily, options);
|
|
491
523
|
if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
|
|
492
524
|
const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
|
|
525
|
+
try {
|
|
526
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
527
|
+
const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
|
|
528
|
+
if (hasPendingImplementation && !reviewCode.latest) {
|
|
529
|
+
const candidate = reviewCode.artifacts.at(-1)?.name ?? 'review-code.md';
|
|
530
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `cannot find completed Activity Log identity for ${candidate}`);
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
534
|
+
}
|
|
493
535
|
const latestCode = inventory.latest;
|
|
494
|
-
const codeMax = latestCode?.round ?? 0;
|
|
536
|
+
const codeMax = inventory.next?.round ? inventory.next.round - 1 : latestCode?.round ?? 0;
|
|
495
537
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
496
538
|
const inputs = [source.latest];
|
|
497
539
|
const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
|
|
498
540
|
const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
|
|
499
541
|
if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name) inputs.push(sourceReview.latest);
|
|
500
|
-
if (
|
|
542
|
+
if (inventory.openRounds.length > 0) {
|
|
501
543
|
try {
|
|
502
544
|
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
503
|
-
|
|
545
|
+
const openRound = inventory.openRounds[0]!;
|
|
504
546
|
const review = reviewCode.latest;
|
|
505
547
|
const openInputs = review ? [...inputs, review] : inputs;
|
|
506
548
|
const openVerdict = review ? parseVerdict(review.path) : null;
|
|
507
549
|
const openRow = locateActivityLog(taskContent)
|
|
508
550
|
? startedBackedRows(pairEntries(locateActivityLog(taskContent)!.entries))
|
|
509
|
-
.find((row) => row.step.startsWith(`Code Task (Round ${
|
|
551
|
+
.find((row) => row.step.startsWith(`Code Task (Round ${openRound}`) && !row.done)
|
|
510
552
|
: null;
|
|
511
553
|
const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
512
554
|
const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
@@ -516,10 +558,9 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
|
|
|
516
558
|
return withCodeMode(
|
|
517
559
|
inventory, openInputs, 'ready', mode, codeMax, reviewMax,
|
|
518
560
|
openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null,
|
|
519
|
-
`Resuming open code round ${
|
|
561
|
+
`Resuming open code round ${openRound}.`,
|
|
520
562
|
decisionInput
|
|
521
563
|
);
|
|
522
|
-
}
|
|
523
564
|
} catch (error) {
|
|
524
565
|
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
525
566
|
}
|
|
@@ -653,6 +694,13 @@ export function validateArtifactPublication(taskDir: string, family: ArtifactFam
|
|
|
653
694
|
if (hasCompletedArtifactRound(task, family, existing.round, name)) {
|
|
654
695
|
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
|
|
655
696
|
}
|
|
697
|
+
const event = completionEvent(family);
|
|
698
|
+
const frontmatter = parseTypedTaskFrontmatter(task);
|
|
699
|
+
const completionFactExists = event !== null && parseCompletionFacts(frontmatter.completion_facts)
|
|
700
|
+
.some((fact) => fact.event === event && fact.output === name);
|
|
701
|
+
if (event && completionFactExists && artifactCompletionOrder(task, name, event) !== null) {
|
|
702
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completion evidence for '${name}' does not match its current content` };
|
|
703
|
+
}
|
|
656
704
|
} catch (error) {
|
|
657
705
|
return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
|
|
658
706
|
}
|
|
@@ -32,7 +32,7 @@ function selectArtifactDisposition(request: ArtifactSelectionRequest): ArtifactS
|
|
|
32
32
|
if (!request.latest) {
|
|
33
33
|
return {
|
|
34
34
|
disposition: 'create',
|
|
35
|
-
reasonCode:
|
|
35
|
+
reasonCode: 'no-completed-history',
|
|
36
36
|
artifact: request.next, writeRequired: true
|
|
37
37
|
};
|
|
38
38
|
}
|
package/lib/task/capabilities.ts
CHANGED
|
@@ -2,8 +2,6 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { parseArtifactName } from './artifact-name.ts';
|
|
5
|
-
import type { InvalidationDocument } from './invalidation.ts';
|
|
6
|
-
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
7
5
|
import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.ts';
|
|
8
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
9
7
|
import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
@@ -19,6 +17,8 @@ import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
|
19
17
|
import { parseImplementationInputs } from './implementation-inputs.ts';
|
|
20
18
|
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
|
|
21
19
|
import { inspectReviewIdentity } from './review-identity.ts';
|
|
20
|
+
import { inspectArtifactDirectory } from './artifact-lifecycle.ts';
|
|
21
|
+
import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.ts';
|
|
22
22
|
|
|
23
23
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
24
24
|
|
|
@@ -43,11 +43,10 @@ type LifecycleFacts = {
|
|
|
43
43
|
taskState: string;
|
|
44
44
|
currentStep: string;
|
|
45
45
|
artifacts: Partial<Record<LifecycleAction, readonly string[]>>;
|
|
46
|
-
staleArtifacts?: Partial<Record<LifecycleAction, readonly string[]>>;
|
|
47
46
|
reviewedInputs?: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', string>>;
|
|
47
|
+
executionInputs?: Partial<Record<'plan' | 'code', readonly string[]>>;
|
|
48
48
|
artifactHashes: Readonly<Record<string, string>>;
|
|
49
49
|
reviews: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', 'approved' | 'changes-requested' | 'rejected'>>;
|
|
50
|
-
invalidation: InvalidationDocument;
|
|
51
50
|
reworkIntents?: readonly ReworkIntent[];
|
|
52
51
|
unresolvedLedger: Record<'analysis' | 'plan' | 'code', number>;
|
|
53
52
|
executionBusy: boolean;
|
|
@@ -106,7 +105,6 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
106
105
|
if (actual !== trigger.sourceSha256) return deny('SOURCE_ARTIFACT_HASH_MISMATCH', trigger.sourceArtifact);
|
|
107
106
|
}
|
|
108
107
|
if (facts.taskState !== 'active') return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
|
|
109
|
-
if (invalidationBlocks(facts.invalidation)) return deny('INVALIDATION_INCOMPLETE');
|
|
110
108
|
if (facts.executionBusy) return deny('EXECUTION_BUSY');
|
|
111
109
|
|
|
112
110
|
const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
|
|
@@ -152,7 +150,8 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
152
150
|
return allow('analysis-review-approved');
|
|
153
151
|
}
|
|
154
152
|
if (action === 'review-plan') {
|
|
155
|
-
|
|
153
|
+
if (!hasArtifact(facts, 'plan')) return deny('PLAN_ARTIFACT_REQUIRED');
|
|
154
|
+
return executionInputsMatchLatest(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_INPUT_STALE');
|
|
156
155
|
}
|
|
157
156
|
if (action === 'code') {
|
|
158
157
|
if (trigger.implementationInput) {
|
|
@@ -179,10 +178,12 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
179
178
|
return allow('plan-review-approved');
|
|
180
179
|
}
|
|
181
180
|
if (action === 'review-code') {
|
|
181
|
+
if (hasArtifact(facts, 'code') && !executionInputsMatchLatest(facts, 'code')) return deny('CODE_INPUT_STALE');
|
|
182
182
|
return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
|
|
183
183
|
}
|
|
184
184
|
if (action === 'manual-validation' || action === 'validation-run') {
|
|
185
185
|
if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
|
|
186
|
+
if (!executionInputsMatchLatest(facts, 'code')) return deny('CODE_INPUT_STALE');
|
|
186
187
|
if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
|
|
187
188
|
if (!facts.reviewCodeIdentityValid) return deny('CODE_REVIEW_IDENTITY_INVALID');
|
|
188
189
|
if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
|
|
@@ -193,8 +194,7 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
function latestArtifact(names: readonly string[]): string | null {
|
|
196
|
-
return names.
|
|
197
|
-
.sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
|
|
197
|
+
return names.at(-1) ?? null;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan' | 'code', review: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
|
|
@@ -202,6 +202,16 @@ function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan'
|
|
|
202
202
|
return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function executionInputsMatchLatest(facts: LifecycleFacts, stage: 'plan' | 'code'): boolean {
|
|
206
|
+
if (!facts.pathState || facts.pathState.status !== 'valid' || facts.executionInputs === undefined) return true;
|
|
207
|
+
return executionReceiptsMatchLatest(stage, facts.pathState, {
|
|
208
|
+
analysis: latestArtifact(facts.artifacts.analysis ?? []) ?? undefined,
|
|
209
|
+
'review-analysis': latestArtifact(facts.artifacts['review-analysis'] ?? []) ?? undefined,
|
|
210
|
+
plan: latestArtifact(facts.artifacts.plan ?? []) ?? undefined,
|
|
211
|
+
'review-plan': latestArtifact(facts.artifacts['review-plan'] ?? []) ?? undefined
|
|
212
|
+
}, facts.executionInputs[stage] ?? []);
|
|
213
|
+
}
|
|
214
|
+
|
|
205
215
|
function qualificationCandidateSnapshotMatches(
|
|
206
216
|
audit: QualificationAudit,
|
|
207
217
|
qualification: TaskQualification
|
|
@@ -239,6 +249,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
|
239
249
|
: { action: stage, reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
|
|
240
250
|
}
|
|
241
251
|
if (stage === 'plan' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
|
|
252
|
+
if (stage === 'plan' && !executionInputsMatchLatest(facts, stage)) return { action: stage, reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
|
|
242
253
|
if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
|
|
243
254
|
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
|
|
244
255
|
return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
|
|
@@ -248,6 +259,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
|
248
259
|
: { action: stage, reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
|
|
249
260
|
}
|
|
250
261
|
if (stage === 'code' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
|
|
262
|
+
if (stage === 'code' && !executionInputsMatchLatest(facts, stage)) return { action: stage, reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
|
|
251
263
|
if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
|
|
252
264
|
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
|
|
253
265
|
return facts.resolvedHumanDecisions.code === 'implementation'
|
|
@@ -269,6 +281,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
|
269
281
|
return { action: 'analysis', reasonCode: 'ANALYSIS_REWORK_REQUIRED', evidence: ['analysis review or ledger is not clear'] };
|
|
270
282
|
}
|
|
271
283
|
if (!hasArtifact(facts, 'plan')) return { action: 'plan', reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
|
|
284
|
+
if (!executionInputsMatchLatest(facts, 'plan')) return { action: 'plan', reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
|
|
272
285
|
if (!hasArtifact(facts, 'review-plan') || !reviewMatchesLatest(facts, 'plan', 'review-plan')) {
|
|
273
286
|
return { action: 'review-plan', reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
|
|
274
287
|
}
|
|
@@ -276,6 +289,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
|
276
289
|
return { action: 'plan', reasonCode: 'PLAN_REWORK_REQUIRED', evidence: ['plan review or ledger is not clear'] };
|
|
277
290
|
}
|
|
278
291
|
if (!hasArtifact(facts, 'code')) return { action: 'code', reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
|
|
292
|
+
if (!executionInputsMatchLatest(facts, 'code')) return { action: 'code', reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
|
|
279
293
|
if (!hasArtifact(facts, 'review-code') || !reviewMatchesLatest(facts, 'code', 'review-code')) {
|
|
280
294
|
return { action: 'review-code', reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
|
|
281
295
|
}
|
|
@@ -288,8 +302,6 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
|
288
302
|
function buildLifecycleFacts(taskDir: string, content: string, taskState = 'active', executionBusy = false): LifecycleFactsResult {
|
|
289
303
|
try {
|
|
290
304
|
const metadata = parseTypedTaskFrontmatter(content);
|
|
291
|
-
const invalidation = parseInvalidationDocument(content);
|
|
292
|
-
if (!invalidation.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalidation.message };
|
|
293
305
|
const rework = parseReworkIntentDocument(content);
|
|
294
306
|
if (!rework.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: rework.message };
|
|
295
307
|
const ledger = parseLedgerDocument(content);
|
|
@@ -307,24 +319,36 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
307
319
|
const family = parseArtifactName(name)?.family;
|
|
308
320
|
return artifactFamilies.find((candidate) => candidate === family) ?? null;
|
|
309
321
|
};
|
|
310
|
-
const activeFiles = files.filter((name) => {
|
|
311
|
-
const family = familyFor(name);
|
|
312
|
-
return !family || !isArtifactInvalidated(invalidation.document, family, name);
|
|
313
|
-
});
|
|
314
322
|
const allArtifactHashes: Record<string, string> = {};
|
|
315
323
|
for (const name of files) allArtifactHashes[name] = sha256File(path.join(taskDir, name));
|
|
316
324
|
const artifactHashes: Record<string, string> = {};
|
|
317
|
-
for (const name of
|
|
325
|
+
for (const name of files) artifactHashes[name] = allArtifactHashes[name]!;
|
|
326
|
+
const completedByFamily = new Map<string, string[]>();
|
|
327
|
+
for (const family of artifactFamilies) {
|
|
328
|
+
const inventory = inspectArtifactDirectory(taskDir, family);
|
|
329
|
+
if (inventory.status !== 'ready') continue;
|
|
330
|
+
completedByFamily.set(family, inventory.completed.map((artifact) => artifact.name));
|
|
331
|
+
}
|
|
332
|
+
const completedFiles = new Set([...completedByFamily.values()].flat());
|
|
333
|
+
const activeFiles = [
|
|
334
|
+
...files.filter((name) => !familyFor(name)),
|
|
335
|
+
...artifactFamilies.flatMap((family) => completedByFamily.get(family) ?? [])
|
|
336
|
+
];
|
|
318
337
|
const artifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
|
|
319
338
|
artifactFamilies.map((family) => [
|
|
320
339
|
family, activeFiles.filter((name) => familyFor(name) === family)
|
|
321
340
|
])
|
|
322
341
|
) as Partial<Record<LifecycleAction, readonly string[]>>;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
])
|
|
327
|
-
|
|
342
|
+
const receipts = parseArtifactReceipts(content).rows;
|
|
343
|
+
const executionInputs: NonNullable<LifecycleFacts['executionInputs']> = {};
|
|
344
|
+
for (const family of ['plan', 'code'] as const) {
|
|
345
|
+
const output = latestArtifact(artifacts[family] ?? []);
|
|
346
|
+
if (!output) continue;
|
|
347
|
+
executionInputs[family] = receipts.filter((receipt) => receipt.output === output
|
|
348
|
+
&& receipt.event === `${family}.completed`
|
|
349
|
+
&& completedFiles.has(receipt.input)
|
|
350
|
+
&& artifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => receipt.input);
|
|
351
|
+
}
|
|
328
352
|
const qualification = parseTaskQualification(content);
|
|
329
353
|
if (!qualification.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: qualification.message };
|
|
330
354
|
const qualificationStaleArtifacts: string[] = [];
|
|
@@ -377,9 +401,8 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
377
401
|
if (verdict.ok) reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
|
|
378
402
|
}
|
|
379
403
|
const reworkClassificationRequired: Array<'analysis' | 'plan' | 'code'> = [];
|
|
380
|
-
const receipts = parseArtifactReceipts(content).rows;
|
|
381
404
|
for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']] as const) {
|
|
382
|
-
const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` &&
|
|
405
|
+
const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && completedFiles.has(receipt.output)
|
|
383
406
|
&& allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
|
|
384
407
|
const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
|
|
385
408
|
const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
|
|
@@ -426,8 +449,8 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
426
449
|
}
|
|
427
450
|
const facts: LifecycleFacts = {
|
|
428
451
|
taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
|
|
429
|
-
|
|
430
|
-
|
|
452
|
+
reviewedInputs, executionInputs, artifactHashes,
|
|
453
|
+
reworkIntents: rework.intents,
|
|
431
454
|
unresolvedLedger,
|
|
432
455
|
executionBusy: executionBusy || hasOpenLifecycleExecution(content),
|
|
433
456
|
recommendedAction: null,
|
|
@@ -12,8 +12,6 @@ import { parseActivityLog, pairEntries, startedBackedRows } from '../activity-lo
|
|
|
12
12
|
import { readRun, type OrchestrationRun } from '../orchestration.ts';
|
|
13
13
|
import type { DelegationReceipt } from '../delegation-receipts.ts';
|
|
14
14
|
import { statusCard, type DisplayMessage } from '../../server/display.ts';
|
|
15
|
-
import { invalidationBlocks, parseInvalidationDocument } from '../invalidation.ts';
|
|
16
|
-
import { parseReworkIntentDocument } from '../rework-intent.ts';
|
|
17
15
|
import { buildLifecycleFacts, recommendNext } from '../capabilities.ts';
|
|
18
16
|
|
|
19
17
|
const USAGE = `Usage: ai task status [--task <ref> | -t <ref>]
|
|
@@ -384,7 +382,6 @@ type StatusModel = {
|
|
|
384
382
|
workflow: WorkflowInfo;
|
|
385
383
|
runtime: RuntimeInfo;
|
|
386
384
|
orchestration: OrchestrationInfo;
|
|
387
|
-
invalidation?: { status: string; processed: number; total: number; pendingIntents: number };
|
|
388
385
|
recommendation?: string;
|
|
389
386
|
recommendationReason?: string;
|
|
390
387
|
lifecyclePath?: string;
|
|
@@ -436,16 +433,6 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
436
433
|
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
437
434
|
}
|
|
438
435
|
|
|
439
|
-
lines.push(
|
|
440
|
-
'',
|
|
441
|
-
'Invalidation',
|
|
442
|
-
...renderPairs([
|
|
443
|
-
['status', model.invalidation?.status ?? 'unknown'],
|
|
444
|
-
['processed', `${model.invalidation?.processed ?? 0}/${model.invalidation?.total ?? 0}`],
|
|
445
|
-
['pending_intents', String(model.invalidation?.pendingIntents ?? 0)]
|
|
446
|
-
])
|
|
447
|
-
);
|
|
448
|
-
|
|
449
436
|
lines.push('', 'Recommendation', ...renderPairs([
|
|
450
437
|
['path', model.lifecyclePath ?? DASH],
|
|
451
438
|
['action', model.recommendation ?? DASH],
|
|
@@ -517,8 +504,6 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
517
504
|
}
|
|
518
505
|
|
|
519
506
|
function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
|
|
520
|
-
if (model.invalidation?.status === 'invalid') return 'danger';
|
|
521
|
-
if (model.invalidation?.status === 'pending' || model.invalidation?.status === 'in-progress') return 'warning';
|
|
522
507
|
if (model.orchestration.status === 'paused') return 'warning';
|
|
523
508
|
if (model.orchestration.status === 'running') return 'running';
|
|
524
509
|
if (model.orchestration.status === 'completed') return 'success';
|
|
@@ -536,21 +521,6 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
536
521
|
const run = input.run ?? makeRunner(input.repoRoot);
|
|
537
522
|
const artifacts = enumerateArtifacts(input.taskDir);
|
|
538
523
|
const workflow = collectWorkflow(content, input.now);
|
|
539
|
-
const invalidation = parseInvalidationDocument(content);
|
|
540
|
-
const rework = parseReworkIntentDocument(content);
|
|
541
|
-
const pendingIntents = rework.ok ? rework.intents.filter((intent) => intent.status === 'pending').length : 0;
|
|
542
|
-
const invalidationInfo = !invalidation.ok
|
|
543
|
-
? { status: 'invalid', processed: 0, total: 0, pendingIntents }
|
|
544
|
-
: {
|
|
545
|
-
status: invalidation.present
|
|
546
|
-
? invalidationBlocks(invalidation.document)
|
|
547
|
-
? invalidation.document.operations.some((operation) => operation.status === 'in-progress') ? 'in-progress' : 'pending'
|
|
548
|
-
: 'completed'
|
|
549
|
-
: 'none',
|
|
550
|
-
processed: invalidation.document.operations.reduce((sum, operation) => sum + operation.processed, 0),
|
|
551
|
-
total: invalidation.document.operations.reduce((sum, operation) => sum + operation.total, 0),
|
|
552
|
-
pendingIntents
|
|
553
|
-
};
|
|
554
524
|
const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
|
|
555
525
|
const next = facts.ok ? recommendNext(facts.facts) : null;
|
|
556
526
|
const recommendation = next?.action ?? DASH;
|
|
@@ -565,7 +535,6 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
565
535
|
workflow,
|
|
566
536
|
runtime: collectRuntime(input.taskDir, workflow, run),
|
|
567
537
|
orchestration: collectOrchestration(input.taskDir),
|
|
568
|
-
invalidation: invalidationInfo,
|
|
569
538
|
recommendation,
|
|
570
539
|
recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
|
|
571
540
|
lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,
|