pi-goal-list-loop-audit 0.38.49 → 0.38.51
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 +43 -0
- package/docs/DESIGN-long-running-supervision.md +2 -1
- package/docs/INDEX.md +1 -1
- package/extensions/completion-summary.ts +140 -18
- package/extensions/goal-commands.ts +11 -1
- package/extensions/goal-loop-core.ts +57 -0
- package/extensions/loops/goal-activation.ts +1 -1
- package/extensions/loops/goal-auditor-hooks.ts +3 -1
- package/extensions/loops/goal-orchestrator.ts +5 -1
- package/extensions/loops/goal-tools.ts +26 -4
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.51 — Bare /list drafts from context like bare /goal (2026-09-12)
|
|
4
|
+
|
|
5
|
+
User note: empty `/list` could not start anything while empty `/goal`
|
|
6
|
+
triggers drafting from context. Bare `/list` (no args) now enters LIST
|
|
7
|
+
drafting with no seed — the same context-draft as bare `/goal` — instead of
|
|
8
|
+
rendering the queue. `/list show` remains the viewer; the stale-handle
|
|
9
|
+
guard mirrors the empty-goal path so a doomed handle latches no drafting
|
|
10
|
+
gate. The `propose_goal_draft` idle hint and the `/list` command
|
|
11
|
+
description name the new entry point.
|
|
12
|
+
|
|
13
|
+
### Preserved
|
|
14
|
+
|
|
15
|
+
- `/list show`, `/list start`, `/list add`, and the natural-language dump
|
|
16
|
+
routing are unchanged; read-only verbs stay available on stale handles.
|
|
17
|
+
|
|
18
|
+
## 0.38.50 — Gemini-gap terminal upgrade: grouped findings, request-echo headline, duration line (2026-09-12)
|
|
19
|
+
|
|
20
|
+
Surveyed four Gemini report screenshots (audit/GEMINI-SUMMARY-GAP-2026-09-11.md)
|
|
21
|
+
and closed the gap in the terminal render only (chat + transcript + archive
|
|
22
|
+
human layer; progress cards, status, widget, external notifies unchanged).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Agent-structured finding groups:** optional `complete_goal findingGroups`
|
|
27
|
+
(one group per work area, findings as `Lead: body` with repo-relative
|
|
28
|
+
`path:line` evidence tokens, sanitized at the claim boundary). Fewer than
|
|
29
|
+
four groups render as nested `#### n. Area` subsections; four or more
|
|
30
|
+
render as an Area | Finding | Evidence table. Evidence extraction is
|
|
31
|
+
mechanical — absolute/home-dir/machine paths never become evidence.
|
|
32
|
+
Single-area work omits the parameter; the flat six-label render stays
|
|
33
|
+
the fallback.
|
|
34
|
+
- **Request-echo headline:** `## Done: <objective> — <outcome>` (and
|
|
35
|
+
`## Aborted: …` for aborted records) so the title mirrors the ask.
|
|
36
|
+
- **Duration line:** `— N turns · … elapsed · M audits` from durable state
|
|
37
|
+
directly under the headline; unknown facts stay absent.
|
|
38
|
+
- **Raised budgets:** findings 8 → 12, values 200 → 400 chars, Next 4 → 6.
|
|
39
|
+
|
|
40
|
+
### Preserved
|
|
41
|
+
|
|
42
|
+
- Auditor-verified-before-posting, the verbatim six-label machine record,
|
|
43
|
+
and the honesty rules (PASS only on `pass`, `NO VERDICT` never renders,
|
|
44
|
+
pipe escaping) are unchanged.
|
|
45
|
+
|
|
3
46
|
## 0.38.49 — audit-pass drift fixes: prompt gate, summary segmentation, task honesty (2026-09-11)
|
|
4
47
|
|
|
5
48
|
Full-project audit pass on v0.38.48 (three parallel scout surveys). Six new
|
|
@@ -113,7 +113,8 @@ available. It never infers a passing test or invents a commit.
|
|
|
113
113
|
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
|
-
(`## Done
|
|
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
118
|
to fixed budgets). Width-bound and external surfaces (status line, widget card,
|
|
118
119
|
external notifies) use the compact single-line projection of all six labels.
|
|
119
120
|
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.51 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,5 +1,5 @@
|
|
|
1
|
-
import type { Goal, Status } from "./goal-loop-core.js";
|
|
2
|
-
import { truncateCells } from "./goal-loop-display.js";
|
|
1
|
+
import type { FindingGroup, Goal, Status } from "./goal-loop-core.js";
|
|
2
|
+
import { fmtElapsed, truncateCells } from "./goal-loop-display.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The durable, user-facing terminal recap contract. Keep this as a small
|
|
@@ -269,18 +269,27 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
/** Rich terminal voice (field 20260911_003839/003903/003907 — the
|
|
272
|
-
* Antigravity close
|
|
273
|
-
*
|
|
274
|
-
*
|
|
272
|
+
* Antigravity close; v0.38.50 Gemini-gap survey audit/GEMINI-SUMMARY-GAP-2026-09-11.md:
|
|
273
|
+
* grouped area sections with nested findings + file:line evidence, a
|
|
274
|
+
* request-echo headline, a duration line, and an automatic table once
|
|
275
|
+
* findings span 4+ groups). Scope is the terminal render only (chat +
|
|
275
276
|
* transcript + archive human layer); progress cards, status line, and
|
|
276
277
|
* external notifies keep their compact projections. Verbose by owner
|
|
277
|
-
* choice: findings cap
|
|
278
|
-
export const RICH_FINDINGS_CAP =
|
|
279
|
-
export const RICH_VALUE_BUDGET =
|
|
280
|
-
export const RICH_NEXT_CAP =
|
|
278
|
+
* choice: findings cap 12, values 400, table cells 400, Next 6. */
|
|
279
|
+
export const RICH_FINDINGS_CAP = 12;
|
|
280
|
+
export const RICH_VALUE_BUDGET = 400;
|
|
281
|
+
export const RICH_NEXT_CAP = 6;
|
|
282
|
+
/** v0.38.50: objective echo clipped to a headline-safe width. */
|
|
283
|
+
export const RICH_OBJECTIVE_ECHO_CHARS = 80;
|
|
284
|
+
/** v0.38.50: findings spanning this many groups render as a table
|
|
285
|
+
* (the 12-row screen-by-screen example); fewer stay nested. */
|
|
286
|
+
export const RICH_TABLE_GROUP_THRESHOLD = 4;
|
|
287
|
+
/** v0.38.50: evidence tokens per finding cell — density, not a dump. */
|
|
288
|
+
export const RICH_EVIDENCE_TOKENS_PER_FINDING = 4;
|
|
281
289
|
|
|
282
290
|
export interface RichTerminalParts {
|
|
283
291
|
headline: string;
|
|
292
|
+
durationLine: string | null;
|
|
284
293
|
findingLines: string[];
|
|
285
294
|
tableLines: string[];
|
|
286
295
|
nextLines: string[];
|
|
@@ -314,6 +323,63 @@ function leadBody(detail: string): { lead: string; body: string } {
|
|
|
314
323
|
return { lead: detail.slice(0, separator).trim() || "Note", body: detail.slice(separator + 1).trim() };
|
|
315
324
|
}
|
|
316
325
|
|
|
326
|
+
/**
|
|
327
|
+
* v0.38.50: repo-relative `path:line` evidence tokens (soundManager.ts:333,
|
|
328
|
+
* sim.ts:4505-4530). Absolute paths, home-dir paths, and machine temp
|
|
329
|
+
* paths are NOT evidence — those stay in the archive only. Extraction is
|
|
330
|
+
* mechanical substring movement, never inference.
|
|
331
|
+
*/
|
|
332
|
+
const EVIDENCE_TOKEN_PATTERN = /(?<![/~+\w])[\w.+][\w./-]*\.[A-Za-z0-9]{1,8}:\d+(?:-\d+)?/g;
|
|
333
|
+
|
|
334
|
+
export function extractEvidenceTokens(text: string): { text: string; evidence: string[] } {
|
|
335
|
+
const evidence: string[] = [];
|
|
336
|
+
const stripped = text
|
|
337
|
+
.replace(EVIDENCE_TOKEN_PATTERN, (match) => {
|
|
338
|
+
if (evidence.length < RICH_EVIDENCE_TOKENS_PER_FINDING && !evidence.includes(match)) evidence.push(match);
|
|
339
|
+
return "";
|
|
340
|
+
})
|
|
341
|
+
.replace(/\s+/g, " ")
|
|
342
|
+
.replace(/\s+([,.;:])/g, "$1")
|
|
343
|
+
.trim();
|
|
344
|
+
return { text: stripped, evidence };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* v0.38.50: enforce the total-findings budget across groups in order —
|
|
349
|
+
* groups that go empty are dropped so the table/nesting never shows a
|
|
350
|
+
* bare area header.
|
|
351
|
+
*/
|
|
352
|
+
export function takeBudgetedGroups(groups: FindingGroup[]): FindingGroup[] {
|
|
353
|
+
const out: FindingGroup[] = [];
|
|
354
|
+
let remaining = RICH_FINDINGS_CAP;
|
|
355
|
+
for (const group of groups) {
|
|
356
|
+
if (remaining <= 0) break;
|
|
357
|
+
const findings = group.findings.slice(0, remaining);
|
|
358
|
+
if (findings.length === 0) continue;
|
|
359
|
+
out.push({ title: group.title, findings });
|
|
360
|
+
remaining -= findings.length;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* v0.38.50: one compact duration line from durable goal state — turns,
|
|
367
|
+
* wall-clock elapsed since creation, and audit count. Only known facts
|
|
368
|
+
* render (absent stays absent); null when nothing is known.
|
|
369
|
+
*/
|
|
370
|
+
export function buildDurationLine(goal: Goal, now = Date.now()): string | null {
|
|
371
|
+
const segs: string[] = [];
|
|
372
|
+
const turns = goal.telemetry?.turns;
|
|
373
|
+
if (typeof turns === "number" && Number.isFinite(turns) && turns >= 0) {
|
|
374
|
+
segs.push(`${turns} turn${turns === 1 ? "" : "s"}`);
|
|
375
|
+
}
|
|
376
|
+
const started = Date.parse(goal.createdAt ?? "");
|
|
377
|
+
if (Number.isFinite(started)) segs.push(`${fmtElapsed(now - started)} elapsed`);
|
|
378
|
+
const audits = Array.isArray(goal.auditHistory) ? goal.auditHistory.length : 0;
|
|
379
|
+
if (audits > 0) segs.push(`${audits} audit${audits === 1 ? "" : "s"}`);
|
|
380
|
+
return segs.length > 0 ? `\u2014 ${segs.join(" \u00b7 ")}` : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
317
383
|
/** Partition informing details into findings / Tests / next buckets. */
|
|
318
384
|
export function partitionRichDetails(details: string[]): { findings: string[]; tests: string[]; next: string[] } {
|
|
319
385
|
const findings: string[] = [];
|
|
@@ -327,18 +393,60 @@ export function partitionRichDetails(details: string[]): { findings: string[]; t
|
|
|
327
393
|
return { findings, tests, next };
|
|
328
394
|
}
|
|
329
395
|
|
|
330
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* v0.38.50: request-echo headline — the title mirrors the ask (the
|
|
398
|
+
* Gemini shape) with the outcome second. Without an objective the legacy
|
|
399
|
+
* `## Done — outcome` shape stays, so direct unit callers are unaffected.
|
|
400
|
+
*/
|
|
401
|
+
export function requestEchoHeadline(kind: "Done" | "Aborted", objective: string | undefined, outcome: string): string {
|
|
402
|
+
const echo = objective?.trim() ? clipSummaryValue(objective.trim(), RICH_OBJECTIVE_ECHO_CHARS) : null;
|
|
403
|
+
return echo ? `## ${kind}: ${echo} \u2014 ${outcome}` : `## ${kind} \u2014 ${outcome}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Build the section parts shared by chat, transcript, and archive.
|
|
407
|
+
* v0.38.50: agent-structured groups render as `#### n. Area` subsections
|
|
408
|
+
* with nested evidence bullets; at RICH_TABLE_GROUP_THRESHOLD groups the
|
|
409
|
+
* same facts render as an Area | Finding | Evidence table instead.
|
|
410
|
+
* Without groups the flat six-label projection stays the fallback. */
|
|
331
411
|
export function buildRichTerminalParts(args: {
|
|
332
412
|
outcome: string;
|
|
333
413
|
details: string[];
|
|
334
414
|
countsLine: string;
|
|
335
415
|
auditHistory?: Goal["auditHistory"];
|
|
416
|
+
objective?: string;
|
|
417
|
+
durationLine?: string | null;
|
|
418
|
+
groups?: FindingGroup[];
|
|
336
419
|
}): RichTerminalParts {
|
|
337
420
|
const { findings, tests, next } = partitionRichDetails(args.details);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
421
|
+
const headline = requestEchoHeadline("Done", args.objective, args.outcome);
|
|
422
|
+
const budgeted = takeBudgetedGroups(args.groups ?? []);
|
|
423
|
+
const useTable = budgeted.length >= RICH_TABLE_GROUP_THRESHOLD;
|
|
424
|
+
const findingLines: string[] = [];
|
|
425
|
+
if (useTable) {
|
|
426
|
+
findingLines.push("| Area | Finding | Evidence |", "| --- | --- | --- |");
|
|
427
|
+
for (const group of budgeted) {
|
|
428
|
+
for (const finding of group.findings) {
|
|
429
|
+
const { text, evidence } = extractEvidenceTokens(finding);
|
|
430
|
+
const { lead, body } = leadBody(text);
|
|
431
|
+
findingLines.push(
|
|
432
|
+
`| ${escapeTableCell(clipSummaryValue(group.title, RICH_OBJECTIVE_ECHO_CHARS))} | ${escapeTableCell(`**${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`)} | ${escapeTableCell(evidence.join(", ") || "\u2014")} |`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else if (budgeted.length > 0) {
|
|
437
|
+
budgeted.forEach((group, i) => {
|
|
438
|
+
findingLines.push(`#### ${i + 1}. ${clipSummaryValue(group.title, RICH_OBJECTIVE_ECHO_CHARS)}`);
|
|
439
|
+
for (const finding of group.findings) {
|
|
440
|
+
const { lead, body } = leadBody(finding);
|
|
441
|
+
findingLines.push(`- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
findings.slice(0, RICH_FINDINGS_CAP).forEach((detail, i) => {
|
|
446
|
+
const { lead, body } = leadBody(detail);
|
|
447
|
+
findingLines.push(`${i + 1}. **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
342
450
|
const tableRows: string[] = [];
|
|
343
451
|
for (const detail of tests.slice(0, 2)) {
|
|
344
452
|
const { body } = leadBody(detail);
|
|
@@ -357,16 +465,19 @@ export function buildRichTerminalParts(args: {
|
|
|
357
465
|
return `- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
|
|
358
466
|
});
|
|
359
467
|
return {
|
|
360
|
-
headline
|
|
468
|
+
headline,
|
|
469
|
+
durationLine: args.durationLine ?? null,
|
|
361
470
|
findingLines,
|
|
362
471
|
tableLines,
|
|
363
472
|
nextLines,
|
|
364
473
|
};
|
|
365
474
|
}
|
|
366
475
|
|
|
367
|
-
/** Compose parts + headline + section headers into markdown lines.
|
|
476
|
+
/** Compose parts + headline + section headers into markdown lines.
|
|
477
|
+
* v0.38.50: the duration line rides directly under the headline. */
|
|
368
478
|
export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { headline?: string }): string[] {
|
|
369
479
|
const lines = [opts?.headline ?? parts.headline, ""];
|
|
480
|
+
if (parts.durationLine) lines.push(parts.durationLine, "");
|
|
370
481
|
if (parts.findingLines.length > 0) {
|
|
371
482
|
lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
|
|
372
483
|
}
|
|
@@ -384,7 +495,7 @@ export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { head
|
|
|
384
495
|
* raw six-label recap stays verbatim in `## Completion summary` as the
|
|
385
496
|
* machine layer. Headline follows terminal status; aborted records never
|
|
386
497
|
* wear a `Done` headline. */
|
|
387
|
-
export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string): string[] {
|
|
498
|
+
export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string, findingGroups?: FindingGroup[]): string[] {
|
|
388
499
|
const facts: CompletionSummaryFacts = { goal, status, archivePath };
|
|
389
500
|
const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
|
|
390
501
|
const brief = humanCompletionBrief(summary, 140, RICH_VALUE_BUDGET);
|
|
@@ -399,10 +510,13 @@ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath:
|
|
|
399
510
|
details: withoutStaleNext(brief.details),
|
|
400
511
|
countsLine,
|
|
401
512
|
auditHistory: history,
|
|
513
|
+
objective: goal.objective,
|
|
514
|
+
durationLine: buildDurationLine(goal),
|
|
515
|
+
groups: findingGroups,
|
|
402
516
|
});
|
|
403
517
|
return [
|
|
404
518
|
...composeRichTerminalLines(parts, {
|
|
405
|
-
headline: status === "complete" ? parts.headline :
|
|
519
|
+
headline: status === "complete" ? parts.headline : requestEchoHeadline("Aborted", goal.objective, brief.outcome),
|
|
406
520
|
}),
|
|
407
521
|
trailerBullet(stripApprovalModel(approval)),
|
|
408
522
|
trailerBullet(countsLine),
|
|
@@ -492,6 +606,11 @@ export interface TerminalApprovalRenderInput {
|
|
|
492
606
|
auditNote?: string;
|
|
493
607
|
/** v0.38.37: what the agent deliberately left out (complete_goal leftOut). */
|
|
494
608
|
leftOut?: string;
|
|
609
|
+
/**
|
|
610
|
+
* v0.38.50: agent-structured finding groups (complete_goal findingGroups,
|
|
611
|
+
* sanitized at claim time). Absent keeps the flat six-label fallback.
|
|
612
|
+
*/
|
|
613
|
+
findingGroups?: FindingGroup[];
|
|
495
614
|
}
|
|
496
615
|
|
|
497
616
|
export interface TerminalApprovalRender {
|
|
@@ -567,6 +686,9 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
567
686
|
details: withoutStaleNext(richDetails),
|
|
568
687
|
countsLine,
|
|
569
688
|
auditHistory: input.goal.auditHistory,
|
|
689
|
+
objective: input.goal.objective,
|
|
690
|
+
durationLine: buildDurationLine(input.goal),
|
|
691
|
+
groups: input.findingGroups,
|
|
570
692
|
});
|
|
571
693
|
const chatBody = composeRichTerminalLines(richParts);
|
|
572
694
|
const transcriptBody = composeRichTerminalLines(richParts);
|
|
@@ -1302,7 +1302,17 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1302
1302
|
return;
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
|
-
|
|
1305
|
+
// v0.38.51 (user note 2026-09-12): bare /list drafts from context like
|
|
1306
|
+
// bare /goal does — /list show remains the viewer. The stale guard
|
|
1307
|
+
// mirrors cmdSet's empty-goal path: a draft seed on a doomed handle
|
|
1308
|
+
// would latch the drafting gate with no live turn to deliver it.
|
|
1309
|
+
if (!sub) {
|
|
1310
|
+
if (staleEntry) return;
|
|
1311
|
+
await startDrafting(ctx, "list");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (sub === "show") {
|
|
1306
1316
|
const memQueue = listQueue();
|
|
1307
1317
|
// v0.34.60: stale-handle fallback. If in-memory is empty but disk has
|
|
1308
1318
|
// queue sidecar files (a fresh pi session that hasn't yet reparsed
|
|
@@ -272,6 +272,52 @@ export type AuditorRecoveryFailureClass = "transport" | "timeout" | "no-verdict"
|
|
|
272
272
|
* completion assertion; the lifecycle fields make an interrupted isolated
|
|
273
273
|
* audit distinguishable from one that is actively running. Fields beyond
|
|
274
274
|
* `at` are optional so v0.34.20 and older claims remain recoverable. */
|
|
275
|
+
/**
|
|
276
|
+
* v0.38.50 (Gemini-gap survey): one agent-structured work area for the
|
|
277
|
+
* grouped terminal render. Presentation only — the six-label
|
|
278
|
+
* completionSummary stays the audited substance. Titles and findings are
|
|
279
|
+
* agent-authored and clipped at render; evidence tokens (`path:line`)
|
|
280
|
+
* are extracted mechanically, never inferred.
|
|
281
|
+
*/
|
|
282
|
+
export interface FindingGroup {
|
|
283
|
+
title: string;
|
|
284
|
+
findings: string[];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const MAX_FINDING_GROUPS = 6;
|
|
288
|
+
export const MAX_GROUP_FINDINGS = 6;
|
|
289
|
+
export const MAX_GROUP_TITLE_CHARS = 120;
|
|
290
|
+
export const MAX_GROUP_FINDING_CHARS = 500;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Bound agent-supplied finding groups at the trust boundary. Drops
|
|
294
|
+
* non-object groups, blank titles, and empty findings; clips the rest.
|
|
295
|
+
* Returns undefined when nothing valid survives — the flat six-label
|
|
296
|
+
* render stays the fallback, never a half-structured projection.
|
|
297
|
+
*/
|
|
298
|
+
export function sanitizeFindingGroups(value: unknown): FindingGroup[] | undefined {
|
|
299
|
+
if (!Array.isArray(value)) return undefined;
|
|
300
|
+
const groups: FindingGroup[] = [];
|
|
301
|
+
for (const entry of value) {
|
|
302
|
+
if (groups.length >= MAX_FINDING_GROUPS) break;
|
|
303
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
304
|
+
const raw = entry as Record<string, unknown>;
|
|
305
|
+
const title = typeof raw.title === "string" ? raw.title.trim().slice(0, MAX_GROUP_TITLE_CHARS) : "";
|
|
306
|
+
if (!title) continue;
|
|
307
|
+
if (!Array.isArray(raw.findings)) continue;
|
|
308
|
+
const findings: string[] = [];
|
|
309
|
+
for (const finding of raw.findings) {
|
|
310
|
+
if (findings.length >= MAX_GROUP_FINDINGS) break;
|
|
311
|
+
if (typeof finding !== "string") continue;
|
|
312
|
+
const text = finding.trim().slice(0, MAX_GROUP_FINDING_CHARS);
|
|
313
|
+
if (text) findings.push(text);
|
|
314
|
+
}
|
|
315
|
+
if (findings.length === 0) continue;
|
|
316
|
+
groups.push({ title, findings });
|
|
317
|
+
}
|
|
318
|
+
return groups.length > 0 ? groups : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
275
321
|
export interface PendingCompletion {
|
|
276
322
|
completionSummary?: string;
|
|
277
323
|
verificationSummary?: string;
|
|
@@ -300,6 +346,12 @@ export interface PendingCompletion {
|
|
|
300
346
|
* terminal user summary; absent means nothing was deliberately left out.
|
|
301
347
|
*/
|
|
302
348
|
leftOut?: string;
|
|
349
|
+
/**
|
|
350
|
+
* v0.38.50: agent-structured finding groups for the grouped terminal
|
|
351
|
+
* render (complete_goal findingGroups, sanitized at claim time).
|
|
352
|
+
* Absent means the flat six-label render stays the fallback.
|
|
353
|
+
*/
|
|
354
|
+
findingGroups?: FindingGroup[];
|
|
303
355
|
/**
|
|
304
356
|
* Durable one-shot recovery fence. A parked claim may receive one
|
|
305
357
|
* automatic retry after a validated healthy lifecycle/recovery event;
|
|
@@ -2157,6 +2209,10 @@ function normalizePendingCompletion(value: unknown): PendingCompletion {
|
|
|
2157
2209
|
_timeoutEscalation <= 100
|
|
2158
2210
|
? _timeoutEscalation
|
|
2159
2211
|
: undefined;
|
|
2212
|
+
// v0.38.50: agent-structured finding groups survive reloads sanitized;
|
|
2213
|
+
// garbage at the boundary degrades to absent (flat fallback), never
|
|
2214
|
+
// to a half-structured projection.
|
|
2215
|
+
const sanitizedGroups = sanitizeFindingGroups(raw.findingGroups);
|
|
2160
2216
|
return {
|
|
2161
2217
|
...canonicalOrUnknown,
|
|
2162
2218
|
...(auditorCandidateRefs !== undefined ? { auditorCandidateRefs } : {}),
|
|
@@ -2169,6 +2225,7 @@ function normalizePendingCompletion(value: unknown): PendingCompletion {
|
|
|
2169
2225
|
...(auditorFallbackExhausted ? { auditorFallbackExhausted: true } : {}),
|
|
2170
2226
|
...(auditorFailureAt ? { auditorFailureAt } : {}),
|
|
2171
2227
|
...(timeoutEscalation !== undefined ? { timeoutEscalation } : {}),
|
|
2228
|
+
...(sanitizedGroups ? { findingGroups: sanitizedGroups } : {}),
|
|
2172
2229
|
...(phase === "running" || phase === "recovery-pending" || phase === "retry-waiting" ? { phase } : {}),
|
|
2173
2230
|
...(typeof raw.retryAttempts === "number"
|
|
2174
2231
|
? { retryAttempts: raw.retryAttempts }
|
|
@@ -846,7 +846,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
846
846
|
},
|
|
847
847
|
});
|
|
848
848
|
pi.registerCommand("list", {
|
|
849
|
-
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list audit [focus] (collect findings, then drain them as items) | /list show | /list start | /list resume | /list tweak <text> | /list next [n] | /list remove <n> | /list clear | /list cancel. /list start explicitly activates the queued head or drafts one clear recent objective with the normal Confirm gate. Settings are under /glla, not /list — bare /glla opens the settings table.",
|
|
849
|
+
description: "Loop 2: the list of audited goals — order is the default, not the law. Bare /list drafts from context like bare /goal does | /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list audit [focus] (collect findings, then drain them as items) | /list show | /list start | /list resume | /list tweak <text> | /list next [n] | /list remove <n> | /list clear | /list cancel. /list start explicitly activates the queued head or drafts one clear recent objective with the normal Confirm gate. Settings are under /glla, not /list — bare /glla opens the settings table.",
|
|
850
850
|
getArgumentCompletions: completions([
|
|
851
851
|
["show", "display the waiting items"],
|
|
852
852
|
["start", "explicitly activate the queued head; with no queue, draft one clear recent objective"],
|
|
@@ -1441,12 +1441,14 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
1441
1441
|
record: approvalRecord,
|
|
1442
1442
|
// v0.38.37: the deliberate non-do the agent claimed, if any.
|
|
1443
1443
|
...(claim.leftOut ? { leftOut: claim.leftOut } : {}),
|
|
1444
|
+
// v0.38.50: agent-structured finding groups ride the audited claim.
|
|
1445
|
+
...(claim.findingGroups ? { findingGroups: claim.findingGroups } : {}),
|
|
1444
1446
|
extras: inspectionSessionPath
|
|
1445
1447
|
? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
|
|
1446
1448
|
: [],
|
|
1447
1449
|
});
|
|
1448
1450
|
const approvalObjective = state.goal.objective;
|
|
1449
|
-
const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})
|
|
1451
|
+
const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`, {}, { findingGroups: claim.findingGroups });
|
|
1450
1452
|
if (!archived) {
|
|
1451
1453
|
// archiveCurrentGoal already preserved the live record and warned the
|
|
1452
1454
|
// user. Keep the approved claim recoverable, but never emit a terminal
|
|
@@ -29,6 +29,7 @@ import { Type } from "typebox";
|
|
|
29
29
|
import { state, replaceState, persistStateLine } from "../goal-state.js";
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
|
+
type FindingGroup,
|
|
32
33
|
type Goal,
|
|
33
34
|
type Policy,
|
|
34
35
|
type State,
|
|
@@ -1099,6 +1100,9 @@ function archiveCurrentGoal(
|
|
|
1099
1100
|
status: Status,
|
|
1100
1101
|
stopReason?: string,
|
|
1101
1102
|
patch: Partial<Pick<Goal, "auditHistory" | "completionSummary" | "pendingTasks">> = {},
|
|
1103
|
+
// v0.38.50: agent-structured finding groups ride the audited claim into
|
|
1104
|
+
// the archive rich section only — abort/wip paths omit it (flat fallback).
|
|
1105
|
+
opts?: { findingGroups?: FindingGroup[] },
|
|
1102
1106
|
): boolean {
|
|
1103
1107
|
if (!state.goal) return false;
|
|
1104
1108
|
if (status !== "complete" && status !== "aborted") return false;
|
|
@@ -1147,7 +1151,7 @@ function archiveCurrentGoal(
|
|
|
1147
1151
|
// it carries the same rich markdown as chat over the verbatim
|
|
1148
1152
|
// six-label machine record. Built from the terminal goal AFTER the
|
|
1149
1153
|
// summary fence resolves, so chat and archive cannot disagree.
|
|
1150
|
-
const richSection = buildRichArchiveSection(terminalGoal, status, archivePath);
|
|
1154
|
+
const richSection = buildRichArchiveSection(terminalGoal, status, archivePath, opts?.findingGroups);
|
|
1151
1155
|
const richMd = `${md}\n## Terminal summary\n\n${richSection.join("\n")}\n`;
|
|
1152
1156
|
// An existing same-id archive is an immutable fence. Check it before
|
|
1153
1157
|
// publishing an intent so an unrelated/sentinel winner can never make the
|
|
@@ -133,6 +133,7 @@ isGoalRevisionCurrent,
|
|
|
133
133
|
type DurableChoiceRecord,
|
|
134
134
|
buildDurableChoiceRecord,
|
|
135
135
|
normalizeDurableDeferRecommendationInput,
|
|
136
|
+
sanitizeFindingGroups,
|
|
136
137
|
} from "../goal-loop-core.js";
|
|
137
138
|
import {
|
|
138
139
|
applyValidatedBatch,
|
|
@@ -536,6 +537,18 @@ function registerAgentTools(pi: any): void {
|
|
|
536
537
|
"Renders as the closing `• Left out:` bullet in the user-facing terminal summary. " +
|
|
537
538
|
"Omit when nothing was deliberately left out — absent stays absent, never invented.",
|
|
538
539
|
})),
|
|
540
|
+
findingGroups: Type.Optional(Type.Array(Type.Object({
|
|
541
|
+
title: Type.String({ maxLength: 120, description: "Work-area name (e.g. a subsystem, screen, or phase)" }),
|
|
542
|
+
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)" }),
|
|
543
|
+
}), {
|
|
544
|
+
maxItems: 6,
|
|
545
|
+
description:
|
|
546
|
+
"v0.38.50: optional finding groups for the terminal summary. Group multi-area work by area " +
|
|
547
|
+
"(subsystem, screen, phase); each finding is `Lead: body` with repo-relative `path:line` evidence tokens. " +
|
|
548
|
+
"4+ groups render as an Area | Finding | Evidence table, fewer as nested area sections. " +
|
|
549
|
+
"Presentation only — the six-label completionSummary stays the audited substance, and file:line tokens are never invented. " +
|
|
550
|
+
"Omit for single-area work — the flat six-label render stays the fallback.",
|
|
551
|
+
})),
|
|
539
552
|
}),
|
|
540
553
|
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
541
554
|
const foreign0 = foreignToolGuard(execCtx);
|
|
@@ -559,7 +572,7 @@ function registerAgentTools(pi: any): void {
|
|
|
559
572
|
}
|
|
560
573
|
return { content: [{ type: "text", text: `No active goal — it is ${state.goal.status}.` }], details: {} };
|
|
561
574
|
}
|
|
562
|
-
const p = params as { completionSummary?: string; verificationSummary?: string; newObjective?: string; leftOut?: string };
|
|
575
|
+
const p = params as { completionSummary?: string; verificationSummary?: string; newObjective?: string; leftOut?: string; findingGroups?: unknown };
|
|
563
576
|
if (state.goal.repairTarget) {
|
|
564
577
|
return {
|
|
565
578
|
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.` }],
|
|
@@ -772,12 +785,16 @@ function registerAgentTools(pi: any): void {
|
|
|
772
785
|
if (!guardGoalBeforeContinuation(ctx, "completion-audit-dispatch", state.goal?.id)) {
|
|
773
786
|
return staleToolResult();
|
|
774
787
|
}
|
|
788
|
+
// v0.38.50: agent-structured finding groups ride the same claim —
|
|
789
|
+
// sanitized once at the boundary, rendered only after approval.
|
|
790
|
+
const sanitizedGroups = sanitizeFindingGroups(p.findingGroups);
|
|
775
791
|
const completionClaim = beginCompletionAudit(ctx, {
|
|
776
792
|
completionSummary: finalSummary,
|
|
777
793
|
verificationSummary: p.verificationSummary,
|
|
778
794
|
// v0.38.37: the deliberate non-do rides the pending claim into
|
|
779
795
|
// the terminal render — the only source the summary may cite.
|
|
780
796
|
...(p.leftOut?.trim() ? { leftOut: p.leftOut.trim().slice(0, 500) } : {}),
|
|
797
|
+
...(sanitizedGroups ? { findingGroups: sanitizedGroups } : {}),
|
|
781
798
|
at: nowIso(),
|
|
782
799
|
}, "complete-goal");
|
|
783
800
|
if (!completionClaim) {
|
|
@@ -1232,7 +1249,9 @@ function registerAgentTools(pi: any): void {
|
|
|
1232
1249
|
if (result.error === "Auditor aborted.") {
|
|
1233
1250
|
// v0.38.37: capture the agent-claimed non-do before the claim is
|
|
1234
1251
|
// cleared below — the Esc-complete path renders the same voice.
|
|
1252
|
+
// v0.38.50: same for agent-structured finding groups.
|
|
1235
1253
|
const escLeftOut = state.goal.pendingCompletion?.leftOut;
|
|
1254
|
+
const escFindingGroups = state.goal.pendingCompletion?.findingGroups;
|
|
1236
1255
|
updateGoal({ status: "active", auditHistory: history, pendingCompletion: undefined, pauseReason: "audit aborted by user (Esc)" }, ctx);
|
|
1237
1256
|
const abortConfirmCtx = freshCtxForGeneration(auditGeneration);
|
|
1238
1257
|
if (!abortConfirmCtx) return staleToolResult();
|
|
@@ -1274,8 +1293,9 @@ function registerAgentTools(pi: any): void {
|
|
|
1274
1293
|
// v0.38.37: the claim was cleared pre-confirm; the non-do was
|
|
1275
1294
|
// captured from it above.
|
|
1276
1295
|
...(escLeftOut ? { leftOut: escLeftOut } : {}),
|
|
1296
|
+
...(escFindingGroups ? { findingGroups: escFindingGroups } : {}),
|
|
1277
1297
|
});
|
|
1278
|
-
if (!archiveCurrentGoal(ctx, "complete", terminalReason)) {
|
|
1298
|
+
if (!archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: escFindingGroups })) {
|
|
1279
1299
|
return {
|
|
1280
1300
|
content: [{ type: "text", text: "The audit was aborted, but the terminal archive could not be persisted. The goal remains active; fix persistence and retry." }],
|
|
1281
1301
|
details: {},
|
|
@@ -1330,13 +1350,15 @@ function registerAgentTools(pi: any): void {
|
|
|
1330
1350
|
record: manualArchiveRecord,
|
|
1331
1351
|
// v0.38.37: the deliberate non-do rides the durable claim.
|
|
1332
1352
|
...(durableCompletionClaim.leftOut ? { leftOut: durableCompletionClaim.leftOut } : {}),
|
|
1353
|
+
// v0.38.50: agent-structured finding groups ride the same claim.
|
|
1354
|
+
...(durableCompletionClaim.findingGroups ? { findingGroups: durableCompletionClaim.findingGroups } : {}),
|
|
1333
1355
|
extras: inspectionSessionPath
|
|
1334
1356
|
? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
|
|
1335
1357
|
: [],
|
|
1336
1358
|
});
|
|
1337
1359
|
const manualObjective = state.goal.objective;
|
|
1338
1360
|
const manualGoalId = state.goal.id;
|
|
1339
|
-
const archived = archiveCurrentGoal(ctx, "complete", terminalReason);
|
|
1361
|
+
const archived = archiveCurrentGoal(ctx, "complete", terminalReason, {}, { findingGroups: durableCompletionClaim.findingGroups });
|
|
1340
1362
|
if (!archived) {
|
|
1341
1363
|
// The archive helper preserves the live objective and emits the
|
|
1342
1364
|
// persistence warning. Stop here: an approved verdict is not a
|
|
@@ -2358,7 +2380,7 @@ function registerAgentTools(pi: any): void {
|
|
|
2358
2380
|
if (!liveCtx) return staleToolResult();
|
|
2359
2381
|
if (draftingTarget !== "goal" && draftingTarget !== "list") {
|
|
2360
2382
|
return {
|
|
2361
|
-
content: [{ type: "text", text: "Not in goal drafting mode. The user starts drafting with /goal or /list add (no args), or activates directly with /goal <objective>." }],
|
|
2383
|
+
content: [{ type: "text", text: "Not in goal drafting mode. The user starts drafting with bare /goal, bare /list, or /list add (no args), or activates directly with /goal <objective>." }],
|
|
2362
2384
|
details: {},
|
|
2363
2385
|
};
|
|
2364
2386
|
}
|
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.51",
|
|
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.
|
|
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.
|