@fitlab-ai/agent-infra 0.9.1 → 0.9.2

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 (283) hide show
  1. package/README.md +16 -4
  2. package/README.zh-CN.md +16 -4
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +15 -0
  6. package/dist/lib/agent-client.js +163 -0
  7. package/dist/lib/agent-clients/adapter.js +294 -0
  8. package/dist/lib/agent-clients/adapters/claude-code.js +62 -0
  9. package/dist/lib/agent-clients/adapters/codex-sandbox.js +210 -0
  10. package/dist/lib/agent-clients/adapters/codex.js +54 -0
  11. package/dist/lib/agent-clients/adapters/gemini-cli.js +64 -0
  12. package/dist/lib/agent-clients/adapters/index.js +20 -0
  13. package/dist/lib/agent-clients/adapters/opencode.js +64 -0
  14. package/dist/lib/agent-clients/config.js +177 -0
  15. package/dist/lib/agent-clients/custom-tuis.js +99 -0
  16. package/dist/lib/agent-clients/invocation.js +9 -0
  17. package/dist/lib/agent-clients/next-steps.js +38 -0
  18. package/dist/lib/agent-clients/project-assets.js +45 -0
  19. package/dist/lib/agent-clients/reconcile.js +235 -0
  20. package/dist/lib/agent-clients/registry.js +72 -0
  21. package/dist/lib/agent-clients/schema.js +57 -0
  22. package/dist/lib/agent-clients/types.js +29 -0
  23. package/dist/lib/builtin-tuis.js +9 -14
  24. package/dist/lib/defaults.json +85 -73
  25. package/dist/lib/git/review-snapshot.js +25 -10
  26. package/dist/lib/git/reviewed-commit-equivalence.js +110 -0
  27. package/dist/lib/init.js +47 -91
  28. package/dist/lib/internal/agent-client.js +117 -0
  29. package/dist/lib/internal/git-workflow.js +1 -1
  30. package/dist/lib/internal/release-workflow.js +71 -16
  31. package/dist/lib/internal/task-orchestration.js +151 -0
  32. package/dist/lib/internal/task-review.js +65 -0
  33. package/dist/lib/merge.js +2 -1
  34. package/dist/lib/platform/adapters.js +38 -1
  35. package/dist/lib/platform/change-request-git-evidence.js +113 -0
  36. package/dist/lib/platform/merged-pr-equivalence.js +41 -11
  37. package/dist/lib/platform/pr-checks.js +38 -10
  38. package/dist/lib/platform/pull-requests.js +110 -6
  39. package/dist/lib/platform/releases.js +1 -1
  40. package/dist/lib/platform/verification-required.js +8 -10
  41. package/dist/lib/prompt.js +15 -3
  42. package/dist/lib/release/channels.js +4 -1
  43. package/dist/lib/release/workflow.js +4 -2
  44. package/dist/lib/remove-dir.js +25 -0
  45. package/dist/lib/run/tui.js +8 -2
  46. package/dist/lib/sandbox/agent-client-reconciler.js +234 -0
  47. package/dist/lib/sandbox/clipboard/linux.js +2 -1
  48. package/dist/lib/sandbox/commands/create.js +76 -180
  49. package/dist/lib/sandbox/commands/enter.js +17 -1
  50. package/dist/lib/sandbox/commands/prune.js +2 -2
  51. package/dist/lib/sandbox/commands/rm.js +3 -2
  52. package/dist/lib/sandbox/config.js +21 -4
  53. package/dist/lib/sandbox/constants.js +3 -0
  54. package/dist/lib/sandbox/dockerfile.js +2 -1
  55. package/dist/lib/sandbox/managed-fs.js +10 -5
  56. package/dist/lib/sandbox/recovery.js +52 -36
  57. package/dist/lib/sandbox/shell.js +1 -1
  58. package/dist/lib/sandbox/tool-types.js +8 -0
  59. package/dist/lib/sandbox/tools.js +2 -126
  60. package/dist/lib/task/artifact-lifecycle.js +1 -49
  61. package/dist/lib/task/commands/decisions.js +50 -20
  62. package/dist/lib/task/delegation-receipts.js +107 -0
  63. package/dist/lib/task/events.js +100 -1
  64. package/dist/lib/task/orchestration.js +420 -0
  65. package/dist/lib/task/review-artifacts.js +143 -59
  66. package/dist/lib/task/review-finalization.js +166 -0
  67. package/dist/lib/task/review-fingerprint.js +4 -0
  68. package/dist/lib/task/verification-checks.js +2 -1
  69. package/dist/lib/task/verification-engine.js +156 -28
  70. package/dist/lib/task/verification-review.js +1 -1
  71. package/dist/lib/task/verification.js +3 -1
  72. package/dist/lib/task/workspace-snapshot.js +34 -0
  73. package/dist/lib/update.js +42 -161
  74. package/lib/agent-client.ts +214 -0
  75. package/lib/agent-clients/adapter.ts +461 -0
  76. package/lib/agent-clients/adapters/claude-code.ts +70 -0
  77. package/lib/agent-clients/adapters/codex-sandbox.ts +245 -0
  78. package/lib/agent-clients/adapters/codex.ts +58 -0
  79. package/lib/agent-clients/adapters/gemini-cli.ts +65 -0
  80. package/lib/agent-clients/adapters/index.ts +27 -0
  81. package/lib/agent-clients/adapters/opencode.ts +66 -0
  82. package/lib/agent-clients/config.ts +278 -0
  83. package/lib/agent-clients/custom-tuis.ts +149 -0
  84. package/lib/agent-clients/invocation.ts +19 -0
  85. package/lib/agent-clients/next-steps.ts +72 -0
  86. package/lib/agent-clients/project-assets.ts +96 -0
  87. package/lib/agent-clients/reconcile.ts +324 -0
  88. package/lib/agent-clients/registry.ts +144 -0
  89. package/lib/agent-clients/schema.ts +62 -0
  90. package/lib/agent-clients/types.ts +78 -0
  91. package/lib/benchmark/schemas/v1.0.0/case-manifest.schema.json +120 -0
  92. package/lib/benchmark/schemas/v1.0.0/common.schema.json +109 -0
  93. package/lib/benchmark/schemas/v1.0.0/grader-result.schema.json +160 -0
  94. package/lib/benchmark/schemas/v1.0.0/run-manifest.schema.json +122 -0
  95. package/lib/benchmark/schemas/v1.0.0/subject.schema.json +227 -0
  96. package/lib/builtin-tuis.ts +16 -15
  97. package/lib/defaults.json +24 -12
  98. package/lib/git/review-snapshot.ts +39 -9
  99. package/lib/git/reviewed-commit-equivalence.ts +135 -0
  100. package/lib/init.ts +53 -121
  101. package/lib/internal/agent-client.ts +131 -0
  102. package/lib/internal/git-workflow.ts +1 -1
  103. package/lib/internal/release-workflow.ts +68 -16
  104. package/lib/internal/task-orchestration.ts +132 -0
  105. package/lib/internal/task-review.ts +65 -0
  106. package/lib/merge.ts +2 -1
  107. package/lib/platform/adapters.ts +142 -3
  108. package/lib/platform/change-request-git-evidence.ts +159 -0
  109. package/lib/platform/merged-pr-equivalence.ts +51 -14
  110. package/lib/platform/pr-checks.ts +50 -22
  111. package/lib/platform/pull-requests.ts +134 -25
  112. package/lib/platform/releases.ts +1 -1
  113. package/lib/platform/verification-required.ts +12 -10
  114. package/lib/prompt.ts +16 -3
  115. package/lib/release/channels.ts +4 -1
  116. package/lib/release/workflow.ts +4 -3
  117. package/lib/remove-dir.ts +28 -0
  118. package/lib/run/tui.ts +9 -1
  119. package/lib/sandbox/agent-client-reconciler.ts +378 -0
  120. package/lib/sandbox/clipboard/linux.ts +2 -1
  121. package/lib/sandbox/commands/create.ts +111 -192
  122. package/lib/sandbox/commands/enter.ts +23 -1
  123. package/lib/sandbox/commands/prune.ts +2 -2
  124. package/lib/sandbox/commands/rm.ts +3 -2
  125. package/lib/sandbox/config.ts +31 -4
  126. package/lib/sandbox/constants.ts +6 -0
  127. package/lib/sandbox/dockerfile.ts +2 -1
  128. package/lib/sandbox/managed-fs.ts +18 -5
  129. package/lib/sandbox/recovery.ts +98 -67
  130. package/lib/sandbox/shell.ts +1 -1
  131. package/lib/sandbox/tool-types.ts +143 -0
  132. package/lib/sandbox/tools.ts +5 -157
  133. package/lib/task/artifact-lifecycle.ts +1 -38
  134. package/lib/task/commands/decisions.ts +53 -20
  135. package/lib/task/delegation-receipts.ts +170 -0
  136. package/lib/task/events.ts +110 -2
  137. package/lib/task/orchestration.ts +543 -0
  138. package/lib/task/review-artifacts.ts +201 -69
  139. package/lib/task/review-finalization.ts +275 -0
  140. package/lib/task/review-fingerprint.ts +5 -0
  141. package/lib/task/verification-checks.ts +2 -1
  142. package/lib/task/verification-engine.ts +177 -28
  143. package/lib/task/verification-review.ts +1 -0
  144. package/lib/task/verification.ts +5 -2
  145. package/lib/task/workspace-snapshot.ts +36 -0
  146. package/lib/update.ts +48 -184
  147. package/package.json +2 -2
  148. package/templates/.agents/README.en.md +21 -20
  149. package/templates/.agents/README.zh-CN.md +21 -20
  150. package/templates/.agents/hooks/lifecycle-delegation.js +64 -0
  151. package/templates/.agents/rules/README.en.md +2 -0
  152. package/templates/.agents/rules/README.zh-CN.md +2 -0
  153. package/templates/.agents/rules/commit-and-pr.en.md +1 -0
  154. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -0
  155. package/templates/.agents/rules/human-decision-context.en.md +14 -12
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +14 -12
  157. package/templates/.agents/rules/lifecycle-orchestration.en.md +21 -0
  158. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +21 -0
  159. package/templates/.agents/rules/next-step-output.en.md +16 -0
  160. package/templates/.agents/rules/next-step-output.zh-CN.md +16 -0
  161. package/templates/.agents/rules/no-mid-flow-questions.en.md +2 -0
  162. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +2 -0
  163. package/templates/.agents/rules/release-commands.github.en.md +11 -5
  164. package/templates/.agents/rules/release-commands.github.zh-CN.md +11 -5
  165. package/templates/.agents/rules/review-handshake.en.md +4 -4
  166. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  167. package/templates/.agents/rules/review-method.en.md +113 -0
  168. package/templates/.agents/rules/review-method.zh-CN.md +98 -0
  169. package/templates/.agents/skills/analyze-task/SKILL.en.md +5 -5
  170. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +5 -5
  171. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  172. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
  173. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -4
  174. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -5
  175. package/templates/.agents/skills/check-task/SKILL.en.md +16 -16
  176. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -16
  177. package/templates/.agents/skills/close-codescan/SKILL.en.md +4 -4
  178. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +4 -4
  179. package/templates/.agents/skills/close-dependabot/SKILL.en.md +4 -4
  180. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +4 -4
  181. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  182. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  183. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +14 -16
  184. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +14 -16
  185. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  186. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  187. package/templates/.agents/skills/commit/SKILL.en.md +11 -1
  188. package/templates/.agents/skills/commit/SKILL.zh-CN.md +11 -1
  189. package/templates/.agents/skills/commit/reference/task-status-update.en.md +24 -28
  190. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +24 -28
  191. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  192. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +10 -10
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +10 -10
  195. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  196. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  197. package/templates/.agents/skills/create-task/SKILL.en.md +11 -11
  198. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +11 -11
  199. package/templates/.agents/skills/import-codescan/SKILL.en.md +5 -5
  200. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +5 -5
  201. package/templates/.agents/skills/import-dependabot/SKILL.en.md +5 -5
  202. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +5 -5
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  205. package/templates/.agents/skills/init-labels/SKILL.en.md +4 -4
  206. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +4 -4
  207. package/templates/.agents/skills/init-milestones/SKILL.en.md +4 -4
  208. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -4
  209. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  210. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  211. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  212. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  213. package/templates/.agents/skills/release/SKILL.en.md +1 -1
  214. package/templates/.agents/skills/review-analysis/SKILL.en.md +13 -2
  215. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -3
  216. package/templates/.agents/skills/review-analysis/config/verify.en.json +6 -0
  217. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +6 -0
  218. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +13 -15
  219. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +13 -15
  220. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +51 -5
  221. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +52 -6
  222. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +25 -22
  223. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +25 -22
  224. package/templates/.agents/skills/review-code/SKILL.en.md +28 -8
  225. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +28 -8
  226. package/templates/.agents/skills/review-code/config/verify.en.json +7 -0
  227. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +7 -0
  228. package/templates/.agents/skills/review-code/reference/concurrency-risks.en.md +25 -0
  229. package/templates/.agents/skills/review-code/reference/concurrency-risks.zh-CN.md +25 -0
  230. package/templates/.agents/skills/review-code/reference/cross-platform-risks.en.md +27 -0
  231. package/templates/.agents/skills/review-code/reference/cross-platform-risks.zh-CN.md +27 -0
  232. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.en.md +13 -0
  233. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.zh-CN.md +13 -0
  234. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +25 -0
  235. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +25 -0
  236. package/templates/.agents/skills/review-code/reference/output-templates.en.md +22 -24
  237. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +22 -24
  238. package/templates/.agents/skills/review-code/reference/report-template.en.md +54 -7
  239. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +54 -7
  240. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +29 -24
  241. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +29 -24
  242. package/templates/.agents/skills/review-code/reference/security-risks.en.md +25 -0
  243. package/templates/.agents/skills/review-code/reference/security-risks.zh-CN.md +25 -0
  244. package/templates/.agents/skills/review-plan/SKILL.en.md +13 -2
  245. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -3
  246. package/templates/.agents/skills/review-plan/config/verify.en.json +6 -0
  247. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +6 -0
  248. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +13 -15
  249. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +13 -15
  250. package/templates/.agents/skills/review-plan/reference/report-template.en.md +61 -5
  251. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +61 -5
  252. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +21 -24
  253. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +21 -24
  254. package/templates/.agents/skills/run-task/SKILL.en.md +22 -0
  255. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +22 -0
  256. package/templates/.agents/skills/run-task/config/verify.en.json +9 -0
  257. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +9 -0
  258. package/templates/.agents/skills/test/SKILL.en.md +4 -4
  259. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -4
  260. package/templates/.agents/skills/test-integration/SKILL.en.md +4 -4
  261. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +4 -4
  262. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -5
  263. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -5
  264. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +647 -203
  265. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +4 -4
  266. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +4 -4
  267. package/templates/.agents/skills/watch-pr/SKILL.en.md +8 -8
  268. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +8 -8
  269. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +6 -0
  270. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +6 -0
  271. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +6 -0
  272. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +6 -0
  273. package/templates/.claude/commands/run-task.en.md +8 -0
  274. package/templates/.claude/commands/run-task.zh-CN.md +8 -0
  275. package/templates/.claude/settings.json +24 -0
  276. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +3 -0
  277. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +3 -0
  278. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +3 -0
  279. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +3 -0
  280. package/templates/.gemini/commands/_project_/run-task.en.toml +8 -0
  281. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +8 -0
  282. package/templates/.opencode/commands/run-task.en.md +11 -0
  283. package/templates/.opencode/commands/run-task.zh-CN.md +11 -0
