@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -11,7 +11,10 @@ export function parseLinuxProcessStat(raw) {
11
11
  if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
12
12
  return null;
13
13
  }
14
- return { state, startTime: startTime };
14
+ const numericStartTime = Number(startTime);
15
+ if (!Number.isSafeInteger(numericStartTime))
16
+ return null;
17
+ return { state, startTime: numericStartTime };
15
18
  }
16
19
  export function buildProcessStartTimeQuery(pid, platform) {
17
20
  if (platform === 'darwin') {
@@ -47,6 +50,17 @@ export function isProcessAlive(pid, platform = process.platform) {
47
50
  return error.code === 'EPERM';
48
51
  }
49
52
  }
53
+ function parseDarwinStartTime(output) {
54
+ const match = output.trim().match(/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2}) (\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/);
55
+ if (!match)
56
+ return null;
57
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
58
+ const month = months.indexOf(match[1]);
59
+ if (month < 0)
60
+ return null;
61
+ const timestamp = Date.UTC(Number(match[6]), month, Number(match[2]), Number(match[3]), Number(match[4]), Number(match[5]));
62
+ return Number.isNaN(timestamp) ? null : timestamp;
63
+ }
50
64
  export function getProcessStartTime(pid, platform = process.platform) {
51
65
  if (!Number.isInteger(pid) || pid <= 0)
52
66
  return null;
@@ -65,43 +79,114 @@ export function getProcessStartTime(pid, platform = process.platform) {
65
79
  if (query === null)
66
80
  return null;
67
81
  try {
68
- const output = execFileSync(query.command, query.args, { encoding: 'utf8' }).trim();
82
+ const output = execFileSync(query.command, query.args, {
83
+ encoding: 'utf8',
84
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
85
+ }).trim();
69
86
  if (output.length === 0)
70
87
  return null;
71
88
  if (platform === 'win32') {
72
89
  const timestamp = new Date(output);
73
- return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
90
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
74
91
  }
75
- return output;
92
+ if (platform === 'darwin')
93
+ return parseDarwinStartTime(output);
94
+ return null;
76
95
  }
77
96
  catch {
78
97
  return null;
79
98
  }
80
99
  }
100
+ export function getProcessIdentityState(identity, platform = process.platform) {
101
+ if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
102
+ return 'dead';
103
+ }
104
+ if (platform === 'linux') {
105
+ try {
106
+ const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
107
+ if (stat === null)
108
+ return 'unknown';
109
+ if (stat.state === 'Z')
110
+ return 'dead';
111
+ return stat.startTime === identity.startTime ? 'alive' : 'dead';
112
+ }
113
+ catch (error) {
114
+ return error.code === 'ENOENT' ? 'dead' : 'unknown';
115
+ }
116
+ }
117
+ try {
118
+ process.kill(identity.pid, 0);
119
+ }
120
+ catch (error) {
121
+ const code = error.code;
122
+ if (code === 'ESRCH')
123
+ return 'dead';
124
+ if (code !== 'EPERM')
125
+ return 'unknown';
126
+ }
127
+ const query = buildProcessStartTimeQuery(identity.pid, platform);
128
+ if (query === null)
129
+ return 'unknown';
130
+ try {
131
+ const output = execFileSync(query.command, query.args, {
132
+ encoding: 'utf8',
133
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
134
+ }).trim();
135
+ if (output.length === 0)
136
+ return 'unknown';
137
+ const startTime = platform === 'win32'
138
+ ? new Date(output).getTime()
139
+ : platform === 'darwin'
140
+ ? parseDarwinStartTime(output)
141
+ : null;
142
+ if (startTime === null || Number.isNaN(startTime))
143
+ return 'unknown';
144
+ return startTime === identity.startTime ? 'alive' : 'dead';
145
+ }
146
+ catch {
147
+ return 'unknown';
148
+ }
149
+ }
81
150
  function parsePidRecord(raw) {
82
151
  const trimmed = raw.trim();
83
152
  if (/^[1-9]\d*$/.test(trimmed)) {
84
153
  const pid = Number(trimmed);
85
- return Number.isSafeInteger(pid) ? { kind: 'legacy', pid } : { kind: 'invalid' };
154
+ return Number.isSafeInteger(pid)
155
+ ? { kind: 'legacy-pid-only', legacy: { pid, raw, format: 'pid-only' } }
156
+ : { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'pid is not a safe integer' };
86
157
  }
87
158
  try {
88
159
  const value = JSON.parse(trimmed);
160
+ const pidValue = value?.pid;
161
+ const startTimeValue = value?.startTime;
89
162
  if (value !== null &&
90
- value.version === 1 &&
91
- Number.isSafeInteger(value.pid) &&
92
- (value.pid ?? 0) > 0 &&
93
- typeof value.startTime === 'string' &&
94
- value.startTime.length > 0) {
163
+ value.version === 2 &&
164
+ typeof pidValue === 'number' &&
165
+ Number.isSafeInteger(pidValue) &&
166
+ pidValue > 0 &&
167
+ typeof startTimeValue === 'number' &&
168
+ Number.isSafeInteger(startTimeValue) &&
169
+ startTimeValue >= 0) {
170
+ const startTime = startTimeValue;
95
171
  return {
96
172
  kind: 'record',
97
- record: { version: 1, pid: value.pid, startTime: value.startTime }
173
+ record: { version: 2, pid: pidValue, startTime }
98
174
  };
99
175
  }
176
+ if (value !== null &&
177
+ value.version === 1 &&
178
+ typeof pidValue === 'number' &&
179
+ Number.isSafeInteger(pidValue) &&
180
+ pidValue > 0 &&
181
+ typeof value.startTime === 'string' &&
182
+ value.startTime.length > 0) {
183
+ return { kind: 'legacy-json', legacy: { pid: pidValue, raw, format: 'json-v1' } };
184
+ }
185
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'unsupported PID record schema' };
100
186
  }
