@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -1,5 +1,9 @@
1
1
  import { defineAgentClientAdapter } from '../adapter.ts';
2
2
  import { hostJoin } from '../../sandbox/engines/wsl2-paths.ts';
3
+ import {
4
+ opencodeBeforeContainerCreateHook,
5
+ opencodeRecoveryChecks
6
+ } from './opencode-sandbox.ts';
3
7
 
4
8
  const YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
5
9
 
@@ -37,9 +41,17 @@ const opencodeAdapter = defineAgentClientAdapter({
37
41
  'zh-CN': '.opencode/commands/update-agent-infra.zh-CN.md'
38
42
  },
39
43
  target: '.opencode/commands/update-agent-infra.md'
40
- }]
44
+ }],
45
+ customCommand: {
46
+ target: '.opencode/commands/${skillName}.md',
47
+ frontmatter: { agent: 'general', subtask: false },
48
+ argumentsToken: '$ARGUMENTS'
49
+ }
41
50
  },
42
51
  sandbox: {
52
+ image: {
53
+ dotfilesExclusions: ['.config/opencode']
54
+ },
43
55
  createTool: ({ home }) => ({
44
56
  id: 'opencode',
45
57
  name: 'OpenCode',
@@ -49,7 +61,9 @@ const opencodeAdapter = defineAgentClientAdapter({
49
61
  versionCmd: 'opencode --version',
50
62
  setupHint: 'Configure OpenCode credentials inside the container before first use.',
51
63
  envVars: {
52
- OPENCODE_CONFIG: '/home/devuser/.local/share/opencode/opencode.json'
64
+ XDG_DATA_HOME: '/home/devuser/.local/share',
65
+ XDG_CONFIG_HOME: '/home/devuser/.local/share/opencode/.xdg/config',
66
+ XDG_STATE_HOME: '/home/devuser/.local/share/opencode/.xdg/state'
53
67
  },
54
68
  hostLiveMounts: [
55
69
  {
@@ -68,7 +82,8 @@ const opencodeAdapter = defineAgentClientAdapter({
68
82
  command: `OPENCODE_PERMISSION='${YOLO_PERMISSION}' opencode; tput ed`
69
83
  }
70
84
  ],
71
- hooks: []
85
+ hooks: [opencodeBeforeContainerCreateHook],
86
+ recoveryChecks: opencodeRecoveryChecks
72
87
  }
73
88
  });
74
89
 
@@ -0,0 +1,65 @@
1
+ RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
2
+ #!/bin/sh
3
+ set -eu
4
+
5
+ SETTINGS_FILE="/home/devuser/.claude/settings.json"
6
+ if [ -r "$SETTINGS_FILE" ] && jq -e '
7
+ def has_nonempty($v):
8
+ if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
9
+ (if type == "object" then . else {} end) as $settings
10
+ | (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
11
+ | has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
12
+ or has_nonempty($env.ANTHROPIC_API_KEY)
13
+ or has_nonempty($settings.apiKeyHelper)
14
+ ' "$SETTINGS_FILE" >/dev/null 2>&1; then
15
+ exit 0
16
+ fi
17
+
18
+ CRED_FILE="/home/devuser/.claude/.credentials.json"
19
+ [ -r "$CRED_FILE" ] || exit 0
20
+
21
+ EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
22
+ case "$EXPIRES_MS" in
23
+ ''|*[!0-9]*) exit 0 ;;
24
+ esac
25
+
26
+ NOW_MS=$(($(date +%s) * 1000))
27
+ DIFF_MS=$((EXPIRES_MS - NOW_MS))
28
+ DIFF_S=$((DIFF_MS / 1000))
29
+
30
+ DIM='#[fg=colour245]'
31
+ YELLOW='#[fg=yellow]'
32
+ YELLOW_BOLD='#[fg=yellow,bold]'
33
+ RED_BOLD='#[fg=red,bold]'
34
+ RED_REV='#[fg=red,reverse]'
35
+ RESET='#[default]'
36
+
37
+ if [ "$DIFF_S" -le 0 ]; then
38
+ ELAPSED=$(( -DIFF_S ))
39
+ M=$((ELAPSED / 60))
40
+ printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
41
+ elif [ "$DIFF_S" -lt 60 ]; then
42
+ printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
43
+ elif [ "$DIFF_S" -lt 300 ]; then
44
+ M=$((DIFF_S / 60))
45
+ S=$((DIFF_S % 60))
46
+ printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
47
+ elif [ "$DIFF_S" -lt 1800 ]; then
48
+ M=$((DIFF_S / 60))
49
+ printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
50
+ elif [ "$DIFF_S" -lt 3600 ]; then
51
+ M=$((DIFF_S / 60))
52
+ printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
53
+ else
54
+ TOTAL_M=$((DIFF_S / 60))
55
+ H=$((TOTAL_M / 60))
56
+ M=$((TOTAL_M % 60))
57
+ printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
58
+ fi
59
+ SCRIPT
60
+
61
+ RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status
62
+
63
+ RUN printf '%s\n' \
64
+ "set -g status-right '#(/usr/local/bin/cc-token-status) | %H:%M'" \
65
+ >> /etc/tmux.conf
@@ -135,7 +135,15 @@ function createAgentClientManifest(): readonly AgentClientManifestEntry[] {
135
135
  ownedPathPrefixes: Object.freeze([...adapter.project.ownedPathPrefixes]),
136
136
  managed: Object.freeze([...adapter.project.managed]),
137
137
  merged: Object.freeze([...adapter.project.merged]),
138
- ejected: Object.freeze([...adapter.project.ejected])
138
+ ejected: Object.freeze([...adapter.project.ejected]),
139
+ ...(adapter.project.customCommand === undefined
140
+ ? {}
141
+ : {
142
+ customCommand: Object.freeze({
143
+ ...adapter.project.customCommand,
144
+ frontmatter: Object.freeze({ ...adapter.project.customCommand.frontmatter })
145
+ })
146
+ })
139
147
  }))
140
148
  );
141
149
  }
