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,960 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Command - Launch the terminal user interface
|
|
3
|
+
*
|
|
4
|
+
* The TUI provides a dashboard for:
|
|
5
|
+
* - Starting/stopping the execution loop
|
|
6
|
+
* - Spawning agent sessions (ideate, coordinator, planner, etc.)
|
|
7
|
+
* - Monitoring loop progress and agent activity
|
|
8
|
+
* - Managing specs and PR workflows
|
|
9
|
+
*
|
|
10
|
+
* In the branch-keyed model:
|
|
11
|
+
* - Current git branch determines the active spec
|
|
12
|
+
* - Switching specs means checking out the spec's branch
|
|
13
|
+
* - Planning directories are keyed by sanitized branch name
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
import { join, basename, dirname, relative } from 'path';
|
|
18
|
+
import { existsSync, readFileSync, renameSync } from 'fs';
|
|
19
|
+
|
|
20
|
+
import { TUI } from '../tui/index.js';
|
|
21
|
+
import type { TUIState, PRActionState, PromptItem, AgentInfo } from '../tui/index.js';
|
|
22
|
+
import {
|
|
23
|
+
readStatus,
|
|
24
|
+
getCurrentBranch,
|
|
25
|
+
updateStatus,
|
|
26
|
+
updatePRReviewStatus,
|
|
27
|
+
initializeStatus,
|
|
28
|
+
getPlanningPaths,
|
|
29
|
+
ensurePlanningDir,
|
|
30
|
+
sanitizeBranchForDir,
|
|
31
|
+
planningDirExists,
|
|
32
|
+
} from '../lib/planning.js';
|
|
33
|
+
import { triggerPRReview } from '../lib/pr-review.js';
|
|
34
|
+
import { loadProjectSettings } from '../hooks/shared.js';
|
|
35
|
+
import { findSpecForPath, extractSpecNameFromFile } from '../lib/planning-utils.js';
|
|
36
|
+
import { getBaseBranch, hasUncommittedChanges, gitExec, validateGitRef, syncWithOriginMain } from '../lib/git.js';
|
|
37
|
+
import { loadAllPrompts, getNextPromptNumber } from '../lib/prompts.js';
|
|
38
|
+
import {
|
|
39
|
+
isTmuxInstalled,
|
|
40
|
+
spawnAgentFromProfile,
|
|
41
|
+
buildTemplateContext,
|
|
42
|
+
getRunningAgents,
|
|
43
|
+
renameWindow,
|
|
44
|
+
getCurrentWindowId,
|
|
45
|
+
} from '../lib/tmux.js';
|
|
46
|
+
import { setHubWindowId, clearTuiSession } from '../lib/session.js';
|
|
47
|
+
import { getProfilesByTuiAction } from '../lib/opencode/index.js';
|
|
48
|
+
import { buildPR } from '../lib/oracle.js';
|
|
49
|
+
import type { PromptFile } from '../lib/prompts.js';
|
|
50
|
+
import { findSpecById, getSpecForBranch, type SpecFile, type SpecType } from '../lib/specs.js';
|
|
51
|
+
import { updateSpecStatus, reindexAfterMove } from './specs.js';
|
|
52
|
+
import { logTuiError, logTuiAction, logTuiLifecycle } from '../lib/trace-store.js';
|
|
53
|
+
import { getFlowsDirectory } from '../lib/flows.js';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Unified scoping flow — all spec types route here.
|
|
57
|
+
* Domain-specific behavior is driven by the WORKFLOW_DOMAIN_PATH template variable,
|
|
58
|
+
* not by flow file selection.
|
|
59
|
+
*/
|
|
60
|
+
export const UNIFIED_SCOPING_FLOW = 'IDEATION_SCOPING.md';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Launch the TUI - can be called directly or via command
|
|
64
|
+
*/
|
|
65
|
+
export async function launchTUI(options: { spec?: string } = {}): Promise<void> {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
|
|
68
|
+
// IMMEDIATELY capture window ID before any slow operations (like tldr indexing)
|
|
69
|
+
// This ensures we track the correct window even if user switches away during init
|
|
70
|
+
const hubWindowId = getCurrentWindowId();
|
|
71
|
+
if (hubWindowId) {
|
|
72
|
+
setHubWindowId(hubWindowId, cwd);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const branch = getCurrentBranch(cwd);
|
|
76
|
+
const planningKey = sanitizeBranchForDir(branch);
|
|
77
|
+
|
|
78
|
+
// Find current spec from branch using planning dir as source of truth
|
|
79
|
+
const currentSpec = getSpecForBranch(branch, cwd);
|
|
80
|
+
|
|
81
|
+
// TLDR semantic indexing now runs in background after TUI launches (see TUI.startBackgroundIndexing)
|
|
82
|
+
|
|
83
|
+
// Load initial state from current branch's planning directory
|
|
84
|
+
// Load prompts if planning directory exists, regardless of status file
|
|
85
|
+
const status = planningDirExists(planningKey, cwd) ? readStatus(planningKey, cwd) : null;
|
|
86
|
+
const prompts = planningDirExists(planningKey, cwd) ? loadAllPrompts(planningKey, cwd) : [];
|
|
87
|
+
|
|
88
|
+
// Convert prompts to PromptItem format
|
|
89
|
+
const promptItems: PromptItem[] = prompts.map((p) => ({
|
|
90
|
+
number: p.frontmatter.number,
|
|
91
|
+
title: p.frontmatter.title,
|
|
92
|
+
status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
|
|
93
|
+
path: p.path,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
// Get active agents from tmux
|
|
97
|
+
const runningAgents = getRunningAgents(branch);
|
|
98
|
+
const activeAgents: AgentInfo[] = runningAgents.map((a) => ({
|
|
99
|
+
name: a.windowName,
|
|
100
|
+
agentType: a.agentType || 'unknown',
|
|
101
|
+
isRunning: true,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const baseBranch = getBaseBranch();
|
|
105
|
+
|
|
106
|
+
// Determine initial PR action state based on PR and review status
|
|
107
|
+
let initialPRActionState: PRActionState = 'create-pr';
|
|
108
|
+
if (status?.pr?.url) {
|
|
109
|
+
// PR exists - check if review feedback was received
|
|
110
|
+
if (status?.prReview?.lastReviewTime) {
|
|
111
|
+
// Review feedback was received - show rerun option
|
|
112
|
+
initialPRActionState = 'rerun-pr-review';
|
|
113
|
+
} else {
|
|
114
|
+
// PR created but waiting for review
|
|
115
|
+
initialPRActionState = 'awaiting-review';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const initialState: Partial<TUIState> = {
|
|
120
|
+
loopEnabled: false, // Always start disabled, regardless of saved status
|
|
121
|
+
parallelEnabled: status?.loop?.parallel ?? false,
|
|
122
|
+
prompts: promptItems,
|
|
123
|
+
activeAgents,
|
|
124
|
+
spec: currentSpec?.id,
|
|
125
|
+
branch,
|
|
126
|
+
baseBranch,
|
|
127
|
+
prActionState: initialPRActionState,
|
|
128
|
+
customFlowCounter: 0,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Rename the hub window (using captured ID to target correct window even if focus changed)
|
|
132
|
+
renameWindow(hubWindowId, 'hub');
|
|
133
|
+
|
|
134
|
+
const tui = new TUI({
|
|
135
|
+
onAction: (action: string, data) => {
|
|
136
|
+
// Get current branch/spec from TUI state (not captured at init time)
|
|
137
|
+
const currentBranch = tui.getState().branch || getCurrentBranch(cwd);
|
|
138
|
+
const currentPlanningKey = sanitizeBranchForDir(currentBranch);
|
|
139
|
+
const spec = getSpecForBranch(currentBranch, cwd);
|
|
140
|
+
handleAction(tui, action, currentPlanningKey, spec, currentBranch, data);
|
|
141
|
+
},
|
|
142
|
+
onExit: () => {
|
|
143
|
+
console.log('\nExiting All Hands TUI...');
|
|
144
|
+
process.exit(0);
|
|
145
|
+
},
|
|
146
|
+
onSpawnExecutor: (prompt, executorBranch, specId) => {
|
|
147
|
+
spawnExecutorForPrompt(tui, prompt, executorBranch, specId);
|
|
148
|
+
},
|
|
149
|
+
onSpawnEmergentPlanning: (emergentBranch, specId) => {
|
|
150
|
+
spawnEmergentPlanningAgent(tui, emergentBranch, specId);
|
|
151
|
+
},
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Set initial state
|
|
156
|
+
tui.updateState(initialState);
|
|
157
|
+
tui.log(`Branch: ${branch}`);
|
|
158
|
+
if (currentSpec) {
|
|
159
|
+
tui.log(`Spec: ${currentSpec.id} (${status?.stage || 'no planning'})`);
|
|
160
|
+
} else {
|
|
161
|
+
tui.log('No spec for this branch. Use Switch Spec to select one.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Log TUI launch
|
|
165
|
+
logTuiLifecycle('tui.start', {
|
|
166
|
+
branch,
|
|
167
|
+
spec: currentSpec?.id,
|
|
168
|
+
promptCount: promptItems.length,
|
|
169
|
+
agentCount: activeAgents.length,
|
|
170
|
+
}, cwd);
|
|
171
|
+
|
|
172
|
+
// Start TUI
|
|
173
|
+
tui.start();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// No register function - TUI is launched directly from CLI when no command is given
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Spawn agents for a TUI action using profile definitions
|
|
180
|
+
*
|
|
181
|
+
* Looks up all agent profiles with matching tui_action and spawns them.
|
|
182
|
+
* Multiple profiles can share the same tui_action (e.g., compound can spawn multiple agents).
|
|
183
|
+
*/
|
|
184
|
+
async function spawnAgentsForAction(
|
|
185
|
+
tui: TUI,
|
|
186
|
+
action: string,
|
|
187
|
+
planningKey: string | null,
|
|
188
|
+
currentSpec: SpecFile | null,
|
|
189
|
+
branch: string,
|
|
190
|
+
status: ReturnType<typeof readStatus>,
|
|
191
|
+
cwd?: string,
|
|
192
|
+
contextOverrides?: Record<string, string>
|
|
193
|
+
): Promise<boolean> {
|
|
194
|
+
const profileMap = getProfilesByTuiAction();
|
|
195
|
+
const profiles = profileMap.get(action);
|
|
196
|
+
|
|
197
|
+
if (!profiles || profiles.length === 0) {
|
|
198
|
+
return false; // No profiles for this action
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if any profile requires spec
|
|
202
|
+
const requiresSpec = profiles.some((p) => p.tuiRequiresSpec);
|
|
203
|
+
if (requiresSpec && !currentSpec) {
|
|
204
|
+
tui.log('Error: No spec for this branch. Checkout a spec branch first.');
|
|
205
|
+
return true; // Handled, but with error
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check tmux availability
|
|
209
|
+
if (!isTmuxInstalled()) {
|
|
210
|
+
tui.log('Error: tmux is required for agent spawning');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Build template context once for all agents
|
|
215
|
+
const context = buildTemplateContext(
|
|
216
|
+
planningKey || 'default', // Use planning key for paths
|
|
217
|
+
status?.name,
|
|
218
|
+
undefined, // promptNumber - not applicable for TUI actions
|
|
219
|
+
undefined, // promptPath - not applicable for TUI actions
|
|
220
|
+
cwd
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Apply context overrides (e.g., WORKFLOW_DOMAIN_PATH for initiative steering)
|
|
224
|
+
if (contextOverrides) {
|
|
225
|
+
Object.assign(context, contextOverrides);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Spawn each profile
|
|
229
|
+
for (const profile of profiles) {
|
|
230
|
+
const label = profile.tuiLabel ?? profile.name;
|
|
231
|
+
tui.log(`Spawning ${label}...`);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = spawnAgentFromProfile(
|
|
235
|
+
{
|
|
236
|
+
agentName: profile.name,
|
|
237
|
+
context,
|
|
238
|
+
focusWindow: profiles.length === 1, // Only focus if single agent
|
|
239
|
+
},
|
|
240
|
+
branch,
|
|
241
|
+
cwd
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
tui.log(`Spawned ${profile.name} in ${result.sessionName}:${result.windowName}`);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
247
|
+
tui.log(`Error spawning ${profile.name}: ${message}`);
|
|
248
|
+
logTuiError('spawn-agent', e instanceof Error ? e : message, {
|
|
249
|
+
action,
|
|
250
|
+
profileName: profile.name,
|
|
251
|
+
spec: currentSpec?.id,
|
|
252
|
+
branch,
|
|
253
|
+
}, cwd);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Track compound_run in status when compound action is triggered
|
|
258
|
+
if (action === 'compound' && planningKey && status) {
|
|
259
|
+
updateStatus({ compound_run: true }, planningKey, cwd);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
updateRunningAgents(tui, branch);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check for uncommitted changes and prompt the user for confirmation.
|
|
268
|
+
* Returns true if no uncommitted changes or user confirms proceeding.
|
|
269
|
+
*/
|
|
270
|
+
async function confirmProceedWithUncommittedChanges(
|
|
271
|
+
tui: TUI,
|
|
272
|
+
cwd: string,
|
|
273
|
+
message: string
|
|
274
|
+
): Promise<boolean> {
|
|
275
|
+
if (hasUncommittedChanges(cwd)) {
|
|
276
|
+
const proceed = await tui.showConfirmation('Uncommitted Changes', message);
|
|
277
|
+
return proceed;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function handleAction(
|
|
283
|
+
tui: TUI,
|
|
284
|
+
action: string,
|
|
285
|
+
planningKey: string | null,
|
|
286
|
+
currentSpec: SpecFile | null,
|
|
287
|
+
branch: string,
|
|
288
|
+
data?: Record<string, unknown>
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const cwd = process.cwd();
|
|
291
|
+
|
|
292
|
+
// Log TUI action
|
|
293
|
+
logTuiAction(action, {
|
|
294
|
+
spec: currentSpec?.id,
|
|
295
|
+
branch,
|
|
296
|
+
planningKey,
|
|
297
|
+
data,
|
|
298
|
+
}, cwd);
|
|
299
|
+
|
|
300
|
+
// Read status from planning directory
|
|
301
|
+
const status = planningKey ? readStatus(planningKey, cwd) : null;
|
|
302
|
+
|
|
303
|
+
// Prepare context overrides for actions that need them
|
|
304
|
+
let contextOverrides: Record<string, string> | undefined;
|
|
305
|
+
if (action === 'initiative-steering' && data?.domain) {
|
|
306
|
+
const selectedDomain = data.domain as string;
|
|
307
|
+
const domainConfigPath = join(cwd, '.allhands', 'workflows', `${selectedDomain}.md`);
|
|
308
|
+
if (existsSync(domainConfigPath)) {
|
|
309
|
+
contextOverrides = {
|
|
310
|
+
WORKFLOW_DOMAIN_PATH: domainConfigPath,
|
|
311
|
+
};
|
|
312
|
+
} else {
|
|
313
|
+
console.warn(`Workflow domain config not found: ${domainConfigPath}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Pre-spawn gate: sync with origin/main before compounding
|
|
318
|
+
if (action === 'compound') {
|
|
319
|
+
const syncResult = syncWithOriginMain(cwd);
|
|
320
|
+
if (!syncResult.success && syncResult.conflicts.length > 0) {
|
|
321
|
+
// Merge conflicts — already aborted by syncWithOriginMain
|
|
322
|
+
await tui.showConfirmation(
|
|
323
|
+
'Compounding Aborted',
|
|
324
|
+
'Merge conflicts with main — compounding aborted',
|
|
325
|
+
'Conflicting files:\n' + syncResult.conflicts.map(f => ' - ' + f).join('\n') + '\n\nResolve conflicts manually, push, and retry compounding.'
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
} else if (!syncResult.success) {
|
|
329
|
+
// Fetch failure (no conflicts — network/remote issue)
|
|
330
|
+
await tui.showConfirmation(
|
|
331
|
+
'Compounding Aborted',
|
|
332
|
+
'Failed to sync with remote main — compounding aborted',
|
|
333
|
+
'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Try to handle as a profile-based agent spawn
|
|
340
|
+
const handledByProfile = await spawnAgentsForAction(tui, action, planningKey, currentSpec, branch, status, cwd, contextOverrides);
|
|
341
|
+
if (handledByProfile) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Handle non-agent actions
|
|
346
|
+
switch (action) {
|
|
347
|
+
case 'create-pr': {
|
|
348
|
+
if (!currentSpec || !planningKey) {
|
|
349
|
+
tui.log('Error: No spec for this branch. Checkout a spec branch first.');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Warn about uncommitted changes — gives user a chance to cancel and commit first
|
|
354
|
+
const proceedWithPR = await confirmProceedWithUncommittedChanges(
|
|
355
|
+
tui, cwd, 'You have uncommitted changes that will not be included in the PR. Proceed anyway?'
|
|
356
|
+
);
|
|
357
|
+
if (!proceedWithPR) break;
|
|
358
|
+
|
|
359
|
+
// Check if we're creating or updating
|
|
360
|
+
const existingStatus = status?.pr?.url ? 'Updating' : 'Creating';
|
|
361
|
+
tui.log(`${existingStatus} PR via oracle...`);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// buildPR uses planning key for reading prompts/alignment
|
|
365
|
+
const result = await buildPR(planningKey, cwd);
|
|
366
|
+
|
|
367
|
+
if (result.success && result.prUrl) {
|
|
368
|
+
const action = result.existingPR ? 'updated' : 'created';
|
|
369
|
+
tui.log(`PR ${action}: ${result.prUrl}`);
|
|
370
|
+
tui.setPRUrl(result.prUrl);
|
|
371
|
+
|
|
372
|
+
// Set lastReviewRunTime to now for comment filtering
|
|
373
|
+
updatePRReviewStatus(
|
|
374
|
+
{ lastReviewRunTime: new Date().toISOString() },
|
|
375
|
+
planningKey,
|
|
376
|
+
cwd
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
tui.log(`Error: ${result.body}`);
|
|
380
|
+
tui.log('You may need to push your branch first or check gh auth status.');
|
|
381
|
+
logTuiError('create-pr', result.body || 'PR creation failed', {
|
|
382
|
+
spec: currentSpec.id,
|
|
383
|
+
branch,
|
|
384
|
+
}, cwd);
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {
|
|
387
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
388
|
+
tui.log(`Error: ${message}`);
|
|
389
|
+
logTuiError('create-pr', e instanceof Error ? e : message, {
|
|
390
|
+
spec: currentSpec?.id,
|
|
391
|
+
branch,
|
|
392
|
+
}, cwd);
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'rerun-pr-review': {
|
|
398
|
+
if (!planningKey) {
|
|
399
|
+
tui.log('Error: No planning context. Checkout a spec branch first.');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Get PR URL from status
|
|
404
|
+
const prStatus = status?.pr;
|
|
405
|
+
if (!prStatus?.url) {
|
|
406
|
+
tui.log('Error: No PR found. Create a PR first.');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Warn about uncommitted changes — gives user a chance to cancel and commit first
|
|
411
|
+
const proceedWithReview = await confirmProceedWithUncommittedChanges(
|
|
412
|
+
tui, cwd, 'You have uncommitted changes that will not be included in the PR. Proceed anyway?'
|
|
413
|
+
);
|
|
414
|
+
if (!proceedWithReview) break;
|
|
415
|
+
|
|
416
|
+
tui.log('Triggering PR re-review...');
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Push any unpushed commits before triggering review
|
|
420
|
+
const workingDir = cwd || process.cwd();
|
|
421
|
+
const unpushedResult = gitExec(['log', '@{u}..HEAD', '--oneline'], workingDir);
|
|
422
|
+
|
|
423
|
+
if (unpushedResult.success && unpushedResult.stdout) {
|
|
424
|
+
tui.log('Pushing local commits to remote...');
|
|
425
|
+
const pushResult = gitExec(['push'], workingDir);
|
|
426
|
+
if (pushResult.success) {
|
|
427
|
+
tui.log('Commits pushed successfully.');
|
|
428
|
+
} else {
|
|
429
|
+
tui.log('Warning: Could not push commits. Continuing with review trigger...');
|
|
430
|
+
}
|
|
431
|
+
} else if (!unpushedResult.success) {
|
|
432
|
+
// No upstream or other git error - try push anyway
|
|
433
|
+
const pushResult = gitExec(['push'], workingDir);
|
|
434
|
+
if (!pushResult.success) {
|
|
435
|
+
tui.log('Warning: Could not push commits. Continuing with review trigger...');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Load settings for rerun comment
|
|
440
|
+
const settings = loadProjectSettings();
|
|
441
|
+
const rerunComment = settings?.prReview?.rerunComment ?? '@greptile';
|
|
442
|
+
|
|
443
|
+
// Post comment to trigger review
|
|
444
|
+
const result = await triggerPRReview(prStatus.url, rerunComment, cwd);
|
|
445
|
+
|
|
446
|
+
if (result.success) {
|
|
447
|
+
tui.log(`Re-review triggered with comment: ${rerunComment}`);
|
|
448
|
+
|
|
449
|
+
// Update lastReviewRunTime and transition to awaiting state
|
|
450
|
+
updatePRReviewStatus(
|
|
451
|
+
{ lastReviewRunTime: new Date().toISOString() },
|
|
452
|
+
planningKey,
|
|
453
|
+
cwd
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// Transition TUI back to awaiting-review state
|
|
457
|
+
tui.setPRUrl(prStatus.url);
|
|
458
|
+
} else {
|
|
459
|
+
tui.log('Error: Failed to post review comment');
|
|
460
|
+
logTuiError('rerun-pr-review', 'Failed to post review comment', {
|
|
461
|
+
prUrl: prStatus.url,
|
|
462
|
+
}, cwd);
|
|
463
|
+
}
|
|
464
|
+
} catch (e) {
|
|
465
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
466
|
+
tui.log(`Error: ${message}`);
|
|
467
|
+
logTuiError('rerun-pr-review', e instanceof Error ? e : message, {
|
|
468
|
+
prUrl: prStatus?.url,
|
|
469
|
+
}, cwd);
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
case 'mark-completed': {
|
|
475
|
+
if (!currentSpec) {
|
|
476
|
+
tui.log('Error: No spec for this branch. Checkout a spec branch first.');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (currentSpec.status === 'completed') {
|
|
481
|
+
tui.log('Spec is already marked as completed.');
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Warn about uncommitted changes — gives user a chance to cancel and commit first
|
|
486
|
+
const proceedWithComplete = await confirmProceedWithUncommittedChanges(
|
|
487
|
+
tui, cwd, 'You have uncommitted changes that will not be included in the final push. Proceed anyway?'
|
|
488
|
+
);
|
|
489
|
+
if (!proceedWithComplete) break;
|
|
490
|
+
|
|
491
|
+
tui.log(`Marking spec as completed: ${currentSpec.id}`);
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
/** Remote sync: fetch + merge origin/main */
|
|
495
|
+
tui.log('Syncing with origin/main...');
|
|
496
|
+
const syncResult = syncWithOriginMain(cwd);
|
|
497
|
+
|
|
498
|
+
if (!syncResult.success && syncResult.conflicts.length > 0) {
|
|
499
|
+
// Merge conflicts detected — already aborted by syncWithOriginMain
|
|
500
|
+
const conflictDetail = "Conflicting files:\n" + syncResult.conflicts.map(f => " - " + f).join("\n") + "\n\nResolve conflicts manually, push, and retry completion.";
|
|
501
|
+
await tui.showConfirmation(
|
|
502
|
+
'Merge Conflicts Detected',
|
|
503
|
+
`Could not merge origin/main into ${branch}.`,
|
|
504
|
+
conflictDetail
|
|
505
|
+
);
|
|
506
|
+
logTuiError('mark-completed', `Merge conflicts: ${syncResult.conflicts.join(', ')}`, {
|
|
507
|
+
spec: currentSpec.id,
|
|
508
|
+
branch,
|
|
509
|
+
conflicts: syncResult.conflicts,
|
|
510
|
+
}, cwd);
|
|
511
|
+
return;
|
|
512
|
+
} else if (!syncResult.success) {
|
|
513
|
+
await tui.showConfirmation(
|
|
514
|
+
'Completion Aborted',
|
|
515
|
+
'Failed to sync with remote main — completion aborted.',
|
|
516
|
+
'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
} else {
|
|
520
|
+
tui.log('Synced with origin/main successfully.');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Compute relative paths for git staging before the move
|
|
524
|
+
const oldRelPath = relative(cwd, currentSpec.path);
|
|
525
|
+
const newRelPath = relative(cwd, join(cwd, 'specs', currentSpec.filename));
|
|
526
|
+
const destPath = join(cwd, 'specs', currentSpec.filename);
|
|
527
|
+
|
|
528
|
+
if (existsSync(destPath)) {
|
|
529
|
+
tui.log(`Error: Destination already exists: ${destPath}`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Move spec file from specs/roadmap/ to specs/
|
|
534
|
+
renameSync(currentSpec.path, destPath);
|
|
535
|
+
tui.log(`Moved spec to: specs/${currentSpec.filename}`);
|
|
536
|
+
|
|
537
|
+
// Update frontmatter on the new path
|
|
538
|
+
updateSpecStatus(destPath, 'completed');
|
|
539
|
+
|
|
540
|
+
// Reindex roadmap and docs indexes after file move
|
|
541
|
+
await reindexAfterMove(cwd, currentSpec.path, destPath, true);
|
|
542
|
+
|
|
543
|
+
// Stage only the moved spec file (deletion of old + addition of new)
|
|
544
|
+
gitExec(['add', '--', oldRelPath], cwd);
|
|
545
|
+
gitExec(['add', '--', newRelPath], cwd);
|
|
546
|
+
|
|
547
|
+
// Commit
|
|
548
|
+
const commitResult = gitExec(['commit', '-m', `chore: mark spec ${currentSpec.id} as completed`], cwd);
|
|
549
|
+
if (!commitResult.success) {
|
|
550
|
+
tui.log(`Error committing: ${commitResult.stderr}`);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Push with -u to ensure upstream tracking is set
|
|
555
|
+
const pushResult = gitExec(['push', '-u', 'origin', 'HEAD'], cwd);
|
|
556
|
+
if (!pushResult.success) {
|
|
557
|
+
tui.log('Warning: Could not push completion commit.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Refresh spec list to reflect completed status — developer stays on feature branch
|
|
561
|
+
const refreshedSpec = getSpecForBranch(branch, cwd);
|
|
562
|
+
tui.syncBranchContext(branch, refreshedSpec);
|
|
563
|
+
|
|
564
|
+
tui.log(`Spec ${currentSpec.id} completed successfully.`);
|
|
565
|
+
} catch (e) {
|
|
566
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
567
|
+
tui.log(`Error: ${message}`);
|
|
568
|
+
logTuiError('mark-completed', e instanceof Error ? e : message, {
|
|
569
|
+
spec: currentSpec?.id,
|
|
570
|
+
branch,
|
|
571
|
+
}, cwd);
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
case 'switch-spec': {
|
|
577
|
+
const specId = data?.specId as string | undefined;
|
|
578
|
+
if (!specId || specId.startsWith('header-') || specId === 'info') {
|
|
579
|
+
// Header or info item selected, ignore
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
tui.log(`Switching to spec: ${specId}`);
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
/** Guard: check for uncommitted changes before any git operations */
|
|
587
|
+
const proceedWithSwitch = await confirmProceedWithUncommittedChanges(
|
|
588
|
+
tui, cwd, 'You have uncommitted changes that may be lost during branch switch. Proceed anyway?'
|
|
589
|
+
);
|
|
590
|
+
if (!proceedWithSwitch) break;
|
|
591
|
+
|
|
592
|
+
// Find spec file using specs library
|
|
593
|
+
const specFile = findSpecById(specId, cwd);
|
|
594
|
+
|
|
595
|
+
if (!specFile) {
|
|
596
|
+
tui.log(`Error: Spec file not found: ${specId}`);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if spec has a branch assigned
|
|
601
|
+
if (!specFile.branch) {
|
|
602
|
+
tui.log(`Error: Spec "${specId}" has no branch assigned.`);
|
|
603
|
+
tui.log('Use "ah specs create <spec_path>" to assign a branch.');
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Completed specs cannot be switched to
|
|
608
|
+
if (specFile.status === 'completed') {
|
|
609
|
+
tui.log(`Spec "${specFile.id}" is completed and cannot be selected.`);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const specBranch = specFile.branch!;
|
|
614
|
+
|
|
615
|
+
// Validate branch name safety
|
|
616
|
+
validateGitRef(specBranch, 'spec branch');
|
|
617
|
+
|
|
618
|
+
/** Cross-worktree detection: check if spec is active in another worktree */
|
|
619
|
+
const worktreeResult = gitExec(['worktree', 'list', '--porcelain'], cwd);
|
|
620
|
+
if (worktreeResult.success) {
|
|
621
|
+
const worktreeLines = worktreeResult.stdout.split('\n');
|
|
622
|
+
const worktreePaths: string[] = [];
|
|
623
|
+
for (const line of worktreeLines) {
|
|
624
|
+
if (line.startsWith('worktree ')) {
|
|
625
|
+
worktreePaths.push(line.substring('worktree '.length));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const sanitizedBranchKey = sanitizeBranchForDir(specBranch);
|
|
630
|
+
for (const wtPath of worktreePaths) {
|
|
631
|
+
// Skip the current working directory
|
|
632
|
+
if (wtPath === cwd) continue;
|
|
633
|
+
const planningPath = join(wtPath, '.planning', sanitizedBranchKey);
|
|
634
|
+
if (existsSync(planningPath)) {
|
|
635
|
+
await tui.showConfirmation(
|
|
636
|
+
'Spec Active in Another Worktree',
|
|
637
|
+
`Cannot activate spec here.`,
|
|
638
|
+
`Spec '${specFile.id}' is already active in another worktree:\n ${wtPath}\n\nSwitch to that directory to continue work on this spec.`
|
|
639
|
+
);
|
|
640
|
+
process.stderr.write(`Error: Spec '${specFile.id}' is already active in worktree: ${wtPath}\n`);
|
|
641
|
+
logTuiError('switch-spec', `Spec active in another worktree: ${wtPath}`, {
|
|
642
|
+
specId,
|
|
643
|
+
specBranch,
|
|
644
|
+
worktreePath: wtPath,
|
|
645
|
+
}, cwd);
|
|
646
|
+
// Use a flag to break out of the switch case
|
|
647
|
+
tui.log(`Spec is active in worktree: ${wtPath}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Branch creation / checkout with remote sync
|
|
654
|
+
const branchExists = gitExec(['rev-parse', '--verify', specBranch], cwd);
|
|
655
|
+
|
|
656
|
+
if (!branchExists.success) {
|
|
657
|
+
// Branch does NOT exist locally — create from origin/main
|
|
658
|
+
tui.log(`Creating new branch from origin/main: ${specBranch}`);
|
|
659
|
+
|
|
660
|
+
// Fetch first (non-blocking on failure)
|
|
661
|
+
const fetchResult = gitExec(['fetch', 'origin', 'main'], cwd);
|
|
662
|
+
if (!fetchResult.success) {
|
|
663
|
+
tui.log('Warning: Could not fetch from origin. Creating branch from local state.');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Create branch from origin/main (or local main if fetch failed)
|
|
667
|
+
const createBase = fetchResult.success ? 'origin/main' : 'main';
|
|
668
|
+
const createResult = gitExec(['checkout', '-b', specBranch, createBase], cwd);
|
|
669
|
+
if (!createResult.success) {
|
|
670
|
+
tui.log(`Error creating branch: ${createResult.stderr}`);
|
|
671
|
+
logTuiError('checkout-branch', createResult.stderr, {
|
|
672
|
+
specId,
|
|
673
|
+
specBranch,
|
|
674
|
+
branch,
|
|
675
|
+
}, cwd);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
// Branch exists — checkout and merge origin/main
|
|
680
|
+
tui.log(`Checking out existing branch: ${specBranch}`);
|
|
681
|
+
|
|
682
|
+
const checkoutResult = gitExec(['checkout', specBranch], cwd);
|
|
683
|
+
if (!checkoutResult.success) {
|
|
684
|
+
tui.log(`Error checking out branch: ${checkoutResult.stderr}`);
|
|
685
|
+
logTuiError('checkout-branch', checkoutResult.stderr, {
|
|
686
|
+
specId,
|
|
687
|
+
specBranch,
|
|
688
|
+
branch,
|
|
689
|
+
}, cwd);
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Sync with origin/main
|
|
694
|
+
tui.log('Syncing with origin/main...');
|
|
695
|
+
const syncResult = syncWithOriginMain(cwd);
|
|
696
|
+
|
|
697
|
+
if (!syncResult.success && syncResult.conflicts.length > 0) {
|
|
698
|
+
// Merge conflicts detected — already aborted by syncWithOriginMain
|
|
699
|
+
const conflictDetail = "Conflicting files:\n" + syncResult.conflicts.map(f => " - " + f).join("\n") + "\n\nResolve conflicts manually and retry.";
|
|
700
|
+
await tui.showConfirmation(
|
|
701
|
+
'Merge Conflicts Detected',
|
|
702
|
+
`Could not merge origin/main into ${specBranch}.`,
|
|
703
|
+
conflictDetail
|
|
704
|
+
);
|
|
705
|
+
process.stderr.write(`Error: Merge conflicts in ${specBranch}: ${syncResult.conflicts.join(', ')}\n`);
|
|
706
|
+
logTuiError('switch-spec', `Merge conflicts: ${syncResult.conflicts.join(', ')}`, {
|
|
707
|
+
specId,
|
|
708
|
+
specBranch,
|
|
709
|
+
conflicts: syncResult.conflicts,
|
|
710
|
+
}, cwd);
|
|
711
|
+
// Return without completing activation — user must resolve conflicts
|
|
712
|
+
return;
|
|
713
|
+
} else if (!syncResult.success) {
|
|
714
|
+
await tui.showConfirmation(
|
|
715
|
+
'Switch Aborted',
|
|
716
|
+
'Failed to sync with remote main — switch aborted.',
|
|
717
|
+
'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
|
|
718
|
+
);
|
|
719
|
+
return;
|
|
720
|
+
} else {
|
|
721
|
+
tui.log('Synced with origin/main successfully.');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Preserve .planning/ directory creation
|
|
726
|
+
const newPlanningKey = sanitizeBranchForDir(specBranch);
|
|
727
|
+
|
|
728
|
+
if (!planningDirExists(newPlanningKey, cwd)) {
|
|
729
|
+
tui.log(`Creating .planning/${newPlanningKey}/`);
|
|
730
|
+
ensurePlanningDir(newPlanningKey, cwd);
|
|
731
|
+
initializeStatus(newPlanningKey, specFile.path, specBranch, cwd);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Update TUI state
|
|
735
|
+
const newPrompts = loadAllPrompts(newPlanningKey, cwd);
|
|
736
|
+
tui.updateState({
|
|
737
|
+
spec: specFile.id,
|
|
738
|
+
branch: specBranch,
|
|
739
|
+
prompts: newPrompts.map((p) => ({
|
|
740
|
+
number: p.frontmatter.number,
|
|
741
|
+
title: p.frontmatter.title,
|
|
742
|
+
status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
|
|
743
|
+
path: p.path,
|
|
744
|
+
})),
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Sync EventLoop state to prevent stale branch detection
|
|
748
|
+
tui.syncBranchContext(specBranch, specFile);
|
|
749
|
+
|
|
750
|
+
tui.log(`Switched to spec: ${specFile.id} on branch: ${specBranch}`);
|
|
751
|
+
} catch (e) {
|
|
752
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
753
|
+
tui.log(`Error: ${message}`);
|
|
754
|
+
logTuiError('switch-spec', e instanceof Error ? e : message, {
|
|
755
|
+
specId: data?.specId,
|
|
756
|
+
spec: currentSpec?.id,
|
|
757
|
+
branch,
|
|
758
|
+
}, cwd);
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
case 'toggle-loop': {
|
|
764
|
+
const enabled = data?.enabled as boolean;
|
|
765
|
+
// Don't persist enabled state - loop always starts disabled
|
|
766
|
+
tui.log(`Loop: ${enabled ? 'Started' : 'Stopped'}`);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
case 'toggle-parallel': {
|
|
771
|
+
const enabled = data?.enabled as boolean;
|
|
772
|
+
if (planningKey && status) {
|
|
773
|
+
updateStatus({ loop: { ...status.loop, parallel: enabled } }, planningKey, cwd);
|
|
774
|
+
}
|
|
775
|
+
tui.log(`Parallel: ${enabled ? 'Enabled' : 'Disabled'}`);
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
case 'select-prompt': {
|
|
780
|
+
const prompt = data?.prompt as { number: number; title: string } | undefined;
|
|
781
|
+
if (prompt) {
|
|
782
|
+
tui.log(`Selected prompt: ${prompt.number}. ${prompt.title}`);
|
|
783
|
+
// TODO: Could show prompt details or offer to edit
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
case 'refresh': {
|
|
789
|
+
// Reload prompts from filesystem
|
|
790
|
+
if (planningKey && planningDirExists(planningKey, cwd)) {
|
|
791
|
+
const refreshedPrompts = loadAllPrompts(planningKey, cwd);
|
|
792
|
+
tui.updateState({
|
|
793
|
+
prompts: refreshedPrompts.map((p) => ({
|
|
794
|
+
number: p.frontmatter.number,
|
|
795
|
+
title: p.frontmatter.title,
|
|
796
|
+
status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
|
|
797
|
+
path: p.path,
|
|
798
|
+
})),
|
|
799
|
+
});
|
|
800
|
+
tui.log(`Refreshed: ${refreshedPrompts.length} prompts`);
|
|
801
|
+
} else {
|
|
802
|
+
tui.log('No planning directory for this branch');
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
case 'new-initiative': {
|
|
808
|
+
const specType = data?.specType as string | undefined;
|
|
809
|
+
if (!specType) {
|
|
810
|
+
tui.log('Error: No spec type selected.');
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
tui.log(`New Initiative: ${specType}`);
|
|
815
|
+
|
|
816
|
+
// Spawn ideation agent with unified scoping flow and domain-specific config
|
|
817
|
+
try {
|
|
818
|
+
const context = buildTemplateContext(
|
|
819
|
+
planningKey || 'default',
|
|
820
|
+
status?.name,
|
|
821
|
+
undefined,
|
|
822
|
+
undefined,
|
|
823
|
+
cwd
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
// Override WORKFLOW_DOMAIN_PATH based on the selected spec type
|
|
827
|
+
context.WORKFLOW_DOMAIN_PATH = join(cwd, '.allhands', 'workflows', `${specType}.md`);
|
|
828
|
+
|
|
829
|
+
// Detect active spec for revision mode
|
|
830
|
+
const activeSpec = getSpecForBranch(branch, cwd);
|
|
831
|
+
if (activeSpec && activeSpec.status !== 'completed') {
|
|
832
|
+
const specAbsPath = activeSpec.path.startsWith('/') ? activeSpec.path : join(cwd, activeSpec.path);
|
|
833
|
+
context.SPEC_PATH = specAbsPath;
|
|
834
|
+
context.SPEC_NAME = activeSpec.id;
|
|
835
|
+
tui.log(`Active spec detected: ${activeSpec.id} — ideation will enter revision mode`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// All spec types route to the unified scoping flow
|
|
839
|
+
const flowOverride = join(getFlowsDirectory(), UNIFIED_SCOPING_FLOW);
|
|
840
|
+
|
|
841
|
+
const result = spawnAgentFromProfile(
|
|
842
|
+
{
|
|
843
|
+
agentName: 'ideation',
|
|
844
|
+
context,
|
|
845
|
+
focusWindow: true,
|
|
846
|
+
flowOverride,
|
|
847
|
+
},
|
|
848
|
+
branch,
|
|
849
|
+
cwd
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
tui.log(`Spawned ideation in ${result.sessionName}:${result.windowName}`);
|
|
853
|
+
updateRunningAgents(tui, branch);
|
|
854
|
+
} catch (e) {
|
|
855
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
856
|
+
tui.log(`Error spawning ideation: ${message}`);
|
|
857
|
+
logTuiError('new-initiative', e instanceof Error ? e : message, {
|
|
858
|
+
specType,
|
|
859
|
+
branch,
|
|
860
|
+
}, cwd);
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
default:
|
|
866
|
+
tui.log(`Action: ${action}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function updateRunningAgents(tui: TUI, branch: string): void {
|
|
871
|
+
const agents = getRunningAgents(branch);
|
|
872
|
+
const activeAgents: AgentInfo[] = agents.map((a) => ({
|
|
873
|
+
name: a.windowName,
|
|
874
|
+
agentType: a.agentType || 'unknown',
|
|
875
|
+
isRunning: true,
|
|
876
|
+
}));
|
|
877
|
+
tui.updateState({ activeAgents });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function spawnExecutorForPrompt(tui: TUI, prompt: PromptFile, branch: string, specId: string): void {
|
|
881
|
+
const promptNumber = prompt.frontmatter.number;
|
|
882
|
+
const cwd = process.cwd();
|
|
883
|
+
const planningKey = sanitizeBranchForDir(branch);
|
|
884
|
+
|
|
885
|
+
tui.log(`Spawning executor for: ${prompt.frontmatter.title}`);
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
// Build context with prompt-specific info (use sanitized planning key for paths)
|
|
889
|
+
const context = buildTemplateContext(
|
|
890
|
+
planningKey,
|
|
891
|
+
specId, // Use spec file name, not prompt title
|
|
892
|
+
promptNumber,
|
|
893
|
+
prompt.path,
|
|
894
|
+
cwd
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
const result = spawnAgentFromProfile(
|
|
898
|
+
{
|
|
899
|
+
agentName: 'executor',
|
|
900
|
+
context,
|
|
901
|
+
promptNumber,
|
|
902
|
+
focusWindow: false, // Don't steal focus from TUI
|
|
903
|
+
},
|
|
904
|
+
branch,
|
|
905
|
+
cwd
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
tui.log(`Spawned executor in ${result.sessionName}:${result.windowName}`);
|
|
909
|
+
updateRunningAgents(tui, branch);
|
|
910
|
+
} catch (e) {
|
|
911
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
912
|
+
tui.log(`Error spawning executor: ${message}`);
|
|
913
|
+
logTuiError('spawn-executor', e instanceof Error ? e : message, {
|
|
914
|
+
promptNumber,
|
|
915
|
+
promptTitle: prompt.frontmatter.title,
|
|
916
|
+
branch,
|
|
917
|
+
}, cwd);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function spawnEmergentPlanningAgent(tui: TUI, branch: string, specId: string): void {
|
|
922
|
+
const cwd = process.cwd();
|
|
923
|
+
const planningKey = sanitizeBranchForDir(branch);
|
|
924
|
+
|
|
925
|
+
// Get next available prompt number for emergent planner window name
|
|
926
|
+
const nextPromptNumber = getNextPromptNumber(planningKey, cwd);
|
|
927
|
+
|
|
928
|
+
tui.log(`Spawning emergent planner (will create prompts from ${nextPromptNumber})`);
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
const context = buildTemplateContext(
|
|
932
|
+
planningKey,
|
|
933
|
+
specId,
|
|
934
|
+
nextPromptNumber,
|
|
935
|
+
undefined,
|
|
936
|
+
cwd
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
const result = spawnAgentFromProfile(
|
|
940
|
+
{
|
|
941
|
+
agentName: 'emergent',
|
|
942
|
+
context,
|
|
943
|
+
promptNumber: nextPromptNumber,
|
|
944
|
+
focusWindow: false, // Don't steal focus from TUI
|
|
945
|
+
},
|
|
946
|
+
branch,
|
|
947
|
+
cwd
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
tui.log(`Spawned emergent planner in ${result.sessionName}:${result.windowName}`);
|
|
951
|
+
updateRunningAgents(tui, branch);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
954
|
+
tui.log(`Error spawning emergent planner: ${message}`);
|
|
955
|
+
logTuiError('spawn-emergent', e instanceof Error ? e : message, {
|
|
956
|
+
promptNumber: nextPromptNumber,
|
|
957
|
+
branch,
|
|
958
|
+
}, cwd);
|
|
959
|
+
}
|
|
960
|
+
}
|