101
187
  catch {
102
- // Invalid JSON is an invalid PID record.
188
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'invalid PID record JSON' };
103
189
  }
104
- return { kind: 'invalid' };
105
190
  }
106
191
  export function readProcessState(pidFile) {
107
192
  let snapshot;
@@ -111,13 +196,15 @@ export function readProcessState(pidFile) {
111
196
  catch (error) {
112
197
  if (error.code === 'ENOENT')
113
198
  return { kind: 'missing', snapshot: null };
114
- return { kind: 'invalid', snapshot: '' };
199
+ return { kind: 'legacy-unknown', snapshot: '', legacy: { raw: '', format: 'malformed' }, reason: 'PID record cannot be read' };
115
200
  }
116
201
  const parsed = parsePidRecord(snapshot);
117
- if (parsed.kind === 'invalid')
118
- return { kind: 'invalid', snapshot };
119
- if (parsed.kind === 'legacy')
120
- return { kind: 'legacy', snapshot, pid: parsed.pid };
202
+ if (parsed.kind === 'legacy-json')
203
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
204
+ if (parsed.kind === 'legacy-pid-only')
205
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
206
+ if (parsed.kind === 'legacy-unknown')
207
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy, reason: parsed.reason };
121
208
  const currentStartTime = getProcessStartTime(parsed.record.pid);
122
209
  if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
123
210
  return { kind: 'stale', snapshot, record: parsed.record };
@@ -162,7 +249,7 @@ export function removePidRecordIfMatches(pidFile, expected) {
162
249
  return false;
163
250
  return removePidFileIfMatches(pidFile, state.snapshot);
164
251
  }
165
- export function processIdentityMatches(record) {
166
- return getProcessStartTime(record.pid) === record.startTime;
252
+ export function processIdentityMatches(identity) {
253
+ return getProcessIdentityState(identity) === 'alive';
167
254
  }
168
255
  //# sourceMappingURL=process-state.js.map
@@ -5,6 +5,7 @@ import { locateActivityLog, appendActivityEntry } from './activity-log.js';
5
5
  import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts, parseArtifactName } from './artifact-lifecycle.js';
6
6
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
7
7
  import { captureTaskWriteMetadata, writeTask } from './write.js';
8
+ import { allowsManualOverride } from './guard-override.js';
8
9
  const HEAD_RE = /^[0-9a-f]{40}$/;
9
10
  const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
10
11
  const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
@@ -292,9 +293,11 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
292
293
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
293
294
  if (!resolved.ok)
294
295
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
295
- if (resolved.state !== 'active') {
296
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
296
297
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
297
298
  }
299
+ if (options.lockAlreadyHeld)
300
+ return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
298
301
  try {
299
302
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity:${intent.kind}`, () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }), options.lockOptions);
300
303
  }
@@ -5,6 +5,7 @@ import { locateActivityLog } from './activity-log.js';
5
5
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.js';
6
6
  import { parseVerdict } from './review-artifacts.js';
7
7
  import { extractSection, findSectionHeading } from './sections.js';
8
+ import { receiptForOutput, sha256File } from './artifact-receipts.js';
8
9
  const artifactFamilyCatalog = [
9
10
  { family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
10
11
  { family: 'review-analysis', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['需求分析审查', 'Analysis Review'] },
@@ -151,11 +152,7 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
151
152
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
152
153
  return null;
153
154
  }
154
- const lines = content.split(/\r?\n/);
155
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
156
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
157
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
158
- const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
155
+ const parsed = parseReviewedInputReference(content, expectedFamily);
159
156
  if (!parsed) {
160
157
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
161
158
  return null;
@@ -165,9 +162,17 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
165
162
  const stat = fs.lstatSync(abs);
166
163
  if (!stat.isFile() || stat.isSymbolicLink())
167
164
  throw new Error('referenced input is not a regular file');
168
- if (stat.mtimeMs > review.mtimeMs) {
169
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name} is newer than ${review.name}`));
170
- return null;
165
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
166
+ const receipt = receiptForOutput(taskContent, review.name);
167
+ if (!receipt)
168
+ throw new Error(`receipt for ${review.name} is missing`);
169
+ const expectedEvent = reviewEventName(review.family);
170
+ if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
171
+ throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
172
+ }
173
+ const actualSha256 = sha256File(abs);
174
+ if (actualSha256 !== receipt.inputSha256) {
175
+ throw new Error(`${parsed.name} content digest does not match receipt`);
171
176
  }
