@iowarp/clio-coder 0.3.6 → 0.3.8
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 +59 -0
- package/README.md +22 -6
- package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
- package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
- package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
- package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
- package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
- package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
- package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
- package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
- package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
- package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
- package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
- package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/chunk-DR52UMZW.js +21 -0
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
- package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
- package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
- package/dist/chunk-FBVTI2TJ.js +518 -0
- package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
- package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
- package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
- package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
- package/dist/chunk-GWS3VEIW.js +195 -0
- package/dist/chunk-H7IXIC72.js +103 -0
- package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
- package/dist/chunk-J3YUBZWY.js +382 -0
- package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
- package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
- package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/chunk-ME6CCNFO.js +108 -0
- package/dist/chunk-MXKJU4JB.js +1100 -0
- package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
- package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/chunk-RVG5JXAL.js +41 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
- package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
- package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
- package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
- package/dist/chunk-TTHACPOM.js +961 -0
- package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
- package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
- package/dist/chunk-VAWNZU7Z.js +242 -0
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
- package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
- package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
- package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
- package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
- package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
- package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
- package/dist/cli/index.js +33 -31
- package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
- package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
- package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
- package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
- package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
- package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
- package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
- package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
- package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
- package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
- package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
- package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
- package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
- package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
- package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
- package/dist/fleet-commands-UVHWM76J.js +70 -0
- package/dist/fleet-graph-6ULH7PES.js +125 -0
- package/dist/fleet-new-RDVJLHHH.js +48 -0
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/fleet-validate-72PC4SLA.js +79 -0
- package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
- package/dist/library-CNTMPLRF.js +217 -0
- package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
- package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
- package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
- package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
- package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
- package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
- package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
- package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
- package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
- package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
- package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
- package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
- package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
- package/dist/verifiers-YAWOJ3H2.js +336 -0
- package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
- package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
- package/dist/worker/entry.js +67 -57
- 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 +11 -6
- package/docs/built-in-agents.md +26 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +83 -3
- package/docs/configuration-and-targets.md +86 -4
- 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 +74 -10
- 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 +228 -16
- package/docs/git-commit-provenance.md +2 -2
- package/docs/glossary.md +22 -2
- 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 +56 -9
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +83 -65
- 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 -2
- package/src/cli/argv.ts +5 -0
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor.ts +5 -1
- package/src/cli/evidence.ts +30 -25
- 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 +111 -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/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/usage.ts +111 -19
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +29 -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/core/path-boundary.ts +100 -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/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +304 -24
- package/src/domains/agents/index.ts +14 -0
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +360 -15
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/config/classify.ts +4 -0
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/active-route-planner.ts +14 -0
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- 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 +48 -0
- package/src/domains/dispatch/delegation-plan.ts +164 -0
- package/src/domains/dispatch/execution-plan.ts +76 -5
- package/src/domains/dispatch/execution-role.ts +12 -2
- package/src/domains/dispatch/execution-scheduler.ts +183 -67
- package/src/domains/dispatch/extension.ts +412 -74
- 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 +791 -0
- package/src/domains/dispatch/gate-role-prompts.ts +47 -0
- package/src/domains/dispatch/host-verification.ts +178 -0
- package/src/domains/dispatch/index.ts +41 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +235 -0
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +9 -4
- package/src/domains/dispatch/state.ts +36 -3
- package/src/domains/dispatch/types.ts +64 -12
- package/src/domains/dispatch/validation.ts +69 -6
- package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
- package/src/domains/dispatch/write-boundary.ts +201 -22
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/runners/clio-run.ts +12 -2
- package/src/domains/evidence/build.ts +69 -11
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +155 -18
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/discovery.ts +88 -1
- package/src/domains/extensions/resources.ts +20 -8
- package/src/domains/extensions/state.ts +6 -2
- package/src/domains/extensions/types.ts +4 -1
- package/src/domains/lifecycle/doctor.ts +140 -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/prompts/contract.ts +3 -5
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/index.ts +20 -0
- package/src/domains/resources/library.ts +326 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/resources/skills/marketplace.ts +37 -12
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +64 -1
- package/src/domains/safety/scope.ts +7 -12
- package/src/domains/session/handoff.ts +629 -0
- package/src/domains/share/archive.ts +67 -2
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- 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 +311 -17
- 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 +23 -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 +221 -30
- 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 +439 -7
- 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/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/watchdog-run.ts +75 -0
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-share.ts +56 -1
- package/src/interactive/worker-stream.ts +15 -0
- package/src/tools/bootstrap.ts +3 -0
- package/src/tools/compete-worktrees.ts +13 -79
- package/src/tools/dispatch-admission.ts +251 -18
- package/src/tools/dispatch-arguments.ts +84 -1
- package/src/tools/dispatch-plan.ts +165 -6
- package/src/tools/dispatch-runner.ts +364 -23
- package/src/tools/dispatch-types.ts +20 -1
- package/src/tools/dispatch.ts +72 -2
- package/src/tools/monitor.ts +29 -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 +21 -14
- package/src/worker/spec-contract.ts +3 -1
- package/dist/chunk-HC4CLZ2Y.js +0 -68
|
@@ -203,12 +203,62 @@ function ruleMatchesAnyPath(rule: ProjectRule, workingPaths: ReadonlyArray<strin
|
|
|
203
203
|
continue;
|
|
204
204
|
}
|
|
205
205
|
for (const path of workingPaths) {
|
|
206
|
-
|
|
206
|
+
const normalized = normalizeGlobInput(path);
|
|
207
|
+
if (regex.test(normalized)) return true;
|
|
208
|
+
if (normalized.endsWith("/") && globMatchesInsideDirectory(regex, pattern, normalized)) return true;
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
return false;
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
function materializedGlobVariants(pattern: string): string[] {
|
|
215
|
+
let variants = [""];
|
|
216
|
+
const normalized = normalizeGlobInput(pattern);
|
|
217
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
218
|
+
const character = normalized[index];
|
|
219
|
+
if (character === undefined) break;
|
|
220
|
+
if (character === "*" && normalized[index + 1] === "*" && normalized[index + 2] === "/") {
|
|
221
|
+
variants = variants.flatMap((prefix) => [prefix, `${prefix}x/`]).slice(0, 64);
|
|
222
|
+
index += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (character === "*" && normalized[index + 1] === "*") {
|
|
226
|
+
variants = variants.map((prefix) => `${prefix}x`);
|
|
227
|
+
index += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (character === "*" || character === "?") {
|
|
231
|
+
variants = variants.map((prefix) => `${prefix}x`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (character === "[") {
|
|
235
|
+
const end = normalized.indexOf("]", index + 1);
|
|
236
|
+
const selected = end === -1 ? "x" : (normalized[index + 1] ?? "x");
|
|
237
|
+
variants = variants.map((prefix) => `${prefix}${selected}`);
|
|
238
|
+
index = end === -1 ? index : end;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
variants = variants.map((prefix) => `${prefix}${character}`);
|
|
242
|
+
}
|
|
243
|
+
return variants;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function globMatchesInsideDirectory(regex: RegExp, pattern: string, directory: string): boolean {
|
|
247
|
+
const candidates = new Set<string>([`${directory}x`, `${directory}x/x`]);
|
|
248
|
+
for (const materialized of materializedGlobVariants(pattern)) {
|
|
249
|
+
if (materialized.startsWith(directory)) candidates.add(materialized);
|
|
250
|
+
const segments = materialized.split("/");
|
|
251
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
252
|
+
const suffix = segments.slice(index).join("/");
|
|
253
|
+
if (suffix.length > 0) candidates.add(`${directory}${suffix}`);
|
|
254
|
+
}
|
|
255
|
+
for (let index = 0; index < materialized.length; index += 1) {
|
|
256
|
+
candidates.add(`${directory}${materialized.slice(index)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return [...candidates].some((candidate) => regex.test(candidate));
|
|
260
|
+
}
|
|
261
|
+
|
|
212
262
|
/**
|
|
213
263
|
* Select the rules that should load now. Unconditional rules (no `paths`) always
|
|
214
264
|
* load; a path-scoped rule loads only when one of its globs matches a path
|
|
@@ -37,6 +37,12 @@ export function routeValidationProjection(
|
|
|
37
37
|
// Presentation-only parentage stamped by the calling tool, on the same
|
|
38
38
|
// terms: a model must not be able to author it.
|
|
39
39
|
parentToolCallId,
|
|
40
|
+
resolvedVerification,
|
|
41
|
+
resultContractOverride,
|
|
42
|
+
fleetGateReceipt,
|
|
43
|
+
taskWorktree,
|
|
44
|
+
runIdHint,
|
|
45
|
+
taskWorktreeDestination,
|
|
40
46
|
...raw
|
|
41
47
|
} = request;
|
|
42
48
|
const awaitingEnvelope =
|
|
@@ -54,6 +60,14 @@ export function routeValidationProjection(
|
|
|
54
60
|
...(assignmentDeadlineAt !== undefined ? { assignmentDeadlineAt } : {}),
|
|
55
61
|
...(ledger !== undefined ? { ledger } : {}),
|
|
56
62
|
...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
|
|
63
|
+
...(resolvedVerification !== undefined
|
|
64
|
+
? { resolvedVerification: resolvedVerification.map((check) => ({ ...check, argv: [...check.argv] })) }
|
|
65
|
+
: {}),
|
|
66
|
+
...(resultContractOverride !== undefined ? { resultContractOverride } : {}),
|
|
67
|
+
...(fleetGateReceipt !== undefined ? { fleetGateReceipt: { ...fleetGateReceipt } } : {}),
|
|
68
|
+
...(taskWorktree !== undefined ? { taskWorktree } : {}),
|
|
69
|
+
...(runIdHint !== undefined ? { runIdHint } : {}),
|
|
70
|
+
...(taskWorktreeDestination !== undefined ? { taskWorktreeDestination } : {}),
|
|
57
71
|
...(awaitingEnvelope ? { failover: "approved" as const } : {}),
|
|
58
72
|
}),
|
|
59
73
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Startup reconciliation for durable assignments. The in-memory retry queue and
|
|
3
|
-
* assignment registry do not survive a restart (Symphony §14.3), so
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* assignment registry do not survive a restart (Symphony §14.3), so a durable
|
|
4
|
+
* record whose process owner is gone is orphaned: no active attempt, no queued
|
|
5
|
+
* retry, and no attached terminal promise can ever settle it. Running records
|
|
6
|
+
* owned by a live sibling process are deliberately left alone.
|
|
6
7
|
*
|
|
7
8
|
* This pass resolves each orphan deterministically against durable ledger
|
|
8
9
|
* evidence so a later `wait`/`collect` can never observe a stuck `running`
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { AssignmentStatus } from "./assignment.js";
|
|
14
|
-
import type { DurableAssignmentRecord } from "./assignment-store.js";
|
|
15
|
+
import type { AssignmentVerdictOwner, DurableAssignmentRecord } from "./assignment-store.js";
|
|
15
16
|
|
|
16
17
|
/** Minimal terminal view of one attempt's durable ledger row. */
|
|
17
18
|
export interface ReconcileAttemptView {
|
|
@@ -25,13 +26,16 @@ export interface ReconcileAttemptView {
|
|
|
25
26
|
export interface AssignmentReconcileDeps {
|
|
26
27
|
/** Durable records currently marked `running`. */
|
|
27
28
|
listRunning: () => ReadonlyArray<DurableAssignmentRecord>;
|
|
29
|
+
/** Decide liveness from the record's durable pid and process birth token. */
|
|
30
|
+
ownerAlive: (record: DurableAssignmentRecord) => boolean;
|
|
28
31
|
/** Resolve one attempt's terminal view from the ledger, or null if pruned. */
|
|
29
32
|
lookupAttempt: (runId: string) => ReconcileAttemptView | null;
|
|
30
|
-
/** Persist the reconciled terminal status. */
|
|
33
|
+
/** Persist the reconciled terminal status, as the record's verdict owner when it has one. */
|
|
31
34
|
settle: (
|
|
32
35
|
assignmentId: string,
|
|
33
36
|
terminalRunId: string,
|
|
34
37
|
status: Exclude<AssignmentStatus, "running">,
|
|
38
|
+
owner?: AssignmentVerdictOwner,
|
|
35
39
|
) => Promise<unknown>;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -44,6 +48,10 @@ export interface AssignmentReconcileSummary {
|
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* Deterministic rule, in priority order:
|
|
51
|
+
* -1. a record owned by a live process is not an orphan and remains running;
|
|
52
|
+
* 0. a record whose verdict belongs to its opener is failed (abandoned): the
|
|
53
|
+
* opener died before reaching a verdict, and a green attempt under it is
|
|
54
|
+
* one step of a run, never the run's answer;
|
|
47
55
|
* 1. a recovered succeeded terminal attempt wins (`succeeded`);
|
|
48
56
|
* 2. otherwise the last terminal attempt marks the assignment `failed`;
|
|
49
57
|
* 3. otherwise no attempt is recoverable and the assignment is failed
|
|
@@ -53,6 +61,15 @@ export async function reconcileOrphanAssignments(deps: AssignmentReconcileDeps):
|
|
|
53
61
|
const summary: AssignmentReconcileSummary = { recovered: 0, abandoned: 0 };
|
|
54
62
|
for (const record of deps.listRunning()) {
|
|
55
63
|
if (record.status !== "running") continue;
|
|
64
|
+
if (deps.ownerAlive(record)) continue;
|
|
65
|
+
if (record.verdictOwner !== undefined) {
|
|
66
|
+
// Attempts under a claimed row are fleet steps, not the fleet verdict.
|
|
67
|
+
// Filing a green step as the terminal id of an abandoned fleet would
|
|
68
|
+
// contradict that step's immutable ledger result.
|
|
69
|
+
await deps.settle(record.assignmentId, record.assignmentId, "failed", record.verdictOwner);
|
|
70
|
+
summary.abandoned += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
56
73
|
const views = record.attempts
|
|
57
74
|
.map((runId) => deps.lookupAttempt(runId))
|
|
58
75
|
.filter((view): view is ReconcileAttemptView => view !== null);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
|
|
8
9
|
import { withStateFileLock } from "../../core/state-file-lock.js";
|
|
9
10
|
import { clioStateDir } from "../../core/xdg.js";
|
|
10
11
|
import { atomicWrite } from "../../engine/session.js";
|
|
@@ -12,11 +13,54 @@ import type { AssignmentStatus } from "./assignment.js";
|
|
|
12
13
|
|
|
13
14
|
const MAX_ASSIGNMENT_RECORDS = 1_000;
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Who decides a record's status.
|
|
18
|
+
*
|
|
19
|
+
* Absent is the ordinary case: the record is one logical dispatch, so the
|
|
20
|
+
* attempts filed under it are the only thing that can settle it. A fleet claims
|
|
21
|
+
* the verdict instead, because a fleet root gathers every step of a run under
|
|
22
|
+
* one id and no single step is the run's answer. Without the claim the last
|
|
23
|
+
* step to settle writes the row, which reports a green step of an aborted run
|
|
24
|
+
* as the run's outcome.
|
|
25
|
+
*/
|
|
26
|
+
export type AssignmentVerdictOwner = "fleet";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Durable identity of the process executing a running assignment. A pid alone
|
|
30
|
+
* is not enough: the OS may recycle it after a crash, so the process birth
|
|
31
|
+
* token must still match before another extension treats the owner as live.
|
|
32
|
+
*/
|
|
33
|
+
export interface AssignmentProcessOwner {
|
|
34
|
+
pid: number;
|
|
35
|
+
processBirthToken: string;
|
|
36
|
+
acquiredAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AssignmentProcessOwnerProbe {
|
|
40
|
+
birthToken(pid: number): string | null;
|
|
41
|
+
/** True when a missing birth token proves that the process is gone. */
|
|
42
|
+
tokenProvesDeath?: boolean;
|
|
43
|
+
/** Liveness fallback for platforms whose birth token is synthetic. */
|
|
44
|
+
alive?(pid: number): boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One durable logical-dispatch row.
|
|
49
|
+
*
|
|
50
|
+
* `attempts` holds terminal run ids: an agent attempt's receipt id, and for a
|
|
51
|
+
* fleet also each deterministic step's `code-*` run id, whose evidence lives
|
|
52
|
+
* under `code-steps/<assignmentId>/`. `status` answers for the whole record,
|
|
53
|
+
* so for a fleet it answers for the whole run rather than for its last step.
|
|
54
|
+
*/
|
|
15
55
|
export interface DurableAssignmentRecord {
|
|
16
56
|
assignmentId: string;
|
|
17
57
|
attempts: string[];
|
|
18
58
|
terminalRunId: string | null;
|
|
19
59
|
status: AssignmentStatus;
|
|
60
|
+
/** Set when something other than the attempts decides `status`. */
|
|
61
|
+
verdictOwner?: AssignmentVerdictOwner;
|
|
62
|
+
/** Process lease held only while this record is running. */
|
|
63
|
+
processOwner?: AssignmentProcessOwner;
|
|
20
64
|
}
|
|
21
65
|
|
|
22
66
|
interface AssignmentStoreFile {
|
|
@@ -28,6 +72,18 @@ function storePath(): string {
|
|
|
28
72
|
return join(clioStateDir(), "assignments.json");
|
|
29
73
|
}
|
|
30
74
|
|
|
75
|
+
function validProcessOwner(value: unknown): value is AssignmentProcessOwner {
|
|
76
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
77
|
+
const owner = value as Partial<AssignmentProcessOwner>;
|
|
78
|
+
return (
|
|
79
|
+
Number.isSafeInteger(owner.pid) &&
|
|
80
|
+
(owner.pid ?? 0) > 0 &&
|
|
81
|
+
typeof owner.processBirthToken === "string" &&
|
|
82
|
+
typeof owner.acquiredAt === "string" &&
|
|
83
|
+
Number.isFinite(Date.parse(owner.acquiredAt))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
31
87
|
function validRecord(value: unknown): value is DurableAssignmentRecord {
|
|
32
88
|
if (typeof value !== "object" || value === null) return false;
|
|
33
89
|
const record = value as Partial<DurableAssignmentRecord>;
|
|
@@ -36,6 +92,8 @@ function validRecord(value: unknown): value is DurableAssignmentRecord {
|
|
|
36
92
|
Array.isArray(record.attempts) &&
|
|
37
93
|
record.attempts.every((runId) => typeof runId === "string") &&
|
|
38
94
|
(record.terminalRunId === null || typeof record.terminalRunId === "string") &&
|
|
95
|
+
(record.verdictOwner === undefined || record.verdictOwner === "fleet") &&
|
|
96
|
+
(record.processOwner === undefined || validProcessOwner(record.processOwner)) &&
|
|
39
97
|
(record.status === "running" ||
|
|
40
98
|
record.status === "succeeded" ||
|
|
41
99
|
record.status === "failed" ||
|
|
@@ -57,7 +115,32 @@ function readStore(): DurableAssignmentRecord[] {
|
|
|
57
115
|
}
|
|
58
116
|
|
|
59
117
|
function copyRecord(record: DurableAssignmentRecord): DurableAssignmentRecord {
|
|
60
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
...record,
|
|
120
|
+
attempts: [...record.attempts],
|
|
121
|
+
...(record.processOwner === undefined ? {} : { processOwner: { ...record.processOwner } }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const defaultProcessOwnerProbe: AssignmentProcessOwnerProbe = {
|
|
126
|
+
birthToken: processBirthToken,
|
|
127
|
+
tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
|
|
128
|
+
alive: processAlive,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** True only when the durable identity still names the same live process. */
|
|
132
|
+
export function assignmentProcessOwnerAlive(
|
|
133
|
+
record: Pick<DurableAssignmentRecord, "processOwner">,
|
|
134
|
+
probe: AssignmentProcessOwnerProbe = defaultProcessOwnerProbe,
|
|
135
|
+
): boolean {
|
|
136
|
+
const owner = record.processOwner;
|
|
137
|
+
if (owner === undefined) return false;
|
|
138
|
+
const current = probe.birthToken(owner.pid);
|
|
139
|
+
if (current === null || current !== owner.processBirthToken) return false;
|
|
140
|
+
// A synthetic `pid-N` token cannot distinguish pid reuse, so require an
|
|
141
|
+
// independent liveness signal on platforms without a real birth-token source.
|
|
142
|
+
if (probe.tokenProvesDeath === false) return probe.alive?.(owner.pid) ?? false;
|
|
143
|
+
return true;
|
|
61
144
|
}
|
|
62
145
|
|
|
63
146
|
function writeStore(assignments: ReadonlyArray<DurableAssignmentRecord>): void {
|
|
@@ -114,49 +197,103 @@ export async function renameStoredAssignment(
|
|
|
114
197
|
return copyRecord(result);
|
|
115
198
|
}
|
|
116
199
|
|
|
117
|
-
|
|
118
|
-
|
|
200
|
+
function currentProcessOwner(): AssignmentProcessOwner {
|
|
201
|
+
const token = processBirthToken();
|
|
202
|
+
if (token === null) throw new Error("dispatch: cannot establish assignment owner process birth token");
|
|
203
|
+
return {
|
|
204
|
+
pid: process.pid,
|
|
205
|
+
processBirthToken: token,
|
|
206
|
+
acquiredAt: new Date().toISOString(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function openRecord(assignmentId: string): DurableAssignmentRecord {
|
|
211
|
+
return {
|
|
119
212
|
assignmentId,
|
|
120
|
-
|
|
121
|
-
|
|
213
|
+
attempts: [],
|
|
214
|
+
terminalRunId: null,
|
|
215
|
+
status: "running",
|
|
216
|
+
processOwner: currentProcessOwner(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ensureRunningProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
|
|
221
|
+
if (record.status !== "running" || record.processOwner !== undefined) return record;
|
|
222
|
+
return { ...record, processOwner: currentProcessOwner() };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function clearProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
|
|
226
|
+
const result = { ...record };
|
|
227
|
+
delete result.processOwner;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
|
|
232
|
+
return updateRecord(assignmentId, (current) => ensureRunningProcessOwner(current ?? openRecord(assignmentId)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Reserve this record's status for `owner`, opening the record if it is new.
|
|
237
|
+
* Attempts filed by anyone are still recorded; only the verdict is reserved,
|
|
238
|
+
* and until the owner settles it the record stays `running`.
|
|
239
|
+
*/
|
|
240
|
+
export function claimAssignmentVerdict(
|
|
241
|
+
assignmentId: string,
|
|
242
|
+
owner: AssignmentVerdictOwner,
|
|
243
|
+
): Promise<DurableAssignmentRecord> {
|
|
244
|
+
return updateRecord(assignmentId, (current) => ({
|
|
245
|
+
...ensureRunningProcessOwner(current ?? openRecord(assignmentId)),
|
|
246
|
+
verdictOwner: owner,
|
|
247
|
+
}));
|
|
122
248
|
}
|
|
123
249
|
|
|
124
250
|
export function recordAssignmentAttempt(assignmentId: string, runId: string): Promise<DurableAssignmentRecord> {
|
|
125
251
|
return updateRecord(assignmentId, (current) => {
|
|
126
|
-
const base = current ??
|
|
252
|
+
const base = ensureRunningProcessOwner(current ?? openRecord(assignmentId));
|
|
127
253
|
return base.attempts.includes(runId) ? base : { ...base, attempts: [...base.attempts, runId] };
|
|
128
254
|
});
|
|
129
255
|
}
|
|
130
256
|
|
|
131
257
|
export function failQueuedAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
|
|
132
258
|
return updateRecord(assignmentId, (current) => {
|
|
133
|
-
const base = current ??
|
|
134
|
-
|
|
259
|
+
const base = current ?? openRecord(assignmentId);
|
|
260
|
+
if (base.verdictOwner !== undefined) return base;
|
|
261
|
+
return base.status === "running" ? clearProcessOwner({ ...base, status: "failed" }) : base;
|
|
135
262
|
});
|
|
136
263
|
}
|
|
137
264
|
|
|
138
265
|
export function timeoutStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
|
|
139
266
|
return updateRecord(assignmentId, (current) => {
|
|
140
|
-
const base = current ??
|
|
141
|
-
|
|
267
|
+
const base = current ?? openRecord(assignmentId);
|
|
268
|
+
if (base.verdictOwner !== undefined) return base;
|
|
269
|
+
return base.status === "running" ? clearProcessOwner({ ...base, status: "timed_out" }) : base;
|
|
142
270
|
});
|
|
143
271
|
}
|
|
144
272
|
|
|
145
273
|
export function cancelStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
|
|
146
274
|
return updateRecord(assignmentId, (current) => {
|
|
147
|
-
const base = current ??
|
|
148
|
-
|
|
275
|
+
const base = current ?? openRecord(assignmentId);
|
|
276
|
+
if (base.verdictOwner !== undefined) return base;
|
|
277
|
+
return base.status === "running" ? clearProcessOwner({ ...base, status: "canceled" }) : base;
|
|
149
278
|
});
|
|
150
279
|
}
|
|
151
280
|
|
|
281
|
+
/**
|
|
282
|
+
* File `terminalRunId` as an attempt and, when the caller owns the verdict, set
|
|
283
|
+
* the record's status. An attempt settling under a claimed record settles
|
|
284
|
+
* itself and not the record: one fleet step reaching a green receipt says
|
|
285
|
+
* nothing about the six steps behind it that never ran.
|
|
286
|
+
*/
|
|
152
287
|
export function settleStoredAssignment(
|
|
153
288
|
assignmentId: string,
|
|
154
289
|
terminalRunId: string,
|
|
155
290
|
status: Exclude<AssignmentStatus, "running">,
|
|
291
|
+
owner?: AssignmentVerdictOwner,
|
|
156
292
|
): Promise<DurableAssignmentRecord> {
|
|
157
293
|
return updateRecord(assignmentId, (current) => {
|
|
158
|
-
const base = current ??
|
|
294
|
+
const base = current ?? openRecord(assignmentId);
|
|
159
295
|
const attempts = base.attempts.includes(terminalRunId) ? base.attempts : [...base.attempts, terminalRunId];
|
|
160
|
-
return { ...base, attempts
|
|
296
|
+
if (base.verdictOwner !== owner) return { ...base, attempts };
|
|
297
|
+
return clearProcessOwner({ ...base, attempts, terminalRunId, status });
|
|
161
298
|
});
|
|
162
299
|
}
|
|
@@ -54,6 +54,7 @@ export function isDeterministicOutcomeCode(code: RunOutcomeCode | null | undefin
|
|
|
54
54
|
code === "worker_tool_call_cap_exhausted" ||
|
|
55
55
|
code === "loop_guard_tools_disabled_exhausted" ||
|
|
56
56
|
code === "result_contract_exhausted" ||
|
|
57
|
-
code === "worker_final_output_missing"
|
|
57
|
+
code === "worker_final_output_missing" ||
|
|
58
|
+
code === "host_verification_rejected"
|
|
58
59
|
);
|
|
59
60
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
|
|
5
|
+
import { withStateFileLockSync } from "../../core/state-file-lock.js";
|
|
6
|
+
import { clioStateDir } from "../../core/xdg.js";
|
|
7
|
+
import { atomicWrite } from "../../engine/session.js";
|
|
8
|
+
|
|
9
|
+
export interface CheckoutWriterLeaseRecord {
|
|
10
|
+
version: 1;
|
|
11
|
+
checkout: string;
|
|
12
|
+
pid: number;
|
|
13
|
+
processBirthToken: string;
|
|
14
|
+
acquiredAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CheckoutWriterLease {
|
|
18
|
+
checkout: string;
|
|
19
|
+
path: string;
|
|
20
|
+
release(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CheckoutWriterLeaseProbe {
|
|
24
|
+
birthToken(pid: number): string | null;
|
|
25
|
+
tokenProvesDeath?: boolean;
|
|
26
|
+
alive?(pid: number): boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const held = new Map<string, { count: number; path: string }>();
|
|
30
|
+
let exitHooksInstalled = false;
|
|
31
|
+
|
|
32
|
+
function canonicalCheckout(checkout: string): string {
|
|
33
|
+
return realpathSync(checkout);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkoutWriterLeasePath(checkout: string): string {
|
|
37
|
+
const canonical = canonicalCheckout(checkout);
|
|
38
|
+
const key = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
39
|
+
return join(clioStateDir(), "checkout-writer-leases", `${key}.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validRecord(value: unknown): value is CheckoutWriterLeaseRecord {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
44
|
+
const record = value as Partial<CheckoutWriterLeaseRecord>;
|
|
45
|
+
return (
|
|
46
|
+
record.version === 1 &&
|
|
47
|
+
typeof record.checkout === "string" &&
|
|
48
|
+
Number.isSafeInteger(record.pid) &&
|
|
49
|
+
(record.pid ?? 0) > 0 &&
|
|
50
|
+
typeof record.processBirthToken === "string" &&
|
|
51
|
+
typeof record.acquiredAt === "string" &&
|
|
52
|
+
Number.isFinite(Date.parse(record.acquiredAt))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRecord(path: string): CheckoutWriterLeaseRecord | null {
|
|
57
|
+
if (!existsSync(path)) return null;
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
60
|
+
if (!validRecord(value)) throw new Error("invalid schema");
|
|
61
|
+
return value;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`checkout writer lease is unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ownerAlive(record: CheckoutWriterLeaseRecord, probe: CheckoutWriterLeaseProbe): boolean {
|
|
70
|
+
const current = probe.birthToken(record.pid);
|
|
71
|
+
if (current === null || current !== record.processBirthToken) return false;
|
|
72
|
+
if (probe.tokenProvesDeath === false) return probe.alive?.(record.pid) ?? false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultProbe: CheckoutWriterLeaseProbe = {
|
|
77
|
+
birthToken: processBirthToken,
|
|
78
|
+
tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
|
|
79
|
+
alive: processAlive,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function releaseAllOnExit(): void {
|
|
83
|
+
for (const [checkout, entry] of held) {
|
|
84
|
+
try {
|
|
85
|
+
withStateFileLockSync(entry.path, () => {
|
|
86
|
+
const record = readRecord(entry.path);
|
|
87
|
+
if (record?.checkout === checkout && record.pid === process.pid) unlinkSync(entry.path);
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// Process exit is best effort. A stale record is reclaimed by the next admission.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
held.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function installExitHooks(): void {
|
|
97
|
+
if (exitHooksInstalled) return;
|
|
98
|
+
exitHooksInstalled = true;
|
|
99
|
+
process.once("exit", releaseAllOnExit);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function acquireCheckoutWriterLease(input: {
|
|
103
|
+
checkout: string;
|
|
104
|
+
nowMs?: number;
|
|
105
|
+
pid?: number;
|
|
106
|
+
processBirthToken?: string;
|
|
107
|
+
probe?: CheckoutWriterLeaseProbe;
|
|
108
|
+
}): CheckoutWriterLease {
|
|
109
|
+
const checkout = canonicalCheckout(input.checkout);
|
|
110
|
+
const path = checkoutWriterLeasePath(checkout);
|
|
111
|
+
const local = held.get(checkout);
|
|
112
|
+
if (local !== undefined && (input.pid === undefined || input.pid === process.pid)) {
|
|
113
|
+
local.count += 1;
|
|
114
|
+
let released = false;
|
|
115
|
+
return {
|
|
116
|
+
checkout,
|
|
117
|
+
path,
|
|
118
|
+
release: () => {
|
|
119
|
+
if (released) return;
|
|
120
|
+
released = true;
|
|
121
|
+
releaseCheckoutWriterLease(checkout);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
126
|
+
const ownerPid = input.pid ?? process.pid;
|
|
127
|
+
const token = input.processBirthToken ?? processBirthToken(ownerPid);
|
|
128
|
+
if (token === null) throw new Error("dispatch: cannot establish checkout writer process birth token");
|
|
129
|
+
withStateFileLockSync(path, () => {
|
|
130
|
+
const existing = readRecord(path);
|
|
131
|
+
if (existing !== null && ownerAlive(existing, input.probe ?? defaultProbe)) {
|
|
132
|
+
throw new Error(`checkout_writer_lease_held: checkout writer lease is held by pid ${existing.pid}`);
|
|
133
|
+
}
|
|
134
|
+
const record: CheckoutWriterLeaseRecord = {
|
|
135
|
+
version: 1,
|
|
136
|
+
checkout,
|
|
137
|
+
pid: ownerPid,
|
|
138
|
+
processBirthToken: token,
|
|
139
|
+
acquiredAt: new Date(input.nowMs ?? Date.now()).toISOString(),
|
|
140
|
+
};
|
|
141
|
+
atomicWrite(path, JSON.stringify(record, null, 2));
|
|
142
|
+
});
|
|
143
|
+
if (ownerPid === process.pid) {
|
|
144
|
+
held.set(checkout, { count: 1, path });
|
|
145
|
+
installExitHooks();
|
|
146
|
+
}
|
|
147
|
+
let released = false;
|
|
148
|
+
return {
|
|
149
|
+
checkout,
|
|
150
|
+
path,
|
|
151
|
+
release: () => {
|
|
152
|
+
if (released) return;
|
|
153
|
+
released = true;
|
|
154
|
+
if (ownerPid === process.pid) releaseCheckoutWriterLease(checkout);
|
|
155
|
+
else releaseLeaseRecord(path, checkout, ownerPid, token);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function releaseLeaseRecord(path: string, checkout: string, pid: number, token: string): void {
|
|
161
|
+
withStateFileLockSync(path, () => {
|
|
162
|
+
const record = readRecord(path);
|
|
163
|
+
if (record?.checkout === checkout && record.pid === pid && record.processBirthToken === token) unlinkSync(path);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function releaseCheckoutWriterLease(checkout: string): void {
|
|
168
|
+
const entry = held.get(checkout);
|
|
169
|
+
if (entry === undefined) return;
|
|
170
|
+
entry.count -= 1;
|
|
171
|
+
if (entry.count > 0) return;
|
|
172
|
+
held.delete(checkout);
|
|
173
|
+
const token = processBirthToken(process.pid);
|
|
174
|
+
if (token !== null) releaseLeaseRecord(entry.path, checkout, process.pid, token);
|
|
175
|
+
}
|
|
@@ -15,6 +15,30 @@ import type { RunEnvelope, RunLineage, RunNodeIdentity, RunPhaseDurations, RunRe
|
|
|
15
15
|
import type { DispatchFailoverCandidate, JobSpec } from "./validation.js";
|
|
16
16
|
|
|
17
17
|
export interface DispatchRequest extends JobSpec {
|
|
18
|
+
/** Coordinator-owned result contract override for a fleet plan step. */
|
|
19
|
+
resultContractOverride?: ResultContract;
|
|
20
|
+
/** Gate path whose post-run hash is sealed into this step's receipt. */
|
|
21
|
+
fleetGateReceipt?: { path: string };
|
|
22
|
+
/** Orchestrator-prepared task worktree state. Model arguments cannot author it. */
|
|
23
|
+
taskWorktree?: {
|
|
24
|
+
root: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
path: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
base: string;
|
|
29
|
+
ownerToken: string;
|
|
30
|
+
};
|
|
31
|
+
/** Run identity allocated before task worktree creation. Model arguments cannot author it. */
|
|
32
|
+
runIdHint?: string;
|
|
33
|
+
/** Parent checkout frozen by plan approval for task worktree application. */
|
|
34
|
+
taskWorktreeDestination?: string;
|
|
35
|
+
/** Admission-resolved commands. Model text never enters these argv vectors. */
|
|
36
|
+
resolvedVerification?: ReadonlyArray<{
|
|
37
|
+
check: string;
|
|
38
|
+
argv: ReadonlyArray<string>;
|
|
39
|
+
cwd: string;
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
}>;
|
|
18
42
|
/**
|
|
19
43
|
* Semantic role this request's first attempt runs under. Derived once by
|
|
20
44
|
* `execution-role.ts` at request construction, never authored by a model, and
|
|
@@ -212,12 +236,36 @@ export interface DispatchContract {
|
|
|
212
236
|
finalPromise: Promise<ReadonlyArray<RunReceipt>>;
|
|
213
237
|
}>;
|
|
214
238
|
|
|
239
|
+
/** Seal a coordinator-only council synthesis receipt without invoking a model. */
|
|
240
|
+
sealCouncilSynthesis?(input: {
|
|
241
|
+
group: string;
|
|
242
|
+
round: number;
|
|
243
|
+
kind: "none" | "vote" | "judge";
|
|
244
|
+
text: string;
|
|
245
|
+
subjects: ReadonlyArray<{ runId: string; digest: string | null }>;
|
|
246
|
+
template: RunReceipt;
|
|
247
|
+
}): Promise<RunReceipt>;
|
|
248
|
+
|
|
215
249
|
/** List runs from the ledger. */
|
|
216
250
|
listRuns(status?: RunStatus): ReadonlyArray<RunEnvelope>;
|
|
217
251
|
|
|
218
252
|
/** Get a specific immutable attempt envelope. */
|
|
219
253
|
getRun(runId: string): RunEnvelope | null;
|
|
220
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Absolute paths this run's own successful tool calls aimed a mutation at,
|
|
257
|
+
* or null when the run kept no usable record: an unknown or evicted run, a
|
|
258
|
+
* runtime that publishes no tool telemetry, telemetry that came back
|
|
259
|
+
* incomplete, or a run that called a tool able to write a path its arguments
|
|
260
|
+
* do not name. Null is not an empty list, and a caller that cannot tell them
|
|
261
|
+
* apart will read "wrote nothing" off a run nobody watched.
|
|
262
|
+
*
|
|
263
|
+
* The write boundary reads this to separate what a step changed from what
|
|
264
|
+
* changed under it, so the answer must stay a record of observation rather
|
|
265
|
+
* than an inference from the checkout.
|
|
266
|
+
*/
|
|
267
|
+
observedRunWrites?(runId: string): ReadonlyArray<string> | null;
|
|
268
|
+
|
|
221
269
|
/** In-memory logical assignment state and complete finalized-attempt history. */
|
|
222
270
|
readonly assignments?: {
|
|
223
271
|
/** Live assignment, including its terminal promise and detailed attempt refs. */
|