freeharness 0.4.1 → 0.4.3
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/NOTICE +2 -1
- package/README.md +24 -19
- package/canonical/HARVEST.yaml +27 -16
- package/canonical/PROVENANCE.yaml +3 -3
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +14 -9
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +4155 -93
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1116 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +206 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +959 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +2 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +1 -1
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +10 -3
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3625 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +187 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +96 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +56 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4035 -327
- package/dist/adapters/harness/claude-code.d.ts +26 -3
- package/dist/adapters/harness/claude-code.js +214 -43
- package/dist/adapters/harness/opencode.js +17 -3
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +172 -33
- package/dist/adapters/memory/doctor.js +750 -139
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +15 -0
- package/dist/adapters/memory/paths.js +47 -0
- package/dist/adapters/memory/vendored.d.ts +52 -30
- package/dist/adapters/memory/vendored.js +161 -169
- package/dist/adapters/memory/worker-control.d.ts +9 -0
- package/dist/adapters/memory/worker-control.js +30 -0
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +204 -75
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +199 -45
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +29 -0
- package/dist/cli/install-facts.js +68 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/profile.d.ts +1 -1
- package/dist/cli/profile.js +10 -10
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +252 -73
- package/dist/infra/fsutil.d.ts +30 -3
- package/dist/infra/fsutil.js +68 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/registry.d.ts +1 -1
- package/node_modules/@freeharness/core/dist/registry.js +3 -3
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +3 -3
- package/provenance/memory.PROVENANCE.yaml +472 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
- package/canonical/openspec/config.base.yaml +0 -89
- package/canonical/rules/safety.md +0 -41
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { Capability } from "@freeharness/core";
|
|
1
|
+
import type { Capability, HarnessTarget } from "@freeharness/core";
|
|
2
2
|
import type { AdapterResult, InstallContext } from "@freeharness/core";
|
|
3
3
|
import type { HarnessPort } from "@freeharness/core";
|
|
4
4
|
export { ROUTING_FILE } from "../../infra/routing.js";
|
|
5
|
+
/** Writes the SDD skill's `reference/tiers.md` under `skillsDir`; nothing when the canonical skills lack the SDD skill. */
|
|
6
|
+
export declare function emitTierReference(canonicalDir: string, skillsDir: string, target: HarnessTarget, dryRun: boolean): string[];
|
|
5
7
|
/**
|
|
6
8
|
* Capabilities that live in the machine-global install (~/.claude). At project scope the
|
|
7
9
|
* adapter refuses them unless `allowProjectCopies` is set: Claude Code loads user and
|
|
@@ -12,10 +14,16 @@ export declare const GLOBAL_ONLY_CAPABILITIES: readonly string[];
|
|
|
12
14
|
export declare const FH_CONTEXT_COMMAND: string;
|
|
13
15
|
/** SessionStart matcher: compact/resume/fork get the re-orientation block, startup/clear the pointer. */
|
|
14
16
|
export declare const SESSION_START_MATCHER = "startup|clear|compact|resume|fork";
|
|
15
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Flags baked into the hook commands (the fragment defaults). `update` resets FreeHarness
|
|
19
|
+
* hook flags to these shipped values, so a flag tuned in settings.json does not survive it:
|
|
20
|
+
* keeping the tuned entry beside the shipped one would run the hook twice.
|
|
21
|
+
*/
|
|
16
22
|
export declare const OUTPUT_PRUNER_FLAGS = "--max-bytes 8192 --max-lines 200";
|
|
17
23
|
export declare const CONTEXT_MONITOR_FLAGS = "--budget 200000 --thresholds 60,85";
|
|
18
|
-
|
|
24
|
+
/** Config-guard allow-marker window in minutes; feeds both the hook flag and the install note. */
|
|
25
|
+
export declare const CONFIG_GUARD_TTL_MIN = 240;
|
|
26
|
+
export declare const CONFIG_GUARD_FLAGS = "--ttl 240";
|
|
19
27
|
export declare const REPEAT_TOOL_FLAGS = "--thresholds 3,5,8 --preview-chars 500";
|
|
20
28
|
/**
|
|
21
29
|
* Per-hook timeout (seconds) on SessionEnd entries. Claude Code gives SessionEnd hooks a
|
|
@@ -24,12 +32,27 @@ export declare const REPEAT_TOOL_FLAGS = "--thresholds 3,5,8 --preview-chars 500
|
|
|
24
32
|
* pushes past 1.5 s.
|
|
25
33
|
*/
|
|
26
34
|
export declare const SESSION_END_TIMEOUT_S = 10;
|
|
35
|
+
/** Status line quota tap, emitted with the cost capability; the owner wires it, never a settings hook. */
|
|
36
|
+
export declare const QUOTA_TAP_SCRIPT = "freeharness-quota-tap.mjs";
|
|
27
37
|
/** Every hook script FreeHarness emits (rules + safety + cost capabilities), for doctor and uninstall. */
|
|
28
38
|
export declare const HOOK_SCRIPTS: readonly string[];
|
|
29
39
|
export declare class ClaudeCodeAdapter implements HarnessPort {
|
|
30
40
|
readonly target: "claude-code";
|
|
41
|
+
private readonly autoMemoryMerged;
|
|
42
|
+
private turnOffAutoMemoryOnce;
|
|
31
43
|
apply(cap: Capability, ctx: InstallContext): Promise<AdapterResult>;
|
|
32
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Mirrors `freeMemEnabled` in `../../cli/install-facts.js`, duplicated rather than
|
|
47
|
+
* shared: adapters never import the cli layer (`scripts/check-arch.mjs`). Exported so a
|
|
48
|
+
* test can hold both copies to the same answers.
|
|
49
|
+
*/
|
|
50
|
+
export declare function freeMemEnabled(claudeDir: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* free-mem is the only memory owner; Claude Code auto memory (and agent memory with it) would
|
|
53
|
+
* keep a second store nothing recalls. Added only when absent, so an owner's explicit value wins.
|
|
54
|
+
*/
|
|
55
|
+
export declare function turnOffAutoMemory(settingsPath: string, dry: boolean, res: AdapterResult): void;
|
|
33
56
|
/**
|
|
34
57
|
* Reports and, only once confirmed, strips a trailing `[1m]` suffix from the scope's own
|
|
35
58
|
* `model` value. The emitted defaults never force or replace a model family: a value with no
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { copyDir, copyExecutable, ensureDir, mergeClaudeHook, mergeSettingsKeys, readText, writeText, writeTextIfAbsent } from "../../infra/fsutil.js";
|
|
6
|
+
import { mergePriceTable } from "../../infra/prices.js";
|
|
5
7
|
import { AGENT_MEMORY_GITIGNORE_LINE, AGENT_MEMORY_LOCAL_GITIGNORE_LINE, SDD_DIR, SDD_GITIGNORE_LINE, ensureGitignoreLine, ensureSddDir, } from "../../infra/dogfood.js";
|
|
6
|
-
import { rewriteAgentModel } from "@freeharness/core";
|
|
8
|
+
import { mapModel, rewriteAgentModel } from "@freeharness/core";
|
|
7
9
|
import { initProject } from "@freeharness/sdd/init";
|
|
8
|
-
import { VendoredMemory, vendoredMemoryEngineDir } from "../memory/vendored.js";
|
|
10
|
+
import { PLUGIN_KEY, VendoredMemory, vendoredMemoryEngineDir } from "../memory/vendored.js";
|
|
9
11
|
import { vendoredSddBin } from "../../infra/paths.js";
|
|
10
12
|
import { ROUTING_FILE, routingSourcePath } from "../../infra/routing.js";
|
|
11
13
|
// Re-exported: callers keep importing the routing file name from the claude-code adapter.
|
|
12
14
|
export { ROUTING_FILE } from "../../infra/routing.js";
|
|
15
|
+
const SDD_SKILL = "subagent-driven-development";
|
|
16
|
+
/**
|
|
17
|
+
* The tier -> model table the SDD skill resolves a brief's `Tier:` through, rendered for one target.
|
|
18
|
+
* The use column repeats the implementer dispatch rule's classes, since the agents README that
|
|
19
|
+
* also states them is never emitted and this file is what a dispatching lead reads.
|
|
20
|
+
*/
|
|
21
|
+
function tierReference(target) {
|
|
22
|
+
const rows = [
|
|
23
|
+
["strong", "security and complex (Effort: high), code, any other class or none, every class below"],
|
|
24
|
+
["standard", "docs, extraction, exploration, mechanical, summary, code (Route: C1-C7)"],
|
|
25
|
+
["fast", "extraction, summary (reading set under 100,000 tokens)"],
|
|
26
|
+
["frontier", "agentic-architect role (per dispatch)"],
|
|
27
|
+
].map(([tier, use]) => `| ${tier} | ${mapModel(target, tier)} | ${use} |`);
|
|
28
|
+
const note = target === "opencode" ? "\nA `${OPENCODE_*_MODEL}` placeholder is the provider model id the environment sets.\n" : "";
|
|
29
|
+
return ["# Tier map", "", "The per-dispatch model for a brief's `Tier:`.", "", "| Tier | Model | Use |", "|------|-------|-----|", ...rows, note].join("\n");
|
|
30
|
+
}
|
|
31
|
+
/** Writes the SDD skill's `reference/tiers.md` under `skillsDir`; nothing when the canonical skills lack the SDD skill. */
|
|
32
|
+
export function emitTierReference(canonicalDir, skillsDir, target, dryRun) {
|
|
33
|
+
if (!fs.existsSync(path.join(canonicalDir, "skills", SDD_SKILL, "SKILL.md")))
|
|
34
|
+
return [];
|
|
35
|
+
return [writeText(path.join(skillsDir, SDD_SKILL, "reference", "tiers.md"), tierReference(target), dryRun)];
|
|
36
|
+
}
|
|
13
37
|
/**
|
|
14
38
|
* Capabilities that live in the machine-global install (~/.claude). At project scope the
|
|
15
39
|
* adapter refuses them unless `allowProjectCopies` is set: Claude Code loads user and
|
|
@@ -20,10 +44,16 @@ export const GLOBAL_ONLY_CAPABILITIES = ["skills", "agents", "safety", "cost", "
|
|
|
20
44
|
export const FH_CONTEXT_COMMAND = path.join(".claude", "commands", "fh-context.md");
|
|
21
45
|
/** SessionStart matcher: compact/resume/fork get the re-orientation block, startup/clear the pointer. */
|
|
22
46
|
export const SESSION_START_MATCHER = "startup|clear|compact|resume|fork";
|
|
23
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Flags baked into the hook commands (the fragment defaults). `update` resets FreeHarness
|
|
49
|
+
* hook flags to these shipped values, so a flag tuned in settings.json does not survive it:
|
|
50
|
+
* keeping the tuned entry beside the shipped one would run the hook twice.
|
|
51
|
+
*/
|
|
24
52
|
export const OUTPUT_PRUNER_FLAGS = "--max-bytes 8192 --max-lines 200";
|
|
25
53
|
export const CONTEXT_MONITOR_FLAGS = "--budget 200000 --thresholds 60,85";
|
|
26
|
-
|
|
54
|
+
/** Config-guard allow-marker window in minutes; feeds both the hook flag and the install note. */
|
|
55
|
+
export const CONFIG_GUARD_TTL_MIN = 240;
|
|
56
|
+
export const CONFIG_GUARD_FLAGS = `--ttl ${CONFIG_GUARD_TTL_MIN}`;
|
|
27
57
|
export const REPEAT_TOOL_FLAGS = "--thresholds 3,5,8 --preview-chars 500";
|
|
28
58
|
/**
|
|
29
59
|
* Per-hook timeout (seconds) on SessionEnd entries. Claude Code gives SessionEnd hooks a
|
|
@@ -32,6 +62,8 @@ export const REPEAT_TOOL_FLAGS = "--thresholds 3,5,8 --preview-chars 500";
|
|
|
32
62
|
* pushes past 1.5 s.
|
|
33
63
|
*/
|
|
34
64
|
export const SESSION_END_TIMEOUT_S = 10;
|
|
65
|
+
/** Status line quota tap, emitted with the cost capability; the owner wires it, never a settings hook. */
|
|
66
|
+
export const QUOTA_TAP_SCRIPT = "freeharness-quota-tap.mjs";
|
|
35
67
|
/** Every hook script FreeHarness emits (rules + safety + cost capabilities), for doctor and uninstall. */
|
|
36
68
|
export const HOOK_SCRIPTS = [
|
|
37
69
|
"freeharness-session-start",
|
|
@@ -43,11 +75,23 @@ export const HOOK_SCRIPTS = [
|
|
|
43
75
|
"freeharness-output-pruner.mjs",
|
|
44
76
|
"freeharness-context-monitor.mjs",
|
|
45
77
|
"freeharness-precompact.mjs",
|
|
46
|
-
"freeharness-
|
|
78
|
+
"freeharness-reload-context.mjs",
|
|
47
79
|
"freeharness-converge.mjs",
|
|
80
|
+
"freeharness-sdd-context.mjs",
|
|
81
|
+
// Doctor warns about a missing tap only when settings.json names it; wrapper wiring is not seen.
|
|
82
|
+
QUOTA_TAP_SCRIPT,
|
|
48
83
|
];
|
|
49
84
|
export class ClaudeCodeAdapter {
|
|
50
85
|
target = "claude-code";
|
|
86
|
+
// The runner keeps one adapter per pass, so a later capability case sees the key this pass
|
|
87
|
+
// already merged and would report it as the owner's value.
|
|
88
|
+
autoMemoryMerged = new Set();
|
|
89
|
+
turnOffAutoMemoryOnce(settingsPath, dry, res) {
|
|
90
|
+
if (this.autoMemoryMerged.has(settingsPath))
|
|
91
|
+
return;
|
|
92
|
+
this.autoMemoryMerged.add(settingsPath);
|
|
93
|
+
turnOffAutoMemory(settingsPath, dry, res);
|
|
94
|
+
}
|
|
51
95
|
async apply(cap, ctx) {
|
|
52
96
|
const res = { capabilityId: cap.id, target: this.target, written: [], actions: [], notes: [] };
|
|
53
97
|
const C = ctx.canonicalDir;
|
|
@@ -69,9 +113,6 @@ export class ClaudeCodeAdapter {
|
|
|
69
113
|
const hooksDir = user
|
|
70
114
|
? path.join(home, ".claude", "freeharness", "hooks")
|
|
71
115
|
: path.join(ctx.projectRoot, ".freeharness", "hooks");
|
|
72
|
-
const safetyFile = user
|
|
73
|
-
? path.join(home, ".claude", "freeharness", "safety.md")
|
|
74
|
-
: path.join(ctx.projectRoot, ".freeharness", "safety.md");
|
|
75
116
|
const ssScript = path.join(hooksDir, "freeharness-session-start");
|
|
76
117
|
const mgScript = path.join(hooksDir, "freeharness-memory-guard.mjs");
|
|
77
118
|
// user scope bakes an absolute path; project scope uses ${CLAUDE_PROJECT_DIR}.
|
|
@@ -89,15 +130,38 @@ export class ClaudeCodeAdapter {
|
|
|
89
130
|
? `node "${ctScript}"`
|
|
90
131
|
: 'node "${CLAUDE_PROJECT_DIR}/.freeharness/hooks/freeharness-cost-tracker.mjs"';
|
|
91
132
|
const hookCmd = (script, flags) => `${user ? `node "${path.join(hooksDir, script)}"` : `node "\${CLAUDE_PROJECT_DIR}/.freeharness/hooks/${script}"`}${flags ? ` ${flags}` : ""}`;
|
|
92
|
-
const
|
|
133
|
+
const mergeOwned = (event, entry, ownerKey) => ({ event, r: mergeClaudeHook(settingsPath, event, entry, dry, ownerKey) });
|
|
134
|
+
const mainNote = (script, cmd, merged, addedNote, presentNote, guard) => {
|
|
135
|
+
const replaced = merged.flatMap((m) => (m.r.outcome === "replaced" ? [m.r] : []));
|
|
136
|
+
const hasAdded = merged.some((m) => m.r.outcome === "added");
|
|
137
|
+
if (replaced.length > 0) {
|
|
138
|
+
const stale = replaced.find((r) => r.replacedCommand !== undefined)?.replacedCommand;
|
|
139
|
+
const updatedNote = stale !== undefined
|
|
140
|
+
? `Updated ${script} hook (flags changed: ${stale} -> ${cmd}). Update resets FreeHarness hook flags to the shipped values.`
|
|
141
|
+
: replaced.some((r) => r.onFailureChanged)
|
|
142
|
+
? `Updated ${script} hook (onFailure reset to the shipped value).`
|
|
143
|
+
: `Updated ${script} hook (collapsed duplicate ${guard ? "guard" : "hook"} entries).`;
|
|
144
|
+
if (hasAdded)
|
|
145
|
+
return [updatedNote, addedNote];
|
|
146
|
+
return [updatedNote];
|
|
147
|
+
}
|
|
148
|
+
if (hasAdded)
|
|
149
|
+
return [addedNote];
|
|
150
|
+
return [presentNote];
|
|
151
|
+
};
|
|
152
|
+
// A shared entry is never rewritten, so the owner removes that copy by hand.
|
|
153
|
+
const hookNotes = (script, cmd, merged, addedNote, presentNote, guard = false) => {
|
|
154
|
+
const shared = merged.filter((m) => m.r.hasSharedCopy).map((m) => `${script} also runs inside a hook entry shared with other commands in ${m.event}; remove it there by hand.`);
|
|
155
|
+
return [...mainNote(script, cmd, merged, addedNote, presentNote, guard), ...shared];
|
|
156
|
+
};
|
|
157
|
+
const emitHook = (script, flags, events, note, guard = false) => {
|
|
93
158
|
res.written.push(writeText(path.join(hooksDir, script), readText(path.join(C, "hooks", script)), dry));
|
|
94
159
|
const cmd = hookCmd(script, flags);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
res.notes.push(added ? `Added ${note}.` : `${script} hook already present.`);
|
|
160
|
+
const merged = events.map((e) => {
|
|
161
|
+
const hook = { type: "command", command: cmd, ...(e.event === "SessionEnd" || e.event === "SubagentStop" ? { timeout: SESSION_END_TIMEOUT_S } : {}) };
|
|
162
|
+
return mergeOwned(e.event, { ...(e.matcher ? { matcher: e.matcher } : {}), hooks: [hook] }, hookCmd(script, ""));
|
|
163
|
+
});
|
|
164
|
+
res.notes.push(...hookNotes(script, cmd, merged, `Added ${note}.`, `${script} hook already present.`, guard));
|
|
101
165
|
};
|
|
102
166
|
switch (cap.id) {
|
|
103
167
|
case "rules": {
|
|
@@ -127,18 +191,23 @@ export class ClaudeCodeAdapter {
|
|
|
127
191
|
}
|
|
128
192
|
res.written.push(copyExecutable(path.join(C, "hooks", "freeharness-session-start"), ssScript, dry));
|
|
129
193
|
const ssEntry = { matcher: SESSION_START_MATCHER, hooks: [{ type: "command", command: ssCmd, async: false }] };
|
|
130
|
-
|
|
131
|
-
|
|
194
|
+
// The session-start script runs without `node`, so its quoted path alone is the owner key.
|
|
195
|
+
const ss = mergeOwned("SessionStart", ssEntry, ssCmd);
|
|
196
|
+
res.notes.push(...hookNotes("freeharness-session-start", ssCmd, [ss], "Added SessionStart pointer hook (additive; memory plugin hooks untouched; compact/resume re-inject the checkpoint).", "SessionStart hook already present."));
|
|
132
197
|
// Routing table beside the script the hook reads it from.
|
|
133
198
|
res.written.push(writeText(path.join(hooksDir, ROUTING_FILE), readText(routingSourcePath(C)), dry));
|
|
134
199
|
res.written.push(writeText(mgScript, readText(path.join(C, "hooks", "freeharness-memory-guard.mjs")), dry));
|
|
200
|
+
// The guards sit under the permission layer, so a guard that cannot run allows the call.
|
|
135
201
|
const mgEntry = { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: mgCmd }] };
|
|
136
|
-
const mg =
|
|
137
|
-
res.notes.push(
|
|
138
|
-
emitHook("freeharness-config-guard.mjs", CONFIG_GUARD_FLAGS, [{ event: "PreToolUse", matcher: "Edit|Write|MultiEdit|Bash" }],
|
|
202
|
+
const mg = mergeOwned("PreToolUse", mgEntry, mgCmd);
|
|
203
|
+
res.notes.push(...hookNotes("freeharness-memory-guard.mjs", mgCmd, [mg], "Added memory-guard hook (PreToolUse denies memory .md writes; saves tokens).", "Memory-guard hook already present.", true));
|
|
204
|
+
emitHook("freeharness-config-guard.mjs", CONFIG_GUARD_FLAGS, [{ event: "PreToolUse", matcher: "Edit|Write|MultiEdit|Bash" }], `config-guard hook (PreToolUse denies edits to live configuration by location: settings in a .claude directory, .mcp.json, the user .claude.json and CLAUDE.md, hook directories and the plugin tree, unless an allow marker from the last ${CONFIG_GUARD_TTL_MIN} minutes is present)`, true);
|
|
139
205
|
emitHook("freeharness-repeat-tool.mjs", REPEAT_TOOL_FLAGS, [{ event: "PreToolUse" }, { event: "UserPromptSubmit" }], "repeat-tool hook (PreToolUse reminds at 3/5/8 identical calls; UserPromptSubmit resets; advisory)");
|
|
140
206
|
// Memory-independent: the skeleton is built locally; the worker summary is best effort.
|
|
141
|
-
emitHook("freeharness-precompact.mjs", "", [{ event: "PreCompact" }], "precompact hook (PreCompact writes the checkpoint skeleton + file-op list for SessionStart and asks a running
|
|
207
|
+
emitHook("freeharness-precompact.mjs", "", [{ event: "PreCompact" }], "precompact hook (PreCompact writes the checkpoint skeleton + file-op list for SessionStart and asks a running memory worker for a summary; worker down = skipped)");
|
|
208
|
+
emitHook("freeharness-reload-context.mjs", "", [{ event: "UserPromptSubmit" }], "reload-context hook (UserPromptSubmit answers the exact prompt `reload context` with the newest saved handoff or PreCompact skeleton; other prompts untouched)");
|
|
209
|
+
if (user && freeMemEnabled(claudeDir))
|
|
210
|
+
this.turnOffAutoMemoryOnce(settingsPath, dry, res);
|
|
142
211
|
break;
|
|
143
212
|
}
|
|
144
213
|
case "skills": {
|
|
@@ -146,6 +215,7 @@ export class ClaudeCodeAdapter {
|
|
|
146
215
|
transform: (c) => rewriteAgentModel(c, this.target),
|
|
147
216
|
transformOnly: (n) => n === "SKILL.md",
|
|
148
217
|
}));
|
|
218
|
+
res.written.push(...emitTierReference(C, skillsDir, this.target, dry));
|
|
149
219
|
const rfScripts = path.join(skillsDir, "report-formatting", "scripts");
|
|
150
220
|
res.actions.push({
|
|
151
221
|
description: "Install report-formatting .docx deps (docx, image-size)",
|
|
@@ -168,13 +238,12 @@ export class ClaudeCodeAdapter {
|
|
|
168
238
|
}));
|
|
169
239
|
break;
|
|
170
240
|
case "safety": {
|
|
171
|
-
res.written.push(writeText(safetyFile, readText(path.join(C, "rules", "safety.md")), dry));
|
|
172
241
|
// Advisory PreToolUse hook: warns on vulnerability sinks in Edit/Write content,
|
|
173
242
|
// never blocks. Same additive merge as the memory guard.
|
|
174
243
|
res.written.push(writeText(sgScript, readText(path.join(C, "hooks", "freeharness-security-guidance.mjs")), dry));
|
|
175
244
|
const sgEntry = { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: sgCmd }] };
|
|
176
|
-
const sg =
|
|
177
|
-
res.notes.push(
|
|
245
|
+
const sg = mergeOwned("PreToolUse", sgEntry, sgCmd);
|
|
246
|
+
res.notes.push(...hookNotes("freeharness-security-guidance.mjs", sgCmd, [sg], "Added security-guidance hook (PreToolUse warns on vulnerability sinks; advisory only).", "Security-guidance hook already present."));
|
|
178
247
|
break;
|
|
179
248
|
}
|
|
180
249
|
case "sdd": {
|
|
@@ -226,14 +295,9 @@ export class ClaudeCodeAdapter {
|
|
|
226
295
|
break;
|
|
227
296
|
}
|
|
228
297
|
case "memory": {
|
|
229
|
-
// Optional tier-0 spike, off by default (FREEHARNESS_AUTO_MEMORY=on); registered even
|
|
230
|
-
// when the vendored engine is absent because it only reads the dogfood log.
|
|
231
|
-
emitHook("freeharness-auto-memory.mjs", "", [{ event: "SessionEnd" }], "auto-memory hook (SessionEnd curates one line per project into ~/.claude/freeharness/MEMORY.md; OFF until FREEHARNESS_AUTO_MEMORY=on)");
|
|
232
298
|
const engineDir = vendoredMemoryEngineDir(C);
|
|
233
299
|
if (!engineDir) {
|
|
234
|
-
|
|
235
|
-
res.actions.push({ description: "Install claude-mem plugin + wire hooks/MCP (bridge: vendored engine absent)", command: "npx claude-mem install", optional: true });
|
|
236
|
-
res.notes.push("Vendored engine not found; bridging to upstream `npx claude-mem install`.");
|
|
300
|
+
res.notes.push("Vendored engine not found; the memory capability is skipped.");
|
|
237
301
|
break;
|
|
238
302
|
}
|
|
239
303
|
// Real plugin install from OUR build (MemoryPort). Dry-run previews every step
|
|
@@ -253,6 +317,9 @@ export class ClaudeCodeAdapter {
|
|
|
253
317
|
merge(await mem.migrateExisting(ctx));
|
|
254
318
|
merge(await mem.install(ctx.scope, ctx));
|
|
255
319
|
merge(await mem.registerHooks(ctx.scope, ctx));
|
|
320
|
+
// Registering enables free-mem, so the key follows in the same run; pass 1 previews it.
|
|
321
|
+
if (user)
|
|
322
|
+
this.turnOffAutoMemoryOnce(settingsPath, dry, res);
|
|
256
323
|
res.notes.push("Worker start + recall are verified on the machine (needs the Claude Agent SDK key / network); not exercised by the installer.");
|
|
257
324
|
break;
|
|
258
325
|
}
|
|
@@ -264,29 +331,30 @@ export class ClaudeCodeAdapter {
|
|
|
264
331
|
continue;
|
|
265
332
|
res.actions.push({
|
|
266
333
|
description: `Register MCP server ${s.name}${s.optional ? " (optional)" : ""}`,
|
|
267
|
-
|
|
334
|
+
// `claude mcp add` exits 1 for a registered name; the get guard keeps a re-run quiet.
|
|
335
|
+
command: `claude mcp get ${s.name} >/dev/null 2>&1 || claude mcp add ${s.name} -s ${sc} -- ${s.command} ${s.args.join(" ")}`,
|
|
268
336
|
optional: true, // a failing/missing `claude` must not abort the whole install
|
|
269
337
|
});
|
|
270
338
|
}
|
|
271
339
|
break;
|
|
272
340
|
}
|
|
273
341
|
case "cost": {
|
|
274
|
-
// Stop + SessionEnd append usage snapshots to cost/usage.jsonl; the price table is
|
|
275
|
-
// seeded once and never overwritten (the user edits it to the account's contract).
|
|
276
342
|
res.written.push(writeText(ctScript, readText(path.join(C, "hooks", "freeharness-cost-tracker.mjs")), dry));
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
(res.skipped ??= []).push(prices.path);
|
|
281
|
-
const stop = mergeClaudeHook(settingsPath, "Stop", { hooks: [{ type: "command", command: ctCmd }] }, dry);
|
|
282
|
-
const end = mergeClaudeHook(settingsPath, "SessionEnd", { hooks: [{ type: "command", command: ctCmd, timeout: SESSION_END_TIMEOUT_S }] }, dry);
|
|
343
|
+
applyPriceTable(ctPrices, readText(path.join(C, "hooks", "cost-prices.json")), dry, res);
|
|
344
|
+
const stop = mergeOwned("Stop", { hooks: [{ type: "command", command: ctCmd }] }, ctCmd);
|
|
345
|
+
const end = mergeOwned("SessionEnd", { hooks: [{ type: "command", command: ctCmd, timeout: SESSION_END_TIMEOUT_S }] }, ctCmd);
|
|
283
346
|
// SubagentStop mirrors SessionEnd: a long dispatch also pushes the subagent's own
|
|
284
347
|
// transcript past the default budget.
|
|
285
|
-
const subagentStop =
|
|
286
|
-
res.notes.push(
|
|
287
|
-
res.notes.push(prices.skipped ? `Price table kept as is: ${ctPrices} (edit to your contract).` : `Price table seeded at ${ctPrices} — placeholders; edit to your contract. Report: freeharness cost report.`);
|
|
348
|
+
const subagentStop = mergeOwned("SubagentStop", { hooks: [{ type: "command", command: ctCmd, timeout: SESSION_END_TIMEOUT_S }] }, ctCmd);
|
|
349
|
+
res.notes.push(...hookNotes("freeharness-cost-tracker.mjs", ctCmd, [stop, end, subagentStop], "Added cost-tracker hooks (Stop + SessionEnd + SubagentStop append usage/ledger entries; never block).", "Cost-tracker hooks already present."));
|
|
288
350
|
emitHook("freeharness-output-pruner.mjs", OUTPUT_PRUNER_FLAGS, [{ event: "PostToolUse", matcher: "Bash" }], "output-pruner hook (PostToolUse trims Bash output over 8 KB / 200 lines; full copy under ~/.claude/freeharness/tool-output/)");
|
|
289
|
-
emitHook("freeharness-context-monitor.mjs", CONTEXT_MONITOR_FLAGS, [{ event: "Stop" }], "context-monitor hook (Stop warns the user once at 60% and 85% of the context budget)");
|
|
351
|
+
emitHook("freeharness-context-monitor.mjs", CONTEXT_MONITOR_FLAGS, [{ event: "Stop" }], "context-monitor hook (Stop warns the user once at 60% and 85% of the context budget, writes a handoff at the highest threshold and suggests /clear)");
|
|
352
|
+
// Emitted beside the tracker, whose brief-path rule it imports from its own directory.
|
|
353
|
+
emitHook("freeharness-sdd-context.mjs", "", [{ event: "SubagentStart" }, { event: "SubagentStop" }], "sdd-context hook (SubagentStart gives every subagent a free-mem project brief; SubagentStop saves an implementer's checkpoint to the task state and leaves the report, tagged with its change and task, with the worker; FREEHARNESS_SDD_CONTEXT_WORKER=off turns the worker calls off)");
|
|
354
|
+
// The status line feeds the tap, not a hook event: no settings entry, and statusLine
|
|
355
|
+
// stays the owner's to wire.
|
|
356
|
+
res.written.push(writeText(path.join(hooksDir, QUOTA_TAP_SCRIPT), readText(path.join(C, "hooks", QUOTA_TAP_SCRIPT)), dry));
|
|
357
|
+
res.notes.push(`${QUOTA_TAP_SCRIPT} emitted; pipe your status line input to it by hand to record quota readings (Documents/ADVANCED.md, Quota tap). FreeHarness never writes statusLine.`);
|
|
290
358
|
// Cache-hygiene settings: additive, existing keys win.
|
|
291
359
|
const tpl = JSON.parse(readText(path.join(C, "settings", "settings.cache-hygiene.claude-code.json")));
|
|
292
360
|
const merged = mergeSettingsKeys(settingsPath, tpl, dry);
|
|
@@ -302,6 +370,109 @@ export class ClaudeCodeAdapter {
|
|
|
302
370
|
return res;
|
|
303
371
|
}
|
|
304
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Mirrors `freeMemEnabled` in `../../cli/install-facts.js`, duplicated rather than
|
|
375
|
+
* shared: adapters never import the cli layer (`scripts/check-arch.mjs`). Exported so a
|
|
376
|
+
* test can hold both copies to the same answers.
|
|
377
|
+
*/
|
|
378
|
+
export function freeMemEnabled(claudeDir) {
|
|
379
|
+
let settings;
|
|
380
|
+
try {
|
|
381
|
+
settings = JSON.parse(fs.readFileSync(path.join(claudeDir, "settings.json"), "utf8"));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
return settings?.enabledPlugins?.[PLUGIN_KEY] === true;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* free-mem is the only memory owner; Claude Code auto memory (and agent memory with it) would
|
|
390
|
+
* keep a second store nothing recalls. Added only when absent, so an owner's explicit value wins.
|
|
391
|
+
*/
|
|
392
|
+
export function turnOffAutoMemory(settingsPath, dry, res) {
|
|
393
|
+
let merged;
|
|
394
|
+
try {
|
|
395
|
+
merged = mergeSettingsKeys(settingsPath, { autoMemoryEnabled: false }, dry);
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
const reason = e instanceof SyntaxError ? "not parsed" : "not updated";
|
|
399
|
+
res.notes.push(`autoMemoryEnabled not set: ${settingsPath} ${reason} (${e instanceof Error ? e.message : String(e)}).`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (merged.added.length > 0) {
|
|
403
|
+
res.notes.push(`${dry ? "Would add" : "Added"} autoMemoryEnabled = false to ${settingsPath} (free-mem is the only memory; Claude Code auto memory and agent memory off).`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
res.notes.push(`autoMemoryEnabled already set in ${settingsPath}; left as the owner set it.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Seeds the price table once, then merges missing rows and previously shipped values into it on later runs. */
|
|
410
|
+
function applyPriceTable(file, shippedText, dry, res) {
|
|
411
|
+
if (!fs.existsSync(file)) {
|
|
412
|
+
const seeded = writeTextIfAbsent(file, shippedText, dry);
|
|
413
|
+
res.written.push(seeded.path);
|
|
414
|
+
res.notes.push(`Price table seeded at ${file} — placeholders; edit to your contract. Report: freeharness cost report.`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
res.written.push(file);
|
|
418
|
+
// An unreadable table is left as is.
|
|
419
|
+
let installedText;
|
|
420
|
+
try {
|
|
421
|
+
installedText = readText(file);
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
(res.skipped ??= []).push(file);
|
|
425
|
+
res.notes.push(`Price table left as is (${e instanceof Error ? e.message : String(e)}): ${file}.`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const merge = mergePriceTable(installedText, shippedText);
|
|
429
|
+
if ("warning" in merge) {
|
|
430
|
+
(res.skipped ??= []).push(file);
|
|
431
|
+
res.notes.push(`Price table left as is (${merge.warning}): ${file}.`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (merge.added.length === 0 && merge.corrected.length === 0) {
|
|
435
|
+
(res.skipped ??= []).push(file);
|
|
436
|
+
res.notes.push(`Price table kept as is: ${file} (edit to your contract).`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Pass-2 notes never print; one wording covers both.
|
|
440
|
+
const changes = [
|
|
441
|
+
...(merge.added.length > 0 ? [`adds rows ${merge.added.join(", ")}`] : []),
|
|
442
|
+
...(merge.corrected.length > 0 ? [`corrects ${merge.corrected.length} previously shipped value(s)`] : []),
|
|
443
|
+
];
|
|
444
|
+
res.notes.push(`Price table ${file}: ${changes.join("; ")}.`);
|
|
445
|
+
for (const key of merge.added)
|
|
446
|
+
res.notes.push(`add ${key}`);
|
|
447
|
+
for (const c of merge.corrected) {
|
|
448
|
+
res.notes.push(`correct ${c.row}.${c.field} ${JSON.stringify(c.from)} -> ${JSON.stringify(c.to)}`);
|
|
449
|
+
}
|
|
450
|
+
if (dry) {
|
|
451
|
+
(res.skipped ??= []).push(file);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// Merge into the symlink's real path so its link survives.
|
|
455
|
+
let target = file;
|
|
456
|
+
try {
|
|
457
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
458
|
+
target = fs.realpathSync(file);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
(res.skipped ??= []).push(file);
|
|
462
|
+
res.notes.push(`Price table left as is (symlink target unreadable: ${e instanceof Error ? e.message : String(e)}): ${file}.`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Random suffix + "wx" refuse a symlinked temp path.
|
|
466
|
+
const tmp = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
467
|
+
try {
|
|
468
|
+
fs.writeFileSync(tmp, merge.text, { encoding: "utf8", flag: "wx" });
|
|
469
|
+
fs.renameSync(tmp, target);
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
fs.rmSync(tmp, { force: true });
|
|
473
|
+
throw e;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
305
476
|
/**
|
|
306
477
|
* Splits a model id on a trailing `[suffix]` tag (e.g. `claude-fable-5-1[1m]`). Mirrors
|
|
307
478
|
* `splitModelSuffix` in `../../cli/doctor.js`, duplicated here rather than shared: adapters
|
|
@@ -6,7 +6,7 @@ import { routingSourcePath } from "../../infra/routing.js";
|
|
|
6
6
|
import { frontmatterHasKey, rewriteAgentModel, stripAgentKeys } from "@freeharness/core";
|
|
7
7
|
import { initProject } from "@freeharness/sdd/init";
|
|
8
8
|
import { vendoredSddBin } from "../../infra/paths.js";
|
|
9
|
-
import { seedProjectEvidence } from "./claude-code.js";
|
|
9
|
+
import { emitTierReference, seedProjectEvidence } from "./claude-code.js";
|
|
10
10
|
/**
|
|
11
11
|
* Whether any canonical agent (excluding README.md) carries a `memory:` front matter key.
|
|
12
12
|
* Reads the source tree directly, independent of `dryRun`, so a preview names the same
|
|
@@ -36,6 +36,19 @@ function anyAgentCarriesMemoryKey(agentsSrc) {
|
|
|
36
36
|
};
|
|
37
37
|
return walk(agentsSrc);
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Drops the free-mem MCP tool ids from an agent's `tools:` front matter line. OpenCode runs no
|
|
41
|
+
* free-mem MCP server, so an agent naming those tools would list tools it can never call; the
|
|
42
|
+
* line's other entries keep their order.
|
|
43
|
+
* @param content The agent file text.
|
|
44
|
+
* @returns The text with every `mcp__plugin_free-mem_` entry removed from the first `tools:` line.
|
|
45
|
+
*/
|
|
46
|
+
function dropFreeMemTools(content) {
|
|
47
|
+
return content.replace(/^tools:[ \t]*(.*)$/m, (_line, list) => {
|
|
48
|
+
const kept = list.split(",").map((t) => t.trim()).filter((t) => t && !t.startsWith("mcp__plugin_free-mem_"));
|
|
49
|
+
return `tools: ${kept.join(", ")}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
39
52
|
// OpenCode adapter: emits AGENTS.md + best-effort skill/agent layouts; no session-start hook (OpenCode has no hook contract we emit to).
|
|
40
53
|
export class OpenCodeAdapter {
|
|
41
54
|
target = "opencode";
|
|
@@ -65,6 +78,7 @@ export class OpenCodeAdapter {
|
|
|
65
78
|
transform: (c) => stripAgentKeys(c, ["disable-model-invocation", "context", "agent", "model"]),
|
|
66
79
|
transformOnly: (n) => n === "SKILL.md",
|
|
67
80
|
}));
|
|
81
|
+
res.written.push(...emitTierReference(C, path.join(P, ".opencode", "skills"), this.target, dry));
|
|
68
82
|
res.notes.push("Skills emitted to .opencode/skills (verify against your OpenCode version).");
|
|
69
83
|
const rfScripts = path.join(P, ".opencode", "skills", "report-formatting", "scripts");
|
|
70
84
|
res.actions.push({
|
|
@@ -86,7 +100,7 @@ export class OpenCodeAdapter {
|
|
|
86
100
|
const droppedMemoryKey = anyAgentCarriesMemoryKey(agentsSrc);
|
|
87
101
|
res.written.push(...copyDir(agentsSrc, path.join(P, ".opencode", "agent"), dry, {
|
|
88
102
|
skip: (name) => name === "README.md",
|
|
89
|
-
transform: (content) => stripAgentKeys(rewriteAgentModel(content, this.target), ["memory"]),
|
|
103
|
+
transform: (content) => dropFreeMemTools(stripAgentKeys(rewriteAgentModel(content, this.target), ["memory", "effort", "omitClaudeMd", "cache_ttl"])),
|
|
90
104
|
}));
|
|
91
105
|
res.notes.push("Agent model tiers mapped to ${OPENCODE_*_MODEL} placeholders; fill them in.");
|
|
92
106
|
if (droppedMemoryKey)
|
|
@@ -94,7 +108,7 @@ export class OpenCodeAdapter {
|
|
|
94
108
|
break;
|
|
95
109
|
}
|
|
96
110
|
case "safety":
|
|
97
|
-
res.
|
|
111
|
+
res.notes.push("safety: OpenCode has no hook contract FreeHarness emits to; nothing emitted.");
|
|
98
112
|
break;
|
|
99
113
|
case "sdd": {
|
|
100
114
|
const src = path.join(C, "openspec");
|
|
@@ -9,36 +9,44 @@ import { rewriteAgentModel } from "@freeharness/core";
|
|
|
9
9
|
import { copyDir, copyExecutable, readText, writeText } from "../../infra/fsutil.js";
|
|
10
10
|
import { cliVersion } from "../../infra/paths.js";
|
|
11
11
|
import { ROUTING_FILE, routingSourcePath } from "../../infra/routing.js";
|
|
12
|
-
import { CONFIG_GUARD_FLAGS, CONTEXT_MONITOR_FLAGS, OUTPUT_PRUNER_FLAGS, REPEAT_TOOL_FLAGS, SESSION_END_TIMEOUT_S, SESSION_START_MATCHER } from "./claude-code.js";
|
|
12
|
+
import { CONFIG_GUARD_FLAGS, CONTEXT_MONITOR_FLAGS, OUTPUT_PRUNER_FLAGS, REPEAT_TOOL_FLAGS, SESSION_END_TIMEOUT_S, SESSION_START_MATCHER, emitTierReference } from "./claude-code.js";
|
|
13
13
|
/** Plugin directory name inside the project root. */
|
|
14
14
|
export const PLUGIN_DIR_NAME = "freeharness-plugin";
|
|
15
15
|
/** Marketplace name (the `@marketplace` suffix in `claude plugin install freeharness@...`). */
|
|
16
16
|
export const MARKETPLACE_NAME = "freeharness-local";
|
|
17
17
|
/**
|
|
18
18
|
* Additively merges a hook entry into the plugin's hooks.json (dedupe by command; the file is
|
|
19
|
-
* created with the official `description` + `hooks` shape).
|
|
19
|
+
* created with the official `description` + `hooks` shape). Entries running only this command
|
|
20
|
+
* fold into the first one, rewritten to the current entry, so a re-emit drops a stale `onFailure`
|
|
21
|
+
* field and a stale copy never keeps running beside a current one; an entry mixing it with other
|
|
22
|
+
* commands is left as is.
|
|
20
23
|
* @param file Absolute path of hooks/hooks.json.
|
|
21
24
|
* @param event Hook event name (SessionStart, PreToolUse, ...).
|
|
22
25
|
* @param entry Entry to add.
|
|
23
26
|
* @param dryRun When true nothing is written.
|
|
24
|
-
* @returns Whether the
|
|
27
|
+
* @returns Whether the file changed (false when the entry was already current).
|
|
25
28
|
*/
|
|
26
29
|
function mergePluginHook(file, event, entry, dryRun) {
|
|
27
30
|
let doc = {
|
|
28
|
-
description: "FreeHarness hooks: SessionStart pointer (+ post-compaction re-orientation), PreCompact checkpoint, memory guard, config guard, repeat-tool reminder, security guidance (advisory), cost tracker, output pruner, context monitor,
|
|
31
|
+
description: "FreeHarness hooks: SessionStart pointer (+ post-compaction re-orientation), PreCompact checkpoint, reload-context recap, memory guard, config guard, repeat-tool reminder, security guidance (advisory), cost tracker, output pruner, context monitor, SDD context (SubagentStart task state and open findings, SubagentStop checkpoint written to the task state). Additive; only the memory guard and the config guard deny.",
|
|
29
32
|
hooks: {},
|
|
30
33
|
};
|
|
31
34
|
if (fs.existsSync(file))
|
|
32
35
|
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
33
36
|
doc.hooks ??= {};
|
|
34
37
|
doc.hooks[event] ??= [];
|
|
35
|
-
const cmd = entry.hooks[0]
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const { command: cmd, onFailure } = entry.hooks[0];
|
|
39
|
+
const arr = doc.hooks[event];
|
|
40
|
+
const own = arr.flatMap((e, i) => (e.hooks.length === 1 && e.hooks[0].command === cmd ? [i] : []));
|
|
41
|
+
const exists = arr.some((e) => e.hooks.some((h) => h.command === cmd));
|
|
42
|
+
const changed = own.length > 1 || (own.length === 1 && arr[own[0]].hooks[0].onFailure !== onFailure) || !exists;
|
|
43
|
+
if (own.length > 0 && changed)
|
|
44
|
+
doc.hooks[event] = arr.flatMap((e, i) => (i === own[0] ? [entry] : own.includes(i) ? [] : [e]));
|
|
45
|
+
else if (!exists)
|
|
46
|
+
arr.push(entry);
|
|
47
|
+
if (!dryRun && changed)
|
|
40
48
|
writeText(file, JSON.stringify(doc, null, 2) + "\n", false);
|
|
41
|
-
return
|
|
49
|
+
return changed;
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
44
52
|
* Emits the FreeHarness content as a Claude Code plugin directory under
|
|
@@ -63,7 +71,8 @@ export class PluginAdapter {
|
|
|
63
71
|
const emitHook = (script, flags, events) => {
|
|
64
72
|
res.written.push(writeText(path.join(hooksDir, script), readText(path.join(C, "hooks", script)), dry));
|
|
65
73
|
for (const e of events) {
|
|
66
|
-
const
|
|
74
|
+
const hook = { type: "command", command: hookCmd(script, "node", flags), ...(e.event === "SessionEnd" || e.event === "SubagentStop" ? { timeout: SESSION_END_TIMEOUT_S } : {}) };
|
|
75
|
+
const entry = { ...(e.matcher ? { matcher: e.matcher } : {}), hooks: [hook] };
|
|
67
76
|
mergePluginHook(hooksFile, e.event, entry, dry);
|
|
68
77
|
}
|
|
69
78
|
};
|
|
@@ -82,11 +91,13 @@ export class PluginAdapter {
|
|
|
82
91
|
// it from its own directory regardless of which target installed it.
|
|
83
92
|
res.written.push(writeText(path.join(hooksDir, ROUTING_FILE), readText(routingSourcePath(C)), dry));
|
|
84
93
|
res.written.push(writeText(path.join(hooksDir, "freeharness-memory-guard.mjs"), readText(path.join(C, "hooks", "freeharness-memory-guard.mjs")), dry));
|
|
94
|
+
// The guards sit under the permission layer, so a guard that cannot run allows the call.
|
|
85
95
|
const mg = { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: hookCmd("freeharness-memory-guard.mjs") }] };
|
|
86
96
|
mergePluginHook(hooksFile, "PreToolUse", mg, dry);
|
|
87
97
|
emitHook("freeharness-config-guard.mjs", CONFIG_GUARD_FLAGS, [{ event: "PreToolUse", matcher: "Edit|Write|MultiEdit|Bash" }]);
|
|
88
98
|
emitHook("freeharness-repeat-tool.mjs", REPEAT_TOOL_FLAGS, [{ event: "PreToolUse" }, { event: "UserPromptSubmit" }]);
|
|
89
99
|
emitHook("freeharness-precompact.mjs", "", [{ event: "PreCompact" }]);
|
|
100
|
+
emitHook("freeharness-reload-context.mjs", "", [{ event: "UserPromptSubmit" }]);
|
|
90
101
|
res.written.push(hooksFile);
|
|
91
102
|
break;
|
|
92
103
|
}
|
|
@@ -95,6 +106,7 @@ export class PluginAdapter {
|
|
|
95
106
|
transform: (c) => rewriteAgentModel(c, this.target),
|
|
96
107
|
transformOnly: (n) => n === "SKILL.md",
|
|
97
108
|
}));
|
|
109
|
+
res.written.push(...emitTierReference(C, path.join(root, "skills"), this.target, dry));
|
|
98
110
|
res.notes.push("skills: report-formatting runtime deps (docx/python-pptx) are installed by the user after the plugin loads (see the plugin README).");
|
|
99
111
|
break;
|
|
100
112
|
case "agents":
|
|
@@ -104,7 +116,7 @@ export class PluginAdapter {
|
|
|
104
116
|
}));
|
|
105
117
|
break;
|
|
106
118
|
case "safety": {
|
|
107
|
-
res.notes.push("safety:
|
|
119
|
+
res.notes.push("safety: the advisory security-guidance hook ships in the plugin.");
|
|
108
120
|
res.written.push(writeText(path.join(hooksDir, "freeharness-security-guidance.mjs"), readText(path.join(C, "hooks", "freeharness-security-guidance.mjs")), dry));
|
|
109
121
|
const sg = { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: hookCmd("freeharness-security-guidance.mjs") }] };
|
|
110
122
|
mergePluginHook(hooksFile, "PreToolUse", sg, dry);
|
|
@@ -136,6 +148,8 @@ export class PluginAdapter {
|
|
|
136
148
|
res.notes.push("cost: SessionEnd hooks run under Claude Code's shared 1.5 s budget, which a plugin timeout does not raise; set CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=10000 for long transcripts.");
|
|
137
149
|
emitHook("freeharness-output-pruner.mjs", OUTPUT_PRUNER_FLAGS, [{ event: "PostToolUse", matcher: "Bash" }]);
|
|
138
150
|
emitHook("freeharness-context-monitor.mjs", CONTEXT_MONITOR_FLAGS, [{ event: "Stop" }]);
|
|
151
|
+
// Beside the tracker: the hook imports its brief-path rule from its own directory.
|
|
152
|
+
emitHook("freeharness-sdd-context.mjs", "", [{ event: "SubagentStart" }, { event: "SubagentStop" }]);
|
|
139
153
|
res.written.push(hooksFile);
|
|
140
154
|
res.notes.push("cost: the hook prices with hooks/cost-prices.json next to it (placeholders; edit, or point FREEHARNESS_COST_PRICES at your table).");
|
|
141
155
|
res.notes.push("cost: a plugin cannot set settings keys; add promptCacheTtl / subagentPromptCacheTtl = 1h to ~/.claude/settings.json by hand or with `freeharness init --scope user --only cost`.");
|
|
@@ -149,9 +163,6 @@ export class PluginAdapter {
|
|
|
149
163
|
break;
|
|
150
164
|
case "memory":
|
|
151
165
|
res.notes.push("memory: not a plugin component (free-mem is its own plugin, built and registered by `freeharness init --scope user --only memory`).");
|
|
152
|
-
emitHook("freeharness-auto-memory.mjs", "", [{ event: "SessionEnd" }]);
|
|
153
|
-
res.notes.push("memory: the auto-memory tier-0 hook ships (SessionEnd; a no-op until FREEHARNESS_AUTO_MEMORY=on).");
|
|
154
|
-
res.written.push(hooksFile);
|
|
155
166
|
break;
|
|
156
167
|
default:
|
|
157
168
|
res.notes.push(`No plugin action for capability '${cap.id}'.`);
|
|
@@ -245,8 +256,8 @@ its agents, skills, and hooks.
|
|
|
245
256
|
|
|
246
257
|
## What is not in the plugin
|
|
247
258
|
|
|
248
|
-
- Behavior rules (\`CLAUDE.md\`)
|
|
249
|
-
\`freeharness init --scope user --only rules
|
|
259
|
+
- Behavior rules (\`CLAUDE.md\`): not a plugin component. Install them per user with
|
|
260
|
+
\`freeharness init --scope user --only rules\`. The security-guidance hook ships in the plugin.
|
|
250
261
|
- SDD (OpenSpec \`/opsx\`): per-project init, \`freeharness bootstrap\` inside the project.
|
|
251
262
|
- Memory (free-mem): its own plugin, \`freeharness init --scope user --only memory\`.
|
|
252
263
|
- Report-formatting runtime deps: after install run \`npm install\` in
|