pi-daddy 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -33
- package/README.md +27 -5
- package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
- package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/contracts/ledger-record/v1/README.md +40 -25
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -5
- package/dist/cli.js.map +1 -1
- package/dist/executors/activity-session.js +116 -65
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/executors/captured-execution.d.ts +13 -0
- package/dist/executors/captured-execution.d.ts.map +1 -0
- package/dist/executors/captured-execution.js +53 -0
- package/dist/executors/captured-execution.js.map +1 -0
- package/dist/executors/child-final.d.ts +31 -0
- package/dist/executors/child-final.d.ts.map +1 -0
- package/dist/executors/child-final.js +197 -0
- package/dist/executors/child-final.js.map +1 -0
- package/dist/executors/executor.d.ts +2 -1
- package/dist/executors/executor.d.ts.map +1 -1
- package/dist/executors/executor.js +12 -0
- package/dist/executors/executor.js.map +1 -1
- package/dist/executors/owned-worker.d.ts +18 -0
- package/dist/executors/owned-worker.d.ts.map +1 -0
- package/dist/executors/owned-worker.js +295 -0
- package/dist/executors/owned-worker.js.map +1 -0
- package/dist/executors/worker-artifact.d.ts +3 -0
- package/dist/executors/worker-artifact.d.ts.map +1 -0
- package/dist/executors/worker-artifact.js +3 -0
- package/dist/executors/worker-artifact.js.map +1 -0
- package/dist/governance/approval-prompt.d.ts +4 -3
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +13 -6
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/captured-worker-record.d.ts +9 -0
- package/dist/governance/captured-worker-record.d.ts.map +1 -0
- package/dist/governance/captured-worker-record.js +123 -0
- package/dist/governance/captured-worker-record.js.map +1 -0
- package/dist/governance/lease-record.d.ts +4 -0
- package/dist/governance/lease-record.d.ts.map +1 -1
- package/dist/governance/lease-record.js +19 -2
- package/dist/governance/lease-record.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +5 -5
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +1 -1
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/workspace-acceptance.d.ts +3 -1
- package/dist/governance/workspace-acceptance.d.ts.map +1 -1
- package/dist/governance/workspace-acceptance.js +21 -5
- package/dist/governance/workspace-acceptance.js.map +1 -1
- package/dist/governance/workspace-lease.d.ts.map +1 -1
- package/dist/governance/workspace-lease.js +44 -1
- package/dist/governance/workspace-lease.js.map +1 -1
- package/dist/kernel/authority-text.d.ts +3 -0
- package/dist/kernel/authority-text.d.ts.map +1 -0
- package/dist/kernel/authority-text.js +22 -0
- package/dist/kernel/authority-text.js.map +1 -0
- package/dist/kernel/bounded-read-failures.d.ts +9 -0
- package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
- package/dist/kernel/bounded-read-failures.js +33 -0
- package/dist/kernel/bounded-read-failures.js.map +1 -0
- package/dist/kernel/bounded-read-sync.d.ts +24 -0
- package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
- package/dist/kernel/bounded-read-sync.js +75 -0
- package/dist/kernel/bounded-read-sync.js.map +1 -0
- package/dist/kernel/bounded-read.d.ts +29 -2
- package/dist/kernel/bounded-read.d.ts.map +1 -1
- package/dist/kernel/bounded-read.js +98 -9
- package/dist/kernel/bounded-read.js.map +1 -1
- package/dist/kernel/capacity.d.ts +36 -0
- package/dist/kernel/capacity.d.ts.map +1 -0
- package/dist/kernel/capacity.js +94 -0
- package/dist/kernel/capacity.js.map +1 -0
- package/dist/kernel/captured-worker-contract.d.ts +38 -0
- package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
- package/dist/kernel/captured-worker-contract.js +2 -0
- package/dist/kernel/captured-worker-contract.js.map +1 -0
- package/dist/kernel/catalog.d.ts +12 -3
- package/dist/kernel/catalog.d.ts.map +1 -1
- package/dist/kernel/catalog.js +28 -8
- package/dist/kernel/catalog.js.map +1 -1
- package/dist/kernel/chain.d.ts +1 -0
- package/dist/kernel/chain.d.ts.map +1 -1
- package/dist/kernel/chain.js.map +1 -1
- package/dist/kernel/definitions.d.ts +11 -1
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js +6 -3
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/fanout.d.ts +5 -25
- package/dist/kernel/fanout.d.ts.map +1 -1
- package/dist/kernel/fanout.js +15 -30
- package/dist/kernel/fanout.js.map +1 -1
- package/dist/kernel/grant-env.d.ts +0 -2
- package/dist/kernel/grant-env.d.ts.map +1 -1
- package/dist/kernel/grant-env.js +0 -1
- package/dist/kernel/grant-env.js.map +1 -1
- package/dist/kernel/model-preflight.d.ts +8 -0
- package/dist/kernel/model-preflight.d.ts.map +1 -1
- package/dist/kernel/model-preflight.js +23 -0
- package/dist/kernel/model-preflight.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts +15 -0
- package/dist/kernel/selected-definitions.d.ts.map +1 -0
- package/dist/kernel/selected-definitions.js +146 -0
- package/dist/kernel/selected-definitions.js.map +1 -0
- package/dist/kernel/skill-packages.d.ts +4 -2
- package/dist/kernel/skill-packages.d.ts.map +1 -1
- package/dist/kernel/skill-packages.js +34 -41
- package/dist/kernel/skill-packages.js.map +1 -1
- package/dist/kernel/skill-resources.d.ts.map +1 -1
- package/dist/kernel/skill-resources.js +8 -11
- package/dist/kernel/skill-resources.js.map +1 -1
- package/dist/kernel/workspace.d.ts +6 -3
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +10 -6
- package/dist/kernel/workspace.js.map +1 -1
- package/dist/products/dashboard-cli.js +1 -1
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +1 -2
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +3 -1
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-session-client.d.ts +1 -2
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/episode-report.d.ts +12 -8
- package/dist/products/episode-report.d.ts.map +1 -1
- package/dist/products/episode-report.js +61 -12
- package/dist/products/episode-report.js.map +1 -1
- package/extensions/approvals.ts +1 -1
- package/extensions/chain-plan.ts +9 -3
- package/extensions/context-staging.ts +30 -35
- package/extensions/dashboard-session-server.ts +1 -2
- package/extensions/definition-describe.ts +44 -0
- package/extensions/definition-runtime.ts +89 -26
- package/extensions/delegate-chain.ts +273 -256
- package/extensions/delegation-activation.ts +1 -1
- package/extensions/delegation.ts +41 -53
- package/extensions/execute-child.ts +167 -134
- package/extensions/fanout-outcome.ts +1 -9
- package/extensions/grants-command.ts +2 -9
- package/extensions/grants.ts +106 -38
- package/extensions/init-command.ts +52 -12
- package/extensions/reload-environment.ts +21 -2
- package/extensions/retired-inputs.ts +7 -0
- package/extensions/run-delegation.ts +212 -265
- package/extensions/session-capacity.ts +53 -0
- package/extensions/session-environment.ts +5 -2
- package/extensions/session-model-prompt.ts +33 -32
- package/extensions/session-report.ts +28 -11
- package/extensions/session.ts +200 -157
- package/extensions/workspace-runtime.ts +2 -1
- package/native/README.md +25 -0
- package/native/linux-x64/worker +0 -0
- package/native/linux-x64/worker.sha256 +1 -0
- package/native/provenance.json +42 -0
- package/native/toolchain.json +18 -0
- package/native/worker.c +209 -0
- package/package.json +15 -7
- package/src/cli.ts +16 -6
- package/src/executors/activity-session.ts +120 -66
- package/src/executors/captured-execution.ts +61 -0
- package/src/executors/child-final.ts +190 -0
- package/src/executors/executor.ts +14 -1
- package/src/executors/owned-worker.ts +322 -0
- package/src/executors/worker-artifact.ts +2 -0
- package/src/governance/approval-prompt.ts +18 -8
- package/src/governance/captured-worker-record.ts +138 -0
- package/src/governance/lease-record.ts +21 -2
- package/src/governance/ledger-events.ts +5 -5
- package/src/governance/ledger-v3-validation.ts +1 -1
- package/src/governance/workspace-acceptance.ts +20 -4
- package/src/governance/workspace-lease.ts +59 -1
- package/src/kernel/authority-text.ts +17 -0
- package/src/kernel/bounded-read-failures.ts +42 -0
- package/src/kernel/bounded-read-sync.ts +87 -0
- package/src/kernel/bounded-read.ts +128 -11
- package/src/kernel/capacity.ts +123 -0
- package/src/kernel/captured-worker-contract.ts +29 -0
- package/src/kernel/catalog.ts +45 -16
- package/src/kernel/chain.ts +1 -0
- package/src/kernel/definitions.ts +11 -2
- package/src/kernel/fanout.ts +17 -32
- package/src/kernel/grant-env.ts +0 -3
- package/src/kernel/model-preflight.ts +29 -0
- package/src/kernel/selected-definitions.ts +156 -0
- package/src/kernel/skill-packages.ts +40 -38
- package/src/kernel/skill-resources.ts +8 -9
- package/src/kernel/workspace.ts +12 -5
- package/src/products/dashboard-cli.ts +1 -1
- package/src/products/dashboard-render.ts +6 -2
- package/src/products/dashboard-session-client.ts +1 -2
- package/src/products/episode-report.ts +80 -17
- package/dist/advisors/advisor.d.ts +0 -90
- package/dist/advisors/advisor.d.ts.map +0 -1
- package/dist/advisors/advisor.js +0 -154
- package/dist/advisors/advisor.js.map +0 -1
- package/dist/advisors/decider.d.ts +0 -88
- package/dist/advisors/decider.d.ts.map +0 -1
- package/dist/advisors/decider.js +0 -29
- package/dist/advisors/decider.js.map +0 -1
- package/dist/advisors/jev.d.ts +0 -41
- package/dist/advisors/jev.d.ts.map +0 -1
- package/dist/advisors/jev.js +0 -139
- package/dist/advisors/jev.js.map +0 -1
- package/dist/advisors/settings.d.ts +0 -49
- package/dist/advisors/settings.d.ts.map +0 -1
- package/dist/advisors/settings.js +0 -99
- package/dist/advisors/settings.js.map +0 -1
- package/dist/governance/episode-cost-gate.d.ts +0 -31
- package/dist/governance/episode-cost-gate.d.ts.map +0 -1
- package/dist/governance/episode-cost-gate.js +0 -112
- package/dist/governance/episode-cost-gate.js.map +0 -1
- package/extensions/advisor-session.ts +0 -67
- package/extensions/effort-advice.ts +0 -112
- package/extensions/pruning-advice.ts +0 -133
- package/src/advisors/advisor.ts +0 -246
- package/src/advisors/decider.ts +0 -74
- package/src/advisors/jev.ts +0 -163
- package/src/advisors/settings.ts +0 -115
- package/src/governance/episode-cost-gate.ts +0 -128
package/src/advisors/advisor.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The wrapper that makes "every use is recorded" structural rather than a rule (ADR-0077).
|
|
3
|
-
*
|
|
4
|
-
* A caller cannot reach a `Decider` directly through this layer's public surface: it asks an `Advisor`, and asking
|
|
5
|
-
* always produces an `advice` record — including when the answer was "no advice", which is the case a reviewer most
|
|
6
|
-
* needs to see, because an advisor that silently stops answering would otherwise look exactly like one nobody used.
|
|
7
|
-
*
|
|
8
|
-
* **What is recorded, and what is not.** The record names the purpose, the decider, the question keys, the answers
|
|
9
|
-
* and how long it took. It does NOT contain the state. A caller composes that state from its own context, which can
|
|
10
|
-
* include task text, file contents and a repository's private material; the ledger has never stored a task
|
|
11
|
-
* (ADR-0021) and an advisor must not become the way it starts. The question keys are the caller's own constants,
|
|
12
|
-
* so they name the decision without describing the situation.
|
|
13
|
-
*/
|
|
14
|
-
import type { Advice, AdviceRequest, Decider } from "./decider.ts";
|
|
15
|
-
|
|
16
|
-
/** Two seconds. An advisor is on the path of a decision a human is waiting for; it is not worth more than that. */
|
|
17
|
-
export const DEFAULT_ADVICE_TIMEOUT_MS = 2000;
|
|
18
|
-
|
|
19
|
-
export type TaskEgressMode = "digest" | "raw";
|
|
20
|
-
|
|
21
|
-
export interface TaskDigest {
|
|
22
|
-
length: number;
|
|
23
|
-
language: string;
|
|
24
|
-
referencedFiles: { count: number; extensions: string[] };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface AdviceRecord {
|
|
28
|
-
/** Stable root episode; absent on records written before episode identity shipped. */
|
|
29
|
-
episodeId?: string;
|
|
30
|
-
/** The governed child this advice applied to; absent on records written before attribution shipped. */
|
|
31
|
-
executionId?: string;
|
|
32
|
-
/** Which task representation was sent; absent on records written before the egress split shipped. */
|
|
33
|
-
taskEgress?: TaskEgressMode;
|
|
34
|
-
/** Which decision this advice was for, from the caller's own closed list. */
|
|
35
|
-
purpose: string;
|
|
36
|
-
decider: string;
|
|
37
|
-
/** Question keys only — never the state, and never a question's free text. */
|
|
38
|
-
questions: string[];
|
|
39
|
-
answered: boolean;
|
|
40
|
-
durationMs: number;
|
|
41
|
-
/** Present only when advice came back. */
|
|
42
|
-
answers?: Readonly<Record<string, { value: string | number | boolean; confidence?: number }>>;
|
|
43
|
-
model?: string;
|
|
44
|
-
resolvedModel: { provider: string; modelId: string } | null;
|
|
45
|
-
modelSource: "explicit" | "session" | "definition" | "global" | "pi";
|
|
46
|
-
thinkingLevel: string | null;
|
|
47
|
-
thinkingSource: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
|
|
48
|
-
tokenDetail: {
|
|
49
|
-
inputTokens: number | null;
|
|
50
|
-
outputTokens: number | null;
|
|
51
|
-
cacheReadTokens: number | null;
|
|
52
|
-
cacheWriteTokens: number | null;
|
|
53
|
-
reasoningTokens: number | null;
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Why there is no advice. `declined` means the advisor answered with nothing; `error` means it could not be
|
|
57
|
-
* reached or its response was unrecognised; `cancelled` means the CALLER went away, which is not the advisor's
|
|
58
|
-
* failure and must not read as one.
|
|
59
|
-
*/
|
|
60
|
-
outcome: "answered" | "disabled" | "timeout" | "error" | "declined" | "cancelled";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface AdviceAttribution {
|
|
64
|
-
resolvedModel: { provider: string; modelId: string } | null;
|
|
65
|
-
modelSource: AdviceRecord["modelSource"];
|
|
66
|
-
thinkingLevel: string | null;
|
|
67
|
-
thinkingSource: AdviceRecord["thinkingSource"];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface Advisor {
|
|
71
|
-
task(task: string): string | TaskDigest;
|
|
72
|
-
ask(
|
|
73
|
-
purpose: string,
|
|
74
|
-
request: AdviceRequest,
|
|
75
|
-
signal?: AbortSignal,
|
|
76
|
-
executionId?: string,
|
|
77
|
-
attribution?: AdviceAttribution | ((advice: Advice | null) => AdviceAttribution),
|
|
78
|
-
): Promise<Advice | null>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function createAdvisor(input: {
|
|
82
|
-
decider: Decider;
|
|
83
|
-
/** Where the record goes. Injected so this layer does no I/O and governance does not import it. */
|
|
84
|
-
record: (entry: AdviceRecord) => void | Promise<void>;
|
|
85
|
-
timeoutMs?: number;
|
|
86
|
-
/** Absent or false means the null decider is used whatever `decider` says. */
|
|
87
|
-
enabled?: boolean;
|
|
88
|
-
episodeId?: string;
|
|
89
|
-
taskEgress?: TaskEgressMode;
|
|
90
|
-
}): Advisor {
|
|
91
|
-
const timeoutMs = input.timeoutMs ?? DEFAULT_ADVICE_TIMEOUT_MS;
|
|
92
|
-
const taskEgress = input.taskEgress ?? "digest";
|
|
93
|
-
return {
|
|
94
|
-
task(task) {
|
|
95
|
-
if (taskEgress === "raw") {
|
|
96
|
-
warnRawTaskEgress();
|
|
97
|
-
return task;
|
|
98
|
-
}
|
|
99
|
-
return digestTask(task);
|
|
100
|
-
},
|
|
101
|
-
async ask(purpose, request, signal, executionId, attribution) {
|
|
102
|
-
const started = Date.now();
|
|
103
|
-
const attributionFor = (advice: Advice | null): AdviceAttribution =>
|
|
104
|
-
typeof attribution === "function"
|
|
105
|
-
? attribution(advice)
|
|
106
|
-
: (attribution ?? {
|
|
107
|
-
resolvedModel: advice?.resolvedModel ?? null,
|
|
108
|
-
modelSource: "pi",
|
|
109
|
-
thinkingLevel: null,
|
|
110
|
-
thinkingSource: "pi",
|
|
111
|
-
});
|
|
112
|
-
const base = {
|
|
113
|
-
...(input.episodeId ? { episodeId: input.episodeId } : {}),
|
|
114
|
-
...(executionId ? { executionId } : {}),
|
|
115
|
-
taskEgress,
|
|
116
|
-
purpose,
|
|
117
|
-
decider: input.decider.name,
|
|
118
|
-
questions: Object.keys(request.questions),
|
|
119
|
-
...attributionFor(null),
|
|
120
|
-
tokenDetail: {
|
|
121
|
-
inputTokens: null,
|
|
122
|
-
outputTokens: null,
|
|
123
|
-
cacheReadTokens: null,
|
|
124
|
-
cacheWriteTokens: null,
|
|
125
|
-
reasoningTokens: null,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
const write = async (entry: AdviceRecord) => {
|
|
129
|
-
try {
|
|
130
|
-
await input.record(entry);
|
|
131
|
-
} catch {
|
|
132
|
-
// Recording is an observation of a decision that has already been taken. Failing to write it must not
|
|
133
|
-
// change what the caller does, for `execute-child`'s reason: an audit failure that discards the work is
|
|
134
|
-
// worse than one that is merely missing.
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
if (input.enabled !== true) {
|
|
138
|
-
await write({ ...base, decider: "none", answered: false, durationMs: 0, outcome: "disabled" });
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const timer = new AbortController();
|
|
142
|
-
const cancel = setTimeout(() => timer.abort(), timeoutMs);
|
|
143
|
-
const linked = signal ? AbortSignal.any([signal, timer.signal]) : timer.signal;
|
|
144
|
-
try {
|
|
145
|
-
// RACED, not merely signalled. A decider that ignores its signal would otherwise run as long as it liked
|
|
146
|
-
// and then be recorded as a timeout — measured at fifty times the configured bound. The `Decider` contract
|
|
147
|
-
// cannot make an implementation honour an abort, so the bound is enforced on this side of it.
|
|
148
|
-
const advice = await Promise.race([
|
|
149
|
-
input.decider.decide(request, linked),
|
|
150
|
-
new Promise<null>((settle) => linked.addEventListener("abort", () => settle(null), { once: true })),
|
|
151
|
-
]);
|
|
152
|
-
const durationMs = Date.now() - started;
|
|
153
|
-
if (!advice) {
|
|
154
|
-
await write({ ...base, answered: false, durationMs, outcome: outcomeFor(timer.signal, signal, "declined") });
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
await write({
|
|
158
|
-
...base,
|
|
159
|
-
answered: true,
|
|
160
|
-
durationMs,
|
|
161
|
-
outcome: "answered",
|
|
162
|
-
answers: Object.fromEntries(
|
|
163
|
-
Object.entries(advice.answers).map(([key, answer]) => [
|
|
164
|
-
key,
|
|
165
|
-
{ value: answer.value, ...(answer.confidence === undefined ? {} : { confidence: answer.confidence }) },
|
|
166
|
-
]),
|
|
167
|
-
),
|
|
168
|
-
...(advice.model ? { model: advice.model } : {}),
|
|
169
|
-
...attributionFor(advice),
|
|
170
|
-
...(advice.tokenDetail ? { tokenDetail: advice.tokenDetail } : {}),
|
|
171
|
-
});
|
|
172
|
-
return advice;
|
|
173
|
-
} catch {
|
|
174
|
-
// Including an abort. A caller asked for advice and is getting none; it proceeds exactly as it would have.
|
|
175
|
-
await write({
|
|
176
|
-
...base,
|
|
177
|
-
answered: false,
|
|
178
|
-
durationMs: Date.now() - started,
|
|
179
|
-
outcome: outcomeFor(timer.signal, signal, "error"),
|
|
180
|
-
});
|
|
181
|
-
return null;
|
|
182
|
-
} finally {
|
|
183
|
-
clearTimeout(cancel);
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
let rawTaskEgressWarned = false;
|
|
190
|
-
|
|
191
|
-
function warnRawTaskEgress(): void {
|
|
192
|
-
if (rawTaskEgressWarned) return;
|
|
193
|
-
rawTaskEgressWarned = true;
|
|
194
|
-
console.warn("pi-daddy: raw advisor task egress is enabled");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function digestTask(task: string): TaskDigest {
|
|
198
|
-
const files = [...task.matchAll(/\b(?:[\w.-]+\/)*[\w-]+\.[A-Za-z0-9]{1,10}\b/g)].map((match) => match[0]);
|
|
199
|
-
const extensions = [
|
|
200
|
-
...new Set(files.map((file) => `.${file.slice(file.lastIndexOf(".") + 1).toLowerCase()}`)),
|
|
201
|
-
].sort();
|
|
202
|
-
const languages = new Set(extensions.map(languageForExtension).filter((language) => language !== "unknown"));
|
|
203
|
-
return {
|
|
204
|
-
length: task.length,
|
|
205
|
-
language: languages.size === 0 ? "unknown" : languages.size === 1 ? [...languages][0] : "mixed",
|
|
206
|
-
referencedFiles: { count: files.length, extensions },
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function languageForExtension(extension: string): string {
|
|
211
|
-
const languages: Readonly<Record<string, string>> = {
|
|
212
|
-
".c": "c",
|
|
213
|
-
".cpp": "cpp",
|
|
214
|
-
".css": "css",
|
|
215
|
-
".go": "go",
|
|
216
|
-
".html": "html",
|
|
217
|
-
".java": "java",
|
|
218
|
-
".js": "javascript",
|
|
219
|
-
".jsx": "javascript",
|
|
220
|
-
".json": "json",
|
|
221
|
-
".md": "markdown",
|
|
222
|
-
".php": "php",
|
|
223
|
-
".py": "python",
|
|
224
|
-
".rb": "ruby",
|
|
225
|
-
".rs": "rust",
|
|
226
|
-
".sh": "shell",
|
|
227
|
-
".sql": "sql",
|
|
228
|
-
".swift": "swift",
|
|
229
|
-
".ts": "typescript",
|
|
230
|
-
".tsx": "typescript",
|
|
231
|
-
".yaml": "yaml",
|
|
232
|
-
".yml": "yaml",
|
|
233
|
-
};
|
|
234
|
-
return languages[extension] ?? "unknown";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** The bound fired, the caller went away, or neither — three different facts a reviewer needs to tell apart. */
|
|
238
|
-
function outcomeFor(
|
|
239
|
-
timer: AbortSignal,
|
|
240
|
-
caller: AbortSignal | undefined,
|
|
241
|
-
otherwise: "declined" | "error",
|
|
242
|
-
): AdviceRecord["outcome"] {
|
|
243
|
-
if (timer.aborted && !caller?.aborted) return "timeout";
|
|
244
|
-
if (caller?.aborted) return "cancelled";
|
|
245
|
-
return otherwise;
|
|
246
|
-
}
|
package/src/advisors/decider.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An advisor is a thing that answers a typed question. It is never a thing that decides (ADR-0077).
|
|
3
|
-
*
|
|
4
|
-
* The boundary is the whole design, and it is structural rather than remembered: **no type in this layer names a
|
|
5
|
-
* `Capability` or a refusal code**, and no function in `kernel/` or `governance/` accepts an advisor's result. An
|
|
6
|
-
* advisor may select among options the caller already had, rank them, annotate them, or propose one. It can never
|
|
7
|
-
* widen an `effective` set, satisfy a gate, or stand in for a human's answer — not because it is asked not to, but
|
|
8
|
-
* because nothing on those paths can receive what it returns. `test/advisors.test.ts` forces that.
|
|
9
|
-
*
|
|
10
|
-
* Non-generative on purpose. The first advisor is a classifier that returns a typed answer with a probability, not
|
|
11
|
-
* prose, which is what makes an advisor auditable: "it chose `b` at 0.91" is a fact a reviewer can disagree with,
|
|
12
|
-
* where a paragraph of reasoning is not.
|
|
13
|
-
*
|
|
14
|
-
* Degradation is "no advice", never a guess. Every path that cannot produce an answer — disabled, missing key,
|
|
15
|
-
* timeout, transport error, a response shape we do not recognise — returns `null`, and the caller does what it
|
|
16
|
-
* would have done without an advisor at all. That is why a caller must be written to work with `nullDecider`
|
|
17
|
-
* first, and why `nullDecider` is the default.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** A question, in the three shapes the first advisor understands. */
|
|
21
|
-
export type Question =
|
|
22
|
-
| { kind: "noul"; instructions: string; whenTrue: string; whenFalse: string }
|
|
23
|
-
| { kind: "choice"; instructions: string; options: Readonly<Record<string, string>> }
|
|
24
|
-
| { kind: "score"; instructions: string; levels: readonly string[] };
|
|
25
|
-
|
|
26
|
-
/** One typed answer. `confidence` is absent when the transport did not report one; it is never invented. */
|
|
27
|
-
export type Answer =
|
|
28
|
-
| { kind: "noul"; value: boolean; confidence?: number }
|
|
29
|
-
| { kind: "choice"; value: string; confidence?: number }
|
|
30
|
-
// `level` is the string the value indexes, so a caller never has to know which end the scale starts at.
|
|
31
|
-
| { kind: "score"; value: number; level: string; confidence?: number };
|
|
32
|
-
|
|
33
|
-
export interface AdviceRequest {
|
|
34
|
-
/**
|
|
35
|
-
* What the advisor is told about the situation, composed by the caller.
|
|
36
|
-
*
|
|
37
|
-
* **Sent, never recorded.** The task is never STORED (ADR-0021) and that still holds — `createAdvisor` writes the
|
|
38
|
-
* question keys and the answers and never this object. Raw task text crosses only when the independent egress
|
|
39
|
-
* switch permits it; otherwise callers send a structural digest. Neither representation is copied into the record.
|
|
40
|
-
*/
|
|
41
|
-
state: Readonly<Record<string, unknown>>;
|
|
42
|
-
questions: Readonly<Record<string, Question>>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface Advice {
|
|
46
|
-
answers: Readonly<Record<string, Answer>>;
|
|
47
|
-
/** What actually answered, as the transport reported it — a dated model id, not the one we asked for. */
|
|
48
|
-
model?: string;
|
|
49
|
-
resolvedModel?: { provider: string; modelId: string };
|
|
50
|
-
tokenDetail?: {
|
|
51
|
-
inputTokens: number | null;
|
|
52
|
-
outputTokens: number | null;
|
|
53
|
-
cacheReadTokens: number | null;
|
|
54
|
-
cacheWriteTokens: number | null;
|
|
55
|
-
reasoningTokens: number | null;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface Decider {
|
|
60
|
-
/** Recorded in the ledger so a reviewer can tell which advisor a decision was taken beside. */
|
|
61
|
-
readonly name: string;
|
|
62
|
-
decide(request: AdviceRequest, signal?: AbortSignal): Promise<Advice | null>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The default, and the one every caller must work correctly with.
|
|
67
|
-
*
|
|
68
|
-
* Not a placeholder: it is how advisors stay optional. A caller that behaves differently under `nullDecider` than
|
|
69
|
-
* under no advisor at all has made advice load-bearing, which is the one thing ADR-0077 forbids.
|
|
70
|
-
*/
|
|
71
|
-
export const nullDecider: Decider = {
|
|
72
|
-
name: "none",
|
|
73
|
-
decide: async () => null,
|
|
74
|
-
};
|
package/src/advisors/jev.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jev, through OpenRouter's Decisions endpoint — the first advisor adapter (ADR-0077).
|
|
3
|
-
*
|
|
4
|
-
* **What is verified and what is not.** The REQUEST shape below is taken from OpenRouter's own SDK reference for
|
|
5
|
-
* `POST /api/alpha/decisions`: `{model, state, questions}`, where each question is `noul` with `criteria.true` and
|
|
6
|
-
* `criteria.false`, `choice` with a `criteria` map of option to description, or `score` with a `criteria` array of
|
|
7
|
-
* level descriptions. That much is documented. The RESPONSE is described there only as an `answers` object beside
|
|
8
|
-
* `id`, `model`, `provider` and `usage`, with probabilities and confidence mentioned but never shown, and the one
|
|
9
|
-
* public guide to this endpoint says plainly that it has not run paid calls either. **So no shape below has been
|
|
10
|
-
* confirmed against a live response.** The parser therefore accepts what the documentation describes, tolerates the
|
|
11
|
-
* obvious variants, and returns `null` for anything else rather than guessing — which is the same thing it does when
|
|
12
|
-
* the endpoint is down. A live check is the `PI_DADDY_IT_JEV=1` tier, and until somebody runs it this adapter's
|
|
13
|
-
* response handling is a reading of documentation, not a measurement.
|
|
14
|
-
*
|
|
15
|
-
* Nothing here can widen anything: it returns `Advice`, and no kernel or governance function accepts one.
|
|
16
|
-
*/
|
|
17
|
-
import type { Advice, AdviceRequest, Answer, Decider, Question } from "./decider.ts";
|
|
18
|
-
|
|
19
|
-
export const JEV_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
|
|
20
|
-
export const JEV_MODEL = "typesafe/jev-1.13";
|
|
21
|
-
|
|
22
|
-
/** The wire form of one question, exactly as OpenRouter's reference documents it. */
|
|
23
|
-
export function wireQuestion(question: Question): Record<string, unknown> {
|
|
24
|
-
if (question.kind === "noul")
|
|
25
|
-
return {
|
|
26
|
-
type: "noul",
|
|
27
|
-
instructions: question.instructions,
|
|
28
|
-
criteria: { true: question.whenTrue, false: question.whenFalse },
|
|
29
|
-
};
|
|
30
|
-
if (question.kind === "choice")
|
|
31
|
-
return { type: "choice", instructions: question.instructions, criteria: { ...question.options } };
|
|
32
|
-
return { type: "score", instructions: question.instructions, criteria: [...question.levels] };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function wireRequest(request: AdviceRequest, model: string): Record<string, unknown> {
|
|
36
|
-
return {
|
|
37
|
-
model,
|
|
38
|
-
state: request.state,
|
|
39
|
-
questions: Object.fromEntries(Object.entries(request.questions).map(([key, q]) => [key, wireQuestion(q)])),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Read one answer out of a response, or nothing.
|
|
45
|
-
*
|
|
46
|
-
* Deliberately generous about WHERE the value and the probability sit, because the documentation names the fields
|
|
47
|
-
* without showing them, and strict about WHAT they are: a `choice` answer must be one of the options that were
|
|
48
|
-
* asked about, and a `score` must be an index into the levels. An answer outside the question's own vocabulary is
|
|
49
|
-
* not a low-confidence answer, it is a response we did not understand, and the honest reading of that is no advice.
|
|
50
|
-
*/
|
|
51
|
-
export function parseAnswer(question: Question, raw: unknown): Answer | undefined {
|
|
52
|
-
if (raw === null || raw === undefined) return undefined;
|
|
53
|
-
const object = typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined;
|
|
54
|
-
const value = object ? (object.value ?? object.answer ?? object.choice ?? object.result) : raw;
|
|
55
|
-
const confidenceRaw = object ? (object.confidence ?? object.probability ?? object.p) : undefined;
|
|
56
|
-
const confidence = typeof confidenceRaw === "number" && Number.isFinite(confidenceRaw) ? confidenceRaw : undefined;
|
|
57
|
-
const withConfidence = <T extends Answer>(answer: T): T =>
|
|
58
|
-
(confidence === undefined ? answer : { ...answer, confidence }) as T;
|
|
59
|
-
|
|
60
|
-
if (question.kind === "noul") {
|
|
61
|
-
if (typeof value !== "boolean") return undefined;
|
|
62
|
-
return withConfidence({ kind: "noul", value });
|
|
63
|
-
}
|
|
64
|
-
if (question.kind === "choice") {
|
|
65
|
-
if (typeof value !== "string" || !Object.hasOwn(question.options, value)) return undefined;
|
|
66
|
-
return withConfidence({ kind: "choice", value });
|
|
67
|
-
}
|
|
68
|
-
// ONE reading: a score is a 0-based index into the `criteria` array that was asked about, and `level` carries the
|
|
69
|
-
// string so a caller never indexes the number itself.
|
|
70
|
-
//
|
|
71
|
-
// The first draft accepted both a 0-based and a 1-based reading "because the documentation shows neither", which
|
|
72
|
-
// pushed the ambiguity onto the caller and into the ledger: with three levels, 1 and 2 were valid under both, so
|
|
73
|
-
// `levels[value]` could read "high" where the model meant "mid". An advisor exists to remove that guess, not to
|
|
74
|
-
// relocate it. **The 0-based reading is an assumption** — it indexes the documented array form — and it is
|
|
75
|
-
// unverified for the same reason everything else about the response is: no live call has been made. If Jev is
|
|
76
|
-
// 1-based, its top level falls outside the array and the whole answer is refused as unrecognised, which is the
|
|
77
|
-
// loud failure rather than a silently shifted one, and the `PI_DADDY_IT_JEV=1` tier is what would show it.
|
|
78
|
-
if (typeof value !== "number" || !Number.isInteger(value)) return undefined;
|
|
79
|
-
if (value < 0 || value >= question.levels.length) return undefined;
|
|
80
|
-
return withConfidence({ kind: "score", value, level: question.levels[value] });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function parseAdvice(request: AdviceRequest, body: unknown): Advice | null {
|
|
84
|
-
if (typeof body !== "object" || body === null) return null;
|
|
85
|
-
const envelope = body as Record<string, unknown>;
|
|
86
|
-
const raw = envelope.answers;
|
|
87
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
88
|
-
const answers: Record<string, Answer> = {};
|
|
89
|
-
for (const [key, question] of Object.entries(request.questions)) {
|
|
90
|
-
const parsed = parseAnswer(question, (raw as Record<string, unknown>)[key]);
|
|
91
|
-
// Every question or none: a caller that asked two questions and silently received one would have to guess which
|
|
92
|
-
// of its branches the missing answer belonged to, and guessing is what an advisor exists to remove.
|
|
93
|
-
if (!parsed) return null;
|
|
94
|
-
answers[key] = parsed;
|
|
95
|
-
}
|
|
96
|
-
const model = typeof envelope.model === "string" ? envelope.model : undefined;
|
|
97
|
-
const provider = typeof envelope.provider === "string" ? envelope.provider : undefined;
|
|
98
|
-
const tokenDetail = parseTokenDetail(envelope.usage);
|
|
99
|
-
return {
|
|
100
|
-
answers,
|
|
101
|
-
...(model ? { model } : {}),
|
|
102
|
-
...(model && provider ? { resolvedModel: { provider, modelId: model } } : {}),
|
|
103
|
-
...(tokenDetail ? { tokenDetail } : {}),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseTokenDetail(value: unknown): Advice["tokenDetail"] | undefined {
|
|
108
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
109
|
-
const usage = value as Record<string, unknown>;
|
|
110
|
-
const details = object(usage.input_tokens_details ?? usage.prompt_tokens_details);
|
|
111
|
-
const outputDetails = object(usage.output_tokens_details ?? usage.completion_tokens_details);
|
|
112
|
-
const read = (...keys: string[]): number | null => {
|
|
113
|
-
const reported = keys.map((key) => usage[key]).find((entry) => typeof entry === "number" && Number.isFinite(entry));
|
|
114
|
-
return typeof reported === "number" && reported > 0 ? reported : null;
|
|
115
|
-
};
|
|
116
|
-
const nested = (source: Record<string, unknown> | undefined, key: string): number | null => {
|
|
117
|
-
const reported = source?.[key];
|
|
118
|
-
return typeof reported === "number" && Number.isFinite(reported) && reported > 0 ? reported : null;
|
|
119
|
-
};
|
|
120
|
-
return {
|
|
121
|
-
inputTokens: read("input_tokens", "prompt_tokens"),
|
|
122
|
-
outputTokens: read("output_tokens", "completion_tokens"),
|
|
123
|
-
cacheReadTokens: read("cache_read_tokens") ?? nested(details, "cached_tokens"),
|
|
124
|
-
cacheWriteTokens: read("cache_write_tokens"),
|
|
125
|
-
reasoningTokens: read("reasoning_tokens") ?? nested(outputDetails, "reasoning_tokens"),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const object = (value: unknown): Record<string, unknown> | undefined =>
|
|
130
|
-
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
131
|
-
|
|
132
|
-
export interface JevConfig {
|
|
133
|
-
apiKey: string;
|
|
134
|
-
model?: string;
|
|
135
|
-
endpoint?: string;
|
|
136
|
-
/** Injected so the adapter is testable without a network, and so nothing here reaches for a global. */
|
|
137
|
-
fetch?: typeof globalThis.fetch;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function jevDecider(config: JevConfig): Decider {
|
|
141
|
-
return {
|
|
142
|
-
name: "jev",
|
|
143
|
-
async decide(request, signal) {
|
|
144
|
-
const send = config.fetch ?? globalThis.fetch;
|
|
145
|
-
const response = await send(config.endpoint ?? JEV_ENDPOINT, {
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey}` },
|
|
148
|
-
body: JSON.stringify(wireRequest(request, config.model ?? JEV_MODEL)),
|
|
149
|
-
...(signal ? { signal } : {}),
|
|
150
|
-
});
|
|
151
|
-
// A dead endpoint and an advisor with nothing to say must not read alike in the ledger: the whole reason for
|
|
152
|
-
// recording the nothing-cases is that an advisor which quietly stopped answering should not look like one
|
|
153
|
-
// nobody called. Thrown, so `createAdvisor` records `error` rather than `declined`; it catches, so nothing
|
|
154
|
-
// reaches the caller but `null` either way.
|
|
155
|
-
if (!response.ok) throw new Error(`advisor endpoint returned ${response.status}`);
|
|
156
|
-
try {
|
|
157
|
-
return parseAdvice(request, await response.json());
|
|
158
|
-
} catch {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
package/src/advisors/settings.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether an advisor is on, and which one (ADR-0077).
|
|
3
|
-
*
|
|
4
|
-
* **Default off, and only the environment can turn it on.** An advisor sends a description of the caller's
|
|
5
|
-
* situation to a third party, so enabling one is `PI_DADDY_ADVISOR=jev` plus a key — both outside the workspace,
|
|
6
|
-
* both stripped from every child.
|
|
7
|
-
*
|
|
8
|
-
* 0.34.0 read the enable from `.pi/pi-daddy/settings.json` and that was wrong for the reason `grant-store.ts`
|
|
9
|
-
* states about the same file: it is writable by any child holding `tool:write`, so it is "the reviewable record of
|
|
10
|
-
* the decision, not the thing the enforcer reads". A grant lives outside the workspace precisely so a child cannot
|
|
11
|
-
* widen the next session's ceiling; an advisor switch a child could flip would make the operator's next session
|
|
12
|
-
* ship its own description to a third party, which is the same self-defeating shape. The settings block may still
|
|
13
|
-
* NARROW — a shorter timeout, or `enabled: false` to turn an advisor off for one project — and can never turn one
|
|
14
|
-
* on, choose its model, or lengthen its bound. A model is a destination rather than a narrowing, so `model` in the
|
|
15
|
-
* block is refused with a message naming `PI_DADDY_ADVISOR_MODEL`, and a timeout is clamped to the default rather
|
|
16
|
-
* than trusted. Malformed configuration disables the advisor and says so: a typo must not be a way to enable anything.
|
|
17
|
-
*
|
|
18
|
-
* **Not a dashboard toggle**, which is what the programme originally sketched. The dashboard is a read-only
|
|
19
|
-
* renderer in a separate process that "never affects enforcement" (ADR-0036), and a control there that wrote to
|
|
20
|
-
* settings would be the first thing it ever wrote. Turning an advisor on is an operator decision that belongs in
|
|
21
|
-
* the reviewable file; `/grants` reports what is in force. That is a deliberate departure from the roadmap line.
|
|
22
|
-
*
|
|
23
|
-
* The key is never in the settings file either, because that file is committed and an API key must not be.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
// Spelled once, in the kernel's table, so this layer cannot drift from the list `childEnv` refuses to write.
|
|
27
|
-
export { ENV_ADVISOR_KEY as ADVISOR_KEY_ENV } from "../kernel/env-names.ts";
|
|
28
|
-
import { ENV_ADVISOR, ENV_ADVISOR_KEY, ENV_ADVISOR_MODEL, ENV_ADVISOR_TASK_EGRESS } from "../kernel/env-names.ts";
|
|
29
|
-
import { DEFAULT_ADVICE_TIMEOUT_MS, type TaskEgressMode } from "./advisor.ts";
|
|
30
|
-
export { ENV_ADVISOR_MODEL, ENV_ADVISOR_TASK_EGRESS } from "../kernel/env-names.ts";
|
|
31
|
-
export { ENV_ADVISOR } from "../kernel/env-names.ts";
|
|
32
|
-
|
|
33
|
-
export interface AdvisorSettings {
|
|
34
|
-
enabled: boolean;
|
|
35
|
-
/** The only decider this release knows besides the null one. */
|
|
36
|
-
decider: "none" | "jev";
|
|
37
|
-
/** Overrides the adapter's pinned model id; absent means the adapter's own default. */
|
|
38
|
-
model?: string;
|
|
39
|
-
timeoutMs?: number;
|
|
40
|
-
taskEgress: TaskEgressMode;
|
|
41
|
-
/** Why an advisor is off when the settings asked for one on — reported, never silently applied. */
|
|
42
|
-
refusal?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const ADVISOR_OFF: AdvisorSettings = Object.freeze({ enabled: false, decider: "none", taskEgress: "digest" });
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Read the `advisor` block of a project settings file. Absent is off; malformed is off WITH a reason.
|
|
49
|
-
*
|
|
50
|
-
* The reason matters more than it looks: an operator who wrote `"enabeld": true` and got silence would conclude the
|
|
51
|
-
* feature does not work, and an operator who wrote it and got an advisor anyway would have a third party reading
|
|
52
|
-
* their session without having successfully asked for it. Both are worse than a sentence naming the field.
|
|
53
|
-
*/
|
|
54
|
-
export function advisorSettingsFrom(raw: unknown, env: NodeJS.ProcessEnv = process.env): AdvisorSettings {
|
|
55
|
-
// The environment decides WHETHER, before the workspace is consulted at all. A settings file that asks for an
|
|
56
|
-
// advisor nobody enabled is not a configuration error; it is simply a project that would use one if the operator
|
|
57
|
-
// turned it on, so it is reported rather than refused.
|
|
58
|
-
const requested = env[ENV_ADVISOR]?.trim();
|
|
59
|
-
if (!requested) return ADVISOR_OFF;
|
|
60
|
-
if (requested !== "jev")
|
|
61
|
-
return { ...ADVISOR_OFF, refusal: `${ENV_ADVISOR}=${requested} names no advisor this release knows` };
|
|
62
|
-
|
|
63
|
-
const block = raw === undefined || raw === null ? {} : raw;
|
|
64
|
-
if (typeof block !== "object" || Array.isArray(block))
|
|
65
|
-
return { ...ADVISOR_OFF, refusal: "settings.advisor must be an object; no advisor is enabled" };
|
|
66
|
-
const fields = block as Record<string, unknown>;
|
|
67
|
-
// `model` stays KNOWN so the refusal below can say why it is refused, rather than reporting it as a typo.
|
|
68
|
-
const unknownKeys = Object.keys(fields).filter((key) => !["enabled", "decider", "model", "timeoutMs"].includes(key));
|
|
69
|
-
// Key names are echoed back, and this file is workspace-writable: a key containing an escape sequence or a
|
|
70
|
-
// newline would otherwise forge lines in the `/grants` panel, which is a trust surface. Review measured a forged
|
|
71
|
-
// `grant tool:*` line. Sanitised and truncated before it reaches any renderer.
|
|
72
|
-
if (unknownKeys.length > 0)
|
|
73
|
-
return {
|
|
74
|
-
...ADVISOR_OFF,
|
|
75
|
-
refusal: `settings.advisor has unknown field(s) ${unknownKeys
|
|
76
|
-
.map((key) => key.replace(/[^\w.-]/g, "?").slice(0, 40))
|
|
77
|
-
.join(", ")}`,
|
|
78
|
-
};
|
|
79
|
-
// A project may switch it OFF; it may never switch it on, which is why `true` is not read. Anything that is not
|
|
80
|
-
// exactly `true` disables: `"false"`, `0` and `null` used to leave the advisor ON with no word said, which is
|
|
81
|
-
// rule 8 inverted — the one control this file retains failing open.
|
|
82
|
-
if (fields.enabled !== undefined && fields.enabled !== true)
|
|
83
|
-
return { ...ADVISOR_OFF, refusal: "settings.advisor.enabled is not true for this project" };
|
|
84
|
-
if (fields.decider !== undefined && fields.decider !== "jev")
|
|
85
|
-
return { ...ADVISOR_OFF, refusal: `settings.advisor.decider must be "jev"; no advisor is enabled` };
|
|
86
|
-
// **A model is a DESTINATION, not a narrowing.** Letting this workspace-writable file choose it would let a child
|
|
87
|
-
// holding `tool:write` point the operator's next session at a generative model of its choosing, billed to the
|
|
88
|
-
// operator's key — the same self-defeating shape this release moved the enable switch to close, one step
|
|
89
|
-
// sideways. Review measured it. The model comes from the environment or not at all.
|
|
90
|
-
if ((fields as Record<string, unknown>).model !== undefined)
|
|
91
|
-
return { ...ADVISOR_OFF, refusal: `settings.advisor.model is not a narrowing; set ${ENV_ADVISOR_MODEL} instead` };
|
|
92
|
-
if (
|
|
93
|
-
fields.timeoutMs !== undefined &&
|
|
94
|
-
(!Number.isInteger(fields.timeoutMs) || (fields.timeoutMs as number) < 1 || (fields.timeoutMs as number) > 30_000)
|
|
95
|
-
)
|
|
96
|
-
return { ...ADVISOR_OFF, refusal: "settings.advisor.timeoutMs must be an integer between 1 and 30000" };
|
|
97
|
-
const model = env[ENV_ADVISOR_MODEL]?.trim();
|
|
98
|
-
const key = env[ENV_ADVISOR_KEY]?.trim();
|
|
99
|
-
if (!key)
|
|
100
|
-
return {
|
|
101
|
-
...ADVISOR_OFF,
|
|
102
|
-
refusal: `settings.advisor is enabled but ${ENV_ADVISOR_KEY} is not set; no advisor is enabled`,
|
|
103
|
-
};
|
|
104
|
-
return {
|
|
105
|
-
enabled: true,
|
|
106
|
-
decider: "jev",
|
|
107
|
-
taskEgress: env[ENV_ADVISOR_TASK_EGRESS]?.trim() === "raw" ? "raw" : "digest",
|
|
108
|
-
...(model ? { model } : {}),
|
|
109
|
-
// Clamped, never raised: a longer bound is not a narrowing either, and a child-writable 30s would be a stall on
|
|
110
|
-
// every delegation.
|
|
111
|
-
...(fields.timeoutMs !== undefined
|
|
112
|
-
? { timeoutMs: Math.min(fields.timeoutMs as number, DEFAULT_ADVICE_TIMEOUT_MS) }
|
|
113
|
-
: {}),
|
|
114
|
-
};
|
|
115
|
-
}
|