@fitlab-ai/agent-infra 0.9.3 → 0.9.4

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 (175) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -123
  6. package/dist/lib/agent-clients/adapter.js +92 -2
  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/opencode-sandbox.js +108 -0
  12. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  13. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  14. package/dist/lib/agent-clients/registry.js +9 -1
  15. package/dist/lib/internal/sandbox-control.js +24 -0
  16. package/dist/lib/internal/task-activity.js +45 -13
  17. package/dist/lib/internal/task-ledger.js +12 -3
  18. package/dist/lib/internal/task-lifecycle.js +19 -1
  19. package/dist/lib/internal/task-orchestration.js +122 -18
  20. package/dist/lib/platform/github-client.js +19 -2
  21. package/dist/lib/platform/pr-checks.js +1 -1
  22. package/dist/lib/platform/pull-requests.js +47 -1
  23. package/dist/lib/platform/releases.js +13 -5
  24. package/dist/lib/process-data/index.js +239 -0
  25. package/dist/lib/process-data/normalize.js +198 -0
  26. package/dist/lib/process-data/privacy.js +33 -0
  27. package/dist/lib/process-data/reconcile.js +95 -0
  28. package/dist/lib/process-data/repair.js +58 -0
  29. package/dist/lib/process-data/sources.js +345 -0
  30. package/dist/lib/process-data/store.js +282 -0
  31. package/dist/lib/process-data/types.js +2 -0
  32. package/dist/lib/run/index.js +4 -3
  33. package/dist/lib/run/tui.js +7 -8
  34. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  35. package/dist/lib/sandbox/capture.js +7 -2
  36. package/dist/lib/sandbox/commands/create.js +109 -228
  37. package/dist/lib/sandbox/commands/enter.js +15 -47
  38. package/dist/lib/sandbox/commands/list-running.js +16 -6
  39. package/dist/lib/sandbox/commands/ls.js +10 -6
  40. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  41. package/dist/lib/sandbox/commands/refresh.js +4 -84
  42. package/dist/lib/sandbox/commands/rm.js +24 -3
  43. package/dist/lib/sandbox/commands/show.js +12 -3
  44. package/dist/lib/sandbox/commands/start.js +7 -4
  45. package/dist/lib/sandbox/config.js +2 -0
  46. package/dist/lib/sandbox/constants.js +6 -0
  47. package/dist/lib/sandbox/control/client.js +44 -0
  48. package/dist/lib/sandbox/control/protocol.js +35 -0
  49. package/dist/lib/sandbox/control/server.js +109 -0
  50. package/dist/lib/sandbox/dockerfile.js +36 -13
  51. package/dist/lib/sandbox/mounts.js +17 -4
  52. package/dist/lib/sandbox/recovery.js +128 -5
  53. package/dist/lib/sandbox/redaction.js +12 -0
  54. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  55. package/dist/lib/sandbox/workspace-identity.js +95 -0
  56. package/dist/lib/sandbox/workspace-view.js +103 -0
  57. package/dist/lib/task/activity-intent.js +257 -51
  58. package/dist/lib/task/commit-finalization.js +267 -0
  59. package/dist/lib/task/commit-intent.js +165 -0
  60. package/dist/lib/task/decision-intents.js +31 -17
  61. package/dist/lib/task/delegation-receipts.js +2 -2
  62. package/dist/lib/task/implementation-inputs.js +54 -7
  63. package/dist/lib/task/ledger-intents.js +35 -1
  64. package/dist/lib/task/orchestration.js +363 -28
  65. package/dist/lib/task/resolve-ref.js +41 -1
  66. package/lib/agent-clients/adapter.ts +126 -2
  67. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  68. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  69. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  71. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  72. package/lib/agent-clients/adapters/opencode.ts +18 -3
  73. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  74. package/lib/agent-clients/registry.ts +9 -1
  75. package/lib/internal/sandbox-control.ts +24 -0
  76. package/lib/internal/task-activity.ts +50 -14
  77. package/lib/internal/task-ledger.ts +12 -3
  78. package/lib/internal/task-lifecycle.ts +22 -1
  79. package/lib/internal/task-orchestration.ts +81 -11
  80. package/lib/platform/github-client.ts +20 -2
  81. package/lib/platform/pr-checks.ts +1 -1
  82. package/lib/platform/pull-requests.ts +49 -1
  83. package/lib/platform/releases.ts +16 -4
  84. package/lib/process-data/index.ts +224 -0
  85. package/lib/process-data/normalize.ts +202 -0
  86. package/lib/process-data/privacy.ts +41 -0
  87. package/lib/process-data/reconcile.ts +110 -0
  88. package/lib/process-data/repair.ts +64 -0
  89. package/lib/process-data/sources.ts +354 -0
  90. package/lib/process-data/store.ts +315 -0
  91. package/lib/process-data/types.ts +135 -0
  92. package/lib/run/index.ts +5 -3
  93. package/lib/run/tui.ts +11 -8
  94. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  95. package/lib/sandbox/capture.ts +9 -2
  96. package/lib/sandbox/commands/create.ts +126 -260
  97. package/lib/sandbox/commands/enter.ts +13 -59
  98. package/lib/sandbox/commands/list-running.ts +23 -6
  99. package/lib/sandbox/commands/ls.ts +22 -6
  100. package/lib/sandbox/commands/rebuild.ts +4 -3
  101. package/lib/sandbox/commands/refresh.ts +4 -126
  102. package/lib/sandbox/commands/rm.ts +24 -3
  103. package/lib/sandbox/commands/show.ts +29 -3
  104. package/lib/sandbox/commands/start.ts +6 -4
  105. package/lib/sandbox/config.ts +4 -0
  106. package/lib/sandbox/constants.ts +8 -0
  107. package/lib/sandbox/control/client.ts +55 -0
  108. package/lib/sandbox/control/protocol.ts +75 -0
  109. package/lib/sandbox/control/server.ts +117 -0
  110. package/lib/sandbox/dockerfile.ts +66 -13
  111. package/lib/sandbox/mounts.ts +24 -4
  112. package/lib/sandbox/recovery.ts +163 -6
  113. package/lib/sandbox/redaction.ts +14 -0
  114. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  115. package/lib/sandbox/tool-types.ts +16 -0
  116. package/lib/sandbox/workspace-identity.ts +123 -0
  117. package/lib/sandbox/workspace-view.ts +143 -0
  118. package/lib/task/activity-intent.ts +350 -61
  119. package/lib/task/commit-finalization.ts +356 -0
  120. package/lib/task/commit-intent.ts +230 -0
  121. package/lib/task/decision-intents.ts +32 -12
  122. package/lib/task/delegation-receipts.ts +2 -2
  123. package/lib/task/implementation-inputs.ts +62 -8
  124. package/lib/task/ledger-intents.ts +43 -3
  125. package/lib/task/orchestration.ts +501 -33
  126. package/lib/task/resolve-ref.ts +46 -1
  127. package/package.json +1 -1
  128. package/templates/.agents/rules/README.en.md +1 -1
  129. package/templates/.agents/rules/README.zh-CN.md +1 -1
  130. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  131. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  132. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  133. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  134. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  135. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  136. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  137. package/templates/.agents/rules/task-management.en.md +2 -0
  138. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  139. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.en.md +21 -11
  142. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
  143. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  144. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  149. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  150. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  151. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  152. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  153. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  156. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  157. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  158. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  159. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  161. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  162. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  163. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  164. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  165. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  166. package/templates/.agents/templates/task.en.md +1 -1
  167. package/templates/.agents/templates/task.zh-CN.md +1 -1
  168. package/templates/.claude/commands/watch-pr.en.md +1 -1
  169. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  170. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  171. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  172. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  173. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  174. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  175. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type {
5
+ AgentClientSandboxHook,
6
+ AgentClientSandboxRecoveryCheck
7
+ } from '../../sandbox/tool-types.ts';
8
+
9
+ type JsonObject = Record<string, unknown>;
10
+
11
+ function readJsonObject(filePath: string): JsonObject | null {
12
+ try {
13
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
14
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
15
+ ? parsed as JsonObject
16
+ : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function resolveXdgRoot(value: string | undefined, fallback: string): string {
23
+ return typeof value === 'string' && value !== '' && path.isAbsolute(value)
24
+ ? value
25
+ : fallback;
26
+ }
27
+
28
+ function ensureOpenCodeSandboxState(
29
+ toolDir: string,
30
+ hostHome: string,
31
+ hostEnv: Readonly<NodeJS.ProcessEnv> = {}
32
+ ): void {
33
+ const canonicalConfigDir = path.join(toolDir, '.xdg', 'config', 'opencode');
34
+ const canonicalStateDir = path.join(toolDir, '.xdg', 'state', 'opencode');
35
+ const canonicalConfigPath = path.join(canonicalConfigDir, 'opencode.json');
36
+ const legacyConfigPath = path.join(toolDir, 'opencode.json');
37
+ fs.mkdirSync(canonicalConfigDir, { recursive: true });
38
+ fs.mkdirSync(canonicalStateDir, { recursive: true });
39
+
40
+ if (!fs.existsSync(canonicalConfigPath) && fs.existsSync(legacyConfigPath)) {
41
+ fs.copyFileSync(legacyConfigPath, canonicalConfigPath, fs.constants.COPYFILE_EXCL);
42
+ }
43
+
44
+ const hostConfigRoot = resolveXdgRoot(
45
+ hostEnv.XDG_CONFIG_HOME,
46
+ path.join(hostHome, '.config')
47
+ );
48
+ const hostConfig = readJsonObject(
49
+ path.join(hostConfigRoot, 'opencode', 'opencode.json')
50
+ );
51
+ if (!hostConfig) {
52
+ return;
53
+ }
54
+
55
+ let sandboxConfig: JsonObject = {};
56
+ if (fs.existsSync(canonicalConfigPath)) {
57
+ const existing = readJsonObject(canonicalConfigPath);
58
+ if (!existing) {
59
+ return;
60
+ }
61
+ sandboxConfig = existing;
62
+ }
63
+
64
+ let changed = false;
65
+ for (const key of ['model', 'small_model']) {
66
+ if (Object.hasOwn(sandboxConfig, key)) {
67
+ continue;
68
+ }
69
+ const value = hostConfig[key];
70
+ if (typeof value === 'string' && value !== '') {
71
+ sandboxConfig[key] = value;
72
+ changed = true;
73
+ }
74
+ }
75
+ if (changed) {
76
+ fs.writeFileSync(canonicalConfigPath, JSON.stringify(sandboxConfig, null, 2), 'utf8');
77
+ }
78
+ }
79
+
80
+ const opencodeBeforeContainerCreateHook: AgentClientSandboxHook = {
81
+ id: 'opencode-before-container-create',
82
+ phase: 'before-container-create',
83
+ async run(context) {
84
+ const create = context.create;
85
+ const entry = create?.resolvedTools.find(({ tool }) => tool.id === 'opencode');
86
+ if (!create || !entry) {
87
+ return {
88
+ status: 'fatal',
89
+ message: 'OpenCode sandbox hook requires its resolved tool state.'
90
+ };
91
+ }
92
+ ensureOpenCodeSandboxState(entry.dir, create.hostHome, create.hostEnv);
93
+ return { status: 'ready' };
94
+ }
95
+ };
96
+
97
+ const opencodeRecoveryChecks: readonly AgentClientSandboxRecoveryCheck[] = [
98
+ {
99
+ id: 'command-available',
100
+ probe: { script: 'command -v "$1"', args: ['opencode'] },
101
+ finding: {
102
+ repairKind: 'hard-failure',
103
+ message: 'OpenCode is not available on PATH inside the sandbox.'
104
+ }
105
+ },
106
+ {
107
+ id: 'config-writable',
108
+ probe: {
109
+ script: 'probe="$1/.agent-infra-opencode-config-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
110
+ args: ['/home/devuser/.local/share/opencode/.xdg/config/opencode']
111
+ },
112
+ finding: {
113
+ repairKind: 'permissions',
114
+ message: 'OpenCode config directory is not writable by devuser.',
115
+ path: '/home/devuser/.local/share/opencode/.xdg/config/opencode'
116
+ }
117
+ },
118
+ {
119
+ id: 'state-writable',
120
+ probe: {
121
+ script: 'probe="$1/.agent-infra-opencode-state-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
122
+ args: ['/home/devuser/.local/share/opencode/.xdg/state/opencode']
123
+ },
124
+ finding: {
125
+ repairKind: 'permissions',
126
+ message: 'OpenCode state directory is not writable by devuser.',
127
+ path: '/home/devuser/.local/share/opencode/.xdg/state/opencode'
128
+ }
129
+ }
130
+ ];
131
+
132
+ export {
133
+ ensureOpenCodeSandboxState,
134
+ opencodeBeforeContainerCreateHook,
135
+ opencodeRecoveryChecks
136
+ };
@@ -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,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
  }
@@ -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
  }
