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 +83 -0
- package/README.md +36 -1
- package/docs/INDEX.md +1 -1
- package/docs/RUNTIME-INVENTORY.md +11 -11
- package/extensions/continuous-supervision.ts +6 -0
- package/extensions/goal-commands.ts +3 -2
- package/extensions/goal-compactor.ts +2 -0
- package/extensions/goal-heartbeat.ts +5 -0
- package/extensions/goal-loop-auditor-process.ts +15 -4
- package/extensions/goal-loop-display.ts +64 -11
- package/extensions/goal-loop-forever.ts +3 -0
- package/extensions/goal-loop.ts +80 -72
- package/extensions/loops/goal-activation.ts +4 -2
- package/extensions/loops/goal-auditor-hooks.ts +2 -1
- package/extensions/loops/goal-orchestrator.ts +2 -1
- package/extensions/loops/goal-tools.ts +26 -1
- package/extensions/loops/goal.ts +2 -0
- package/extensions/respec-builder-audit.ts +21 -0
- package/extensions/respec-builder-runtime.ts +274 -0
- package/extensions/respec-builder-ui.ts +56 -0
- package/extensions/respec-builder.ts +128 -0
- package/extensions/summary-renderer.ts +90 -0
- package/package.json +1 -1
- package/prompts/goal-loop-respec-builder.md +59 -0
- package/scripts/generate-inventory.mjs +1 -1
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
|
-
|
|
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.
|
|
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:
|
|
6
|
-
- Extension/script lines:
|
|
7
|
-
- Top-level test files:
|
|
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) |
|
|
18
|
-
| [extensions/loops/goal-activation.ts](../extensions/loops/goal-activation.ts) |
|
|
19
|
-
| [extensions/goal-commands.ts](../extensions/goal-commands.ts) |
|
|
20
|
-
| [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) |
|
|
21
|
-
| [extensions/goal-loop-auditor-process.ts](../extensions/goal-loop-auditor-process.ts) |
|
|
22
|
-
| [extensions/loops/goal-auditor-hooks.ts](../extensions/loops/goal-auditor-hooks.ts) |
|
|
23
|
-
| [extensions/goal-heartbeat.ts](../extensions/goal-heartbeat.ts) |
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|