@aragon-agent/cli 0.6.44 → 0.6.45

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.
Files changed (236) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +69 -2
  3. package/dist/agent/controller.d.ts +69 -0
  4. package/dist/agent/controller.js +224 -11
  5. package/dist/agent/governance-view.d.ts +40 -0
  6. package/dist/agent/governance-view.js +40 -0
  7. package/dist/agent/governance-wiring.d.ts +162 -0
  8. package/dist/agent/governance-wiring.js +470 -0
  9. package/dist/agent/plan-prompt.d.ts +7 -0
  10. package/dist/agent/plan-prompt.js +2 -1
  11. package/dist/agent/reducer.d.ts +33 -1
  12. package/dist/agent/reducer.js +45 -9
  13. package/dist/agent/system-prompt.d.ts +19 -0
  14. package/dist/agent/system-prompt.js +9 -1
  15. package/dist/cli.js +45 -1
  16. package/dist/commands/builtins.js +10 -4
  17. package/dist/commands/governance-command.d.ts +35 -0
  18. package/dist/commands/governance-command.js +292 -0
  19. package/dist/commands/governance-runs-table.d.ts +40 -0
  20. package/dist/commands/governance-runs-table.js +130 -0
  21. package/dist/commands/registry.d.ts +12 -0
  22. package/dist/config/app-paths.d.ts +10 -0
  23. package/dist/config/app-paths.js +12 -0
  24. package/dist/config/cli-commands.d.ts +18 -0
  25. package/dist/config/cli-commands.js +47 -1
  26. package/dist/config/env.d.ts +2 -1
  27. package/dist/config/env.js +10 -1
  28. package/dist/config/governance-config.d.ts +125 -0
  29. package/dist/config/governance-config.js +164 -0
  30. package/dist/config/governance-launch.d.ts +71 -0
  31. package/dist/config/governance-launch.js +58 -0
  32. package/dist/config/load.d.ts +15 -0
  33. package/dist/config/load.js +26 -1
  34. package/dist/config/model-profile-store.d.ts +4 -2
  35. package/dist/config/schema.d.ts +22 -0
  36. package/dist/config/schema.js +6 -0
  37. package/dist/config/store.js +14 -1
  38. package/dist/config/ui-state.d.ts +11 -0
  39. package/dist/config/ui-state.js +11 -0
  40. package/dist/diagnostics/doctor.js +20 -0
  41. package/dist/governance/card-normalize.d.ts +24 -0
  42. package/dist/governance/card-normalize.js +76 -0
  43. package/dist/governance/copy.d.ts +109 -0
  44. package/dist/governance/copy.js +223 -0
  45. package/dist/governance/engine/aftermath.d.ts +49 -0
  46. package/dist/governance/engine/aftermath.js +134 -0
  47. package/dist/governance/engine/artifact-persist.d.ts +35 -0
  48. package/dist/governance/engine/artifact-persist.js +86 -0
  49. package/dist/governance/engine/budget.d.ts +52 -0
  50. package/dist/governance/engine/budget.js +112 -0
  51. package/dist/governance/engine/cards.d.ts +18 -0
  52. package/dist/governance/engine/cards.js +186 -0
  53. package/dist/governance/engine/digest.d.ts +32 -0
  54. package/dist/governance/engine/digest.js +63 -0
  55. package/dist/governance/engine/engine-core.d.ts +219 -0
  56. package/dist/governance/engine/engine-core.js +562 -0
  57. package/dist/governance/engine/engine.d.ts +68 -0
  58. package/dist/governance/engine/engine.js +314 -0
  59. package/dist/governance/engine/estimate.d.ts +38 -0
  60. package/dist/governance/engine/estimate.js +46 -0
  61. package/dist/governance/engine/inspector.d.ts +16 -0
  62. package/dist/governance/engine/inspector.js +396 -0
  63. package/dist/governance/engine/integration.d.ts +35 -0
  64. package/dist/governance/engine/integration.js +220 -0
  65. package/dist/governance/engine/lifecycle.d.ts +29 -0
  66. package/dist/governance/engine/lifecycle.js +656 -0
  67. package/dist/governance/engine/pause.d.ts +56 -0
  68. package/dist/governance/engine/pause.js +107 -0
  69. package/dist/governance/engine/phase-members.d.ts +15 -0
  70. package/dist/governance/engine/phase-members.js +34 -0
  71. package/dist/governance/engine/phase-planner.d.ts +53 -0
  72. package/dist/governance/engine/phase-planner.js +282 -0
  73. package/dist/governance/engine/rebuild.d.ts +27 -0
  74. package/dist/governance/engine/rebuild.js +57 -0
  75. package/dist/governance/engine/report.d.ts +66 -0
  76. package/dist/governance/engine/report.js +220 -0
  77. package/dist/governance/engine/run-digest.d.ts +20 -0
  78. package/dist/governance/engine/run-digest.js +42 -0
  79. package/dist/governance/engine/spec-builder.d.ts +117 -0
  80. package/dist/governance/engine/spec-builder.js +321 -0
  81. package/dist/governance/engine/stage-runner.d.ts +22 -0
  82. package/dist/governance/engine/stage-runner.js +664 -0
  83. package/dist/governance/engine/start-input.d.ts +19 -0
  84. package/dist/governance/engine/start-input.js +111 -0
  85. package/dist/governance/engine/state-machine.d.ts +101 -0
  86. package/dist/governance/engine/state-machine.js +586 -0
  87. package/dist/governance/engine/submission-queries.d.ts +42 -0
  88. package/dist/governance/engine/submission-queries.js +97 -0
  89. package/dist/governance/engine/submission-render.d.ts +12 -0
  90. package/dist/governance/engine/submission-render.js +92 -0
  91. package/dist/governance/engine/submissions.d.ts +27 -0
  92. package/dist/governance/engine/submissions.js +455 -0
  93. package/dist/governance/engine/visibility.d.ts +104 -0
  94. package/dist/governance/engine/visibility.js +223 -0
  95. package/dist/governance/governed-dispatch.d.ts +19 -0
  96. package/dist/governance/governed-dispatch.js +17 -0
  97. package/dist/governance/limits.d.ts +107 -0
  98. package/dist/governance/limits.js +131 -0
  99. package/dist/governance/posture-transition.d.ts +38 -0
  100. package/dist/governance/posture-transition.js +44 -0
  101. package/dist/governance/posture.d.ts +35 -0
  102. package/dist/governance/posture.js +59 -0
  103. package/dist/governance/profiles.d.ts +48 -0
  104. package/dist/governance/profiles.js +140 -0
  105. package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
  106. package/dist/governance/prompts/orchestrator-block.js +128 -0
  107. package/dist/governance/prompts/plan-addendum.d.ts +22 -0
  108. package/dist/governance/prompts/plan-addendum.js +60 -0
  109. package/dist/governance/prompts/refusals.d.ts +48 -0
  110. package/dist/governance/prompts/refusals.js +82 -0
  111. package/dist/governance/prompts/role-blocks.d.ts +40 -0
  112. package/dist/governance/prompts/role-blocks.js +255 -0
  113. package/dist/governance/report-reader.d.ts +31 -0
  114. package/dist/governance/report-reader.js +57 -0
  115. package/dist/governance/roles.d.ts +83 -0
  116. package/dist/governance/roles.js +142 -0
  117. package/dist/governance/rules/claims.d.ts +83 -0
  118. package/dist/governance/rules/claims.js +250 -0
  119. package/dist/governance/rules/elimination.d.ts +66 -0
  120. package/dist/governance/rules/elimination.js +105 -0
  121. package/dist/governance/rules/events-table.d.ts +46 -0
  122. package/dist/governance/rules/events-table.js +94 -0
  123. package/dist/governance/rules/failure-analysis.d.ts +28 -0
  124. package/dist/governance/rules/failure-analysis.js +93 -0
  125. package/dist/governance/rules/ledger-replay.d.ts +53 -0
  126. package/dist/governance/rules/ledger-replay.js +53 -0
  127. package/dist/governance/rules/ledger-state.d.ts +85 -0
  128. package/dist/governance/rules/ledger-state.js +175 -0
  129. package/dist/governance/rules/ledger.d.ts +228 -0
  130. package/dist/governance/rules/ledger.js +560 -0
  131. package/dist/governance/rules/rca.d.ts +68 -0
  132. package/dist/governance/rules/rca.js +220 -0
  133. package/dist/governance/rules/settlement.d.ts +129 -0
  134. package/dist/governance/rules/settlement.js +363 -0
  135. package/dist/governance/rules/suspicion.d.ts +66 -0
  136. package/dist/governance/rules/suspicion.js +169 -0
  137. package/dist/governance/store/artifacts.d.ts +81 -0
  138. package/dist/governance/store/artifacts.js +173 -0
  139. package/dist/governance/store/atomic.d.ts +79 -0
  140. package/dist/governance/store/atomic.js +119 -0
  141. package/dist/governance/store/events-log.d.ts +35 -0
  142. package/dist/governance/store/events-log.js +86 -0
  143. package/dist/governance/store/run-lock.d.ts +51 -0
  144. package/dist/governance/store/run-lock.js +122 -0
  145. package/dist/governance/store/run-store.d.ts +229 -0
  146. package/dist/governance/store/run-store.js +644 -0
  147. package/dist/governance/store/scoreboard-export.d.ts +53 -0
  148. package/dist/governance/store/scoreboard-export.js +65 -0
  149. package/dist/governance/tools/child-tools.d.ts +31 -0
  150. package/dist/governance/tools/child-tools.js +65 -0
  151. package/dist/governance/tools/guard.d.ts +71 -0
  152. package/dist/governance/tools/guard.js +194 -0
  153. package/dist/governance/tools/inspector-tools.d.ts +63 -0
  154. package/dist/governance/tools/inspector-tools.js +424 -0
  155. package/dist/governance/tools/lead-tools.d.ts +42 -0
  156. package/dist/governance/tools/lead-tools.js +226 -0
  157. package/dist/governance/tools/normalize-util.d.ts +101 -0
  158. package/dist/governance/tools/normalize-util.js +230 -0
  159. package/dist/governance/tools/scan-incomplete.d.ts +79 -0
  160. package/dist/governance/tools/scan-incomplete.js +486 -0
  161. package/dist/governance/tools/shared-tools.d.ts +44 -0
  162. package/dist/governance/tools/shared-tools.js +125 -0
  163. package/dist/governance/tools/submit-normalizers.d.ts +28 -0
  164. package/dist/governance/tools/submit-normalizers.js +474 -0
  165. package/dist/governance/tools/submit-tools.d.ts +26 -0
  166. package/dist/governance/tools/submit-tools.js +171 -0
  167. package/dist/governance/types.d.ts +661 -0
  168. package/dist/governance/types.js +27 -0
  169. package/dist/session/validate-session.js +32 -1
  170. package/dist/team/runtime.d.ts +8 -0
  171. package/dist/team/runtime.js +9 -0
  172. package/dist/team/subagent.d.ts +23 -0
  173. package/dist/team/subagent.js +14 -3
  174. package/dist/team/types.d.ts +6 -0
  175. package/dist/tools/index.d.ts +30 -7
  176. package/dist/tools/index.js +14 -0
  177. package/dist/tools/plan-tools.d.ts +14 -2
  178. package/dist/tools/plan-tools.js +5 -2
  179. package/dist/ui/App.js +145 -25
  180. package/dist/ui/BottomStatusRow.js +1 -1
  181. package/dist/ui/Composer.d.ts +7 -1
  182. package/dist/ui/Composer.js +9 -6
  183. package/dist/ui/GovernancePanel.d.ts +50 -0
  184. package/dist/ui/GovernancePanel.js +77 -0
  185. package/dist/ui/GovernanceRun.d.ts +18 -0
  186. package/dist/ui/GovernanceRun.js +11 -0
  187. package/dist/ui/StatusBar.d.ts +15 -2
  188. package/dist/ui/StatusBar.js +10 -4
  189. package/dist/ui/Transcript.js +3 -0
  190. package/dist/ui/entries/GovernanceCard.d.ts +30 -0
  191. package/dist/ui/entries/GovernanceCard.js +32 -0
  192. package/dist/ui/governance-copy.d.ts +69 -0
  193. package/dist/ui/governance-copy.js +104 -0
  194. package/dist/ui/governance-status.d.ts +145 -0
  195. package/dist/ui/governance-status.js +326 -0
  196. package/dist/ui/interaction-copy.d.ts +4 -0
  197. package/dist/ui/interaction-copy.js +5 -0
  198. package/dist/ui/interaction-hints.d.ts +17 -0
  199. package/dist/ui/interaction-hints.js +14 -2
  200. package/dist/ui/layout/governance-board.d.ts +121 -0
  201. package/dist/ui/layout/governance-board.js +382 -0
  202. package/dist/ui/layout/governance-card.d.ts +53 -0
  203. package/dist/ui/layout/governance-card.js +134 -0
  204. package/dist/ui/layout/governance-chip.d.ts +38 -0
  205. package/dist/ui/layout/governance-chip.js +64 -0
  206. package/dist/ui/layout/governance-panel.d.ts +85 -0
  207. package/dist/ui/layout/governance-panel.js +358 -0
  208. package/dist/ui/layout/governance-roster.d.ts +65 -0
  209. package/dist/ui/layout/governance-roster.js +275 -0
  210. package/dist/ui/layout/status-detail-layout.js +3 -3
  211. package/dist/ui/layout/status-layout.d.ts +5 -2
  212. package/dist/ui/layout/status-layout.js +10 -6
  213. package/dist/ui/layout/virtual-window.js +11 -0
  214. package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
  215. package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
  216. package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
  217. package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
  218. package/dist/ui/overlays/HelpOverlay.js +4 -3
  219. package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
  220. package/dist/ui/overlays/QuestionOverlay.js +4 -2
  221. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  222. package/dist/ui/overlays/SettingsScreen.js +21 -1
  223. package/dist/ui/overlays/governance-settings.d.ts +79 -0
  224. package/dist/ui/overlays/governance-settings.js +140 -0
  225. package/dist/ui/overlays/model-profile-state.d.ts +6 -1
  226. package/dist/ui/overlays/model-profile-state.js +10 -0
  227. package/dist/ui/palettes.d.ts +17 -0
  228. package/dist/ui/palettes.js +9 -0
  229. package/dist/ui/scrollback/live-preview.js +2 -0
  230. package/dist/ui/scrollback/settled-boundary.js +3 -0
  231. package/dist/ui/shortcuts.js +2 -2
  232. package/dist/ui/theme.d.ts +7 -0
  233. package/dist/ui/theme.js +1 -0
  234. package/dist/ui/transcript-text.js +9 -0
  235. package/dist/ui/use-wheel-routing.js +3 -1
  236. package/package.json +2 -2