@@ -1,21 +1,26 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
2
  import {
3
+ abortCommitIntent,
3
4
  activateMatchingOrchestrationDelegation,
4
5
  activateOrchestrationDelegation,
5
6
  advanceOrchestration,
7
+ beginCommitIntent,
6
8
  beginOrResumeOrchestration,
7
- completeCommitOrchestrationStage,
9
+ checkpointCommitIntent,
10
+ completeCommitIntent,
8
11
  pauseOrchestration,
9
12
  prepareOrchestrationDelegation,
13
+ recoverCommitIntent,
10
14
  routeOrchestration,
11
15
  sealMatchingOrchestrationDelegation,
12
16
  sealOrchestrationDelegation,
17
+ statusCommitIntent,
13
18
  statusOrchestration
14
19
  } from '../task/orchestration.ts';
15
20
  import { isAgentClientId } from '../agent-clients/types.ts';
16
21
  import type { AgentClientId } from '../agent-clients/types.ts';
17
22
 
18
- const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|stage-completed|hook-stop|advance|pause|status> [options]\n';
23
+ const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-status> [options]\n';
19
24
 
20
25
  function usageFailure(message: string): void {
21
26
  process.stdout.write(`${JSON.stringify({
@@ -36,21 +41,32 @@ function taskOrchestration(args: string[] = []): void {
36
41
  return;
37
42
  }
38
43
  const [taskRef, intent] = args;
39
- if (!['begin-or-resume', 'route', 'prepare', 'hook-start', 'stage-completed', 'hook-stop', 'advance', 'pause', 'status'].includes(intent!)) {
44
+ if (![
45
+ 'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
46
+ 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
47
+ ].includes(intent!)) {
40
48
  usageFailure(`unknown intent '${intent}'`);
41
49
  return;
42
50
  }
43
51
  const values: Record<string, string> = {};
44
52
  const seen = new Set<string>();
53
+ let orchestrated = false;
45
54
  for (let index = 2; index < args.length; index += 1) {
46
55
  const flag = args[index]!;
56
+ if (flag === '--orchestrated') {
57
+ if (seen.has(flag)) { usageFailure(`duplicate option '${flag}'`); return; }
58
+ seen.add(flag);
59
+ orchestrated = true;
60
+ continue;
61
+ }
47
62
  if (![
48
63
  '--max-steps', '--executor-model', '--executor-reasoning-effort',
49
64
  '--reviewer-model', '--reviewer-reasoning-effort',
50
65
  '--client', '--requested-model', '--requested-reasoning-effort', '--parent-id', '--before-fingerprint',
51
66
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
52
67
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
53
- '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent'
68
+ '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
69
+ '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
54
70
  ].includes(flag)) {
55
71
  usageFailure(`unknown option '${flag}'`);
56
72
  return;
@@ -67,13 +83,73 @@ function taskOrchestration(args: string[] = []): void {
67
83
  }
68
84
  values[flag] = value;
69
85
  }
86
+ const commitIntentFlags: Record<string, readonly string[]> = {
87
+ 'commit-begin': ['--agent', '--baseline-head', '--orchestrated'],
88
+ 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref'],
89
+ 'commit-complete': ['--token', '--agent'],
90
+ 'commit-recover': ['--agent'],
91
+ 'commit-abort': ['--token', '--expected-head'],
92
+ 'commit-status': []
93
+ };
94
+ if (intent! in commitIntentFlags) {
95
+ const allowed = commitIntentFlags[intent!]!;
96
+ const invalid = [...seen].find((flag) => !allowed.includes(flag));
97
+ if (invalid) { usageFailure(`intent '${intent}' does not accept '${invalid}'`); return; }
98
+ } else {
99
+ const commitOnly = [...seen].find((flag) => [
100
+ '--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
101
+ ].includes(flag));
102
+ if (commitOnly) { usageFailure(`intent '${intent}' does not accept '${commitOnly}'`); return; }
103
+ }
70
104
  const requireValues = (flags: string[]) => flags.find((flag) => values[flag] === undefined);
71
105
  if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
72
106
  usageFailure(`unknown client '${values['--client']}'`);
73
107
  return;
74
108
  }
75
109
  let result;
76
- if (intent === 'begin-or-resume') {
110
+ if (intent === 'commit-begin') {
111
+ const missing = requireValues(['--agent', '--baseline-head']);
112
+ if (missing) { usageFailure(`intent 'commit-begin' requires '${missing}'`); return; }
113
+ const agent = normalizeAgentToken(values['--agent']!);
114
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
115
+ result = beginCommitIntent(taskRef!, {
116
+ agent, orchestrated, baselineHead: values['--baseline-head']!
117
+ });
118
+ } else if (intent === 'commit-checkpoint') {
119
+ const missing = requireValues(['--token', '--kind', '--head']);
120
+ if (missing) { usageFailure(`intent 'commit-checkpoint' requires '${missing}'`); return; }
121
+ if (!['committed', 'pushed'].includes(values['--kind']!)) {
122
+ usageFailure('--kind must be committed or pushed'); return;
123
+ }
124
+ if (values['--kind'] === 'pushed') {
125
+ const pushedMissing = requireValues(['--remote', '--ref']);
126
+ if (pushedMissing) { usageFailure(`pushed checkpoint requires '${pushedMissing}'`); return; }
127
+ } else if (values['--remote'] !== undefined || values['--ref'] !== undefined) {
128
+ usageFailure('committed checkpoint does not accept --remote or --ref'); return;
129
+ }
130
+ result = checkpointCommitIntent(taskRef!, {
131
+ token: values['--token']!, kind: values['--kind'] as 'committed' | 'pushed',
132
+ head: values['--head']!, remote: values['--remote'], ref: values['--ref']
133
+ });
134
+ } else if (intent === 'commit-complete') {
135
+ const missing = requireValues(['--token', '--agent']);
136
+ if (missing) { usageFailure(`intent 'commit-complete' requires '${missing}'`); return; }
137
+ const agent = normalizeAgentToken(values['--agent']!);
138
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
139
+ result = completeCommitIntent(taskRef!, { token: values['--token']!, agent });
140
+ } else if (intent === 'commit-recover') {
141
+ const missing = requireValues(['--agent']);
142
+ if (missing) { usageFailure(`intent 'commit-recover' requires '${missing}'`); return; }
143
+ const agent = normalizeAgentToken(values['--agent']!);
144
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
145
+ result = recoverCommitIntent(taskRef!, { agent });
146
+ } else if (intent === 'commit-abort') {
147
+ const missing = requireValues(['--token', '--expected-head']);
148
+ if (missing) { usageFailure(`intent 'commit-abort' requires '${missing}'`); return; }
149
+ result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! });
150
+ } else if (intent === 'commit-status') {
151
+ result = statusCommitIntent(taskRef!);
152
+ } else if (intent === 'begin-or-resume') {
77
153
  const missing = requireValues(['--client']);
78
154
  if (missing) { usageFailure(`intent 'begin-or-resume' requires '${missing}'`); return; }
79
155
  const maxSteps = values['--max-steps'] === undefined ? undefined : Number(values['--max-steps']);
@@ -150,12 +226,6 @@ function taskOrchestration(args: string[] = []): void {
150
226
  changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
151
227
  });
152
228
  }
153
- } else if (intent === 'stage-completed') {
154
- const missing = requireValues(['--agent']);
155
- if (missing) { usageFailure(`intent 'stage-completed' requires '${missing}'`); return; }
156
- const agent = normalizeAgentToken(values['--agent']!);
157
- if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
158
- result = completeCommitOrchestrationStage(taskRef!, agent);
159
229
  } else if (intent === 'advance') {
160
230
  result = advanceOrchestration(taskRef!);
161
231
  } else {
@@ -24,6 +24,15 @@ type ClientOptions = {
24
24
  };
25
25
 
26
26
  const MINIMUM_GITHUB_CLI_VERSION = '2.16.0';
27
+ const GITHUB_CLI_MAX_BUFFER = 64 * 1024 * 1024;
28
+ const ERROR_DETAIL_LIMIT = 4096;
29
+
30
+ function boundedFailureDetail(result: RunResult): string {
31
+ const diagnostic = `${result.stderr}\n${result.error?.message || ''}`.trim() || result.stdout.trim();
32
+ return diagnostic.length <= ERROR_DETAIL_LIMIT
33
+ ? diagnostic
34
+ : `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
35
+ }
27
36
 
28
37
  function defaultRunner(args: string[], options: RunOptions): RunResult {
29
38
  const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
@@ -38,6 +47,7 @@ function defaultRunner(args: string[], options: RunOptions): RunResult {
38
47
  encoding: 'utf8',
39
48
  env: process.env,
40
49
  input: options.input,
50
+ maxBuffer: GITHUB_CLI_MAX_BUFFER,
41
51
  shell: false
42
52
  });
43
53
  return {
@@ -60,7 +70,15 @@ function defaultSleep(delayMs: number): void {
60
70
  }
61
71
 
62
72
  function classifyGitHubFailure(result: RunResult): PlatformError {
63
- const detail = `${result.stderr}\n${result.stdout}\n${result.error?.message || ''}`.trim();
73
+ const errorCode = result.error && 'code' in result.error ? (result.error as NodeJS.ErrnoException).code : undefined;
74
+ if (errorCode === 'ENOBUFS') {
75
+ return {
76
+ code: 'PLATFORM_OUTPUT_TOO_LARGE',
77
+ message: 'GitHub CLI output exceeded the configured limit',
78
+ retryable: false
79
+ };
80
+ }
81
+ const detail = boundedFailureDetail(result);
64
82
  const lower = detail.toLowerCase();
65
83
  if (/\b401\b|bad credentials|authentication required|not logged into/.test(lower)) {
66
84
  return { code: 'AUTH_REQUIRED', message: detail || 'GitHub authentication is required', retryable: false };
@@ -77,7 +95,7 @@ function classifyGitHubFailure(result: RunResult): PlatformError {
77
95
  if (/\b422\b|validation failed/.test(lower)) {
78
96
  return { code: 'PLATFORM_REQUEST_INVALID', message: detail || 'GitHub rejected the request', retryable: false };
79
97
  }
80
- if (result.error && 'code' in result.error && (result.error as NodeJS.ErrnoException).code === 'ENOENT') {
98
+ if (errorCode === 'ENOENT') {
81
99
  return { code: 'PLATFORM_DEPENDENCY_MISSING', message: detail || 'GitHub CLI is unavailable', retryable: false };
82
100
  }
83
101
  return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', retryable: false };