harnery 0.43.0 → 0.44.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 +3 -3
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +8 -12
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +7 -30
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +33 -5
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +7 -1
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +33 -5
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/zip/index.ts +483 -0
package/src/commands/browse.ts
CHANGED
|
@@ -134,6 +134,7 @@ import {
|
|
|
134
134
|
import {
|
|
135
135
|
harvestDomain,
|
|
136
136
|
harvestWindowsChromeCookies,
|
|
137
|
+
keepWindowsChromeSession,
|
|
137
138
|
stopWindowsChromeProfile,
|
|
138
139
|
windowsChromePath,
|
|
139
140
|
windowsLocalAppData,
|
|
@@ -3465,15 +3466,18 @@ function useWindowsPlainLogin(): boolean {
|
|
|
3465
3466
|
|
|
3466
3467
|
/**
|
|
3467
3468
|
* The WSL form of `--login`: the person signs in through the Windows
|
|
3468
|
-
* Chrome on a dedicated Windows profile with nothing attached.
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
3469
|
+
* Chrome on a dedicated Windows profile with nothing attached. Before the
|
|
3470
|
+
* window opens, the profile is set to continue where it left off, so Chrome
|
|
3471
|
+
* keeps browser-session cookies when the window closes. When the window
|
|
3472
|
+
* closes, the profile's cookies for the sign-in site are read and merged
|
|
3473
|
+
* into the shared cookie store, which later `browse` runs attach, and the
|
|
3474
|
+
* last page open on that site is reported so an agent can resume from it.
|
|
3471
3475
|
*/
|
|
3472
3476
|
async function runWindowsPlainLogin(
|
|
3473
3477
|
url: string,
|
|
3474
3478
|
storePath: string,
|
|
3475
3479
|
loginCloseFile: string | null,
|
|
3476
|
-
emit:
|
|
3480
|
+
emit: Pick<EmitContext, "log" | "data">,
|
|
3477
3481
|
): Promise<void> {
|
|
3478
3482
|
const chromeLinux = windowsChromePath();
|
|
3479
3483
|
const localAppData = windowsLocalAppData();
|
|
@@ -3484,6 +3488,13 @@ async function runWindowsPlainLogin(
|
|
|
3484
3488
|
}
|
|
3485
3489
|
const chromeWindows = wslPathToWindows(chromeLinux);
|
|
3486
3490
|
const profileWindows = windowsPlainProfile(localAppData);
|
|
3491
|
+
const keep = keepWindowsChromeSession({ chromeWindows, profileWindows });
|
|
3492
|
+
if (!keep.kept) {
|
|
3493
|
+
emit.log(
|
|
3494
|
+
"[--login] Could not set the sign-in profile to keep browser-session cookies. A sign-in that relies on them survives only if you end it from the terminal instead of closing the window.",
|
|
3495
|
+
"warn",
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3487
3498
|
const child = spawn(
|
|
3488
3499
|
chromeLinux,
|
|
3489
3500
|
[`--user-data-dir=${profileWindows}`, "--no-first-run", "--no-default-browser-check", url],
|
|
@@ -3507,24 +3518,49 @@ async function runWindowsPlainLogin(
|
|
|
3507
3518
|
);
|
|
3508
3519
|
await Promise.race(waits);
|
|
3509
3520
|
lifecycleAbort.abort();
|
|
3510
|
-
stopWindowsChromeProfile(profileWindows);
|
|
3521
|
+
const stop = stopWindowsChromeProfile(profileWindows);
|
|
3522
|
+
if (stop.forced > 0) {
|
|
3523
|
+
emit.log(
|
|
3524
|
+
"[--login] Chrome did not close when asked and was stopped. Cookies set in its last 30 seconds may be missing; run the sign-in again if later pages are signed out.",
|
|
3525
|
+
"warn",
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3511
3528
|
await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
|
|
3512
3529
|
const domain = harvestDomain(url);
|
|
3513
|
-
const cookies = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
|
|
3530
|
+
const { cookies, pages } = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
|
|
3531
|
+
const lastPage = pages[0] ?? null;
|
|
3532
|
+
const names = [...new Set(cookies.map((cookie) => cookie.name))].sort();
|
|
3533
|
+
const sessionCookies = cookies.filter((cookie) => cookie.expires <= 0).length;
|
|
3514
3534
|
if (cookies.length === 0) {
|
|
3515
3535
|
emit.log(
|
|
3516
3536
|
`[--login] No ${domain} cookies were found in the Windows profile; the sign-in may not have completed.`,
|
|
3517
3537
|
"warn",
|
|
3518
3538
|
);
|
|
3519
|
-
|
|
3539
|
+
} else {
|
|
3540
|
+
const jar = new CookieJar({ path: storePath, source: "harn-browse" });
|
|
3541
|
+
jar.save(mergeCookies(jar.load(), cookies));
|
|
3542
|
+
emit.log(
|
|
3543
|
+
`[--login] Copied ${cookies.length} ${domain} cookies (${sessionCookies} browser-session) into ${storePath}: ${names.join(", ")}.`,
|
|
3544
|
+
"info",
|
|
3545
|
+
);
|
|
3520
3546
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3547
|
+
if (lastPage) {
|
|
3548
|
+
emit.log(
|
|
3549
|
+
`[--login] Last page: ${lastPage} (the last page you had open on ${domain}; open it to continue where the sign-in left off, since its address may carry a sign-in token that cookies alone do not replace).`,
|
|
3550
|
+
"info",
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
emit.data({
|
|
3554
|
+
login: {
|
|
3555
|
+
domain,
|
|
3556
|
+
store: storePath,
|
|
3557
|
+
cookies: cookies.length,
|
|
3558
|
+
sessionCookies,
|
|
3559
|
+
names,
|
|
3560
|
+
lastPage,
|
|
3561
|
+
pages,
|
|
3562
|
+
},
|
|
3563
|
+
});
|
|
3528
3564
|
}
|
|
3529
3565
|
|
|
3530
3566
|
async function waitForLoginCloseFile(path: string, signal?: AbortSignal): Promise<void> {
|
|
@@ -50,6 +50,7 @@ export function registerDiagnosticsCommand(
|
|
|
50
50
|
findings,
|
|
51
51
|
bundles,
|
|
52
52
|
capability,
|
|
53
|
+
supervisor: readSupervisorStatus(repoRoot),
|
|
53
54
|
total_findings: findings.length,
|
|
54
55
|
total_bundles: bundles.length,
|
|
55
56
|
});
|
|
@@ -68,6 +69,7 @@ export function registerDiagnosticsCommand(
|
|
|
68
69
|
schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
|
|
69
70
|
kind: "diagnostic_finding",
|
|
70
71
|
finding,
|
|
72
|
+
supervisor: readSupervisorStatus(repoRoot),
|
|
71
73
|
timeline:
|
|
72
74
|
readSupervisorTimeline(repoRoot, finding.id) ?? buildSupervisorTimeline(finding),
|
|
73
75
|
explanation:
|
|
@@ -118,6 +120,7 @@ export function registerDiagnosticsCommand(
|
|
|
118
120
|
schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
|
|
119
121
|
kind: "diagnostic_advice",
|
|
120
122
|
source: { mode: "live" },
|
|
123
|
+
supervisor: readSupervisorStatus(repoRoot),
|
|
121
124
|
advice: buildDiagnosticAdvice({
|
|
122
125
|
assessment: published.assessment,
|
|
123
126
|
priorHysteresis: published.prior_hysteresis,
|
|
@@ -204,6 +207,13 @@ function liveFindingsCapability(repoRoot: string, reportPresent: boolean): Super
|
|
|
204
207
|
}
|
|
205
208
|
const status = readSupervisorStatus(repoRoot);
|
|
206
209
|
if (status.running) return { source_kind: "supervisor.findings", state: "supported" };
|
|
210
|
+
if (status.liveness === "unknown")
|
|
211
|
+
return {
|
|
212
|
+
source_kind: "supervisor.findings",
|
|
213
|
+
state: "error",
|
|
214
|
+
reason_code: "supervisor_generation_unknown",
|
|
215
|
+
detail: status.build_status,
|
|
216
|
+
};
|
|
207
217
|
return {
|
|
208
218
|
source_kind: "supervisor.findings",
|
|
209
219
|
state: "expired",
|
package/src/commands/doctor.ts
CHANGED
|
@@ -421,15 +421,34 @@ function checkWorkflowAdapter(adapter: AdapterName): { check: Check; installed:
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
function checkNode(): Check {
|
|
424
|
-
|
|
424
|
+
return nodeCheck(whichVersion("node"), process.versions);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Report the `node` on PATH, which is what Node-based tools run. Under Bun,
|
|
429
|
+
* `process.versions.node` is the Node version Bun emulates, not an installed
|
|
430
|
+
* Node, so it is used only when doctor itself runs on Node and PATH has none.
|
|
431
|
+
*/
|
|
432
|
+
export function nodeCheck(
|
|
433
|
+
onPath: { ok: boolean; out: string },
|
|
434
|
+
runtime: { node: string; bun?: string },
|
|
435
|
+
): Check {
|
|
436
|
+
const hint = "https://nodejs.org/en/download";
|
|
437
|
+
let v = onPath.ok ? onPath.out.replace(/^v/, "") : "";
|
|
438
|
+
if (!v) {
|
|
439
|
+
if (runtime.bun) {
|
|
440
|
+
return {
|
|
441
|
+
name: "node",
|
|
442
|
+
severity: "warn",
|
|
443
|
+
detail: "not on PATH (Bun runs harnery; Node-based tools need it)",
|
|
444
|
+
hint,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
v = runtime.node;
|
|
448
|
+
}
|
|
425
449
|
const major = Number.parseInt(v.split(".")[0], 10);
|
|
426
450
|
if (Number.isNaN(major) || major < 20) {
|
|
427
|
-
return {
|
|
428
|
-
name: "node",
|
|
429
|
-
severity: "fail",
|
|
430
|
-
detail: `${v} (need ≥ 20)`,
|
|
431
|
-
hint: "https://nodejs.org/en/download",
|
|
432
|
-
};
|
|
451
|
+
return { name: "node", severity: "fail", detail: `${v} (need ≥ 20)`, hint };
|
|
433
452
|
}
|
|
434
453
|
return { name: "node", severity: "ok", detail: v };
|
|
435
454
|
}
|
package/src/commands/governor.ts
CHANGED
|
@@ -385,12 +385,13 @@ function registerServiceCommand(
|
|
|
385
385
|
.action(async (opts: { json?: boolean }) => {
|
|
386
386
|
await withGovernorRootAsync(emit, async (coordRoot) => {
|
|
387
387
|
let status = requestGovernorServiceStop(coordRoot);
|
|
388
|
+
const signal = { signal_sent: status.signal_sent, signal_verdict: status.signal_verdict };
|
|
388
389
|
const deadline = Date.now() + 5_000;
|
|
389
390
|
while (status.running && Date.now() < deadline) {
|
|
390
391
|
await delay(50);
|
|
391
392
|
status = readGovernorServiceStatus(coordRoot);
|
|
392
393
|
}
|
|
393
|
-
emitServiceStatus(status, opts.json, emit);
|
|
394
|
+
emitServiceStatus({ ...status, ...signal }, opts.json, emit);
|
|
394
395
|
});
|
|
395
396
|
});
|
|
396
397
|
|
|
@@ -534,19 +535,31 @@ function emitServiceStatus(
|
|
|
534
535
|
emit.data(status);
|
|
535
536
|
return;
|
|
536
537
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
538
|
+
const state =
|
|
539
|
+
status.liveness === "unknown"
|
|
540
|
+
? "unknown"
|
|
541
|
+
: !status.config && !status.record
|
|
542
|
+
? "unconfigured"
|
|
543
|
+
: status.running
|
|
544
|
+
? (status.record?.state ?? "running")
|
|
545
|
+
: status.stale
|
|
546
|
+
? "stale"
|
|
547
|
+
: "stopped";
|
|
546
548
|
const lines = [
|
|
547
549
|
`governor service: ${state}`,
|
|
550
|
+
`generation: ${status.generation}`,
|
|
551
|
+
`liveness: ${status.liveness}`,
|
|
552
|
+
`running build: ${status.running_build ? `${status.running_build.version} (${status.running_build.revision})` : "unknown"}`,
|
|
553
|
+
`installed build: ${status.installed_build.version} (${status.installed_build.revision})`,
|
|
554
|
+
`build: ${status.build_status}`,
|
|
548
555
|
`goals: ${status.config?.goal_ids.join(", ") || "none"}`,
|
|
549
556
|
];
|
|
557
|
+
if (status.signal_sent !== undefined) {
|
|
558
|
+
lines.push(`signal sent: ${status.signal_sent ? "yes" : "no"} (${status.signal_verdict})`);
|
|
559
|
+
if (!status.signal_sent && status.record?.state !== "stopped") {
|
|
560
|
+
lines.push("The durable stop request remains for the service to read.");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
550
563
|
if (status.record) {
|
|
551
564
|
lines.push(`pid: ${status.record.pid} on ${status.record.host}`);
|
|
552
565
|
lines.push(`heartbeat: ${status.record.heartbeat_at}`);
|
package/src/commands/inbox.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext } from "../commander.ts";
|
|
3
3
|
import type { HarneryInboxService } from "../core/inbox/service.ts";
|
|
4
|
+
import { watchInbox } from "../core/inbox/watcher.ts";
|
|
4
5
|
|
|
5
6
|
export function registerInboxCommand(
|
|
6
7
|
program: Command,
|
|
7
8
|
emit: EmitContext,
|
|
8
9
|
service?: HarneryInboxService,
|
|
10
|
+
defaultRecipient?: () => string,
|
|
9
11
|
): void {
|
|
10
12
|
const inbox = program.command("inbox").description("Inspect private coordination inbox state");
|
|
11
13
|
inbox
|
|
12
|
-
.command("list
|
|
14
|
+
.command("list [recipient]")
|
|
13
15
|
.option("--json", "Emit stable JSON")
|
|
14
|
-
.action((recipient: string, options: { json?: boolean }) => {
|
|
16
|
+
.action((recipient: string | undefined, options: { json?: boolean }) => {
|
|
15
17
|
run(emit, () => {
|
|
18
|
+
recipient = requireRecipient(recipient, defaultRecipient);
|
|
16
19
|
const rows = requireInboxService(service)
|
|
17
20
|
.pending(recipient)
|
|
18
21
|
.map((record) => ({
|
|
@@ -22,6 +25,7 @@ export function registerInboxCommand(
|
|
|
22
25
|
created_at: record.created_at,
|
|
23
26
|
body_bytes: record.body_bytes,
|
|
24
27
|
body: record.body,
|
|
28
|
+
state: "pending",
|
|
25
29
|
}));
|
|
26
30
|
if (options.json) {
|
|
27
31
|
emit.config({ format: "json" });
|
|
@@ -41,6 +45,52 @@ export function registerInboxCommand(
|
|
|
41
45
|
} else emit.rows([status as unknown as Record<string, unknown>]);
|
|
42
46
|
});
|
|
43
47
|
});
|
|
48
|
+
inbox
|
|
49
|
+
.command("watch [recipient]")
|
|
50
|
+
.description("Surface pending messages for this instance; poll history until timeout")
|
|
51
|
+
.option("--timeout <seconds>", "Stop after this many seconds", "60")
|
|
52
|
+
.option("--json", "Emit one JSON record per surfaced message")
|
|
53
|
+
.action(async (recipient: string | undefined, options: { json?: boolean; timeout: string }) => {
|
|
54
|
+
let controller: AbortController | undefined;
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
try {
|
|
57
|
+
const instanceId = requireRecipient(recipient, defaultRecipient);
|
|
58
|
+
const inboxService = requireInboxService(service);
|
|
59
|
+
const seconds = Number(options.timeout);
|
|
60
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 3600)
|
|
61
|
+
throw new Error("--timeout must be greater than 0 and at most 3600 seconds");
|
|
62
|
+
controller = new AbortController();
|
|
63
|
+
timer = setTimeout(() => controller!.abort(), seconds * 1000);
|
|
64
|
+
if (options.json) emit.config({ format: "json" });
|
|
65
|
+
for await (const message of watchInbox(inboxService, instanceId, {
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
})) {
|
|
68
|
+
const row = { ...message, state: "surfaced" };
|
|
69
|
+
if (options.json) emit.data(row);
|
|
70
|
+
else emit.rows([row]);
|
|
71
|
+
// An output error leaves this ID pending for prompt recovery.
|
|
72
|
+
await new Promise<void>((resolve, reject) =>
|
|
73
|
+
process.stdout.write("", (error) => (error ? reject(error) : resolve())),
|
|
74
|
+
);
|
|
75
|
+
inboxService.acknowledgeSurface(instanceId, [message.message_id]);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
emit.error({
|
|
79
|
+
code: "inbox_failed",
|
|
80
|
+
message: error instanceof Error ? error.message : String(error),
|
|
81
|
+
});
|
|
82
|
+
emit.setExitCode(1);
|
|
83
|
+
} finally {
|
|
84
|
+
if (timer) clearTimeout(timer);
|
|
85
|
+
controller?.abort();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireRecipient(recipient: string | undefined, fallback?: () => string): string {
|
|
91
|
+
const resolved = recipient ?? fallback?.();
|
|
92
|
+
if (!resolved) throw new Error("recipient instance is required outside an agent session");
|
|
93
|
+
return resolved;
|
|
44
94
|
}
|
|
45
95
|
|
|
46
96
|
function requireInboxService(service?: HarneryInboxService): HarneryInboxService {
|
package/src/commands/rm.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import type { Command } from "commander";
|
|
5
5
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
6
6
|
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
|
|
7
|
+
import { artifactsRoot, parseArtifactManifest } from "../core/artifacts/index.ts";
|
|
7
8
|
import {
|
|
8
9
|
readCoordinationViewV3,
|
|
9
10
|
requireAuthoritySafeCoordinationViewV3,
|
|
@@ -25,6 +26,15 @@ export function registerRmCommand(
|
|
|
25
26
|
"Allowed directory; each target must be strictly inside it",
|
|
26
27
|
)
|
|
27
28
|
.option("-r, --recursive", "Permit directory removal after inspecting its complete contents")
|
|
29
|
+
.option(
|
|
30
|
+
"--clean-clone",
|
|
31
|
+
"Remove only clean standalone clones inside one owned artifact; refuse local-only commits and worktrees",
|
|
32
|
+
)
|
|
33
|
+
.option(
|
|
34
|
+
"--entry-limit <n>",
|
|
35
|
+
"Maximum inspected entries, 1-100000 (default 10000); larger limits require one current caller-owned managed artifact as --root",
|
|
36
|
+
(value: string) => Number(value),
|
|
37
|
+
)
|
|
28
38
|
.option(
|
|
29
39
|
"--allow-protected <path>",
|
|
30
40
|
"Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; repeat per path",
|
|
@@ -65,7 +75,34 @@ export function registerRmCommand(
|
|
|
65
75
|
const liveRoots = [coordRoot, gitTopLevel(process.cwd())].filter((dir): dir is string =>
|
|
66
76
|
Boolean(dir),
|
|
67
77
|
);
|
|
68
|
-
const
|
|
78
|
+
const assertOwnedArtifact = (root: string): void => {
|
|
79
|
+
if (!coordRoot || dirname(root) !== realpathSync(artifactsRoot(coordRoot)))
|
|
80
|
+
throw new Error("Copied state requires --root to name one owned managed artifact");
|
|
81
|
+
const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
|
|
82
|
+
const owner = resolveOwner({ payload: null, coordRoot });
|
|
83
|
+
const ownId = owner ? liveInstanceIdV3(owner.instance_id) : undefined;
|
|
84
|
+
if (!ownId || !view.instances[ownId]?.authority_eligible)
|
|
85
|
+
throw new Error("Copied state requires an authenticated current artifact owner");
|
|
86
|
+
const record = resolve(root, ".harnery-artifact.json");
|
|
87
|
+
const stat = lstatSync(record);
|
|
88
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
89
|
+
throw new Error("An owned artifact requires a regular manifest file");
|
|
90
|
+
const parsed = parseArtifactManifest(JSON.parse(readFileSync(record, "utf8")));
|
|
91
|
+
if (
|
|
92
|
+
!parsed.ok ||
|
|
93
|
+
parsed.manifest.released_at ||
|
|
94
|
+
!parsed.manifest.created_by?.instance_id ||
|
|
95
|
+
liveInstanceIdV3(parsed.manifest.created_by.instance_id) !== ownId
|
|
96
|
+
)
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Copied state requires a valid unreleased artifact owned by the caller",
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
const report = guardedRemove(
|
|
102
|
+
paths,
|
|
103
|
+
{ ...options, liveRoots, assertOwnedArtifact },
|
|
104
|
+
checkClaims,
|
|
105
|
+
);
|
|
69
106
|
if (options.json) {
|
|
70
107
|
emit.config({ format: "json" });
|
|
71
108
|
emit.data(report);
|
|
@@ -76,7 +113,9 @@ export function registerRmCommand(
|
|
|
76
113
|
];
|
|
77
114
|
if (report.protected.length)
|
|
78
115
|
lines.push(
|
|
79
|
-
|
|
116
|
+
options.cleanClone
|
|
117
|
+
? "Verified clone metadata:"
|
|
118
|
+
: "Protected paths named with --allow-protected:",
|
|
80
119
|
...report.protected.map((path) => ` ${path}`),
|
|
81
120
|
);
|
|
82
121
|
if (report.links.length)
|
package/src/commands/servers.ts
CHANGED
|
@@ -120,6 +120,7 @@ export function registerServersCommand(
|
|
|
120
120
|
type: "unregistered",
|
|
121
121
|
label: stray.command,
|
|
122
122
|
pid: stray.pid,
|
|
123
|
+
...(stray.start_token ? { start_token: stray.start_token } : {}),
|
|
123
124
|
host: hostname(),
|
|
124
125
|
started_at: new Date().toISOString(),
|
|
125
126
|
registered_at: new Date().toISOString(),
|
|
@@ -207,10 +208,20 @@ export function registerServersCommand(
|
|
|
207
208
|
emit.config({ format: "json" });
|
|
208
209
|
emit.data({ applied: Boolean(options.yes), pruned: report.pruned, decisions, results });
|
|
209
210
|
} else {
|
|
210
|
-
const lines = decisions.map(
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
const lines = decisions.map((decision) => {
|
|
212
|
+
const result = results.find((item) => item.id === decision.id);
|
|
213
|
+
const action =
|
|
214
|
+
decision.action === "keep"
|
|
215
|
+
? "keep"
|
|
216
|
+
: !options.yes
|
|
217
|
+
? "would stop"
|
|
218
|
+
: result?.stopped
|
|
219
|
+
? "stopped"
|
|
220
|
+
: result?.method === "not-running"
|
|
221
|
+
? "already stopped"
|
|
222
|
+
: "could not stop";
|
|
223
|
+
return `${action} ${decision.id} ${result?.detail ?? decision.reason}`;
|
|
224
|
+
});
|
|
214
225
|
if (report.pruned.length)
|
|
215
226
|
lines.push(`Removed records of exited servers: ${report.pruned.join(", ")}`);
|
|
216
227
|
if (!decisions.length) lines.push("No registered servers.");
|
|
@@ -218,6 +229,7 @@ export function registerServersCommand(
|
|
|
218
229
|
lines.push("Preview only. Re-run with --yes to stop.");
|
|
219
230
|
emit.text(lines.join("\n"));
|
|
220
231
|
}
|
|
232
|
+
if (results.some((result) => result.method === "refused")) emit.setExitCode(1);
|
|
221
233
|
} catch (error) {
|
|
222
234
|
fail(error);
|
|
223
235
|
}
|
|
@@ -83,11 +83,16 @@ export function registerSupervisorCommand(
|
|
|
83
83
|
.action(async (options: { root?: string }) => {
|
|
84
84
|
try {
|
|
85
85
|
const root = resolve(options.root ?? coordRoot(context));
|
|
86
|
-
|
|
86
|
+
const requested = requestSupervisorStop(root);
|
|
87
|
+
let status = requested;
|
|
87
88
|
const deadline = Date.now() + 5_000;
|
|
88
89
|
while (status.running && Date.now() < deadline) {
|
|
89
90
|
await new Promise((done) => setTimeout(done, 50));
|
|
90
|
-
status =
|
|
91
|
+
status = {
|
|
92
|
+
...readSupervisorStatus(root),
|
|
93
|
+
signal_sent: requested.signal_sent,
|
|
94
|
+
signal_verdict: requested.signal_verdict,
|
|
95
|
+
};
|
|
91
96
|
}
|
|
92
97
|
emit.data(status);
|
|
93
98
|
} catch (error) {
|
package/src/commands/toc.ts
CHANGED
|
@@ -172,17 +172,25 @@ function renderToc(r: TocResult): string {
|
|
|
172
172
|
return lines.join("\n");
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
async function runSection(
|
|
175
|
+
export async function runSection(
|
|
176
|
+
file: string,
|
|
177
|
+
query: string,
|
|
178
|
+
opts: SectionOpts,
|
|
179
|
+
): Promise<SectionResult> {
|
|
176
180
|
if (!query.trim()) throw new Error("header query required");
|
|
177
181
|
const tocResult = await runToc(file, {});
|
|
178
182
|
const absPath = resolve(file);
|
|
179
183
|
const lines = readFileSync(absPath, "utf8").split("\n");
|
|
180
184
|
|
|
181
|
-
const
|
|
182
|
-
opts.caseSensitive
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
const same = (a: string, b: string) =>
|
|
186
|
+
opts.caseSensitive ? a === b : a.toLowerCase() === b.toLowerCase();
|
|
187
|
+
const contains = (a: string, b: string) =>
|
|
188
|
+
opts.caseSensitive ? a.includes(b) : a.toLowerCase().includes(b.toLowerCase());
|
|
189
|
+
// A header that equals the query wins over headers that merely contain it,
|
|
190
|
+
// so "Media" can still select "## Media" beside "## Media recipes".
|
|
191
|
+
const exact = tocResult.headers.filter((h) => same(h.text, query.trim()));
|
|
192
|
+
const matches =
|
|
193
|
+
exact.length > 0 ? exact : tocResult.headers.filter((h) => contains(h.text, query));
|
|
186
194
|
if (matches.length === 0) {
|
|
187
195
|
throw new Error(
|
|
188
196
|
`no header matching "${query}". Run \`${resolveBinName()} toc ${file}\` to list available headers.`,
|
package/src/commands/workflow.ts
CHANGED
|
@@ -618,11 +618,11 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
618
618
|
workflow
|
|
619
619
|
.command("reclaim <run-id>")
|
|
620
620
|
.description(
|
|
621
|
-
"
|
|
621
|
+
"Preview reclaim of an inactive owned workspace, or confirm salvage or discard and release.",
|
|
622
622
|
)
|
|
623
623
|
.option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
|
|
624
624
|
.option("--discard", "Throw the uncommitted work away instead of salvaging it")
|
|
625
|
-
.option("--json", "Emit the reclaim
|
|
625
|
+
.option("--json", "Emit the reclaim preview or confirmed result as JSON")
|
|
626
626
|
.action(async (runId: string, opts: WorkflowReclaimOpts) => {
|
|
627
627
|
const coordRoot = resolveCoordRoot();
|
|
628
628
|
if (!coordRoot) {
|
|
@@ -632,53 +632,53 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
632
632
|
});
|
|
633
633
|
process.exit(1);
|
|
634
634
|
}
|
|
635
|
-
if (!opts.yes) {
|
|
636
|
-
emit.error({
|
|
637
|
-
code: "reclaim_confirmation_required",
|
|
638
|
-
message: opts.discard
|
|
639
|
-
? "reclaim --discard permanently destroys uncommitted work; pass --yes to confirm"
|
|
640
|
-
: "workspace reclaim commits uncommitted work to its branch and then releases the worktree; pass --yes to confirm",
|
|
641
|
-
});
|
|
642
|
-
process.exit(1);
|
|
643
|
-
}
|
|
644
635
|
try {
|
|
645
|
-
const {
|
|
636
|
+
const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
|
|
646
637
|
"../core/workflow/index.ts"
|
|
647
638
|
);
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
emit.error({ code: "workflow_reclaim_failed", message: inspection.error });
|
|
651
|
-
process.exit(1);
|
|
652
|
-
}
|
|
653
|
-
const allocation = inspection.value.allocation;
|
|
654
|
-
if (!allocation) {
|
|
639
|
+
const binding = readWorkflowRunManifest(coordRoot, runId).execution.workspace_binding;
|
|
640
|
+
if (binding?.provider.id !== "local-git-worktree") {
|
|
655
641
|
emit.error({
|
|
656
642
|
code: "workflow_reclaim_unavailable",
|
|
657
643
|
message: `run ${runId} has no provider-owned Git workspace to reclaim`,
|
|
658
644
|
});
|
|
659
645
|
process.exit(1);
|
|
660
646
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
647
|
+
const provider = createLocalGitWorktreeProvider({ coordRoot });
|
|
648
|
+
const result = await provider.reclaim(binding, {
|
|
663
649
|
mode: opts.discard ? "discard" : "salvage",
|
|
664
|
-
|
|
650
|
+
yes: opts.yes,
|
|
665
651
|
});
|
|
666
|
-
// Cleanup still owns removal. Reclaim only changes whether the tree it
|
|
667
|
-
// finds is dirty, so there is one audited release path, not two.
|
|
668
|
-
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
669
|
-
const cleanup = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
670
652
|
if (opts.json) {
|
|
671
653
|
emit.config({ format: "json" });
|
|
672
|
-
emit.data(
|
|
654
|
+
emit.data(result);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (result.action === "preview") {
|
|
658
|
+
const holders = result.lease_holder.map(
|
|
659
|
+
(holder) =>
|
|
660
|
+
`${holder.scope}: ${holder.owner_id ?? "unknown"}${holder.pid ? ` (pid ${holder.pid})` : ""}${holder.detail ? `; ${holder.detail}` : ""}`,
|
|
661
|
+
);
|
|
662
|
+
emit.text(
|
|
663
|
+
`reclaim preview: ${result.run_id}\n` +
|
|
664
|
+
`mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
|
|
665
|
+
`lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
|
|
666
|
+
`dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
|
|
667
|
+
(result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
|
|
668
|
+
`Pass --yes to confirm ${result.mode} and release after all checks pass.\n`,
|
|
669
|
+
);
|
|
673
670
|
return;
|
|
674
671
|
}
|
|
675
672
|
emit.text(
|
|
676
|
-
`reclaim ${preparation.action}: ${preparation.detail}\n` +
|
|
677
|
-
`workspace cleanup ${cleanup.status}: ${cleanup.binding_id}\n`,
|
|
673
|
+
`reclaim ${result.preparation.action}: ${result.preparation.detail}\n` +
|
|
674
|
+
`workspace cleanup ${result.cleanup.status}: ${result.cleanup.binding_id}\n`,
|
|
678
675
|
);
|
|
679
676
|
} catch (error) {
|
|
680
677
|
emit.error({
|
|
681
|
-
code:
|
|
678
|
+
code:
|
|
679
|
+
error instanceof Error && "code" in error && typeof error.code === "string"
|
|
680
|
+
? error.code
|
|
681
|
+
: "workflow_reclaim_failed",
|
|
682
682
|
message: error instanceof Error ? error.message : String(error),
|
|
683
683
|
});
|
|
684
684
|
process.exit(1);
|
|
@@ -88,6 +88,7 @@ interface LiveAuthorityBaseV3 {
|
|
|
88
88
|
nativeSessionId: string;
|
|
89
89
|
adapter: Adapter;
|
|
90
90
|
observationId?: string;
|
|
91
|
+
expectedGenerationId?: `gen_${string}`;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
@@ -648,6 +649,13 @@ function recordLiveAuthority<S extends CoordinationAuthoritySignalV3>(
|
|
|
648
649
|
platform: livePlatformV3(),
|
|
649
650
|
expected_prior_state_digest: coordinationAuthorityStateDigestV3(expected),
|
|
650
651
|
desired_state_digest: coordinationAuthorityStateDigestV3(desired),
|
|
652
|
+
expected_generation_id:
|
|
653
|
+
input.expectedGenerationId ??
|
|
654
|
+
((subject === input.owner
|
|
655
|
+
? expected.v3_generation_id
|
|
656
|
+
: readLiveCoordinationRow(input.coordRoot, input.owner)?.v3_generation_id) as
|
|
657
|
+
| `gen_${string}`
|
|
658
|
+
| undefined),
|
|
651
659
|
reconciler: {
|
|
652
660
|
readStateDigest: () => {
|
|
653
661
|
const heartbeat = readHeartbeat(input.coordRoot, subject);
|
|
@@ -681,6 +689,12 @@ export function liveCoordinationWriteModeV3(coordRoot: string): EventV3WriteMode
|
|
|
681
689
|
}
|
|
682
690
|
|
|
683
691
|
function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat {
|
|
692
|
+
if (
|
|
693
|
+
input.expectedGenerationId &&
|
|
694
|
+
readLiveCoordinationRow(input.coordRoot, owner)?.v3_generation_id !== input.expectedGenerationId
|
|
695
|
+
) {
|
|
696
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
697
|
+
}
|
|
684
698
|
const heartbeat = ensureLiveCoordinationHeartbeat(
|
|
685
699
|
input.coordRoot,
|
|
686
700
|
owner,
|
|
@@ -688,6 +702,9 @@ function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat
|
|
|
688
702
|
input.adapter,
|
|
689
703
|
);
|
|
690
704
|
if (!heartbeat) throw new LiveCoordinationAuthorityV3Error(`heartbeat_missing:${owner}`);
|
|
705
|
+
if (input.expectedGenerationId && heartbeat.v3_generation_id !== input.expectedGenerationId) {
|
|
706
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
707
|
+
}
|
|
691
708
|
return heartbeat;
|
|
692
709
|
}
|
|
693
710
|
|