@fitlab-ai/agent-infra 0.9.4 → 0.9.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 (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -0,0 +1,300 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
6
+ import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
7
+ import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
8
+ const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
9
+ const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
10
+ function output(value) {
11
+ process.stdout.write(`${JSON.stringify(value)}\n`);
12
+ }
13
+ function failure(code, message) {
14
+ output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
15
+ process.exitCode = 1;
16
+ }
17
+ function outputBridgeResult(result) {
18
+ if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
19
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
20
+ return;
21
+ }
22
+ output(result);
23
+ if (result.status !== 'running')
24
+ process.exitCode = 1;
25
+ }
26
+ function parse(args) {
27
+ if (args[0] === '--help' || args[0] === '-h') {
28
+ process.stdout.write(USAGE);
29
+ return null;
30
+ }
31
+ const operation = args[0];
32
+ if (!operation) {
33
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
34
+ return null;
35
+ }
36
+ const allowed = {
37
+ 'hook-event': ['--event', '--bridge'],
38
+ 'resolve-start': ['--child-id'],
39
+ 'resolve-stop': ['--child-id'],
40
+ preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
41
+ consume: ['--child-id', '--consumer']
42
+ }[operation];
43
+ if (!allowed) {
44
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
45
+ return null;
46
+ }
47
+ const values = {};
48
+ for (let index = 1; index < args.length; index += 1) {
49
+ const flag = args[index];
50
+ if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
51
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
52
+ return null;
53
+ }
54
+ const value = args[++index];
55
+ if (!value || value.startsWith('--')) {
56
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
57
+ return null;
58
+ }
59
+ values[flag] = value;
60
+ }
61
+ return Object.freeze({ operation, values: Object.freeze(values) });
62
+ }
63
+ function cliVersion() {
64
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
65
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
66
+ if (result.status !== 0 || !version)
67
+ throw new Error('Codex CLI version is unavailable');
68
+ return version;
69
+ }
70
+ function hookDefinitionHash() {
71
+ const file = path.join(process.cwd(), '.codex', 'hooks.json');
72
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
73
+ }
74
+ async function readStdin() {
75
+ let input = '';
76
+ process.stdin.setEncoding('utf8');
77
+ for await (const chunk of process.stdin) {
78
+ input += chunk;
79
+ if (Buffer.byteLength(input) > 64 * 1024)
80
+ throw new Error('Codex lifecycle input exceeds 64 KiB');
81
+ }
82
+ return JSON.parse(input || '{}');
83
+ }
84
+ function recordFromPayload(phase, payload) {
85
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
86
+ throw new Error('Codex lifecycle hook payload must be an object');
87
+ }
88
+ const value = payload;
89
+ const text = (key) => typeof value[key] === 'string' ? value[key] : '';
90
+ if (!MANAGED_AGENT.test(text('nativeAgent')))
91
+ return null;
92
+ if (phase === 'pre-tool')
93
+ return {
94
+ type: 'hook-spawn',
95
+ sessionId: text('sessionId'),
96
+ turnId: text('turnId'),
97
+ toolUseId: text('toolUseId'),
98
+ nativeAgent: text('nativeAgent'),
99
+ ...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
100
+ ...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
101
+ hookDefinitionHash: text('hookDefinitionHash')
102
+ };
103
+ if (phase === 'subagent-start')
104
+ return {
105
+ type: 'hook-child',
106
+ sessionId: text('sessionId'),
107
+ turnId: text('turnId'),
108
+ childThreadId: text('childThreadId'),
109
+ nativeAgent: text('nativeAgent')
110
+ };
111
+ if (phase === 'subagent-stop')
112
+ return {
113
+ type: 'hook-stop',
114
+ sessionId: text('sessionId'),
115
+ turnId: text('turnId'),
116
+ childThreadId: text('childThreadId'),
117
+ nativeAgent: text('nativeAgent')
118
+ };
119
+ if (phase === 'post-tool')
120
+ return null;
121
+ throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
122
+ }
123
+ function payloadText(payload, key) {
124
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
125
+ && typeof payload[key] === 'string'
126
+ ? payload[key]
127
+ : '';
128
+ }
129
+ async function codexLifecycle(args = []) {
130
+ const parsed = parse(args);
131
+ if (!parsed || process.exitCode)
132
+ return;
133
+ try {
134
+ if (parsed.operation === 'preflight') {
135
+ const format = parsed.values['--format'] ?? 'json';
136
+ if (!['text', 'json'].includes(format))
137
+ throw new Error("format must be 'text' or 'json'");
138
+ const runtimeFields = [
139
+ parsed.values['--session-id'],
140
+ parsed.values['--turn-id'],
141
+ parsed.values['--tool-use-id']
142
+ ];
143
+ if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
144
+ throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
145
+ }
146
+ const runtimeIdentity = runtimeFields.every(Boolean) ? {
147
+ sessionId: runtimeFields[0],
148
+ turnId: runtimeFields[1],
149
+ toolUseId: runtimeFields[2]
150
+ } : undefined;
151
+ const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
152
+ if (format === 'text') {
153
+ process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
154
+ }
155
+ else
156
+ output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
157
+ return;
158
+ }
159
+ const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
160
+ if (parsed.operation === 'hook-event') {
161
+ const phase = parsed.values['--event'];
162
+ if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
163
+ throw new Error('hook-event requires a known --event');
164
+ }
165
+ if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
166
+ throw new Error("hook-event --bridge must be 'true'");
167
+ }
168
+ const payload = await readStdin();
169
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
170
+ const toolName = payloadText(payload, 'toolName');
171
+ if (toolName === 'collaborationspawn_agent') {
172
+ const nativeAgent = payloadText(payload, 'nativeAgent');
173
+ if (!MANAGED_AGENT.test(nativeAgent)) {
174
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
175
+ return;
176
+ }
177
+ const requestedModel = payloadText(payload, 'requestedModel');
178
+ const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
179
+ const bridged = await activateCodexSpawnDelegation({
180
+ sessionId: payloadText(payload, 'sessionId'),
181
+ turnId: payloadText(payload, 'turnId'),
182
+ toolUseId: payloadText(payload, 'toolUseId'),
183
+ transcriptPath: payloadText(payload, 'transcriptPath'),
184
+ nativeAgent,
185
+ taskName: payloadText(payload, 'taskName'),
186
+ ...(requestedModel ? { requestedModel } : {}),
187
+ ...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
188
+ }, { store });
189
+ outputBridgeResult(bridged);
190
+ return;
191
+ }
192
+ if (toolName === 'collaborationwait_agent') {
193
+ outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
194
+ return;
195
+ }
196
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
197
+ return;
198
+ }
199
+ const event = recordFromPayload(phase, payload);
200
+ if (!event) {
201
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
202
+ const value = payload;
203
+ const childThreadId = String(value.childThreadId ?? '');
204
+ const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
205
+ outputBridgeResult(reconciled);
206
+ return;
207
+ }
208
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
209
+ return;
210
+ }
211
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
212
+ try {
213
+ if (store.read(event.childThreadId).consumer) {
214
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
215
+ outputBridgeResult(bridged);
216
+ return;
217
+ }
218
+ }
219
+ catch {
220
+ // The normal apply path below owns missing or invalid evidence errors.
221
+ }
222
+ }
223
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
224
+ const resolved = await resolveCodexThread(event.childThreadId);
225
+ store.apply({
226
+ ...event,
227
+ parentThreadId: resolved.resolution.thread.parentThreadId
228
+ });
229
+ const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
230
+ store,
231
+ resolveThread: async () => resolved
232
+ });
233
+ outputBridgeResult(bridged);
234
+ return;
235
+ }
236
+ if (event.type === 'hook-child') {
237
+ const resolved = await resolveCodexThread(event.childThreadId);
238
+ const result = store.apply({
239
+ ...event,
240
+ parentThreadId: resolved.resolution.thread.parentThreadId
241
+ });
242
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
243
+ if (result.state.status === 'invalid')
244
+ process.exitCode = 1;
245
+ return;
246
+ }
247
+ const result = store.apply(event);
248
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
249
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
250
+ outputBridgeResult(bridged);
251
+ return;
252
+ }
253
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
254
+ if (result.state.status === 'invalid')
255
+ process.exitCode = 1;
256
+ return;
257
+ }
258
+ const childThreadId = parsed.values['--child-id'];
259
+ if (!childThreadId)
260
+ throw new Error(`operation '${parsed.operation}' requires --child-id`);
261
+ if (parsed.operation === 'resolve-start') {
262
+ if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
263
+ throw new Error('Codex lifecycle hook definition hash is stale');
264
+ }
265
+ const resolved = await resolveCodexThread(childThreadId);
266
+ let latest = store.apply(resolved.resolution.thread);
267
+ for (const reroute of resolved.reroutes)
268
+ latest = store.apply(reroute);
269
+ latest = store.apply(resolved.resolution.settings);
270
+ output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
271
+ if (latest.state.status !== 'start-ready')
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ if (parsed.operation === 'resolve-stop') {
276
+ const terminal = await resolveCodexTerminal(childThreadId);
277
+ const latest = store.apply(terminal);
278
+ if (latest.state.status !== 'stop-ready') {
279
+ throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
280
+ }
281
+ output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
282
+ return;
283
+ }
284
+ if (parsed.operation === 'consume') {
285
+ const consumer = parsed.values['--consumer'];
286
+ if (!consumer)
287
+ throw new Error('consume requires --consumer');
288
+ const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
289
+ output({ status: 'consumed', changed: true, evidence: {
290
+ start: consumed.state.startEvidence,
291
+ stop: consumed.state.stopEvidence
292
+ }, diagnostics: [], error: null });
293
+ }
294
+ }
295
+ catch (error) {
296
+ failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
297
+ }
298
+ }
299
+ export { codexLifecycle };
300
+ //# sourceMappingURL=codex-lifecycle.js.map
@@ -154,11 +154,47 @@ function inspectLocalReleaseFacts(cwd, version, run = command) {
154
154
  const localTagConflict = Boolean(tagCommit && !localTag && !localTagAncestor);
155
155
  const postSubject = `chore: prepare next dev iteration after v${version}`;
156
156
  const postLog = localTag || localTagAncestor
157
- ? git(cwd, ['log', '--format=%s', `${tag}..HEAD`], run) ?? ''
157
+ ? git(cwd, ['log', '--format=%H%x00%s', `${tag}..HEAD`], run) ?? ''
158
158
  : '';
159
- const postCommit = postLog.split('\n').some((subject) => subject === postSubject);
159
+ const postCommit = postLog.split('\n').map((line) => line.split('\0'))
160
+ .find(([, subject]) => subject === postSubject)?.[0] ?? null;
160
161
  return { localTag, localTagAncestor, localTagConflict, postCommit };
161
162
  }
