@fitlab-ai/agent-infra 0.11.4 → 0.11.6

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 (149) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/merge.js +62 -10
  15. package/dist/lib/platform/github-provider.js +9 -14
  16. package/dist/lib/platform/github-release-notes.js +80 -11
  17. package/dist/lib/platform/none-provider.js +1 -1
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/provider-contract.d.ts +39 -16
  20. package/dist/lib/platform/provider-contract.js +1 -1
  21. package/dist/lib/platform/provider-validation.js +83 -13
  22. package/dist/lib/platform/release-notes.js +7 -8
  23. package/dist/lib/platform/verification-sync.js +12 -49
  24. package/dist/lib/sandbox/capture.js +1 -1
  25. package/dist/lib/sandbox/commands/create.js +46 -1
  26. package/dist/lib/sandbox/commands/enter.js +1 -1
  27. package/dist/lib/sandbox/commands/start.js +1 -1
  28. package/dist/lib/sandbox/config.js +10 -0
  29. package/dist/lib/sandbox/control/client.js +4 -1
  30. package/dist/lib/sandbox/control/executor.js +12 -4
  31. package/dist/lib/sandbox/control/lifecycle.js +18 -13
  32. package/dist/lib/sandbox/control/protocol.js +2 -1
  33. package/dist/lib/sandbox/removal.js +15 -8
  34. package/dist/lib/sandbox/task-resolver.js +62 -2
  35. package/dist/lib/task/activity-intent.js +1 -2
  36. package/dist/lib/task/artifact-operations.js +36 -2
  37. package/dist/lib/task/artifact-schema.js +7 -7
  38. package/dist/lib/task/control-authority.js +9 -166
  39. package/dist/lib/task/decision-intents.js +2 -4
  40. package/dist/lib/task/events.js +7 -28
  41. package/dist/lib/task/finalization-handoff.js +92 -0
  42. package/dist/lib/task/finalization.js +123 -12
  43. package/dist/lib/task/intermediate-cleanup.js +9 -148
  44. package/dist/lib/task/ledger-intents.js +3 -6
  45. package/dist/lib/task/lifecycle.js +50 -11
  46. package/dist/lib/task/local-artifact-finalization.js +13 -28
  47. package/dist/lib/task/resolve-ref.js +3 -3
  48. package/dist/lib/task/review-command.js +1 -2
  49. package/dist/lib/task/review-finalization.js +1 -2
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +16 -11
  52. package/dist/lib/task/verification.js +1 -13
  53. package/dist/lib/task/workflow-warning-intents.js +2 -4
  54. package/dist/lib/task/write.js +122 -3
  55. package/dist/lifecycle-build-manifest.json +3 -3
  56. package/lib/decide.ts +5 -35
  57. package/lib/git/branch-worktree.ts +41 -0
  58. package/lib/internal/cli-route-inventory.ts +0 -2
  59. package/lib/internal/task-activity.ts +6 -34
  60. package/lib/internal/task-event.ts +4 -37
  61. package/lib/internal/task-finalization.ts +9 -2
  62. package/lib/internal/task-ledger.ts +5 -38
  63. package/lib/internal/task-lifecycle.ts +2 -2
  64. package/lib/internal/task-operation-registry.ts +1 -4
  65. package/lib/internal/task-review.ts +5 -31
  66. package/lib/internal/task-verify.ts +3 -39
  67. package/lib/internal/task-warning.ts +3 -32
  68. package/lib/merge.ts +59 -10
  69. package/lib/platform/github-provider.ts +11 -18
  70. package/lib/platform/github-release-notes.ts +89 -14
  71. package/lib/platform/none-provider.ts +1 -1
  72. package/lib/platform/pr-summary.ts +4 -25
  73. package/lib/platform/provider-contract.ts +38 -12
  74. package/lib/platform/provider-validation.ts +83 -12
  75. package/lib/platform/release-notes.ts +11 -12
  76. package/lib/platform/verification-sync.ts +13 -53
  77. package/lib/sandbox/capture.ts +1 -1
  78. package/lib/sandbox/commands/create.ts +52 -1
  79. package/lib/sandbox/commands/enter.ts +1 -1
  80. package/lib/sandbox/commands/start.ts +1 -1
  81. package/lib/sandbox/config.ts +12 -0
  82. package/lib/sandbox/control/client.ts +5 -1
  83. package/lib/sandbox/control/executor.ts +13 -4
  84. package/lib/sandbox/control/lifecycle.ts +22 -13
  85. package/lib/sandbox/control/protocol.ts +3 -2
  86. package/lib/sandbox/removal.ts +23 -7
  87. package/lib/sandbox/task-resolver.ts +51 -2
  88. package/lib/task/activity-intent.ts +1 -2
  89. package/lib/task/artifact-operations.ts +58 -2
  90. package/lib/task/artifact-schema.ts +9 -7
  91. package/lib/task/control-authority.ts +14 -188
  92. package/lib/task/decision-intents.ts +2 -4
  93. package/lib/task/events.ts +8 -27
  94. package/lib/task/finalization-handoff.ts +104 -0
  95. package/lib/task/finalization.ts +120 -12
  96. package/lib/task/intermediate-cleanup.ts +9 -125
  97. package/lib/task/ledger-intents.ts +3 -6
  98. package/lib/task/lifecycle.ts +45 -13
  99. package/lib/task/local-artifact-finalization.ts +16 -35
  100. package/lib/task/resolve-ref.ts +3 -3
  101. package/lib/task/review-command.ts +2 -5
  102. package/lib/task/review-finalization.ts +1 -4
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +20 -10
  105. package/lib/task/verification.ts +1 -15
  106. package/lib/task/workflow-warning-intents.ts +2 -4
  107. package/lib/task/write.ts +116 -5
  108. package/package.json +1 -1
  109. package/templates/.agents/rules/evidence-reporting.en.md +1 -2
  110. package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  113. package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
  116. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  117. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  119. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  120. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  121. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  122. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
  125. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
  126. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  127. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  128. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  129. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  130. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  131. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  132. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  133. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  134. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  137. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  138. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  139. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  140. package/dist/lib/internal/task-override.js +0 -138
  141. package/dist/lib/task/artifact-recovery.js +0 -476
  142. package/dist/lib/task/artifact-repair-intent.js +0 -181
  143. package/dist/lib/task/guard-override.js +0 -31
  144. package/dist/lib/task/human-override.js +0 -1259
  145. package/lib/internal/task-override.ts +0 -144
  146. package/lib/task/artifact-recovery.ts +0 -572
  147. package/lib/task/artifact-repair-intent.ts +0 -227
  148. package/lib/task/guard-override.ts +0 -50
  149. package/lib/task/human-override.ts +0 -1522
