harnery 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
package/src/core/config.ts
CHANGED
|
@@ -80,6 +80,8 @@ interface HarneryConfig {
|
|
|
80
80
|
/** Optional host prompt-context extension. Project config only. */
|
|
81
81
|
hooks?: {
|
|
82
82
|
promptContext?: unknown;
|
|
83
|
+
/** Project-only opt-in; read via `codexWindowsBridgeConfig()`. */
|
|
84
|
+
codexWindowsBridge?: unknown;
|
|
83
85
|
};
|
|
84
86
|
/**
|
|
85
87
|
* Agent-ritual policy owned by the host project. Git finalization is opt-in:
|
|
@@ -107,7 +109,7 @@ interface HarneryConfig {
|
|
|
107
109
|
* `workflow run` in this repo to subscription billing (API-key vars are
|
|
108
110
|
* scrubbed from child envs) without anyone having to remember the flag.
|
|
109
111
|
*/
|
|
110
|
-
workflow?: { subscriptionOnly?: boolean };
|
|
112
|
+
workflow?: { subscriptionOnly?: boolean; workerFullAccess?: unknown };
|
|
111
113
|
/**
|
|
112
114
|
* Cross-machine presence (ADR 0016). `{ enabled: false }` opts a repo out of
|
|
113
115
|
* the git-refs transport (publishing `refs/harnery/presence/<machine>` to
|
|
@@ -166,11 +168,22 @@ interface HarneryConfig {
|
|
|
166
168
|
* Managed working-artifact defaults. `default_retention_days` is the
|
|
167
169
|
* create-time TTL when the caller does not pass `artifacts create --days`.
|
|
168
170
|
*/
|
|
171
|
+
/** Local server registry (`servers` command, `harnery/core/servers`). */
|
|
172
|
+
servers?: {
|
|
173
|
+
/** Inclusive port range per server type, e.g. `"preview": [3100, 3199]`. */
|
|
174
|
+
port_ranges?: Record<string, unknown>;
|
|
175
|
+
idle_hours?: number;
|
|
176
|
+
owner_stale_hours?: number;
|
|
177
|
+
};
|
|
169
178
|
artifacts?: {
|
|
170
179
|
default_retention_days?: number;
|
|
171
180
|
auto_clean?: boolean;
|
|
172
181
|
max_bytes?: number;
|
|
173
182
|
max_unit_bytes?: number;
|
|
183
|
+
idle_grace_hours?: number;
|
|
184
|
+
hold_days?: number;
|
|
185
|
+
max_held_bytes?: number;
|
|
186
|
+
min_free_bytes?: number;
|
|
174
187
|
};
|
|
175
188
|
/**
|
|
176
189
|
* Page review packs. `auto_clean` lets `qa-run` and `review-pack create`
|
|
@@ -494,6 +507,42 @@ export function resolveHooksSetupHint(coordRoot?: string | null): string | null
|
|
|
494
507
|
* values fail closed to no reminder so a malformed optional setting cannot
|
|
495
508
|
* break the prompt hook.
|
|
496
509
|
*/
|
|
510
|
+
/** A plain Windows command name, so it is safe to splice into a PowerShell hook command. */
|
|
511
|
+
const CODEX_WINDOWS_BRIDGE_ENTRY_POINT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
512
|
+
|
|
513
|
+
export interface CodexWindowsBridgeConfig {
|
|
514
|
+
/** Command name Windows-native Codex hooks hand off to, or null when not opted in. */
|
|
515
|
+
entryPoint: string | null;
|
|
516
|
+
/** Why the configured value was ignored, when it was. */
|
|
517
|
+
error: string | null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Per-project opt-in that routes Windows-native Codex hooks through a resident
|
|
522
|
+
* WSL bridge (ADR 0197). Read from the project config only: the value is
|
|
523
|
+
* written into the committed `.codex/hooks.json`, so a user-level setting
|
|
524
|
+
* would make `init` output differ between machines. An invalid value is
|
|
525
|
+
* ignored and reported rather than spliced into a shell command.
|
|
526
|
+
*/
|
|
527
|
+
export function codexWindowsBridgeConfig(projectRoot: string): CodexWindowsBridgeConfig {
|
|
528
|
+
const raw = readProjectConfig(projectRoot).hooks?.codexWindowsBridge;
|
|
529
|
+
if (raw === undefined) return { entryPoint: null, error: null };
|
|
530
|
+
const entryPoint = isPlainObject(raw) ? raw.entryPoint : undefined;
|
|
531
|
+
if (typeof entryPoint !== "string") {
|
|
532
|
+
return {
|
|
533
|
+
entryPoint: null,
|
|
534
|
+
error: "hooks.codexWindowsBridge.entryPoint must be a command name",
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (!CODEX_WINDOWS_BRIDGE_ENTRY_POINT.test(entryPoint)) {
|
|
538
|
+
return {
|
|
539
|
+
entryPoint: null,
|
|
540
|
+
error: `hooks.codexWindowsBridge.entryPoint ${JSON.stringify(entryPoint)} is not a plain command name`,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return { entryPoint, error: null };
|
|
544
|
+
}
|
|
545
|
+
|
|
497
546
|
export function hostPromptReminder(coordRoot?: string | null): string | null {
|
|
498
547
|
const root = coordRoot ?? findCoordRoot();
|
|
499
548
|
if (!root) return null;
|
|
@@ -601,6 +650,27 @@ export function workflowSubscriptionOnly(coordRoot?: string | null): boolean {
|
|
|
601
650
|
return readConfig(root).workflow?.subscriptionOnly === true;
|
|
602
651
|
}
|
|
603
652
|
|
|
653
|
+
/**
|
|
654
|
+
* The host's raw worker full-access policy (ADR 0192), validated by
|
|
655
|
+
* `parseWorkerFullAccessPolicy` in the workflow layer, which knows each
|
|
656
|
+
* adapter's effort scale.
|
|
657
|
+
*
|
|
658
|
+
* `.harnery/config.jsonc`:
|
|
659
|
+
* `{ "workflow": { "workerFullAccess": { "enabled": true, "floors": { ... } } } }`
|
|
660
|
+
*
|
|
661
|
+
* This is a trust boundary, so it comes only from the project config, like
|
|
662
|
+
* `agents.finalizationRoots`: a user-global file cannot remove the sandbox from
|
|
663
|
+
* one project's workers. `HARNERY_WORKFLOW_WORKER_FULL_ACCESS=0` turns it off
|
|
664
|
+
* for one process. There is deliberately no `=1`: an environment variable can
|
|
665
|
+
* withdraw the grant but never supply it.
|
|
666
|
+
*/
|
|
667
|
+
export function workflowWorkerFullAccessConfig(coordRoot?: string | null): unknown {
|
|
668
|
+
if (coordEnv("WORKFLOW_WORKER_FULL_ACCESS") === "0") return { enabled: false };
|
|
669
|
+
const root = coordRoot ?? findCoordRoot();
|
|
670
|
+
if (!root) return undefined;
|
|
671
|
+
return readProjectConfig(root).workflow?.workerFullAccess;
|
|
672
|
+
}
|
|
673
|
+
|
|
604
674
|
/**
|
|
605
675
|
* How this project's `.harnery/` state is shared: `private` or `group`. `HARNERY_STORAGE_SHARING`
|
|
606
676
|
* overrides per process; an unrecognized value falls back to `private`, the safer reading.
|
|
@@ -765,6 +835,46 @@ export function resolveWebPort(explicitPort?: string, coordRoot?: string | null)
|
|
|
765
835
|
return DEFAULT_WEB_PORT;
|
|
766
836
|
}
|
|
767
837
|
|
|
838
|
+
/** The configured inclusive port range for one server type, or null. */
|
|
839
|
+
export function serverPortRange(type: string, coordRoot?: string | null): [number, number] | null {
|
|
840
|
+
const root = coordRoot ?? findCoordRoot();
|
|
841
|
+
const value = root ? readConfig(root).servers?.port_ranges?.[type] : undefined;
|
|
842
|
+
if (!Array.isArray(value) || value.length !== 2) return null;
|
|
843
|
+
const [low, high] = value as unknown[];
|
|
844
|
+
const valid = (n: unknown): n is number =>
|
|
845
|
+
Number.isInteger(n) && (n as number) > 0 && (n as number) < 65536;
|
|
846
|
+
return valid(low) && valid(high) && low <= high ? [low, high] : null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Every configured server port range, keyed by type (invalid entries dropped). */
|
|
850
|
+
export function serverPortRanges(coordRoot?: string | null): Record<string, [number, number]> {
|
|
851
|
+
const root = coordRoot ?? findCoordRoot();
|
|
852
|
+
const ranges = root ? readConfig(root).servers?.port_ranges : undefined;
|
|
853
|
+
const out: Record<string, [number, number]> = {};
|
|
854
|
+
for (const type of Object.keys(ranges ?? {})) {
|
|
855
|
+
const range = serverPortRange(type, root);
|
|
856
|
+
if (range) out[type] = range;
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Configured `servers gc` windows; undefined fields use the built-in defaults. */
|
|
862
|
+
export function serverGcConfig(coordRoot?: string | null): {
|
|
863
|
+
idle_hours?: number;
|
|
864
|
+
owner_stale_hours?: number;
|
|
865
|
+
} {
|
|
866
|
+
const root = coordRoot ?? findCoordRoot();
|
|
867
|
+
const configured = root ? readConfig(root).servers : undefined;
|
|
868
|
+
const positive = (n: unknown): n is number =>
|
|
869
|
+
typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
870
|
+
return {
|
|
871
|
+
...(positive(configured?.idle_hours) ? { idle_hours: configured.idle_hours } : {}),
|
|
872
|
+
...(positive(configured?.owner_stale_hours)
|
|
873
|
+
? { owner_stale_hours: configured.owner_stale_hours }
|
|
874
|
+
: {}),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
768
878
|
/** Policy for converging independent termination signals on one finalizer. */
|
|
769
879
|
export function sessionFinalizationConfig(coordRoot?: string | null): SessionFinalizationConfig {
|
|
770
880
|
const root = coordRoot ?? findCoordRoot();
|
|
@@ -843,12 +953,81 @@ export function artifactMaxUnitBytes(coordRoot?: string | null): number {
|
|
|
843
953
|
);
|
|
844
954
|
}
|
|
845
955
|
|
|
956
|
+
/**
|
|
957
|
+
* How long an unexpired workspace must sit idle before a size rule (the
|
|
958
|
+
* per-bundle ceiling or the repository budget) may delete it. Idle means no
|
|
959
|
+
* change inside the workspace and no heartbeat from its owner. An agent waiting
|
|
960
|
+
* on a human reply looks idle, so a short window deletes work someone is still
|
|
961
|
+
* reviewing. Precedence: `HARNERY_ARTIFACT_IDLE_GRACE_HOURS` ->
|
|
962
|
+
* `artifacts.idle_grace_hours` -> 24. Expiry never waits for this grace.
|
|
963
|
+
*/
|
|
964
|
+
export function artifactIdleGraceHours(coordRoot?: string | null): number {
|
|
965
|
+
const root = coordRoot ?? findCoordRoot();
|
|
966
|
+
return integerSetting(
|
|
967
|
+
coordEnv("ARTIFACT_IDLE_GRACE_HOURS"),
|
|
968
|
+
root ? readConfig(root).artifacts?.idle_grace_hours : undefined,
|
|
969
|
+
1,
|
|
970
|
+
24 * 365,
|
|
971
|
+
24,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Default lifetime of an artifact hold. A hold that is not renewed lapses after
|
|
977
|
+
* this many days and the workspace returns to ordinary retention. Precedence:
|
|
978
|
+
* `HARNERY_ARTIFACT_HOLD_DAYS` -> `artifacts.hold_days` -> 14.
|
|
979
|
+
*/
|
|
980
|
+
export function artifactHoldDays(coordRoot?: string | null): number {
|
|
981
|
+
const root = coordRoot ?? findCoordRoot();
|
|
982
|
+
return integerSetting(
|
|
983
|
+
coordEnv("ARTIFACT_HOLD_DAYS"),
|
|
984
|
+
root ? readConfig(root).artifacts?.hold_days : undefined,
|
|
985
|
+
1,
|
|
986
|
+
365,
|
|
987
|
+
14,
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Advisory budget for held artifact bytes. Exceeding it produces warnings
|
|
993
|
+
* only: holds record owner intent, so cleanup never deletes held work.
|
|
994
|
+
* Precedence: `HARNERY_ARTIFACT_MAX_HELD_BYTES` -> `artifacts.max_held_bytes`
|
|
995
|
+
* -> 20 GiB.
|
|
996
|
+
*/
|
|
997
|
+
export function artifactMaxHeldBytes(coordRoot?: string | null): number {
|
|
998
|
+
const root = coordRoot ?? findCoordRoot();
|
|
999
|
+
return integerSetting(
|
|
1000
|
+
coordEnv("ARTIFACT_MAX_HELD_BYTES"),
|
|
1001
|
+
root ? readConfig(root).artifacts?.max_held_bytes : undefined,
|
|
1002
|
+
64 * 1024 * 1024,
|
|
1003
|
+
1024 * 1024 * 1024 * 1024,
|
|
1004
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Free-disk floor for the artifact store. Below it, artifact commands warn and
|
|
1010
|
+
* `artifacts create --big` refuses unless `--allow-low-disk` is passed.
|
|
1011
|
+
* Precedence: `HARNERY_ARTIFACT_MIN_FREE_BYTES` -> `artifacts.min_free_bytes`
|
|
1012
|
+
* -> 20 GiB. Zero disables the floor.
|
|
1013
|
+
*/
|
|
1014
|
+
export function artifactMinFreeBytes(coordRoot?: string | null): number {
|
|
1015
|
+
const root = coordRoot ?? findCoordRoot();
|
|
1016
|
+
return integerSetting(
|
|
1017
|
+
coordEnv("ARTIFACT_MIN_FREE_BYTES"),
|
|
1018
|
+
root ? readConfig(root).artifacts?.min_free_bytes : undefined,
|
|
1019
|
+
0,
|
|
1020
|
+
1024 * 1024 * 1024 * 1024 * 1024,
|
|
1021
|
+
20 * 1024 * 1024 * 1024,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
846
1025
|
/**
|
|
847
1026
|
* Whether opportunistic cleanup of expired artifact workspaces runs.
|
|
848
1027
|
* Precedence: `HARNERY_ARTIFACT_AUTO_CLEAN` (0/false disables) ->
|
|
849
|
-
* `artifacts.auto_clean` -> enabled. The sweep
|
|
850
|
-
* `managed-expired`
|
|
851
|
-
* `artifacts clean --yes
|
|
1028
|
+
* `artifacts.auto_clean` -> enabled. The sweep deletes the same
|
|
1029
|
+
* `managed-expired`, `managed-oversize`, and `managed-over-budget` entries as
|
|
1030
|
+
* `artifacts clean --yes`, through the same guarded classifier.
|
|
852
1031
|
*/
|
|
853
1032
|
export function artifactAutoCleanEnabled(coordRoot?: string | null): boolean {
|
|
854
1033
|
const env = coordEnv("ARTIFACT_AUTO_CLEAN");
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "../artifacts/index.ts";
|
|
16
16
|
import {
|
|
17
17
|
ARTIFACT_MANIFEST,
|
|
18
|
+
artifactHoldState,
|
|
18
19
|
artifactsRoot,
|
|
19
20
|
configuredArtifactRetentionDays,
|
|
20
21
|
createArtifact,
|
|
@@ -412,7 +413,7 @@ function candidateArtifactId(artifactPath: string): string | undefined {
|
|
|
412
413
|
function candidateClassification(
|
|
413
414
|
manifest: ArtifactManifestV2,
|
|
414
415
|
): ArtifactInventoryEntry["classification"] {
|
|
415
|
-
if (manifest.
|
|
416
|
+
if (artifactHoldState(manifest).active.length > 0) return "managed-held";
|
|
416
417
|
if (!manifest.released_at) return "managed-active";
|
|
417
418
|
return Date.parse(manifest.retention.expires_at) <= Date.now()
|
|
418
419
|
? "managed-expired"
|
|
@@ -1130,6 +1130,22 @@ function processHookSignalLocked(
|
|
|
1130
1130
|
if (input.signal === "sub-agent-start") {
|
|
1131
1131
|
if (!sourceId) return { state: "ignored" };
|
|
1132
1132
|
delegation = state.delegations.find((candidate) => candidate.source_id === sourceId);
|
|
1133
|
+
if (delegation?.open_event_id) {
|
|
1134
|
+
// SendMessage to a running background agent re-fires SubagentStart.
|
|
1135
|
+
// The delegation is already open; a second agent.started is noise.
|
|
1136
|
+
writeProducerDiagnosticV3(input.coordRoot, "duplicate_subagent_start_suppressed", {
|
|
1137
|
+
adapter: input.adapter,
|
|
1138
|
+
signal: input.signal,
|
|
1139
|
+
reason: "delegation_already_open",
|
|
1140
|
+
instance_id: input.instance_id,
|
|
1141
|
+
session_hash: sessionHash,
|
|
1142
|
+
generation_id: state.generation_id,
|
|
1143
|
+
delegation_id: delegation.delegation_id,
|
|
1144
|
+
payload: input.payload,
|
|
1145
|
+
});
|
|
1146
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1147
|
+
return { state: "ignored" };
|
|
1148
|
+
}
|
|
1133
1149
|
if (!delegation) {
|
|
1134
1150
|
const opened = openSpanStateV3({
|
|
1135
1151
|
span_id: spanIdV3(),
|
|
@@ -539,7 +539,17 @@ function applyAgentStarted(
|
|
|
539
539
|
projection: SafetyProjectionV3,
|
|
540
540
|
event: Extract<EventV3, { event_type: "agent.started" }>,
|
|
541
541
|
): void {
|
|
542
|
-
|
|
542
|
+
const existing = projection.delegations[event.payload.delegation_id];
|
|
543
|
+
if (existing) {
|
|
544
|
+
// Claude Code re-fires SubagentStart when SendMessage resumes a background
|
|
545
|
+
// agent. A restart naming the same parent and child is that resume, not a
|
|
546
|
+
// conflicting delegation, and must not strip the parent's authority.
|
|
547
|
+
if (
|
|
548
|
+
existing.parent_generation_id === generationIdOf(event) &&
|
|
549
|
+
existing.child_generation_id === event.payload.child_generation_id
|
|
550
|
+
) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
543
553
|
addDiagnostic(projection, {
|
|
544
554
|
code: "delegation_duplicate_start",
|
|
545
555
|
event_id: event.event_id,
|
|
@@ -69,6 +69,20 @@ export interface AdapterSpec {
|
|
|
69
69
|
* wandered (the session shell `cd`ing into a subdirectory or off-repo).
|
|
70
70
|
*/
|
|
71
71
|
projectDirEnv?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Per-handler `timeout` (seconds) written on every Harnery hook entry. Without
|
|
74
|
+
* one, an adapter's own default applies (Codex: 600 seconds), so a stalled
|
|
75
|
+
* hook can hold a tool call for ten minutes. `bySubcommand` overrides
|
|
76
|
+
* `default` for one event.
|
|
77
|
+
*/
|
|
78
|
+
hookTimeouts?: { default: number; bySubcommand?: Record<string, number> };
|
|
79
|
+
/**
|
|
80
|
+
* The adapter runs a handler's `commandWindows` instead of `command` on
|
|
81
|
+
* Windows. A project that opts in (`hooks.codexWindowsBridge`) gets one that
|
|
82
|
+
* hands the hook to a resident WSL bridge (ADR 0197); otherwise init leaves
|
|
83
|
+
* the field off.
|
|
84
|
+
*/
|
|
85
|
+
windowsBridgeHooks?: boolean;
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
/** Claude Code: `.claude/settings.json`. */
|
|
@@ -191,6 +205,11 @@ export const ADAPTER_SPECS: Record<AdapterId, AdapterSpec> = {
|
|
|
191
205
|
legacyEvents: LEGACY_CODEX_EVENTS,
|
|
192
206
|
allowedTopLevelKeys: ["description", "hooks"],
|
|
193
207
|
allowedEventKeys: CODEX_ALLOWED_EVENT_KEYS,
|
|
208
|
+
// A Codex hook that times out fails open: the run is marked failed and
|
|
209
|
+
// the tool proceeds. Codex clamps SessionEnd to 3 seconds and defaults it
|
|
210
|
+
// to 1, which is too short for a cold hook start.
|
|
211
|
+
hookTimeouts: { default: 20, bySubcommand: { "session-end": 3 } },
|
|
212
|
+
windowsBridgeHooks: true,
|
|
194
213
|
},
|
|
195
214
|
opencode: {
|
|
196
215
|
settingsFile: ".opencode/plugins/harnery/index.ts",
|
|
@@ -671,7 +671,9 @@ function claudeModelContextCapability(model: string): ClaudeModelContextCapabili
|
|
|
671
671
|
const normalized = model.toLowerCase();
|
|
672
672
|
const oneMillionTokenModels = new Set([
|
|
673
673
|
"claude-fable-5",
|
|
674
|
+
"claude-opus-5-5",
|
|
674
675
|
"claude-opus-5",
|
|
676
|
+
"claude-sonnet-5-5",
|
|
675
677
|
"claude-sonnet-5",
|
|
676
678
|
"claude-opus-4-8",
|
|
677
679
|
"claude-opus-4-7",
|
|
@@ -679,7 +681,7 @@ function claudeModelContextCapability(model: string): ClaudeModelContextCapabili
|
|
|
679
681
|
"claude-sonnet-4-6",
|
|
680
682
|
]);
|
|
681
683
|
if (oneMillionTokenModels.has(normalized)) {
|
|
682
|
-
return { limit_tokens: 1_000_000, authority: "
|
|
684
|
+
return { limit_tokens: 1_000_000, authority: "anthropic_model_capabilities_2026_09" };
|
|
683
685
|
}
|
|
684
686
|
if (normalized === "claude-haiku-4-5" || normalized === "claude-haiku-4-5-20251001") {
|
|
685
687
|
return { limit_tokens: 200_000, authority: "anthropic_model_capabilities_2026_08" };
|
|
@@ -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 { resolveBinName } from "../../config.ts";
|
|
17
|
+
import { codexWindowsBridgeConfig, resolveBinName } from "../../config.ts";
|
|
18
18
|
import {
|
|
19
19
|
ADAPTER_SPECS,
|
|
20
20
|
type AdapterId,
|
|
@@ -24,10 +24,19 @@ import {
|
|
|
24
24
|
} from "./events.ts";
|
|
25
25
|
import { checkOpenCodePlugin, isOwnedOpenCodePlugin } from "./opencode-plugin.ts";
|
|
26
26
|
|
|
27
|
-
/** Claude Code
|
|
27
|
+
/** One Claude Code or Codex command handler. */
|
|
28
|
+
export interface ClaudeHookHandler {
|
|
29
|
+
type: string;
|
|
30
|
+
command: string;
|
|
31
|
+
/** Codex: run instead of `command` on Windows. */
|
|
32
|
+
commandWindows?: string;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Claude Code + Codex entry: `{ hooks: [{ type, command, commandWindows?, timeout? }] }`. */
|
|
28
37
|
export interface ClaudeHookGroup {
|
|
29
38
|
matcher?: string;
|
|
30
|
-
hooks:
|
|
39
|
+
hooks: ClaudeHookHandler[];
|
|
31
40
|
}
|
|
32
41
|
/** Cursor entry: a flat `{ command }`. */
|
|
33
42
|
export interface CursorHookGroup {
|
|
@@ -53,14 +62,92 @@ export interface SettingsFile {
|
|
|
53
62
|
export function makeEntry(
|
|
54
63
|
shape: HookEntryShape,
|
|
55
64
|
command: string,
|
|
56
|
-
extras?: { loop_limit?: number },
|
|
65
|
+
extras?: { loop_limit?: number; timeout?: number; commandWindows?: string },
|
|
57
66
|
): HookGroup {
|
|
58
|
-
if (shape !== "cursor")
|
|
67
|
+
if (shape !== "cursor") {
|
|
68
|
+
return {
|
|
69
|
+
hooks: [
|
|
70
|
+
orderHandlerKeys({
|
|
71
|
+
type: "command",
|
|
72
|
+
command,
|
|
73
|
+
commandWindows: extras?.commandWindows,
|
|
74
|
+
timeout: extras?.timeout,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
59
79
|
return extras?.loop_limit !== undefined
|
|
60
80
|
? { command, loop_limit: extras.loop_limit }
|
|
61
81
|
: { command };
|
|
62
82
|
}
|
|
63
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Rebuild a handler with Harnery's fields first in a fixed order, dropping
|
|
86
|
+
* undefined ones and keeping any other fields after them.
|
|
87
|
+
*/
|
|
88
|
+
export function orderHandlerKeys(handler: ClaudeHookHandler): ClaudeHookHandler {
|
|
89
|
+
const { type, command, commandWindows, timeout, ...rest } = handler;
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
command,
|
|
93
|
+
...(commandWindows !== undefined ? { commandWindows } : {}),
|
|
94
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
95
|
+
...rest,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The Windows command for an opted-in project (ADR 0197). It hands the hook to
|
|
101
|
+
* the bridge entry point when that is on PATH and the working directory is a
|
|
102
|
+
* WSL UNC path, and otherwise runs the usual command. `ProviderPath` drops the
|
|
103
|
+
* provider prefix that `Path` carries. The trailing `exit $LASTEXITCODE` is
|
|
104
|
+
* required: without it PowerShell reports any non-zero exit from inside the
|
|
105
|
+
* `if` block as 1, which would turn a hook's blocking exit 2 into a failure.
|
|
106
|
+
*/
|
|
107
|
+
export function hookCommandWindows(
|
|
108
|
+
spec: AdapterSpec,
|
|
109
|
+
agentHookPath: string,
|
|
110
|
+
subcommand: string,
|
|
111
|
+
adapter: AdapterId,
|
|
112
|
+
entryPoint: string,
|
|
113
|
+
): string {
|
|
114
|
+
const direct = hookCommand(spec, agentHookPath, subcommand, adapter);
|
|
115
|
+
return (
|
|
116
|
+
`if ((Get-Command ${entryPoint} -ErrorAction Ignore) -and $PWD.ProviderPath -like '\\\\wsl*') ` +
|
|
117
|
+
`{ ${entryPoint} ${subcommand} --adapter ${adapter} } else { ${direct} }; exit $LASTEXITCODE`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Project-level choices that change what init writes. */
|
|
122
|
+
export interface WiringOptions {
|
|
123
|
+
/** Bridge entry point from `hooks.codexWindowsBridge`, or null when not opted in. */
|
|
124
|
+
windowsBridgeEntryPoint?: string | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The `commandWindows` init writes for one event, or undefined for none. */
|
|
128
|
+
export function expectedCommandWindows(
|
|
129
|
+
spec: AdapterSpec,
|
|
130
|
+
agentHookPath: string,
|
|
131
|
+
subcommand: string,
|
|
132
|
+
adapter: AdapterId,
|
|
133
|
+
options?: WiringOptions,
|
|
134
|
+
): string | undefined {
|
|
135
|
+
if (!spec.windowsBridgeHooks || !options?.windowsBridgeEntryPoint) return undefined;
|
|
136
|
+
return hookCommandWindows(
|
|
137
|
+
spec,
|
|
138
|
+
agentHookPath,
|
|
139
|
+
subcommand,
|
|
140
|
+
adapter,
|
|
141
|
+
options.windowsBridgeEntryPoint,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The `timeout` init writes on one event's Harnery hook, or undefined for none. */
|
|
146
|
+
export function hookTimeoutFor(spec: AdapterSpec, subcommand: string): number | undefined {
|
|
147
|
+
if (!spec.hookTimeouts) return undefined;
|
|
148
|
+
return spec.hookTimeouts.bySubcommand?.[subcommand] ?? spec.hookTimeouts.default;
|
|
149
|
+
}
|
|
150
|
+
|
|
64
151
|
/** Pin Cursor Stop followups to Harnery's loop cap. Returns true when the entry changed. */
|
|
65
152
|
export function applyCursorStopLoopLimit(group: HookGroup, subcommand: string): boolean {
|
|
66
153
|
if (subcommand !== "stop") return false;
|
|
@@ -144,7 +231,7 @@ export interface WiringDiff {
|
|
|
144
231
|
duplicates: HookEvent[];
|
|
145
232
|
/** Spec events wired under an event key other than the canonical one. */
|
|
146
233
|
misplaced: HookEvent[];
|
|
147
|
-
/** Spec events whose correct-key command differs from init
|
|
234
|
+
/** Spec events whose correct-key command, `commandWindows`, or `timeout` differs from what init writes. */
|
|
148
235
|
stale: HookEvent[];
|
|
149
236
|
/** Settings fields rejected by a adapter with a strict top-level schema. */
|
|
150
237
|
invalidTopLevelKeys: string[];
|
|
@@ -159,7 +246,7 @@ export interface WiringDiff {
|
|
|
159
246
|
export function diffWiring(
|
|
160
247
|
settings: SettingsFile,
|
|
161
248
|
spec: AdapterSpec,
|
|
162
|
-
expected?: { agentHookPath: string; adapter: AdapterId },
|
|
249
|
+
expected?: { agentHookPath: string; adapter: AdapterId } & WiringOptions,
|
|
163
250
|
): WiringDiff {
|
|
164
251
|
const missing: HookEvent[] = [];
|
|
165
252
|
const present: HookEvent[] = [];
|
|
@@ -182,7 +269,27 @@ export function diffWiring(
|
|
|
182
269
|
event.subcommand,
|
|
183
270
|
expected.adapter,
|
|
184
271
|
);
|
|
185
|
-
|
|
272
|
+
const timeout = hookTimeoutFor(spec, event.subcommand);
|
|
273
|
+
const commandWindows = expectedCommandWindows(
|
|
274
|
+
spec,
|
|
275
|
+
expected.agentHookPath,
|
|
276
|
+
event.subcommand,
|
|
277
|
+
expected.adapter,
|
|
278
|
+
expected,
|
|
279
|
+
);
|
|
280
|
+
const handlerDrift = groups.some(
|
|
281
|
+
(group) =>
|
|
282
|
+
"hooks" in group &&
|
|
283
|
+
Array.isArray(group.hooks) &&
|
|
284
|
+
group.hooks.some(
|
|
285
|
+
(hook) =>
|
|
286
|
+
typeof hook.command === "string" &&
|
|
287
|
+
commandWiresSubcommand(hook.command, event.subcommand) &&
|
|
288
|
+
((timeout !== undefined && hook.timeout !== timeout) ||
|
|
289
|
+
(spec.windowsBridgeHooks === true && hook.commandWindows !== commandWindows)),
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
if (handlerDrift || commands.some((command) => command !== canonical)) stale.push(event);
|
|
186
293
|
}
|
|
187
294
|
const wrongKey = Object.entries(hooks).some(
|
|
188
295
|
([key, otherGroups]) =>
|
|
@@ -325,6 +432,7 @@ export function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[] {
|
|
|
325
432
|
? {
|
|
326
433
|
agentHookPath: agentHookPathForProject(projectRoot, packageRoot),
|
|
327
434
|
adapter: id,
|
|
435
|
+
windowsBridgeEntryPoint: codexWindowsBridgeConfig(projectRoot).entryPoint,
|
|
328
436
|
}
|
|
329
437
|
: undefined,
|
|
330
438
|
);
|
package/src/core/hooks/cli.ts
CHANGED
|
@@ -90,6 +90,7 @@ import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/
|
|
|
90
90
|
import { stableScopeId } from "../workflow/scope-id.ts";
|
|
91
91
|
import { type AdapterBehavior, adapterBehavior } from "./adapter/behaviors/index.ts";
|
|
92
92
|
import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.ts";
|
|
93
|
+
import { ADAPTER_SPECS } from "./adapter/events.ts";
|
|
93
94
|
import {
|
|
94
95
|
extractBashCommand,
|
|
95
96
|
extractToolDescription,
|
|
@@ -98,6 +99,7 @@ import {
|
|
|
98
99
|
type ParsedPayload,
|
|
99
100
|
parsePayload,
|
|
100
101
|
} from "./adapter/parse.ts";
|
|
102
|
+
import { hookTimeoutFor } from "./adapter/wiring.ts";
|
|
101
103
|
import { scheduleBackupSnapshot } from "./backup-schedule.ts";
|
|
102
104
|
import {
|
|
103
105
|
captureImages,
|
|
@@ -131,6 +133,7 @@ import {
|
|
|
131
133
|
scanStatusBoxPresent,
|
|
132
134
|
scanTranscriptRuntime,
|
|
133
135
|
} from "./resolve/transcript.ts";
|
|
136
|
+
import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.ts";
|
|
134
137
|
import { sessionNamePresence } from "./session-name-presence.ts";
|
|
135
138
|
import { shellWaiterReason } from "./shell-waiter.ts";
|
|
136
139
|
|
|
@@ -140,6 +143,7 @@ interface Argv {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
let hookHealthState: HookHealthState | undefined;
|
|
146
|
+
let hookRunMarkerPath: string | null = null;
|
|
143
147
|
|
|
144
148
|
function parseArgv(argv: string[]): Argv {
|
|
145
149
|
const out: Argv = { eventName: null, extra: [] };
|
|
@@ -351,7 +355,12 @@ function buildEventData(
|
|
|
351
355
|
const forkedFrom: string | undefined = undefined;
|
|
352
356
|
// Assign (or recover) name + kind in-process. Idempotent: resume
|
|
353
357
|
// returns the original name; new owner consumes a counter slot.
|
|
354
|
-
|
|
358
|
+
// Workflow children stay unnamed, as on the tool path: a pool name
|
|
359
|
+
// would make them the addressee of mail held for that name.
|
|
360
|
+
const assigned =
|
|
361
|
+
coordEnv("WORKFLOW_CHILD") === "1"
|
|
362
|
+
? undefined
|
|
363
|
+
: assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
|
|
355
364
|
// Write the adapter pid-map row so `harn agents whoami` ppid-walks find
|
|
356
365
|
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
357
366
|
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
@@ -416,7 +425,10 @@ function buildEventData(
|
|
|
416
425
|
// SessionStart. Mint durable display identity here so a later
|
|
417
426
|
// SessionStart remains idempotent and Codec is not stuck on the
|
|
418
427
|
// 8-character native id.
|
|
419
|
-
if (
|
|
428
|
+
if (
|
|
429
|
+
coordEnv("WORKFLOW_CHILD") !== "1" &&
|
|
430
|
+
!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
|
|
431
|
+
) {
|
|
420
432
|
assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
|
|
421
433
|
}
|
|
422
434
|
const prompt = p?.prompt ?? "";
|
|
@@ -663,6 +675,24 @@ async function main(): Promise<number> {
|
|
|
663
675
|
const { eventName, extra } = parseArgv(process.argv.slice(2));
|
|
664
676
|
const adapter = detectAdapter(process.argv.slice(2));
|
|
665
677
|
const behavior = adapter ? adapterBehavior(adapter) : null;
|
|
678
|
+
// Adapters that kill a hook at its timeout get a start marker, so a killed
|
|
679
|
+
// run leaves evidence for doctor (ADR 0197). Written before stdin is read.
|
|
680
|
+
const markerSpec = adapter ? ADAPTER_SPECS[adapter] : undefined;
|
|
681
|
+
const markerTimeout =
|
|
682
|
+
markerSpec && eventName && markerSpec.events.some((e) => e.subcommand === eventName)
|
|
683
|
+
? hookTimeoutFor(markerSpec, eventName)
|
|
684
|
+
: undefined;
|
|
685
|
+
if (markerTimeout !== undefined && adapter && eventName && coordEnv("AGENT_COORD_OFF") !== "1") {
|
|
686
|
+
const markerRoot = resolveCoordRoot(process.cwd());
|
|
687
|
+
if (markerRoot) {
|
|
688
|
+
hookRunMarkerPath = writeHookRunMarker(markerRoot, {
|
|
689
|
+
event: eventName,
|
|
690
|
+
adapter,
|
|
691
|
+
pid: process.pid,
|
|
692
|
+
timeout_sec: markerTimeout,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
666
696
|
const raw = await readStdin();
|
|
667
697
|
hookHealthState = beginHookHealth({
|
|
668
698
|
started_at_ms: hookStartedAt,
|
|
@@ -1189,9 +1219,12 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
|
1189
1219
|
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
|
|
1190
1220
|
"../storage/maintenance-providers.ts"
|
|
1191
1221
|
);
|
|
1192
|
-
// Effect: throttled
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1222
|
+
// Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
|
|
1223
|
+
// deletes expired workspaces, plus unheld workspaces over the per-bundle
|
|
1224
|
+
// ceiling or repository budget once they have been idle past
|
|
1225
|
+
// artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
|
|
1226
|
+
// in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
|
|
1227
|
+
// here: a failure logs and never blocks session start.
|
|
1195
1228
|
try {
|
|
1196
1229
|
autoCleanArtifacts(coordRoot);
|
|
1197
1230
|
} catch (err) {
|
|
@@ -1564,7 +1597,8 @@ async function handlePostToolUse(run: HookRun): Promise<void> {
|
|
|
1564
1597
|
// status is the wrong place to ask.
|
|
1565
1598
|
try {
|
|
1566
1599
|
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
1567
|
-
|
|
1600
|
+
// A workflow child has no user-visible title to set.
|
|
1601
|
+
const name = coordEnv("WORKFLOW_CHILD") === "1" ? undefined : sessionNameDisplayPending(row);
|
|
1568
1602
|
if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
|
|
1569
1603
|
// Record the title before asking for it. The suggestion can still
|
|
1570
1604
|
// change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
|
|
@@ -1765,6 +1799,7 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1765
1799
|
payload: ParsedPayload | null,
|
|
1766
1800
|
coordination: Heartbeat | null,
|
|
1767
1801
|
): Promise<boolean> {
|
|
1802
|
+
if (coordEnv("WORKFLOW_CHILD") === "1") return true;
|
|
1768
1803
|
const name = sessionNameDisplayPending(coordination);
|
|
1769
1804
|
if (!name) return true;
|
|
1770
1805
|
|
|
@@ -2168,6 +2203,10 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
2168
2203
|
recoveryBriefing = "",
|
|
2169
2204
|
hostPromptContext = "",
|
|
2170
2205
|
): Promise<boolean> {
|
|
2206
|
+
// Workflow children get no prompt-time coordination context, matching
|
|
2207
|
+
// SessionStart. The turn ritual is not enforced for them, the naming nudge
|
|
2208
|
+
// would register them as named peers, and rendering drains peer mail.
|
|
2209
|
+
if (coordEnv("WORKFLOW_CHILD") === "1") return false;
|
|
2171
2210
|
const behavior = adapterBehavior(adapter);
|
|
2172
2211
|
const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
|
|
2173
2212
|
let additionalContext = renderPromptContext({
|
|
@@ -2307,6 +2346,7 @@ function completeRecoveryInjection(
|
|
|
2307
2346
|
|
|
2308
2347
|
main()
|
|
2309
2348
|
.then(async (code) => {
|
|
2349
|
+
clearHookRunMarker(hookRunMarkerPath);
|
|
2310
2350
|
writeHookHealthCompletion(hookHealthState, {
|
|
2311
2351
|
finished_at_ms: performance.now(),
|
|
2312
2352
|
finished_rss_bytes: process.memoryUsage().rss,
|
|
@@ -2321,6 +2361,7 @@ main()
|
|
|
2321
2361
|
pid: process.pid,
|
|
2322
2362
|
phase: "top-level",
|
|
2323
2363
|
});
|
|
2364
|
+
clearHookRunMarker(hookRunMarkerPath);
|
|
2324
2365
|
writeHookHealthCompletion(hookHealthState, {
|
|
2325
2366
|
finished_at_ms: performance.now(),
|
|
2326
2367
|
finished_rss_bytes: process.memoryUsage().rss,
|