@@ -0,0 +1,86 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ type GitWorktreeIdentity = Readonly<{
6
+ worktreeRoot: string;
7
+ commonDir: string;
8
+ branch: string;
9
+ }>;
10
+
11
+ function normalizeCanonicalPath(value: string, platform: NodeJS.Platform = process.platform): string {
12
+ return platform === 'win32'
13
+ ? path.win32.normalize(value).replaceAll('/', '\\').toLowerCase()
14
+ : path.posix.normalize(value);
15
+ }
16
+
17
+ function sameFilesystemEntry(left: string, right: string, platform: NodeJS.Platform = process.platform): boolean {
18
+ const leftReal = fs.realpathSync.native(left);
19
+ const rightReal = fs.realpathSync.native(right);
20
+ const leftStat = fs.statSync(leftReal, { bigint: true });
21
+ const rightStat = fs.statSync(rightReal, { bigint: true });
22
+ if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
23
+ return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
24
+ }
25
+ return normalizeCanonicalPath(leftReal, platform) === normalizeCanonicalPath(rightReal, platform);
26
+ }
27
+
28
+ function git(root: string, args: string[]): string {
29
+ return execFileSync('git', args, {
30
+ cwd: root,
31
+ encoding: 'utf8',
32
+ stdio: ['ignore', 'pipe', 'pipe']
33
+ }).trim();
34
+ }
35
+
36
+ function resolveGitWorktreeIdentity(worktreeRoot: string): GitWorktreeIdentity {
37
+ try {
38
+ const canonical = fs.realpathSync.native(worktreeRoot);
39
+ const topLevel = fs.realpathSync.native(git(canonical, ['rev-parse', '--show-toplevel']));
40
+ if (!sameFilesystemEntry(canonical, topLevel)) {
41
+ throw new Error('worktree root is not the Git top-level directory');
42
+ }
43
+ const commonDir = fs.realpathSync.native(git(canonical, [
44
+ 'rev-parse', '--path-format=absolute', '--git-common-dir'
45
+ ]));
46
+ const branch = git(canonical, ['symbolic-ref', '--short', 'HEAD']);
47
+ return { worktreeRoot: canonical, commonDir, branch };
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: ${message}`);
51
+ }
52
+ }
53
+
54
+ function assertGitWorktreeBinding(
55
+ stateRoot: string,
56
+ worktreeRoot: string,
57
+ expectedBranch: string
58
+ ): GitWorktreeIdentity {
59
+ const state = resolveGitWorktreeIdentity(stateRoot);
60
+ const worktree = resolveGitWorktreeIdentity(worktreeRoot);
61
+ if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
62
+ throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
63
+ }
64
+ if (worktree.branch !== expectedBranch) {
65
+ throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: expected branch '${expectedBranch}', got '${worktree.branch}'`);
66
+ }
67
+ return worktree;
68
+ }
69
+
70
+ function assertGitRepositoryBinding(stateRoot: string, worktreeRoot: string): GitWorktreeIdentity {
71
+ const state = resolveGitWorktreeIdentity(stateRoot);
72
+ const worktree = resolveGitWorktreeIdentity(worktreeRoot);
73
+ if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
74
+ throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
75
+ }
76
+ return worktree;
77
+ }
78
+
79
+ export {
80
+ assertGitWorktreeBinding,
81
+ assertGitRepositoryBinding,
82
+ normalizeCanonicalPath,
83
+ resolveGitWorktreeIdentity,
84
+ sameFilesystemEntry
85
+ };
86
+ export type { GitWorktreeIdentity };
@@ -0,0 +1,204 @@
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
+
6
+ import {
7
+ preflightCodexLifecycleEvidence,
8
+ resolveCodexTerminal,
9
+ resolveCodexThread
10
+ } from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
11
+ import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
12
+ import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
13
+
14
+ const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
15
+ const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
16
+
17
+ type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
18
+
19
+ function output(value: unknown): void {
20
+ process.stdout.write(`${JSON.stringify(value)}\n`);
21
+ }
22
+
23
+ function failure(code: string, message: string): void {
24
+ output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
25
+ process.exitCode = 1;
26
+ }
27
+
28
+ function parse(args: string[]): Parsed | null {
29
+ if (args[0] === '--help' || args[0] === '-h') {
30
+ process.stdout.write(USAGE);
31
+ return null;
32
+ }
33
+ const operation = args[0];
34
+ if (!operation) {
35
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
36
+ return null;
37
+ }
38
+ const allowed = {
39
+ 'hook-event': ['--event'],
40
+ 'resolve-start': ['--child-id'],
41
+ 'resolve-stop': ['--child-id'],
42
+ preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
43
+ consume: ['--child-id', '--consumer']
44
+ }[operation];
45
+ if (!allowed) {
46
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
47
+ return null;
48
+ }
49
+ const values: Record<string, string> = {};
50
+ for (let index = 1; index < args.length; index += 1) {
51
+ const flag = args[index]!;
52
+ if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
53
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
54
+ return null;
55
+ }
56
+ const value = args[++index];
57
+ if (!value || value.startsWith('--')) {
58
+ failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
59
+ return null;
60
+ }
61
+ values[flag] = value;
62
+ }
63
+ return Object.freeze({ operation, values: Object.freeze(values) });
64
+ }
65
+
66
+ function cliVersion(): string {
67
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
68
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
69
+ if (result.status !== 0 || !version) throw new Error('Codex CLI version is unavailable');
70
+ return version;
71
+ }
72
+
73
+ function hookDefinitionHash(): string {
74
+ const file = path.join(process.cwd(), '.codex', 'hooks.json');
75
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
76
+ }
77
+
78
+ async function readStdin(): Promise<unknown> {
79
+ let input = '';
80
+ process.stdin.setEncoding('utf8');
81
+ for await (const chunk of process.stdin) {
82
+ input += chunk;
83
+ if (Buffer.byteLength(input) > 64 * 1024) throw new Error('Codex lifecycle input exceeds 64 KiB');
84
+ }
85
+ return JSON.parse(input || '{}') as unknown;
86
+ }
87
+
88
+ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | null {
89
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
90
+ throw new Error('Codex lifecycle hook payload must be an object');
91
+ }
92
+ const value = payload as Record<string, unknown>;
93
+ const text = (key: string) => typeof value[key] === 'string' ? value[key] as string : '';
94
+ if (!MANAGED_AGENT.test(text('nativeAgent'))) return null;
95
+ if (phase === 'pre-tool') return {
96
+ type: 'hook-spawn',
97
+ sessionId: text('sessionId'),
98
+ turnId: text('turnId'),
99
+ toolUseId: text('toolUseId'),
100
+ nativeAgent: text('nativeAgent'),
101
+ ...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
102
+ ...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
103
+ hookDefinitionHash: text('hookDefinitionHash')
104
+ };
105
+ if (phase === 'subagent-start') return {
106
+ type: 'hook-child',
107
+ sessionId: text('sessionId'),
108
+ turnId: text('turnId'),
109
+ childThreadId: text('childThreadId'),
110
+ nativeAgent: text('nativeAgent')
111
+ };
112
+ if (phase === 'subagent-stop') 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') return null;
120
+ throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
121
+ }
122
+
123
+ async function codexLifecycle(args: string[] = []): Promise<void> {
124
+ const parsed = parse(args);
125
+ if (!parsed || process.exitCode) return;
126
+ try {
127
+ if (parsed.operation === 'preflight') {
128
+ const format = parsed.values['--format'] ?? 'json';
129
+ if (!['text', 'json'].includes(format)) throw new Error("format must be 'text' or 'json'");
130
+ const runtimeFields = [
131
+ parsed.values['--session-id'],
132
+ parsed.values['--turn-id'],
133
+ parsed.values['--tool-use-id']
134
+ ];
135
+ if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
136
+ throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
137
+ }
138
+ const runtimeIdentity = runtimeFields.every(Boolean) ? {
139
+ sessionId: runtimeFields[0]!,
140
+ turnId: runtimeFields[1]!,
141
+ toolUseId: runtimeFields[2]!
142
+ } : undefined;
143
+ const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
144
+ if (format === 'text') {
145
+ process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
146
+ } else output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
147
+ return;
148
+ }
149
+
150
+ const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
151
+ if (parsed.operation === 'hook-event') {
152
+ const phase = parsed.values['--event'];
153
+ if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
154
+ throw new Error('hook-event requires a known --event');
155
+ }
156
+ const event = recordFromPayload(phase, await readStdin());
157
+ if (!event) {
158
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
159
+ return;
160
+ }
161
+ const result = store.apply(event);
162
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
163
+ if (result.state.status === 'invalid') process.exitCode = 1;
164
+ return;
165
+ }
166
+
167
+ const childThreadId = parsed.values['--child-id'];
168
+ if (!childThreadId) throw new Error(`operation '${parsed.operation}' requires --child-id`);
169
+ if (parsed.operation === 'resolve-start') {
170
+ if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
171
+ throw new Error('Codex lifecycle hook definition hash is stale');
172
+ }
173
+ const resolved = await resolveCodexThread(childThreadId);
174
+ let latest = store.apply(resolved.resolution.thread);
175
+ for (const reroute of resolved.reroutes) latest = store.apply(reroute);
176
+ latest = store.apply(resolved.resolution.settings);
177
+ output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
178
+ if (latest.state.status !== 'start-ready') process.exitCode = 1;
179
+ return;
180
+ }
181
+ if (parsed.operation === 'resolve-stop') {
182
+ const terminal = await resolveCodexTerminal(childThreadId);
183
+ const latest = store.apply(terminal);
184
+ if (latest.state.status !== 'stop-ready') {
185
+ throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
186
+ }
187
+ output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
188
+ return;
189
+ }
190
+ if (parsed.operation === 'consume') {
191
+ const consumer = parsed.values['--consumer'];
192
+ if (!consumer) throw new Error('consume requires --consumer');
193
+ const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
194
+ output({ status: 'consumed', changed: true, evidence: {
195
+ start: consumed.state.startEvidence,
196
+ stop: consumed.state.stopEvidence
197
+ }, diagnostics: [], error: null });
198
+ }
199
+ } catch (error) {
200
+ failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
201
+ }
202
+ }
203
+
204
+ export { codexLifecycle };
@@ -0,0 +1,24 @@
1
+ import { requestSandboxControl } from '../sandbox/control/client.ts';
2
+ import { serveSandboxControl } from '../sandbox/control/server.ts';
3
+
4
+ function sandboxControl(args: string[]): void {
5
+ const [operation, ...rest] = args;
6
+ if (operation === 'serve') {
7
+ const manifestIndex = rest.indexOf('--manifest');
8
+ const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
9
+ if (!manifest) throw new Error('sandbox-control serve requires --manifest');
10
+ serveSandboxControl(manifest);
11
+ return;
12
+ }
13
+ if (operation === 'client') {
14
+ const [family = '', ...commandArgs] = rest;
15
+ const response = requestSandboxControl({ family, args: commandArgs });
16
+ process.stdout.write(response.stdout);
17
+ process.stderr.write(response.stderr);
18
+ process.exitCode = response.exitCode;
19
+ return;
20
+ }
21
+ throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
22
+ }
23
+
24
+ export { sandboxControl };
@@ -1,13 +1,36 @@
1
- import { applyActivityAppendIntent } from '../task/activity-intent.ts';
2
- import type { ActivityAppendIntent } from '../task/activity-intent.ts';
1
+ import {
2
+ applyPrReviewActivityIntent,
3
+ inspectPrReviewActivity
4
+ } from '../task/activity-intent.ts';
5
+ import type {
6
+ PrReviewActivityIntent,
7
+ PrReviewInspectIntent
8
+ } from '../task/activity-intent.ts';
3
9
 
