@fitlab-ai/agent-infra 0.9.6 → 0.9.7

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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { extractSection, findSectionHeading, parseTable } from './sections.js';
4
+ const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
5
+ const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
6
+ const RECEIPT_EVENTS = new Set([
7
+ 'review-analysis.completed',
8
+ 'review-plan.completed',
9
+ 'review-code.completed',
10
+ 'code.completed'
11
+ ]);
12
+ const SHA256_RE = /^[a-f0-9]{64}$/;
13
+ const ARTIFACT_NAME_RE = /^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r([2-9]|[1-9]\d+))?\.md$/;
14
+ const RECEIPT_SHAPES = {
15
+ 'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
16
+ 'review-plan.completed': { output: 'review-plan', input: 'plan' },
17
+ 'review-code.completed': { output: 'review-code', input: 'code' },
18
+ 'code.completed': { output: 'code', input: 'plan' }
19
+ };
20
+ class ArtifactReceiptError extends Error {
21
+ code = 'ARTIFACT_RECEIPT_INVALID';
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'ArtifactReceiptError';
25
+ }
26
+ }
27
+ function sha256Bytes(value) {
28
+ return createHash('sha256').update(value).digest('hex');
29
+ }
30
+ function sha256File(filePath) {
31
+ return sha256Bytes(fs.readFileSync(filePath));
32
+ }
33
+ function parseReceiptArtifactName(name) {
34
+ const match = ARTIFACT_NAME_RE.exec(name);
35
+ if (!match)
36
+ return null;
37
+ const roundText = match[2];
38
+ if (roundText !== undefined) {
39
+ const round = Number(roundText);
40
+ if (!Number.isSafeInteger(round) || String(round) !== roundText)
41
+ return null;
42
+ }
43
+ return { family: match[1] };
44
+ }
45
+ function validateReceiptShape(event, output, input) {
46
+ const outputIdentity = parseReceiptArtifactName(output);
47
+ const inputIdentity = parseReceiptArtifactName(input);
48
+ if (!outputIdentity || !inputIdentity)
49
+ throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
50
+ const shape = RECEIPT_SHAPES[event];
51
+ if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
52
+ throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
53
+ }
54
+ }
55
+ function parseArtifactReceipts(content) {
56
+ const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
57
+ if (!section)
58
+ return { present: false, rows: [] };
59
+ let table;
60
+ try {
61
+ table = parseTable(content, {
62
+ sectionAliases: [...RECEIPT_SECTION_ALIASES],
63
+ columns: [...RECEIPT_COLUMNS],
64
+ keyColumn: 'output'
65
+ });
66
+ }
67
+ catch (error) {
68
+ throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
69
+ }
70
+ if (!table)
71
+ throw new ArtifactReceiptError('receipt section has no receipt table');
72
+ const rows = table.rows.map((row) => {
73
+ const event = row.values.event ?? '';
74
+ const output = row.values.output ?? '';
75
+ const input = row.values.input ?? '';
76
+ const inputSha256 = row.values.input_sha256 ?? '';
77
+ const completedAt = row.values.completed_at ?? '';
78
+ if (!RECEIPT_EVENTS.has(event))
79
+ throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
80
+ if (!output || !input)
81
+ throw new ArtifactReceiptError('receipt output and input are required');
82
+ validateReceiptShape(event, output, input);
83
+ if (!SHA256_RE.test(inputSha256))
84
+ throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
85
+ if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
86
+ throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
87
+ }
88
+ return {
89
+ event: event,
90
+ output,
91
+ input,
92
+ inputSha256,
93
+ completedAt
94
+ };
95
+ });
96
+ return { present: true, rows };
97
+ }
98
+ function receiptForOutput(content, output) {
99
+ const parsed = parseArtifactReceipts(content);
100
+ return parsed.rows.find((row) => row.output === output) ?? null;
101
+ }
102
+ function upsertArtifactReceipt(content, receipt) {
103
+ if (!RECEIPT_EVENTS.has(receipt.event))
104
+ throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
105
+ if (!receipt.output || !receipt.input)
106
+ throw new ArtifactReceiptError('receipt output and input are required');
107
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
108
+ if (!SHA256_RE.test(receipt.inputSha256))
109
+ throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
110
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
111
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
112
+ }
113
+ const existing = parseArtifactReceipts(content).rows;
114
+ const previous = existing.find((row) => row.output === receipt.output);
115
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
116
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
117
+ }
118
+ const rows = previous ? existing : [...existing, receipt];
119
+ const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
120
+ const body = [
121
+ `| ${RECEIPT_COLUMNS.join(' | ')} |`,
122
+ `| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
123
+ ...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
124
+ ].join('\n');
125
+ return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
126
+ }
127
+ export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
128
+ //# sourceMappingURL=artifact-receipts.js.map
@@ -1,6 +1,11 @@
1
+ import fs from 'node:fs';
1
2
  import { preflightCodexLifecycleEvidence, resolveCodexSpawnedChild, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
2
3
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
3
- import { activateMatchingOrchestrationDelegation, pauseMatchingOrchestrationDelegation, prepareOrchestrationDelegation, reconcileMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence } from './orchestration.js';
4
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
5
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
6
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
7
+ import { resolveTaskRef } from './resolve-ref.js';
8
+ import { activateMatchingOrchestrationDelegation, hasActivatableOrchestrationDelegation, hasSealableOrchestrationDelegation, pauseMatchingOrchestrationDelegation, prepareOrchestrationDelegation, reconcileMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence } from './orchestration.js';
4
9
  function coreOptions(options) {
5
10
  return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
6
11
  }
@@ -21,19 +26,61 @@ function requiredStore(options) {
21
26
  throw new Error('Codex lifecycle store is required for orchestration bridge events');
22
27
  return options.store;
23
28
  }
29
+ function controllerContext(taskId, repoRoot) {
30
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
31
+ return contextPath
32
+ ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
33
+ : null;
34
+ }
24
35
  async function prepareCodexOrchestrationDelegation(taskRef, input, options = {}) {
25
36
  if (input.client !== 'codex')
26
37
  return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
27
38
  try {
28
- await (options.preflight ?? preflightCodexLifecycleEvidence)(options.repoRoot ?? process.cwd());
39
+ const repoRoot = options.repoRoot ?? process.cwd();
40
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
41
+ if (!resolved.ok)
42
+ return bridgeFailure(resolved.code, resolved.message);
43
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
44
+ if (!input.capabilityToken) {
45
+ return bridgeFailure('ORCHESTRATION_CODEX_CAPABILITY_REQUIRED', 'Codex prepare requires a current-session capability token');
46
+ }
47
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
48
+ const controller = controllerContext(resolved.taskId, repoRoot);
49
+ const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
50
+ const consumed = capabilityStore.consume(input.capabilityToken, {
51
+ taskId: resolved.taskId,
52
+ hookDefinitionHash: preflight.hookDefinitionHash,
53
+ buildIdentity,
54
+ ...(controller ? { controller: {
55
+ instanceDigest: controller.controllerInstanceDigest,
56
+ controlGeneration: controller.controlGeneration
57
+ } } : {})
58
+ });
59
+ return prepareOrchestrationDelegation(taskRef, {
60
+ ...input,
61
+ lifecycleProvenance: {
62
+ ...consumed.buildIdentity,
63
+ hookDefinitionHash: preflight.hookDefinitionHash,
64
+ ...preflight.hookProvenance,
65
+ capabilitySessionId: consumed.sessionId,
66
+ capabilityTurnId: consumed.turnId,
67
+ capabilityToolUseId: consumed.toolUseId,
68
+ controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
69
+ controlGeneration: controller?.controlGeneration ?? null
70
+ }
71
+ }, coreOptions(options));
29
72
  }
30
73
  catch (error) {
31
- return bridgeFailure('ORCHESTRATION_CLIENT_PREFLIGHT_FAILED', error instanceof Error ? error.message : String(error));
74
+ return bridgeFailure(error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
75
+ ? error.name
76
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED', error instanceof Error ? error.message : String(error));
32
77
  }
33
- return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
34
78
  }
35
79
  async function activateCodexOrchestrationDelegation(childThreadId, options = {}) {
36
80
  try {
81
+ if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
82
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
83
+ }
37
84
  const store = requiredStore(options);
38
85
  const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
39
86
  store.apply(resolved.resolution.thread);
@@ -45,6 +92,23 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
45
92
  if (record.state.status !== 'start-ready' || !evidence) {
46
93
  return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
47
94
  }
95
+ const repoRoot = options.repoRoot ?? process.cwd();
96
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
97
+ sessionId: evidence.parentThreadId,
98
+ turnId: evidence.parentTurnId,
99
+ toolUseId: evidence.spawnToolUseId
100
+ });
101
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
102
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
103
+ const contextTaskId = contextPath
104
+ ? JSON.parse(fs.readFileSync(contextPath, 'utf8')).taskId
105
+ : null;
106
+ if (contextPath && typeof contextTaskId !== 'string') {
107
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
108
+ }
109
+ const controller = contextPath
110
+ ? verifyCodexSandboxControllerContext(contextPath, contextTaskId, { repoRoot })
111
+ : null;
48
112
  return activateMatchingOrchestrationDelegation('codex', {
49
113
  nativeAgent: evidence.nativeAgent,
50
114
  childId: evidence.childThreadId,
@@ -57,9 +121,17 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
57
121
  ? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
58
122
  : {}),
59
123
  hostEvidence: {
60
- kind: 'codex-lifecycle-v1',
124
+ kind: 'codex-lifecycle-v2',
61
125
  hookDefinitionHash: evidence.hookDefinitionHash,
62
- startRevision: record.revision
126
+ startRevision: record.revision,
127
+ ...buildIdentity,
128
+ ...preflight.hookProvenance,
129
+ capabilitySessionId: evidence.parentThreadId,
130
+ capabilityTurnId: evidence.parentTurnId,
131
+ spawnToolUseId: evidence.spawnToolUseId,
132
+ spawnObservedAt: record.spawnObservedAt ?? undefined,
133
+ controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
134
+ controlGeneration: controller?.controlGeneration ?? null
63
135
  }
64
136
  }, coreOptions(options));
65
137
  }
@@ -93,6 +165,9 @@ async function activateCodexSpawnDelegation(spawn, options = {}) {
93
165
  }
94
166
  async function sealCodexOrchestrationDelegation(childThreadId, options = {}) {
95
167
  try {
168
+ if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
169
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
170
+ }
96
171
  const store = requiredStore(options);
97
172
  const existing = store.read(childThreadId);
98
173
  if (!existing.consumer) {
@@ -168,7 +168,7 @@ function inplaceValidation(config, target, options) {
168
168
  const nonce = randomUUID();
169
169
  const issuedAt = Date.now();
170
170
  const lease = {
171
- version: 1, generation: manifest.generation, nonce,
171
+ version: 2, generation: manifest.generation, nonce,
172
172
  owner: { pid: process.pid, startTime }, issuedAt,
173
173
  expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
174
174
  taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
@@ -181,6 +181,13 @@ function assertRealDirectory(directory, code) {
181
181
  if (!stat.isDirectory() || stat.isSymbolicLink())
182
182
  throw new Error(code);
183
183
  }
184
+ function ensureRealDirectory(directory, code) {
185
+ if (fs.existsSync(directory)) {
186
+ assertRealDirectory(directory, code);
187
+ return;
188
+ }
189
+ fs.mkdirSync(directory, { mode: 0o700 });
190
+ }
184
191
  function withCreateLock(repoRoot, workspaceRoot, operation) {
185
192
  const lockRoot = path.join(workspaceRoot, '.task-create.lock');
186
193
  fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
@@ -254,8 +261,9 @@ function createLocalTask(value, options) {
254
261
  const repoRoot = fs.realpathSync.native(options.repoRoot);
255
262
  const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
256
263
  const activeRoot = path.join(workspaceRoot, 'active');
257
- assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
258
- assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
264
+ ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
265
+ ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
266
+ ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
259
267
  const project = readProject(repoRoot);
260
268
  const canonical = canonicalTaskCreateCandidate(candidate);
261
269
  const candidateDigest = sha256(canonical);
@@ -11,6 +11,7 @@ function fail(code, message) {
11
11
  return { ok: false, code, message };
12
12
  }
13
13
  function prepareDelegation(input, options = {}) {
14
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
14
15
  return Object.freeze({
15
16
  ...input,
16
17
  id: (options.id ?? randomUUID)(),
@@ -27,17 +28,55 @@ function prepareDelegation(input, options = {}) {
27
28
  afterFingerprint: null,
28
29
  changedPaths: Object.freeze([]),
29
30
  createdAt: (options.now ?? (() => new Date().toISOString()))(),
31
+ preparedMonotonicMs: monotonic,
32
+ spawnDispatchMonotonicMs: null,
33
+ activationDeadlineMonotonicMs: null,
34
+ spawnDispatchedAt: null,
35
+ activationDeadlineAt: null,
36
+ startEvidenceMonotonicMs: null,
37
+ activatedMonotonicMs: null,
30
38
  activatedAt: null,
31
39
  sealedAt: null,
32
40
  consumedAt: null
33
41
  });
34
42
  }
43
+ function dispatchDelegation(receipt, options = {}) {
44
+ if (receipt.status !== 'prepared') {
45
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
46
+ }
47
+ if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
48
+ return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
49
+ }
50
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
51
+ const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
52
+ const window = options.activationWindowMs ?? 15_000;
53
+ return { ok: true, receipt: Object.freeze({
54
+ ...receipt,
55
+ spawnDispatchMonotonicMs: monotonic,
56
+ activationDeadlineMonotonicMs: monotonic + window,
57
+ spawnDispatchedAt: dispatchedAt,
58
+ activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
59
+ }) };
60
+ }
35
61
  function activateDelegation(receipt, event, options = {}) {
36
62
  const managedRole = managedDelegationRole(event.nativeAgent);
37
63
  if (!managedRole)
38
64
  return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
39
65
  if (receipt.status !== 'prepared')
40
66
  return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
67
+ if (receipt.spawnDispatchMonotonicMs == null
68
+ || receipt.spawnDispatchedAt == null
69
+ || receipt.activationDeadlineAt == null
70
+ || receipt.activationDeadlineMonotonicMs == null
71
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
72
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt))) {
73
+ return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
74
+ }
75
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
76
+ const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
77
+ if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
78
+ return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
79
+ }
41
80
  if (managedRole !== receipt.role)
42
81
  return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
43
82
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
@@ -56,6 +95,9 @@ function activateDelegation(receipt, event, options = {}) {
56
95
  if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
57
96
  return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
58
97
  }
98
+ if (receipt.client === 'codex'
99
+ && event.hostEvidence?.kind !== 'codex-lifecycle-v2')
100
+ return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
59
101
  if (actualReasoningEffort !== receipt.requestedReasoningEffort
60
102
  && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
61
103
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
@@ -72,6 +114,32 @@ function activateDelegation(receipt, event, options = {}) {
72
114
  || !Number.isSafeInteger(event.hostEvidence.startRevision)
73
115
  || event.hostEvidence.startRevision < 1))
74
116
  return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
117
+ if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
118
+ const expected = receipt.lifecycleProvenance;
119
+ const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
120
+ const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
121
+ const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
122
+ if (!expected
123
+ || event.hostEvidence.protocolVersion !== expected.protocolVersion
124
+ || event.hostEvidence.packageVersion !== expected.packageVersion
125
+ || event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
126
+ || event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
127
+ || event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
128
+ || event.hostEvidence.hookSource !== expected.hookSource
129
+ || event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
130
+ || event.hostEvidence.hookSourceHash !== expected.hookSourceHash
131
+ || event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
132
+ || event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
133
+ || !event.hostEvidence.spawnToolUseId?.trim()
134
+ || event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
135
+ || !Number.isFinite(spawnObservedAt)
136
+ || spawnObservedAt < spawnDispatchedAt
137
+ || spawnObservedAt > activationDeadlineAt
138
+ || event.parentId !== expected.capabilitySessionId
139
+ || (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
140
+ || (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration)
141
+ return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
142
+ }
75
143
  return { ok: true, receipt: Object.freeze({
76
144
  ...receipt,
77
145
  status: 'activated',
@@ -88,9 +156,20 @@ function activateDelegation(receipt, event, options = {}) {
88
156
  consumer: null,
89
157
  consumedAt: null
90
158
  }) : receipt.hostEvidence ?? null,
91
- activatedAt: (options.now ?? (() => new Date().toISOString()))()
159
+ startEvidenceMonotonicMs: monotonic,
160
+ activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))(),
161
+ activatedAt: new Date(wallNow).toISOString()
92
162
  }) };
93
163
  }
164
+ function abortPreparedDelegation(receipt) {
165
+ if (receipt.status !== 'prepared') {
166
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
167
+ }
168
+ return {
169
+ ok: true,
170
+ receipt: Object.freeze({ ...receipt, status: 'aborted' })
171
+ };
172
+ }
94
173
  function completeDelegationStage(receipt, event) {
95
174
  if (receipt.status !== 'activated')
96
175
  return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
@@ -105,6 +184,9 @@ function completeDelegationStage(receipt, event) {
105
184
  function sealDelegation(receipt, event, options = {}) {
106
185
  if (receipt.status !== 'stage-completed')
107
186
  return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
187
+ if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
188
+ return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
189
+ }
108
190
  if (event.childId !== receipt.childId || event.exitCode !== 0)
109
191
  return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
110
192
  if (receipt.role === 'reviewer') {
@@ -149,5 +231,5 @@ function consumeDelegation(receipt, options = {}) {
149
231
  consumedAt: (options.now ?? (() => new Date().toISOString()))()
150
232
  }) };
151
233
  }
152
- export { activateDelegation, completeDelegationStage, consumeDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
234
+ export { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
153
235
  //# sourceMappingURL=delegation-receipts.js.map
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-log.js';
3
- import { buildArtifactLinkSection, artifactName, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
3
+ import { buildArtifactLinkSection, artifactName, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
4
+ import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
4
5
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
6
  import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
6
7
  import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
@@ -237,6 +238,92 @@ function openStartedIdentity(rows, family) {
237
238
  });
238
239
  return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true } : null;
239
240
  }
241
+ function reviewInputFamily(family) {
242
+ return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
243
+ }
244
+ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
245
+ if (family === 'code') {
246
+ let content;
247
+ try {
248
+ content = fs.readFileSync(artifact.path, 'utf8');
249
+ }
250
+ catch (error) {
251
+ return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
252
+ }
253
+ const input = parseCodePlanInputReference(content);
254
+ if (!input)
255
+ return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
256
+ const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
257
+ const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
258
+ if (!startedInput || !startedSha256)
259
+ return { ok: false, message: 'code.started plan input context is missing' };
260
+ if (input.name !== startedInput)
261
+ return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
262
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
263
+ if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
264
+ return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
265
+ }
266
+ try {
267
+ const inputSha256 = sha256File(plan.latest.path);
268
+ if (inputSha256 !== startedSha256)
269
+ return { ok: false, message: `code input ${input.name} changed after code.started` };
270
+ return {
271
+ ok: true,
272
+ receipt: {
273
+ event: 'code.completed', output: artifact.name, input: input.name,
274
+ inputSha256, completedAt
275
+ }
276
+ };
277
+ }
278
+ catch (error) {
279
+ return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
280
+ }
281
+ }
282
+ if (!family.startsWith('review-'))
283
+ return null;
284
+ const expectedFamily = reviewInputFamily(family);
285
+ let content;
286
+ try {
287
+ content = fs.readFileSync(artifact.path, 'utf8');
288
+ }
289
+ catch (error) {
290
+ return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
291
+ }
292
+ const input = parseReviewedInputReference(content, expectedFamily);
293
+ if (!input)
294
+ return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
295
+ const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
296
+ const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
297
+ if (!startedInput || !startedSha256)
298
+ return { ok: false, message: 'review started input context is missing' };
299
+ if (input.name !== startedInput)
300
+ return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
301
+ const current = inspectArtifactDirectory(taskDir, expectedFamily);
302
+ if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
303
+ return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
304
+ }
305
+ const event = family === 'review-analysis'
306
+ ? 'review-analysis.completed'
307
+ : family === 'review-plan' ? 'review-plan.completed' : 'review-code.completed';
308
+ try {
309
+ const stat = fs.lstatSync(current.latest.path);
310
+ if (!stat.isFile() || stat.isSymbolicLink())
311
+ throw new Error('review input is not a regular file');
312
+ const inputSha256 = sha256File(current.latest.path);
313
+ if (inputSha256 !== startedSha256)
314
+ return { ok: false, message: `review input ${input.name} changed after review.started` };
315
+ return {
316
+ ok: true,
317
+ receipt: {
318
+ event, output: artifact.name, input: input.name,
319
+ inputSha256, completedAt
320
+ }
321
+ };
322
+ }
323
+ catch (error) {
324
+ return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
325
+ }
326
+ }
240
327
  function applyTaskEventUnlocked(request, options = {}) {
241
328
  const invalid = validateTaskEventRequest(request);
242
329
  if (invalid)
@@ -363,19 +450,69 @@ function applyTaskEventUnlocked(request, options = {}) {
363
450
  catch (error) {
364
451
  return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
365
452
  }
453
+ let completionReceipt = null;
454
+ if (eventIdentity.phase === 'completed' && completedArtifact) {
455
+ const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
456
+ if (receipt && !receipt.ok) {
457
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
458
+ }
459
+ completionReceipt = receipt?.receipt ?? null;
460
+ }
366
461
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
367
462
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
368
463
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
369
464
  const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
465
+ let frontmatterRemove;
466
+ if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
467
+ const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
468
+ if (!planInput)
469
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
470
+ try {
471
+ frontmatterSet.code_input_artifact = planInput.name;
472
+ frontmatterSet.code_input_sha256 = sha256File(planInput.path);
473
+ }
474
+ catch (error) {
475
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
476
+ }
477
+ }
478
+ else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
479
+ frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
480
+ }
481
+ else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
482
+ const expectedFamily = reviewInputFamily(eventIdentity.family);
483
+ const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
484
+ if (!input)
485
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
486
+ try {
487
+ frontmatterSet.review_input_artifact = input.name;
488
+ frontmatterSet.review_input_sha256 = sha256File(input.path);
489
+ }
490
+ catch (error) {
491
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
492
+ }
493
+ }
494
+ else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
495
+ frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
496
+ }
370
497
  if (eventIdentity.phase === 'started' && normalized.implementationInput)
371
498
  frontmatterSet.last_reviewed_commit = '';
372
499
  const mutations = [
373
- { kind: 'frontmatter', set: frontmatterSet }
500
+ { kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
374
501
  ];
375
502
  if (completedArtifact) {
376
503
  const link = buildArtifactLinkSection(content, completedArtifact);
377
504
  mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
378
505
  }
506
+ if (completionReceipt) {
507
+ try {
508
+ const receiptSection = upsertArtifactReceipt(content, completionReceipt);
509
+ mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
510
+ }
511
+ catch (error) {
512
+ const message = error instanceof ArtifactReceiptError ? error.message : String(error);
513
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
514
+ }
515
+ }
379
516
  if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
380
517
  let implementationRows;
381
518
  try {
@@ -31,8 +31,20 @@ function validateLedgerRows(rows) {
31
31
  if (seen.has(row.id))
32
32
  return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
33
33
  seen.add(row.id);
34
- if (/^PRC-[1-9]\d*$/.test(row.id))
34
+ if (/^PRC-[1-9]\d*$/.test(row.id)) {
35
+ if (row.stage !== 'post-review-commit' ||
36
+ row.round !== '-' ||
37
+ row.severity !== '-' ||
38
+ row.status !== 'human-decided' ||
39
+ row.evidence === '' ||
40
+ /[\r\n]/.test(row.evidence)) {
41
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
42
+ }
35
43
  continue;
44
+ }
45
+ if (row.stage === 'post-review-commit') {
46
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
47
+ }
36
48
  if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id))
37
49
  return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
38
50
  if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage))