pi-goal-list-loop-audit 0.38.48 → 0.38.50
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 +68 -0
- package/INSTALL.md +2 -2
- package/README.md +7 -5
- package/docs/DESIGN-long-running-supervision.md +10 -4
- package/docs/INDEX.md +1 -1
- package/extensions/approval-render-store.ts +4 -0
- package/extensions/completion-summary.ts +151 -30
- package/extensions/glla-update-check.ts +5 -0
- package/extensions/goal-loop-core.ts +57 -0
- package/extensions/loops/goal-auditor-hooks.ts +3 -1
- package/extensions/loops/goal-orchestrator.ts +5 -1
- package/extensions/loops/goal-tools.ts +38 -8
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.50 — Gemini-gap terminal upgrade: grouped findings, request-echo headline, duration line (2026-09-12)
|
|
4
|
+
|
|
5
|
+
Surveyed four Gemini report screenshots (audit/GEMINI-SUMMARY-GAP-2026-09-11.md)
|
|
6
|
+
and closed the gap in the terminal render only (chat + transcript + archive
|
|
7
|
+
human layer; progress cards, status, widget, external notifies unchanged).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Agent-structured finding groups:** optional `complete_goal findingGroups`
|
|
12
|
+
(one group per work area, findings as `Lead: body` with repo-relative
|
|
13
|
+
`path:line` evidence tokens, sanitized at the claim boundary). Fewer than
|
|
14
|
+
four groups render as nested `#### n. Area` subsections; four or more
|
|
15
|
+
render as an Area | Finding | Evidence table. Evidence extraction is
|
|
16
|
+
mechanical — absolute/home-dir/machine paths never become evidence.
|
|
17
|
+
Single-area work omits the parameter; the flat six-label render stays
|
|
18
|
+
the fallback.
|
|
19
|
+
- **Request-echo headline:** `## Done: <objective> — <outcome>` (and
|
|
20
|
+
`## Aborted: …` for aborted records) so the title mirrors the ask.
|
|
21
|
+
- **Duration line:** `— N turns · … elapsed · M audits` from durable state
|
|
22
|
+
directly under the headline; unknown facts stay absent.
|
|
23
|
+
- **Raised budgets:** findings 8 → 12, values 200 → 400 chars, Next 4 → 6.
|
|
24
|
+
|
|
25
|
+
### Preserved
|
|
26
|
+
|
|
27
|
+
- Auditor-verified-before-posting, the verbatim six-label machine record,
|
|
28
|
+
and the honesty rules (PASS only on `pass`, `NO VERDICT` never renders,
|
|
29
|
+
pipe escaping) are unchanged.
|
|
30
|
+
|
|
31
|
+
## 0.38.49 — audit-pass drift fixes: prompt gate, summary segmentation, task honesty (2026-09-11)
|
|
32
|
+
|
|
33
|
+
Full-project audit pass on v0.38.48 (three parallel scout surveys). Six new
|
|
34
|
+
findings, all fixed on main; scout claims that were unproven, stale, or
|
|
35
|
+
by-design are disposed with rationale in the pass note, not as findings.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **Continuation prompt drift:** the Available-tools list and task workflow
|
|
40
|
+
now name `update_task_batch` (validate whole, verify milestones, persist
|
|
41
|
+
once) and the pending-task completion gate (`complete_goal` refused
|
|
42
|
+
before audit while committed tasks are open; `record_goal_judgment`
|
|
43
|
+
`choice=deferred` + `taskId` exempts a blocked item). Completion-voice
|
|
44
|
+
guidance matches the rich-section renderer budgets.
|
|
45
|
+
- **README verify path:** the `complete_goal` steps now include the
|
|
46
|
+
pending-task refusal before audit.
|
|
47
|
+
- **Docs versions/voice:** INSTALL updating examples show the current
|
|
48
|
+
versions; the supervision design doc distinguishes the verbatim six-label
|
|
49
|
+
archive record from the rich chat/transcript sections and the compact
|
|
50
|
+
width-bound projection.
|
|
51
|
+
- **Summary gate segmentation:** `missingCompletionSummaryLabels` segments
|
|
52
|
+
on the same last-occurrence positions as every projector, so a label
|
|
53
|
+
named inside an earlier value can no longer pass the gate while rendering
|
|
54
|
+
shifted (removed the now-dead `labelIndex`).
|
|
55
|
+
- **Task-tool honesty:** completing an already-complete task reports
|
|
56
|
+
`Task <id> is already complete.` and same-status `update_task_status`
|
|
57
|
+
moves report `already <status>` instead of the not-found fallthrough or
|
|
58
|
+
a no-op persist.
|
|
59
|
+
- **Sidecar pending boundary:** the approval-render outbox and the
|
|
60
|
+
update-check refresh defer while sessionDir resolution is pending instead
|
|
61
|
+
of creating (or latching degraded on) a `<cwd>/.pi-glla` fallback tree.
|
|
62
|
+
|
|
63
|
+
### Tests
|
|
64
|
+
|
|
65
|
+
- Re-baselined `tests/context-growth-measurement.test.ts` to 22_570 chars
|
|
66
|
+
/ 22_670 bytes per payload (+769 chars incl. one em dash) with exact
|
|
67
|
+
multiples preserved across probe sizes. New pins: stolen-label gate
|
|
68
|
+
agreement, already-complete/no-op task messages, sidecar pending deferral.
|
|
69
|
+
`tsc` clean.
|
|
70
|
+
|
|
3
71
|
## 0.38.48 — atomic task batches + pending-task completion gate (2026-09-11)
|
|
4
72
|
|
|
5
73
|
### Added
|
package/INSTALL.md
CHANGED
|
@@ -31,11 +31,11 @@ GLLA loads into new pi sessions. If pi is already open, reload that session:
|
|
|
31
31
|
|
|
32
32
|
## Updating
|
|
33
33
|
|
|
34
|
-
The status line always shows the running version (`glla: … · v0.38.
|
|
34
|
+
The status line always shows the running version (`glla: … · v0.38.48`).
|
|
35
35
|
When the npm registry is ahead, it also nudges:
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
glla: … · v0.38.
|
|
38
|
+
glla: … · v0.38.47 · update v0.38.48 available
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The nudge comes from a daily sidecar check (`.pi-glla/update-check.json`),
|
package/README.md
CHANGED
|
@@ -250,12 +250,14 @@ When the agent calls `complete_goal`, GLLA:
|
|
|
250
250
|
|
|
251
251
|
1. runs the contract's mechanical checks when a release or command check is
|
|
252
252
|
specified;
|
|
253
|
-
2.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
2. refuses the claim before audit when committed tasks are still open
|
|
254
|
+
(tasks with a recorded deferral are exempt), naming each open task;
|
|
255
|
+
3. writes an identity-bound completion claim;
|
|
256
|
+
4. starts a detached, fresh pi RPC worker for the audit;
|
|
257
|
+
5. asks the worker to inspect the repository and run bounded checks;
|
|
258
|
+
6. requires raw evidence for each verification-contract item through the
|
|
257
259
|
orchestrator-side regression shield;
|
|
258
|
-
|
|
260
|
+
7. keeps the goal open on infrastructure failure, missing evidence, or
|
|
259
261
|
disapproval instead of silently archiving it.
|
|
260
262
|
|
|
261
263
|
The auditor is intentionally isolated from the implementing conversation and
|
|
@@ -110,10 +110,16 @@ status/reason, durable telemetry, captured audit verdicts, and known archive
|
|
|
110
110
|
path. It says `not recorded` when a changed-file manifest or test result is not
|
|
111
111
|
available. It never infers a passing test or invents a commit.
|
|
112
112
|
|
|
113
|
-
The full recap lives in the archive
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
The full six-label recap lives verbatim in the archive (`## Completion summary`)
|
|
114
|
+
and the status/history surfaces. The human layer in chat, transcript, and the
|
|
115
|
+
archive's `## Terminal summary` renders the same facts as rich sections
|
|
116
|
+
(`## Done: <objective> — <outcome>`, duration line, Key Findings grouped by area
|
|
117
|
+
or tabulated at 4+ groups, Verification Summary table, Next, capped and clipped
|
|
118
|
+
to fixed budgets). Width-bound and external surfaces (status line, widget card,
|
|
119
|
+
external notifies) use the compact single-line projection of all six labels.
|
|
120
|
+
Every terminal goal notification, including version-bearing already-shipped claims,
|
|
121
|
+
explicit goal/list cancellation, and `/glla wipe`, carries either the rich sections
|
|
122
|
+
or the compact projection; loop notifications do the same. The terminal
|
|
117
123
|
notification may use a compact excerpt. The executor recap and independent
|
|
118
124
|
auditor verdict stay separate: an approval is not manufactured from the
|
|
119
125
|
presence of a summary.
|
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.50 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,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { appendLedger, ensureDirs, nowIso, piGlaDir, runPersistStep } from "./goal-loop-core.js";
|
|
4
|
+
import { stateRootPending } from "./glla-state-root.js";
|
|
4
5
|
|
|
5
6
|
/** Durable terminal-summary outbox. A toast or a live host is not delivery:
|
|
6
7
|
* only a confirmed visible session message acknowledges a render. */
|
|
@@ -67,6 +68,9 @@ function readRenders(cwd: string): PendingApprovalRender[] {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function writeRenders(cwd: string, renders: PendingApprovalRender[]): boolean {
|
|
71
|
+
// v0.38.49 audit: pending sessionDir resolution defers every write that
|
|
72
|
+
// would otherwise recreate <cwd>/.pi-glla — the sidecar is no exception.
|
|
73
|
+
if (stateRootPending()) return false;
|
|
70
74
|
const landed = runPersistStep("persistApprovalRender", () => {
|
|
71
75
|
ensureDirs(cwd);
|
|
72
76
|
const file = approvalRenderStorePath(cwd);
|
|
@@ -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
|
|
@@ -31,10 +31,6 @@ export interface CompletionSummaryFacts {
|
|
|
31
31
|
archivePath?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function labelIndex(text: string, label: CompletionSummaryLabel): number {
|
|
35
|
-
return text.toLowerCase().indexOf(label.toLowerCase());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
/**
|
|
39
35
|
* Validation/version annotations are metadata, not recap fields. Keep them
|
|
40
36
|
* out of the parser so a generated NOTE containing examples such as
|
|
@@ -45,17 +41,20 @@ function completionSummaryBody(text: string): string {
|
|
|
45
41
|
return annotation?.index === undefined ? text : text.slice(0, annotation.index);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
/** Return labels that are absent or have no value after the label.
|
|
44
|
+
/** Return labels that are absent or have no value after the label. Segments on
|
|
45
|
+
* the same last-occurrence positions as every projector (compact, multi-line,
|
|
46
|
+
* rich): a label named inside an earlier value must not shift the gate away
|
|
47
|
+
* from what the user will actually read. */
|
|
49
48
|
export function missingCompletionSummaryLabels(text: string): CompletionSummaryLabel[] {
|
|
50
49
|
const normalized = completionSummaryBody(text).trim();
|
|
50
|
+
const positions = labelPositions(normalized.toLowerCase());
|
|
51
51
|
return COMPLETION_SUMMARY_LABELS.filter((label) => {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
const valueStart = start + label.length;
|
|
55
|
-
const next =
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.map((index) => valueStart + index)
|
|
52
|
+
const current = positions.find((entry) => entry.label === label);
|
|
53
|
+
if (!current) return true;
|
|
54
|
+
const valueStart = current.start + label.length;
|
|
55
|
+
const next = positions
|
|
56
|
+
.filter((entry) => entry.start > current.start)
|
|
57
|
+
.map((entry) => entry.start)
|
|
59
58
|
.sort((a, b) => a - b)[0];
|
|
60
59
|
return normalized.slice(valueStart, next ?? normalized.length).trim().length === 0;
|
|
61
60
|
});
|
|
@@ -270,18 +269,27 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
/** Rich terminal voice (field 20260911_003839/003903/003907 — the
|
|
273
|
-
* Antigravity close
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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 +
|
|
276
276
|
* transcript + archive human layer); progress cards, status line, and
|
|
277
277
|
* external notifies keep their compact projections. Verbose by owner
|
|
278
|
-
* choice: findings cap
|
|
279
|
-
export const RICH_FINDINGS_CAP =
|
|
280
|
-
export const RICH_VALUE_BUDGET =
|
|
281
|
-
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;
|
|
282
289
|
|
|
283
290
|
export interface RichTerminalParts {
|
|
284
291
|
headline: string;
|
|
292
|
+
durationLine: string | null;
|
|
285
293
|
findingLines: string[];
|
|
286
294
|
tableLines: string[];
|
|
287
295
|
nextLines: string[];
|
|
@@ -315,6 +323,63 @@ function leadBody(detail: string): { lead: string; body: string } {
|
|
|
315
323
|
return { lead: detail.slice(0, separator).trim() || "Note", body: detail.slice(separator + 1).trim() };
|
|
316
324
|
}
|
|
317
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
|
+
|
|
318
383
|
/** Partition informing details into findings / Tests / next buckets. */
|
|
319
384
|
export function partitionRichDetails(details: string[]): { findings: string[]; tests: string[]; next: string[] } {
|
|
320
385
|
const findings: string[] = [];
|
|
@@ -328,18 +393,60 @@ export function partitionRichDetails(details: string[]): { findings: string[]; t
|
|
|
328
393
|
return { findings, tests, next };
|
|
329
394
|
}
|
|
330
395
|
|
|
331
|
-
/**
|
|
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. */
|
|
332
411
|
export function buildRichTerminalParts(args: {
|
|
333
412
|
outcome: string;
|
|
334
413
|
details: string[];
|
|
335
414
|
countsLine: string;
|
|
336
415
|
auditHistory?: Goal["auditHistory"];
|
|
416
|
+
objective?: string;
|
|
417
|
+
durationLine?: string | null;
|
|
418
|
+
groups?: FindingGroup[];
|
|
337
419
|
}): RichTerminalParts {
|
|
338
420
|
const { findings, tests, next } = partitionRichDetails(args.details);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
450
|
const tableRows: string[] = [];
|
|
344
451
|
for (const detail of tests.slice(0, 2)) {
|
|
345
452
|
const { body } = leadBody(detail);
|
|
@@ -358,16 +465,19 @@ export function buildRichTerminalParts(args: {
|
|
|
358
465
|
return `- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
|
|
359
466
|
});
|
|
360
467
|
return {
|
|
361
|
-
headline
|
|
468
|
+
headline,
|
|
469
|
+
durationLine: args.durationLine ?? null,
|
|
362
470
|
findingLines,
|
|
363
471
|
tableLines,
|
|
364
472
|
nextLines,
|
|
365
473
|
};
|
|
366
474
|
}
|
|
367
475
|
|
|
368
|
-
/** 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. */
|
|
369
478
|
export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { headline?: string }): string[] {
|
|
370
479
|
const lines = [opts?.headline ?? parts.headline, ""];
|
|
480
|
+
if (parts.durationLine) lines.push(parts.durationLine, "");
|
|
371
481
|
if (parts.findingLines.length > 0) {
|
|
372
482
|
lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
|
|
373
483
|
}
|
|
@@ -385,7 +495,7 @@ export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { head
|
|
|
385
495
|
* raw six-label recap stays verbatim in `## Completion summary` as the
|
|
386
496
|
* machine layer. Headline follows terminal status; aborted records never
|
|
387
497
|
* wear a `Done` headline. */
|
|
388
|
-
export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string): string[] {
|
|
498
|
+
export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string, findingGroups?: FindingGroup[]): string[] {
|
|
389
499
|
const facts: CompletionSummaryFacts = { goal, status, archivePath };
|
|
390
500
|
const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
|
|
391
501
|
const brief = humanCompletionBrief(summary, 140, RICH_VALUE_BUDGET);
|
|
@@ -400,10 +510,13 @@ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath:
|
|
|
400
510
|
details: withoutStaleNext(brief.details),
|
|
401
511
|
countsLine,
|
|
402
512
|
auditHistory: history,
|
|
513
|
+
objective: goal.objective,
|
|
514
|
+
durationLine: buildDurationLine(goal),
|
|
515
|
+
groups: findingGroups,
|
|
403
516
|
});
|
|
404
517
|
return [
|
|
405
518
|
...composeRichTerminalLines(parts, {
|
|
406
|
-
headline: status === "complete" ? parts.headline :
|
|
519
|
+
headline: status === "complete" ? parts.headline : requestEchoHeadline("Aborted", goal.objective, brief.outcome),
|
|
407
520
|
}),
|
|
408
521
|
trailerBullet(stripApprovalModel(approval)),
|
|
409
522
|
trailerBullet(countsLine),
|
|
@@ -493,6 +606,11 @@ export interface TerminalApprovalRenderInput {
|
|
|
493
606
|
auditNote?: string;
|
|
494
607
|
/** v0.38.37: what the agent deliberately left out (complete_goal leftOut). */
|
|
495
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[];
|
|
496
614
|
}
|
|
497
615
|
|
|
498
616
|
export interface TerminalApprovalRender {
|
|
@@ -568,6 +686,9 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
568
686
|
details: withoutStaleNext(richDetails),
|
|
569
687
|
countsLine,
|
|
570
688
|
auditHistory: input.goal.auditHistory,
|
|
689
|
+
objective: input.goal.objective,
|
|
690
|
+
durationLine: buildDurationLine(input.goal),
|
|
691
|
+
groups: input.findingGroups,
|
|
571
692
|
});
|
|
572
693
|
const chatBody = composeRichTerminalLines(richParts);
|
|
573
694
|
const transcriptBody = composeRichTerminalLines(richParts);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { stateRootPending } from "./glla-state-root.js";
|
|
4
5
|
import {
|
|
5
6
|
compareVersions,
|
|
6
7
|
GLLA_PACKAGE_NAME,
|
|
@@ -60,6 +61,10 @@ export function refreshUpdateCheck(
|
|
|
60
61
|
now = Date.now(),
|
|
61
62
|
spawnFn: SpawnFn = spawn as unknown as SpawnFn,
|
|
62
63
|
): void {
|
|
64
|
+
// v0.38.49 audit: while sessionDir resolution is pending there is no
|
|
65
|
+
// session root yet — skip the refresh (and its cache write) instead of
|
|
66
|
+
// creating a <cwd>/.pi-glla fallback tree.
|
|
67
|
+
if (stateRootPending()) return;
|
|
63
68
|
try {
|
|
64
69
|
const cached = readUpdateCheckRaw(cwd);
|
|
65
70
|
if (cached && now - cached.checkedAt < UPDATE_CHECK_TTL_MS) return;
|
|
@@ -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 }
|
|
@@ -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
|
|
@@ -2213,9 +2235,14 @@ function registerAgentTools(pi: any): void {
|
|
|
2213
2235
|
const tl = state.goal.taskList;
|
|
2214
2236
|
// v0.38.48: copy-swap — the live list is never mutated in place, so a
|
|
2215
2237
|
// kill between verify and persist (or a persist failure) cannot leave
|
|
2216
|
-
// partial state. Message strings are
|
|
2238
|
+
// partial state. Message strings are pinned by
|
|
2239
|
+
// tests/task-atomicity-gate.test.ts.
|
|
2217
2240
|
const t = findTask(tl, p.id);
|
|
2218
|
-
if (t
|
|
2241
|
+
if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2242
|
+
// v0.38.49 audit: an existing but already-complete task is a state
|
|
2243
|
+
// fact, not a typo — report it instead of the not-found fallthrough.
|
|
2244
|
+
if (t.status === "complete") return { content: [{ type: "text", text: `Task ${p.id} is already complete.` }], details: {} };
|
|
2245
|
+
if (t) {
|
|
2219
2246
|
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
2220
2247
|
if (checkRes) {
|
|
2221
2248
|
return {
|
|
@@ -2231,7 +2258,6 @@ function registerAgentTools(pi: any): void {
|
|
|
2231
2258
|
}
|
|
2232
2259
|
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
2233
2260
|
}
|
|
2234
|
-
return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2235
2261
|
},
|
|
2236
2262
|
}));
|
|
2237
2263
|
|
|
@@ -2253,8 +2279,13 @@ function registerAgentTools(pi: any): void {
|
|
|
2253
2279
|
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2254
2280
|
}
|
|
2255
2281
|
const tl = state.goal.taskList;
|
|
2256
|
-
// v0.38.48: copy-swap (see complete_task). Message strings
|
|
2282
|
+
// v0.38.48: copy-swap (see complete_task). Message strings are pinned
|
|
2283
|
+
// by tests/task-atomicity-gate.test.ts.
|
|
2257
2284
|
const t = findTask(tl, p.id);
|
|
2285
|
+
if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2286
|
+
// v0.38.49 audit: same-status moves are no-ops with a state-fact
|
|
2287
|
+
// reply, not a persist round-trip reporting a change that happened.
|
|
2288
|
+
if (t.status === p.status) return { content: [{ type: "text", text: `Task ${p.id} is already ${p.status}.` }], details: {} };
|
|
2258
2289
|
if (t) {
|
|
2259
2290
|
if (p.status === "complete") {
|
|
2260
2291
|
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
@@ -2273,7 +2304,6 @@ function registerAgentTools(pi: any): void {
|
|
|
2273
2304
|
}
|
|
2274
2305
|
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
2275
2306
|
}
|
|
2276
|
-
return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2277
2307
|
},
|
|
2278
2308
|
}));
|
|
2279
2309
|
|
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.50",
|
|
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",
|
|
@@ -55,9 +55,9 @@ When a goal, list item, or pending task explicitly says `Agent: Designer`, `Role
|
|
|
55
55
|
|
|
56
56
|
## Available tools
|
|
57
57
|
|
|
58
|
-
You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, the `subagent` tool, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `record_goal_judgment`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
|
|
58
|
+
You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, the `subagent` tool, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `update_task_batch`, `record_goal_judgment`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
|
|
59
59
|
|
|
60
|
-
If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` as you go (not
|
|
60
|
+
If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` / `update_task_batch` as you go (not as a post-hoc checklist at the end). For several status changes at once, prefer one `update_task_batch` call: the whole batch validates first, milestones verify, then all statuses persist in one write. Limits: 20 tasks, 5 subtasks per task.
|
|
61
61
|
|
|
62
62
|
When the agent calls any of these, the orchestrator tracks the call and persists state to `.pi-glla/active.jsonl`.
|
|
63
63
|
|
|
@@ -156,6 +156,8 @@ Do NOT conclude "the loop is too eager" or "I am broken" before checking what a
|
|
|
156
156
|
|
|
157
157
|
Use tasks as PROGRESS TRACKERS during your work — not as a post-hoc checklist to batch-mark at the end.
|
|
158
158
|
|
|
159
|
+
Pending-task gate: `complete_goal` is refused BEFORE the auditor when committed tasks are still open — the refusal names each open task and sends nothing to the auditor. Finish them with `complete_task` / `update_task_batch`, or record an explicit deferral with `record_goal_judgment` (`choice="deferred"` + `taskId`) for a genuinely blocked item.
|
|
160
|
+
|
|
159
161
|
Before deciding that the goal is achieved, perform a completion audit against the actual current state:
|
|
160
162
|
|
|
161
163
|
- Restate the objective as concrete deliverables or success criteria.
|
|
@@ -174,7 +176,7 @@ recap and synthesizes a recorded-facts-only fallback at terminalization when a
|
|
|
174
176
|
legacy or incomplete caller omits it; never invent a commit, changed file, or
|
|
175
177
|
passing test to make the recap sound complete.
|
|
176
178
|
|
|
177
|
-
Then call `complete_goal
|
|
179
|
+
Then call `complete_goal` — only with every committed task finished or explicitly deferred, otherwise the claim is refused before audit (see the pending-task gate above). The orchestrator will spawn an **isolated auditor** in a fresh session to verify, and either accept (mark goal complete) or reject (continue work).
|
|
178
180
|
|
|
179
181
|
If your work has shifted to items different from the original objective (the original was blocked, higher-ROI items emerged): pass `newObjective` to `complete_goal` to atomically update the objective and audit against the NEW one — do NOT call `complete_goal` on the original objective after shipping different work, the auditor will disapprove because the original isn't shipped. Alternatively `pause_goal` proposing a `/goal tweak` if the shift needs the user's call.
|
|
180
182
|
|
|
@@ -203,4 +205,4 @@ The orchestrator's backstop is the stall watchdog: three consecutive turns with
|
|
|
203
205
|
|
|
204
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.
|
|
205
207
|
|
|
206
|
-
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
|
|
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.
|