harnery 0.31.6 → 0.32.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- 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/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.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +7 -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/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- 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 +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -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/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/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/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 +169 -27
- 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 +27 -0
- 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/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/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/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 +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- 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/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +10 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- 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 +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- 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/output.ts +9 -4
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- 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 +27 -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/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/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Refuse only when another process is still alive, or when cached remote
|
|
13
13
|
* presence reports the name.
|
|
14
14
|
*/
|
|
15
|
-
export type IdentityAssumeErrorCode = "identity_busy" | "identity_in_use" | "identity_not_found" | "invalid_identity" | "no_heartbeat" | "not_session" | "projection_failed";
|
|
15
|
+
export type IdentityAssumeErrorCode = "identity_busy" | "identity_in_use" | "identity_is_ancestor" | "identity_not_found" | "invalid_identity" | "no_heartbeat" | "not_session" | "projection_failed";
|
|
16
16
|
export declare class IdentityAssumeError extends Error {
|
|
17
17
|
readonly code: IdentityAssumeErrorCode;
|
|
18
18
|
constructor(code: IdentityAssumeErrorCode, message: string);
|
|
@@ -47,5 +47,7 @@ export declare function findIdentityConflict(coordRoot: string, instanceId: stri
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function reclaimAbandonedLocalConflict(coordRoot: string, conflict: IdentityConflict): boolean;
|
|
49
49
|
/** Assume `target` for one live main session. Safe to retry. */
|
|
50
|
-
export declare function assumeIdentity(coordRoot: string, instanceId: string, target: string
|
|
50
|
+
export declare function assumeIdentity(coordRoot: string, instanceId: string, target: string, opts?: {
|
|
51
|
+
forceAncestor?: boolean;
|
|
52
|
+
}): IdentityAssumeResult;
|
|
51
53
|
//# sourceMappingURL=assume.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assume.d.ts","sourceRoot":"","sources":["../../../src/lib/identities/assume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA2BH,MAAM,MAAM,uBAAuB,GAC/B,eAAe,GACf,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,cAAc,GACd,aAAa,GACb,mBAAmB,CAAC;AAExB,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,uBAAuB;gBAA7B,IAAI,EAAE,uBAAuB,EAC7C,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,+DAA+D;IAC/D,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAOD;+EAC+E;AAC/E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,SAAa,GACjB,gBAAgB,GAAG,IAAI,CAiCzB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CA2CT;AAyED,gEAAgE;AAChE,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"assume.d.ts","sourceRoot":"","sources":["../../../src/lib/identities/assume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA2BH,MAAM,MAAM,uBAAuB,GAC/B,eAAe,GACf,iBAAiB,GACjB,sBAAsB,GACtB,oBAAoB,GACpB,kBAAkB,GAClB,cAAc,GACd,aAAa,GACb,mBAAmB,CAAC;AAExB,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,uBAAuB;gBAA7B,IAAI,EAAE,uBAAuB,EAC7C,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,+DAA+D;IAC/D,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAOD;+EAC+E;AAC/E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,SAAa,GACjB,gBAAgB,GAAG,IAAI,CAiCzB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CA2CT;AAyED,gEAAgE;AAChE,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACjC,oBAAoB,CA4HtB"}
|
|
@@ -18,7 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
import { emitAndProject } from "../../core/agents/cli-emit.js";
|
|
19
19
|
import { emit } from "../../core/agents/events/emit.js";
|
|
20
20
|
import { readHeartbeat } from "../../core/agents/state/heartbeat-writer.js";
|
|
21
|
-
import { recordNameAssumption } from "../../core/agents/state/names.js";
|
|
21
|
+
import { recordNameAssumption, resolveForkAncestry } from "../../core/agents/state/names.js";
|
|
22
22
|
import { instanceHasLivePid, removePidmapRowsForInstance } from "../../core/agents/state/pidmap.js";
|
|
23
23
|
import { coordFreshnessSeconds } from "../../core/config.js";
|
|
24
24
|
import { readRemoteMachines } from "../../core/presence/index.js";
|
|
@@ -194,7 +194,7 @@ function adapterOf(platform) {
|
|
|
194
194
|
return "claude-code";
|
|
195
195
|
}
|
|
196
196
|
/** Assume `target` for one live main session. Safe to retry. */
|
|
197
|
-
export function assumeIdentity(coordRoot, instanceId, target) {
|
|
197
|
+
export function assumeIdentity(coordRoot, instanceId, target, opts) {
|
|
198
198
|
const release = acquireLock(coordRoot);
|
|
199
199
|
try {
|
|
200
200
|
const hb = readHeartbeat(coordRoot, instanceId);
|
|
@@ -205,6 +205,19 @@ export function assumeIdentity(coordRoot, instanceId, target) {
|
|
|
205
205
|
throw new IdentityAssumeError("not_session", `identity assumption is limited to main sessions (current kind: ${hb.kind})`);
|
|
206
206
|
}
|
|
207
207
|
const targetIdentity = resolveTarget(coordRoot, target);
|
|
208
|
+
// Recorded-fork-lineage guard: a branched session inherits a transcript
|
|
209
|
+
// full of its ancestor's name, so "assume <ancestor>" is far more likely
|
|
210
|
+
// a confused fork trusting its scrollback than a legitimate role handoff.
|
|
211
|
+
// The liveness check below cannot catch this once the ancestor exits;
|
|
212
|
+
// lineage can. --force-ancestor is the deliberate-successor escape hatch.
|
|
213
|
+
if (!opts?.forceAncestor) {
|
|
214
|
+
const ancestor = resolveForkAncestry(coordRoot, instanceId).find((a) => (a.name ?? "").toLowerCase() === targetIdentity.name.toLowerCase());
|
|
215
|
+
if (ancestor) {
|
|
216
|
+
throw new IdentityAssumeError("identity_is_ancestor", `agent-${targetIdentity.name} is this session's fork ancestor (${ancestor.instance_id}): ` +
|
|
217
|
+
`this conversation was branched from that session, so its name in your context is ` +
|
|
218
|
+
`inherited history, not your role. If you are deliberately succeeding it, rerun with --force-ancestor.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
208
221
|
let reclaimedInstanceId = null;
|
|
209
222
|
let conflict = findIdentityConflict(coordRoot, instanceId, targetIdentity.name);
|
|
210
223
|
if (conflict && reclaimAbandonedLocalConflict(coordRoot, conflict)) {
|
|
@@ -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
|
@@ -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,
|
package/src/commander.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { registerArtifactsCommand } from "./commands/artifacts.ts";
|
|
|
24
24
|
import { registerBackupCommand } from "./commands/backup.ts";
|
|
25
25
|
import { registerBrowseCommand } from "./commands/browse.ts";
|
|
26
26
|
import { registerBrowseAiCommand } from "./commands/browse-ai.ts";
|
|
27
|
+
import { registerBrowseSessionCommand } from "./commands/browse-session.ts";
|
|
27
28
|
import { registerCallersCommand } from "./commands/callers.ts";
|
|
28
29
|
import { registerCheckpointCommand } from "./commands/checkpoint.ts";
|
|
29
30
|
import { registerClaudeDesktopCommand } from "./commands/claude-desktop.ts";
|
|
@@ -280,6 +281,7 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
|
|
|
280
281
|
registerFetchCommand(program, emit, opts.context);
|
|
281
282
|
registerReadCommand(program, emit);
|
|
282
283
|
registerBrowseCommand(program, emit, opts.context);
|
|
284
|
+
registerBrowseSessionCommand(program, emit);
|
|
283
285
|
registerBrowseAiCommand(program, emit);
|
|
284
286
|
registerCompletionCommand(program, emit, opts.context);
|
|
285
287
|
registerCheckpointCommand(program, emit, opts.context);
|