pi-goal-list-loop-audit 0.38.12 → 0.38.14

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.14 — human end-of-objective briefing (2026-09-04)
4
+
5
+ ### Changed
6
+ Every `✓ done` chat notify is now a human briefing: outcome first in its own words, only informing labels after it (`none` / `not recorded` / empty filler dropped; `none — <content>` keeps the content). Word-boundary cuts, auditor/no-audit trailer, archive keeps the full six-label record. Runbook in `audit/HUMAN-BRIEF-2026-09-04.md`.
7
+
8
+ ## 0.38.13 — good completion summary (2026-09-04)
9
+
10
+ ### Changed
11
+ The `✓ done` chat notify is now the six-label block (one `Label: value` line each, 240-char word-bounded values) instead of the single-line mash with mid-word cuts. New `clipSummaryValue` cuts at word boundaries everywhere (hard-cut only for spaceless tokens); widget card and external notifies keep the compact line. Fixed a post-archive null crash that silently swallowed the notify. Runbook in `audit/COMPLETION-SUMMARY-2026-09-04.md`.
12
+
3
13
  ## 0.38.12 — last-wins sessions + objectives (2026-09-04)
4
14
 
5
15
  ### Changed
package/docs/INDEX.md CHANGED
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
18
18
  failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
19
19
  handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
20
20
  shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
21
- v0.35.14–v0.38.12 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.14 continue through the supervisor freeze (`/glla pause`),
22
22
  load hold, auditor picker parity, Windows launch fix, zombie-watchdog
23
23
  subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
24
24
  durable state-root selection, blank-until-resume auditor context, frozen
@@ -77,6 +77,21 @@ export function isUsefulCompletionSummary(text: string | undefined): boolean {
77
77
  * Missing labels are retained as `not recorded` so a compact notification
78
78
  * cannot accidentally imply evidence that the durable recap did not contain.
79
79
  */
80
+ /** Cut a summary value at a word boundary, never mid-word (the
81
+ * Screenshot_20260903_204003/204005 complaint: `0 o…`, `qu…`, `belo…`).
82
+ * Falls back to a hard cut only when the head holds no space past the
83
+ * halfway mark — a long token such as a commit hash must not eviscerate
84
+ * the whole value. Short values pass through untouched (no `…`). */
85
+ export function clipSummaryValue(value: string, limit: number): string {
86
+ const clean = value.replace(/\s+/g, " ").trim();
87
+ const capped = Number.isFinite(limit) ? Math.max(8, Math.floor(limit)) : 72;
88
+ if (clean.length <= capped) return clean;
89
+ const head = clean.slice(0, capped - 1);
90
+ const space = head.lastIndexOf(" ");
91
+ const kept = (space > capped / 2 ? head.slice(0, space) : head).trimEnd();
92
+ return `${kept}…`;
93
+ }
94
+
80
95
  export function compactCompletionSummary(text: string | undefined, maxValueLength = 72): string {
81
96
  const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
82
97
  if (!source) return "not recorded";
@@ -96,11 +111,78 @@ export function compactCompletionSummary(text: string | undefined, maxValueLengt
96
111
  .sort((a, b) => a - b)[0] ?? source.length;
97
112
  const rawValue = source.slice(valueStart, nextStart).trim();
98
113
  const value = rawValue || "not recorded";
99
- return `${name}: ${value.length > limit ? `${value.slice(0, limit - 1)}…` : value}`;
114
+ return `${name}: ${clipSummaryValue(value, limit)}`;
100
115
  });
101
116
  return parts.join(" · ");
102
117
  }
103
118
 
