pi-goal-list-loop-audit 0.38.113 → 0.39.4

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
@@ -2,6 +2,89 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.39.4 — project builder, audit recovery and opportunistic compaction (2026-10-05)
6
+
7
+ ### check compaction before idle loop dispatch
8
+
9
+ - Metric loops and project builder turns check the configured context-token
10
+ threshold at their actual idle dispatch boundary, matching goal/list sends.
11
+ Hosts without a settled-turn event can still attempt opportunistic compaction.
12
+ - A failed optional compaction resumes the same active loop through a fresh
13
+ owner context. Busy hosts, queued messages, paused work and audit dispatches
14
+ retain their guards; an attempted episode is not retried every turn.
15
+
16
+ ### preserve project audit boundaries and recovery evidence
17
+
18
+ - Startup and manual audit-job cleanup preserve jobs belonging to unresolved
19
+ goal or project increment claims, including held work. Once those claims
20
+ settle, ordinary age and worker-identity retention applies again.
21
+ - Health reports exclude claim-protected jobs from cleanup candidates. Cleanup
22
+ also refuses to delete jobs when the durable claim journal cannot be read.
23
+ - Settled-turn and idle-boundary transcript compaction wait while an exact
24
+ project increment audit dispatch owns the boundary.
25
+ - The project-audit heartbeat regression now exercises six actual heartbeat
26
+ ticks during a dispatched worker run, beyond the stall escalation threshold.
27
+
28
+ ### protect project increment audits from continuation stalls
29
+
30
+ - The main-session heartbeat no longer counts failed continuation attempts
31
+ while the exact project increment audit dispatch is running. Detached worker
32
+ timeout handling remains responsible for the audit. A saved claim alone, a
33
+ replacement claim, or a cancelled dispatch cannot suppress the heartbeat.
34
+ - Held project audits say **Audit held** and show the recorded stop reason
35
+ instead of displaying an ambiguous auditing phase with generic held text.
36
+
37
+ ### explain long audit tool waits
38
+
39
+ - Audit cards keep the current tool's ticking elapsed time and timeout visible
40
+ when it stops producing events. The card says it is waiting on the tool,
41
+ rather than turning the current call into a historical "last tool" row.
42
+ - Compact footers lead with the tool wait and its elapsed/timeout clock.
43
+ Waiting is distinct from fresh activity; cancelled, overdue and invalid
44
+ telemetry cannot claim a healthy wait. Timeout handling stays automatic.
45
+ - Automatic and manual audit progress use the worker's effective granted tool
46
+ timeout, matching the watchdog rather than showing only the base budget.
47
+
48
+ ### respec project builder
49
+
50
+ - `/loop respec [project direction]` drafts intended requirements for confirmation,
51
+ builds coherent increments, runs isolated audits and replans unfinished work.
52
+ Task claims and spec checkboxes cannot verify requirements. Later audits cover
53
+ previous capabilities as regressions; completion requires all adopted criteria.
54
+ - Scope refinements require confirmation, retain removal/task history and fence
55
+ old verdicts by revision. Blockers stay visible; all-blocked projects park,
56
+ and clearing a blocker preserves pause until explicit resume.
57
+ - Durable audit identities recover saved verdicts across restart, concurrent
58
+ wakes share one worker, pause/stop cancels audits, and archive failures retain
59
+ approved evidence for settlement retry. Completion archives the project before
60
+ exposing terminal state and announces waiting queue items.
61
+ - Project audit cards show live worker/retry state, model, elapsed time and last
62
+ activity. `/loop status` lists every requirement and its acceptance/evidence.
63
+ Verified project summaries use semantic colors and bold, archive before
64
+ delivery, and replay unacknowledged receipts on reload.
65
+ - Full audit fixes persistence rollback, supervisor freeze/retry gating,
66
+ rapid pause/resume rearming, blocked-project resume and continuous audit
67
+ visibility. Project receipts use the shared durable outbox across project
68
+ replacement; large summaries disclose omissions and retain full archives.
69
+ Generic loop recaps preserve verified project summaries.
70
+ - Project cards/status expose phase, increment, verified/remaining coverage,
71
+ blockers, evidence and next action using the existing semantic palette.
72
+ - Regression-shield rejection reports unfinished work and carries missing
73
+ contract evidence into replanning. Stale audit wakes are rejected before
74
+ model resolution or dispatch.
75
+
76
+ ### semantic summary colors
77
+
78
+ - Summary emphasis uses bold, including Markdown italic emphasis and quotes;
79
+ summaries do not render italics.
80
+ - Terminal completion summaries use the active theme’s semantic colors for
81
+ outcomes, remaining work, next actions and verification results. Unknown
82
+ verification stays neutral; reported or skipped checks use amber, and
83
+ failures use red without recoloring unrelated approval references.
84
+ - Styling is applied by a registered renderer. Saved summary Markdown,
85
+ archive content, receipt identity and headless output retain plain content;
86
+ continuation messages continue through Pi’s default renderer.
87
+
5
88
  ## 0.38.113 — make background audits clear (2026-10-03)
