pi-warden 1.0.0 → 1.4.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +3 -0
  3. package/dist/adaptive.d.ts +1 -1
  4. package/dist/adaptive.js +2 -2
  5. package/dist/ask-gate.d.ts +5 -3
  6. package/dist/ask-gate.js +19 -4
  7. package/dist/build-info.d.ts +27 -0
  8. package/dist/build-info.js +104 -0
  9. package/dist/cause.d.ts +108 -0
  10. package/dist/cause.js +182 -0
  11. package/dist/config.d.ts +11 -1
  12. package/dist/config.js +20 -4
  13. package/dist/done.d.ts +30 -9
  14. package/dist/done.js +408 -41
  15. package/dist/extension.d.ts +2 -2
  16. package/dist/extension.js +240 -43
  17. package/dist/guard.d.ts +9 -0
  18. package/dist/guard.js +142 -57
  19. package/dist/index.d.ts +6 -4
  20. package/dist/index.js +4 -3
  21. package/dist/learning.d.ts +12 -0
  22. package/dist/learning.js +35 -1
  23. package/dist/script-bodies.d.ts +16 -0
  24. package/dist/script-bodies.js +383 -0
  25. package/dist/shape.d.ts +1 -1
  26. package/dist/shape.js +5 -2
  27. package/dist/trace.d.ts +3 -1
  28. package/dist/trace.js +15 -0
  29. package/dist/turn-rules.d.ts +1 -1
  30. package/dist/turn-rules.js +102 -25
  31. package/dist/widget.d.ts +5 -0
  32. package/dist/widget.js +16 -0
  33. package/package.json +4 -4
  34. package/dist/conscience-config.d.ts +0 -21
  35. package/dist/conscience-config.js +0 -82
  36. package/dist/conscience-loader.d.ts +0 -26
  37. package/dist/conscience-loader.js +0 -82
  38. package/dist/conscience-policy.d.ts +0 -24
  39. package/dist/conscience-policy.js +0 -28
  40. package/dist/conscience-privacy.d.ts +0 -4
  41. package/dist/conscience-privacy.js +0 -11
  42. package/dist/extension.js.bak +0 -3060
  43. package/dist/jev.d.ts +0 -23
  44. package/dist/jev.js +0 -19
  45. package/dist/laya-download.d.ts +0 -40
  46. package/dist/laya-download.js +0 -288
  47. package/dist/laya-judge.d.ts +0 -62
  48. package/dist/laya-judge.js +0 -495
  49. package/dist/tool-recommendation.d.ts +0 -15
  50. package/dist/tool-recommendation.js +0 -31