4
- const USAGE = `Usage: agent-infra-internal task-activity <task-ref> append --step <step> --agent <agent> --note <note> [--artifact <canonical.md>] [--dry-run]
10
+ const USAGE = `Usage: agent-infra-internal task-activity <task-ref> pr-review-inspect
11
+ agent-infra-internal task-activity <task-ref> pr-review-start --agent <agent> --artifact <canonical.md> --head <40hex> [--dry-run]
12
+ agent-infra-internal task-activity <task-ref> pr-review-complete --agent <agent> --artifact <canonical.md> --head <40hex> --verdict <approved|changes-requested|commented> --blockers <N> --major <N> --minor <N> [--dry-run]
13
+ agent-infra-internal task-activity <task-ref> pr-review-terminate --agent <agent> --artifact <canonical.md> --head <40hex> --outcome <aborted|superseded> --reason <single-line> [--dry-run]
5
14
  `;
15
+
16
+ const OPERATIONS = new Set(['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate']);
6
17
  const FLAGS: Record<string, string> = {
7
- '--step': 'step',
8
18
  '--agent': 'agent',
9
- '--note': 'note',
10
- '--artifact': 'artifact'
19
+ '--artifact': 'artifact',
20
+ '--head': 'head',
21
+ '--verdict': 'verdict',
22
+ '--blockers': 'blockers',
23
+ '--major': 'major',
24
+ '--minor': 'minor',
25
+ '--outcome': 'outcome',
26
+ '--reason': 'reason'
27
+ };
28
+ const COMMON = ['agent', 'artifact', 'head'] as const;
29
+ const ALLOWED: Record<string, ReadonlySet<string>> = {
30
+ 'pr-review-inspect': new Set(['kind', 'taskRef']),
31
+ 'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun']),
32
+ 'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun']),
33
+ 'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun'])
11
34
  };
12
35
 
13
36
  function usageFailure(message: string): void {
@@ -19,8 +42,8 @@ function usageFailure(message: string): void {
19
42
  function taskActivity(args: string[] = []): void {
20
43
  if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
21
44
  const [taskRef, kind] = args;
22
- if (!taskRef || taskRef.startsWith('--') || !kind || kind !== 'append') {
23
- usageFailure('task ref and the append intent are required');
45
+ if (!taskRef || taskRef.startsWith('--') || !kind || !OPERATIONS.has(kind)) {
46
+ usageFailure('task ref and a supported PR review intent are required');
24
47
  return;
25
48
  }
26
49
  const values: Record<string, unknown> = { kind, taskRef };
@@ -39,15 +62,28 @@ function taskActivity(args: string[] = []): void {
39
62
  const value = args[++index];
40
63
  if (value === undefined || value.startsWith('--')) { usageFailure(`option '${flag}' requires a value`); return; }
41
64
  seen.add(flag);
42
- values[key] = value;
65
+ values[key] = ['blockers', 'major', 'minor'].includes(key) ? Number(value) : value;
43
66
  }
44
- const allowed = new Set(['kind', 'taskRef', 'dryRun', 'step', 'agent', 'note', 'artifact']);
67
+
68
+ const allowed = ALLOWED[kind]!;
45
69
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
46
- if (unexpected) { usageFailure(`append does not accept '${unexpected}'`); return; }
47
- for (const key of ['step', 'agent', 'note']) {
48
- if (values[key] === undefined) { usageFailure(`append requires '${key}'`); return; }
70
+ if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
71
+ if (kind === 'pr-review-inspect') {
72
+ const result = inspectPrReviewActivity(values as PrReviewInspectIntent);
73
+ process.stdout.write(`${JSON.stringify(result)}\n`);
74
+ if (result.status === 'failed') process.exitCode = 1;
75
+ return;
76
+ }
77
+ for (const key of COMMON) {
78
+ if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
79
+ }
80
+ const operationRequired = kind === 'pr-review-complete'
81
+ ? ['verdict', 'blockers', 'major', 'minor']
82
+ : kind === 'pr-review-terminate' ? ['outcome', 'reason'] : [];
83
+ for (const key of operationRequired) {
84
+ if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
49
85
  }
50
- const result = applyActivityAppendIntent(values as ActivityAppendIntent);
86
+ const result = applyPrReviewActivityIntent(values as PrReviewActivityIntent);
51
87
  process.stdout.write(`${JSON.stringify(result)}\n`);
52
88
  if (result.status === 'failed') process.exitCode = 1;
53
89
  }
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { requestSandboxTaskCreate } from '../sandbox/control/client.ts';
5
+ import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.ts';
6
+ import { validateTaskCreateCandidate } from '../task/create.ts';
7
+ import { createTask, type TaskCreateResult } from '../task/create-service.ts';
8
+
9
+ function failed(code: string, message: string): TaskCreateResult {
10
+ return {
11
+ status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
12
+ operations: [], warnings: [], error: { code, message, retryable: false }
13
+ };
14
+ }
15
+
16
+ function output(result: TaskCreateResult): void {
17
+ process.stdout.write(`${JSON.stringify(result)}\n`);
18
+ process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
19
+ }
20
+
21
+ function taskCreate(args: string[]): void {
22
+ const inputIndex = args.indexOf('--input');
23
+ const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
24
+ if (!input || args.length !== 2 || inputIndex !== 0) {
25
+ output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
26
+ return;
27
+ }
28
+ try {
29
+ const inputPath = path.resolve(input);
30
+ const stat = fs.lstatSync(inputPath);
31
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
32
+ if (stat.size > SANDBOX_CONTROL_MAX_BYTES) throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
33
+ const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
34
+ if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
35
+ const response = requestSandboxTaskCreate({ candidate });
36
+ process.stdout.write(response.stdout);
37
+ process.stderr.write(response.stderr);
38
+ process.exitCode = response.exitCode;
39
+ return;
40
+ }
41
+ output(createTask(candidate, { repoRoot: process.cwd() }));
42
+ } catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
45
+ output(failed(code, message));
46
+ }
47
+ }
48
+
49
+ export { taskCreate };
@@ -9,9 +9,10 @@ const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [inte
9
9
  const FLAGS: Record<string, string> = {
10
10
  '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
11
11
  '--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
12
- '--status': 'status', '--artifact': 'artifact'
12
+ '--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
13
13
  };
14
14
  const NUMERIC = new Set(['ordinal', 'round']);
15
+ const BOOLEAN = new Set(['needsImplementation']);
15
16
 
16
17
  function usageFailure(message: string): void {
17
18
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LEDGER_PAYLOAD_INVALID', message } })}\n`);
@@ -39,7 +40,11 @@ function taskLedger(args: string[] = []): void {
39
40
  if (seen.has(flag)) { usageFailure(`duplicate option '${flag}'`); return; }
40
41
  const value = args[++index];
41
42
  if (value === undefined || value.startsWith('--')) { usageFailure(`option '${flag}' requires a value`); return; }
42
- seen.add(flag); values[key] = NUMERIC.has(key) ? Number(value) : value;
43
+ if (BOOLEAN.has(key) && value !== 'true' && value !== 'false') {
44
+ usageFailure(`option '${flag}' must be true or false`); return;
45
+ }
46
+ seen.add(flag);
47
+ values[key] = NUMERIC.has(key) ? Number(value) : BOOLEAN.has(key) ? value === 'true' : value;
43
48
  }
