pi-warden 0.38.3 → 0.40.2
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 +52 -0
- package/README.md +8 -4
- package/dist/action-guard.d.ts +4 -4
- package/dist/backend.d.ts +4 -7
- package/dist/backend.js +11 -19
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -3
- package/dist/conscience.d.ts +49 -3
- package/dist/conscience.js +157 -99
- package/dist/extension.d.ts +3 -2
- package/dist/extension.js +284 -29
- package/dist/extension.js.bak +307 -30
- package/dist/guard.d.ts +6 -4
- package/dist/guard.js +12 -7
- package/dist/hashing.d.ts +4 -0
- package/dist/hashing.js +22 -0
- package/dist/index-cmd.d.ts +87 -0
- package/dist/index-cmd.js +330 -0
- package/dist/index.d.ts +1 -1
- package/dist/load.d.ts +12 -3
- package/dist/load.js +20 -11
- package/dist/rules-file.d.ts +23 -8
- package/dist/rules-file.js +61 -31
- package/dist/rules.d.ts +13 -1
- package/dist/rules.js +12 -8
- package/dist/shape.js +2 -2
- package/package.json +2 -2
package/dist/conscience.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { choice, noul, score } from "pi-typesafe";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { redact } from "./redact.js";
|
|
4
|
+
import { fileContentHash, toolSourceHash } from "./hashing.js";
|
|
4
5
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
5
6
|
export const SCORE_LEVELS = [
|
|
6
7
|
"No useful contribution to the current request, or conflicts with the supplied constraints.",
|
|
@@ -54,7 +55,7 @@ export function buildBatchQuestions(batch, task, recentContext, activeSkills, su
|
|
|
54
55
|
const idMap = new Map();
|
|
55
56
|
// Shared disposition question
|
|
56
57
|
const dispositionKey = "conscience_disposition";
|
|
57
|
-
questions[dispositionKey] = choice(
|
|
58
|
+
questions[dispositionKey] = choice(`Disposition of the current request. A message that reports status, shares context, or narrates what the user is doing elsewhere, without asking the agent to do anything, is no_gap; select nothing on it.`, {
|
|
58
59
|
advance: "A useful next step can be taken now.",
|
|
59
60
|
awaiting_user: "The assistant has already asked for information and must wait.",
|
|
60
61
|
no_gap: "The request is satisfied with no useful missing step.",
|
|
@@ -63,7 +64,7 @@ export function buildBatchQuestions(batch, task, recentContext, activeSkills, su
|
|
|
63
64
|
// Per-candidate Score questions
|
|
64
65
|
for (const { opaqueId, candidate } of batch) {
|
|
65
66
|
idMap.set(opaqueId, candidate);
|
|
66
|
-
questions[opaqueId] = score(`Judge candidates.${opaqueId} against task
|
|
67
|
+
questions[opaqueId] = score(`Judge candidates.${opaqueId} against what the user is asking the agent to do now in task, not against the subjects the prompt mentions in passing; a candidate that matches words in the prompt but does not serve the actual request belongs at the lowest level. When the request needs information the repository cannot supply, a research-role candidate serves the request; when the request is about the repository's own code or behaviour, an evidence-role candidate does.`, [...SCORE_LEVELS]);
|
|
67
68
|
}
|
|
68
69
|
return { questions, dispositionKey, idMap };
|
|
69
70
|
}
|
|
@@ -73,11 +74,10 @@ export function buildBatchQuestions(batch, task, recentContext, activeSkills, su
|
|
|
73
74
|
export function buildState(task, recentContext, activeSkills, suppliedSkills, batch) {
|
|
74
75
|
const candidates = {};
|
|
75
76
|
for (const { opaqueId, candidate } of batch) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
name: candidate.id,
|
|
79
|
-
description: candidate.description,
|
|
80
|
-
};
|
|
77
|
+
const entry = candidate.indexEntry;
|
|
78
|
+
candidates[opaqueId] = entry
|
|
79
|
+
? { kind: candidate.kind, name: candidate.id, lead: entry.lead, useWhen: entry.useWhen, examples: entry.examples }
|
|
80
|
+
: { kind: candidate.kind, name: candidate.id, description: candidate.description, unindexed: true };
|
|
81
81
|
}
|
|
82
82
|
return {
|
|
83
83
|
task: sanitizeDescription(redact(task)),
|
|
@@ -90,8 +90,27 @@ export function buildState(task, recentContext, activeSkills, suppliedSkills, ba
|
|
|
90
90
|
/**
|
|
91
91
|
* Compute a deterministic hash of the assessment questions for deduplication.
|
|
92
92
|
* Recursively sorts all object keys for canonical form.
|
|
93
|
+
*
|
|
94
|
+
* Opaque candidate ids (c1, c2, …) are positional, not semantic: which batch a prompt happens to
|
|
95
|
+
* produce must not change the hash the activation gate compares. A conscience question set
|
|
96
|
+
* (recognised by the `conscience_disposition` key) is therefore hashed as the disposition question
|
|
97
|
+
* plus one canonical candidate question, keyed `c1` — every candidate question carries the same
|
|
98
|
+
* wording by design (per-candidate state lives in the request state, not the question text), so any
|
|
99
|
+
* one of them is the canonical representative.
|
|
100
|
+
*
|
|
101
|
+
* Why the hash was constant before this change: each assessment issues several requests (one per
|
|
102
|
+
* batch), each with its own question keys, and `assess` kept only the hash of whichever batch wrote
|
|
103
|
+
* `state_.hash` last. Every recorded replay row showed `fb2d35042f667b3c` only because those final
|
|
104
|
+
* batches happened to serialize identically — an accident of batch order, not a guarantee.
|
|
93
105
|
*/
|
|
94
106
|
export function questionHash(questions) {
|
|
107
|
+
if (questions.conscience_disposition) {
|
|
108
|
+
const firstCandidate = Object.entries(questions).find(([key]) => /^c\d+$/.test(key));
|
|
109
|
+
questions = {
|
|
110
|
+
conscience_disposition: questions.conscience_disposition,
|
|
111
|
+
...(firstCandidate ? { [firstCandidate[0]]: firstCandidate[1] } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
95
114
|
const sorted = JSON.parse(JSON.stringify(questions, (_key, value) => {
|
|
96
115
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
97
116
|
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
@@ -113,11 +132,19 @@ function isExcluded(id, exclude) {
|
|
|
113
132
|
}
|
|
114
133
|
return false;
|
|
115
134
|
}
|
|
116
|
-
/** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. */
|
|
117
|
-
export function eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills) {
|
|
135
|
+
/** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. Attaches index entries when available. */
|
|
136
|
+
export function eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills, globalIndex, projectIndex) {
|
|
118
137
|
const candidates = [];
|
|
119
138
|
let skillOverflow = false;
|
|
120
139
|
let toolOverflow = false;
|
|
140
|
+
const findEntry = (name, sourceHash) => {
|
|
141
|
+
const search = (index) => {
|
|
142
|
+
if (!index)
|
|
143
|
+
return undefined;
|
|
144
|
+
return index.entries.find(e => e.name === name && e.sourceHash === sourceHash);
|
|
145
|
+
};
|
|
146
|
+
return search(projectIndex) ?? search(globalIndex);
|
|
147
|
+
};
|
|
121
148
|
if (config.skills.mode !== "off") {
|
|
122
149
|
let count = 0;
|
|
123
150
|
for (const skill of skills) {
|
|
@@ -133,11 +160,14 @@ export function eligibleCandidates(skills, tools, config, activeSkills, supplied
|
|
|
133
160
|
skillOverflow = true;
|
|
134
161
|
break;
|
|
135
162
|
}
|
|
163
|
+
const sourceHash = skill.filePath ? fileContentHash(skill.filePath) : "missing";
|
|
164
|
+
const entry = findEntry(skill.name, sourceHash);
|
|
136
165
|
candidates.push({
|
|
137
166
|
kind: "skill",
|
|
138
167
|
id: skill.name,
|
|
139
168
|
description: sanitizeDescription(skill.description),
|
|
140
169
|
skill,
|
|
170
|
+
...(entry ? { role: entry.role, indexEntry: entry } : {}),
|
|
141
171
|
});
|
|
142
172
|
count++;
|
|
143
173
|
}
|
|
@@ -151,10 +181,13 @@ export function eligibleCandidates(skills, tools, config, activeSkills, supplied
|
|
|
151
181
|
toolOverflow = true;
|
|
152
182
|
break;
|
|
153
183
|
}
|
|
184
|
+
const sourceHash = toolSourceHash(tool.name, tool.description);
|
|
185
|
+
const entry = findEntry(tool.name, sourceHash);
|
|
154
186
|
candidates.push({
|
|
155
187
|
kind: "tool",
|
|
156
188
|
id: tool.name,
|
|
157
189
|
description: sanitizeDescription(tool.description),
|
|
190
|
+
...(entry ? { role: entry.role, indexEntry: entry } : {}),
|
|
158
191
|
});
|
|
159
192
|
count++;
|
|
160
193
|
}
|
|
@@ -241,7 +274,7 @@ function parseScoreAnswer(answer) {
|
|
|
241
274
|
/**
|
|
242
275
|
* Run assessment for the given prompt revision.
|
|
243
276
|
* Pre-measurement: returns trace-only results. No steers, no auto-loading.
|
|
244
|
-
* Issues
|
|
277
|
+
* Issues concurrent requests (up to four in flight), skills first, shared deadline.
|
|
245
278
|
*/
|
|
246
279
|
export async function assess(prompt, recentContext, skills, tools, activeSkills, suppliedSkills, deps) {
|
|
247
280
|
const start = deps.now?.() ?? Date.now();
|
|
@@ -252,120 +285,145 @@ export async function assess(prompt, recentContext, skills, tools, activeSkills,
|
|
|
252
285
|
if (!judge) {
|
|
253
286
|
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "no_consent" };
|
|
254
287
|
}
|
|
255
|
-
const { candidates, skillOverflow, toolOverflow } = eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills);
|
|
288
|
+
const { candidates, skillOverflow, toolOverflow } = eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills, deps.globalIndex, deps.projectIndex);
|
|
256
289
|
if (candidates.length === 0) {
|
|
257
290
|
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "no_match" };
|
|
258
291
|
}
|
|
259
|
-
// Split into skills and tools for sequential assessment
|
|
260
|
-
const skillCandidates = candidates.filter(c => c.kind === "skill");
|
|
261
|
-
const toolCandidates = candidates.filter(c => c.kind === "tool");
|
|
262
|
-
// Cap descriptions
|
|
263
|
-
const cappedSkills = skillCandidates.map(c => capDescription(c));
|
|
264
|
-
const cappedTools = toolCandidates.map(c => capDescription(c));
|
|
265
292
|
const effectiveTimeout = Math.min(config.timeoutMs, sharedTimeoutMs);
|
|
266
293
|
const deadline = start + effectiveTimeout;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let hash = "";
|
|
272
|
-
let bestScored = null;
|
|
273
|
-
// Process skills first, then tools
|
|
274
|
-
const batches = [
|
|
275
|
-
{ items: cappedSkills, overflow: skillOverflow, categoryOverflowReason: "catalog_limit" },
|
|
276
|
-
{ items: cappedTools, overflow: toolOverflow, categoryOverflowReason: "catalog_limit" },
|
|
294
|
+
const MAX_IN_FLIGHT = 4;
|
|
295
|
+
const categoryMeta = [
|
|
296
|
+
{ category: "skill", overflow: skillOverflow },
|
|
297
|
+
{ category: "tool", overflow: toolOverflow },
|
|
277
298
|
];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
299
|
+
const cappedByCategory = {
|
|
300
|
+
skill: candidates.filter(c => c.kind === "skill").map(c => ({ candidate: c, capped: capDescription(c) })),
|
|
301
|
+
tool: candidates.filter(c => c.kind === "tool").map(c => ({ candidate: c, capped: capDescription(c) })),
|
|
302
|
+
};
|
|
303
|
+
const maxCandidatesPerRequest = MAX_QUESTIONS_PER_REQUEST - 1; // -1 for disposition
|
|
304
|
+
const workQueue = [];
|
|
305
|
+
for (const meta of categoryMeta) {
|
|
306
|
+
const items = cappedByCategory[meta.category];
|
|
307
|
+
if (items.length === 0 && !meta.overflow)
|
|
282
308
|
continue;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const chunks = [];
|
|
286
|
-
for (let i = 0; i < batch.items.length; i += maxCandidatesPerRequest) {
|
|
287
|
-
chunks.push(batch.items.slice(i, i + maxCandidatesPerRequest));
|
|
309
|
+
for (let i = 0; i < items.length; i += maxCandidatesPerRequest) {
|
|
310
|
+
workQueue.push({ category: meta.category, items: items.slice(i, i + maxCandidatesPerRequest), overflow: false });
|
|
288
311
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
312
|
+
if (items.length === 0 && meta.overflow) {
|
|
313
|
+
workQueue.push({ category: meta.category, items: [], overflow: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Per-category tracking: a category whose every batch fails or times out is discarded.
|
|
317
|
+
const categoryCompleted = { skill: false, tool: false };
|
|
318
|
+
const categoryFailed = { skill: false, tool: false };
|
|
319
|
+
const judgeError = { value: null };
|
|
320
|
+
const state_ = { disposition: "unclear", pAdvance: 0, hash: "", requestCount: 0, bestScored: null };
|
|
321
|
+
// Execute chunks with at most MAX_IN_FLIGHT concurrent judge calls.
|
|
322
|
+
// Each chunk races its own evaluate against the shared deadline.
|
|
323
|
+
// A settled chunk (success or error) frees its slot for the next waiting chunk.
|
|
324
|
+
let nextIdx = 0;
|
|
325
|
+
const inFlight = [];
|
|
326
|
+
function launchNext() {
|
|
327
|
+
while (inFlight.length < MAX_IN_FLIGHT && nextIdx < workQueue.length) {
|
|
328
|
+
const work = workQueue[nextIdx];
|
|
329
|
+
nextIdx++;
|
|
330
|
+
const p = runChunk(work).finally(() => {
|
|
331
|
+
const pos = inFlight.indexOf(p);
|
|
332
|
+
if (pos !== -1)
|
|
333
|
+
inFlight.splice(pos, 1);
|
|
334
|
+
launchNext();
|
|
335
|
+
});
|
|
336
|
+
inFlight.push(p);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function runChunk(work) {
|
|
340
|
+
if (work.items.length === 0) {
|
|
341
|
+
if (work.overflow)
|
|
342
|
+
categoryCompleted[work.category] = true;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const opaqueBatch = assignOpaqueIds(work.items.map(e => e.capped.candidate));
|
|
346
|
+
const { questions, dispositionKey } = buildBatchQuestions(opaqueBatch, prompt, recentContext, activeSkills, suppliedSkills);
|
|
347
|
+
state_.hash = questionHash(questions);
|
|
348
|
+
const state = buildState(prompt, recentContext, activeSkills, suppliedSkills, opaqueBatch);
|
|
349
|
+
const remaining = deadline - (deps.now?.() ?? Date.now());
|
|
350
|
+
if (remaining <= 0) {
|
|
351
|
+
categoryFailed[work.category] = true;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let answers;
|
|
355
|
+
const j = judge;
|
|
356
|
+
try {
|
|
357
|
+
const result = await Promise.race([
|
|
358
|
+
j.evaluate({ state, questions }),
|
|
359
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), remaining)),
|
|
360
|
+
]);
|
|
361
|
+
answers = result.answers;
|
|
362
|
+
state_.requestCount++;
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
categoryFailed[work.category] = true;
|
|
366
|
+
judgeError.value = { message: err instanceof Error ? err.message : String(err), category: classifyError(err) };
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
categoryCompleted[work.category] = true;
|
|
370
|
+
if (state_.disposition === "unclear") {
|
|
371
|
+
const dispRaw = parseChoiceAnswer(answers[dispositionKey]);
|
|
372
|
+
if (dispRaw) {
|
|
373
|
+
state_.disposition = dispRaw.label;
|
|
374
|
+
state_.pAdvance = dispRaw.pAdvance;
|
|
317
375
|
}
|
|
318
|
-
|
|
319
|
-
|
|
376
|
+
}
|
|
377
|
+
if (state_.disposition !== "advance")
|
|
378
|
+
return;
|
|
379
|
+
for (const { opaqueId, candidate } of opaqueBatch) {
|
|
380
|
+
const answer = parseScoreAnswer(answers[opaqueId]);
|
|
381
|
+
if (!answer)
|
|
320
382
|
continue;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!bestScored || pUseful > bestScored.usefulness ||
|
|
329
|
-
(pUseful === bestScored.usefulness && answer.level > bestScored.level) ||
|
|
330
|
-
(pUseful === bestScored.usefulness && answer.level === bestScored.level &&
|
|
331
|
-
(candidate.kind === "skill" && bestScored.candidate.kind !== "skill" ||
|
|
332
|
-
(candidate.kind === bestScored.candidate.kind && candidate.id < bestScored.candidate.id)))) {
|
|
333
|
-
bestScored = { candidate, usefulness: pUseful, level: answer.level };
|
|
334
|
-
}
|
|
383
|
+
const pUseful = (answer.probabilities[2] ?? 0) + (answer.probabilities[3] ?? 0);
|
|
384
|
+
if (!state_.bestScored || pUseful > state_.bestScored.usefulness ||
|
|
385
|
+
(pUseful === state_.bestScored.usefulness && answer.level > state_.bestScored.level) ||
|
|
386
|
+
(pUseful === state_.bestScored.usefulness && answer.level === state_.bestScored.level &&
|
|
387
|
+
(candidate.kind === "skill" && state_.bestScored.candidate.kind !== "skill" ||
|
|
388
|
+
(candidate.kind === state_.bestScored.candidate.kind && candidate.id < state_.bestScored.candidate.id)))) {
|
|
389
|
+
state_.bestScored = { candidate, usefulness: pUseful, level: answer.level };
|
|
335
390
|
}
|
|
336
391
|
}
|
|
337
|
-
if (batch.items.length > 0 || batch.overflow) {
|
|
338
|
-
skillAssessmentComplete = true;
|
|
339
|
-
}
|
|
340
|
-
// If skills had catalog_limit, do not proceed to tools with partial results
|
|
341
|
-
if (batch.overflow && batch.categoryOverflowReason === "catalog_limit") {
|
|
342
|
-
// Per spec: "if a required batch for a category fails or times out, discard that category's selection"
|
|
343
|
-
// But catalog_limit means the category is incomplete, not failed. Selection may use the other category.
|
|
344
|
-
// Record catalog_limit but continue to tools.
|
|
345
|
-
}
|
|
346
392
|
}
|
|
347
|
-
|
|
393
|
+
launchNext();
|
|
394
|
+
await Promise.all(inFlight);
|
|
395
|
+
const elapsedMs = (deps.now?.() ?? Date.now()) - start;
|
|
396
|
+
const { disposition, pAdvance, hash, requestCount, bestScored } = state_;
|
|
397
|
+
const skillUsable = categoryCompleted.skill && !categoryFailed.skill;
|
|
398
|
+
const toolUsable = categoryCompleted.tool && !categoryFailed.tool;
|
|
399
|
+
// When every category failed, the judge is broken — surface the error, not no_match.
|
|
400
|
+
if (!skillUsable && !toolUsable && judgeError.value) {
|
|
401
|
+
return { disposition: "unclear", selected: null, usefulness: 0, pAdvance: 0, questionHash: state_.hash, elapsedMs, requestCount: state_.requestCount, skipReason: "error", errorCategory: judgeError.value.category };
|
|
402
|
+
}
|
|
348
403
|
if (disposition !== "advance") {
|
|
349
404
|
const skipReason = disposition === "awaiting_user" ? "awaiting_user" : undefined;
|
|
350
|
-
const result = { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs
|
|
405
|
+
const result = { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs, requestCount };
|
|
351
406
|
if (skipReason)
|
|
352
407
|
result.skipReason = skipReason;
|
|
353
408
|
return result;
|
|
354
409
|
}
|
|
355
|
-
// No candidate scored
|
|
356
410
|
if (!bestScored) {
|
|
357
411
|
const skipReason = skillOverflow ? "catalog_limit" : "no_match";
|
|
358
|
-
return { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs
|
|
412
|
+
return { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs, requestCount, skipReason };
|
|
413
|
+
}
|
|
414
|
+
if (bestScored.candidate.kind === "skill" && !skillUsable) {
|
|
415
|
+
return { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs, requestCount, skipReason: "no_match" };
|
|
416
|
+
}
|
|
417
|
+
if (bestScored.candidate.kind === "tool" && !toolUsable) {
|
|
418
|
+
return { disposition, selected: null, usefulness: 0, pAdvance, questionHash: hash, elapsedMs, requestCount, skipReason: "no_match" };
|
|
359
419
|
}
|
|
360
|
-
// Threshold gate: both P(useful now) and P(advance) must pass
|
|
361
420
|
const passesUsefulness = bestScored.usefulness >= config.recommendThreshold;
|
|
362
|
-
const passesAdvance = pAdvance >= config.
|
|
421
|
+
const passesAdvance = pAdvance >= config.advanceThreshold;
|
|
363
422
|
if (!passesUsefulness || !passesAdvance) {
|
|
364
|
-
return { disposition, selected: null, usefulness: bestScored.usefulness, pAdvance, questionHash: hash, elapsedMs
|
|
423
|
+
return { disposition, selected: null, usefulness: bestScored.usefulness, pAdvance, questionHash: hash, elapsedMs, requestCount, skipReason: "below_threshold" };
|
|
365
424
|
}
|
|
366
|
-
// Catalog completeness gate: do not select from an incomplete category
|
|
367
425
|
if (bestScored.candidate.kind === "skill" && skillOverflow) {
|
|
368
|
-
return { disposition, selected: null, usefulness: bestScored.usefulness, pAdvance, questionHash: hash, elapsedMs
|
|
426
|
+
return { disposition, selected: null, usefulness: bestScored.usefulness, pAdvance, questionHash: hash, elapsedMs, requestCount, skipReason: "catalog_limit" };
|
|
369
427
|
}
|
|
370
428
|
return {
|
|
371
429
|
disposition,
|
|
@@ -373,7 +431,7 @@ export async function assess(prompt, recentContext, skills, tools, activeSkills,
|
|
|
373
431
|
usefulness: bestScored.usefulness,
|
|
374
432
|
pAdvance,
|
|
375
433
|
questionHash: hash,
|
|
376
|
-
elapsedMs
|
|
434
|
+
elapsedMs,
|
|
377
435
|
requestCount,
|
|
378
436
|
};
|
|
379
437
|
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
4
|
import type { ShapeResult } from "./shape.js";
|
|
5
|
-
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name
|
|
5
|
+
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name, role, lead, useWhen, examples when an index entry matches; bare description otherwise; full skill instructions never go to Jev). The index is built locally by the session model; only sanitized entries reach Jev; advertised locations never do. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
6
|
/** Which guard spent the user's attention. The status line reports one count per guard. */
|
|
7
7
|
export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "prose" | "runaway" | "subagent" | "conscience";
|
|
8
8
|
/**
|
|
@@ -33,5 +33,6 @@ export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[
|
|
|
33
33
|
* failed until Pi was restarted. The sections this build reads are therefore checked here as well, in the module that reads them.
|
|
34
34
|
*/
|
|
35
35
|
export declare function guardCurrentSections(result: ShapeResult): ShapeResult;
|
|
36
|
-
/**
|
|
36
|
+
/** Test-only: set the index-running state and write paths for the action guard bypass. */
|
|
37
|
+
export declare function _testSetIndexRunning(running: boolean, paths?: string[]): void;
|
|
37
38
|
export default function wardenExtension(pi: ExtensionAPI): void;
|