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,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Directory Management
|
|
3
|
+
*
|
|
4
|
+
* Handles .planning/ directory structure:
|
|
5
|
+
* - .planning/{branch}/prompts/ - Prompt files for execution
|
|
6
|
+
* - .planning/{branch}/alignment.md - Alignment doc with decisions
|
|
7
|
+
* - .planning/{branch}/status.yaml - Session state
|
|
8
|
+
*
|
|
9
|
+
* In the branch-keyed model:
|
|
10
|
+
* - Planning directories are keyed by sanitized branch name (feature/foo → feature-foo)
|
|
11
|
+
* - The spec's frontmatter.branch field is the source of truth for which branch belongs to which spec
|
|
12
|
+
* - Current git branch determines the active spec via findSpecByBranch()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
19
|
+
import { getBaseBranch } from './git.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Locked branch patterns - branches that should never have planning dirs.
|
|
23
|
+
* Includes BASE_BRANCH, common protected branches, and worktree/quick prefixes.
|
|
24
|
+
*/
|
|
25
|
+
const LOCKED_BRANCH_NAMES = new Set([
|
|
26
|
+
'main',
|
|
27
|
+
'master',
|
|
28
|
+
'develop',
|
|
29
|
+
'dev',
|
|
30
|
+
'stage',
|
|
31
|
+
'staging',
|
|
32
|
+
'prod',
|
|
33
|
+
'production',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const LOCKED_BRANCH_PREFIXES = ['wt-', 'quick/'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize a branch name for use as a directory name.
|
|
40
|
+
* Converts slashes and other non-safe characters to hyphens.
|
|
41
|
+
* Example: feature/foo-bar → feature-foo-bar
|
|
42
|
+
*/
|
|
43
|
+
export function sanitizeBranchForDir(branch: string): string {
|
|
44
|
+
return branch.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a branch is a "locked" branch that should not have planning.
|
|
49
|
+
* Locked branches: BASE_BRANCH, main, develop, dev, stage, staging, prod, production, wt-*, quick/*
|
|
50
|
+
*/
|
|
51
|
+
export function isLockedBranch(branch: string): boolean {
|
|
52
|
+
// Check if it's the configured base branch
|
|
53
|
+
const baseBranch = getBaseBranch();
|
|
54
|
+
if (branch === baseBranch) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check against known locked names
|
|
59
|
+
if (LOCKED_BRANCH_NAMES.has(branch)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check prefixes
|
|
64
|
+
for (const prefix of LOCKED_BRANCH_PREFIXES) {
|
|
65
|
+
if (branch.startsWith(prefix)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LoopConfig {
|
|
74
|
+
enabled?: boolean; // Deprecated: loop always starts disabled, not persisted
|
|
75
|
+
parallel?: boolean; // Parallel execution enabled (persisted per spec)
|
|
76
|
+
iteration: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PRStatus {
|
|
80
|
+
url: string;
|
|
81
|
+
number: number;
|
|
82
|
+
created: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PRReviewStatus {
|
|
86
|
+
reviewCycle: number;
|
|
87
|
+
lastReviewTime: string | null;
|
|
88
|
+
lastReviewRunTime: string | null; // When we started waiting for review
|
|
89
|
+
status: 'pending' | 'reviewing' | 'completed' | 'none';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface StatusFile {
|
|
93
|
+
name: string; // Directory key (sanitized branch name)
|
|
94
|
+
branch?: string; // Original branch name (for collision detection)
|
|
95
|
+
spec: string; // Path to spec file
|
|
96
|
+
stage: 'planning' | 'executing' | 'reviewing' | 'pr' | 'compound' | 'steering';
|
|
97
|
+
loop: LoopConfig;
|
|
98
|
+
compound_run: boolean;
|
|
99
|
+
created: string;
|
|
100
|
+
updated: string;
|
|
101
|
+
pr?: PRStatus;
|
|
102
|
+
prReview?: PRReviewStatus;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface AlignmentFrontmatter {
|
|
106
|
+
name: string; // Spec name
|
|
107
|
+
spec: string; // Path to spec file
|
|
108
|
+
created: string;
|
|
109
|
+
updated: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface DecisionEntry {
|
|
113
|
+
promptNumber: number;
|
|
114
|
+
promptTitle: string;
|
|
115
|
+
decision: string;
|
|
116
|
+
files: string[];
|
|
117
|
+
summary: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the current git branch name
|
|
122
|
+
*/
|
|
123
|
+
export function getCurrentBranch(cwd?: string): string {
|
|
124
|
+
try {
|
|
125
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
126
|
+
cwd: cwd || process.cwd(),
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
|
+
}).trim();
|
|
130
|
+
return branch;
|
|
131
|
+
} catch {
|
|
132
|
+
return 'main';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the root of the git repository
|
|
138
|
+
*/
|
|
139
|
+
export function getGitRoot(cwd?: string): string {
|
|
140
|
+
try {
|
|
141
|
+
const root = execSync('git rev-parse --show-toplevel', {
|
|
142
|
+
cwd: cwd || process.cwd(),
|
|
143
|
+
encoding: 'utf-8',
|
|
144
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
145
|
+
}).trim();
|
|
146
|
+
return root;
|
|
147
|
+
} catch {
|
|
148
|
+
return process.cwd();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the .planning directory path for a key (sanitized branch name)
|
|
154
|
+
*/
|
|
155
|
+
export function getPlanningDir(key: string, cwd?: string): string {
|
|
156
|
+
const gitRoot = getGitRoot(cwd);
|
|
157
|
+
return join(gitRoot, '.planning', key);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get paths within the planning directory for a key
|
|
162
|
+
*/
|
|
163
|
+
export function getPlanningPaths(key: string, cwd?: string) {
|
|
164
|
+
const planningDir = getPlanningDir(key, cwd);
|
|
165
|
+
return {
|
|
166
|
+
root: planningDir,
|
|
167
|
+
prompts: join(planningDir, 'prompts'),
|
|
168
|
+
alignment: join(planningDir, 'alignment.md'),
|
|
169
|
+
status: join(planningDir, 'status.yaml'),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ensure the .planning directory structure exists for a key
|
|
175
|
+
*/
|
|
176
|
+
export function ensurePlanningDir(key: string, cwd?: string): void {
|
|
177
|
+
const paths = getPlanningPaths(key, cwd);
|
|
178
|
+
|
|
179
|
+
// Create directories
|
|
180
|
+
mkdirSync(paths.root, { recursive: true });
|
|
181
|
+
mkdirSync(paths.prompts, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if planning directory exists for a key
|
|
186
|
+
*/
|
|
187
|
+
export function planningDirExists(key: string, cwd?: string): boolean {
|
|
188
|
+
const paths = getPlanningPaths(key, cwd);
|
|
189
|
+
return existsSync(paths.root);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read the status file for a key
|
|
194
|
+
*/
|
|
195
|
+
export function readStatus(key: string, cwd?: string): StatusFile | null {
|
|
196
|
+
const paths = getPlanningPaths(key, cwd);
|
|
197
|
+
|
|
198
|
+
if (!existsSync(paths.status)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const content = readFileSync(paths.status, 'utf-8');
|
|
204
|
+
return parseYaml(content) as StatusFile;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if a branch matches the status file's original branch.
|
|
212
|
+
* Returns true if they match or if no branch is stored (backwards compatibility).
|
|
213
|
+
* Returns false if there's a collision (different branches sanitized to same key).
|
|
214
|
+
*/
|
|
215
|
+
export function validateBranchForStatus(
|
|
216
|
+
currentBranch: string,
|
|
217
|
+
key: string,
|
|
218
|
+
cwd?: string
|
|
219
|
+
): { valid: boolean; storedBranch?: string } {
|
|
220
|
+
const status = readStatus(key, cwd);
|
|
221
|
+
if (!status) {
|
|
222
|
+
return { valid: true }; // No status file, no collision
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!status.branch) {
|
|
226
|
+
return { valid: true }; // Old status file without branch, assume valid
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (status.branch === currentBranch) {
|
|
230
|
+
return { valid: true, storedBranch: status.branch };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Collision detected: different branch maps to same key
|
|
234
|
+
return { valid: false, storedBranch: status.branch };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Write the status file for a key
|
|
239
|
+
*/
|
|
240
|
+
export function writeStatus(status: StatusFile, key: string, cwd?: string): void {
|
|
241
|
+
const paths = getPlanningPaths(key, cwd);
|
|
242
|
+
ensurePlanningDir(key, cwd);
|
|
243
|
+
|
|
244
|
+
const content = stringifyYaml(status);
|
|
245
|
+
writeFileSync(paths.status, content);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Update specific fields in the status file for a key
|
|
250
|
+
*/
|
|
251
|
+
export function updateStatus(
|
|
252
|
+
updates: Partial<StatusFile>,
|
|
253
|
+
key: string,
|
|
254
|
+
cwd?: string
|
|
255
|
+
): StatusFile {
|
|
256
|
+
const current = readStatus(key, cwd);
|
|
257
|
+
if (!current) {
|
|
258
|
+
throw new Error('No status file exists. Initialize planning first.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const updated: StatusFile = {
|
|
262
|
+
...current,
|
|
263
|
+
...updates,
|
|
264
|
+
updated: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
writeStatus(updated, key, cwd);
|
|
268
|
+
return updated;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create initial status file for a new planning directory
|
|
273
|
+
*
|
|
274
|
+
* @param key - The directory key (sanitized branch name)
|
|
275
|
+
* @param specPath - Path to the spec file
|
|
276
|
+
* @param originalBranch - Original branch name (for collision detection)
|
|
277
|
+
* @param cwd - Working directory
|
|
278
|
+
*/
|
|
279
|
+
export function initializeStatus(
|
|
280
|
+
key: string,
|
|
281
|
+
specPath: string,
|
|
282
|
+
originalBranch?: string | null,
|
|
283
|
+
cwd?: string
|
|
284
|
+
): StatusFile {
|
|
285
|
+
const now = new Date().toISOString();
|
|
286
|
+
|
|
287
|
+
const status: StatusFile = {
|
|
288
|
+
name: key,
|
|
289
|
+
branch: originalBranch ?? undefined,
|
|
290
|
+
spec: specPath,
|
|
291
|
+
stage: 'planning',
|
|
292
|
+
loop: {
|
|
293
|
+
iteration: 0,
|
|
294
|
+
},
|
|
295
|
+
compound_run: false,
|
|
296
|
+
created: now,
|
|
297
|
+
updated: now,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
writeStatus(status, key, cwd);
|
|
301
|
+
return status;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Read the alignment doc frontmatter for a key
|
|
306
|
+
*/
|
|
307
|
+
export function readAlignmentFrontmatter(
|
|
308
|
+
key: string,
|
|
309
|
+
cwd?: string
|
|
310
|
+
): AlignmentFrontmatter | null {
|
|
311
|
+
const paths = getPlanningPaths(key, cwd);
|
|
312
|
+
|
|
313
|
+
if (!existsSync(paths.alignment)) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const content = readFileSync(paths.alignment, 'utf-8');
|
|
319
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
320
|
+
if (!frontmatterMatch) return null;
|
|
321
|
+
|
|
322
|
+
return parseYaml(frontmatterMatch[1]) as AlignmentFrontmatter;
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read the full alignment doc for a key
|
|
330
|
+
*/
|
|
331
|
+
export function readAlignment(key: string, cwd?: string): string | null {
|
|
332
|
+
const paths = getPlanningPaths(key, cwd);
|
|
333
|
+
|
|
334
|
+
if (!existsSync(paths.alignment)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return readFileSync(paths.alignment, 'utf-8');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Create initial alignment doc for a spec
|
|
343
|
+
*/
|
|
344
|
+
export function initializeAlignment(
|
|
345
|
+
specName: string,
|
|
346
|
+
specPath: string,
|
|
347
|
+
overview: string,
|
|
348
|
+
hardRequirements: string[],
|
|
349
|
+
cwd?: string
|
|
350
|
+
): void {
|
|
351
|
+
const paths = getPlanningPaths(specName, cwd);
|
|
352
|
+
ensurePlanningDir(specName, cwd);
|
|
353
|
+
|
|
354
|
+
const now = new Date().toISOString();
|
|
355
|
+
const frontmatter = stringifyYaml({
|
|
356
|
+
name: specName,
|
|
357
|
+
spec: specPath,
|
|
358
|
+
created: now,
|
|
359
|
+
updated: now,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const requirementsList = hardRequirements.map((r) => `- ${r}`).join('\n');
|
|
363
|
+
|
|
364
|
+
const content = `---
|
|
365
|
+
${frontmatter.trim()}
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Overview
|
|
369
|
+
|
|
370
|
+
${overview}
|
|
371
|
+
|
|
372
|
+
## Hard Requirements
|
|
373
|
+
|
|
374
|
+
${requirementsList}
|
|
375
|
+
|
|
376
|
+
## Key Decisions
|
|
377
|
+
|
|
378
|
+
<!-- Decisions appended by executing agents -->
|
|
379
|
+
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
writeFileSync(paths.alignment, content);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Append a decision to the alignment doc for a key
|
|
387
|
+
*/
|
|
388
|
+
export function appendDecision(
|
|
389
|
+
entry: DecisionEntry,
|
|
390
|
+
key: string,
|
|
391
|
+
cwd?: string
|
|
392
|
+
): void {
|
|
393
|
+
const paths = getPlanningPaths(key, cwd);
|
|
394
|
+
|
|
395
|
+
if (!existsSync(paths.alignment)) {
|
|
396
|
+
throw new Error('No alignment doc exists. Initialize planning first.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const content = readFileSync(paths.alignment, 'utf-8');
|
|
400
|
+
const filesList = entry.files.map((f) => `\`${f}\``).join(', ');
|
|
401
|
+
const promptLink = `./prompts/${String(entry.promptNumber).padStart(2, '0')}-${entry.promptTitle.toLowerCase().replace(/\s+/g, '-')}.md`;
|
|
402
|
+
|
|
403
|
+
const decisionBlock = `
|
|
404
|
+
### Prompt ${String(entry.promptNumber).padStart(2, '0')}: ${entry.promptTitle}
|
|
405
|
+
|
|
406
|
+
**Decision**: ${entry.decision}
|
|
407
|
+
|
|
408
|
+
**Files**: ${filesList}
|
|
409
|
+
|
|
410
|
+
**Summary**: ${entry.summary}
|
|
411
|
+
|
|
412
|
+
**Link**: \`${promptLink}\`
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
// Update frontmatter updated timestamp
|
|
416
|
+
const updatedContent = content.replace(
|
|
417
|
+
/^(---\n[\s\S]*?updated:\s*).+(\n---)/m,
|
|
418
|
+
`$1${new Date().toISOString()}$2`
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
writeFileSync(paths.alignment, updatedContent + decisionBlock);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List all prompt files in the planning directory for a key
|
|
426
|
+
*/
|
|
427
|
+
export function listPromptFiles(key: string, cwd?: string): string[] {
|
|
428
|
+
const paths = getPlanningPaths(key, cwd);
|
|
429
|
+
|
|
430
|
+
if (!existsSync(paths.prompts)) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return readdirSync(paths.prompts)
|
|
435
|
+
.filter((f) => f.endsWith('.md'))
|
|
436
|
+
.sort();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get alignment doc token count estimate (rough) for a key
|
|
441
|
+
*/
|
|
442
|
+
export function getAlignmentTokenCount(key: string, cwd?: string): number {
|
|
443
|
+
const content = readAlignment(key, cwd);
|
|
444
|
+
if (!content) return 0;
|
|
445
|
+
|
|
446
|
+
// Rough estimate: ~4 chars per token
|
|
447
|
+
return Math.ceil(content.length / 4);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Update PR status in status file for a key
|
|
452
|
+
*/
|
|
453
|
+
export function updatePRStatus(
|
|
454
|
+
url: string,
|
|
455
|
+
number: number,
|
|
456
|
+
key: string,
|
|
457
|
+
cwd?: string
|
|
458
|
+
): StatusFile {
|
|
459
|
+
return updateStatus(
|
|
460
|
+
{
|
|
461
|
+
pr: {
|
|
462
|
+
url,
|
|
463
|
+
number,
|
|
464
|
+
created: new Date().toISOString(),
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
key,
|
|
468
|
+
cwd
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Update PR review status in status file for a key
|
|
474
|
+
*/
|
|
475
|
+
export function updatePRReviewStatus(
|
|
476
|
+
state: Partial<PRReviewStatus>,
|
|
477
|
+
key: string,
|
|
478
|
+
cwd?: string
|
|
479
|
+
): StatusFile {
|
|
480
|
+
const current = readStatus(key, cwd);
|
|
481
|
+
if (!current) {
|
|
482
|
+
throw new Error('No status file exists. Initialize planning first.');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const currentPRReview = current.prReview || {
|
|
486
|
+
reviewCycle: 0,
|
|
487
|
+
lastReviewTime: null,
|
|
488
|
+
lastReviewRunTime: null,
|
|
489
|
+
status: 'none' as const,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
return updateStatus(
|
|
493
|
+
{
|
|
494
|
+
prReview: {
|
|
495
|
+
...currentPRReview,
|
|
496
|
+
...state,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
key,
|
|
500
|
+
cwd
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Reset planning artifacts for re-ideation.
|
|
506
|
+
*
|
|
507
|
+
* Deletes all prompt files and the alignment doc, then resets
|
|
508
|
+
* status.yaml stage to 'planning'. Keeps the status.yaml and
|
|
509
|
+
* directory structure intact.
|
|
510
|
+
*
|
|
511
|
+
* @param key - The directory key (sanitized branch name)
|
|
512
|
+
* @param cwd - Working directory
|
|
513
|
+
* @returns true if artifacts were reset, false if no planning dir exists
|
|
514
|
+
*/
|
|
515
|
+
export function resetPlanningArtifacts(key: string, cwd?: string): boolean {
|
|
516
|
+
const paths = getPlanningPaths(key, cwd);
|
|
517
|
+
|
|
518
|
+
if (!existsSync(paths.root)) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Delete all files in prompts/ subdirectory
|
|
523
|
+
if (existsSync(paths.prompts)) {
|
|
524
|
+
const promptFiles = readdirSync(paths.prompts);
|
|
525
|
+
for (const file of promptFiles) {
|
|
526
|
+
unlinkSync(join(paths.prompts, file));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Delete alignment.md if it exists
|
|
531
|
+
if (existsSync(paths.alignment)) {
|
|
532
|
+
unlinkSync(paths.alignment);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Reset status.yaml stage to 'planning'
|
|
536
|
+
if (existsSync(paths.status)) {
|
|
537
|
+
const status = readStatus(key, cwd);
|
|
538
|
+
if (status) {
|
|
539
|
+
updateStatus({ stage: 'planning' }, key, cwd);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ============================================================================
|
|
547
|
+
// Planning Directory Listing
|
|
548
|
+
// ============================================================================
|
|
549
|
+
|
|
550
|
+
export interface PlanningInfo {
|
|
551
|
+
/** Directory key (sanitized branch name) */
|
|
552
|
+
key: string;
|
|
553
|
+
/** Path to spec file */
|
|
554
|
+
specPath: string;
|
|
555
|
+
/** Current stage */
|
|
556
|
+
stage: string;
|
|
557
|
+
/** Whether this is for the current git branch */
|
|
558
|
+
isCurrent: boolean;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* List all planning directories
|
|
563
|
+
*/
|
|
564
|
+
export function listPlanningDirs(cwd?: string): PlanningInfo[] {
|
|
565
|
+
const gitRoot = getGitRoot(cwd);
|
|
566
|
+
const planningRoot = join(gitRoot, '.planning');
|
|
567
|
+
|
|
568
|
+
if (!existsSync(planningRoot)) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
573
|
+
const currentKey = sanitizeBranchForDir(currentBranch);
|
|
574
|
+
const entries = readdirSync(planningRoot, { withFileTypes: true });
|
|
575
|
+
const dirs: PlanningInfo[] = [];
|
|
576
|
+
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
// Skip non-directories and hidden files
|
|
579
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const statusPath = join(planningRoot, entry.name, 'status.yaml');
|
|
584
|
+
if (!existsSync(statusPath)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const content = readFileSync(statusPath, 'utf-8');
|
|
590
|
+
const status = parseYaml(content) as StatusFile;
|
|
591
|
+
|
|
592
|
+
dirs.push({
|
|
593
|
+
key: entry.name,
|
|
594
|
+
specPath: status.spec,
|
|
595
|
+
stage: status.stage,
|
|
596
|
+
isCurrent: entry.name === currentKey,
|
|
597
|
+
});
|
|
598
|
+
} catch {
|
|
599
|
+
// Skip malformed status files
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return dirs;
|
|
605
|
+
}
|