specpi 0.30.0 → 0.31.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 +34 -0
- package/NPM_RELEASE.md +2 -2
- package/README.md +23 -15
- package/SECURITY_MODEL.md +14 -10
- package/THIRD_PARTY.md +1 -1
- package/extensions/jev-advisor/broker.mjs +117 -10
- package/extensions/jev-advisor/client.mjs +33 -10
- package/extensions/jev-advisor/config.mjs +36 -36
- package/extensions/jev-advisor/consent.mjs +19 -7
- package/extensions/jev-advisor/gate.mjs +26 -18
- package/extensions/jev-advisor/index.ts +241 -288
- package/extensions/jev-advisor/layer.mjs +1 -1
- package/extensions/jev-advisor/ledger.mjs +16 -2
- package/extensions/jev-advisor/questions/capabilities.mjs +3 -13
- package/extensions/jev-advisor/questions/gap.mjs +61 -88
- package/extensions/jev-advisor/questions/retention.mjs +11 -19
- package/extensions/jev-advisor/questions/sources.mjs +27 -56
- package/extensions/jev-advisor/questions/untrusted.mjs +30 -6
- package/extensions/jev-advisor/sanitize.mjs +0 -0
- package/extensions/tool-wishlist/core.mjs +86 -0
- package/extensions/tool-wishlist/index.ts +81 -8
- package/extensions/workflow-controls/index.ts +23 -0
- package/package.json +2 -2
- package/scripts/specpi.mjs +0 -1
- package/extensions/jev-advisor/questions/progress.mjs +0 -195
|
@@ -7,10 +7,10 @@ import { consentPath, granted, revokeConsent } from "./consent.mjs";
|
|
|
7
7
|
import { createBroker } from "./broker.mjs";
|
|
8
8
|
import { ledgerPath, read as readLedger } from "./ledger.mjs";
|
|
9
9
|
import { usagePath } from "./usage.mjs";
|
|
10
|
+
import { compact } from "./sanitize.mjs";
|
|
10
11
|
import * as retention from "./questions/retention.mjs";
|
|
11
12
|
import * as gap from "./questions/gap.mjs";
|
|
12
13
|
import * as sources from "./questions/sources.mjs";
|
|
13
|
-
import * as progress from "./questions/progress.mjs";
|
|
14
14
|
import * as untrusted from "./questions/untrusted.mjs";
|
|
15
15
|
import * as capabilities from "./questions/capabilities.mjs";
|
|
16
16
|
|
|
@@ -27,37 +27,49 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
27
27
|
const broker = createBroker({ loadSettings: () => settings });
|
|
28
28
|
const recent: { tool: string; outcome: string }[] = [];
|
|
29
29
|
let objective = "";
|
|
30
|
+
let currentRequest = "";
|
|
31
|
+
let taskIdentity = "";
|
|
32
|
+
let taskGeneration = 0;
|
|
33
|
+
let sessionActive = false;
|
|
34
|
+
|
|
35
|
+
const refreshObjective = async (ctx: ExtensionContext) => {
|
|
36
|
+
if (!sessionActive) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
40
|
+
const generation = taskGeneration;
|
|
41
|
+
const replies: Promise<{ objective: string; digest: string } | undefined>[] = [];
|
|
42
|
+
let contract;
|
|
43
|
+
try {
|
|
44
|
+
if (settings.master) {
|
|
45
|
+
pi.events.emit("specpi:task-objective", {
|
|
46
|
+
ctx,
|
|
47
|
+
reply: (value: Promise<{ objective: string; digest: string } | undefined>) => replies.push(value),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
contract = replies.length === 1 ? await replies[0] : undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
// A missing or unavailable workflow owner must not fail an otherwise valid tool.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (generation !== taskGeneration) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const next = compact(contract?.objective || currentRequest, 180);
|
|
61
|
+
const identity = contract?.digest ?? currentRequest;
|
|
62
|
+
if (objective !== next || taskIdentity !== identity) {
|
|
63
|
+
objective = next;
|
|
64
|
+
taskIdentity = identity;
|
|
65
|
+
taskGeneration += 1;
|
|
66
|
+
recent.length = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
58
70
|
};
|
|
59
71
|
|
|
60
|
-
const enabled = (system: string) => settings.master && settings.systems[system] === true;
|
|
72
|
+
const enabled = (system: string) => sessionActive && settings.master && settings.systems[system] === true;
|
|
61
73
|
|
|
62
74
|
// System 6: decide once, before the first provider request, whether this session will need a
|
|
63
75
|
// withdrawn tool group -- and offer it now rather than at turn 6.
|
|
@@ -80,14 +92,17 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
pi.on("session_start", () => {
|
|
95
|
+
sessionActive = true;
|
|
83
96
|
settings = loadSettings();
|
|
84
97
|
if (!settings.startup) {
|
|
85
98
|
settings = { ...settings, master: false };
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
broker.reset();
|
|
102
|
+
objective = "";
|
|
103
|
+
currentRequest = "";
|
|
104
|
+
taskGeneration += 1;
|
|
89
105
|
recent.length = 0;
|
|
90
|
-
resetHistory();
|
|
91
106
|
capabilityAsked = false;
|
|
92
107
|
capabilityDeclined.clear();
|
|
93
108
|
});
|
|
@@ -96,24 +111,36 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
96
111
|
// finish, not reset: the counts are published once more as an ended session so anything
|
|
97
112
|
// reading them from outside -- SpecPi Chat's panel, most of all -- shows what the session
|
|
98
113
|
// actually spent rather than a zeroed live one.
|
|
114
|
+
sessionActive = false;
|
|
99
115
|
broker.finish();
|
|
116
|
+
objective = "";
|
|
117
|
+
currentRequest = "";
|
|
118
|
+
taskGeneration += 1;
|
|
100
119
|
recent.length = 0;
|
|
101
|
-
resetHistory();
|
|
102
120
|
});
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
// The workflow owner reads its active contract, not rendered Markdown. Without one, use only
|
|
123
|
+
// the bounded current request; never recover an objective by searching stored conversations.
|
|
124
|
+
pi.on("before_agent_start", async (event: any, ctx: ExtensionContext) => {
|
|
125
|
+
currentRequest = compact(event?.prompt ?? "", 180);
|
|
126
|
+
taskGeneration += 1;
|
|
127
|
+
pi.events.emit("specpi:task-changing");
|
|
128
|
+
await refreshObjective(ctx);
|
|
107
129
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
objective = match[1];
|
|
130
|
+
pi.on("input", async (event: any, ctx: ExtensionContext) => {
|
|
131
|
+
if (event.streamingBehavior === "steer" && event.source !== "extension") {
|
|
132
|
+
currentRequest = compact(event.text ?? "", 180);
|
|
133
|
+
taskGeneration += 1;
|
|
134
|
+
pi.events.emit("specpi:task-changing");
|
|
135
|
+
await refreshObjective(ctx);
|
|
115
136
|
}
|
|
116
137
|
});
|
|
138
|
+
pi.on("session_tree", () => {
|
|
139
|
+
objective = "";
|
|
140
|
+
currentRequest = "";
|
|
141
|
+
taskGeneration += 1;
|
|
142
|
+
recent.length = 0;
|
|
143
|
+
});
|
|
117
144
|
|
|
118
145
|
// Once per session, whatever the answer: asking later would be the mid-session flip the probe
|
|
119
146
|
// priced at three times the cost of doing it now.
|
|
@@ -166,8 +193,12 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
166
193
|
return;
|
|
167
194
|
}
|
|
168
195
|
|
|
169
|
-
const
|
|
196
|
+
const generation = taskGeneration;
|
|
197
|
+
await broker.request({
|
|
170
198
|
system: "capability",
|
|
199
|
+
isCurrent: () => generation === taskGeneration,
|
|
200
|
+
profile: "capability",
|
|
201
|
+
trigger: local.reasons,
|
|
171
202
|
state: capabilities.buildInput({
|
|
172
203
|
prompt: event?.prompt,
|
|
173
204
|
reasons: local.reasons,
|
|
@@ -180,44 +211,51 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
180
211
|
decide: (answers: any) => {
|
|
181
212
|
const advice = capabilities.decide(answers, available);
|
|
182
213
|
|
|
183
|
-
return {
|
|
214
|
+
return { decision: advice };
|
|
184
215
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"info",
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
for (const id of advice.propose) {
|
|
201
|
-
const capability = table.findCapability(id);
|
|
202
|
-
if (!capability || capabilityDeclined.has(id)) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
216
|
+
apply: async (advice: any) => {
|
|
217
|
+
const effects: string[] = [];
|
|
218
|
+
if (advice.suggestDelegation) {
|
|
219
|
+
// A suggestion, never an activation: delegation binds a model and a host and has
|
|
220
|
+
// its own command, which is why the capability table deliberately omits it.
|
|
221
|
+
ctx.ui.notify(
|
|
222
|
+
"Jev: this looks like a question a delegated read-only session could answer over many files. Run /delegate on if you want it.",
|
|
223
|
+
"info",
|
|
224
|
+
);
|
|
225
|
+
effects.push("notification");
|
|
226
|
+
}
|
|
205
227
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
for (const id of advice.propose) {
|
|
229
|
+
const capability = table.findCapability(id);
|
|
230
|
+
if (!capability || capabilityDeclined.has(id)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const pending = table.missingTools(pi.getActiveTools(), capability);
|
|
235
|
+
// The same confirmation `request_capability` shows, pre-filled and moved to turn 0.
|
|
236
|
+
// Authority is unchanged: the human still decides, and declining is remembered so
|
|
237
|
+
// nothing asks twice in one session.
|
|
238
|
+
const accepted = await ctx.ui.confirm(
|
|
239
|
+
`Allow ${capability.label} for this session?`,
|
|
240
|
+
`Jev expects this request to ${capability.summary}, from the request itself rather than from anything it has done yet.\n\nThis offers ${pending.length} tool${pending.length === 1 ? "" : "s"} for the rest of this session and adds ${capability.schemaCost}. Accepting now is materially cheaper than accepting later: activating it mid-session also discards the cached prompt prefix, which measured about 20% of a mid-length attempt's cost. Withdraw it with ${capability.command} off.`,
|
|
241
|
+
);
|
|
242
|
+
effects.push("capability-proposal");
|
|
243
|
+
if (generation !== taskGeneration || !enabled("capability") || ctx.signal?.aborted) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!accepted) {
|
|
248
|
+
capabilityDeclined.add(id);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
syncActiveTools(pi, capability.tools, true);
|
|
253
|
+
effects.push("capability-activated");
|
|
254
|
+
}
|
|
218
255
|
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
return { applied: effects.length > 0, effects };
|
|
257
|
+
},
|
|
258
|
+
});
|
|
221
259
|
} catch {
|
|
222
260
|
// Nothing here may prevent a session from starting.
|
|
223
261
|
}
|
|
@@ -226,27 +264,13 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
226
264
|
// System 1: condense a spent tool result before it is appended. Doing this after the fact would
|
|
227
265
|
// rewrite a cached prefix; on arrival it never touches one.
|
|
228
266
|
pi.on("tool_result", async (event: any, ctx: ExtensionContext) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// exactly the short repeated failures system 5 exists to notice.
|
|
232
|
-
if (event?.isError === true) {
|
|
233
|
-
history.consecutiveErrors += 1;
|
|
234
|
-
history.errors.push(retention.resultText(event).slice(0, 200));
|
|
235
|
-
if (history.errors.length > HISTORY_WINDOW) {
|
|
236
|
-
history.errors.shift();
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
history.consecutiveErrors = 0;
|
|
240
|
-
if (progress.MUTATING_TOOLS.has(event?.toolName)) {
|
|
241
|
-
history.filesChanged += 1;
|
|
242
|
-
history.changedThisTurn = true;
|
|
243
|
-
}
|
|
267
|
+
if (!(await refreshObjective(ctx))) {
|
|
268
|
+
return;
|
|
244
269
|
}
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// their question sets said history was what they weighed.
|
|
271
|
+
const generation = taskGeneration;
|
|
272
|
+
// Every result, not only the ones retention asked about, so retention's own view of recent
|
|
273
|
+
// work does not depend on which results it happened to be interested in.
|
|
250
274
|
recent.push({ tool: String(event?.toolName ?? ""), outcome: event?.isError === true ? "error" : "ok" });
|
|
251
275
|
if (recent.length > MAX_RECENT) {
|
|
252
276
|
recent.shift();
|
|
@@ -271,6 +295,8 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
271
295
|
// interested. System 7's own budget therefore only binds when retention is off or
|
|
272
296
|
// the result was too small for it.
|
|
273
297
|
system: wantRetention ? "retention" : "untrusted",
|
|
298
|
+
profile: wantRetention ? "retention" : "untrusted",
|
|
299
|
+
isCurrent: () => generation === taskGeneration,
|
|
274
300
|
state: retention.buildInput({ event, objective, recent }),
|
|
275
301
|
questions: {
|
|
276
302
|
...(wantRetention ? retention.questions() : {}),
|
|
@@ -283,17 +309,22 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
283
309
|
// because its length is the saving: computing it a second time to measure it would
|
|
284
310
|
// be the measurement inventing its own number.
|
|
285
311
|
decide: (answers: any) => {
|
|
286
|
-
|
|
287
|
-
? retention.decide(answers)
|
|
288
|
-
: { elide: false, reason: "retention-off" };
|
|
312
|
+
let verdict = wantRetention ? retention.decide(answers) : { elide: false, reason: "retention-off" };
|
|
289
313
|
const flagged = wantUntrusted && untrusted.decide(answers).banner;
|
|
290
314
|
// Order matters: shorten first, then mark. A banner belongs at the top of
|
|
291
315
|
// whatever the model is actually going to read.
|
|
292
316
|
const body = verdict.elide ? retention.digest(text, { tool: event.toolName, bytes }) : text;
|
|
293
|
-
|
|
317
|
+
let replacement = flagged ? untrusted.mark(body) : body;
|
|
318
|
+
if (verdict.elide && Buffer.byteLength(replacement, "utf8") >= bytes) {
|
|
319
|
+
verdict = { elide: false, reason: "no-byte-saving" };
|
|
320
|
+
replacement = flagged ? untrusted.mark(text) : text;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const marked = flagged && replacement !== body;
|
|
294
324
|
|
|
295
325
|
return {
|
|
296
|
-
applied: verdict.elide ||
|
|
326
|
+
applied: verdict.elide || marked,
|
|
327
|
+
effects: [...(verdict.elide ? ["elision"] : []), ...(marked ? ["warning"] : [])],
|
|
297
328
|
savedBytes: verdict.elide ? bytes - Buffer.byteLength(replacement, "utf8") : 0,
|
|
298
329
|
// retention.decide already names why it declined; carrying that into the
|
|
299
330
|
// ledger is what makes "asked and did nothing" diagnosable later.
|
|
@@ -334,213 +365,134 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
334
365
|
});
|
|
335
366
|
|
|
336
367
|
pi.on("tool_call", async (event: any, ctx: ExtensionContext) => {
|
|
337
|
-
|
|
338
|
-
history.tools.push(event.toolName);
|
|
339
|
-
if (history.signatures.length > HISTORY_WINDOW) {
|
|
340
|
-
history.signatures.shift();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (history.tools.length > HISTORY_WINDOW) {
|
|
344
|
-
history.tools.shift();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// System 2: triage a capability gap before tool-wishlist writes it. `event.input` is
|
|
348
|
-
// documented as mutable, so this patches the report in place rather than duplicating any
|
|
349
|
-
// of the wishlist's authority logic. Nothing here records a decision.
|
|
350
|
-
if (event.toolName === "report_capability_gap" && enabled("gap")) {
|
|
351
|
-
try {
|
|
352
|
-
const result = await broker.request({
|
|
353
|
-
system: "gap",
|
|
354
|
-
state: gap.buildInput({ gap: event.input, existing: [] }),
|
|
355
|
-
questions: gap.questions({ gap: event.input, existing: [] }),
|
|
356
|
-
ctx,
|
|
357
|
-
root: ctx.cwd,
|
|
358
|
-
decide: (answers: any) => {
|
|
359
|
-
const built = gap.decide(answers);
|
|
360
|
-
// Exactly the conditions the caller applies below, so the ledger line says
|
|
361
|
-
// what happened rather than what was available. A gated answer that
|
|
362
|
-
// duplicates a field the model already filled in changed nothing.
|
|
363
|
-
const changes =
|
|
364
|
-
(built.blockForSanitization ? 1 : 0) +
|
|
365
|
-
(built.canonicalKey && typeof event.input?.canonicalKey !== "string" ? 1 : 0) +
|
|
366
|
-
(built.suggestedFix && !event.input?.suggestedFix ? 1 : 0) +
|
|
367
|
-
(built.independentImpact ? 1 : 0);
|
|
368
|
-
|
|
369
|
-
return { applied: changes > 0, decision: built };
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
if (!result.ok) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const advice = result.decision;
|
|
377
|
-
if (advice.blockForSanitization) {
|
|
378
|
-
return {
|
|
379
|
-
block: true,
|
|
380
|
-
reason: "This report appears to contain a credential, an absolute path or other machine-specific detail. Rewrite it with the specifics removed and report it again.",
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (advice.canonicalKey && typeof event.input?.canonicalKey !== "string") {
|
|
385
|
-
event.input.canonicalKey = advice.canonicalKey;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (advice.suggestedFix && !event.input?.suggestedFix) {
|
|
389
|
-
event.input.suggestedFix = advice.suggestedFix;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Recorded alongside the model's own claim, never over it: a human reading the
|
|
393
|
-
// wishlist should still see what was originally reported.
|
|
394
|
-
if (advice.independentImpact) {
|
|
395
|
-
event.input.independentImpact = advice.independentImpact;
|
|
396
|
-
}
|
|
397
|
-
} catch {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
368
|
+
const generation = taskGeneration;
|
|
403
369
|
|
|
404
370
|
// System 4: order the sources a delegation batch will snapshot. Ordering only — the same
|
|
405
371
|
// set is frozen either way, but a child pages through `list_sources` in this order.
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
decide: (answers: any) => {
|
|
422
|
-
const built = sources.decide(answers, candidates);
|
|
423
|
-
const order = built.ordered.map((item: any) => item.path);
|
|
424
|
-
// An ungated run returns the caller's own order, which is not a change and
|
|
425
|
-
// must not be recorded as one.
|
|
426
|
-
const moved = order.some((item: string, index: number) => item !== candidates[index]?.path);
|
|
427
|
-
|
|
428
|
-
return { applied: moved, decision: { ranked: built, ordered: order } };
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
if (!result.ok) {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
372
|
+
if (
|
|
373
|
+
event.toolName === "delegate" &&
|
|
374
|
+
enabled("sources") &&
|
|
375
|
+
event.input?.operation === "run" &&
|
|
376
|
+
Array.isArray(event.input?.packet?.jobs)
|
|
377
|
+
) {
|
|
378
|
+
for (const job of event.input.packet.jobs) {
|
|
379
|
+
try {
|
|
380
|
+
if (
|
|
381
|
+
!Array.isArray(job.sources) ||
|
|
382
|
+
job.sources.length < 2 ||
|
|
383
|
+
!job.sources.every((item: unknown) => typeof item === "string")
|
|
384
|
+
) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
434
387
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
388
|
+
const original = [...job.sources];
|
|
389
|
+
const candidates = original.map((item: string) => ({ path: item }));
|
|
390
|
+
await broker.request({
|
|
391
|
+
system: "sources",
|
|
392
|
+
profile: "sources",
|
|
393
|
+
isCurrent: () => generation === taskGeneration && sessionActive,
|
|
394
|
+
state: sources.buildInput({ question: job.question, mode: job.mode, candidates }),
|
|
395
|
+
questions: sources.questions({ candidates }),
|
|
396
|
+
ctx,
|
|
397
|
+
root: ctx.cwd,
|
|
398
|
+
decide: (answers: any) => ({ decision: sources.decide(answers, candidates) }),
|
|
399
|
+
apply: (ranked: any, { recordEffect }: any) => {
|
|
400
|
+
const ordered = ranked.ordered.map((item: any) => item.path);
|
|
401
|
+
const moved = ordered.some((item: string, index: number) => item !== original[index]);
|
|
402
|
+
// No deduplication: even multiplicity and ungated positions are preserved.
|
|
403
|
+
job.sources = ordered;
|
|
404
|
+
const effects = moved ? ["sources-reordered"] : [];
|
|
405
|
+
if (moved) {
|
|
406
|
+
recordEffect("sources-reordered");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (ranked.notWorthDelegating && ctx.hasUI) {
|
|
410
|
+
ctx.ui.notify(
|
|
411
|
+
`Jev rates this ${job.mode} job a poor fit for delegation. Running anyway; ${ordered.length} sources remain selected.`,
|
|
412
|
+
"warning",
|
|
413
|
+
);
|
|
414
|
+
effects.push("warning");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { applied: effects.length > 0, effects };
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
// One unavailable job must not prevent the next job or the delegation call.
|
|
451
422
|
}
|
|
452
|
-
} catch {
|
|
453
|
-
return;
|
|
454
423
|
}
|
|
455
424
|
}
|
|
456
425
|
});
|
|
457
426
|
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// answer. This one acts on the next turn, and at roughly 300ms a call, awaiting it on a
|
|
463
|
-
// thrashing session would add seconds to an attempt to deliver advice that could not have
|
|
464
|
-
// changed the turn it was asked during.
|
|
465
|
-
pi.on("turn_end", (event: any, ctx: ExtensionContext) => {
|
|
466
|
-
// Kept whether or not the system is on, so switching it on mid-session does not start from
|
|
467
|
-
// a blank history and immediately look healthy.
|
|
468
|
-
history.turnsSinceChange = history.changedThisTurn ? 0 : history.turnsSinceChange + 1;
|
|
469
|
-
if (!enabled("progress") || history.nudged) {
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const local = progress.suspicious(history);
|
|
474
|
-
if (!local.ask) {
|
|
427
|
+
// Wishlist owns collection and persistence. This handshake runs only after its local consent
|
|
428
|
+
// gate; advisory fields never enter model-authored tool arguments or its authority decisions.
|
|
429
|
+
pi.events.on("specpi:gap-triage", (request: any) => {
|
|
430
|
+
if (!enabled("gap")) {
|
|
475
431
|
return;
|
|
476
432
|
}
|
|
477
433
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
434
|
+
request.reply(
|
|
435
|
+
(async () => {
|
|
436
|
+
const existing = gap.shortlist(request.existing(), request.gap);
|
|
437
|
+
let stored: any;
|
|
438
|
+
let blocked = false;
|
|
439
|
+
let storageError: unknown;
|
|
483
440
|
const result = await broker.request({
|
|
484
|
-
system: "
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
441
|
+
system: "gap",
|
|
442
|
+
profile: "gap",
|
|
443
|
+
state: gap.buildInput({ gap: request.gap, existing }),
|
|
444
|
+
questions: gap.questions({ existing }),
|
|
445
|
+
ctx: request.ctx,
|
|
446
|
+
root: request.ctx.cwd,
|
|
447
|
+
signal: request.signal,
|
|
448
|
+
isCurrent: request.isCurrent,
|
|
449
|
+
decide: (answers: any) => ({ decision: gap.decide(answers, existing) }),
|
|
450
|
+
apply: async (advice: any, { isCurrent, recordEffect }: any) => {
|
|
451
|
+
if (advice.blockForSanitization) {
|
|
452
|
+
blocked = true;
|
|
453
|
+
|
|
454
|
+
return { applied: true, effects: ["report-blocked"] };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
stored = await request.record(advice, {
|
|
459
|
+
isCurrent,
|
|
460
|
+
onRecorded: ({ assessmentRecorded }: any) => {
|
|
461
|
+
if (assessmentRecorded) {
|
|
462
|
+
recordEffect("assessment-recorded");
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
storageError = error;
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
491
470
|
|
|
492
471
|
return {
|
|
493
|
-
applied:
|
|
494
|
-
|
|
495
|
-
decision: advice,
|
|
472
|
+
applied: stored.assessmentRecorded,
|
|
473
|
+
effects: stored.assessmentRecorded ? ["assessment-recorded"] : [],
|
|
496
474
|
};
|
|
497
475
|
},
|
|
498
476
|
});
|
|
499
|
-
if (
|
|
500
|
-
|
|
477
|
+
if (storageError) {
|
|
478
|
+
throw storageError;
|
|
501
479
|
}
|
|
502
480
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
// to a prefix that is cached behind it by the time anyone regrets it.
|
|
506
|
-
history.nudged = true;
|
|
507
|
-
if (ctx.hasUI) {
|
|
508
|
-
ctx.ui.notify(
|
|
509
|
-
result.decision.needsHuman
|
|
510
|
-
? `Jev progress check: this session looks blocked on something only you can answer. ${result.decision.nudge}`
|
|
511
|
-
: `Jev progress check: ${result.decision.nudge}`,
|
|
512
|
-
"warning",
|
|
513
|
-
);
|
|
481
|
+
if (blocked) {
|
|
482
|
+
return { blocked: true };
|
|
514
483
|
}
|
|
515
484
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// has exactly one user prompt, so a nextTurn message would never be delivered -- in
|
|
519
|
-
// precisely the case the argument for this system rests on, a headless attempt
|
|
520
|
-
// burning its wall clock. "steer" is delivered after the current tool calls finish
|
|
521
|
-
// and before the next model request, which is the same append at the same boundary
|
|
522
|
-
// and is actually read. triggerTurn is left off so this can never add a turn.
|
|
523
|
-
// Suppressed when the session is blocked on something only a person can answer:
|
|
524
|
-
// steering a model past a missing credential costs a turn to say nothing.
|
|
525
|
-
if (
|
|
526
|
-
settings.progressNudge === "message" &&
|
|
527
|
-
!result.decision.needsHuman &&
|
|
528
|
-
typeof pi.sendMessage === "function"
|
|
529
|
-
) {
|
|
530
|
-
pi.sendMessage(
|
|
531
|
-
{
|
|
532
|
-
customType: "specpi-jev-progress",
|
|
533
|
-
content: result.decision.nudge,
|
|
534
|
-
display: true,
|
|
535
|
-
details: { mode: result.decision.mode, reasons: local.reasons },
|
|
536
|
-
},
|
|
537
|
-
{ deliverAs: "steer" },
|
|
538
|
-
);
|
|
485
|
+
if (stored) {
|
|
486
|
+
return { stored };
|
|
539
487
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
488
|
+
|
|
489
|
+
if (["session-changed", "context-changed"].includes(result.reason)) {
|
|
490
|
+
return { cancelled: true };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return undefined;
|
|
494
|
+
})(),
|
|
495
|
+
);
|
|
544
496
|
});
|
|
545
497
|
|
|
546
498
|
pi.registerCommand("jev", {
|
|
@@ -696,9 +648,10 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
696
648
|
return;
|
|
697
649
|
}
|
|
698
650
|
|
|
699
|
-
const lines = entries.map(
|
|
700
|
-
|
|
701
|
-
`${entry.at} ${entry.system}
|
|
651
|
+
const lines = entries.map((entry: any) =>
|
|
652
|
+
entry.sent === false
|
|
653
|
+
? `${entry.at} ${entry.system} local abstention: ${entry.reason}`
|
|
654
|
+
: `${entry.at} ${entry.system} ${entry.stateBytes}B ${entry.ok ? `${entry.latencyMs}ms` : entry.reason} ${String(entry.payloadSha256 ?? "").slice(0, 12)} [${(entry.questionKeys ?? []).join(", ")}]`,
|
|
702
655
|
);
|
|
703
656
|
ctx.ui.notify(`${lines.join("\n")}\n\nLedger: ${ledgerPath()}`, "info");
|
|
704
657
|
|