6
89
 
7
90
  - The live audit widget uses a compact card, keeping audit state, completed
package/README.md CHANGED
@@ -214,6 +214,8 @@ drafting flow. Ambiguous context is never queued automatically.
214
214
  /loop start "reduce flaky tests" measure="..." direction=min
215
215
  /loop start "keep improving the spec" measure=none max=20 cadence=900
216
216
  /loop audit # recurring project-audit cadence
217
+ /loop respec [project direction] # draft intended scope, build, audit and replan
218
+ /loop refine <suggestion> # steer the next round; scope changes need confirmation
217
219
  /loop status
218
220
  /loop stop
219
221
  ```
@@ -238,7 +240,40 @@ There are three loop styles:
238
240
  - **Project audit:** each iteration looks for the next important finding,
239
241
  appends evidence to the audit ledger, and works through the findings.
240
242
 
241
- If the work has a finish line, use `/goal`, not an endless loop.
243
+ Use `/goal` for one defined outcome, `/list` for a known task queue, and
244
+ `/loop respec` for developing a project through evolving build and audit batches.
245
+
246
+ `/loop respec` develops a project through substantial increments. It researches
247
+ the code and root `SPEC.md` (or `spec.md`), drafts intended capabilities with
248
+ observable acceptance criteria, and asks you to confirm that scope. Existing
249
+ behavior and desired behavior are kept distinct; missing features become work
250
+ to build. `## Rules` remains binding project guidance.
251
+
252
+ After confirmation, the builder plans a coherent batch, implements it, and
253
+ submits it to the isolated auditor. A task marked implemented is a claim;
254
+ only independent approval verifies its requirements. Failed checks and unfinished
255
+ requirements feed replanning. Each later audit also checks previously verified
256
+ capabilities for regressions. The project finishes only when every adopted
257
+ requirement is independently verified and the project archive is written.
258
+
259
+ The widget and `/loop status` show phase, increment, verified/remaining coverage
260
+ and blockers. During auditing, the card shows the actual worker state, model,
261
+ elapsed time and last activity, including retries; a restored claim without
262
+ live telemetry is shown as waiting for dispatch. `/loop status` lists every
263
+ requirement, its acceptance criteria, blocker and verification evidence.
264
+ Verified completion posts a semantic summary of shipped capabilities,
265
+ acceptance checks and evidence, saved in the journal and project archive;
266
+ unacknowledged delivery uses the durable outbox and retries on reload, even
267
+ after a new project replaces the old loop, without rerunning the audit. Large
268
+ summary cards name omitted requirements/reports; the archive and `/loop status`
269
+ retain the full criteria and evidence.
270
+ `/loop pause` holds work, `/loop resume` continues saved state, and
271
+ `/loop stop` stops it without calling unfinished work complete. If all remaining
272
+ work is blocked, automation parks with reasons; clearing a blocker leaves work
273
+ paused until resume. `/loop refine <suggestion>` supplies direction for the next
274
+ round. Changes to adopted requirements show current/proposed acceptance criteria
275
+ and need your confirmation. Text after `respec` is project direction, not metric
276
+ or budget option syntax. Generic `/loop start` remains the metric/bounded path.
242
277
 
243
278
  ## The autonomy model
244
279
 
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.113 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.39.4 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
@@ -2,25 +2,25 @@
2
2
 
3
3
  Generated by `node scripts/generate-inventory.mjs`. Run with `--check` to detect drift. Counts describe files and literal registrations, not executed coverage.
4
4
 
5
- - Extension/script files: 91
6
- - Extension/script lines: 64275
7
- - Top-level test files: 342
5
+ - Extension/script files: 96
6
+ - Extension/script lines: 64969
7
+ - Top-level test files: 348
8
8
  - Ambient bridge names: 179 (audit baseline: 183)
9
9
  - Commands: `/glla`, `/goal`, `/list`, `/loop`, `/review`
10
- - Tools: `complete_goal`, `complete_task`, `list_activate`, `list_add`, `list_status`, `pause_goal`, `propose_goal_draft`, `propose_loop_draft`, `propose_loop_refine`, `propose_task_list`, `record_goal_judgment`, `resume_goal`, `update_task_batch`, `update_task_status`
10
+ - Tools: `audit_project_increment`, `block_project_requirement`, `claim_project_task`, `complete_goal`, `complete_task`, `list_activate`, `list_add`, `list_status`, `pause_goal`, `plan_project_increment`, `propose_goal_draft`, `propose_loop_draft`, `propose_loop_refine`, `propose_project_refinement`, `propose_project_requirements`, `propose_task_list`, `record_goal_judgment`, `resume_goal`, `unblock_project_requirement`, `update_task_batch`, `update_task_status`
11
11
 
12
12
  ## Largest modules
13
13
 
14
14
  | Module | Lines |
15
15
  |---|---:|
16
16
  | [extensions/goal-loop-core.ts](../extensions/goal-loop-core.ts) | 4966 |