package/lib/init.ts CHANGED
@@ -8,13 +8,13 @@ import { resolveTemplateDir } from './paths.ts';
8
8
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.ts';
9
9
  import { enginesForPlatform } from './sandbox/engines/index.ts';
10
10
  import { VERSION } from './version.ts';
11
+ import { listAgentClientAdapters } from './agent-clients/registry.ts';
12
+ import { serializeAgentClients } from './agent-clients/config.ts';
11
13
  import {
12
- BUILTIN_TUI_IDS,
13
- BUILTIN_TUI_DISPLAY,
14
- isPathOwnedByDisabledTUI,
15
- resolveEnabledTUIs
16
- } from './builtin-tuis.ts';
17
- import type { BuiltinTUIId } from './builtin-tuis.ts';
14
+ applyAgentClientReconciliation,
15
+ planAgentClientReconciliation
16
+ } from './agent-clients/reconcile.ts';
17
+ import type { AgentClientState } from './agent-clients/types.ts';
18
18
 
19
19
  type FileRegistry = {
20
20
  managed: string[];
@@ -28,6 +28,7 @@ type SourceEntry = {
28
28
  };
29
29
 
30
30
  type Defaults = {
31
+ agentClients: AgentConfig['agentClients'];
31
32
  files: FileRegistry;
32
33
  sandbox: Record<string, unknown>;
33
34
  task: { shortIdLength: number };
@@ -44,7 +45,7 @@ type AgentConfig = {
44
45
  task: { shortIdLength: number };
45
46
  labels: Record<string, unknown>;
46
47
  files: FileRegistry;
47
- tuis: string[];
48
+ agentClients: ReturnType<typeof serializeAgentClients>;
48
49
  templates?: { sources: SourceEntry[] };
49
50
  skills?: { sources: SourceEntry[] };
50
51
  };
@@ -59,28 +60,6 @@ const PLATFORM_DEFAULT_ENGINES = Object.freeze({
59
60
  win32: 'wsl2'
60
61
  });
61
62
 
62
- function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
63
- const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
64
- if (!top.startsWith('.')) return false;
65
-
66
- const candidate = top.slice(1);
67
- if (!KNOWN_PLATFORMS.has(candidate)) return false;
68
- return candidate !== platformType;
69
- }
70
-
71
- function buildDefaultFiles(platformType: string, enabledTUIs: Set<BuiltinTUIId>): FileRegistry {
72
- const ownedByDisabled = (entry: string) => isPathOwnedByDisabledTUI(entry, enabledTUIs);
73
- return {
74
- managed: (defaults.files.managed || []).filter(
75
- (entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)
76
- ),
77
- merged: (defaults.files.merged || []).filter(
78
- (entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)
79
- ),
80
- ejected: structuredClone(defaults.files.ejected || [])
81
- };
82
- }
83
-
84
63
  function detectProjectName(): string {
85
64
  try {
86
65
  const url = execSync('git remote get-url origin', { stdio: ['pipe', 'pipe', 'pipe'] })
@@ -222,11 +201,12 @@ async function cmdInit(): Promise<void> {
222
201
  );
223
202
  }
224
203
 
225
- let enabledTUIs: string[];
204
+ const adapters = listAgentClientAdapters();
205
+ let enabledClientIds: string[];
226
206
  try {
227
- enabledTUIs = await multiSelect(
228
- 'Built-in TUI command files to install/manage',
229
- BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] }))
207
+ enabledClientIds = await multiSelect(
208
+ 'Agent Client project integrations to enable',
209
+ adapters.map((adapter) => ({ id: adapter.id, label: adapter.displayName }))
230
210
  );