44
49
  const required: Record<string, string[]> = {
45
50
  'finding-upsert': ['stage', 'reviewArtifact', 'ordinal', 'severity', 'evidence'],
@@ -49,7 +54,11 @@ function taskLedger(args: string[] = []): void {
49
54
  'decision-upsert': ['id', 'stage', 'artifact'],
50
55
  'stage-status': ['stage']
51
56
  };
52
- const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!]);
57
+ const optional: Record<string, string[]> = {
58
+ 'finding-review': ['needsImplementation'],
59
+ 'decision-upsert': ['needsImplementation']
60
+ };
61
+ const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? [])]);
53
62
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
54
63
  const missing = required[kind]!.find((key) => values[key] === undefined);
55
64
  if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
@@ -1,6 +1,8 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
2
  import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.ts';
3
3
  import type { TaskLifecycleRequest } from '../task/lifecycle.ts';
4
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
5
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
4
6
 
5
7
  const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
6
8
  const FLAGS: Record<string, string> = {
@@ -37,7 +39,26 @@ function taskLifecycle(args: string[] = []): void {
37
39
  const agent = normalizeAgentToken(String(values.agent ?? ''));
38
40
  if (!agent) { usageFailure(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
39
41
  values.agent = agent;
40
- const result = applyTaskLifecycle(values as TaskLifecycleRequest);
42
+ const resolved = resolveTaskRef(String(values.taskRef));
43
+ if (!resolved.ok) {
44
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ let result: ReturnType<typeof applyTaskLifecycle>;
49
+ try {
50
+ result = withTaskExecutionLock(
51
+ resolved.repoRoot,
52
+ resolved.taskId,
53
+ `task-lifecycle.${String(values.intent)}`,
54
+ () => applyTaskLifecycle(values as TaskLifecycleRequest)
55
+ );
56
+ } catch (error) {
57
+ if (!(error instanceof TaskExecutionLockError)) throw error;
58
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
41
62
  process.stdout.write(`${JSON.stringify(result)}\n`);
42
63
  if (result.status === 'failed') process.exitCode = 1;
43
64
  }