package/dist/cause.js ADDED
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Cause-check: the sibling of the done-check.
3
+ *
4
+ * The done-check catches "done" with no check behind it. This one catches "the cause is X" with no check behind it: a
5
+ * final reply that names a likely cause, or hands a check to a person, when no tool result in the run tested it. The
6
+ * agent gets one follow-up turn: check it with its own tools and give the evidence, or say plainly that the cause is
7
+ * unverified and why it cannot check it. An unchecked cause is remembered per project for a week; a later unchecked
8
+ * cause of the same problem, matched by Jev, gets a stronger steer that names the earlier date.
9
+ *
10
+ * One offline pass runs first (`causePreFilter`), so a reply with no causal or hand-off wording costs no request.
11
+ */
12
+ import { ask, choice, noul } from "pi-typesafe";
13
+ import { redact } from "./redact.js";
14
+ import { commandOf } from "./tools.js";
15
+ import { DEFAULT_TEMPLATES, causeTokens, renderTemplate } from "./widget.js";
16
+ /** P(checked) at or above which the stated cause counts as checked by the run. */
17
+ const CHECKED_AT = 0.5;
18
+ export function emptyCauseActivity() {
19
+ return { calls: [] };
20
+ }
21
+ const ACTIVITY_LIMIT = 40;
22
+ const ACTIVITY_CALL_CHARS = 200;
23
+ const ACTIVITY_OUTPUT_CHARS = 160;
24
+ /** Earlier unchecked causes one request may carry for the repeat match, newest first. */
25
+ const EARLIER_LIMIT = 5;
26
+ /** Characters kept from the cause sentence for the stored summary and the repeat match. */
27
+ const SUMMARY_CHARS = 300;
28
+ /** Record one finished tool call: its redacted command or path, and a short redacted output sample. */
29
+ export function recordCauseActivity(activity, tool, input, output) {
30
+ const view = commandOf(tool, input);
31
+ const path = typeof input.path === "string" ? input.path : typeof input.file_path === "string" ? input.file_path : undefined;
32
+ const call = view?.command ?? (path === undefined ? tool : `${tool} ${path}`);
33
+ const sample = output === undefined ? "" : redact(output.replace(/\s+/g, " ").slice(0, ACTIVITY_OUTPUT_CHARS));
34
+ activity.calls.push({ call: redact(call.length > ACTIVITY_CALL_CHARS ? `${call.slice(0, ACTIVITY_CALL_CHARS)}…` : call), output: sample });
35
+ if (activity.calls.length > ACTIVITY_LIMIT)
36
+ activity.calls.splice(0, activity.calls.length - ACTIVITY_LIMIT);
37
+ }
38
+ /** A cause named as likely or as fact: the wording that makes a final reply worth judging. */
39
+ const CAUSE_WORDING = /\b(?:probably|likely|presumably|perhaps|maybe|i suspect|suspect(?:ed)? (?:that|the|this|it)|seems? to|appears? to|the cause|root cause|caused by|due to|the reason|reasons? (?:is|are)|the (?:bug|issue|problem|error|failure|regression|alert|warning) (?:is|was|comes|lies)|introduced by|stems from|traces? (?:back )?to|must have been|someone (?:changed|deleted|renamed|broke|edited)|points? to)\b/i;
40
+ /** A check handed to a person: wording that asks someone else to inspect what the agent could inspect. */
41
+ const HAND_OFF_WORDING = /\b(?:ask (?:them|him|her|someone|the team|the user|whoever|the owner)|check with|confirm with|find out (?:from|whether|if)|have (?:them|someone|the team) (?:check|confirm|verify|look)|(?:could|can|would) you (?:check|confirm|verify|find out|look)|please (?:check|confirm|verify)|worth (?:checking|verifying|confirming)|not sure (?:who|whether|if))\b/i;
42
+ /**
43
+ * The offline pre-filter: true when the final reply carries causal or hand-off wording, so a reply about results or
44
+ * next steps sends no request. Deliberately broad: a Jev answer still decides, and a missed ask loses a real catch.
45
+ */
46
+ export function causePreFilter(text) {
47
+ return CAUSE_WORDING.test(text) || HAND_OFF_WORDING.test(text);
48
+ }
49
+ /** The sentence that carries the first causal or hand-off match, or undefined when none does. */
50
+ function causeSentence(text) {
51
+ const causal = CAUSE_WORDING.exec(text);
52
+ const handOff = HAND_OFF_WORDING.exec(text);
53
+ const match = causal !== null && handOff !== null ? (causal.index <= handOff.index ? causal : handOff) : causal ?? handOff;
54
+ if (match === null)
55
+ return undefined;
56
+ const at = match.index;
57
+ let start = 0;
58
+ for (let index = at - 1; index >= 0; index--) {
59
+ const character = text[index];
60
+ if (character === "." || character === "!" || character === "?" || character === "\n") {
61
+ start = index + 1;
62
+ break;
63
+ }
64
+ }
65
+ let end = text.length;
66
+ for (let index = at; index < text.length; index++) {
67
+ const character = text[index];
68
+ if (character === "." || character === "!" || character === "?" || character === "\n") {
69
+ end = index + 1;
70
+ break;
71
+ }
72
+ }
73
+ return text.slice(start, end).trim();
74
+ }
75
+ export const causeQuestions = {
76
+ states_cause: noul("Does `final_message` state a cause, explanation, or diagnosis for a problem, as a likely cause or as a fact?", {
77
+ true: "Yes: it names why something happened or what is wrong, for example a component, a change, a condition, or an action by someone.",
78
+ false: "No: it only reports status or results, names remaining work, asks a question, or describes a plan.",
79
+ }),
80
+ hands_off: noul("Does `final_message` ask a person to check, confirm, or find out something that the agent could inspect with its own tools?", {
81
+ true: "Yes: it asks someone else, such as the user, a colleague, or another team, to verify or investigate a fact the agent could query, read, or run itself.",
82
+ false: "No: it asks only for a decision a person must make, reports what it checked, or states a cause.",
83
+ }),
84
+ checked: noul("Did `run` check the cause `final_message` states, with a tool result that directly tested it?", {
85
+ true: "Yes: a tool result inspected the stated cause or its effect, so the cause rests on evidence from the run.",
86
+ false: "No: no tool result in `run` inspected the stated cause; the reply only asserts it or asks someone else.",
87
+ }),
88
+ };
89
+ /** One choice question: which earlier cause is the same cause of the same problem, or `none`. */
90
+ function earlierCauseQuestion(earlier) {
91
+ const criteria = {
92
+ none: "The cause in `final_message` is not the same cause of the same problem as any earlier cause, or no earlier cause applies.",
93
+ };
94
+ for (const cause of earlier)
95
+ criteria[cause.key] = `The cause in \`final_message\` is the same cause of the same problem as ${cause.key} (recorded ${cause.date}): ${cause.summary}`;
96
+ return choice("Which earlier cause, if any, is the same cause of the same problem as the cause in `final_message`?", criteria);
97
+ }
98
+ export function buildCauseRequest(task, finalMessage, activity, earlier = []) {
99
+ return {
100
+ state: {
101
+ task: task?.trim() ? (task.trim().length > 1500 ? `${task.trim().slice(0, 1500)}…` : task.trim()) : "(no user request recorded in this session)",
102
+ final_message: redact(finalMessage.length > 2000 ? `${finalMessage.slice(0, 2000)}…` : finalMessage),
103
+ run: {
104
+ tool_calls: activity.calls.map(call => ({ call: call.call, output: call.output || "(no output recorded)" })),
105
+ },
106
+ ...(earlier.length ? { earlier_causes: earlier.map(cause => ({ key: cause.key, date: cause.date, cause: cause.summary })) } : {}),
107
+ },
108
+ questions: earlier.length ? { ...causeQuestions, same_cause: earlierCauseQuestion(earlier) } : causeQuestions,
109
+ };
110
+ }
111
+ function clip(text, limit = 160) {
112
+ const flat = text.replace(/\s+/g, " ").trim();
113
+ return flat.length <= limit ? flat : `${flat.slice(0, limit)}…`;
114
+ }
115
+ function day(at) {
116
+ return new Date(at).toISOString().slice(0, 10);
117
+ }
118
+ /** The project's earlier unchecked causes inside the window, newest first, at most `EARLIER_LIMIT`, each with a key. */
119
+ async function readEarlierCauses(options, since, now) {
120
+ const store = options.store;
121
+ const projectRoot = options.projectRoot;
122
+ if (store === undefined || projectRoot === undefined)
123
+ return [];
124
+ // A store that answers outside the window cannot escalate; the guard owns the 7-day rule.
125
+ const rows = await store.recall(projectRoot, since, EARLIER_LIMIT);
126
+ return rows
127
+ .filter(row => row.at >= since && row.at < now)
128
+ .sort((a, b) => b.at - a.at)
129
+ .slice(0, EARLIER_LIMIT)
130
+ .map((row, index) => ({ key: `c${index + 1}`, date: day(row.at), summary: row.summary, at: row.at }));
131
+ }
132
+ export async function evaluateCause(task, finalMessage, activity, options) {
133
+ const summary = redact(clip(causeSentence(finalMessage) ?? finalMessage, SUMMARY_CHARS));
134
+ if (!options.config.enabled)
135
+ return { unchecked: false, handsOff: false, summary, reasons: [] };
136
+ const now = options.now ?? Date.now();
137
+ const earlier = await readEarlierCauses(options, now - options.config.windowDays * 86_400_000, now);
138
+ const result = await ask(options.judge, buildCauseRequest(task, finalMessage, activity, earlier), { timeoutMs: options.timeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
139
+ if (!result.ok)
140
+ return { unchecked: false, handsOff: false, summary, reasons: [], error: result.error, ...(result.errorCode ? { errorCode: result.errorCode } : {}) };
141
+ // The request type is assembled at runtime, so the answer shape is read loosely here: the repeat question is asked
142
+ // only when an earlier cause exists.
143
+ const answers = result.answers;
144
+ const judgment = {
145
+ statesCause: answers.states_cause.noul,
146
+ handsOff: answers.hands_off.noul,
147
+ checked: answers.checked.noul,
148
+ model: result.model,
149
+ elapsedMs: result.elapsedMs,
150
+ };
151
+ const states = Math.max(judgment.statesCause, judgment.handsOff) >= options.config.claimsCause;
152
+ const unchecked = states && judgment.checked < CHECKED_AT;
153
+ const handsOff = judgment.handsOff > judgment.statesCause;
154
+ // Escalate only when Jev says the reply repeats a cause it was shown, never on a shared label.
155
+ const matched = unchecked && earlier.length ? answers.same_cause?.choice : undefined;
156
+ const previous = matched === undefined || matched === "none" ? undefined : earlier.find(cause => cause.key === matched);
157
+ const reasons = [];
158
+ if (unchecked) {
159
+ const stated = handsOff
160
+ ? `hands a check to a person (${judgment.handsOff.toFixed(2)})`
161
+ : `states a cause (${judgment.statesCause.toFixed(2)})`;
162
+ reasons.push(`the final reply ${stated} and no tool result in the run checked it (checked ${judgment.checked.toFixed(2)})`);
163
+ if (previous !== undefined)
164
+ reasons.push(`the same unchecked cause was recorded on ${day(previous.at)}`);
165
+ }
166
+ if (unchecked && options.store !== undefined && options.projectRoot !== undefined) {
167
+ await options.store.record(options.projectRoot, { summary, at: now });
168
+ }
169
+ return { unchecked, handsOff, summary, reasons, judgment, ...(previous !== undefined ? { previous: { summary: previous.summary, at: previous.at } } : {}) };
170
+ }
171
+ /** The follow-up for the agent: check the cause with its own tools, or say plainly that it is unverified. */
172
+ export function causeNudge(verdict) {
173
+ const base = "check it with your own tools (a query, a log, a file, or a command) and give the evidence, or say plainly that the cause is unverified and why you cannot check it";
174
+ const opening = verdict.handsOff
175
+ ? "the final reply asks a person to check something that the agent can check itself, and nothing in the run checked it"
176
+ : "the final reply states a cause that nothing in the run checked";
177
+ const repeat = verdict.previous === undefined ? "" : ` You gave the same unchecked cause on ${day(verdict.previous.at)}.`;
178
+ return `pi-warden: ${opening}.${repeat} ${base[0].toUpperCase()}${base.slice(1)}.`;
179
+ }
180
+ export function formatCause(verdict, template = DEFAULT_TEMPLATES.cause) {
181
+ return renderTemplate(template, causeTokens(verdict));
182
+ }
package/dist/config.d.ts CHANGED
@@ -157,6 +157,15 @@ export interface DoneGuardConfig {
157
157
  uiFiles: string[];
158
158
  visualTools: VisualToolsConfig;
159
159
  }
160
+ export interface CauseGuardConfig {
161
+ enabled: boolean;
162
+ /** P(the final reply states a cause or hands a check to a person) at or above this is judged. */
163
+ claimsCause: number;
164
+ /** Also send the agent a follow-up asking it to check the cause. Triggers one more LLM turn. */
165
+ nudge: boolean;
166
+ /** Days an unchecked cause is remembered for the repeat steer. */
167
+ windowDays: number;
168
+ }
160
169
  /** Tool calls that show the rendered UI. Matching is case-insensitive. */
161
170
  export interface VisualToolsConfig {
162
171
  /** Heads of a shell command segment: `agent-browser`, `npx playwright`. */
@@ -423,6 +432,7 @@ export interface WardenConfig {
423
432
  action: ActionGuardConfig;
424
433
  stuck: StuckGuardConfig;
425
434
  done: DoneGuardConfig;
435
+ cause: CauseGuardConfig;
426
436
  slop: SlopGuardConfig;
427
437
  security: SecurityConfig;
428
438
  rules: RulesConfig;
@@ -461,7 +471,7 @@ export interface WardenConfig {
461
471
  }
462
472
  export declare const PACKAGE_NAME = "pi-warden";
463
473
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
464
- export declare const CONFIG_SCHEMA = 12;
474
+ export declare const CONFIG_SCHEMA = 13;
465
475
  export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
466
476
  export declare function defaultConfig(): WardenConfig;
467
477
  /** The user file is in the host's agent directory: `~/.pi/agent` on Pi, `~/.omp/agent` on oh-my-pi. `PI_CODING_AGENT_DIR` overrides it. */
package/dist/config.js CHANGED
@@ -12,9 +12,12 @@ export const DEFAULT_UI_FILES = [
12
12
  ];
13
13
  export function defaultVisualTools() {
14
14
  return {
15
- commands: ["agent-browser", "playwright", "npx playwright", "flutter test", "fvm flutter test", "idb", "xcrun simctl io", "chrome", "chromium", "google-chrome"],
16
- commandWords: ["screenshot"],
17
- tools: ["screenshot", "take_snapshot", "navigate"],
15
+ // A head names the command that shows the page: a page text snapshot, or a browser or device test run. A screenshot
16
+ // subcommand only writes a file (`agent-browser screenshot PATH`, `idb screenshot`, `chrome --headless --screenshot`),
17
+ // so it is not a head; the saved image counts once something reads it.
18
+ commands: ["agent-browser snapshot", "agent-browser get text", "playwright test", "npx playwright test", "flutter test", "fvm flutter test"],
19
+ commandWords: [],
20
+ tools: ["take_snapshot"],
18
21
  images: ["png", "jpg", "jpeg", "webp"],
19
22
  };
20
23
  }
@@ -24,7 +27,7 @@ export function isRecallTool(value) {
24
27
  }
25
28
  export const PACKAGE_NAME = "pi-warden";
26
29
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
27
- export const CONFIG_SCHEMA = 12;
30
+ export const CONFIG_SCHEMA = 13;
28
31
  export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
29
32
  export function defaultConfig() {
30
33
  return {
@@ -60,6 +63,7 @@ export function defaultConfig() {
60
63
  },
61
64
  stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true, repeatSteer: true, evidence: true, diffLimit: 3000, tailLimit: 1000 },
62
65
  done: { enabled: true, claimsDone: 0.7, nudge: true, uiProof: true, uiFiles: [...DEFAULT_UI_FILES], visualTools: defaultVisualTools() },
66
+ cause: { enabled: true, claimsCause: 0.7, nudge: true, windowDays: 7 },
63
67
  slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
64
68
  security: { enabled: true, threshold: 0.7, maskOutput: true },
65
69
  rules: { enabled: true, threshold: 0.7, softThreshold: 0, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
@@ -471,6 +475,16 @@ function applyDone(base, raw) {
471
475
  },
472
476
  };
473
477
  }
478
+ function applyCause(base, raw) {
479
+ if (!isObject(raw))
480
+ return base;
481
+ return {
482
+ enabled: boolean(raw.enabled, base.enabled),
483
+ claimsCause: probability(raw.claimsCause, base.claimsCause),
484
+ nudge: boolean(raw.nudge, base.nudge),
485
+ windowDays: Math.max(1, positiveInteger(raw.windowDays, base.windowDays)),
486
+ };
487
+ }
474
488
  function applyProse(base, raw) {
475
489
  if (!isObject(raw))
476
490
  return base;
@@ -523,6 +537,7 @@ function applyWidget(base, raw) {
523
537
  action: template(raw.action, base.action),
524
538
  stuck: template(raw.stuck, base.stuck),
525
539
  done: template(raw.done, base.done),
540
+ cause: template(raw.cause, base.cause),
526
541
  prose: template(raw.prose, base.prose),
527
542
  security: template(raw.security, base.security),
528
543
  context: template(raw.context, base.context),
@@ -553,6 +568,7 @@ function applyGuards(base, raw, timeoutMs, source, warnings) {
553
568
  action: applyAction(base.action, raw.action, timeoutMs, source, warnings),
554
569
  stuck: applyStuck(base.stuck, raw.stuck),
555
570
  done: applyDone(base.done, raw.done),
571
+ cause: applyCause(base.cause, raw.cause),
556
572
  slop: applySlop(base.slop, raw.slop),
557
573
  security: isObject(raw.security) ? {
558
574
  enabled: source === "project" ? projectSwitch("security.enabled", raw.security.enabled, base.security.enabled, warnings) : boolean(raw.security.enabled, base.security.enabled),
package/dist/done.d.ts CHANGED
@@ -3,14 +3,17 @@ import type { DoneGuardConfig, VisualToolsConfig } from "./config.js";
3
3
  export type ToolOutcome = "read" | "mutation" | "check-pass" | "check-fail" | "unknown";
4
4
  /**
5
5
  * What a finished tool call contributes to the run's evidence. Only write/edit inside the project count as code changes: shell side effects
6
- * (deleting a temp dir, installing a package) are too varied to demand a test run for. Custom tools are unknown.
6
+ * (deleting a temp dir, installing a package) are too varied to demand a test run for. Custom tools are unknown. A shell command counts as a
7
+ * check only when a check runner starts one of its shell segments; a runner name in an argument (`grep -n jest package.json`) does not.
7
8
  */
8
9
  export declare function classifyToolResult(tool: string, input: Record<string, unknown>, failed: boolean, output?: string, cwd?: string): ToolOutcome;
10
+ /** Why a run that names a check runner is not counted as a check (for one trace detail); undefined when it counts or names no runner. */
11
+ export declare function checkSkipReason(tool: string, input: Record<string, unknown>, failed: boolean, output?: string, cwd?: string): string | undefined;
9
12
  /**
10
- * Recognise a test/type-check runner's own summary in tool output: node:test, jest/vitest, pytest, cargo, go test,
11
- * tsc. Returns the outcome the summary reports, or undefined when no runner summary is present.
13
+ * Recognise a runner's own summary in tool output. A failure marker anywhere in the tail wins over a passing one; the
14
+ * caller sees a pass only when no failure marker is present.
12
15
  */
13
- export declare function checkSummary(output: string): "pass" | "fail" | undefined;
16
+ export declare function checkSummary(output: string, command?: string): "pass" | "fail" | undefined;
14
17
  export interface RunEvidence {
15
18
  mutations: number;
16
19
  checks: Array<{
@@ -22,14 +25,17 @@ export interface RunEvidence {
22
25
  unseenUi?: string;
23
26
  }
24
27
  export declare function emptyEvidence(): RunEvidence;
25
- export declare function recordOutcome(evidence: RunEvidence, outcome: ToolOutcome, input: Record<string, unknown>, tool?: string): void;
28
+ export declare function recordOutcome(evidence: RunEvidence, outcome: ToolOutcome, input: Record<string, unknown>, tool?: string): string | undefined;
26
29
  /** A path matches when some glob matches it and no `!` glob does. */
27
30
  export declare function isUiFile(path: string, globs: readonly string[]): boolean;
28
31
  /**
29
- * Whether a successful tool call showed the rendered UI: a browser or device command, a screenshot, a browser MCP tool, or
30
- * reading an image. A test run proves the code runs; only this proves what the user will see.
32
+ * Whether a successful tool call showed the rendered UI to the agent: an image in the result, a page text snapshot, a browser or device test
33
+ * run, or reading an image file. A test run proves the code runs; only this proves what the user will see. A screenshot command that only
34
+ * writes a file shows nothing until something reads that file.
31
35
  */
32
- export declare function isVisualCheck(tool: string, input: Record<string, unknown>, failed: boolean, visual: VisualToolsConfig): boolean;
36
+ export declare function isVisualCheck(tool: string, input: Record<string, unknown>, failed: boolean, visual: VisualToolsConfig, content?: ReadonlyArray<{
37
+ type: string;
38
+ }>): boolean;
33
39
  /** Paths a successful call changed that match the UI globs: `write`/`edit` paths, plus files a shell command writes. */
34
40
  export declare function recordUi(evidence: RunEvidence, changed: readonly string[], visual: boolean, globs: readonly string[]): void;
35
41
  interface MessageLike {
@@ -102,7 +108,22 @@ export interface DoneOptions {
102
108
  signal?: AbortSignal | undefined;
103
109
  }
104
110
  export declare function evaluateDone(task: string | undefined, finalMessage: string, evidence: RunEvidence, options: DoneOptions): Promise<DoneVerdict>;
111
+ export declare function localUrl(text: string): string | undefined;
112
+ /**
113
+ * The project's own check command: the `check` then `test` script of its `package.json`, else the same Makefile targets. Only files in the
114
+ * project root are read; a project with neither keeps the generic nudge.
115
+ */
116
+ export declare function projectCheckCommand(root: string): string | undefined;
117
+ /** Session facts the nudge can name instead of a generic ask. */
118
+ export interface NudgeHint {
119
+ /** The last check command that passed earlier in this session in this project. */
120
+ lastCheck?: string | undefined;
121
+ /** The project's own check command (`projectCheckCommand`). */
122
+ projectCheck?: string | undefined;
123
+ /** The last local URL a tool result printed in this session. */
124
+ localUrl?: string | undefined;
125
+ }
105
126
  /** Follow-up for the agent: verify or say plainly that nothing was verified. */
106
- export declare function doneNudge(verdict: DoneVerdict): string;
127
+ export declare function doneNudge(verdict: DoneVerdict, hint?: NudgeHint): string;
107
128
  export declare function formatDone(verdict: DoneVerdict, template?: string): string;
108
129
  export {};