163
+ function inspectPostReleaseFacts(cwd, commit, run = command) {
164
+ const inspected = inspectGitWorkflow(cwd);
165
+ const snapshot = inspected.snapshot;
166
+ const branch = snapshot?.branch ?? '';
167
+ const remoteLine = branch ? git(cwd, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`], run) : null;
168
+ const remoteHead = remoteLine?.split(/\s+/)[0] ?? null;
169
+ const packageJson = commit ? git(cwd, ['show', `${commit}:package.json`], run, false) : null;
170
+ let newVersion = null;
171
+ if (packageJson) {
172
+ try {
173
+ const version = JSON.parse(packageJson).version;
174
+ newVersion = typeof version === 'string' ? version : null;
175
+ }
176
+ catch {
177
+ newVersion = null;
178
+ }
179
+ }
180
+ const paths = commit ? git(cwd, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-z', commit], run, false) : null;
181
+ const changedPaths = paths ? paths.split('\0').filter(Boolean)
182
+ .sort((left, right) => Buffer.from(left).compare(Buffer.from(right))) : [];
183
+ const digest = commit ? git(cwd, ['show', `${commit}:${DEMO_DIGEST_PATH}`], run) : null;
184
+ return {
185
+ commit,
186
+ isHead: Boolean(commit && snapshot?.head === commit),
187
+ published: Boolean(commit && remoteHead === commit),
188
+ branch,
189
+ upstream: snapshot?.upstream ?? null,
190
+ remoteHead,
191
+ newVersion,
192
+ changedPaths,
193
+ demoInputSha256: digest && /^[0-9a-f]{64}$/.test(digest) ? digest : null,
194
+ worktree: snapshot?.worktree ?? [],
195
+ staged: snapshot?.staged ?? []
196
+ };
197
+ }
162
198
  function releaseSmokeStatus(workflows, version, tagCommit) {
163
199
  const manualTitle = `Post-Release Smoke v${version}`;
164
200
  const targets = workflows.filter((run) => {
@@ -209,12 +245,15 @@ async function inspectFacts(cwd, version) {
209
245
  const workflows = platform.workflows;
210
246
  const smoke = releaseSmokeStatus(workflows, version, localTagTarget);
211
247
  return {
212
- ...local,
248
+ localTag: local.localTag,
249
+ localTagAncestor: local.localTagAncestor,
250
+ localTagConflict: local.localTagConflict,
213
251
  remoteBranch: Boolean(remoteBranch && remoteBranch.split(/\s+/)[0] === head), remoteTag: Boolean(remoteTag),
214
252
  githubRelease: platform.platform.type !== 'github'
215
253
  ? true
216
254
  : platform.status === 'blocked' ? null : Boolean(platform.release?.published),
217
- npm: npm.published, homebrew: homebrew.published, smoke
255
+ npm: npm.published, homebrew: homebrew.published, smoke,
256
+ post: inspectPostReleaseFacts(cwd, local.postCommit)
218
257
  };
219
258
  }
220
259
  function parsePorcelainPath(record) {
@@ -245,13 +284,21 @@ function updateReleaseMetadata(cwd, version) {
245
284
  fs.writeFileSync(filePath, content);
246
285
  }
247
286
  }
287
+ function optionValues(args, name) {
288
+ const values = [];
289
+ for (let index = 0; index < args.length; index += 1) {
290
+ if (args[index] === name)
291
+ values.push(args[index + 1] ?? '');
292
+ }
293
+ return values;
294
+ }
248
295
  async function releaseWorkflow(args = []) {
249
296
  const [action, rawVersion, ...rest] = args;
250
297
  const version = String(rawVersion || '').replace(/^v/, '');
251
298
  const cwdIndex = rest.indexOf('--cwd');
252
299
  const cwd = path.resolve(cwdIndex >= 0 && rest[cwdIndex + 1] ? rest[cwdIndex + 1] : process.cwd());
253
- if (!['inspect', 'prepare', 'publish', 'post'].includes(action || '') || !semver.valid(version)) {
254
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post <version>' } })}\n`);
300
+ if (!['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'].includes(action || '') || !semver.valid(version)) {
301
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post-prepare|post-publish <version>' } })}\n`);
255
302
  process.exitCode = 1;
256
303
  return;
257
304
  }
@@ -331,10 +378,56 @@ async function releaseWorkflow(args = []) {
331
378
  process.exitCode = result.status === 'failed' ? 1 : result.status === 'degraded' ? 2 : 0;
332
379
  return;
333
380
  }
381
+ if (action === 'post-publish') {
382
+ const expectedValues = optionValues(rest, '--expected-sha256');
383
+ if (expectedValues.length !== 1 || !/^sha256:[0-9a-f]{64}$/.test(expectedValues[0])) {
384
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_INPUT_INVALID', message: 'post-publish requires one --expected-sha256 sha256:<64 hex>' } })}\n`);
385
+ process.exitCode = 1;
386
+ return;
387
+ }
388
+ if (before.phase === 'complete') {
389
+ process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
390
+ return;
391
+ }
392
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
393
+ if (before.phase !== 'post-prepared' || !confirmation) {
394
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: confirmation ? 'RELEASE_PHASE_INVALID' : 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post-release facts cannot authorize publish' } })}\n`);
395
+ process.exitCode = 1;
396
+ return;
397
+ }
398
+ if (confirmation.sha256 !== expectedValues[0]) {
399
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_POST_SNAPSHOT_MISMATCH', message: 'Post-release confirmation snapshot changed' } })}\n`);
400
+ process.exitCode = 1;
401
+ return;
402
+ }
403
+ const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
404
+ if (!channelsComplete || before.facts.smoke !== 'success') {
405
+ process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
406
+ process.exitCode = before.facts.smoke === 'failed' ? 1 : 2;
407
+ return;
408
+ }
409
+ const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [before.facts.post.branch] });
410
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
411
+ if (snapshot.phase === 'complete') {
412
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: 'applied', changed: true, snapshot, error: null })}\n`);
413
+ return;
414
+ }
415
+ const blocked = pushed.status !== 'failed';
416
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: blocked ? 'blocked' : 'failed', snapshot, error: pushed.error ?? { code: 'RELEASE_PROGRESS_PENDING', message: 'Post-release push is not visible at the expected remote head' } })}\n`);
417
+ process.exitCode = blocked ? 2 : 1;
418
+ return;
419
+ }
334
420
  if (before.phase === 'complete') {
335
421
  process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
336
422
  return;
337
423
  }
