@iowarp/clio-coder 0.3.7 → 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 +31 -0
- package/README.md +7 -2
- package/dist/{acp-SK4MD6MM.js → acp-U67UHUK2.js} +13 -13
- package/dist/{agents-2FN2K6ME.js → agents-YU6SGALZ.js} +35 -33
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-QIYZWM5I.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-YD734TPH.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-WJHBC77E.js → chunk-2HFZQUHL.js} +5 -5
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-EOOQZZDE.js → chunk-4SPRNWDE.js} +18 -18
- package/dist/{chunk-EBEFWSGL.js → chunk-5DHKRSMQ.js} +7 -7
- package/dist/{chunk-OBMAI2DP.js → chunk-5H3GB5BO.js} +11 -9
- package/dist/{chunk-JTSEDYVQ.js → chunk-5Q2VVUKB.js} +7 -7
- package/dist/{chunk-VQNODYQ4.js → chunk-7RFXX52T.js} +294 -3704
- package/dist/{chunk-XEGB6BCN.js → chunk-7RGZWPB6.js} +5 -5
- package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-CEYBNUGC.js → chunk-A3WNZD3P.js} +323 -27
- package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-KCMKRQX4.js → chunk-E77JEWSD.js} +42 -49
- package/dist/{chunk-J7PIKKWC.js → chunk-EMYUUSFG.js} +7 -7
- package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
- package/dist/{chunk-UVDSQ6LW.js → chunk-FBVTI2TJ.js} +74 -28
- package/dist/{chunk-BMWK7ZIZ.js → chunk-FHJEP5SW.js} +13 -13
- package/dist/{chunk-FOT2FX5J.js → chunk-GN57SG4G.js} +7 -7
- package/dist/{chunk-SROCI7ZU.js → chunk-GPIEI3LY.js} +5 -5
- package/dist/{chunk-UFQ3F4FW.js → chunk-GU2UIAFZ.js} +4 -4
- package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
- package/dist/{chunk-VREKEFLL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-LADCF22A.js → chunk-IGWKHNIQ.js} +113 -54
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-UHXRNZ2J.js → chunk-IJ7RPIYJ.js} +4 -4
- package/dist/{chunk-M4AKACEO.js → chunk-J3YUBZWY.js} +2 -2
- package/dist/{chunk-AB4XIIVB.js → chunk-JOZYP4GM.js} +6 -6
- package/dist/{chunk-ZGH7FGS5.js → chunk-K4XHGFR5.js} +742 -12
- package/dist/{chunk-EELBMBT6.js → chunk-KTYTFRMB.js} +62 -5
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/{chunk-DQA7QLMD.js → chunk-ME6CCNFO.js} +9 -24
- package/dist/{chunk-KZ2H5X4G.js → chunk-MXKJU4JB.js} +88 -14
- package/dist/{chunk-DMD2AGVS.js → chunk-N22QMJKY.js} +20 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-5C77SEEY.js → chunk-P43ETTHK.js} +2 -2
- package/dist/{chunk-JNXPYBB4.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-C4JBQ5SR.js → chunk-TANS5ZJS.js} +6 -6
- package/dist/{chunk-YTYFXUI3.js → chunk-TB5666IT.js} +9 -9
- package/dist/{chunk-UUANF5CR.js → chunk-TLQJPP24.js} +375 -687
- package/dist/{chunk-OB5HIGJY.js → chunk-TT36MB5S.js} +2 -1
- package/dist/{chunk-DJNLUABN.js → chunk-TTHACPOM.js} +135 -17
- package/dist/{chunk-6M7VS3J3.js → chunk-TYPGUK6W.js} +4 -4
- package/dist/{chunk-THKY7CD7.js → chunk-U6MBIEMB.js} +108 -24
- package/dist/{chunk-PD3MESLB.js → chunk-VAWNZU7Z.js} +4 -4
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-MXI6J5JF.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-QCTRSGHQ.js → chunk-WLFILSD5.js} +36 -36
- package/dist/{chunk-5UJ6ECTS.js → chunk-WNIJTQQK.js} +6 -6
- package/dist/{chunk-4DWFMQDR.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-3HAPLH5M.js → chunk-WWCZ5F23.js} +112 -10
- package/dist/{chunk-X2KV5FXT.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-DOOEX22V.js → chunk-XWSF374K.js} +4 -4
- package/dist/{chunk-6TUKSZVF.js → chunk-YS5VLNH5.js} +8 -8
- 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 +31 -31
- package/dist/{clio-WBVQEBKO.js → clio-QVTYJ57A.js} +8 -8
- package/dist/code-nav-FGGFIE7L.js +2 -2
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
- package/dist/{config-TRBL3RCF.js → config-LW5IJFQN.js} +58 -53
- package/dist/{configure-OLCVPHNM.js → configure-7XIZCOU4.js} +26 -21
- package/dist/{context-XEWE3MOJ.js → context-L3WL3X7K.js} +47 -41
- package/dist/{context-WFPKQSM6.js → context-N52ZA626.js} +22 -22
- package/dist/{context-MJIJ6GOX.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-KNOS2JPB.js → context-clear-MBQRLSDQ.js} +47 -41
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-EUXAZI6N.js → context-working-set-GS6DSO7F.js} +14 -14
- package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-22ZCNOM3.js} +83 -55
- package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
- package/dist/{doctor-RN4YKO2X.js → doctor-M7YEDGAE.js} +24 -20
- package/dist/{eval-RUBJVSNQ.js → eval-BEC2WHDA.js} +71 -19
- package/dist/{evidence-JZNBUOQZ.js → evidence-REJUMSKM.js} +62 -56
- package/dist/{evolve-FJVC4KKI.js → evolve-PY5ZBA5K.js} +41 -35
- package/dist/{extensions-IQL36S7K.js → extensions-HVKU65YU.js} +6 -6
- package/dist/{fleet-BDKYJFCP.js → fleet-7WZEWRFA.js} +72 -67
- package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-UVHWM76J.js} +10 -10
- package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-6ULH7PES.js} +13 -13
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-72PC4SLA.js} +17 -17
- package/dist/{init-LQUB5COQ.js → init-OG3TPGQG.js} +61 -56
- package/dist/{library-NJAHIGG4.js → library-CNTMPLRF.js} +18 -18
- package/dist/{memory-OG6HOYKM.js → memory-6IS7F275.js} +43 -37
- package/dist/{models-5ZG5XY7J.js → models-ENRJDA5W.js} +33 -28
- package/dist/{monitor-TJ7AMTGB.js → monitor-XLDVO7TN.js} +56 -38
- package/dist/{orchestrator-WZYB54DM.js → orchestrator-6KSPYRHA.js} +421 -303
- package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-PXQT45IY.js → reset-RZ4ER727.js} +10 -10
- package/dist/{run-FQ74YF62.js → run-Y2CNK5RU.js} +79 -74
- package/dist/{share-FW7SVCL3.js → share-A55GYP6Z.js} +16 -16
- package/dist/{skills-7E7IRB3R.js → skills-ALC5J6AT.js} +19 -19
- package/dist/{skills-eval-LI75W6OK.js → skills-eval-JPBEBYQU.js} +52 -47
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-4CIFKCTW.js → targets-VGNXIR3S.js} +42 -33
- package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-WOBR64YA.js} +3 -3
- package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
- package/dist/{upgrade-K2HVIVMQ.js → upgrade-FUSUAGHR.js} +27 -25
- package/dist/{usage-GTZELZQX.js → usage-N4MKVHKD.js} +51 -46
- package/dist/{verifiers-RLAHT27O.js → verifiers-YAWOJ3H2.js} +13 -13
- package/dist/{verify-BX3BRKH5.js → verify-LTDHYBGY.js} +9 -9
- package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-6M7GHTBJ.js} +66 -61
- package/dist/worker/entry.js +68 -61
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +6 -5
- package/docs/built-in-agents.md +1 -1
- package/docs/commands-and-modes.md +2 -2
- package/docs/configuration-and-targets.md +8 -3
- package/docs/context-engine.md +1 -1
- package/docs/documentation-guide.md +1 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +70 -6
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +1 -1
- package/docs/fleet-dispatch.md +34 -9
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +1 -1
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +2 -2
- 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 +41 -38
- package/docs/safety-model.md +1 -1
- package/docs/scientific-validation.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +1 -1
- package/docs/trace-store.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +1 -1
- 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-preflight.ts +2 -12
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +25 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +30 -12
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +128 -17
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/contract.ts +15 -1
- package/src/domains/dispatch/delegation-plan.ts +2 -5
- package/src/domains/dispatch/execution-role.ts +9 -1
- package/src/domains/dispatch/extension.ts +156 -67
- package/src/domains/dispatch/fleet-run.ts +57 -3
- package/src/domains/dispatch/gate-role-prompts.ts +38 -0
- package/src/domains/dispatch/index.ts +3 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +84 -8
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -7
- package/src/domains/dispatch/validation.ts +6 -3
- 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 +145 -17
- 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/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/prompts/loader.ts +119 -14
- 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/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/interactive/dispatch-board.ts +53 -3
- package/src/interactive/interactive-event-projection.ts +14 -0
- package/src/interactive/overlays/settings.ts +141 -47
- package/src/interactive/slash-commands.ts +7 -2
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-stream.ts +8 -0
- package/src/tools/dispatch-admission.ts +12 -13
- package/src/tools/dispatch-arguments.ts +27 -0
- package/src/tools/dispatch-plan.ts +29 -0
- package/src/tools/dispatch-runner.ts +48 -13
- package/src/tools/monitor.ts +13 -0
- package/src/tools/worker-evidence.ts +19 -13
- package/src/worker/spec-contract.ts +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const PATH_BOUNDARY_MAX_ENTRIES = 32;
|
|
4
|
+
|
|
5
|
+
export type PathBoundary = ReadonlyArray<string>;
|
|
6
|
+
|
|
7
|
+
type PathBoundaryErrorCode = "empty" | "separator" | "glob" | "absolute" | "parent" | "dot";
|
|
8
|
+
|
|
9
|
+
export class PathBoundaryError extends Error {
|
|
10
|
+
readonly code: PathBoundaryErrorCode;
|
|
11
|
+
|
|
12
|
+
constructor(code: PathBoundaryErrorCode, message: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "PathBoundaryError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const GLOB_CHARACTERS = /[*?[\]{}]/u;
|
|
20
|
+
|
|
21
|
+
/** Normalize one repository-relative boundary entry. */
|
|
22
|
+
export function normalizePathBoundaryEntry(entry: string): string {
|
|
23
|
+
const raw = entry.trim();
|
|
24
|
+
if (raw.length === 0) throw new PathBoundaryError("empty", "write boundary: entry must be a non-empty path");
|
|
25
|
+
if (raw.includes("\\")) {
|
|
26
|
+
throw new PathBoundaryError("separator", `write boundary: entry '${entry}' must use '/' separators`);
|
|
27
|
+
}
|
|
28
|
+
if (GLOB_CHARACTERS.test(raw)) {
|
|
29
|
+
throw new PathBoundaryError("glob", `write boundary: entry '${entry}' looks like a glob, which v1 does not support`);
|
|
30
|
+
}
|
|
31
|
+
if (raw.startsWith("/") || /^[A-Za-z]:/u.test(raw)) {
|
|
32
|
+
throw new PathBoundaryError("absolute", `write boundary: entry '${entry}' must be repository-relative, not absolute`);
|
|
33
|
+
}
|
|
34
|
+
const subtree = raw.endsWith("/");
|
|
35
|
+
const segments = raw.split("/").filter((segment) => segment.length > 0);
|
|
36
|
+
if (segments.length === 0) {
|
|
37
|
+
throw new PathBoundaryError("empty", `write boundary: entry '${entry}' must name a path inside the repository`);
|
|
38
|
+
}
|
|
39
|
+
for (const segment of segments) {
|
|
40
|
+
if (segment === "..") {
|
|
41
|
+
throw new PathBoundaryError("parent", `write boundary: entry '${entry}' must not contain '..'`);
|
|
42
|
+
}
|
|
43
|
+
if (segment === ".") {
|
|
44
|
+
throw new PathBoundaryError("dot", `write boundary: entry '${entry}' must not contain '.' segments`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const joined = segments.join("/");
|
|
48
|
+
return subtree ? `${joined}/` : joined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Normalize, deduplicate, and sort a boundary declaration. */
|
|
52
|
+
export function normalizePathBoundary(entries: ReadonlyArray<string>): string[] {
|
|
53
|
+
if (entries.length > PATH_BOUNDARY_MAX_ENTRIES) {
|
|
54
|
+
throw new Error(`write boundary: at most ${PATH_BOUNDARY_MAX_ENTRIES} entries may be declared`);
|
|
55
|
+
}
|
|
56
|
+
return [...new Set(entries.map(normalizePathBoundaryEntry))].sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toPosixBoundaryPath(value: string): string {
|
|
60
|
+
return value.split("\\").join("/");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isDirectoryPathBoundary(entry: string): boolean {
|
|
64
|
+
return toPosixBoundaryPath(entry).endsWith("/");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Convert a path to the subtree form spoken by the boundary grammar. */
|
|
68
|
+
export function asDirectoryPathBoundary(entry: string): string {
|
|
69
|
+
const normalized = toPosixBoundaryPath(entry);
|
|
70
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve a repository-relative boundary while retaining exact or subtree meaning. */
|
|
74
|
+
export function resolvePathBoundary(cwd: string, entry: string): string {
|
|
75
|
+
const directory = isDirectoryPathBoundary(entry);
|
|
76
|
+
const withoutMarker = directory ? entry.slice(0, -1) : entry;
|
|
77
|
+
const resolved = toPosixBoundaryPath(path.resolve(cwd, withoutMarker));
|
|
78
|
+
return directory ? asDirectoryPathBoundary(resolved) : resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether one boundary entry contains another exact path or subtree. */
|
|
82
|
+
export function pathBoundaryEntryCovers(outer: string, inner: string): boolean {
|
|
83
|
+
const normalizedOuter = toPosixBoundaryPath(outer);
|
|
84
|
+
const normalizedInner = toPosixBoundaryPath(inner);
|
|
85
|
+
return normalizedOuter.endsWith("/")
|
|
86
|
+
? normalizedInner.startsWith(normalizedOuter)
|
|
87
|
+
: !normalizedInner.endsWith("/") && normalizedInner === normalizedOuter;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether any entry in a boundary contains the candidate. */
|
|
91
|
+
export function pathBoundaryCovers(boundary: PathBoundary, candidate: string): boolean {
|
|
92
|
+
return boundary.some((entry) => pathBoundaryEntryCovers(entry, candidate));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether two boundary declarations share any possible path. */
|
|
96
|
+
export function pathBoundariesOverlap(left: PathBoundary, right: PathBoundary): boolean {
|
|
97
|
+
return left.some((entry) =>
|
|
98
|
+
right.some((other) => pathBoundaryEntryCovers(entry, other) || pathBoundaryEntryCovers(other, entry)),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import type { DomainBundle, DomainContext, DomainExtension } from "../../core/domain-loader.js";
|
|
3
|
-
import { resolvePackageRoot } from "../../core/package-root.js";
|
|
4
|
-
import { clioConfigDir } from "../../core/xdg.js";
|
|
5
2
|
import { assertAgentIdNamespace } from "../config/agent-namespace.js";
|
|
6
3
|
import type { ConfigContract } from "../config/contract.js";
|
|
7
4
|
import type { AgentsContract } from "./contract.js";
|
|
8
5
|
import type { AgentRecipe } from "./recipe.js";
|
|
9
|
-
import { type AgentRecipeDiagnostic,
|
|
6
|
+
import { type AgentRecipeDiagnostic, discoverAgentRecipes } from "./registry.js";
|
|
10
7
|
import { type AgentSpec, normalizeAgentSpec } from "./spec.js";
|
|
11
8
|
|
|
12
9
|
export function createAgentsBundle(_context: DomainContext): DomainBundle<AgentsContract> {
|
|
@@ -15,14 +12,8 @@ export function createAgentsBundle(_context: DomainContext): DomainBundle<Agents
|
|
|
15
12
|
let diagnostics: ReadonlyArray<AgentRecipeDiagnostic> = [];
|
|
16
13
|
|
|
17
14
|
function discover(): void {
|
|
18
|
-
const builtinDir = path.join(resolvePackageRoot(), "src", "domains", "agents", "builtins");
|
|
19
|
-
const userDir = path.join(clioConfigDir(), "agents");
|
|
20
|
-
const projectDir = path.join(process.cwd(), ".clio-coder", "agents");
|
|
21
15
|
const nextDiagnostics: AgentRecipeDiagnostic[] = [];
|
|
22
|
-
const
|
|
23
|
-
const user = loadRecipesFromDir({ dir: userDir, source: "user" }, nextDiagnostics);
|
|
24
|
-
const project = loadRecipesFromDir({ dir: projectDir, source: "project" }, nextDiagnostics);
|
|
25
|
-
const merged = mergeRecipes(builtin, user, project);
|
|
16
|
+
const merged = discoverAgentRecipes(process.cwd(), nextDiagnostics);
|
|
26
17
|
const config = _context.getContract<ConfigContract>("config");
|
|
27
18
|
assertAgentIdNamespace(merged, config?.get()?.delegation?.agents ?? []);
|
|
28
19
|
recipes = merged;
|
|
@@ -16,6 +16,7 @@ import { Type } from "typebox";
|
|
|
16
16
|
import { Value } from "typebox/value";
|
|
17
17
|
import { resolvePackageRoot } from "../../core/package-root.js";
|
|
18
18
|
import { clioConfigDir } from "../../core/xdg.js";
|
|
19
|
+
import { enabledExtensionResourceRoots } from "../extensions/index.js";
|
|
19
20
|
import { type FleetCommandRegistry, loadFleetCommands } from "./fleet-commands.js";
|
|
20
21
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
21
22
|
import { normalizeWriteBoundary, WRITE_BOUNDARY_MAX_ENTRIES } from "./write-boundary.js";
|
|
@@ -198,7 +199,7 @@ export interface FleetContract {
|
|
|
198
199
|
path: string;
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
export type FleetContractSource = "builtin" | "user" | "project";
|
|
202
|
+
export type FleetContractSource = "builtin" | "extension" | "user" | "project";
|
|
202
203
|
|
|
203
204
|
export interface FleetContractListing {
|
|
204
205
|
name: string;
|
|
@@ -758,6 +759,7 @@ export function parseFleetContract(raw: string, sourcePath: string): FleetContra
|
|
|
758
759
|
}
|
|
759
760
|
if (version < FLEET_WRITE_BOUNDARY_VERSION) assertNoWritesBefore(frontmatter, sourcePath, version);
|
|
760
761
|
if (version < FLEET_DYNAMIC_STEP_VERSION) assertNoV5FieldsBefore(frontmatter, sourcePath, version);
|
|
762
|
+
if (version >= FLEET_DYNAMIC_STEP_VERSION) assertNoGateWrites(frontmatter, sourcePath);
|
|
761
763
|
const schemaError = firstSchemaError(frontmatter, version);
|
|
762
764
|
if (schemaError !== null) {
|
|
763
765
|
throw new Error(`fleet contract ${sourcePath}: ${schemaError}`);
|
|
@@ -849,6 +851,17 @@ function assertNoWritesBefore(frontmatter: Record<string, unknown>, sourcePath:
|
|
|
849
851
|
}
|
|
850
852
|
}
|
|
851
853
|
|
|
854
|
+
function assertNoGateWrites(frontmatter: Record<string, unknown>, sourcePath: string): void {
|
|
855
|
+
for (const value of Array.isArray(frontmatter.steps) ? frontmatter.steps : []) {
|
|
856
|
+
if (typeof value !== "object" || value === null) continue;
|
|
857
|
+
const step = value as Record<string, unknown>;
|
|
858
|
+
if (step.kind !== "gate" || typeof step.id !== "string" || step.id.length === 0 || !("writes" in step)) continue;
|
|
859
|
+
throw new Error(
|
|
860
|
+
`fleet contract ${sourcePath}: gate step '${step.id}' must not declare 'writes'; its write boundary is derived from 'path'`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
852
865
|
function assertNoV5FieldsBefore(frontmatter: Record<string, unknown>, sourcePath: string, version: number): void {
|
|
853
866
|
const forbidden = ["target", "profile"] as const;
|
|
854
867
|
if ("writers" in frontmatter) {
|
|
@@ -958,13 +971,22 @@ function builtinFleetsDir(): string {
|
|
|
958
971
|
return join(resolvePackageRoot(), "src", "domains", "agents", "fleets");
|
|
959
972
|
}
|
|
960
973
|
|
|
974
|
+
function fleetSources(cwd: string): ReadonlyArray<{ dir: string; source: FleetContractSource }> {
|
|
975
|
+
return [
|
|
976
|
+
{ dir: builtinFleetsDir(), source: "builtin" },
|
|
977
|
+
...enabledExtensionResourceRoots("fleets", cwd)
|
|
978
|
+
.sort((left, right) => left.source.localeCompare(right.source))
|
|
979
|
+
.map((root) => ({ dir: root.path, source: "extension" as const })),
|
|
980
|
+
{ dir: join(clioConfigDir(), "fleets"), source: "user" },
|
|
981
|
+
{ dir: fleetsDir(cwd), source: "project" },
|
|
982
|
+
];
|
|
983
|
+
}
|
|
984
|
+
|
|
961
985
|
function fleetContractPath(cwd: string, name: string): { path: string; source: FleetContractSource } | null {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const builtinPath = join(builtinFleetsDir(), `${name}.md`);
|
|
967
|
-
if (existsSync(builtinPath)) return { path: builtinPath, source: "builtin" };
|
|
986
|
+
for (const source of [...fleetSources(cwd)].reverse()) {
|
|
987
|
+
const candidate = join(source.dir, `${name}.md`);
|
|
988
|
+
if (existsSync(candidate)) return { path: candidate, source: source.source };
|
|
989
|
+
}
|
|
968
990
|
return null;
|
|
969
991
|
}
|
|
970
992
|
|
|
@@ -1004,11 +1026,7 @@ export function listFleetContracts(cwd: string): FleetContractListing[] {
|
|
|
1004
1026
|
registryError = err instanceof Error ? err.message : String(err);
|
|
1005
1027
|
}
|
|
1006
1028
|
const listings = new Map<string, FleetContractListing>();
|
|
1007
|
-
const sources
|
|
1008
|
-
{ dir: builtinFleetsDir(), source: "builtin" },
|
|
1009
|
-
{ dir: join(clioConfigDir(), "fleets"), source: "user" },
|
|
1010
|
-
{ dir: fleetsDir(cwd), source: "project" },
|
|
1011
|
-
];
|
|
1029
|
+
const sources = fleetSources(cwd);
|
|
1012
1030
|
for (const { dir, source } of sources) {
|
|
1013
1031
|
for (const file of listDirectory(dir)) {
|
|
1014
1032
|
const path = join(dir, file);
|
|
@@ -131,7 +131,7 @@ export interface AgentRecipe {
|
|
|
131
131
|
resultContract: ResultContract;
|
|
132
132
|
product?: AgentProduct;
|
|
133
133
|
tags: ReadonlyArray<string>;
|
|
134
|
-
source: "builtin" | "user" | "project";
|
|
134
|
+
source: "builtin" | "extension" | "user" | "project";
|
|
135
135
|
filepath: string;
|
|
136
136
|
/** Persona prompt body. */
|
|
137
137
|
body: string;
|
|
@@ -140,6 +140,12 @@ export interface AgentRecipe {
|
|
|
140
140
|
export interface RecipeSource {
|
|
141
141
|
dir: string;
|
|
142
142
|
source: AgentRecipe["source"];
|
|
143
|
+
/** Project root whose extension and skill resources are being discovered. */
|
|
144
|
+
cwd?: string;
|
|
145
|
+
/** Stable provenance used to correlate resources from one extension. */
|
|
146
|
+
origin?: string;
|
|
147
|
+
/** The declaring extension's skill root; extension agents may bind only here. */
|
|
148
|
+
skillRoot?: string;
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
export function recipeIdFromPath(absPath: string, rootDir: string): string {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolvePackageRoot } from "../../core/package-root.js";
|
|
4
|
+
import { clioConfigDir } from "../../core/xdg.js";
|
|
5
|
+
import { enabledExtensionResourceRoots } from "../extensions/index.js";
|
|
3
6
|
import { loadSkills } from "../resources/skills/loader.js";
|
|
4
7
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
5
8
|
import { type AgentRecipe, type RecipeSource, recipeIdFromPath } from "./recipe.js";
|
|
@@ -20,10 +23,29 @@ function resolveBoundSkills(recipe: AgentRecipe, source: RecipeSource): AgentRec
|
|
|
20
23
|
// only the operator's discovered skill roots; a recipe cannot smuggle an
|
|
21
24
|
// arbitrary filesystem path into worker context through a skill name.
|
|
22
25
|
const packageSkills = path.resolve(source.dir, "..", "..", "..", "..", "skills");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (source.source === "extension" && source.skillRoot === undefined) {
|
|
27
|
+
throw new Error(`agent recipe: ${recipe.filepath}: extension declares bound skills but no skills resource root`);
|
|
28
|
+
}
|
|
29
|
+
const skills =
|
|
30
|
+
source.source === "extension"
|
|
31
|
+
? loadSkills({
|
|
32
|
+
cwd: source.cwd ?? process.cwd(),
|
|
33
|
+
disableDiscovery: true,
|
|
34
|
+
explicitSkillPaths: source.skillRoot === undefined ? [] : [source.skillRoot],
|
|
35
|
+
})
|
|
36
|
+
: loadSkills({
|
|
37
|
+
cwd: source.cwd ?? process.cwd(),
|
|
38
|
+
...(source.source === "builtin" && existsSync(packageSkills) ? { explicitSkillPaths: [packageSkills] } : {}),
|
|
39
|
+
});
|
|
40
|
+
if (source.source === "extension" && source.skillRoot !== undefined) {
|
|
41
|
+
const canonicalRoot = realpathSync(source.skillRoot);
|
|
42
|
+
for (const skill of skills.items) {
|
|
43
|
+
const relative = path.relative(canonicalRoot, realpathSync(skill.filePath));
|
|
44
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
45
|
+
throw new Error(`agent recipe: ${recipe.filepath}: bound skill escapes its extension: ${skill.filePath}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
const byName = new Map(skills.items.map((skill) => [skill.name, skill.filePath]));
|
|
28
50
|
const missing = recipe.skills.filter((skill) => !byName.has(skill));
|
|
29
51
|
if (missing.length > 0) {
|
|
@@ -95,6 +117,10 @@ export function mergeRecipes(...sources: ReadonlyArray<ReadonlyArray<AgentRecipe
|
|
|
95
117
|
continue;
|
|
96
118
|
}
|
|
97
119
|
const builtin = builtinById.get(recipe.id);
|
|
120
|
+
if (recipe.source === "extension" && builtin) {
|
|
121
|
+
process.stderr.write(`[clio:agents] ignore override id=${recipe.id} by=extension reason=reserved-builtin\n`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
98
124
|
if (recipe.source === "user" && builtin && isShadowAgent(normalizeAgentSpec(builtin))) {
|
|
99
125
|
process.stderr.write(`[clio:agents] ignore override id=${recipe.id} by=user reason=reserved-shadow\n`);
|
|
100
126
|
continue;
|
|
@@ -109,3 +135,45 @@ export function mergeRecipes(...sources: ReadonlyArray<ReadonlyArray<AgentRecipe
|
|
|
109
135
|
}
|
|
110
136
|
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
111
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Discover the one native-agent catalog used by listing, fleet admission, and
|
|
141
|
+
* dispatch. Extension recipes occupy the same precedence slot as extension
|
|
142
|
+
* fleets: after builtins and before operator/user and project recipes. Their
|
|
143
|
+
* skill bindings remain constrained to the declaring extension's skill root.
|
|
144
|
+
*/
|
|
145
|
+
export function discoverAgentRecipes(
|
|
146
|
+
cwd = process.cwd(),
|
|
147
|
+
diagnostics: AgentRecipeDiagnostic[] = [],
|
|
148
|
+
): ReadonlyArray<AgentRecipe> {
|
|
149
|
+
const builtin = loadRecipesFromDir(
|
|
150
|
+
{
|
|
151
|
+
dir: path.join(resolvePackageRoot(), "src", "domains", "agents", "builtins"),
|
|
152
|
+
source: "builtin",
|
|
153
|
+
cwd,
|
|
154
|
+
},
|
|
155
|
+
diagnostics,
|
|
156
|
+
);
|
|
157
|
+
const skillRoots = new Map(enabledExtensionResourceRoots("skills", cwd).map((root) => [root.source, root.path]));
|
|
158
|
+
const extensionRecipes = enabledExtensionResourceRoots("agents", cwd)
|
|
159
|
+
.sort((left, right) => left.source.localeCompare(right.source))
|
|
160
|
+
.map((root) => {
|
|
161
|
+
const skillRoot = skillRoots.get(root.source);
|
|
162
|
+
return loadRecipesFromDir(
|
|
163
|
+
{
|
|
164
|
+
dir: root.path,
|
|
165
|
+
source: "extension",
|
|
166
|
+
cwd,
|
|
167
|
+
origin: root.source,
|
|
168
|
+
...(skillRoot === undefined ? {} : { skillRoot }),
|
|
169
|
+
},
|
|
170
|
+
diagnostics,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
const user = loadRecipesFromDir({ dir: path.join(clioConfigDir(), "agents"), source: "user", cwd }, diagnostics);
|
|
174
|
+
const project = loadRecipesFromDir(
|
|
175
|
+
{ dir: path.join(cwd, ".clio-coder", "agents"), source: "project", cwd },
|
|
176
|
+
diagnostics,
|
|
177
|
+
);
|
|
178
|
+
return mergeRecipes(builtin, ...extensionRecipes, user, project);
|
|
179
|
+
}
|
|
@@ -8,6 +8,27 @@ export type ResultContract =
|
|
|
8
8
|
| { kind: "scout-report" }
|
|
9
9
|
| { kind: "verifier-report" }
|
|
10
10
|
| { kind: "council-report" }
|
|
11
|
+
/**
|
|
12
|
+
* One council member's ballot under `synthesis: "vote"`.
|
|
13
|
+
*
|
|
14
|
+
* The vote is a strict majority over the members' `verdict` fields, computed
|
|
15
|
+
* by the coordinator with no model call. Nothing asked a member for that
|
|
16
|
+
* field before this kind existed, and nothing could: an unnamed council
|
|
17
|
+
* seats the builtin `researcher`, whose `research-report` accepts only
|
|
18
|
+
* `source` and `findings`, so a member that emitted a verdict failed its own
|
|
19
|
+
* postcondition and a member that obeyed its postcondition emitted no
|
|
20
|
+
* verdict. Every vote therefore resolved to `no_verdict_field` with an empty
|
|
21
|
+
* tally, on every input (#230).
|
|
22
|
+
*
|
|
23
|
+
* The ballot is the coordinator's postcondition for that slot, not the
|
|
24
|
+
* recipe's, in the same way `reviewer` and the compete `judge` answer the
|
|
25
|
+
* gate that dispatched them. It rides in as a `resultContractOverride` on
|
|
26
|
+
* the member request, so the seated recipe keeps its persona and any recipe
|
|
27
|
+
* can be voted with. No recipe may declare it: `parseResultContract` rejects
|
|
28
|
+
* the kind, and a Scout subtask may not request it, because it is a
|
|
29
|
+
* topology's ballot rather than an agent's work product.
|
|
30
|
+
*/
|
|
31
|
+
| { kind: "council-ballot" }
|
|
11
32
|
| { kind: "debugger-report" }
|
|
12
33
|
| { kind: "research-report" }
|
|
13
34
|
| { kind: "mutation-report" }
|
|
@@ -273,6 +294,67 @@ export function parseDelegationPlanResult(output: string | null): DelegationPlan
|
|
|
273
294
|
return { tasks };
|
|
274
295
|
}
|
|
275
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Longest verdict token a ballot may cast, in bytes.
|
|
299
|
+
*
|
|
300
|
+
* A tally groups members by their exact verdict string, so a verdict that is a
|
|
301
|
+
* sentence can only ever tie with itself and a council of any size reports
|
|
302
|
+
* `no_majority`. The bound is what makes a majority reachable: it forces the
|
|
303
|
+
* decision into a token members can match (`yes`, `no`, `keep`, an option
|
|
304
|
+
* name) and pushes the reasoning into `text`, where the report already reads
|
|
305
|
+
* it. A newline is refused for the same reason.
|
|
306
|
+
*/
|
|
307
|
+
export const COUNCIL_BALLOT_VERDICT_MAX_BYTES = 64;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The ballot's wire shape, quoted verbatim to the member that must cast it and
|
|
311
|
+
* by the repair round that corrects it. Exported because the council composes
|
|
312
|
+
* the member's ask from it, which is the rule this module already states: a
|
|
313
|
+
* contract's example is written once so a prompt cannot drift from its
|
|
314
|
+
* validator.
|
|
315
|
+
*/
|
|
316
|
+
export const COUNCIL_BALLOT_SHAPE = '{"verdict":"yes","text":"the answer and the evidence behind it"}';
|
|
317
|
+
|
|
318
|
+
interface CouncilBallot {
|
|
319
|
+
/** The comparable token the tally counts. */
|
|
320
|
+
verdict: string;
|
|
321
|
+
/** The member's answer in prose, which is what the council report shows. */
|
|
322
|
+
text: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Read a council member's ballot. The council reads a verdict out of the
|
|
327
|
+
* member's terminal text with its own reader, because the judge synthesis
|
|
328
|
+
* carries the same two fields under no contract at all; this one is what
|
|
329
|
+
* decides whether the member is allowed to seal that text as its result.
|
|
330
|
+
*/
|
|
331
|
+
function parseCouncilBallot(output: string | null): CouncilBallot | null {
|
|
332
|
+
const parsed = parseJson(output);
|
|
333
|
+
if (!parsed.ok) return null;
|
|
334
|
+
const value = parsed.value;
|
|
335
|
+
if (!hasOnlyKeys(value, ["verdict", "text"]) || !string(value.verdict) || !string(value.text)) return null;
|
|
336
|
+
const verdict = value.verdict.trim();
|
|
337
|
+
if (verdict.includes("\n") || Buffer.byteLength(verdict, "utf8") > COUNCIL_BALLOT_VERDICT_MAX_BYTES) return null;
|
|
338
|
+
return { verdict, text: value.text.trim() };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function validateCouncilBallot(contract: ResultContract, output: string | null): ResultContractValidation {
|
|
342
|
+
const parsed = parseJson(output);
|
|
343
|
+
if (!parsed.ok) return failure(contract, "unmeasured", parsed.reason);
|
|
344
|
+
const ballot = parseCouncilBallot(output);
|
|
345
|
+
if (ballot === null) {
|
|
346
|
+
return failure(
|
|
347
|
+
contract,
|
|
348
|
+
"unmeasured",
|
|
349
|
+
`A council ballot must carry only verdict and text, both non-empty strings, with verdict a single line of at most ${COUNCIL_BALLOT_VERDICT_MAX_BYTES} bytes so the tally can match it against the other members`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
// Unmeasured, like every other contract whose shape carries no correctness
|
|
353
|
+
// claim: casting a well-formed ballot says nothing about whether the member
|
|
354
|
+
// was right, and the tally is a count of opinions rather than a check.
|
|
355
|
+
return success(contract, "unmeasured", ballot);
|
|
356
|
+
}
|
|
357
|
+
|
|
276
358
|
export interface CouncilReportMember {
|
|
277
359
|
label: string;
|
|
278
360
|
runId: string;
|
|
@@ -1249,6 +1331,8 @@ export function validateResultContract(input: ResultContractValidationInput): Re
|
|
|
1249
1331
|
? failure(input.contract, "fail", "council-report is malformed")
|
|
1250
1332
|
: success(input.contract, "unmeasured", { council: report });
|
|
1251
1333
|
}
|
|
1334
|
+
case "council-ballot":
|
|
1335
|
+
return validateCouncilBallot(input.contract, input.output);
|
|
1252
1336
|
case "debugger-report":
|
|
1253
1337
|
return validateDebugger(input.contract, input.output);
|
|
1254
1338
|
case "research-report":
|
|
@@ -1347,6 +1431,8 @@ function resultContractShape(contract: ResultContract): string {
|
|
|
1347
1431
|
return '{"verdict":"pass","checks":[{"name":"npm run typecheck","passed":true,"evidence":"exit 0"}]}';
|
|
1348
1432
|
case "council-report":
|
|
1349
1433
|
return '{"members":[{"label":"alpha","runId":"run-1","round":1,"answer":"..."}],"synthesis":{"kind":"none"}}';
|
|
1434
|
+
case "council-ballot":
|
|
1435
|
+
return COUNCIL_BALLOT_SHAPE;
|
|
1350
1436
|
case "debugger-report":
|
|
1351
1437
|
return '{"diagnosis":"...","reproduction":"reproduced","evidence":["..."]}';
|
|
1352
1438
|
case "research-report":
|
|
@@ -1463,8 +1549,31 @@ export function resultContractRepairMessages(
|
|
|
1463
1549
|
] as const;
|
|
1464
1550
|
}
|
|
1465
1551
|
|
|
1466
|
-
/**
|
|
1467
|
-
|
|
1552
|
+
/** Kinds an agent recipe may declare in its frontmatter. */
|
|
1553
|
+
const RECIPE_DECLARABLE_KINDS = [
|
|
1554
|
+
"scout-report",
|
|
1555
|
+
"verifier-report",
|
|
1556
|
+
"council-report",
|
|
1557
|
+
"debugger-report",
|
|
1558
|
+
"research-report",
|
|
1559
|
+
"mutation-report",
|
|
1560
|
+
"provenance-report",
|
|
1561
|
+
"oracle-report",
|
|
1562
|
+
"external-delegation",
|
|
1563
|
+
"artifact-report",
|
|
1564
|
+
"context-handbook",
|
|
1565
|
+
"delegation-plan",
|
|
1566
|
+
] as const;
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Kinds the coordinator authors and hands to a worker on the wire, which no
|
|
1570
|
+
* recipe may declare. A council ballot is the topology's postcondition for a
|
|
1571
|
+
* vote member's slot, so it arrives as a `resultContractOverride` on the
|
|
1572
|
+
* request and never out of frontmatter.
|
|
1573
|
+
*/
|
|
1574
|
+
const COORDINATOR_AUTHORED_KINDS = ["council-ballot"] as const;
|
|
1575
|
+
|
|
1576
|
+
function parseContract(value: unknown, sourcePath: string, kinds: ReadonlyArray<string>): ResultContract {
|
|
1468
1577
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1469
1578
|
throw new Error(`agent recipe: ${sourcePath}: resultContract must be an object`);
|
|
1470
1579
|
}
|
|
@@ -1481,22 +1590,24 @@ export function parseResultContract(value: unknown, sourcePath: string): ResultC
|
|
|
1481
1590
|
}
|
|
1482
1591
|
return { kind: "architect-plan", path: record.path };
|
|
1483
1592
|
}
|
|
1484
|
-
|
|
1485
|
-
"scout-report",
|
|
1486
|
-
"verifier-report",
|
|
1487
|
-
"council-report",
|
|
1488
|
-
"debugger-report",
|
|
1489
|
-
"research-report",
|
|
1490
|
-
"mutation-report",
|
|
1491
|
-
"provenance-report",
|
|
1492
|
-
"oracle-report",
|
|
1493
|
-
"external-delegation",
|
|
1494
|
-
"artifact-report",
|
|
1495
|
-
"context-handbook",
|
|
1496
|
-
"delegation-plan",
|
|
1497
|
-
] as const;
|
|
1498
|
-
if (!(kinds as ReadonlyArray<string>).includes(record.kind) || !only("kind")) {
|
|
1593
|
+
if (!kinds.includes(record.kind) || !only("kind")) {
|
|
1499
1594
|
throw new Error(`agent recipe: ${sourcePath}: resultContract.kind is unsupported or has unknown keys`);
|
|
1500
1595
|
}
|
|
1501
1596
|
return { kind: record.kind as Exclude<ResultContract["kind"], "architect-plan"> };
|
|
1502
1597
|
}
|
|
1598
|
+
|
|
1599
|
+
/** Strict frontmatter parser for the one recipe result-contract schema. */
|
|
1600
|
+
export function parseResultContract(value: unknown, sourcePath: string): ResultContract {
|
|
1601
|
+
return parseContract(value, sourcePath, RECIPE_DECLARABLE_KINDS);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* The same parser for a contract that arrived on a WorkerSpec. It admits what a
|
|
1606
|
+
* recipe may declare plus what the coordinator may author, because a dispatch
|
|
1607
|
+
* request can override the seated recipe's postcondition and the worker has to
|
|
1608
|
+
* be able to read the contract it will be validated against. Keeping the recipe
|
|
1609
|
+
* parser strict is the point: a recipe still cannot claim a coordinator's kind.
|
|
1610
|
+
*/
|
|
1611
|
+
export function parseWorkerResultContract(value: unknown, sourcePath: string): ResultContract {
|
|
1612
|
+
return parseContract(value, sourcePath, [...RECIPE_DECLARABLE_KINDS, ...COORDINATOR_AUTHORED_KINDS]);
|
|
1613
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizePathBoundary,
|
|
3
|
+
normalizePathBoundaryEntry,
|
|
4
|
+
PATH_BOUNDARY_MAX_ENTRIES,
|
|
5
|
+
type PathBoundary,
|
|
6
|
+
pathBoundariesOverlap,
|
|
7
|
+
pathBoundaryCovers,
|
|
8
|
+
} from "../../core/path-boundary.js";
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Declared write boundaries: the repo paths a fleet step is allowed to change.
|
|
3
12
|
*
|
|
@@ -30,37 +39,14 @@
|
|
|
30
39
|
* a step that needs more of the tree than that should say `workspace` and mean
|
|
31
40
|
* it in a contract that does not claim a boundary.
|
|
32
41
|
*/
|
|
33
|
-
export const WRITE_BOUNDARY_MAX_ENTRIES =
|
|
42
|
+
export const WRITE_BOUNDARY_MAX_ENTRIES = PATH_BOUNDARY_MAX_ENTRIES;
|
|
34
43
|
|
|
35
44
|
/** A normalized allowlist. Empty means "changes nothing", which is `readonly`. */
|
|
36
|
-
export type WriteBoundary =
|
|
37
|
-
|
|
38
|
-
const GLOB_CHARACTERS = /[*?[\]{}]/u;
|
|
45
|
+
export type WriteBoundary = PathBoundary;
|
|
39
46
|
|
|
40
47
|
/** Normalize one declared entry, or throw explaining exactly what is wrong. */
|
|
41
48
|
export function normalizeWriteBoundaryEntry(entry: string): string {
|
|
42
|
-
|
|
43
|
-
if (raw.length === 0) throw new Error("write boundary: entry must be a non-empty path");
|
|
44
|
-
if (raw.includes("\\")) {
|
|
45
|
-
throw new Error(`write boundary: entry '${entry}' must use '/' separators`);
|
|
46
|
-
}
|
|
47
|
-
if (GLOB_CHARACTERS.test(raw)) {
|
|
48
|
-
throw new Error(`write boundary: entry '${entry}' looks like a glob, which v1 does not support`);
|
|
49
|
-
}
|
|
50
|
-
if (raw.startsWith("/") || /^[A-Za-z]:/u.test(raw)) {
|
|
51
|
-
throw new Error(`write boundary: entry '${entry}' must be repository-relative, not absolute`);
|
|
52
|
-
}
|
|
53
|
-
const subtree = raw.endsWith("/");
|
|
54
|
-
const segments = raw.split("/").filter((segment) => segment.length > 0);
|
|
55
|
-
if (segments.length === 0) {
|
|
56
|
-
throw new Error(`write boundary: entry '${entry}' must name a path inside the repository`);
|
|
57
|
-
}
|
|
58
|
-
for (const segment of segments) {
|
|
59
|
-
if (segment === "..") throw new Error(`write boundary: entry '${entry}' must not contain '..'`);
|
|
60
|
-
if (segment === ".") throw new Error(`write boundary: entry '${entry}' must not contain '.' segments`);
|
|
61
|
-
}
|
|
62
|
-
const joined = segments.join("/");
|
|
63
|
-
return subtree ? `${joined}/` : joined;
|
|
49
|
+
return normalizePathBoundaryEntry(entry);
|
|
64
50
|
}
|
|
65
51
|
|
|
66
52
|
/**
|
|
@@ -69,38 +55,17 @@ export function normalizeWriteBoundaryEntry(entry: string): string {
|
|
|
69
55
|
* and a sealed verdict can both carry.
|
|
70
56
|
*/
|
|
71
57
|
export function normalizeWriteBoundary(entries: ReadonlyArray<string>): string[] {
|
|
72
|
-
|
|
73
|
-
throw new Error(`write boundary: at most ${WRITE_BOUNDARY_MAX_ENTRIES} entries may be declared`);
|
|
74
|
-
}
|
|
75
|
-
const normalized = new Set(entries.map(normalizeWriteBoundaryEntry));
|
|
76
|
-
// A file entry covered by a declared subtree is redundant, not an error: the
|
|
77
|
-
// operator said the directory, and saying one of its files again changes
|
|
78
|
-
// nothing about what is allowed.
|
|
79
|
-
return [...normalized].sort();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Repo-relative path with the separators this grammar speaks. */
|
|
83
|
-
function toRepoRelativePosix(path: string): string {
|
|
84
|
-
return path.split("\\").join("/");
|
|
58
|
+
return normalizePathBoundary(entries);
|
|
85
59
|
}
|
|
86
60
|
|
|
87
61
|
/** Whether the boundary permits a change to one repo-relative path. */
|
|
88
62
|
export function writeBoundaryCovers(boundary: WriteBoundary, path: string): boolean {
|
|
89
|
-
|
|
90
|
-
return boundary.some((entry) => (entry.endsWith("/") ? candidate.startsWith(entry) : candidate === entry));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Whether two declared entries can describe the same path. */
|
|
94
|
-
function entriesOverlap(left: string, right: string): boolean {
|
|
95
|
-
if (left === right) return true;
|
|
96
|
-
if (left.endsWith("/") && right.startsWith(left)) return true;
|
|
97
|
-
if (right.endsWith("/") && left.startsWith(right)) return true;
|
|
98
|
-
return false;
|
|
63
|
+
return pathBoundaryCovers(boundary, path);
|
|
99
64
|
}
|
|
100
65
|
|
|
101
66
|
/** Whether two boundaries share any path. Empty boundaries never overlap. */
|
|
102
67
|
export function writeBoundariesOverlap(left: WriteBoundary, right: WriteBoundary): boolean {
|
|
103
|
-
return left
|
|
68
|
+
return pathBoundariesOverlap(left, right);
|
|
104
69
|
}
|
|
105
70
|
|
|
106
71
|
/** Operator-facing rendering, used verbatim in violation messages. */
|