@fitlab-ai/agent-infra 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -1
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-finalization.js +41 -1
  7. package/dist/lib/internal/task-ledger.js +13 -5
  8. package/dist/lib/internal/task-operation-registry.js +1 -0
  9. package/dist/lib/platform/completion-backfill.js +63 -17
  10. package/dist/lib/sandbox/control/server.js +26 -143
  11. package/dist/lib/sandbox/task-cleanup.js +13 -20
  12. package/dist/lib/task/artifact-command.js +11 -4
  13. package/dist/lib/task/artifact-lifecycle.js +217 -85
  14. package/dist/lib/task/artifact-operations.js +1 -26
  15. package/dist/lib/task/artifact-receipts.js +4 -1
  16. package/dist/lib/task/artifact-schema.js +1 -0
  17. package/dist/lib/task/artifact-selection.js +25 -0
  18. package/dist/lib/task/capabilities.js +158 -39
  19. package/dist/lib/task/commands/status.js +9 -2
  20. package/dist/lib/task/control-authority.js +3 -2
  21. package/dist/lib/task/decision-intents.js +12 -0
  22. package/dist/lib/task/delivery.js +1 -43
  23. package/dist/lib/task/events.js +80 -64
  24. package/dist/lib/task/finalization.js +301 -143
  25. package/dist/lib/task/ledger-intents.js +157 -15
  26. package/dist/lib/task/lifecycle-path.js +75 -0
  27. package/dist/lib/task/lifecycle.js +10 -0
  28. package/dist/lib/task/local-artifact-finalization.js +1 -20
  29. package/dist/lib/task/orchestration.js +17 -7
  30. package/dist/lib/task/review-finalization.js +3 -16
  31. package/dist/lib/task/rework-intent.js +70 -4
  32. package/dist/lib/task/verification-engine.js +20 -30
  33. package/dist/lib/task/verification.js +1 -0
  34. package/dist/lib/task/workflow-warning-intents.js +21 -6
  35. package/dist/lib/task/workspace-snapshot.js +7 -2
  36. package/dist/lifecycle-build-manifest.json +3 -3
  37. package/lib/internal/cli-route-inventory.ts +1 -1
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +40 -1
  40. package/lib/internal/task-ledger.ts +12 -5
  41. package/lib/internal/task-operation-registry.ts +1 -0
  42. package/lib/platform/completion-backfill.ts +78 -16
  43. package/lib/sandbox/control/server.ts +21 -146
  44. package/lib/sandbox/task-cleanup.ts +12 -20
  45. package/lib/task/artifact-command.ts +14 -4
  46. package/lib/task/artifact-lifecycle.ts +236 -85
  47. package/lib/task/artifact-operations.ts +1 -26
  48. package/lib/task/artifact-receipts.ts +4 -1
  49. package/lib/task/artifact-schema.ts +1 -0
  50. package/lib/task/artifact-selection.ts +53 -0
  51. package/lib/task/capabilities.ts +154 -40
  52. package/lib/task/commands/status.ts +11 -2
  53. package/lib/task/control-authority.ts +3 -1
  54. package/lib/task/decision-intents.ts +9 -0
  55. package/lib/task/delivery.ts +1 -40
  56. package/lib/task/events.ts +82 -64
  57. package/lib/task/finalization.ts +269 -111
  58. package/lib/task/ledger-intents.ts +155 -15
  59. package/lib/task/lifecycle-path.ts +98 -0
  60. package/lib/task/lifecycle.ts +13 -0
  61. package/lib/task/local-artifact-finalization.ts +2 -25
  62. package/lib/task/orchestration.ts +26 -8
  63. package/lib/task/review-finalization.ts +3 -16
  64. package/lib/task/rework-intent.ts +75 -5
  65. package/lib/task/verification-engine.ts +19 -30
  66. package/lib/task/verification.ts +2 -1
  67. package/lib/task/workflow-warning-intents.ts +18 -6
  68. package/lib/task/workspace-snapshot.ts +3 -2
  69. package/package.json +1 -1
  70. package/templates/.agents/rules/review-handshake.en.md +4 -4
  71. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  72. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  73. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  74. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  75. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  76. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  77. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  78. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  79. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  82. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  83. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  84. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  85. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  86. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  87. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  88. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  89. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  90. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  91. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  92. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  93. package/templates/.agents/templates/task.en.md +2 -2
  94. package/templates/.agents/templates/task.zh-CN.md +2 -2
  95. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  96. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  97. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  98. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  99. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  100. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  101. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  102. package/lib/sandbox/control/finalization-response.ts +0 -35