231
211
  } catch (e) {
232
212
  err(e instanceof Error ? e.message : String(e));
@@ -234,7 +214,7 @@ async function cmdInit(): Promise<void> {
234
214
  process.exitCode = 1;
235
215
  return;
236
216
  }
237
- const enabledTUISet = resolveEnabledTUIs(enabledTUIs);
217
+ const enabledClientSet = new Set(enabledClientIds);
238
218
 
239
219
  const templateSources = parseLocalSources(await prompt(
240
220
  'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
@@ -249,6 +229,39 @@ async function cmdInit(): Promise<void> {
249
229
  const project = projectName;
250
230
  const replacements = { project, org: orgName };
251
231
 
232
+ const defaultState = Object.fromEntries(defaults.agentClients.map((entry) => [
233
+ entry.id,
234
+ {
235
+ enabled: enabledClientSet.has(entry.id),
236
+ installInSandbox: entry.installInSandbox
237
+ }
238
+ ])) as AgentClientState;
239
+ const config: AgentConfig = {
240
+ project: projectName,
241
+ org: orgName,
242
+ language,
243
+ platform: { type: platformType },
244
+ templateVersion: VERSION,
245
+ sandbox: structuredClone(defaults.sandbox),
246
+ task: structuredClone(defaults.task),
247
+ labels: structuredClone(defaults.labels),
248
+ files: { managed: [], merged: [], ejected: [] },
249
+ agentClients: serializeAgentClients(defaultState)
250
+ };
251
+
252
+ if (sandboxEngine) config.sandbox.engine = sandboxEngine;
253
+ if (templateSources.length > 0) config.templates = { sources: templateSources };
254
+ if (skillSources.length > 0) config.skills = { sources: skillSources };
255
+
256
+ const workflowPlan = planAgentClientReconciliation({
257
+ config: config as unknown as Record<string, unknown>,
258
+ mutation: { type: 'none' },
259
+ projectRoot: process.cwd(),
260
+ templateRoot: templateDir,
261
+ platformType,
262
+ language
263
+ });
264
+
252
265
  console.log('');
253
266
  if (orgName) {
254
267
  info(`Installing update-agent-infra seed command for: ${projectName} (${orgName})`);
@@ -257,18 +270,6 @@ async function cmdInit(): Promise<void> {
257
270
  }
258
271
  console.log('');
259
272
 
260
- // select language-specific template filenames
261
- let claudeSrc, geminiSrc, opencodeSrc;
262
- if (language === 'zh-CN') {
263
- claudeSrc = 'update-agent-infra.zh-CN.md';
264
- geminiSrc = 'update-agent-infra.zh-CN.toml';
265
- opencodeSrc = 'update-agent-infra.zh-CN.md';
266
- } else {
267
- claudeSrc = 'update-agent-infra.en.md';
268
- geminiSrc = 'update-agent-infra.en.toml';
269
- opencodeSrc = 'update-agent-infra.en.md';
270
- }
271
-
272
273
  // install skill
273
274
  copySkillDir(
274
275
  path.join(templateDir, '.agents', 'skills', 'update-agent-infra'),
@@ -285,68 +286,8 @@ async function cmdInit(): Promise<void> {
285
286
  );
286
287
  ok('Installed .agents/scripts/lib/agent-infra-package.js');
287
288
 
288
- // install Claude command (only if enabled)
289
- if (enabledTUISet.has('claude-code')) {
290
- renderFile(
291
- path.join(templateDir, '.claude', 'commands', claudeSrc),
292
- path.join('.claude', 'commands', 'update-agent-infra.md'),
293
- replacements
294
- );
295
- ok('Installed .claude/commands/update-agent-infra.md');
296
- }
297
-
298
- // install Gemini command (only if enabled)
299
- if (enabledTUISet.has('gemini-cli')) {
300
- renderFile(
301
- path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc),
302
- path.join('.gemini', 'commands', project, 'update-agent-infra.toml'),
303
- replacements
304
- );
305
- ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
306
- }
307
-
308
- // install OpenCode command (only if enabled)
309
- if (enabledTUISet.has('opencode')) {
310
- renderFile(
311
- path.join(templateDir, '.opencode', 'commands', opencodeSrc),
312
- path.join('.opencode', 'commands', 'update-agent-infra.md'),
313
- replacements
314
- );
315
- ok('Installed .opencode/commands/update-agent-infra.md');
316
- }
317
-
318
- // generate .agents/.airc.json
319
- const config: AgentConfig = {
320
- project: projectName,
321
- org: orgName,
322
- language,
323
- platform: { type: platformType },
324
- templateVersion: VERSION,
325
- sandbox: structuredClone(defaults.sandbox),
326
- task: structuredClone(defaults.task),
327
- labels: structuredClone(defaults.labels),
328
- files: buildDefaultFiles(platformType, enabledTUISet),
329
- tuis: enabledTUIs
330
- };
331
-
332
- if (sandboxEngine) {
333
- config.sandbox.engine = sandboxEngine;
334
- }
335
-
336
- if (templateSources.length > 0) {
337
- config.templates = {
338
- sources: templateSources
339
- };
340
- }
341
-
342
- if (skillSources.length > 0) {
343
- config.skills = {
344
- sources: skillSources
345
- };
346
- }
347
-
348
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
349
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
289
+ const reconcileResult = applyAgentClientReconciliation(workflowPlan);
290
+ for (const target of reconcileResult.applied) ok(`Installed ${target}`);
350
291
  ok(`Generated ${configPath}`);
351
292
 
352
293
  // done
@@ -356,24 +297,15 @@ async function cmdInit(): Promise<void> {
356
297
  console.log(' If this init used npx, install agent-infra persistently before update or validation:');
357
298
  console.log(' npm install -g @fitlab-ai/agent-infra');
358
299
  console.log('');
359
- if (enabledTUISet.size === 0) {
360
- console.log(' No built-in TUI selected.');
300
+ if (workflowPlan.nextSteps.length === 0) {
301
+ console.log(' No Agent Client project integration enabled.');
361
302
  console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
362
303
  console.log('');
363
304
  } else {
364
305
  console.log(' Next step: open this project in any AI TUI and run:');
365
306
  console.log('');
366
- const claudeOrOpencode: string[] = [];
367
- if (enabledTUISet.has('claude-code')) claudeOrOpencode.push('Claude Code');
368
- if (enabledTUISet.has('opencode')) claudeOrOpencode.push('OpenCode');
369
- if (claudeOrOpencode.length > 0) {
370
- console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
371
- }
372
- if (enabledTUISet.has('gemini-cli')) {
373
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
374
- }
375
- if (enabledTUISet.has('codex')) {
376
- console.log(' Codex CLI: $update-agent-infra');
307
+ for (const nextStep of workflowPlan.nextSteps) {
308
+ console.log(` ${nextStep.displayName}: ${nextStep.command}`);
377
309
  }
378
310
  console.log('');
379
311
  console.log(' This will render all templates and set up the full');
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ AgentClientConfigError,
6
+ normalizeAgentClients
7
+ } from '../agent-clients/config.ts';
8
+ import { normalizeCustomTUIs } from '../agent-clients/custom-tuis.ts';
9
+ import { renderNextStepCommands } from '../agent-clients/next-steps.ts';
10
+
11
+ const USAGE = 'Usage: agent-infra-internal agent-client next-steps --skill <skill-name> [--task-ref <NN|TASK-id>] [--format text|json]\n';
12
+
13
+ type ParsedArgs = Readonly<{
14
+ skillName: string;
15
+ taskRef?: string;
16
+ format: 'text' | 'json';
17
+ }>;
18
+
19
+ function failure(code: string, message: string): void {
20
+ process.stdout.write(`${JSON.stringify({
21
+ status: 'failed',
22
+ changed: false,
23
+ commands: [],
24
+ diagnostics: [],
25
+ error: { code, message }
26
+ })}\n`);
27
+ process.exitCode = 1;
28
+ }
29
+
30
+ function parseArgs(args: string[]): ParsedArgs | null {
31
+ if (args[0] === '--help' || args[0] === '-h') {
32
+ process.stdout.write(USAGE);
33
+ return null;
34
+ }
35
+ if (args[0] !== 'next-steps') {
36
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', "operation must be 'next-steps'");
37
+ return null;
38
+ }
39
+ let skillName: string | undefined;
40
+ let taskRef: string | undefined;
41
+ let format: 'text' | 'json' = 'text';
42
+ const seen = new Set<string>();
43
+ for (let index = 1; index < args.length; index += 1) {
44
+ const flag = args[index]!;
45
+ if (!['--skill', '--task-ref', '--format'].includes(flag)) {
46
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `unknown option '${flag}'`);
47
+ return null;
48
+ }
49
+ if (seen.has(flag)) {
50
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `duplicate option '${flag}'`);
51
+ return null;
52
+ }
53
+ const value = args[++index];
54
+ if (!value || value.startsWith('--')) {
55
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `option '${flag}' requires a value`);
56
+ return null;
57
+ }
58
+ seen.add(flag);
59
+ if (flag === '--skill') skillName = value;
60
+ if (flag === '--task-ref') taskRef = value;
61
+ if (flag === '--format') {
62
+ if (value !== 'text' && value !== 'json') {
63
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', "format must be 'text' or 'json'");
64
+ return null;
65
+ }
66
+ format = value;
67
+ }
68
+ }
69
+ if (!skillName) {
70
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', "option '--skill' is required");
71
+ return null;
72
+ }
73
+ return { skillName, ...(taskRef ? { taskRef } : {}), format };
74
+ }
75
+
76
+ function agentClient(args: string[] = []): void {
77
+ const parsed = parseArgs(args);
78
+ if (!parsed || process.exitCode) return;
79
+
80
+ let config: Record<string, unknown>;
81
+ try {
82
+ const configPath = path.join(process.cwd(), '.agents', '.airc.json');
83
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as unknown;
84
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
85
+ throw new Error('configuration root must be an object');
86
+ }
87
+ config = raw as Record<string, unknown>;
88
+ } catch (error) {
89
+ failure('AGENT_CLIENT_CONFIG_INVALID', String(error));
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const clients = normalizeAgentClients(config);
95
+ const custom = normalizeCustomTUIs(process.cwd(), config.customTUIs ?? []);
96
+ const commands = renderNextStepCommands({
97
+ projectName: String(config.project ?? ''),
98
+ state: clients.state,
99
+ customTUIs: custom.items,
100
+ skillName: parsed.skillName,
101
+ ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {})
102
+ });
103
+ if (parsed.format === 'json') {
104
+ process.stdout.write(`${JSON.stringify({
105
+ status: 'rendered',
106
+ changed: false,
107
+ commands,
108
+ diagnostics: custom.diagnostics,
109
+ error: null
110
+ })}\n`);
111
+ return;
112
+ }
113
+ for (const command of commands) {
114
+ process.stdout.write(` - ${command.displayName}: ${command.command}\n`);
115
+ }
116
+ for (const diagnostic of custom.diagnostics) {
117
+ process.stderr.write(`${diagnostic.code} at ${diagnostic.path}\n`);
118
+ }
119
+ } catch (error) {
120
+ if (error instanceof AgentClientConfigError) {
121
+ failure(error.code, error.message);
122
+ return;
123
+ }
124
+ failure(
125
+ 'AGENT_CLIENT_RENDER_INVALID',
126
+ error instanceof Error ? error.message : String(error)
127
+ );
128
+ }
129
+ }
130
+
131
+ export { agentClient };
@@ -27,7 +27,7 @@ function gitWorkflow(args: string[] = []): void {
27
27
  const inputIndex = rest.indexOf('--input');
28
28
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
29
29
  if (!input) result = { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
30
- else if (action === 'snapshot') result = { status: 'no-op', changed: false, snapshot: snapshotReview({ cwd: cwd ?? process.cwd(), mode: input.mode, baseline: input.baseline, globs: input.globs ?? resolvePostReviewGlobs({}, {}) }), error: null };
30
+ else if (action === 'snapshot') result = { status: 'no-op', changed: false, snapshot: snapshotReview({ cwd: cwd ?? process.cwd(), mode: input.mode, baseline: input.baseline, diffBase: input.diffBase, globs: input.globs ?? resolvePostReviewGlobs({}, {}) }), error: null };
31
31
  else result = { status: 'no-op', changed: false, comparison: compareReviewTrees({ cwd: cwd ?? process.cwd(), expected: input.expected, actual: input.actual }), error: null };
32
32
  } else result = { status: 'failed', changed: false, error: { code: 'GIT_ACTION_INVALID', message: `Unknown git-workflow action '${action ?? ''}'` } };
33
33
  output(result);
@@ -150,11 +150,58 @@ function runOptionalDemo(cwd: string, run: CommandRunner = command): DemoResult
150
150
  return { status: 'recorded', reasonCode: null, message: null, outputPath };
151
151
  }
152
152
 
153
- function git(cwd: string, args: string[]): string | null {
154
- const result = command(cwd, 'git', args);
153
+ function git(cwd: string, args: string[], run: CommandRunner = command): string | null {
154
+ const result = run(cwd, 'git', args);
155
155
  return result.status === 0 ? String(result.stdout).trim() : null;
156
156
  }
157
157
 
158
+ function inspectLocalReleaseFacts(cwd: string, version: string, run: CommandRunner = command) {
159
+ const tag = `v${version}`;
160
+ const head = git(cwd, ['rev-parse', 'HEAD'], run);
161
+ const tagCommit = git(cwd, ['rev-parse', '--verify', `${tag}^{commit}`], run);
162
+ const localTag = Boolean(tagCommit && tagCommit === head);
163
+ const localTagAncestor = Boolean(
164
+ tagCommit && head && !localTag
165
+ && run(cwd, 'git', ['merge-base', '--is-ancestor', tagCommit, head]).status === 0
166
+ );
167
+ const localTagConflict = Boolean(tagCommit && !localTag && !localTagAncestor);
168
+ const postSubject = `chore: prepare next dev iteration after v${version}`;
169
+ const postLog = localTag || localTagAncestor
170
+ ? git(cwd, ['log', '--format=%s', `${tag}..HEAD`], run) ?? ''
171
+ : '';
172
+ const postCommit = postLog.split('\n').some((subject) => subject === postSubject);
173
+ return { localTag, localTagAncestor, localTagConflict, postCommit };
174
+ }
175
+
176
+ function releaseSmokeStatus(workflows: Array<Record<string, unknown>>, version: string, tagCommit: string | null): ReleaseFacts['smoke'] {
177
+ const manualTitle = `Post-Release Smoke v${version}`;
178
+ const targets = workflows.filter((run) => {
179
+ if (String(run.workflowName || run.name) !== 'Post-Release Smoke') return false;
180
+ if (String(run.event) === 'workflow_run') return Boolean(tagCommit && String(run.headSha) === tagCommit);
181
+ return String(run.event) === 'workflow_dispatch' && String(run.displayTitle) === manualTitle;
182
+ }).sort((left, right) => {
183
+ const leftOrder = [Date.parse(String(left.createdAt || '')) || 0, Number(left.databaseId) || 0, Number(left.attempt) || 0];
184
+ const rightOrder = [Date.parse(String(right.createdAt || '')) || 0, Number(right.databaseId) || 0, Number(right.attempt) || 0];
185
+ for (let index = 0; index < leftOrder.length; index += 1) {
186
+ if (leftOrder[index] !== rightOrder[index]) return rightOrder[index]! - leftOrder[index]!;
187
+ }
188
+ return 0;
189
+ });
190
+ const latest = targets[0];
191
+ if (!latest) return null;
192
+ if (String(latest.status) !== 'completed') return 'pending';
193
+ return String(latest.conclusion) === 'success' ? 'success' : 'failed';
194
+ }
195
+
196
+ function inspectPostWorktree(cwd: string) {
197
+ const inspected = inspectGitWorkflow(cwd);
198
+ if (!inspected.snapshot) return inspected.error ?? { code: 'GIT_INSPECT_FAILED', message: 'Unable to inspect Git repository' };
199
+ if (inspected.snapshot.worktree.length || inspected.snapshot.staged.length) {
200
+ return { code: 'WORKTREE_DIRTY', message: 'Release post requires a clean worktree' };
201
+ }
202
+ return null;
203
+ }
204
+
158
205
  async function inspectFacts(cwd: string, version: string): Promise<ReleaseFacts> {
159
206
  const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')) as { name: string };
160
207
  const config = JSON.parse(fs.readFileSync(path.join(cwd, '.agents', '.airc.json'), 'utf8')) as { project: string; org: string };
@@ -162,6 +209,7 @@ async function inspectFacts(cwd: string, version: string): Promise<ReleaseFacts>
162
209
  const branch = git(cwd, ['branch', '--show-current']) || '';
163
210
  const head = git(cwd, ['rev-parse', 'HEAD']);
164
211
  const localTagTarget = git(cwd, ['rev-parse', '--verify', `${tag}^{commit}`]);
212
+ const local = inspectLocalReleaseFacts(cwd, version);
165
213
  const remoteBranch = branch ? git(cwd, ['ls-remote', '--heads', 'origin', branch]) : null;
166
214
  const remoteTag = git(cwd, ['ls-remote', '--tags', 'origin', `refs/tags/${tag}`]);
167
215
  const platform = inspectPlatformRelease(tag, { cwd });
@@ -169,18 +217,14 @@ async function inspectFacts(cwd: string, version: string): Promise<ReleaseFacts>
169
217
  const formulaUrl = `https://raw.githubusercontent.com/${config.org}/homebrew-tap/main/Formula/${config.project}.rb`;
170
218
  const homebrew = await inspectHomebrewChannel(formulaUrl, version);
171
219
  const workflows = platform.workflows as Array<Record<string, unknown>>;
172
- const smokeRun = workflows.find((run) => String(run.name).toLowerCase().includes('post-release-smoke') && String(run.headBranch) === tag);
173
- const smoke = smokeRun ? String(smokeRun.conclusion) === 'success' ? 'success' : String(smokeRun.status) === 'completed' ? 'failed' : 'pending' : null;
174
- const postMessage = git(cwd, ['log', '-1', '--pretty=%s']) || '';
220
+ const smoke = releaseSmokeStatus(workflows, version, localTagTarget);
175
221
  return {
176
- localTag: Boolean(localTagTarget && localTagTarget === head),
177
- localTagConflict: Boolean(localTagTarget && localTagTarget !== head),
222
+ ...local,
178
223
  remoteBranch: Boolean(remoteBranch && remoteBranch.split(/\s+/)[0] === head), remoteTag: Boolean(remoteTag),
179
224
  githubRelease: platform.platform.type !== 'github'
180
225
  ? true
181
226
  : platform.status === 'blocked' ? null : Boolean(platform.release?.published),
182
- npm: npm.published, homebrew: homebrew.published, smoke,
183
- postCommit: postMessage.includes(`after v${version}`)
227
+ npm: npm.published, homebrew: homebrew.published, smoke
184
228
  };
185
229
  }
186
230
 
@@ -218,12 +262,11 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
218
262
  }
219
263
  const before = releaseSnapshot(version, await inspectFacts(cwd, version));
220
264
  if (action === 'inspect') { process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`); return; }
265
+ if (before.facts.localTagConflict) {
266
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'GIT_TAG_CONFLICT', message: `Tag v${version} is not reachable from HEAD` } })}\n`);
267
+ process.exitCode = 1; return;
268
+ }
221
269
  if (action === 'prepare') {
222
- if (before.facts.localTagConflict) {
223
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'GIT_TAG_CONFLICT', message: `Tag v${version} does not point to HEAD` } })}\n`);
224
- process.exitCode = 1;
225
- return;
226
- }
227
270
  if (before.phase !== 'unprepared') {
228
271
  const milestones = reconcileReleaseMilestones(version, { cwd });
229
272
  process.stdout.write(`${JSON.stringify({ ...milestones, snapshot: before })}\n`);
@@ -254,16 +297,25 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
254
297
  process.exitCode = failed ? 1 : blocked ? 2 : 0; return;
255
298
  }
256
299
  if (action === 'publish') {
257
- if (!['prepared', 'partially-published'].includes(before.phase)) { process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_PHASE_INVALID', message: `Cannot publish from ${before.phase}` } })}\n`); process.exitCode = 1; return; }
300
+ if (!before.facts.localTag || !['prepared', 'partially-published'].includes(before.phase)) { process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_PHASE_INVALID', message: `Cannot publish from ${before.phase}` } })}\n`); process.exitCode = 1; return; }
258
301
  const branch = git(cwd, ['branch', '--show-current']) || '';
259
302
  const refs = [...(!before.facts.remoteBranch ? [branch] : []), ...(!before.facts.remoteTag ? [`refs/tags/v${version}`] : [])];
260
303
  const result = pushGitRefs({ cwd, remote: 'origin', refs });
261
304
  process.stdout.write(`${JSON.stringify({ ...result, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = result.status === 'failed' ? 1 : result.status === 'degraded' ? 2 : 0; return;
262
305
  }
306
+ if (before.phase === 'complete') {
307
+ process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
308
+ return;
309
+ }
263
310
  const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
264
311
  if (!['published', 'post-pending'].includes(before.phase) || !channelsComplete || before.facts.smoke !== 'success') {
265
312
  process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`); process.exitCode = before.facts.smoke === 'failed' ? 1 : 2; return;
266
313
  }
