harnery 0.6.0 → 0.7.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 +16 -6
- package/dist/commander.d.ts +19 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +51 -4
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/devtools.d.ts +4 -0
- package/dist/commands/devtools.d.ts.map +1 -0
- package/dist/commands/devtools.js +239 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +69 -1
- package/dist/commands/doctor.js +12 -4
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +3 -63
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +54 -14
- package/dist/commands/scratch.js +1 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +273 -62
- package/dist/commands/web-fetch.js +1 -1
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +32 -8
- package/dist/core/agents/events/emit.d.ts.map +1 -1
- package/dist/core/agents/events/emit.js +4 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +16 -5
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/hooks/cli.js +3 -3
- package/dist/core/hooks/effects/index.d.ts +11 -7
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +15 -18
- package/dist/core/hooks/events/emit.d.ts.map +1 -1
- package/dist/core/hooks/events/emit.js +4 -0
- package/dist/core/hooks/events/rotate.d.ts +43 -0
- package/dist/core/hooks/events/rotate.d.ts.map +1 -0
- package/dist/core/hooks/events/rotate.js +142 -0
- package/dist/core/hooks/harness/events.d.ts +11 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +22 -3
- package/dist/core/hooks/harness/wiring.d.ts +8 -0
- package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
- package/dist/core/hooks/harness/wiring.js +34 -5
- package/dist/core/scratch/index.d.ts.map +1 -0
- package/dist/{lib → core}/scratch/index.js +2 -2
- package/dist/lib/devtools.d.ts +178 -0
- package/dist/lib/devtools.d.ts.map +1 -0
- package/dist/lib/devtools.js +1328 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter-migrate.js +364 -0
- package/dist/lib/docs-frontmatter.d.ts +33 -0
- package/dist/lib/docs-frontmatter.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter.js +130 -0
- package/dist/lib/docs-index.d.ts +1 -0
- package/dist/lib/docs-index.d.ts.map +1 -1
- package/dist/lib/docs-index.js +4 -5
- package/dist/lib/docs-lint.d.ts +2 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +18 -12
- package/dist/lib/docs-meta.d.ts +14 -0
- package/dist/lib/docs-meta.d.ts.map +1 -0
- package/dist/lib/docs-meta.js +34 -0
- package/dist/lib/docs-sweep.d.ts +12 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -1
- package/dist/lib/docs-sweep.js +98 -103
- package/dist/lib/format.js +2 -2
- package/dist/lib/http/index.d.ts +1 -0
- package/dist/lib/http/index.d.ts.map +1 -1
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/http/request.d.ts +77 -0
- package/dist/lib/http/request.d.ts.map +1 -0
- package/dist/lib/http/request.js +105 -0
- package/dist/lib/instructions/apply.d.ts +63 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -0
- package/dist/lib/instructions/apply.js +255 -0
- package/dist/lib/instructions/splice.d.ts +73 -0
- package/dist/lib/instructions/splice.d.ts.map +1 -0
- package/dist/lib/instructions/splice.js +118 -0
- package/dist/lib/instructions/templates.d.ts +45 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -0
- package/dist/lib/instructions/templates.js +258 -0
- package/dist/lib/tunnel/gate.d.ts +1 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +14 -9
- package/dist/lib/tunnel/state.d.ts +11 -1
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +8 -3
- package/package.json +7 -6
- package/src/commander.ts +23 -0
- package/src/commands/agents.ts +50 -3
- package/src/commands/deinit.ts +5 -0
- package/src/commands/devtools.ts +284 -0
- package/src/commands/docs.ts +81 -1
- package/src/commands/doctor.ts +13 -4
- package/src/commands/env.ts +11 -77
- package/src/commands/init.ts +66 -15
- package/src/commands/scratch.ts +1 -1
- package/src/commands/tunnel.ts +316 -65
- package/src/commands/web-fetch.ts +1 -1
- package/src/core/agents/coord-client.ts +34 -7
- package/src/core/agents/events/emit.ts +5 -0
- package/src/core/agents/rules/claim-conflict.ts +17 -6
- package/src/core/config.ts +14 -0
- package/src/core/hooks/cli.ts +3 -3
- package/src/core/hooks/effects/index.ts +23 -17
- package/src/core/hooks/events/emit.ts +5 -0
- package/src/core/hooks/events/rotate.ts +151 -0
- package/src/core/hooks/harness/events.ts +30 -3
- package/src/core/hooks/harness/wiring.ts +46 -5
- package/src/{lib → core}/scratch/index.ts +2 -2
- package/src/lib/devtools.ts +1653 -0
- package/src/lib/docs-frontmatter-migrate.ts +427 -0
- package/src/lib/docs-frontmatter.ts +151 -0
- package/src/lib/docs-index.ts +4 -5
- package/src/lib/docs-lint.ts +17 -11
- package/src/lib/docs-meta.ts +44 -0
- package/src/lib/docs-sweep.ts +104 -102
- package/src/lib/format.ts +2 -2
- package/src/lib/http/index.ts +1 -0
- package/src/lib/http/request.ts +154 -0
- package/src/lib/instructions/apply.ts +318 -0
- package/src/lib/instructions/splice.ts +148 -0
- package/src/lib/instructions/templates.ts +295 -0
- package/src/lib/tunnel/gate.ts +14 -9
- package/src/lib/tunnel/state.ts +19 -4
- package/dist/lib/scratch/index.d.ts.map +0 -1
- /package/dist/{lib → core}/scratch/index.d.ts +0 -0
|
@@ -58,10 +58,21 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
58
58
|
}
|
|
59
59
|
// Ordering check: if we hold any claim with path < req.path, fine.
|
|
60
60
|
// Otherwise the new claim would create a backward-edge in the dependency
|
|
61
|
-
// graph and risk deadlock. Only applies when
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
61
|
+
// graph and risk deadlock. Only applies when a fresh peer genuinely CONTENDS
|
|
62
|
+
// with us — i.e. holds a path that also sits in our own footprint (held
|
|
63
|
+
// claims ∪ the path we're now requesting). A wait-for cycle is a
|
|
64
|
+
// strongly-connected set of agents linked by shared files; if no fresh peer
|
|
65
|
+
// shares any file with our footprint, we're in a disjoint component of the
|
|
66
|
+
// resource graph and cannot be part of any cycle, so sorted-order acquisition
|
|
67
|
+
// buys nothing and the block is pure false-positive friction. Sharing a file
|
|
68
|
+
// is the necessary condition for a cycle through this agent, so this narrowing
|
|
69
|
+
// leaves the deadlock-prevention invariant intact for genuine contention while
|
|
70
|
+
// removing the dominant real-world cost — a peer editing unrelated files
|
|
71
|
+
// walling off every backward-order edit. (Single-agent flow can't deadlock
|
|
72
|
+
// with itself and never arms, since there are no other peers to contend.)
|
|
73
|
+
const myFootprint = new Set(myPeer?.files_touched ?? []);
|
|
74
|
+
myFootprint.add(req.path);
|
|
75
|
+
const hasContendingPeer = otherPeers.some((p) => isFresh(p.last_heartbeat) && p.files_touched.some((f) => myFootprint.has(f)));
|
|
65
76
|
// Re-editing a path already in our own files_touched acquires no new lock
|
|
66
77
|
// edge, so it can't create a circular wait — the ordering rule must not block
|
|
67
78
|
// it. Without this exemption, an agent that edits a higher-sorting file and
|
|
@@ -70,7 +81,7 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
70
81
|
// both agent-Gibson holding README.md and agent-Ophelia holding AGENTS.md were
|
|
71
82
|
// blocked re-editing those held files after touching a higher path, 2026-07-03).
|
|
72
83
|
const alreadyHeld = myPeer?.files_touched.includes(req.path) ?? false;
|
|
73
|
-
if (
|
|
84
|
+
if (hasContendingPeer && myPeer && myPeer.files_touched.length > 0 && !alreadyHeld) {
|
|
74
85
|
// Only ACTIVE (uncommitted) edits should constrain lock ordering. A claim on
|
|
75
86
|
// a committed-clean file is a finished edit, not a held lock, so it must not
|
|
76
87
|
// wall off a lower-sorted acquisition. Without this, a long session
|
package/dist/core/config.d.ts
CHANGED
|
@@ -26,6 +26,16 @@ export declare function stripJsonComments(input: string): string;
|
|
|
26
26
|
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
27
27
|
*/
|
|
28
28
|
export declare function resolveBinName(coordRoot?: string | null): string;
|
|
29
|
+
/**
|
|
30
|
+
* The binName explicitly pinned in `<projectRoot>/.harnery/config.jsonc`, or
|
|
31
|
+
* null when absent. Unlike `resolveBinName()` this ignores `HARNERY_BIN` and
|
|
32
|
+
* never falls back to the default — it answers "did someone deliberately pin
|
|
33
|
+
* a name for THIS project?". `init` uses it so a re-run from a different host
|
|
34
|
+
* CLI can't silently re-stamp its own name over a committed pin (the harnery
|
|
35
|
+
* repo itself pins `"harn"` while living embedded in a host monorepo whose
|
|
36
|
+
* CLI would otherwise stamp the host's name into public, committed surfaces).
|
|
37
|
+
*/
|
|
38
|
+
export declare function pinnedBinName(projectRoot: string): string | null;
|
|
29
39
|
/**
|
|
30
40
|
* The host's git-hook (re)install command, for the "commit guard not wired"
|
|
31
41
|
* nudge. Returns the configured `hooksSetupHint` (e.g. "scripts/setup-hooks.sh")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAgBvC,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqCvD;AAyBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAShE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK9E"}
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAgBvC,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqCvD;AAyBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAShE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK9E"}
|
package/dist/core/config.js
CHANGED
|
@@ -106,6 +106,19 @@ export function resolveBinName(coordRoot) {
|
|
|
106
106
|
}
|
|
107
107
|
return DEFAULT_BIN_NAME;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The binName explicitly pinned in `<projectRoot>/.harnery/config.jsonc`, or
|
|
111
|
+
* null when absent. Unlike `resolveBinName()` this ignores `HARNERY_BIN` and
|
|
112
|
+
* never falls back to the default — it answers "did someone deliberately pin
|
|
113
|
+
* a name for THIS project?". `init` uses it so a re-run from a different host
|
|
114
|
+
* CLI can't silently re-stamp its own name over a committed pin (the harnery
|
|
115
|
+
* repo itself pins `"harn"` while living embedded in a host monorepo whose
|
|
116
|
+
* CLI would otherwise stamp the host's name into public, committed surfaces).
|
|
117
|
+
*/
|
|
118
|
+
export function pinnedBinName(projectRoot) {
|
|
119
|
+
const binName = readConfig(projectRoot).binName;
|
|
120
|
+
return typeof binName === "string" && binName.trim() ? binName.trim() : null;
|
|
121
|
+
}
|
|
109
122
|
/**
|
|
110
123
|
* The host's git-hook (re)install command, for the "commit guard not wired"
|
|
111
124
|
* nudge. Returns the configured `hooksSetupHint` (e.g. "scripts/setup-hooks.sh")
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ import { consumeSince, writeCursor } from "../agents/events/consume.js";
|
|
|
26
26
|
import { evaluateStopHook } from "../agents/rules/stop-hook.js";
|
|
27
27
|
import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
|
|
28
28
|
import { shellMutationPaths } from "../agents/state/shell-mutation.js";
|
|
29
|
-
import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCounters, runTurnSummary, scratchArchive, scratchJanitor, scratchRecoveryCue, soundForEvent,
|
|
29
|
+
import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCounters, runSessionSyncExtension, runTurnSummary, scratchArchive, scratchJanitor, scratchRecoveryCue, soundForEvent, } from "./effects/index.js";
|
|
30
30
|
import { emit } from "./events/emit.js";
|
|
31
31
|
import { canonicalize } from "./guard-path.js";
|
|
32
32
|
import { detectHarness } from "./harness/detect.js";
|
|
@@ -441,7 +441,7 @@ async function main() {
|
|
|
441
441
|
// session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
|
|
442
442
|
if (harness === "claude-code") {
|
|
443
443
|
scratchArchive(coordRoot, owner.instance_id);
|
|
444
|
-
|
|
444
|
+
runSessionSyncExtension(coordRoot, true);
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
// Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
|
|
@@ -528,7 +528,7 @@ async function main() {
|
|
|
528
528
|
// CC effects: rate-limited session-telemetry sync + turn-summary Haiku
|
|
529
529
|
// auto-summary.
|
|
530
530
|
if (harness === "claude-code") {
|
|
531
|
-
|
|
531
|
+
runSessionSyncExtension(coordRoot, false);
|
|
532
532
|
runTurnSummary(coordRoot, owner.instance_id, sessionId, payload?.transcript_path);
|
|
533
533
|
}
|
|
534
534
|
// Master-state heartbeat projection. Drains events.ndjson since the last
|
|
@@ -37,14 +37,18 @@ export declare function scratchRecoveryCue(repoRoot: string): string;
|
|
|
37
37
|
/** Archive the ending agent's scratchpad. Fire-and-forget. */
|
|
38
38
|
export declare function scratchArchive(repoRoot: string, owner: string): void;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
40
|
+
* Fire the optional host session-sync extension on turn stop / session end.
|
|
41
|
+
* harnery core has no session-telemetry sink of its own; a host that wants one
|
|
42
|
+
* drops an executable at
|
|
43
|
+
* `scripts/hooks/harness/claude_code/extensions/session-sync.sh` under the coord
|
|
44
|
+
* root, and core runs it detached + unref'd so a slow sink never blocks the
|
|
45
|
+
* hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
|
|
46
|
+
* host can rate-limit the stop path and force-flush on end. No-op when the
|
|
47
|
+
* script is absent, so a plain public install spawns nothing. Mirrors
|
|
48
|
+
* `runTurnSummary`'s extension-script pattern. Caller gates to the claude-code
|
|
49
|
+
* harness.
|
|
46
50
|
*/
|
|
47
|
-
export declare function
|
|
51
|
+
export declare function runSessionSyncExtension(repoRoot: string, force: boolean): void;
|
|
48
52
|
/**
|
|
49
53
|
* Fire the turn-summary extension (Haiku auto-summary of the turn → heartbeat
|
|
50
54
|
* `turn_summary`). Detached + unref'd; it makes an Anthropic API call and must
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/effects/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/effects/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAI,GACX,IAAI,CAYN;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW3F;AAOD,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAYpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAqB9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,IAAI,CAqBN;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAa1D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOnD"}
|
|
@@ -16,7 +16,6 @@ import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { applyDetection } from "../../../lib/presence.js";
|
|
19
|
-
import { resolveBinName } from "../../config.js";
|
|
20
19
|
export { captureImages, imageJanitor } from "./image-capture.js";
|
|
21
20
|
/**
|
|
22
21
|
* Play a notification sound via the cross-platform utility
|
|
@@ -111,26 +110,24 @@ export function scratchArchive(repoRoot, owner) {
|
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
113
|
+
* Fire the optional host session-sync extension on turn stop / session end.
|
|
114
|
+
* harnery core has no session-telemetry sink of its own; a host that wants one
|
|
115
|
+
* drops an executable at
|
|
116
|
+
* `scripts/hooks/harness/claude_code/extensions/session-sync.sh` under the coord
|
|
117
|
+
* root, and core runs it detached + unref'd so a slow sink never blocks the
|
|
118
|
+
* hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
|
|
119
|
+
* host can rate-limit the stop path and force-flush on end. No-op when the
|
|
120
|
+
* script is absent, so a plain public install spawns nothing. Mirrors
|
|
121
|
+
* `runTurnSummary`'s extension-script pattern. Caller gates to the claude-code
|
|
122
|
+
* harness.
|
|
120
123
|
*/
|
|
121
|
-
export function
|
|
124
|
+
export function runSessionSyncExtension(repoRoot, force) {
|
|
122
125
|
try {
|
|
123
|
-
const
|
|
124
|
-
if (!existsSync(
|
|
126
|
+
const script = join(repoRoot, "scripts", "hooks", "harness", "claude_code", "extensions", "session-sync.sh");
|
|
127
|
+
if (!existsSync(script))
|
|
125
128
|
return;
|
|
126
|
-
const
|
|
127
|
-
...process.env,
|
|
128
|
-
HARNERY_OUTPUT_SESSION_TEE: "0",
|
|
129
|
-
};
|
|
130
|
-
if (force)
|
|
131
|
-
env.HARNERY_CLAUDE_SESSIONS_FORCE = "1";
|
|
132
|
-
const child = spawn("bash", [bin, "claude-sessions", "sync", "--quiet"], {
|
|
133
|
-
env,
|
|
129
|
+
const child = spawn("bash", [script, force ? "1" : "0"], {
|
|
130
|
+
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
134
131
|
detached: true,
|
|
135
132
|
stdio: "ignore",
|
|
136
133
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/events/emit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/events/emit.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,OAAO,EAEZ,KAAK,MAAM,EACZ,MAAM,aAAa,CAAC;AAQrB,MAAM,WAAW,SAAS,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK;IACvD,UAAU,EAAE,KAAK,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EAC1D,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAC7B,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAe7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EACjD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAC7B,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAyC7B"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { maybeRotateEventStream } from "./rotate.js";
|
|
3
4
|
import { SCHEMA_VERSION, } from "./schema.js";
|
|
4
5
|
import { ulid } from "./ulid.js";
|
|
5
6
|
const LOCK_FILE = ".harnery/events.ndjson.lock";
|
|
@@ -40,6 +41,9 @@ export function emit(coordRoot, input) {
|
|
|
40
41
|
const envelope = buildEnvelope(input);
|
|
41
42
|
const streamPath = join(coordRoot, STREAM_FILE);
|
|
42
43
|
const lockPath = join(coordRoot, LOCK_FILE);
|
|
44
|
+
// Size-triggered rotation: roll the active file to a dated archive when it
|
|
45
|
+
// crosses the cap, so this append lands in a bounded fresh file. Fail-soft.
|
|
46
|
+
maybeRotateEventStream(coordRoot);
|
|
43
47
|
ensureDir(dirname(streamPath));
|
|
44
48
|
ensureFile(lockPath);
|
|
45
49
|
let line = `${JSON.stringify(envelope)}\n`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size-triggered rotation for the canonical event stream
|
|
3
|
+
* `.harnery/events.ndjson`.
|
|
4
|
+
*
|
|
5
|
+
* The stream is a deliberately-immutable, append-only ledger. Left unbounded it
|
|
6
|
+
* grows past V8's ~512MB max string length, at which point any code that does a
|
|
7
|
+
* whole-file `readFileSync` throws ("Cannot create a string longer than
|
|
8
|
+
* 0x1fffffe8 characters"). Rather than police every current + future reader,
|
|
9
|
+
* we bound the *active* file: once it crosses a byte cap we rename it to a dated
|
|
10
|
+
* archive (`events-YYYY-MM-DD.ndjson`) and start a fresh active file. Archives
|
|
11
|
+
* are kept — the audit trail is preserved, just spread across files that readers
|
|
12
|
+
* glob newest-first.
|
|
13
|
+
*
|
|
14
|
+
* Both independent append paths (agent-hooks `hooks/events/emit.ts` and
|
|
15
|
+
* agent-coord `agents/events/emit.ts`) call `maybeRotateEventStream` before
|
|
16
|
+
* appending, so a roll is triggered continuously, not only at session
|
|
17
|
+
* boundaries.
|
|
18
|
+
*
|
|
19
|
+
* Concurrency: many short-lived hook processes append at once. The rename itself
|
|
20
|
+
* is atomic — a concurrent appender opening the path by name lands its line in
|
|
21
|
+
* whichever inode the name currently resolves to (old archive or new active
|
|
22
|
+
* file), never nowhere. The only hazard is two processes both renaming (the
|
|
23
|
+
* second would move the fresh empty file to a second archive name), which an
|
|
24
|
+
* `O_EXCL` roll-lock prevents.
|
|
25
|
+
*
|
|
26
|
+
* Design + rationale: harnery ADR 0009; decision docket
|
|
27
|
+
* `should-harnery-adopt-a-retention-2026-07-07-ed07`.
|
|
28
|
+
*/
|
|
29
|
+
/** Archive glob prefix. Readers enumerate `events-*.ndjson` newest-first.
|
|
30
|
+
* The pre-existing manual `events-legacy.ndjson` matches and is picked up for
|
|
31
|
+
* free. Kept in sync with the reader-side pattern in web/lib/coord-reader.ts. */
|
|
32
|
+
export declare const ARCHIVE_PREFIX = "events-";
|
|
33
|
+
export declare const ARCHIVE_SUFFIX = ".ndjson";
|
|
34
|
+
/**
|
|
35
|
+
* Roll `events.ndjson` to a dated archive when it exceeds the byte cap. Cheap
|
|
36
|
+
* no-op (a single `statSync`) below the cap. Fail-soft: any error is swallowed
|
|
37
|
+
* so a rotation problem can never break the append that triggered it.
|
|
38
|
+
*
|
|
39
|
+
* `nowMs` is injectable for the stale-lock check in tests; production leaves it
|
|
40
|
+
* unset (the roll date always comes from the file's mtime, never `nowMs`).
|
|
41
|
+
*/
|
|
42
|
+
export declare function maybeRotateEventStream(coordRoot: string, nowMs?: number): boolean;
|
|
43
|
+
//# sourceMappingURL=rotate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotate.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/events/rotate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAyBH;;iFAEiF;AACjF,eAAO,MAAM,cAAc,YAAY,CAAC;AACxC,eAAO,MAAM,cAAc,YAAY,CAAC;AA6BxC;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAmCjF"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size-triggered rotation for the canonical event stream
|
|
3
|
+
* `.harnery/events.ndjson`.
|
|
4
|
+
*
|
|
5
|
+
* The stream is a deliberately-immutable, append-only ledger. Left unbounded it
|
|
6
|
+
* grows past V8's ~512MB max string length, at which point any code that does a
|
|
7
|
+
* whole-file `readFileSync` throws ("Cannot create a string longer than
|
|
8
|
+
* 0x1fffffe8 characters"). Rather than police every current + future reader,
|
|
9
|
+
* we bound the *active* file: once it crosses a byte cap we rename it to a dated
|
|
10
|
+
* archive (`events-YYYY-MM-DD.ndjson`) and start a fresh active file. Archives
|
|
11
|
+
* are kept — the audit trail is preserved, just spread across files that readers
|
|
12
|
+
* glob newest-first.
|
|
13
|
+
*
|
|
14
|
+
* Both independent append paths (agent-hooks `hooks/events/emit.ts` and
|
|
15
|
+
* agent-coord `agents/events/emit.ts`) call `maybeRotateEventStream` before
|
|
16
|
+
* appending, so a roll is triggered continuously, not only at session
|
|
17
|
+
* boundaries.
|
|
18
|
+
*
|
|
19
|
+
* Concurrency: many short-lived hook processes append at once. The rename itself
|
|
20
|
+
* is atomic — a concurrent appender opening the path by name lands its line in
|
|
21
|
+
* whichever inode the name currently resolves to (old archive or new active
|
|
22
|
+
* file), never nowhere. The only hazard is two processes both renaming (the
|
|
23
|
+
* second would move the fresh empty file to a second archive name), which an
|
|
24
|
+
* `O_EXCL` roll-lock prevents.
|
|
25
|
+
*
|
|
26
|
+
* Design + rationale: harnery ADR 0009; decision docket
|
|
27
|
+
* `should-harnery-adopt-a-retention-2026-07-07-ed07`.
|
|
28
|
+
*/
|
|
29
|
+
import { closeSync, existsSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, } from "node:fs";
|
|
30
|
+
import { dirname, join } from "node:path";
|
|
31
|
+
import { coordEnv } from "../../../lib/env.js";
|
|
32
|
+
const STREAM_FILE = ".harnery/events.ndjson";
|
|
33
|
+
const ROLL_LOCK_FILE = ".harnery/events.ndjson.roll.lock";
|
|
34
|
+
/** Default active-file cap: 256 MiB — half of V8's ~512MB string cliff, so even
|
|
35
|
+
* a reader that loads the whole active file whole stays clear of the limit. */
|
|
36
|
+
const DEFAULT_ROLL_BYTES = 256 * 1024 * 1024;
|
|
37
|
+
/** A roll-lock older than this is treated as abandoned (a crashed roller) and
|
|
38
|
+
* stolen. Rolls are sub-millisecond, so 60s is orders of magnitude of slack. */
|
|
39
|
+
const STALE_LOCK_MS = 60_000;
|
|
40
|
+
/** Archive glob prefix. Readers enumerate `events-*.ndjson` newest-first.
|
|
41
|
+
* The pre-existing manual `events-legacy.ndjson` matches and is picked up for
|
|
42
|
+
* free. Kept in sync with the reader-side pattern in web/lib/coord-reader.ts. */
|
|
43
|
+
export const ARCHIVE_PREFIX = "events-";
|
|
44
|
+
export const ARCHIVE_SUFFIX = ".ndjson";
|
|
45
|
+
function resolveRollBytes() {
|
|
46
|
+
const env = coordEnv("EVENTS_ROLL_BYTES");
|
|
47
|
+
const n = env ? Number(env) : Number.NaN;
|
|
48
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_ROLL_BYTES;
|
|
49
|
+
}
|
|
50
|
+
/** `YYYY-MM-DD` (UTC) from a millisecond timestamp. Uses the file's own mtime
|
|
51
|
+
* rather than `Date.now()` so it's deterministic and independent of the harness
|
|
52
|
+
* runtime's clock surface. */
|
|
53
|
+
function utcDateStamp(mtimeMs) {
|
|
54
|
+
const d = new Date(mtimeMs);
|
|
55
|
+
const y = d.getUTCFullYear();
|
|
56
|
+
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
57
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
58
|
+
return `${y}-${mo}-${day}`;
|
|
59
|
+
}
|
|
60
|
+
/** First non-colliding `events-<stamp>[.N].ndjson` path under `coordDir`. */
|
|
61
|
+
function archivePathFor(coordDir, stamp) {
|
|
62
|
+
const base = join(coordDir, `${ARCHIVE_PREFIX}${stamp}${ARCHIVE_SUFFIX}`);
|
|
63
|
+
if (!existsSync(base))
|
|
64
|
+
return base;
|
|
65
|
+
for (let n = 1;; n++) {
|
|
66
|
+
const cand = join(coordDir, `${ARCHIVE_PREFIX}${stamp}.${n}${ARCHIVE_SUFFIX}`);
|
|
67
|
+
if (!existsSync(cand))
|
|
68
|
+
return cand;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Roll `events.ndjson` to a dated archive when it exceeds the byte cap. Cheap
|
|
73
|
+
* no-op (a single `statSync`) below the cap. Fail-soft: any error is swallowed
|
|
74
|
+
* so a rotation problem can never break the append that triggered it.
|
|
75
|
+
*
|
|
76
|
+
* `nowMs` is injectable for the stale-lock check in tests; production leaves it
|
|
77
|
+
* unset (the roll date always comes from the file's mtime, never `nowMs`).
|
|
78
|
+
*/
|
|
79
|
+
export function maybeRotateEventStream(coordRoot, nowMs) {
|
|
80
|
+
try {
|
|
81
|
+
const streamPath = join(coordRoot, STREAM_FILE);
|
|
82
|
+
if (!existsSync(streamPath))
|
|
83
|
+
return false;
|
|
84
|
+
const cap = resolveRollBytes();
|
|
85
|
+
let st = statSync(streamPath);
|
|
86
|
+
if (st.size < cap)
|
|
87
|
+
return false;
|
|
88
|
+
const coordDir = dirname(streamPath);
|
|
89
|
+
const lockPath = join(coordRoot, ROLL_LOCK_FILE);
|
|
90
|
+
mkdirSync(coordDir, { recursive: true });
|
|
91
|
+
if (!acquireRollLock(lockPath, nowMs))
|
|
92
|
+
return false; // another roller active
|
|
93
|
+
try {
|
|
94
|
+
// Re-check under the lock: a concurrent process may have just rolled.
|
|
95
|
+
if (!existsSync(streamPath))
|
|
96
|
+
return false;
|
|
97
|
+
st = statSync(streamPath);
|
|
98
|
+
if (st.size < cap)
|
|
99
|
+
return false;
|
|
100
|
+
const archive = archivePathFor(coordDir, utcDateStamp(st.mtimeMs));
|
|
101
|
+
renameSync(streamPath, archive);
|
|
102
|
+
// Recreate an empty active file so the very next append (and any reader's
|
|
103
|
+
// existsSync) sees a valid, present stream.
|
|
104
|
+
closeSync(openSync(streamPath, "a"));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
try {
|
|
109
|
+
unlinkSync(lockPath);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* best-effort */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false; // fail-soft: never break the caller's append
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Create the roll-lock with O_EXCL. Returns true on acquisition. On collision,
|
|
121
|
+
* steals a stale lock (crashed roller) once, else returns false. */
|
|
122
|
+
function acquireRollLock(lockPath, nowMs) {
|
|
123
|
+
try {
|
|
124
|
+
closeSync(openSync(lockPath, "wx"));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Exists: steal if abandoned.
|
|
129
|
+
try {
|
|
130
|
+
const age = (nowMs ?? Date.now()) - statSync(lockPath).mtimeMs;
|
|
131
|
+
if (age > STALE_LOCK_MS) {
|
|
132
|
+
unlinkSync(lockPath);
|
|
133
|
+
closeSync(openSync(lockPath, "wx"));
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* lost the steal race — someone else holds it */
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -30,6 +30,12 @@ export interface HarnessSpec {
|
|
|
30
30
|
entryShape: HookEntryShape;
|
|
31
31
|
/** When set, ensure this top-level `version` key in the file (Cursor requires `1`). */
|
|
32
32
|
rootVersion?: number;
|
|
33
|
+
/** Harness-owned entries from older specs that `init` should remove during migration. */
|
|
34
|
+
legacyEvents?: HookEvent[];
|
|
35
|
+
/** Strict top-level settings keys accepted by this harness, when its parser is closed. */
|
|
36
|
+
allowedTopLevelKeys?: string[];
|
|
37
|
+
/** Strict hook event keys accepted by this harness, including events harnery does not wire. */
|
|
38
|
+
allowedEventKeys?: string[];
|
|
33
39
|
}
|
|
34
40
|
/** Claude Code: `.claude/settings.json`. */
|
|
35
41
|
export declare const CLAUDE_CODE_EVENTS: HookEvent[];
|
|
@@ -38,8 +44,12 @@ export declare const CLAUDE_CODE_EVENTS: HookEvent[];
|
|
|
38
44
|
* no `StopFailure` event, but a distinct `beforeShellExecution` (shell-mutation warn).
|
|
39
45
|
*/
|
|
40
46
|
export declare const CURSOR_EVENTS: HookEvent[];
|
|
41
|
-
/** Codex
|
|
47
|
+
/** Codex events that harnery uses from the current native lifecycle surface. */
|
|
42
48
|
export declare const CODEX_EVENTS: HookEvent[];
|
|
49
|
+
/** Entries written by harnery before Codex adopted a strict native hook schema. */
|
|
50
|
+
export declare const LEGACY_CODEX_EVENTS: HookEvent[];
|
|
51
|
+
/** Every hook key accepted by Codex 0.144, including events harnery does not consume. */
|
|
52
|
+
export declare const CODEX_ALLOWED_EVENT_KEYS: string[];
|
|
43
53
|
/** Every supported harness, fully wireable by `harn init`. */
|
|
44
54
|
export declare const HARNESS_SPECS: Record<HarnessId, HarnessSpec>;
|
|
45
55
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,SAAS;IACxB,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,cAAc,CAAC;IAC3B,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,SAAS;IACxB,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,cAAc,CAAC;IAC3B,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+FAA+F;IAC/F,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,EAAE,SAAS,EAWzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,EAWpC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,EAQnC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,EAAE,SAAS,EAI1C,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,UAWpC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,WAAW,CAoBxD,CAAC"}
|
|
@@ -37,19 +37,35 @@ export const CURSOR_EVENTS = [
|
|
|
37
37
|
{ settingsKey: "beforeSubmitPrompt", subcommand: "user-prompt-submit" },
|
|
38
38
|
{ settingsKey: "stop", subcommand: "stop" },
|
|
39
39
|
];
|
|
40
|
-
/** Codex
|
|
40
|
+
/** Codex events that harnery uses from the current native lifecycle surface. */
|
|
41
41
|
export const CODEX_EVENTS = [
|
|
42
42
|
{ settingsKey: "SessionStart", subcommand: "session-start" },
|
|
43
|
-
{ settingsKey: "SessionEnd", subcommand: "session-end" },
|
|
44
43
|
{ settingsKey: "PreToolUse", subcommand: "pre-tool-use" },
|
|
45
44
|
{ settingsKey: "PostToolUse", subcommand: "post-tool-use" },
|
|
46
|
-
{ settingsKey: "PostToolUseFailure", subcommand: "post-tool-use-failure" },
|
|
47
45
|
{ settingsKey: "UserPromptSubmit", subcommand: "user-prompt-submit" },
|
|
48
46
|
{ settingsKey: "SubagentStart", subcommand: "sub-agent-start" },
|
|
49
47
|
{ settingsKey: "SubagentStop", subcommand: "sub-agent-stop" },
|
|
50
48
|
{ settingsKey: "Stop", subcommand: "stop" },
|
|
49
|
+
];
|
|
50
|
+
/** Entries written by harnery before Codex adopted a strict native hook schema. */
|
|
51
|
+
export const LEGACY_CODEX_EVENTS = [
|
|
52
|
+
{ settingsKey: "SessionEnd", subcommand: "session-end" },
|
|
53
|
+
{ settingsKey: "PostToolUseFailure", subcommand: "post-tool-use-failure" },
|
|
51
54
|
{ settingsKey: "StopFailure", subcommand: "stop-failure" },
|
|
52
55
|
];
|
|
56
|
+
/** Every hook key accepted by Codex 0.144, including events harnery does not consume. */
|
|
57
|
+
export const CODEX_ALLOWED_EVENT_KEYS = [
|
|
58
|
+
"SessionStart",
|
|
59
|
+
"PreToolUse",
|
|
60
|
+
"PermissionRequest",
|
|
61
|
+
"PostToolUse",
|
|
62
|
+
"PreCompact",
|
|
63
|
+
"PostCompact",
|
|
64
|
+
"UserPromptSubmit",
|
|
65
|
+
"SubagentStart",
|
|
66
|
+
"SubagentStop",
|
|
67
|
+
"Stop",
|
|
68
|
+
];
|
|
53
69
|
/** Every supported harness, fully wireable by `harn init`. */
|
|
54
70
|
export const HARNESS_SPECS = {
|
|
55
71
|
"claude-code": {
|
|
@@ -67,5 +83,8 @@ export const HARNESS_SPECS = {
|
|
|
67
83
|
settingsFile: ".codex/hooks.json",
|
|
68
84
|
events: CODEX_EVENTS,
|
|
69
85
|
entryShape: "claude",
|
|
86
|
+
legacyEvents: LEGACY_CODEX_EVENTS,
|
|
87
|
+
allowedTopLevelKeys: ["description", "hooks"],
|
|
88
|
+
allowedEventKeys: CODEX_ALLOWED_EVENT_KEYS,
|
|
70
89
|
},
|
|
71
90
|
};
|
|
@@ -28,6 +28,7 @@ export interface CursorHookGroup {
|
|
|
28
28
|
export type HookGroup = ClaudeHookGroup | CursorHookGroup;
|
|
29
29
|
export interface SettingsFile {
|
|
30
30
|
version?: number;
|
|
31
|
+
description?: string;
|
|
31
32
|
hooks?: Record<string, HookGroup[]>;
|
|
32
33
|
[k: string]: unknown;
|
|
33
34
|
}
|
|
@@ -51,6 +52,10 @@ export interface WiringDiff {
|
|
|
51
52
|
* these — they need explicit removal — so they're surfaced separately.
|
|
52
53
|
*/
|
|
53
54
|
orphans: string[];
|
|
55
|
+
/** Settings fields rejected by a harness with a strict top-level schema. */
|
|
56
|
+
invalidTopLevelKeys: string[];
|
|
57
|
+
/** Hook event names rejected by a harness with a strict event schema. */
|
|
58
|
+
invalidEventKeys: string[];
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
56
61
|
* Pure diff of one settings object against one harness spec. Read-only inverse
|
|
@@ -63,6 +68,9 @@ export interface HarnessWiringStatus {
|
|
|
63
68
|
settingsFile: string;
|
|
64
69
|
missing: HookEvent[];
|
|
65
70
|
orphans: string[];
|
|
71
|
+
invalidTopLevelKeys: string[];
|
|
72
|
+
invalidEventKeys: string[];
|
|
73
|
+
parseError?: string;
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
68
76
|
* Inspect every harness whose settings file exists under `projectRoot` and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wiring.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5C;AACD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAE3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAMxD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEnF;AAQD,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"wiring.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5C;AACD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAE3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAMxD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEnF;AAQD,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CA2ChF;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,SAAS,CAAC;IACnB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAwC5E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAkBvC"}
|
|
@@ -65,7 +65,23 @@ export function diffWiring(settings, spec) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
const invalidTopLevelKeys = spec.allowedTopLevelKeys
|
|
69
|
+
? Object.keys(settings)
|
|
70
|
+
.filter((key) => !spec.allowedTopLevelKeys.includes(key))
|
|
71
|
+
.sort()
|
|
72
|
+
: [];
|
|
73
|
+
const invalidEventKeys = spec.allowedEventKeys
|
|
74
|
+
? Object.keys(hooks)
|
|
75
|
+
.filter((key) => !spec.allowedEventKeys.includes(key))
|
|
76
|
+
.sort()
|
|
77
|
+
: [];
|
|
78
|
+
return {
|
|
79
|
+
missing,
|
|
80
|
+
present,
|
|
81
|
+
orphans: [...orphans].sort(),
|
|
82
|
+
invalidTopLevelKeys,
|
|
83
|
+
invalidEventKeys,
|
|
84
|
+
};
|
|
69
85
|
}
|
|
70
86
|
/**
|
|
71
87
|
* Inspect every harness whose settings file exists under `projectRoot` and
|
|
@@ -88,21 +104,34 @@ export function loadHarnessWiring(projectRoot) {
|
|
|
88
104
|
try {
|
|
89
105
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
90
106
|
}
|
|
91
|
-
catch {
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
catch (error) {
|
|
108
|
+
out.push({
|
|
109
|
+
harness: id,
|
|
110
|
+
settingsFile: spec.settingsFile,
|
|
111
|
+
missing: [],
|
|
112
|
+
orphans: [],
|
|
113
|
+
invalidTopLevelKeys: [],
|
|
114
|
+
invalidEventKeys: [],
|
|
115
|
+
parseError: error.message,
|
|
116
|
+
});
|
|
94
117
|
continue;
|
|
95
118
|
}
|
|
96
119
|
const diff = diffWiring(settings, spec);
|
|
97
120
|
if (diff.present.length === 0)
|
|
98
121
|
continue; // not opted in → not drift
|
|
99
|
-
if (diff.missing.length === 0 &&
|
|
122
|
+
if (diff.missing.length === 0 &&
|
|
123
|
+
diff.orphans.length === 0 &&
|
|
124
|
+
diff.invalidTopLevelKeys.length === 0 &&
|
|
125
|
+
diff.invalidEventKeys.length === 0) {
|
|
100
126
|
continue; // current
|
|
127
|
+
}
|
|
101
128
|
out.push({
|
|
102
129
|
harness: id,
|
|
103
130
|
settingsFile: spec.settingsFile,
|
|
104
131
|
missing: diff.missing,
|
|
105
132
|
orphans: diff.orphans,
|
|
133
|
+
invalidTopLevelKeys: diff.invalidTopLevelKeys,
|
|
134
|
+
invalidEventKeys: diff.invalidEventKeys,
|
|
106
135
|
});
|
|
107
136
|
}
|
|
108
137
|
return out;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/scratch/index.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,kBAAkB,iFAQrB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAC5C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,QACyF,CAAC;AAEtH,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAID,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtD;AAeD,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,CAAC,GAAE,IAAiB,GAAG,MAAM,CAW1D;AAID,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CA+DtE;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAkBxD;AAmBD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,SAAoB,GAAG,SAAS,EAAE,CA2DrF;AAID,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,GACX,UAAU,CAgEZ;AAID,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhE;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,SAAyB,GAAG,MAAM,CAkBnE;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAajD;AAED,qDAAqD;AACrD,wBAAgB,YAAY,IAAI;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,EAAE,CAWF;AAID,wBAAgB,mBAAmB,IAAI,MAAM,CAM5C;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAIjE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB9D"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { resolveMachineLabel } from "../../lib/machine.js";
|
|
4
|
+
import { monorepoRoot, readHeartbeat, resolveOwner } from "../agents/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* Agent scratchpad: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
|
|
7
7
|
*
|