harnery 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +8 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +37 -0
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/grep.d.ts +35 -2
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +427 -139
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +82 -0
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +14 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +24 -4
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +24 -7
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +77 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +210 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +309 -66
- package/dist/core/hooks/events/schema.d.ts +42 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +4 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +108 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +349 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +21 -0
- package/dist/core/workflow/spawn-claude.d.ts +25 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +103 -0
- package/dist/core/workflow/spawn-codex.d.ts +22 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +88 -0
- package/dist/core/workflow/spawn-cursor.d.ts +29 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +91 -0
- package/dist/core/workflow/types.d.ts +152 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +8 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +44 -0
- package/src/commands/eml.ts +22 -4
- package/src/commands/grep.ts +535 -142
- package/src/commands/harness.ts +147 -0
- package/src/commands/init.ts +138 -17
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +123 -0
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +15 -6
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/claim-conflict.ts +26 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +23 -2
- package/src/core/agents/state/heartbeat-writer.ts +33 -7
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +271 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +361 -65
- package/src/core/hooks/events/schema.ts +72 -0
- package/src/core/hooks/harness/events.ts +11 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +152 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +403 -0
- package/src/core/workflow/harnesses.ts +33 -0
- package/src/core/workflow/spawn-claude.ts +118 -0
- package/src/core/workflow/spawn-codex.ts +93 -0
- package/src/core/workflow/spawn-cursor.ts +108 -0
- package/src/core/workflow/types.ts +160 -0
- package/src/core/workflow/validate.ts +75 -0
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -20,6 +20,7 @@ export const CLAUDE_CODE_EVENTS = [
|
|
|
20
20
|
{ settingsKey: "PreToolUse", subcommand: "pre-tool-use" },
|
|
21
21
|
{ settingsKey: "PostToolUse", subcommand: "post-tool-use" },
|
|
22
22
|
{ settingsKey: "PostToolUseFailure", subcommand: "post-tool-use-failure" },
|
|
23
|
+
{ settingsKey: "PreCompact", subcommand: "pre-compact" },
|
|
23
24
|
];
|
|
24
25
|
/**
|
|
25
26
|
* Cursor: `.cursor/hooks.json`. camelCase event keys; flat `{ command }` entries;
|
|
@@ -45,6 +46,8 @@ export const CODEX_EVENTS = [
|
|
|
45
46
|
{ settingsKey: "UserPromptSubmit", subcommand: "user-prompt-submit" },
|
|
46
47
|
{ settingsKey: "SubagentStart", subcommand: "sub-agent-start" },
|
|
47
48
|
{ settingsKey: "SubagentStop", subcommand: "sub-agent-stop" },
|
|
49
|
+
{ settingsKey: "PreCompact", subcommand: "pre-compact" },
|
|
50
|
+
{ settingsKey: "PostCompact", subcommand: "post-compact" },
|
|
48
51
|
{ settingsKey: "Stop", subcommand: "stop" },
|
|
49
52
|
];
|
|
50
53
|
/** Entries written by harnery before Codex adopted a strict native hook schema. */
|
|
@@ -72,6 +75,7 @@ export const HARNESS_SPECS = {
|
|
|
72
75
|
settingsFile: ".claude/settings.json",
|
|
73
76
|
events: CLAUDE_CODE_EVENTS,
|
|
74
77
|
entryShape: "claude",
|
|
78
|
+
projectDirEnv: "CLAUDE_PROJECT_DIR",
|
|
75
79
|
},
|
|
76
80
|
cursor: {
|
|
77
81
|
settingsFile: ".cursor/hooks.json",
|
|
@@ -63,5 +63,5 @@ export declare function normalizeEventName(eventName: string): {
|
|
|
63
63
|
event_type: NormalizedEventType;
|
|
64
64
|
intra_turn: boolean;
|
|
65
65
|
} | null;
|
|
66
|
-
export type NormalizedEventType = "session.start" | "session.end" | "user_prompt.submit" | "turn.stop" | "subagent.start" | "subagent.stop" | "tool.pre_use" | "tool.post_use" | "tool.post_use_failure";
|
|
66
|
+
export type NormalizedEventType = "session.start" | "session.end" | "user_prompt.submit" | "turn.stop" | "subagent.start" | "subagent.stop" | "tool.pre_use" | "tool.post_use" | "tool.post_use_failure" | "context.compaction.started" | "context.compaction.completed";
|
|
67
67
|
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkCjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/harness/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkCjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAiCjE;AAED,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,cAAc,GACd,eAAe,GACf,uBAAuB,GACvB,4BAA4B,GAC5B,8BAA8B,CAAC"}
|
|
@@ -126,6 +126,10 @@ export function normalizeEventName(eventName) {
|
|
|
126
126
|
return { event_type: "tool.post_use", intra_turn: true };
|
|
127
127
|
case "post-tool-use-failure":
|
|
128
128
|
return { event_type: "tool.post_use_failure", intra_turn: true };
|
|
129
|
+
case "pre-compact":
|
|
130
|
+
return { event_type: "context.compaction.started", intra_turn: false };
|
|
131
|
+
case "post-compact":
|
|
132
|
+
return { event_type: "context.compaction.completed", intra_turn: false };
|
|
129
133
|
default:
|
|
130
134
|
return null;
|
|
131
135
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Walk up from `start` looking for a directory containing `.harnery/`. The
|
|
3
3
|
* single coord-root resolution so every adapter agrees on the same root.
|
|
4
|
+
*
|
|
5
|
+
* Resolution precedence:
|
|
6
|
+
* 1. HARNERY_COORD_ROOT_OVERRIDE — explicit pin (tests, git hooks).
|
|
7
|
+
* 2. The harness's project dir (CLAUDE_PROJECT_DIR) — hook processes
|
|
8
|
+
* inherit the session's *shell* cwd, which follows `cd` into
|
|
9
|
+
* subdirectories/submodules that may carry a `.harnery/` of their own
|
|
10
|
+
* (or none at all, e.g. a scratch dir under /tmp). The session's
|
|
11
|
+
* coordination home is the project the harness opened, not wherever the
|
|
12
|
+
* shell happens to sit, so a project dir that resolves to a coord root
|
|
13
|
+
* wins over the cwd walk.
|
|
14
|
+
* 3. Walk up from `start` (default cwd).
|
|
4
15
|
*/
|
|
5
16
|
export declare function findCoordRoot(start?: string): string | null;
|
|
6
17
|
//# sourceMappingURL=coord-root.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coord-root.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/coord-root.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"coord-root.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/coord-root.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAiB1E"}
|
|
@@ -4,17 +4,39 @@ import { coordEnv } from "../../../lib/env.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Walk up from `start` looking for a directory containing `.harnery/`. The
|
|
6
6
|
* single coord-root resolution so every adapter agrees on the same root.
|
|
7
|
+
*
|
|
8
|
+
* Resolution precedence:
|
|
9
|
+
* 1. HARNERY_COORD_ROOT_OVERRIDE — explicit pin (tests, git hooks).
|
|
10
|
+
* 2. The harness's project dir (CLAUDE_PROJECT_DIR) — hook processes
|
|
11
|
+
* inherit the session's *shell* cwd, which follows `cd` into
|
|
12
|
+
* subdirectories/submodules that may carry a `.harnery/` of their own
|
|
13
|
+
* (or none at all, e.g. a scratch dir under /tmp). The session's
|
|
14
|
+
* coordination home is the project the harness opened, not wherever the
|
|
15
|
+
* shell happens to sit, so a project dir that resolves to a coord root
|
|
16
|
+
* wins over the cwd walk.
|
|
17
|
+
* 3. Walk up from `start` (default cwd).
|
|
7
18
|
*/
|
|
8
19
|
export function findCoordRoot(start = process.cwd()) {
|
|
9
|
-
// HARNERY_COORD_ROOT_OVERRIDE:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
20
|
+
// HARNERY_COORD_ROOT_OVERRIDE: explicit pin matched by agent-coord and the
|
|
21
|
+
// bash side. Lets the sandboxed coord-test suite run against a temp
|
|
22
|
+
// directory rather than the real monorepo root. agent-hook's session.start
|
|
23
|
+
// handler may be the FIRST thing to create .harnery/ in a fresh sandbox,
|
|
24
|
+
// so the override is honored unconditionally (we don't require .harnery/
|
|
25
|
+
// to pre-exist).
|
|
15
26
|
const override = coordEnv("COORD_ROOT_OVERRIDE");
|
|
16
27
|
if (override)
|
|
17
28
|
return override;
|
|
29
|
+
// Claude Code exports CLAUDE_PROJECT_DIR to every hook process; other
|
|
30
|
+
// harnesses don't set it, so this is inert outside claude-code hooks.
|
|
31
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR;
|
|
32
|
+
if (projectDir) {
|
|
33
|
+
const fromProject = walkUp(projectDir);
|
|
34
|
+
if (fromProject)
|
|
35
|
+
return fromProject;
|
|
36
|
+
}
|
|
37
|
+
return walkUp(start);
|
|
38
|
+
}
|
|
39
|
+
function walkUp(start) {
|
|
18
40
|
let dir = resolve(start);
|
|
19
41
|
while (true) {
|
|
20
42
|
if (existsSync(join(dir, ".harnery")))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence blob: the per-machine payload published to
|
|
3
|
+
* `refs/harnery/presence/<machine>` (ADR 0016). One blob describes every live
|
|
4
|
+
* session on this machine; peers render it in their agents list / status box /
|
|
5
|
+
* peer table, labeled by machine.
|
|
6
|
+
*/
|
|
7
|
+
export interface PresenceAgent {
|
|
8
|
+
instance_id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
kind?: string;
|
|
11
|
+
session_id?: string;
|
|
12
|
+
platform?: string;
|
|
13
|
+
task?: string;
|
|
14
|
+
turn_summary?: string;
|
|
15
|
+
files_touched?: string[];
|
|
16
|
+
last_tool?: string;
|
|
17
|
+
started_at?: string;
|
|
18
|
+
last_heartbeat?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PresenceBlob {
|
|
21
|
+
v: 1;
|
|
22
|
+
machine: string;
|
|
23
|
+
published_at: string;
|
|
24
|
+
agents: PresenceAgent[];
|
|
25
|
+
}
|
|
26
|
+
export interface BuiltBlob {
|
|
27
|
+
blob: PresenceBlob;
|
|
28
|
+
/** Stable hash of the semantically-relevant projection — the change
|
|
29
|
+
* detector that decides whether a publish is worth a push. */
|
|
30
|
+
basisHash: string;
|
|
31
|
+
json: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build this machine's presence blob from `.harnery/active/` heartbeats.
|
|
35
|
+
* Includes live sessions + subagents; excludes kind=transient stubs (they are
|
|
36
|
+
* fold-artifacts of the local claim model, not sessions) and anything past the
|
|
37
|
+
* freshness window.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildPresenceBlob(coordRoot: string, now?: Date): BuiltBlob;
|
|
40
|
+
//# sourceMappingURL=blob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../../src/core/presence/blob.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,CAAC,CAAC;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB;kEAC8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,SAAS,CA6DtF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence blob: the per-machine payload published to
|
|
3
|
+
* `refs/harnery/presence/<machine>` (ADR 0016). One blob describes every live
|
|
4
|
+
* session on this machine; peers render it in their agents list / status box /
|
|
5
|
+
* peer table, labeled by machine.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { resolveMachineLabel } from "../../lib/machine.js";
|
|
11
|
+
/** Mirror of the local heartbeat freshness window (commands/agents.ts). */
|
|
12
|
+
const FRESHNESS_SECS = 600;
|
|
13
|
+
/** Caps that bound the blob to ~1-2KB regardless of local state. */
|
|
14
|
+
const MAX_AGENTS = 20;
|
|
15
|
+
const MAX_FILES_PER_AGENT = 10;
|
|
16
|
+
/**
|
|
17
|
+
* Build this machine's presence blob from `.harnery/active/` heartbeats.
|
|
18
|
+
* Includes live sessions + subagents; excludes kind=transient stubs (they are
|
|
19
|
+
* fold-artifacts of the local claim model, not sessions) and anything past the
|
|
20
|
+
* freshness window.
|
|
21
|
+
*/
|
|
22
|
+
export function buildPresenceBlob(coordRoot, now = new Date()) {
|
|
23
|
+
const machine = resolveMachineLabel();
|
|
24
|
+
const agents = [];
|
|
25
|
+
const activeDir = join(coordRoot, ".harnery", "active");
|
|
26
|
+
const cutoffMs = now.getTime() - FRESHNESS_SECS * 1000;
|
|
27
|
+
if (existsSync(activeDir)) {
|
|
28
|
+
for (const f of readdirSync(activeDir)) {
|
|
29
|
+
if (!f.endsWith(".json"))
|
|
30
|
+
continue;
|
|
31
|
+
let hb;
|
|
32
|
+
try {
|
|
33
|
+
hb = JSON.parse(readFileSync(join(activeDir, f), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (typeof hb.instance_id !== "string")
|
|
39
|
+
continue;
|
|
40
|
+
if (hb.kind === "transient")
|
|
41
|
+
continue;
|
|
42
|
+
const ts = Date.parse(hb.last_heartbeat ?? "");
|
|
43
|
+
if (!Number.isFinite(ts) || ts < cutoffMs)
|
|
44
|
+
continue;
|
|
45
|
+
agents.push({
|
|
46
|
+
instance_id: hb.instance_id,
|
|
47
|
+
name: strOr(hb.name),
|
|
48
|
+
kind: strOr(hb.kind),
|
|
49
|
+
session_id: strOr(hb.session_id),
|
|
50
|
+
platform: strOr(hb.platform),
|
|
51
|
+
task: strOr(hb.task),
|
|
52
|
+
turn_summary: clamp(strOr(hb.turn_summary), 160),
|
|
53
|
+
files_touched: Array.isArray(hb.files_touched)
|
|
54
|
+
? hb.files_touched.slice(0, MAX_FILES_PER_AGENT)
|
|
55
|
+
: undefined,
|
|
56
|
+
last_tool: strOr(hb.last_tool),
|
|
57
|
+
started_at: strOr(hb.started_at),
|
|
58
|
+
last_heartbeat: strOr(hb.last_heartbeat),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
agents.sort((a, b) => a.instance_id.localeCompare(b.instance_id));
|
|
63
|
+
const capped = agents.slice(0, MAX_AGENTS);
|
|
64
|
+
const blob = {
|
|
65
|
+
v: 1,
|
|
66
|
+
machine,
|
|
67
|
+
published_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
68
|
+
agents: capped,
|
|
69
|
+
};
|
|
70
|
+
// Basis: the fields whose change should trigger a re-publish. Timestamps
|
|
71
|
+
// and last_tool are excluded (pure churn); task/turn_summary/files are the
|
|
72
|
+
// signal peers actually read.
|
|
73
|
+
const basis = capped.map((a) => ({
|
|
74
|
+
i: a.instance_id,
|
|
75
|
+
n: a.name ?? null,
|
|
76
|
+
k: a.kind ?? null,
|
|
77
|
+
t: a.task ?? null,
|
|
78
|
+
s: a.turn_summary ?? null,
|
|
79
|
+
f: [...(a.files_touched ?? [])].sort(),
|
|
80
|
+
}));
|
|
81
|
+
const basisHash = createHash("sha256").update(JSON.stringify(basis)).digest("hex").slice(0, 16);
|
|
82
|
+
return { blob, basisHash, json: JSON.stringify(blob) };
|
|
83
|
+
}
|
|
84
|
+
function strOr(v) {
|
|
85
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
86
|
+
}
|
|
87
|
+
function clamp(v, max) {
|
|
88
|
+
if (!v)
|
|
89
|
+
return undefined;
|
|
90
|
+
return v.length > max ? v.slice(0, max) : v;
|
|
91
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git plumbing for the cross-machine presence transport (ADR 0016).
|
|
3
|
+
*
|
|
4
|
+
* Presence blobs travel as parentless commits (empty tree, JSON message)
|
|
5
|
+
* force-pushed to `refs/harnery/presence/<machine>` on the host repo's own
|
|
6
|
+
* `origin`. Non-branch refs: invisible to branch tooling, safe to force-push,
|
|
7
|
+
* no history accumulation. The remote is the rendezvous every install already
|
|
8
|
+
* shares, and repo access is the ACL.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is best-effort and fail-silent by design — presence must
|
|
11
|
+
* never break a hook or a render. Network operations (push/fetch) run either
|
|
12
|
+
* detached (hook path: never block a turn) or synchronously (CLI path: the
|
|
13
|
+
* user wants to see errors).
|
|
14
|
+
*/
|
|
15
|
+
/** The ref namespace. Hardcoded — never interpolated from config — so the
|
|
16
|
+
* publisher is structurally unable to touch refs/heads/* (ADR 0016). */
|
|
17
|
+
export declare const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
|
|
18
|
+
/** Does the repo have an `origin` remote to rendezvous on? */
|
|
19
|
+
export declare function hasOrigin(root: string): boolean;
|
|
20
|
+
/** The origin remote URL, or null. Input to relay room derivation. */
|
|
21
|
+
export declare function originUrl(root: string): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* The repo's root-commit SHA — the stable repo-identity input to relay room
|
|
24
|
+
* derivation. Repos can have multiple roots (merged histories); the list is
|
|
25
|
+
* sorted so every clone picks the same one. Null when the repo has no commits.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rootCommitSha(root: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Sanitize a machine label into a valid single ref component: lowercase,
|
|
30
|
+
* [a-z0-9._-] only, no leading dot, no `.lock` suffix, never empty.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sanitizeRefComponent(label: string): string;
|
|
33
|
+
/** Build the parentless presence commit carrying `message` (the JSON blob).
|
|
34
|
+
* Returns the commit sha, or null on failure. Local-only, fast (~10ms). */
|
|
35
|
+
export declare function writePresenceCommit(root: string, message: string): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Force-push a presence commit to `refs/harnery/presence/<machine>`.
|
|
38
|
+
* `sync: true` (CLI/tests) waits and reports; default is detached fire-and-
|
|
39
|
+
* forget (hook path — a slow or credential-less push must not block a turn).
|
|
40
|
+
*/
|
|
41
|
+
export declare function pushPresenceRef(root: string, machine: string, sha: string, opts?: {
|
|
42
|
+
sync?: boolean;
|
|
43
|
+
}): {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
error?: string;
|
|
46
|
+
};
|
|
47
|
+
/** Fetch every peer presence ref from origin into the local ref namespace. */
|
|
48
|
+
export declare function fetchPresenceRefs(root: string, opts?: {
|
|
49
|
+
sync?: boolean;
|
|
50
|
+
}): {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
error?: string;
|
|
53
|
+
};
|
|
54
|
+
/** Read every locally-known presence ref → [{ref, machine, message}]. */
|
|
55
|
+
export declare function readPresenceRefs(root: string): Array<{
|
|
56
|
+
machine: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Parse `for-each-ref --format='%(refname)%00%(contents)%01'` output.
|
|
61
|
+
* Records are %01-terminated (contents may contain newlines); refname and
|
|
62
|
+
* contents are %00-separated. Exported for unit tests.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseForEachRefOutput(out: string): Array<{
|
|
65
|
+
machine: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}>;
|
|
68
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/presence/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;wEACwE;AACxE,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAwC5D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;2EAC2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcjC;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcjC;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ1F;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe9F"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git plumbing for the cross-machine presence transport (ADR 0016).
|
|
3
|
+
*
|
|
4
|
+
* Presence blobs travel as parentless commits (empty tree, JSON message)
|
|
5
|
+
* force-pushed to `refs/harnery/presence/<machine>` on the host repo's own
|
|
6
|
+
* `origin`. Non-branch refs: invisible to branch tooling, safe to force-push,
|
|
7
|
+
* no history accumulation. The remote is the rendezvous every install already
|
|
8
|
+
* shares, and repo access is the ACL.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is best-effort and fail-silent by design — presence must
|
|
11
|
+
* never break a hook or a render. Network operations (push/fetch) run either
|
|
12
|
+
* detached (hook path: never block a turn) or synchronously (CLI path: the
|
|
13
|
+
* user wants to see errors).
|
|
14
|
+
*/
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
+
/** The ref namespace. Hardcoded — never interpolated from config — so the
|
|
17
|
+
* publisher is structurally unable to touch refs/heads/* (ADR 0016). */
|
|
18
|
+
export const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
|
|
19
|
+
/** Well-known SHA of git's empty tree; `git mktree` below guarantees the
|
|
20
|
+
* object exists in the local odb before we build a commit on it. */
|
|
21
|
+
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
22
|
+
const GIT_TIMEOUT_MS = 8000;
|
|
23
|
+
/** Non-interactive env for every presence git call: never prompt for creds
|
|
24
|
+
* (a hook must not hang on a password), and a fixed identity so commit-tree
|
|
25
|
+
* works on machines with no user.name/user.email configured. */
|
|
26
|
+
function gitEnv() {
|
|
27
|
+
return {
|
|
28
|
+
...process.env,
|
|
29
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
30
|
+
GIT_AUTHOR_NAME: "harnery-presence",
|
|
31
|
+
GIT_AUTHOR_EMAIL: "presence@harnery.invalid",
|
|
32
|
+
GIT_COMMITTER_NAME: "harnery-presence",
|
|
33
|
+
GIT_COMMITTER_EMAIL: "presence@harnery.invalid",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function runGit(root, args, input) {
|
|
37
|
+
try {
|
|
38
|
+
const r = spawnSync("git", ["-C", root, ...args], {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
timeout: GIT_TIMEOUT_MS,
|
|
41
|
+
env: gitEnv(),
|
|
42
|
+
...(input !== undefined ? { input } : {}),
|
|
43
|
+
});
|
|
44
|
+
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { ok: false, stdout: "", stderr: "spawn failed" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Does the repo have an `origin` remote to rendezvous on? */
|
|
51
|
+
export function hasOrigin(root) {
|
|
52
|
+
return runGit(root, ["remote", "get-url", "origin"]).ok;
|
|
53
|
+
}
|
|
54
|
+
/** The origin remote URL, or null. Input to relay room derivation. */
|
|
55
|
+
export function originUrl(root) {
|
|
56
|
+
const r = runGit(root, ["remote", "get-url", "origin"]);
|
|
57
|
+
const url = r.stdout.trim();
|
|
58
|
+
return r.ok && url ? url : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The repo's root-commit SHA — the stable repo-identity input to relay room
|
|
62
|
+
* derivation. Repos can have multiple roots (merged histories); the list is
|
|
63
|
+
* sorted so every clone picks the same one. Null when the repo has no commits.
|
|
64
|
+
*/
|
|
65
|
+
export function rootCommitSha(root) {
|
|
66
|
+
const r = runGit(root, ["rev-list", "--max-parents=0", "HEAD"]);
|
|
67
|
+
if (!r.ok)
|
|
68
|
+
return null;
|
|
69
|
+
const shas = r.stdout
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((l) => l.trim())
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.sort();
|
|
74
|
+
return shas[0] ?? null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sanitize a machine label into a valid single ref component: lowercase,
|
|
78
|
+
* [a-z0-9._-] only, no leading dot, no `.lock` suffix, never empty.
|
|
79
|
+
*/
|
|
80
|
+
export function sanitizeRefComponent(label) {
|
|
81
|
+
let s = label
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
84
|
+
.replace(/\.{2,}/g, ".")
|
|
85
|
+
.replace(/^[.-]+/, "")
|
|
86
|
+
.replace(/\.lock$/, "");
|
|
87
|
+
if (!s)
|
|
88
|
+
s = "unknown";
|
|
89
|
+
return s;
|
|
90
|
+
}
|
|
91
|
+
/** Build the parentless presence commit carrying `message` (the JSON blob).
|
|
92
|
+
* Returns the commit sha, or null on failure. Local-only, fast (~10ms). */
|
|
93
|
+
export function writePresenceCommit(root, message) {
|
|
94
|
+
// Ensure the empty-tree object exists in this odb (mktree with empty stdin
|
|
95
|
+
// writes it idempotently).
|
|
96
|
+
const mk = runGit(root, ["mktree"], "");
|
|
97
|
+
const tree = mk.ok && mk.stdout.trim() ? mk.stdout.trim() : EMPTY_TREE_SHA;
|
|
98
|
+
const r = runGit(root, ["commit-tree", tree, "-m", message]);
|
|
99
|
+
return r.ok && r.stdout.trim() ? r.stdout.trim() : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Force-push a presence commit to `refs/harnery/presence/<machine>`.
|
|
103
|
+
* `sync: true` (CLI/tests) waits and reports; default is detached fire-and-
|
|
104
|
+
* forget (hook path — a slow or credential-less push must not block a turn).
|
|
105
|
+
*/
|
|
106
|
+
export function pushPresenceRef(root, machine, sha, opts = {}) {
|
|
107
|
+
const refspec = `+${sha}:${PRESENCE_REF_PREFIX}${sanitizeRefComponent(machine)}`;
|
|
108
|
+
const args = ["-C", root, "push", "--quiet", "--no-verify", "origin", refspec];
|
|
109
|
+
if (opts.sync) {
|
|
110
|
+
const r = runGit(root, args.slice(2));
|
|
111
|
+
return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
|
|
115
|
+
child.unref();
|
|
116
|
+
return { ok: true };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { ok: false, error: "spawn failed" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Fetch every peer presence ref from origin into the local ref namespace. */
|
|
123
|
+
export function fetchPresenceRefs(root, opts = {}) {
|
|
124
|
+
const refspec = `+${PRESENCE_REF_PREFIX}*:${PRESENCE_REF_PREFIX}*`;
|
|
125
|
+
const args = ["-C", root, "fetch", "--quiet", "--no-tags", "origin", refspec];
|
|
126
|
+
if (opts.sync) {
|
|
127
|
+
const r = runGit(root, args.slice(2));
|
|
128
|
+
return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
|
|
132
|
+
child.unref();
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { ok: false, error: "spawn failed" };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Read every locally-known presence ref → [{ref, machine, message}]. */
|
|
140
|
+
export function readPresenceRefs(root) {
|
|
141
|
+
const r = runGit(root, [
|
|
142
|
+
"for-each-ref",
|
|
143
|
+
"--format=%(refname)%00%(contents)%01",
|
|
144
|
+
`${PRESENCE_REF_PREFIX}*`,
|
|
145
|
+
]);
|
|
146
|
+
if (!r.ok)
|
|
147
|
+
return [];
|
|
148
|
+
return parseForEachRefOutput(r.stdout);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse `for-each-ref --format='%(refname)%00%(contents)%01'` output.
|
|
152
|
+
* Records are %01-terminated (contents may contain newlines); refname and
|
|
153
|
+
* contents are %00-separated. Exported for unit tests.
|
|
154
|
+
*/
|
|
155
|
+
export function parseForEachRefOutput(out) {
|
|
156
|
+
const rows = [];
|
|
157
|
+
for (const rec of out.split("\u0001")) {
|
|
158
|
+
const trimmed = rec.replace(/^\n+/, "");
|
|
159
|
+
if (!trimmed)
|
|
160
|
+
continue;
|
|
161
|
+
const sep = trimmed.indexOf("\u0000");
|
|
162
|
+
if (sep === -1)
|
|
163
|
+
continue;
|
|
164
|
+
const refname = trimmed.slice(0, sep).trim();
|
|
165
|
+
if (!refname.startsWith(PRESENCE_REF_PREFIX))
|
|
166
|
+
continue;
|
|
167
|
+
rows.push({
|
|
168
|
+
machine: refname.slice(PRESENCE_REF_PREFIX.length),
|
|
169
|
+
message: trimmed.slice(sep + 1).trim(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine presence (ADR 0016), phase 1: the git-refs transport.
|
|
3
|
+
*
|
|
4
|
+
* publishPresence(): serialize this machine's live sessions into a presence
|
|
5
|
+
* blob and force-push it to `refs/harnery/presence/<machine>` on origin —
|
|
6
|
+
* change-batched (only when the blob materially changed) with a keepalive
|
|
7
|
+
* (~5 min) so peers can distinguish "no change" from "machine gone".
|
|
8
|
+
*
|
|
9
|
+
* fetchPresence(): throttled fetch of every peer machine's presence ref.
|
|
10
|
+
*
|
|
11
|
+
* readRemoteMachines(): parse the locally-known refs into render-ready rows
|
|
12
|
+
* (self machine excluded, stale blobs dropped).
|
|
13
|
+
*
|
|
14
|
+
* All entry points are fail-silent and gated on `presenceEnabled()` — presence
|
|
15
|
+
* must never break a hook, a render, or a repo with no origin.
|
|
16
|
+
*/
|
|
17
|
+
import { type PresenceAgent } from "./blob.js";
|
|
18
|
+
export type { PresenceAgent, PresenceBlob } from "./blob.js";
|
|
19
|
+
export { buildPresenceBlob } from "./blob.js";
|
|
20
|
+
export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.js";
|
|
21
|
+
export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.js";
|
|
22
|
+
export type PublishResult = {
|
|
23
|
+
status: "published";
|
|
24
|
+
machine: string;
|
|
25
|
+
agents: number;
|
|
26
|
+
sha: string;
|
|
27
|
+
} | {
|
|
28
|
+
status: "skipped";
|
|
29
|
+
reason: string;
|
|
30
|
+
} | {
|
|
31
|
+
status: "error";
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
export type FetchResult = {
|
|
35
|
+
status: "fetched";
|
|
36
|
+
} | {
|
|
37
|
+
status: "skipped";
|
|
38
|
+
reason: string;
|
|
39
|
+
} | {
|
|
40
|
+
status: "error";
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Publish this machine's presence blob to origin if it changed (or the
|
|
45
|
+
* keepalive elapsed). `sync: true` pushes in-process and reports errors (CLI
|
|
46
|
+
* path); default spawns the push detached (hook path).
|
|
47
|
+
*/
|
|
48
|
+
export declare function publishPresence(coordRoot: string, opts?: {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
sync?: boolean;
|
|
51
|
+
}): PublishResult;
|
|
52
|
+
/** Throttled fetch of peer presence refs from origin. */
|
|
53
|
+
export declare function fetchPresence(coordRoot: string, opts?: {
|
|
54
|
+
force?: boolean;
|
|
55
|
+
sync?: boolean;
|
|
56
|
+
}): FetchResult;
|
|
57
|
+
export interface RemoteMachine {
|
|
58
|
+
machine: string;
|
|
59
|
+
published_at: string;
|
|
60
|
+
age_secs: number;
|
|
61
|
+
agents: PresenceAgent[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Render-ready view of every OTHER machine's presence, merged from BOTH
|
|
65
|
+
* transports: the locally-fetched git refs (the floor) and the relay
|
|
66
|
+
* daemon's cache files at `.harnery/presence/remote/` (the live upgrade).
|
|
67
|
+
* Freshest source per machine wins. Self machine excluded; blobs older than
|
|
68
|
+
* the stale window or with zero agents dropped.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readRemoteMachines(coordRoot: string, opts?: {
|
|
71
|
+
includeSelf?: boolean;
|
|
72
|
+
includeStale?: boolean;
|
|
73
|
+
}): RemoteMachine[];
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,EAAqB,KAAK,aAAa,EAAqB,MAAM,WAAW,CAAC;AAUrF,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAYzF,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAwCvC;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,aAAa,CA2Cf;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,WAAW,CAsCb;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,aAAa,EAAE,CAuDjB"}
|