harnery 0.9.0 → 0.11.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 +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +6 -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 +2 -3
- 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/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- 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.map +1 -1
- package/dist/commands/workflow.js +53 -17
- 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 +4 -2
- 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/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- 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.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +11 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +3 -3
- 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 +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +191 -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 +280 -63
- package/dist/core/hooks/events/schema.d.ts +38 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +3 -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/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 +1 -1
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +7 -1
- package/dist/core/workflow/engine.d.ts +5 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +164 -16
- package/dist/core/workflow/harnesses.d.ts +3 -3
- package/dist/core/workflow/harnesses.d.ts.map +1 -1
- package/dist/core/workflow/harnesses.js +8 -17
- package/dist/core/workflow/index.d.ts +5 -0
- package/dist/core/workflow/index.d.ts.map +1 -0
- package/dist/core/workflow/index.js +3 -0
- package/dist/core/workflow/proof.d.ts +42 -0
- package/dist/core/workflow/proof.d.ts.map +1 -0
- package/dist/core/workflow/proof.js +342 -0
- package/dist/core/workflow/spawn-claude.d.ts +4 -1
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +39 -18
- package/dist/core/workflow/spawn-codex.d.ts +4 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +44 -22
- package/dist/core/workflow/spawn-cursor.d.ts +4 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +35 -16
- package/dist/core/workflow/types.d.ts +156 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +1 -1
- 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 +22 -6
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +6 -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 +2 -3
- package/src/commands/eml.ts +22 -4
- package/src/commands/harness.ts +147 -0
- 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 +62 -19
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +4 -2
- 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/commit-conflict.ts +4 -33
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +10 -1
- package/src/core/agents/state/heartbeat-writer.ts +3 -2
- 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 +248 -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 +331 -62
- package/src/core/hooks/events/schema.ts +68 -0
- package/src/core/hooks/harness/events.ts +3 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- 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 +8 -2
- package/src/core/workflow/engine.ts +182 -16
- package/src/core/workflow/harnesses.ts +14 -19
- package/src/core/workflow/index.ts +43 -0
- package/src/core/workflow/proof.ts +434 -0
- package/src/core/workflow/spawn-claude.ts +39 -20
- package/src/core/workflow/spawn-codex.ts +47 -28
- package/src/core/workflow/spawn-cursor.ts +37 -18
- package/src/core/workflow/types.ts +176 -2
- 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
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
`harnery` keeps multiple AI coding agents from stepping on each other in a shared checkout. It was extracted from years of running several Claude Code / Cursor / Codex sessions at once against the same monorepo:
|
|
21
21
|
|
|
22
22
|
- **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
|
|
23
|
+
- **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
|
|
23
24
|
- **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages.
|
|
24
25
|
- **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
|
|
25
26
|
- **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
|
package/dist/commander.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
|
package/dist/commander.js
CHANGED
|
@@ -22,6 +22,7 @@ import { registerBackupCommand } from "./commands/backup.js";
|
|
|
22
22
|
import { registerBrowseCommand } from "./commands/browse.js";
|
|
23
23
|
import { registerBrowseAiCommand } from "./commands/browse-ai.js";
|
|
24
24
|
import { registerCallersCommand } from "./commands/callers.js";
|
|
25
|
+
import { registerClaudeDesktopCommand } from "./commands/claude-desktop.js";
|
|
25
26
|
import { registerCompletionCommand } from "./commands/completion.js";
|
|
26
27
|
import { registerConfigGetCommand } from "./commands/config-get.js";
|
|
27
28
|
import { registerContextCommand } from "./commands/context.js";
|
|
@@ -37,10 +38,12 @@ import { registerEnvCommand } from "./commands/env.js";
|
|
|
37
38
|
import { registerFetchCommand } from "./commands/fetch.js";
|
|
38
39
|
import { registerFileHistoryCommand } from "./commands/file-history.js";
|
|
39
40
|
import { registerGrepCommand } from "./commands/grep.js";
|
|
41
|
+
import { registerHarnessCommand } from "./commands/harness.js";
|
|
40
42
|
import { registerInitCommand } from "./commands/init.js";
|
|
41
43
|
import { registerOutlineCommand } from "./commands/outline.js";
|
|
42
44
|
import { registerPresenceCommand } from "./commands/presence.js";
|
|
43
45
|
import { registerReadCommand } from "./commands/read.js";
|
|
46
|
+
import { registerRelayCommand } from "./commands/relay.js";
|
|
44
47
|
import { registerScratchCommand } from "./commands/scratch.js";
|
|
45
48
|
import { registerSessionCommand } from "./commands/session.js";
|
|
46
49
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
@@ -96,6 +99,7 @@ export function createHarneryProgram(opts = {}) {
|
|
|
96
99
|
registerEmlCommand(program, emit);
|
|
97
100
|
registerEnvCommand(program, emit, opts.context);
|
|
98
101
|
registerPresenceCommand(program, emit);
|
|
102
|
+
registerRelayCommand(program, emit);
|
|
99
103
|
registerConfigGetCommand(program, emit);
|
|
100
104
|
registerFileHistoryCommand(program, emit);
|
|
101
105
|
registerOutlineCommand(program, emit);
|
|
@@ -104,6 +108,7 @@ export function createHarneryProgram(opts = {}) {
|
|
|
104
108
|
registerCallersCommand(program, emit, opts.context);
|
|
105
109
|
registerEditBatchCommand(program, emit);
|
|
106
110
|
registerGrepCommand(program, emit, opts.context);
|
|
111
|
+
registerHarnessCommand(program, emit);
|
|
107
112
|
registerCookiesCommand(program, emit);
|
|
108
113
|
registerFetchCommand(program, emit, opts.context);
|
|
109
114
|
registerReadCommand(program, emit);
|
|
@@ -122,6 +127,7 @@ export function createHarneryProgram(opts = {}) {
|
|
|
122
127
|
registerInitCommand(program, emit, opts.binName);
|
|
123
128
|
registerDeinitCommand(program, emit, opts.binName);
|
|
124
129
|
registerBackupCommand(program, emit);
|
|
130
|
+
registerClaudeDesktopCommand(program, emit);
|
|
125
131
|
registerSyncCommand(program, emit);
|
|
126
132
|
registerWorkflowCommand(program, emit);
|
|
127
133
|
if (include("web"))
|
|
@@ -14,5 +14,29 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from "commander";
|
|
16
16
|
import type { EmitContext } from "../commander.js";
|
|
17
|
+
/**
|
|
18
|
+
* Spawn options for agent-coord child processes: pin the coord root the
|
|
19
|
+
* command already resolved (git-superproject-aware via `monorepoRoot()`), so
|
|
20
|
+
* the helper can't re-resolve a DIFFERENT root by walking up from a drifted
|
|
21
|
+
* shell cwd. The concrete failure this prevents: a shell cd'd into an
|
|
22
|
+
* embedded harnery checkout (which carries its own committed `.harnery/`)
|
|
23
|
+
* made agent-coord resolve that nested root and miss the session's real
|
|
24
|
+
* heartbeat — `set-task: no heartbeat at .harnery/active/<id>.json` while
|
|
25
|
+
* `status` (which resolves in-process) worked fine. Mirrors the hooks side's
|
|
26
|
+
* `childEnv()`; every agent-coord spawn must carry this.
|
|
27
|
+
*/
|
|
28
|
+
export declare function coordHelperOpts(root: string): {
|
|
29
|
+
cwd: string;
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
};
|
|
17
32
|
export declare function registerAgentsCommand(program: Command, emitParam: EmitContext): void;
|
|
33
|
+
/**
|
|
34
|
+
* Build the copy-pasteable session name from the coord identity + the agent's
|
|
35
|
+
* description parts. Pure (no coord-state reads) so it's unit-testable; collapses
|
|
36
|
+
* internal whitespace and trims. Returns null when the description is empty.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildSuggestedName(agentName: string, descriptionParts: string[]): {
|
|
39
|
+
suggestedName: string;
|
|
40
|
+
description: string;
|
|
41
|
+
} | null;
|
|
18
42
|
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAyEnD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,CAErF;AAgCD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA6PpF;AA0iCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EAAE,GACzB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvD"}
|
package/dist/commands/agents.js
CHANGED
|
@@ -18,7 +18,8 @@ import { homedir, tmpdir } from "node:os";
|
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
19
|
import { readStreamTailBounded } from "../core/agents/events/consume.js";
|
|
20
20
|
import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, } from "../core/agents/index.js";
|
|
21
|
-
import { resolveBinName } from "../core/config.js";
|
|
21
|
+
import { coordFreshnessSeconds, resolveBinName } from "../core/config.js";
|
|
22
|
+
import { readRemoteMachines } from "../core/presence/index.js";
|
|
22
23
|
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
23
24
|
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
24
25
|
* never throw; covers ample recent history for a diagnostic scan. */
|
|
@@ -26,11 +27,34 @@ const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
|
26
27
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
|
|
27
28
|
import { appendEntry, resolveOwnerByName } from "../core/scratch/index.js";
|
|
28
29
|
import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
|
|
30
|
+
import { assumeIdentity, IdentityAssumeError } from "../lib/identities/assume.js";
|
|
29
31
|
import { displayName as displayAgentName, ensureIdentity, listIdentities, lookupById as lookupIdentityById, lookupByName as lookupIdentityByName, } from "../lib/identities/index.js";
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Heartbeat-freshness window (seconds). Reads `coord.freshness_seconds` /
|
|
34
|
+
* `HARNERY_AGENT_COORD_FRESHNESS` via the shared accessor (defaults to 600 =
|
|
35
|
+
* 10 min). A function, not a const, so a config edit takes effect without a
|
|
36
|
+
* process restart (mtime-cached).
|
|
37
|
+
*/
|
|
38
|
+
function freshnessCutoffSecs() {
|
|
39
|
+
return coordFreshnessSeconds();
|
|
40
|
+
}
|
|
31
41
|
const SUBAGENT_NOTE = "Bash identity is process-level in v1; if you're running inside a subagent, " +
|
|
32
42
|
"this resolves to the parent group's name, not the subagent's. A subagent-aware " +
|
|
33
43
|
"bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
|
|
44
|
+
/**
|
|
45
|
+
* Spawn options for agent-coord child processes: pin the coord root the
|
|
46
|
+
* command already resolved (git-superproject-aware via `monorepoRoot()`), so
|
|
47
|
+
* the helper can't re-resolve a DIFFERENT root by walking up from a drifted
|
|
48
|
+
* shell cwd. The concrete failure this prevents: a shell cd'd into an
|
|
49
|
+
* embedded harnery checkout (which carries its own committed `.harnery/`)
|
|
50
|
+
* made agent-coord resolve that nested root and miss the session's real
|
|
51
|
+
* heartbeat — `set-task: no heartbeat at .harnery/active/<id>.json` while
|
|
52
|
+
* `status` (which resolves in-process) worked fine. Mirrors the hooks side's
|
|
53
|
+
* `childEnv()`; every agent-coord spawn must carry this.
|
|
54
|
+
*/
|
|
55
|
+
export function coordHelperOpts(root) {
|
|
56
|
+
return { cwd: root, env: { ...process.env, HARNERY_COORD_ROOT_OVERRIDE: root } };
|
|
57
|
+
}
|
|
34
58
|
function formatPlatformLabel(platform) {
|
|
35
59
|
if (platform === "cursor")
|
|
36
60
|
return "Cursor";
|
|
@@ -70,6 +94,17 @@ export function registerAgentsCommand(program, emitParam) {
|
|
|
70
94
|
.action((opts) => {
|
|
71
95
|
runStatus(opts);
|
|
72
96
|
});
|
|
97
|
+
cmd
|
|
98
|
+
.command("suggest-name [description...]")
|
|
99
|
+
.description('Reprint a session name ("Agent <you> - <description>") for the operator to set as their ' +
|
|
100
|
+
"harness tab title. With no arg, derives it from your current task. The primary naming path " +
|
|
101
|
+
"is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
|
|
102
|
+
"or re-suggest after a topic pivot. Read-only.")
|
|
103
|
+
.option("--json", "JSON output instead of the bare name")
|
|
104
|
+
.option("--session-id <id>", "Lookup heartbeat by session_id directly, bypassing the ppid walk.")
|
|
105
|
+
.action((description, opts) => {
|
|
106
|
+
runSuggestName(description, opts);
|
|
107
|
+
});
|
|
73
108
|
cmd
|
|
74
109
|
.command("watch")
|
|
75
110
|
.description("Stream peer state changes in real time (file watcher on .harnery/active/). " +
|
|
@@ -279,6 +314,59 @@ function registerIdentityCommands(parent) {
|
|
|
279
314
|
meta: { action: "identity-ensure" },
|
|
280
315
|
});
|
|
281
316
|
});
|
|
317
|
+
identity
|
|
318
|
+
.command("assume <name-or-id>")
|
|
319
|
+
.description("Bind this live session to a durable persona. Reclaims an abandoned " +
|
|
320
|
+
"local namesake (no live process); refuses when another live process " +
|
|
321
|
+
"or known remote session still holds the name. Updates name history, " +
|
|
322
|
+
"the event ledger, and the heartbeat.")
|
|
323
|
+
.option("--json", "JSON envelope output")
|
|
324
|
+
.option("--session-id <id>", "Bind the heartbeat with this session_id directly, bypassing the ppid walk.")
|
|
325
|
+
.action((target, opts) => {
|
|
326
|
+
runIdentityAssume(target, opts);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function runIdentityAssume(target, opts) {
|
|
330
|
+
if (opts.json)
|
|
331
|
+
emit.config({ format: "json" });
|
|
332
|
+
const root = monorepoRoot();
|
|
333
|
+
if (!root) {
|
|
334
|
+
emit.error({
|
|
335
|
+
code: "not_in_repo",
|
|
336
|
+
message: "not in an agent session; coord_root() returned null",
|
|
337
|
+
});
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
if (!opts.sessionId)
|
|
341
|
+
ensureCursorSession(root);
|
|
342
|
+
const owner = opts.sessionId ?? resolveOwner();
|
|
343
|
+
if (!owner) {
|
|
344
|
+
emit.error({
|
|
345
|
+
code: "no_pidmap_entry",
|
|
346
|
+
message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
347
|
+
});
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const result = assumeIdentity(root, owner, target);
|
|
352
|
+
emit.data({
|
|
353
|
+
...result,
|
|
354
|
+
display_name: displayAgentName(result.name),
|
|
355
|
+
action: "identity-assume",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (error instanceof IdentityAssumeError) {
|
|
360
|
+
emit.error({ code: error.code, message: error.message });
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
emit.error({
|
|
364
|
+
code: "identity_assume_failed",
|
|
365
|
+
message: error instanceof Error ? error.message : String(error),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
282
370
|
}
|
|
283
371
|
function registerCouncilCommands(parent) {
|
|
284
372
|
const council = parent
|
|
@@ -454,6 +542,7 @@ function runWhoami(opts) {
|
|
|
454
542
|
}
|
|
455
543
|
const row = {
|
|
456
544
|
name: hb.name || "unknown",
|
|
545
|
+
agent_id: hb.agent_id || null,
|
|
457
546
|
instance_id: hb.instance_id,
|
|
458
547
|
session_id: hb.session_id,
|
|
459
548
|
kind: normalizeKind(hb.kind),
|
|
@@ -512,7 +601,7 @@ function runList(opts) {
|
|
|
512
601
|
}
|
|
513
602
|
// Apply staleness filter unless --stale.
|
|
514
603
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
515
|
-
const cutoff = nowSec -
|
|
604
|
+
const cutoff = nowSec - freshnessCutoffSecs();
|
|
516
605
|
const live = opts.stale
|
|
517
606
|
? heartbeats
|
|
518
607
|
: heartbeats.filter((h) => {
|
|
@@ -542,6 +631,7 @@ function runList(opts) {
|
|
|
542
631
|
// Orphan transient case
|
|
543
632
|
rows.push({
|
|
544
633
|
name: h.name || "unknown",
|
|
634
|
+
agent_id: h.agent_id || null,
|
|
545
635
|
instance_id: h.instance_id,
|
|
546
636
|
session_id: h.session_id,
|
|
547
637
|
kind: "transient",
|
|
@@ -566,6 +656,7 @@ function runList(opts) {
|
|
|
566
656
|
}
|
|
567
657
|
rows.push({
|
|
568
658
|
name: h.name || "unknown",
|
|
659
|
+
agent_id: h.agent_id || null,
|
|
569
660
|
instance_id: h.instance_id,
|
|
570
661
|
session_id: h.session_id,
|
|
571
662
|
kind: normalizeKind(h.kind),
|
|
@@ -585,6 +676,30 @@ function runList(opts) {
|
|
|
585
676
|
// (legacy zombies), and an unguarded .localeCompare throws, which is exactly
|
|
586
677
|
// what made `harn agents list --all --stale` crash.
|
|
587
678
|
rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
|
|
679
|
+
// Cross-machine presence (ADR 0016): append sessions on OTHER machines from
|
|
680
|
+
// the locally-fetched presence refs. Advisory rows (relation=remote) — they
|
|
681
|
+
// don't participate in local claim blocking in v1.
|
|
682
|
+
for (const rm of readRemoteMachines(root)) {
|
|
683
|
+
for (const a of rm.agents) {
|
|
684
|
+
rows.push({
|
|
685
|
+
name: a.name || "unknown",
|
|
686
|
+
instance_id: a.instance_id,
|
|
687
|
+
session_id: a.session_id ?? a.instance_id,
|
|
688
|
+
kind: normalizeKind(a.kind),
|
|
689
|
+
relation: "remote",
|
|
690
|
+
started_at: a.started_at ?? "",
|
|
691
|
+
last_heartbeat: a.last_heartbeat ?? "",
|
|
692
|
+
files_touched: [...(a.files_touched ?? [])].sort(),
|
|
693
|
+
last_tool: a.last_tool ?? null,
|
|
694
|
+
last_tool_target: null,
|
|
695
|
+
task: a.task ?? null,
|
|
696
|
+
turn_summary: a.turn_summary ?? null,
|
|
697
|
+
turn_summary_updated_at: null,
|
|
698
|
+
platform: a.platform ?? "claude_code",
|
|
699
|
+
machine: rm.machine,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
588
703
|
// Emit. JSON format gets {rows, note}; TTY gets the rows with note as a footnote.
|
|
589
704
|
emit.data({ rows, note: SUBAGENT_NOTE });
|
|
590
705
|
if (process.stdout.isTTY && !opts.json) {
|
|
@@ -691,7 +806,7 @@ async function runWatch(pollMs) {
|
|
|
691
806
|
function listActiveHeartbeats(activeDir) {
|
|
692
807
|
const out = [];
|
|
693
808
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
694
|
-
const cutoff = nowSec -
|
|
809
|
+
const cutoff = nowSec - freshnessCutoffSecs();
|
|
695
810
|
for (const file of readdirSync(activeDir)) {
|
|
696
811
|
if (!file.endsWith(".json"))
|
|
697
812
|
continue;
|
|
@@ -735,7 +850,7 @@ async function runShow(name, opts) {
|
|
|
735
850
|
// Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
|
|
736
851
|
const matches = [];
|
|
737
852
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
738
|
-
const cutoff = nowSec -
|
|
853
|
+
const cutoff = nowSec - freshnessCutoffSecs();
|
|
739
854
|
for (const file of readdirSync(activeDir)) {
|
|
740
855
|
if (!file.endsWith(".json"))
|
|
741
856
|
continue;
|
|
@@ -885,6 +1000,12 @@ function shouldBootstrapCursorSession() {
|
|
|
885
1000
|
function ensureCursorSession(root) {
|
|
886
1001
|
if (!shouldBootstrapCursorSession())
|
|
887
1002
|
return;
|
|
1003
|
+
// Explicit owner override means the caller already knows who they are.
|
|
1004
|
+
// Bootstrapping sessionStart inherits that env into the child hook, which
|
|
1005
|
+
// re-projects a fresh heartbeat onto the override owner and can wipe an
|
|
1006
|
+
// assumed persona agent_id (seen under Cursor integration fixtures).
|
|
1007
|
+
if (process.env.HARNERY_AGENT_COORD_OWNER?.trim())
|
|
1008
|
+
return;
|
|
888
1009
|
if (resolveOwnerBySessionEnv(root))
|
|
889
1010
|
return;
|
|
890
1011
|
const sessionId = cursorEnvSessionId();
|
|
@@ -941,6 +1062,7 @@ function runReleaseClaim(path) {
|
|
|
941
1062
|
const helper = resolve(root, "harnery", "bin", "agent-coord");
|
|
942
1063
|
const result = spawnSync(helper, ["release-claim", myOwner, canonical], {
|
|
943
1064
|
encoding: "utf8",
|
|
1065
|
+
...coordHelperOpts(root),
|
|
944
1066
|
});
|
|
945
1067
|
if (result.status !== 0) {
|
|
946
1068
|
emit.error({
|
|
@@ -973,10 +1095,20 @@ function runSetTask(task, opts) {
|
|
|
973
1095
|
});
|
|
974
1096
|
process.exit(1);
|
|
975
1097
|
}
|
|
1098
|
+
// Snapshot BEFORE the mutation: a heartbeat that has never carried a task
|
|
1099
|
+
// has no `task_updated_at` stamp (setTask always stamps it, even on a clear),
|
|
1100
|
+
// so its absence is the "focus never declared this session" signal — i.e. the
|
|
1101
|
+
// first set-task of the session. That's the natural moment to also hand the
|
|
1102
|
+
// operator a session name built from the same declaration (one source of
|
|
1103
|
+
// truth: the focus you declare names the session). Distinct from task===""
|
|
1104
|
+
// (a deliberate clear), which carries a stamp and does NOT re-trigger.
|
|
1105
|
+
const priorHb = readHeartbeat(myOwner);
|
|
1106
|
+
const firstOfSession = !priorHb?.task_updated_at;
|
|
976
1107
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
977
1108
|
const helper = resolve(root, "harnery", "bin", "agent-coord");
|
|
978
1109
|
const result = spawnSync(helper, ["set-task", myOwner, task], {
|
|
979
1110
|
encoding: "utf8",
|
|
1111
|
+
...coordHelperOpts(root),
|
|
980
1112
|
});
|
|
981
1113
|
if (result.status !== 0) {
|
|
982
1114
|
emit.error({
|
|
@@ -993,13 +1125,87 @@ function runSetTask(task, opts) {
|
|
|
993
1125
|
harness: normalizeHarness(hb?.platform),
|
|
994
1126
|
data: { task, cleared: !task || task.length === 0 },
|
|
995
1127
|
});
|
|
1128
|
+
// On the first focus declaration of a session, surface a suggested session
|
|
1129
|
+
// name for the operator to set as their harness tab title. Read the result
|
|
1130
|
+
// (`suggested_session_name`) and reproduce it in a fenced code block so the
|
|
1131
|
+
// UI's Copy button hands over the exact string. null on every later call.
|
|
1132
|
+
const built = firstOfSession && !opts?.sessionId ? buildSuggestedName(hb?.name ?? "unknown", [task]) : null;
|
|
996
1133
|
emit.data({
|
|
997
1134
|
instance_id: myOwner,
|
|
998
1135
|
name: hb?.name ?? null,
|
|
999
1136
|
task: hb?.task ?? null,
|
|
1000
1137
|
cleared: !task || task.length === 0,
|
|
1138
|
+
first_of_session: firstOfSession,
|
|
1139
|
+
suggested_session_name: built?.suggestedName ?? null,
|
|
1001
1140
|
});
|
|
1002
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Build the copy-pasteable session name from the coord identity + the agent's
|
|
1144
|
+
* description parts. Pure (no coord-state reads) so it's unit-testable; collapses
|
|
1145
|
+
* internal whitespace and trims. Returns null when the description is empty.
|
|
1146
|
+
*/
|
|
1147
|
+
export function buildSuggestedName(agentName, descriptionParts) {
|
|
1148
|
+
const description = descriptionParts.join(" ").replace(/\s+/g, " ").trim();
|
|
1149
|
+
if (!description)
|
|
1150
|
+
return null;
|
|
1151
|
+
const name = agentName?.trim() || "unknown";
|
|
1152
|
+
return { suggestedName: `Agent ${name} - ${description}`, description };
|
|
1153
|
+
}
|
|
1154
|
+
function runSuggestName(descriptionParts, opts) {
|
|
1155
|
+
const root = monorepoRoot();
|
|
1156
|
+
if (!root) {
|
|
1157
|
+
emit.error({
|
|
1158
|
+
code: "not_in_repo",
|
|
1159
|
+
message: "not in an agent session; coord_root() returned null",
|
|
1160
|
+
});
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
// Identity: prefer explicit --session-id (ppid-walk-free escape hatch, mirrors
|
|
1164
|
+
// `status`/`set-task`), fall back to the ppid walk for interactive shell use.
|
|
1165
|
+
if (!opts.sessionId)
|
|
1166
|
+
ensureCursorSession(root);
|
|
1167
|
+
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1168
|
+
if (!myOwner) {
|
|
1169
|
+
emit.error({
|
|
1170
|
+
code: "no_pidmap_entry",
|
|
1171
|
+
message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1172
|
+
});
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
const hb = readHeartbeat(myOwner);
|
|
1176
|
+
const agentName = hb?.name || "unknown";
|
|
1177
|
+
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1178
|
+
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
1179
|
+
// session's name (handy after a topic pivot). Read-only — mutates no state;
|
|
1180
|
+
// the primary naming path is set-task's first-of-session suggestion.
|
|
1181
|
+
const parts = descriptionParts.length > 0 ? descriptionParts : hb?.task ? [hb.task] : [];
|
|
1182
|
+
const built = buildSuggestedName(agentName, parts);
|
|
1183
|
+
if (!built) {
|
|
1184
|
+
emit.error({
|
|
1185
|
+
code: "no_description",
|
|
1186
|
+
message: 'pass a 2-5 word topic (e.g. suggest-name "Auth Refactor"), or declare one first with set-task',
|
|
1187
|
+
});
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
const { suggestedName, description } = built;
|
|
1191
|
+
const displayName = `agent-${agentName}`;
|
|
1192
|
+
if (opts.json) {
|
|
1193
|
+
emit.config({ format: "json" });
|
|
1194
|
+
emit.data({
|
|
1195
|
+
name: displayName,
|
|
1196
|
+
suggested_session_name: suggestedName,
|
|
1197
|
+
agent_name: agentName,
|
|
1198
|
+
description,
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
// Emit the bare name only — no box. The agent reproduces this exact string in
|
|
1203
|
+
// a fenced code block in its reply, which every chat UI decorates with a
|
|
1204
|
+
// one-click Copy button so the operator can grab it without hand-selecting.
|
|
1205
|
+
// (A drawn box would copy its borders + labels too.) Direct write, not
|
|
1206
|
+
// emit.text: runs via Bash with no TTY, same chat-paste contract as runStatus.
|
|
1207
|
+
process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
|
|
1208
|
+
}
|
|
1003
1209
|
function runStatus(opts) {
|
|
1004
1210
|
const root = monorepoRoot();
|
|
1005
1211
|
if (!root) {
|
|
@@ -1038,6 +1244,7 @@ function runStatus(opts) {
|
|
|
1038
1244
|
spawnSync(helper, ["stamp-status-call", myOwner], {
|
|
1039
1245
|
encoding: "utf8",
|
|
1040
1246
|
timeout: 2000,
|
|
1247
|
+
...coordHelperOpts(root),
|
|
1041
1248
|
});
|
|
1042
1249
|
}
|
|
1043
1250
|
catch {
|
|
@@ -1060,7 +1267,7 @@ function runStatus(opts) {
|
|
|
1060
1267
|
: 0;
|
|
1061
1268
|
const activeDir = resolve(root, ".harnery", "active");
|
|
1062
1269
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1063
|
-
const cutoff = nowSec -
|
|
1270
|
+
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1064
1271
|
const livePeers = [];
|
|
1065
1272
|
let peersStale = 0;
|
|
1066
1273
|
if (existsSync(activeDir)) {
|
|
@@ -1097,7 +1304,9 @@ function runStatus(opts) {
|
|
|
1097
1304
|
});
|
|
1098
1305
|
const filesHeld = hb.files_touched ?? [];
|
|
1099
1306
|
const filesStr = formatList(filesHeld.map((p) => basename(p)), 4, "0 held");
|
|
1100
|
-
|
|
1307
|
+
// Cross-machine presence (ADR 0016): sessions on other machines, advisory.
|
|
1308
|
+
const remoteMachines = readRemoteMachines(root);
|
|
1309
|
+
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1101
1310
|
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
1102
1311
|
let ctxStr;
|
|
1103
1312
|
if (!ctxUsage) {
|
|
@@ -1136,6 +1345,15 @@ function runStatus(opts) {
|
|
|
1136
1345
|
name: p.name || "unnamed",
|
|
1137
1346
|
files: p.files_touched?.length ?? 0,
|
|
1138
1347
|
})),
|
|
1348
|
+
remote_machines: remoteMachines.map((m) => ({
|
|
1349
|
+
machine: m.machine,
|
|
1350
|
+
age_secs: m.age_secs,
|
|
1351
|
+
agents: m.agents.map((a) => ({
|
|
1352
|
+
name: a.name || "unnamed",
|
|
1353
|
+
task: a.task ?? null,
|
|
1354
|
+
files: a.files_touched?.length ?? 0,
|
|
1355
|
+
})),
|
|
1356
|
+
})),
|
|
1139
1357
|
pending_councils: pendingCouncils,
|
|
1140
1358
|
context_used: ctxUsage?.used ?? null,
|
|
1141
1359
|
context_window: ctxUsage?.window ?? null,
|
|
@@ -1183,8 +1401,8 @@ function formatList(items, cap, emptyLabel) {
|
|
|
1183
1401
|
const shown = items.slice(0, cap).join(", ");
|
|
1184
1402
|
return `${shown}, +${items.length - cap} more`;
|
|
1185
1403
|
}
|
|
1186
|
-
function formatPeers(peers, cap, staleCount) {
|
|
1187
|
-
if (peers.length === 0 && staleCount === 0)
|
|
1404
|
+
function formatPeers(peers, cap, staleCount, remoteMachines = []) {
|
|
1405
|
+
if (peers.length === 0 && staleCount === 0 && remoteMachines.length === 0)
|
|
1188
1406
|
return "none";
|
|
1189
1407
|
const labels = peers.map((p) => {
|
|
1190
1408
|
const name = p.name || "unnamed";
|
|
@@ -1203,7 +1421,17 @@ function formatPeers(peers, cap, staleCount) {
|
|
|
1203
1421
|
else {
|
|
1204
1422
|
main = `${labels.slice(0, cap).join(", ")}, +${labels.length - cap} more`;
|
|
1205
1423
|
}
|
|
1206
|
-
|
|
1424
|
+
if (staleCount > 0)
|
|
1425
|
+
main = `${main}; ${staleCount} stale`;
|
|
1426
|
+
// Remote machines (presence transport): `Name @machine` labels, capped.
|
|
1427
|
+
if (remoteMachines.length > 0) {
|
|
1428
|
+
const remote = remoteMachines
|
|
1429
|
+
.flatMap((m) => m.agents.map((a) => `${a.name || "unnamed"} @${m.machine}`))
|
|
1430
|
+
.slice(0, cap);
|
|
1431
|
+
const extra = remoteMachines.reduce((n, m) => n + m.agents.length, 0) - remote.length;
|
|
1432
|
+
main = `${main}; ${remote.join(", ")}${extra > 0 ? `, +${extra} more` : ""}`;
|
|
1433
|
+
}
|
|
1434
|
+
return main;
|
|
1207
1435
|
}
|
|
1208
1436
|
function fmtTokens(n) {
|
|
1209
1437
|
if (n < 1000)
|
|
@@ -1825,6 +2053,9 @@ function traceLine(ev, allTools) {
|
|
|
1825
2053
|
case "state.status_checked":
|
|
1826
2054
|
detail = "status box rendered";
|
|
1827
2055
|
break;
|
|
2056
|
+
case "identity.assumed":
|
|
2057
|
+
detail = `${s("previous_name") || "unknown"} → ${s("name") || "unknown"}${s("agent_id") ? ` · persona=${s("agent_id").slice(0, 8)}…` : ""}`;
|
|
2058
|
+
break;
|
|
1828
2059
|
case "claim.acquire":
|
|
1829
2060
|
case "claim.release":
|
|
1830
2061
|
case "claim.conflict":
|
|
@@ -2090,7 +2321,7 @@ function runHealth(opts) {
|
|
|
2090
2321
|
activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
|
|
2091
2322
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
2092
2323
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
2093
|
-
if (ageMs >
|
|
2324
|
+
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
2094
2325
|
staleHeartbeats++;
|
|
2095
2326
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
2096
2327
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
@@ -2126,7 +2357,7 @@ function runHealth(opts) {
|
|
|
2126
2357
|
}
|
|
2127
2358
|
}
|
|
2128
2359
|
if (staleHeartbeats > 0) {
|
|
2129
|
-
anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(
|
|
2360
|
+
anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(freshnessCutoffSecs() / 60)}min; heal mechanism may not be firing`);
|
|
2130
2361
|
}
|
|
2131
2362
|
const unexpectedSchemas = Object.keys(activeBySchema).filter((k) => k !== "v1");
|
|
2132
2363
|
if (unexpectedSchemas.length > 0) {
|
|
@@ -2806,8 +3037,8 @@ function runHeal(opts) {
|
|
|
2806
3037
|
// agent-coord binary at harnery/bin/agent-coord.
|
|
2807
3038
|
const helper = `${root}/harnery/bin/agent-coord`;
|
|
2808
3039
|
const proc = spawnSync(helper, helperArgs, {
|
|
2809
|
-
cwd: root,
|
|
2810
3040
|
encoding: "utf8",
|
|
3041
|
+
...coordHelperOpts(root),
|
|
2811
3042
|
});
|
|
2812
3043
|
if (proc.status !== 0) {
|
|
2813
3044
|
emit.error({
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* restore restic restore <id>
|
|
12
12
|
* prune restic forget --keep-daily 7 --keep-weekly 4 --prune
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Defaults (repo path, password file, and the keep-daily/weekly/monthly prune
|
|
15
|
+
* policy) come from `.harnery/config.jsonc` `backup.*`, overridable per field by
|
|
16
|
+
* env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE) and CLI flags —
|
|
17
|
+
* see `backupConfig()`. The wrapper deliberately doesn't try to abstract restic;
|
|
18
|
+
* it surfaces it, with opinionated defaults plus passthrough after `--`.
|
|
18
19
|
*/
|
|
19
20
|
import type { Command } from "commander";
|
|
20
21
|
import type { EmitContext } from "../commander.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA4EnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuM/E"}
|
package/dist/commands/backup.js
CHANGED
|
@@ -11,24 +11,22 @@
|
|
|
11
11
|
* restore restic restore <id>
|
|
12
12
|
* prune restic forget --keep-daily 7 --keep-weekly 4 --prune
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Defaults (repo path, password file, and the keep-daily/weekly/monthly prune
|
|
15
|
+
* policy) come from `.harnery/config.jsonc` `backup.*`, overridable per field by
|
|
16
|
+
* env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE) and CLI flags —
|
|
17
|
+
* see `backupConfig()`. The wrapper deliberately doesn't try to abstract restic;
|
|
18
|
+
* it surfaces it, with opinionated defaults plus passthrough after `--`.
|
|
18
19
|
*/
|
|
19
20
|
import { spawnSync } from "node:child_process";
|
|
20
21
|
import { randomBytes } from "node:crypto";
|
|
21
22
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
-
import os from "node:os";
|
|
23
23
|
import path from "node:path";
|
|
24
|
+
import { backupConfig } from "../core/config.js";
|
|
24
25
|
function defaultRepo() {
|
|
25
|
-
|
|
26
|
-
return process.env.HARNERY_RESTIC_REPO ?? path.join(home, ".cache", "harnery", "restic-repo");
|
|
26
|
+
return backupConfig().repo;
|
|
27
27
|
}
|
|
28
28
|
function defaultPasswordFile() {
|
|
29
|
-
|
|
30
|
-
return (process.env.HARNERY_RESTIC_PASSWORD_FILE ??
|
|
31
|
-
path.join(home, ".config", "harnery", "restic-password"));
|
|
29
|
+
return backupConfig().passwordFile;
|
|
32
30
|
}
|
|
33
31
|
function findHarneryDir() {
|
|
34
32
|
let dir = process.cwd();
|
|
@@ -203,12 +201,15 @@ export function registerBackupCommand(program, emit) {
|
|
|
203
201
|
emit.setExitCode(r.exitCode);
|
|
204
202
|
});
|
|
205
203
|
// -- prune --
|
|
204
|
+
// Prune-policy defaults come from backup.keep_* in config.jsonc (falling back
|
|
205
|
+
// to 7/4/6); an explicit --keep-* flag still overrides per invocation.
|
|
206
|
+
const prunePolicy = backupConfig();
|
|
206
207
|
backup
|
|
207
208
|
.command("prune")
|
|
208
|
-
.description(
|
|
209
|
-
.option("--keep-daily <n>", "Daily snapshots to keep",
|
|
210
|
-
.option("--keep-weekly <n>", "Weekly snapshots to keep",
|
|
211
|
-
.option("--keep-monthly <n>", "Monthly snapshots to keep",
|
|
209
|
+
.description(`Forget + prune snapshots (default policy: keep ${prunePolicy.keepDaily} daily / ${prunePolicy.keepWeekly} weekly / ${prunePolicy.keepMonthly} monthly).`)
|
|
210
|
+
.option("--keep-daily <n>", "Daily snapshots to keep", String(prunePolicy.keepDaily))
|
|
211
|
+
.option("--keep-weekly <n>", "Weekly snapshots to keep", String(prunePolicy.keepWeekly))
|
|
212
|
+
.option("--keep-monthly <n>", "Monthly snapshots to keep", String(prunePolicy.keepMonthly))
|
|
212
213
|
.option("--dry-run", "Show what would be pruned without actually pruning")
|
|
213
214
|
.option("--repo <path>", "Repository path")
|
|
214
215
|
.option("--password-file <path>", "Password file")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAmH1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAoLN"}
|