17
- | [extensions/loops/goal-tools.ts](../extensions/loops/goal-tools.ts) | 4147 |
18
- | [extensions/loops/goal-activation.ts](../extensions/loops/goal-activation.ts) | 3738 |
19
- | [extensions/goal-commands.ts](../extensions/goal-commands.ts) | 3526 |
20
- | [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) | 2685 |
21
- | [extensions/goal-loop-auditor-process.ts](../extensions/goal-loop-auditor-process.ts) | 2664 |
22
- | [extensions/loops/goal-auditor-hooks.ts](../extensions/loops/goal-auditor-hooks.ts) | 2655 |
23
- | [extensions/goal-heartbeat.ts](../extensions/goal-heartbeat.ts) | 2093 |
17
+ | [extensions/loops/goal-tools.ts](../extensions/loops/goal-tools.ts) | 4172 |
18
+ | [extensions/loops/goal-activation.ts](../extensions/loops/goal-activation.ts) | 3740 |
19
+ | [extensions/goal-commands.ts](../extensions/goal-commands.ts) | 3527 |
20
+ | [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) | 2738 |
21
+ | [extensions/goal-loop-auditor-process.ts](../extensions/goal-loop-auditor-process.ts) | 2675 |
22
+ | [extensions/loops/goal-auditor-hooks.ts](../extensions/loops/goal-auditor-hooks.ts) | 2656 |
23
+ | [extensions/goal-heartbeat.ts](../extensions/goal-heartbeat.ts) | 2098 |
24
24
  | [extensions/loops/goal-session.ts](../extensions/loops/goal-session.ts) | 2062 |
25
25
  | [extensions/loops/goal-settings-ui.ts](../extensions/loops/goal-settings-ui.ts) | 2023 |
26
26
 
@@ -55,6 +55,11 @@ function durableStateSignature(state: State, liveSubagentCount: number): string
55
55
  iteration: state.loop.iteration,
56
56
  stopReason: state.loop.stopReason,
57
57
  bestValue: state.loop.bestValue,
58
+ builder: state.loop.builder ? {
59
+ phase: state.loop.builder.phase, revision: state.loop.builder.revision,
60
+ cycle: state.loop.builder.cycle, attemptId: state.loop.builder.audit?.attemptId,
61
+ requirements: state.loop.builder.requirements?.map(r => ({ id: r.id, status: r.status })),
62
+ } : undefined,
58
63
  }
59
64
  : null;
60
65
  const recovery = state.mainModelRecovery
@@ -85,6 +90,7 @@ export function activeSupervisionPlanes(state: State, liveSubagentCount = 0): Su
85
90
  if (goal.status === "auditing" || !!goal.pendingCompletion) planes.push("auditor");
86
91
  }
87
92
  if (state.loop?.active) planes.push("loop");
93
+ if (state.loop?.builder?.phase === "auditing" && state.loop.builder.audit && !planes.includes("auditor")) planes.push("auditor");
88
94
  if ((state.list?.length ?? 0) > 0) planes.push("queue");
89
95
  if (state.mainModelRecovery) planes.push("provider-recovery");
90
96
  if (liveSubagentCount > 0) planes.push("subagent");
@@ -2875,8 +2875,9 @@ function cmdAudits(args: string, ctx: ExtensionContext): void {
2875
2875
  const action = /\bcleanup\b/.test(args) ? "cleanup" : "health";
2876
2876
  ctx.ui.notify(
2877
2877
  `glla audit-job ${action}: ${report.total} director${report.total === 1 ? "y" : "ies"} · ${report.live} live · ${report.dead} proven dead · ${report.ambiguous} ambiguous · ${report.bytes} bytes${report.cleanupCandidates > 0 ? ` · ${report.cleanupCandidates} old dead candidate(s)` : ""}.` +
2878
- (report.ambiguous > 0 ? " Ambiguous locks were preserved." : ""),
2879
- report.ambiguous > 0 ? "warning" : "info",
2878
+ (report.ambiguous > 0 ? " Ambiguous locks were preserved." : "") +
2879
+ (report.retentionBlocked ? ` Cleanup held: ${report.retentionBlocked}.` : ""),
2880
+ report.ambiguous > 0 || report.retentionBlocked ? "warning" : "info",
2880
2881
  );
2881
2882
  return;
2882
2883
  }
@@ -28,6 +28,7 @@ import {
28
28
  } from "./goal-loop-core.js";
29
29
  import { loadGlobalSettings, type Settings } from "./goal-settings.js";
30
30
  import { PLAN_B_MAX_ATTEMPTS, resolveCompactorModel } from "./compactor-model.js";
31
+ import { respecBuilderAuditInFlight } from "./respec-builder-runtime.js";
31
32
 
32
33
  /** Hard cap on the persisted brief: a handoff, not a transcript. */
33
34
  export const COMPACTOR_BRIEF_MAX_CHARS = 2000;