@@ -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 = { repoRoot?: string };
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 plan = inspectArtifactDirectory(taskDir, 'plan');
264
- if (plan.status !== 'ready') return null;
265
- const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
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; requireReference?: boolean }>> = {
286
- analysis: { family: 'review-analysis', requireReference: true },
287
- plan: { family: 'review-plan', requireReference: true },
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
- const QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS = new Set<ArtifactFamily>(['analysis', 'plan']);
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
- if (inventory.family === 'code') return resolveCodeContext(inventory, options);
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
- return { ...inventory, inputs, codeMode: null };
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 plan = inspectTaskArtifacts(taskRef, 'plan', options);
342
- if (plan.status === 'failed' || !plan.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest plan artifact is required');
343
- const reviewPlan = inspectTaskArtifacts(taskRef, 'review-plan', options);
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 = [plan.latest];
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
- const qualificationError = qualificationErrorForArtifact(latestCode);
353
- if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, latestCode.name);
354
- }
355
- if (reviewCode.latest) {
356
- const qualificationError = qualificationErrorForArtifact(reviewCode.latest);
357
- if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewCode.latest.name);
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 contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
509
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null,
510
+ `Code input receipt for ${latestCode.name} is unavailable.`);
374
511
  }
375
- if (reviewPlan.latest && !reviewPlan.reviewedInput) {
376
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
377
- }
378
- if (reviewPlan.latest) {
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 contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `${expected} is required before another code round`, expected);
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
- if (verdict.verdict === 'Approved') {
402
- const decision = resolveDecisionImplementationInput(inventory, review);
403
- if ('error' in decision) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
404
- if (decision.input) {
405
- return withCodeMode(
406
- inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax,
407
- verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`,
408
- decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence
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' || verdict.verdict === 'Rejected') {
413
- return withCodeMode(inventory, [...inputs, review], 'refused', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
414
- verdict.verdict === 'Approved'
415
- ? `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`
416
- : `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
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 || inventory.artifacts.some((artifact) => artifact.name === name)) return null;
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,
@@ -294,23 +288,6 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
294
288
  return identity?.family === family ? identity.round : null;
295
289
  }
296
290
 
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
291
  function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
315
292
  const schema = getArtifactSchema(request.family);
316
293
  if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
@@ -333,9 +310,7 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
333
310
  artifact: request.artifact,
334
311
  ...(request.locale ? { locale: request.locale } : {})
335
312
  });
336
- const audit = qualificationAuditForInit(request);
337
- if (!audit.ok) return resultFailure(audit.code, audit.message);
338
- const content = `${skeleton.trimEnd()}${audit.content}`;
313
+ const content = skeleton.trimEnd();
339
314
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
340
315
  try {
341
316
  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
- if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
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
  }
@@ -30,6 +30,7 @@ const LOCAL_SECTIONS = {
30
30
  ['impact', '影响评估', 'Impact Assessment'],
31
31
  ['risks', '技术风险', 'Technical Risks'],
32
32
  ['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
33
+ ['flow-decision', '流程裁定', 'Flow Decision'],
33
34
  ['state-check', '状态核对', 'State Check']
34
35
  ],
35
36
  plan: [
@@ -0,0 +1,53 @@
1
+ import type { ArtifactFamily } from './artifact-name.ts';
2
+
3
+ type SelectionArtifact = Readonly<{
4
+ family: ArtifactFamily;
5
+ round: number;
6
+ name: string;
7
+ }>;
8
+
9
+ type ArtifactDisposition = 'create' | 'resume' | 'reuse';
10
+
11
+ type ArtifactSelection = Readonly<{
12
+ disposition: ArtifactDisposition;
13
+ reasonCode: string;
14
+ artifact: SelectionArtifact;
15
+ writeRequired: boolean;
16
+ }>;
17
+
18
+ type ArtifactSelectionRequest = Readonly<{
19
+ next: SelectionArtifact;
20
+ latest: SelectionArtifact | null;
21
+ open: boolean;
22
+ hasChangeEvidence: boolean;
23
+ }>;
24
+
25
+ function selectArtifactDisposition(request: ArtifactSelectionRequest): ArtifactSelection {
26
+ if (request.open) {
27
+ return {
28
+ disposition: 'resume', reasonCode: 'open-round', artifact: request.next,
29
+ writeRequired: true
30
+ };
31
+ }
32
+ if (!request.latest) {
33
+ return {
34
+ disposition: 'create',
35
+ reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
36
+ artifact: request.next, writeRequired: true
37
+ };
38
+ }
39
+ const reasonCode = request.hasChangeEvidence ? 'change-evidence' : 'history-matched';
40
+ const reuse = reasonCode === 'history-matched';
41
+ return {
42
+ disposition: reuse ? 'reuse' : 'create',
43
+ reasonCode,
44
+ artifact: reuse ? request.latest : request.next,
45
+ writeRequired: !reuse
46
+ };
47
+ }
48
+
49
+ export { selectArtifactDisposition };
50
+ export type {
51
+ ArtifactDisposition,
52
+ ArtifactSelection,
53
+ };