424
+ if (before.phase === 'post-prepared') {
425
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
426
+ const status = confirmation ? 'no-op' : 'blocked';
427
+ process.stdout.write(`${JSON.stringify({ status, changed: false, snapshot: before, error: confirmation ? null : { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post commit is not the clean current HEAD' } })}\n`);
428
+ process.exitCode = confirmation ? 0 : 2;
429
+ return;
430
+ }
338
431
  const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
339
432
  if (!['published', 'post-pending'].includes(before.phase) || !channelsComplete || before.facts.smoke !== 'success') {
340
433
  process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
@@ -373,10 +466,14 @@ async function releaseWorkflow(args = []) {
373
466
  process.exitCode = 1;
374
467
  return;
375
468
  }
376
- const branch = git(cwd, ['branch', '--show-current']) || '';
377
- const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [branch] });
378
- process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`);
379
- process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
469
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
470
+ const confirmation = 'postConfirmation' in snapshot ? snapshot.postConfirmation : null;
471
+ if (snapshot.phase !== 'post-prepared' || !confirmation) {
472
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: true, demo, snapshot, error: { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Prepared post commit did not produce a confirmation snapshot' } })}\n`);
473
+ process.exitCode = 1;
474
+ return;
475
+ }
476
+ process.stdout.write(`${JSON.stringify({ status: 'applied', changed: true, demo, snapshot, error: null })}\n`);
380
477
  }