@@ -0,0 +1,644 @@
1
+ /**
2
+ * RunStore - the single writer of `<root>/<runId>/` (governance-mode section 12.1).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * RESPONSIBILITIES AND WHY THEY ARE ONE CLASS. Every write funnels through ONE FIFO mutex
7
+ * so that two concurrent engine paths (a submission landing while a stage-end checkpoint
8
+ * is being written) can never interleave their bytes or their sequence numbers.
9
+ * Cross-process exclusion is a separate mechanism (`run.lock`, see `run-lock.ts`).
10
+ *
11
+ * TRUTH VS CHECKPOINT. `events.jsonl` is the truth (append + fsync, one JSON row per
12
+ * line); `run.json` and `ledger.json` are atomic-replace checkpoints. `load()` compares
13
+ * the checkpoint `seq` values with the log tail and reports `needsReplay`; the actual
14
+ * fold of events into state is supplied by the engine as a callback, which keeps this
15
+ * file free of any dependency on `rules/ledger.ts` (I-G2 stays the ledger's business).
16
+ * Checkpoint writers must therefore stamp the CURRENT tail seq, otherwise `needsReplay`
17
+ * is true forever.
18
+ *
19
+ * SEQUENCES ARE ASSIGNED HERE, not by callers: the store caches the tail per run, so a
20
+ * caller can never reuse or skip a number. A row dropped by compaction consumes no seq,
21
+ * which keeps the checkpoint-vs-tail comparison honest.
22
+ *
23
+ * The root directory is a constructor argument (`getGovernanceDir()` lives in
24
+ * `config/app-paths.ts`); nothing here reads the environment.
25
+ */
26
+ import { join, resolve } from 'node:path';
27
+ import { GOV_LIMITS, GOV_SCHEMA_VERSION, RUN_ID_PATTERN } from '../limits.js';
28
+ import { appendLineFsync, atomicWriteFile, ensureDir, nodeGovFs, } from './atomic.js';
29
+ import { EVENTS_KEPT_AFTER_CAP, parseEventsText, serializeEvent } from './events-log.js';
30
+ import { acquireLock, defaultIsPidAlive, isLockLive, readLock, refreshLock, removeLock, } from './run-lock.js';
31
+ import { buildScoreboard } from './scoreboard-export.js';
32
+ const DAY_MS = 86_400_000;
33
+ const TERMINAL = new Set(['closed', 'abandoned']);
34
+ /** Programmer-facing store failures (the engine turns them into tool text). */
35
+ export class GovStoreError extends Error {
36
+ code;
37
+ constructor(code, message) {
38
+ super(message);
39
+ this.code = code;
40
+ this.name = 'GovStoreError';
41
+ }
42
+ }
43
+ /** FIFO async mutex. Never call `run` from inside a `run` callback (self-deadlock). */
44
+ class FifoMutex {
45
+ tail = Promise.resolve();
46
+ run(fn) {
47
+ const result = this.tail.then(fn);
48
+ this.tail = result.then(() => undefined, () => undefined);
49
+ return result;
50
+ }
51
+ }
52
+ /** Relative path inside a run folder: no absolute paths, no `..`, no NUL. */
53
+ function safeRelative(rel) {
54
+ const unified = rel.replace(/\\/g, '/');
55
+ const bad = unified === '' ||
56
+ unified.includes('\0') ||
57
+ unified.startsWith('/') ||
58
+ /^[a-zA-Z]:/.test(unified) ||
59
+ unified.split('/').some((segment) => segment === '..' || segment === '');
60
+ if (bad)
61
+ throw new GovStoreError('BAD_PATH', `unsafe relative path: ${JSON.stringify(rel)}`);
62
+ return unified;
63
+ }
64
+ function normalizeCwd(cwd, platform) {
65
+ const unified = resolve(cwd).replace(/\\/g, '/').replace(/\/+$/, '');
66
+ return platform === 'win32' ? unified.toLowerCase() : unified;
67
+ }
68
+ export class RunStore {
69
+ root;
70
+ fs;
71
+ platform;
72
+ now;
73
+ newId;
74
+ pid;
75
+ isPidAlive;
76
+ heartbeatMs;
77
+ sleep;
78
+ onLockLost;
79
+ mutex = new FifoMutex();
80
+ caches = new Map();
81
+ heartbeats = new Map();
82
+ heldLocks = new Set();
83
+ constructor(options) {
84
+ this.root = options.root;
85
+ this.fs = options.fs ?? nodeGovFs;
86
+ this.platform = options.platform ?? process.platform;
87
+ this.now = options.now ?? Date.now;
88
+ this.newId = options.newId ?? defaultSuffix;
89
+ this.pid = options.pid ?? process.pid;
90
+ this.isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
91
+ this.heartbeatMs = options.heartbeatMs ?? GOV_LIMITS.lockHeartbeatMs;
92
+ this.sleep = options.sleep;
93
+ this.onLockLost = options.onLockLost;
94
+ }
95
+ // -------------------------------------------------------------------------
96
+ // Paths and ids
97
+ // -------------------------------------------------------------------------
98
+ /** Validates the id first: a run id is the only caller-influenced path segment. */
99
+ runDir(runId) {
100
+ if (!RUN_ID_PATTERN.test(runId)) {
101
+ throw new GovStoreError('BAD_RUN_ID', `invalid run id: ${JSON.stringify(runId)}`);
102
+ }
103
+ return join(this.root, runId);
104
+ }
105
+ runFile(runId, rel) {
106
+ return join(this.runDir(runId), ...safeRelative(rel).split('/'));
107
+ }
108
+ /** "g" + base36(timeMs) + 3 random [a-z0-9]; retried until the folder is unused. */
109
+ async allocateRunId() {
110
+ for (let attempt = 0; attempt < 8; attempt++) {
111
+ const candidate = `g${this.now().toString(36)}${this.newId()}`;
112
+ if (!RUN_ID_PATTERN.test(candidate))
113
+ continue;
114
+ const taken = await this.fs.stat(join(this.root, candidate)).then(() => true, () => false);
115
+ if (!taken)
116
+ return candidate;
117
+ }
118
+ throw new GovStoreError('EXISTS', 'could not allocate a free run id');
119
+ }
120
+ // -------------------------------------------------------------------------
121
+ // Run creation and checkpoints
122
+ // -------------------------------------------------------------------------
123
+ /** Create the folder, `spec.md` (verbatim) and the first `run.json`; index the run. */
124
+ async createRun(input) {
125
+ const { state, spec } = input;
126
+ const dir = this.runDir(state.runId);
127
+ await this.mutex.run(async () => {
128
+ const exists = await this.fs.stat(join(dir, 'run.json')).then(() => true, () => false);
129
+ if (exists)
130
+ throw new GovStoreError('EXISTS', `run ${state.runId} already exists`);
131
+ await ensureDir(join(dir, 'artifacts'), this.fs);
132
+ await this.atomic(join(dir, 'spec.md'), spec);
133
+ await this.atomic(join(dir, 'run.json'), JSON.stringify(state, null, 2));
134
+ this.caches.set(state.runId, this.freshCache(state.seq));
135
+ await this.appendIndexUnlocked(state);
136
+ });
137
+ }
138
+ /**
139
+ * Atomic replace of `run.json`; appends an index row when the status changed OR, for a
140
+ * caller that supplies one, when the digest did. `index.jsonl` is append-only and
141
+ * `listRuns` keeps the newest row per run, so appending IS the update. A caller without a
142
+ * digest behaves byte-for-byte as before.
143
+ */
144
+ async writeRun(state, digest) {
145
+ const dir = this.runDir(state.runId);
146
+ await this.mutex.run(async () => {
147
+ const cache = await this.ensureCache(state.runId);
148
+ this.assertWritable(state.runId, cache);
149
+ await this.atomic(join(dir, 'run.json'), JSON.stringify(state, null, 2));
150
+ const digestChanged = digest !== undefined && JSON.stringify(digest) !== cache.indexedDigest;
151
+ if (cache.indexedStatus !== state.status || digestChanged) {
152
+ await this.appendIndexUnlocked(state, digest);
153
+ }
154
+ });
155
+ }
156
+ /** Atomic replace of `ledger.json`. The caller stamps `ledger.seq`. */
157
+ async writeLedger(runId, ledger) {
158
+ const dir = this.runDir(runId);
159
+ await this.mutex.run(async () => {
160
+ this.assertWritable(runId, await this.ensureCache(runId));
161
+ await this.atomic(join(dir, 'ledger.json'), JSON.stringify(ledger, null, 2));
162
+ });
163
+ }
164
+ /** One-way `token_ledger.py` v2.0 projection. Never read back. */
165
+ async exportScoreboard(runId, ledger) {
166
+ const dir = this.runDir(runId);
167
+ await this.mutex.run(async () => {
168
+ const text = JSON.stringify(buildScoreboard(ledger, this.now()), null, 2);
169
+ await this.atomic(join(dir, 'scoreboard.json'), text);
170
+ });
171
+ }
172
+ // -------------------------------------------------------------------------
173
+ // Events (the truth)
174
+ // -------------------------------------------------------------------------
175
+ /**
176
+ * Append one event and fsync. Returns the stored row, or `null` when compaction
177
+ * dropped it (past the byte cap only audit-only types are dropped).
178
+ */
179
+ async appendEvent(runId, input) {
180
+ this.runDir(runId);
181
+ return this.mutex.run(async () => {
182
+ const cache = await this.ensureCache(runId);
183
+ this.assertWritable(runId, cache);
184
+ const overCap = cache.eventsBytes >= GOV_LIMITS.eventsFileBytes;
185
+ if (overCap && !cache.compacted)
186
+ await this.writeCompactionMarker(runId, cache, input);
187
+ if (overCap && !EVENTS_KEPT_AFTER_CAP.has(input.type)) {
188
+ cache.droppedRows += 1;
189
+ return null;
190
+ }
191
+ return this.writeEventRow(runId, cache, input);
192
+ });
193
+ }
194
+ async writeCompactionMarker(runId, cache, trigger) {
195
+ cache.compacted = true;
196
+ await this.writeEventRow(runId, cache, {
197
+ type: 'compacted',
198
+ actor: 'engine',
199
+ payload: {
200
+ reason: 'events.jsonl passed its byte budget; audit-only rows are dropped from here',
201
+ limitBytes: GOV_LIMITS.eventsFileBytes,
202
+ keptTypes: [...EVENTS_KEPT_AFTER_CAP],
203
+ firstTriggerType: trigger.type,
204
+ },
205
+ });
206
+ }
207
+ async writeEventRow(runId, cache, input) {
208
+ const row = {
209
+ seq: input.seq ?? cache.lastSeq + 1,
210
+ at: input.at ?? this.now(),
211
+ type: input.type,
212
+ actor: input.actor,
213
+ payload: input.payload,
214
+ };
215
+ const line = serializeEvent(row);
216
+ await appendLineFsync(this.runFile(runId, 'events.jsonl'), line, {
217
+ fs: this.fs,
218
+ leadingNewline: cache.tornTail,
219
+ });
220
+ cache.lastSeq = Math.max(cache.lastSeq, row.seq);
221
+ cache.tornTail = false;
222
+ cache.eventsBytes += Buffer.byteLength(line) + 1;
223
+ return row;
224
+ }
225
+ /** Rows with `seq > afterSeq`, in file order. Missing file reads as empty. */
226
+ async readEvents(runId, afterSeq = 0) {
227
+ const parsed = await this.parseEvents(runId);
228
+ return parsed.events.filter((event) => event.seq > afterSeq);
229
+ }
230
+ /** How many rows compaction dropped in this process (the marker row says why). */
231
+ droppedEventCount(runId) {
232
+ return this.caches.get(runId)?.droppedRows ?? 0;
233
+ }
234
+ async parseEvents(runId) {
235
+ const text = await this.fs.readFile(this.runFile(runId, 'events.jsonl')).catch(() => '');
236
+ return { ...parseEventsText(text), bytes: Buffer.byteLength(text) };
237
+ }
238
+ // -------------------------------------------------------------------------
239
+ // Load / replay
240
+ // -------------------------------------------------------------------------
241
+ /** Read both checkpoints and the log tail; optionally repair via `options.replay`. */
242
+ async load(runId, options = {}) {
243
+ this.runDir(runId);
244
+ const problems = [];
245
+ const parsed = await this.parseEvents(runId);
246
+ let run = await this.readCheckpoint(runId, 'run.json', problems);
247
+ let ledger = await this.readCheckpoint(runId, 'ledger.json', problems);
248
+ const readOnly = this.isNewerSchema(run) || this.isNewerSchema(ledger);
249
+ const cache = await this.ensureCache(runId);
250
+ cache.readOnly = readOnly;
251
+ if (run)
252
+ cache.indexedStatus = cache.indexedStatus ?? run.status;
253
+ if (parsed.tornTail)
254
+ problems.push('events.jsonl ends with a torn row (crash during append)');
255
+ if (parsed.badLines > 0) {
256
+ problems.push(`events.jsonl: ${parsed.badLines} unreadable row(s) skipped`);
257
+ }
258
+ const eventsUnreadable = parsed.bytes > 0 && parsed.events.length === 0;
259
+ const needsReplay = this.checkpointsBehind(run, ledger, parsed.lastSeq);
260
+ let replayed = false;
261
+ if (needsReplay && options.replay && !readOnly && !eventsUnreadable) {
262
+ const out = options.replay({ run, ledger, events: parsed.events, tailSeq: parsed.lastSeq });
263
+ if (out.run !== undefined)
264
+ run = out.run;
265
+ if (out.ledger !== undefined)
266
+ ledger = out.ledger;
267
+ await this.writeRepaired(runId, { run, ledger });
268
+ replayed = true;
269
+ }
270
+ return {
271
+ run,
272
+ ledger,
273
+ tailSeq: parsed.lastSeq,
274
+ needsReplay: needsReplay && !replayed,
275
+ replayed,
276
+ readOnly,
277
+ eventsUnreadable,
278
+ problems,
279
+ };
280
+ }
281
+ isNewerSchema(value) {
282
+ return value !== null && value.schemaVersion > GOV_SCHEMA_VERSION;
283
+ }
284
+ checkpointsBehind(run, ledger, tailSeq) {
285
+ if (run === null || ledger === null)
286
+ return tailSeq > 0 || run === null;
287
+ return run.seq < tailSeq || ledger.seq < tailSeq;
288
+ }
289
+ async readCheckpoint(runId, name, problems) {
290
+ let text;
291
+ try {
292
+ text = await this.fs.readFile(this.runFile(runId, name));
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ try {
298
+ return JSON.parse(text);
299
+ }
300
+ catch {
301
+ problems.push(`${name} is corrupt (unparseable); it will be rebuilt from events.jsonl`);
302
+ return null;
303
+ }
304
+ }
305
+ async writeRepaired(runId, repaired) {
306
+ if (repaired.run)
307
+ await this.writeRun(repaired.run);
308
+ if (repaired.ledger)
309
+ await this.writeLedger(runId, repaired.ledger);
310
+ }
311
+ // -------------------------------------------------------------------------
312
+ // Lock
313
+ // -------------------------------------------------------------------------
314
+ lockDeps() {
315
+ return {
316
+ fs: this.fs,
317
+ now: this.now,
318
+ pid: this.pid,
319
+ isPidAlive: this.isPidAlive,
320
+ staleMs: GOV_LIMITS.lockStaleMs,
321
+ };
322
+ }
323
+ /** Take `run.lock` (stale locks are taken over). Refusal text names the holder pid. */
324
+ async acquireLock(runId) {
325
+ // No mkdir: locking a run whose folder does not exist is a caller bug (ENOENT), and
326
+ // creating the folder here would conjure an empty run directory.
327
+ const result = await acquireLock(this.runFile(runId, 'run.lock'), this.lockDeps());
328
+ if (result.ok)
329
+ this.heldLocks.add(runId);
330
+ return result;
331
+ }
332
+ /** Start the unref'd heartbeat timer; idempotent. */
333
+ startHeartbeat(runId) {
334
+ if (this.heartbeats.has(runId))
335
+ return;
336
+ const timer = setInterval(() => {
337
+ void this.heartbeatOnce(runId);
338
+ }, this.heartbeatMs);
339
+ timer.unref();
340
+ this.heartbeats.set(runId, timer);
341
+ }
342
+ stopHeartbeat(runId) {
343
+ const timer = this.heartbeats.get(runId);
344
+ if (timer)
345
+ clearInterval(timer);
346
+ this.heartbeats.delete(runId);
347
+ }
348
+ /** One heartbeat tick (public so tests need no fake timers). False = lock lost. */
349
+ async heartbeatOnce(runId) {
350
+ let alive = false;
351
+ try {
352
+ alive = await refreshLock(this.runFile(runId, 'run.lock'), this.lockDeps());
353
+ }
354
+ catch {
355
+ return true;
356
+ }
357
+ if (!alive) {
358
+ this.stopHeartbeat(runId);
359
+ this.heldLocks.delete(runId);
360
+ this.onLockLost?.(runId);
361
+ }
362
+ return alive;
363
+ }
364
+ async releaseLock(runId) {
365
+ this.stopHeartbeat(runId);
366
+ this.heldLocks.delete(runId);
367
+ await removeLock(this.runFile(runId, 'run.lock'), this.lockDeps());
368
+ }
369
+ /** Stop every timer and release every lock this process holds. Safe to call twice. */
370
+ async dispose() {
371
+ for (const runId of [...this.heartbeats.keys()])
372
+ this.stopHeartbeat(runId);
373
+ for (const runId of [...this.heldLocks])
374
+ await this.releaseLock(runId);
375
+ }
376
+ async isRunLocked(runId) {
377
+ const info = await readLock(this.runFile(runId, 'run.lock'), this.fs);
378
+ return isLockLive(info, this.lockDeps()) || (info !== null && info.pid === this.pid);
379
+ }
380
+ // -------------------------------------------------------------------------
381
+ // Run files and artifacts
382
+ // -------------------------------------------------------------------------
383
+ /** Atomic write of a projection (`board.md`, `reports/phase-1.md`). Not budgeted. */
384
+ async writeRunFile(runId, rel, text) {
385
+ const path = this.runFile(runId, rel);
386
+ await this.mutex.run(async () => {
387
+ this.assertWritable(runId, await this.ensureCache(runId));
388
+ await this.atomic(path, text);
389
+ });
390
+ }
391
+ async readRunFile(runId, rel) {
392
+ return this.fs.readFile(this.runFile(runId, rel)).catch(() => null);
393
+ }
394
+ async listRunDir(runId, rel) {
395
+ const dir = rel === '' ? this.runDir(runId) : this.runFile(runId, rel);
396
+ const names = await this.fs.readdir(dir).catch(() => []);
397
+ const entries = [];
398
+ for (const name of names.sort()) {
399
+ const stat = await this.fs.stat(join(dir, name)).catch(() => null);
400
+ if (stat)
401
+ entries.push({ name, isDirectory: stat.isDirectory(), size: stat.size });
402
+ }
403
+ return entries;
404
+ }
405
+ /**
406
+ * Write under `artifacts/`, enforcing the per-run byte budget. The budget is a refusal
407
+ * (never a silent drop): the caller reports `NOT ACCEPTED: run storage budget exhausted`
408
+ * and the engine escalates the run.
409
+ */
410
+ async writeArtifactFile(runId, rel, text) {
411
+ const path = this.runFile(runId, `artifacts/${safeRelative(rel)}`);
412
+ return this.mutex.run(async () => {
413
+ const cache = await this.ensureCache(runId);
414
+ this.assertWritable(runId, cache);
415
+ const used = cache.artifactBytes ?? (await this.measureArtifacts(runId));
416
+ const bytes = Buffer.byteLength(text);
417
+ const previous = await this.fs.stat(path).then((s) => s.size, () => 0);
418
+ const total = used - previous + bytes;
419
+ if (total > GOV_LIMITS.artifactBytesPerRun) {
420
+ cache.artifactBytes = used;
421
+ return {
422
+ ok: false,
423
+ exhausted: true,
424
+ message: 'NOT ACCEPTED: run storage budget exhausted',
425
+ };
426
+ }
427
+ await this.atomic(path, text);
428
+ cache.artifactBytes = total;
429
+ return { ok: true, bytes, totalBytes: total };
430
+ });
431
+ }
432
+ async measureArtifacts(runId) {
433
+ const walk = async (dir) => {
434
+ let sum = 0;
435
+ for (const name of await this.fs.readdir(dir).catch(() => [])) {
436
+ const stat = await this.fs.stat(join(dir, name)).catch(() => null);
437
+ if (!stat)
438
+ continue;
439
+ sum += stat.isDirectory() ? await walk(join(dir, name)) : stat.size;
440
+ }
441
+ return sum;
442
+ };
443
+ return walk(this.runFile(runId, 'artifacts'));
444
+ }
445
+ // -------------------------------------------------------------------------
446
+ // Index, listing, pruning
447
+ // -------------------------------------------------------------------------
448
+ get indexPath() {
449
+ return join(this.root, 'index.jsonl');
450
+ }
451
+ async appendIndexUnlocked(state, digest) {
452
+ const entry = {
453
+ runId: state.runId,
454
+ cwd: state.cwd,
455
+ ...(state.sessionId ? { sessionId: state.sessionId } : {}),
456
+ goal: state.goal,
457
+ status: state.status,
458
+ createdAt: state.createdAt,
459
+ updatedAt: state.updatedAt,
460
+ ...(digest !== undefined ? { digest } : {}),
461
+ };
462
+ await appendLineFsync(this.indexPath, JSON.stringify(entry), { fs: this.fs });
463
+ const cache = this.caches.get(state.runId);
464
+ if (cache) {
465
+ cache.indexedStatus = state.status;
466
+ cache.indexedDigest = digest === undefined ? null : JSON.stringify(digest);
467
+ }
468
+ }
469
+ async readIndexUnlocked() {
470
+ const text = await this.fs.readFile(this.indexPath).catch(() => '');
471
+ const latest = new Map();
472
+ for (const line of text.split('\n')) {
473
+ if (line.trim() === '')
474
+ continue;
475
+ try {
476
+ const row = JSON.parse(line);
477
+ if (typeof row.runId === 'string' && RUN_ID_PATTERN.test(row.runId)) {
478
+ const digest = parseRunDigest(row.digest);
479
+ // A hand-edited or damaged digest is dropped, never trusted: the row still lists.
480
+ const { digest: _raw, ...rest } = row;
481
+ latest.set(row.runId, digest === undefined ? rest : { ...rest, digest });
482
+ }
483
+ }
484
+ catch {
485
+ // A torn index row is skipped; the run folder is still on disk.
486
+ }
487
+ }
488
+ return [...latest.values()];
489
+ }
490
+ /** Latest row per run, newest first. */
491
+ async listRuns() {
492
+ const rows = await this.readIndexUnlocked();
493
+ return rows.sort((a, b) => b.updatedAt - a.updatedAt);
494
+ }
495
+ /**
496
+ * Newest run of ANY status for `cwd` whose folder still exists. `/gov report` uses this
497
+ * rather than `latestResumable`: the report people ask for most is the one of the run
498
+ * that just closed, and a closed run is by definition not resumable.
499
+ */
500
+ async latestForCwd(cwd) {
501
+ const wanted = normalizeCwd(cwd, this.platform);
502
+ for (const row of await this.listRuns()) {
503
+ if (normalizeCwd(row.cwd, this.platform) !== wanted)
504
+ continue;
505
+ const present = await this.fs.stat(this.runDir(row.runId)).then(() => true, () => false);
506
+ if (present)
507
+ return row;
508
+ }
509
+ return null;
510
+ }
511
+ /** Newest unfinished run for `cwd` whose folder still exists. */
512
+ async latestResumable(cwd) {
513
+ const wanted = normalizeCwd(cwd, this.platform);
514
+ for (const row of await this.listRuns()) {
515
+ if (TERMINAL.has(row.status))
516
+ continue;
517
+ if (normalizeCwd(row.cwd, this.platform) !== wanted)
518
+ continue;
519
+ const present = await this.fs.stat(this.runDir(row.runId)).then(() => true, () => false);
520
+ if (present)
521
+ return row;
522
+ }
523
+ return null;
524
+ }
525
+ /**
526
+ * Delete closed / abandoned runs older than the retention window. Best effort and
527
+ * bounded (`pruneMaxPerPass`); a locked run is never touched. Never throws.
528
+ */
529
+ async pruneOld() {
530
+ const result = { pruned: [], skippedLocked: [], failed: [] };
531
+ try {
532
+ await this.mutex.run(() => this.pruneUnlocked(result));
533
+ }
534
+ catch {
535
+ // Pruning is housekeeping: a failure here must never reach the user.
536
+ }
537
+ return result;
538
+ }
539
+ async pruneUnlocked(result) {
540
+ const cutoff = this.now() - GOV_LIMITS.retentionDays * DAY_MS;
541
+ const rows = await this.readIndexUnlocked();
542
+ const candidates = rows
543
+ .filter((row) => TERMINAL.has(row.status) && row.updatedAt < cutoff)
544
+ .sort((a, b) => a.updatedAt - b.updatedAt)
545
+ .slice(0, GOV_LIMITS.pruneMaxPerPass);
546
+ for (const row of candidates) {
547
+ if (await this.isRunLocked(row.runId)) {
548
+ result.skippedLocked.push(row.runId);
549
+ continue;
550
+ }
551
+ try {
552
+ await this.fs.rm(this.runDir(row.runId), { recursive: true, force: true });
553
+ this.caches.delete(row.runId);
554
+ result.pruned.push(row.runId);
555
+ }
556
+ catch {
557
+ result.failed.push(row.runId);
558
+ }
559
+ }
560
+ if (result.pruned.length === 0)
561
+ return;
562
+ const gone = new Set(result.pruned);
563
+ const kept = rows.filter((row) => !gone.has(row.runId));
564
+ const text = kept.map((row) => JSON.stringify(row)).join('\n');
565
+ await this.atomic(this.indexPath, text === '' ? '' : `${text}\n`);
566
+ }
567
+ // -------------------------------------------------------------------------
568
+ // Internals
569
+ // -------------------------------------------------------------------------
570
+ atomic(path, data) {
571
+ return atomicWriteFile(path, data, {
572
+ fs: this.fs,
573
+ platform: this.platform,
574
+ ...(this.sleep ? { sleep: this.sleep } : {}),
575
+ });
576
+ }
577
+ freshCache(lastSeq) {
578
+ return {
579
+ lastSeq,
580
+ eventsBytes: 0,
581
+ tornTail: false,
582
+ compacted: false,
583
+ droppedRows: 0,
584
+ artifactBytes: null,
585
+ readOnly: false,
586
+ indexedStatus: null,
587
+ indexedDigest: null,
588
+ };
589
+ }
590
+ assertWritable(runId, cache) {
591
+ if (cache.readOnly) {
592
+ throw new GovStoreError('READ_ONLY', `run ${runId} was written by a newer CLI version and is read-only here`);
593
+ }
594
+ }
595
+ /** Lazily derive the tail seq / byte count from disk, once per run per process. */
596
+ async ensureCache(runId) {
597
+ const known = this.caches.get(runId);
598
+ if (known)
599
+ return known;
600
+ const parsed = await this.parseEvents(runId);
601
+ const existing = this.caches.get(runId);
602
+ if (existing)
603
+ return existing;
604
+ const cache = this.freshCache(parsed.lastSeq);
605
+ cache.eventsBytes = parsed.bytes;
606
+ cache.tornTail = parsed.tornTail;
607
+ cache.compacted = parsed.hasCompacted;
608
+ this.caches.set(runId, cache);
609
+ return cache;
610
+ }
611
+ }
612
+ function finiteNumber(value) {
613
+ return typeof value === 'number' && Number.isFinite(value);
614
+ }
615
+ /** Validate an index row's digest field; anything malformed reads as "no digest". */
616
+ export function parseRunDigest(raw) {
617
+ if (typeof raw !== 'object' || raw === null)
618
+ return undefined;
619
+ const d = raw;
620
+ const health = d.health === null ? null : d.health;
621
+ const costUsd = d.costUsd === null ? null : d.costUsd;
622
+ const ok = finiteNumber(d.phase) && finiteNumber(d.phaseCount) && finiteNumber(d.agents) &&
623
+ finiteNumber(d.tokens) && finiteNumber(d.dispatches) && finiteNumber(d.eliminations) &&
624
+ (health === null || finiteNumber(health)) && (costUsd === null || finiteNumber(costUsd));
625
+ if (!ok)
626
+ return undefined;
627
+ return {
628
+ phase: d.phase,
629
+ phaseCount: d.phaseCount,
630
+ agents: d.agents,
631
+ health: health,
632
+ tokens: d.tokens,
633
+ costUsd: costUsd,
634
+ dispatches: d.dispatches,
635
+ eliminations: d.eliminations,
636
+ };
637
+ }
638
+ function defaultSuffix() {
639
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
640
+ let out = '';
641
+ for (let i = 0; i < 3; i++)
642
+ out += alphabet[Math.floor(Math.random() * alphabet.length)];
643
+ return out;
644
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One-way export of the ledger in the `token_ledger.py` v2.0 field layout
3
+ * (governance-mode section 12.3).
4
+ *
5
+ * ASCII ONLY (`governance/` is glyph-scanned).
6
+ *
7
+ * The point is that a user can point the reference skill's scripts at
8
+ * `scoreboard.json` for ad-hoc analysis. It is NEVER read back: the ledger of record is
9
+ * `events.jsonl` (+ `ledger.json`), and a second readable copy would be a second source
10
+ * of truth that could drift. Python's vocabulary only knows `provisional` and
11
+ * `confirmed` as history statuses, so `immediate` positives (which land in `confirmed`,
12
+ * RV-9) are exported as `confirmed`.
13
+ */
14
+ import type { LedgerState } from '../types.js';
15
+ export declare const SCOREBOARD_VERSION = "2.0";
16
+ interface ScoreboardHistoryRow {
17
+ event: string;
18
+ delta: number;
19
+ status: 'provisional' | 'confirmed';
20
+ balance_after: number;
21
+ confirmed_after: number;
22
+ provisional_after: number;
23
+ reason: string;
24
+ timestamp: string;
25
+ }
26
+ interface ScoreboardEscrowRow {
27
+ action: 'hold' | 'confirm' | 'clawback';
28
+ phase: number;
29
+ delta: number;
30
+ reason: string;
31
+ timestamp: string;
32
+ }
33
+ export interface ScoreboardAgent {
34
+ role: string;
35
+ generation: number;
36
+ tokens: number;
37
+ tokens_confirmed: number;
38
+ tokens_provisional: number;
39
+ status: 'active' | 'eliminated';
40
+ replaced_by: string | null;
41
+ history: ScoreboardHistoryRow[];
42
+ escrow_history: ScoreboardEscrowRow[];
43
+ }
44
+ export interface ScoreboardFile {
45
+ version: string;
46
+ generated_by: string;
47
+ generated_at: string;
48
+ agents: Record<string, ScoreboardAgent>;
49
+ phase_reports: unknown[];
50
+ }
51
+ /** Build the export document. Pure: `now` is injected so the output is reproducible. */
52
+ export declare function buildScoreboard(ledger: LedgerState, now: number): ScoreboardFile;
53
+ export {};