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,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validation Library
|
|
3
|
+
*
|
|
4
|
+
* Loads YAML schema definitions and validates frontmatter/content against them.
|
|
5
|
+
* Schemas are the single source of truth for file structure requirements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { parse as parseYaml } from 'yaml';
|
|
12
|
+
import { minimatch } from 'minimatch';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
export interface SchemaField {
|
|
18
|
+
type: 'string' | 'integer' | 'boolean' | 'date' | 'enum' | 'array' | 'object';
|
|
19
|
+
required?: boolean;
|
|
20
|
+
default?: unknown;
|
|
21
|
+
description?: string;
|
|
22
|
+
values?: string[]; // for enum type
|
|
23
|
+
items?: string; // for array type (item type)
|
|
24
|
+
properties?: Record<string, SchemaField>; // for object type
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BodySection {
|
|
28
|
+
name: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Schema {
|
|
34
|
+
frontmatter?: Record<string, SchemaField>;
|
|
35
|
+
fields?: Record<string, SchemaField>; // alternative to frontmatter for status.yaml
|
|
36
|
+
body?: {
|
|
37
|
+
description?: string;
|
|
38
|
+
sections?: BodySection[];
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ValidationError {
|
|
43
|
+
field: string;
|
|
44
|
+
message: string;
|
|
45
|
+
expected?: string;
|
|
46
|
+
received?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ValidationResult {
|
|
50
|
+
valid: boolean;
|
|
51
|
+
errors: ValidationError[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const schemaCache = new Map<string, Schema>();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the schema directory path
|
|
58
|
+
* Path: harness/src/lib/ -> harness/src/ -> harness/ -> .allhands/ -> schemas/
|
|
59
|
+
*/
|
|
60
|
+
function getSchemaDir(): string {
|
|
61
|
+
return join(__dirname, '..', '..', '..', 'schemas');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load a schema by type name
|
|
66
|
+
*/
|
|
67
|
+
export function loadSchema(type: string): Schema | null {
|
|
68
|
+
if (schemaCache.has(type)) {
|
|
69
|
+
return schemaCache.get(type)!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const schemaPath = join(getSchemaDir(), `${type}.yaml`);
|
|
73
|
+
if (!existsSync(schemaPath)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
79
|
+
const schema = parseYaml(content) as Schema;
|
|
80
|
+
schemaCache.set(type, schema);
|
|
81
|
+
return schema;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* List available schema types
|
|
89
|
+
*/
|
|
90
|
+
export function listSchemas(): string[] {
|
|
91
|
+
const schemaDir = getSchemaDir();
|
|
92
|
+
if (!existsSync(schemaDir)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return readdirSync(schemaDir)
|
|
97
|
+
.filter((f) => f.endsWith('.yaml'))
|
|
98
|
+
.map((f) => f.replace('.yaml', ''));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate a value against a field schema
|
|
103
|
+
*/
|
|
104
|
+
function validateField(
|
|
105
|
+
value: unknown,
|
|
106
|
+
field: SchemaField,
|
|
107
|
+
fieldName: string
|
|
108
|
+
): ValidationError | null {
|
|
109
|
+
// Check required
|
|
110
|
+
if (field.required && (value === undefined || value === null)) {
|
|
111
|
+
return {
|
|
112
|
+
field: fieldName,
|
|
113
|
+
message: `Required field is missing`,
|
|
114
|
+
expected: field.type,
|
|
115
|
+
received: 'undefined',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If not required and not present, use default or skip
|
|
120
|
+
if (value === undefined || value === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Type validation
|
|
125
|
+
switch (field.type) {
|
|
126
|
+
case 'string':
|
|
127
|
+
if (typeof value !== 'string') {
|
|
128
|
+
return {
|
|
129
|
+
field: fieldName,
|
|
130
|
+
message: `Expected string`,
|
|
131
|
+
expected: 'string',
|
|
132
|
+
received: typeof value,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'integer':
|
|
138
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
139
|
+
return {
|
|
140
|
+
field: fieldName,
|
|
141
|
+
message: `Expected integer`,
|
|
142
|
+
expected: 'integer',
|
|
143
|
+
received: typeof value,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'boolean':
|
|
149
|
+
if (typeof value !== 'boolean') {
|
|
150
|
+
return {
|
|
151
|
+
field: fieldName,
|
|
152
|
+
message: `Expected boolean`,
|
|
153
|
+
expected: 'boolean',
|
|
154
|
+
received: typeof value,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'date':
|
|
160
|
+
if (typeof value !== 'string' || isNaN(Date.parse(value))) {
|
|
161
|
+
return {
|
|
162
|
+
field: fieldName,
|
|
163
|
+
message: `Expected ISO 8601 date string`,
|
|
164
|
+
expected: 'date (ISO 8601)',
|
|
165
|
+
received: String(value),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'enum':
|
|
171
|
+
if (!field.values?.includes(String(value))) {
|
|
172
|
+
return {
|
|
173
|
+
field: fieldName,
|
|
174
|
+
message: `Value must be one of: ${field.values?.join(', ')}`,
|
|
175
|
+
expected: field.values?.join(' | '),
|
|
176
|
+
received: String(value),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'array':
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
return {
|
|
184
|
+
field: fieldName,
|
|
185
|
+
message: `Expected array`,
|
|
186
|
+
expected: 'array',
|
|
187
|
+
received: typeof value,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (field.items) {
|
|
191
|
+
const itemType = field.items;
|
|
192
|
+
const invalidItems = itemType === 'integer'
|
|
193
|
+
? value.some(item => typeof item !== 'number' || !Number.isInteger(item))
|
|
194
|
+
: value.some(item => typeof item !== itemType);
|
|
195
|
+
if (invalidItems) {
|
|
196
|
+
return {
|
|
197
|
+
field: fieldName,
|
|
198
|
+
message: `Array contains non-${itemType} items`,
|
|
199
|
+
expected: `${itemType}[]`,
|
|
200
|
+
received: `mixed array`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'object':
|
|
207
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
208
|
+
return {
|
|
209
|
+
field: fieldName,
|
|
210
|
+
message: `Expected object`,
|
|
211
|
+
expected: 'object',
|
|
212
|
+
received: typeof value,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Validate nested properties if defined
|
|
216
|
+
if (field.properties) {
|
|
217
|
+
for (const [propName, propSchema] of Object.entries(field.properties)) {
|
|
218
|
+
const propValue = (value as Record<string, unknown>)[propName];
|
|
219
|
+
const error = validateField(propValue, propSchema, `${fieldName}.${propName}`);
|
|
220
|
+
if (error) {
|
|
221
|
+
return error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate frontmatter against a schema
|
|
233
|
+
*/
|
|
234
|
+
export function validateFrontmatter(
|
|
235
|
+
frontmatter: Record<string, unknown>,
|
|
236
|
+
schema: Schema
|
|
237
|
+
): ValidationResult {
|
|
238
|
+
const errors: ValidationError[] = [];
|
|
239
|
+
const fields = schema.frontmatter || schema.fields || {};
|
|
240
|
+
|
|
241
|
+
for (const [fieldName, fieldSchema] of Object.entries(fields)) {
|
|
242
|
+
const error = validateField(frontmatter[fieldName], fieldSchema, fieldName);
|
|
243
|
+
if (error) {
|
|
244
|
+
errors.push(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
valid: errors.length === 0,
|
|
250
|
+
errors,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extract frontmatter from markdown content
|
|
256
|
+
*/
|
|
257
|
+
export function extractFrontmatter(content: string): {
|
|
258
|
+
frontmatter: Record<string, unknown> | null;
|
|
259
|
+
body: string;
|
|
260
|
+
} {
|
|
261
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
262
|
+
const match = content.match(frontmatterRegex);
|
|
263
|
+
|
|
264
|
+
if (!match) {
|
|
265
|
+
return { frontmatter: null, body: content };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const frontmatter = parseYaml(match[1]) as Record<string, unknown>;
|
|
270
|
+
return { frontmatter, body: match[2] };
|
|
271
|
+
} catch {
|
|
272
|
+
return { frontmatter: null, body: content };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Validate a file's content against its schema type
|
|
278
|
+
*/
|
|
279
|
+
export function validateFile(
|
|
280
|
+
content: string,
|
|
281
|
+
schemaType: string
|
|
282
|
+
): ValidationResult {
|
|
283
|
+
const schema = loadSchema(schemaType);
|
|
284
|
+
if (!schema) {
|
|
285
|
+
return {
|
|
286
|
+
valid: false,
|
|
287
|
+
errors: [{ field: '_schema', message: `Unknown schema type: ${schemaType}` }],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { frontmatter } = extractFrontmatter(content);
|
|
292
|
+
if (!frontmatter) {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
errors: [{ field: '_frontmatter', message: 'Missing or invalid frontmatter' }],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return validateFrontmatter(frontmatter, schema);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Apply defaults from schema to frontmatter
|
|
304
|
+
*/
|
|
305
|
+
export function applyDefaults(
|
|
306
|
+
frontmatter: Record<string, unknown>,
|
|
307
|
+
schema: Schema
|
|
308
|
+
): Record<string, unknown> {
|
|
309
|
+
const result = { ...frontmatter };
|
|
310
|
+
const fields = schema.frontmatter || schema.fields || {};
|
|
311
|
+
|
|
312
|
+
for (const [fieldName, fieldSchema] of Object.entries(fields)) {
|
|
313
|
+
if (result[fieldName] === undefined && fieldSchema.default !== undefined) {
|
|
314
|
+
result[fieldName] = fieldSchema.default;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Format validation errors for display
|
|
323
|
+
*/
|
|
324
|
+
export function formatErrors(result: ValidationResult): string {
|
|
325
|
+
if (result.valid) {
|
|
326
|
+
return 'Validation passed';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return result.errors
|
|
330
|
+
.map((e) => {
|
|
331
|
+
let msg = `• ${e.field}: ${e.message}`;
|
|
332
|
+
if (e.expected) msg += ` (expected: ${e.expected})`;
|
|
333
|
+
if (e.received) msg += ` (got: ${e.received})`;
|
|
334
|
+
return msg;
|
|
335
|
+
})
|
|
336
|
+
.join('\n');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
340
|
+
// Schema Type Detection
|
|
341
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
export type SchemaType = 'prompt' | 'alignment' | 'spec' | 'documentation' | 'solution' | 'validation-suite' | 'skill' | 'workflow';
|
|
344
|
+
|
|
345
|
+
interface SchemaPattern {
|
|
346
|
+
pattern: string;
|
|
347
|
+
schemaType: SchemaType;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const SCHEMA_PATTERNS: SchemaPattern[] = [
|
|
351
|
+
{ pattern: '.planning/**/prompts/*.md', schemaType: 'prompt' },
|
|
352
|
+
{ pattern: '.planning/**/alignment.md', schemaType: 'alignment' },
|
|
353
|
+
{ pattern: 'specs/**/*.spec.md', schemaType: 'spec' },
|
|
354
|
+
{ pattern: 'specs/roadmap/**/*.spec.md', schemaType: 'spec' },
|
|
355
|
+
{ pattern: 'docs/solutions/**/*.md', schemaType: 'solution' },
|
|
356
|
+
{ pattern: 'docs/**/*.md', schemaType: 'documentation' },
|
|
357
|
+
{ pattern: '.allhands/validation/*.md', schemaType: 'validation-suite' },
|
|
358
|
+
{ pattern: '.allhands/skills/*/SKILL.md', schemaType: 'skill' },
|
|
359
|
+
{ pattern: '.allhands/workflows/*.md', schemaType: 'workflow' },
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Detect which schema type applies to a file path.
|
|
364
|
+
* Uses glob pattern matching against known schema patterns.
|
|
365
|
+
*
|
|
366
|
+
* @param filePath - Absolute or relative file path
|
|
367
|
+
* @param projectDir - Optional project root directory for relative path calculation
|
|
368
|
+
* @returns The detected schema type, or null if no schema applies
|
|
369
|
+
*/
|
|
370
|
+
export function detectSchemaType(filePath: string, projectDir?: string): SchemaType | null {
|
|
371
|
+
// Make path relative to project
|
|
372
|
+
let relativePath = filePath;
|
|
373
|
+
if (projectDir && filePath.startsWith(projectDir)) {
|
|
374
|
+
relativePath = filePath.slice(projectDir.length + 1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const { pattern, schemaType } of SCHEMA_PATTERNS) {
|
|
378
|
+
if (minimatch(relativePath, pattern)) {
|
|
379
|
+
return schemaType;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Infer schema type from file path using string matching.
|
|
387
|
+
* Fallback method when glob patterns don't match.
|
|
388
|
+
*
|
|
389
|
+
* @param file - File path to analyze
|
|
390
|
+
* @returns The inferred schema type, or null if unknown
|
|
391
|
+
*/
|
|
392
|
+
export function inferSchemaType(file: string): SchemaType | null {
|
|
393
|
+
if (file.includes('/prompts/') || file.match(/prompt.*\.md$/i)) {
|
|
394
|
+
return 'prompt';
|
|
395
|
+
}
|
|
396
|
+
if (file.includes('alignment') || file.match(/alignment\.md$/i)) {
|
|
397
|
+
return 'alignment';
|
|
398
|
+
}
|
|
399
|
+
if (file.includes('/specs/') || file.endsWith('.spec.md')) {
|
|
400
|
+
return 'spec';
|
|
401
|
+
}
|
|
402
|
+
if (file.includes('/docs/solutions/') && file.endsWith('.md')) {
|
|
403
|
+
return 'solution';
|
|
404
|
+
}
|
|
405
|
+
if (file.includes('/docs/') && file.endsWith('.md')) {
|
|
406
|
+
return 'documentation';
|
|
407
|
+
}
|
|
408
|
+
if (file.includes('/validation/') && file.endsWith('.md')) {
|
|
409
|
+
return 'validation-suite';
|
|
410
|
+
}
|
|
411
|
+
if (file.includes('/skills/') && file.endsWith('SKILL.md')) {
|
|
412
|
+
return 'skill';
|
|
413
|
+
}
|
|
414
|
+
if (file.includes('/workflows/') && file.endsWith('.md')) {
|
|
415
|
+
return 'workflow';
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Profile Schema
|
|
3
|
+
*
|
|
4
|
+
* Zod schema for validating agent profile YAML files.
|
|
5
|
+
* Replaces the static YAML schema with runtime validation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { TemplateVarNameSchema, validateTemplateString, type TemplateVarName } from './template-vars.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Raw agent profile as parsed from YAML (snake_case)
|
|
13
|
+
*/
|
|
14
|
+
export const RawAgentProfileSchema = z.object({
|
|
15
|
+
name: z.string().min(1).describe('Agent identifier (also used as tmux window name)'),
|
|
16
|
+
|
|
17
|
+
flow: z.string().min(1).describe('Flow file name relative to .allhands/flows/'),
|
|
18
|
+
|
|
19
|
+
prompt_scoped: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe('If true, multiple instances can run (one per prompt)'),
|
|
23
|
+
|
|
24
|
+
message_template: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Template string with ${VAR} interpolation'),
|
|
28
|
+
|
|
29
|
+
template_vars: z
|
|
30
|
+
.array(TemplateVarNameSchema)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Required variables for message_template'),
|
|
33
|
+
|
|
34
|
+
// TUI integration
|
|
35
|
+
tui_action: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('TUI action name that spawns this agent (e.g., "ideation", "compound")'),
|
|
39
|
+
|
|
40
|
+
tui_label: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Display label in TUI (defaults to capitalized name)'),
|
|
44
|
+
|
|
45
|
+
tui_requires_spec: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.default(false)
|
|
48
|
+
.describe('If true, TUI action requires an active spec'),
|
|
49
|
+
|
|
50
|
+
non_coding: z
|
|
51
|
+
.boolean()
|
|
52
|
+
.default(false)
|
|
53
|
+
.describe('If true, agent is non-coding (affects some behaviors)'),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type RawAgentProfile = z.infer<typeof RawAgentProfileSchema>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalized agent profile (camelCase, with defaults applied)
|
|
60
|
+
*/
|
|
61
|
+
export interface AgentProfile {
|
|
62
|
+
name: string;
|
|
63
|
+
flow: string;
|
|
64
|
+
promptScoped: boolean;
|
|
65
|
+
messageTemplate?: string;
|
|
66
|
+
templateVars: TemplateVarName[];
|
|
67
|
+
tuiAction?: string;
|
|
68
|
+
tuiLabel?: string;
|
|
69
|
+
tuiRequiresSpec: boolean;
|
|
70
|
+
nonCoding: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Transform raw YAML profile to normalized TypeScript interface
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeProfile(raw: RawAgentProfile): AgentProfile {
|
|
77
|
+
return {
|
|
78
|
+
name: raw.name,
|
|
79
|
+
flow: raw.flow,
|
|
80
|
+
promptScoped: raw.prompt_scoped,
|
|
81
|
+
messageTemplate: raw.message_template,
|
|
82
|
+
templateVars: (raw.template_vars ?? []) as TemplateVarName[],
|
|
83
|
+
tuiAction: raw.tui_action,
|
|
84
|
+
tuiLabel: raw.tui_label,
|
|
85
|
+
tuiRequiresSpec: raw.tui_requires_spec,
|
|
86
|
+
nonCoding: raw.non_coding,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validation result for an agent profile
|
|
92
|
+
*/
|
|
93
|
+
export interface ProfileValidation {
|
|
94
|
+
valid: boolean;
|
|
95
|
+
errors: string[];
|
|
96
|
+
warnings: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate an agent profile beyond basic schema validation.
|
|
101
|
+
* Checks:
|
|
102
|
+
* - Template variables in message_template match template_vars list
|
|
103
|
+
* - No unknown template variables
|
|
104
|
+
*/
|
|
105
|
+
export function validateProfileSemantics(profile: AgentProfile): ProfileValidation {
|
|
106
|
+
const errors: string[] = [];
|
|
107
|
+
const warnings: string[] = [];
|
|
108
|
+
|
|
109
|
+
// Validate message_template references
|
|
110
|
+
if (profile.messageTemplate) {
|
|
111
|
+
const templateCheck = validateTemplateString(profile.messageTemplate);
|
|
112
|
+
|
|
113
|
+
if (!templateCheck.valid) {
|
|
114
|
+
errors.push(`Invalid template variables in message_template: ${templateCheck.invalidVars.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check that all referenced vars are in template_vars
|
|
118
|
+
const referencedVars = profile.messageTemplate.match(/\$\{([^}]+)\}/g)?.map((m) => m.slice(2, -1)) ?? [];
|
|
119
|
+
|
|
120
|
+
for (const refVar of referencedVars) {
|
|
121
|
+
if (!profile.templateVars.includes(refVar as TemplateVarName)) {
|
|
122
|
+
warnings.push(`Template references ${refVar} but it's not in template_vars list`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check for unused template_vars
|
|
127
|
+
for (const declaredVar of profile.templateVars) {
|
|
128
|
+
if (!referencedVars.includes(declaredVar)) {
|
|
129
|
+
warnings.push(`template_vars declares ${declaredVar} but it's not used in message_template`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else if (profile.templateVars.length > 0) {
|
|
133
|
+
warnings.push('template_vars defined but no message_template to use them');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
valid: errors.length === 0,
|
|
138
|
+
errors,
|
|
139
|
+
warnings,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Variables Registry
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all template variables used in agent profiles.
|
|
5
|
+
* Provides type safety, runtime validation, and self-documentation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All valid template variables that can be used in agent message_template fields.
|
|
12
|
+
* Each variable has a Zod schema for validation and a description.
|
|
13
|
+
*/
|
|
14
|
+
export const TemplateVars = {
|
|
15
|
+
// Path variables
|
|
16
|
+
SPEC_PATH: z.string().describe('Path to spec file'),
|
|
17
|
+
ALIGNMENT_PATH: z.string().describe('Path to alignment doc'),
|
|
18
|
+
PROMPTS_FOLDER: z.string().describe('Path to prompts directory'),
|
|
19
|
+
PROMPT_PATH: z.string().describe('Path to specific prompt file'),
|
|
20
|
+
OUTPUT_PATH: z.string().describe('Output file path'),
|
|
21
|
+
PLANNING_FOLDER: z.string().describe('Path to .planning/{branch} directory'),
|
|
22
|
+
|
|
23
|
+
// Identifier variables
|
|
24
|
+
SPEC_NAME: z.string().describe('Current spec name'),
|
|
25
|
+
PROMPT_NUMBER: z
|
|
26
|
+
.string()
|
|
27
|
+
.regex(/^\d{2}$/)
|
|
28
|
+
.describe('Prompt number as two digits (01, 02, etc.)'),
|
|
29
|
+
|
|
30
|
+
// Branch/context variables
|
|
31
|
+
BRANCH: z.string().describe('Current git branch name'),
|
|
32
|
+
|
|
33
|
+
// Spec metadata variables
|
|
34
|
+
SPEC_TYPE: z
|
|
35
|
+
.string()
|
|
36
|
+
.describe('Spec type from frontmatter (milestone, investigation, optimization, refactor, documentation, triage)'),
|
|
37
|
+
|
|
38
|
+
// Workflow domain variables
|
|
39
|
+
WORKFLOW_DOMAIN_PATH: z
|
|
40
|
+
.string()
|
|
41
|
+
.describe('Path to workflow domain config file in .allhands/workflows/'),
|
|
42
|
+
|
|
43
|
+
// Emergent planner variables
|
|
44
|
+
HYPOTHESIS_DOMAINS: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe('Comma-separated list of hypothesis domains from settings.json'),
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Union type of all valid template variable names
|
|
51
|
+
*/
|
|
52
|
+
export type TemplateVarName = keyof typeof TemplateVars;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Array of all valid template variable names (for runtime checks)
|
|
56
|
+
*/
|
|
57
|
+
export const TEMPLATE_VAR_NAMES = Object.keys(TemplateVars) as TemplateVarName[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Context object passed to template resolution.
|
|
61
|
+
* All variables are optional - validation happens against profile requirements.
|
|
62
|
+
*/
|
|
63
|
+
export type TemplateContext = Partial<Record<TemplateVarName, string | null>>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Zod schema for validating a single template variable name
|
|
67
|
+
*/
|
|
68
|
+
export const TemplateVarNameSchema = z.enum(TEMPLATE_VAR_NAMES as [TemplateVarName, ...TemplateVarName[]]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate that a string is a valid template variable name
|
|
72
|
+
*/
|
|
73
|
+
export function isValidTemplateVar(name: string): name is TemplateVarName {
|
|
74
|
+
return TEMPLATE_VAR_NAMES.includes(name as TemplateVarName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a context object against required variables.
|
|
79
|
+
* Returns errors for missing or invalid variables.
|
|
80
|
+
*/
|
|
81
|
+
export function validateContext(
|
|
82
|
+
context: TemplateContext,
|
|
83
|
+
requiredVars: TemplateVarName[]
|
|
84
|
+
): { valid: boolean; errors: string[] } {
|
|
85
|
+
const errors: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (const varName of requiredVars) {
|
|
88
|
+
const value = context[varName];
|
|
89
|
+
|
|
90
|
+
// Reject undefined or empty string
|
|
91
|
+
if (value === undefined || value === '') {
|
|
92
|
+
errors.push(`Missing required template variable: ${varName}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate value against the variable's schema
|
|
97
|
+
const schema = TemplateVars[varName];
|
|
98
|
+
const result = schema.safeParse(value);
|
|
99
|
+
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
errors.push(`Invalid value for ${varName}: ${result.error.issues[0]?.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
valid: errors.length === 0,
|
|
107
|
+
errors,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get description for a template variable
|
|
113
|
+
*/
|
|
114
|
+
export function getTemplateVarDescription(name: TemplateVarName): string {
|
|
115
|
+
return TemplateVars[name].description ?? name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract template variable references from a template string.
|
|
120
|
+
* Returns array of variable names found in ${VAR_NAME} patterns.
|
|
121
|
+
*/
|
|
122
|
+
export function extractTemplateVars(template: string): string[] {
|
|
123
|
+
const matches = template.matchAll(/\$\{([^}]+)\}/g);
|
|
124
|
+
return [...matches].map((m) => m[1]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate that all variables referenced in a template are valid.
|
|
129
|
+
*/
|
|
130
|
+
export function validateTemplateString(template: string): { valid: boolean; invalidVars: string[] } {
|
|
131
|
+
const refs = extractTemplateVars(template);
|
|
132
|
+
const invalidVars = refs.filter((v) => !isValidTemplateVar(v));
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
valid: invalidVars.length === 0,
|
|
136
|
+
invalidVars,
|
|
137
|
+
};
|
|
138
|
+
}
|