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.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- 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
|
+
}
|