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,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Hook Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common types, I/O helpers, and cache utilities for Claude Code hooks.
|
|
5
|
+
* Hooks communicate via stdin/stdout JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
import { logHookStart, logHookSuccess } from '../lib/trace-store.js';
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Types
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Input from Claude Code hooks (stdin JSON) */
|
|
18
|
+
export interface HookInput {
|
|
19
|
+
session_id?: string;
|
|
20
|
+
/** Working directory of the Claude Code session */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
tool_name?: string;
|
|
23
|
+
tool_input?: Record<string, unknown>;
|
|
24
|
+
/** Claude Code PostToolUse sends this as tool_response; normalized to tool_result in readHookInput */
|
|
25
|
+
tool_response?: unknown;
|
|
26
|
+
/** Normalized from tool_response for backward compat - handlers should read this field */
|
|
27
|
+
tool_result?: unknown;
|
|
28
|
+
transcript_path?: string;
|
|
29
|
+
stop_hook_active?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** PreToolUse hook output */
|
|
33
|
+
export interface PreToolUseOutput {
|
|
34
|
+
hookSpecificOutput: {
|
|
35
|
+
hookEventName: 'PreToolUse';
|
|
36
|
+
permissionDecision?: 'allow' | 'deny' | 'ask';
|
|
37
|
+
permissionDecisionReason?: string;
|
|
38
|
+
updatedInput?: Record<string, unknown>;
|
|
39
|
+
additionalContext?: string;
|
|
40
|
+
};
|
|
41
|
+
systemMessage?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** PostToolUse hook output - uses hookSpecificOutput for model-visible context */
|
|
45
|
+
export interface PostToolUseOutput {
|
|
46
|
+
continue?: boolean;
|
|
47
|
+
suppressOutput?: boolean;
|
|
48
|
+
systemMessage?: string;
|
|
49
|
+
hookSpecificOutput?: {
|
|
50
|
+
hookEventName: 'PostToolUse';
|
|
51
|
+
additionalContext?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stop/SubagentStop hook output */
|
|
56
|
+
export interface StopHookOutput {
|
|
57
|
+
decision: 'approve' | 'block';
|
|
58
|
+
reason?: string;
|
|
59
|
+
systemMessage?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** PreCompact hook output - standard format with systemMessage */
|
|
63
|
+
export interface PreCompactOutput {
|
|
64
|
+
continue?: boolean;
|
|
65
|
+
systemMessage?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// I/O Helpers
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read hook input from stdin (synchronous for hook context).
|
|
74
|
+
*/
|
|
75
|
+
export async function readHookInput(): Promise<HookInput> {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
let data = '';
|
|
78
|
+
process.stdin.setEncoding('utf8');
|
|
79
|
+
|
|
80
|
+
process.stdin.on('data', (chunk) => {
|
|
81
|
+
data += chunk;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
process.stdin.on('end', () => {
|
|
85
|
+
try {
|
|
86
|
+
if (!data.trim()) {
|
|
87
|
+
resolve({});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const parsed = JSON.parse(data) as HookInput;
|
|
91
|
+
// Claude Code PostToolUse sends tool_response; normalize to tool_result
|
|
92
|
+
if (parsed.tool_response !== undefined && parsed.tool_result === undefined) {
|
|
93
|
+
parsed.tool_result = parsed.tool_response;
|
|
94
|
+
}
|
|
95
|
+
resolve(parsed);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
reject(new Error(`Failed to parse hook input: ${e}`));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
process.stdin.on('error', reject);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deny a tool use with a reason.
|
|
107
|
+
* Outputs JSON to stdout and exits with 0 (success for hooks).
|
|
108
|
+
* The reason is shown to Claude via permissionDecisionReason.
|
|
109
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
110
|
+
*/
|
|
111
|
+
export function denyTool(reason: string, hookName?: string): never {
|
|
112
|
+
if (hookName) {
|
|
113
|
+
logHookSuccess(hookName, { action: 'deny', reason });
|
|
114
|
+
}
|
|
115
|
+
const output = {
|
|
116
|
+
hookSpecificOutput: {
|
|
117
|
+
hookEventName: 'PreToolUse',
|
|
118
|
+
permissionDecision: 'deny',
|
|
119
|
+
permissionDecisionReason: reason,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
console.log(JSON.stringify(output));
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Allow a tool use (silent exit).
|
|
128
|
+
* Hooks that don't output anything allow the tool.
|
|
129
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
130
|
+
* @see denyTool for blocking with a reason
|
|
131
|
+
*/
|
|
132
|
+
export function allowTool(hookName?: string): never {
|
|
133
|
+
if (hookName) {
|
|
134
|
+
logHookSuccess(hookName, { action: 'allow' });
|
|
135
|
+
}
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Output additional context for PostToolUse hooks.
|
|
141
|
+
* Uses hookSpecificOutput.additionalContext per Claude Code hooks spec.
|
|
142
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
143
|
+
*/
|
|
144
|
+
export function outputContext(context: string, hookName?: string): never {
|
|
145
|
+
if (hookName) {
|
|
146
|
+
logHookSuccess(hookName, { action: 'context', hasContext: true });
|
|
147
|
+
}
|
|
148
|
+
const output = {
|
|
149
|
+
hookSpecificOutput: {
|
|
150
|
+
hookEventName: 'PostToolUse',
|
|
151
|
+
additionalContext: context,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
console.log(JSON.stringify(output));
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Block a PostToolUse action with a message.
|
|
160
|
+
* Uses decision: 'block' with reason for reliable visibility.
|
|
161
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
162
|
+
*/
|
|
163
|
+
export function blockTool(message: string, hookName?: string): never {
|
|
164
|
+
if (hookName) {
|
|
165
|
+
logHookSuccess(hookName, { action: 'block', message });
|
|
166
|
+
}
|
|
167
|
+
// Use decision: 'block' with reason for reliable visibility (like Continuous-Claude-v3)
|
|
168
|
+
const output = {
|
|
169
|
+
decision: 'block',
|
|
170
|
+
reason: message,
|
|
171
|
+
};
|
|
172
|
+
console.log(JSON.stringify(output));
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Output Stop/SubagentStop hook result.
|
|
178
|
+
* Use decision: "approve" to allow stop, "block" to continue.
|
|
179
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
180
|
+
*/
|
|
181
|
+
export function outputStopHook(decision: 'approve' | 'block', reason?: string, hookName?: string): never {
|
|
182
|
+
if (hookName) {
|
|
183
|
+
logHookSuccess(hookName, { action: decision, reason });
|
|
184
|
+
}
|
|
185
|
+
const output: StopHookOutput = {
|
|
186
|
+
decision,
|
|
187
|
+
reason,
|
|
188
|
+
};
|
|
189
|
+
console.log(JSON.stringify(output));
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Output PreCompact hook result.
|
|
195
|
+
* Use systemMessage to inject context that survives compaction.
|
|
196
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
197
|
+
*/
|
|
198
|
+
export function outputPreCompact(systemMessage?: string, hookName?: string): never {
|
|
199
|
+
if (hookName) {
|
|
200
|
+
logHookSuccess(hookName, { action: 'precompact', hasMessage: !!systemMessage });
|
|
201
|
+
}
|
|
202
|
+
const output: PreCompactOutput = {
|
|
203
|
+
continue: true,
|
|
204
|
+
systemMessage,
|
|
205
|
+
};
|
|
206
|
+
console.log(JSON.stringify(output));
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
// Language Detection (for AST-grep, TLDR, etc.)
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** Map of file extensions to AST-grep compatible language names */
|
|
215
|
+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
216
|
+
// TypeScript
|
|
217
|
+
'.ts': 'typescript',
|
|
218
|
+
'.tsx': 'tsx',
|
|
219
|
+
'.mts': 'typescript',
|
|
220
|
+
'.cts': 'typescript',
|
|
221
|
+
// JavaScript
|
|
222
|
+
'.js': 'javascript',
|
|
223
|
+
'.jsx': 'javascript',
|
|
224
|
+
'.mjs': 'javascript',
|
|
225
|
+
'.cjs': 'javascript',
|
|
226
|
+
// Python
|
|
227
|
+
'.py': 'python',
|
|
228
|
+
'.pyi': 'python',
|
|
229
|
+
'.pyx': 'python',
|
|
230
|
+
// Go
|
|
231
|
+
'.go': 'go',
|
|
232
|
+
// Rust
|
|
233
|
+
'.rs': 'rust',
|
|
234
|
+
// C/C++
|
|
235
|
+
'.c': 'c',
|
|
236
|
+
'.h': 'c',
|
|
237
|
+
'.cpp': 'cpp',
|
|
238
|
+
'.cc': 'cpp',
|
|
239
|
+
'.cxx': 'cpp',
|
|
240
|
+
'.hpp': 'cpp',
|
|
241
|
+
'.hh': 'cpp',
|
|
242
|
+
// Java
|
|
243
|
+
'.java': 'java',
|
|
244
|
+
// Kotlin
|
|
245
|
+
'.kt': 'kotlin',
|
|
246
|
+
'.kts': 'kotlin',
|
|
247
|
+
// Ruby
|
|
248
|
+
'.rb': 'ruby',
|
|
249
|
+
// Swift
|
|
250
|
+
'.swift': 'swift',
|
|
251
|
+
// C#
|
|
252
|
+
'.cs': 'c-sharp',
|
|
253
|
+
// Lua
|
|
254
|
+
'.lua': 'lua',
|
|
255
|
+
// HTML/CSS
|
|
256
|
+
'.html': 'html',
|
|
257
|
+
'.css': 'css',
|
|
258
|
+
'.scss': 'scss',
|
|
259
|
+
// JSON/YAML
|
|
260
|
+
'.json': 'json',
|
|
261
|
+
'.yaml': 'yaml',
|
|
262
|
+
'.yml': 'yaml',
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** Map of ripgrep type names to AST-grep language names */
|
|
266
|
+
const TYPE_TO_LANGUAGE: Record<string, string> = {
|
|
267
|
+
'ts': 'typescript',
|
|
268
|
+
'typescript': 'typescript',
|
|
269
|
+
'tsx': 'tsx',
|
|
270
|
+
'js': 'javascript',
|
|
271
|
+
'javascript': 'javascript',
|
|
272
|
+
'jsx': 'javascript',
|
|
273
|
+
'py': 'python',
|
|
274
|
+
'python': 'python',
|
|
275
|
+
'go': 'go',
|
|
276
|
+
'rust': 'rust',
|
|
277
|
+
'rs': 'rust',
|
|
278
|
+
'c': 'c',
|
|
279
|
+
'cpp': 'cpp',
|
|
280
|
+
'java': 'java',
|
|
281
|
+
'kotlin': 'kotlin',
|
|
282
|
+
'kt': 'kotlin',
|
|
283
|
+
'ruby': 'ruby',
|
|
284
|
+
'rb': 'ruby',
|
|
285
|
+
'swift': 'swift',
|
|
286
|
+
'cs': 'c-sharp',
|
|
287
|
+
'csharp': 'c-sharp',
|
|
288
|
+
'lua': 'lua',
|
|
289
|
+
'html': 'html',
|
|
290
|
+
'css': 'css',
|
|
291
|
+
'json': 'json',
|
|
292
|
+
'yaml': 'yaml',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/** Map of code patterns to likely languages */
|
|
296
|
+
const PATTERN_TO_LANGUAGE: Record<string, string> = {
|
|
297
|
+
'def ': 'python',
|
|
298
|
+
'async def ': 'python',
|
|
299
|
+
'class ': 'python', // Could be multiple languages, default to python
|
|
300
|
+
'function ': 'typescript',
|
|
301
|
+
'async function ': 'typescript',
|
|
302
|
+
'const ': 'typescript',
|
|
303
|
+
'let ': 'typescript',
|
|
304
|
+
'export ': 'typescript',
|
|
305
|
+
'import ': 'typescript',
|
|
306
|
+
'func ': 'go',
|
|
307
|
+
'fn ': 'rust',
|
|
308
|
+
'pub fn ': 'rust',
|
|
309
|
+
'impl ': 'rust',
|
|
310
|
+
'package ': 'go',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Detect language from various inputs.
|
|
315
|
+
* Checks in order: glob patterns, ripgrep type, code patterns.
|
|
316
|
+
* Returns AST-grep compatible language name.
|
|
317
|
+
*/
|
|
318
|
+
export function detectLanguage(options: {
|
|
319
|
+
glob?: string;
|
|
320
|
+
type?: string;
|
|
321
|
+
pattern?: string;
|
|
322
|
+
filePath?: string;
|
|
323
|
+
}): string {
|
|
324
|
+
const { glob, type, pattern, filePath } = options;
|
|
325
|
+
|
|
326
|
+
// 1. Check file path extension
|
|
327
|
+
if (filePath) {
|
|
328
|
+
const ext = filePath.substring(filePath.lastIndexOf('.'));
|
|
329
|
+
if (EXTENSION_TO_LANGUAGE[ext]) {
|
|
330
|
+
return EXTENSION_TO_LANGUAGE[ext];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 2. Check glob pattern for extensions
|
|
335
|
+
if (glob) {
|
|
336
|
+
for (const [ext, lang] of Object.entries(EXTENSION_TO_LANGUAGE)) {
|
|
337
|
+
if (glob.includes(ext)) {
|
|
338
|
+
return lang;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 3. Check ripgrep type parameter
|
|
344
|
+
if (type) {
|
|
345
|
+
const lowerType = type.toLowerCase();
|
|
346
|
+
if (TYPE_TO_LANGUAGE[lowerType]) {
|
|
347
|
+
return TYPE_TO_LANGUAGE[lowerType];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 4. Check code pattern for language hints
|
|
352
|
+
if (pattern) {
|
|
353
|
+
for (const [hint, lang] of Object.entries(PATTERN_TO_LANGUAGE)) {
|
|
354
|
+
if (pattern.includes(hint)) {
|
|
355
|
+
return lang;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Default to typescript (most common in this codebase)
|
|
361
|
+
return 'typescript';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get all file extensions for a given language.
|
|
366
|
+
* Useful for building glob patterns.
|
|
367
|
+
*/
|
|
368
|
+
export function getExtensionsForLanguage(language: string): string[] {
|
|
369
|
+
const extensions: string[] = [];
|
|
370
|
+
for (const [ext, lang] of Object.entries(EXTENSION_TO_LANGUAGE)) {
|
|
371
|
+
if (lang === language) {
|
|
372
|
+
extensions.push(ext);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return extensions;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
// Path Helpers
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get the project directory.
|
|
384
|
+
* Priority:
|
|
385
|
+
* 1. CLAUDE_PROJECT_DIR env var (set by Claude Code)
|
|
386
|
+
* 2. Find .allhands/harness directory going up from cwd (indicates project root)
|
|
387
|
+
* 3. Fall back to cwd
|
|
388
|
+
*/
|
|
389
|
+
export function getProjectDir(): string {
|
|
390
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
391
|
+
return process.env.CLAUDE_PROJECT_DIR;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Find project root by looking for .allhands/harness directory
|
|
395
|
+
// This is more reliable than just .allhands since harness may have nested .allhands
|
|
396
|
+
let dir = process.cwd();
|
|
397
|
+
while (dir !== '/') {
|
|
398
|
+
const harnessPath = join(dir, '.allhands', 'harness');
|
|
399
|
+
const ahScript = join(harnessPath, 'ah');
|
|
400
|
+
// Check for the ah script to confirm this is the project root
|
|
401
|
+
if (existsSync(ahScript)) {
|
|
402
|
+
return dir;
|
|
403
|
+
}
|
|
404
|
+
dir = join(dir, '..');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return process.cwd();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get the cache directory (.allhands/harness/.cache/).
|
|
412
|
+
* Creates the directory if it doesn't exist.
|
|
413
|
+
*/
|
|
414
|
+
export function getCacheDir(): string {
|
|
415
|
+
const projectDir = getProjectDir();
|
|
416
|
+
const cacheDir = join(projectDir, '.allhands', 'harness', '.cache');
|
|
417
|
+
|
|
418
|
+
if (!existsSync(cacheDir)) {
|
|
419
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return cacheDir;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get a specific cache subdirectory.
|
|
427
|
+
*/
|
|
428
|
+
export function getCacheSubdir(name: string): string {
|
|
429
|
+
const subdir = join(getCacheDir(), name);
|
|
430
|
+
|
|
431
|
+
if (!existsSync(subdir)) {
|
|
432
|
+
mkdirSync(subdir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return subdir;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
439
|
+
// PreToolUse Context Injection Helpers
|
|
440
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Output PreToolUse with context injection (modifies tool input).
|
|
444
|
+
* Prepends additionalContext to the specified field (default: 'prompt').
|
|
445
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
446
|
+
*/
|
|
447
|
+
export function injectContext(
|
|
448
|
+
originalInput: Record<string, unknown>,
|
|
449
|
+
additionalContext: string,
|
|
450
|
+
targetField: string = 'prompt',
|
|
451
|
+
hookName?: string
|
|
452
|
+
): never {
|
|
453
|
+
if (hookName) {
|
|
454
|
+
logHookSuccess(hookName, { action: 'inject', targetField });
|
|
455
|
+
}
|
|
456
|
+
const currentValue = (originalInput[targetField] as string) || '';
|
|
457
|
+
const output: PreToolUseOutput = {
|
|
458
|
+
hookSpecificOutput: {
|
|
459
|
+
hookEventName: 'PreToolUse',
|
|
460
|
+
permissionDecision: 'allow',
|
|
461
|
+
updatedInput: {
|
|
462
|
+
...originalInput,
|
|
463
|
+
[targetField]: `${additionalContext}\n\n---\n${currentValue}`,
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
console.log(JSON.stringify(output));
|
|
468
|
+
process.exit(0);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Output PreToolUse additional context without modifying input.
|
|
473
|
+
* Adds context to the conversation via systemMessage.
|
|
474
|
+
* Optionally logs to trace-store if hookName is provided.
|
|
475
|
+
*/
|
|
476
|
+
export function preToolContext(context: string, hookName?: string): never {
|
|
477
|
+
if (hookName) {
|
|
478
|
+
logHookSuccess(hookName, { action: 'preToolContext', hasContext: true });
|
|
479
|
+
}
|
|
480
|
+
const output: PreToolUseOutput = {
|
|
481
|
+
hookSpecificOutput: {
|
|
482
|
+
hookEventName: 'PreToolUse',
|
|
483
|
+
permissionDecision: 'allow',
|
|
484
|
+
},
|
|
485
|
+
systemMessage: context,
|
|
486
|
+
};
|
|
487
|
+
console.log(JSON.stringify(output));
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
492
|
+
// Project Settings (.allhands/settings.json)
|
|
493
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
/** Format pattern configuration */
|
|
496
|
+
export interface FormatPattern {
|
|
497
|
+
match: string;
|
|
498
|
+
command: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Format configuration */
|
|
502
|
+
export interface FormatConfig {
|
|
503
|
+
enabled?: boolean;
|
|
504
|
+
command?: string;
|
|
505
|
+
patterns?: FormatPattern[];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Validation section of settings */
|
|
509
|
+
export interface ValidationSettings {
|
|
510
|
+
format?: FormatConfig;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Git settings */
|
|
514
|
+
export interface GitSettings {
|
|
515
|
+
baseBranch?: string;
|
|
516
|
+
localBaseBranch?: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** TLDR settings */
|
|
520
|
+
export interface TldrSettings {
|
|
521
|
+
enableForHarness?: boolean;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Knowledge search settings */
|
|
525
|
+
export interface KnowledgeSettings {
|
|
526
|
+
similarityThreshold?: number;
|
|
527
|
+
fullContextSimilarityThreshold?: number;
|
|
528
|
+
contextTokenLimit?: number;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Oracle inference settings */
|
|
532
|
+
export interface OracleSettings {
|
|
533
|
+
defaultProvider?: 'gemini' | 'openai';
|
|
534
|
+
/** LLM provider for compaction analysis (defaults to gemini for large context) */
|
|
535
|
+
compactionProvider?: 'gemini' | 'openai';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** OpenCode SDK agent execution settings */
|
|
539
|
+
export interface OpencodeSdkSettings {
|
|
540
|
+
model?: string;
|
|
541
|
+
codesearchToolBudget?: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Spawn settings for parallel execution */
|
|
545
|
+
export interface SpawnSettings {
|
|
546
|
+
maxParallelPrompts?: number;
|
|
547
|
+
/** Autocompact percentage threshold for prompt-scoped agents (1-100, default 65) */
|
|
548
|
+
promptScopedAutocompactAt?: number;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Event loop timing settings */
|
|
552
|
+
export interface EventLoopSettings {
|
|
553
|
+
tickIntervalMs?: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** PR review detection and triggering settings */
|
|
557
|
+
export interface PRReviewSettings {
|
|
558
|
+
reviewMatchPattern?: string;
|
|
559
|
+
rerunComment?: string;
|
|
560
|
+
checkFrequency?: number;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Project settings structure (.allhands/settings.json) */
|
|
564
|
+
export interface DaemonSettings {
|
|
565
|
+
enabled?: boolean;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Emergent work settings (hypothesis domains for emergent planner) */
|
|
569
|
+
export interface EmergentSettings {
|
|
570
|
+
hypothesisDomains?: string[];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export interface ProjectSettings {
|
|
574
|
+
daemon?: DaemonSettings;
|
|
575
|
+
validation?: ValidationSettings;
|
|
576
|
+
git?: GitSettings;
|
|
577
|
+
tldr?: TldrSettings;
|
|
578
|
+
knowledge?: KnowledgeSettings;
|
|
579
|
+
oracle?: OracleSettings;
|
|
580
|
+
opencodeSdk?: OpencodeSdkSettings;
|
|
581
|
+
spawn?: SpawnSettings;
|
|
582
|
+
eventLoop?: EventLoopSettings;
|
|
583
|
+
prReview?: PRReviewSettings;
|
|
584
|
+
emergent?: EmergentSettings;
|
|
585
|
+
disabledHooks?: string[];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Load project settings from .allhands/settings.json.
|
|
590
|
+
* Returns null if file doesn't exist or is invalid.
|
|
591
|
+
*/
|
|
592
|
+
export function loadProjectSettings(): ProjectSettings | null {
|
|
593
|
+
const settingsPath = join(getProjectDir(), '.allhands', 'settings.json');
|
|
594
|
+
if (!existsSync(settingsPath)) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const content = readFileSync(settingsPath, 'utf-8');
|
|
600
|
+
return JSON.parse(content) as ProjectSettings;
|
|
601
|
+
} catch {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get base branch from settings or default.
|
|
608
|
+
* Priority: settings.json > "main"
|
|
609
|
+
*/
|
|
610
|
+
export function getBaseBranch(): string {
|
|
611
|
+
const settings = loadProjectSettings();
|
|
612
|
+
return settings?.git?.baseBranch || 'main';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get local base branch for checkout operations.
|
|
617
|
+
* Priority: settings.git.localBaseBranch > settings.git.baseBranch > "main"
|
|
618
|
+
*/
|
|
619
|
+
export function getLocalBaseBranch(): string {
|
|
620
|
+
const settings = loadProjectSettings();
|
|
621
|
+
return settings?.git?.localBaseBranch || settings?.git?.baseBranch || 'main';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
625
|
+
// Search Context (for hook coordination)
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
/** Search context passed between hooks */
|
|
629
|
+
export interface SearchContext {
|
|
630
|
+
timestamp: number;
|
|
631
|
+
queryType: 'structural' | 'semantic' | 'literal';
|
|
632
|
+
pattern: string;
|
|
633
|
+
target: string | null;
|
|
634
|
+
targetType: 'function' | 'class' | 'variable' | 'import' | 'decorator' | 'unknown';
|
|
635
|
+
suggestedLayers: string[];
|
|
636
|
+
definitionLocation?: string;
|
|
637
|
+
callers?: string[];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Get the search context file path for a session.
|
|
642
|
+
*/
|
|
643
|
+
function getSearchContextPath(sessionId: string): string {
|
|
644
|
+
const tmpDir = '/tmp/claude-search-context';
|
|
645
|
+
if (!existsSync(tmpDir)) {
|
|
646
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
647
|
+
}
|
|
648
|
+
return join(tmpDir, `${sessionId}.json`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Save search context for downstream hooks.
|
|
653
|
+
*/
|
|
654
|
+
export function saveSearchContext(sessionId: string, context: SearchContext): void {
|
|
655
|
+
const path = getSearchContextPath(sessionId);
|
|
656
|
+
writeFileSync(path, JSON.stringify(context, null, 2));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Load search context from upstream hooks.
|
|
661
|
+
* Returns null if not found or expired (>5 min).
|
|
662
|
+
*/
|
|
663
|
+
export function loadSearchContext(sessionId: string): SearchContext | null {
|
|
664
|
+
const path = getSearchContextPath(sessionId);
|
|
665
|
+
if (!existsSync(path)) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const data = readFileSync(path, 'utf-8');
|
|
671
|
+
const context = JSON.parse(data) as SearchContext;
|
|
672
|
+
|
|
673
|
+
// Check if expired (5 minute TTL)
|
|
674
|
+
const age = Date.now() - context.timestamp;
|
|
675
|
+
if (age > 5 * 60 * 1000) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return context;
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
686
|
+
// Declarative Hook Definitions
|
|
687
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/** Error fallback strategies for when a hook throws */
|
|
690
|
+
export type ErrorFallback =
|
|
691
|
+
| { type: 'allowTool' }
|
|
692
|
+
| { type: 'outputStopHook'; decision: 'approve' | 'block' }
|
|
693
|
+
| { type: 'outputPreCompact' }
|
|
694
|
+
| { type: 'continue' }
|
|
695
|
+
| { type: 'silent' };
|
|
696
|
+
|
|
697
|
+
/** Single hook definition */
|
|
698
|
+
export interface HookDefinition {
|
|
699
|
+
/** Hook name (e.g., 'agent-stop') */
|
|
700
|
+
name: string;
|
|
701
|
+
/** Description for CLI help */
|
|
702
|
+
description: string;
|
|
703
|
+
/** Handler function */
|
|
704
|
+
handler: (input: HookInput) => void | Promise<void>;
|
|
705
|
+
/** Error fallback strategy (default: silent exit) */
|
|
706
|
+
errorFallback?: ErrorFallback;
|
|
707
|
+
/** Optional payload generator for trace logging */
|
|
708
|
+
logPayload?: (input: HookInput) => Record<string, unknown>;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Category of related hooks */
|
|
712
|
+
export interface HookCategory {
|
|
713
|
+
/** Category name (e.g., 'lifecycle') */
|
|
714
|
+
name: string;
|
|
715
|
+
/** Description for CLI help */
|
|
716
|
+
description: string;
|
|
717
|
+
/** Hooks in this category */
|
|
718
|
+
hooks: HookDefinition[];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Type for daemon handler registration function */
|
|
722
|
+
export type RegisterFn = (category: string, name: string, handler: (input: HookInput) => void | Promise<void>) => void;
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Execute error fallback strategy.
|
|
726
|
+
*/
|
|
727
|
+
function executeErrorFallback(fallback: ErrorFallback | undefined, hookName: string): never {
|
|
728
|
+
if (!fallback) {
|
|
729
|
+
// Default: silent exit
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
switch (fallback.type) {
|
|
734
|
+
case 'allowTool':
|
|
735
|
+
logHookSuccess(hookName, { action: 'allow', error: true });
|
|
736
|
+
process.exit(0);
|
|
737
|
+
case 'outputStopHook':
|
|
738
|
+
logHookSuccess(hookName, { action: fallback.decision, error: true });
|
|
739
|
+
console.log(JSON.stringify({ decision: fallback.decision }));
|
|
740
|
+
process.exit(0);
|
|
741
|
+
case 'outputPreCompact':
|
|
742
|
+
logHookSuccess(hookName, { action: 'continue', error: true });
|
|
743
|
+
console.log(JSON.stringify({ continue: true }));
|
|
744
|
+
process.exit(0);
|
|
745
|
+
case 'continue':
|
|
746
|
+
logHookSuccess(hookName, { action: 'continue', error: true });
|
|
747
|
+
console.log(JSON.stringify({ continue: true }));
|
|
748
|
+
process.exit(0);
|
|
749
|
+
case 'silent':
|
|
750
|
+
default:
|
|
751
|
+
process.exit(0);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Check if a hook is disabled via disabledHooks in settings.
|
|
757
|
+
* Disabled hooks pass through without executing.
|
|
758
|
+
*/
|
|
759
|
+
function isHookDisabled(hookName: string): boolean {
|
|
760
|
+
const settings = loadProjectSettings();
|
|
761
|
+
return settings?.disabledHooks?.includes(hookName) ?? false;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Register a hook category to Commander.js.
|
|
766
|
+
* Creates subcommands with consistent error handling and trace logging.
|
|
767
|
+
*/
|
|
768
|
+
export function registerCategory(parent: Command, category: HookCategory): void {
|
|
769
|
+
const cmd = parent
|
|
770
|
+
.command(category.name)
|
|
771
|
+
.description(category.description);
|
|
772
|
+
|
|
773
|
+
for (const hook of category.hooks) {
|
|
774
|
+
const hookName = `${category.name} ${hook.name}`;
|
|
775
|
+
|
|
776
|
+
cmd
|
|
777
|
+
.command(hook.name)
|
|
778
|
+
.description(hook.description)
|
|
779
|
+
.action(async () => {
|
|
780
|
+
if (isHookDisabled(hookName)) {
|
|
781
|
+
process.exit(0);
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const input = await readHookInput();
|
|
785
|
+
const payload = hook.logPayload ? hook.logPayload(input) : { tool: input.tool_name };
|
|
786
|
+
logHookStart(hookName, payload);
|
|
787
|
+
await hook.handler(input);
|
|
788
|
+
} catch {
|
|
789
|
+
executeErrorFallback(hook.errorFallback, hookName);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Register a hook category for daemon mode.
|
|
797
|
+
* Handlers are called directly with input (daemon manages I/O).
|
|
798
|
+
*/
|
|
799
|
+
export function registerCategoryForDaemon(category: HookCategory, register: RegisterFn): void {
|
|
800
|
+
for (const hook of category.hooks) {
|
|
801
|
+
const hookName = `${category.name} ${hook.name}`;
|
|
802
|
+
const wrappedHandler = async (input: HookInput): Promise<void> => {
|
|
803
|
+
if (isHookDisabled(hookName)) {
|
|
804
|
+
process.exit(0);
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
await hook.handler(input);
|
|
808
|
+
} catch {
|
|
809
|
+
executeErrorFallback(hook.errorFallback, `${category.name}.${hook.name}`);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
register(category.name, hook.name, wrappedHandler);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|