pi-goal-list-loop-audit 0.39.21 → 0.39.23
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 +28 -0
- package/docs/INDEX.md +1 -1
- package/docs/RUNTIME-INVENTORY.md +4 -4
- package/extensions/completion-summary.ts +28 -6
- package/extensions/finding-lead.ts +17 -2
- package/extensions/goal-loop-display.ts +35 -9
- package/extensions/loops/goal-tools.ts +2 -0
- package/extensions/loops/goal-ui.ts +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.39.23 — terminal summary hardening: agent-proof headings, punctuation, clips (2026-10-10)
|
|
6
|
+
|
|
7
|
+
### Summary render
|
|
8
|
+
|
|
9
|
+
- Agent-written `###`/`####` lines inside summary findings demote to
|
|
10
|
+
bold on chat (the Pi TUI has no heading style); archives keep real
|
|
11
|
+
headings.
|
|
12
|
+
- Finding outcomes strip stranded leading punctuation, matching the
|
|
13
|
+
reason husk fix.
|
|
14
|
+
- The headline clipper extends through a closing backtick instead of
|
|
15
|
+
stranding an unmatched one inside a code span.
|
|
16
|
+
- The `complete_goal` `leftOut` guidance forbids repeating Unresolved
|
|
17
|
+
facts (Unresolved = open problems; leftOut = deferred scope + why).
|
|
18
|
+
|
|
19
|
+
## 0.39.22 — stalled-audit UI: dead audits named on every surface (2026-10-10)
|
|
20
|
+
|
|
21
|
+
### Audit visibility
|
|
22
|
+
|
|
23
|
+
- Past one hour of worker silence with no in-budget tool, a quiet audit
|
|
24
|
+
escalates to **stalled** on every surface: the detailed card drops the
|
|
25
|
+
live-worker claim (`no worker responds`) and offers the retry
|
|
26
|
+
(`/goal resume retries the claim`); the compact glance card and footer
|
|
27
|
+
agree (`AUDIT STALLED`, error tone, ✖ glyph) instead of claiming
|
|
28
|
+
`detached worker` / `No action needed`. The quiet watcher fires its own
|
|
29
|
+
one-shot stall notice.
|
|
30
|
+
- Elapsed ages read in days past 24h (`15d 00h`, not `360h 00m`).
|
|
31
|
+
- Display-only: no retry or recovery behavior changed.
|
|
32
|
+
|
|
5
33
|
## 0.39.21 — 5h retry-window policy, stall-proof test cleanup (2026-10-09)
|
|
6
34
|
|
|
7
35
|
### Recovery policy
|
package/docs/INDEX.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
## Versions and shipped content
|
|
32
32
|
|
|
33
33
|
[CHANGELOG.md](../CHANGELOG.md) records the release history from
|
|
34
|
-
v0.35.14–v0.39.
|
|
34
|
+
v0.35.14–v0.39.23 and unreleased work. `/glla version` reports the loaded version
|
|
35
35
|
and registry comparison. A checkout can contain changes not yet published.
|
|
36
36
|
|
|
37
37
|
The package includes this documentation, [examples](../examples/),
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Generated by `node scripts/generate-inventory.mjs`. Run with `--check` to detect drift. Counts describe files and literal registrations, not executed coverage.
|
|
4
4
|
|
|
5
5
|
- Extension/script files: 109
|
|
6
|
-
- Extension/script lines:
|
|
7
|
-
- Top-level test files:
|
|
6
|
+
- Extension/script lines: 67574
|
|
7
|
+
- Top-level test files: 380
|
|
8
8
|
- Ambient bridge names: 179 (audit baseline: 179)
|
|
9
9
|
- Commands: `/glla`, `/goal`, `/list`, `/loop`, `/review`
|
|
10
10
|
- Tools: `audit_project_increment`, `block_project_requirement`, `claim_project_task`, `complete_goal`, `complete_task`, `list_activate`, `list_add`, `list_status`, `pause_goal`, `plan_project_increment`, `propose_goal_draft`, `propose_loop_draft`, `propose_loop_refine`, `propose_project_refinement`, `propose_project_requirements`, `propose_task_list`, `record_goal_judgment`, `resume_goal`, `unblock_project_requirement`, `update_task_batch`, `update_task_status`, `wait_for_background`
|
|
@@ -14,10 +14,10 @@ Generated by `node scripts/generate-inventory.mjs`. Run with `--check` to detect
|
|
|
14
14
|
| Module | Lines |
|
|
15
15
|
|---|---:|
|
|
16
16
|
| [extensions/goal-loop-core.ts](../extensions/goal-loop-core.ts) | 5043 |
|
|
17
|
-
| [extensions/loops/goal-tools.ts](../extensions/loops/goal-tools.ts) |
|
|
17
|
+
| [extensions/loops/goal-tools.ts](../extensions/loops/goal-tools.ts) | 4229 |
|
|
18
18
|
| [extensions/loops/goal-activation.ts](../extensions/loops/goal-activation.ts) | 3880 |
|
|
19
19
|
| [extensions/goal-commands.ts](../extensions/goal-commands.ts) | 3614 |
|
|
20
|
-
| [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) |
|
|
20
|
+
| [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) | 2884 |
|
|
21
21
|
| [extensions/goal-loop-auditor-process.ts](../extensions/goal-loop-auditor-process.ts) | 2685 |
|
|
22
22
|
| [extensions/loops/goal-auditor-hooks.ts](../extensions/loops/goal-auditor-hooks.ts) | 2654 |
|
|
23
23
|
| [extensions/goal-heartbeat.ts](../extensions/goal-heartbeat.ts) | 2104 |
|
|
@@ -509,10 +509,14 @@ function findingPresentation(finding: string, chat: boolean): { outcome: string;
|
|
|
509
509
|
// evidence tokens ride mid-sentence, so strip a leading `,;:` husk.
|
|
510
510
|
// Content words are untouched — only the join seam is cleaned.
|
|
511
511
|
const normalizedReason = [...new Set(reasonParts)].join(" · ").trim().replace(/^[,;:]+/, "").trim();
|
|
512
|
+
// v0.39.23 (field 2026-10-10 — Clean-Web card `- , - with …`): the same
|
|
513
|
+
// stranded-punctuation husk can lead the outcome when the agent doubles
|
|
514
|
+
// punctuation around the em-dash split. Strip it like the reason husk.
|
|
515
|
+
const stripHusk = (text: string): string => text.trim().replace(/^[,;:]+/, "").trim();
|
|
512
516
|
return {
|
|
513
517
|
// Chat removes machine receipts; the archive keeps the full evidence
|
|
514
518
|
// prose. The outcome/reason split itself is shared by both surfaces.
|
|
515
|
-
outcome: chat ? chatNarrative(parsed.outcome) : sanitizeDisplayText(parsed.outcome),
|
|
519
|
+
outcome: stripHusk(chat ? chatNarrative(parsed.outcome) : sanitizeDisplayText(parsed.outcome)),
|
|
516
520
|
reason: chat ? chatNarrative(normalizedReason) : sanitizeDisplayText(normalizedReason),
|
|
517
521
|
evidence: parsed.evidence,
|
|
518
522
|
};
|
|
@@ -1058,6 +1062,15 @@ export function buildRichTerminalParts(args: {
|
|
|
1058
1062
|
};
|
|
1059
1063
|
}
|
|
1060
1064
|
|
|
1065
|
+
/** Demote agent-written markdown headings to TUI-safe bold for chat.
|
|
1066
|
+
* A `#`-heading line that survives into chat content renders literally
|
|
1067
|
+
* (the Pi TUI has no heading style); `**bold**` renders everywhere the
|
|
1068
|
+
* card does. Archive/non-chat lines bypass this — a .md file renders
|
|
1069
|
+
* headings natively. */
|
|
1070
|
+
function terminalizeHeadings(line: string): string {
|
|
1071
|
+
return line.replace(/^(\s*)#{1,6}\s+(.*\S)\s*$/, "$1**$2**");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1061
1074
|
/** Compose parts + banner + section headers into markdown lines.
|
|
1062
1075
|
* v0.38.50: the duration line rides directly under the headline.
|
|
1063
1076
|
* Audit 2026-09-13: findings-first order is pinned — findings, then the
|
|
@@ -1081,22 +1094,31 @@ export function composeRichTerminalLines(parts: RichTerminalParts): string[] {
|
|
|
1081
1094
|
// the screen literally. Chat/terminal surfaces get plain section labels;
|
|
1082
1095
|
// the archived markdown keeps its headings (a .md file renders them).
|
|
1083
1096
|
const head = (text: string): string => (parts.chat ? text : `### ${text}`);
|
|
1097
|
+
// v0.39.23 (field 2026-10-10 — Clean-Web terminal screenshots): agents
|
|
1098
|
+
// write `###`/`####` headers INSIDE finding/remaining/next text and the
|
|
1099
|
+
// Pi TUI renders bold/lists but not `#` headings, so they reached the
|
|
1100
|
+
// screen literally. Chat-facing content demotes markdown headings to
|
|
1101
|
+
// bold (which the TUI renders); the archived markdown keeps them.
|
|
1102
|
+
const contentLine = (line: string): string => {
|
|
1103
|
+
const clean = sanitizeDisplayText(line);
|
|
1104
|
+
return parts.chat ? terminalizeHeadings(clean) : clean;
|
|
1105
|
+
};
|
|
1084
1106
|
if (parts.summaryLines.length > 0) {
|
|
1085
|
-
lines.push(head("Summary"), ...parts.summaryLines.map(
|
|
1107
|
+
lines.push(head("Summary"), ...parts.summaryLines.map(contentLine), "");
|
|
1086
1108
|
}
|
|
1087
1109
|
if (parts.findingLines.length > 0) {
|
|
1088
|
-
lines.push(head("What Changed"), ...parts.findingLines.map(
|
|
1110
|
+
lines.push(head("What Changed"), ...parts.findingLines.map(contentLine), "");
|
|
1089
1111
|
}
|
|
1090
1112
|
if (parts.remainingLines.length > 0) {
|
|
1091
|
-
lines.push(head("Remaining"), ...parts.remainingLines.map(
|
|
1113
|
+
lines.push(head("Remaining"), ...parts.remainingLines.map(contentLine), "");
|
|
1092
1114
|
}
|
|
1093
1115
|
if (parts.verificationSummaryLine) {
|
|
1094
|
-
lines.push(head("Verification"),
|
|
1116
|
+
lines.push(head("Verification"), contentLine(parts.verificationSummaryLine), "");
|
|
1095
1117
|
} else if (!parts.chat && parts.tableLines.length > 0) {
|
|
1096
1118
|
lines.push("### Verification Evidence", ...parts.tableLines.map((line) => sanitizeDisplayText(line)), "");
|
|
1097
1119
|
}
|
|
1098
1120
|
if (parts.nextLines.length > 0) {
|
|
1099
|
-
lines.push(head("Next"), ...parts.nextLines.map(
|
|
1121
|
+
lines.push(head("Next"), ...parts.nextLines.map(contentLine), "");
|
|
1100
1122
|
}
|
|
1101
1123
|
if (parts.repoLines.length > 0) {
|
|
1102
1124
|
lines.push(head("Final Repository State"), ...parts.repoLines.map((line) => `- ${sanitizeDisplayText(line)}`), "");
|
|
@@ -166,9 +166,24 @@ export function clipSummaryValue(value: string, limit: number): string {
|
|
|
166
166
|
headUnits.forEach((unit, i) => {
|
|
167
167
|
if (/[,;:·—–(+[\[]/u.test(unit)) boundary = i;
|
|
168
168
|
});
|
|
169
|
+
// v0.39.23 (field 2026-10-10 — Clean-Web card headline stranded
|
|
170
|
+
// "lands on `sendThreshold…" with an unmatched backtick): never cut
|
|
171
|
+
// inside an inline code span. When the kept head holds an unmatched
|
|
172
|
+
// backtick, extend through the closing backtick when it lands within a
|
|
173
|
+
// short bound; the capped overrun keeps the span readable instead of
|
|
174
|
+
// dangling.
|
|
175
|
+
const finalize = (text: string): string => {
|
|
176
|
+
const opens = (text.match(/`/g) ?? []).length;
|
|
177
|
+
if (opens % 2 === 0) return `${text}…`;
|
|
178
|
+
const consumed = [...text].length;
|
|
179
|
+
const rest = units.slice(consumed, consumed + 40).join("");
|
|
180
|
+
const close = rest.indexOf("`");
|
|
181
|
+
if (close >= 0) return `${text}${rest.slice(0, close + 1)}…`;
|
|
182
|
+
return `${text}…`;
|
|
183
|
+
};
|
|
169
184
|
if (boundary >= floor) {
|
|
170
185
|
const cut = headUnits.slice(0, boundary).join("").replace(/[,;:·—–(+[\[\s]+$/u, "");
|
|
171
|
-
if ([...cut].length >= Math.min(floor, 16)) return
|
|
186
|
+
if ([...cut].length >= Math.min(floor, 16)) return finalize(cut);
|
|
172
187
|
}
|
|
173
188
|
let space = -1;
|
|
174
189
|
headUnits.forEach((unit, i) => {
|
|
@@ -177,5 +192,5 @@ export function clipSummaryValue(value: string, limit: number): string {
|
|
|
177
192
|
const kept = (space > capped / 2 ? headUnits.slice(0, space).join("") : head)
|
|
178
193
|
.trimEnd()
|
|
179
194
|
.replace(/[,;:·—–(+\[]$/u, "");
|
|
180
|
-
return
|
|
195
|
+
return finalize(kept);
|
|
181
196
|
}
|
|
@@ -58,7 +58,11 @@ export function fmtElapsed(ms: number): string {
|
|
|
58
58
|
// liveness signal — minute-only granularity looks frozen on a 1s tick.
|
|
59
59
|
if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
|
|
60
60
|
const h = Math.floor(m / 60);
|
|
61
|
-
return `${h}h ${String(m % 60).padStart(2, "0")}m`;
|
|
61
|
+
if (h < 24) return `${h}h ${String(m % 60).padStart(2, "0")}m`;
|
|
62
|
+
// v0.39.22: day granularity past 24h — a 15-day stalled audit reads
|
|
63
|
+
// "15d 00h", not "360h 00m". Only affects renders past a day.
|
|
64
|
+
const d = Math.floor(h / 24);
|
|
65
|
+
return `${d}d ${String(h % 24).padStart(2, "0")}h`;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export function fmtTokens(n: number): string {
|
|
@@ -801,11 +805,17 @@ export interface AuditDisplayProgress {
|
|
|
801
805
|
sessionPath?: string;
|
|
802
806
|
}
|
|
803
807
|
|
|
804
|
-
type AuditorDisplayPhase = "queued" | "running" | "quiet" | "blocked" | "awaiting-verdict";
|
|
808
|
+
type AuditorDisplayPhase = "queued" | "running" | "quiet" | "stalled" | "blocked" | "awaiting-verdict";
|
|
805
809
|
/** v0.35.15: exported — the parent-side quiet watcher (goal-ui ticker) uses
|
|
806
810
|
* the same threshold for the one-shot proactive notify so the notification
|
|
807
811
|
* and the status chip can never disagree about when "quiet" begins. */
|
|
808
812
|
export const AUDITOR_QUIET_MS = 3 * 60_000;
|
|
813
|
+
/** v0.39.22 (field 2026-10-10: seo goal quiet 15 days, worker dead):
|
|
814
|
+
* quiet past this bound with no worker activity and no in-budget tool is
|
|
815
|
+
* a dead worker, not a slow one. An hour of total silence exceeds any
|
|
816
|
+
* legitimate inter-tool gap; the stalled presentation names the death and
|
|
817
|
+
* offers the retry (/goal resume) instead of only the discard. */
|
|
818
|
+
export const AUDITOR_STALLED_MS = 60 * 60_000;
|
|
809
819
|
/** v0.35.15: how long the "silent Xm then resumed" fact stays in the
|
|
810
820
|
* detailed auditor card after a quiet stretch ends — long enough to be seen,
|
|
811
821
|
* short enough not to become permanent noise. */
|
|
@@ -911,6 +921,7 @@ export function auditorDisplayPhase(g: Goal, audit: AuditDisplayProgress | null
|
|
|
911
921
|
toolAgeMs < audit.toolTimeoutMs
|
|
912
922
|
)
|
|
913
923
|
return "running";
|
|
924
|
+
if (age > AUDITOR_STALLED_MS) return "stalled";
|
|
914
925
|
return "quiet";
|
|
915
926
|
}
|
|
916
927
|
// Same lifecycle scope as above: a stale goal snapshot that still
|
|
@@ -959,6 +970,7 @@ function auditorPhaseLabel(phase: AuditorDisplayPhase): string {
|
|
|
959
970
|
case "queued": return "queued";
|
|
960
971
|
case "running": return "running";
|
|
961
972
|
case "quiet": return "quiet";
|
|
973
|
+
case "stalled": return "stalled";
|
|
962
974
|
case "blocked": return "blocked";
|
|
963
975
|
case "awaiting-verdict": return "awaiting completion review";
|
|
964
976
|
}
|
|
@@ -973,6 +985,7 @@ export function auditorPhaseGlyph(phase: AuditorDisplayPhase): string {
|
|
|
973
985
|
case "queued": return "⋯"; // waiting to start
|
|
974
986
|
case "running": return "▶"; // worker alive
|
|
975
987
|
case "quiet": return "◌"; // silent — possibly stuck
|
|
988
|
+
case "stalled": return "✖"; // silent past the death bound — worker dead
|
|
976
989
|
case "blocked": return "⛔"; // infra failure / needs intervention
|
|
977
990
|
case "awaiting-verdict": return "✓"; // worker done, verdict pending
|
|
978
991
|
}
|
|
@@ -1508,11 +1521,11 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1508
1521
|
// v0.35.15: leading phase glyph + draining activity meter — a glance
|
|
1509
1522
|
// answers "is the audit alive?" without reading the sentence.
|
|
1510
1523
|
const phaseText = `auditor ${auditorPhaseGlyph(phase)} ${observed}`;
|
|
1511
|
-
const color = phase === "blocked" || phase === "quiet" ? "warning" : live ? "success" : "accent";
|
|
1524
|
+
const color = phase === "stalled" ? "error" : phase === "blocked" || phase === "quiet" ? "warning" : live ? "success" : "accent";
|
|
1512
1525
|
const activityMeter = auditorActivityMeter(audit, phase, now);
|
|
1513
1526
|
const label = live
|
|
1514
1527
|
? `${paint(theme, "success", phaseText)} ${paint(theme, "accent", activityMeter)} ${activityBadge("AUDITOR · DETACHED · LIVE", now, theme)}`
|
|
1515
|
-
: `${paint(theme, color, phaseText)} ${paint(theme, color === "warning"
|
|
1528
|
+
: `${paint(theme, color, phaseText)} ${paint(theme, color === "warning" || color === "error" ? color : "dim", activityMeter)}`;
|
|
1516
1529
|
// The persistent footer is the liveness surface only: host, phase, and
|
|
1517
1530
|
// freshness. The auditor card above already owns the transition hint
|
|
1518
1531
|
// ("next:"), the worker attribution ("detached worker"), and the
|
|
@@ -1521,7 +1534,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1521
1534
|
const quietAge = phase === "quiet" ? auditorActivityAge(audit, now) : undefined;
|
|
1522
1535
|
const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
|
|
1523
1536
|
const compactPrefix = extras?.compactAuditCard
|
|
1524
|
-
? `${paint(theme, color, durableLabel ?? (toolWait ? `AUDIT TOOL WAIT · ${sanitizeDisplayText(audit!.currentTool!)} ${fmtElapsed(now - audit!.currentToolStartedAt!)} / ${fmtElapsed(audit!.toolTimeoutMs!)}` : phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
|
|
1537
|
+
? `${paint(theme, color, durableLabel ?? (toolWait ? `AUDIT TOOL WAIT · ${sanitizeDisplayText(audit!.currentTool!)} ${fmtElapsed(now - audit!.currentToolStartedAt!)} / ${fmtElapsed(audit!.toolTimeoutMs!)}` : phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "stalled" ? "AUDIT STALLED — worker dead" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
|
|
1525
1538
|
: "";
|
|
1526
1539
|
const activityAge = auditorActivityAge(audit, now);
|
|
1527
1540
|
const freshness = extras?.compactAuditCard && phase !== "quiet" && !toolWait
|
|
@@ -1983,12 +1996,13 @@ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?:
|
|
|
1983
1996
|
}
|
|
1984
1997
|
|
|
1985
1998
|
/** Phase colour for the auditing card's lead row. Blocked/quiet are
|
|
1986
|
-
* warnings; a verdict that landed or live worker evidence is success;
|
|
1999
|
+
* warnings, stalled is an error; a verdict that landed or live worker evidence is success;
|
|
1987
2000
|
* queued/running-without-evidence stay neutral accent — a hung provider
|
|
1988
2001
|
* must never wear success (v0.34.39). Colour always pairs with the phase
|
|
1989
2002
|
* words (paint wraps the label, never replaces it), so a themeless
|
|
1990
2003
|
* renderer loses colour but never meaning. */
|
|
1991
2004
|
function auditorPhaseTone(phase: AuditorDisplayPhase, live: boolean): DisplayColor {
|
|
2005
|
+
if (phase === "stalled") return "error";
|
|
1992
2006
|
if (phase === "blocked" || phase === "quiet") return "warning";
|
|
1993
2007
|
if (phase === "awaiting-verdict" || (phase === "running" && live)) return "success";
|
|
1994
2008
|
return "accent";
|
|
@@ -2009,6 +2023,7 @@ function auditorCardModelRef(audit: AuditDisplayProgress | null | undefined, cla
|
|
|
2009
2023
|
* row and the closing line can never name different actions. */
|
|
2010
2024
|
function auditorNextAction(phase: AuditorDisplayPhase): string {
|
|
2011
2025
|
if (phase === "quiet") return "/goal cancel discards the claim";
|
|
2026
|
+
if (phase === "stalled") return "/goal resume retries the claim";
|
|
2012
2027
|
if (phase === "blocked") return "/goal resume retries the claim";
|
|
2013
2028
|
if (phase === "awaiting-verdict") return "completion review applying";
|
|
2014
2029
|
if (phase === "queued") return "worker starting";
|
|
@@ -2082,7 +2097,11 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
|
|
|
2082
2097
|
const staleSeg = activity === undefined && durable?.stale && durable.idleMs !== undefined
|
|
2083
2098
|
? ` · no progress ${fmtElapsed(durable.idleMs)}`
|
|
2084
2099
|
: "";
|
|
2085
|
-
|
|
2100
|
+
// v0.39.22: a stalled audit asserts no live worker — printing
|
|
2101
|
+
// "detached worker" would claim a process that has not spoken for over
|
|
2102
|
+
// an hour. The `auditor:` anchor stays; only the worker segment changes.
|
|
2103
|
+
const workerSeg = phase === "stalled" ? "no worker responds" : "detached worker";
|
|
2104
|
+
const lead = [`├─ auditor: ${paint(theme, auditorPhaseTone(phase, phaseLive), effectivePhaseLabel)}${detail} · ${workerSeg} · ${ageSeg}${staleSeg}`];
|
|
2086
2105
|
// Lead row 2: current tool + time budget, effective model + thinking.
|
|
2087
2106
|
// Absent facts are omitted, never invented. The tool observation lives
|
|
2088
2107
|
// ONLY here (it moved out of the tail) so the card keeps its
|
|
@@ -2161,7 +2180,9 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
|
|
|
2161
2180
|
tail.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", observation)}`);
|
|
2162
2181
|
});
|
|
2163
2182
|
const last = auditorLastActivity(audit, now);
|
|
2164
|
-
if (phase === "
|
|
2183
|
+
if (phase === "stalled") {
|
|
2184
|
+
tail.push(`└─ ${paint(theme, "error", `auditor stalled ${fmtElapsed(activity ?? 0)} — no worker activity, claim durable; /goal resume retries, /goal cancel discards`)}`);
|
|
2185
|
+
} else if (phase === "quiet") {
|
|
2165
2186
|
const quietMs = activity ?? 0;
|
|
2166
2187
|
tail.push(`└─ ${paint(theme, "warning", `auditor quiet ${fmtElapsed(quietMs)}${last} — may be stuck; /goal cancel discards the claim`)}`);
|
|
2167
2188
|
} else if (phase === "blocked") {
|
|
@@ -2306,7 +2327,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
2306
2327
|
if (extras?.compactAuditCard) {
|
|
2307
2328
|
const phase = auditorDisplayPhase(g, audit, now);
|
|
2308
2329
|
const durable = auditLifecycleProjection(g.pendingCompletion, { now });
|
|
2309
|
-
|
|
2330
|
+
// v0.39.22: stalled needs recovery like quiet/blocked — without it
|
|
2331
|
+
// the glance path falls through to "Audit review pending · detached
|
|
2332
|
+
// worker" + "No action needed", preserving the can't-tell gap on
|
|
2333
|
+
// the production surface. With it, the glance reuses the detailed
|
|
2334
|
+
// lead (stalled · no worker responds) and its retry action row.
|
|
2335
|
+
const needsRecovery = auditRecoveryPending(g) || phase === "quiet" || phase === "stalled" || phase === "blocked" || durable?.phase === "settling";
|
|
2310
2336
|
const elapsed = auditorElapsedMs(audit, now);
|
|
2311
2337
|
const finished = audit?.toolCalls?.filter(call => call.finishedAt !== undefined).length;
|
|
2312
2338
|
const age = auditorActivityAge(audit, now);
|
|
@@ -637,6 +637,8 @@ function registerAgentTools(pi: any): void {
|
|
|
637
637
|
"v0.38.37: what this turn deliberately left out (scope cut, deferred item + why, in plain words). " +
|
|
638
638
|
"Renders under `### Remaining` in the user-facing terminal summary. " +
|
|
639
639
|
"Bounds match the documented 10k-char value guard — longer input clips at a clause boundary, never a refusal. " +
|
|
640
|
+
"Never repeat a fact already covered by Unresolved: Unresolved names open problems and risks, leftOut names deliberately deferred scope and why. " +
|
|
641
|
+
"When both would say the same thing, put it in Unresolved once and omit leftOut. " +
|
|
640
642
|
"Omit when nothing was deliberately left out — absent stays absent, never invented.",
|
|
641
643
|
})),
|
|
642
644
|
showVerification: Type.Optional(Type.Boolean({
|
|
@@ -914,6 +914,8 @@ function scheduleUIRefresh(): void {
|
|
|
914
914
|
// suppresses the notify (it is automatic machinery), never the recording.
|
|
915
915
|
let auditorQuietSince: number | null = null;
|
|
916
916
|
let auditorQuietNotified = false;
|
|
917
|
+
let auditorStallSince: number | null = null;
|
|
918
|
+
let auditorStallNotified = false;
|
|
917
919
|
let lastAuditorQuietStretch: { ms: number; endedAt: number } | null = null;
|
|
918
920
|
|
|
919
921
|
/** Drive the quiet watcher once per UI tick. Returns a warning message to
|
|
@@ -929,6 +931,8 @@ export function __auditorQuietWatchTick(now = Date.now()): string | null {
|
|
|
929
931
|
auditorQuietSince = null;
|
|
930
932
|
auditorQuietNotified = false;
|
|
931
933
|
}
|
|
934
|
+
auditorStallSince = null;
|
|
935
|
+
auditorStallNotified = false;
|
|
932
936
|
return null;
|
|
933
937
|
}
|
|
934
938
|
const phase = auditorDisplayPhase(g, latestAuditProgress, now);
|
|
@@ -945,6 +949,23 @@ export function __auditorQuietWatchTick(now = Date.now()): string | null {
|
|
|
945
949
|
}
|
|
946
950
|
return null;
|
|
947
951
|
}
|
|
952
|
+
// v0.39.22: a stalled worker gets its own one-shot notify — the quiet
|
|
953
|
+
// notice fired an hour ago and reads as routine by now. The claim stays
|
|
954
|
+
// durable either way; the notify names the retry, not just the discard.
|
|
955
|
+
if (phase === "stalled") {
|
|
956
|
+
if (auditorStallSince === null) {
|
|
957
|
+
auditorStallSince = typeof latestAuditProgress?.lastActivityAt === "number"
|
|
958
|
+
? latestAuditProgress.lastActivityAt
|
|
959
|
+
: now;
|
|
960
|
+
}
|
|
961
|
+
if (!auditorStallNotified && !supervisorPaused(state)) {
|
|
962
|
+
auditorStallNotified = true;
|
|
963
|
+
return `glla: the detached auditor is STALLED — no worker activity for ${fmtElapsed(now - auditorStallSince)} and no tool running. The claim is durable; /goal resume retries it, /goal cancel discards it.`;
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
if (auditorStallSince !== null) auditorStallSince = null;
|
|
968
|
+
auditorStallNotified = false;
|
|
948
969
|
// Left the quiet phase: record the ended stretch for the footer summary.
|
|
949
970
|
if (auditorQuietSince !== null) {
|
|
950
971
|
const ms = now - auditorQuietSince;
|
|
@@ -1212,6 +1233,8 @@ export function __testOnlyAuditorQuietWatchTick(
|
|
|
1212
1233
|
export function __testOnlyResetAuditorQuietWatch(): void {
|
|
1213
1234
|
auditorQuietSince = null;
|
|
1214
1235
|
auditorQuietNotified = false;
|
|
1236
|
+
auditorStallSince = null;
|
|
1237
|
+
auditorStallNotified = false;
|
|
1215
1238
|
lastAuditorQuietStretch = null;
|
|
1216
1239
|
}
|
|
1217
1240
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.23",
|
|
4
4
|
"description": "Durable work supervision for Pi: turn outcomes into goals, audited queues and spec-driven builds, recover interruptions, and verify completion against evidence.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|