@iowarp/clio-coder 0.3.6 → 0.3.7
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/CHANGELOG.md +28 -0
- package/README.md +16 -5
- package/dist/{acp-2BEHC4DL.js → acp-SK4MD6MM.js} +10 -10
- package/dist/{agents-LNNFTM53.js → agents-2FN2K6ME.js} +30 -25
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-KXXFI2VS.js → auth-QIYZWM5I.js} +13 -13
- package/dist/{chunk-KHSFENX2.js → chunk-3HAPLH5M.js} +10 -10
- package/dist/{chunk-24I7BN55.js → chunk-465YSENW.js} +2 -2
- package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
- package/dist/{chunk-CYQKWTG3.js → chunk-4DWFMQDR.js} +4 -4
- package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
- package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
- package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
- package/dist/{chunk-K7T3E2SR.js → chunk-5UJ6ECTS.js} +10 -9
- package/dist/{chunk-6US73PDB.js → chunk-6M7VS3J3.js} +5 -5
- package/dist/{chunk-CJUB2JJ2.js → chunk-6TUKSZVF.js} +5 -5
- package/dist/{chunk-5JGRAMKL.js → chunk-AB4XIIVB.js} +8 -6
- package/dist/{chunk-R46L2BIR.js → chunk-BMWK7ZIZ.js} +14 -20
- package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
- package/dist/{chunk-XXQNGV4M.js → chunk-CEYBNUGC.js} +243 -63
- package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
- package/dist/chunk-DJNLUABN.js +843 -0
- package/dist/{chunk-RY3LY4J5.js → chunk-DMD2AGVS.js} +5 -4
- package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
- package/dist/chunk-DQA7QLMD.js +123 -0
- package/dist/chunk-DR52UMZW.js +21 -0
- package/dist/{chunk-XF5N4U5A.js → chunk-EBEFWSGL.js} +6 -5
- package/dist/{chunk-EYPA3EGJ.js → chunk-EELBMBT6.js} +120 -13
- package/dist/{chunk-CKXWIANG.js → chunk-EOOQZZDE.js} +16 -14
- package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
- package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
- package/dist/chunk-GWS3VEIW.js +195 -0
- package/dist/{chunk-LYF7OHWH.js → chunk-J7PIKKWC.js} +8 -463
- package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
- package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
- package/dist/{chunk-6XXKFVSN.js → chunk-JTSEDYVQ.js} +7 -7
- package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
- package/dist/chunk-KZ2H5X4G.js +1026 -0
- package/dist/{chunk-QNQHSOLF.js → chunk-LADCF22A.js} +12 -12
- package/dist/chunk-M4AKACEO.js +382 -0
- package/dist/{chunk-XYDYPRZI.js → chunk-MXI6J5JF.js} +7 -7
- package/dist/{chunk-G7MUEIGA.js → chunk-OB5HIGJY.js} +1 -1
- package/dist/{chunk-EKY57CSP.js → chunk-OBMAI2DP.js} +61 -767
- package/dist/chunk-PD3MESLB.js +242 -0
- package/dist/{chunk-ZRGEBJ4T.js → chunk-QCTRSGHQ.js} +21 -21
- package/dist/chunk-RVG5JXAL.js +41 -0
- package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
- package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
- package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
- package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
- package/dist/chunk-UND3GU2L.js +103 -0
- package/dist/{chunk-QM3F2GKX.js → chunk-UUANF5CR.js} +2247 -2096
- package/dist/chunk-UVDSQ6LW.js +472 -0
- package/dist/{chunk-DJVECN66.js → chunk-VQNODYQ4.js} +14 -14
- package/dist/{chunk-3BPUFZDL.js → chunk-VREKEFLL.js} +3 -3
- package/dist/{chunk-PBTHKCPN.js → chunk-WJHBC77E.js} +6 -6
- package/dist/{chunk-XE2VEJHX.js → chunk-X2KV5FXT.js} +2 -2
- package/dist/{chunk-ZXF4XRKW.js → chunk-XEGB6BCN.js} +157 -7
- package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
- package/dist/{verifiers-NCBTHHN2.js → chunk-YTYFXUI3.js} +65 -322
- package/dist/{chunk-OH3TOQTB.js → chunk-ZGH7FGS5.js} +13 -7
- package/dist/cli/index.js +32 -30
- package/dist/{clio-M2KGYUFZ.js → clio-WBVQEBKO.js} +7 -7
- package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +3 -3
- package/dist/{components-5TTYYX6G.js → components-F7OEATSO.js} +4 -4
- package/dist/{config-XUUYQIWO.js → config-TRBL3RCF.js} +34 -29
- package/dist/{configure-IHJ7YOMV.js → configure-OLCVPHNM.js} +15 -15
- package/dist/{context-74JLXAWD.js → context-MJIJ6GOX.js} +11 -11
- package/dist/{context-ZQ7SIFJV.js → context-WFPKQSM6.js} +19 -3
- package/dist/{context-75MIWW3U.js → context-XEWE3MOJ.js} +31 -26
- package/dist/{context-clear-GYKWNUML.js → context-clear-KNOS2JPB.js} +31 -26
- package/dist/{context-working-set-UX5KEP4J.js → context-working-set-EUXAZI6N.js} +8 -8
- package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-B7MTOVKL.js} +313 -53
- package/dist/{docs-6FZSCG5B.js → docs-FLJTIDSE.js} +4 -4
- package/dist/{doctor-SVJ5BZCW.js → doctor-RN4YKO2X.js} +14 -14
- package/dist/{eval-CG6LLBLD.js → eval-RUBJVSNQ.js} +8 -7
- package/dist/{evidence-ZYFIEN42.js → evidence-JZNBUOQZ.js} +30 -25
- package/dist/{evolve-QGEXEMDW.js → evolve-FJVC4KKI.js} +30 -25
- package/dist/{extensions-ADGNCJJD.js → extensions-IQL36S7K.js} +4 -4
- package/dist/{fleet-S5R4ZOQY.js → fleet-BDKYJFCP.js} +214 -360
- package/dist/fleet-commands-ZFIWZSB3.js +70 -0
- package/dist/fleet-graph-Y6HPXIVF.js +125 -0
- package/dist/fleet-new-RDVJLHHH.js +48 -0
- package/dist/fleet-validate-BIYREGIK.js +79 -0
- package/dist/{init-5DRU55YR.js → init-LQUB5COQ.js} +44 -37
- package/dist/library-NJAHIGG4.js +217 -0
- package/dist/{memory-7YKKR6UC.js → memory-OG6HOYKM.js} +31 -26
- package/dist/{models-ZPOLRU2C.js → models-5ZG5XY7J.js} +21 -20
- package/dist/{monitor-US5F5YGZ.js → monitor-TJ7AMTGB.js} +49 -30
- package/dist/{orchestrator-E2AL4T5N.js → orchestrator-WZYB54DM.js} +3827 -581
- package/dist/{paths-E7KYAQWE.js → paths-XUC7GS6E.js} +4 -4
- package/dist/{reset-KZ652EK6.js → reset-PXQT45IY.js} +7 -7
- package/dist/{run-SRNBKDWD.js → run-FQ74YF62.js} +53 -45
- package/dist/{share-CGZE33UP.js → share-FW7SVCL3.js} +33 -9
- package/dist/{skills-S2X4DLY5.js → skills-7E7IRB3R.js} +24 -8
- package/dist/{skills-eval-W2GGIC4R.js → skills-eval-LI75W6OK.js} +34 -27
- package/dist/{targets-54SWINWB.js → targets-4CIFKCTW.js} +23 -22
- package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WUZY7ZV5.js} +4 -4
- package/dist/{uninstall-BVLWXKBT.js → uninstall-7FV7IP4E.js} +4 -4
- package/dist/{upgrade-JKAR27XC.js → upgrade-K2HVIVMQ.js} +20 -19
- package/dist/{usage-MSAWCLX4.js → usage-GTZELZQX.js} +116 -49
- package/dist/verifiers-RLAHT27O.js +336 -0
- package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
- package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-ASIFASCN.js} +45 -37
- package/dist/worker/entry.js +38 -35
- package/docs/README.md +3 -2
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-versions.md +10 -6
- package/docs/built-in-agents.md +26 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +82 -2
- package/docs/configuration-and-targets.md +79 -2
- package/docs/context-engine.md +1 -1
- package/docs/development-pipeline.md +1 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +3 -3
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +5 -5
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +4 -1
- package/docs/extensions-and-sharing.md +6 -2
- package/docs/fleet-demo-runbook.md +2 -2
- package/docs/fleet-dispatch.md +197 -10
- package/docs/git-commit-provenance.md +2 -2
- package/docs/glossary.md +1 -1
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +2 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +55 -8
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +1 -1
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +79 -64
- package/docs/resource-library.md +59 -0
- package/docs/safety-model.md +2 -2
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +37 -1
- package/docs/skills-marketplace.md +16 -3
- package/docs/tool-usage.md +14 -7
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +3 -3
- package/package.json +1 -1
- package/src/cli/fleet-commands.ts +37 -0
- package/src/cli/fleet-graph.ts +102 -0
- package/src/cli/fleet-new.ts +36 -0
- package/src/cli/fleet-preflight.ts +121 -0
- package/src/cli/fleet-validate.ts +30 -0
- package/src/cli/fleet.ts +173 -335
- package/src/cli/index.ts +3 -1
- package/src/cli/library.ts +190 -0
- package/src/cli/share.ts +13 -1
- package/src/cli/usage.ts +111 -19
- package/src/core/bus-events.ts +4 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/config.ts +130 -0
- package/src/core/defaults.ts +81 -0
- package/src/domains/agents/builtins/architect.md +1 -0
- package/src/domains/agents/builtins/oracle.md +33 -0
- package/src/domains/agents/catalog.ts +13 -1
- package/src/domains/agents/fleet-contract.ts +278 -16
- package/src/domains/agents/index.ts +14 -0
- package/src/domains/agents/result-contract.ts +235 -1
- package/src/domains/config/classify.ts +4 -0
- package/src/domains/dispatch/active-route-planner.ts +14 -0
- package/src/domains/dispatch/backoff.ts +2 -1
- package/src/domains/dispatch/capability-match.ts +1 -0
- package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
- package/src/domains/dispatch/contract.ts +34 -0
- package/src/domains/dispatch/delegation-plan.ts +167 -0
- package/src/domains/dispatch/execution-plan.ts +76 -5
- package/src/domains/dispatch/execution-role.ts +3 -1
- package/src/domains/dispatch/execution-scheduler.ts +183 -67
- package/src/domains/dispatch/extension.ts +258 -9
- package/src/domains/dispatch/fleet-gate.ts +14 -0
- package/src/domains/dispatch/fleet-plan.ts +63 -3
- package/src/domains/dispatch/fleet-run.ts +737 -0
- package/src/domains/dispatch/gate-role-prompts.ts +9 -0
- package/src/domains/dispatch/host-verification.ts +178 -0
- package/src/domains/dispatch/index.ts +38 -0
- package/src/domains/dispatch/intent.ts +159 -0
- package/src/domains/dispatch/receipt-integrity.ts +8 -4
- package/src/domains/dispatch/state.ts +36 -3
- package/src/domains/dispatch/types.ts +51 -6
- package/src/domains/dispatch/validation.ts +66 -6
- package/src/domains/evidence/trust-status.ts +10 -1
- package/src/domains/middleware/index.ts +15 -0
- package/src/domains/middleware/watchdog.ts +281 -0
- package/src/domains/observability/contract.ts +3 -1
- package/src/domains/observability/cost.ts +12 -1
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/observability/index.ts +10 -0
- package/src/domains/observability/out-of-turn-usage.ts +223 -0
- package/src/domains/resources/index.ts +20 -0
- package/src/domains/resources/library.ts +326 -0
- package/src/domains/resources/skills/marketplace.ts +37 -12
- package/src/domains/session/handoff.ts +629 -0
- package/src/domains/share/archive.ts +67 -2
- package/src/entry/orchestrator.ts +37 -0
- package/src/interactive/bus-notices.ts +26 -0
- package/src/interactive/chat-loop.ts +235 -1
- package/src/interactive/chat-renderer.ts +22 -0
- package/src/interactive/cost-overlay.ts +31 -3
- package/src/interactive/council-dispatch.ts +30 -0
- package/src/interactive/council-grid.ts +213 -0
- package/src/interactive/council.ts +99 -0
- package/src/interactive/dispatch-board.ts +260 -16
- package/src/interactive/fleet-run-preview.ts +307 -0
- package/src/interactive/footer/notifications.ts +219 -0
- package/src/interactive/handoff-round.ts +56 -0
- package/src/interactive/interactive-application.ts +43 -1
- package/src/interactive/interactive-event-projection.ts +9 -1
- package/src/interactive/interactive-slash-runtime.ts +52 -2
- package/src/interactive/interactive-subscriptions.ts +14 -2
- package/src/interactive/oracle.ts +179 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
- package/src/interactive/overlay-general-openers.ts +190 -1
- package/src/interactive/overlay-key-routing.ts +17 -1
- package/src/interactive/overlay-lifecycle.ts +41 -1
- package/src/interactive/overlay-permission-lifecycle.ts +10 -0
- package/src/interactive/overlay-resource-openers.ts +11 -3
- package/src/interactive/overlay-session-lifecycle.ts +234 -2
- package/src/interactive/overlays/fleet-run-approval.ts +208 -0
- package/src/interactive/overlays/handoff-review.ts +185 -0
- package/src/interactive/overlays/library-install-confirm.ts +151 -0
- package/src/interactive/overlays/list-overlay.ts +168 -2
- package/src/interactive/overlays/settings.ts +101 -4
- package/src/interactive/overlays/side-question.ts +139 -0
- package/src/interactive/overlays/skills-hub.ts +401 -15
- package/src/interactive/side-question.ts +171 -0
- package/src/interactive/slash-commands.ts +432 -5
- package/src/interactive/slash-spec.ts +19 -6
- package/src/interactive/theme/tokens.ts +30 -0
- package/src/interactive/turn-middleware.ts +15 -1
- package/src/interactive/watchdog-run.ts +75 -0
- package/src/interactive/worker-share.ts +56 -1
- package/src/interactive/worker-stream.ts +7 -0
- package/src/tools/bootstrap.ts +3 -0
- package/src/tools/compete-worktrees.ts +13 -79
- package/src/tools/dispatch-admission.ts +242 -8
- package/src/tools/dispatch-arguments.ts +57 -1
- package/src/tools/dispatch-plan.ts +136 -6
- package/src/tools/dispatch-runner.ts +319 -13
- package/src/tools/dispatch-types.ts +20 -1
- package/src/tools/dispatch.ts +72 -2
- package/src/tools/monitor.ts +16 -0
- package/src/tools/profiles.ts +18 -4
- package/src/tools/task-worktree.ts +238 -0
- package/src/tools/verify/authoring.ts +61 -1
- package/src/tools/verify/scripts.ts +62 -0
- package/src/tools/worker-evidence.ts +2 -1
- package/src/worker/spec-contract.ts +1 -0
- package/dist/chunk-HC4CLZ2Y.js +0 -68
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import { createRequire as __clioCreateRequire } from "node:module"; const require = __clioCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
describeWriteBoundary,
|
|
4
|
+
normalizeWriteBoundary,
|
|
5
|
+
writeBoundaryCovers
|
|
6
|
+
} from "./chunk-KCMKRQX4.js";
|
|
7
|
+
import {
|
|
8
|
+
atomicWrite
|
|
9
|
+
} from "./chunk-FO5ZOVUY.js";
|
|
10
|
+
import {
|
|
11
|
+
clioStateDir
|
|
12
|
+
} from "./chunk-BNAZZHFG.js";
|
|
13
|
+
import {
|
|
14
|
+
init_esm_shims
|
|
15
|
+
} from "./chunk-3R73A4XB.js";
|
|
16
|
+
|
|
17
|
+
// src/tools/compete-worktrees.ts
|
|
18
|
+
init_esm_shims();
|
|
19
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
20
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
existsSync as existsSync2,
|
|
23
|
+
lstatSync,
|
|
24
|
+
mkdirSync as mkdirSync2,
|
|
25
|
+
readdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
realpathSync as realpathSync2,
|
|
28
|
+
renameSync,
|
|
29
|
+
rmdirSync,
|
|
30
|
+
rmSync as rmSync2,
|
|
31
|
+
unlinkSync,
|
|
32
|
+
writeFileSync as writeFileSync2
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { hostname } from "node:os";
|
|
35
|
+
import { join as join2, relative as relative2, resolve as resolve2 } from "node:path";
|
|
36
|
+
|
|
37
|
+
// src/tools/task-worktree.ts
|
|
38
|
+
init_esm_shims();
|
|
39
|
+
import { execFileSync } from "node:child_process";
|
|
40
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
41
|
+
import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
42
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
43
|
+
var SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
44
|
+
var OWNER_FILE_SUFFIX = ".task-owner.json";
|
|
45
|
+
var COMMIT_IDENTITY = "clio-task";
|
|
46
|
+
function gitBytes(root, args) {
|
|
47
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
timeout: 3e4
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function git(root, args) {
|
|
53
|
+
return gitBytes(root, args).toString("utf8").trim();
|
|
54
|
+
}
|
|
55
|
+
function isCanonicalWorktreePathInside(parent, candidate) {
|
|
56
|
+
let canonicalParent;
|
|
57
|
+
let canonicalCandidate;
|
|
58
|
+
try {
|
|
59
|
+
canonicalParent = realpathSync(parent);
|
|
60
|
+
} catch {
|
|
61
|
+
canonicalParent = resolve(parent);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
canonicalCandidate = realpathSync(candidate);
|
|
65
|
+
} catch {
|
|
66
|
+
canonicalCandidate = resolve(candidate);
|
|
67
|
+
}
|
|
68
|
+
const rel = relative(canonicalParent, canonicalCandidate);
|
|
69
|
+
return rel.length > 0 && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
70
|
+
}
|
|
71
|
+
function commitWorktreePath(path, identity, message) {
|
|
72
|
+
git(path, ["add", "-A"]);
|
|
73
|
+
if (git(path, ["status", "--porcelain"]).length === 0) return false;
|
|
74
|
+
git(path, [
|
|
75
|
+
"-c",
|
|
76
|
+
`user.name=${identity}`,
|
|
77
|
+
"-c",
|
|
78
|
+
`user.email=${identity}@local`,
|
|
79
|
+
"commit",
|
|
80
|
+
"-m",
|
|
81
|
+
message,
|
|
82
|
+
"--no-verify"
|
|
83
|
+
]);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function worktreeBranchDiffStat(root, branch) {
|
|
87
|
+
try {
|
|
88
|
+
return git(root, ["diff", "--shortstat", `HEAD...${branch}`]) || "no changes";
|
|
89
|
+
} catch {
|
|
90
|
+
return "diff unavailable";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function protectedPathsChangedByWorktreeBranch(root, branch, protectedPaths) {
|
|
94
|
+
const canonical = realpathSync(root);
|
|
95
|
+
const protectedInside = protectedPaths.map((path) => resolve(path)).filter((path) => {
|
|
96
|
+
const rel = relative(canonical, path);
|
|
97
|
+
return rel.length > 0 && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
98
|
+
});
|
|
99
|
+
if (protectedInside.length === 0) return [];
|
|
100
|
+
const changed = git(canonical, ["diff", "--name-only", "-z", "--no-renames", `HEAD...${branch}`]).split("\0").filter((path) => path.length > 0).map((path) => resolve(canonical, path));
|
|
101
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const candidate of changed) {
|
|
103
|
+
for (const protectedPath of protectedInside) {
|
|
104
|
+
const rel = relative(protectedPath, candidate);
|
|
105
|
+
if (candidate === protectedPath || rel.length > 0 && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) {
|
|
106
|
+
blocked.add(protectedPath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...blocked].sort();
|
|
111
|
+
}
|
|
112
|
+
function mergeWorktreeBranch(root, branch, identity) {
|
|
113
|
+
try {
|
|
114
|
+
git(root, [
|
|
115
|
+
"-c",
|
|
116
|
+
`user.name=${identity}`,
|
|
117
|
+
"-c",
|
|
118
|
+
`user.email=${identity}@local`,
|
|
119
|
+
"merge",
|
|
120
|
+
"--no-edit",
|
|
121
|
+
"--no-verify",
|
|
122
|
+
branch
|
|
123
|
+
]);
|
|
124
|
+
return { ok: true };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
git(root, ["merge", "--abort"]);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: error instanceof Error ? error.message.split("\n")[0] ?? "merge failed" : String(error)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function validateRunId(runId) {
|
|
137
|
+
if (!SAFE_RUN_ID.test(runId) || runId === "." || runId === "..")
|
|
138
|
+
throw new Error(`invalid task worktree run id '${runId}'`);
|
|
139
|
+
}
|
|
140
|
+
function gitCheckoutRoot(cwd) {
|
|
141
|
+
try {
|
|
142
|
+
return realpathSync(git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function createTaskWorktree(root, runId, base) {
|
|
148
|
+
validateRunId(runId);
|
|
149
|
+
const canonical = realpathSync(root);
|
|
150
|
+
const resolvedBase = base ?? git(canonical, ["rev-parse", "HEAD"]);
|
|
151
|
+
const parent = join(canonical, ".clio-coder", "worktrees");
|
|
152
|
+
const path = join(parent, runId);
|
|
153
|
+
const branch = `clio/task/${runId}`;
|
|
154
|
+
mkdirSync(parent, { recursive: true });
|
|
155
|
+
if (!isCanonicalWorktreePathInside(parent, path))
|
|
156
|
+
throw new Error(`task worktree path escapes its parent for run ${runId}`);
|
|
157
|
+
git(canonical, ["worktree", "add", "-b", branch, path, resolvedBase]);
|
|
158
|
+
const ownerToken = randomBytes(16).toString("hex");
|
|
159
|
+
writeFileSync(
|
|
160
|
+
`${path}${OWNER_FILE_SUFFIX}`,
|
|
161
|
+
`${JSON.stringify({ version: 1, kind: "clio-task-worktree", root: canonical, runId, branch, base: resolvedBase, ownerToken }, null, 2)}
|
|
162
|
+
`,
|
|
163
|
+
{ encoding: "utf8", flag: "wx" }
|
|
164
|
+
);
|
|
165
|
+
return { root: canonical, runId, path, branch, base: resolvedBase, ownerToken };
|
|
166
|
+
}
|
|
167
|
+
function assertOwnership(worktree) {
|
|
168
|
+
const expected = join(worktree.root, ".clio-coder", "worktrees", worktree.runId);
|
|
169
|
+
if (resolve(worktree.path) !== resolve(expected) || !isCanonicalWorktreePathInside(join(worktree.root, ".clio-coder", "worktrees"), worktree.path)) {
|
|
170
|
+
throw new Error(`task worktree ${worktree.runId} has an invalid ownership path`);
|
|
171
|
+
}
|
|
172
|
+
if (!existsSync(`${worktree.path}${OWNER_FILE_SUFFIX}`))
|
|
173
|
+
throw new Error(`task worktree ${worktree.runId} has no ownership file`);
|
|
174
|
+
}
|
|
175
|
+
function commitTaskWorktree(worktree, message = `Clio task ${worktree.runId}`) {
|
|
176
|
+
assertOwnership(worktree);
|
|
177
|
+
return commitWorktreePath(worktree.path, COMMIT_IDENTITY, message);
|
|
178
|
+
}
|
|
179
|
+
function taskWorktreeDiffHash(worktree) {
|
|
180
|
+
const bytes = gitBytes(worktree.root, ["diff", `${worktree.base}..${worktree.branch}`]);
|
|
181
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
182
|
+
}
|
|
183
|
+
function protectedPathsChangedByTaskBranch(worktree, protectedPaths) {
|
|
184
|
+
return protectedPathsChangedByWorktreeBranch(worktree.root, worktree.branch, protectedPaths);
|
|
185
|
+
}
|
|
186
|
+
function applyTaskWorktree(input) {
|
|
187
|
+
assertOwnership(input.worktree);
|
|
188
|
+
commitTaskWorktree(input.worktree);
|
|
189
|
+
const receipt = {
|
|
190
|
+
path: input.worktree.path,
|
|
191
|
+
branch: input.worktree.branch,
|
|
192
|
+
diffHash: taskWorktreeDiffHash(input.worktree),
|
|
193
|
+
apply: input.apply,
|
|
194
|
+
applied: false
|
|
195
|
+
};
|
|
196
|
+
if (input.apply === "preserve") return receipt;
|
|
197
|
+
const protectedChanges = protectedPathsChangedByTaskBranch(input.worktree, input.protectedPaths ?? []);
|
|
198
|
+
if (protectedChanges.length > 0) return { ...receipt, reason: "protected_artifact_changed" };
|
|
199
|
+
const merged = mergeWorktreeBranch(input.worktree.root, input.worktree.branch, COMMIT_IDENTITY);
|
|
200
|
+
if (!merged.ok) return { ...receipt, reason: "worktree_merge_conflict" };
|
|
201
|
+
return { ...receipt, applied: true };
|
|
202
|
+
}
|
|
203
|
+
function cleanupTaskWorktree(worktree, deleteBranch) {
|
|
204
|
+
assertOwnership(worktree);
|
|
205
|
+
try {
|
|
206
|
+
git(worktree.root, ["worktree", "remove", "--force", worktree.path]);
|
|
207
|
+
} catch {
|
|
208
|
+
if (existsSync(worktree.path)) rmSync(worktree.path, { recursive: true, force: true });
|
|
209
|
+
git(worktree.root, ["worktree", "prune"]);
|
|
210
|
+
}
|
|
211
|
+
if (deleteBranch) git(worktree.root, ["branch", "-D", worktree.branch]);
|
|
212
|
+
rmSync(`${worktree.path}${OWNER_FILE_SUFFIX}`, { force: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/tools/compete-worktrees.ts
|
|
216
|
+
var COMPETE_PARENT_SEGMENTS = [".clio-coder", "worktrees"];
|
|
217
|
+
var COMPETE_MANIFEST_FILE = ".clio-coder-compete-owner.json";
|
|
218
|
+
var COMPETE_MANIFEST_KIND = "clio-compete-group";
|
|
219
|
+
var COMPETE_COMMIT_IDENTITY = "clio-compete";
|
|
220
|
+
var SAFE_GROUP = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
221
|
+
var PROCESS_EXIT_GRACE_MS = 750;
|
|
222
|
+
var PROCESS_POLL_MS = 25;
|
|
223
|
+
function git2(root, args) {
|
|
224
|
+
return execFileSync2("git", ["-C", root, ...args], {
|
|
225
|
+
encoding: "utf8",
|
|
226
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
227
|
+
timeout: 3e4
|
|
228
|
+
}).trim();
|
|
229
|
+
}
|
|
230
|
+
function errorMessage(err) {
|
|
231
|
+
return err instanceof Error ? err.message : String(err);
|
|
232
|
+
}
|
|
233
|
+
function processSnapshot(pid) {
|
|
234
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { alive: false, birthToken: null };
|
|
235
|
+
try {
|
|
236
|
+
process.kill(pid, 0);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err.code !== "EPERM") return { alive: false, birthToken: null };
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8").trim();
|
|
242
|
+
const close = stat.lastIndexOf(")");
|
|
243
|
+
if (close > 0) {
|
|
244
|
+
const fields = stat.slice(close + 1).trim().split(/\s+/u);
|
|
245
|
+
if (fields[0] === "Z") return { alive: false, birthToken: fields[19] ?? null };
|
|
246
|
+
const startTicks = fields[19];
|
|
247
|
+
if (startTicks !== void 0 && /^\d+$/u.test(startTicks)) {
|
|
248
|
+
return { alive: true, birthToken: `linux:${startTicks}` };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const started = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
255
|
+
encoding: "utf8",
|
|
256
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
257
|
+
timeout: 2e3
|
|
258
|
+
}).trim();
|
|
259
|
+
if (started.length > 0) return { alive: true, birthToken: `ps:${started}` };
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
return { alive: true, birthToken: null };
|
|
263
|
+
}
|
|
264
|
+
function currentProcessLease() {
|
|
265
|
+
return {
|
|
266
|
+
host: hostname(),
|
|
267
|
+
pid: process.pid,
|
|
268
|
+
birthToken: processSnapshot(process.pid).birthToken
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function validProcessLease(value) {
|
|
272
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
273
|
+
const lease = value;
|
|
274
|
+
return typeof lease.host === "string" && lease.host.length > 0 && typeof lease.pid === "number" && Number.isSafeInteger(lease.pid) && lease.pid > 0 && (lease.birthToken === null || typeof lease.birthToken === "string");
|
|
275
|
+
}
|
|
276
|
+
function validRunLease(value) {
|
|
277
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
278
|
+
const lease = value;
|
|
279
|
+
return typeof lease.runId === "string" && lease.runId.length > 0 && (lease.pid === null || typeof lease.pid === "number" && Number.isSafeInteger(lease.pid) && lease.pid > 0) && (lease.birthToken === null || typeof lease.birthToken === "string") && typeof lease.processGroup === "boolean";
|
|
280
|
+
}
|
|
281
|
+
function ownerIsAlive(owner) {
|
|
282
|
+
if (owner.host !== hostname()) return true;
|
|
283
|
+
const snapshot = processSnapshot(owner.pid);
|
|
284
|
+
if (!snapshot.alive) return false;
|
|
285
|
+
if (owner.birthToken === null || snapshot.birthToken === null) return true;
|
|
286
|
+
return owner.birthToken === snapshot.birthToken;
|
|
287
|
+
}
|
|
288
|
+
function waitForProcessExit(pid, birthToken) {
|
|
289
|
+
const deadline = Date.now() + PROCESS_EXIT_GRACE_MS;
|
|
290
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
291
|
+
while (Date.now() < deadline) {
|
|
292
|
+
const snapshot = processSnapshot(pid);
|
|
293
|
+
if (!snapshot.alive || snapshot.birthToken !== birthToken) return true;
|
|
294
|
+
Atomics.wait(sleeper, 0, 0, PROCESS_POLL_MS);
|
|
295
|
+
}
|
|
296
|
+
const finalSnapshot = processSnapshot(pid);
|
|
297
|
+
return !finalSnapshot.alive || finalSnapshot.birthToken !== birthToken;
|
|
298
|
+
}
|
|
299
|
+
function terminateRunLease(lease) {
|
|
300
|
+
if (lease.pid === null) return true;
|
|
301
|
+
const snapshot = processSnapshot(lease.pid);
|
|
302
|
+
if (!snapshot.alive) return true;
|
|
303
|
+
if (lease.birthToken === null || snapshot.birthToken === null) return false;
|
|
304
|
+
if (lease.birthToken !== snapshot.birthToken) return true;
|
|
305
|
+
const target = lease.processGroup && process.platform !== "win32" ? -lease.pid : lease.pid;
|
|
306
|
+
try {
|
|
307
|
+
process.kill(target, "SIGTERM");
|
|
308
|
+
} catch (err) {
|
|
309
|
+
if (err.code === "ESRCH") return true;
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (waitForProcessExit(lease.pid, lease.birthToken)) return true;
|
|
313
|
+
try {
|
|
314
|
+
process.kill(target, "SIGKILL");
|
|
315
|
+
} catch (err) {
|
|
316
|
+
if (err.code === "ESRCH") return true;
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return waitForProcessExit(lease.pid, lease.birthToken);
|
|
320
|
+
}
|
|
321
|
+
function validateGroup(group) {
|
|
322
|
+
if (!SAFE_GROUP.test(group) || group === "." || group === "..") {
|
|
323
|
+
throw new Error(`invalid compete group '${group}'`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function canonicalRoot(root) {
|
|
327
|
+
return realpathSync2(root);
|
|
328
|
+
}
|
|
329
|
+
function competeParent(root) {
|
|
330
|
+
return join2(root, ...COMPETE_PARENT_SEGMENTS);
|
|
331
|
+
}
|
|
332
|
+
function groupDirectory(root, group) {
|
|
333
|
+
validateGroup(group);
|
|
334
|
+
return join2(competeParent(root), group);
|
|
335
|
+
}
|
|
336
|
+
function manifestPath(directory) {
|
|
337
|
+
return join2(directory, COMPETE_MANIFEST_FILE);
|
|
338
|
+
}
|
|
339
|
+
function isPlainDirectory(path) {
|
|
340
|
+
try {
|
|
341
|
+
const stat = lstatSync(path);
|
|
342
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
343
|
+
} catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function isCanonicalDirectoryAt(path) {
|
|
348
|
+
if (!isPlainDirectory(path)) return false;
|
|
349
|
+
try {
|
|
350
|
+
return relative2(realpathSync2(path), resolve2(path)) === "";
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function isCanonicalPathInside(parent, candidate) {
|
|
356
|
+
return isCanonicalWorktreePathInside(parent, candidate);
|
|
357
|
+
}
|
|
358
|
+
function manifestFromUnknown(value) {
|
|
359
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
360
|
+
const manifest = value;
|
|
361
|
+
if (manifest.version !== 2 || manifest.kind !== COMPETE_MANIFEST_KIND || typeof manifest.root !== "string" || typeof manifest.group !== "string" || typeof manifest.token !== "string" || manifest.token.length === 0 || manifest.state !== "active" && manifest.state !== "cleanup-ready" && manifest.state !== "winner-preserved" || typeof manifest.createdAt !== "string" || typeof manifest.updatedAt !== "string") {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (!validProcessLease(manifest.owner) || !Array.isArray(manifest.runs) || !manifest.runs.every(validRunLease) || new Set(manifest.runs.map((run) => run.runId)).size !== manifest.runs.length) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (manifest.winnerIndex !== void 0 && (typeof manifest.winnerIndex !== "number" || !Number.isInteger(manifest.winnerIndex) || manifest.winnerIndex < 1)) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return manifest;
|
|
371
|
+
}
|
|
372
|
+
function readManifest(root, group) {
|
|
373
|
+
const canonical = canonicalRoot(root);
|
|
374
|
+
const directory = groupDirectory(canonical, group);
|
|
375
|
+
if (!isCanonicalDirectoryAt(directory)) return null;
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(readFileSync(manifestPath(directory), "utf8"));
|
|
378
|
+
const manifest = manifestFromUnknown(parsed);
|
|
379
|
+
if (manifest === null || manifest.root !== canonical || manifest.group !== group) return null;
|
|
380
|
+
return manifest;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function ownershipFromManifest(manifest) {
|
|
386
|
+
return {
|
|
387
|
+
root: manifest.root,
|
|
388
|
+
group: manifest.group,
|
|
389
|
+
directory: groupDirectory(manifest.root, manifest.group),
|
|
390
|
+
token: manifest.token,
|
|
391
|
+
state: manifest.state,
|
|
392
|
+
...manifest.winnerIndex !== void 0 ? { winnerIndex: manifest.winnerIndex } : {}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function assertOwnership2(ownership) {
|
|
396
|
+
const manifest = readManifest(ownership.root, ownership.group);
|
|
397
|
+
if (manifest === null || manifest.token !== ownership.token) {
|
|
398
|
+
throw new Error(`compete group ${ownership.group} has no matching ownership manifest; refusing cleanup`);
|
|
399
|
+
}
|
|
400
|
+
if (resolve2(ownership.directory) !== resolve2(groupDirectory(manifest.root, manifest.group))) {
|
|
401
|
+
throw new Error(`compete group ${ownership.group} ownership path changed; refusing cleanup`);
|
|
402
|
+
}
|
|
403
|
+
return manifest;
|
|
404
|
+
}
|
|
405
|
+
function replaceManifest(ownership, manifest) {
|
|
406
|
+
assertOwnership2(ownership);
|
|
407
|
+
const destination = manifestPath(ownership.directory);
|
|
408
|
+
const temporary = `${destination}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
409
|
+
try {
|
|
410
|
+
writeFileSync2(temporary, `${JSON.stringify(manifest, null, 2)}
|
|
411
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
412
|
+
renameSync(temporary, destination);
|
|
413
|
+
} finally {
|
|
414
|
+
if (existsSync2(temporary)) rmSync2(temporary, { force: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function isGitRepository(root) {
|
|
418
|
+
try {
|
|
419
|
+
return git2(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function competeBranch(group, index) {
|
|
425
|
+
validateGroup(group);
|
|
426
|
+
if (!Number.isInteger(index) || index < 1) throw new Error(`invalid compete candidate index ${index}`);
|
|
427
|
+
return `clio/compete/${group}/${index}`;
|
|
428
|
+
}
|
|
429
|
+
function claimCompeteGroup(root, group) {
|
|
430
|
+
const canonical = canonicalRoot(root);
|
|
431
|
+
const directory = groupDirectory(canonical, group);
|
|
432
|
+
const parent = competeParent(canonical);
|
|
433
|
+
mkdirSync2(parent, { recursive: true });
|
|
434
|
+
if (!isCanonicalDirectoryAt(parent)) {
|
|
435
|
+
throw new Error(`compete parent ${parent} is not a canonical local directory`);
|
|
436
|
+
}
|
|
437
|
+
mkdirSync2(directory);
|
|
438
|
+
if (!isCanonicalDirectoryAt(directory)) {
|
|
439
|
+
throw new Error(`compete group directory ${directory} is not canonical`);
|
|
440
|
+
}
|
|
441
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
442
|
+
const manifest = {
|
|
443
|
+
version: 2,
|
|
444
|
+
kind: COMPETE_MANIFEST_KIND,
|
|
445
|
+
root: canonical,
|
|
446
|
+
group,
|
|
447
|
+
token: randomBytes2(16).toString("hex"),
|
|
448
|
+
state: "active",
|
|
449
|
+
createdAt: now,
|
|
450
|
+
updatedAt: now,
|
|
451
|
+
owner: currentProcessLease(),
|
|
452
|
+
runs: []
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
writeFileSync2(manifestPath(directory), `${JSON.stringify(manifest, null, 2)}
|
|
456
|
+
`, {
|
|
457
|
+
encoding: "utf8",
|
|
458
|
+
flag: "wx"
|
|
459
|
+
});
|
|
460
|
+
} catch (err) {
|
|
461
|
+
try {
|
|
462
|
+
rmdirSync(directory);
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
467
|
+
return ownershipFromManifest(manifest);
|
|
468
|
+
}
|
|
469
|
+
function loadCompeteGroup(root, group) {
|
|
470
|
+
validateGroup(group);
|
|
471
|
+
const manifest = readManifest(root, group);
|
|
472
|
+
return manifest === null ? null : ownershipFromManifest(manifest);
|
|
473
|
+
}
|
|
474
|
+
function registerCompeteGroupRun(ownership, admission) {
|
|
475
|
+
const current = assertOwnership2(ownership);
|
|
476
|
+
if (current.state !== "active") {
|
|
477
|
+
throw new Error(`compete group ${ownership.group} is ${current.state}; refusing new run ${admission.runId}`);
|
|
478
|
+
}
|
|
479
|
+
if (admission.runId.length === 0) throw new Error("worktree transaction run id cannot be empty");
|
|
480
|
+
if (admission.pid !== null && (!Number.isSafeInteger(admission.pid) || admission.pid <= 0)) {
|
|
481
|
+
throw new Error(`worktree transaction run ${admission.runId} has invalid pid ${admission.pid}`);
|
|
482
|
+
}
|
|
483
|
+
const lease = {
|
|
484
|
+
runId: admission.runId,
|
|
485
|
+
pid: admission.pid,
|
|
486
|
+
birthToken: admission.pid === null ? null : processSnapshot(admission.pid).birthToken,
|
|
487
|
+
processGroup: admission.runtimeKind === "acp-delegation" && process.platform !== "win32"
|
|
488
|
+
};
|
|
489
|
+
const next = {
|
|
490
|
+
...current,
|
|
491
|
+
runs: [...current.runs.filter((run) => run.runId !== admission.runId), lease],
|
|
492
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
493
|
+
};
|
|
494
|
+
replaceManifest(ownership, next);
|
|
495
|
+
}
|
|
496
|
+
function settleCompeteGroupRun(ownership, runId) {
|
|
497
|
+
const current = assertOwnership2(ownership);
|
|
498
|
+
if (current.state !== "active") return;
|
|
499
|
+
if (!current.runs.some((run) => run.runId === runId)) return;
|
|
500
|
+
const next = {
|
|
501
|
+
...current,
|
|
502
|
+
runs: current.runs.filter((run) => run.runId !== runId),
|
|
503
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
504
|
+
};
|
|
505
|
+
replaceManifest(ownership, next);
|
|
506
|
+
}
|
|
507
|
+
function markCompeteGroupCleanupReady(ownership) {
|
|
508
|
+
const current = assertOwnership2(ownership);
|
|
509
|
+
const next = {
|
|
510
|
+
...current,
|
|
511
|
+
state: "cleanup-ready",
|
|
512
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
513
|
+
};
|
|
514
|
+
Reflect.deleteProperty(next, "winnerIndex");
|
|
515
|
+
replaceManifest(ownership, next);
|
|
516
|
+
return ownershipFromManifest(next);
|
|
517
|
+
}
|
|
518
|
+
function markCompeteGroupWinnerPreserved(ownership, winnerIndex) {
|
|
519
|
+
competeBranch(ownership.group, winnerIndex);
|
|
520
|
+
const current = assertOwnership2(ownership);
|
|
521
|
+
const next = {
|
|
522
|
+
...current,
|
|
523
|
+
state: "winner-preserved",
|
|
524
|
+
winnerIndex,
|
|
525
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
526
|
+
};
|
|
527
|
+
replaceManifest(ownership, next);
|
|
528
|
+
return ownershipFromManifest(next);
|
|
529
|
+
}
|
|
530
|
+
function createCandidateWorktree(ownership, index, baseline) {
|
|
531
|
+
assertOwnership2(ownership);
|
|
532
|
+
const branch = competeBranch(ownership.group, index);
|
|
533
|
+
const path = join2(ownership.directory, `candidate-${index}`);
|
|
534
|
+
if (!isCanonicalPathInside(ownership.directory, path)) {
|
|
535
|
+
throw new Error(`candidate ${index} path escapes compete group ${ownership.group}`);
|
|
536
|
+
}
|
|
537
|
+
git2(ownership.root, ["worktree", "add", "-b", branch, path, baseline]);
|
|
538
|
+
return { index, branch, path };
|
|
539
|
+
}
|
|
540
|
+
function commitCandidateWork(worktree, message) {
|
|
541
|
+
return commitWorktreePath(worktree.path, COMPETE_COMMIT_IDENTITY, message);
|
|
542
|
+
}
|
|
543
|
+
function candidateDiffStat(root, branch) {
|
|
544
|
+
return worktreeBranchDiffStat(root, branch);
|
|
545
|
+
}
|
|
546
|
+
function protectedPathsChangedByCompeteBranch(root, branch, protectedPaths) {
|
|
547
|
+
return protectedPathsChangedByWorktreeBranch(root, branch, protectedPaths);
|
|
548
|
+
}
|
|
549
|
+
function registeredWorktreePaths(root) {
|
|
550
|
+
const listing = git2(root, ["worktree", "list", "--porcelain", "-z"]);
|
|
551
|
+
return listing.split("\0").filter((entry) => entry.startsWith("worktree ")).map((entry) => entry.slice("worktree ".length)).filter((path) => path !== void 0 && path.length > 0);
|
|
552
|
+
}
|
|
553
|
+
function exactGroupBranches(root, group) {
|
|
554
|
+
return git2(root, ["branch", "--list", "--format=%(refname:short)"]).split("\n").filter((branch) => {
|
|
555
|
+
const parts = branch.split("/");
|
|
556
|
+
return parts.length === 4 && parts[0] === "clio" && parts[1] === "compete" && parts[2] === group && /^\d+$/.test(parts[3] ?? "");
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function removeRegisteredWorktree(root, path) {
|
|
560
|
+
try {
|
|
561
|
+
git2(root, ["worktree", "remove", "--force", path]);
|
|
562
|
+
} catch {
|
|
563
|
+
if (existsSync2(path)) rmSync2(path, { recursive: true, force: true });
|
|
564
|
+
git2(root, ["worktree", "prune"]);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function removeCandidateWorktree(ownership, worktree, deleteBranch) {
|
|
568
|
+
assertOwnership2(ownership);
|
|
569
|
+
const expectedPath = join2(ownership.directory, `candidate-${worktree.index}`);
|
|
570
|
+
const expectedBranch = competeBranch(ownership.group, worktree.index);
|
|
571
|
+
if (resolve2(worktree.path) !== resolve2(expectedPath) || worktree.branch !== expectedBranch || !isCanonicalPathInside(ownership.directory, worktree.path)) {
|
|
572
|
+
throw new Error(`candidate ${worktree.index} does not belong to compete group ${ownership.group}`);
|
|
573
|
+
}
|
|
574
|
+
removeRegisteredWorktree(ownership.root, worktree.path);
|
|
575
|
+
if (deleteBranch && exactGroupBranches(ownership.root, ownership.group).includes(worktree.branch)) {
|
|
576
|
+
git2(ownership.root, ["branch", "-D", worktree.branch]);
|
|
577
|
+
}
|
|
578
|
+
const stillRegistered = registeredWorktreePaths(ownership.root).some(
|
|
579
|
+
(path) => resolve2(path) === resolve2(worktree.path)
|
|
580
|
+
);
|
|
581
|
+
const branchRemains = deleteBranch && exactGroupBranches(ownership.root, ownership.group).includes(worktree.branch);
|
|
582
|
+
if (stillRegistered || existsSync2(worktree.path) || branchRemains) {
|
|
583
|
+
throw new Error(`failed to remove candidate ${worktree.index} from compete group ${ownership.group}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function mergeWinnerBranch(root, branch) {
|
|
587
|
+
return mergeWorktreeBranch(root, branch, COMPETE_COMMIT_IDENTITY);
|
|
588
|
+
}
|
|
589
|
+
function cleanupWorktreeGroup(ownership) {
|
|
590
|
+
assertOwnership2(ownership);
|
|
591
|
+
for (const path of registeredWorktreePaths(ownership.root)) {
|
|
592
|
+
if (!isCanonicalPathInside(ownership.directory, path)) continue;
|
|
593
|
+
removeRegisteredWorktree(ownership.root, path);
|
|
594
|
+
}
|
|
595
|
+
git2(ownership.root, ["worktree", "prune"]);
|
|
596
|
+
const registeredRemain = registeredWorktreePaths(ownership.root).filter(
|
|
597
|
+
(path) => isCanonicalPathInside(ownership.directory, path)
|
|
598
|
+
);
|
|
599
|
+
if (registeredRemain.length > 0) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`failed to remove ${registeredRemain.length} registered worktree(s) for compete group ${ownership.group}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
for (const branch of exactGroupBranches(ownership.root, ownership.group)) {
|
|
605
|
+
git2(ownership.root, ["branch", "-D", branch]);
|
|
606
|
+
}
|
|
607
|
+
const branchesRemain = exactGroupBranches(ownership.root, ownership.group);
|
|
608
|
+
if (branchesRemain.length > 0) {
|
|
609
|
+
throw new Error(`failed to remove ${branchesRemain.length} branch(es) for compete group ${ownership.group}`);
|
|
610
|
+
}
|
|
611
|
+
assertOwnership2(ownership);
|
|
612
|
+
for (const entry of readdirSync(ownership.directory)) {
|
|
613
|
+
if (entry === COMPETE_MANIFEST_FILE) continue;
|
|
614
|
+
rmSync2(join2(ownership.directory, entry), { recursive: true, force: true });
|
|
615
|
+
}
|
|
616
|
+
assertOwnership2(ownership);
|
|
617
|
+
const unexpected = readdirSync(ownership.directory).filter((entry) => entry !== COMPETE_MANIFEST_FILE);
|
|
618
|
+
if (unexpected.length > 0) {
|
|
619
|
+
throw new Error(`compete group ${ownership.group} changed during cleanup; ownership is preserved`);
|
|
620
|
+
}
|
|
621
|
+
unlinkSync(manifestPath(ownership.directory));
|
|
622
|
+
rmdirSync(ownership.directory);
|
|
623
|
+
try {
|
|
624
|
+
rmdirSync(competeParent(ownership.root));
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function cleanupCompeteGroup(ownership) {
|
|
629
|
+
cleanupWorktreeGroup(ownership);
|
|
630
|
+
}
|
|
631
|
+
function settleRecoveredCompeteDecision(root, group, winnerIndex) {
|
|
632
|
+
let ownership = loadCompeteGroup(root, group);
|
|
633
|
+
if (ownership === null) return null;
|
|
634
|
+
if (ownership.state === "cleanup-ready") {
|
|
635
|
+
cleanupWorktreeGroup(ownership);
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
if (ownership.state === "winner-preserved") {
|
|
639
|
+
if (winnerIndex !== null && ownership.winnerIndex === winnerIndex) return ownership;
|
|
640
|
+
throw new Error(`compete group ${group} was already preserved with a different winner`);
|
|
641
|
+
}
|
|
642
|
+
const manifest = assertOwnership2(ownership);
|
|
643
|
+
if (ownerIsAlive(manifest.owner)) {
|
|
644
|
+
throw new Error(`compete group ${group} still has a live coordinator; refusing restart settlement`);
|
|
645
|
+
}
|
|
646
|
+
const unterminated = manifest.runs.filter((run) => !terminateRunLease(run));
|
|
647
|
+
if (unterminated.length > 0) {
|
|
648
|
+
throw new Error(`could not safely terminate ${unterminated.length} leased compete worker(s) for ${group}`);
|
|
649
|
+
}
|
|
650
|
+
if (winnerIndex === null) {
|
|
651
|
+
ownership = markCompeteGroupCleanupReady(ownership);
|
|
652
|
+
cleanupWorktreeGroup(ownership);
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const winnerBranch = competeBranch(group, winnerIndex);
|
|
656
|
+
const branches = exactGroupBranches(ownership.root, group);
|
|
657
|
+
const winnerPath = join2(ownership.directory, `candidate-${winnerIndex}`);
|
|
658
|
+
if (!branches.includes(winnerBranch) || !registeredWorktreePaths(ownership.root).some((path) => resolve2(path) === resolve2(winnerPath))) {
|
|
659
|
+
throw new Error(`recovered winner ${winnerBranch} has no intact candidate worktree`);
|
|
660
|
+
}
|
|
661
|
+
ownership = markCompeteGroupWinnerPreserved(ownership, winnerIndex);
|
|
662
|
+
for (const branch of branches) {
|
|
663
|
+
const index = Number.parseInt(branch.split("/").at(-1) ?? "", 10);
|
|
664
|
+
if (index === winnerIndex) continue;
|
|
665
|
+
removeCandidateWorktree(ownership, { index, branch, path: join2(ownership.directory, `candidate-${index}`) }, true);
|
|
666
|
+
}
|
|
667
|
+
return ownership;
|
|
668
|
+
}
|
|
669
|
+
function recoverCleanupReadyCompeteGroups(root, options = {}) {
|
|
670
|
+
const canonical = canonicalRoot(root);
|
|
671
|
+
const parent = competeParent(canonical);
|
|
672
|
+
const result = { cleaned: [], preserved: [], failed: [] };
|
|
673
|
+
if (!isPlainDirectory(parent)) return result;
|
|
674
|
+
for (const entry of readdirSync(parent, { withFileTypes: true })) {
|
|
675
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
676
|
+
result.preserved.push(entry.name);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
let ownership = null;
|
|
680
|
+
let manifest = null;
|
|
681
|
+
try {
|
|
682
|
+
manifest = readManifest(canonical, entry.name);
|
|
683
|
+
ownership = manifest === null ? null : ownershipFromManifest(manifest);
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
if (ownership === null || manifest === null) {
|
|
687
|
+
result.preserved.push(entry.name);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (ownership.state === "winner-preserved") {
|
|
691
|
+
result.preserved.push(entry.name);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (ownership.state === "active") {
|
|
695
|
+
if (ownerIsAlive(manifest.owner)) {
|
|
696
|
+
result.preserved.push(entry.name);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const unterminated = manifest.runs.filter((run) => !terminateRunLease(run));
|
|
700
|
+
if (unterminated.length > 0) {
|
|
701
|
+
result.failed.push({
|
|
702
|
+
group: entry.name,
|
|
703
|
+
message: `could not safely terminate ${unterminated.length} leased compete worker(s)`
|
|
704
|
+
});
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (options.preserveAllActive === true || options.preserveActiveGroups?.has(entry.name) === true) {
|
|
708
|
+
result.preserved.push(entry.name);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
ownership = markCompeteGroupCleanupReady(ownership);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
result.failed.push({ group: entry.name, message: errorMessage(err) });
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
cleanupWorktreeGroup(ownership);
|
|
720
|
+
result.cleaned.push(entry.name);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
result.failed.push({ group: entry.name, message: errorMessage(err) });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/domains/dispatch/write-boundary.ts
|
|
729
|
+
init_esm_shims();
|
|
730
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
731
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
732
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readlinkSync, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
733
|
+
import { dirname, join as join3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
734
|
+
var WRITE_BOUNDARY_VIOLATION_REASON = "writes_boundary_violation";
|
|
735
|
+
var ABSENT = "absent";
|
|
736
|
+
var GIT_TIMEOUT_MS = 3e4;
|
|
737
|
+
function git3(root, args, input) {
|
|
738
|
+
return execFileSync3("git", ["-C", root, ...args], {
|
|
739
|
+
encoding: "utf8",
|
|
740
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
741
|
+
timeout: GIT_TIMEOUT_MS,
|
|
742
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
743
|
+
...input === void 0 ? {} : { input }
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function gitBytes2(root, args) {
|
|
747
|
+
return execFileSync3("git", ["-C", root, ...args], {
|
|
748
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
749
|
+
timeout: GIT_TIMEOUT_MS,
|
|
750
|
+
maxBuffer: 256 * 1024 * 1024
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function dirtyPaths(root) {
|
|
754
|
+
const raw = git3(root, ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"]);
|
|
755
|
+
const journal = repoRelative(root, clioStateDir());
|
|
756
|
+
return raw.split("\0").filter((entry) => entry.length > 3).map((entry) => entry.slice(3)).filter((path) => journal === null || !(path === journal || path.startsWith(`${journal}/`)));
|
|
757
|
+
}
|
|
758
|
+
function contentTokens(root, paths) {
|
|
759
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
760
|
+
const hashable = [];
|
|
761
|
+
for (const path of paths) {
|
|
762
|
+
const full = join3(root, path);
|
|
763
|
+
let stat;
|
|
764
|
+
try {
|
|
765
|
+
stat = lstatSync2(full);
|
|
766
|
+
} catch {
|
|
767
|
+
tokens.set(path, ABSENT);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (stat.isSymbolicLink()) {
|
|
771
|
+
tokens.set(path, `link:${readlinkSync(full)}`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (stat.isDirectory()) {
|
|
775
|
+
tokens.set(path, "dir");
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (!stat.isFile()) {
|
|
779
|
+
tokens.set(path, `special:${stat.mode}`);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (path.includes("\n")) {
|
|
783
|
+
tokens.set(path, `blob:${git3(root, ["hash-object", "--", full]).trim()}`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
hashable.push(path);
|
|
787
|
+
}
|
|
788
|
+
if (hashable.length > 0) {
|
|
789
|
+
const hashes = git3(root, ["hash-object", "--stdin-paths"], `${hashable.map((path) => join3(root, path)).join("\n")}
|
|
790
|
+
`).split("\n").filter((line) => line.length > 0);
|
|
791
|
+
hashable.forEach((path, index) => {
|
|
792
|
+
tokens.set(path, `blob:${hashes[index] ?? "unknown"}`);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return tokens;
|
|
796
|
+
}
|
|
797
|
+
function baselineTokens(root, head, paths) {
|
|
798
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
799
|
+
if (paths.length === 0) return tokens;
|
|
800
|
+
const query = paths.map((path) => `${head}:${path}`).join("\n");
|
|
801
|
+
const answered = git3(root, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], `${query}
|
|
802
|
+
`).split("\n").filter((line) => line.length > 0);
|
|
803
|
+
paths.forEach((path, index) => {
|
|
804
|
+
const line = answered[index] ?? "";
|
|
805
|
+
const [objectName, objectType] = line.split(" ");
|
|
806
|
+
if (objectType === "blob" && objectName !== void 0) tokens.set(path, `blob:${objectName}`);
|
|
807
|
+
else if (objectType === "tree") tokens.set(path, "dir");
|
|
808
|
+
else tokens.set(path, ABSENT);
|
|
809
|
+
});
|
|
810
|
+
return tokens;
|
|
811
|
+
}
|
|
812
|
+
function captureWorkspaceSnapshot(root) {
|
|
813
|
+
const absolute = resolve3(root);
|
|
814
|
+
if (!isGitRepository(absolute)) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`write boundary: ${absolute} is not a git repository, so a declared boundary cannot be verified (enforcement fails closed)`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const head = git3(absolute, ["rev-parse", "HEAD"]).trim();
|
|
820
|
+
const paths = dirtyPaths(absolute);
|
|
821
|
+
return { root: absolute, head, entries: contentTokens(absolute, paths), capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
822
|
+
}
|
|
823
|
+
function diffWorkspace(snapshot) {
|
|
824
|
+
const candidates = [.../* @__PURE__ */ new Set([...snapshot.entries.keys(), ...dirtyPaths(snapshot.root)])].sort();
|
|
825
|
+
if (candidates.length === 0) return [];
|
|
826
|
+
const unknownBefore = candidates.filter((path) => !snapshot.entries.has(path));
|
|
827
|
+
const baseline = baselineTokens(snapshot.root, snapshot.head, unknownBefore);
|
|
828
|
+
const now = contentTokens(snapshot.root, candidates);
|
|
829
|
+
const changes = [];
|
|
830
|
+
for (const path of candidates) {
|
|
831
|
+
const before = snapshot.entries.get(path) ?? baseline.get(path) ?? ABSENT;
|
|
832
|
+
const after = now.get(path) ?? ABSENT;
|
|
833
|
+
if (before !== after) changes.push({ path, before, after });
|
|
834
|
+
}
|
|
835
|
+
return changes;
|
|
836
|
+
}
|
|
837
|
+
function canonicalVerdict(verdict) {
|
|
838
|
+
return JSON.stringify(verdict);
|
|
839
|
+
}
|
|
840
|
+
function rollbackPath(snapshot, change) {
|
|
841
|
+
const full = join3(snapshot.root, change.path);
|
|
842
|
+
if (snapshot.entries.has(change.path)) {
|
|
843
|
+
return {
|
|
844
|
+
unrecoverable: {
|
|
845
|
+
path: change.path,
|
|
846
|
+
reason: `path was already modified or untracked before the step, so its prior content is not recorded anywhere`
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
if (change.before === ABSENT) {
|
|
851
|
+
try {
|
|
852
|
+
rmSync3(full, { force: true, recursive: false });
|
|
853
|
+
} catch (error) {
|
|
854
|
+
return {
|
|
855
|
+
unrecoverable: { path: change.path, reason: `could not remove: ${error.message}` }
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
return { rolledBack: { path: change.path, action: "removed", restoredFrom: "did not exist at baseline" } };
|
|
859
|
+
}
|
|
860
|
+
if (!change.before.startsWith("blob:")) {
|
|
861
|
+
return {
|
|
862
|
+
unrecoverable: {
|
|
863
|
+
path: change.path,
|
|
864
|
+
reason: `baseline state '${change.before}' is not a regular file and is not restorable in place`
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const content = gitBytes2(snapshot.root, ["cat-file", "blob", `${snapshot.head}:${change.path}`]);
|
|
870
|
+
mkdirSync3(dirname(full), { recursive: true });
|
|
871
|
+
writeFileSync3(full, content);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
return {
|
|
874
|
+
unrecoverable: { path: change.path, reason: `could not restore from baseline: ${error.message}` }
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
return { rolledBack: { path: change.path, action: "restored", restoredFrom: `${snapshot.head}:${change.path}` } };
|
|
878
|
+
}
|
|
879
|
+
function enforceWriteBoundary(input) {
|
|
880
|
+
const allow = normalizeWriteBoundary(input.allow);
|
|
881
|
+
const changes = diffWorkspace(input.snapshot);
|
|
882
|
+
const violations = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
|
|
883
|
+
const rolledBack = [];
|
|
884
|
+
const unrecoverable = [];
|
|
885
|
+
for (const change of violations) {
|
|
886
|
+
const outcome = rollbackPath(input.snapshot, change);
|
|
887
|
+
if (outcome.rolledBack !== void 0) rolledBack.push(outcome.rolledBack);
|
|
888
|
+
if (outcome.unrecoverable !== void 0) unrecoverable.push(outcome.unrecoverable);
|
|
889
|
+
}
|
|
890
|
+
const status = violations.length === 0 ? "clean" : unrecoverable.length === 0 ? "rolled-back" : "rollback-incomplete";
|
|
891
|
+
const attribution = input.stepIds.length === 1 ? `step '${input.stepIds[0]}'` : `steps ${input.stepIds.map((id) => `'${id}'`).join(", ")} ran concurrently in one checkout, so the write cannot be attributed to one of them`;
|
|
892
|
+
const detail = violations.length === 0 ? null : [
|
|
893
|
+
`${attribution} wrote outside its declared boundary.`,
|
|
894
|
+
`Unauthorized paths: ${violations.map((change) => change.path).join(", ")}.`,
|
|
895
|
+
`Declared writes: ${describeWriteBoundary(allow)}.`,
|
|
896
|
+
status === "rolled-back" ? "Those paths were rolled back to the baseline commit." : `Rollback is incomplete; the working tree is left as the step made it. Unrestorable: ${unrecoverable.map((entry) => `${entry.path} (${entry.reason})`).join("; ")}.`,
|
|
897
|
+
"If the change was legitimate, widen the step's `writes:` declaration to cover it."
|
|
898
|
+
].join(" ");
|
|
899
|
+
const body = {
|
|
900
|
+
version: 1,
|
|
901
|
+
window: input.window,
|
|
902
|
+
stepIds: [...input.stepIds],
|
|
903
|
+
allow,
|
|
904
|
+
baselineHead: input.snapshot.head,
|
|
905
|
+
capturedAt: input.snapshot.capturedAt,
|
|
906
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
907
|
+
changedPaths: changes.map((change) => change.path),
|
|
908
|
+
violations: violations.map((change) => change.path),
|
|
909
|
+
rolledBack,
|
|
910
|
+
unrecoverable,
|
|
911
|
+
status,
|
|
912
|
+
reason: violations.length === 0 ? null : WRITE_BOUNDARY_VIOLATION_REASON,
|
|
913
|
+
detail
|
|
914
|
+
};
|
|
915
|
+
return { ...body, digest: createHash2("sha256").update(canonicalVerdict(body), "utf8").digest("hex") };
|
|
916
|
+
}
|
|
917
|
+
function assertWriteBoundaryInsideRoot(root, allow) {
|
|
918
|
+
const realRoot = realpathSync3(resolve3(root));
|
|
919
|
+
for (const entry of allow) {
|
|
920
|
+
const relativeEntry = entry.endsWith("/") ? entry.slice(0, -1) : entry;
|
|
921
|
+
let probe = join3(realRoot, relativeEntry);
|
|
922
|
+
while (probe !== realRoot && !existsSync3(probe)) probe = dirname(probe);
|
|
923
|
+
const resolved = realpathSync3(probe);
|
|
924
|
+
if (resolved !== realRoot && !resolved.startsWith(realRoot + sep2)) {
|
|
925
|
+
throw new Error(`write boundary: entry '${entry}' resolves to ${resolved}, outside the workspace at ${realRoot}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function repoRelative(root, path) {
|
|
930
|
+
const rel = relative3(resolve3(root), resolve3(path));
|
|
931
|
+
if (rel.length === 0 || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return null;
|
|
932
|
+
return rel.split(sep2).join("/");
|
|
933
|
+
}
|
|
934
|
+
function writeBoundaryDir(rootId) {
|
|
935
|
+
return join3(clioStateDir(), "write-boundaries", rootId);
|
|
936
|
+
}
|
|
937
|
+
function writeBoundaryVerdictPath(rootId, window) {
|
|
938
|
+
return join3(writeBoundaryDir(rootId), `${window.replace(/[^A-Za-z0-9._-]/gu, "_")}.json`);
|
|
939
|
+
}
|
|
940
|
+
function writeWriteBoundaryVerdict(rootId, verdict) {
|
|
941
|
+
const path = writeBoundaryVerdictPath(rootId, verdict.window);
|
|
942
|
+
atomicWrite(path, `${JSON.stringify(verdict, null, 2)}
|
|
943
|
+
`);
|
|
944
|
+
return path;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/domains/dispatch/write-boundary-enforcer.ts
|
|
948
|
+
init_esm_shims();
|
|
949
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
950
|
+
function preflightWriteBoundaries(plan, root) {
|
|
951
|
+
const declared = plan.steps.filter((step) => step.writes !== void 0);
|
|
952
|
+
if (declared.length === 0) return;
|
|
953
|
+
if (!existsSync4(root) || !isGitRepository(root)) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`write boundaries are declared but ${root} is not a git repository; boundary enforcement compares the checkout before and after each step and fails closed without one`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
for (const step of declared) assertWriteBoundaryInsideRoot(root, step.writes ?? []);
|
|
959
|
+
}
|
|
960
|
+
function createWriteBoundaryEnforcer(input) {
|
|
961
|
+
const open = /* @__PURE__ */ new Map();
|
|
962
|
+
const allowFor = (stepIds) => {
|
|
963
|
+
const allow = /* @__PURE__ */ new Set();
|
|
964
|
+
for (const stepId of stepIds) for (const entry of input.boundaryFor(stepId) ?? []) allow.add(entry);
|
|
965
|
+
return [...allow].sort();
|
|
966
|
+
};
|
|
967
|
+
return {
|
|
968
|
+
begin(window, stepIds) {
|
|
969
|
+
const allow = allowFor(stepIds);
|
|
970
|
+
assertWriteBoundaryInsideRoot(input.root, allow);
|
|
971
|
+
open.set(window, { snapshot: captureWorkspaceSnapshot(input.root), allow });
|
|
972
|
+
},
|
|
973
|
+
async verify(window, stepIds) {
|
|
974
|
+
const pending = open.get(window);
|
|
975
|
+
if (pending === void 0) {
|
|
976
|
+
throw new Error(`write boundary: window '${window}' was verified without a snapshot`);
|
|
977
|
+
}
|
|
978
|
+
open.delete(window);
|
|
979
|
+
const verdict = enforceWriteBoundary({
|
|
980
|
+
snapshot: pending.snapshot,
|
|
981
|
+
window,
|
|
982
|
+
stepIds,
|
|
983
|
+
allow: pending.allow
|
|
984
|
+
});
|
|
985
|
+
const path = writeWriteBoundaryVerdict(input.rootId, verdict);
|
|
986
|
+
input.onVerdict?.(verdict, path);
|
|
987
|
+
return {
|
|
988
|
+
window,
|
|
989
|
+
violated: verdict.reason !== null,
|
|
990
|
+
// Every step in the window is named. With one step that is exact;
|
|
991
|
+
// with several it is the honest answer, because one checkout cannot
|
|
992
|
+
// tell the scheduler which of them wrote the path.
|
|
993
|
+
failedStepIds: verdict.reason === null ? [] : [...stepIds],
|
|
994
|
+
detail: verdict.detail
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export {
|
|
1001
|
+
gitCheckoutRoot,
|
|
1002
|
+
createTaskWorktree,
|
|
1003
|
+
applyTaskWorktree,
|
|
1004
|
+
cleanupTaskWorktree,
|
|
1005
|
+
isGitRepository,
|
|
1006
|
+
claimCompeteGroup,
|
|
1007
|
+
loadCompeteGroup,
|
|
1008
|
+
registerCompeteGroupRun,
|
|
1009
|
+
settleCompeteGroupRun,
|
|
1010
|
+
markCompeteGroupCleanupReady,
|
|
1011
|
+
markCompeteGroupWinnerPreserved,
|
|
1012
|
+
createCandidateWorktree,
|
|
1013
|
+
commitCandidateWork,
|
|
1014
|
+
candidateDiffStat,
|
|
1015
|
+
protectedPathsChangedByCompeteBranch,
|
|
1016
|
+
removeCandidateWorktree,
|
|
1017
|
+
mergeWinnerBranch,
|
|
1018
|
+
cleanupCompeteGroup,
|
|
1019
|
+
settleRecoveredCompeteDecision,
|
|
1020
|
+
recoverCleanupReadyCompeteGroups,
|
|
1021
|
+
WRITE_BOUNDARY_VIOLATION_REASON,
|
|
1022
|
+
captureWorkspaceSnapshot,
|
|
1023
|
+
preflightWriteBoundaries,
|
|
1024
|
+
createWriteBoundaryEnforcer
|
|
1025
|
+
};
|
|
1026
|
+
//# sourceMappingURL=chunk-KZ2H5X4G.js.map
|