@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,289 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { loadConfig } from '../../sandbox/config.js';
7
+ import { detectEngine } from '../../sandbox/engine.js';
8
+ import { runEngine } from '../../sandbox/shell.js';
9
+ import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.js';
10
+ import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.js';
11
+ import { containerNameCandidates } from '../../sandbox/constants.js';
12
+ import { resolveSandboxTarget } from '../../sandbox/workspace-identity.js';
13
+ import { sandboxControlPaths } from '../../sandbox/workspace-view.js';
14
+ import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../../sandbox/control/state.js';
15
+ import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../../sandbox/control/protocol.js';
16
+ import { getProcessStartTime } from '../../server/process-state.js';
17
+ import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
18
+ const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
19
+ const MAX_TIMEOUT_MS = 60 * 60 * 1000;
20
+ const RECOVERY_GRACE_MS = 30_000;
21
+ export function parseValidateArgs(args) {
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', command: [], help: true };
24
+ }
25
+ const separator = args.indexOf('--');
26
+ if (separator < 0)
27
+ throw new Error(`${USAGE}\nA literal -- must separate validation options from the command.`);
28
+ const optionArgs = args.slice(0, separator);
29
+ const command = args.slice(separator + 1);
30
+ let scope = 'snapshot';
31
+ let timeoutMs = 300_000;
32
+ let format = 'text';
33
+ const positionals = [];
34
+ for (let index = 0; index < optionArgs.length; index += 1) {
35
+ const arg = optionArgs[index];
36
+ if (arg === '--scope') {
37
+ const value = optionArgs[++index];
38
+ if (value !== 'snapshot' && value !== 'inplace')
39
+ throw new Error('validate --scope must be snapshot or inplace');
40
+ scope = value;
41
+ }
42
+ else if (arg === '--timeout') {
43
+ const value = Number(optionArgs[++index]);
44
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS)
45
+ throw new Error(`validate --timeout must be 1..${MAX_TIMEOUT_MS}`);
46
+ timeoutMs = value;
47
+ }
48
+ else if (arg === '--format') {
49
+ const value = optionArgs[++index];
50
+ if (value !== 'text' && value !== 'json')
51
+ throw new Error('validate --format must be text or json');
52
+ format = value;
53
+ }
54
+ else if (arg.startsWith('-')) {
55
+ throw new Error(`Unknown option: ${arg}`);
56
+ }
57
+ else {
58
+ positionals.push(arg);
59
+ }
60
+ }
61
+ if (positionals.length !== 1 || command.length === 0)
62
+ throw new Error(USAGE);
63
+ return { target: positionals[0], scope, timeoutMs, format, command, help: false };
64
+ }
65
+ function runValidationCommand(command, cwd, scope, timeoutMs, json) {
66
+ const [file, ...args] = command;
67
+ const result = spawnSync(file, args, {
68
+ cwd,
69
+ env: { ...process.env, AGENT_INFRA_VALIDATION_SCOPE: scope },
70
+ encoding: 'utf8',
71
+ timeout: timeoutMs,
72
+ stdio: ['inherit', 'pipe', 'pipe'],
73
+ shell: process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file)
74
+ });
75
+ if (json) {
76
+ if (result.stdout)
77
+ process.stderr.write(result.stdout);
78
+ if (result.stderr)
79
+ process.stderr.write(result.stderr);
80
+ }
81
+ else {
82
+ if (result.stdout)
83
+ process.stdout.write(result.stdout);
84
+ if (result.stderr)
85
+ process.stderr.write(result.stderr);
86
+ }
87
+ return { exitCode: result.status ?? 1, signal: result.signal ?? null };
88
+ }
89
+ function git(root, args) {
90
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
91
+ }
92
+ function waitForState(statusDir, predicate, timeoutMs = 5_000) {
93
+ const deadline = Date.now() + timeoutMs;
94
+ while (Date.now() < deadline) {
95
+ try {
96
+ const status = readSandboxControlStatus(statusDir);
97
+ if (predicate(status))
98
+ return;
99
+ }
100
+ catch {
101
+ // Atomic status publication may be between renames.
102
+ }
103
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
104
+ }
105
+ throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
106
+ }
107
+ function snapshotValidation(repoRoot, commit, options) {
108
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
109
+ const worktree = path.join(tempRoot, 'worktree');
110
+ let cleanup = 'not-created';
111
+ try {
112
+ git(repoRoot, ['worktree', 'add', '--detach', worktree, commit]);
113
+ cleanup = 'pending';
114
+ return { ...runValidationCommand(options.command, worktree, 'snapshot', options.timeoutMs, options.format === 'json'), cleanup: () => cleanup };
115
+ }
116
+ finally {
117
+ let cleanupError = null;
118
+ if (fs.existsSync(worktree)) {
119
+ try {
120
+ git(repoRoot, ['worktree', 'remove', '--force', worktree]);
121
+ cleanup = 'completed';
122
+ }
123
+ catch (error) {
124
+ cleanupError = error;
125
+ }
126
+ }
127
+ try {
128
+ git(repoRoot, ['worktree', 'prune']);
129
+ }
130
+ catch (error) {
131
+ cleanupError ??= error;
132
+ }
133
+ try {
134
+ fs.rmSync(tempRoot, { recursive: true, force: true });
135
+ }
136
+ catch (error) {
137
+ cleanupError ??= error;
138
+ }
139
+ if (cleanupError)
140
+ throw cleanupError;
141
+ }
142
+ }
143
+ function inplaceValidation(config, target, options) {
144
+ const engine = detectEngine(config);
145
+ const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
146
+ const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
147
+ if (!row)
148
+ throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
149
+ const control = sandboxControlPaths({
150
+ base: config.controlBase, project: config.project,
151
+ container: row.name, identity: target.workspace
152
+ });
153
+ const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
154
+ const initial = readSandboxControlStatus(control.statusDir);
155
+ const now = Date.now();
156
+ if (initial.generation !== manifest.generation
157
+ || now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
158
+ || initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
159
+ throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
160
+ }
161
+ if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
162
+ throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
163
+ }
164
+ const startTime = getProcessStartTime(process.pid);
165
+ if (!startTime)
166
+ throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
167
+ const leasePath = path.join(control.root, 'lease.json');
168
+ const nonce = randomUUID();
169
+ const issuedAt = Date.now();
170
+ const lease = {
171
+ version: 1, generation: manifest.generation, nonce,
172
+ owner: { pid: process.pid, startTime }, issuedAt,
173
+ expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
174
+ taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
175
+ branch: target.branch, reason: 'manual-validation'
176
+ };
177
+ const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
178
+ const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
179
+ fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
180
+ let stopped = false;
181
+ let restored = false;
182
+ try {
183
+ appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
184
+ waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
185
+ if (row.running) {
186
+ runEngine(engine, 'docker', ['stop', row.name]);
187
+ stopped = true;
188
+ }
189
+ const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
190
+ const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
191
+ if (!refreshed || refreshed.running)
192
+ throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
193
+ const activeLease = readActiveLease(manifest);
194
+ if (!activeLease || activeLease.nonce !== nonce)
195
+ throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
196
+ assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
197
+ const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
198
+ return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
199
+ }
200
+ finally {
201
+ const recoveryErrors = [];
202
+ try {
203
+ const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
204
+ if (currentBranch !== originalBranch)
205
+ git(manifest.worktreeRoot, ['switch', originalBranch]);
206
+ }
207
+ catch {
208
+ try {
209
+ git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
210
+ git(manifest.worktreeRoot, ['switch', originalBranch]);
211
+ }
212
+ catch (fallbackError) {
213
+ recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
214
+ }
215
+ }
216
+ if (stopped) {
217
+ try {
218
+ runEngine(engine, 'docker', ['start', row.name]);
219
+ }
220
+ catch (error) {
221
+ recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
222
+ }
223
+ }
224
+ try {
225
+ const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
226
+ if (currentLease.nonce !== nonce)
227
+ throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
228
+ }
229
+ catch (error) {
230
+ recoveryErrors.push(error instanceof Error ? error.message : String(error));
231
+ }
232
+ if (recoveryErrors.length === 0) {
233
+ try {
234
+ fs.unlinkSync(leasePath);
235
+ appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
236
+ waitForState(control.statusDir, (status) => status.state === 'healthy');
237
+ restored = true;
238
+ }
239
+ catch (error) {
240
+ try {
241
+ if (!fs.existsSync(leasePath)) {
242
+ fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
243
+ }
244
+ appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
245
+ }
246
+ catch (leaseError) {
247
+ recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
248
+ }
249
+ recoveryErrors.push(error instanceof Error ? error.message : String(error));
250
+ }
251
+ }
252
+ if (recoveryErrors.length > 0) {
253
+ throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
254
+ }
255
+ }
256
+ }
257
+ export async function validate(args) {
258
+ const options = parseValidateArgs(args);
259
+ if (options.help) {
260
+ process.stdout.write(`${USAGE}\n`);
261
+ return 0;
262
+ }
263
+ const config = loadConfig();
264
+ const target = resolveSandboxTarget(options.target, config.repoRoot);
265
+ const commit = git(config.repoRoot, ['rev-parse', target.branch]);
266
+ const startedAt = new Date().toISOString();
267
+ const result = options.scope === 'snapshot'
268
+ ? snapshotValidation(config.repoRoot, commit, options)
269
+ : inplaceValidation(config, target, options);
270
+ const evidence = {
271
+ version: 1,
272
+ taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
273
+ branch: target.branch,
274
+ scope: options.scope,
275
+ commit,
276
+ command: path.basename(options.command[0]),
277
+ startedAt,
278
+ completedAt: new Date().toISOString(),
279
+ exitCode: result.exitCode,
280
+ signal: result.signal,
281
+ cleanup: result.cleanup()
282
+ };
283
+ if (options.format === 'json')
284
+ process.stdout.write(`${JSON.stringify(evidence)}\n`);
285
+ else
286
+ process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
287
+ return result.exitCode;
288
+ }
289
+ //# sourceMappingURL=validate.js.map
@@ -1,11 +1,11 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.js';
4
+ import { locateActivityLog, appendActivityEntry, pairEntries, parseCommitAttemptStarted } from './activity-log.js';
5
5
  import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