381
- export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
478
+ export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
382
479
  //# sourceMappingURL=release-workflow.js.map
@@ -1,24 +1,61 @@
1
- import { requestSandboxControl } from '../sandbox/control/client.js';
1
+ import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
2
2
  import { serveSandboxControl } from '../sandbox/control/server.js';
3
- function sandboxControl(args) {
3
+ import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
4
+ async function sandboxControl(args) {
4
5
  const [operation, ...rest] = args;
5
6
  if (operation === 'serve') {
6
7
  const manifestIndex = rest.indexOf('--manifest');
7
8
  const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
8
9
  if (!manifest)
9
10
  throw new Error('sandbox-control serve requires --manifest');
10
- serveSandboxControl(manifest);
11
+ const controller = new AbortController();
12
+ const abort = () => controller.abort();
13
+ process.once('SIGINT', abort);
14
+ process.once('SIGTERM', abort);
15
+ try {
16
+ await serveSandboxControl(manifest, controller.signal);
17
+ }
18
+ finally {
19
+ process.off('SIGINT', abort);
20
+ process.off('SIGTERM', abort);
21
+ }
22
+ return;
23
+ }
24
+ if (operation === 'execute') {
25
+ const requestIndex = rest.indexOf('--request');
26
+ const nonceIndex = rest.indexOf('--nonce');
27
+ const request = requestIndex >= 0 ? rest[requestIndex + 1] : undefined;
28
+ const nonce = nonceIndex >= 0 ? rest[nonceIndex + 1] : undefined;
29
+ if (!request || !nonce)
30
+ throw new Error('sandbox-control execute requires --request and --nonce');
31
+ await runSandboxControlExecutor(request, nonce);
11
32
  return;
12
33
  }
13
34
  if (operation === 'client') {
14
35
  const [family = '', ...commandArgs] = rest;
15
- const response = requestSandboxControl({ family, args: commandArgs });
36
+ let response;
37
+ try {
38
+ response = requestSandboxControl({ family, args: commandArgs });
39
+ }
40
+ catch (error) {
41
+ if (!(error instanceof SandboxControlClientError))
42
+ throw error;
43
+ process.stderr.write(`${error.detail.message}\n`);
44
+ process.exitCode = error.detail.retryable ? 75 : 1;
45
+ return;
46
+ }
16
47
  process.stdout.write(response.stdout);
17
48
  process.stderr.write(response.stderr);
18
- process.exitCode = response.exitCode;
49
+ if (response.phase === 'rejected') {
50
+ process.stderr.write(response.error?.message ?? response.stderr);
51
+ process.exitCode = response.error?.retryable ? 75 : 1;
52
+ }
53
+ else {
54
+ process.exitCode = response.exitCode ?? 1;
55
+ }
19
56
  return;
20
57
  }
21
- throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
58
+ throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | execute --request <path> --nonce <nonce> | client <family> [args...]');
22
59
  }
23
60
  export { sandboxControl };
24
61
  //# sourceMappingURL=sandbox-control.js.map
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { requestSandboxTaskCreate, SandboxControlClientError } from '../sandbox/control/client.js';
4
+ import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.js';
5
+ import { validateTaskCreateCandidate } from '../task/create.js';
6
+ import { createTask } from '../task/create-service.js';
7
+ function failed(code, message, retryable = false) {
8
+ return {
9
+ status: retryable ? 'blocked' : 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
10
+ operations: [], warnings: [], error: { code, message, retryable }
11
+ };
12
+ }
13
+ function output(result) {
14
+ process.stdout.write(`${JSON.stringify(result)}\n`);
15
+ process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
16
+ }
17
+ function taskCreate(args) {
18
+ const inputIndex = args.indexOf('--input');
19
+ const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
20
+ if (!input || args.length !== 2 || inputIndex !== 0) {
21
+ output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
22
+ return;
23
+ }
24
+ try {
25
+ const inputPath = path.resolve(input);
26
+ const stat = fs.lstatSync(inputPath);
27
+ if (!stat.isFile() || stat.isSymbolicLink())
28
+ throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
29
+ if (stat.size > SANDBOX_CONTROL_MAX_BYTES)
30
+ throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
31
+ const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
32
+ if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
33
+ const response = requestSandboxTaskCreate({ candidate });
34
+ if (response.phase === 'rejected') {
35
+ output(failed(response.error?.code ?? 'SANDBOX_CONTROL_REJECTED', response.error?.message ?? response.stderr, response.error?.retryable ?? false));
36
+ return;
37
+ }
38
+ process.stdout.write(response.stdout);
39
+ process.stderr.write(response.stderr);
40
+ process.exitCode = response.exitCode;
41
+ return;
42
+ }
43
+ output(createTask(candidate, { repoRoot: process.cwd() }));
44
+ }
45
+ catch (error) {
46
+ if (error instanceof SandboxControlClientError) {
47
+ output(failed(error.detail.code, error.detail.message, error.detail.retryable));
48
+ return;
49
+ }
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
52
+ output(failed(code, message));
53
+ }
54
+ }
55
+ export { taskCreate };
56
+ //# sourceMappingURL=task-create.js.map