@@ -138,6 +139,7 @@ export function maybeCompactTranscriptAtBoundary(
138
139
  onFailure?: () => void,
139
140
  ): boolean {
140
141
  if (!flags.supervising || flags.auditInFlight || flags.paused) return false;
142
+ if (respecBuilderAuditInFlight(ctx.cwd)) return false;
141
143
  let idle = false;
142
144
  let pending = true;
143
145
  try {
@@ -44,6 +44,7 @@ import {
44
44
  } from "./goal-loop-backoff.js";
45
45
  import { isLoopActive, loopTimerPending, scheduleLoopTick } from "./goal-loop.js";
46
46
  import { auditorWorkerLiveForAttempt } from "./goal-loop-auditor-process.js";
47
+ import { respecBuilderAuditInFlight } from "./respec-builder-runtime.js";
47
48
  import { mainModelRecoveryActive, markCompletionAuditRecoveryPending, probeMainModelRecovery } from "./goal-recovery.js";
48
49
  import type { ContinuationDispatch } from "./goal-loop-dispatch.js";
49
50
  import type { AgentPhase, AgentStatus } from "./goal-agents-panel.js";
@@ -1881,6 +1882,10 @@ function heartbeatTick(): void {
1881
1882
  // windows are already covered precisely — busy mid-turn, pending
1882
1883
  // messages, scheduled timers — plus the audit-in-flight flag below.
1883
1884
  if (flags.completionAuditInFlight) return;
1885
+ // Increment audits intentionally leave the main session idle. Refiring
1886
+ // its continuation cannot start a turn and would eventually park healthy
1887
+ // detached work. Protect only the exact live dispatch, never an old claim.
1888
+ if (respecBuilderAuditInFlight(ctx.cwd)) return;
1884
1889
  noteActivity();
1885
1890
  flags.consecutiveStalls++;
1886
1891
  appendLedger(ctx.cwd, "heartbeat_refire", { nudgesSoFar: flags.heartbeatNudges, consecutiveStalls: flags.consecutiveStalls });
@@ -19,6 +19,8 @@ import { fileURLToPath } from "node:url";
19
19
  import {
20
20
  piGlaDir,
21
21
  appendLedger,
22
+ readState,
23
+ isPersistenceDegraded,
22
24
  stripThinkBlocks,
23
25
  captureGoalRevision,
24
26
  isRetriableInfraError,
@@ -1090,6 +1092,8 @@ export interface AuditJobHealthEntry {
1090
1092
  * `pid` beside it is a PARENT identity (or a reused pid) and re-verifying its
1091
1093
  * liveness before a reap would protect nothing. */
1092
1094
  provenFinished?: boolean;
1095
+ /** Unsettled durable claims still need this job's recovery evidence. */
1096
+ retainedForClaim?: boolean;
1093
1097
  }
1094
1098
 
1095
1099
  export interface AuditJobHealthReport {
@@ -1102,6 +1106,7 @@ export interface AuditJobHealthReport {
1102
1106
  bytes: number;
1103
1107
  cleanupCandidates: number;
1104
1108
  entries: AuditJobHealthEntry[];
1109
+ retentionBlocked?: string;
1105
1110
  }
1106
1111
 
1107
1112
  /** A deliberately conservative threshold: an auditor that is merely slow is
@@ -1145,6 +1150,10 @@ export function inspectAuditJobHealth(
1145
1150
  ): AuditJobHealthReport {
1146
1151
  const root = path.join(piGlaDir(cwd), "audit-jobs");
1147
1152
  const entries: AuditJobHealthEntry[] = [];
1153
+ const durable = readState(cwd);
1154
+ const retentionBlocked = isPersistenceDegraded() ? "durable claims could not be read safely" : undefined;
1155
+ const pendingIds = [durable.goal?.pendingCompletion?.attemptId, durable.loop?.builder?.audit?.attemptId]
1156
+ .filter((id): id is string => typeof id === "string" && id.length > 0);
1148
1157
  let dirs: Array<{ name: string; isDirectory: () => boolean }> = [];
1149
1158
  try {
1150
1159
  dirs = readdirSync(root, { withFileTypes: true });
@@ -1236,22 +1245,24 @@ export function inspectAuditJobHealth(
1236
1245
  }
1237
1246
  }
1238
1247
  }
1239
- entries.push({ attemptId: entry.name, dir, ageMs, bytes, status, ...(pid !== undefined ? { pid } : {}), ...(provenFinished ? { provenFinished: true } : {}), ...(reason ? { reason } : {}) });
1248
+ const retainedForClaim = pendingIds.some(id => entry.name === id || entry.name.startsWith(`${id}-`));
1249
+ entries.push({ attemptId: entry.name, dir, ageMs, bytes, status, ...(retainedForClaim ? { retainedForClaim: true } : {}), ...(pid !== undefined ? { pid } : {}), ...(provenFinished ? { provenFinished: true } : {}), ...(reason ? { reason } : {}) });
1240
1250
  }
1241
1251
  const live = entries.filter((entry) => entry.status === "live").length;
1242
1252
  const dead = entries.filter((entry) => entry.status === "dead").length;
1243
1253
  const ambiguous = entries.filter((entry) => entry.status === "ambiguous").length;
1244
1254
  const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
1245
- const cleanupCandidates = entries.filter((entry) => entry.status === "dead" && entry.ageMs >= maxAgeMs).length;
1246
- return { root, scannedAt: new Date(nowMs).toISOString(), total: entries.length, live, dead, ambiguous, bytes, cleanupCandidates, entries };
1255
+ const cleanupCandidates = retentionBlocked ? 0 : entries.filter((entry) => entry.status === "dead" && !entry.retainedForClaim && entry.ageMs >= maxAgeMs).length;
1256
+ return { root, scannedAt: new Date(nowMs).toISOString(), total: entries.length, live, dead, ambiguous, bytes, cleanupCandidates, entries, ...(retentionBlocked ? { retentionBlocked } : {}) };
1247
1257
  }
1248
1258
 
1249
1259
  /** Explicit, age-bounded cleanup for only proven-dead worker identities.
1250
1260
  * Ambiguous locks are intentionally left for operator inspection. */
1251
1261
  export function cleanupDeadAuditJobs(cwd: string, maxAgeMs = AUDIT_JOB_CLEANUP_MIN_AGE_MS, nowMs = Date.now()): AuditJobHealthReport {
1252
1262
  const report = inspectAuditJobHealth(cwd, nowMs, maxAgeMs);
1263
+ if (report.retentionBlocked) return report;
1253
1264
  for (const entry of report.entries) {
1254
- if (entry.status !== "dead" || entry.ageMs < maxAgeMs) continue;
1265
+ if (entry.status !== "dead" || entry.retainedForClaim || entry.ageMs < maxAgeMs) continue;
1255
1266
  // A dead entry WITH a pid always re-verifies liveness first (PID reuse
1256
1267
  // between scan and reap). A dead entry WITHOUT a pid is only reachable
1257
1268
  // via the no-lock+result path above — provably finished, no pid to
@@ -1,3 +1,4 @@
1
+ import { getRespecAuditLive, respecAuditStatus } from "./respec-builder-ui.js";
1
2
  /**
2
3
  * pi-goal-list-loop-audit — v0.9.0
3
4
  * extensions/goal-loop-display.ts
@@ -17,6 +18,7 @@ import { auditVerdictLabel, auditLifecycleLine, auditLifecycleProjection, auditP
17
18
 
18
19
  export { isMonitorGoal };
19
20
  import { HELD_ON_RESTORE, type LoopState } from "./goal-loop-forever.js";
21
+ import { respecCoverage } from "./respec-builder.js";
20
22
  import { normalizeFindingLead } from "./finding-lead.js";
21
23
  import { auditorSurfaceSuppressed } from "./loops/goal-auditor-surface.js";
22
24
 
@@ -460,6 +462,7 @@ export function buildDurableDeferDecisionLines(input: DurableDeferRecommendation
460
462
  export type DisplayColor = "accent" | "success" | "warning" | "error" | "muted" | "dim";
461
463
  export interface DisplayTheme {
462
464
  fg(color: DisplayColor, text: string): string;
465
+ bold?(text: string): string;
463
466
  }
464
467
  export const paint = (theme: DisplayTheme | undefined, color: DisplayColor, text: string): string => (theme ? theme.fg(color, text) : text);
465
468
 
@@ -998,7 +1001,20 @@ function auditorObservedPhase(audit: AuditDisplayProgress | null | undefined, ph
998
1001
  }
999
1002
  }
1000
1003
 
1001
- function auditorPhaseForDisplay(audit: AuditDisplayProgress | null | undefined, phase: AuditorDisplayPhase, live: boolean): string {
1004
+ /** An unfinished tool is a wait, not fresh evidence of forward progress.
1005
+ * Retain its observed clock while it is inside the dispatched timeout. */
1006
+ function auditorToolWait(audit: AuditDisplayProgress | null | undefined, phase: AuditorDisplayPhase, now: number): boolean {
1007
+ return phase === "running" && !!audit?.currentTool
1008
+ && (audit.phase === "running" || audit.phase === "tool_executing" || audit.phase === undefined)
1009
+ && typeof audit.currentToolStartedAt === "number" && Number.isFinite(audit.currentToolStartedAt)
1010
+ && audit.currentToolStartedAt <= now
1011
+ && (audit.lastActivityAt === undefined || Number.isFinite(audit.lastActivityAt) && audit.lastActivityAt <= now)
1012
+ && typeof audit.toolTimeoutMs === "number" && Number.isFinite(audit.toolTimeoutMs) && audit.toolTimeoutMs > 0
1013
+ && now - audit.currentToolStartedAt < audit.toolTimeoutMs;
1014
+ }
1015
+
1016
+ function auditorPhaseForDisplay(audit: AuditDisplayProgress | null | undefined, phase: AuditorDisplayPhase, live: boolean, now: number): string {
1017
+ if (!live && auditorToolWait(audit, phase, now)) return `waiting on ${sanitizeDisplayText(audit!.currentTool!)}`;
1002
1018
  // Once a worker timestamp exists, a stale tool snapshot is historical
1003
1019
  // context, not a claim that the detached process is still in that call.
1004
1020
  if (!live && phase === "running" && audit?.lastActivityAt !== undefined && audit.currentTool) {
@@ -1340,6 +1356,10 @@ export function loopCadenceCountdown(l: { minimumIterationIntervalMs?: number; l
1340
1356
  }
1341
1357
 
1342
1358
  function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, extras?: WidgetExtras, width?: number): string | undefined {
1359
+ if (state.loop?.builder && (state.loop.active || !state.goal)) {
1360
+ const l = state.loop, builder = l.builder!, coverage = respecCoverage(builder);
1361
+ return `glla: project ${paint(theme, builder.phase === "complete" ? "success" : !l.active ? "warning" : "accent", !l.active && builder.phase === "auditing" ? "audit held — /loop resume" : builder.phase)}${l.active || builder.phase === "complete" || builder.phase === "auditing" ? "" : " · held"} · increment ${builder.cycle} · verified ${coverage.verified}/${coverage.total} · remaining ${coverage.remaining} · blocked ${coverage.blocked}${l.active && builder.phase === "auditing" ? ` · ${respecAuditStatus(builder, now)}` : ""}`;
1362
+ }
1343
1363
  if (state.loop?.active) {
1344
1364
  const l = state.loop;
1345
1365
  // v0.26.1: surface the refire streak — a spinning supervisor is the
@@ -1393,10 +1413,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1393
1413
  // settling, workerless, and contradicted-starting claims are named by
1394
1414
  // their durable phase; any other real progress object speaks for itself.
1395
1415
  const durableLabel = durablePhaseOverride(g.pendingCompletion, audit, now);
1416
+ const toolWait = auditorToolWait(audit, phase, now);
1396
1417
  const observed = durableLabel
1418
+ ?? (toolWait ? `waiting on ${sanitizeDisplayText(audit!.currentTool!)}` : undefined)
1397
1419
  ?? (signals && phase === "running"
1398
- ? (auditorProgressPhaseLabel(audit) ?? auditorPhaseForDisplay(audit, phase, live))
1399
- : auditorPhaseForDisplay(audit, phase, live));
1420
+ ? (auditorProgressPhaseLabel(audit) ?? auditorPhaseForDisplay(audit, phase, live, now))
1421
+ : auditorPhaseForDisplay(audit, phase, live, now));
1400
1422
  // v0.35.15: leading phase glyph + draining activity meter — a glance
1401
1423
  // answers "is the audit alive?" without reading the sentence.
1402
1424
  const phaseText = `auditor ${auditorPhaseGlyph(phase)} ${observed}`;
@@ -1413,10 +1435,10 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1413
1435
  const quietAge = phase === "quiet" ? auditorActivityAge(audit, now) : undefined;
1414
1436
  const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
1415
1437
  const compactPrefix = extras?.compactAuditCard
1416
- ? `${paint(theme, color, durableLabel ?? (phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
1438
+ ? `${paint(theme, color, durableLabel ?? (toolWait ? `AUDIT TOOL WAIT · ${sanitizeDisplayText(audit!.currentTool!)} ${fmtElapsed(now - audit!.currentToolStartedAt!)} / ${fmtElapsed(audit!.toolTimeoutMs!)}` : phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
1417
1439
  : "";
1418
1440
  const activityAge = auditorActivityAge(audit, now);
1419
- const freshness = extras?.compactAuditCard && phase !== "quiet"
1441
+ const freshness = extras?.compactAuditCard && phase !== "quiet" && !toolWait
1420
1442
  ? `${activityAge !== undefined ? `activity ${fmtElapsed(activityAge)} ago` : "no worker activity yet"} · `
1421
1443
  : "";
1422
1444
  return `glla: ${compactPrefix}${freshness}${host} · ${label}${quietSuffix}${heldSuffix}`;
@@ -1783,6 +1805,7 @@ function waitingListLines(state: State, theme?: DisplayTheme, width?: number): s
1783
1805
  }
1784
1806
 
1785
1807
  function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] | undefined {
1808
+ if (state.loop?.builder && (state.loop.active || !state.goal)) return respecBuilderLines(state.loop, now, theme, width);
1786
1809
  if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
1787
1810
  if (state.loop && !state.loop.active && state.mainModelRecovery?.kind === "loop") return parkedLoopRecoveryLines(state.loop, state.mainModelRecovery, now, theme, width, extras?.mainModelFallbacks);
1788
1811
  if (!state.goal && state.mainModelRecovery) return standaloneRecoveryLines(state.mainModelRecovery, now, theme, width, extras?.mainModelFallbacks);
@@ -1910,8 +1933,8 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
1910
1933
  // on ("reading source…" / "writing report…"); the coarse label otherwise.
1911
1934
  const signals = extras?.auditorProgressSignals !== false;
1912
1935
  const phaseLabel = signals && phase === "running"
1913
- ? (auditorProgressPhaseLabel(audit) ?? auditorPhaseForDisplay(audit, phase, phaseLive))
1914
- : auditorPhaseForDisplay(audit, phase, phaseLive);
1936
+ ? (auditorProgressPhaseLabel(audit) ?? auditorPhaseForDisplay(audit, phase, phaseLive, now))
1937
+ : auditorPhaseForDisplay(audit, phase, phaseLive, now);
1915
1938
  // v0.38.99: the durable lifecycle names the state whenever in-process
1916
1939
  // progress cannot (see durablePhaseOverride). The coarse display phase only
1917
1940
  // speaks for a claim a LIVE worker is driving.
@@ -1953,7 +1976,7 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
1953
1976
  // ONLY here (it moved out of the tail) so the card keeps its
1954
1977
  // one-current-observation rule.
1955
1978
  let toolSeg: string | undefined;
1956
- if (phase === "running" && phaseLive && audit?.currentTool) {
1979
+ if (phase === "running" && (phaseLive || auditorToolWait(audit, phase, now)) && audit?.currentTool) {
1957
1980
  const target = auditorToolTarget(audit.currentToolArgs);
1958
1981
  const duration = audit.currentToolStartedAt !== undefined && Number.isFinite(audit.currentToolStartedAt)
1959
1982
  ? ` · ${fmtElapsed(now - audit.currentToolStartedAt)}`
@@ -2177,11 +2200,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2177
2200
  const age = auditorActivityAge(audit, now);
2178
2201
  const facts = [finished !== undefined ? `${finished} calls finished` : undefined,
2179
2202
  age !== undefined ? `activity ${fmtElapsed(age)} ago` : "no activity yet"].filter(Boolean);
2203
+ const toolWait = auditorToolWait(audit, phase, now);
2180
2204
  const action = block.lead.find(line => line.startsWith("│ next:"));
2181
2205
  // No history or model plaques in the glance card: Pi clips its tail.
2182
2206
  // Keep state, real evidence and the user action ahead of all details.
2183
- const phaseLine = needsRecovery ? (phase === "quiet" ? `├─ ${paint(theme, "warning", "Audit quiet — may be stuck")}` : block.lead[0]!)
2184
- : `├─ ${paint(theme, "accent", phase === "running" ? `Audit running · ${(extras?.auditorProgressSignals !== false ? auditorProgressPhaseLabel(audit) : undefined) ?? auditorPhaseForDisplay(audit, phase, auditorHasLiveEvidence(audit, phase, now))} · detached worker` : phase === "queued" ? "Audit starting · detached worker" : "Audit review pending · detached worker")}`;
2207
+ const phaseLine = toolWait ? `├─ ${paint(theme, "accent", `Waiting on ${sanitizeDisplayText(audit!.currentTool!)}${audit?.round === 2 ? " · second audit pass" : ""} · detached auditor`)}` : needsRecovery ? (phase === "quiet" ? `├─ ${paint(theme, "warning", "Audit quiet — may be stuck")}` : block.lead[0]!)
2208
+ : `├─ ${paint(theme, "accent", phase === "running" ? `Audit running · ${(extras?.auditorProgressSignals !== false ? auditorProgressPhaseLabel(audit) : undefined) ?? auditorPhaseForDisplay(audit, phase, auditorHasLiveEvidence(audit, phase, now), now)} · detached worker` : phase === "queued" ? "Audit starting · detached worker" : "Audit review pending · detached worker")}`;
2185
2209
  const toolLine = block.lead.find(line => /^│ (?:last )?tool:/.test(line));
2186
2210
  const modelRef = auditorCardModelRef(audit, g.pendingCompletion);
2187
2211
  const compactToolLine = modelRef ? toolLine?.split(` · ${modelRef}`)[0] : toolLine;
@@ -2189,7 +2213,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2189
2213
  ...(facts.length ? [`│ ${facts.join(" · ")}`] : []),
2190
2214
  ...(elapsed !== undefined ? [`│ audit elapsed ${fmtElapsed(elapsed)}`] : []),
2191
2215
  ...(compactToolLine ? [compactToolLine.startsWith("│ tool:") ? compactToolLine : `│ last tool: ${lastAuditorTool(audit) ?? audit?.currentTool}`] : []),
2192
- ...(needsRecovery ? (action ? [action] : block.lead.slice(1)) : [`│ No action needed — review applies automatically`]),
2216
+ ...(needsRecovery ? (action ? [action] : block.lead.slice(1)) : [toolWait ? `│ No tool completion yet — timeout handling is automatic` : `│ No action needed — review applies automatically`]),
2193
2217
  `└─ /goal status for full audit details`];
2194
2218
  }
2195
2219
  lines.push(...block.lead);
@@ -2623,6 +2647,35 @@ function completedGoalLines(g: Goal, now: number, theme?: DisplayTheme, width?:
2623
2647
  return [`${paint(theme, "dim", "─")} ${paint(theme, "dim", `${outcome} · ${truncate(recap, objBudget)} · ${tail}`)}`];
2624
2648
  }
2625
2649
 
2650
+ function respecBuilderLines(loop: LoopState, now: number, theme?: DisplayTheme, width?: number): string[] {
2651
+ const builder = loop.builder!;
2652
+ const coverage = respecCoverage(builder);
2653
+ const tone = builder.phase === "complete" ? "success" : !loop.active ? "warning" : "accent";
2654
+ const phase = !loop.active && builder.phase === "auditing" ? "Audit held" : builder.phase === "planning" ? "Planning" : builder.phase[0]!.toUpperCase() + builder.phase.slice(1);
2655
+ const current = builder.tasks.find(t => t.status === "pending") ?? builder.tasks.at(-1);
2656
+ const blocker = builder.requirements.find(r => r.status === "blocked");
2657
+ const evidence = [...builder.requirements].reverse().find(r => r.evidence)?.evidence?.report ?? builder.history?.at(-1)?.report;
2658
+ const action = builder.phase === "complete" ? "All adopted requirements independently verified" : !loop.active ? "Work held · /loop status · /loop resume when ready" : builder.phase === "auditing" ? "Independent verification pending · no action needed" : "Work continues · /loop pause · /loop refine";
2659
+ const narrowAction = builder.phase === "complete" ? `All ${coverage.total} requirements verified` : !loop.active ? "/loop resume when ready · /loop status" : builder.phase === "auditing" ? "Independent audit pending · no action" : "Work continues · /loop pause";
2660
+ const progress = loop.active && builder.phase === "auditing" ? getRespecAuditLive(builder) : undefined;
2661
+ const auditLines = loop.active && builder.phase === "auditing" ? progress ? [
2662
+ `auditor: ${progress.phase} · ${fmtElapsed(now - progress.startedAt)} · ${sanitizeDisplayText(progress.model ?? "model pending")}`,
2663
+ `last activity: ${progress.lastActivityAt !== undefined ? `${fmtElapsed(now - progress.lastActivityAt)} ago` : "not observed"}${progress.activity ? ` · ${sanitizeDisplayText(progress.activity)}` : ""}${progress.retryAt !== undefined ? ` · retry in ${fmtElapsed(Math.max(0, progress.retryAt - now))}` : ""}`,
2664
+ ] : ["auditor: waiting for dispatch · no live worker observed"] : [];
2665
+ const title = paint(theme, tone, `Project · ${phase}`);
2666
+ return [
2667
+ `${theme?.bold ? theme.bold(title) : title}${loop.active || builder.phase === "complete" || builder.phase === "auditing" ? "" : " · held"} · increment ${builder.cycle}`,
2668
+ `├─ ${truncate(sanitizeDisplayText(builder.vision), budgetFor(width, 3, 75))}`,
2669
+ `├─ ${paint(theme, coverage.verified > 0 ? "success" : "dim", `verified ${coverage.verified}/${coverage.total}`)} · ${paint(theme, coverage.remaining > 0 ? "warning" : "dim", `remaining ${coverage.remaining} · blocked ${coverage.blocked}`)}`,
2670
+ ...(current ? [`├─ ${builder.phase === "auditing" ? "claim" : "task"}: ${truncate(sanitizeDisplayText(current.text), budgetFor(width, 10, 70))}`] : []),
2671
+ ...auditLines.map(line => `├─ ${paint(theme, progress?.phase === "retrying" ? "warning" : "accent", truncate(line, budgetFor(width, 3, 90)))}`),
2672
+ ...(!loop.active && loop.stopReason ? [`├─ ${paint(theme, "warning", `held: ${truncate(sanitizeDisplayText(loop.stopReason), budgetFor(width, 9, 90))}`)}`] : []),
2673
+ ...(blocker ? [`├─ ${paint(theme, "warning", "blocked")}: ${truncate(sanitizeDisplayText(blocker.blockedReason ?? "reason missing"), budgetFor(width, 12, 70))}`] : []),
2674
+ ...(evidence ? [`├─ ${paint(theme, "dim", `evidence: ${truncate(sanitizeDisplayText(evidence), budgetFor(width, 13, 65))}`)}`] : []),
2675
+ `└─ ${paint(theme, tone, width && width < 60 ? narrowAction : action)}`,
2676
+ ];
2677
+ }
2678
+
2626
2679
  function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
2627
2680
  // v0.33.0: slim loop card — header icon names the kind (∞ metricless,
2628
2681
  // ↓/↑ metric), all state folds into middot segments; line 2 is the live
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
14
14
  import { existsSync, readFileSync, statSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { piGlaDir } from "./goal-loop-core.js";
17
+ import type { RespecBuilderState } from "./respec-builder.js";
17
18
 
18
19
  export type LoopDirection = "min" | "max";
19
20
 
@@ -167,6 +168,8 @@ export interface LoopState {
167
168
  specFile?: string;
168
169
  /** Missing-spec respec bootstrap is drafting, not reconciliation. */
169
170
  respecPhase?: "draft" | "reconcile";
171
+ /** Intended-project requirements and independently audited build cycles. */
172
+ builder?: RespecBuilderState;
170
173
  /** Sticky draft handoff: the agent emitted [RESPEC DRAFT COMPLETE] on
171
174
  * some earlier turn. Reconciliation still waits for a structurally
172
175
  * complete spec; cleared on handoff. */