@@ -107,16 +107,11 @@ type TaskLifecycleOptions = {
107
107
  fileSystem?: Partial<LifecycleFileSystem>;
108
108
  taskFileSystem?: Partial<TaskFileSystem>;
109
109
  directoryRenameSync?: (source: string, target: string) => void;
110
- manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
111
110
  /** Write the terminal task document and durable journal, but leave the move
112
111
  * and short-id mutation for a separately authorized commit. */
113
112
  prepareOnly?: boolean;
114
113
  };
115
114
 
116
- function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
117
- return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
118
- }
119
-
120
115
  const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
121
116
  const DEFAULT_IO: LifecycleFileSystem = {
122
117
  renameSync: (source, target) => fs.renameSync(source, target),
@@ -362,6 +357,44 @@ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string):
362
357
  } catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
363
358
  }
364
359
 
360
+ export function inspectTaskLifecycleProgress(
361
+ repoRoot: string, taskId: string, agent: string
362
+ ): 'not-started' | 'started-recoverable' | 'unknown' {
363
+ try {
364
+ const hot = locateHotTaskDirs(repoRoot, taskId);
365
+ if (hot.length !== 1 || (hot[0]!.state !== 'active' && hot[0]!.state !== 'completed')) return 'unknown';
366
+ const { state, taskDir } = hot[0]!;
367
+ const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
368
+ const frontmatter = parseTypedTaskFrontmatter(content);
369
+ if (frontmatter.id !== taskId) return 'unknown';
370
+ const journalPath = path.join(taskDir, '.task-lifecycle.json');
371
+ if (!fs.existsSync(journalPath)) {
372
+ if (state === 'active') return frontmatter.status === 'active' ? 'not-started' : 'unknown';
373
+ return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
374
+ ? 'started-recoverable' : 'unknown';
375
+ }
376
+ const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
377
+ const request: TaskLifecycleRequest = { taskRef: taskId, intent: 'complete', agent };
378
+ if (journal.taskId !== taskId || journal.intent !== 'complete'
379
+ || journal.intentDigest !== intentIdentity(request, taskId)
380
+ || journal.sourceState !== 'active' || journal.targetState !== 'completed') return 'unknown';
381
+ if (journal.completedSteps.some((step, index) => STEPS[index] !== step)) return 'unknown';
382
+ const directoryMoved = journal.completedSteps.includes('directory-moved');
383
+ const registryCommitted = journal.completedSteps.includes('registry-committed');
384
+ const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
385
+ if (state === 'active' && (directoryMoved || !hasShortId)) return 'unknown';
386
+ if (state === 'completed' && (!journal.completedSteps.includes('task-written')
387
+ || !matchingCompletion(content, request) || (registryCommitted && hasShortId))) return 'unknown';
388
+ if (frontmatter.status !== state
389
+ && !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
390
+ return 'unknown';
391
+ }
392
+ return 'started-recoverable';
393
+ } catch {
394
+ return 'unknown';
395
+ }
396
+ }
397
+
365
398
  type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
