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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { lstatSync, readdirSync, readlinkSync, realpathSync, rmSync, type Stats } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, isAbsolute, parse, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
5
5
|
|
|
6
6
|
export interface RemoveOptions {
|
|
7
7
|
root: string;
|
|
@@ -16,6 +16,12 @@ export interface RemoveOptions {
|
|
|
16
16
|
allowProtected?: string[];
|
|
17
17
|
/** Directories whose own Git metadata and Harnery state may never be allowed (the live workspace). */
|
|
18
18
|
liveRoots?: string[];
|
|
19
|
+
/** Command-layer validation of the current caller's ownership of this exact managed artifact root. */
|
|
20
|
+
assertOwnedArtifact?: (root: string) => void;
|
|
21
|
+
/** Inspection bound; values above 10,000 require verified ownership of the exact artifact root. */
|
|
22
|
+
entryLimit?: number;
|
|
23
|
+
/** Remove only verified-clean standalone clones inside the caller's owned artifact. */
|
|
24
|
+
cleanClone?: boolean;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface RemovalLink {
|
|
@@ -30,6 +36,7 @@ export interface RemovalReport {
|
|
|
30
36
|
root: string;
|
|
31
37
|
targets: string[];
|
|
32
38
|
entries: number;
|
|
39
|
+
entry_limit: number;
|
|
33
40
|
bytes: number;
|
|
34
41
|
/** Protected paths removed because the caller named them with allowProtected. */
|
|
35
42
|
protected: string[];
|
|
@@ -39,6 +46,7 @@ export interface RemovalReport {
|
|
|
39
46
|
|
|
40
47
|
const protectedNames = new Set([".git", ".harnery", ".credentials", ".ssh", ".gnupg"]);
|
|
41
48
|
const MAX_ENTRIES = 10_000;
|
|
49
|
+
const MAX_OWNED_ARTIFACT_ENTRIES = 100_000;
|
|
42
50
|
|
|
43
51
|
/** Permanent removal is deliberately limited to inspected, untracked local outputs. */
|
|
44
52
|
export function guardedRemove(
|
|
@@ -48,18 +56,48 @@ export function guardedRemove(
|
|
|
48
56
|
): RemovalReport {
|
|
49
57
|
const cwd = realpathSync(process.cwd());
|
|
50
58
|
const root = checkedPath(options.root, new Set(), true);
|
|
51
|
-
|
|
59
|
+
if (options.cleanClone) {
|
|
60
|
+
if (!options.recursive) throw new Error("Clean clone removal requires --recursive");
|
|
61
|
+
if (options.allowProtected?.length)
|
|
62
|
+
throw new Error("Clean clone removal cannot be combined with --allow-protected");
|
|
63
|
+
if (!options.assertOwnedArtifact)
|
|
64
|
+
throw new Error("Clean clone removal requires one current caller-owned managed artifact");
|
|
65
|
+
options.assertOwnedArtifact(root);
|
|
66
|
+
}
|
|
67
|
+
const entryLimit = options.entryLimit ?? MAX_ENTRIES;
|
|
68
|
+
if (
|
|
69
|
+
!Number.isSafeInteger(entryLimit) ||
|
|
70
|
+
entryLimit < 1 ||
|
|
71
|
+
entryLimit > MAX_OWNED_ARTIFACT_ENTRIES
|
|
72
|
+
)
|
|
73
|
+
throw new Error(`The entry limit must be an integer from 1 to ${MAX_OWNED_ARTIFACT_ENTRIES}`);
|
|
74
|
+
const assertEntryLimitAuthority = () => {
|
|
75
|
+
if (entryLimit <= MAX_ENTRIES) return;
|
|
76
|
+
if (!options.assertOwnedArtifact)
|
|
77
|
+
throw new Error(
|
|
78
|
+
"An entry limit above 10000 requires one current caller-owned managed artifact",
|
|
79
|
+
);
|
|
80
|
+
options.assertOwnedArtifact(root);
|
|
81
|
+
};
|
|
82
|
+
const allowed = allowedProtected(
|
|
83
|
+
options.allowProtected ?? [],
|
|
84
|
+
root,
|
|
85
|
+
options.liveRoots ?? [],
|
|
86
|
+
options.assertOwnedArtifact,
|
|
87
|
+
);
|
|
52
88
|
const rootStat = lstatSync(root);
|
|
53
89
|
const rootFingerprint = fingerprint(rootStat);
|
|
54
90
|
if (!rootStat.isDirectory()) throw new Error("--root must be a directory");
|
|
55
91
|
if (root === parse(root).root || root === realpathSync(homedir()))
|
|
56
92
|
throw new Error("--root cannot be a filesystem root or home directory");
|
|
93
|
+
assertEntryLimitAuthority();
|
|
57
94
|
if (paths.length === 0 || paths.length > 100) throw new Error("Supply 1-100 explicit targets");
|
|
58
95
|
const targets = paths.map((path) => checkedPath(path, allowed));
|
|
59
96
|
const snapshots = new Map<string, string>();
|
|
60
97
|
const removedProtected = new Set<string>();
|
|
61
98
|
const links: RemovalLink[] = [];
|
|
62
99
|
const gitDirs = new Set<string>();
|
|
100
|
+
const cleanClones = new Set<string>();
|
|
63
101
|
let bytes = 0;
|
|
64
102
|
const devices = new Set<number>();
|
|
65
103
|
|
|
@@ -110,6 +148,10 @@ export function guardedRemove(
|
|
|
110
148
|
if (stat.isDirectory() && !options.recursive)
|
|
111
149
|
throw new Error(`Directory removal requires --recursive: ${target}`);
|
|
112
150
|
const device = stat.dev;
|
|
151
|
+
if (options.cleanClone) {
|
|
152
|
+
assertCleanClone(target);
|
|
153
|
+
cleanClones.add(target);
|
|
154
|
+
}
|
|
113
155
|
devices.add(device);
|
|
114
156
|
const pending = [target];
|
|
115
157
|
while (pending.length) {
|
|
@@ -118,6 +160,12 @@ export function guardedRemove(
|
|
|
118
160
|
if (basename(path) === ARTIFACT_RECORD && insideArtifactWorkspace(path, null))
|
|
119
161
|
throw new Error(`An artifact's record is managed by the artifact commands: ${path}`);
|
|
120
162
|
if (protectedNames.has(basename(path).toLowerCase())) {
|
|
163
|
+
if (options.cleanClone && basename(path) === ".git") {
|
|
164
|
+
const clone = dirname(path);
|
|
165
|
+
assertCleanClone(clone);
|
|
166
|
+
cleanClones.add(clone);
|
|
167
|
+
allowed.add(path);
|
|
168
|
+
}
|
|
121
169
|
if (!allowed.has(path)) throw new Error(protectedMessage(path));
|
|
122
170
|
removedProtected.add(path);
|
|
123
171
|
}
|
|
@@ -126,9 +174,9 @@ export function guardedRemove(
|
|
|
126
174
|
// read, followed, or counted. (The target itself and its ancestors
|
|
127
175
|
// were already refused if they were links, in checkedPath.)
|
|
128
176
|
if (path === target) throw new Error(`Path contains a symlink: ${path}`);
|
|
129
|
-
if (snapshots.size >=
|
|
177
|
+
if (snapshots.size >= entryLimit)
|
|
130
178
|
throw new Error(
|
|
131
|
-
`Removal exceeds the ${
|
|
179
|
+
`Removal exceeds the ${entryLimit}-entry limit; use the owning cleanup command`,
|
|
132
180
|
);
|
|
133
181
|
snapshots.set(path, fingerprint(info));
|
|
134
182
|
links.push({ path, target: readlinkSync(path) });
|
|
@@ -136,9 +184,9 @@ export function guardedRemove(
|
|
|
136
184
|
}
|
|
137
185
|
if (info.dev !== device || (!info.isDirectory() && !info.isFile()))
|
|
138
186
|
throw new Error(`Mount boundary or special file cannot be removed: ${path}`);
|
|
139
|
-
if (snapshots.size >=
|
|
187
|
+
if (snapshots.size >= entryLimit)
|
|
140
188
|
throw new Error(
|
|
141
|
-
`Removal exceeds the ${
|
|
189
|
+
`Removal exceeds the ${entryLimit}-entry limit; use the owning cleanup command`,
|
|
142
190
|
);
|
|
143
191
|
snapshots.set(path, fingerprint(info));
|
|
144
192
|
if (info.isFile()) bytes += info.size;
|
|
@@ -172,6 +220,7 @@ export function guardedRemove(
|
|
|
172
220
|
root,
|
|
173
221
|
targets,
|
|
174
222
|
entries: snapshots.size,
|
|
223
|
+
entry_limit: entryLimit,
|
|
175
224
|
bytes,
|
|
176
225
|
protected: [...removedProtected].sort(),
|
|
177
226
|
links,
|
|
@@ -185,11 +234,21 @@ export function guardedRemove(
|
|
|
185
234
|
fingerprint(lstatSync(root)) !== rootFingerprint
|
|
186
235
|
)
|
|
187
236
|
throw new Error("Allowed root changed during inspection");
|
|
237
|
+
assertEntryLimitAuthority();
|
|
238
|
+
if (options.cleanClone) options.assertOwnedArtifact!(root);
|
|
239
|
+
// Ownership may have been released or changed while the contents were inspected.
|
|
240
|
+
allowedProtected(
|
|
241
|
+
options.allowProtected ?? [],
|
|
242
|
+
root,
|
|
243
|
+
options.liveRoots ?? [],
|
|
244
|
+
options.assertOwnedArtifact,
|
|
245
|
+
);
|
|
188
246
|
for (const target of targets) {
|
|
189
247
|
checkedPath(target, allowed);
|
|
190
248
|
assertUntracked(target);
|
|
191
249
|
}
|
|
192
250
|
for (const dir of gitDirs) assertNoUnpushedHistory(dir);
|
|
251
|
+
for (const clone of cleanClones) assertCleanClone(clone);
|
|
193
252
|
checkClaims(targets);
|
|
194
253
|
for (const [path, previous] of snapshots) {
|
|
195
254
|
if (fingerprint(lstatSync(path)) !== previous)
|
|
@@ -200,6 +259,47 @@ export function guardedRemove(
|
|
|
200
259
|
return { ...report, applied: true };
|
|
201
260
|
}
|
|
202
261
|
|
|
262
|
+
/** A disposable clone must not be the only copy of work or a registered worktree. */
|
|
263
|
+
function assertCleanClone(clone: string): void {
|
|
264
|
+
const metadata = join(clone, ".git");
|
|
265
|
+
const stat = lstatSync(metadata);
|
|
266
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
267
|
+
throw new Error(`Clean clone removal requires a standalone .git directory: ${clone}`);
|
|
268
|
+
const git = (args: string[]) => {
|
|
269
|
+
const result = spawnSync("git", ["-c", "core.fsmonitor=false", "-C", clone, ...args], {
|
|
270
|
+
encoding: "utf8",
|
|
271
|
+
timeout: 10_000,
|
|
272
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
273
|
+
env: {
|
|
274
|
+
...process.env,
|
|
275
|
+
LC_ALL: "C",
|
|
276
|
+
GIT_DIR: undefined,
|
|
277
|
+
GIT_COMMON_DIR: undefined,
|
|
278
|
+
GIT_WORK_TREE: undefined,
|
|
279
|
+
GIT_INDEX_FILE: undefined,
|
|
280
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
if (result.error || result.status !== 0)
|
|
284
|
+
throw new Error(`Cannot verify clean clone ${clone}: git ${args[0]} failed`);
|
|
285
|
+
return result.stdout;
|
|
286
|
+
};
|
|
287
|
+
if (
|
|
288
|
+
realpathSync(git(["rev-parse", "--show-toplevel"]).trim()) !== clone ||
|
|
289
|
+
realpathSync(resolve(clone, git(["rev-parse", "--git-common-dir"]).trim())) !== metadata
|
|
290
|
+
)
|
|
291
|
+
throw new Error(`Clean clone removal refuses shared repository metadata: ${clone}`);
|
|
292
|
+
const worktrees = git(["worktree", "list", "--porcelain", "-z"])
|
|
293
|
+
.split("\0")
|
|
294
|
+
.filter((entry) => entry.startsWith("worktree "));
|
|
295
|
+
if (worktrees.length !== 1 || worktrees[0] !== `worktree ${clone}`)
|
|
296
|
+
throw new Error(`Clean clone removal refuses registered or linked worktrees: ${clone}`);
|
|
297
|
+
if (git(["status", "--porcelain=v1", "--untracked-files=all"]).trim())
|
|
298
|
+
throw new Error(`Clean clone has uncommitted or untracked work: ${clone}`);
|
|
299
|
+
if (git(["rev-list", "--max-count=1", "HEAD", "--all", "--reflog", "--not", "--remotes"]).trim())
|
|
300
|
+
throw new Error(`Clean clone has HEAD, ref or reflog commits not on any remote: ${clone}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
203
303
|
/** The metadata file the artifact commands keep in each artifact workspace. */
|
|
204
304
|
const ARTIFACT_RECORD = ".harnery-artifact.json";
|
|
205
305
|
|
|
@@ -238,9 +338,14 @@ function insideAllowed(path: string, allowed: ReadonlySet<string>): boolean {
|
|
|
238
338
|
* protected path strictly inside the root, reached without symlinks, and
|
|
239
339
|
* never the Git metadata or Harnery state of a live workspace root.
|
|
240
340
|
*/
|
|
241
|
-
function allowedProtected(
|
|
341
|
+
function allowedProtected(
|
|
342
|
+
paths: string[],
|
|
343
|
+
root: string,
|
|
344
|
+
liveRoots: string[],
|
|
345
|
+
assertOwnedArtifact?: (root: string) => void,
|
|
346
|
+
): Set<string> {
|
|
242
347
|
const allowed = new Set<string>();
|
|
243
|
-
const live = liveRoots.map((dir) =>
|
|
348
|
+
const live = liveRoots.map((dir) => realpathSync(dir));
|
|
244
349
|
for (const input of paths) {
|
|
245
350
|
if (!input?.trim() || /[\0\r\n*?[\]$]/.test(input))
|
|
246
351
|
throw new Error("Use a nonempty explicit --allow-protected path without wildcards");
|
|
@@ -261,13 +366,26 @@ function allowedProtected(paths: string[], root: string, liveRoots: string[]): S
|
|
|
261
366
|
if (!named && !(info.isDirectory() && isRepositoryMetadata(path)))
|
|
262
367
|
throw new Error(`--allow-protected must name a protected path itself: ${path}`);
|
|
263
368
|
for (const dir of live) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
369
|
+
if (path === dir || strictlyInside(path, dir))
|
|
370
|
+
throw new Error(`The live workspace's own state cannot be allowed: ${path}`);
|
|
371
|
+
for (const name of [".git", ".harnery"]) {
|
|
372
|
+
const state = resolve(dir, name);
|
|
373
|
+
if (path !== state && !strictlyInside(state, path)) continue;
|
|
374
|
+
// Only an explicitly named copied state directory inside one owned
|
|
375
|
+
// artifact may cross the outer live-state ancestor. Other live roots
|
|
376
|
+
// (including a checkout inside that artifact) still veto the path.
|
|
377
|
+
if (
|
|
378
|
+
name === ".harnery" &&
|
|
379
|
+
basename(path) === ".harnery" &&
|
|
380
|
+
info.isDirectory() &&
|
|
381
|
+
dirname(root) === resolve(state, "artifacts") &&
|
|
382
|
+
assertOwnedArtifact
|
|
383
|
+
) {
|
|
384
|
+
assertOwnedArtifact(root);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
270
387
|
throw new Error(`The live workspace's own state cannot be allowed: ${path}`);
|
|
388
|
+
}
|
|
271
389
|
}
|
|
272
390
|
allowed.add(realpathSync(path));
|
|
273
391
|
}
|
|
@@ -140,6 +140,7 @@ export function reclaimAbandonedLocalConflict(
|
|
|
140
140
|
let sessionId = conflict.instance_id;
|
|
141
141
|
let ageSecs: number | undefined;
|
|
142
142
|
const liveRow = readLiveCoordinationRow(coordRoot, conflict.instance_id);
|
|
143
|
+
let expectedGenerationId = liveRow?.v3_generation_id ?? null;
|
|
143
144
|
if (liveRow) {
|
|
144
145
|
adapter = adapterOf(liveRow.platform);
|
|
145
146
|
sessionId = liveRow.session_id || conflict.instance_id;
|
|
@@ -153,7 +154,9 @@ export function reclaimAbandonedLocalConflict(
|
|
|
153
154
|
// It is required to join the exact producer state when recording the sweep.
|
|
154
155
|
adapter = adapterOf(hb.platform);
|
|
155
156
|
sessionId = hb.session_id || conflict.instance_id;
|
|
157
|
+
expectedGenerationId = hb.v3_generation_id ?? null;
|
|
156
158
|
} catch {
|
|
159
|
+
expectedGenerationId = null;
|
|
157
160
|
/* content unreadable — still reclaim the file */
|
|
158
161
|
}
|
|
159
162
|
try {
|
|
@@ -168,6 +171,7 @@ export function reclaimAbandonedLocalConflict(
|
|
|
168
171
|
owner: conflict.instance_id,
|
|
169
172
|
nativeSessionId: sessionId,
|
|
170
173
|
adapter,
|
|
174
|
+
expectedGenerationId,
|
|
171
175
|
observation: "stale_heartbeat",
|
|
172
176
|
ageMs: Math.max(0, (ageSecs ?? 0) * 1_000),
|
|
173
177
|
});
|