pi-goal-list-loop-audit 0.38.45 → 0.38.46

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.46 — rich Antigravity-style terminal summaries (2026-09-11)
4
+
5
+ ### Changed
6
+
7
+ - **Rich terminal voice (field 20260911_003839/003903/003907):** the terminal approval render is now `## Done — outcome` + `### Key Findings & Remediation` (numbered, bold lead + code refs) + `### Verification Summary` (`| Check | Status | Details |` table with Tests PASS/FAIL and the audit verdict row) + `### Next`, closed by the pinned approval/record trailer. The flat six-bullet card is gone from chat and transcript.
8
+ - **Rich archive too:** `archiveCurrentGoal` appends a `## Terminal summary` section built from the same parts over the verbatim six-label machine record; aborted records wear a `## Aborted —` headline, never `Done`.
9
+ - **Scope held:** progress cards, status line, and external notifies keep their compact projections; the legacy `buildApprovalChatLines` helper is untouched.
10
+ - **Budgets (verbose by owner choice):** findings cap 8, values/table cells 200 chars, Next cap 4; table pipes escaped; Tests status honest (PASS only on `pass` without nonzero `fail`, else FAIL/REPORTED); empty sections omitted, never invented.
11
+ - **Pins:** `tests/rich-terminal-summary.test.ts` (8) — headline, numbering, table, FAIL detection, pipe escaping, stale-Next behavior, trailer contract, no-audit path, archive headlines; terminal-render + communication suites migrated to the rich shape.
12
+
3
13
  ## 0.38.45 — full-project audit pass: 16 fixes across version, summary, and approval paths (2026-09-10)
4
14
 
5
15
  ### Fixed
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.45 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.46 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
@@ -269,6 +269,148 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
269
269
  return kept;
270
270
  }
271
271
 
