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 +33 -0
- package/docs/DESIGN-long-running-supervision.md +4 -1
- package/docs/INDEX.md +1 -1
- package/extensions/completion-summary.ts +55 -15
- package/extensions/goal-loop-core.ts +74 -1
- package/extensions/loops/goal-auditor-hooks.ts +3 -1
- package/extensions/loops/goal-orchestrator.ts +4 -2
- package/extensions/loops/goal-tools.ts +24 -3
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +1 -1
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|