all-hands-cli 0.1.0

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 (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Specs Commands (Agent-Facing)
3
+ *
4
+ * High-level spec management operations.
5
+ *
6
+ * Commands:
7
+ * - ah specs list - List all specs grouped by domain_name
8
+ * - ah specs complete <name> - Mark spec completed, move spec out of roadmap
9
+ * - ah specs create <path> - Create spec: validate, assign branch, commit and push
10
+ */
11
+
12
+ import { Command } from 'commander';
13
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
14
+ import { join, dirname, relative, basename } from 'path';
15
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
16
+ import { getGitRoot, getCurrentBranch } from '../lib/planning.js';
17
+ import { getBaseBranch, gitExec } from '../lib/git.js';
18
+ import { KnowledgeService } from '../lib/knowledge.js';
19
+ import { findSpecByBranch, getSpecForBranch, loadAllSpecs as loadAllSpecGroups, type SpecFile, type SpecFrontmatter } from '../lib/specs.js';
20
+ import { logCommandStart, logCommandSuccess, logCommandError } from '../lib/trace-store.js';
21
+
22
+ /**
23
+ * Parse spec file frontmatter
24
+ */
25
+ function parseSpecFrontmatter(filePath: string): SpecFrontmatter | null {
26
+ if (!existsSync(filePath)) return null;
27
+
28
+ try {
29
+ const content = readFileSync(filePath, 'utf-8');
30
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
31
+ if (!frontmatterMatch) return null;
32
+
33
+ return parseYaml(frontmatterMatch[1]) as SpecFrontmatter;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Update spec file frontmatter status
41
+ */
42
+ export function updateSpecStatus(filePath: string, newStatus: 'roadmap' | 'in_progress' | 'completed'): boolean {
43
+ if (!existsSync(filePath)) return false;
44
+
45
+ try {
46
+ const content = readFileSync(filePath, 'utf-8');
47
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
48
+ if (!frontmatterMatch) return false;
49
+
50
+ const frontmatter = parseYaml(frontmatterMatch[1]) as SpecFrontmatter;
51
+ frontmatter.status = newStatus;
52
+
53
+ const newContent = `---\n${stringifyYaml(frontmatter).trim()}\n---\n${frontmatterMatch[2]}`;
54
+ writeFileSync(filePath, newContent);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Find a spec by ID (filename without extension)
63
+ */
64
+ function findSpecByName(name: string): SpecFile | null {
65
+ const groups = loadAllSpecGroups();
66
+ for (const group of groups) {
67
+ const spec = group.specs.find((s) => s.id === name);
68
+ if (spec) return spec;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Reindex knowledge bases after spec file moves.
75
+ * Updates both 'docs' and 'roadmap' indexes to reflect the file move.
76
+ */
77
+ export async function reindexAfterMove(
78
+ gitRoot: string,
79
+ oldPath: string,
80
+ newPath: string,
81
+ quiet: boolean = false
82
+ ): Promise<void> {
83
+ const service = new KnowledgeService(gitRoot, { quiet });
84
+ const oldRelPath = relative(gitRoot, oldPath);
85
+ const newRelPath = relative(gitRoot, newPath);
86
+
87
+ // Update roadmap index: remove old location if it was in roadmap
88
+ if (oldRelPath.startsWith('specs/roadmap/')) {
89
+ await service.reindexFromChanges('roadmap', [
90
+ { path: oldRelPath, deleted: true },
91
+ ]);
92
+ }
93
+ // Add new location if it's now in roadmap
94
+ if (newRelPath.startsWith('specs/roadmap/')) {
95
+ await service.reindexFromChanges('roadmap', [
96
+ { path: newRelPath, added: true },
97
+ ]);
98
+ }
99
+
100
+ // Update docs index: docs includes all of specs/
101
+ // Remove from old location, add to new location
102
+ await service.reindexFromChanges('docs', [
103
+ { path: oldRelPath, deleted: true },
104
+ { path: newRelPath, added: true },
105
+ ]);
106
+ }
107
+
108
+ export function register(program: Command): void {
109
+ const specs = program
110
+ .command('specs')
111
+ .description('Spec management');
112
+
113
+ // ah specs list
114
+ specs
115
+ .command('list')
116
+ .description('List all specs grouped by domain')
117
+ .option('--json', 'Output as JSON')
118
+ .option('--domains-only', 'Only list domain names')
119
+ .option('--domain <name>', 'Filter to a specific domain')
120
+ .option('--roadmap', 'Only show specs in the roadmap (not completed)')
121
+ .option('--completed', 'Only show completed specs')
122
+ .option('--in-progress', 'Only show in-progress specs')
123
+ .action(async (options: { json?: boolean; domainsOnly?: boolean; domain?: string; roadmap?: boolean; completed?: boolean; inProgress?: boolean }) => {
124
+ const groups = loadAllSpecGroups();
125
+ let allSpecs = groups.flatMap((g) => g.specs);
126
+
127
+ // Apply status filters
128
+ if (options.roadmap) {
129
+ allSpecs = allSpecs.filter((s) => s.status === 'roadmap');
130
+ } else if (options.completed) {
131
+ allSpecs = allSpecs.filter((s) => s.status === 'completed');
132
+ } else if (options.inProgress) {
133
+ allSpecs = allSpecs.filter((s) => s.status === 'in_progress');
134
+ }
135
+
136
+ // Group by domain_name
137
+ const byDomain: Record<string, SpecFile[]> = {};
138
+ for (const spec of allSpecs) {
139
+ const domain = spec.domain_name || 'uncategorized';
140
+ if (!byDomain[domain]) {
141
+ byDomain[domain] = [];
142
+ }
143
+ byDomain[domain].push(spec);
144
+ }
145
+
146
+ const domains = Object.keys(byDomain).sort();
147
+
148
+ // Handle --domains-only
149
+ if (options.domainsOnly) {
150
+ if (options.json) {
151
+ console.log(JSON.stringify({
152
+ success: true,
153
+ count: domains.length,
154
+ domains,
155
+ }, null, 2));
156
+ } else {
157
+ for (const domain of domains) {
158
+ console.log(domain);
159
+ }
160
+ }
161
+ return;
162
+ }
163
+
164
+ // Handle --domain <name>
165
+ if (options.domain) {
166
+ const domainSpecs = byDomain[options.domain];
167
+ if (!domainSpecs) {
168
+ if (options.json) {
169
+ console.log(JSON.stringify({ success: false, error: `Domain not found: ${options.domain}` }));
170
+ } else {
171
+ console.error(`Domain not found: ${options.domain}`);
172
+ }
173
+ process.exit(1);
174
+ }
175
+
176
+ const sortedSpecs = domainSpecs.sort((a, b) => a.id.localeCompare(b.id));
177
+
178
+ if (options.json) {
179
+ console.log(JSON.stringify({
180
+ success: true,
181
+ domain: options.domain,
182
+ count: sortedSpecs.length,
183
+ specs: sortedSpecs,
184
+ }, null, 2));
185
+ } else {
186
+ console.log(`## ${options.domain}\n`);
187
+ for (const spec of sortedSpecs) {
188
+ const statusIcon = spec.status === 'completed' ? '[x]' : spec.status === 'in_progress' ? '[>]' : '[ ]';
189
+ const deps = spec.dependencies.length > 0 ? ` (deps: ${spec.dependencies.join(', ')})` : '';
190
+ console.log(` ${statusIcon} ${spec.id}${deps}`);
191
+ }
192
+ }
193
+ return;
194
+ }
195
+
196
+ // Default: list all specs grouped by domain
197
+ if (options.json) {
198
+ console.log(JSON.stringify({
199
+ success: true,
200
+ count: allSpecs.length,
201
+ domains: byDomain,
202
+ }, null, 2));
203
+ return;
204
+ }
205
+
206
+ console.log(`Found ${allSpecs.length} spec(s):\n`);
207
+
208
+ for (const domain of domains) {
209
+ console.log(`## ${domain}`);
210
+ const domainSpecs = byDomain[domain].sort((a, b) => a.id.localeCompare(b.id));
211
+ for (const spec of domainSpecs) {
212
+ const statusIcon = spec.status === 'completed' ? '[x]' : spec.status === 'in_progress' ? '[>]' : '[ ]';
213
+ const deps = spec.dependencies.length > 0 ? ` (deps: ${spec.dependencies.join(', ')})` : '';
214
+ console.log(` ${statusIcon} ${spec.id}${deps}`);
215
+ }
216
+ console.log();
217
+ }
218
+ });
219
+
220
+ // ah specs current
221
+ specs
222
+ .command('current')
223
+ .description('Show spec for current git branch')
224
+ .option('--json', 'Output as JSON')
225
+ .action((options: { json?: boolean }) => {
226
+ const branch = getCurrentBranch();
227
+ const spec = getSpecForBranch(branch);
228
+
229
+ if (!spec) {
230
+ if (options.json) {
231
+ console.log(JSON.stringify({
232
+ success: true,
233
+ hasSpec: false,
234
+ branch,
235
+ message: 'No spec for this branch',
236
+ }, null, 2));
237
+ } else {
238
+ console.log(`No spec for branch: ${branch}`);
239
+ }
240
+ return;
241
+ }
242
+
243
+ if (options.json) {
244
+ console.log(JSON.stringify({
245
+ success: true,
246
+ hasSpec: true,
247
+ branch,
248
+ spec: {
249
+ id: spec.id,
250
+ name: spec.title,
251
+ path: spec.path,
252
+ domain: spec.domain_name,
253
+ status: spec.status,
254
+ dependencies: spec.dependencies,
255
+ },
256
+ }, null, 2));
257
+ } else {
258
+ console.log(`Branch: ${branch}`);
259
+ console.log(`Spec: ${spec.id}`);
260
+ console.log(` Title: ${spec.title}`);
261
+ console.log(` Path: ${spec.path}`);
262
+ console.log(` Domain: ${spec.domain_name}`);
263
+ console.log(` Status: ${spec.status}`);
264
+ if (spec.dependencies.length > 0) {
265
+ console.log(` Dependencies: ${spec.dependencies.join(', ')}`);
266
+ }
267
+ }
268
+ });
269
+
270
+ // ah specs complete <name>
271
+ specs
272
+ .command('complete <name>')
273
+ .description('Mark spec completed and move to specs/')
274
+ .option('--json', 'Output as JSON')
275
+ .action(async (name: string, options: { json?: boolean }) => {
276
+ const commandName = 'specs complete';
277
+ const commandArgs = { name, options };
278
+ logCommandStart(commandName, commandArgs);
279
+
280
+ const spec = findSpecByName(name);
281
+
282
+ if (!spec) {
283
+ const error = `Spec not found: ${name}`;
284
+ logCommandError(commandName, error, commandArgs);
285
+ if (options.json) {
286
+ console.log(JSON.stringify({ success: false, error }));
287
+ } else {
288
+ console.error(error);
289
+ }
290
+ process.exit(1);
291
+ }
292
+
293
+ if (spec.status === 'completed') {
294
+ const error = `Spec already completed: ${name}`;
295
+ logCommandError(commandName, error, commandArgs);
296
+ if (options.json) {
297
+ console.log(JSON.stringify({ success: false, error }));
298
+ } else {
299
+ console.error(error);
300
+ }
301
+ process.exit(1);
302
+ }
303
+
304
+ const gitRoot = getGitRoot();
305
+ const specsDir = join(gitRoot, 'specs');
306
+ const targetPath = join(specsDir, `${name}.spec.md`);
307
+
308
+ // Update status in frontmatter
309
+ if (!updateSpecStatus(spec.path, 'completed')) {
310
+ const error = 'Failed to update spec status';
311
+ logCommandError(commandName, error, commandArgs);
312
+ if (options.json) {
313
+ console.log(JSON.stringify({ success: false, error }));
314
+ } else {
315
+ console.error(error);
316
+ }
317
+ process.exit(1);
318
+ }
319
+
320
+ // Move file if it's in roadmap
321
+ const wasInRoadmap = spec.path.includes('/roadmap/');
322
+ if (wasInRoadmap) {
323
+ mkdirSync(dirname(targetPath), { recursive: true });
324
+ renameSync(spec.path, targetPath);
325
+
326
+ // Reindex knowledge bases to reflect the move
327
+ if (!options.json) {
328
+ console.log(' Reindexing knowledge bases...');
329
+ }
330
+ await reindexAfterMove(gitRoot, spec.path, targetPath, options.json);
331
+ }
332
+
333
+ // Log success
334
+ logCommandSuccess(commandName, {
335
+ name,
336
+ status: 'completed',
337
+ path: wasInRoadmap ? targetPath : spec.path,
338
+ reindexed: wasInRoadmap,
339
+ });
340
+
341
+ if (options.json) {
342
+ console.log(JSON.stringify({
343
+ success: true,
344
+ name,
345
+ status: 'completed',
346
+ path: wasInRoadmap ? targetPath : spec.path,
347
+ reindexed: wasInRoadmap,
348
+ }, null, 2));
349
+ return;
350
+ }
351
+
352
+ console.log(`Marked spec completed: ${name}`);
353
+ if (wasInRoadmap) {
354
+ console.log(` Moved to: ${targetPath}`);
355
+ console.log(' Knowledge indexes updated ✓');
356
+ }
357
+ });
358
+
359
+ // ah specs create <path>
360
+ specs
361
+ .command('create <path>')
362
+ .description('Create spec: validate, assign branch, commit and push to base branch')
363
+ .option('--json', 'Output as JSON')
364
+ .action((specPath: string, options: { json?: boolean }) => {
365
+ const commandName = 'specs create';
366
+ const commandArgs = { specPath, options };
367
+ logCommandStart(commandName, commandArgs);
368
+
369
+ const gitRoot = getGitRoot();
370
+ const baseBranch = getBaseBranch();
371
+ const currentBranch = getCurrentBranch();
372
+
373
+ // 1. Enforce being on base branch
374
+ if (currentBranch !== baseBranch) {
375
+ const error = `Spec creation requires being on the base branch (${baseBranch}). Currently on: ${currentBranch}`;
376
+ logCommandError(commandName, error, commandArgs);
377
+ if (options.json) {
378
+ console.log(JSON.stringify({ success: false, error }));
379
+ } else {
380
+ console.error(error);
381
+ }
382
+ process.exit(1);
383
+ }
384
+
385
+ // 2. Validate spec file
386
+ const absolutePath = specPath.startsWith('/') ? specPath : join(process.cwd(), specPath);
387
+ const specRelativePath = relative(gitRoot, absolutePath);
388
+
389
+ if (!existsSync(absolutePath)) {
390
+ const error = `Spec file not found: ${specPath}`;
391
+ logCommandError(commandName, error, commandArgs);
392
+ if (options.json) {
393
+ console.log(JSON.stringify({ success: false, error }));
394
+ } else {
395
+ console.error(error);
396
+ }
397
+ process.exit(1);
398
+ }
399
+
400
+ if (!absolutePath.endsWith('.spec.md')) {
401
+ const error = 'File must be a .spec.md file';
402
+ logCommandError(commandName, error, commandArgs);
403
+ if (options.json) {
404
+ console.log(JSON.stringify({ success: false, error }));
405
+ } else {
406
+ console.error(error);
407
+ }
408
+ process.exit(1);
409
+ }
410
+
411
+ if (!specRelativePath.startsWith('specs/roadmap/')) {
412
+ const error = `Spec file must be in specs/roadmap/. Got: ${specRelativePath}`;
413
+ logCommandError(commandName, error, commandArgs);
414
+ if (options.json) {
415
+ console.log(JSON.stringify({ success: false, error }));
416
+ } else {
417
+ console.error(error);
418
+ }
419
+ process.exit(1);
420
+ }
421
+
422
+ // 3. Derive branch name from spec type
423
+ const specName = basename(absolutePath, '.spec.md');
424
+ const frontmatter = parseSpecFrontmatter(absolutePath);
425
+ const specType = frontmatter?.type || 'milestone';
426
+
427
+ const SPEC_TYPE_BRANCH_PREFIX: Record<string, string> = {
428
+ milestone: 'feature/',
429
+ investigation: 'fix/',
430
+ optimization: 'optimize/',
431
+ refactor: 'refactor/',
432
+ documentation: 'docs/',
433
+ triage: 'triage/',
434
+ };
435
+
436
+ const prefix = SPEC_TYPE_BRANCH_PREFIX[specType] || 'feature/';
437
+ let specBranch = frontmatter?.branch || `${prefix}${specName}`;
438
+
439
+ // Handle branch name collisions
440
+ const baseBranchName = specBranch;
441
+ let suffix = 1;
442
+ let existingSpec = findSpecByBranch(specBranch, gitRoot);
443
+ while (existingSpec && existingSpec.id !== specName) {
444
+ suffix++;
445
+ specBranch = `${baseBranchName}-${suffix}`;
446
+ existingSpec = findSpecByBranch(specBranch, gitRoot);
447
+ }
448
+
449
+ // 4. Write branch to spec frontmatter if not present or changed
450
+ if (!frontmatter?.branch || frontmatter.branch !== specBranch) {
451
+ try {
452
+ const content = readFileSync(absolutePath, 'utf-8');
453
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
454
+
455
+ if (frontmatterMatch) {
456
+ const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
457
+ fm.branch = specBranch;
458
+ const newContent = `---\n${stringifyYaml(fm).trim()}\n---\n${frontmatterMatch[2]}`;
459
+ writeFileSync(absolutePath, newContent);
460
+ }
461
+ } catch (e) {
462
+ const error = `Failed to update frontmatter: ${e instanceof Error ? e.message : String(e)}`;
463
+ logCommandError(commandName, error, commandArgs);
464
+ if (options.json) {
465
+ console.log(JSON.stringify({ success: false, error }));
466
+ } else {
467
+ console.error(error);
468
+ }
469
+ process.exit(1);
470
+ }
471
+ }
472
+
473
+ // 5. Stage only the spec file
474
+ const addResult = gitExec(['add', '--', specRelativePath], gitRoot);
475
+ if (!addResult.success) {
476
+ const error = `Failed to stage spec file: ${addResult.stderr}`;
477
+ logCommandError(commandName, error, commandArgs);
478
+ if (options.json) {
479
+ console.log(JSON.stringify({ success: false, error }));
480
+ } else {
481
+ console.error(error);
482
+ }
483
+ process.exit(1);
484
+ }
485
+
486
+ // 6. Commit
487
+ const commitResult = gitExec(['commit', '-m', `spec: ${specName}`], gitRoot);
488
+ if (!commitResult.success) {
489
+ const error = `Failed to commit: ${commitResult.stderr}`;
490
+ logCommandError(commandName, error, commandArgs);
491
+ if (options.json) {
492
+ console.log(JSON.stringify({ success: false, error }));
493
+ } else {
494
+ console.error(error);
495
+ }
496
+ process.exit(1);
497
+ }
498
+
499
+ // 7. Push to origin
500
+ const pushResult = gitExec(['push', 'origin', baseBranch], gitRoot);
501
+ if (!pushResult.success) {
502
+ const error = `Failed to push to origin ${baseBranch}: ${pushResult.stderr}`;
503
+ logCommandError(commandName, error, commandArgs);
504
+ if (options.json) {
505
+ console.log(JSON.stringify({ success: false, error }));
506
+ } else {
507
+ console.error(error);
508
+ }
509
+ process.exit(1);
510
+ }
511
+
512
+ // 8. Report results
513
+ logCommandSuccess(commandName, {
514
+ name: specName,
515
+ branch: specBranch,
516
+ path: specRelativePath,
517
+ });
518
+
519
+ if (options.json) {
520
+ console.log(JSON.stringify({
521
+ success: true,
522
+ name: specName,
523
+ branch: specBranch,
524
+ path: specRelativePath,
525
+ }, null, 2));
526
+ } else {
527
+ console.log(`Created spec: ${specName}`);
528
+ console.log(` Branch: ${specBranch}`);
529
+ console.log(` Path: ${specRelativePath}`);
530
+ }
531
+ });
532
+ }