pi-goal-list-loop-audit 0.38.44 → 0.38.46
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 +24 -0
- package/docs/INDEX.md +1 -1
- package/extensions/approval-render-store.ts +17 -2
- package/extensions/completion-summary.ts +224 -26
- package/extensions/glla-update-check.ts +37 -26
- package/extensions/glla-version.ts +61 -14
- package/extensions/goal-commands.ts +0 -1
- package/extensions/goal-loop-display.ts +5 -2
- package/extensions/goal-recovery.ts +5 -1
- package/extensions/loops/goal-orchestrator.ts +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.46 — rich Antigravity-style terminal summaries (2026-09-11)
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Rich terminal voice (field 20260911_003839/003903/003907):** the terminal approval render is now `## Done — outcome` + `### Key Findings & Remediation` (numbered, bold lead + code refs) + `### Verification Summary` (`| Check | Status | Details |` table with Tests PASS/FAIL and the audit verdict row) + `### Next`, closed by the pinned approval/record trailer. The flat six-bullet card is gone from chat and transcript.
|
|
8
|
+
- **Rich archive too:** `archiveCurrentGoal` appends a `## Terminal summary` section built from the same parts over the verbatim six-label machine record; aborted records wear a `## Aborted —` headline, never `Done`.
|
|
9
|
+
- **Scope held:** progress cards, status line, and external notifies keep their compact projections; the legacy `buildApprovalChatLines` helper is untouched.
|
|
10
|
+
- **Budgets (verbose by owner choice):** findings cap 8, values/table cells 200 chars, Next cap 4; table pipes escaped; Tests status honest (PASS only on `pass` without nonzero `fail`, else FAIL/REPORTED); empty sections omitted, never invented.
|
|
11
|
+
- **Pins:** `tests/rich-terminal-summary.test.ts` (8) — headline, numbering, table, FAIL detection, pipe escaping, stale-Next behavior, trailer contract, no-audit path, archive headlines; terminal-render + communication suites migrated to the rich shape.
|
|
12
|
+
|
|
13
|
+
## 0.38.45 — full-project audit pass: 16 fixes across version, summary, and approval paths (2026-09-10)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Update-check spawn hardening (HIGH):** `refreshUpdateCheck` now subscribes to async `error` (a missing/broken npm crashed the host unhandled), detaches via `unref`, ignores stderr/stdin, and caches only version-shaped stdout tokens — registry noise can no longer poison the sidecar.
|
|
18
|
+
- **`/glla version` honesty (HIGH):** no staleness claim when the running version is unreadable (previously printed a false "up to date"); both surfaces share one strict sidecar reader so the status tail and the command cannot disagree.
|
|
19
|
+
- **Recorded-facts Next survives (HIGH):** `withoutStaleNext` exempts the concrete "review the durable record at …" fallback Next that the `/review/i` clause ate, leaving fallback approval chats actionless.
|
|
20
|
+
- **No per-contact spawn storm:** the TTL skip uses the raw sidecar read, so a future-dated cache (clock skew) suppresses the spawn instead of causing one per command contact.
|
|
21
|
+
- **Approval re-render after delivery:** delivered history dedups exact duplicates only (repeated settlements never double-notify); a genuine re-approval with new verdict lines queues fresh — content, not goalId, decides.
|
|
22
|
+
- **SessionDir-aware sidecar path:** `updateCheckPath` resolves via `resolveGllaStateDir` instead of hardcoding `cwd/.pi-glla`.
|
|
23
|
+
- **String-safety:** `clipSummaryValue` is code-point-safe (no more split surrogate pairs); `chatSafeDetailValue` converges nested machine-path groups to a fixpoint; label segmentation uses last-occurrence search so a label named inside a value no longer steals later segments.
|
|
24
|
+
- **Verdict tally uses its clock:** `auditorVerdictTally(now)` suppresses future `lastAt` instead of printing "0s ago"; dead `void now` and dead `readUpdateCheck` import removed; `recovery-resume.json` documented as a reserved hook with no current producer.
|
|
25
|
+
- **Pins:** spawn-error swallow, token validation, future-cache skip, prerelease compare, unknown-version silence, recorded-facts survival, nested-husk convergence, surrogate safety, last-restatement-wins, delivery-then-reapproval, paused-branch tail, future-verdict silence.
|
|
26
|
+
|
|
3
27
|
## 0.38.44 — stale-version surfacing: running version in the status line (2026-09-10)
|
|
4
28
|
|
|
5
29
|
### Fixed
|
package/docs/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
|
|
|
18
18
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
19
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
20
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14–v0.38.
|
|
21
|
+
v0.35.14–v0.38.46 continue through the supervisor freeze (`/glla pause`),
|
|
22
22
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
23
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
24
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -90,13 +90,28 @@ export function persistApprovalRender(cwd: string, render: {
|
|
|
90
90
|
}): boolean {
|
|
91
91
|
const at = nowIso();
|
|
92
92
|
const existing = readRenders(cwd);
|
|
93
|
-
|
|
93
|
+
// v0.38.30 truncation (also code-point-safe): compare what WOULD be
|
|
94
|
+
// stored, so an identical re-persist of long lines still dedups.
|
|
95
|
+
const incomingLines = render.chatLines.slice(0, MAX_RENDER_CHAT_LINES)
|
|
96
|
+
.map((line) => [...line].slice(0, MAX_RENDER_LINE_CHARS).join(""));
|
|
97
|
+
// Dedup has two layers. (1) An UNDELIVERED entry for the goal means a
|
|
98
|
+
// render is already queued — a second persist before delivery changes
|
|
99
|
+
// nothing. (2) v0.38.45 audit: delivered history dedups only EXACT
|
|
100
|
+
// duplicates (a repeated settlement re-persisting identical lines must
|
|
101
|
+
// not double-notify) — but a re-approval carries new verdict lines, and
|
|
102
|
+
// dropping those silently hid an archive-failure retry's chat summary
|
|
103
|
+
// while the caller believed it queued. Content, not goalId, decides.
|
|
104
|
+
if (existing.some((entry) => !entry.deliveredAt && entry.goalId === render.goalId)) return true;
|
|
105
|
+
const sameLines = (a: string[], b: string[]): boolean =>
|
|
106
|
+
a.length === b.length && a.every((line, i) => line === b[i]);
|
|
107
|
+
if (existing.some((entry) =>
|
|
108
|
+
entry.deliveredAt && entry.goalId === render.goalId && sameLines(entry.chatLines, incomingLines))) return true;
|
|
94
109
|
const entry: PendingApprovalRender = {
|
|
95
110
|
goalId: render.goalId,
|
|
96
111
|
// v0.38.30 audit: code-point truncation (char slice split surrogate
|
|
97
112
|
// pairs) + bounded chat lines (the 20-entry cap never bound bytes).
|
|
98
113
|
objective: [...render.objective].slice(0, 300).join(""),
|
|
99
|
-
chatLines:
|
|
114
|
+
chatLines: incomingLines,
|
|
100
115
|
createdAt: at,
|
|
101
116
|
};
|
|
102
117
|
// The cap trims DELIVERED history only: undelivered renders are never
|
|
@@ -90,32 +90,53 @@ export function isUsefulCompletionSummary(text: string | undefined): boolean {
|
|
|
90
90
|
export function clipSummaryValue(value: string, limit: number): string {
|
|
91
91
|
const clean = value.replace(/\s+/g, " ").trim();
|
|
92
92
|
const capped = Number.isFinite(limit) ? Math.max(8, Math.floor(limit)) : 72;
|
|
93
|
-
|
|
94
|
-
const
|
|
93
|
+
// v0.38.45 audit: code-point-safe — UTF-16 slice split surrogate pairs.
|
|
94
|
+
const units = [...clean];
|
|
95
|
+
if (units.length <= capped) return clean;
|
|
96
|
+
const headUnits = units.slice(0, capped - 1);
|
|
97
|
+
const head = headUnits.join("");
|
|
95
98
|
const floor = Math.max(16, Math.floor((capped - 1) * 0.4));
|
|
96
99
|
let boundary = -1;
|
|
97
|
-
|
|
98
|
-
if (/[,;:·—–(+[\[]
|
|
99
|
-
}
|
|
100
|
+
headUnits.forEach((unit, i) => {
|
|
101
|
+
if (/[,;:·—–(+[\[]/u.test(unit)) boundary = i;
|
|
102
|
+
});
|
|
100
103
|
if (boundary >= floor) {
|
|
101
|
-
const cut =
|
|
102
|
-
if (cut.length >= Math.min(floor, 16)) return `${cut}…`;
|
|
104
|
+
const cut = headUnits.slice(0, boundary).join("").replace(/[,;:·—–(+[\[\s]+$/u, "");
|
|
105
|
+
if ([...cut].length >= Math.min(floor, 16)) return `${cut}…`;
|
|
103
106
|
}
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
let space = -1;
|
|
108
|
+
headUnits.forEach((unit, i) => {
|
|
109
|
+
if (unit === " ") space = i;
|
|
110
|
+
});
|
|
111
|
+
const kept = (space > capped / 2 ? headUnits.slice(0, space).join("") : head)
|
|
106
112
|
.trimEnd()
|
|
107
113
|
.replace(/[,;:·—–(+\[]$/u, "");
|
|
108
114
|
return `${kept}…`;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/** v0.38.45 audit: LAST-occurrence label search — first-occurrence
|
|
118
|
+
* indexOf let a label named inside an earlier label's VALUE steal the
|
|
119
|
+
* segmentation of every later label ("Outcome: see Tests: x. Tests: y"
|
|
120
|
+
* read Outcome as "see"). The last restatement wins, which also favors
|
|
121
|
+
* the final wording when an agent echoes the skeleton twice. Residual
|
|
122
|
+
* ambiguity (a single skeleton whose value names a later label once)
|
|
123
|
+
* is accepted: values keep their text, only the boundary moves. */
|
|
124
|
+
function labelPositions(lower: string): Array<{ label: string; start: number }> {
|
|
125
|
+
const positions: Array<{ label: string; start: number }> = [];
|
|
126
|
+
for (const label of COMPLETION_SUMMARY_LABELS) {
|
|
127
|
+
const start = lower.lastIndexOf(label.toLowerCase());
|
|
128
|
+
if (start < 0) continue;
|
|
129
|
+
positions.push({ label, start });
|
|
130
|
+
}
|
|
131
|
+
return positions;
|
|
132
|
+
}
|
|
133
|
+
|
|
111
134
|
export function compactCompletionSummary(text: string | undefined, maxValueLength = 72): string {
|
|
112
135
|
const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
|
|
113
136
|
if (!source) return "not recorded";
|
|
114
137
|
const lower = source.toLowerCase();
|
|
115
138
|
const limit = Number.isFinite(maxValueLength) ? Math.max(8, Math.floor(maxValueLength)) : 72;
|
|
116
|
-
const positions =
|
|
117
|
-
.map((label) => ({ label, start: lower.indexOf(label.toLowerCase()) }))
|
|
118
|
-
.filter((entry) => entry.start >= 0);
|
|
139
|
+
const positions = labelPositions(lower);
|
|
119
140
|
const parts = COMPLETION_SUMMARY_LABELS.map((label) => {
|
|
120
141
|
const current = positions.find((entry) => entry.label === label);
|
|
121
142
|
const name = label.slice(0, -1);
|
|
@@ -159,7 +180,11 @@ export interface HumanCompletionBrief {
|
|
|
159
180
|
* the durable archive keeps the full text. Falls back to the original
|
|
160
181
|
* when stripping would empty the value. */
|
|
161
182
|
export function chatSafeDetailValue(value: string): string {
|
|
162
|
-
|
|
183
|
+
// v0.38.45 audit: loop the innermost-group strip to a fixpoint
|
|
184
|
+
// (bounded) — a single pass left nested husks like "(log )" behind.
|
|
185
|
+
let withoutGroups = value;
|
|
186
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
187
|
+
const next = withoutGroups.replace(/\([^()]*\)/g, (group) => {
|
|
163
188
|
// A parenthesized group that carries nothing but stripped tokens and
|
|
164
189
|
// receipt words is machine packaging — drop the whole group so no
|
|
165
190
|
// `( tarball )` husk survives. Groups with real words stay verbatim.
|
|
@@ -169,7 +194,10 @@ export function chatSafeDetailValue(value: string): string {
|
|
|
169
194
|
.replace(/\S+\.tgz\b/g, "")
|
|
170
195
|
.replace(/\btarballs?\b|\blogs?\b/gi, "");
|
|
171
196
|
return /[a-z]/i.test(inner) ? group : "";
|
|
172
|
-
|
|
197
|
+
});
|
|
198
|
+
if (next === withoutGroups) break;
|
|
199
|
+
withoutGroups = next;
|
|
200
|
+
}
|
|
173
201
|
const stripped = withoutGroups
|
|
174
202
|
.replace(/(?:\/var)?\/tmp\/\S+/g, "")
|
|
175
203
|
.replace(/\btarballs?\s+\S+\.tgz\b/gi, "")
|
|
@@ -215,6 +243,10 @@ export function humanCompletionBrief(
|
|
|
215
243
|
* approval`, …) still drop, but the first concrete action survives as
|
|
216
244
|
* the closing bullet. The full six-label record stays in the archive. */
|
|
217
245
|
const STALE_NEXT_PATTERN = /auditor|verdict|approv|audit|settl|review/i;
|
|
246
|
+
/** The recorded-facts fallback Next is concrete (`review the durable
|
|
247
|
+
* record at …`) and must survive the stale-Next filter (v0.38.45 audit:
|
|
248
|
+
* /review/i ate it, leaving fallback approval chats with no next action). */
|
|
249
|
+
const RECORDED_FACTS_NEXT_PATTERN = /review the durable/i;
|
|
218
250
|
export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
219
251
|
const kept: string[] = [];
|
|
220
252
|
let actionKept = false;
|
|
@@ -223,6 +255,12 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
|
223
255
|
kept.push(detail);
|
|
224
256
|
continue;
|
|
225
257
|
}
|
|
258
|
+
if (RECORDED_FACTS_NEXT_PATTERN.test(detail)) {
|
|
259
|
+
if (actionKept) continue;
|
|
260
|
+
actionKept = true;
|
|
261
|
+
kept.push(detail);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
226
264
|
if (STALE_NEXT_PATTERN.test(detail)) continue;
|
|
227
265
|
if (actionKept) continue;
|
|
228
266
|
actionKept = true;
|
|
@@ -231,6 +269,148 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
|
231
269
|
return kept;
|
|
232
270
|
}
|
|
233
271
|
|
|
272
|
+
/** Rich terminal voice (field 20260911_003839/003903/003907 — the
|
|
273
|
+
* Antigravity close): section headers, numbered findings with bold
|
|
274
|
+
* leads + code refs, and a verification table, instead of the flat
|
|
275
|
+
* six-bullet card. Scope is the terminal render only (chat +
|
|
276
|
+
* transcript + archive human layer); progress cards, status line, and
|
|
277
|
+
* external notifies keep their compact projections. Verbose by owner
|
|
278
|
+
* choice: findings cap 8, values 200, table cells 200. */
|
|
279
|
+
export const RICH_FINDINGS_CAP = 8;
|
|
280
|
+
export const RICH_VALUE_BUDGET = 200;
|
|
281
|
+
export const RICH_NEXT_CAP = 4;
|
|
282
|
+
|
|
283
|
+
export interface RichTerminalParts {
|
|
284
|
+
headline: string;
|
|
285
|
+
findingLines: string[];
|
|
286
|
+
tableLines: string[];
|
|
287
|
+
nextLines: string[];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function escapeTableCell(value: string): string {
|
|
291
|
+
return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function testsRowStatus(value: string): string {
|
|
295
|
+
const fail = /(\d+)\s*fail/i.exec(value);
|
|
296
|
+
if (fail && Number.parseInt(fail[1]!, 10) > 0) return "FAIL";
|
|
297
|
+
if (/pass/i.test(value)) return "PASS";
|
|
298
|
+
return "REPORTED";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function auditRowStatus(history: Goal["auditHistory"]): string {
|
|
302
|
+
const entries = Array.isArray(history) ? history : [];
|
|
303
|
+
const last = entries[entries.length - 1];
|
|
304
|
+
if (!last) return "NO VERDICT";
|
|
305
|
+
if (last.approved) return `APPROVED \u00d7${entries.length}`;
|
|
306
|
+
if (last.disapproved) return `DISAPPROVED \u00d7${entries.length}`;
|
|
307
|
+
if (last.impossible) return "IMPOSSIBLE";
|
|
308
|
+
return "NO VERDICT";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Split a stale-filtered `Label: value` detail into a bold lead + body. */
|
|
312
|
+
function leadBody(detail: string): { lead: string; body: string } {
|
|
313
|
+
const separator = detail.indexOf(":");
|
|
314
|
+
if (separator < 0) return { lead: "Note", body: detail };
|
|
315
|
+
return { lead: detail.slice(0, separator).trim() || "Note", body: detail.slice(separator + 1).trim() };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Partition informing details into findings / Tests / next buckets. */
|
|
319
|
+
export function partitionRichDetails(details: string[]): { findings: string[]; tests: string[]; next: string[] } {
|
|
320
|
+
const findings: string[] = [];
|
|
321
|
+
const tests: string[] = [];
|
|
322
|
+
const next: string[] = [];
|
|
323
|
+
for (const detail of details) {
|
|
324
|
+
if (/^\s*Tests\s*:/i.test(detail)) tests.push(detail);
|
|
325
|
+
else if (/^\s*(Next|Unresolved|Left out)\s*:/i.test(detail)) next.push(detail);
|
|
326
|
+
else findings.push(detail);
|
|
327
|
+
}
|
|
328
|
+
return { findings, tests, next };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Build the section parts shared by chat, transcript, and archive. */
|
|
332
|
+
export function buildRichTerminalParts(args: {
|
|
333
|
+
outcome: string;
|
|
334
|
+
details: string[];
|
|
335
|
+
countsLine: string;
|
|
336
|
+
auditHistory?: Goal["auditHistory"];
|
|
337
|
+
}): RichTerminalParts {
|
|
338
|
+
const { findings, tests, next } = partitionRichDetails(args.details);
|
|
339
|
+
const findingLines = findings.slice(0, RICH_FINDINGS_CAP).map((detail, i) => {
|
|
340
|
+
const { lead, body } = leadBody(detail);
|
|
341
|
+
return `${i + 1}. **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
|
|
342
|
+
});
|
|
343
|
+
const tableRows: string[] = [];
|
|
344
|
+
for (const detail of tests.slice(0, 2)) {
|
|
345
|
+
const { body } = leadBody(detail);
|
|
346
|
+
tableRows.push(`| Tests | ${testsRowStatus(body)} | ${escapeTableCell(clipSummaryValue(body, RICH_VALUE_BUDGET))} |`);
|
|
347
|
+
}
|
|
348
|
+
const auditStatus = auditRowStatus(args.auditHistory);
|
|
349
|
+
if (auditStatus !== "NO VERDICT") {
|
|
350
|
+
const auditBody = args.countsLine.replace(/^\u2014\s*/, "").replace(/\.\s*$/, "");
|
|
351
|
+
tableRows.push(`| Audit | ${auditStatus} | ${escapeTableCell(clipSummaryValue(auditBody, RICH_VALUE_BUDGET))} |`);
|
|
352
|
+
}
|
|
353
|
+
const tableLines = tableRows.length > 0
|
|
354
|
+
? ["| Check | Status | Details |", "| --- | --- | --- |", ...tableRows]
|
|
355
|
+
: [];
|
|
356
|
+
const nextLines = next.slice(0, RICH_NEXT_CAP).map((detail) => {
|
|
357
|
+
const { lead, body } = leadBody(detail);
|
|
358
|
+
return `- **${lead}** \u2014 ${clipSummaryValue(body, RICH_VALUE_BUDGET)}`;
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
headline: `## Done \u2014 ${args.outcome}`,
|
|
362
|
+
findingLines,
|
|
363
|
+
tableLines,
|
|
364
|
+
nextLines,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Compose parts + headline + section headers into markdown lines. */
|
|
369
|
+
export function composeRichTerminalLines(parts: RichTerminalParts, opts?: { headline?: string }): string[] {
|
|
370
|
+
const lines = [opts?.headline ?? parts.headline, ""];
|
|
371
|
+
if (parts.findingLines.length > 0) {
|
|
372
|
+
lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
|
|
373
|
+
}
|
|
374
|
+
if (parts.tableLines.length > 0) {
|
|
375
|
+
lines.push("### Verification Summary", ...parts.tableLines, "");
|
|
376
|
+
}
|
|
377
|
+
if (parts.nextLines.length > 0) {
|
|
378
|
+
lines.push("### Next", ...parts.nextLines, "");
|
|
379
|
+
}
|
|
380
|
+
return lines;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Rich human layer for the durable archive (owner: the archive IS for
|
|
384
|
+
* humans). Same sections as chat, composed from durable goal state — the
|
|
385
|
+
* raw six-label recap stays verbatim in `## Completion summary` as the
|
|
386
|
+
* machine layer. Headline follows terminal status; aborted records never
|
|
387
|
+
* wear a `Done` headline. */
|
|
388
|
+
export function buildRichArchiveSection(goal: Goal, status: Status, archivePath: string): string[] {
|
|
389
|
+
const facts: CompletionSummaryFacts = { goal, status, archivePath };
|
|
390
|
+
const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
|
|
391
|
+
const brief = humanCompletionBrief(summary, 140, RICH_VALUE_BUDGET);
|
|
392
|
+
const history = goal.auditHistory ?? [];
|
|
393
|
+
const latest = history[history.length - 1];
|
|
394
|
+
const approval = latest
|
|
395
|
+
? `\u2014 auditor ${latest.approved ? "approved" : latest.disapproved ? "disapproved" : latest.impossible ? "impossible" : "left no verdict"}.`
|
|
396
|
+
: "\u2014 completed without a recorded auditor verdict.";
|
|
397
|
+
const countsLine = buildAuditCountsLine(goal);
|
|
398
|
+
const parts = buildRichTerminalParts({
|
|
399
|
+
outcome: brief.outcome,
|
|
400
|
+
details: withoutStaleNext(brief.details),
|
|
401
|
+
countsLine,
|
|
402
|
+
auditHistory: history,
|
|
403
|
+
});
|
|
404
|
+
return [
|
|
405
|
+
...composeRichTerminalLines(parts, {
|
|
406
|
+
headline: status === "complete" ? parts.headline : `## Aborted \u2014 ${brief.outcome}`,
|
|
407
|
+
}),
|
|
408
|
+
trailerBullet(stripApprovalModel(approval)),
|
|
409
|
+
trailerBullet(countsLine),
|
|
410
|
+
trailerBullet(`\u2014 record: ${archivePath}`),
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
234
414
|
/** v0.38.20: the approval chat notify — outcome first, all bounded
|
|
235
415
|
* informing details (the full record lives in the archive and the
|
|
236
416
|
* transcript notice), then the approval trailer and the record pointer.
|
|
@@ -344,15 +524,23 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
344
524
|
};
|
|
345
525
|
const candidate = input.completionSummary ?? input.goal.completionSummary;
|
|
346
526
|
const recap = compactTerminalCompletionSummary(facts, candidate);
|
|
347
|
-
|
|
527
|
+
// Rich voice (field 20260911_*): the chat/transcript render uses a
|
|
528
|
+
// verbose brief (200-char values) while the outcome headline keeps
|
|
529
|
+
// the 140-char budget. Filler drops via the same briefValueContent
|
|
530
|
+
// filter; absent stays absent.
|
|
531
|
+
const richBrief = humanCompletionBrief(
|
|
532
|
+
resolveCompletionSummary(facts, candidate).summary,
|
|
533
|
+
140,
|
|
534
|
+
RICH_VALUE_BUDGET,
|
|
535
|
+
);
|
|
348
536
|
// v0.38.37 (audit 2026-09-08): the deliberate non-do comes from the
|
|
349
537
|
// agent's complete_goal leftOut claim — never invented. Filler ("none")
|
|
350
538
|
// drops via the same briefValueContent filter; absent stays absent.
|
|
351
539
|
const leftOut = input.leftOut?.trim();
|
|
352
540
|
const leftOutContent = leftOut ? briefValueContent(leftOut) : null;
|
|
353
|
-
const
|
|
354
|
-
?
|
|
355
|
-
:
|
|
541
|
+
const richDetails = leftOutContent
|
|
542
|
+
? [...richBrief.details, `Left out: ${clipSummaryValue(leftOutContent, RICH_VALUE_BUDGET)}`]
|
|
543
|
+
: richBrief.details;
|
|
356
544
|
const countsLine = input.countsLine ?? buildAuditCountsLine(input.goal, input.auditNote);
|
|
357
545
|
// v0.38.42 (field 20260909_140404): one canonical approval bullet in
|
|
358
546
|
// chat and transcript — no model ID in either, via-retry news kept. A
|
|
@@ -370,19 +558,31 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
370
558
|
const approvalBullet = foldCounts
|
|
371
559
|
? `• ${chatApproval.replace(/^—\s*/, "").replace(/\.\s*$/, "")} (${history.length} verdict).`
|
|
372
560
|
: trailerBullet(chatApproval);
|
|
561
|
+
const recordBullet = trailerBullet(input.record);
|
|
562
|
+
// Rich voice: headline + Key Findings + Verification table + Next,
|
|
563
|
+
// closed by the pinned trailer (approval/counts/record, record last).
|
|
564
|
+
// The transcript mirrors chat without the record pointer and extras —
|
|
565
|
+
// the record lives in chat and the archive, matching the old contract.
|
|
566
|
+
const richParts = buildRichTerminalParts({
|
|
567
|
+
outcome: richBrief.outcome,
|
|
568
|
+
details: withoutStaleNext(richDetails),
|
|
569
|
+
countsLine,
|
|
570
|
+
auditHistory: input.goal.auditHistory,
|
|
571
|
+
});
|
|
572
|
+
const chatBody = composeRichTerminalLines(richParts);
|
|
573
|
+
const transcriptBody = composeRichTerminalLines(richParts);
|
|
373
574
|
return {
|
|
374
575
|
chatLines: [
|
|
375
|
-
|
|
376
|
-
...withoutStaleNext(brief.details).map((detail) => `• ${detail}`),
|
|
576
|
+
...chatBody,
|
|
377
577
|
approvalBullet,
|
|
378
578
|
...(foldCounts ? [] : [trailerBullet(countsLine)]),
|
|
379
|
-
|
|
579
|
+
recordBullet,
|
|
380
580
|
...(input.extras ?? []),
|
|
381
581
|
],
|
|
382
582
|
recap,
|
|
383
|
-
transcriptLines: [...
|
|
583
|
+
transcriptLines: [...transcriptBody, approvalBullet],
|
|
384
584
|
countsLine,
|
|
385
|
-
outcome:
|
|
585
|
+
outcome: richBrief.outcome,
|
|
386
586
|
approval: input.approval,
|
|
387
587
|
};
|
|
388
588
|
}
|
|
@@ -394,9 +594,7 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
394
594
|
export function completionSummaryLines(text: string | undefined, maxValueLength = 240, lineWidth?: number): string[] {
|
|
395
595
|
const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
|
|
396
596
|
const lower = source.toLowerCase();
|
|
397
|
-
const positions =
|
|
398
|
-
.map((label) => ({ label, start: lower.indexOf(label.toLowerCase()) }))
|
|
399
|
-
.filter((entry) => entry.start >= 0);
|
|
597
|
+
const positions = labelPositions(lower);
|
|
400
598
|
return COMPLETION_SUMMARY_LABELS.map((label) => {
|
|
401
599
|
const name = label.slice(0, -1);
|
|
402
600
|
const current = positions.find((entry) => entry.label === label);
|
|
@@ -1,9 +1,18 @@
|
|
|
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 {
|
|
4
|
+
import {
|
|
5
|
+
compareVersions,
|
|
6
|
+
GLLA_PACKAGE_NAME,
|
|
7
|
+
isVersionLike,
|
|
8
|
+
readUpdateCheck,
|
|
9
|
+
readUpdateCheckRaw,
|
|
10
|
+
updateCheckPath,
|
|
11
|
+
type UpdateCheckCache,
|
|
12
|
+
} from "./glla-version.js";
|
|
5
13
|
|
|
6
|
-
export { compareVersions, updateCheckPath };
|
|
14
|
+
export { compareVersions, readUpdateCheck, updateCheckPath };
|
|
15
|
+
export type { UpdateCheckCache };
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* v0.38.44 (field 20260909_161057): a live session rendered the
|
|
@@ -21,37 +30,30 @@ export { compareVersions, updateCheckPath };
|
|
|
21
30
|
export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
22
31
|
export const UPDATE_CHECK_TIMEOUT_MS = 15_000;
|
|
23
32
|
|
|
24
|
-
export interface
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** Read the cache; null when missing, unreadable, malformed, or future-dated. */
|
|
30
|
-
export function readUpdateCheck(cwd: string): UpdateCheckCache | null {
|
|
31
|
-
try {
|
|
32
|
-
const parsed: unknown = JSON.parse(fs.readFileSync(updateCheckPath(cwd), "utf8"));
|
|
33
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
34
|
-
const record = parsed as Record<string, unknown>;
|
|
35
|
-
if (typeof record.latest !== "string" || !record.latest.trim()) return null;
|
|
36
|
-
if (typeof record.checkedAt !== "number" || !Number.isFinite(record.checkedAt)) return null;
|
|
37
|
-
if (record.checkedAt > Date.now() + 60_000) return null;
|
|
38
|
-
return { latest: record.latest.trim(), checkedAt: record.checkedAt };
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
33
|
+
export interface SpawnedRefreshChild {
|
|
34
|
+
on(event: "close", listener: (code: number | null) => void): void;
|
|
35
|
+
on(event: "error", listener: (err: unknown) => void): void;
|
|
36
|
+
unref?: () => void;
|
|
37
|
+
stdout: { on(event: "data", listener: (chunk: Buffer) => void): void };
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export type SpawnFn = (
|
|
45
41
|
command: string,
|
|
46
42
|
args: string[],
|
|
47
|
-
options: { timeout: number },
|
|
48
|
-
) =>
|
|
43
|
+
options: { timeout: number; stdio?: Array<"ignore" | "pipe"> },
|
|
44
|
+
) => SpawnedRefreshChild;
|
|
49
45
|
|
|
50
46
|
/**
|
|
51
47
|
* Throttled fire-and-forget refresh: returns immediately in every case.
|
|
52
48
|
* Skips when the cache is within TTL; otherwise shells `npm view` with a
|
|
53
49
|
* bounded timeout and rewrites the cache on success. Never throws — an
|
|
54
50
|
* offline session simply keeps rendering without a nudge.
|
|
51
|
+
* v0.38.45 audit: the spawned child carries an `error` listener (a
|
|
52
|
+
* missing/broken npm emits async `error`, which the outer try/catch
|
|
53
|
+
* cannot catch and which crashes the host unhandled), detaches via
|
|
54
|
+
* unref, and ignores stderr/stdin; only version-shaped stdout tokens
|
|
55
|
+
* are cached. The TTL skip uses the RAW read so a future-dated cache
|
|
56
|
+
* (clock skew) suppresses the spawn instead of causing one per contact.
|
|
55
57
|
*/
|
|
56
58
|
export function refreshUpdateCheck(
|
|
57
59
|
cwd: string,
|
|
@@ -59,9 +61,18 @@ export function refreshUpdateCheck(
|
|
|
59
61
|
spawnFn: SpawnFn = spawn as unknown as SpawnFn,
|
|
60
62
|
): void {
|
|
61
63
|
try {
|
|
62
|
-
const cached =
|
|
64
|
+
const cached = readUpdateCheckRaw(cwd);
|
|
63
65
|
if (cached && now - cached.checkedAt < UPDATE_CHECK_TTL_MS) return;
|
|
64
|
-
const child = spawnFn("npm", ["view", GLLA_PACKAGE_NAME, "version"], {
|
|
66
|
+
const child = spawnFn("npm", ["view", GLLA_PACKAGE_NAME, "version"], {
|
|
67
|
+
timeout: UPDATE_CHECK_TIMEOUT_MS,
|
|
68
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
69
|
+
});
|
|
70
|
+
// A missing/broken npm emits async `error`, not a sync throw and
|
|
71
|
+
// not a nonzero close — without this listener the host crashes.
|
|
72
|
+
child.on("error", () => {
|
|
73
|
+
// Fail-silent by design: keep rendering without a nudge.
|
|
74
|
+
});
|
|
75
|
+
if (typeof child.unref === "function") child.unref();
|
|
65
76
|
let out = "";
|
|
66
77
|
child.stdout.on("data", (chunk) => {
|
|
67
78
|
out += chunk.toString();
|
|
@@ -71,10 +82,10 @@ export function refreshUpdateCheck(
|
|
|
71
82
|
try {
|
|
72
83
|
if (code !== 0) return;
|
|
73
84
|
const latest = out.trim().split(/\s+/).pop() ?? "";
|
|
74
|
-
if (!latest) return;
|
|
85
|
+
if (!isVersionLike(latest)) return;
|
|
75
86
|
const dir = path.dirname(updateCheckPath(cwd));
|
|
76
87
|
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
-
fs.writeFileSync(updateCheckPath(cwd), JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
88
|
+
fs.writeFileSync(updateCheckPath(cwd), JSON.stringify({ latest: latest.trim(), checkedAt: Date.now() }));
|
|
78
89
|
} catch {
|
|
79
90
|
// Cache write failure is invisible by design.
|
|
80
91
|
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolveGllaStateDir } from "./glla-state-root.js";
|
|
4
5
|
|
|
5
6
|
export const GLLA_PACKAGE_NAME = "pi-goal-list-loop-audit";
|
|
6
7
|
|
|
7
8
|
/** Sidecar path shared with the update check (kept here so both
|
|
8
|
-
* glla-version and glla-update-check resolve it without a cycle).
|
|
9
|
+
* glla-version and glla-update-check resolve it without a cycle).
|
|
10
|
+
* v0.38.45 audit: root-aware — sessionDir mode keeps the sidecar beside
|
|
11
|
+
* every other state file instead of splitting it under cwd. */
|
|
9
12
|
export function updateCheckPath(cwd: string): string {
|
|
10
|
-
return path.join(cwd, "
|
|
13
|
+
return path.join(resolveGllaStateDir(cwd), "update-check.json");
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
/** Numeric semver-ish compare: 1 when a > b, -1 when a < b, 0 when equal
|
|
14
|
-
* or unparseable. Pure — shared by the version command and the nudge.
|
|
17
|
+
* or unparseable. Pure — shared by the version command and the nudge.
|
|
18
|
+
* v0.38.45 audit: strictly numeric segments — parseInt equated
|
|
19
|
+
* prereleases (`3-beta`→3), so a prerelease running build never nudged. */
|
|
15
20
|
export function compareVersions(a: string, b: string): number {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const numParts = (s: string): number[] | null => {
|
|
22
|
+
const parts = s.trim().replace(/^v/, "").split(".");
|
|
23
|
+
if (parts.some((part) => !/^\d+$/.test(part))) return null;
|
|
24
|
+
return parts.map((part) => Number.parseInt(part, 10));
|
|
25
|
+
};
|
|
26
|
+
const pa = numParts(a);
|
|
27
|
+
const pb = numParts(b);
|
|
28
|
+
if (!pa || !pb) return 0;
|
|
19
29
|
const len = Math.max(pa.length, pb.length);
|
|
20
30
|
for (let i = 0; i < len; i++) {
|
|
21
31
|
const x = pa[i] ?? 0;
|
|
@@ -31,6 +41,42 @@ const PACKAGE_JSON_PATH = path.resolve(
|
|
|
31
41
|
"../package.json",
|
|
32
42
|
);
|
|
33
43
|
|
|
44
|
+
export interface UpdateCheckCache {
|
|
45
|
+
latest: string;
|
|
46
|
+
checkedAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Raw sidecar read: null when missing/unreadable/malformed. Accepts
|
|
50
|
+
* future-dated entries — the refresh skip-check uses this so one skewed
|
|
51
|
+
* clock does not become a per-contact spawn storm (v0.38.45 audit). */
|
|
52
|
+
export function readUpdateCheckRaw(cwd: string): UpdateCheckCache | null {
|
|
53
|
+
try {
|
|
54
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(updateCheckPath(cwd), "utf8"));
|
|
55
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
56
|
+
const record = parsed as Record<string, unknown>;
|
|
57
|
+
if (typeof record.latest !== "string" || !record.latest.trim()) return null;
|
|
58
|
+
if (typeof record.checkedAt !== "number" || !Number.isFinite(record.checkedAt)) return null;
|
|
59
|
+
return { latest: record.latest.trim(), checkedAt: record.checkedAt };
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Strict sidecar read for render surfaces: additionally rejects
|
|
66
|
+
* future-dated entries (clock skew must read as unknown, never fresh). */
|
|
67
|
+
export function readUpdateCheck(cwd: string): UpdateCheckCache | null {
|
|
68
|
+
const raw = readUpdateCheckRaw(cwd);
|
|
69
|
+
if (!raw) return null;
|
|
70
|
+
if (raw.checkedAt > Date.now() + 60_000) return null;
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** npm-version-shaped tokens only — the refresh caches stdout, so
|
|
75
|
+
* registry noise must never poison the sidecar (v0.38.45 audit). */
|
|
76
|
+
export function isVersionLike(token: string): boolean {
|
|
77
|
+
return /^v?\d+\.\d+\.\d+([\-+][\w.]+)?$/.test(token.trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
34
80
|
export interface GllaVersionInfo {
|
|
35
81
|
name: string;
|
|
36
82
|
version: string;
|
|
@@ -70,16 +116,17 @@ export function formatGllaVersion(cwd?: string, info: GllaVersionInfo = readGlla
|
|
|
70
116
|
// cached registry latest, say whether this session is stale and how
|
|
71
117
|
// to fix it. No cache (offline / never checked) keeps the legacy
|
|
72
118
|
// three lines — absence of evidence is not evidence of freshness.
|
|
73
|
-
|
|
119
|
+
// v0.38.45 audit: the strict shared reader (same validation as the
|
|
120
|
+
// status tail, so the two surfaces cannot disagree), and no staleness
|
|
121
|
+
// claim at all when the running version is unreadable — "unknown"
|
|
122
|
+
// compared equal and printed a false "up to date".
|
|
123
|
+
if (cwd && info.version !== "unknown") {
|
|
74
124
|
try {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: undefined;
|
|
79
|
-
if (typeof latest === "string" && latest.trim()) {
|
|
80
|
-
lines.push(`Registry latest: v${latest.trim()}.`);
|
|
125
|
+
const cached = readUpdateCheck(cwd);
|
|
126
|
+
if (cached) {
|
|
127
|
+
lines.push(`Registry latest: v${cached.latest}.`);
|
|
81
128
|
lines.push(
|
|
82
|
-
compareVersions(latest, info.version) > 0
|
|
129
|
+
compareVersions(cached.latest, info.version) > 0
|
|
83
130
|
? `This session is stale — update: pi install npm:${info.name}@latest then /reload.`
|
|
84
131
|
: "This session is up to date.",
|
|
85
132
|
);
|
|
@@ -36,7 +36,6 @@ import type { SettingsSectionId } from "./settings-menu.js";
|
|
|
36
36
|
import { cmdLoop, clearLoopTimer, finishLoopGit, isLoopActive, scheduleLoopTick } from "./goal-loop.js";
|
|
37
37
|
import { chooseObjectiveConflict, liveObjectives } from "./goal-objective-conflict.js";
|
|
38
38
|
import { formatGllaVersion } from "./glla-version.js";
|
|
39
|
-
import { readUpdateCheck } from "./glla-update-check.js";
|
|
40
39
|
import { cmdGllaOwner, cmdGllaTakeover } from "./state-root-owner.js";
|
|
41
40
|
import { AUDIT_JOB_CLEANUP_MIN_AGE_MS, cancelDetachedGoalCompletionAuditor, cleanupDeadAuditJobs, inspectAuditJobHealth, DEFAULT_AUDITOR_STALL_MS, DEFAULT_AUDITOR_TOOL_TIMEOUT_MS } from "./goal-loop-auditor-process.js";
|
|
42
41
|
import { releaseAuditorSurface } from "./loops/goal-auditor-surface.js";
|
|
@@ -609,7 +609,6 @@ export interface AuditorVerdictTally {
|
|
|
609
609
|
lastLabel: string | null;
|
|
610
610
|
}
|
|
611
611
|
export function auditorVerdictTally(history: Goal["auditHistory"], now = Date.now()): AuditorVerdictTally {
|
|
612
|
-
void now;
|
|
613
612
|
const entries = Array.isArray(history) ? history : [];
|
|
614
613
|
let approvals = 0;
|
|
615
614
|
let disapprovals = 0;
|
|
@@ -620,11 +619,15 @@ export function auditorVerdictTally(history: Goal["auditHistory"], now = Date.no
|
|
|
620
619
|
}
|
|
621
620
|
const last = entries[entries.length - 1];
|
|
622
621
|
const lastMs = last ? Date.parse(last.at) : Number.NaN;
|
|
622
|
+
// v0.38.45 audit: `now` earns its parameter slot — a future lastAt
|
|
623
|
+
// (clock skew) suppresses the age instead of printing "0s ago" via
|
|
624
|
+
// fmtElapsed's clamp, matching auditorLastActivity's handling.
|
|
625
|
+
const lastOk = last && Number.isFinite(lastMs) && (lastMs as number) <= now;
|
|
623
626
|
return {
|
|
624
627
|
total: entries.length,
|
|
625
628
|
approvals,
|
|
626
629
|
disapprovals,
|
|
627
|
-
lastAt:
|
|
630
|
+
lastAt: lastOk ? (lastMs as number) : null,
|
|
628
631
|
lastLabel: last ? auditVerdictLabel(last) : null,
|
|
629
632
|
};
|
|
630
633
|
}
|
|
@@ -52,7 +52,11 @@ const RECOVERY_RESUME_FRESH_MS = 300_000;
|
|
|
52
52
|
|
|
53
53
|
/** v0.34.13: consume the sidecar marker on session restore. Single-use,
|
|
54
54
|
* freshness-bounded — a stale marker from an abandoned recovery must not
|
|
55
|
-
* surprise-resume a later session.
|
|
55
|
+
* surprise-resume a later session.
|
|
56
|
+
* v0.38.45 audit: no production writer plants this marker today (repo-wide
|
|
57
|
+
* grep: only tests do) — the restore-gate disjuncts below read it as a
|
|
58
|
+
* reserved hook, always false live. Do not remove the reads: an older or
|
|
59
|
+
* foreign producer dropping this file must still consent a resume. */
|
|
56
60
|
export function consumeRecoveryResume(cwd: string): boolean {
|
|
57
61
|
try {
|
|
58
62
|
const p = path.join(piGlaDir(cwd), RECOVERY_RESUME_MARKER);
|
|
@@ -241,6 +241,7 @@ import {
|
|
|
241
241
|
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
242
242
|
import {
|
|
243
243
|
buildLoopCompletionSummary,
|
|
244
|
+
buildRichArchiveSection,
|
|
244
245
|
compactLoopCompletionSummary,
|
|
245
246
|
compactTerminalCompletionSummary,
|
|
246
247
|
isTerminalLoopStopReason,
|
|
@@ -1142,6 +1143,12 @@ function archiveCurrentGoal(
|
|
|
1142
1143
|
completionSummary: summaryResolution.summary,
|
|
1143
1144
|
};
|
|
1144
1145
|
const md = renderGoalMarkdown(terminalGoal);
|
|
1146
|
+
// Field 20260911_* (rich terminal voice): the archive is for humans —
|
|
1147
|
+
// it carries the same rich markdown as chat over the verbatim
|
|
1148
|
+
// six-label machine record. Built from the terminal goal AFTER the
|
|
1149
|
+
// summary fence resolves, so chat and archive cannot disagree.
|
|
1150
|
+
const richSection = buildRichArchiveSection(terminalGoal, status, archivePath);
|
|
1151
|
+
const richMd = `${md}\n## Terminal summary\n\n${richSection.join("\n")}\n`;
|
|
1145
1152
|
// An existing same-id archive is an immutable fence. Check it before
|
|
1146
1153
|
// publishing an intent so an unrelated/sentinel winner can never make the
|
|
1147
1154
|
// startup reconciler terminalize this live goal.
|
|
@@ -1179,7 +1186,7 @@ function archiveCurrentGoal(
|
|
|
1179
1186
|
// mitigates, this removes the predictability too).
|
|
1180
1187
|
const temp = `${target}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
1181
1188
|
try {
|
|
1182
|
-
fs.writeFileSync(temp,
|
|
1189
|
+
fs.writeFileSync(temp, richMd, { encoding: "utf-8", flag: "wx" });
|
|
1183
1190
|
fs.linkSync(temp, target);
|
|
1184
1191
|
return true;
|
|
1185
1192
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.46",
|
|
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",
|