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,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs Commands (Agent-Facing)
|
|
3
|
+
*
|
|
4
|
+
* High-level spec management operations.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* - ah specs list - List all specs grouped by domain_name
|
|
8
|
+
* - ah specs complete <name> - Mark spec completed, move spec out of roadmap
|
|
9
|
+
* - ah specs create <path> - Create spec: validate, assign branch, commit and push
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join, dirname, relative, basename } from 'path';
|
|
15
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
16
|
+
import { getGitRoot, getCurrentBranch } from '../lib/planning.js';
|
|
17
|
+
import { getBaseBranch, gitExec } from '../lib/git.js';
|
|
18
|
+
import { KnowledgeService } from '../lib/knowledge.js';
|
|
19
|
+
import { findSpecByBranch, getSpecForBranch, loadAllSpecs as loadAllSpecGroups, type SpecFile, type SpecFrontmatter } from '../lib/specs.js';
|
|
20
|
+
import { logCommandStart, logCommandSuccess, logCommandError } from '../lib/trace-store.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse spec file frontmatter
|
|
24
|
+
*/
|
|
25
|
+
function parseSpecFrontmatter(filePath: string): SpecFrontmatter | null {
|
|
26
|
+
if (!existsSync(filePath)) return null;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
30
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
31
|
+
if (!frontmatterMatch) return null;
|
|
32
|
+
|
|
33
|
+
return parseYaml(frontmatterMatch[1]) as SpecFrontmatter;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update spec file frontmatter status
|
|
41
|
+
*/
|
|
42
|
+
export function updateSpecStatus(filePath: string, newStatus: 'roadmap' | 'in_progress' | 'completed'): boolean {
|
|
43
|
+
if (!existsSync(filePath)) return false;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
47
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
48
|
+
if (!frontmatterMatch) return false;
|
|
49
|
+
|
|
50
|
+
const frontmatter = parseYaml(frontmatterMatch[1]) as SpecFrontmatter;
|
|
51
|
+
frontmatter.status = newStatus;
|
|
52
|
+
|
|
53
|
+
const newContent = `---\n${stringifyYaml(frontmatter).trim()}\n---\n${frontmatterMatch[2]}`;
|
|
54
|
+
writeFileSync(filePath, newContent);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find a spec by ID (filename without extension)
|
|
63
|
+
*/
|
|
64
|
+
function findSpecByName(name: string): SpecFile | null {
|
|
65
|
+
const groups = loadAllSpecGroups();
|
|
66
|
+
for (const group of groups) {
|
|
67
|
+
const spec = group.specs.find((s) => s.id === name);
|
|
68
|
+
if (spec) return spec;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reindex knowledge bases after spec file moves.
|
|
75
|
+
* Updates both 'docs' and 'roadmap' indexes to reflect the file move.
|
|
76
|
+
*/
|
|
77
|
+
export async function reindexAfterMove(
|
|
78
|
+
gitRoot: string,
|
|
79
|
+
oldPath: string,
|
|
80
|
+
newPath: string,
|
|
81
|
+
quiet: boolean = false
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const service = new KnowledgeService(gitRoot, { quiet });
|
|
84
|
+
const oldRelPath = relative(gitRoot, oldPath);
|
|
85
|
+
const newRelPath = relative(gitRoot, newPath);
|
|
86
|
+
|
|
87
|
+
// Update roadmap index: remove old location if it was in roadmap
|
|
88
|
+
if (oldRelPath.startsWith('specs/roadmap/')) {
|
|
89
|
+
await service.reindexFromChanges('roadmap', [
|
|
90
|
+
{ path: oldRelPath, deleted: true },
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
// Add new location if it's now in roadmap
|
|
94
|
+
if (newRelPath.startsWith('specs/roadmap/')) {
|
|
95
|
+
await service.reindexFromChanges('roadmap', [
|
|
96
|
+
{ path: newRelPath, added: true },
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Update docs index: docs includes all of specs/
|
|
101
|
+
// Remove from old location, add to new location
|
|
102
|
+
await service.reindexFromChanges('docs', [
|
|
103
|
+
{ path: oldRelPath, deleted: true },
|
|
104
|
+
{ path: newRelPath, added: true },
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function register(program: Command): void {
|
|
109
|
+
const specs = program
|
|
110
|
+
.command('specs')
|
|
111
|
+
.description('Spec management');
|
|
112
|
+
|
|
113
|
+
// ah specs list
|
|
114
|
+
specs
|
|
115
|
+
.command('list')
|
|
116
|
+
.description('List all specs grouped by domain')
|
|
117
|
+
.option('--json', 'Output as JSON')
|
|
118
|
+
.option('--domains-only', 'Only list domain names')
|
|
119
|
+
.option('--domain <name>', 'Filter to a specific domain')
|
|
120
|
+
.option('--roadmap', 'Only show specs in the roadmap (not completed)')
|
|
121
|
+
.option('--completed', 'Only show completed specs')
|
|
122
|
+
.option('--in-progress', 'Only show in-progress specs')
|
|
123
|
+
.action(async (options: { json?: boolean; domainsOnly?: boolean; domain?: string; roadmap?: boolean; completed?: boolean; inProgress?: boolean }) => {
|
|
124
|
+
const groups = loadAllSpecGroups();
|
|
125
|
+
let allSpecs = groups.flatMap((g) => g.specs);
|
|
126
|
+
|
|
127
|
+
// Apply status filters
|
|
128
|
+
if (options.roadmap) {
|
|
129
|
+
allSpecs = allSpecs.filter((s) => s.status === 'roadmap');
|
|
130
|
+
} else if (options.completed) {
|
|
131
|
+
allSpecs = allSpecs.filter((s) => s.status === 'completed');
|
|
132
|
+
} else if (options.inProgress) {
|
|
133
|
+
allSpecs = allSpecs.filter((s) => s.status === 'in_progress');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Group by domain_name
|
|
137
|
+
const byDomain: Record<string, SpecFile[]> = {};
|
|
138
|
+
for (const spec of allSpecs) {
|
|
139
|
+
const domain = spec.domain_name || 'uncategorized';
|
|
140
|
+
if (!byDomain[domain]) {
|
|
141
|
+
byDomain[domain] = [];
|
|
142
|
+
}
|
|
143
|
+
byDomain[domain].push(spec);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const domains = Object.keys(byDomain).sort();
|
|
147
|
+
|
|
148
|
+
// Handle --domains-only
|
|
149
|
+
if (options.domainsOnly) {
|
|
150
|
+
if (options.json) {
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
success: true,
|
|
153
|
+
count: domains.length,
|
|
154
|
+
domains,
|
|
155
|
+
}, null, 2));
|
|
156
|
+
} else {
|
|
157
|
+
for (const domain of domains) {
|
|
158
|
+
console.log(domain);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle --domain <name>
|
|
165
|
+
if (options.domain) {
|
|
166
|
+
const domainSpecs = byDomain[options.domain];
|
|
167
|
+
if (!domainSpecs) {
|
|
168
|
+
if (options.json) {
|
|
169
|
+
console.log(JSON.stringify({ success: false, error: `Domain not found: ${options.domain}` }));
|
|
170
|
+
} else {
|
|
171
|
+
console.error(`Domain not found: ${options.domain}`);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sortedSpecs = domainSpecs.sort((a, b) => a.id.localeCompare(b.id));
|
|
177
|
+
|
|
178
|
+
if (options.json) {
|
|
179
|
+
console.log(JSON.stringify({
|
|
180
|
+
success: true,
|
|
181
|
+
domain: options.domain,
|
|
182
|
+
count: sortedSpecs.length,
|
|
183
|
+
specs: sortedSpecs,
|
|
184
|
+
}, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`## ${options.domain}\n`);
|
|
187
|
+
for (const spec of sortedSpecs) {
|
|
188
|
+
const statusIcon = spec.status === 'completed' ? '[x]' : spec.status === 'in_progress' ? '[>]' : '[ ]';
|
|
189
|
+
const deps = spec.dependencies.length > 0 ? ` (deps: ${spec.dependencies.join(', ')})` : '';
|
|
190
|
+
console.log(` ${statusIcon} ${spec.id}${deps}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default: list all specs grouped by domain
|
|
197
|
+
if (options.json) {
|
|
198
|
+
console.log(JSON.stringify({
|
|
199
|
+
success: true,
|
|
200
|
+
count: allSpecs.length,
|
|
201
|
+
domains: byDomain,
|
|
202
|
+
}, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`Found ${allSpecs.length} spec(s):\n`);
|
|
207
|
+
|
|
208
|
+
for (const domain of domains) {
|
|
209
|
+
console.log(`## ${domain}`);
|
|
210
|
+
const domainSpecs = byDomain[domain].sort((a, b) => a.id.localeCompare(b.id));
|
|
211
|
+
for (const spec of domainSpecs) {
|
|
212
|
+
const statusIcon = spec.status === 'completed' ? '[x]' : spec.status === 'in_progress' ? '[>]' : '[ ]';
|
|
213
|
+
const deps = spec.dependencies.length > 0 ? ` (deps: ${spec.dependencies.join(', ')})` : '';
|
|
214
|
+
console.log(` ${statusIcon} ${spec.id}${deps}`);
|
|
215
|
+
}
|
|
216
|
+
console.log();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ah specs current
|
|
221
|
+
specs
|
|
222
|
+
.command('current')
|
|
223
|
+
.description('Show spec for current git branch')
|
|
224
|
+
.option('--json', 'Output as JSON')
|
|
225
|
+
.action((options: { json?: boolean }) => {
|
|
226
|
+
const branch = getCurrentBranch();
|
|
227
|
+
const spec = getSpecForBranch(branch);
|
|
228
|
+
|
|
229
|
+
if (!spec) {
|
|
230
|
+
if (options.json) {
|
|
231
|
+
console.log(JSON.stringify({
|
|
232
|
+
success: true,
|
|
233
|
+
hasSpec: false,
|
|
234
|
+
branch,
|
|
235
|
+
message: 'No spec for this branch',
|
|
236
|
+
}, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(`No spec for branch: ${branch}`);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(JSON.stringify({
|
|
245
|
+
success: true,
|
|
246
|
+
hasSpec: true,
|
|
247
|
+
branch,
|
|
248
|
+
spec: {
|
|
249
|
+
id: spec.id,
|
|
250
|
+
name: spec.title,
|
|
251
|
+
path: spec.path,
|
|
252
|
+
domain: spec.domain_name,
|
|
253
|
+
status: spec.status,
|
|
254
|
+
dependencies: spec.dependencies,
|
|
255
|
+
},
|
|
256
|
+
}, null, 2));
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`Branch: ${branch}`);
|
|
259
|
+
console.log(`Spec: ${spec.id}`);
|
|
260
|
+
console.log(` Title: ${spec.title}`);
|
|
261
|
+
console.log(` Path: ${spec.path}`);
|
|
262
|
+
console.log(` Domain: ${spec.domain_name}`);
|
|
263
|
+
console.log(` Status: ${spec.status}`);
|
|
264
|
+
if (spec.dependencies.length > 0) {
|
|
265
|
+
console.log(` Dependencies: ${spec.dependencies.join(', ')}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ah specs complete <name>
|
|
271
|
+
specs
|
|
272
|
+
.command('complete <name>')
|
|
273
|
+
.description('Mark spec completed and move to specs/')
|
|
274
|
+
.option('--json', 'Output as JSON')
|
|
275
|
+
.action(async (name: string, options: { json?: boolean }) => {
|
|
276
|
+
const commandName = 'specs complete';
|
|
277
|
+
const commandArgs = { name, options };
|
|
278
|
+
logCommandStart(commandName, commandArgs);
|
|
279
|
+
|
|
280
|
+
const spec = findSpecByName(name);
|
|
281
|
+
|
|
282
|
+
if (!spec) {
|
|
283
|
+
const error = `Spec not found: ${name}`;
|
|
284
|
+
logCommandError(commandName, error, commandArgs);
|
|
285
|
+
if (options.json) {
|
|
286
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
287
|
+
} else {
|
|
288
|
+
console.error(error);
|
|
289
|
+
}
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (spec.status === 'completed') {
|
|
294
|
+
const error = `Spec already completed: ${name}`;
|
|
295
|
+
logCommandError(commandName, error, commandArgs);
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
298
|
+
} else {
|
|
299
|
+
console.error(error);
|
|
300
|
+
}
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const gitRoot = getGitRoot();
|
|
305
|
+
const specsDir = join(gitRoot, 'specs');
|
|
306
|
+
const targetPath = join(specsDir, `${name}.spec.md`);
|
|
307
|
+
|
|
308
|
+
// Update status in frontmatter
|
|
309
|
+
if (!updateSpecStatus(spec.path, 'completed')) {
|
|
310
|
+
const error = 'Failed to update spec status';
|
|
311
|
+
logCommandError(commandName, error, commandArgs);
|
|
312
|
+
if (options.json) {
|
|
313
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
314
|
+
} else {
|
|
315
|
+
console.error(error);
|
|
316
|
+
}
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Move file if it's in roadmap
|
|
321
|
+
const wasInRoadmap = spec.path.includes('/roadmap/');
|
|
322
|
+
if (wasInRoadmap) {
|
|
323
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
324
|
+
renameSync(spec.path, targetPath);
|
|
325
|
+
|
|
326
|
+
// Reindex knowledge bases to reflect the move
|
|
327
|
+
if (!options.json) {
|
|
328
|
+
console.log(' Reindexing knowledge bases...');
|
|
329
|
+
}
|
|
330
|
+
await reindexAfterMove(gitRoot, spec.path, targetPath, options.json);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Log success
|
|
334
|
+
logCommandSuccess(commandName, {
|
|
335
|
+
name,
|
|
336
|
+
status: 'completed',
|
|
337
|
+
path: wasInRoadmap ? targetPath : spec.path,
|
|
338
|
+
reindexed: wasInRoadmap,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (options.json) {
|
|
342
|
+
console.log(JSON.stringify({
|
|
343
|
+
success: true,
|
|
344
|
+
name,
|
|
345
|
+
status: 'completed',
|
|
346
|
+
path: wasInRoadmap ? targetPath : spec.path,
|
|
347
|
+
reindexed: wasInRoadmap,
|
|
348
|
+
}, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`Marked spec completed: ${name}`);
|
|
353
|
+
if (wasInRoadmap) {
|
|
354
|
+
console.log(` Moved to: ${targetPath}`);
|
|
355
|
+
console.log(' Knowledge indexes updated ✓');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ah specs create <path>
|
|
360
|
+
specs
|
|
361
|
+
.command('create <path>')
|
|
362
|
+
.description('Create spec: validate, assign branch, commit and push to base branch')
|
|
363
|
+
.option('--json', 'Output as JSON')
|
|
364
|
+
.action((specPath: string, options: { json?: boolean }) => {
|
|
365
|
+
const commandName = 'specs create';
|
|
366
|
+
const commandArgs = { specPath, options };
|
|
367
|
+
logCommandStart(commandName, commandArgs);
|
|
368
|
+
|
|
369
|
+
const gitRoot = getGitRoot();
|
|
370
|
+
const baseBranch = getBaseBranch();
|
|
371
|
+
const currentBranch = getCurrentBranch();
|
|
372
|
+
|
|
373
|
+
// 1. Enforce being on base branch
|
|
374
|
+
if (currentBranch !== baseBranch) {
|
|
375
|
+
const error = `Spec creation requires being on the base branch (${baseBranch}). Currently on: ${currentBranch}`;
|
|
376
|
+
logCommandError(commandName, error, commandArgs);
|
|
377
|
+
if (options.json) {
|
|
378
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
379
|
+
} else {
|
|
380
|
+
console.error(error);
|
|
381
|
+
}
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 2. Validate spec file
|
|
386
|
+
const absolutePath = specPath.startsWith('/') ? specPath : join(process.cwd(), specPath);
|
|
387
|
+
const specRelativePath = relative(gitRoot, absolutePath);
|
|
388
|
+
|
|
389
|
+
if (!existsSync(absolutePath)) {
|
|
390
|
+
const error = `Spec file not found: ${specPath}`;
|
|
391
|
+
logCommandError(commandName, error, commandArgs);
|
|
392
|
+
if (options.json) {
|
|
393
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
394
|
+
} else {
|
|
395
|
+
console.error(error);
|
|
396
|
+
}
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!absolutePath.endsWith('.spec.md')) {
|
|
401
|
+
const error = 'File must be a .spec.md file';
|
|
402
|
+
logCommandError(commandName, error, commandArgs);
|
|
403
|
+
if (options.json) {
|
|
404
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
405
|
+
} else {
|
|
406
|
+
console.error(error);
|
|
407
|
+
}
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!specRelativePath.startsWith('specs/roadmap/')) {
|
|
412
|
+
const error = `Spec file must be in specs/roadmap/. Got: ${specRelativePath}`;
|
|
413
|
+
logCommandError(commandName, error, commandArgs);
|
|
414
|
+
if (options.json) {
|
|
415
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
416
|
+
} else {
|
|
417
|
+
console.error(error);
|
|
418
|
+
}
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 3. Derive branch name from spec type
|
|
423
|
+
const specName = basename(absolutePath, '.spec.md');
|
|
424
|
+
const frontmatter = parseSpecFrontmatter(absolutePath);
|
|
425
|
+
const specType = frontmatter?.type || 'milestone';
|
|
426
|
+
|
|
427
|
+
const SPEC_TYPE_BRANCH_PREFIX: Record<string, string> = {
|
|
428
|
+
milestone: 'feature/',
|
|
429
|
+
investigation: 'fix/',
|
|
430
|
+
optimization: 'optimize/',
|
|
431
|
+
refactor: 'refactor/',
|
|
432
|
+
documentation: 'docs/',
|
|
433
|
+
triage: 'triage/',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const prefix = SPEC_TYPE_BRANCH_PREFIX[specType] || 'feature/';
|
|
437
|
+
let specBranch = frontmatter?.branch || `${prefix}${specName}`;
|
|
438
|
+
|
|
439
|
+
// Handle branch name collisions
|
|
440
|
+
const baseBranchName = specBranch;
|
|
441
|
+
let suffix = 1;
|
|
442
|
+
let existingSpec = findSpecByBranch(specBranch, gitRoot);
|
|
443
|
+
while (existingSpec && existingSpec.id !== specName) {
|
|
444
|
+
suffix++;
|
|
445
|
+
specBranch = `${baseBranchName}-${suffix}`;
|
|
446
|
+
existingSpec = findSpecByBranch(specBranch, gitRoot);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 4. Write branch to spec frontmatter if not present or changed
|
|
450
|
+
if (!frontmatter?.branch || frontmatter.branch !== specBranch) {
|
|
451
|
+
try {
|
|
452
|
+
const content = readFileSync(absolutePath, 'utf-8');
|
|
453
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
454
|
+
|
|
455
|
+
if (frontmatterMatch) {
|
|
456
|
+
const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
|
|
457
|
+
fm.branch = specBranch;
|
|
458
|
+
const newContent = `---\n${stringifyYaml(fm).trim()}\n---\n${frontmatterMatch[2]}`;
|
|
459
|
+
writeFileSync(absolutePath, newContent);
|
|
460
|
+
}
|
|
461
|
+
} catch (e) {
|
|
462
|
+
const error = `Failed to update frontmatter: ${e instanceof Error ? e.message : String(e)}`;
|
|
463
|
+
logCommandError(commandName, error, commandArgs);
|
|
464
|
+
if (options.json) {
|
|
465
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
466
|
+
} else {
|
|
467
|
+
console.error(error);
|
|
468
|
+
}
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 5. Stage only the spec file
|
|
474
|
+
const addResult = gitExec(['add', '--', specRelativePath], gitRoot);
|
|
475
|
+
if (!addResult.success) {
|
|
476
|
+
const error = `Failed to stage spec file: ${addResult.stderr}`;
|
|
477
|
+
logCommandError(commandName, error, commandArgs);
|
|
478
|
+
if (options.json) {
|
|
479
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
480
|
+
} else {
|
|
481
|
+
console.error(error);
|
|
482
|
+
}
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 6. Commit
|
|
487
|
+
const commitResult = gitExec(['commit', '-m', `spec: ${specName}`], gitRoot);
|
|
488
|
+
if (!commitResult.success) {
|
|
489
|
+
const error = `Failed to commit: ${commitResult.stderr}`;
|
|
490
|
+
logCommandError(commandName, error, commandArgs);
|
|
491
|
+
if (options.json) {
|
|
492
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
493
|
+
} else {
|
|
494
|
+
console.error(error);
|
|
495
|
+
}
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 7. Push to origin
|
|
500
|
+
const pushResult = gitExec(['push', 'origin', baseBranch], gitRoot);
|
|
501
|
+
if (!pushResult.success) {
|
|
502
|
+
const error = `Failed to push to origin ${baseBranch}: ${pushResult.stderr}`;
|
|
503
|
+
logCommandError(commandName, error, commandArgs);
|
|
504
|
+
if (options.json) {
|
|
505
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
506
|
+
} else {
|
|
507
|
+
console.error(error);
|
|
508
|
+
}
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 8. Report results
|
|
513
|
+
logCommandSuccess(commandName, {
|
|
514
|
+
name: specName,
|
|
515
|
+
branch: specBranch,
|
|
516
|
+
path: specRelativePath,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (options.json) {
|
|
520
|
+
console.log(JSON.stringify({
|
|
521
|
+
success: true,
|
|
522
|
+
name: specName,
|
|
523
|
+
branch: specBranch,
|
|
524
|
+
path: specRelativePath,
|
|
525
|
+
}, null, 2));
|
|
526
|
+
} else {
|
|
527
|
+
console.log(`Created spec: ${specName}`);
|
|
528
|
+
console.log(` Branch: ${specBranch}`);
|
|
529
|
+
console.log(` Path: ${specRelativePath}`);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|