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
package/src/core/config.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Harnery config reader: the settings the coord/hook layer consults when it
|
|
3
|
+
* can't see the consumer CLI's own process.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (`agent-hook`, `agent-coord`) and the web UI run as harnery itself, so they
|
|
9
|
-
* have no other way to learn the consumer's bin name; `harn init` stamps it here
|
|
10
|
-
* for them to read back. The `files` deny/override section is parsed separately
|
|
11
|
-
* by `web/lib/files.ts`.
|
|
5
|
+
* Two layers, project-over-user (project wins field-by-field):
|
|
6
|
+
* 1. `~/.config/harnery/config.jsonc` — user-global base (optional)
|
|
7
|
+
* 2. `<project-root>/.harnery/config.jsonc` — project override (authoritative)
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Fields owned here: `binName` (host CLI name for agent-facing strings),
|
|
10
|
+
* `hooksSetupHint`, `tools`, `workflow`, `skills`, `presence`, plus the tunable
|
|
11
|
+
* `coord` (heartbeat freshness), `backup` (restic repo/password/prune policy),
|
|
12
|
+
* and `sync` (rclone remote/prefix) sections. The `files` deny/override section
|
|
13
|
+
* is parsed separately by `web/lib/files.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Env vars and CLI flags override any config value per invocation (each accessor
|
|
16
|
+
* documents its own precedence). Dependency-free (no jsonc npm dep) so it runs on
|
|
17
|
+
* both Bun and Node, and so the ADR-009 vendored copies stay portable.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
18
22
|
import { join } from "node:path";
|
|
19
23
|
import { coordEnv } from "../lib/env.ts";
|
|
20
24
|
import { findCoordRoot } from "./hooks/resolve/coord-root.ts";
|
|
@@ -22,6 +26,9 @@ import { findCoordRoot } from "./hooks/resolve/coord-root.ts";
|
|
|
22
26
|
/** The standalone CLI's bin name: the resolution floor when nothing else is set. */
|
|
23
27
|
export const DEFAULT_BIN_NAME = "harn";
|
|
24
28
|
|
|
29
|
+
/** Heartbeat-freshness default (seconds): the sweep window when nothing overrides it. */
|
|
30
|
+
export const DEFAULT_FRESHNESS_SECS = 600;
|
|
31
|
+
|
|
25
32
|
interface HarneryConfig {
|
|
26
33
|
/** Host CLI bin name, stamped by `harn init` for a consumer (e.g. "acme"). */
|
|
27
34
|
binName?: string;
|
|
@@ -40,6 +47,48 @@ interface HarneryConfig {
|
|
|
40
47
|
* a missing rg only produces a rate-limited install hint.
|
|
41
48
|
*/
|
|
42
49
|
tools?: { ripgrep?: { autoInstall?: boolean } };
|
|
50
|
+
/**
|
|
51
|
+
* Workflow-engine defaults. `{ subscriptionOnly: true }` pins every
|
|
52
|
+
* `workflow run` in this repo to subscription billing (API-key vars are
|
|
53
|
+
* scrubbed from child envs) without anyone having to remember the flag.
|
|
54
|
+
*/
|
|
55
|
+
workflow?: { subscriptionOnly?: boolean };
|
|
56
|
+
/**
|
|
57
|
+
* Cross-machine presence (ADR 0016). `{ enabled: false }` opts a repo out of
|
|
58
|
+
* the git-refs transport (publishing `refs/harnery/presence/<machine>` to
|
|
59
|
+
* origin + fetching peers'). Default is ON when an origin remote exists —
|
|
60
|
+
* the zero-config story — and every operation is fail-silent.
|
|
61
|
+
*
|
|
62
|
+
* `relay` (optional) is the live upgrade: a wss:// URL of a presence relay
|
|
63
|
+
* (the reference public one is wss://relay.harnery.com; self-hosters run
|
|
64
|
+
* `harn relay serve` or deploy relay/worker/ to their own Cloudflare
|
|
65
|
+
* account). When set, hooks keep a per-machine daemon connected to the
|
|
66
|
+
* relay for seconds-latency presence; the git-refs transport stays on as
|
|
67
|
+
* the floor. Unset → git-refs only.
|
|
68
|
+
*/
|
|
69
|
+
presence?: { enabled?: boolean; relay?: string };
|
|
70
|
+
/**
|
|
71
|
+
* Coord-layer tunables. `freshness_seconds` is the heartbeat age above which
|
|
72
|
+
* the sweeper prunes an agent (default 600). Read via `coordFreshnessSeconds()`.
|
|
73
|
+
*/
|
|
74
|
+
coord?: { freshness_seconds?: number };
|
|
75
|
+
/**
|
|
76
|
+
* `harn backup` (restic) defaults: `repo` path/URL, `password_file`, and the
|
|
77
|
+
* `keep_daily`/`keep_weekly`/`keep_monthly` prune policy. Read via `backupConfig()`.
|
|
78
|
+
*/
|
|
79
|
+
backup?: {
|
|
80
|
+
repo?: string;
|
|
81
|
+
password_file?: string;
|
|
82
|
+
keep_daily?: number;
|
|
83
|
+
keep_weekly?: number;
|
|
84
|
+
keep_monthly?: number;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* `harn sync` (rclone) defaults: the `remote` name and `prefix` subpath. Read
|
|
88
|
+
* via `syncJsoncConfig()`. `harn sync init` also persists these to
|
|
89
|
+
* `~/.config/harnery/sync.json`, which is consulted as a lower-precedence fallback.
|
|
90
|
+
*/
|
|
91
|
+
sync?: { remote?: string; prefix?: string };
|
|
43
92
|
[k: string]: unknown;
|
|
44
93
|
}
|
|
45
94
|
|
|
@@ -83,29 +132,92 @@ export function stripJsonComments(input: string): string {
|
|
|
83
132
|
return out;
|
|
84
133
|
}
|
|
85
134
|
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The user-global config file, the lower-precedence base under the project file.
|
|
137
|
+
* Honors `XDG_CONFIG_HOME` (falling back to `~/.config`) per the XDG base-dir spec.
|
|
138
|
+
*/
|
|
139
|
+
function userConfigPath(): string {
|
|
140
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
141
|
+
const base = xdg?.trim() ? xdg : join(homedir(), ".config");
|
|
142
|
+
return join(base, "harnery", "config.jsonc");
|
|
143
|
+
}
|
|
88
144
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let mtimeMs = -1;
|
|
145
|
+
/** File mtime in ms, or -1 when the file can't be stat'd (missing). */
|
|
146
|
+
function statMtime(p: string): number {
|
|
92
147
|
try {
|
|
93
|
-
|
|
148
|
+
return statSync(p).mtimeMs;
|
|
94
149
|
} catch {
|
|
95
|
-
|
|
150
|
+
return -1;
|
|
96
151
|
}
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Parse one JSONC config file to an object; missing/unparseable → `{}`. */
|
|
155
|
+
function parseConfigFile(p: string): HarneryConfig {
|
|
99
156
|
try {
|
|
100
157
|
const parsed = JSON.parse(stripJsonComments(readFileSync(p, "utf8"))) as HarneryConfig | null;
|
|
101
|
-
if (parsed && typeof parsed === "object")
|
|
158
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
102
159
|
} catch {
|
|
103
|
-
/* missing or unparseable → defaults (
|
|
160
|
+
/* missing or unparseable → defaults (files-section resolver fails loud; the rest is non-critical) */
|
|
161
|
+
}
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
166
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deep-merge `override` onto `base` (override wins), recursing into plain
|
|
171
|
+
* objects only so a project that sets just `web.port` doesn't wipe a
|
|
172
|
+
* user-global `web.bind`. Arrays + scalars replace wholesale.
|
|
173
|
+
*/
|
|
174
|
+
function mergeConfig(base: HarneryConfig, override: HarneryConfig): HarneryConfig {
|
|
175
|
+
const out: Record<string, unknown> = { ...base };
|
|
176
|
+
for (const [k, v] of Object.entries(override)) {
|
|
177
|
+
const prev = out[k];
|
|
178
|
+
out[k] = isPlainObject(prev) && isPlainObject(v) ? mergeConfig(prev, v) : v;
|
|
179
|
+
}
|
|
180
|
+
return out as HarneryConfig;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// mtime-keyed per-process cache (both layers): a stat is cheap, a parse on every render isn't.
|
|
184
|
+
let cache: { root: string; projMtime: number; userMtime: number; cfg: HarneryConfig } | null = null;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The effective config for `root`: user-global (`~/.config/harnery/config.jsonc`)
|
|
188
|
+
* as the base, the project file (`<root>/.harnery/config.jsonc`) merged on top.
|
|
189
|
+
* Project values win field-by-field.
|
|
190
|
+
*/
|
|
191
|
+
function readConfig(root: string): HarneryConfig {
|
|
192
|
+
const projPath = join(root, ".harnery", "config.jsonc");
|
|
193
|
+
const userPath = userConfigPath();
|
|
194
|
+
const projMtime = statMtime(projPath);
|
|
195
|
+
const userMtime = statMtime(userPath);
|
|
196
|
+
if (
|
|
197
|
+
cache &&
|
|
198
|
+
cache.root === root &&
|
|
199
|
+
cache.projMtime === projMtime &&
|
|
200
|
+
cache.userMtime === userMtime
|
|
201
|
+
) {
|
|
202
|
+
return cache.cfg;
|
|
104
203
|
}
|
|
105
|
-
|
|
204
|
+
const user = userMtime === -1 ? {} : parseConfigFile(userPath);
|
|
205
|
+
const project = projMtime === -1 ? {} : parseConfigFile(projPath);
|
|
206
|
+
const cfg = mergeConfig(user, project);
|
|
207
|
+
cache = { root, projMtime, userMtime, cfg };
|
|
106
208
|
return cfg;
|
|
107
209
|
}
|
|
108
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The PROJECT config file only — no user-global merge. `pinnedBinName` uses this
|
|
213
|
+
* so a user-global `binName` can never masquerade as a deliberate project pin
|
|
214
|
+
* (the pin guards committed, public surfaces; see `pinnedBinName`).
|
|
215
|
+
*/
|
|
216
|
+
function readProjectConfig(root: string): HarneryConfig {
|
|
217
|
+
const p = join(root, ".harnery", "config.jsonc");
|
|
218
|
+
return statMtime(p) === -1 ? {} : parseConfigFile(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
109
221
|
/**
|
|
110
222
|
* Resolve the host CLI's bin name for user-facing strings. Precedence:
|
|
111
223
|
* 1. `HARNERY_BIN` env (explicit per-process override)
|
|
@@ -135,7 +247,7 @@ export function resolveBinName(coordRoot?: string | null): string {
|
|
|
135
247
|
* CLI would otherwise stamp the host's name into public, committed surfaces).
|
|
136
248
|
*/
|
|
137
249
|
export function pinnedBinName(projectRoot: string): string | null {
|
|
138
|
-
const binName =
|
|
250
|
+
const binName = readProjectConfig(projectRoot).binName;
|
|
139
251
|
return typeof binName === "string" && binName.trim() ? binName.trim() : null;
|
|
140
252
|
}
|
|
141
253
|
|
|
@@ -168,3 +280,138 @@ export function ripgrepAutoInstall(coordRoot?: string | null): boolean {
|
|
|
168
280
|
if (!root) return false;
|
|
169
281
|
return readConfig(root).tools?.ripgrep?.autoInstall === true;
|
|
170
282
|
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Whether this repo pins workflow runs to subscription billing:
|
|
286
|
+
* `.harnery/config.jsonc` `{ "workflow": { "subscriptionOnly": true } }`.
|
|
287
|
+
* The `workflow run --subscription-only` flag turns it on per invocation;
|
|
288
|
+
* `HARNERY_WORKFLOW_SUBSCRIPTION_ONLY=1|0` overrides per process (the `0`
|
|
289
|
+
* escape hatch exists for a key-only CI job inside a pinned repo).
|
|
290
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
291
|
+
*/
|
|
292
|
+
export function workflowSubscriptionOnly(coordRoot?: string | null): boolean {
|
|
293
|
+
const env = coordEnv("WORKFLOW_SUBSCRIPTION_ONLY");
|
|
294
|
+
if (env === "1") return true;
|
|
295
|
+
if (env === "0") return false;
|
|
296
|
+
const root = coordRoot ?? findCoordRoot();
|
|
297
|
+
if (!root) return false;
|
|
298
|
+
return readConfig(root).workflow?.subscriptionOnly === true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Whether cross-machine presence (ADR 0016) is enabled for this repo.
|
|
303
|
+
* Default ON — the transport itself additionally gates on an origin remote
|
|
304
|
+
* existing and fails silent everywhere. Opt out via
|
|
305
|
+
* `.harnery/config.jsonc` `{ "presence": { "enabled": false } }`;
|
|
306
|
+
* `HARNERY_PRESENCE=1|0` overrides per process.
|
|
307
|
+
*/
|
|
308
|
+
export function presenceEnabled(coordRoot?: string | null): boolean {
|
|
309
|
+
const env = coordEnv("PRESENCE");
|
|
310
|
+
if (env === "1") return true;
|
|
311
|
+
if (env === "0") return false;
|
|
312
|
+
const root = coordRoot ?? findCoordRoot();
|
|
313
|
+
if (!root) return false;
|
|
314
|
+
return readConfig(root).presence?.enabled !== false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The presence relay URL for this repo, or null when the relay transport is
|
|
319
|
+
* not configured (git-refs only). `HARNERY_PRESENCE_RELAY` overrides per
|
|
320
|
+
* process (empty string or "0" disables). Requires `presenceEnabled()` to be
|
|
321
|
+
* true — a disabled presence section disables the relay too.
|
|
322
|
+
*/
|
|
323
|
+
export function presenceRelayUrl(coordRoot?: string | null): string | null {
|
|
324
|
+
const root = coordRoot ?? findCoordRoot();
|
|
325
|
+
if (!presenceEnabled(root)) return null;
|
|
326
|
+
const env = coordEnv("PRESENCE_RELAY");
|
|
327
|
+
if (env !== undefined && env !== null) {
|
|
328
|
+
const t = env.trim();
|
|
329
|
+
return t && t !== "0" ? t : null;
|
|
330
|
+
}
|
|
331
|
+
if (!root) return null;
|
|
332
|
+
const relay = readConfig(root).presence?.relay;
|
|
333
|
+
return typeof relay === "string" && relay.trim() ? relay.trim() : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** A positive integer from `v`, else `fallback`. Floors non-integer numbers. */
|
|
337
|
+
function posIntOr(v: unknown, fallback: number): number {
|
|
338
|
+
const n = typeof v === "number" ? v : Number.NaN;
|
|
339
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The heartbeat-freshness window (seconds): the age above which the sweeper
|
|
344
|
+
* prunes an agent, and the cutoff the `agents` surface uses to fold stale peers.
|
|
345
|
+
* Precedence:
|
|
346
|
+
* 1. `HARNERY_AGENT_COORD_FRESHNESS` env (canonical), or `HARNERY_AGENT_FRESHNESS` (legacy alias)
|
|
347
|
+
* 2. `.harnery/config.jsonc` `coord.freshness_seconds`
|
|
348
|
+
* 3. `600` (10 minutes)
|
|
349
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
350
|
+
*/
|
|
351
|
+
export function coordFreshnessSeconds(coordRoot?: string | null): number {
|
|
352
|
+
const env = coordEnv("AGENT_COORD_FRESHNESS") ?? coordEnv("AGENT_FRESHNESS");
|
|
353
|
+
if (env !== undefined) {
|
|
354
|
+
const n = Number.parseInt(env, 10);
|
|
355
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
356
|
+
}
|
|
357
|
+
const root = coordRoot ?? findCoordRoot();
|
|
358
|
+
if (root) return posIntOr(readConfig(root).coord?.freshness_seconds, DEFAULT_FRESHNESS_SECS);
|
|
359
|
+
return DEFAULT_FRESHNESS_SECS;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Resolved `harn backup` defaults (restic repo/password + prune policy). */
|
|
363
|
+
export interface BackupConfig {
|
|
364
|
+
repo: string;
|
|
365
|
+
passwordFile: string;
|
|
366
|
+
keepDaily: number;
|
|
367
|
+
keepWeekly: number;
|
|
368
|
+
keepMonthly: number;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* `harn backup` (restic) defaults. Per field, precedence is env → config → built-in:
|
|
373
|
+
* repo: `HARNERY_RESTIC_REPO` → `backup.repo` → `~/.cache/harnery/restic-repo`
|
|
374
|
+
* passwordFile: `HARNERY_RESTIC_PASSWORD_FILE` → `backup.password_file` → `~/.config/harnery/restic-password`
|
|
375
|
+
* keepDaily/Weekly/Monthly: `backup.keep_*` → 7 / 4 / 6
|
|
376
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
377
|
+
*/
|
|
378
|
+
export function backupConfig(coordRoot?: string | null): BackupConfig {
|
|
379
|
+
const home = homedir();
|
|
380
|
+
const root = coordRoot ?? findCoordRoot();
|
|
381
|
+
const b = root ? (readConfig(root).backup ?? {}) : {};
|
|
382
|
+
const cfgStr = (v: unknown): string | undefined =>
|
|
383
|
+
typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
384
|
+
const repo =
|
|
385
|
+
coordEnv("RESTIC_REPO") ?? cfgStr(b.repo) ?? join(home, ".cache", "harnery", "restic-repo");
|
|
386
|
+
const passwordFile =
|
|
387
|
+
coordEnv("RESTIC_PASSWORD_FILE") ??
|
|
388
|
+
cfgStr(b.password_file) ??
|
|
389
|
+
join(home, ".config", "harnery", "restic-password");
|
|
390
|
+
return {
|
|
391
|
+
repo,
|
|
392
|
+
passwordFile,
|
|
393
|
+
keepDaily: posIntOr(b.keep_daily, 7),
|
|
394
|
+
keepWeekly: posIntOr(b.keep_weekly, 4),
|
|
395
|
+
keepMonthly: posIntOr(b.keep_monthly, 6),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* `harn sync` (rclone) remote/prefix from `.harnery/config.jsonc` `sync`, or null
|
|
401
|
+
* when unset. This is the config-file layer only; `harn sync` consults env
|
|
402
|
+
* (`HARNERY_SYNC_REMOTE`/`_PREFIX`) first and the `~/.config/harnery/sync.json`
|
|
403
|
+
* file (written by `harn sync init`) as a lower-precedence fallback.
|
|
404
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
405
|
+
*/
|
|
406
|
+
export function syncJsoncConfig(
|
|
407
|
+
coordRoot?: string | null,
|
|
408
|
+
): { remote: string; prefix: string } | null {
|
|
409
|
+
const root = coordRoot ?? findCoordRoot();
|
|
410
|
+
if (!root) return null;
|
|
411
|
+
const s = readConfig(root).sync ?? {};
|
|
412
|
+
if (typeof s.remote === "string" && s.remote.trim()) {
|
|
413
|
+
const prefix = typeof s.prefix === "string" && s.prefix.trim() ? s.prefix.trim() : "harnery";
|
|
414
|
+
return { remote: s.remote.trim(), prefix };
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|