harnery 0.31.6 → 0.33.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/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +5 -1
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +280 -94
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/claude-desktop.d.ts +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +74 -3
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +141 -19
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -5
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +4 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +2 -3
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +6 -5
- package/dist/core/agents/cli.js +128 -89
- package/dist/core/agents/coord-client.d.ts +18 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/render/prompt-context.d.ts +12 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +70 -16
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +23 -4
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +98 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +86 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/agents/state/session-state.d.ts +44 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +81 -0
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/parse.d.ts +1 -1
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +2 -0
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +258 -32
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +57 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/presence/blob.d.ts +4 -0
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +12 -0
- package/dist/core/presence/index.js +15 -1
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/claude-desktop.d.ts +47 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +169 -1
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/bundle.d.ts +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +2 -2
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +5 -1
- package/src/commands/agents.ts +341 -113
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +155 -21
- package/src/commands/init.ts +28 -9
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +7 -6
- package/src/core/agents/cli.ts +143 -89
- package/src/core/agents/coord-client.ts +21 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +96 -16
- package/src/core/agents/render/session-context.ts +29 -4
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +108 -1
- package/src/core/agents/state/heartbeat-writer.ts +129 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +296 -27
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +71 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/bundle.ts +199 -0
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Build the content identity for the repo-authored instruction surface an
|
|
6
|
+
* adapter can load at one working path. The identity intentionally excludes
|
|
7
|
+
* timestamps and git revisions: two checkouts with byte-identical effective
|
|
8
|
+
* instructions belong to the same cohort.
|
|
9
|
+
*/
|
|
10
|
+
export function buildInstructionBundle(options) {
|
|
11
|
+
const coordRoot = resolve(options.coordRoot);
|
|
12
|
+
const cwd = resolve(options.cwd);
|
|
13
|
+
const roots = resolveInstructionRoots(coordRoot, cwd);
|
|
14
|
+
const profileRoot = roots.at(-1) ?? coordRoot;
|
|
15
|
+
const components = dedupeCandidates(roots.flatMap((root) => adapterCandidates(root, options.adapter)), coordRoot);
|
|
16
|
+
const canonicalSources = dedupeCandidates(roots.flatMap((root) => canonicalCandidates(root)), coordRoot);
|
|
17
|
+
return {
|
|
18
|
+
schema_version: 1,
|
|
19
|
+
adapter: options.adapter,
|
|
20
|
+
coord_root: coordRoot,
|
|
21
|
+
profile_root: relativePath(coordRoot, profileRoot),
|
|
22
|
+
instruction_roots: roots.map((root) => relativePath(coordRoot, root)),
|
|
23
|
+
instruction_bundle_id: identity("instruction-bundle-v1", options.adapter, components),
|
|
24
|
+
canonical_source_id: identity("instruction-source-v1", null, canonicalSources),
|
|
25
|
+
components,
|
|
26
|
+
canonical_sources: canonicalSources,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveInstructionRoots(coordRoot, cwd) {
|
|
30
|
+
const roots = [coordRoot];
|
|
31
|
+
if (!isWithin(coordRoot, cwd))
|
|
32
|
+
return roots;
|
|
33
|
+
const ancestors = [];
|
|
34
|
+
let cursor = cwd;
|
|
35
|
+
while (cursor !== coordRoot) {
|
|
36
|
+
if (existsSync(join(cursor, "AGENTS.md")) || existsSync(join(cursor, "CLAUDE.md"))) {
|
|
37
|
+
ancestors.push(cursor);
|
|
38
|
+
}
|
|
39
|
+
const parent = dirname(cursor);
|
|
40
|
+
if (parent === cursor)
|
|
41
|
+
break;
|
|
42
|
+
cursor = parent;
|
|
43
|
+
}
|
|
44
|
+
roots.push(...ancestors.reverse().filter((root) => root !== coordRoot));
|
|
45
|
+
return roots;
|
|
46
|
+
}
|
|
47
|
+
function adapterCandidates(root, adapter) {
|
|
48
|
+
const candidates = [];
|
|
49
|
+
const addFile = (path, role) => {
|
|
50
|
+
if (existsSync(path) && lstatSync(path).isFile())
|
|
51
|
+
candidates.push({ absolutePath: path, role });
|
|
52
|
+
};
|
|
53
|
+
const addTree = (path, role) => {
|
|
54
|
+
for (const file of walkFiles(path))
|
|
55
|
+
candidates.push({ absolutePath: file, role });
|
|
56
|
+
};
|
|
57
|
+
if (adapter === "claude-code") {
|
|
58
|
+
addFile(join(root, "CLAUDE.md"), "always_loaded");
|
|
59
|
+
addTree(join(root, ".claude", "rules"), "always_loaded");
|
|
60
|
+
addTree(join(root, ".claude", "skills"), "just_in_time");
|
|
61
|
+
addFile(join(root, ".claude", "settings.json"), "runtime_config");
|
|
62
|
+
addFile(join(root, ".claude", "settings.local.json"), "runtime_config");
|
|
63
|
+
}
|
|
64
|
+
else if (adapter === "cursor") {
|
|
65
|
+
addFile(join(root, "AGENTS.md"), "always_loaded");
|
|
66
|
+
addTree(join(root, ".cursor", "rules"), "always_loaded");
|
|
67
|
+
addTree(join(root, ".agents", "skills"), "just_in_time");
|
|
68
|
+
addTree(join(root, ".cursor", "skills"), "just_in_time");
|
|
69
|
+
addFile(join(root, ".cursor", "hooks.json"), "runtime_config");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
addFile(join(root, "AGENTS.md"), "always_loaded");
|
|
73
|
+
addTree(join(root, ".harnery", "skills"), "just_in_time");
|
|
74
|
+
addFile(join(root, ".codex", "hooks.json"), "runtime_config");
|
|
75
|
+
}
|
|
76
|
+
// Harnery's host configuration governs hook enforcement for all adapters.
|
|
77
|
+
addFile(join(root, ".harnery", "config.jsonc"), "runtime_config");
|
|
78
|
+
return candidates;
|
|
79
|
+
}
|
|
80
|
+
function canonicalCandidates(root) {
|
|
81
|
+
const candidates = [];
|
|
82
|
+
const agents = join(root, "AGENTS.md");
|
|
83
|
+
if (existsSync(agents) && lstatSync(agents).isFile()) {
|
|
84
|
+
candidates.push({ absolutePath: agents, role: "always_loaded" });
|
|
85
|
+
}
|
|
86
|
+
for (const file of walkFiles(join(root, ".agents"))) {
|
|
87
|
+
candidates.push({ absolutePath: file, role: "just_in_time" });
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
function walkFiles(root) {
|
|
92
|
+
if (!existsSync(root) || !lstatSync(root).isDirectory())
|
|
93
|
+
return [];
|
|
94
|
+
const files = [];
|
|
95
|
+
const visit = (dir) => {
|
|
96
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
97
|
+
const path = join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory())
|
|
99
|
+
visit(path);
|
|
100
|
+
else if (entry.isFile())
|
|
101
|
+
files.push(path);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
visit(root);
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
function dedupeCandidates(candidates, coordRoot) {
|
|
108
|
+
const byPath = new Map();
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
const key = resolve(candidate.absolutePath);
|
|
111
|
+
const prior = byPath.get(key);
|
|
112
|
+
if (!prior || roleRank(candidate.role) < roleRank(prior.role))
|
|
113
|
+
byPath.set(key, candidate);
|
|
114
|
+
}
|
|
115
|
+
return [...byPath.values()]
|
|
116
|
+
.map(({ absolutePath, role }) => {
|
|
117
|
+
const bytes = readFileSync(absolutePath);
|
|
118
|
+
return {
|
|
119
|
+
path: relativePath(coordRoot, absolutePath),
|
|
120
|
+
role,
|
|
121
|
+
bytes: bytes.byteLength,
|
|
122
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.role.localeCompare(b.role));
|
|
126
|
+
}
|
|
127
|
+
function identity(prefix, adapter, components) {
|
|
128
|
+
const body = components.map(({ path, role, sha256 }) => ({ path, role, sha256 }));
|
|
129
|
+
return `sha256:${createHash("sha256")
|
|
130
|
+
.update(JSON.stringify({ prefix, ...(adapter ? { adapter } : {}), components: body }))
|
|
131
|
+
.digest("hex")}`;
|
|
132
|
+
}
|
|
133
|
+
function relativePath(root, path) {
|
|
134
|
+
const rel = relative(root, path);
|
|
135
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
136
|
+
}
|
|
137
|
+
function isWithin(root, path) {
|
|
138
|
+
if (!isAbsolute(root) || !isAbsolute(path))
|
|
139
|
+
return false;
|
|
140
|
+
const rel = relative(root, path);
|
|
141
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== "..");
|
|
142
|
+
}
|
|
143
|
+
function roleRank(role) {
|
|
144
|
+
if (role === "always_loaded")
|
|
145
|
+
return 0;
|
|
146
|
+
if (role === "runtime_config")
|
|
147
|
+
return 1;
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-hook side of ADR 0008's lifecycle contract: the coordination content of
|
|
3
|
+
* a consumer's git hooks is machine-owned, exactly like the AGENTS.md block.
|
|
4
|
+
* `init` installs/refreshes it, `deinit` removes it, `init --check` flags
|
|
5
|
+
* drift.
|
|
6
|
+
*
|
|
7
|
+
* What lives in the hook file is deliberately tiny: a hash-versioned managed
|
|
8
|
+
* region (# harnery:begin/end markers) that locates `agent-coord` and invokes
|
|
9
|
+
* `git-hook <event>`. Every piece of actual behavior — staged collection,
|
|
10
|
+
* submodule canonicalization, gitlink discrimination, verdict, claim pruning —
|
|
11
|
+
* lives in harnery and upgrades with the package. The region only changes when
|
|
12
|
+
* the invocation contract does, and then `init` re-splices it and `--check`
|
|
13
|
+
* catches a stale copy. History's lesson (the first host carried ~200 lines of
|
|
14
|
+
* coordination bash that decayed for months): logic in a host hook file is
|
|
15
|
+
* logic outside the upgrade path.
|
|
16
|
+
*
|
|
17
|
+
* A hook file harnery created whole is deletable by `deinit` (nothing but the
|
|
18
|
+
* shebang remains after the region is removed); a host-authored hook keeps all
|
|
19
|
+
* host content and only loses the managed region.
|
|
20
|
+
*/
|
|
21
|
+
import type { GitHookEvent } from "../../core/agents/git-hook.js";
|
|
22
|
+
import { type ManagedStatus } from "./splice.js";
|
|
23
|
+
export declare const GIT_HOOK_EVENTS: readonly ["pre-commit", "post-commit", "post-checkout"];
|
|
24
|
+
export type { GitHookEvent };
|
|
25
|
+
/**
|
|
26
|
+
* The managed region body for one hook event. POSIX sh, `harnery_`-prefixed
|
|
27
|
+
* variables (the region runs inside arbitrary host hook scripts), and a bin
|
|
28
|
+
* lookup that covers both consumer layouts: git-submodule
|
|
29
|
+
* (`<root>/harnery/bin/`) and npm (`<root>/node_modules/harnery/bin/`).
|
|
30
|
+
*
|
|
31
|
+
* pre-commit propagates a blocking exit code; post-commit / post-checkout are
|
|
32
|
+
* best-effort and never fail the git operation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderGitHookBody(event: GitHookEvent): string;
|
|
35
|
+
/**
|
|
36
|
+
* The effective hooks dir for the repo at `projectRoot`: honors
|
|
37
|
+
* `core.hooksPath` (relative values resolve against the repo root) and
|
|
38
|
+
* worktree layouts, because that's what `git rev-parse --git-path hooks`
|
|
39
|
+
* reports. Null when `projectRoot` isn't a git repo.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveHooksDir(projectRoot: string): string | null;
|
|
42
|
+
export interface GitHooksResult {
|
|
43
|
+
actions: string[];
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}
|
|
46
|
+
/** Install or refresh the managed region in each of the three hook files. */
|
|
47
|
+
export declare function applyGitHooks(projectRoot: string, opts: {
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
}): GitHooksResult;
|
|
50
|
+
/**
|
|
51
|
+
* Has this project adopted harnery-managed git hooks at all? True when any of
|
|
52
|
+
* the three hook files carries a managed region. Adoption is the gate between
|
|
53
|
+
* "never installed" (a consumer that upgraded but hasn't opted in — not drift,
|
|
54
|
+
* `--check` stays green, `doctor` nudges) and "decayed" (a region existed and
|
|
55
|
+
* is now stale or partially deleted — drift, `--check` goes red).
|
|
56
|
+
*/
|
|
57
|
+
export declare function gitHooksInstalled(projectRoot: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Drift report for `init --check`: per-hook managed-region freshness. A
|
|
60
|
+
* project that never adopted git hooks reports fresh — an upgrade must not
|
|
61
|
+
* turn a consumer's CI red for a feature they haven't installed.
|
|
62
|
+
*/
|
|
63
|
+
export declare function checkGitHooks(projectRoot: string): {
|
|
64
|
+
status: ManagedStatus;
|
|
65
|
+
issues: string[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Remove the managed region from each hook. A file that was harnery-created
|
|
69
|
+
* whole (nothing but a shebang left) is deleted; host-authored hooks keep
|
|
70
|
+
* every host line.
|
|
71
|
+
*/
|
|
72
|
+
export declare function removeGitHooks(projectRoot: string, opts: {
|
|
73
|
+
dryRun?: boolean;
|
|
74
|
+
}): GitHooksResult;
|
|
75
|
+
//# sourceMappingURL=git-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/git-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,eAAe,yDAIgB,CAAC;AAC7C,YAAY,EAAE,YAAY,EAAE,CAAC;AAM7B;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAkB7D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAoE7F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAa9D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2B9F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CA8B9F"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-hook side of ADR 0008's lifecycle contract: the coordination content of
|
|
3
|
+
* a consumer's git hooks is machine-owned, exactly like the AGENTS.md block.
|
|
4
|
+
* `init` installs/refreshes it, `deinit` removes it, `init --check` flags
|
|
5
|
+
* drift.
|
|
6
|
+
*
|
|
7
|
+
* What lives in the hook file is deliberately tiny: a hash-versioned managed
|
|
8
|
+
* region (# harnery:begin/end markers) that locates `agent-coord` and invokes
|
|
9
|
+
* `git-hook <event>`. Every piece of actual behavior — staged collection,
|
|
10
|
+
* submodule canonicalization, gitlink discrimination, verdict, claim pruning —
|
|
11
|
+
* lives in harnery and upgrades with the package. The region only changes when
|
|
12
|
+
* the invocation contract does, and then `init` re-splices it and `--check`
|
|
13
|
+
* catches a stale copy. History's lesson (the first host carried ~200 lines of
|
|
14
|
+
* coordination bash that decayed for months): logic in a host hook file is
|
|
15
|
+
* logic outside the upgrade path.
|
|
16
|
+
*
|
|
17
|
+
* A hook file harnery created whole is deletable by `deinit` (nothing but the
|
|
18
|
+
* shebang remains after the region is removed); a host-authored hook keeps all
|
|
19
|
+
* host content and only loses the managed region.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
24
|
+
import { checkRegion, regionBlock, removeRegion, spliceRegion, } from "./splice.js";
|
|
25
|
+
export const GIT_HOOK_EVENTS = [
|
|
26
|
+
"pre-commit",
|
|
27
|
+
"post-commit",
|
|
28
|
+
"post-checkout",
|
|
29
|
+
];
|
|
30
|
+
function regionName(event) {
|
|
31
|
+
return `git-hook-${event}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The managed region body for one hook event. POSIX sh, `harnery_`-prefixed
|
|
35
|
+
* variables (the region runs inside arbitrary host hook scripts), and a bin
|
|
36
|
+
* lookup that covers both consumer layouts: git-submodule
|
|
37
|
+
* (`<root>/harnery/bin/`) and npm (`<root>/node_modules/harnery/bin/`).
|
|
38
|
+
*
|
|
39
|
+
* pre-commit propagates a blocking exit code; post-commit / post-checkout are
|
|
40
|
+
* best-effort and never fail the git operation.
|
|
41
|
+
*/
|
|
42
|
+
export function renderGitHookBody(event) {
|
|
43
|
+
const blocking = event === "pre-commit";
|
|
44
|
+
const invoke = blocking
|
|
45
|
+
? ` "$harnery_hook_bin" git-hook ${event} "$@"\n` +
|
|
46
|
+
` harnery_hook_rc=$?\n` +
|
|
47
|
+
` [ "$harnery_hook_rc" -ne 0 ] && exit "$harnery_hook_rc"\n`
|
|
48
|
+
: ` "$harnery_hook_bin" git-hook ${event} "$@" >/dev/null 2>&1 || true\n`;
|
|
49
|
+
return (`# Coordination for this hook lives in harnery (agent-coord git-hook ${event});\n` +
|
|
50
|
+
`# upgrading harnery upgrades the behavior. Managed region: do not edit by hand.\n` +
|
|
51
|
+
`harnery_hook_root=$(git rev-parse --show-superproject-working-tree 2>/dev/null)\n` +
|
|
52
|
+
`[ -n "$harnery_hook_root" ] || harnery_hook_root=$(git rev-parse --show-toplevel 2>/dev/null)\n` +
|
|
53
|
+
`for harnery_hook_bin in "$harnery_hook_root/harnery/bin/agent-coord" "$harnery_hook_root/node_modules/harnery/bin/agent-coord"; do\n` +
|
|
54
|
+
` [ -x "$harnery_hook_bin" ] || continue\n` +
|
|
55
|
+
invoke +
|
|
56
|
+
` break\n` +
|
|
57
|
+
`done`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The effective hooks dir for the repo at `projectRoot`: honors
|
|
61
|
+
* `core.hooksPath` (relative values resolve against the repo root) and
|
|
62
|
+
* worktree layouts, because that's what `git rev-parse --git-path hooks`
|
|
63
|
+
* reports. Null when `projectRoot` isn't a git repo.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveHooksDir(projectRoot) {
|
|
66
|
+
const r = spawnSync("git", ["rev-parse", "--git-path", "hooks"], {
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 5000,
|
|
70
|
+
});
|
|
71
|
+
if (r.status !== 0)
|
|
72
|
+
return null;
|
|
73
|
+
const p = (r.stdout ?? "").trim();
|
|
74
|
+
if (!p)
|
|
75
|
+
return null;
|
|
76
|
+
return isAbsolute(p) ? p : resolve(projectRoot, p);
|
|
77
|
+
}
|
|
78
|
+
/** Install or refresh the managed region in each of the three hook files. */
|
|
79
|
+
export function applyGitHooks(projectRoot, opts) {
|
|
80
|
+
const dryRun = opts.dryRun === true;
|
|
81
|
+
const actions = [];
|
|
82
|
+
const warnings = [];
|
|
83
|
+
const hooksDir = resolveHooksDir(projectRoot);
|
|
84
|
+
if (!hooksDir) {
|
|
85
|
+
warnings.push("not a git repository; skipped git-hook regions");
|
|
86
|
+
return { actions, warnings };
|
|
87
|
+
}
|
|
88
|
+
if (!existsSync(hooksDir) && !dryRun)
|
|
89
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
90
|
+
for (const event of GIT_HOOK_EVENTS) {
|
|
91
|
+
const file = join(hooksDir, event);
|
|
92
|
+
const body = renderGitHookBody(event);
|
|
93
|
+
const region = regionName(event);
|
|
94
|
+
const display = relative(projectRoot, file);
|
|
95
|
+
if (!existsSync(file)) {
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
actions.push(`+ would create ${display} (harnery-owned git hook)`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
writeFileSync(file, `#!/bin/sh\n${regionBlock(region, body, "hash")}\n`);
|
|
101
|
+
chmodSync(file, 0o755);
|
|
102
|
+
actions.push(`+ created ${display} (harnery-owned git hook)`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const content = readFileSync(file, "utf8");
|
|
106
|
+
const status = checkRegion(content, region, body, "hash");
|
|
107
|
+
if (status === "fresh") {
|
|
108
|
+
actions.push(`· ${display} git-hook region current`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (status === "stale") {
|
|
112
|
+
const spliced = spliceRegion(content, region, body, "hash");
|
|
113
|
+
if (dryRun)
|
|
114
|
+
actions.push(`+ would refresh stale git-hook region in ${display}`);
|
|
115
|
+
else {
|
|
116
|
+
writeFileSync(file, spliced.text);
|
|
117
|
+
actions.push(`+ refreshed stale git-hook region in ${display}`);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Missing from an existing, host-authored hook: insert right after the
|
|
122
|
+
// shebang. Coordination runs first on purpose — an agent should hear
|
|
123
|
+
// "this commit is blocked by a peer's claim" before paying for the host's
|
|
124
|
+
// typecheck/lint checks, and post-checkout's claim release must beat any
|
|
125
|
+
// host short-circuit below it.
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
actions.push(`+ would insert git-hook region into ${display} (after shebang)`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const block = regionBlock(region, body, "hash");
|
|
131
|
+
let text;
|
|
132
|
+
if (content.startsWith("#!")) {
|
|
133
|
+
const nl = content.indexOf("\n");
|
|
134
|
+
text = `${content.slice(0, nl + 1)}\n${block}\n${content.slice(nl + 1)}`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
text = `${block}\n\n${content}`;
|
|
138
|
+
}
|
|
139
|
+
writeFileSync(file, text);
|
|
140
|
+
actions.push(`+ inserted git-hook region into ${display} (after shebang)`);
|
|
141
|
+
}
|
|
142
|
+
return { actions, warnings };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Has this project adopted harnery-managed git hooks at all? True when any of
|
|
146
|
+
* the three hook files carries a managed region. Adoption is the gate between
|
|
147
|
+
* "never installed" (a consumer that upgraded but hasn't opted in — not drift,
|
|
148
|
+
* `--check` stays green, `doctor` nudges) and "decayed" (a region existed and
|
|
149
|
+
* is now stale or partially deleted — drift, `--check` goes red).
|
|
150
|
+
*/
|
|
151
|
+
export function gitHooksInstalled(projectRoot) {
|
|
152
|
+
const hooksDir = resolveHooksDir(projectRoot);
|
|
153
|
+
if (!hooksDir)
|
|
154
|
+
return false;
|
|
155
|
+
for (const event of GIT_HOOK_EVENTS) {
|
|
156
|
+
const file = join(hooksDir, event);
|
|
157
|
+
if (!existsSync(file))
|
|
158
|
+
continue;
|
|
159
|
+
try {
|
|
160
|
+
if (readFileSync(file, "utf8").includes(`harnery:begin ${regionName(event)}`))
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* unreadable file counts as absent */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Drift report for `init --check`: per-hook managed-region freshness. A
|
|
171
|
+
* project that never adopted git hooks reports fresh — an upgrade must not
|
|
172
|
+
* turn a consumer's CI red for a feature they haven't installed.
|
|
173
|
+
*/
|
|
174
|
+
export function checkGitHooks(projectRoot) {
|
|
175
|
+
const issues = [];
|
|
176
|
+
let worst = "fresh";
|
|
177
|
+
const hooksDir = resolveHooksDir(projectRoot);
|
|
178
|
+
if (!hooksDir)
|
|
179
|
+
return { status: "fresh", issues };
|
|
180
|
+
if (!gitHooksInstalled(projectRoot))
|
|
181
|
+
return { status: "fresh", issues };
|
|
182
|
+
for (const event of GIT_HOOK_EVENTS) {
|
|
183
|
+
const file = join(hooksDir, event);
|
|
184
|
+
const display = relative(projectRoot, file);
|
|
185
|
+
if (!existsSync(file)) {
|
|
186
|
+
issues.push(`${display}: missing (run init to create the harnery git hook)`);
|
|
187
|
+
worst = "missing";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const status = checkRegion(readFileSync(file, "utf8"), regionName(event), renderGitHookBody(event), "hash");
|
|
191
|
+
if (status !== "fresh") {
|
|
192
|
+
issues.push(`${display}: git-hook region ${status} (re-run init to refresh)`);
|
|
193
|
+
if (worst === "fresh")
|
|
194
|
+
worst = status;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { status: worst, issues };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Remove the managed region from each hook. A file that was harnery-created
|
|
201
|
+
* whole (nothing but a shebang left) is deleted; host-authored hooks keep
|
|
202
|
+
* every host line.
|
|
203
|
+
*/
|
|
204
|
+
export function removeGitHooks(projectRoot, opts) {
|
|
205
|
+
const dryRun = opts.dryRun === true;
|
|
206
|
+
const actions = [];
|
|
207
|
+
const warnings = [];
|
|
208
|
+
const hooksDir = resolveHooksDir(projectRoot);
|
|
209
|
+
if (!hooksDir)
|
|
210
|
+
return { actions, warnings };
|
|
211
|
+
for (const event of GIT_HOOK_EVENTS) {
|
|
212
|
+
const file = join(hooksDir, event);
|
|
213
|
+
if (!existsSync(file))
|
|
214
|
+
continue;
|
|
215
|
+
const display = relative(projectRoot, file);
|
|
216
|
+
const content = readFileSync(file, "utf8");
|
|
217
|
+
const { text, removed } = removeRegion(content, regionName(event), "hash");
|
|
218
|
+
if (!removed)
|
|
219
|
+
continue;
|
|
220
|
+
const residue = text.replace(/^#![^\n]*\n?/, "").trim();
|
|
221
|
+
if (residue === "") {
|
|
222
|
+
if (dryRun)
|
|
223
|
+
actions.push(`+ would remove ${display} (was harnery-owned)`);
|
|
224
|
+
else {
|
|
225
|
+
rmSync(file);
|
|
226
|
+
actions.push(`+ removed ${display} (was harnery-owned)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (dryRun) {
|
|
230
|
+
actions.push(`+ would remove git-hook region from ${display} (host content kept)`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
writeFileSync(file, text);
|
|
234
|
+
actions.push(`+ removed git-hook region from ${display} (host content kept)`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { actions, warnings };
|
|
238
|
+
}
|
|
@@ -22,8 +22,14 @@
|
|
|
22
22
|
*/
|
|
23
23
|
/** 8-hex-char content hash stamped into every managed marker. */
|
|
24
24
|
export declare function shortHash(s: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Marker comment style. Markdown/HTML files wrap markers in HTML comments;
|
|
27
|
+
* shell files (git hooks) use `#` line comments. The style only changes the
|
|
28
|
+
* marker syntax — hashing, splice, remove, and check semantics are identical.
|
|
29
|
+
*/
|
|
30
|
+
export type CommentStyle = "html" | "hash";
|
|
25
31
|
/** Canonical region block: begin-marker, body flanked by newlines, end-marker. */
|
|
26
|
-
export declare function regionBlock(region: string, body: string): string;
|
|
32
|
+
export declare function regionBlock(region: string, body: string, style?: CommentStyle): string;
|
|
27
33
|
export type ManagedStatus = "fresh" | "stale" | "missing";
|
|
28
34
|
export interface SpliceResult {
|
|
29
35
|
text: string;
|
|
@@ -40,19 +46,19 @@ export interface SpliceResult {
|
|
|
40
46
|
* absent, the block is appended after existing content (blank-line separated);
|
|
41
47
|
* an empty/whitespace-only `content` becomes just the block.
|
|
42
48
|
*/
|
|
43
|
-
export declare function spliceRegion(content: string, region: string, body: string): SpliceResult;
|
|
49
|
+
export declare function spliceRegion(content: string, region: string, body: string, style?: CommentStyle): SpliceResult;
|
|
44
50
|
/**
|
|
45
51
|
* Remove a managed region, collapsing the blank lines it leaves behind. Returns
|
|
46
52
|
* `removed: false` (content unchanged) when the region is absent. When the region
|
|
47
53
|
* was the file's only content, the result is the empty string — the caller
|
|
48
54
|
* decides whether to delete the file.
|
|
49
55
|
*/
|
|
50
|
-
export declare function removeRegion(content: string, region: string): {
|
|
56
|
+
export declare function removeRegion(content: string, region: string, style?: CommentStyle): {
|
|
51
57
|
text: string;
|
|
52
58
|
removed: boolean;
|
|
53
59
|
};
|
|
54
60
|
/** Region freshness: missing, stale (hash or body drifted), or fresh. */
|
|
55
|
-
export declare function checkRegion(content: string, region: string, body: string): ManagedStatus;
|
|
61
|
+
export declare function checkRegion(content: string, region: string, body: string, style?: CommentStyle): ManagedStatus;
|
|
56
62
|
/** True when a file carries harnery's ownership header (deinit may delete it). */
|
|
57
63
|
export declare function isOwnedFile(content: string): boolean;
|
|
58
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"splice.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;
|
|
1
|
+
{"version":3,"file":"splice.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAMD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAiB3C,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,YAAqB,GAAG,MAAM,CAI9F;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,GAAG,EAAE,OAAO,CAAC;IACb,6EAA6E;IAC7E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,YAAqB,GAC3B,YAAY,CAad;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,YAAqB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CASpC;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,YAAqB,GAC3B,aAAa,CAIf;AAMD,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAcT;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CAKjF"}
|
|
@@ -28,14 +28,22 @@ export function shortHash(s) {
|
|
|
28
28
|
function escapeRe(s) {
|
|
29
29
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30
30
|
}
|
|
31
|
+
function markerPair(style) {
|
|
32
|
+
return style === "hash" ? { open: "#", close: "" } : { open: "<!--", close: "-->" };
|
|
33
|
+
}
|
|
31
34
|
/** Capture regex for a named managed region: begin-marker, body, end-marker. */
|
|
32
|
-
function regionRe(region) {
|
|
35
|
+
function regionRe(region, style = "html") {
|
|
33
36
|
const r = escapeRe(region);
|
|
34
|
-
|
|
37
|
+
const { open, close } = markerPair(style);
|
|
38
|
+
const o = escapeRe(open);
|
|
39
|
+
const c = close ? `\\s*${escapeRe(close)}` : "";
|
|
40
|
+
return new RegExp(`(${o}\\s*harnery:begin ${r}(?:\\s+v=([0-9a-f]*))?${c ? c : "[ \\t]*"})([\\s\\S]*?)(${o}\\s*harnery:end ${r}${c ? c : "[ \\t]*"})`);
|
|
35
41
|
}
|
|
36
42
|
/** Canonical region block: begin-marker, body flanked by newlines, end-marker. */
|
|
37
|
-
export function regionBlock(region, body) {
|
|
38
|
-
|
|
43
|
+
export function regionBlock(region, body, style = "html") {
|
|
44
|
+
const { open, close } = markerPair(style);
|
|
45
|
+
const tail = close ? ` ${close}` : "";
|
|
46
|
+
return `${open} harnery:begin ${region} v=${shortHash(body)}${tail}\n${body}\n${open} harnery:end ${region}${tail}`;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Re-splice (or first-time append) a managed region into `content`. Idempotent:
|
|
@@ -44,10 +52,10 @@ export function regionBlock(region, body) {
|
|
|
44
52
|
* absent, the block is appended after existing content (blank-line separated);
|
|
45
53
|
* an empty/whitespace-only `content` becomes just the block.
|
|
46
54
|
*/
|
|
47
|
-
export function spliceRegion(content, region, body) {
|
|
48
|
-
const re = regionRe(region);
|
|
55
|
+
export function spliceRegion(content, region, body, style = "html") {
|
|
56
|
+
const re = regionRe(region, style);
|
|
49
57
|
const m = content.match(re);
|
|
50
|
-
const fresh = regionBlock(region, body);
|
|
58
|
+
const fresh = regionBlock(region, body, style);
|
|
51
59
|
if (m) {
|
|
52
60
|
const stale = m[2] !== shortHash(body) || m[3] !== `\n${body}\n`;
|
|
53
61
|
// Replacer fn avoids `$`-in-body being read as a capture reference.
|
|
@@ -64,8 +72,8 @@ export function spliceRegion(content, region, body) {
|
|
|
64
72
|
* was the file's only content, the result is the empty string — the caller
|
|
65
73
|
* decides whether to delete the file.
|
|
66
74
|
*/
|
|
67
|
-
export function removeRegion(content, region) {
|
|
68
|
-
const re = regionRe(region);
|
|
75
|
+
export function removeRegion(content, region, style = "html") {
|
|
76
|
+
const re = regionRe(region, style);
|
|
69
77
|
if (!re.test(content))
|
|
70
78
|
return { text: content, removed: false };
|
|
71
79
|
const stripped = content
|
|
@@ -76,8 +84,8 @@ export function removeRegion(content, region) {
|
|
|
76
84
|
return { text: stripped ? `${stripped}\n` : "", removed: true };
|
|
77
85
|
}
|
|
78
86
|
/** Region freshness: missing, stale (hash or body drifted), or fresh. */
|
|
79
|
-
export function checkRegion(content, region, body) {
|
|
80
|
-
const m = content.match(regionRe(region));
|
|
87
|
+
export function checkRegion(content, region, body, style = "html") {
|
|
88
|
+
const m = content.match(regionRe(region, style));
|
|
81
89
|
if (!m)
|
|
82
90
|
return "missing";
|
|
83
91
|
return m[2] === shortHash(body) && m[3] === `\n${body}\n` ? "fresh" : "stale";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"check:layering": "bun run scripts/check-layering.ts",
|
|
186
186
|
"lint:fix": "bun x @biomejs/biome check --write src",
|
|
187
187
|
"format": "bun x @biomejs/biome format --write src",
|
|
188
|
-
"test": "bun test
|
|
188
|
+
"test": "bun run scripts/test.mjs",
|
|
189
189
|
"test:web": "cd web && bun install && bun test",
|
|
190
190
|
"test:integration": "bash tests/integration/run.sh",
|
|
191
191
|
"smoke": "node scripts/smoke-test.mjs",
|
|
@@ -23,6 +23,38 @@
|
|
|
23
23
|
"type": "string",
|
|
24
24
|
"description": "Host-specific command that (re)installs the project's git hooks (e.g. 'scripts/setup-hooks.sh'). Surfaced verbatim in the SessionStart 'commit guard not wired' nudge. harnery doesn't own git-hook installation (each host wires its own pre-commit to invoke `agent-coord verdict`) and the path/command is host-specific, so the host declares it here. Unset -> a generic, host-agnostic hint."
|
|
25
25
|
},
|
|
26
|
+
"agents": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"description": "Host-owned agent ritual policy. Harnery capabilities stay available when a policy is disabled; automatic prompts and Stop remediation follow these switches.",
|
|
30
|
+
"properties": {
|
|
31
|
+
"requireGitFinalization": {
|
|
32
|
+
"type": "boolean",
|
|
33
|
+
"default": false,
|
|
34
|
+
"description": "Require `agents status --end-turn` instead of plain `agents status` at the end of tool-using turns. The guarded call verifies that session-owned paths are committed and their repositories are pushed. Env override: HARNERY_AGENTS_REQUIRE_GIT_FINALIZATION=1|0."
|
|
35
|
+
},
|
|
36
|
+
"finalizationRoots": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"default": [],
|
|
39
|
+
"description": "Project-owned authority for guarded writes outside the coordination root. Each exact root declares one end-turn disposition: `git` requires the path to be a Git repository root and applies dirty/remote checks; `output` explicitly accepts intentional non-Git output. Paths may be absolute or relative to the coordination root. The coordination root is implicitly `git` when it is a Git repository. User-global config cannot grant these roots.",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["path", "disposition"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"path": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"minLength": 1
|
|
48
|
+
},
|
|
49
|
+
"disposition": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"enum": ["git", "output"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
26
58
|
"coord": {
|
|
27
59
|
"type": "object",
|
|
28
60
|
"additionalProperties": false,
|