pi-goal-list-loop-audit 0.38.51 → 0.38.52

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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.52 — Second Gemini-gap: gate-inventory table + per-finding Test Results (2026-09-12)
4
+
5
+ Surveyed five more Antigravity report screenshots
6
+ (audit/SECOND-GEMINI-SUMMARY-GAP-2026-09-12.md) and closed the second gap
7
+ in the terminal render only (chat + transcript + archive human layer;
8
+ progress cards, status, widget, external notifies unchanged).
9
+
10
+ ### Added
11
+
12
+ - **Agent-supplied gate inventory:** optional `complete_goal gateRows`
13
+ (one row per quality gate with scope and outcome notes, sanitized at
14
+ the claim boundary, max 10). The Verification Summary widens to
15
+ Quality Gate | Scope | Status | Notes and supersedes the mechanical
16
+ Tests rows. Statuses are mechanically derived, never claimed: PASS
17
+ only when the notes say pass with zero failures, FAIL on nonzero
18
+ failures, REPORTED otherwise — so bare-exit notes like `Clean exit 0`
19
+ honestly stay REPORTED. Absent inventory keeps the 3-col mechanical
20
+ table byte-identical.
21
+ - **Per-finding test proof:** optional parallel `tests` array on each
22
+ `findingGroups` entry (`tests[i]` proves `findings[i]`, clipped to
23
+ findings, sanitized at the claim boundary). Nested area sections render
24
+ each as a `Test Results:` sub-bullet; the 4+-group table rides the
25
+ proof in the Evidence cell (`Evidence · Tests:`). Groups without tests
26
+ render exactly as v0.38.50.
27
+
28
+ ### Preserved
29
+
30
+ - Chat close stays archive-only: no commit hash, emoji markers, or
31
+ artifact links in chat (survey rejects). The verbatim six-label
32
+ machine record, auditor-verified-before-posting, and the honesty
33
+ rules (PASS only on pass, NO VERDICT never renders, pipe escaping)
34
+ are unchanged.
35
+
3
36
  ## 0.38.51 — Bare /list drafts from context like bare /goal (2026-09-12)
4
37
 
5
38
  User note: empty `/list` could not start anything while empty `/goal`
@@ -114,7 +114,10 @@ The full six-label recap lives verbatim in the archive (`## Completion summary`)
114
114
  and the status/history surfaces. The human layer in chat, transcript, and the
115
115
  archive's `## Terminal summary` renders the same facts as rich sections
116
116
  (`## Done: <objective> — <outcome>`, duration line, Key Findings grouped by area
117
- or tabulated at 4+ groups, Verification Summary table, Next, capped and clipped
117
+ or tabulated at 4+ groups with per-finding `Test Results:` proof lines,
118
+ Verification Summary table — widened to Quality Gate | Scope | Status | Notes
119
+ with derived statuses when the agent supplies a gate inventory — Next, capped
120
+ and clipped
118
121
  to fixed budgets). Width-bound and external surfaces (status line, widget card,
119
122
  external notifies) use the compact single-line projection of all six labels.
120
123
  Every terminal goal notification, including version-bearing already-shipped claims,
package/docs/INDEX.md CHANGED
@@ -16,7 +16,7 @@ Policy contracts and recent changes live in the `audit/` directory of the
16
16
  failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
17
17
  handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
18
18
  shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
19
- v0.35.14–v0.38.51 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.38.52 continue through the supervisor freeze (`/glla pause`),
20
20
  load hold, auditor picker parity, Windows launch fix, zombie-watchdog
21
21
  subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
22
22
  durable state-root selection, blank-until-resume auditor context, frozen
@@ -1,4 +1,4 @@
1
- import type { FindingGroup, Goal, Status } from "./goal-loop-core.js";
1
+ import type { FindingGroup, GateRow, Goal, Status } from "./goal-loop-core.js";
2
2
  import { fmtElapsed, truncateCells } from "./goal-loop-display.js";
3
3
 
4
4
  /**
@@ -347,7 +347,8 @@ export function extractEvidenceTokens(text: string): { text: string; evidence: s
347
347
  /**
348
348
  * v0.38.50: enforce the total-findings budget across groups in order —
349
349
  * groups that go empty are dropped so the table/nesting never shows a
350
- * bare area header.
350
+ * bare area header. v0.38.52: parallel `tests` ride along clipped to the
351
+ * surviving findings so tests[i] still proves findings[i].
351
352
  */
