pi-goal-list-loop-audit 0.38.48 → 0.38.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.49 — audit-pass drift fixes: prompt gate, summary segmentation, task honesty (2026-09-11)
4
+
5
+ Full-project audit pass on v0.38.48 (three parallel scout surveys). Six new
6
+ findings, all fixed on main; scout claims that were unproven, stale, or
7
+ by-design are disposed with rationale in the pass note, not as findings.
8
+
9
+ ### Fixed
10
+
11
+ - **Continuation prompt drift:** the Available-tools list and task workflow
12
+ now name `update_task_batch` (validate whole, verify milestones, persist
13
+ once) and the pending-task completion gate (`complete_goal` refused
14
+ before audit while committed tasks are open; `record_goal_judgment`
15
+ `choice=deferred` + `taskId` exempts a blocked item). Completion-voice
16
+ guidance matches the rich-section renderer budgets.
17
+ - **README verify path:** the `complete_goal` steps now include the
18
+ pending-task refusal before audit.
19
+ - **Docs versions/voice:** INSTALL updating examples show the current
20
+ versions; the supervision design doc distinguishes the verbatim six-label
21
+ archive record from the rich chat/transcript sections and the compact
22
+ width-bound projection.
23
+ - **Summary gate segmentation:** `missingCompletionSummaryLabels` segments
24
+ on the same last-occurrence positions as every projector, so a label
25
+ named inside an earlier value can no longer pass the gate while rendering
26
+ shifted (removed the now-dead `labelIndex`).
27
+ - **Task-tool honesty:** completing an already-complete task reports
28
+ `Task <id> is already complete.` and same-status `update_task_status`
29
+ moves report `already <status>` instead of the not-found fallthrough or
30
+ a no-op persist.
31
+ - **Sidecar pending boundary:** the approval-render outbox and the
32
+ update-check refresh defer while sessionDir resolution is pending instead
33
+ of creating (or latching degraded on) a `<cwd>/.pi-glla` fallback tree.
34
+
35
+ ### Tests
36
+
37
+ - Re-baselined `tests/context-growth-measurement.test.ts` to 22_570 chars
38
+ / 22_670 bytes per payload (+769 chars incl. one em dash) with exact
39
+ multiples preserved across probe sizes. New pins: stolen-label gate
40
+ agreement, already-complete/no-op task messages, sidecar pending deferral.
41
+ `tsc` clean.
42
+
3
43
  ## 0.38.48 — atomic task batches + pending-task completion gate (2026-09-11)
4
44
 
5
45
  ### 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.46`).
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.45 · update v0.38.46 available
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. writes an identity-bound completion claim;
254
- 3. starts a detached, fresh pi RPC worker for the audit;
255
- 4. asks the worker to inspect the repository and run bounded checks;
256
- 5. requires raw evidence for each verification-contract item through the
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
- 6. keeps the goal open on infrastructure failure, missing evidence, or
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,15 @@ 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 and status/history surfaces. Every
114
- terminal goal notification—including version-bearing already-shipped claims,
115
- explicit goal/list cancellation, and `/glla wipe`—includes a compact
116
- projection of all six labels; loop notifications do the same. The terminal
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`, Key Findings, Verification Summary table, Next, capped and clipped
117
+ to fixed budgets). Width-bound and external surfaces (status line, widget card,
118
+ external notifies) use the compact single-line projection of all six labels.
119
+ Every terminal goal notification, including version-bearing already-shipped claims,
120
+ explicit goal/list cancellation, and `/glla wipe`, carries either the rich sections
121
+ or the compact projection; loop notifications do the same. The terminal
117
122
  notification may use a compact excerpt. The executor recap and independent
118
123
  auditor verdict stay separate: an approval is not manufactured from the
119
124
  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.48 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.38.49 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);
@@ -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 start = labelIndex(normalized, label);
53
- if (start < 0) return true;
54
- const valueStart = start + label.length;
55
- const next = COMPLETION_SUMMARY_LABELS
56
- .map((candidate) => labelIndex(normalized.slice(valueStart), candidate))
57
- .filter((index) => index >= 0)
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
  });
@@ -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;
@@ -2213,9 +2213,14 @@ function registerAgentTools(pi: any): void {
2213
2213
  const tl = state.goal.taskList;
2214
2214
  // v0.38.48: copy-swap — the live list is never mutated in place, so a
2215
2215
  // kill between verify and persist (or a persist failure) cannot leave
2216
- // partial state. Message strings are unchanged (pinned by tests).
2216
+ // partial state. Message strings are pinned by
2217
+ // tests/task-atomicity-gate.test.ts.
2217
2218
  const t = findTask(tl, p.id);
2218
- if (t && t.status !== "complete") {
2219
+ if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
2220
+ // v0.38.49 audit: an existing but already-complete task is a state
2221
+ // fact, not a typo — report it instead of the not-found fallthrough.
2222
+ if (t.status === "complete") return { content: [{ type: "text", text: `Task ${p.id} is already complete.` }], details: {} };
2223
+ if (t) {
2219
2224
  const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
2220
2225
  if (checkRes) {
2221
2226
  return {
@@ -2231,7 +2236,6 @@ function registerAgentTools(pi: any): void {
2231
2236
  }
2232
2237
  return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
2233
2238
  }
2234
- return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
2235
2239
  },
2236
2240
  }));
2237
2241
 
@@ -2253,8 +2257,13 @@ function registerAgentTools(pi: any): void {
2253
2257
  return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
2254
2258
  }
2255
2259
  const tl = state.goal.taskList;
2256
- // v0.38.48: copy-swap (see complete_task). Message strings unchanged.
2260
+ // v0.38.48: copy-swap (see complete_task). Message strings are pinned
2261
+ // by tests/task-atomicity-gate.test.ts.
2257
2262
  const t = findTask(tl, p.id);
2263
+ if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
2264
+ // v0.38.49 audit: same-status moves are no-ops with a state-fact
2265
+ // reply, not a persist round-trip reporting a change that happened.
2266
+ if (t.status === p.status) return { content: [{ type: "text", text: `Task ${p.id} is already ${p.status}.` }], details: {} };
2258
2267
  if (t) {
2259
2268
  if (p.status === "complete") {
2260
2269
  const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
@@ -2273,7 +2282,6 @@ function registerAgentTools(pi: any): void {
2273
2282
  }
2274
2283
  return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
2275
2284
  }
2276
- return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
2277
2285
  },
2278
2286
  }));
2279
2287
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.48",
3
+ "version": "0.38.49",
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 in a batch at the end). Limits: 20 tasks, 5 subtasks per task.
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`. The orchestrator will spawn an **isolated auditor** in a fresh session to verify, and either accept (mark goal complete) or reject (continue work).
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 line of ~90 chars so every chat bullet fits without a trailing …; 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.