harnery 0.46.1 → 0.47.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 +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +28 -3
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +27 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
package/src/core/config.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* `coord` (heartbeat freshness), `logs` (structured-log storage budgets),
|
|
12
12
|
* `artifacts` (working-file retention),
|
|
13
13
|
* `backup` (restic repo/password/prune policy), `sync` (rclone
|
|
14
|
-
* remote/prefix),
|
|
14
|
+
* remote/prefix), `computer` (opt-in desktop control), `autoUpdate` (activity-fired
|
|
15
|
+
* project self-update), and `web` (dashboard port) sections. The `files` deny/override section
|
|
15
16
|
* is parsed separately by `web/lib/files.ts`.
|
|
16
17
|
*
|
|
17
18
|
* Env vars and CLI flags override any config value per invocation (each accessor
|
|
@@ -24,6 +25,7 @@ import { homedir } from "node:os";
|
|
|
24
25
|
import { isAbsolute, join, resolve } from "node:path";
|
|
25
26
|
import { coordEnv } from "../lib/env.ts";
|
|
26
27
|
import { DEFAULT_WEB_PORT } from "../lib/local-file-url.ts";
|
|
28
|
+
import { AUTOUPDATE_DEFAULTS, type AutoUpdateConfig } from "./autoupdate/types.ts";
|
|
27
29
|
import { resolveEventLedgerRotateActiveBytesV3 } from "./events/v3/rotation-config.ts";
|
|
28
30
|
import {
|
|
29
31
|
type PromptContextConfig,
|
|
@@ -82,6 +84,8 @@ interface HarneryConfig {
|
|
|
82
84
|
promptContext?: unknown;
|
|
83
85
|
/** Project-only opt-in; read via `codexWindowsBridgeConfig()`. */
|
|
84
86
|
codexWindowsBridge?: unknown;
|
|
87
|
+
/** Project-only opt-in; read via `portableHookCommands()`. */
|
|
88
|
+
portableCommands?: unknown;
|
|
85
89
|
};
|
|
86
90
|
/**
|
|
87
91
|
* Agent-ritual policy owned by the host project. Git finalization is opt-in:
|
|
@@ -220,6 +224,24 @@ interface HarneryConfig {
|
|
|
220
224
|
sync?: { remote?: string; prefix?: string };
|
|
221
225
|
/** Standalone dashboard defaults. */
|
|
222
226
|
web?: { port?: number; bind?: string };
|
|
227
|
+
/**
|
|
228
|
+
* Opt-in desktop control (`computer` command, ADR 0228). Inert unless
|
|
229
|
+
* `enabled` is true. Project config only; read via `readComputerConfig()`.
|
|
230
|
+
*/
|
|
231
|
+
computer?: { enabled?: boolean; idle_seconds?: number; wait_seconds?: number };
|
|
232
|
+
/**
|
|
233
|
+
* Activity-fired project self-update (ADR 0229). Inert unless `enabled` is
|
|
234
|
+
* true. Project config only; read via `readAutoUpdateConfig()`.
|
|
235
|
+
*/
|
|
236
|
+
autoUpdate?: {
|
|
237
|
+
enabled?: boolean;
|
|
238
|
+
checkMinutes?: number;
|
|
239
|
+
steps?: unknown;
|
|
240
|
+
dailySteps?: unknown;
|
|
241
|
+
watch?: unknown;
|
|
242
|
+
timeoutMinutes?: number;
|
|
243
|
+
cleanup?: boolean;
|
|
244
|
+
};
|
|
223
245
|
[k: string]: unknown;
|
|
224
246
|
}
|
|
225
247
|
|
|
@@ -545,6 +567,18 @@ export function codexWindowsBridgeConfig(projectRoot: string): CodexWindowsBridg
|
|
|
545
567
|
return { entryPoint, error: null };
|
|
546
568
|
}
|
|
547
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Per-project opt-in that makes `init` write the same adapter hook commands on
|
|
572
|
+
* every OS: `agent-hook` from PATH, plus a native Codex `commandWindows`. A
|
|
573
|
+
* project that commits its settings files and is cloned on Windows and POSIX
|
|
574
|
+
* needs it, because the default form depends on the OS that ran `init`. Read
|
|
575
|
+
* from the project config only, for the same reason as the Windows bridge.
|
|
576
|
+
* Anything but `true` keeps the default.
|
|
577
|
+
*/
|
|
578
|
+
export function portableHookCommands(projectRoot: string): boolean {
|
|
579
|
+
return readProjectConfig(projectRoot).hooks?.portableCommands === true;
|
|
580
|
+
}
|
|
581
|
+
|
|
548
582
|
export function hostPromptReminder(coordRoot?: string | null): string | null {
|
|
549
583
|
const root = coordRoot ?? findCoordRoot();
|
|
550
584
|
if (!root) return null;
|
|
@@ -718,6 +752,113 @@ export function readAdmissionConfig(coordRoot?: string | null): AdmissionProject
|
|
|
718
752
|
};
|
|
719
753
|
}
|
|
720
754
|
|
|
755
|
+
/** Effective `computer` settings; unset tunables fall back to the command's defaults. */
|
|
756
|
+
export interface ComputerProjectConfig {
|
|
757
|
+
enabled: boolean;
|
|
758
|
+
/** Desktop lease idle expiry in seconds. */
|
|
759
|
+
idle_seconds?: number;
|
|
760
|
+
/** How long `computer call` waits for another agent's lease, in seconds. */
|
|
761
|
+
wait_seconds?: number;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Desktop-control settings from `.harnery/config.jsonc`:
|
|
766
|
+
*
|
|
767
|
+
* ```jsonc
|
|
768
|
+
* "computer": { "enabled": true, "idle_seconds": 120, "wait_seconds": 30 }
|
|
769
|
+
* ```
|
|
770
|
+
*
|
|
771
|
+
* Project config only: enabling desktop control is a per-project decision, so a
|
|
772
|
+
* user-global value never turns it on. Malformed values are dropped one by one;
|
|
773
|
+
* `enabled` is true only when the project sets it to exactly `true`.
|
|
774
|
+
*/
|
|
775
|
+
export function readComputerConfig(coordRoot?: string | null): ComputerProjectConfig {
|
|
776
|
+
const root = coordRoot ?? findCoordRoot();
|
|
777
|
+
if (!root) return { enabled: false };
|
|
778
|
+
const raw = readProjectConfig(root).computer;
|
|
779
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { enabled: false };
|
|
780
|
+
const record = raw as Record<string, unknown>;
|
|
781
|
+
const idle = positiveNumber(record.idle_seconds, 86_400);
|
|
782
|
+
const wait = record.wait_seconds;
|
|
783
|
+
const waitOk = typeof wait === "number" && Number.isFinite(wait) && wait >= 0 && wait <= 3600;
|
|
784
|
+
return {
|
|
785
|
+
enabled: record.enabled === true,
|
|
786
|
+
...(idle !== undefined ? { idle_seconds: idle } : {}),
|
|
787
|
+
...(waitOk ? { wait_seconds: wait } : {}),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Project-only parse cache keyed by the file's stat signature. Hooks call the
|
|
792
|
+
// auto-update reader on every prompt, so an unchanged file costs one stat.
|
|
793
|
+
let projectCache: { path: string; signature: string; cfg: HarneryConfig } | null = null;
|
|
794
|
+
|
|
795
|
+
function readProjectConfigCached(root: string): HarneryConfig {
|
|
796
|
+
const p = join(root, ".harnery", "config.jsonc");
|
|
797
|
+
const signature = statSignature(p);
|
|
798
|
+
if (signature === null) return {};
|
|
799
|
+
if (projectCache && projectCache.path === p && projectCache.signature === signature) {
|
|
800
|
+
return projectCache.cfg;
|
|
801
|
+
}
|
|
802
|
+
const cfg = parseConfigFile(p);
|
|
803
|
+
projectCache = { path: p, signature, cfg };
|
|
804
|
+
return cfg;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function clampInteger(value: unknown, min: number, max: number, fallback: number): number {
|
|
808
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
809
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Keep each argv that is a non-empty array of non-empty strings; drop the rest. */
|
|
813
|
+
function argvList(value: unknown): string[][] {
|
|
814
|
+
if (!Array.isArray(value)) return [];
|
|
815
|
+
const out: string[][] = [];
|
|
816
|
+
for (const entry of value) {
|
|
817
|
+
if (!Array.isArray(entry) || entry.length === 0) continue;
|
|
818
|
+
if (!entry.every((part) => typeof part === "string" && part.length > 0)) continue;
|
|
819
|
+
out.push([...(entry as string[])]);
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Project self-update settings from `.harnery/config.jsonc` (ADR 0229):
|
|
826
|
+
*
|
|
827
|
+
* ```jsonc
|
|
828
|
+
* "autoUpdate": {
|
|
829
|
+
* "enabled": true,
|
|
830
|
+
* "steps": [["bin/pull-all"]],
|
|
831
|
+
* "dailySteps": [["bin/doctor", "--quiet"]],
|
|
832
|
+
* "watch": ["~/.local/share/example/hooks.json"],
|
|
833
|
+
* "checkMinutes": 15,
|
|
834
|
+
* "timeoutMinutes": 20,
|
|
835
|
+
* "cleanup": true
|
|
836
|
+
* }
|
|
837
|
+
* ```
|
|
838
|
+
*
|
|
839
|
+
* Project config only: running commands on activity is a per-project
|
|
840
|
+
* decision, so a user-global value never turns it on. Null when the project
|
|
841
|
+
* has no `autoUpdate` section. Malformed values are dropped one by one;
|
|
842
|
+
* `enabled` is true only when the project sets it to exactly `true`.
|
|
843
|
+
*/
|
|
844
|
+
export function readAutoUpdateConfig(coordRoot: string): AutoUpdateConfig | null {
|
|
845
|
+
const raw = readProjectConfigCached(coordRoot).autoUpdate;
|
|
846
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
847
|
+
const record = raw as Record<string, unknown>;
|
|
848
|
+
const watch = Array.isArray(record.watch)
|
|
849
|
+
? record.watch.filter((p): p is string => typeof p === "string" && p.trim().length > 0)
|
|
850
|
+
: [];
|
|
851
|
+
return {
|
|
852
|
+
enabled: record.enabled === true,
|
|
853
|
+
checkMinutes: clampInteger(record.checkMinutes, 1, 1440, AUTOUPDATE_DEFAULTS.checkMinutes),
|
|
854
|
+
steps: argvList(record.steps),
|
|
855
|
+
dailySteps: argvList(record.dailySteps),
|
|
856
|
+
watch,
|
|
857
|
+
timeoutMinutes: clampInteger(record.timeoutMinutes, 1, 240, AUTOUPDATE_DEFAULTS.timeoutMinutes),
|
|
858
|
+
cleanup: record.cleanup !== false,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
721
862
|
/**
|
|
722
863
|
* Whether this repo pins workflow runs to subscription billing:
|
|
723
864
|
* `.harnery/config.jsonc` `{ "workflow": { "subscriptionOnly": true } }`.
|
|
@@ -16,6 +16,7 @@ export function currentHarneryRuntimeBuild(): string {
|
|
|
16
16
|
const root = harneryPackageRoot();
|
|
17
17
|
if (existsSync(resolve(root, ".git"))) {
|
|
18
18
|
const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
19
|
+
windowsHide: true,
|
|
19
20
|
encoding: "utf8",
|
|
20
21
|
timeout: 5_000,
|
|
21
22
|
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { codexWindowsBridgeConfig, resolveBinName } from "../../config.ts";
|
|
17
|
+
import { codexWindowsBridgeConfig, portableHookCommands, resolveBinName } from "../../config.ts";
|
|
18
18
|
import {
|
|
19
19
|
ADAPTER_SPECS,
|
|
20
20
|
type AdapterId,
|
|
@@ -122,6 +122,22 @@ export function hookCommandWindows(
|
|
|
122
122
|
export interface WiringOptions {
|
|
123
123
|
/** Bridge entry point from `hooks.codexWindowsBridge`, or null when not opted in. */
|
|
124
124
|
windowsBridgeEntryPoint?: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* `hooks.portableCommands`: write the same `agent-hook` commands from PATH on
|
|
127
|
+
* every OS, plus a native Codex `commandWindows`, so one committed settings
|
|
128
|
+
* file is current everywhere.
|
|
129
|
+
*/
|
|
130
|
+
portableCommands?: boolean;
|
|
131
|
+
/** The OS to write for. Defaults to the host; tests inject it. */
|
|
132
|
+
platform?: NodeJS.Platform;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The wiring options a project's committed config selects. */
|
|
136
|
+
export function projectWiringOptions(projectRoot: string): WiringOptions {
|
|
137
|
+
return {
|
|
138
|
+
windowsBridgeEntryPoint: codexWindowsBridgeConfig(projectRoot).entryPoint,
|
|
139
|
+
portableCommands: portableHookCommands(projectRoot),
|
|
140
|
+
};
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
/** The `commandWindows` init writes for one event, or undefined for none. */
|
|
@@ -132,7 +148,12 @@ export function expectedCommandWindows(
|
|
|
132
148
|
adapter: AdapterId,
|
|
133
149
|
options?: WiringOptions,
|
|
134
150
|
): string | undefined {
|
|
135
|
-
if (!spec.windowsBridgeHooks
|
|
151
|
+
if (!spec.windowsBridgeHooks) return undefined;
|
|
152
|
+
if (!options?.windowsBridgeEntryPoint) {
|
|
153
|
+
return (options?.platform ?? process.platform) === "win32" || options?.portableCommands
|
|
154
|
+
? nativeHookCommand(agentHookPath, subcommand, adapter)
|
|
155
|
+
: undefined;
|
|
156
|
+
}
|
|
136
157
|
return hookCommandWindows(
|
|
137
158
|
spec,
|
|
138
159
|
agentHookPath,
|
|
@@ -193,7 +214,11 @@ export function hookCommand(
|
|
|
193
214
|
agentHookPath: string,
|
|
194
215
|
subcommand: string,
|
|
195
216
|
adapter: AdapterId,
|
|
217
|
+
options?: WiringOptions,
|
|
196
218
|
): string {
|
|
219
|
+
if ((options?.platform ?? process.platform) === "win32") {
|
|
220
|
+
return nativeHookCommand(agentHookPath, subcommand, adapter);
|
|
221
|
+
}
|
|
197
222
|
if (agentHookPath === "agent-hook") {
|
|
198
223
|
return `agent-hook ${subcommand} --adapter ${adapter}`;
|
|
199
224
|
}
|
|
@@ -201,13 +226,29 @@ export function hookCommand(
|
|
|
201
226
|
return `bash ${anchor}${agentHookPath} ${subcommand} --adapter ${adapter}`;
|
|
202
227
|
}
|
|
203
228
|
|
|
229
|
+
function nativeHookCommand(agentHookPath: string, subcommand: string, adapter: AdapterId): string {
|
|
230
|
+
if (agentHookPath === "agent-hook") return `agent-hook ${subcommand} --adapter ${adapter}`;
|
|
231
|
+
const runner = join(dirname(agentHookPath), "run.mjs").replaceAll("\\", "/");
|
|
232
|
+
return `node "${runner}" agent-hook ${subcommand} --adapter ${adapter}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
204
235
|
/**
|
|
205
236
|
* Pick a committed hook target that survives cloning the consumer elsewhere.
|
|
206
237
|
* Projects that contain Harnery can use its repo-relative launcher. Standalone
|
|
207
238
|
* consumers use the package's installed `agent-hook` executable from PATH
|
|
208
239
|
* instead of recording a relative path back into one developer's package tree.
|
|
209
240
|
*/
|
|
210
|
-
export function agentHookPathForProject(
|
|
241
|
+
export function agentHookPathForProject(
|
|
242
|
+
projectRoot: string,
|
|
243
|
+
packageRoot: string,
|
|
244
|
+
options?: WiringOptions,
|
|
245
|
+
): string {
|
|
246
|
+
// Native npm shims run the linked source checkout without Bash, and do not
|
|
247
|
+
// depend on a hook's working directory after an agent changes directories.
|
|
248
|
+
// A project that commits one settings file for every OS asks for the same.
|
|
249
|
+
if ((options?.platform ?? process.platform) === "win32" || options?.portableCommands) {
|
|
250
|
+
return "agent-hook";
|
|
251
|
+
}
|
|
211
252
|
const hook = join(packageRoot, "bin", "agent-hook");
|
|
212
253
|
const candidate = relative(projectRoot, hook);
|
|
213
254
|
if (candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) {
|
|
@@ -268,6 +309,7 @@ export function diffWiring(
|
|
|
268
309
|
expected.agentHookPath,
|
|
269
310
|
event.subcommand,
|
|
270
311
|
expected.adapter,
|
|
312
|
+
expected,
|
|
271
313
|
);
|
|
272
314
|
const timeout = hookTimeoutFor(spec, event.subcommand);
|
|
273
315
|
const commandWindows = expectedCommandWindows(
|
|
@@ -386,7 +428,9 @@ function pluginDrift(
|
|
|
386
428
|
const packageRoot = harneryPackageRoot();
|
|
387
429
|
const check = checkOpenCodePlugin(projectRoot, {
|
|
388
430
|
binName: resolveBinName(projectRoot),
|
|
389
|
-
agentHook: packageRoot
|
|
431
|
+
agentHook: packageRoot
|
|
432
|
+
? agentHookPathForProject(projectRoot, packageRoot, projectWiringOptions(projectRoot))
|
|
433
|
+
: "agent-hook",
|
|
390
434
|
});
|
|
391
435
|
if (check.status === "fresh") return null;
|
|
392
436
|
if (check.status === "missing") return null;
|
|
@@ -425,14 +469,15 @@ export function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[] {
|
|
|
425
469
|
continue;
|
|
426
470
|
}
|
|
427
471
|
const packageRoot = harneryPackageRoot();
|
|
472
|
+
const options = projectWiringOptions(projectRoot);
|
|
428
473
|
const diff = diffWiring(
|
|
429
474
|
settings,
|
|
430
475
|
spec,
|
|
431
476
|
packageRoot
|
|
432
477
|
? {
|
|
433
|
-
agentHookPath: agentHookPathForProject(projectRoot, packageRoot),
|
|
478
|
+
agentHookPath: agentHookPathForProject(projectRoot, packageRoot, options),
|
|
434
479
|
adapter: id,
|
|
435
|
-
|
|
480
|
+
...options,
|
|
436
481
|
}
|
|
437
482
|
: undefined,
|
|
438
483
|
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { coordEnv } from "../../lib/env.ts";
|
|
2
|
+
import type { AutoUpdateNotice, TickResult } from "../autoupdate/types.ts";
|
|
3
|
+
import { readAutoUpdateConfig } from "../config.ts";
|
|
4
|
+
|
|
5
|
+
/** Prefix on every self-update line injected into an agent's context. */
|
|
6
|
+
export const AUTOUPDATE_CUE_PREFIX = "Auto-update: ";
|
|
7
|
+
|
|
8
|
+
export interface AutoUpdateCueDeps {
|
|
9
|
+
tick: (coordRoot: string) => TickResult;
|
|
10
|
+
pendingNotices: (coordRoot: string, sessionKey: string) => AutoUpdateNotice[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AutoUpdateCueOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Whether this hook will show context to the agent. When false, the tick
|
|
16
|
+
* still runs but notices stay queued for a session that can read them.
|
|
17
|
+
*/
|
|
18
|
+
deliver?: boolean;
|
|
19
|
+
/** Test seam; production loads the engine lazily. */
|
|
20
|
+
deps?: AutoUpdateCueDeps;
|
|
21
|
+
/** Receives tick or delivery failures. The cue itself never throws. */
|
|
22
|
+
onError?: (err: unknown, phase: "autoupdate-tick" | "autoupdate-notices") => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadDeps(): Promise<AutoUpdateCueDeps> {
|
|
26
|
+
const engine = await import("../autoupdate/index.ts");
|
|
27
|
+
return {
|
|
28
|
+
tick: (root) => engine.autoUpdateTick(root),
|
|
29
|
+
pendingNotices: (root, key) => engine.pendingNotices(root, key),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fire the activity-driven self-update check (ADR 0229) and collect the
|
|
35
|
+
* notices this session has not seen yet, one context line each.
|
|
36
|
+
*
|
|
37
|
+
* Projects without an `autoUpdate` section pay one config stat and never load
|
|
38
|
+
* the engine. With the section, the tick's hot path is one stamp stat; any
|
|
39
|
+
* real work runs in a detached child, so the hook never waits on it. Fails
|
|
40
|
+
* open: an error is reported through `onError` and the cue is empty.
|
|
41
|
+
*/
|
|
42
|
+
export async function autoUpdateHookContext(
|
|
43
|
+
coordRoot: string,
|
|
44
|
+
sessionKey: string,
|
|
45
|
+
options: AutoUpdateCueOptions = {},
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
if (coordEnv("AUTOUPDATE_CHILD") === "1") return "";
|
|
48
|
+
let deps: AutoUpdateCueDeps;
|
|
49
|
+
try {
|
|
50
|
+
if (!readAutoUpdateConfig(coordRoot)) return "";
|
|
51
|
+
deps = options.deps ?? (await loadDeps());
|
|
52
|
+
} catch (err) {
|
|
53
|
+
options.onError?.(err, "autoupdate-tick");
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
deps.tick(coordRoot);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
options.onError?.(err, "autoupdate-tick");
|
|
60
|
+
}
|
|
61
|
+
if (options.deliver === false || !sessionKey) return "";
|
|
62
|
+
try {
|
|
63
|
+
return deps
|
|
64
|
+
.pendingNotices(coordRoot, sessionKey)
|
|
65
|
+
.map((notice) => `${AUTOUPDATE_CUE_PREFIX}${notice.text.trim()}`)
|
|
66
|
+
.filter((line) => line.length > AUTOUPDATE_CUE_PREFIX.length)
|
|
67
|
+
.join("\n");
|
|
68
|
+
} catch (err) {
|
|
69
|
+
options.onError?.(err, "autoupdate-notices");
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/core/hooks/cli.ts
CHANGED
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
parsePayload,
|
|
109
109
|
} from "./adapter/parse.ts";
|
|
110
110
|
import { hookTimeoutFor } from "./adapter/wiring.ts";
|
|
111
|
+
import { autoUpdateHookContext } from "./autoupdate-cue.ts";
|
|
111
112
|
import { scheduleBackupSnapshot } from "./backup-schedule.ts";
|
|
112
113
|
import {
|
|
113
114
|
captureImages,
|
|
@@ -1316,6 +1317,13 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
|
1316
1317
|
} catch (err) {
|
|
1317
1318
|
logError(coordRoot, err, { phase: "backup-schedule" });
|
|
1318
1319
|
}
|
|
1320
|
+
// Project self-update (ADR 0229) fires on activity. The tick costs one stat
|
|
1321
|
+
// when fresh and otherwise launches a detached child; queued notices for
|
|
1322
|
+
// this session ride the same context as the backup cue.
|
|
1323
|
+
const autoUpdateCue = await autoUpdateHookContext(coordRoot, owner.instance_id, {
|
|
1324
|
+
deliver: coordEnv("WORKFLOW_CHILD") !== "1",
|
|
1325
|
+
onError: (err, phase) => logError(coordRoot, err, { phase }),
|
|
1326
|
+
});
|
|
1319
1327
|
// Storage maintenance can walk large stores. It runs in a detached worker so
|
|
1320
1328
|
// session start emits its context without waiting for it; an adapter that
|
|
1321
1329
|
// stops a slow hook would otherwise drop that context entirely.
|
|
@@ -1344,7 +1352,7 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
|
1344
1352
|
data,
|
|
1345
1353
|
adapter,
|
|
1346
1354
|
recovery?.briefing ?? "",
|
|
1347
|
-
backupCue,
|
|
1355
|
+
[backupCue, autoUpdateCue].filter(Boolean).join("\n\n"),
|
|
1348
1356
|
);
|
|
1349
1357
|
if (injected && recovery) {
|
|
1350
1358
|
completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
|
|
@@ -1473,6 +1481,11 @@ async function handleTurnStarted(run: HookRun): Promise<void> {
|
|
|
1473
1481
|
logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
|
|
1474
1482
|
}
|
|
1475
1483
|
}
|
|
1484
|
+
// Project self-update (ADR 0229): see handleSessionStarted.
|
|
1485
|
+
const autoUpdateCue = await autoUpdateHookContext(coordRoot, owner.instance_id, {
|
|
1486
|
+
deliver: coordEnv("WORKFLOW_CHILD") !== "1",
|
|
1487
|
+
onError: (err, phase) => logError(coordRoot, err, { phase }),
|
|
1488
|
+
});
|
|
1476
1489
|
try {
|
|
1477
1490
|
const injected = await emitUserPromptSubmitSystemMessage(
|
|
1478
1491
|
coordRoot,
|
|
@@ -1482,6 +1495,7 @@ async function handleTurnStarted(run: HookRun): Promise<void> {
|
|
|
1482
1495
|
payload?.cwd,
|
|
1483
1496
|
recovery?.briefing ?? "",
|
|
1484
1497
|
hostPromptContext,
|
|
1498
|
+
autoUpdateCue,
|
|
1485
1499
|
);
|
|
1486
1500
|
if (injected && recovery) {
|
|
1487
1501
|
completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
|
|
@@ -1542,6 +1556,14 @@ async function handleStop(run: HookRun): Promise<number | undefined> {
|
|
|
1542
1556
|
});
|
|
1543
1557
|
}
|
|
1544
1558
|
if (!verdict.allow) {
|
|
1559
|
+
// Re-arm the prompt-time ritual reminder; it is otherwise shown only on a
|
|
1560
|
+
// session's first prompt so the per-prompt context stays small.
|
|
1561
|
+
try {
|
|
1562
|
+
const { markTurnRitualBounced } = await import("../agents/render/prompt-context.ts");
|
|
1563
|
+
markTurnRitualBounced(coordRoot, owner.instance_id);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
logError(coordRoot, err, { phase: "stop-ritual-rearm" });
|
|
1566
|
+
}
|
|
1545
1567
|
// Backstop for sessions whose evidence can never land (e.g. a headless
|
|
1546
1568
|
// child whose status command resolves to a different owner): after
|
|
1547
1569
|
// `cap` consecutive blocked stops in one cycle, allow the stop instead
|
|
@@ -2435,6 +2457,7 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
2435
2457
|
workspaceCwd?: string,
|
|
2436
2458
|
recoveryBriefing = "",
|
|
2437
2459
|
hostPromptContext = "",
|
|
2460
|
+
autoUpdateCue = "",
|
|
2438
2461
|
): Promise<boolean> {
|
|
2439
2462
|
// Workflow children get no prompt-time coordination context, matching
|
|
2440
2463
|
// SessionStart. The turn ritual is not enforced for them, the naming nudge
|
|
@@ -2463,6 +2486,9 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
2463
2486
|
if (hostPromptContext) {
|
|
2464
2487
|
additionalContext = [additionalContext, hostPromptContext].filter(Boolean).join("\n\n");
|
|
2465
2488
|
}
|
|
2489
|
+
if (autoUpdateCue) {
|
|
2490
|
+
additionalContext = [additionalContext, autoUpdateCue].filter(Boolean).join("\n\n");
|
|
2491
|
+
}
|
|
2466
2492
|
if (!additionalContext) return false;
|
|
2467
2493
|
|
|
2468
2494
|
const { emitContext } = await import("./adapter/output.ts");
|
|
@@ -17,7 +17,7 @@ import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { applyDetection } from "../../../lib/presence.ts";
|
|
20
|
-
import { coordBinPath } from "../../agents/coord-bin.ts";
|
|
20
|
+
import { coordBinPath, spawnCoordBinSync } from "../../agents/coord-bin.ts";
|
|
21
21
|
|
|
22
22
|
export { captureImages, imageJanitor, recordImageArtifactsV3 } from "./image-capture.ts";
|
|
23
23
|
|
|
@@ -70,7 +70,7 @@ export function journalJanitor(repoRoot: string): void {
|
|
|
70
70
|
try {
|
|
71
71
|
const bin = harnBin(repoRoot);
|
|
72
72
|
if (!bin) return;
|
|
73
|
-
|
|
73
|
+
spawnCoordBinSync(bin, ["journal", "janitor", "--quiet"], {
|
|
74
74
|
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
75
75
|
timeout: 5000,
|
|
76
76
|
stdio: "ignore",
|
|
@@ -89,7 +89,7 @@ export function journalRecoveryCue(repoRoot: string): string {
|
|
|
89
89
|
try {
|
|
90
90
|
const bin = harnBin(repoRoot);
|
|
91
91
|
if (!bin) return "";
|
|
92
|
-
const r =
|
|
92
|
+
const r = spawnCoordBinSync(bin, ["journal", "recovery-cue"], {
|
|
93
93
|
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
94
94
|
timeout: 5000,
|
|
95
95
|
encoding: "utf8",
|
|
@@ -105,7 +105,7 @@ export function journalArchive(repoRoot: string, owner: string): void {
|
|
|
105
105
|
try {
|
|
106
106
|
const bin = harnBin(repoRoot);
|
|
107
107
|
if (!bin || !owner) return;
|
|
108
|
-
|
|
108
|
+
spawnCoordBinSync(bin, ["journal", "archive", "--owner", owner], {
|
|
109
109
|
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
110
110
|
timeout: 5000,
|
|
111
111
|
stdio: "ignore",
|
|
@@ -91,6 +91,7 @@ function gitRevParse(start: string, flag: string): string | null {
|
|
|
91
91
|
// A stale adapter project directory may no longer exist. Treat spawn
|
|
92
92
|
// failure as no path evidence instead of turning discovery into an error.
|
|
93
93
|
const result = spawnSync("git", ["rev-parse", flag], {
|
|
94
|
+
windowsHide: true,
|
|
94
95
|
cwd: start,
|
|
95
96
|
encoding: "utf8",
|
|
96
97
|
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { hostname } from "node:os";
|
|
19
19
|
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
20
21
|
import { coordEnv } from "../../lib/env.ts";
|
|
21
22
|
import {
|
|
22
23
|
type ReadLedgerV3SinceResult,
|
|
@@ -150,7 +151,9 @@ export async function spawnSemanticService(
|
|
|
150
151
|
const sharedLogs = coordEnv("SHARED_LOGS") !== "0";
|
|
151
152
|
const logFd = sharedLogs ? undefined : openSync(paths.log, "a", stateFileMode());
|
|
152
153
|
if (logFd !== undefined) chmodSync(paths.log, stateFileMode());
|
|
153
|
-
const harnBin =
|
|
154
|
+
const harnBin = fileURLToPath(
|
|
155
|
+
new URL(import.meta.url.endsWith(".ts") ? "../../cli.ts" : "../../cli.js", import.meta.url),
|
|
156
|
+
);
|
|
154
157
|
if (!existsSync(harnBin)) {
|
|
155
158
|
if (logFd !== undefined) closeSync(logFd);
|
|
156
159
|
throw new Error(`cannot find harn executable at ${harnBin}`);
|
|
@@ -160,7 +163,8 @@ export async function spawnSemanticService(
|
|
|
160
163
|
args.push("--calls-per-hour", String(options.callsPerHour));
|
|
161
164
|
}
|
|
162
165
|
let spawnError: Error | undefined;
|
|
163
|
-
const child = spawn(harnBin, args, {
|
|
166
|
+
const child = spawn(process.execPath, [harnBin, ...args], {
|
|
167
|
+
windowsHide: true,
|
|
164
168
|
cwd: coordRoot,
|
|
165
169
|
detached: true,
|
|
166
170
|
stdio: logFd === undefined ? ["ignore", "ignore", "ignore"] : ["ignore", logFd, logFd],
|
|
@@ -54,6 +54,12 @@ export function stateFileMode(): number {
|
|
|
54
54
|
* True when a state file or directory is readable or writable by someone it should not be: anyone
|
|
55
55
|
* but the owner in private mode, anyone outside the owner and group in group mode.
|
|
56
56
|
*/
|
|
57
|
-
export function stateModeTooOpen(
|
|
57
|
+
export function stateModeTooOpen(
|
|
58
|
+
mode: number,
|
|
59
|
+
platform: NodeJS.Platform = process.platform,
|
|
60
|
+
): boolean {
|
|
61
|
+
// Windows synthesizes POSIX mode bits; access is controlled by inherited
|
|
62
|
+
// NTFS ACLs, which chmod and stat.mode cannot inspect or tighten.
|
|
63
|
+
if (platform === "win32") return false;
|
|
58
64
|
return (mode & (stateSharing() === "group" ? 0o007 : 0o077)) !== 0;
|
|
59
65
|
}
|
|
@@ -61,10 +61,24 @@ export function writeImmutableJson(path: string, value: unknown): boolean {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export function fsyncParentDirectory(path: string): void {
|
|
64
|
-
|
|
64
|
+
fsyncDirectory(dirname(path));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Flush directory entries where the runtime supports it; file fsync remains mandatory. */
|
|
68
|
+
export function fsyncDirectory(directory: string): void {
|
|
65
69
|
const fd = openSync(directory, "r");
|
|
66
70
|
try {
|
|
67
71
|
fsyncSync(fd);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Windows does not expose directory FlushFileBuffers through node:fs.
|
|
74
|
+
// Keep atomic links/renames and file flushes, but do not claim POSIX
|
|
75
|
+
// directory-entry durability on that platform. Other errors still fail.
|
|
76
|
+
if (
|
|
77
|
+
process.platform !== "win32" ||
|
|
78
|
+
!["EPERM", "EINVAL", "ENOTSUP"].includes((error as NodeJS.ErrnoException).code ?? "")
|
|
79
|
+
) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
68
82
|
} finally {
|
|
69
83
|
closeSync(fd);
|
|
70
84
|
}
|
|
@@ -1101,6 +1101,7 @@ async function executeWorkflow(
|
|
|
1101
1101
|
runId,
|
|
1102
1102
|
agentId: id,
|
|
1103
1103
|
subscriptionOnly,
|
|
1104
|
+
...(agentOpts.mcp ? { mcp: agentOpts.mcp } : {}),
|
|
1104
1105
|
// A full-access launch has no sandbox to project a policy into.
|
|
1105
1106
|
...(access.mode === "full-access"
|
|
1106
1107
|
? { access: "full-access" as const }
|
|
@@ -1505,6 +1506,8 @@ function agentCallKey(
|
|
|
1505
1506
|
agentOpts.schema ?? null,
|
|
1506
1507
|
];
|
|
1507
1508
|
if (agentOpts.specialist) parts.push(agentOpts.specialist);
|
|
1509
|
+
// Appended only when set so existing resume keys stay stable.
|
|
1510
|
+
if (agentOpts.mcp) parts.push({ mcp: agentOpts.mcp });
|
|
1508
1511
|
parts.push(prompt);
|
|
1509
1512
|
const basis = JSON.stringify(parts);
|
|
1510
1513
|
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
@@ -16,9 +16,17 @@
|
|
|
16
16
|
* visible to peers — the point of running them under harnery. So the child
|
|
17
17
|
* gets HARNERY_WORKFLOW_CHILD=1 and the stop-hook rule exempts it
|
|
18
18
|
* (stop-hook.ts), keeping heartbeats + events on.
|
|
19
|
+
* 3. **No MCP servers or claude.ai connectors unless the spawn asks.** A
|
|
20
|
+
* headless child otherwise loads every connector on the account (about 130
|
|
21
|
+
* tool schemas) into every turn. `req.mcp` defaults to `"none"` (strict
|
|
22
|
+
* empty MCP config plus ENABLE_CLAUDEAI_MCP_SERVERS=false); `"inherit"` or
|
|
23
|
+
* a server map is the per-spawn opt-in. Project settings, hooks, and
|
|
24
|
+
* CLAUDE.md still load: workflow children need them, so this adapter never
|
|
25
|
+
* passes `--setting-sources ""`.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import { exec } from "../../lib/exec.ts";
|
|
29
|
+
import { claudeMcpLaunch } from "../../lib/headless/index.ts";
|
|
22
30
|
import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.ts";
|
|
23
31
|
import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
|
|
24
32
|
import { notFoundError } from "./adapters.ts";
|
|
@@ -62,6 +70,7 @@ export function buildClaudeInvocation(req: SpawnRequest): AdapterInvocation {
|
|
|
62
70
|
if (fullAccess) argv.push(...fullAccess);
|
|
63
71
|
if (req.model) argv.push("--model", req.model);
|
|
64
72
|
if (req.effort) argv.push("--effort", req.effort);
|
|
73
|
+
argv.push(...claudeMcpLaunch(req.mcp).args);
|
|
65
74
|
return { argv };
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -152,10 +161,13 @@ export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<Spa
|
|
|
152
161
|
}
|
|
153
162
|
const r = await exec(invocation.argv, {
|
|
154
163
|
cwd: req.cwd,
|
|
155
|
-
env:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
env: {
|
|
165
|
+
...buildChildEnv(req.runId, {
|
|
166
|
+
subscriptionOnly: req.subscriptionOnly,
|
|
167
|
+
agentId: req.agentId,
|
|
168
|
+
}),
|
|
169
|
+
...claudeMcpLaunch(req.mcp).env,
|
|
170
|
+
},
|
|
159
171
|
timeout: req.timeoutMs,
|
|
160
172
|
});
|
|
161
173
|
return normalizeClaudeResult({ ...r, durationMs: Date.now() - t0 });
|