172
177
  return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
173
178
  }
@@ -176,6 +181,64 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
176
181
  return null;
177
182
  }
178
183
  }
184
+ function parseReviewedInputReference(content, expectedFamily) {
185
+ const lines = content.split(/\r?\n/);
186
+ const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
187
+ const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
188
+ const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
189
+ return candidates
190
+ .map((name) => parseArtifactName(name))
191
+ .find((item) => item?.family === expectedFamily) ?? null;
192
+ }
193
+ function parseCodePlanInputReference(content) {
194
+ const lines = content.split(/\r?\n/);
195
+ const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
196
+ if (sectionStart < 0)
197
+ return null;
198
+ const bodyLines = lines.slice(sectionStart + 1);
199
+ const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
200
+ const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
201
+ const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
202
+ if (!match)
203
+ return null;
204
+ const parsed = parseArtifactName(match[1]);
205
+ return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
206
+ }
207
+ function reviewEventName(family) {
208
+ if (family === 'review-analysis')
209
+ return 'review-analysis.completed';
210
+ if (family === 'review-plan')
211
+ return 'review-plan.completed';
212
+ if (family === 'review-code')
213
+ return 'review-code.completed';
214
+ throw new Error(`unsupported review family '${family}'`);
215
+ }
216
+ function resolveCodeInputReceipt(taskDir, code) {
217
+ let taskContent;
218
+ try {
219
+ taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ let receipt;
225
+ try {
226
+ receipt = receiptForOutput(taskContent, code.name);
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ if (!receipt || receipt.event !== 'code.completed')
232
+ return null;
233
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
234
+ if (plan.status !== 'ready')
235
+ return null;
236
+ const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
237
+ return input ? { input, inputSha256: receipt.inputSha256 } : null;
238
+ }
239
+ function resolveCodePlanInput(taskDir, code) {
240
+ return resolveCodeInputReceipt(taskDir, code);
241
+ }
179
242
  function assertWritableInventory(inventory) {
180
243
  if (inventory.status === 'failed')
181
244
  return inventory.error;
@@ -248,10 +311,18 @@ function resolveCodeContext(inventory, options) {
248
311
  }
249
312
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, 'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
250
313
  }
314
+ const codePlanInput = resolveCodePlanInput(inventory.taskDir, latestCode);
315
+ if (!codePlanInput) {
316
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
317
+ }
318
+ if (reviewPlan.latest && !reviewPlan.reviewedInput) {
319
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
320
+ }
251
321
  if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
252
322
  const verdict = parseVerdict(reviewPlan.latest.path);
253
- if (verdict.ok && verdict.verdict === 'Approved' && reviewPlan.latest.mtimeMs > latestCode.mtimeMs) {
254
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} is approved and newer than the latest code artifact. Entering replan-driven init.`);
323
+ const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
324
+ if (verdict.ok && verdict.verdict === 'Approved' && (codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256)) {
325
+ return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
255
326
  }
256
327
  }
257
328
  if (reviewMax < codeMax) {
@@ -377,5 +448,5 @@ function buildArtifactLinkSection(content, artifact) {
377
448
  body = lines.join('\n').replace(/^\n+|\n+$/g, '');
378
449
  return { aliases, heading, body };
379
450
  }
380
- export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
451
+ export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
381
452
  //# sourceMappingURL=artifact-lifecycle.js.map
@@ -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);