6
6
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
- import { parseReviewSummary } from './review-artifacts.js';
8
- import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
7
+ import { artifactName, maxRound, parseReviewSummary } from './review-artifacts.js';
8
+ import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewedGitTree } from './review-fingerprint.js';
9
9
  const SHA_RE = /^[a-f0-9]{40,64}$/;
10
10
  function git(repoRoot, args) {
11
11
  return execFileSync('git', args, {
@@ -26,6 +26,22 @@ function isAncestor(repoRoot, ancestor, descendant) {
26
26
  stdio: 'ignore'
27
27
  }).status === 0;
28
28
  }
29
+ function hasMatchingApprovedReview(taskDir, beforeRound, baseline, tree) {
30
+ const entries = fs.readdirSync(taskDir);
31
+ for (let round = Math.min(beforeRound - 1, maxRound(entries, 'review-code')); round >= 1; round -= 1) {
32
+ const file = path.join(taskDir, artifactName('review-code', round));
33
+ if (!fs.existsSync(file))
34
+ continue;
35
+ const content = fs.readFileSync(file, 'utf8');
36
+ const summary = parseReviewSummary(content);
37
+ if (summary.ok
38
+ && summary.summary.verdict === 'Approved'
39
+ && extractReviewBaseline(content) === baseline
40
+ && extractReviewedSnapshotTree(content) === tree)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
29
45
  function outcome(disposition, overrides = {}) {
30
46
  return {
31
47
  disposition,
@@ -34,10 +50,12 @@ function outcome(disposition, overrides = {}) {
34
50
  currentHead: '',
35
51
  committedHead: null,
36
52
  commitNote: null,
53
+ activityNote: null,
37
54
  needsAnchor: false,
38
55
  needsLog: false,
39
56
  intent: null,
40
57
  intentDigest: null,
58
+ attempt: null,
41
59
  ...overrides
42
60
  };
43
61
  }
@@ -63,6 +81,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
63
81
  const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
64
82
  const file = commitIntentPath(taskDir);
65
83
  if (!fs.existsSync(file)) {
84
+ if (openRows.length === 1) {
85
+ const attempt = parseCommitAttemptStarted(openRows[0].note);
86
+ if (attempt && attempt.baseline === currentHead) {
87
+ return outcome('retryable-start', {
88
+ message: 'a structured Commit attempt can be safely reused',
89
+ currentHead,
90
+ attempt
91
+ });
92
+ }
93
+ }
66
94
  if (openRows.length > 0) {
67
95
  return outcome('orphaned-start', {
68
96
  code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
@@ -119,6 +147,12 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
119
147
  message: 'repository HEAD or ancestry conflicts with the commit intent'
120
148
  });
121
149
  }
150
+ const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
151
+ const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
152
+ const pushOnlyRetry = intent.phase === 'pushed'
153
+ && intent.baselineHead === intent.committedHead
154
+ && matchingDone.length === 1
155
+ && openRows.length === 1;
122
156
  const review = findAuthoritativeReviewCodeArtifact(taskDir);
123
157
  let needsAnchor = false;
124
158
  if (review.ok && review.path) {
@@ -132,11 +166,13 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
132
166
  const summary = parseReviewSummary(reviewContent);
133
167
  const reviewBaseline = extractReviewBaseline(reviewContent);
134
168
  const reviewTree = extractReviewedSnapshotTree(reviewContent);
135
- if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !SHA_RE.test(reviewTree)) {
169
+ if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !parseReviewedGitTree(reviewTree)) {
136
170
  return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
137
171
  }
138
172
  const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
139
- if (reviewBaseline !== intent.baselineHead || reviewTree !== committedTree) {
173
+ const matchingCurrentReview = reviewBaseline === intent.baselineHead && reviewTree === committedTree;
174
+ if (!matchingCurrentReview
175
+ && !hasMatchingApprovedReview(taskDir, review.round, intent.baselineHead, committedTree)) {
140
176
  return outcome('conflict', {
141
177
  ...shared,
142
178
  code: 'COMMIT_FINALIZATION_CONFLICT',
@@ -144,18 +180,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
144
180
  });
145
181
  }
146
182
  const anchor = String(metadata.last_reviewed_commit ?? '').trim();
147
- if (anchor && anchor !== intent.committedHead) {
183
+ if (anchor && anchor !== intent.committedHead && anchor !== intent.baselineHead) {
148
184
  return outcome('conflict', {
149
185
  ...shared,
150
186
  code: 'COMMIT_FINALIZATION_CONFLICT',
151
187
  message: 'task review anchor conflicts with the committed intent'
152
188
  });
153
189
  }
154
- needsAnchor = anchor === '';
190
+ needsAnchor = anchor !== intent.committedHead;
155
191
  }
156
- const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
157
- const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
158
- const needsLog = matchingDone.length === 0 && openRows.length === 1;
192
+ const needsLog = (matchingDone.length === 0 && openRows.length === 1) || pushOnlyRetry;
159
193
  if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
160
194
  return outcome('conflict', {
161
195
  ...shared,
@@ -168,12 +202,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
168
202
  ...shared,
169
203
  message: 'commit finalization can be recovered',
170
204
  commitNote,
205
+ activityNote: pushOnlyRetry ? `Pushed ${commitNote}` : commitNote,
171
206
  needsAnchor,
172
207
  needsLog
173
208
  });
174
209
  }
175
210
  function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
176
- if (inspection.disposition !== 'recoverable' || !inspection.committedHead || !inspection.commitNote) {
211
+ if (inspection.disposition !== 'recoverable'
212
+ || !inspection.committedHead
213
+ || !inspection.commitNote
214
+ || !inspection.activityNote) {
177
215
  throw new Error('commit finalization task mutations require a recoverable inspection');
178
216
  }
179
217
  const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
@@ -196,7 +234,7 @@ function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
196
234
  time: timestamp,
197
235
  step: 'Commit',
198
236
  agent,
199
- note: inspection.commitNote
237
+ note: inspection.activityNote
200
238
  })
201
239
  });
202
240
  }
@@ -0,0 +1,137 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { syncPlatformComment } from '../platform/issue-comments.js';
4
+ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
5
+ import { VERSION } from '../version.js';
6
+ import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
7
+ import { createLocalTask } from './create.js';
8
+ import { verifyTaskEvent } from './verification.js';
9
+ const DEFAULT_DEPENDENCIES = {
10
+ createIssue: createPlatformIssue,
11
+ syncIssue: syncPlatformIssue,
12
+ syncComment: syncPlatformComment,
13
+ addWarning: applyWorkflowWarningIntent
14
+ };
15
+ function verifyCreatedTask(repoRoot, taskId) {
16
+ const verification = verifyTaskEvent({ taskRef: taskId, event: 'create-task.completed' }, { repoRoot });
17
+ if (verification.status === 'pass') {
18
+ return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
19
+ }
20
+ const blocked = verification.status === 'blocked';
21
+ return {
22
+ operation: {
23
+ name: 'task:verify', status: verification.status,
24
+ reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
25
+ },
26
+ error: {
27
+ code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
28
+ message: `Created task did not pass the ${verification.event} gate`,
29
+ retryable: blocked
30
+ }
31
+ };
32
+ }
33
+ function platformOperations(prefix, operations) {
34
+ return operations.map((operation) => ({
35
+ name: `${prefix}:${operation.name}`,
36
+ status: operation.status,
37
+ reasonCode: operation.reasonCode ?? null
38
+ }));
39
+ }
40
+ function issueFromTask(repoRoot, taskId) {
41
+ const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
42
+ const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
43
+ return match ? { number: Number(match[1]), url: '' } : null;
44
+ }
45
+ function createTask(value, options) {
46
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
47
+ let candidate;
48
+ let local;
49
+ try {
50
+ candidate = value;
51
+ local = createLocalTask(candidate, {
52
+ repoRoot: options.repoRoot,
53
+ agentInfraVersion: options.agentInfraVersion ?? VERSION
54
+ });
55
+ }
56
+ catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
59
+ return {
60
+ status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
61
+ operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
62
+ };
63
+ }
64
+ const operations = [{
65
+ name: 'task:local', status: local.status, reasonCode: null
66
+ }];
67
+ let issue = issueFromTask(options.repoRoot, local.task.id);
68
+ const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
69
+ operations.push(...platformOperations('platform-create', created.operations));
70
+ if (created.issue)
71
+ issue = { number: created.issue.number, url: created.issue.url };
72
+ let platformFailure = created.status === 'failed' || created.status === 'blocked' ? created : null;
73
+ if (!platformFailure && (created.task.issueNumber || issue)) {
74
+ const synced = dependencies.syncIssue(local.task.id, {
75
+ cwd: options.repoRoot,
76
+ agent: candidate.agent,
77
+ status: 'waiting-for-triage',
78
+ assignees: 'current',
79
+ milestone: 'initial',
80
+ issueType: true,
81
+ fields: true
82
+ });
83
+ operations.push(...platformOperations('platform-sync', synced.operations));
84
+ if (synced.issue)
85
+ issue = { number: synced.issue.number, url: synced.issue.url };
86
+ if (synced.status === 'failed' || synced.status === 'blocked')
87
+ platformFailure = synced;
88
+ if (!platformFailure) {
89
+ const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
90
+ operations.push(...platformOperations('platform-comment', commented.operations));
91
+ if (commented.status === 'failed' || commented.status === 'blocked')
92
+ platformFailure = commented;
93
+ }
94
+ }
95
+ if (platformFailure) {
96
+ const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
97
+ dependencies.addWarning({
98
+ kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
99
+ code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
100
+ action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
101
+ }, { repoRoot: options.repoRoot });
102
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
103
+ operations.push(verified.operation);
104
+ if (verified.error)
105
+ return {
106
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
107
+ task: local.task, issue, operations, warnings: [], error: verified.error
108
+ };
109
+ return {
110
+ status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
111
+ changed: local.changed,
112
+ task: local.task,
113
+ issue,
114
+ operations,
115
+ warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
116
+ error
117
+ };
118
+ }
119
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
120
+ operations.push(verified.operation);
121
+ if (verified.error)
122
+ return {
123
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
124
+ task: local.task, issue, operations, warnings: [], error: verified.error
125
+ };
126
+ return {
127
+ status: local.status,
128
+ changed: local.changed,
129
+ task: local.task,
130
+ issue,
131
+ operations,
132
+ warnings: [],
133
+ error: null
134
+ };
135
+ }
136
+ export { createTask };
137
+ //# sourceMappingURL=create-service.js.map