119
+ /** A recap value informs the human only when it is not an empty / none /
120
+ * not-recorded placeholder. `None — <real content>` (a common agent habit)
121
+ * keeps just the content. Returns null for filler. */
122
+ export function briefValueContent(value: string): string | null {
123
+ const clean = value.replace(/\s+/g, " ").trim();
124
+ if (!clean) return null;
125
+ // System placeholder vocabulary: the `— explanation` is still a placeholder.
126
+ if (/^not recorded\b/i.test(clean)) return null;
127
+ // Agent habit: `none — <real content>` hides information behind filler.
128
+ const prefixed = /^none\s*[—–:\-]\s*(.+)$/i.exec(clean);
129
+ const body = (prefixed?.[1] ?? clean).trim();
130
+ if (/^(none|n\/a|nil|nothing)(\s+for\s+this\s+\w+)?\.?$/i.test(body)) return null;
131
+ return body;
132
+ }
133
+
134
+ export interface HumanCompletionBrief {
135
+ outcome: string;
136
+ details: string[];
137
+ }
138
+
139
+ /** The human briefing: outcome first in its own words, then only the
140
+ * labels that carry real content (filler like `Unresolved: none` or
141
+ * `Changed: not recorded` is dropped, never shown). The durable archive
142
+ * keeps the full six-label record; this is the end-of-objective voice
143
+ * that informs the user at a glance. */
144
+ export function humanCompletionBrief(
145
+ text: string | undefined,
146
+ outcomeBudget = 140,
147
+ valueBudget = 120,
148
+ ): HumanCompletionBrief {
149
+ const lines = completionSummaryLines(text, Math.max(outcomeBudget, valueBudget));
150
+ const rawOutcome = (lines[0] ?? "").replace(/^Outcome:\s*/, "");
151
+ const outcome = clipSummaryValue(briefValueContent(rawOutcome) ?? "done", outcomeBudget);
152
+ const details: string[] = [];
153
+ for (const line of lines.slice(1)) {
154
+ const separator = line.indexOf(":");
155
+ if (separator < 0) continue;
156
+ const content = briefValueContent(line.slice(separator + 1));
157
+ if (content) details.push(`${line.slice(0, separator)}: ${clipSummaryValue(content, valueBudget)}`);
158
+ }
159
+ return { outcome, details };
160
+ }
161
+
162
+ /** Multi-line projection: one `Label: value` line per label with generous
163
+ * word-bounded values. This is the user-facing `✓ done` block — six short
164
+ * facts that stay scannable in chat. The single-line projection remains
165
+ * for width-bound surfaces (TUI widget card, external notifies). */
166
+ export function completionSummaryLines(text: string | undefined, maxValueLength = 240): string[] {
167
+ const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
168
+ const lower = source.toLowerCase();
169
+ const positions = COMPLETION_SUMMARY_LABELS
170
+ .map((label) => ({ label, start: lower.indexOf(label.toLowerCase()) }))
171
+ .filter((entry) => entry.start >= 0);
172
+ return COMPLETION_SUMMARY_LABELS.map((label) => {
173
+ const name = label.slice(0, -1);
174
+ const current = positions.find((entry) => entry.label === label);
175
+ if (!source || !current) return `${name}: not recorded`;
176
+ const valueStart = current.start + label.length;
177
+ const nextStart = positions
178
+ .filter((entry) => entry.start > current.start)
179
+ .map((entry) => entry.start)
180
+ .sort((a, b) => a - b)[0] ?? source.length;
181
+ const rawValue = source.slice(valueStart, nextStart).trim();
182
+ return `${name}: ${clipSummaryValue(rawValue || "not recorded", maxValueLength)}`;
183
+ });
184
+ }
185
+
104
186
  function safeFact(value: unknown, fallback = "not recorded"): string {
105
187
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
106
188
  return text || fallback;
@@ -197,6 +279,25 @@ export function compactTerminalCompletionSummary(
197
279
  return compactCompletionSummary(resolveCompletionSummary(facts, candidate).summary, maxValueLength);
198
280
  }
199
281
 
282
+ /** Brief twin of compactTerminalCompletionSummary for the `✓ done` chat
283
+ * notifies: same resolved facts, outcome + informing labels only. */
284
+ export function terminalHumanBrief(
285
+ facts: CompletionSummaryFacts,
286
+ candidate = facts.goal.completionSummary,
287
+ ): HumanCompletionBrief {
288
+ return humanCompletionBrief(resolveCompletionSummary(facts, candidate).summary);
289
+ }
290
+
291
+ /** Multi-line twin of compactTerminalCompletionSummary for the `✓ done`
292
+ * chat notifies: same resolved facts, one `Label: value` line each. */
293
+ export function terminalCompletionSummaryLines(
294
+ facts: CompletionSummaryFacts,
295
+ candidate = facts.goal.completionSummary,
296
+ maxValueLength = 240,
297
+ ): string[] {
298
+ return completionSummaryLines(resolveCompletionSummary(facts, candidate).summary, maxValueLength);
299
+ }
300
+
200
301
  /**
201
302
  * Loop terminal states use the same six-label user-facing contract. Loop
202
303
  * state is converted to the smaller Goal-shaped fact set by the caller, so
@@ -240,7 +240,7 @@ import {
240
240
  pushCapped as pushRepetitionCapped,
241
241
  } from "../goal-loop-repetition.js";
242
242
  import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
243
- import { compactCompletionSummary, compactTerminalCompletionSummary, isGenericCompletionSummary, missingCompletionSummaryLabels } from "../completion-summary.js";
243
+ import { compactCompletionSummary, compactTerminalCompletionSummary, isGenericCompletionSummary, missingCompletionSummaryLabels, terminalHumanBrief } from "../completion-summary.js";
244
244
  import {
245
245
  defaultAgentDir,
246
246
  resolveEffectiveSubagentModel,
@@ -1415,6 +1415,13 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
1415
1415
  stopReason: terminalReason,
1416
1416
  archivePath: path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id),
1417
1417
  }, state.goal.completionSummary);
1418
+ // Computed pre-archive: archiveCurrentGoal clears state.goal.
1419
+ const brief = terminalHumanBrief({
1420
+ goal: state.goal,
1421
+ status: "complete",
1422
+ stopReason: terminalReason,
1423
+ archivePath: path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id),
1424
+ }, state.goal.completionSummary);
1418
1425
  const approvalVia = `${origin === "manual" ? " on /goal verify" : origin === "session-recovery" ? " after session recovery" : " on the provider retry"}${fallbackUsed ? " after an auditor-model fallback" : ""}`;
1419
1426
  const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`);
1420
1427
  if (!archived) {
@@ -1431,7 +1438,10 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
1431
1438
  appendLedger(liveCtx.cwd, "goal_archive_failed_after_approval", { goalId, attemptId: claim.attemptId, origin });
1432
1439
  return;
1433
1440
  }
1434
- liveCtx.ui.notify(`✓ done: ${recap} — auditor ${result.model} approved${approvalVia}.`, "info");
1441
+ // v0.38.14: the chat notify is the human briefing — outcome first,
1442
+ // filler labels dropped — while the external notify keeps the compact
1443
+ // single line (pager/sound safe).
1444
+ liveCtx.ui.notify(`✓ done — ${brief.outcome}\n${[...brief.details, `— auditor ${result.model} approved${approvalVia}.`].join("\n")}`, "info");
1435
1445
  notifyExternal(liveCtx, `Goal complete (auditor approved, ${origin}): ${recap}`);
1436
1446
  return;
1437
1447
  }
@@ -250,7 +250,7 @@ import {
250
250
  pushCapped as pushRepetitionCapped,
251
251
  } from "../goal-loop-repetition.js";
252
252
  import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
253
- import { compactCompletionSummary, compactTerminalCompletionSummary, resolveCompletionSummary } from "../completion-summary.js";
253
+ import { compactCompletionSummary, compactTerminalCompletionSummary, resolveCompletionSummary, terminalHumanBrief } from "../completion-summary.js";
254
254
  import {
255
255
  defaultAgentDir,
256
256
  resolveEffectiveSubagentModel,
@@ -1216,9 +1216,16 @@ function registerAgentTools(pi: any): void {
1216
1216
  details: {},
1217
1217
  };
1218
1218
  }
1219
- ctx.ui.notify(`✓ done without audit (user choice): ${recap}`, "info");
1219
+ const brief = terminalHumanBrief({
1220
+ goal: terminalGoal,
1221
+ status: "complete",
1222
+ stopReason: terminalReason,
1223
+ archivePath: path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, terminalGoal.id)) || archivedGoalPath(ctx.cwd, terminalGoal.id),
1224
+ });
1225
+ const briefBlock = [...brief.details, `— completed without audit (your choice).`].join("\n");
1226
+ ctx.ui.notify(`✓ done — ${brief.outcome}\n${briefBlock}`, "info");
1220
1227
  notifyExternal(ctx, `Goal complete without audit (user choice): ${recap}`);
1221
- return { content: [{ type: "text", text: `Goal marked complete without audit (user choice).\n\n${recap}` }], details: {} };
1228
+ return { content: [{ type: "text", text: `Goal marked complete without audit (user choice).\n\n${briefBlock}` }], details: {} };
1222
1229
  }
1223
1230
  scheduleContinuation(ctx, true);
1224
1231
  return {
@@ -1243,6 +1250,13 @@ function registerAgentTools(pi: any): void {
1243
1250
  stopReason: terminalReason,
1244
1251
  archivePath: path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, state.goal.id)) || archivedGoalPath(ctx.cwd, state.goal.id),
1245
1252
  }, state.goal.completionSummary);
1253
+ // Computed pre-archive: archiveCurrentGoal clears state.goal.
1254
+ const brief = terminalHumanBrief({
1255
+ goal: state.goal,
1256
+ status: "complete",
1257
+ stopReason: terminalReason,
1258
+ archivePath: path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, state.goal.id)) || archivedGoalPath(ctx.cwd, state.goal.id),
1259
+ }, state.goal.completionSummary);
1246
1260
  const archived = archiveCurrentGoal(ctx, "complete", terminalReason);
1247
1261
  if (!archived) {
1248
1262
  // The archive helper preserves the live objective and emits the
@@ -1258,7 +1272,7 @@ function registerAgentTools(pi: any): void {
1258
1272
  appendLedger(ctx.cwd, "goal_archive_failed_after_approval", { goalId: state.goal?.id, origin: "manual-verify", model: result.model });
1259
1273
  return { content: [{ type: "text", text: "The auditor approved, but the terminal archive could not be persisted. The goal is paused; fix persistence, resume, and retry complete_goal." }], details: {} };
1260
1274
  }
1261
- ctx.ui.notify(`✓ done: ${recap} — auditor ${result.model} approved.`, "info");
1275
+ ctx.ui.notify(`✓ done — ${brief.outcome}\n${[...brief.details, `— auditor ${result.model} approved.`].join("\n")}`, "info");
1262
1276
  notifyExternal(ctx, `Goal complete (auditor approved): ${recap}`);
1263
1277
  return { content: [{ type: "text", text: `Goal approved by auditor ${result.model}.` }], details: {} };
1264
1278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.12",
3
+ "version": "0.38.14",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",