366
399
 
367
400
  function directoryManifest(root: string): DirectoryEntry[] {
@@ -468,7 +501,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
468
501
  }
469
502
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
470
503
  }
471
- if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
504
+ if (!spec.sources.includes(sourceState) && !existingJournal) {
472
505
  return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
473
506
  }
474
507
  if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
@@ -479,7 +512,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
479
512
  try { frontmatter = parseTypedTaskFrontmatter(content); }
480
513
  catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
481
514
  if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
482
- if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING')) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
515
+ if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
483
516
  if (spec.target === 'active') {
484
517
  const contract = validateCurrentTaskContract(content);
485
518
  if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
@@ -493,10 +526,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
493
526
  }
494
527
  const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
495
528
  const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
496
- if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
529
+ if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
497
530
  return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
498
531
  }
499
- if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
532
+ if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
500
533
  return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
501
534
  }
502
535
  const digest = intentIdentity(request, taskId);
@@ -514,8 +547,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
514
547
  if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
515
548
  return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
516
549
  }
517
- const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
518
- if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
550
+ if (journal.intentDigest !== digest) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
519
551
  } else {
520
552
  let metadata: TaskWriteMetadata;
521
553
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
@@ -525,7 +557,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
525
557
  }
526
558
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
527
559
  if (request.dryRun) {
528
- const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
560
+ const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
529
561
  if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
530
562
  const taskPlan = writeTask(
531
563
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
@@ -561,7 +593,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
561
593
  if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
562
594
  if (!completed.has('task-written')) {
563
595
  const current = io.readFileSync(path.join(sourcePath, 'task.md'));
564
- const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
596
+ const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
565
597
  if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
566
598
  const writeResult = writeTask(
567
599
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
@@ -2,10 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { inspectDecisionDetailDuplicates } from './decision-details.ts';
5
- import { scanVisibleMarkdown } from './markdown.ts';
6
5
  import { parseArtifactName } from './artifact-name.ts';
6
+ import { parseLifecyclePathDecision } from './lifecycle-path.ts';
7
7
  import { resolveTaskRef } from './resolve-ref.ts';
8
- import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
8
+ import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
9
9
  import { getArtifactSchema } from './artifact-schema.ts';
10
10
 
11
11
  type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
@@ -24,7 +24,8 @@ type LocalArtifactDiagnosticCode =
24
24
  | 'LOCAL_DECISION_DETAIL_DUPLICATE'
25
25
  | 'LOCAL_REQUIRED_PATTERN_MISSING'
26
26
  | 'LOCAL_STRUCTURAL_INVALID'
27
- | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION';
27
+ | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
28
+ | 'LOCAL_FLOW_DECISION_INVALID';
28
29
 
29
30
  type LocalArtifactDiagnostic = {
30
31
  code: LocalArtifactDiagnosticCode;
@@ -71,7 +72,9 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
71
72
  ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
72
73
  ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
73
74
  ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
74
- ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
75
+ ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
76
+ ARTIFACT_STATE_CHECK_PATTERN_MISSING: 'LOCAL_STATUS_COMMAND_MISSING',
77
+ ARTIFACT_REQUIRED_PATTERN_MISSING: 'LOCAL_REQUIRED_PATTERN_MISSING'
75
78
  };
76
79
  return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
77
80
  }
@@ -80,47 +83,25 @@ function localDiagnostic(item: { code: string; message: string; line: number | n
80
83
  return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
81
84
  }
82
85
 
83
- function isStatusPattern(pattern: string): boolean {
84
- return pattern === '^\\$ ';
85
- }
86
-
87
86
  function validateLocalArtifact(
88
87
  content: string,
89
88
  options: LocalArtifactValidationOptions
90
89
  ): LocalArtifactValidationResult {
91
90
  const schema = getArtifactSchema(options.family)!;
92
91
  const diagnostics: LocalArtifactDiagnostic[] = [];
93
- const scanned = scanVisibleMarkdown(content);
94
- const structure = inspectArtifactStructure(content, schema);
95
- for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
96
-
97
- const statusSection = schema.sections.find((section) => section.id === 'state-check');
98
- if (statusSection) {
99
- const statusHeading = scanned.headings.find((heading) => (
100
- heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (
101
- heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`
102
- ))
103
- ));
104
- if (statusHeading) {
105
- const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
106
- const body = content.slice(statusHeading.end, next?.start ?? content.length);
107
- for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
108
- if (!new RegExp(pattern, 'm').test(body)) {
109
- diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
110
- }
111
- }
112
- }
113
- }
114
-
115
- const patternInspection = inspectArtifactPatterns(content, {
116
- ...schema,
117
- requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
118
- });
119
- for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
92
+ const inspection = inspectArtifactContract(content, schema, { scopeStateCheckSection: true });
93
+ for (const item of inspection.diagnostics) diagnostics.push(localDiagnostic(item));
120
94
 
121
95
  const decisionDetails = inspectDecisionDetailDuplicates(content);
122
96
  if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
123
97
 
98
+ if (options.family === 'analysis') {
99
+ const flowDecision = parseLifecyclePathDecision(content);
100
+ if (flowDecision.status !== 'valid') {
101
+ diagnostics.push({ code: 'LOCAL_FLOW_DECISION_INVALID', message: flowDecision.message, line: null });
102
+ }
103
+ }
104
+
124
105
  return {
125
106
  ok: diagnostics.length === 0,
126
107
  family: options.family,
@@ -81,7 +81,7 @@ function listSortedNumeric(dir: string, width: number): string[] {
81
81
  }
82
82
 
83
83
  function findInArchive(repoRoot: string, taskId: string): string | null {
84
- // archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
84
+ // archive-tasks stores local material at .agents/workspace/archive/YYYY/MM/DD/{taskId}/local/task.md
85
85
  // where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
86
86
  // the task id's creation date. So we cannot derive the path from taskId alone;
87
87
  // walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
@@ -91,7 +91,7 @@ function findInArchive(repoRoot: string, taskId: string): string | null {
91
91
  for (const month of listSortedNumeric(yearDir, 2)) {
92
92
  const monthDir = path.join(yearDir, month);
93
93
  for (const day of listSortedNumeric(monthDir, 2)) {
94
- const candidate = path.join(monthDir, day, taskId, 'task.md');
94
+ const candidate = path.join(monthDir, day, taskId, 'local', 'task.md');
95
95
  if (fs.existsSync(candidate)) return candidate;
96
96
  }
97
97
  }
@@ -162,7 +162,7 @@ function enumerateAllTaskDirs(repoRoot: string): { taskId: string; taskDir: stri
162
162
  const monthDir = path.join(yearDir, month);
163
163
  for (const day of listSortedNumeric(monthDir, 2).reverse()) {
164
164
  const dayDir = path.join(monthDir, day);
165
- for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId), 'archive');
165
+ for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId, 'local'), 'archive');
166
166
  }
167
167
  }
168
168
  }
@@ -1,13 +1,10 @@
1
1
  import type { ReviewFinalizationRequest } from './review-finalization.ts';
2
2
 
3
- export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest & {
4
- overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
5
- } {
3
+ export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest {
6
4
  if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
7
5
  const values: Record<string, string | boolean> = {};
8
6
  const flags = {
9
- '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
10
- '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
7
+ '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
11
8
  } as const;
12
9
  for (let index = 2; index < args.length; index += 1) {
13
10
  const flag = args[index]!;
@@ -12,8 +12,6 @@ import { resolveTaskRef } from './resolve-ref.ts';
12
12
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
13
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
14
  import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
15
- import { allowsManualOverride } from './guard-override.ts';
16
- import type { ManualOverrideCapability } from './guard-override.ts';
17
15
  import { getArtifactSchema } from './artifact-schema.ts';
18
16
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
19
17
 
@@ -70,7 +68,6 @@ type ReviewFileSystem = {
70
68
  type ReviewFinalizationOptions = {
71
69
  repoRoot?: string;
72
70
  fileSystem?: Partial<ReviewFileSystem>;
73
- manualOverride?: ManualOverrideCapability;
74
71
  lockAlreadyHeld?: boolean;
75
72
  };
76
73
 
@@ -224,7 +221,7 @@ function prepareReviewSummaryCandidate(
224
221
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
225
222
  taskId = resolved.taskId ?? null;
226
223
  if (!resolved.ok) return reject(resolved.code, resolved.message);
227
- if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
224
+ if (resolved.state !== 'active') {
228
225
  return reject('TASK_STATE_MISMATCH', `task ${taskId} is ${resolved.state}, expected active`);
229
226
  }
230
227
  const parsed = parseArtifactName(request.artifact);
@@ -97,22 +97,22 @@ function collectTaskSnapshot(taskRef: string, options: SnapshotOptions = {}): Ta
97
97
  const observations: SnapshotObservation[] = [];
98
98
  try {
99
99
  const output = stripOneTrailingNewline((options.gitStatus ?? defaultGitStatus)(resolved.repoRoot));
100
- observations.push({ name: 'git', status: 'ready', command: '$ git status -s', output, error: null });
100
+ observations.push({ name: 'git', status: 'ready', command: 'git status --short --untracked-files=all', output, error: null });
101
101
  } catch (error) {
102
- return failedSnapshot(resolved, taskRef, observations, 'git', '$ git status -s', 'SNAPSHOT_GIT_FAILED', error);
102
+ return failedSnapshot(resolved, taskRef, observations, 'git', 'git status --short --untracked-files=all', 'SNAPSHOT_GIT_FAILED', error);
103
103
  }
104
104
  try {
105
105
  const entries = (options.readDirectory ?? ((dir) => fs.readdirSync(dir, { withFileTypes: true })))(resolved.taskDir);
106
106
  const output = renderDirectory(entries, resolved.taskDir, options.lstat ?? fs.lstatSync);
107
- observations.push({ name: 'task-directory', status: 'ready', command: `$ ls -la ${taskDirPath}/`, output, error: null });
107
+ observations.push({ name: 'task-directory', status: 'ready', command: `fs.readdirSync ${taskDirPath}/`, output, error: null });
108
108
  } catch (error) {
109
- return failedSnapshot(resolved, taskRef, observations, 'task-directory', `$ ls -la ${taskDirPath}/`, 'SNAPSHOT_DIRECTORY_READ_FAILED', error);
109
+ return failedSnapshot(resolved, taskRef, observations, 'task-directory', `fs.readdirSync ${taskDirPath}/`, 'SNAPSHOT_DIRECTORY_READ_FAILED', error);
110
110
  }
111
111
  try {
112
112
  const content = (options.readTask ?? ((file) => fs.readFileSync(file, 'utf8')))(resolved.taskMdPath);
113
- observations.push({ name: 'task-tail', status: 'ready', command: `$ tail ${taskPath}`, output: renderTail(content), error: null });
113
+ observations.push({ name: 'task-tail', status: 'ready', command: `fs.readFileSync ${taskPath} (last 10 lines)`, output: renderTail(content), error: null });
114
114
  } catch (error) {
115
- return failedSnapshot(resolved, taskRef, observations, 'task-tail', `$ tail ${taskPath}`, 'SNAPSHOT_TASK_READ_FAILED', error);
115
+ return failedSnapshot(resolved, taskRef, observations, 'task-tail', `fs.readFileSync ${taskPath} (last 10 lines)`, 'SNAPSHOT_TASK_READ_FAILED', error);
116
116
  }
117
117
  return {
118
118
  status: 'ready', changed: false, requestRef: taskRef, taskId: resolved.taskId,
@@ -23,6 +23,7 @@ import { inspectPlatformPullRequest } from "../platform/pull-requests.ts";
23
23
  import { resolveMaterializedReviewedHeadRelation } from "../platform/change-request-git-evidence.ts";
24
24
  import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.ts";
25
25
  import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.ts";
26
+ import { resolveBranchWorktree } from "../git/branch-worktree.ts";
26
27
  import { parseTypedTaskFrontmatter } from "./frontmatter.ts";
27
28
  import { LEDGER_TERMINAL, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.ts";
28
29
  import type { LedgerRow } from "./ledger.ts";
@@ -570,6 +571,23 @@ function checkArtifact({ taskDir, config, artifactFile, skillName }: any): any {
570
571
  }
571
572
  }
572
573
 
574
+ const stateCheckPatterns = config.state_check_patterns || [];
575
+ if (stateCheckPatterns.length > 0) {
576
+ const scanned = scanVisibleMarkdown(content);
577
+ const stateCheckHeading = scanned.headings.find((heading) => (
578
+ heading.level === 2 && (heading.text === "状态核对" || heading.text === "State Check")
579
+ ));
580
+ const nextHeading = stateCheckHeading && scanned.headings.find((heading) => (
581
+ heading.start > stateCheckHeading.start && heading.level <= 2
582
+ ));
583
+ const stateCheckBody = stateCheckHeading ? content.slice(stateCheckHeading.end, nextHeading?.start ?? content.length) : "";
584
+ for (const pattern of stateCheckPatterns) {
585
+ if (!new RegExp(pattern, "m").test(stateCheckBody)) {
586
+ return failResult("artifact", `${path.basename(artifactPath)} state-check section is missing required command: ${pattern}`);
587
+ }
588
+ }
589
+ }
590
+
573
591
  return passResult(
574
592
  "artifact",
575
593
  `${path.basename(artifactPath)} passed (${requiredSections.length} sections)`
@@ -1251,16 +1269,8 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }: any): any {
1251
1269
 
1252
1270
  function reviewWorktreeForTask(repositoryRoot: string, branch: string): string | null {
1253
1271
  if (!branch) return null;
1254
- const expectedRef = `refs/heads/${branch}`;
1255
- const records = execFileSync("git", ["-C", repositoryRoot, "worktree", "list", "--porcelain"], {
1256
- encoding: "utf8",
1257
- stdio: ["pipe", "pipe", "pipe"]
1258
- }).trim().split(/\r?\n\r?\n/);
1259
- for (const record of records) {
1260
- const worktree = /^worktree (.+)$/m.exec(record)?.[1];
1261
- const branchRef = /^branch (.+)$/m.exec(record)?.[1];
1262
- if (worktree && branchRef === expectedRef) return worktree;
1263
- }
1272
+ const worktree = resolveBranchWorktree(repositoryRoot, branch);
1273
+ if (worktree) return worktree;
1264
1274
  throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
1265
1275
  }
1266
1276
 
@@ -7,8 +7,6 @@ import { parseArtifactName } from './artifact-name.ts';
7
7
  import type { ArtifactFamily } from './artifact-lifecycle.ts';
8
8
  import { resolveTaskRef } from './resolve-ref.ts';
9
9
  import type { TaskWorkspaceState } from './resolve-ref.ts';
10
- import { allowsManualOverride } from './guard-override.ts';
11
- import type { ManualOverrideCapability } from './guard-override.ts';
12
10
 
13
11
  type VerificationEvent =
14
12
  | 'analyze.awaiting-input' | 'analyze.completed'
@@ -50,7 +48,6 @@ type TaskVerificationResult = {
50
48
  type VerificationOptions = {
51
49
  repoRoot?: string;
52
50
  engine?: (input: Parameters<typeof verifyInProcess>[0]) => Record<string, unknown> | Promise<Record<string, unknown>>;
53
- manualOverride?: ManualOverrideCapability;
54
51
  };
55
52
 
56
53
  const gate = (skill: string, expectedState: VerificationSpec['expectedState'], artifactFamily?: ArtifactFamily): VerificationSpec => ({
@@ -98,7 +95,7 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
98
95
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
99
96
  if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
100
97
  const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode } as const;
101
- if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
98
+ if (resolved.state !== spec.expectedState) {
102
99
  return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
103
100
  }
104
101
  if (spec.artifactFamily) {
@@ -116,22 +113,11 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
116
113
  if (spec.mode === 'gate') {
117
114
  const payload = await engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot });
118
115
  const status = payload.gate as 'pass' | 'fail' | 'blocked';
119
- if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
120
- invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
121
- return {
122
- status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
123
- ...identity, artifact: request.artifact ?? null, invocations, error: null
124
- };
125
- }
126
116
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
127
117
  } else {
128
118
  for (const check of spec.checks ?? []) {
129
119
  const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
130
120
  const status = (payload.effectiveStatus ?? payload.status) as 'pass' | 'fail' | 'blocked';
131
- if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
132
- invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
133
- break;
134
- }
135
121
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
136
122
  if (status !== 'pass') break;
137
123
  }
@@ -12,7 +12,6 @@ import {
12
12
  import type { WorkflowWarning } from './workflow-warnings.ts';
13
13
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
14
14
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
15
- import { allowsManualOverride } from './guard-override.ts';
16
15
  import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
17
16
 
18
17
  type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
@@ -77,8 +76,7 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
77
76
  function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
78
77
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
79
78
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
80
- const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
81
- if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
79
+ if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
82
80
  let content: string;
83
81
  let rows: WorkflowWarning[];
84
82
  try {
@@ -122,7 +120,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
122
120
  }
123
121
 
124
122
  const writeResult = writeTask({
125
- taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
123
+ taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
126
124
  }, {
127
125
  ...options,
128
126
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
package/lib/task/write.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import crypto from 'node:crypto';
3
4
 
4
5
  import { isValidAgentInfraVersion, VERSION } from '../version.ts';
5
6
  import {
@@ -12,8 +13,6 @@ import type {
12
13
  ResolveTaskRefErrorCode,
13
14
  TaskWorkspaceState
14
15
  } from './resolve-ref.ts';
15
- import { allowsManualOverride } from './guard-override.ts';
16
- import type { ManualOverrideCapability } from './guard-override.ts';
17
16
  import { mutateTableRow, upsertSection } from './sections.ts';
18
17
  import { validateCurrentTaskContract } from './current-contract.ts';
19
18
  import { invalidationBlocks, parseInvalidationDocument } from './invalidation.ts';
@@ -163,7 +162,6 @@ type TaskWriteOptions = {
163
162
  metadataProvider?: () => TaskWriteMetadata;
164
163
  randomSuffix?: () => string;
165
164
  fileSystem?: Partial<TaskFileSystem>;
166
- manualOverride?: ManualOverrideCapability;
167
165
  invalidationContext?: 'standard' | 'source-completion' | 'reconcile';
168
166
  };
169
167
 
@@ -226,7 +224,7 @@ function errorDetails(error: unknown, fallback: TaskWriteErrorCode): TaskWriteEr
226
224
  };
227
225
  }
228
226
 
229
- function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
227
+ function writeTaskCore(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
230
228
  const resolved = options.taskLocation
231
229
  ? {
232
230
  ok: true as const,
@@ -250,7 +248,7 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
250
248
  taskMdPath: resolved.taskMdPath,
251
249
  actualState: resolved.state
252
250
  };
253
- if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
251
+ if (resolved.state !== request.expectedState) {
254
252
  return failure(
255
253
  request,
256
254
  identity,
@@ -482,6 +480,119 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
482
480
  return { ...successBase, status: 'applied', changed: true };
483
481
  }
484
482
 
483
+ function archiveContentsHash(localDir: string): string {
484
+ const files: string[] = [];
485
+ const walk = (dir: string) => {
486
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
487
+ if (entry.name.includes('\n') || entry.name.includes('\r') || entry.name === 'manifest.md') {
488
+ throw new Error(`Archive source path is not valid: ${path.join(dir, entry.name)}`);
489
+ }
490
+ const target = path.join(dir, entry.name);
491
+ const stat = fs.lstatSync(target);
492
+ if (stat.isSymbolicLink()) throw new Error(`Archive source cannot contain symbolic links: ${target}`);
493
+ if (stat.isDirectory()) walk(target);
494
+ else if (stat.isFile()) {
495
+ if (entry.name !== 'contents.sha256') files.push(target);
496
+ }
497
+ else throw new Error(`Archive source cannot contain special files: ${target}`);
498
+ }
499
+ };
500
+ walk(localDir);
501
+ const rows = files
502
+ .map((file) => ({ file, relative: path.relative(localDir, file).split(path.sep).join('/') }))
503
+ .sort((left, right) => Buffer.compare(Buffer.from(left.relative), Buffer.from(right.relative)))
504
+ .map(({ file, relative }) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${relative}`);
505
+ return `${rows.join('\n')}${rows.length ? '\n' : ''}`;
506
+ }
507
+
508
+ function writeArchiveContentsHash(localDir: string): void {
509
+ const contents = archiveContentsHash(localDir);
510
+ const hashPath = path.join(localDir, 'contents.sha256');
511
+ const tempPath = path.join(localDir, `.contents.sha256.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
512
+ try {
513
+ fs.writeFileSync(tempPath, contents, { flag: 'wx' });
514
+ fs.renameSync(tempPath, hashPath);
515
+ } catch (error) {
516
+ try {
517
+ fs.unlinkSync(tempPath);
518
+ } catch (cleanupError) {
519
+ if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
520
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
521
+ }
522
+ }
523
+ throw error;
524
+ }
525
+ }
526
+
527
+ function restoreArchivedTask(taskMdPath: string, contents: Buffer, mode: number): void {
528
+ const tempPath = `${taskMdPath}.${process.pid}.${Math.random().toString(36).slice(2)}.rollback`;
529
+ fs.writeFileSync(tempPath, contents, { flag: 'wx', mode });
530
+ try {
531
+ fs.renameSync(tempPath, taskMdPath);
532
+ } catch (error) {
533
+ try {
534
+ fs.unlinkSync(tempPath);
535
+ } catch (cleanupError) {
536
+ if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
537
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
538
+ }
539
+ }
540
+ throw error;
541
+ }
542
+ }
543
+
544
+ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
545
+ const resolved = options.taskLocation
546
+ ? {
547
+ ok: true as const,
548
+ repoRoot: options.taskLocation.repoRoot,
549
+ taskId: options.taskLocation.taskId,
550
+ taskDir: path.dirname(options.taskLocation.taskMdPath),
551
+ taskMdPath: options.taskLocation.taskMdPath,
552
+ state: options.taskLocation.state
553
+ }
554
+ : resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
555
+ if (!resolved.ok) {
556
+ return failure(request, { taskId: resolved.taskId, taskMdPath: null, actualState: null }, resolved.code, resolved.message);
557
+ }
558
+ if (resolved.state !== 'archive' || request.expectedState !== 'archive' || request.dryRun) {
559
+ return writeTaskCore(request, { ...options, taskLocation: {
560
+ repoRoot: resolved.repoRoot,
561
+ taskId: resolved.taskId,
562
+ taskMdPath: resolved.taskMdPath,
563
+ state: resolved.state
564
+ } });
565
+ }
566
+
567
+ const identity = { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, actualState: resolved.state };
568
+ try {
569
+ const originalTask = fs.readFileSync(resolved.taskMdPath);
570
+ const originalMode = fs.statSync(resolved.taskMdPath).mode;
571
+ archiveContentsHash(resolved.taskDir);
572
+ const result = writeTaskCore(request, { ...options, taskLocation: {
573
+ repoRoot: resolved.repoRoot,
574
+ taskId: resolved.taskId,
575
+ taskMdPath: resolved.taskMdPath,
576
+ state: resolved.state
577
+ } });
578
+ if (result.status === 'applied') {
579
+ try {
580
+ writeArchiveContentsHash(resolved.taskDir);
581
+ } catch (error) {
582
+ try {
583
+ restoreArchivedTask(resolved.taskMdPath, originalTask, originalMode);
584
+ } catch (rollbackError) {
585
+ throw new Error(`Archive checksum update failed: ${error instanceof Error ? error.message : String(error)}; task rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
586
+ }
587
+ throw error;
588
+ }
589
+ }
590
+ return result;
591
+ } catch (error) {
592
+ return failure(request, identity, 'TASK_READ_FAILED', error instanceof Error ? error.message : String(error));
593
+ }
594
+ }
595
+
485
596
  export { writeTask, captureTaskWriteMetadata, canonicalTimestamp };
486
597
  export type {
487
598
  FrontmatterMutation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,8 +21,7 @@ Record a reproducible command or entry point, the exact file/line/object/version
21
21
  - Preserve artifact SHA-256, semantic digest, review head, receipt, request-id, snapshot tree, and platform marker exactly.
22
22
  - The Issue continues to mirror the complete report; the report must not depend on logs accessible only on the local machine.
23
23
  - Manual validation keeps its stricter basename-only and sanitized-result rules; this shared rule does not expand public fields.
24
- - A `$ ` command line satisfies the structural gate but does not require complete successful output.
25
24
 
26
25
  ## Evidence Pairing
27
26
 
28
- Pair every verification claim with a `$ ` command and a proportionate result summary. Include a decisive raw excerpt only for failure, blocking, or dispute conclusions. When a command contains sensitive arguments, use its allowed public name or a redacted command.
27
+ Pair every verification claim with an executable command and a proportionate result summary. Include a decisive raw excerpt only for failure, blocking, or dispute conclusions. When a command contains sensitive arguments, use its allowed public name or a redacted command.
@@ -21,8 +21,7 @@
21
21
  - artifact SHA-256、semantic digest、review head、receipt、request-id、快照树和平台 marker 必须精确保留。
22
22
  - Issue 继续镜像报告全文;报告不得依赖只有本机可访问的日志。
23
23
  - 人工验证继续使用 basename-only、sanitized result 等更严格规则;共享规则不扩大可公开字段。
24
- - `$ ` 命令行标记满足结构门禁,但不要求附带完整成功输出。
25
24
 
26
25
  ## 证据配对
27
26
 
28
- 报告中的每个验证断言都应配一个 `$ ` 命令和相称结果摘要;只有失败、阻塞或争议结论才附决定性原文摘录。命令含敏感参数时使用允许公开的命令名或脱敏命令。
27
+ 报告中的每个验证断言都应配一个可执行命令和相称结果摘要;只有失败、阻塞或争议结论才附决定性原文摘录。命令含敏感参数时使用允许公开的命令名或脱敏命令。