352
353
  export function takeBudgetedGroups(groups: FindingGroup[]): FindingGroup[] {
353
354
  const out: FindingGroup[] = [];
@@ -356,7 +357,9 @@ export function takeBudgetedGroups(groups: FindingGroup[]): FindingGroup[] {
356
357
  if (remaining <= 0) break;
357
358
  const findings = group.findings.slice(0, remaining);
358
359
  if (findings.length === 0) continue;
359
- out.push({ title: group.title, findings });
360
+ // v0.38.52: keep the parallel test lines aligned with the survivors.
361
+ const aligned = group.tests?.slice(0, findings.length);
362
+ out.push(aligned && aligned.length > 0 ? { title: group.title, findings, tests: aligned } : { title: group.title, findings });
360
363
  remaining -= findings.length;
361
364
  }
362
365
  return out;
@@ -407,7 +410,12 @@ export function requestEchoHeadline(kind: "Done" | "Aborted", objective: string
407
410
  * v0.38.50: agent-structured groups render as `#### n. Area` subsections
408
411
  * with nested evidence bullets; at RICH_TABLE_GROUP_THRESHOLD groups the
409
412
  * same facts render as an Area | Finding | Evidence table instead.
410
- * Without groups the flat six-label projection stays the fallback. */
413
+ * Without groups the flat six-label projection stays the fallback.
414
+ * v0.38.52: optional per-finding `tests` render as a `Test Results:`
415
+ * sub-bullet (nested) or ride the Evidence cell (table); optional agent
416
+ * `gates` widen the Verification table to Gate | Scope | Status | Notes
417
+ * with mechanically derived statuses, superseding the mechanical Tests
418
+ * rows. Without gates the 3-col mechanical table is byte-identical. */
411
419
  export function buildRichTerminalParts(args: {
412
420
  outcome: string;
413
421
  details: string[];
@@ -416,6 +424,7 @@ export function buildRichTerminalParts(args: {
416
424
  objective?: string;
417
425
  durationLine?: string | null;
418
426
  groups?: FindingGroup[];
427
+ gates?: GateRow[];
419
428
  }): RichTerminalParts {
420
429
  const { findings, tests, next } = partitionRichDetails(args.details);
421
430
  const headline = requestEchoHeadline("Done", args.objective, args.outcome);
@@ -425,21 +434,28 @@ export function buildRichTerminalParts(args: {
425
434
  if (useTable) {
426
435
  findingLines.push("| Area | Finding | Evidence |", "| --- | --- | --- |");
427
436
  for (const group of budgeted) {
428
- for (const finding of group.findings) {
437
+ group.findings.forEach((finding, fi) => {
429
438
  const { text, evidence } = extractEvidenceTokens(finding);
430
439
  const { lead, body } = leadBody(text);
440
+ // v0.38.52: test proof rides the Evidence cell (tables have no
441
+ // sub-bullets); cells stay pipe-escaped and budgeted.
442
+ const proof = group.tests?.[fi]?.trim();
443
+ const evidenceCell = [evidence.join(", ") || "\u2014", ...(proof ? [`Tests: ${proof}`] : [])].join(" \u00b7 ");
431
444
  findingLines.push(
432
- `| ${escapeTableCell(clipSummaryValue(group.title, RICH_OBJECTIVE_ECHO_CHARS))} | ${escapeTableCell(`**${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`)} | ${escapeTableCell(evidence.join(", ") || "\u2014")} |`,
445
+ `| ${escapeTableCell(clipSummaryValue(group.title, RICH_OBJECTIVE_ECHO_CHARS))} | ${escapeTableCell(`**${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`)} | ${escapeTableCell(clipSummaryValue(evidenceCell, RICH_VALUE_BUDGET))} |`,
433
446
  );
434
- }
447
+ });
435
448
  }
436
449
  } else if (budgeted.length > 0) {
437
450
  budgeted.forEach((group, i) => {
438
451
  findingLines.push(`#### ${i + 1}. ${clipSummaryValue(group.title, RICH_OBJECTIVE_ECHO_CHARS)}`);
439
- for (const finding of group.findings) {
452
+ group.findings.forEach((finding, fi) => {
440
453
  const { lead, body } = leadBody(finding);
441
454
  findingLines.push(`- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`);
442
- }
455
+ // v0.38.52: per-finding test proof (shot C) — absent stays absent.
456
+ const proof = group.tests?.[fi]?.trim();
457
+ if (proof) findingLines.push(` - Test Results: ${clipSummaryValue(proof, RICH_VALUE_BUDGET)}`);
458
+ });
443
459
  });
444
460
  } else {
445
461
  findings.slice(0, RICH_FINDINGS_CAP).forEach((detail, i) => {
@@ -448,17 +464,33 @@ export function buildRichTerminalParts(args: {
448
464
  });
449
465
  }
450
466
  const tableRows: string[] = [];
451
- for (const detail of tests.slice(0, 2)) {
452
- const { body } = leadBody(detail);
453
- tableRows.push(`| Tests | ${testsRowStatus(body)} | ${escapeTableCell(clipSummaryValue(body, RICH_VALUE_BUDGET))} |`);
467
+ const gates = (args.gates ?? []).slice(0, 10);
468
+ if (gates.length > 0) {
469
+ // v0.38.52 (shots B/D): the agent inventory supersedes the mechanical
470
+ // Tests rows — one curated gate list, never a duplicated one. Status
471
+ // is derived, never claimed: PASS only when the notes say pass with
472
+ // zero failures ("Clean exit 0"-style notes honestly stay REPORTED).
473
+ for (const row of gates) {
474
+ const derived = testsRowStatus(row.notes ?? "");
475
+ tableRows.push(`| ${escapeTableCell(clipSummaryValue(row.gate, RICH_OBJECTIVE_ECHO_CHARS))} | ${escapeTableCell(clipSummaryValue(row.scope?.trim() || "\u2014", RICH_VALUE_BUDGET))} | ${derived} | ${escapeTableCell(clipSummaryValue(row.notes?.trim() || "\u2014", RICH_VALUE_BUDGET))} |`);
476
+ }
477
+ } else {
478
+ for (const detail of tests.slice(0, 2)) {
479
+ const { body } = leadBody(detail);
480
+ tableRows.push(`| Tests | ${testsRowStatus(body)} | ${escapeTableCell(clipSummaryValue(body, RICH_VALUE_BUDGET))} |`);
481
+ }
454
482
  }
455
483
  const auditStatus = auditRowStatus(args.auditHistory);
456
484
  if (auditStatus !== "NO VERDICT") {
457
485
  const auditBody = args.countsLine.replace(/^\u2014\s*/, "").replace(/\.\s*$/, "");
458
- tableRows.push(`| Audit | ${auditStatus} | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} |`);
486
+ if (gates.length > 0) {
487
+ tableRows.push(`| Audit | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} | ${auditStatus} | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} |`);
488
+ } else {
489
+ tableRows.push(`| Audit | ${auditStatus} | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} |`);
490
+ }
459
491
  }
460
492
  const tableLines = tableRows.length > 0
461
- ? ["| Check | Status | Details |", "| --- | --- | --- |", ...tableRows]
493
+ ? [(gates.length > 0 ? "| Quality Gate | Scope | Status | Notes |" : "| Check | Status | Details |"), (gates.length > 0 ? "| --- | --- | --- | --- |" : "| --- | --- | --- |"), ...tableRows]
462
494
  : [];
463
495
  const nextLines = next.slice(0, RICH_NEXT_CAP).map((detail) => {
464
496
  const { lead, body } = leadBody(detail);
@@ -495,7 +527,7 @@ export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { head
495
527
  * raw six-label recap stays verbatim in `## Completion summary` as the
496
528
  * machine layer. Headline follows terminal status; aborted records never
497
529
  * wear a `Done` headline. */
498
- export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string, findingGroups?: FindingGroup[]): string[] {
530
+ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string, findingGroups?: FindingGroup[], gateRows?: GateRow[]): string[] {
499
531
  const facts: CompletionSummaryFacts = { goal, status, archivePath };
500
532
  const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
501
533
  const brief = humanCompletionBrief(summary, 140, RICH_VALUE_BUDGET);
@@ -513,6 +545,7 @@ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath:
513
545
  objective: goal.objective,
514
546
  durationLine: buildDurationLine(goal),
515
547
  groups: findingGroups,
548
+ gates: gateRows,
516
549
  });
517
550
  return [
518
551
  ...composeRichTerminalLines(parts, {
@@ -611,6 +644,12 @@ export interface TerminalApprovalRenderInput {
611
644
  * sanitized at claim time). Absent keeps the flat six-label fallback.
612
645
  */
613
646
  findingGroups?: FindingGroup[];
647
+ /**
648
+ * v0.38.52: agent-supplied verification gate rows (complete_goal
649
+ * gateRows, sanitized at claim time). Absent keeps the mechanical
650
+ * 3-col verification table.
651
+ */
652
+ gateRows?: GateRow[];
614
653
  }
615
654
 
616
655
  export interface TerminalApprovalRender {
@@ -689,6 +728,7 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
689
728
  objective: input.goal.objective,
690
729
  durationLine: buildDurationLine(input.goal),
691
730
  groups: input.findingGroups,
731
+ gates: input.gateRows,
692
732
  });
693
733
  const chatBody = composeRichTerminalLines(richParts);
694
734
  const transcriptBody = composeRichTerminalLines(richParts);
@@ -282,12 +282,21 @@ export type AuditorRecoveryFailureClass = "transport" | "timeout" | "no-verdict"
282
282
  export interface FindingGroup {
283
283
  title: string;
284
284
  findings: string[];
285
+ /**
286
+ * v0.38.52 (second Gemini-gap survey, shot C): optional per-finding
287
+ * test-result lines, aligned by index with `findings` (tests[i] proves
288
+ * findings[i]). Absent/short entries simply omit the sub-line — old
289
+ * claims without `tests` render byte-identical to v0.38.50.
290
+ */
291
+ tests?: string[];
285
292
  }
286
293
 
287
294
  export const MAX_FINDING_GROUPS = 6;
288
295
  export const MAX_GROUP_FINDINGS = 6;
289
296
  export const MAX_GROUP_TITLE_CHARS = 120;
290
297
  export const MAX_GROUP_FINDING_CHARS = 500;
298
+ /** v0.38.52: per-finding test-result lines share the finding budget. */
299
+ export const MAX_GROUP_TESTS_CHARS = 500;
291
300
 
292
301
  /**
293
302
  * Bound agent-supplied finding groups at the trust boundary. Drops
@@ -313,11 +322,64 @@ export function sanitizeFindingGroups(value: unknown): FindingGroup[] | undefine
313
322
  if (text) findings.push(text);
314
323
  }
315
324
  if (findings.length === 0) continue;
316
- groups.push({ title, findings });
325
+ // v0.38.52: optional test-result lines ride parallel to findings.
326
+ // Non-string entries drop; the array is clipped to the finding count
327
+ // so tests[i] always proves findings[i] after budgeting.
328
+ let tests: string[] | undefined;
329
+ if (Array.isArray(raw.tests)) {
330
+ const parsed: string[] = [];
331
+ for (const entry of raw.tests) {
332
+ if (typeof entry !== "string") continue;
333
+ const text = entry.trim().slice(0, MAX_GROUP_TESTS_CHARS);
334
+ if (text) parsed.push(text);
335
+ }
336
+ if (parsed.length > 0) tests = parsed.slice(0, findings.length);
337
+ }
338
+ groups.push(tests ? { title, findings, tests } : { title, findings });
317
339
  }
318
340
  return groups.length > 0 ? groups : undefined;
319
341
  }
320
342
 
343
+ /**
344
+ * v0.38.52 (second Gemini-gap survey, shots B/D): one agent-supplied
345
+ * verification gate row for the widened terminal table. Presentation
346
+ * only — the six-label completionSummary stays the audited substance.
347
+ * Status is NEVER agent-controlled: the renderer derives PASS/FAIL/
348
+ * REPORTED mechanically from `notes` via testsRowStatus, so a gate can
349
+ * only claim PASS when its notes say so with zero failures.
350
+ */
351
+ export interface GateRow {
352
+ gate: string;
353
+ scope?: string;
354
+ notes?: string;
355
+ }
356
+
357
+ export const MAX_GATE_ROWS = 10;
358
+ export const MAX_GATE_GATE_CHARS = 120;
359
+ export const MAX_GATE_SCOPE_CHARS = 200;
360
+ export const MAX_GATE_NOTES_CHARS = 400;
361
+
362
+ /**
363
+ * Bound agent-supplied gate rows at the trust boundary. Drops blank
364
+ * gates; clips cells. Returns undefined when nothing valid survives —
365
+ * the mechanical 3-col table stays the fallback.
366
+ */
367
+ export function sanitizeGateRows(value: unknown): GateRow[] | undefined {
368
+ if (!Array.isArray(value)) return undefined;
369
+ const rows: GateRow[] = [];
370
+ for (const entry of value) {
371
+ if (rows.length >= MAX_GATE_ROWS) break;
372
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
373
+ const raw = entry as Record<string, unknown>;
374
+ const gate = typeof raw.gate === "string" ? raw.gate.trim().slice(0, MAX_GATE_GATE_CHARS) : "";
375
+ if (!gate) continue;
376
+ const scope = typeof raw.scope === "string" ? raw.scope.trim().slice(0, MAX_GATE_SCOPE_CHARS) : "";
377
+ const notes = typeof raw.notes === "string" ? raw.notes.trim().slice(0, MAX_GATE_NOTES_CHARS) : "";
378
+ rows.push({ gate, ...(scope ? { scope } : {}), ...(notes ? { notes } : {}) });
379
+ }
380
+ return rows.length > 0 ? rows : undefined;
381
+ }
382
+
321
383
  export interface PendingCompletion {
322
384
  completionSummary?: string;
323
385
  verificationSummary?: string;
@@ -352,6 +414,12 @@ export interface PendingCompletion {
352
414
  * Absent means the flat six-label render stays the fallback.
353
415
  */
354
416
  findingGroups?: FindingGroup[];
417
+ /**
418
+ * v0.38.52: agent-supplied verification gate rows for the widened
419
+ * terminal table (complete_goal gateRows, sanitized at claim time).
420
+ * Absent means the mechanical 3-col table stays the fallback.
421
+ */
422
+ gateRows?: GateRow[];
355
423
  /**
356
424
  * Durable one-shot recovery fence. A parked claim may receive one
357
425
  * automatic retry after a validated healthy lifecycle/recovery event;
@@ -2213,6 +2281,10 @@ function normalizePendingCompletion(value: unknown): PendingCompletion {
2213
2281
  // garbage at the boundary degrades to absent (flat fallback), never
2214
2282
  // to a half-structured projection.
2215
2283
  const sanitizedGroups = sanitizeFindingGroups(raw.findingGroups);
2284
+ // v0.38.52: agent-supplied gate rows survive reloads sanitized; garbage
2285
+ // at the boundary degrades to absent (mechanical fallback), never to a
2286
+ // half-structured projection.
2287
+ const sanitizedGates = sanitizeGateRows(raw.gateRows);
2216
2288
  return {
2217
2289
  ...canonicalOrUnknown,
2218
2290
  ...(auditorCandidateRefs !== undefined ? { auditorCandidateRefs } : {}),
@@ -2226,6 +2298,7 @@ function normalizePendingCompletion(value: unknown): PendingCompletion {
2226
2298
  ...(auditorFailureAt ? { auditorFailureAt } : {}),
2227
2299
  ...(timeoutEscalation !== undefined ? { timeoutEscalation } : {}),
2228
2300
  ...(sanitizedGroups ? { findingGroups: sanitizedGroups } : {}),
2301
+ ...(sanitizedGates ? { gateRows: sanitizedGates } : {}),
2229
2302
  ...(phase === "running" || phase === "recovery-pending" || phase === "retry-waiting" ? { phase } : {}),
2230
2303
  ...(typeof raw.retryAttempts === "number"
2231
2304
  ? { retryAttempts: raw.retryAttempts }
@@ -1442,13 +1442,15 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
1442
1442
  // v0.38.37: the deliberate non-do the agent claimed, if any.
1443
1443
  ...(claim.leftOut ? { leftOut: claim.leftOut } : {}),
1444
1444
  // v0.38.50: agent-structured finding groups ride the audited claim.
1445
+ // v0.38.52: same for the gate inventory.
1445
1446
  ...(claim.findingGroups ? { findingGroups: claim.findingGroups } : {}),
1447
+ ...(claim.gateRows ? { gateRows: claim.gateRows } : {}),
1446
1448
  extras: inspectionSessionPath
1447
1449
  ? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
1448
1450
  : [],
1449
1451
  });
1450
1452
  const approvalObjective = state.goal.objective;
1451
- const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`, {}, { findingGroups: claim.findingGroups });
1453
+ const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`, {}, { findingGroups: claim.findingGroups, gateRows: claim.gateRows });
1452
1454
  if (!archived) {
1453
1455
  // archiveCurrentGoal already preserved the live record and warned the
1454
1456
  // user. Keep the approved claim recoverable, but never emit a terminal
@@ -30,6 +30,7 @@ import { state, replaceState, persistStateLine } from "../goal-state.js";
30
30
 
31
31
  import {
32
32
  type FindingGroup,
33
+ type GateRow,
33
34
  type Goal,
34
35
  type Policy,
35
36
  type State,
@@ -1102,7 +1103,8 @@ function archiveCurrentGoal(
1102
1103
  patch: Partial<Pick<Goal, "auditHistory" | "completionSummary" | "pendingTasks">> = {},
1103
1104
  // v0.38.50: agent-structured finding groups ride the audited claim into
1104
1105
  // the archive rich section only — abort/wip paths omit it (flat fallback).
1105
- opts?: { findingGroups?: FindingGroup[] },
1106
+ // v0.38.52: same for the gate inventory.
1107
+ opts?: { findingGroups?: FindingGroup[]; gateRows?: GateRow[] },
1106
1108
  ): boolean {
1107
1109
  if (!state.goal) return false;
1108
1110
  if (status !== "complete" && status !== "aborted") return false;
@@ -1151,7 +1153,7 @@ function archiveCurrentGoal(
1151
1153
  // it carries the same rich markdown as chat over the verbatim
1152
1154
  // six-label machine record. Built from the terminal goal AFTER the
1153
1155
  // summary fence resolves, so chat and archive cannot disagree.
1154
- const richSection = buildRichArchiveSection(terminalGoal, status, archivePath, opts?.findingGroups);
1156
+ const richSection = buildRichArchiveSection(terminalGoal, status, archivePath, opts?.findingGroups, opts?.gateRows);
1155
1157
  const richMd = `${md}\n## Terminal summary\n\n${richSection.join("\n")}\n`;
1156
1158
  // An existing same-id archive is an immutable fence. Check it before
1157
1159
  // publishing an intent so an unrelated/sentinel winner can never make the
@@ -134,6 +134,7 @@ isGoalRevisionCurrent,
134
134
  buildDurableChoiceRecord,
135
135
  normalizeDurableDeferRecommendationInput,
136
136
  sanitizeFindingGroups,
137
+ sanitizeGateRows,
137
138
  } from "../goal-loop-core.js";
138
139
  import {
139
140
  applyValidatedBatch,
@@ -540,6 +541,7 @@ function registerAgentTools(pi: any): void {
540
541
  findingGroups: Type.Optional(Type.Array(Type.Object({
541
542
  title: Type.String({ maxLength: 120, description: "Work-area name (e.g. a subsystem, screen, or phase)" }),
542
543
  findings: Type.Array(Type.String({ maxLength: 500 }), { maxItems: 6, description: "Findings in this area as `Lead: body with path:line evidence` (max 6 per area)" }),
544
+ tests: Type.Optional(Type.Array(Type.String({ maxLength: 500 }), { maxItems: 6, description: "v0.38.52: optional per-finding test-result lines, aligned by index with findings (tests[i] proves findings[i]); each renders as a `Test Results:` sub-line" })),
543
545
  }), {
544
546
  maxItems: 6,
545
547
  description:
@@ -549,6 +551,18 @@ function registerAgentTools(pi: any): void {
549
551
  "Presentation only — the six-label completionSummary stays the audited substance, and file:line tokens are never invented. " +
550
552
  "Omit for single-area work — the flat six-label render stays the fallback.",
551
553
  })),
554
+ gateRows: Type.Optional(Type.Array(Type.Object({
555
+ gate: Type.String({ maxLength: 120, description: "Gate name (e.g. Unit Tests, Typecheck, E2E, Production Build)" }),
556
+ scope: Type.Optional(Type.String({ maxLength: 200, description: "What the gate covered (e.g. suites, tiers, invariants)" })),
557
+ notes: Type.Optional(Type.String({ maxLength: 400, description: "Outcome notes with counts (e.g. 743 passed, 0 failed). Status is DERIVED from these notes — PASS only when they say pass with zero failures, never claimed." })),
558
+ }), {
559
+ maxItems: 10,
560
+ description:
561
+ "v0.38.52: optional verification gate inventory for the terminal summary. " +
562
+ "Widens the Verification table to Quality Gate | Scope | Status | Notes and supersedes the mechanical Tests rows. " +
563
+ "Presentation only — the six-label completionSummary stays the audited substance, and counts are never invented. " +
564
+ "Omit when there is no gate inventory — the mechanical 3-col table stays the fallback.",
565
+ })),
552
566
  }),
553
567
  async execute(_id, params, signal, _onUpdate, execCtx) {
554
568
  const foreign0 = foreignToolGuard(execCtx);
@@ -572,7 +586,7 @@ function registerAgentTools(pi: any): void {
572
586
  }
573
587
  return { content: [{ type: "text", text: `No active goal — it is ${state.goal.status}.` }], details: {} };
574
588
  }
575
- const p = params as { completionSummary?: string; verificationSummary?: string; newObjective?: string; leftOut?: string; findingGroups?: unknown };
589
+ const p = params as { completionSummary?: string; verificationSummary?: string; newObjective?: string; leftOut?: string; findingGroups?: unknown; gateRows?: unknown };
576
590
  if (state.goal.repairTarget) {
577
591
  return {
578
592
  content: [{ type: "text", text: `This repair card cannot be completed yet. Redraft the original target as a confirmed task list with propose_task_list (include objective: ${state.goal.repairTarget.objective.slice(0, 180)}), then continue the real work.` }],
@@ -787,7 +801,9 @@ function registerAgentTools(pi: any): void {
787
801
  }
788
802
  // v0.38.50: agent-structured finding groups ride the same claim —
789
803
  // sanitized once at the boundary, rendered only after approval.
804
+ // v0.38.52: same for the agent-supplied gate inventory.
790
805
  const sanitizedGroups = sanitizeFindingGroups(p.findingGroups);
806
+ const sanitizedGates = sanitizeGateRows(p.gateRows);
791
807
  const completionClaim = beginCompletionAudit(ctx, {
792
808
  completionSummary: finalSummary,
793
809
  verificationSummary: p.verificationSummary,
@@ -795,6 +811,7 @@ function registerAgentTools(pi: any): void {
795
811
  // the terminal render — the only source the summary may cite.
796
812
  ...(p.leftOut?.trim() ? { leftOut: p.leftOut.trim().slice(0, 500) } : {}),
797
813
  ...(sanitizedGroups ? { findingGroups: sanitizedGroups } : {}),
814
+ ...(sanitizedGates ? { gateRows: sanitizedGates } : {}),
798
815
  at: nowIso(),
799
816
  }, "complete-goal");
800
817
  if (!completionClaim) {
@@ -1252,6 +1269,7 @@ function registerAgentTools(pi: any): void {
1252
1269
  // v0.38.50: same for agent-structured finding groups.
1253
1270
  const escLeftOut = state.goal.pendingCompletion?.leftOut;
1254
1271
  const escFindingGroups = state.goal.pendingCompletion?.findingGroups;
1272
+ const escGateRows = state.goal.pendingCompletion?.gateRows;
1255
1273
  updateGoal({ status: "active", auditHistory: history, pendingCompletion: undefined, pauseReason: "audit aborted by user (Esc)" }, ctx);
1256
1274
  const abortConfirmCtx = freshCtxForGeneration(auditGeneration);
1257
1275
  if (!abortConfirmCtx) return staleToolResult();
@@ -1294,8 +1312,9 @@ function registerAgentTools(pi: any): void {
1294
1312
  // captured from it above.
1295
1313
  ...(escLeftOut ? { leftOut: escLeftOut } : {}),
1296
1314
  ...(escFindingGroups ? { findingGroups: escFindingGroups } : {}),
1315
+ ...(escGateRows ? { gateRows: escGateRows } : {}),
1297
1316
  });
1298
- if (!archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: escFindingGroups })) {
1317
+ if (!archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: escFindingGroups, gateRows: escGateRows })) {
1299
1318
  return {
1300
1319
  content: [{ type: "text", text: "The audit was aborted, but the terminal archive could not be persisted. The goal remains active; fix persistence and retry." }],
1301
1320
  details: {},
@@ -1351,14 +1370,16 @@ function registerAgentTools(pi: any): void {
1351
1370
  // v0.38.37: the deliberate non-do rides the durable claim.
1352
1371
  ...(durableCompletionClaim.leftOut ? { leftOut: durableCompletionClaim.leftOut } : {}),
1353
1372
  // v0.38.50: agent-structured finding groups ride the same claim.
1373
+ // v0.38.52: same for the gate inventory.
1354
1374
  ...(durableCompletionClaim.findingGroups ? { findingGroups: durableCompletionClaim.findingGroups } : {}),
1375
+ ...(durableCompletionClaim.gateRows ? { gateRows: durableCompletionClaim.gateRows } : {}),
1355
1376
  extras: inspectionSessionPath
1356
1377
  ? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
1357
1378
  : [],
1358
1379
  });
1359
1380
  const manualObjective = state.goal.objective;
1360
1381
  const manualGoalId = state.goal.id;
1361
- const archived = archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: durableCompletionClaim.findingGroups });
1382
+ const archived = archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: durableCompletionClaim.findingGroups, gateRows: durableCompletionClaim.gateRows });
1362
1383
  if (!archived) {
1363
1384
  // The archive helper preserves the live objective and emits the
1364
1385
  // persistence warning. Stop here: an approved verdict is not a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.51",
3
+ "version": "0.38.52",
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",
@@ -205,4 +205,4 @@ The orchestrator's backstop is the stall watchdog: three consecutive turns with
205
205
 
206
206
  `complete_goal` submits a nonterminal claim, not a completion verdict. While the detached audit is pending, do not say the goal is done/complete/approved or present a final success summary. Do not wait or poll. If a response is needed, give one brief pending-status sentence. GLLA posts the concrete outcome-first summary into chat after verified approval and durable archive, without needing another user prompt. Do not duplicate that summary. A rejected claim remains unfinished and repair work continues.
207
207
 
208
- The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Keep each label value to one or two short clauses so it survives the renderer's clause-boundary clipping (rich sections budget ~200 chars per value); cite human-readable proof (counts, versions, repo-relative paths, PR numbers) and never machine paths (/var/tmp logs, tarballs — those live in the archive only). Cut values at clause boundaries, never mid-word; write Next: as one concrete follow-up action or none (audit-self-referential lines like "verdict decides" are stripped at render); pass what was deliberately left out as the complete_goal leftOut parameter — it renders as the closing bullet, and is omitted when absent. When the work spans distinct areas (subsystems, screens, phases), group the findings with the complete_goal findingGroups parameter — one group per area, each finding as `Lead: body` with repo-relative `path:line` evidence tokens (never invented paths); 4+ groups render as an Area | Finding | Evidence table, fewer as nested area sections. Single-area work omits findingGroups — the flat six-label render stays the fallback.
208
+ The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Keep each label value to one or two short clauses so it survives the renderer's clause-boundary clipping (rich sections budget ~200 chars per value); cite human-readable proof (counts, versions, repo-relative paths, PR numbers) and never machine paths (/var/tmp logs, tarballs — those live in the archive only). Cut values at clause boundaries, never mid-word; write Next: as one concrete follow-up action or none (audit-self-referential lines like "verdict decides" are stripped at render); pass what was deliberately left out as the complete_goal leftOut parameter — it renders as the closing bullet, and is omitted when absent. When the work spans distinct areas (subsystems, screens, phases), group the findings with the complete_goal findingGroups parameter — one group per area, each finding as `Lead: body` with repo-relative `path:line` evidence tokens (never invented paths); 4+ groups render as an Area | Finding | Evidence table, fewer as nested area sections. Add per-finding test proof with the parallel `tests` array (tests[i] proves findings[i]) — each renders as a `Test Results:` sub-line. When the work ran named quality gates (unit, typecheck, E2E, build, invariants), pass the complete_goal gateRows inventory — one row per gate with scope and outcome notes; the Verification table widens to Quality Gate | Scope | Status | Notes with mechanically derived statuses (PASS only when the notes say pass with zero failures) and supersedes the mechanical Tests rows. Single-area work omits findingGroups — the flat six-label render stays the fallback; work with no gate inventory omits gateRows — the mechanical 3-col table stays the fallback.