272
+ /** Rich terminal voice (field 20260911_003839/003903/003907 — the
273
+ * Antigravity close): section headers, numbered findings with bold
274
+ * leads + code refs, and a verification table, instead of the flat
275
+ * six-bullet card. Scope is the terminal render only (chat +
276
+ * transcript + archive human layer); progress cards, status line, and
277
+ * external notifies keep their compact projections. Verbose by owner
278
+ * choice: findings cap 8, values 200, table cells 200. */
279
+ export const RICH_FINDINGS_CAP = 8;
280
+ export const RICH_VALUE_BUDGET = 200;
281
+ export const RICH_NEXT_CAP = 4;
282
+
283
+ export interface RichTerminalParts {
284
+ headline: string;
285
+ findingLines: string[];
286
+ tableLines: string[];
287
+ nextLines: string[];
288
+ }
289
+
290
+ function escapeTableCell(value: string): string {
291
+ return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
292
+ }
293
+
294
+ function testsRowStatus(value: string): string {
295
+ const fail = /(\d+)\s*fail/i.exec(value);
296
+ if (fail && Number.parseInt(fail[1]!, 10) > 0) return "FAIL";
297
+ if (/pass/i.test(value)) return "PASS";
298
+ return "REPORTED";
299
+ }
300
+
301
+ function auditRowStatus(history: Goal["auditHistory"]): string {
302
+ const entries = Array.isArray(history) ? history : [];
303
+ const last = entries[entries.length - 1];
304
+ if (!last) return "NO VERDICT";
305
+ if (last.approved) return `APPROVED \u00d7${entries.length}`;
306
+ if (last.disapproved) return `DISAPPROVED \u00d7${entries.length}`;
307
+ if (last.impossible) return "IMPOSSIBLE";
308
+ return "NO VERDICT";
309
+ }
310
+
311
+ /** Split a stale-filtered `Label: value` detail into a bold lead + body. */
312
+ function leadBody(detail: string): { lead: string; body: string } {
313
+ const separator = detail.indexOf(":");
314
+ if (separator < 0) return { lead: "Note", body: detail };
315
+ return { lead: detail.slice(0, separator).trim() || "Note", body: detail.slice(separator + 1).trim() };
316
+ }
317
+
318
+ /** Partition informing details into findings / Tests / next buckets. */
319
+ export function partitionRichDetails(details: string[]): { findings: string[]; tests: string[]; next: string[] } {
320
+ const findings: string[] = [];
321
+ const tests: string[] = [];
322
+ const next: string[] = [];
323
+ for (const detail of details) {
324
+ if (/^\s*Tests\s*:/i.test(detail)) tests.push(detail);
325
+ else if (/^\s*(Next|Unresolved|Left out)\s*:/i.test(detail)) next.push(detail);
326
+ else findings.push(detail);
327
+ }
328
+ return { findings, tests, next };
329
+ }
330
+
331
+ /** Build the section parts shared by chat, transcript, and archive. */
332
+ export function buildRichTerminalParts(args: {
333
+ outcome: string;
334
+ details: string[];
335
+ countsLine: string;
336
+ auditHistory?: Goal["auditHistory"];
337
+ }): RichTerminalParts {
338
+ const { findings, tests, next } = partitionRichDetails(args.details);
339
+ const findingLines = findings.slice(0, RICH_FINDINGS_CAP).map((detail, i) => {
340
+ const { lead, body } = leadBody(detail);
341
+ return `${i + 1}. **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
342
+ });
343
+ const tableRows: string[] = [];
344
+ for (const detail of tests.slice(0, 2)) {
345
+ const { body } = leadBody(detail);
346
+ tableRows.push(`| Tests | ${testsRowStatus(body)} | ${escapeTableCell(clipSummaryValue(body, RICH_VALUE_BUDGET))} |`);
347
+ }
348
+ const auditStatus = auditRowStatus(args.auditHistory);
349
+ if (auditStatus !== "NO VERDICT") {
350
+ const auditBody = args.countsLine.replace(/^\u2014\s*/, "").replace(/\.\s*$/, "");
351
+ tableRows.push(`| Audit | ${auditStatus} | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} |`);
352
+ }
353
+ const tableLines = tableRows.length > 0
354
+ ? ["| Check | Status | Details |", "| --- | --- | --- |", ...tableRows]
355
+ : [];
356
+ const nextLines = next.slice(0, RICH_NEXT_CAP).map((detail) => {
357
+ const { lead, body } = leadBody(detail);
358
+ return `- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
359
+ });
360
+ return {
361
+ headline: `## Done \u2014 ${args.outcome}`,
362
+ findingLines,
363
+ tableLines,
364
+ nextLines,
365
+ };
366
+ }
367
+
368
+ /** Compose parts + headline + section headers into markdown lines. */
369
+ export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { headline?: string }): string[] {
370
+ const lines = [opts?.headline ?? parts.headline, ""];
371
+ if (parts.findingLines.length > 0) {
372
+ lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
373
+ }
374
+ if (parts.tableLines.length > 0) {
375
+ lines.push("### Verification Summary", ...parts.tableLines, "");
376
+ }
377
+ if (parts.nextLines.length > 0) {
378
+ lines.push("### Next", ...parts.nextLines, "");
379
+ }
380
+ return lines;
381
+ }
382
+
383
+ /** Rich human layer for the durable archive (owner: the archive IS for
384
+ * humans). Same sections as chat, composed from durable goal state — the
385
+ * raw six-label recap stays verbatim in `## Completion summary` as the
386
+ * machine layer. Headline follows terminal status; aborted records never
387
+ * wear a `Done` headline. */
388
+ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string): string[] {
389
+ const facts: CompletionSummaryFacts = { goal, status, archivePath };
390
+ const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
391
+ const brief = humanCompletionBrief(summary, 140, RICH_VALUE_BUDGET);
392
+ const history = goal.auditHistory ?? [];
393
+ const latest = history[history.length - 1];
394
+ const approval = latest
395
+ ? `\u2014 auditor ${latest.approved ? "approved" : latest.disapproved ? "disapproved" : latest.impossible ? "impossible" : "left no verdict"}.`
396
+ : "\u2014 completed without a recorded auditor verdict.";
397
+ const countsLine = buildAuditCountsLine(goal);
398
+ const parts = buildRichTerminalParts({
399
+ outcome: brief.outcome,
400
+ details: withoutStaleNext(brief.details),
401
+ countsLine,
402
+ auditHistory: history,
403
+ });
404
+ return [
405
+ ...composeRichTerminalLines(parts, {
406
+ headline: status === "complete" ? parts.headline : `## Aborted \u2014 ${brief.outcome}`,
407
+ }),
408
+ trailerBullet(stripApprovalModel(approval)),
409
+ trailerBullet(countsLine),
410
+ trailerBullet(`\u2014 record: ${archivePath}`),
411
+ ];
412
+ }
413
+
272
414
  /** v0.38.20: the approval chat notify — outcome first, all bounded
273
415
  * informing details (the full record lives in the archive and the
274
416
  * transcript notice), then the approval trailer and the record pointer.
@@ -382,15 +524,23 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
382
524
  };
383
525
  const candidate = input.completionSummary ?? input.goal.completionSummary;
384
526
  const recap = compactTerminalCompletionSummary(facts, candidate);
385
- const baseBrief = terminalHumanBrief(facts, candidate);
527
+ // Rich voice (field 20260911_*): the chat/transcript render uses a
528
+ // verbose brief (200-char values) while the outcome headline keeps
529
+ // the 140-char budget. Filler drops via the same briefValueContent
530
+ // filter; absent stays absent.
531
+ const richBrief = humanCompletionBrief(
532
+ resolveCompletionSummary(facts, candidate).summary,
533
+ 140,
534
+ RICH_VALUE_BUDGET,
535
+ );
386
536
  // v0.38.37 (audit 2026-09-08): the deliberate non-do comes from the
387
537
  // agent's complete_goal leftOut claim — never invented. Filler ("none")
388
538
  // drops via the same briefValueContent filter; absent stays absent.
389
539
  const leftOut = input.leftOut?.trim();
390
540
  const leftOutContent = leftOut ? briefValueContent(leftOut) : null;
391
- const brief = leftOutContent
392
- ? { ...baseBrief, details: [...baseBrief.details, `Left out: ${clipSummaryValue(leftOutContent, 120)}`] }
393
- : baseBrief;
541
+ const richDetails = leftOutContent
542
+ ? [...richBrief.details, `Left out: ${clipSummaryValue(leftOutContent, RICH_VALUE_BUDGET)}`]
543
+ : richBrief.details;
394
544
  const countsLine = input.countsLine ?? buildAuditCountsLine(input.goal, input.auditNote);
395
545
  // v0.38.42 (field 20260909_140404): one canonical approval bullet in
396
546
  // chat and transcript — no model ID in either, via-retry news kept. A
@@ -408,19 +558,31 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
408
558
  const approvalBullet = foldCounts
409
559
  ? `• ${chatApproval.replace(/^—\s*/, "").replace(/\.\s*$/, "")} (${history.length} verdict).`
410
560
  : trailerBullet(chatApproval);
561
+ const recordBullet = trailerBullet(input.record);
562
+ // Rich voice: headline + Key Findings + Verification table + Next,
563
+ // closed by the pinned trailer (approval/counts/record, record last).
564
+ // The transcript mirrors chat without the record pointer and extras —
565
+ // the record lives in chat and the archive, matching the old contract.
566
+ const richParts = buildRichTerminalParts({
567
+ outcome: richBrief.outcome,
568
+ details: withoutStaleNext(richDetails),
569
+ countsLine,
570
+ auditHistory: input.goal.auditHistory,
571
+ });
572
+ const chatBody = composeRichTerminalLines(richParts);
573
+ const transcriptBody = composeRichTerminalLines(richParts);
411
574
  return {
412
575
  chatLines: [
413
- `✓ done — ${brief.outcome}`,
414
- ...withoutStaleNext(brief.details).map((detail) => `• ${detail}`),
576
+ ...chatBody,
415
577
  approvalBullet,
416
578
  ...(foldCounts ? [] : [trailerBullet(countsLine)]),
417
- trailerBullet(input.record),
579
+ recordBullet,
418
580
  ...(input.extras ?? []),
419
581
  ],
420
582
  recap,
421
- transcriptLines: [...withoutStaleNext(brief.details).map((detail) => `• ${detail}`), approvalBullet],
583
+ transcriptLines: [...transcriptBody, approvalBullet],
422
584
  countsLine,
423
- outcome: brief.outcome,
585
+ outcome: richBrief.outcome,
424
586
  approval: input.approval,
425
587
  };
426
588
  }
@@ -241,6 +241,7 @@ import {
241
241
  import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
242
242
  import {
243
243
  buildLoopCompletionSummary,
244
+ buildRichArchiveSection,
244
245
  compactLoopCompletionSummary,
245
246
  compactTerminalCompletionSummary,
246
247
  isTerminalLoopStopReason,
@@ -1142,6 +1143,12 @@ function archiveCurrentGoal(
1142
1143
  completionSummary: summaryResolution.summary,
1143
1144
  };
1144
1145
  const md = renderGoalMarkdown(terminalGoal);
1146
+ // Field 20260911_* (rich terminal voice): the archive is for humans —
1147
+ // it carries the same rich markdown as chat over the verbatim
1148
+ // six-label machine record. Built from the terminal goal AFTER the
1149
+ // summary fence resolves, so chat and archive cannot disagree.
1150
+ const richSection = buildRichArchiveSection(terminalGoal, status, archivePath);
1151
+ const richMd = `${md}\n## Terminal summary\n\n${richSection.join("\n")}\n`;
1145
1152
  // An existing same-id archive is an immutable fence. Check it before
1146
1153
  // publishing an intent so an unrelated/sentinel winner can never make the
1147
1154
  // startup reconciler terminalize this live goal.
@@ -1179,7 +1186,7 @@ function archiveCurrentGoal(
1179
1186
  // mitigates, this removes the predictability too).
1180
1187
  const temp = `${target}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
1181
1188
  try {
1182
- fs.writeFileSync(temp, md, { encoding: "utf-8", flag: "wx" });
1189
+ fs.writeFileSync(temp, richMd, { encoding: "utf-8", flag: "wx" });
1183
1190
  fs.linkSync(temp, target);
1184
1191
  return true;
1185
1192
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.45",
3
+ "version": "0.38.46",
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",