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,164 @@
1
+ /**
2
+ * Session State Management
3
+ *
4
+ * Persists TUI session state to .allhands/harness/.cache/session.json
5
+ * This file is:
6
+ * - NOT git tracked
7
+ * - Polled by EventLoop for changes (agents can modify it)
8
+ * - Persisted between TUI sessions
9
+ *
10
+ * Note: Active spec is now determined by the current git branch and
11
+ * the spec's frontmatter.branch field. See findSpecByBranch() in specs.ts.
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
+ import { dirname, join } from 'path';
16
+ import { lockSync, unlockSync } from 'proper-lockfile';
17
+ import { getGitRoot } from './planning.js';
18
+
19
+ export interface SessionState {
20
+ /** The tmux window ID where the TUI is running (e.g., @0) */
21
+ hub_window_id: string | null;
22
+ /** Window names spawned by this TUI session */
23
+ spawned_windows: string[];
24
+ }
25
+
26
+ const DEFAULT_SESSION: SessionState = {
27
+ hub_window_id: null,
28
+ spawned_windows: [],
29
+ };
30
+
31
+ /**
32
+ * Execute a function with file locking to prevent race conditions.
33
+ * Uses proper-lockfile for cross-process synchronization.
34
+ */
35
+ function withSessionLock<T>(cwd: string | undefined, fn: () => T): T {
36
+ const sessionPath = getSessionPath(cwd);
37
+ const cacheDir = dirname(sessionPath);
38
+
39
+ // Ensure cache directory and file exist before locking
40
+ if (!existsSync(cacheDir)) {
41
+ mkdirSync(cacheDir, { recursive: true });
42
+ }
43
+ if (!existsSync(sessionPath)) {
44
+ writeFileSync(sessionPath, JSON.stringify(DEFAULT_SESSION, null, 2));
45
+ }
46
+
47
+ lockSync(sessionPath);
48
+ try {
49
+ return fn();
50
+ } finally {
51
+ try {
52
+ unlockSync(sessionPath);
53
+ } catch {
54
+ // Ignore unlock errors (file may have been deleted)
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get the path to the session cache file
61
+ */
62
+ export function getSessionPath(cwd?: string): string {
63
+ const gitRoot = getGitRoot(cwd);
64
+ return join(gitRoot, '.allhands', 'harness', '.cache', 'session.json');
65
+ }
66
+
67
+ /**
68
+ * Read the current session state
69
+ */
70
+ export function readSession(cwd?: string): SessionState {
71
+ const sessionPath = getSessionPath(cwd);
72
+
73
+ if (!existsSync(sessionPath)) {
74
+ return { ...DEFAULT_SESSION };
75
+ }
76
+
77
+ try {
78
+ const content = readFileSync(sessionPath, 'utf-8');
79
+ const parsed = JSON.parse(content);
80
+ return {
81
+ hub_window_id: parsed.hub_window_id ?? null,
82
+ spawned_windows: parsed.spawned_windows ?? [],
83
+ };
84
+ } catch {
85
+ return { ...DEFAULT_SESSION };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Write the session state
91
+ */
92
+ export function writeSession(state: SessionState, cwd?: string): void {
93
+ const sessionPath = getSessionPath(cwd);
94
+ const cacheDir = dirname(sessionPath);
95
+
96
+ // Ensure cache directory exists
97
+ if (!existsSync(cacheDir)) {
98
+ mkdirSync(cacheDir, { recursive: true });
99
+ }
100
+
101
+ writeFileSync(sessionPath, JSON.stringify(state, null, 2));
102
+ }
103
+
104
+ /**
105
+ * Set the hub window ID (called at TUI startup)
106
+ */
107
+ export function setHubWindowId(windowId: string | null, cwd?: string): void {
108
+ withSessionLock(cwd, () => {
109
+ const session = readSession(cwd);
110
+ session.hub_window_id = windowId;
111
+ writeSession(session, cwd);
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Get the hub window ID
117
+ */
118
+ export function getHubWindowId(cwd?: string): string | null {
119
+ return readSession(cwd).hub_window_id;
120
+ }
121
+
122
+ /**
123
+ * Register a spawned window (persisted to disk)
124
+ */
125
+ export function addSpawnedWindow(windowName: string, cwd?: string): void {
126
+ withSessionLock(cwd, () => {
127
+ const session = readSession(cwd);
128
+ if (!session.spawned_windows.includes(windowName)) {
129
+ session.spawned_windows.push(windowName);
130
+ writeSession(session, cwd);
131
+ }
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Unregister a spawned window (persisted to disk)
137
+ */
138
+ export function removeSpawnedWindow(windowName: string, cwd?: string): void {
139
+ withSessionLock(cwd, () => {
140
+ const session = readSession(cwd);
141
+ session.spawned_windows = session.spawned_windows.filter(w => w !== windowName);
142
+ writeSession(session, cwd);
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Get all spawned windows
148
+ */
149
+ export function getSpawnedWindows(cwd?: string): string[] {
150
+ return readSession(cwd).spawned_windows;
151
+ }
152
+
153
+ /**
154
+ * Clear all TUI session state (called on clean exit)
155
+ */
156
+ export function clearTuiSession(cwd?: string): void {
157
+ withSessionLock(cwd, () => {
158
+ const session = readSession(cwd);
159
+ session.hub_window_id = null;
160
+ session.spawned_windows = [];
161
+ writeSession(session, cwd);
162
+ });
163
+ }
164
+
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Spec File Management
3
+ *
4
+ * Handles discovery and loading of spec files for spec selection.
5
+ * Scans specs/roadmap/ for planned/in-progress specs and specs/ for completed specs.
6
+ * Only .spec.md files are matched. Parses YAML frontmatter for domain_name and status fields.
7
+ */
8
+
9
+ import { existsSync, readdirSync, readFileSync } from 'fs';
10
+ import { join, basename } from 'path';
11
+ import { parse as parseYaml } from 'yaml';
12
+ import { ensurePlanningDir, initializeStatus } from './planning.js';
13
+
14
+ export type SpecType = 'milestone' | 'investigation' | 'optimization' | 'refactor' | 'documentation' | 'triage';
15
+
16
+ export interface SpecFrontmatter {
17
+ name?: string;
18
+ domain_name?: string;
19
+ type?: SpecType;
20
+ status?: 'roadmap' | 'in_progress' | 'completed';
21
+ dependencies?: string[];
22
+ branch?: string; // Source of truth for spec's working branch
23
+ initial_workflow_domain?: SpecType;
24
+ }
25
+
26
+ export interface SpecFile {
27
+ id: string;
28
+ filename: string;
29
+ path: string;
30
+ title: string;
31
+ category: 'roadmap' | 'active' | 'completed';
32
+ domain_name: string;
33
+ type: SpecType;
34
+ status: 'roadmap' | 'in_progress' | 'completed';
35
+ dependencies: string[];
36
+ branch?: string; // Source of truth for spec's working branch
37
+ }
38
+
39
+ export interface SpecGroup {
40
+ category: 'roadmap' | 'active' | 'completed';
41
+ label: string;
42
+ specs: SpecFile[];
43
+ }
44
+
45
+ export interface DomainGroup {
46
+ domain_name: string;
47
+ specs: SpecFile[];
48
+ }
49
+
50
+ /**
51
+ * Parse frontmatter from spec file content
52
+ */
53
+ export function parseFrontmatter(content: string): SpecFrontmatter | null {
54
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
55
+ if (!match) return null;
56
+
57
+ try {
58
+ return parseYaml(match[1]) as SpecFrontmatter;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Extract title from spec file content
66
+ * Looks for first H1 heading or uses filename
67
+ */
68
+ function extractTitle(content: string, filename: string): string {
69
+ const h1Match = content.match(/^#\s+(.+)$/m);
70
+ if (h1Match) {
71
+ return h1Match[1].trim();
72
+ }
73
+ // Fall back to filename without extension
74
+ return filename.replace(/\.spec\.md$/i, '').replace(/\.md$/i, '').replace(/-/g, ' ');
75
+ }
76
+
77
+ /**
78
+ * Scan a directory for spec files
79
+ */
80
+ export function scanSpecDir(
81
+ dir: string,
82
+ category: 'roadmap' | 'active' | 'completed'
83
+ ): SpecFile[] {
84
+ if (!existsSync(dir)) {
85
+ return [];
86
+ }
87
+
88
+ const files = readdirSync(dir).filter(
89
+ (f) => f.endsWith('.spec.md')
90
+ );
91
+
92
+ return files.map((filename) => {
93
+ const path = join(dir, filename);
94
+ const content = readFileSync(path, 'utf-8');
95
+ const title = extractTitle(content, filename);
96
+ const id = filename.replace(/\.spec\.md$/i, '').replace(/\.md$/i, '');
97
+ const frontmatter = parseFrontmatter(content);
98
+
99
+ return {
100
+ id,
101
+ filename,
102
+ path,
103
+ title,
104
+ category,
105
+ domain_name: frontmatter?.domain_name || 'uncategorized',
106
+ type: frontmatter?.type || 'milestone',
107
+ status: frontmatter?.status || (category === 'completed' ? 'completed' : category === 'roadmap' ? 'roadmap' : 'in_progress'),
108
+ dependencies: frontmatter?.dependencies || [],
109
+ branch: frontmatter?.branch,
110
+ };
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Load all spec files grouped by category
116
+ */
117
+ export function loadAllSpecs(cwd?: string): SpecGroup[] {
118
+ const basePath = cwd || process.cwd();
119
+ const groups: SpecGroup[] = [];
120
+
121
+ // Roadmap specs (planned)
122
+ const roadmapDir = join(basePath, 'specs', 'roadmap');
123
+ const roadmapSpecs = scanSpecDir(roadmapDir, 'roadmap');
124
+ if (roadmapSpecs.length > 0) {
125
+ groups.push({
126
+ category: 'roadmap',
127
+ label: 'Roadmap (Planned)',
128
+ specs: roadmapSpecs,
129
+ });
130
+ }
131
+
132
+ // Completed specs (in specs/ root)
133
+ const specsDir = join(basePath, 'specs');
134
+ const completedSpecs = scanSpecDir(specsDir, 'completed');
135
+ if (completedSpecs.length > 0) {
136
+ groups.push({
137
+ category: 'completed',
138
+ label: 'Completed',
139
+ specs: completedSpecs,
140
+ });
141
+ }
142
+
143
+ return groups;
144
+ }
145
+
146
+ /**
147
+ * Find a spec file by ID
148
+ */
149
+ export function findSpecById(specId: string, cwd?: string): SpecFile | null {
150
+ const groups = loadAllSpecs(cwd);
151
+ for (const group of groups) {
152
+ const spec = group.specs.find((s) => s.id === specId);
153
+ if (spec) {
154
+ return spec;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Convert spec groups to modal items format
162
+ */
163
+ export function specsToModalItems(
164
+ groups: SpecGroup[]
165
+ ): Array<{ id: string; label: string; type: 'header' | 'item' }> {
166
+ const items: Array<{ id: string; label: string; type: 'header' | 'item' }> = [];
167
+
168
+ for (const group of groups) {
169
+ // Add header
170
+ items.push({
171
+ id: `header-${group.category}`,
172
+ label: `── ${group.label} ──`,
173
+ type: 'header',
174
+ });
175
+
176
+ // Add specs (use id which is the filename without extension)
177
+ for (const spec of group.specs) {
178
+ items.push({
179
+ id: spec.id,
180
+ label: spec.id,
181
+ type: 'item',
182
+ });
183
+ }
184
+ }
185
+
186
+ // If no specs found, add a helpful message
187
+ if (items.length === 0) {
188
+ items.push({
189
+ id: 'header-empty',
190
+ label: '── No specs found ──',
191
+ type: 'header',
192
+ });
193
+ items.push({
194
+ id: 'info',
195
+ label: 'Add .md files to specs/',
196
+ type: 'item',
197
+ });
198
+ }
199
+
200
+ return items;
201
+ }
202
+
203
+ /**
204
+ * Load all specs grouped by domain_name
205
+ */
206
+ export function loadSpecsByDomain(cwd?: string): DomainGroup[] {
207
+ const groups = loadAllSpecs(cwd);
208
+ const allSpecs = groups.flatMap((g) => g.specs);
209
+
210
+ // Group by domain_name
211
+ const byDomain: Record<string, SpecFile[]> = {};
212
+ for (const spec of allSpecs) {
213
+ const domain = spec.domain_name;
214
+ if (!byDomain[domain]) {
215
+ byDomain[domain] = [];
216
+ }
217
+ byDomain[domain].push(spec);
218
+ }
219
+
220
+ // Convert to array sorted by domain name
221
+ return Object.entries(byDomain)
222
+ .sort(([a], [b]) => a.localeCompare(b))
223
+ .map(([domain_name, specs]) => ({
224
+ domain_name,
225
+ specs: specs.sort((a, b) => a.id.localeCompare(b.id)),
226
+ }));
227
+ }
228
+
229
+ /**
230
+ * Get all specs with a specific status
231
+ */
232
+ export function getSpecsByStatus(
233
+ status: 'roadmap' | 'in_progress' | 'completed',
234
+ cwd?: string
235
+ ): SpecFile[] {
236
+ const groups = loadAllSpecs(cwd);
237
+ return groups.flatMap((g) => g.specs).filter((s) => s.status === status);
238
+ }
239
+
240
+ /**
241
+ * Find a spec by its branch field
242
+ * Returns the spec whose frontmatter.branch matches the given branch name
243
+ *
244
+ * NOTE: This only works if the spec file exists on the current branch.
245
+ * For robust lookup that works across branch switches, use getSpecForBranch().
246
+ */
247
+ export function findSpecByBranch(branch: string, cwd?: string): SpecFile | null {
248
+ const groups = loadAllSpecs(cwd);
249
+ for (const group of groups) {
250
+ const spec = group.specs.find((s) => s.branch === branch);
251
+ if (spec) {
252
+ return spec;
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Get the spec associated with a branch, using .planning/ as the source of truth.
260
+ *
261
+ * This function works correctly even when spec files don't exist on the current
262
+ * branch (e.g., when on a feature branch but specs live on main).
263
+ *
264
+ * Lookup order:
265
+ * 1. Check .planning/{sanitized_branch}/status.yaml for persisted spec path
266
+ * 2. If found, extract spec ID and try to load full spec from current tree
267
+ * 3. If spec file not on current branch, return minimal SpecFile with ID
268
+ * 4. Fall back to findSpecByBranch() - if found, create planning dir
269
+ */
270
+ export function getSpecForBranch(branch: string, cwd?: string): SpecFile | null {
271
+ const basePath = cwd || process.cwd();
272
+ const planningKey = branch.replace(/[^a-zA-Z0-9_-]/g, '-');
273
+ const statusPath = join(basePath, '.planning', planningKey, 'status.yaml');
274
+
275
+ // Check if planning directory exists with status file
276
+ if (existsSync(statusPath)) {
277
+ try {
278
+ const content = readFileSync(statusPath, 'utf-8');
279
+ const status = parseYaml(content) as { spec?: string; name?: string };
280
+
281
+ if (status?.spec) {
282
+ // Extract spec ID from path (e.g., "specs/roadmap/my-spec.md" -> "my-spec")
283
+ const specId = basename(status.spec)
284
+ .replace(/\.spec\.md$/i, '')
285
+ .replace(/\.md$/i, '');
286
+
287
+ // Try to find the full spec in current tree
288
+ const fullSpec = findSpecById(specId, cwd);
289
+ if (fullSpec) {
290
+ return fullSpec;
291
+ }
292
+
293
+ // Spec file not on current branch - return minimal SpecFile
294
+ // This happens when spec is on main but we're on a feature branch
295
+ return {
296
+ id: specId,
297
+ filename: basename(status.spec),
298
+ path: status.spec,
299
+ title: specId.replace(/-/g, ' '),
300
+ category: 'active',
301
+ domain_name: 'unknown',
302
+ type: 'milestone',
303
+ status: 'in_progress',
304
+ dependencies: [],
305
+ branch: branch,
306
+ };
307
+ }
308
+ } catch {
309
+ // Status file parse error - fall through to findSpecByBranch
310
+ }
311
+ }
312
+
313
+ // No planning dir or no spec in status - try branch-based lookup
314
+ const spec = findSpecByBranch(branch, cwd);
315
+
316
+ // If we found a spec but no planning dir exists, create it
317
+ if (spec) {
318
+ try {
319
+ ensurePlanningDir(planningKey, cwd);
320
+ initializeStatus(planningKey, spec.path, branch, cwd);
321
+ } catch {
322
+ // Failed to create planning dir - continue without it
323
+ }
324
+ }
325
+
326
+ return spec;
327
+ }
328
+
329
+ const VALID_WORKFLOW_DOMAINS: SpecType[] = ['milestone', 'investigation', 'optimization', 'refactor', 'documentation', 'triage'];
330
+
331
+ /**
332
+ * Get the workflow domain from a spec file's frontmatter.
333
+ * Uses proper YAML parsing via parseFrontmatter(). Validates against
334
+ * the SpecType union and defaults to 'milestone' for unknown values.
335
+ */
336
+ export function getWorkflowDomain(specPath: string): SpecType {
337
+ try {
338
+ if (!existsSync(specPath)) return 'milestone';
339
+ const content = readFileSync(specPath, 'utf-8');
340
+ const frontmatter = parseFrontmatter(content);
341
+ if (frontmatter?.initial_workflow_domain && VALID_WORKFLOW_DOMAINS.includes(frontmatter.initial_workflow_domain)) {
342
+ return frontmatter.initial_workflow_domain;
343
+ }
344
+ } catch {
345
+ // Ignore parse errors, use default
346
+ }
347
+ return 'milestone';
348
+ }