pi-goal-list-loop-audit 0.38.28 → 0.38.29
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 +8 -0
- package/docs/INDEX.md +2 -2
- package/extensions/goal-loop-display.ts +113 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.29 — compact active-card recovery and judgment details (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The active goal card now renders main-model recovery as one compact, truthful row instead of repeating the full chain, current model, pending switch, attempts, and skip details. Full recovery diagnostics remain available through `/goal status`; the status footer no longer duplicates that report.
|
|
7
|
+
- Durable-vs-defer plaques cap their glance-card prose while preserving ordering, recommendation, and selected-choice facts. The final judgment row closes the tree cleanly.
|
|
8
|
+
- Markdown emphasis/code wrappers are removed from the display-only objective projection, so `**objective**` and backtick-wrapped commands no longer add visual noise. Stored objectives remain unchanged.
|
|
9
|
+
- Added regression coverage for compact recovery, duplicate suppression, Markdown cleanup, and the closed judgment tail.
|
|
10
|
+
|
|
3
11
|
## 0.38.28 — close redundant provenance rows in the active goal card (2026-09-08)
|
|
4
12
|
|
|
5
13
|
### 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.29 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
|
|
@@ -28,7 +28,7 @@ For shipped docs, the relevant entry points are:
|
|
|
28
28
|
packed-artifact release verification, and the 2026-09-07 display/lifecycle/
|
|
29
29
|
settings audit pass (abort-latch send guards, ownership compare-and-swap,
|
|
30
30
|
auditor inherit/clear parity) and the v0.38.25 post-objective summary
|
|
31
|
-
(canonical approval render, persist + replay, audit-goal counts line), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count),
|
|
31
|
+
(canonical approval render, persist + replay, audit-goal counts line), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count), the v0.38.28 provenance-row closure fix, and the v0.38.29 compact active-card recovery/judgment projection — see CHANGELOG.md for the full trail.
|
|
32
32
|
- `../README.md` — what the plugin is, install, quickstart, and the
|
|
33
33
|
architectural guarantee (drafting + confirm + detached auditor).
|
|
34
34
|
- `../INSTALL.md` — source install / local development setup; the recommended
|
|
@@ -87,6 +87,17 @@ function displayPauseReason(reason: string): string {
|
|
|
87
87
|
return compactDisplayText(sanitizeProviderDisplayText(reason));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Objectives are stored verbatim, but the glance card is a plain-text
|
|
91
|
+
* surface. Remove the common Markdown emphasis/code wrappers that otherwise
|
|
92
|
+
* turn a long objective into noisy `**...**` and backtick litter. This is a
|
|
93
|
+
* display-only projection; prompts, archives, and state keep the original. */
|
|
94
|
+
function displayObjective(objective: string): string {
|
|
95
|
+
return compactDisplayText(objective)
|
|
96
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
97
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
98
|
+
.replace(/`([^`\n]*)`/g, "$1");
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
/** v0.33.1: painted strings measure by their terminal-cell width. */
|
|
91
102
|
function visibleLen(s: string): number {
|
|
92
103
|
return tuiVisibleWidth(s);
|
|
@@ -280,6 +291,53 @@ function modelSourceLabel(source: string): string {
|
|
|
280
291
|
return source.trim();
|
|
281
292
|
}
|
|
282
293
|
|
|
294
|
+
function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, configuredBackups: string[] = [], now = Date.now()): string | undefined {
|
|
295
|
+
if (!recovery || !recovery.primary.trim()) return undefined;
|
|
296
|
+
const primary = recovery.primary.trim();
|
|
297
|
+
const current = (recovery.active ?? primary).trim() || primary;
|
|
298
|
+
const backups = uniqueModelRefs(configuredBackups).slice(0, 10);
|
|
299
|
+
const chain = [primary, ...backups];
|
|
300
|
+
const currentIndex = chain.findIndex((ref) => ref.toLowerCase() === current.toLowerCase());
|
|
301
|
+
const selected = currentIndex === 0
|
|
302
|
+
? "primary"
|
|
303
|
+
: currentIndex > 0
|
|
304
|
+
? `backup ${currentIndex}/${backups.length}`
|
|
305
|
+
: "fallback";
|
|
306
|
+
const retryMs = recovery.retryAt ? Date.parse(recovery.retryAt) - now : Number.NaN;
|
|
307
|
+
const probeMs = recovery.primaryProbeAt ? Date.parse(recovery.primaryProbeAt) - now : Number.NaN;
|
|
308
|
+
const phase = recovery.manualResumeRequired
|
|
309
|
+
? "manual hold"
|
|
310
|
+
: recovery.pendingModelSwitch
|
|
311
|
+
? `switching → ${truncate(recovery.pendingModelSwitch, 48)}`
|
|
312
|
+
: Number.isFinite(retryMs)
|
|
313
|
+
? retryMs <= 0 ? "retrying now" : `retrying in ${fmtElapsed(retryMs)}`
|
|
314
|
+
: recovery.primaryProbeInFlight
|
|
315
|
+
? "primary probe pending"
|
|
316
|
+
: Number.isFinite(probeMs)
|
|
317
|
+
? probeMs <= 0 ? "probing primary now" : `primary probe in ${fmtElapsed(probeMs)}`
|
|
318
|
+
: `${selected} selected`;
|
|
319
|
+
const attempted = recovery.attempted?.length ?? 0;
|
|
320
|
+
const skipped = recovery.skipped?.length ?? 0;
|
|
321
|
+
const hasEpisode = !!recovery.manualResumeRequired
|
|
322
|
+
|| !!recovery.pendingModelSwitch
|
|
323
|
+
|| Number.isFinite(retryMs)
|
|
324
|
+
|| Number.isFinite(probeMs)
|
|
325
|
+
|| !!recovery.primaryProbeInFlight
|
|
326
|
+
|| recovery.attempts > 0
|
|
327
|
+
|| attempted > 1
|
|
328
|
+
|| skipped > 0
|
|
329
|
+
|| currentIndex !== 0;
|
|
330
|
+
// A recovery object can briefly survive the successful primary selection.
|
|
331
|
+
// Do not label that normal state as an incident, but keep the selected model
|
|
332
|
+
// on the card when no separate provenance row is available.
|
|
333
|
+
if (!hasEpisode) return `model: ${truncate(current, 56)} · primary`;
|
|
334
|
+
const history = [
|
|
335
|
+
recovery.attempts > 0 ? `attempts ${recovery.attempts}` : "",
|
|
336
|
+
skipped > 0 ? `skipped ${skipped}` : "",
|
|
337
|
+
].filter(Boolean);
|
|
338
|
+
return `recovery: ${phase} · ${selected} ${truncate(current, 48)}${history.length > 0 ? ` · ${history.join(" · ")}` : ""}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
283
341
|
function modelProvenanceLines(provenance: ModelProvenanceDisplay | undefined, width?: number): string[] {
|
|
284
342
|
if (!provenance) return [];
|
|
285
343
|
const budget = budgetFor(width, 3, 60);
|
|
@@ -328,9 +386,14 @@ export function buildDurableDeferDecisionLines(input: DurableDeferRecommendation
|
|
|
328
386
|
// Reserve room for the recommendation marker and the outer tree prefix
|
|
329
387
|
// so a normal 80-column production widget cannot truncate away the
|
|
330
388
|
// fact that the durable plaque is the selected action.
|
|
389
|
+
// Recommendation bodies are durable facts, but the glance card is not
|
|
390
|
+
// the command's full report. A wide terminal used to turn these plaques
|
|
391
|
+
// into sentence-length banners and crowd out the actual work state;
|
|
392
|
+
// /goal status retains the complete text.
|
|
393
|
+
const bodyCap = plaque.kind === "durable" ? 96 : 72;
|
|
331
394
|
const bodyBudget = width && width > 0
|
|
332
|
-
? Math.max(8, width - WIDGET_HORIZONTAL_MARGIN - 3 - visibleLen(prefix) - visibleLen(marker))
|
|
333
|
-
: budgetFor(width, 3, 60);
|
|
395
|
+
? Math.max(8, Math.min(bodyCap, width - WIDGET_HORIZONTAL_MARGIN - 3 - visibleLen(prefix) - visibleLen(marker)))
|
|
396
|
+
: Math.min(bodyCap, budgetFor(width, 3, 60));
|
|
334
397
|
return `${prefix}${truncate(plaque.body, bodyBudget)}${marker}`;
|
|
335
398
|
}),
|
|
336
399
|
`selected: ${recommendation.choice}${recommendation.choice === "inline" ? " (durable fix)" : " (reversible workaround)"}`,
|
|
@@ -1163,21 +1226,21 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1163
1226
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${pausedStatusSuffix(g, state, extras, now)}${heldSuffix}`;
|
|
1164
1227
|
}
|
|
1165
1228
|
if (g.status === "active") {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1229
|
+
// The footer is a glance/liveness surface. Recovery details belong to the
|
|
1230
|
+
// card (and the full /goal status report), not a second horizontally
|
|
1231
|
+
// concatenated copy here. This keeps exceptions readable without making
|
|
1232
|
+
// a healthy WORKING line look like an incident.
|
|
1170
1233
|
// v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE.
|
|
1171
1234
|
// It outranks any older operational note on the same state snapshot.
|
|
1172
1235
|
if (g.interruptedAt) {
|
|
1173
1236
|
const label = interruptedForNoStart(g)
|
|
1174
1237
|
? "⚠ turn start not observed — automatic retry held"
|
|
1175
1238
|
: "⚠ interrupted — stale handle · /new (or a fresh session_start) rebinds";
|
|
1176
|
-
return
|
|
1239
|
+
return `glla: ${paint(theme, "error", label)}${heldSuffix}`;
|
|
1177
1240
|
}
|
|
1178
1241
|
const attention = activeAttention(g);
|
|
1179
1242
|
if (attention) {
|
|
1180
|
-
return
|
|
1243
|
+
return `glla: ${paint(theme, attention.color, `⚠ ${attention.label}`)}${heldSuffix}`;
|
|
1181
1244
|
}
|
|
1182
1245
|
const activity = goalDisplayActivity(g, extras, now);
|
|
1183
1246
|
// v0.34.97: while the post-compaction grace window is open, surface
|
|
@@ -1186,10 +1249,10 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1186
1249
|
const compactAgeMs = state.lastCompactionAt ? now - state.lastCompactionAt : Number.POSITIVE_INFINITY;
|
|
1187
1250
|
const compacting = Number.isFinite(compactAgeMs) && compactAgeMs >= 0 && compactAgeMs < 180_000; // COMPACTION_GRACE_MS = 3 min
|
|
1188
1251
|
if (compacting) {
|
|
1189
|
-
return
|
|
1252
|
+
return `glla: ${paint(theme, "warning", `⏳ compacting… (${fmtElapsed(compactAgeMs)} ago)`)}${heldSuffix}`;
|
|
1190
1253
|
}
|
|
1191
1254
|
if (activity === "awaiting-first-turn") {
|
|
1192
|
-
return
|
|
1255
|
+
return `glla: ${activityStateBadge("AWAITING FIRST TURN", theme, "warning")}${heldSuffix}`;
|
|
1193
1256
|
}
|
|
1194
1257
|
if (activity === "idle") {
|
|
1195
1258
|
// Audit 2026-09-07 (DECIDED: head owns liveness): the status keeps
|
|
@@ -1200,7 +1263,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1200
1263
|
goalTotalText(g, now),
|
|
1201
1264
|
(state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
|
|
1202
1265
|
].filter(Boolean);
|
|
1203
|
-
return
|
|
1266
|
+
return `glla: ${activityStateBadge("IDLE", theme, "warning")}${idleDetails.length > 0 ? ` ${idleDetails.join(" · ")}` : ""}${heldSuffix}`;
|
|
1204
1267
|
}
|
|
1205
1268
|
// v0.34.39: distinguish durable state from evidence of a live host turn.
|
|
1206
1269
|
// A spinner is reserved for recent stream/tool evidence; BUSY without
|
|
@@ -1212,7 +1275,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1212
1275
|
g.taskList ? `${countDone(g)}/${countTotal(g)} tasks` : "",
|
|
1213
1276
|
(state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
|
|
1214
1277
|
].filter(Boolean);
|
|
1215
|
-
return
|
|
1278
|
+
return `glla: ${activityStateBadge("BUSY", theme, "warning")}${busyDetails.length > 0 ? ` ${busyDetails.join(" · ")}` : ""}${heldSuffix}`;
|
|
1216
1279
|
}
|
|
1217
1280
|
// v0.34.16: a fresh session_start owns the handoff. A cold boot still
|
|
1218
1281
|
// follows the global autoResume setting, so the widget names the actual
|
|
@@ -1258,7 +1321,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1258
1321
|
monitoring ? "next check" : "",
|
|
1259
1322
|
n > 0 ? `${n} queued` : "",
|
|
1260
1323
|
].filter(Boolean);
|
|
1261
|
-
return
|
|
1324
|
+
return `glla: ${marker}${details.length > 0 ? ` ${details.join(" · ")}` : ""}${recoverySuffix}${heldSuffix}`;
|
|
1262
1325
|
}
|
|
1263
1326
|
// v0.34.65: a terminal goal names its outcome + wall duration instead of
|
|
1264
1327
|
// clearing the segment (note.md 2026-08-07: "this seems weak for a complete
|
|
@@ -1352,6 +1415,16 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
|
|
|
1352
1415
|
withAgents = orphanHead ? [orphanHead, ...agentLines] : [...agentLines];
|
|
1353
1416
|
}
|
|
1354
1417
|
}
|
|
1418
|
+
// Keep the final tree row visually closed when the compact judgment block
|
|
1419
|
+
// ends without a queue footer. Without this, its last `├─` reads like a
|
|
1420
|
+
// missing continuation and makes an already dense card feel unfinished.
|
|
1421
|
+
if (withAgents && inner && detailedAgents.length === 0 && extras?.durableDeferRecommendation && withAgents.length > 0) {
|
|
1422
|
+
const tailIndex = withAgents.length - 1;
|
|
1423
|
+
const tail = withAgents[tailIndex]!;
|
|
1424
|
+
if (tail.startsWith("├─ ") || tail.startsWith("│ ")) {
|
|
1425
|
+
withAgents[tailIndex] = `└─ ${tail.slice(3)}`;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1355
1428
|
// v0.28.6 (E1): a persistence failure outranks everything — first line,
|
|
1356
1429
|
// on every render, until a write lands again.
|
|
1357
1430
|
let lines: string[] | undefined = withAgents;
|
|
@@ -1373,7 +1446,7 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
|
|
|
1373
1446
|
function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, width?: number): string {
|
|
1374
1447
|
const queue = state.list ?? [];
|
|
1375
1448
|
const head = queue[0];
|
|
1376
|
-
const objective = head?.objective?.trim() ?
|
|
1449
|
+
const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
|
|
1377
1450
|
const hold = typeof state.loadHoldAt === "number" ? " · held on restore" : "";
|
|
1378
1451
|
// Audit 2026-09-06: the width budget truncates the END of the status
|
|
1379
1452
|
// line — the `/glla resume` action must survive, so the OBJECTIVE takes
|
|
@@ -1390,7 +1463,7 @@ function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, wid
|
|
|
1390
1463
|
function waitingListLines(state: State, theme?: DisplayTheme, width?: number): string[] {
|
|
1391
1464
|
const queue = state.list ?? [];
|
|
1392
1465
|
const head = queue[0];
|
|
1393
|
-
const objective = head?.objective?.trim() ?
|
|
1466
|
+
const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
|
|
1394
1467
|
const objectiveBudget = budgetFor(width, visibleLen("├─ up next: "), 56);
|
|
1395
1468
|
const action = typeof state.loadHoldAt === "number"
|
|
1396
1469
|
? "held on restore · /glla resume starts the queue · /list next skips/chooses"
|
|
@@ -1555,7 +1628,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1555
1628
|
const headIcon = headLive
|
|
1556
1629
|
? paint(theme, headLive.band === "fresh" ? "success" : headLive.band === "aging" ? "warning" : "error", headLive.breath)
|
|
1557
1630
|
: icon;
|
|
1558
|
-
const head = `${headIcon} ${truncate(g.objective
|
|
1631
|
+
const head = `${headIcon} ${truncate(displayObjective(g.objective), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
|
|
1559
1632
|
const lines = [head];
|
|
1560
1633
|
// v0.38.8: durable verdict tally as a first-class card row — the widget
|
|
1561
1634
|
// is the glance surface, and stored verdicts are the progress evidence
|
|
@@ -1572,20 +1645,22 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1572
1645
|
lines.push(`├─ ${paint(theme, "dim", `Recovery: ${repairStep}`)}`);
|
|
1573
1646
|
}
|
|
1574
1647
|
// A model switch crosses an asynchronous boundary while the goal remains
|
|
1575
|
-
// active. Keep
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1648
|
+
// active. Keep that fact visible, but do not print the full recovery report
|
|
1649
|
+
// into the glance card; it made the card taller than the space below the
|
|
1650
|
+
// editor. `/goal status` remains the detailed recovery surface.
|
|
1651
|
+
let recoveryLine: string | undefined;
|
|
1578
1652
|
if (g.status === "active" && state.mainModelRecovery) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
|
|
1582
|
-
});
|
|
1653
|
+
recoveryLine = compactMainModelRecoveryLine(state.mainModelRecovery, extras?.mainModelFallbacks, now);
|
|
1654
|
+
if (recoveryLine) lines.push(`├─ ${paint(theme, "dim", recoveryLine)}`);
|
|
1583
1655
|
}
|
|
1584
1656
|
// Model provenance is a card fact, not a notification: keep it visible
|
|
1585
|
-
// across active, interrupted, auditing, and paused branches.
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
|
|
1657
|
+
// across active, interrupted, auditing, and paused branches. During an
|
|
1658
|
+
// active recovery the compact recovery row owns the model fact, so do not
|
|
1659
|
+
// print a second primary/current row beside it. The full chain and skipped
|
|
1660
|
+
// refs remain available through `/goal status`.
|
|
1661
|
+
const provenance = state.mainModelRecovery
|
|
1662
|
+
? []
|
|
1663
|
+
: modelProvenanceLines(extras?.modelProvenance, width);
|
|
1589
1664
|
const provenanceStart = lines.length;
|
|
1590
1665
|
provenance.forEach((line, i) => {
|
|
1591
1666
|
lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
|
|
@@ -1915,12 +1990,22 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1915
1990
|
// no recent action, pending task, or queue footer. Close that block with a
|
|
1916
1991
|
// terminator instead of leaving `├─`/`│` suggesting missing rows.
|
|
1917
1992
|
const provenanceEnd = provenanceStart + provenance.length;
|
|
1918
|
-
|
|
1993
|
+
const detailedAgentRows = extras?.agents?.lines ?? (extras?.agents?.line ? [extras.agents.line] : []);
|
|
1994
|
+
if (provenance.length > 0 && detailedAgentRows.length === 0 && provenanceEnd === lines.length) {
|
|
1919
1995
|
const last = lines[provenanceEnd - 1]!;
|
|
1920
1996
|
if (last.startsWith("├─ ") || last.startsWith("│ ")) {
|
|
1921
1997
|
lines[provenanceEnd - 1] = `└─ ${last.slice(3)}`;
|
|
1922
1998
|
}
|
|
1923
1999
|
}
|
|
2000
|
+
// A compact recovery/model row can be the final detail when no other card
|
|
2001
|
+
// fact follows it. Close that one row without changing the established
|
|
2002
|
+
// open-row shape for recent actions or worker details.
|
|
2003
|
+
if (recoveryLine && detailedAgentRows.length === 0 && lines.length > 0) {
|
|
2004
|
+
const last = lines[lines.length - 1]!;
|
|
2005
|
+
if (last.includes(recoveryLine) && (last.startsWith("├─ ") || last.startsWith("│ "))) {
|
|
2006
|
+
lines[lines.length - 1] = `└─ ${last.slice(3)}`;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1924
2009
|
return lines;
|
|
1925
2010
|
}
|
|
1926
2011
|
|
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.29",
|
|
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",
|