314
+ const worktreeError = inspectPostWorktree(cwd);
315
+ if (worktreeError) {
316
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: worktreeError })}\n`);
317
+ process.exitCode = 1; return;
318
+ }
267
319
  const built = command(cwd, 'npm', ['run', 'build']);
268
320
  if (built.status !== 0) {
269
321
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: true, snapshot: before, error: { code: 'RELEASE_POST_COMMAND_FAILED', message: String(built.stderr || built.stdout) } })}\n`);
@@ -284,5 +336,5 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
284
336
  process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
285
337
  }
286
338
 
287
- export { computeDemoInputDigest, inspectFacts, releaseWorkflow, runOptionalDemo };
339
+ export { computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
288
340
  export type { CommandRunner, DemoResult };
@@ -0,0 +1,132 @@
1
+ import {
2
+ activateMatchingOrchestrationDelegation,
3
+ activateOrchestrationDelegation,
4
+ advanceOrchestration,
5
+ beginOrResumeOrchestration,
6
+ completeCommitOrchestrationStage,
7
+ pauseOrchestration,
8
+ prepareOrchestrationDelegation,
9
+ routeOrchestration,
10
+ sealMatchingOrchestrationDelegation,
11
+ sealOrchestrationDelegation,
12
+ statusOrchestration
13
+ } from '../task/orchestration.ts';
14
+ import type { AgentClientId } from '../agent-clients/types.ts';
15
+
16
+ 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';
17
+
18
+ function usageFailure(message: string): void {
19
+ process.stdout.write(`${JSON.stringify({
20
+ status: 'failed', changed: false,
21
+ error: { code: 'ORCHESTRATION_PAYLOAD_INVALID', message }
22
+ })}\n`);
23
+ process.stderr.write(USAGE);
24
+ process.exitCode = 2;
25
+ }
26
+
27
+ function taskOrchestration(args: string[] = []): void {
28
+ if (args[0] === '--help' || args[0] === '-h') {
29
+ process.stdout.write(USAGE);
30
+ return;
31
+ }
32
+ if (args.length < 2) {
33
+ usageFailure('task ref and intent are required');
34
+ return;
35
+ }
36
+ const [taskRef, intent] = args;
37
+ if (!['begin-or-resume', 'route', 'prepare', 'hook-start', 'stage-completed', 'hook-stop', 'advance', 'pause', 'status'].includes(intent!)) {
38
+ usageFailure(`unknown intent '${intent}'`);
39
+ return;
40
+ }
41
+ const values: Record<string, string> = {};
42
+ const seen = new Set<string>();
43
+ for (let index = 2; index < args.length; index += 1) {
44
+ const flag = args[index]!;
45
+ if (![
46
+ '--max-steps', '--client', '--requested-model', '--parent-id', '--before-fingerprint',
47
+ '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--fallback-reason',
48
+ '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent'
49
+ ].includes(flag)) {
50
+ usageFailure(`unknown option '${flag}'`);
51
+ return;
52
+ }
53
+ if (seen.has(flag)) {
54
+ usageFailure(`duplicate option '${flag}'`);
55
+ return;
56
+ }
57
+ seen.add(flag);
58
+ const value = args[++index];
59
+ if (!value || value.startsWith('--')) {
60
+ usageFailure(`option '${flag}' requires a value`);
61
+ return;
62
+ }
63
+ values[flag] = value;
64
+ }
65
+ const requireValues = (flags: string[]) => flags.find((flag) => values[flag] === undefined);
66
+ let result;
67
+ if (intent === 'begin-or-resume') {
68
+ const maxSteps = values['--max-steps'] === undefined ? undefined : Number(values['--max-steps']);
69
+ if (maxSteps !== undefined && (!Number.isInteger(maxSteps) || maxSteps < 1)) {
70
+ usageFailure('--max-steps must be a positive integer'); return;
71
+ }
72
+ result = beginOrResumeOrchestration(taskRef!, { maxSteps });
73
+ } else if (intent === 'route') {
74
+ result = routeOrchestration(taskRef!);
75
+ } else if (intent === 'status') {
76
+ result = statusOrchestration(taskRef!);
77
+ } else if (intent === 'prepare') {
78
+ const missing = requireValues(['--client']);
79
+ if (missing) { usageFailure(`intent 'prepare' requires '${missing}'`); return; }
80
+ result = prepareOrchestrationDelegation(taskRef!, {
81
+ client: values['--client'] as AgentClientId,
82
+ requestedModel: values['--requested-model']
83
+ });
84
+ } else if (intent === 'hook-start') {
85
+ const missing = requireValues([
86
+ ...(taskRef === 'auto' ? ['--client'] : []),
87
+ '--native-agent', '--child-id', '--parent-id', '--spawn-mode'
88
+ ]);
89
+ if (missing) { usageFailure(`intent 'hook-start' requires '${missing}'`); return; }
90
+ const event = {
91
+ nativeAgent: values['--native-agent']!, childId: values['--child-id']!,
92
+ parentId: values['--parent-id']!, spawnMode: values['--spawn-mode']!,
93
+ actualModel: values['--actual-model'], modelFallbackReason: values['--fallback-reason']
94
+ };
95
+ result = taskRef === 'auto'
96
+ ? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event)
97
+ : activateOrchestrationDelegation(taskRef!, event);
98
+ } else if (intent === 'hook-stop') {
99
+ if (taskRef === 'auto') {
100
+ const missing = requireValues(['--client', '--native-agent', '--child-id']);
101
+ if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
102
+ result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
103
+ nativeAgent: values['--native-agent']!, childId: values['--child-id']!
104
+ });
105
+ } else {
106
+ const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
107
+ if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
108
+ const exitCode = Number(values['--exit-code']);
109
+ if (!Number.isInteger(exitCode)) { usageFailure('--exit-code must be an integer'); return; }
110
+ result = sealOrchestrationDelegation(taskRef!, {
111
+ childId: values['--child-id']!, exitCode,
112
+ afterFingerprint: values['--after-fingerprint']!,
113
+ changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
114
+ });
115
+ }
116
+ } else if (intent === 'stage-completed') {
117
+ const missing = requireValues(['--agent']);
118
+ if (missing) { usageFailure(`intent 'stage-completed' requires '${missing}'`); return; }
119
+ result = completeCommitOrchestrationStage(taskRef!, values['--agent']!);
120
+ } else if (intent === 'advance') {
121
+ result = advanceOrchestration(taskRef!);
122
+ } else {
123
+ const missing = requireValues(['--code', '--message', '--recoverable']);
124
+ if (missing) { usageFailure(`intent 'pause' requires '${missing}'`); return; }
125
+ if (!['true', 'false'].includes(values['--recoverable']!)) { usageFailure('--recoverable must be true or false'); return; }
126
+ result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true');
127
+ }
128
+ process.stdout.write(`${JSON.stringify(result)}\n`);
129
+ if (result.status === 'failed') process.exitCode = 1;
130
+ }
131
+
132
+ export { taskOrchestration };