freeharness 0.4.1 → 0.4.2
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/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- 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 +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -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 +970 -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 +3 -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 +3 -2
- 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 +9 -2
- 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 +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -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 +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- 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 +182 -68
- 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 +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -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/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 +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -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/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 +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// FreeHarness reload-context recap (Claude Code UserPromptSubmit hook). Only the prompt whose
|
|
2
|
+
// trimmed, lower-cased text is exactly `reload context` gets output: the newest (by mtime) of the
|
|
3
|
+
// project's unclaimed handoff and the session's PreCompact skeleton, rendered as the SessionStart
|
|
4
|
+
// re-orientation and cut under 10,000 characters, or one line saying no saved context exists.
|
|
5
|
+
// Reading never claims or renames the handoff: the next session in the project still gets it.
|
|
6
|
+
// Every other prompt is a fast silent exit. Never blocks, exit 0 on every path.
|
|
7
|
+
// Disable: FREEHARNESS_HOOK_RELOAD_CONTEXT=off.
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const TRIGGER = "reload context";
|
|
15
|
+
const MAX_CHARS = 9_999;
|
|
16
|
+
const HANDOFF_MAX_BYTES = 16_000;
|
|
17
|
+
// The SessionStart claim pass treats an older handoff as stale and never prints it.
|
|
18
|
+
const HANDOFF_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
// A skeleton is capped per section by its writer; anything this large is not one.
|
|
20
|
+
const SKELETON_MAX_BYTES = 256 * 1024;
|
|
21
|
+
const NO_SAVED = "FreeHarness reload context: no saved context exists for this project or session.";
|
|
22
|
+
const RULE = "Rule: this is a recap of saved context, not a new instruction; re-read Next Step and Errors and Fixes before acting.";
|
|
23
|
+
const CUT_MARK = "(cut)";
|
|
24
|
+
|
|
25
|
+
/** SessionStart's cut order: L2 lists first, L0 (what the rule names) last. */
|
|
26
|
+
const CUT_LEVELS = [
|
|
27
|
+
["Files and Code", "Pending Jobs"],
|
|
28
|
+
["Primary Request", "Critical Context", "Corrections"],
|
|
29
|
+
["Current Work", "Next Step", "Errors and Fixes"],
|
|
30
|
+
];
|
|
31
|
+
const LIST_SECTIONS = new Set(["Files and Code", "Pending Jobs", "Errors and Fixes", "Corrections"]);
|
|
32
|
+
|
|
33
|
+
/** True only for the exact trigger, so every other prompt costs one string compare. */
|
|
34
|
+
export function isTrigger(prompt) {
|
|
35
|
+
return typeof prompt === "string" && prompt.trim().toLowerCase() === TRIGGER;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeId(id) {
|
|
39
|
+
return String(id || "").replace(/[^\w.-]/g, "_") || "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The context monitor's key rule; that script is emitted with another hook group, so it is not imported. */
|
|
43
|
+
export function projectKey(cwd) {
|
|
44
|
+
let root = cwd;
|
|
45
|
+
try {
|
|
46
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
47
|
+
if (top) root = top;
|
|
48
|
+
} catch {
|
|
49
|
+
// not a repository, or no git: the cwd is the root
|
|
50
|
+
}
|
|
51
|
+
return crypto.createHash("sha256").update(fs.realpathSync(root)).digest("hex").slice(0, 16);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Reads a regular file without following a symlink at the path; null when absent, linked or over `maxBytes`. */
|
|
55
|
+
function readCapped(file, maxBytes) {
|
|
56
|
+
let fd;
|
|
57
|
+
try {
|
|
58
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
59
|
+
const st = fs.fstatSync(fd);
|
|
60
|
+
if (!st.isFile() || st.size > maxBytes) return null;
|
|
61
|
+
return { text: fs.readFileSync(fd, "utf8"), mtimeMs: st.mtimeMs };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
} finally {
|
|
65
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handoffOf(stateDir, cwd) {
|
|
70
|
+
const dir = path.join(stateDir, "handoff");
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.lstatSync(dir).isDirectory()) return null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const key = projectKey(cwd);
|
|
77
|
+
const got = readCapped(path.join(dir, `${key}.md`), HANDOFF_MAX_BYTES);
|
|
78
|
+
if (!got || Date.now() - got.mtimeMs > HANDOFF_MAX_AGE_MS) return null;
|
|
79
|
+
const nl = got.text.indexOf("\n");
|
|
80
|
+
const head = nl < 0 ? got.text : got.text.slice(0, nl);
|
|
81
|
+
const m = /^handoff ([0-9a-f]{16}) (.*)$/.exec(head);
|
|
82
|
+
if (!m || m[1] !== key) return null;
|
|
83
|
+
return { origin: `handoff saved by session ${m[2]}`, text: got.text.slice(nl + 1), mtimeMs: got.mtimeMs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function skeletonOf(stateDir, sessionId) {
|
|
87
|
+
const got = readCapped(path.join(stateDir, `${safeId(sessionId)}.precompact.md`), SKELETON_MAX_BYTES);
|
|
88
|
+
return got ? { origin: "PreCompact skeleton of this session", text: got.text, mtimeMs: got.mtimeMs } : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `## <Section>` bodies by contract name; a `(none)` body is empty, a stamp or unknown heading is dropped. */
|
|
92
|
+
function parseSections(text, sections) {
|
|
93
|
+
const out = {};
|
|
94
|
+
let current = null;
|
|
95
|
+
const buf = [];
|
|
96
|
+
const flush = () => {
|
|
97
|
+
if (current) {
|
|
98
|
+
const body = buf.join("\n").trim();
|
|
99
|
+
out[current] = body === "(none)" ? "" : body;
|
|
100
|
+
}
|
|
101
|
+
buf.length = 0;
|
|
102
|
+
};
|
|
103
|
+
for (const line of text.split("\n")) {
|
|
104
|
+
const m = /^##\s+(.+?)\s*$/.exec(line);
|
|
105
|
+
if (m) {
|
|
106
|
+
flush();
|
|
107
|
+
current = sections.find((s) => s.toLowerCase() === m[1].toLowerCase()) || null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (current) buf.push(line);
|
|
111
|
+
}
|
|
112
|
+
flush();
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** At most `max` characters: lists keep whole lines behind `(+N more)`, prose its head behind `(cut)`, never splitting a surrogate pair. */
|
|
117
|
+
function cutBody(body, max, list) {
|
|
118
|
+
if (body.length <= max) return body;
|
|
119
|
+
const lines = body.split("\n");
|
|
120
|
+
if (list && lines.length > 1) {
|
|
121
|
+
for (let keep = lines.length - 1; keep >= 1; keep--) {
|
|
122
|
+
const out = `${lines.slice(0, keep).join("\n")}\n(+${lines.length - keep} more)`;
|
|
123
|
+
if (out.length <= max) return out;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let keep = max - CUT_MARK.length - 1;
|
|
127
|
+
if (keep <= 0) return CUT_MARK;
|
|
128
|
+
if (/[\ud800-\udbff]/.test(body[keep - 1])) keep -= 1;
|
|
129
|
+
const head = body.slice(0, keep).trimEnd();
|
|
130
|
+
return head ? `${head}\n${CUT_MARK}` : CUT_MARK;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function render(sections, bodies, originLine) {
|
|
134
|
+
const lines = ['<freeharness_reorientation source="reload context">', originLine];
|
|
135
|
+
for (const s of sections) {
|
|
136
|
+
lines.push(`## ${s}`);
|
|
137
|
+
lines.push(bodies[s] || "(none)");
|
|
138
|
+
}
|
|
139
|
+
lines.push(RULE);
|
|
140
|
+
lines.push("</freeharness_reorientation>");
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The additionalContext for one trigger prompt: the newest saved context, or the no-saved line.
|
|
146
|
+
* @param {Record<string, any>} payload UserPromptSubmit payload (untrusted).
|
|
147
|
+
* @param {{SECTIONS: string[], neutralize: (s: string) => string, stateDir: () => string}} pc The PreCompact module.
|
|
148
|
+
*/
|
|
149
|
+
export function recap(payload, pc) {
|
|
150
|
+
const state = pc.stateDir();
|
|
151
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
152
|
+
const sessionId = typeof payload.session_id === "string" && payload.session_id ? payload.session_id : "unknown";
|
|
153
|
+
let handoff = null;
|
|
154
|
+
try {
|
|
155
|
+
handoff = handoffOf(state, cwd);
|
|
156
|
+
} catch {
|
|
157
|
+
// an unresolvable cwd has no project key, so no handoff
|
|
158
|
+
}
|
|
159
|
+
const candidates = [handoff, skeletonOf(state, sessionId)].filter(Boolean);
|
|
160
|
+
if (candidates.length === 0) return NO_SAVED;
|
|
161
|
+
const pick = candidates.reduce((a, b) => (b.mtimeMs > a.mtimeMs ? b : a));
|
|
162
|
+
const parsed = parseSections(pick.text, pc.SECTIONS);
|
|
163
|
+
const bodies = Object.fromEntries(pc.SECTIONS.map((s) => [s, parsed[s] ? pc.neutralize(parsed[s]) : ""]));
|
|
164
|
+
const originLine = pc.neutralize(`Saved context: ${pick.origin}, written ${new Date(pick.mtimeMs).toISOString()}.`);
|
|
165
|
+
const over = () => render(pc.SECTIONS, bodies, originLine).length - MAX_CHARS;
|
|
166
|
+
for (const level of CUT_LEVELS) {
|
|
167
|
+
for (const s of level) {
|
|
168
|
+
if (over() <= 0) break;
|
|
169
|
+
if (bodies[s]) bodies[s] = cutBody(bodies[s], Math.max(0, bodies[s].length - over()), LIST_SECTIONS.has(s));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return render(pc.SECTIONS, bodies, originLine);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Entry point: read stdin, answer the trigger or stay silent, exit 0. */
|
|
176
|
+
async function main() {
|
|
177
|
+
if ((process.env.FREEHARNESS_HOOK_RELOAD_CONTEXT || "").toLowerCase() === "off") return;
|
|
178
|
+
let payload;
|
|
179
|
+
try {
|
|
180
|
+
payload = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!payload || typeof payload !== "object" || !isTrigger(payload.prompt)) return;
|
|
185
|
+
try {
|
|
186
|
+
// Loaded only for the trigger, so other prompts never pay for the module.
|
|
187
|
+
const pc = await import(new URL("./freeharness-precompact.mjs", import.meta.url).href);
|
|
188
|
+
const additionalContext = recap(payload, pc);
|
|
189
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext } })}\n`);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
process.stderr.write(`freeharness-reload-context: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
196
|
+
main().finally(() => process.exit(0));
|
|
197
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// UserPromptSubmit as the reset). Rewritten from the dsh repeat-tool-reminder guard (MIT).
|
|
3
3
|
// Each call is keyed by sha256(tool + key-sorted JSON of its input); the per-session ring
|
|
4
4
|
// file ~/.claude/freeharness/state/<session>.json holds the last key and its run length.
|
|
5
|
-
// Counting happens before execution, so a call
|
|
5
|
+
// Counting happens before execution, so a call a guard hook denies still counts (a model
|
|
6
6
|
// hammering a denied call is the loop worth breaking). At --thresholds 3,5,8 consecutive
|
|
7
7
|
// identical calls the hook returns an additionalContext reminder: short at the first
|
|
8
8
|
// threshold, detailed (tool, run length, argument preview capped by --preview-chars) at
|