@fitlab-ai/agent-infra 0.11.3 → 0.11.5
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 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- 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 +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- 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 +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- 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 +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- 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 +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- 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 +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- 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/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +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-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- 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-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- 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/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- 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/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/sandbox/control/finalization-response.ts +0 -35
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -13,6 +13,9 @@ export type ArtifactCommand = Readonly<{
|
|
|
13
13
|
family: string;
|
|
14
14
|
artifact: string;
|
|
15
15
|
locale?: 'zh-CN' | 'en';
|
|
16
|
+
sourceFinding?: string;
|
|
17
|
+
sourceArtifact?: string;
|
|
18
|
+
sourceSha256?: string;
|
|
16
19
|
}>;
|
|
17
20
|
|
|
18
21
|
/** One option contract for the CLI and trusted projection executor. */
|
|
@@ -23,7 +26,7 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
|
23
26
|
}
|
|
24
27
|
const fields: Record<string, string> = {};
|
|
25
28
|
const allowed = {
|
|
26
|
-
inspect: ['--family'],
|
|
29
|
+
inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
27
30
|
init: ['--family', '--artifact', '--locale'],
|
|
28
31
|
'preflight': ['--family', '--artifact'],
|
|
29
32
|
'finalize-local': ['--family', '--artifact']
|
|
@@ -41,8 +44,10 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
|
41
44
|
const locale = fields['--locale'];
|
|
42
45
|
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
43
46
|
return {
|
|
44
|
-
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
|
|
45
|
-
artifact: fields['--artifact'] ?? '', locale
|
|
47
|
+
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'] ?? '',
|
|
48
|
+
artifact: fields['--artifact'] ?? '', locale,
|
|
49
|
+
sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
|
|
50
|
+
sourceSha256: fields['--source-sha256']
|
|
46
51
|
};
|
|
47
52
|
}
|
|
48
53
|
/** Resolve authoritative task metadata separately from a trusted candidate directory. */
|
|
@@ -53,7 +58,12 @@ export function executeArtifactCommand(
|
|
|
53
58
|
const { taskRef, operation, family, artifact, locale } = command;
|
|
54
59
|
const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
|
|
55
60
|
if (operation === 'inspect') {
|
|
56
|
-
const result = resolveArtifactContext(taskRef, family, {
|
|
61
|
+
const result = resolveArtifactContext(taskRef, family, {
|
|
62
|
+
repoRoot: options.repoRoot,
|
|
63
|
+
sourceFinding: command.sourceFinding,
|
|
64
|
+
sourceArtifact: command.sourceArtifact,
|
|
65
|
+
sourceSha256: command.sourceSha256
|
|
66
|
+
});
|
|
57
67
|
return family === 'code' && result.codeMode ? {
|
|
58
68
|
...result, mode: result.codeMode.mode, code_max: result.codeMode.codeMax, rev_max: result.codeMode.reviewMax,
|
|
59
69
|
verdict: result.codeMode.verdict, next_round: result.next?.round ?? null, next_artifact: result.next?.name ?? null,
|
|
@@ -11,8 +11,12 @@ import { extractSection, findSectionHeading } from './sections.ts';
|
|
|
11
11
|
import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
12
12
|
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
13
13
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
14
|
-
import { validateQualificationAudit } from './qualification-audit.ts';
|
|
15
14
|
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
|
|
15
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
16
|
+
import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
17
|
+
import { parseReworkIntentDocument } from './rework-intent.ts';
|
|
18
|
+
import { selectArtifactDisposition } from './artifact-selection.ts';
|
|
19
|
+
import type { ArtifactSelection } from './artifact-selection.ts';
|
|
16
20
|
|
|
17
21
|
const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
|
|
18
22
|
const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
|
|
@@ -33,6 +37,16 @@ export function hasOpenArtifactRound(content: string, family: string, round: num
|
|
|
33
37
|
return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
function hasCompletedArtifactRound(content: string, family: string, round: number, name: string): boolean {
|
|
41
|
+
const activity = locateActivityLog(content);
|
|
42
|
+
const step = ARTIFACT_STEPS[family];
|
|
43
|
+
if (!activity || !step) return false;
|
|
44
|
+
const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
|
|
45
|
+
return startedBackedRows(pairEntries(activity.entries)).some((row) =>
|
|
46
|
+
expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
type ArtifactIdentity = {
|
|
37
51
|
family: ArtifactFamily;
|
|
38
52
|
round: number;
|
|
@@ -55,9 +69,12 @@ type ArtifactErrorCode =
|
|
|
55
69
|
| ResolveTaskRefErrorCode | 'ARTIFACT_FAMILY_UNKNOWN' | 'ARTIFACT_DIRECTORY_READ_FAILED'
|
|
56
70
|
| 'ARTIFACT_TOPOLOGY_CONFLICT' | 'ARTIFACT_INPUT_MISSING'
|
|
57
71
|
| 'ARTIFACT_REFERENCE_INVALID' | 'ARTIFACT_PATH_INVALID'
|
|
72
|
+
| 'LIFECYCLE_PATH_INVALID'
|
|
58
73
|
| 'ARTIFACT_IDENTITY_INVALID' | 'ARTIFACT_NOT_FOUND'
|
|
59
74
|
| 'ARTIFACT_NOT_REGULAR' | 'ARTIFACT_NOT_READABLE' | 'ARTIFACT_VERDICT_INVALID'
|
|
60
|
-
| 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
|
|
75
|
+
| 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
|
|
76
|
+
| 'ARTIFACT_SELECTION_INDETERMINATE';
|
|
77
|
+
|
|
61
78
|
type ArtifactError = { code: ArtifactErrorCode; message: string };
|
|
62
79
|
type ArtifactInventoryResult = {
|
|
63
80
|
status: 'ready' | 'failed';
|
|
@@ -79,6 +96,7 @@ type CodeArtifactMode = 'init' | 'fix' | 'decision' | 'refused' | 'error';
|
|
|
79
96
|
type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
|
|
80
97
|
status: ArtifactContextStatus;
|
|
81
98
|
inputs: readonly ArtifactIdentity[];
|
|
99
|
+
selection?: ArtifactSelection | null;
|
|
82
100
|
codeMode: {
|
|
83
101
|
mode: CodeArtifactMode;
|
|
84
102
|
codeMax: number;
|
|
@@ -91,7 +109,13 @@ type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
|
|
|
91
109
|
message: string;
|
|
92
110
|
} | null;
|
|
93
111
|
};
|
|
94
|
-
type InspectOptions = {
|
|
112
|
+
type InspectOptions = {
|
|
113
|
+
repoRoot?: string;
|
|
114
|
+
reasonCode?: string;
|
|
115
|
+
sourceFinding?: string;
|
|
116
|
+
sourceArtifact?: string;
|
|
117
|
+
sourceSha256?: string;
|
|
118
|
+
};
|
|
95
119
|
|
|
96
120
|
const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
|
|
97
121
|
'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
|
|
@@ -260,9 +284,11 @@ function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { inp
|
|
|
260
284
|
try { receipt = receiptForOutput(taskContent, code.name); }
|
|
261
285
|
catch { return null; }
|
|
262
286
|
if (!receipt || receipt.event !== 'code.completed') return null;
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
const
|
|
287
|
+
const identity = parseArtifactName(receipt.input);
|
|
288
|
+
if (!identity || !['analysis', 'plan'].includes(identity.family)) return null;
|
|
289
|
+
const source = inspectArtifactDirectory(taskDir, identity.family);
|
|
290
|
+
if (source.status !== 'ready') return null;
|
|
291
|
+
const input = source.artifacts.find((artifact) => artifact.name === receipt.input);
|
|
266
292
|
return input ? { input, inputSha256: receipt.inputSha256 } : null;
|
|
267
293
|
}
|
|
268
294
|
|
|
@@ -270,6 +296,16 @@ function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input:
|
|
|
270
296
|
return resolveCodeInputReceipt(taskDir, code);
|
|
271
297
|
}
|
|
272
298
|
|
|
299
|
+
function lifecyclePathState(taskDir: string): LifecyclePathState {
|
|
300
|
+
const analysis = inspectArtifactDirectory(taskDir, 'analysis');
|
|
301
|
+
if (analysis.status !== 'ready' || !analysis.latest) return { status: 'missing', decision: null, message: 'analysis artifact is missing' };
|
|
302
|
+
try {
|
|
303
|
+
return parseLifecyclePathDecision(fs.readFileSync(analysis.latest.path, 'utf8'), analysis.latest.name, sha256File(analysis.latest.path));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { status: 'invalid', decision: null, message: error instanceof Error ? error.message : String(error) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
273
309
|
function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
|
|
274
310
|
if (inventory.status === 'failed') return inventory.error;
|
|
275
311
|
const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
|
|
@@ -282,21 +318,127 @@ function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactEr
|
|
|
282
318
|
const REQUIRED_INPUT: Partial<Record<ArtifactFamily, ArtifactFamily>> = {
|
|
283
319
|
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
|
|
284
320
|
};
|
|
285
|
-
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily
|
|
286
|
-
analysis: { family: 'review-analysis'
|
|
287
|
-
plan: { family: 'review-plan'
|
|
321
|
+
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily }>> = {
|
|
322
|
+
analysis: { family: 'review-analysis' },
|
|
323
|
+
plan: { family: 'review-plan' },
|
|
288
324
|
'review-code': { family: 'review-plan' },
|
|
289
325
|
'manual-validation': { family: 'review-code' },
|
|
290
326
|
'validation-run': { family: 'review-code' }
|
|
291
327
|
};
|
|
292
|
-
|
|
328
|
+
function hasSelectionEvidence(
|
|
329
|
+
content: string,
|
|
330
|
+
family: ArtifactFamily,
|
|
331
|
+
options: InspectOptions,
|
|
332
|
+
context: ArtifactContextResult
|
|
333
|
+
): boolean {
|
|
334
|
+
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256) return true;
|
|
335
|
+
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt') return true;
|
|
336
|
+
if (family.startsWith('review-')) {
|
|
337
|
+
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
338
|
+
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
339
|
+
if (currentInput && context.reviewedInput?.name !== currentInput.name) return true;
|
|
340
|
+
}
|
|
341
|
+
const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
|
|
342
|
+
if (target) {
|
|
343
|
+
const parsed = parseReworkIntentDocument(content);
|
|
344
|
+
if (!parsed.ok) throw new Error(`${parsed.code}: ${parsed.message}`);
|
|
345
|
+
if (parsed.intents.some((intent) => intent.status === 'pending'
|
|
346
|
+
&& (intent.target === target || family === 'analysis' && intent.target === 'plan'))) return true;
|
|
347
|
+
}
|
|
348
|
+
if (family === 'code') {
|
|
349
|
+
if (context.latest && context.codeMode?.mode === 'init') return true;
|
|
350
|
+
if (parseImplementationInputs(content).rows.some((input) => input.needsImplementation && input.status === 'pending')) return true;
|
|
351
|
+
}
|
|
352
|
+
return context.inputs.filter((candidate) => candidate.family.startsWith('review-')).some((input) => {
|
|
353
|
+
const verdict = parseVerdict(input.path);
|
|
354
|
+
if (!verdict.ok || verdict.verdict !== 'Changes Requested' || !context.latest) return false;
|
|
355
|
+
const receipt = receiptForOutput(content, input.name);
|
|
356
|
+
return receipt?.event === reviewEventName(input.family)
|
|
357
|
+
&& receipt.input === context.latest.name
|
|
358
|
+
&& receipt.inputSha256 === sha256File(context.latest.path);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function attachArtifactSelection(
|
|
363
|
+
context: ArtifactContextResult,
|
|
364
|
+
pathState: LifecyclePathState,
|
|
365
|
+
options: InspectOptions
|
|
366
|
+
): ArtifactContextResult {
|
|
367
|
+
if (context.status !== 'ready' || !context.taskDir || !context.taskId
|
|
368
|
+
|| (context.family !== 'analysis' && pathState.status !== 'valid')) return context;
|
|
369
|
+
const family = context.family as ArtifactFamily;
|
|
370
|
+
const fail = (code: ArtifactErrorCode, message: string): ArtifactContextResult => ({
|
|
371
|
+
...context, status: 'failed', selection: null, error: { code, message }
|
|
372
|
+
});
|
|
373
|
+
if (!context.next) return fail('ARTIFACT_SELECTION_INDETERMINATE', 'artifact inventory did not provide a writable identity');
|
|
374
|
+
let taskContent: string;
|
|
375
|
+
try {
|
|
376
|
+
taskContent = fs.readFileSync(path.join(context.taskDir, 'task.md'), 'utf8');
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
379
|
+
}
|
|
380
|
+
const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
|
|
381
|
+
const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
|
|
382
|
+
const open = openLatest || openNext;
|
|
383
|
+
const selectedIdentity = openLatest ? context.latest! : context.next;
|
|
384
|
+
try {
|
|
385
|
+
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
|
|
386
|
+
const selection = selectArtifactDisposition({
|
|
387
|
+
next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
|
|
388
|
+
latest: context.latest,
|
|
389
|
+
open,
|
|
390
|
+
hasChangeEvidence
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
...context,
|
|
394
|
+
selection,
|
|
395
|
+
next: selection.writeRequired ? { round: selection.artifact.round, name: selection.artifact.name } : null
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
293
401
|
|
|
294
402
|
function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
|
|
295
403
|
const inventory = inspectTaskArtifacts(taskRef, family, options);
|
|
296
404
|
if (inventory.status === 'failed') return { ...inventory, inputs: [], codeMode: null };
|
|
297
405
|
const writableError = assertWritableInventory(inventory);
|
|
298
406
|
if (writableError) return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
|
|
299
|
-
|
|
407
|
+
const pathState = lifecyclePathState(inventory.taskDir!);
|
|
408
|
+
if (inventory.family !== 'analysis' && pathState.status !== 'valid') {
|
|
409
|
+
return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
|
|
410
|
+
}
|
|
411
|
+
if (inventory.family === 'code') {
|
|
412
|
+
const code = resolveCodeContext(inventory, options);
|
|
413
|
+
const selected = attachArtifactSelection(code, pathState, options);
|
|
414
|
+
if (selected.status !== 'ready' || !selected.selection) return selected;
|
|
415
|
+
if (selected.selection.disposition === 'reuse') return selected;
|
|
416
|
+
if (selected.codeMode?.mode === 'error') {
|
|
417
|
+
return {
|
|
418
|
+
...selected,
|
|
419
|
+
status: 'ready',
|
|
420
|
+
codeMode: {
|
|
421
|
+
...selected.codeMode,
|
|
422
|
+
mode: 'init',
|
|
423
|
+
message: 'A new implementation round was requested.'
|
|
424
|
+
},
|
|
425
|
+
error: null
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (selected.codeMode?.mode === 'refused') {
|
|
429
|
+
return {
|
|
430
|
+
...selected,
|
|
431
|
+
status: 'ready',
|
|
432
|
+
codeMode: {
|
|
433
|
+
...selected.codeMode,
|
|
434
|
+
mode: 'init',
|
|
435
|
+
message: 'A new implementation round was requested.'
|
|
436
|
+
},
|
|
437
|
+
error: null
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return selected;
|
|
441
|
+
}
|
|
300
442
|
const required = REQUIRED_INPUT[inventory.family as ArtifactFamily];
|
|
301
443
|
const inputs: ArtifactIdentity[] = [];
|
|
302
444
|
if (required) {
|
|
@@ -304,116 +446,113 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
|
|
|
304
446
|
if (input.status === 'failed' || !input.latest) {
|
|
305
447
|
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
|
|
306
448
|
}
|
|
307
|
-
const qualificationError = qualificationErrorForArtifact(input.latest);
|
|
308
|
-
if (qualificationError) return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
|
|
309
449
|
inputs.push(input.latest);
|
|
310
450
|
}
|
|
311
451
|
const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
|
|
312
452
|
if (optional) {
|
|
313
453
|
const context = inspectTaskArtifacts(taskRef, optional.family, options);
|
|
314
454
|
if (context.status === 'ready' && context.latest) {
|
|
315
|
-
if (optional.requireReference && !context.reviewedInput) {
|
|
316
|
-
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: `${context.latest.name} has no valid reviewed input` } };
|
|
317
|
-
}
|
|
318
|
-
const qualificationError = qualificationErrorForArtifact(context.latest);
|
|
319
|
-
if (qualificationError && !QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS.has(inventory.family as ArtifactFamily)) {
|
|
320
|
-
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
|
|
321
|
-
}
|
|
322
455
|
inputs.push(context.latest);
|
|
323
456
|
}
|
|
324
457
|
}
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function qualificationErrorForArtifact(artifact: ArtifactIdentity): string | null {
|
|
329
|
-
try {
|
|
330
|
-
const taskContent = fs.readFileSync(path.join(path.dirname(artifact.path), 'task.md'), 'utf8');
|
|
331
|
-
const artifactContent = fs.readFileSync(artifact.path, 'utf8');
|
|
332
|
-
const result = validateQualificationAudit(taskContent, artifactContent);
|
|
333
|
-
return result.ok ? null : `${result.code}: ${result.message}`;
|
|
334
|
-
} catch (error) {
|
|
335
|
-
return error instanceof Error ? error.message : String(error);
|
|
458
|
+
if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
|
|
459
|
+
return { ...inventory, inputs, codeMode: null };
|
|
336
460
|
}
|
|
461
|
+
return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
|
|
337
462
|
}
|
|
338
463
|
|
|
339
464
|
function resolveCodeContext(inventory: ArtifactInventoryResult, options: InspectOptions): ArtifactContextResult {
|
|
340
465
|
const taskRef = inventory.requestRef;
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
const
|
|
466
|
+
const pathState = lifecyclePathState(inventory.taskDir!);
|
|
467
|
+
if (pathState.status !== 'valid') return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
|
|
468
|
+
const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
|
|
469
|
+
const source = inspectTaskArtifacts(taskRef, inputFamily, options);
|
|
470
|
+
if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
|
|
344
471
|
const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
|
|
345
472
|
const latestCode = inventory.latest;
|
|
346
473
|
const codeMax = latestCode?.round ?? 0;
|
|
347
474
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
348
|
-
const inputs = [
|
|
349
|
-
const planQualificationError = qualificationErrorForArtifact(plan.latest);
|
|
350
|
-
if (planQualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', planQualificationError);
|
|
475
|
+
const inputs = [source.latest];
|
|
351
476
|
if (latestCode) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
477
|
+
try {
|
|
478
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
479
|
+
if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
|
|
480
|
+
const review = reviewCode.latest;
|
|
481
|
+
const openInputs = review ? [...inputs, review] : inputs;
|
|
482
|
+
const openVerdict = review ? parseVerdict(review.path) : null;
|
|
483
|
+
const openRow = locateActivityLog(taskContent)
|
|
484
|
+
? startedBackedRows(pairEntries(locateActivityLog(taskContent)!.entries))
|
|
485
|
+
.find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
|
|
486
|
+
: null;
|
|
487
|
+
const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
488
|
+
const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
489
|
+
const mode = decisionInput ? 'decision'
|
|
490
|
+
: fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
|
|
491
|
+
: 'init';
|
|
492
|
+
return withCodeMode(
|
|
493
|
+
inventory, openInputs, 'ready', mode, codeMax, reviewMax,
|
|
494
|
+
openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null,
|
|
495
|
+
`Resuming open code round ${latestCode.round}.`,
|
|
496
|
+
decisionInput
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
501
|
+
}
|
|
358
502
|
}
|
|
359
503
|
if (!latestCode) {
|
|
360
|
-
if (!reviewPlan.latest || reviewPlan.reviewedInput?.name !== plan.latest.name) {
|
|
361
|
-
return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest plan '${plan.latest.name}' requires a matching approved review-plan`);
|
|
362
|
-
}
|
|
363
|
-
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
364
|
-
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
|
|
365
|
-
if (verdict.verdict !== 'Approved') {
|
|
366
|
-
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `latest ${reviewPlan.latest.name} is not approved`, reviewPlan.latest.name);
|
|
367
|
-
}
|
|
368
504
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
369
505
|
'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
|
|
370
506
|
}
|
|
371
507
|
const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
|
|
372
508
|
if (!codePlanInput) {
|
|
373
|
-
return
|
|
509
|
+
return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null,
|
|
510
|
+
`Code input receipt for ${latestCode.name} is unavailable.`);
|
|
374
511
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const qualificationError = qualificationErrorForArtifact(reviewPlan.latest);
|
|
380
|
-
if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewPlan.latest.name);
|
|
381
|
-
}
|
|
382
|
-
if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
|
|
383
|
-
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
384
|
-
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
|
|
385
|
-
const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
|
|
386
|
-
if (verdict.ok && verdict.verdict === 'Approved' && (
|
|
387
|
-
codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
|
|
388
|
-
)) {
|
|
389
|
-
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
|
|
390
|
-
`Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
391
|
-
}
|
|
512
|
+
const currentInputSha256 = sha256File(source.latest.path);
|
|
513
|
+
if (codePlanInput.input.name !== source.latest.name || codePlanInput.inputSha256 !== currentInputSha256) {
|
|
514
|
+
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
515
|
+
`Latest ${source.latest.name} is not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
392
516
|
}
|
|
393
517
|
if (!reviewCode.latest || reviewCode.reviewedInput?.name !== latestCode.name) {
|
|
394
518
|
const expected = reviewCode.next?.name ?? artifactName('review-code', reviewMax + 1);
|
|
395
|
-
return
|
|
519
|
+
return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, expected,
|
|
520
|
+
`${expected} is required before another code round`);
|
|
396
521
|
}
|
|
397
522
|
const review = reviewCode.latest;
|
|
398
523
|
if (!review) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest review-code artifact is required');
|
|
399
524
|
const verdict = parseVerdict(review.path);
|
|
400
525
|
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, review.name);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
526
|
+
const decision = resolveDecisionImplementationInput(inventory, review);
|
|
527
|
+
if ('error' in decision) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
|
|
528
|
+
if (decision.input) {
|
|
529
|
+
return withCodeMode(
|
|
530
|
+
inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax,
|
|
531
|
+
verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`,
|
|
532
|
+
decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
537
|
+
const rework = parseReworkIntentDocument(taskContent);
|
|
538
|
+
const reviewSha256 = sha256File(review.path);
|
|
539
|
+
if (rework.ok && rework.intents.some((intent) => intent.status === 'pending'
|
|
540
|
+
&& intent.target === 'code'
|
|
541
|
+
&& intent.sourceArtifact === review.name
|
|
542
|
+
&& intent.sourceSha256 === reviewSha256)) {
|
|
543
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
|
|
544
|
+
`Pending rework evidence requires a new code round after ${review.name}.`);
|
|
410
545
|
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error), review.name);
|
|
411
548
|
}
|
|
412
|
-
if (verdict.verdict === 'Approved'
|
|
413
|
-
return withCodeMode(inventory, [...inputs, review], '
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
549
|
+
if (verdict.verdict === 'Approved') {
|
|
550
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
|
|
551
|
+
`Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`);
|
|
552
|
+
}
|
|
553
|
+
if (verdict.verdict === 'Rejected') {
|
|
554
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
|
|
555
|
+
`Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
|
|
417
556
|
}
|
|
418
557
|
return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
|
|
419
558
|
`Latest ${review.name} requires or permits fixes. Entering fix mode.`);
|
|
@@ -482,7 +621,19 @@ export function validateArtifactPublication(taskDir: string, family: ArtifactFam
|
|
|
482
621
|
const inventory = inspectArtifactDirectory(taskDir, family);
|
|
483
622
|
const error = assertWritableInventory(inventory);
|
|
484
623
|
if (error) return error;
|
|
485
|
-
if (inventory.next?.name === name
|
|
624
|
+
if (inventory.next?.name === name) return null;
|
|
625
|
+
const existing = inventory.artifacts.find((artifact) => artifact.name === name);
|
|
626
|
+
if (existing) {
|
|
627
|
+
try {
|
|
628
|
+
const task = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
629
|
+
if (hasCompletedArtifactRound(task, family, existing.round, name)) {
|
|
630
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
486
637
|
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
|
|
487
638
|
}
|
|
488
639
|
|
|
@@ -10,12 +10,6 @@ import {
|
|
|
10
10
|
renderArtifactSkeleton
|
|
11
11
|
} from './artifact-schema.ts';
|
|
12
12
|
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
13
|
-
import {
|
|
14
|
-
buildQualificationAudit,
|
|
15
|
-
expectedQualificationRelations,
|
|
16
|
-
parseTaskQualification,
|
|
17
|
-
renderQualificationAudit
|
|
18
|
-
} from './qualification-audit.ts';
|
|
19
13
|
import type {
|
|
20
14
|
ArtifactSchema,
|
|
21
15
|
ArtifactSchemaFamily,
|
|
@@ -33,6 +27,7 @@ type ArtifactStructuralDiagnosticCode =
|
|
|
33
27
|
| 'ARTIFACT_EMPTY_SECTION'
|
|
34
28
|
| 'ARTIFACT_SECTION_ORDER_INVALID'
|
|
35
29
|
| 'ARTIFACT_HEADING_TRAILING_PUNCTUATION'
|
|
30
|
+
| 'ARTIFACT_STATE_CHECK_PATTERN_MISSING'
|
|
36
31
|
| 'ARTIFACT_REQUIRED_PATTERN_MISSING';
|
|
37
32
|
|
|
38
33
|
type ArtifactStructuralDiagnostic = Readonly<{
|
|
@@ -260,13 +255,68 @@ function inspectArtifactPatterns(
|
|
|
260
255
|
};
|
|
261
256
|
}
|
|
262
257
|
|
|
258
|
+
function inspectArtifactStateCheck(
|
|
259
|
+
content: string,
|
|
260
|
+
schema: ArtifactSchema,
|
|
261
|
+
scopeToStateCheckSection: boolean
|
|
262
|
+
): ArtifactStructureResult {
|
|
263
|
+
const stateCheck = schema.sections.find((section) => section.id === 'state-check');
|
|
264
|
+
if (!stateCheck || schema.stateCheckPatterns.length === 0) {
|
|
265
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!scopeToStateCheckSection) {
|
|
269
|
+
const diagnostics = schema.stateCheckPatterns
|
|
270
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(content))
|
|
271
|
+
.map((pattern) => diagnostic(
|
|
272
|
+
'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
|
|
273
|
+
`artifact is missing required state-check command '${pattern}'`,
|
|
274
|
+
stateCheck.id,
|
|
275
|
+
null
|
|
276
|
+
));
|
|
277
|
+
return {
|
|
278
|
+
ok: diagnostics.length === 0,
|
|
279
|
+
family: schema.family,
|
|
280
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
281
|
+
diagnostics
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const scanned = scanVisibleMarkdown(content);
|
|
286
|
+
const headings = scanned.headings.filter((heading) => (
|
|
287
|
+
heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)
|
|
288
|
+
));
|
|
289
|
+
if (headings.length !== 1) {
|
|
290
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const bodyBounds = sectionBodyBounds(content, headings[0]!, scanned);
|
|
294
|
+
const body = content.slice(bodyBounds.start, bodyBounds.end);
|
|
295
|
+
const diagnostics = schema.stateCheckPatterns
|
|
296
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(body))
|
|
297
|
+
.map((pattern) => diagnostic(
|
|
298
|
+
'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
|
|
299
|
+
`state-check section is missing required command '${pattern}'`,
|
|
300
|
+
stateCheck.id,
|
|
301
|
+
lineNumber(content, headings[0]!.start)
|
|
302
|
+
));
|
|
303
|
+
return {
|
|
304
|
+
ok: diagnostics.length === 0,
|
|
305
|
+
family: schema.family,
|
|
306
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
307
|
+
diagnostics
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
263
311
|
function inspectArtifactContract(
|
|
264
312
|
content: string,
|
|
265
|
-
schema: ArtifactSchema
|
|
313
|
+
schema: ArtifactSchema,
|
|
314
|
+
options: { scopeStateCheckSection?: boolean } = {}
|
|
266
315
|
): ArtifactStructureResult {
|
|
267
316
|
const structure = inspectArtifactStructure(content, schema);
|
|
317
|
+
const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
|
|
268
318
|
const patterns = inspectArtifactPatterns(content, schema);
|
|
269
|
-
const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
|
|
319
|
+
const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
|
|
270
320
|
return {
|
|
271
321
|
ok: diagnostics.length === 0,
|
|
272
322
|
family: schema.family,
|
|
@@ -294,23 +344,6 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
|
|
|
294
344
|
return identity?.family === family ? identity.round : null;
|
|
295
345
|
}
|
|
296
346
|
|
|
297
|
-
function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
|
|
298
|
-
let taskContent: string;
|
|
299
|
-
try {
|
|
300
|
-
taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
|
|
303
|
-
}
|
|
304
|
-
const qualification = parseTaskQualification(taskContent);
|
|
305
|
-
if (!qualification.ok) return qualification;
|
|
306
|
-
if (!qualification.qualification.present) return { ok: true, content: '' };
|
|
307
|
-
const relations = expectedQualificationRelations(taskContent, request.family);
|
|
308
|
-
if (!relations.ok) return relations;
|
|
309
|
-
const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
|
|
310
|
-
if (!audit.ok) return audit;
|
|
311
|
-
return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
347
|
function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
|
|
315
348
|
const schema = getArtifactSchema(request.family);
|
|
316
349
|
if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
|
|
@@ -333,9 +366,7 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
|
|
|
333
366
|
artifact: request.artifact,
|
|
334
367
|
...(request.locale ? { locale: request.locale } : {})
|
|
335
368
|
});
|
|
336
|
-
const
|
|
337
|
-
if (!audit.ok) return resultFailure(audit.code, audit.message);
|
|
338
|
-
const content = `${skeleton.trimEnd()}${audit.content}`;
|
|
369
|
+
const content = skeleton.trimEnd();
|
|
339
370
|
const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
|
|
340
371
|
try {
|
|
341
372
|
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
@@ -64,7 +64,10 @@ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input
|
|
|
64
64
|
const inputIdentity = parseArtifactName(input);
|
|
65
65
|
if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
66
66
|
const shape = RECEIPT_SHAPES[event];
|
|
67
|
-
|
|
67
|
+
const inputMatches = event === 'code.completed'
|
|
68
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
|
|
69
|
+
: inputIdentity.family === shape.input;
|
|
70
|
+
if (outputIdentity.family !== shape.output || !inputMatches) {
|
|
68
71
|
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
69
72
|
}
|
|
70
73
|
}
|