@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
|
@@ -16,6 +16,7 @@ import { probeCapabilitiesForModel } from "./model-capabilities.js";
|
|
|
16
16
|
import { loadPluginRuntimes } from "./plugins.js";
|
|
17
17
|
import { getRuntimeRegistry } from "./registry.js";
|
|
18
18
|
import { registerBuiltinRuntimes } from "./runtimes/builtins.js";
|
|
19
|
+
import { listKnownModelsForRuntime } from "./support.js";
|
|
19
20
|
import type { CapabilityFlags } from "./types/capability-flags.js";
|
|
20
21
|
import { EMPTY_CAPABILITIES } from "./types/capability-flags.js";
|
|
21
22
|
import {
|
|
@@ -204,6 +205,32 @@ export function mergeProbeResult(
|
|
|
204
205
|
return merge;
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Whether a reachable target can serve the model it is configured to serve by
|
|
210
|
+
* default. Reachability alone was the whole of `health`, so a target whose
|
|
211
|
+
* `defaultModel` the server had never heard of printed `healthy` until the
|
|
212
|
+
* first turn. This is judged only from a live list on a runtime with no static
|
|
213
|
+
* catalog: the catalog is the authority for cloud runtimes, and a cached or
|
|
214
|
+
* descriptor-supplied list says nothing about the server in front of us.
|
|
215
|
+
* Returns the reason when the default is not served, null when it is or when
|
|
216
|
+
* there is no live evidence either way.
|
|
217
|
+
*/
|
|
218
|
+
export function unservedDefaultModelReason(
|
|
219
|
+
desc: Pick<RuntimeDescriptor, "id">,
|
|
220
|
+
target: Pick<TargetDescriptor, "defaultModel">,
|
|
221
|
+
merge: Pick<ProbeMerge, "discoveredModels" | "discoveredModelsSource" | "discoveredModelStates">,
|
|
222
|
+
): string | null {
|
|
223
|
+
const model = target.defaultModel;
|
|
224
|
+
if (!model) return null;
|
|
225
|
+
if (merge.discoveredModelsSource !== "probe" || merge.discoveredModels.length === 0) return null;
|
|
226
|
+
if (listKnownModelsForRuntime(desc.id).length > 0) return null;
|
|
227
|
+
if (merge.discoveredModels.includes(model)) return null;
|
|
228
|
+
// LM Studio lists a loaded model under its instance id and keeps the model
|
|
229
|
+
// key in the state map; the request path resolves either.
|
|
230
|
+
if (merge.discoveredModelStates && model in merge.discoveredModelStates) return null;
|
|
231
|
+
return `default model '${model}' is not advertised by the target`;
|
|
232
|
+
}
|
|
233
|
+
|
|
207
234
|
export function createProvidersBundle(context: DomainContext): DomainBundle<ProvidersContract> {
|
|
208
235
|
const registry = getRuntimeRegistry();
|
|
209
236
|
const authStore = openAuthStorage();
|
|
@@ -292,13 +319,14 @@ export function createProvidersBundle(context: DomainContext): DomainBundle<Prov
|
|
|
292
319
|
const merge = mergeProbeResult(desc, target, probe, previous);
|
|
293
320
|
const { capabilities, contextWindowProvenance } = capabilitiesFor(desc, target, merge, kb);
|
|
294
321
|
const healthy = probe !== null ? probe.ok : null;
|
|
322
|
+
const unservedDefault = probe?.ok ? unservedDefaultModelReason(desc, target, merge) : null;
|
|
295
323
|
const health: TargetHealth =
|
|
296
324
|
probe === null
|
|
297
325
|
? (previous?.health ?? emptyHealth())
|
|
298
326
|
: {
|
|
299
|
-
status: healthy ? "healthy" : "down",
|
|
327
|
+
status: healthy ? (unservedDefault === null ? "healthy" : "degraded") : "down",
|
|
300
328
|
lastCheckAt: new Date().toISOString(),
|
|
301
|
-
lastError: probe.error ??
|
|
329
|
+
lastError: probe.error ?? unservedDefault,
|
|
302
330
|
latencyMs: probe.latencyMs ?? null,
|
|
303
331
|
};
|
|
304
332
|
const available = availability.available && (probe === null || probe.ok);
|
|
@@ -7,6 +7,8 @@ import type { ResourceDiagnostic, ResourceScope, ResourceSourceInfo } from "./co
|
|
|
7
7
|
|
|
8
8
|
export interface ResourceRoot {
|
|
9
9
|
path: string;
|
|
10
|
+
/** Package root for safely resolving extension-relative prompt references. */
|
|
11
|
+
rootPath?: string;
|
|
10
12
|
scope: ResourceScope;
|
|
11
13
|
source?: string;
|
|
12
14
|
/** Collision rank, higher wins. See {@link ResourceSourceInfo.precedence}. */
|
|
@@ -44,6 +46,7 @@ export function defaultScopedResourceRoots(kind: ExtensionResourceKind, cwd: str
|
|
|
44
46
|
return [
|
|
45
47
|
...enabledExtensionResourceRoots(kind, cwd).map((root) => ({
|
|
46
48
|
path: root.path,
|
|
49
|
+
rootPath: root.rootPath,
|
|
47
50
|
scope: "package" as const,
|
|
48
51
|
source: root.source,
|
|
49
52
|
precedence: COMPAT_RESOURCE_PRECEDENCE.extension,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseCommandArgs, substituteArgs } from "../../../engine/prompt-templates.js";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
COMPAT_RESOURCE_PRECEDENCE,
|
|
15
15
|
defaultScopedResourceRoots,
|
|
16
|
-
readRootEntries,
|
|
17
16
|
sourceInfoForRoot,
|
|
18
17
|
splitYamlFrontmatter,
|
|
19
18
|
stringField,
|
|
@@ -33,6 +32,8 @@ export interface PromptTemplate {
|
|
|
33
32
|
|
|
34
33
|
export interface PromptTemplateRoot {
|
|
35
34
|
path: string;
|
|
35
|
+
/** Present only for an installed extension resource root. */
|
|
36
|
+
rootPath?: string;
|
|
36
37
|
scope: ResourceScope;
|
|
37
38
|
source?: string;
|
|
38
39
|
precedence?: number;
|
|
@@ -139,6 +140,61 @@ function fallbackDescription(body: string): string {
|
|
|
139
140
|
return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Keep the suffix deliberately conservative. Prompt prose commonly places a
|
|
144
|
+
// comma, parenthesis, or full stop immediately after an @path; treating that
|
|
145
|
+
// punctuation as part of the filesystem reference makes a valid template look
|
|
146
|
+
// unresolved. Extension bundles use portable path components, so these are the
|
|
147
|
+
// only characters needed in a reference while still allowing `..` to be
|
|
148
|
+
// detected and rejected by the containment check below.
|
|
149
|
+
const EXTENSION_ROOT_REFERENCE = /\$\{extensionRoot\}(\/[A-Za-z0-9._~%+@/-]*)?/g;
|
|
150
|
+
const EXTENSION_ROOT_TOKEN = "$" + "{extensionRoot}";
|
|
151
|
+
|
|
152
|
+
function contained(root: string, candidate: string): boolean {
|
|
153
|
+
const relative = path.relative(root, candidate);
|
|
154
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Resolve only existing paths contained by the declaring extension package. */
|
|
158
|
+
function resolveExtensionReferences(
|
|
159
|
+
body: string,
|
|
160
|
+
root: PromptTemplateRoot,
|
|
161
|
+
filePath: string,
|
|
162
|
+
diagnostics: ResourceDiagnostic[],
|
|
163
|
+
): string | null {
|
|
164
|
+
if (root.rootPath === undefined || !body.includes(EXTENSION_ROOT_TOKEN)) return body;
|
|
165
|
+
let canonicalRoot: string;
|
|
166
|
+
try {
|
|
167
|
+
canonicalRoot = realpathSync(root.rootPath);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
diagnostics.push({
|
|
170
|
+
type: "warning",
|
|
171
|
+
message: `extension root could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
172
|
+
path: filePath,
|
|
173
|
+
});
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
let invalid: string | null = null;
|
|
177
|
+
const resolved = body.replace(EXTENSION_ROOT_REFERENCE, (reference, suffix: string | undefined) => {
|
|
178
|
+
const candidate = path.resolve(canonicalRoot, `.${suffix ?? ""}`);
|
|
179
|
+
try {
|
|
180
|
+
const canonicalCandidate = realpathSync(candidate);
|
|
181
|
+
if (!contained(canonicalRoot, canonicalCandidate)) invalid = reference;
|
|
182
|
+
} catch {
|
|
183
|
+
invalid = reference;
|
|
184
|
+
}
|
|
185
|
+
return candidate;
|
|
186
|
+
});
|
|
187
|
+
if (invalid !== null) {
|
|
188
|
+
diagnostics.push({
|
|
189
|
+
type: "warning",
|
|
190
|
+
message: `prompt template has an unresolved or escaping extension reference: ${invalid}`,
|
|
191
|
+
path: filePath,
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return resolved;
|
|
196
|
+
}
|
|
197
|
+
|
|
142
198
|
function loadPromptFile(
|
|
143
199
|
filePath: string,
|
|
144
200
|
root: PromptTemplateRoot,
|
|
@@ -155,16 +211,28 @@ function loadPromptFile(
|
|
|
155
211
|
return null;
|
|
156
212
|
}
|
|
157
213
|
|
|
158
|
-
const
|
|
214
|
+
const relativePath = path.relative(root.path, filePath);
|
|
215
|
+
if (
|
|
216
|
+
relativePath === "" ||
|
|
217
|
+
relativePath === ".." ||
|
|
218
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
219
|
+
path.isAbsolute(relativePath)
|
|
220
|
+
) {
|
|
221
|
+
diagnostics.push({ type: "warning", message: "prompt template escaped its discovery root", path: filePath });
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const name = relativePath.slice(0, -ext.length).split(path.sep).join(":");
|
|
159
225
|
if (name.trim().length === 0) return null;
|
|
160
226
|
const { frontmatter, body } = splitOptionalFrontmatter(raw, filePath, diagnostics);
|
|
161
|
-
const
|
|
227
|
+
const resolvedBody = resolveExtensionReferences(body, root, filePath, diagnostics);
|
|
228
|
+
if (resolvedBody === null) return null;
|
|
229
|
+
const description = stringField(frontmatter, "description") ?? fallbackDescription(resolvedBody);
|
|
162
230
|
const argumentHint = stringField(frontmatter, "argument-hint") ?? stringField(frontmatter, "argumentHint");
|
|
163
231
|
const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
|
|
164
232
|
const template: PromptTemplate = {
|
|
165
233
|
name,
|
|
166
234
|
description,
|
|
167
|
-
content:
|
|
235
|
+
content: resolvedBody.trim(),
|
|
168
236
|
filePath,
|
|
169
237
|
sourceInfo,
|
|
170
238
|
trusted: root.trusted !== false,
|
|
@@ -178,10 +246,42 @@ function loadPromptRoot(
|
|
|
178
246
|
diagnostics: ResourceDiagnostic[],
|
|
179
247
|
): ResourceCandidate<PromptTemplate>[] {
|
|
180
248
|
const candidates: ResourceCandidate<PromptTemplate>[] = [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
249
|
+
const pending = [root.path];
|
|
250
|
+
while (pending.length > 0) {
|
|
251
|
+
const directory = pending.pop();
|
|
252
|
+
if (!directory) continue;
|
|
253
|
+
let entries: import("node:fs").Dirent[];
|
|
254
|
+
try {
|
|
255
|
+
const stat = lstatSync(directory);
|
|
256
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
257
|
+
diagnostics.push({
|
|
258
|
+
type: "warning",
|
|
259
|
+
message: "prompt template root contains a non-directory or symbolic link",
|
|
260
|
+
path: directory,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
267
|
+
diagnostics.push({
|
|
268
|
+
type: "warning",
|
|
269
|
+
message: `prompt template directory could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
+
path: directory,
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const entry of [...entries].reverse()) {
|
|
275
|
+
const filePath = path.join(directory, entry.name);
|
|
276
|
+
if (entry.isSymbolicLink()) {
|
|
277
|
+
diagnostics.push({ type: "warning", message: "prompt template symbolic link was ignored", path: filePath });
|
|
278
|
+
} else if (entry.isDirectory()) {
|
|
279
|
+
pending.push(filePath);
|
|
280
|
+
} else if (entry.isFile()) {
|
|
281
|
+
const candidate = loadPromptFile(filePath, root, diagnostics);
|
|
282
|
+
if (candidate) candidates.push(candidate);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
185
285
|
}
|
|
186
286
|
return candidates;
|
|
187
287
|
}
|
|
@@ -210,13 +310,18 @@ export function loadPromptTemplates(input: LoadPromptTemplatesInput = {}): Promp
|
|
|
210
310
|
}
|
|
211
311
|
|
|
212
312
|
function parsePromptCommand(input: string): { name: string; rest: string } | null {
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
215
|
-
const withoutSlash =
|
|
313
|
+
const framed = input.trimStart();
|
|
314
|
+
if (!framed.startsWith("/")) return null;
|
|
315
|
+
const withoutSlash = framed.slice(1);
|
|
216
316
|
const separator = withoutSlash.search(/\s/);
|
|
217
317
|
const name = separator === -1 ? withoutSlash : withoutSlash.slice(0, separator);
|
|
218
318
|
if (name.length === 0 || name.includes("/")) return null;
|
|
219
|
-
|
|
319
|
+
// Leading whitespace before `/` and the first whitespace after the command
|
|
320
|
+
// are framing. A CRLF pair is one delimiter. Every subsequent byte belongs
|
|
321
|
+
// to the argument payload, including indentation, blank lines, and trailing
|
|
322
|
+
// whitespace, so raw `$ARGUMENTS` substitution can be exact at its boundary.
|
|
323
|
+
const delimiterWidth = separator !== -1 && withoutSlash.slice(separator, separator + 2) === "\r\n" ? 2 : 1;
|
|
324
|
+
const rest = separator === -1 ? "" : withoutSlash.slice(separator + delimiterWidth);
|
|
220
325
|
return { name, rest };
|
|
221
326
|
}
|
|
222
327
|
|
|
@@ -238,7 +343,7 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
|
|
|
238
343
|
const args = parseCommandArgs(command.rest);
|
|
239
344
|
return {
|
|
240
345
|
expanded: true,
|
|
241
|
-
text: substituteArgs(template.content, args),
|
|
346
|
+
text: substituteArgs(template.content, args, command.rest),
|
|
242
347
|
args,
|
|
243
348
|
template,
|
|
244
349
|
diagnostics: templates.diagnostics,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { artifactDefaultPath } from "../../core/artifact-paths.js";
|
|
4
|
+
import { pathBoundaryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
|
|
4
5
|
import { canonicalizeExistingPath } from "../../core/path-canonical.js";
|
|
5
6
|
import { ToolNames } from "../../core/tool-names.js";
|
|
6
7
|
import { clioConfigDir } from "../../core/xdg.js";
|
|
@@ -89,7 +90,8 @@ export interface SafetyPolicyEngineOptions {
|
|
|
89
90
|
rulePacks?: RulePacks;
|
|
90
91
|
projectPolicy?: LoadedProjectSafetyPolicy;
|
|
91
92
|
/**
|
|
92
|
-
* Absolute
|
|
93
|
+
* Absolute path boundaries a write-class tool call is confined to for this run.
|
|
94
|
+
* Exact files omit a trailing slash and subtrees retain one.
|
|
93
95
|
* When present and non-empty, a write/edit target outside every root is a
|
|
94
96
|
* final BLOCK (reason code "write-root"). Empty or absent disables the check.
|
|
95
97
|
* Enforced at the worker safety seam so both the native worker registry and
|
|
@@ -182,9 +184,7 @@ function evaluateWriteRoots(roots: ReadonlyArray<string>, writeRootCwd: string,
|
|
|
182
184
|
const target = writeRootTargetPath(call);
|
|
183
185
|
if (target === null) return null;
|
|
184
186
|
const resolved = path.resolve(writeRootCwd, target);
|
|
185
|
-
|
|
186
|
-
if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) return null;
|
|
187
|
-
}
|
|
187
|
+
if (pathBoundaryCovers(roots, resolved)) return null;
|
|
188
188
|
return `write target '${target}' resolves to '${resolved}', which is outside the permitted write roots for this run: ${roots.join(", ")}`;
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -193,7 +193,7 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
|
|
|
193
193
|
// Write-root containment resolves lexically, so it keeps its own un-canonicalized
|
|
194
194
|
// cwd and roots to compare like against like (the design mandates no symlink chasing).
|
|
195
195
|
const writeRootCwd = path.resolve(options.cwd ?? process.cwd());
|
|
196
|
-
const writeRoots = (options.writeRoots ?? []).map((root) =>
|
|
196
|
+
const writeRoots = (options.writeRoots ?? []).map((root) => resolvePathBoundary(writeRootCwd, root));
|
|
197
197
|
const packs = options.rulePacks ?? getCachedDefaultRulePacks();
|
|
198
198
|
const projectPolicy = options.projectPolicy ?? loadProjectSafetyPolicy(cwd);
|
|
199
199
|
const projectPolicyRoot =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { ToolNames } from "../../core/tool-names.js";
|
|
3
|
+
import { classify } from "./action-classifier.js";
|
|
3
4
|
import { typedValidationSummary } from "./finish-contract.js";
|
|
4
5
|
import {
|
|
5
6
|
detectValidationCommand,
|
|
@@ -25,6 +26,13 @@ import {
|
|
|
25
26
|
* bash command that writes through a script) leaves the path absent from the
|
|
26
27
|
* set and the validator falls back to the filesystem rather than to a failed
|
|
27
28
|
* conformance.
|
|
29
|
+
*
|
|
30
|
+
* `writeRecordComplete` is the other half of that sentence, and it exists for
|
|
31
|
+
* the reader that cannot fall back to the filesystem. The write boundary uses
|
|
32
|
+
* this record the other way round, to decide that a path the run did *not*
|
|
33
|
+
* write is somebody else's and must survive. Reading an under-reporting set as
|
|
34
|
+
* a closed list would clear a step of a write it really made, so the recorder
|
|
35
|
+
* says outright whether the set is closed.
|
|
28
36
|
*/
|
|
29
37
|
export interface RunEffects {
|
|
30
38
|
/** Absolute paths the run's successful tool calls aimed a mutation at. */
|
|
@@ -56,6 +64,18 @@ export interface RunEffects {
|
|
|
56
64
|
* wider set.
|
|
57
65
|
*/
|
|
58
66
|
verificationCommands: ReadonlySet<string>;
|
|
67
|
+
/**
|
|
68
|
+
* False once the run made a successful call to a tool that can mutate a path
|
|
69
|
+
* its own arguments do not name. After such a call `mutatedPaths` is a lower
|
|
70
|
+
* bound rather than the whole list, so a reader that would treat an absent
|
|
71
|
+
* path as proof the run did not write it has to stop treating it that way.
|
|
72
|
+
*
|
|
73
|
+
* The downgrade covers the whole run rather than the one call, because an
|
|
74
|
+
* opaque call's effect is unbounded. A shell command, a project script, or a
|
|
75
|
+
* spawned worker can write anywhere the process can reach, so once one has
|
|
76
|
+
* run there is no path left in the checkout that the record can clear.
|
|
77
|
+
*/
|
|
78
|
+
writeRecordComplete: boolean;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
export interface RunEffectsRecorder {
|
|
@@ -70,6 +90,39 @@ interface PendingEffects {
|
|
|
70
90
|
paths: ReadonlyArray<string>;
|
|
71
91
|
validationCommand: string | null;
|
|
72
92
|
verificationCommand: string | null;
|
|
93
|
+
opaque: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Action classes whose successful calls write only paths their own arguments
|
|
98
|
+
* name. `read` covers the observe, retrieve, interact, and orchestrate-bookkeeping
|
|
99
|
+
* tools, none of which touch a source-workspace file: `git` is a closed
|
|
100
|
+
* status/diff/log surface, and `tasks`, `ledger`, and `context` write Clio's own
|
|
101
|
+
* state. `write` covers `write`, `edit`, and `artifact`, whose target
|
|
102
|
+
* `toolMutationPaths` resolves from the call itself, including the artifact
|
|
103
|
+
* tool's default path.
|
|
104
|
+
*/
|
|
105
|
+
const ENUMERABLE_ACTION_CLASSES: ReadonlySet<string> = new Set(["read", "write"]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether a successful call to this tool can mutate a path its own recorded
|
|
109
|
+
* arguments do not name.
|
|
110
|
+
*
|
|
111
|
+
* Asked of the classifier rather than of a hand-written list, because
|
|
112
|
+
* `src/tools/policy.ts` asserts at bootstrap that every registered tool's
|
|
113
|
+
* `baseActionClass` equals what this classifier returns and what the plane
|
|
114
|
+
* table declares. A tool added to the execute or orchestrate-dispatch plane is
|
|
115
|
+
* therefore opaque the day it is registered, with no list to remember to update.
|
|
116
|
+
*
|
|
117
|
+
* The classifier is asked about the tool name alone. Bash's argument scan can
|
|
118
|
+
* escalate a single call to `system_modify` or `git_destructive`, and those are
|
|
119
|
+
* still bash; the question here is what the tool can do, not what one call was
|
|
120
|
+
* caught doing. An unregistered name (a dynamic or MCP tool) classifies as
|
|
121
|
+
* `unknown` and is opaque, which is the fail-closed answer for a tool whose
|
|
122
|
+
* arguments this process has no schema for.
|
|
123
|
+
*/
|
|
124
|
+
export function toolWritesOpaquely(toolName: string): boolean {
|
|
125
|
+
return !ENUMERABLE_ACTION_CLASSES.has(classify({ tool: toolName }).actionClass);
|
|
73
126
|
}
|
|
74
127
|
|
|
75
128
|
/**
|
|
@@ -157,18 +210,23 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
|
157
210
|
const failedMutationPaths = new Set<string>();
|
|
158
211
|
const validationCommands = new Set<string>();
|
|
159
212
|
const verificationCommands = new Set<string>();
|
|
213
|
+
let opaqueWriteObserved = false;
|
|
160
214
|
return {
|
|
161
215
|
start(toolCallId, toolName, args) {
|
|
162
216
|
const paths = mutationTargets(toolName, args);
|
|
163
217
|
const validationCommand = validationCommandOf(toolName, args, "finish-contract");
|
|
164
218
|
const verificationCommand = validationCommandOf(toolName, args, "grounding");
|
|
165
|
-
|
|
219
|
+
const opaque = toolWritesOpaquely(toolName);
|
|
220
|
+
// An opaque call is tracked even when it exposed no path and no
|
|
221
|
+
// command, because the fact that it ran at all is the finding.
|
|
222
|
+
if (!opaque && paths.length === 0 && validationCommand === null && verificationCommand === null) return;
|
|
166
223
|
// A bash call may re-base its relative paths with its own cwd argument.
|
|
167
224
|
const base = typeof args?.cwd === "string" && args.cwd.length > 0 ? path.resolve(cwd, args.cwd) : cwd;
|
|
168
225
|
pending.set(toolCallId, {
|
|
169
226
|
paths: paths.map((target) => path.resolve(base, target)),
|
|
170
227
|
validationCommand,
|
|
171
228
|
verificationCommand,
|
|
229
|
+
opaque,
|
|
172
230
|
});
|
|
173
231
|
},
|
|
174
232
|
finish(toolCallId, failed) {
|
|
@@ -179,6 +237,10 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
|
179
237
|
for (const target of effects.paths) failedMutationPaths.add(target);
|
|
180
238
|
return;
|
|
181
239
|
}
|
|
240
|
+
// Success-gated on purpose. A shell command the safety policy blocked,
|
|
241
|
+
// or one that came back an error, never reached the filesystem, so it
|
|
242
|
+
// leaves the record closed.
|
|
243
|
+
if (effects.opaque) opaqueWriteObserved = true;
|
|
182
244
|
for (const target of effects.paths) mutatedPaths.add(target);
|
|
183
245
|
if (effects.validationCommand !== null) validationCommands.add(effects.validationCommand);
|
|
184
246
|
if (effects.verificationCommand !== null) verificationCommands.add(effects.verificationCommand);
|
|
@@ -191,6 +253,7 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
|
191
253
|
failedMutationPaths: new Set([...failedMutationPaths].filter((target) => !mutatedPaths.has(target))),
|
|
192
254
|
validationCommands: new Set(validationCommands),
|
|
193
255
|
verificationCommands: new Set(verificationCommands),
|
|
256
|
+
writeRecordComplete: !opaqueWriteObserved,
|
|
194
257
|
};
|
|
195
258
|
},
|
|
196
259
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { asDirectoryPathBoundary, pathBoundaryEntryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
|
|
2
3
|
import type { ActionClass } from "./action-classifier.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -15,15 +16,7 @@ export interface ScopeSpec {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function normalizeRoot(root: string): string {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isUnder(child: string, parent: string): boolean {
|
|
22
|
-
const c = normalizeRoot(child);
|
|
23
|
-
const p = normalizeRoot(parent);
|
|
24
|
-
if (c === p) return true;
|
|
25
|
-
const rel = path.relative(p, c);
|
|
26
|
-
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
19
|
+
return resolvePathBoundary(process.cwd(), root);
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
|
|
@@ -31,7 +24,9 @@ export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
|
|
|
31
24
|
if (!orchestrator.allowedActions.has(action)) return false;
|
|
32
25
|
}
|
|
33
26
|
for (const root of worker.allowedWriteRoots) {
|
|
34
|
-
const covered = orchestrator.allowedWriteRoots.some((outer) =>
|
|
27
|
+
const covered = orchestrator.allowedWriteRoots.some((outer) =>
|
|
28
|
+
pathBoundaryEntryCovers(normalizeRoot(outer), normalizeRoot(root)),
|
|
29
|
+
);
|
|
35
30
|
if (!covered) return false;
|
|
36
31
|
}
|
|
37
32
|
if (worker.allowNetwork && !orchestrator.allowNetwork) return false;
|
|
@@ -48,14 +43,14 @@ export const READONLY_SCOPE: ScopeSpec = {
|
|
|
48
43
|
|
|
49
44
|
export const WORKSPACE_SCOPE: ScopeSpec = {
|
|
50
45
|
allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch"]),
|
|
51
|
-
allowedWriteRoots: [process.cwd()],
|
|
46
|
+
allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
|
|
52
47
|
allowNetwork: true,
|
|
53
48
|
allowDispatch: true,
|
|
54
49
|
};
|
|
55
50
|
|
|
56
51
|
export const CONFIRMED_SCOPE: ScopeSpec = {
|
|
57
52
|
allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch", "system_modify"]),
|
|
58
|
-
allowedWriteRoots: [process.cwd()],
|
|
53
|
+
allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
|
|
59
54
|
allowNetwork: true,
|
|
60
55
|
allowDispatch: true,
|
|
61
56
|
};
|
package/src/engine/acp/server.ts
CHANGED
|
@@ -1233,7 +1233,10 @@ function safeProbeReason(
|
|
|
1233
1233
|
} catch {
|
|
1234
1234
|
return "not-configured";
|
|
1235
1235
|
}
|
|
1236
|
-
|
|
1236
|
+
// Degraded is reachable with a default the server does not serve; a client
|
|
1237
|
+
// that names its own model can still use the target.
|
|
1238
|
+
const reachable = status.health.status === "healthy" || status.health.status === "degraded";
|
|
1239
|
+
return status.available && reachable ? null : "unreachable";
|
|
1237
1240
|
}
|
|
1238
1241
|
|
|
1239
1242
|
interface AcpPermissionBridge {
|
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { parseCommandArgs, substituteArgs as substituteParsedArgs } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export { parseCommandArgs };
|
|
4
|
+
|
|
5
|
+
const RAW_ARGUMENTS_PLACEHOLDER = /\$ARGUMENTS/g;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Substitute prompt arguments while optionally retaining the exact raw text
|
|
9
|
+
* for `$ARGUMENTS`. Positional, slice, and `$@` placeholders keep Pi's
|
|
10
|
+
* shell-style parsing semantics.
|
|
11
|
+
*/
|
|
12
|
+
export function substituteArgs(content: string, args: string[], rawArguments?: string): string {
|
|
13
|
+
if (rawArguments === undefined) return substituteParsedArgs(content, args);
|
|
14
|
+
return content
|
|
15
|
+
.split(RAW_ARGUMENTS_PLACEHOLDER)
|
|
16
|
+
.map((segment) => substituteParsedArgs(segment, args))
|
|
17
|
+
.join(rawArguments);
|
|
18
|
+
}
|
|
@@ -23,7 +23,7 @@ import { type ToolName, ToolNames } from "../core/tool-names.js";
|
|
|
23
23
|
import {
|
|
24
24
|
type ObservedReadRanges,
|
|
25
25
|
type ObservedRunEffects,
|
|
26
|
-
|
|
26
|
+
parseWorkerResultContract,
|
|
27
27
|
RESULT_CONTRACT_REPAIR_LIMIT,
|
|
28
28
|
type ResultContract,
|
|
29
29
|
resultContractRepairMessages,
|
|
@@ -362,8 +362,11 @@ export function startWorkerRun(input: WorkerRunInput, emit: WorkerEventEmit): Wo
|
|
|
362
362
|
assertResponseSchemaRuntime(input);
|
|
363
363
|
// The wire carries the contract as data; the one strict parser owns its
|
|
364
364
|
// shape. The worker subprocess may not import it (it stays slim), so the
|
|
365
|
-
// check lands here, before any model call.
|
|
366
|
-
|
|
365
|
+
// check lands here, before any model call. The wire parser, not the recipe
|
|
366
|
+
// one: a dispatch request may override the seated recipe's postcondition
|
|
367
|
+
// with a kind the coordinator authors, and the recipe parser refusing those
|
|
368
|
+
// killed every council vote member with a fatal spec error.
|
|
369
|
+
if (input.resultContract !== undefined) parseWorkerResultContract(input.resultContract, "WorkerSpec.resultContract");
|
|
367
370
|
if (input.runtime.id === "claude-sdk") {
|
|
368
371
|
return startClaudeSdkWorkerRun(input, emit);
|
|
369
372
|
}
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
type RunStatus,
|
|
25
25
|
runKindSupportsLiveSteering,
|
|
26
26
|
} from "../domains/dispatch/types.js";
|
|
27
|
+
import {
|
|
28
|
+
summarizeTrustStatus,
|
|
29
|
+
type TrustSummaryProjection,
|
|
30
|
+
type TrustVerdict,
|
|
31
|
+
} from "../domains/evidence/trust-projection.js";
|
|
27
32
|
import {
|
|
28
33
|
COST_NOT_MEASURED,
|
|
29
34
|
costAggregateForAmount,
|
|
@@ -113,6 +118,13 @@ export interface DispatchBoardRow {
|
|
|
113
118
|
council?: { group: string; label: string; color?: string; round: number };
|
|
114
119
|
/** Orchestrator-executed declared verification status. */
|
|
115
120
|
hostVerification?: "verified" | "rejected" | "skipped";
|
|
121
|
+
/**
|
|
122
|
+
* The canonical trust projection of the sealed receipt, read back from
|
|
123
|
+
* disk and authenticated against the ledger row. Absent while the run is
|
|
124
|
+
* live or when the receipt could not be read back, so the card never
|
|
125
|
+
* implies a verdict the terminal bus event cannot carry.
|
|
126
|
+
*/
|
|
127
|
+
trust?: TrustSummaryProjection;
|
|
116
128
|
/** Dead-node failover hops recorded on this run's chain. */
|
|
117
129
|
rerouteCount?: number;
|
|
118
130
|
/** Assignment retry attempts observed on the assignment event stream. */
|
|
@@ -651,7 +663,15 @@ export function renderDispatchCard(
|
|
|
651
663
|
`${theme.fg("dim", "ttft")} ${theme.fg("muted", ttft)}`,
|
|
652
664
|
`${theme.fg("dim", "cost")} ${theme.fg("muted", cost)}`,
|
|
653
665
|
];
|
|
654
|
-
|
|
666
|
+
// The trust line is the canonical projection, styled by its verdict tier;
|
|
667
|
+
// a host check is folded into that projection as validation grounding, so
|
|
668
|
+
// it is named as a secondary fact and never as independent verification.
|
|
669
|
+
const trustUnits = [
|
|
670
|
+
row.trust === undefined
|
|
671
|
+
? theme.fg("dim", isTerminalStatus(row.status) ? "trust: receipt not read back" : "trust: not sealed yet")
|
|
672
|
+
: theme.fg(trustVerdictToken(row.trust.verdict), `${trustVerdictGlyph(row.trust.verdict)} ${row.trust.text}`),
|
|
673
|
+
...(row.hostVerification !== undefined ? [theme.fg("muted", `host checks ${row.hostVerification}`)] : []),
|
|
674
|
+
];
|
|
655
675
|
const contextUnit = formatWorkerContextMeter(row.lastContextTokens ?? 0, row.contextWindow, theme);
|
|
656
676
|
// The phase column: a fleet step says which wave it belongs to and which
|
|
657
677
|
// step it is; every other run leaves the cell empty rather than inventing a
|
|
@@ -670,7 +690,7 @@ export function renderDispatchCard(
|
|
|
670
690
|
? [truncateToWidth(`${cardKvKey(theme, "task")}${theme.fg("muted", row.taskSummary)}`, contentWidth, "…", false)]
|
|
671
691
|
: []),
|
|
672
692
|
cardUnitsLine(theme, "status", statusUnits, contentWidth),
|
|
673
|
-
cardUnitsLine(theme, "
|
|
693
|
+
cardUnitsLine(theme, "trust", trustUnits, contentWidth),
|
|
674
694
|
...(row.budget !== undefined
|
|
675
695
|
? [
|
|
676
696
|
truncateToWidth(
|
|
@@ -1160,6 +1180,33 @@ function parseOptionalDetail(value: unknown): string | null {
|
|
|
1160
1180
|
return detail.length > 0 ? detail : null;
|
|
1161
1181
|
}
|
|
1162
1182
|
|
|
1183
|
+
/** Only an authenticated independent pass earns the success token; sealed alone is never green. */
|
|
1184
|
+
function trustVerdictToken(verdict: TrustVerdict): ClioToken {
|
|
1185
|
+
switch (verdict) {
|
|
1186
|
+
case "reviewed":
|
|
1187
|
+
return "success";
|
|
1188
|
+
case "grounded":
|
|
1189
|
+
return "info";
|
|
1190
|
+
case "unverified":
|
|
1191
|
+
return "muted";
|
|
1192
|
+
case "compromised":
|
|
1193
|
+
return "error";
|
|
1194
|
+
default:
|
|
1195
|
+
return "dim";
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function trustVerdictGlyph(verdict: TrustVerdict): string {
|
|
1200
|
+
switch (verdict) {
|
|
1201
|
+
case "reviewed":
|
|
1202
|
+
return GLYPH.ok;
|
|
1203
|
+
case "compromised":
|
|
1204
|
+
return GLYPH.error;
|
|
1205
|
+
default:
|
|
1206
|
+
return "◇";
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1163
1210
|
function terminalDetail(row: DispatchBoardRow): string | null {
|
|
1164
1211
|
if (row.status !== "failed" && row.status !== "dead" && row.status !== "aborted") return null;
|
|
1165
1212
|
return parseOptionalDetail(row.outcomeDetail);
|
|
@@ -1319,6 +1366,7 @@ function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
|
|
|
1319
1366
|
...(entry.gate !== undefined ? { gate: { ...entry.gate } } : {}),
|
|
1320
1367
|
...(entry.council !== undefined ? { council: { ...entry.council } } : {}),
|
|
1321
1368
|
...(entry.hostVerification !== undefined ? { hostVerification: entry.hostVerification } : {}),
|
|
1369
|
+
...(entry.trust !== undefined ? { trust: entry.trust } : {}),
|
|
1322
1370
|
...(entry.rerouteCount !== undefined ? { rerouteCount: entry.rerouteCount } : {}),
|
|
1323
1371
|
...(entry.failoverHops !== undefined ? { failoverHops: entry.failoverHops } : {}),
|
|
1324
1372
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
@@ -1394,8 +1442,10 @@ export function createDispatchBoardStore(
|
|
|
1394
1442
|
* before it publishes one, so this is the first moment the file exists.
|
|
1395
1443
|
*/
|
|
1396
1444
|
const settleFromReceipt = (entry: DispatchBoardEntry): void => {
|
|
1397
|
-
const
|
|
1445
|
+
const facts = readReceipt?.(entry.runId);
|
|
1446
|
+
const text = facts?.text;
|
|
1398
1447
|
entry.progress.settle(typeof text === "string" && text.trim().length > 0 ? text : undefined);
|
|
1448
|
+
if (facts?.trust !== undefined) entry.trust = summarizeTrustStatus(facts.trust);
|
|
1399
1449
|
};
|
|
1400
1450
|
|
|
1401
1451
|
// Payloads arrive typed off the bus, but the board keeps its runtime
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ConfigReloadFailedPayload,
|
|
4
4
|
type ContextPrunedPayload,
|
|
5
5
|
type ContextWarningPayload,
|
|
6
|
+
type DispatchScopeNoticePayload,
|
|
6
7
|
type LoopBlockedPayload,
|
|
7
8
|
type RuntimeNoticePayload,
|
|
8
9
|
type ToolBudgetExceededPayload,
|
|
@@ -271,6 +272,19 @@ export function createInteractiveEventProjection(deps: InteractiveEventProjectio
|
|
|
271
272
|
);
|
|
272
273
|
|
|
273
274
|
remainingUnsubscribers.push(
|
|
275
|
+
deps.bus.on(BusChannels.DispatchScopeNotice, (payload) => {
|
|
276
|
+
const event = payload as DispatchScopeNoticePayload | null | undefined;
|
|
277
|
+
if (
|
|
278
|
+
!event ||
|
|
279
|
+
typeof event !== "object" ||
|
|
280
|
+
!["typed_scope_replaced_inferred_paths", "legacy_scope_inferred", "legacy_scope_empty"].includes(event.code) ||
|
|
281
|
+
typeof event.message !== "string"
|
|
282
|
+
) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
deps.appendTranscriptNotice("warn", event.message);
|
|
286
|
+
deps.requestRender();
|
|
287
|
+
}),
|
|
274
288
|
deps.bus.on(BusChannels.LoopBlocked, (payload) => {
|
|
275
289
|
const event = payload as LoopBlockedPayload | null | undefined;
|
|
276
290
|
if (!event || typeof event !== "object" || typeof event.tool !== "string" || typeof event.repeatCount !== "number") {
|