@astrosheep/keiyaku 2.9.7 → 2.9.8

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 (52) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  5. package/build/agents/providers/codex-app-server/session.js +8 -7
  6. package/build/agents/selector.js +12 -1
  7. package/build/cli/commands/akuma/view/handler.js +3 -11
  8. package/build/cli/commands/contract/amend/handler.js +1 -1
  9. package/build/cli/commands/contract/amend/meta.js +4 -4
  10. package/build/cli/commands/projection/status/handler.js +5 -4
  11. package/build/cli/commands/projection/status/meta.js +2 -2
  12. package/build/cli/commands/task/add/meta.js +9 -1
  13. package/build/cli/commands/task/shared.js +2 -1
  14. package/build/cli/completion.js +8 -0
  15. package/build/cli/render/kanshi.js +2 -2
  16. package/build/cli/render/path-prefix-compaction.js +119 -76
  17. package/build/cli/render/projection-activity.js +10 -1
  18. package/build/cli/render/shared.js +8 -7
  19. package/build/cli/render/status.js +8 -5
  20. package/build/cli/render/wait.js +1 -1
  21. package/build/config/settings/disease.js +4 -4
  22. package/build/config/settings/loader.js +44 -21
  23. package/build/core/addressing.js +40 -9
  24. package/build/core/amend.js +21 -5
  25. package/build/core/call/context.js +19 -3
  26. package/build/core/call/execution.js +43 -20
  27. package/build/core/ledger-batch.js +194 -0
  28. package/build/core/projection/generation/database.js +22 -0
  29. package/build/core/projection/generation/projection-generation-execution.js +45 -37
  30. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  31. package/build/core/projection/generation/projection-generation-process.js +3 -1
  32. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  33. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  34. package/build/core/projection/generation/store.js +17 -1
  35. package/build/core/projection/generation/transitions.js +89 -12
  36. package/build/core/projection/index.js +3 -3
  37. package/build/core/projection/projection-kill.js +22 -10
  38. package/build/core/projection/projection-runner-lock.js +177 -37
  39. package/build/core/projection/projection-status.js +143 -55
  40. package/build/core/projection/projection-wake.js +171 -72
  41. package/build/core/status/board.js +42 -4
  42. package/build/core/status/drift.js +21 -5
  43. package/build/core/status/ledger-batch.js +1 -158
  44. package/build/core/task/task-git-runtime.js +8 -10
  45. package/build/core/task/task-git-store.js +5 -3
  46. package/build/core/worktree-path.js +39 -25
  47. package/build/flow-error.js +1 -1
  48. package/build/generated/version.js +2 -2
  49. package/build/git/refs.js +47 -1
  50. package/package.json +1 -1
  51. package/skills/keiyaku-akuma/SKILL.md +18 -0
  52. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -40,6 +40,95 @@ function readJournalMode(database) {
40
40
  function assertCurrentLockSchema(database) {
41
41
  database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
42
42
  }
43
+ function assertBusyTimeout(busyTimeoutMs) {
44
+ if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0) {
45
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock busy timeout must be a non-negative safe integer");
46
+ }
47
+ }
48
+ function openGuard(lockPath, busyTimeoutMs) {
49
+ const database = new DatabaseSync(lockPath);
50
+ try {
51
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
52
+ database.exec("BEGIN DEFERRED");
53
+ assertCurrentLockSchema(database);
54
+ if (readJournalMode(database) !== "delete") {
55
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
56
+ }
57
+ return database;
58
+ }
59
+ catch (error) {
60
+ closeTransaction(database);
61
+ throw error;
62
+ }
63
+ }
64
+ function openReservation(lockPath, busyTimeoutMs) {
65
+ const database = new DatabaseSync(lockPath);
66
+ try {
67
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
68
+ database.exec("BEGIN IMMEDIATE");
69
+ return database;
70
+ }
71
+ catch (error) {
72
+ closeTransaction(database);
73
+ throw error;
74
+ }
75
+ }
76
+ function closeTransaction(database) {
77
+ let failure;
78
+ try {
79
+ database.exec("ROLLBACK");
80
+ }
81
+ catch (error) {
82
+ failure = error;
83
+ }
84
+ try {
85
+ database.close();
86
+ }
87
+ catch (error) {
88
+ failure ??= error;
89
+ }
90
+ return failure;
91
+ }
92
+ function createHeldLock(lockPath, busyTimeoutMs, guard, initialReservation) {
93
+ let closed = false;
94
+ let reservation = initialReservation;
95
+ return {
96
+ path: lockPath,
97
+ releaseReservationForTransfer() {
98
+ if (closed || reservation === undefined)
99
+ return;
100
+ const released = reservation;
101
+ reservation = undefined;
102
+ const failure = closeTransaction(released);
103
+ if (failure !== undefined) {
104
+ throw new ProjectionRunnerLockUnavailableError(`projection runner reservation release failed: ${errorDetail(failure)}`, { cause: failure });
105
+ }
106
+ },
107
+ reclaimReservation() {
108
+ if (closed || reservation !== undefined)
109
+ return;
110
+ try {
111
+ reservation = openReservation(lockPath, busyTimeoutMs);
112
+ }
113
+ catch (error) {
114
+ throw new ProjectionRunnerLockUnavailableError(`projection runner reservation reclaim failed: ${errorDetail(error)}`, { cause: error });
115
+ }
116
+ },
117
+ close() {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ let failure;
122
+ if (reservation !== undefined)
123
+ failure = closeTransaction(reservation);
124
+ const guardFailure = closeTransaction(guard);
125
+ failure ??= guardFailure;
126
+ if (failure !== undefined) {
127
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(failure)}`, { cause: failure });
128
+ }
129
+ },
130
+ };
131
+ }
43
132
  function closeAfterFailedAcquire(database, error) {
44
133
  try {
45
134
  database.close();
@@ -95,13 +184,12 @@ export function initializeProjectionRunnerLockDatabase(projectionDirectory, plat
95
184
  return lockPath;
96
185
  }
97
186
  /**
98
- * Open the already initialized projection-local lock database and hold one
99
- * rollback-mode EXCLUSIVE transaction until close() or process death. SQLite
100
- * requires a writable connection for this write-grade lock; the preceding
101
- * regular-file check prevents ordinary missing storage from being created.
102
- * Database bytes and timestamps are deliberately absent from the returned authority.
187
+ * Hold one rollback-mode SHARED guard plus the sole RESERVED claim until
188
+ * close() or process death. An EXCLUSIVE probe cannot pass either component.
189
+ * During handoff the child overlaps the guard before the RESERVED claim moves,
190
+ * so kernel exclusion remains continuous without transferring a connection.
103
191
  */
104
- export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform) {
192
+ export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
105
193
  if (!isProjectionRunnerLockPlatformSupported(platform)) {
106
194
  throw new ProjectionRunnerLockUnsupportedError(platform);
107
195
  }
@@ -116,30 +204,96 @@ export function acquireProjectionRunnerLock(projectionDirectory, platform = proc
116
204
  throw error;
117
205
  throw new ProjectionRunnerLockUnavailableError(`projection runner lock database is not initialized: ${errorDetail(error)}`, { cause: error });
118
206
  }
119
- let database;
207
+ assertBusyTimeout(busyTimeoutMs);
208
+ let guard;
120
209
  try {
121
- database = new DatabaseSync(lockPath);
210
+ guard = openGuard(lockPath, busyTimeoutMs);
122
211
  }
123
212
  catch (error) {
124
213
  throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be opened: ${errorDetail(error)}`, { cause: error });
125
214
  }
215
+ let reservation;
126
216
  try {
127
- database.exec("PRAGMA busy_timeout=0");
217
+ reservation = openReservation(lockPath, busyTimeoutMs);
218
+ }
219
+ catch (error) {
220
+ if (reservation)
221
+ closeTransaction(reservation);
222
+ closeTransaction(guard);
223
+ return closeAfterFailedAcquire(guard, error);
224
+ }
225
+ return createHeldLock(lockPath, busyTimeoutMs, guard, reservation);
226
+ }
227
+ export function acquireProjectionRunnerTransferTarget(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
228
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
229
+ throw new ProjectionRunnerLockUnsupportedError(platform);
230
+ }
231
+ assertBusyTimeout(busyTimeoutMs);
232
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
233
+ let guard;
234
+ try {
235
+ guard = openGuard(lockPath, busyTimeoutMs);
236
+ }
237
+ catch (error) {
238
+ if (guard)
239
+ closeTransaction(guard);
240
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer acquisition failed: ${errorDetail(error)}`, { cause: error });
241
+ }
242
+ let closed = false;
243
+ return {
244
+ path: lockPath,
245
+ promote() {
246
+ if (closed)
247
+ throw new ProjectionRunnerLockUnavailableError("projection runner transfer target is closed");
248
+ let reservation;
249
+ try {
250
+ reservation = openReservation(lockPath, busyTimeoutMs);
251
+ }
252
+ catch (error) {
253
+ if (reservation)
254
+ closeTransaction(reservation);
255
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer promotion failed: ${errorDetail(error)}`, { cause: error });
256
+ }
257
+ closed = true;
258
+ return createHeldLock(lockPath, busyTimeoutMs, guard, reservation);
259
+ },
260
+ close() {
261
+ if (closed)
262
+ return;
263
+ closed = true;
264
+ const failure = closeTransaction(guard);
265
+ if (failure !== undefined) {
266
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer release failed: ${errorDetail(failure)}`, { cause: failure });
267
+ }
268
+ },
269
+ };
270
+ }
271
+ /** Acquire positive death evidence. Any active guard or transfer blocks this. */
272
+ export function acquireProjectionRunnerSettlementLock(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
273
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
274
+ throw new ProjectionRunnerLockUnsupportedError(platform);
275
+ }
276
+ assertBusyTimeout(busyTimeoutMs);
277
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
278
+ let database;
279
+ try {
280
+ database = new DatabaseSync(lockPath);
281
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
128
282
  database.exec("BEGIN EXCLUSIVE");
283
+ assertCurrentLockSchema(database);
129
284
  if (readJournalMode(database) !== "delete") {
130
285
  throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
131
286
  }
132
- assertCurrentLockSchema(database);
133
287
  }
134
288
  catch (error) {
135
289
  try {
136
- database.exec("ROLLBACK");
290
+ database.close();
137
291
  }
138
- catch {
139
- // A failure before BEGIN has no transaction; a failure after BEGIN is
140
- // still released by closeAfterFailedAcquire if rollback cannot run.
292
+ catch { }
293
+ if (isSqliteBusy(error)) {
294
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock is already held", { cause: error });
141
295
  }
142
- return closeAfterFailedAcquire(database, error);
296
+ throw new ProjectionRunnerLockUnavailableError(`projection runner settlement lock acquisition failed: ${errorDetail(error)}`, { cause: error });
143
297
  }
144
298
  let closed = false;
145
299
  return {
@@ -148,31 +302,17 @@ export function acquireProjectionRunnerLock(projectionDirectory, platform = proc
148
302
  if (closed)
149
303
  return;
150
304
  closed = true;
151
- let rollbackError;
152
- try {
153
- database.exec("ROLLBACK");
154
- }
155
- catch (error) {
156
- rollbackError = error;
157
- }
158
- try {
159
- database.close();
160
- }
161
- catch (error) {
162
- rollbackError ??= error;
163
- }
164
- if (rollbackError !== undefined) {
165
- throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(rollbackError)}`, { cause: rollbackError });
305
+ const failure = closeTransaction(database);
306
+ if (failure !== undefined) {
307
+ throw new ProjectionRunnerLockUnavailableError(`projection runner settlement lock release failed: ${errorDetail(failure)}`, { cause: failure });
166
308
  }
167
309
  },
168
310
  };
169
311
  }
170
312
  /**
171
- * Probe without creating the lock database. BEGIN DEFERRED itself acquires no
172
- * file lock, so the sqlite_schema SELECT is mandatory: SQLITE_BUSY there is
173
- * positive evidence of the runner's EXCLUSIVE transaction. Successful reads
174
- * hold a compatible SHARED transaction only long enough to verify DELETE
175
- * journal mode, then roll back. Every other failure remains unknown.
313
+ * Probe without creating the lock database. BEGIN EXCLUSIVE is the positive
314
+ * death test: SQLITE_BUSY proves a SHARED guard or RESERVED owner remains;
315
+ * success proves both are gone. Every other failure remains unknown.
176
316
  */
177
317
  export function observeProjectionRunnerLock(projectionDirectory, platform = process.platform) {
178
318
  if (!isProjectionRunnerLockPlatformSupported(platform)) {
@@ -185,7 +325,7 @@ export function observeProjectionRunnerLock(projectionDirectory, platform = proc
185
325
  const lockPath = projectionRunnerLockPath(projectionDirectory);
186
326
  let database;
187
327
  try {
188
- database = new DatabaseSync(lockPath, { readOnly: true });
328
+ database = new DatabaseSync(lockPath);
189
329
  }
190
330
  catch (error) {
191
331
  return { state: "unknown", reason: "open-failed", detail: errorDetail(error) };
@@ -193,7 +333,7 @@ export function observeProjectionRunnerLock(projectionDirectory, platform = proc
193
333
  let transactionOpen = false;
194
334
  try {
195
335
  database.exec("PRAGMA busy_timeout=0");
196
- database.exec("BEGIN DEFERRED");
336
+ database.exec("BEGIN EXCLUSIVE");
197
337
  transactionOpen = true;
198
338
  assertCurrentLockSchema(database);
199
339
  const journalMode = readJournalMode(database);
@@ -1,4 +1,4 @@
1
- import * as fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isErrnoException } from "../../errno.js";
4
4
  import { executionEventsPath, isProjectionId, projectionRoot, } from "./projection-core.js";
@@ -9,7 +9,7 @@ import { readProjectionIdentity } from "./projection-identity.js";
9
9
  import { readProjectionTellCounts } from "./tell/store.js";
10
10
  import { ProjectionGenerationStoreError, } from "./generation/store.js";
11
11
  import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
12
- import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
12
+ import { observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
13
13
  import { selectCurrentGeneration, } from "./projection-life-protocol.js";
14
14
  import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents } from "../stored-agent-event.js";
15
15
  import { worktreeContainsCoordinate } from "../worktree-path.js";
@@ -63,10 +63,8 @@ function compareProjectionStatusRows(a, b) {
63
63
  }
64
64
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
65
65
  }
66
- export function prioritizeProjectionStatusRows(rows, showAll) {
66
+ export function prioritizeProjectionStatusRows(rows) {
67
67
  const ordered = [...rows].sort(compareProjectionStatusRows);
68
- if (showAll)
69
- return { rows: ordered };
70
68
  const visible = ordered.filter((row) => !isOmissibleProjectionStatusState(row.state));
71
69
  const byState = {
72
70
  failed: 0,
@@ -83,6 +81,9 @@ export function prioritizeProjectionStatusRows(rows, showAll) {
83
81
  const total = Object.values(byState).reduce((sum, count) => sum + count, 0);
84
82
  return total > 0 ? { rows: visible, omitted: { total, byState } } : { rows: visible };
85
83
  }
84
+ export function requiresProjectionStatusDetails(state) {
85
+ return !isOmissibleProjectionStatusState(state);
86
+ }
86
87
  class ProjectionStatusFileError extends Error {
87
88
  diagnostic;
88
89
  constructor(kind, filePath) {
@@ -284,12 +285,40 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
284
285
  function terminalTimestamp(current) {
285
286
  return extractTerminalTimestamp(current?.verdict?.facts);
286
287
  }
287
- function readProjectionRow(projectionDir, id, alias, nowMs) {
288
+ function readProjectionSummary(projectionDir, id, alias, nowMs) {
288
289
  const diagnostics = [];
289
290
  const snapshot = observeProjectionLifeSnapshot(projectionDir, {
290
291
  nowMs,
291
292
  startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
292
293
  });
294
+ if (snapshot.compatibility === "incompatible") {
295
+ return {
296
+ id,
297
+ ...(alias ? { alias } : {}),
298
+ projectionDir,
299
+ snapshot,
300
+ identityFacts: null,
301
+ diagnostics,
302
+ state: "incompatible",
303
+ };
304
+ }
305
+ const identityFacts = readIdentityFacts(projectionDir, diagnostics);
306
+ const life = snapshot.life;
307
+ const phase = life.phase;
308
+ const state = phase === "active" || phase === "quiescent" ? "out" : phase;
309
+ return {
310
+ id,
311
+ ...(alias ? { alias } : {}),
312
+ projectionDir,
313
+ snapshot,
314
+ identityFacts,
315
+ diagnostics,
316
+ state,
317
+ };
318
+ }
319
+ function readProjectionRow(summary, nowMs, readDetails) {
320
+ const { id, alias, projectionDir, snapshot } = summary;
321
+ const diagnostics = [...summary.diagnostics];
293
322
  if (snapshot.compatibility === "incompatible") {
294
323
  return {
295
324
  id,
@@ -300,10 +329,7 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
300
329
  diagnostics,
301
330
  };
302
331
  }
303
- const identityFacts = readIdentityFacts(projectionDir, diagnostics);
304
- const tellCounts = identityFacts.identity
305
- ? readHeartTellCounts(projectionDir, diagnostics)
306
- : emptyTellCounts();
332
+ const identityFacts = summary.identityFacts;
307
333
  if (!identityFacts.identity) {
308
334
  const diagnostic = diagnostics.map((entry) => entry.detail).find((detail) => detail !== undefined)
309
335
  ?? "projection identity could not be observed";
@@ -315,19 +341,22 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
315
341
  compatibility: "current",
316
342
  lifecycle: { phase: "unknown", diagnostic },
317
343
  },
318
- tellCounts,
344
+ tellCounts: emptyTellCounts(),
319
345
  diagnostics,
320
346
  };
321
347
  }
322
348
  const life = snapshot.life;
323
- const current = snapshot.records
324
- ? selectCurrentGeneration(snapshot.records)
325
- : null;
349
+ const current = snapshot.records ? selectCurrentGeneration(snapshot.records) : null;
326
350
  const adoptedAt = current?.adoption?.facts.adoptedAt;
327
351
  const adoptedAtMs = typeof adoptedAt === "string"
328
352
  ? Date.parse(adoptedAt)
329
353
  : Number.NaN;
330
- const activityFacts = readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt);
354
+ const tellCounts = readDetails
355
+ ? readHeartTellCounts(projectionDir, diagnostics)
356
+ : emptyTellCounts();
357
+ const activityFacts = readDetails
358
+ ? readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt)
359
+ : {};
331
360
  const mintedAtMs = Date.parse(identityFacts.mintedAt ?? "");
332
361
  const terminalAt = terminalTimestamp(current);
333
362
  const durationAnchorMs = current?.verdict
@@ -365,26 +394,7 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
365
394
  diagnostics,
366
395
  };
367
396
  }
368
- export async function readProjectionStatusBoard(cwd, nowMs, options = {}) {
369
- const root = await projectionRoot(cwd);
370
- try {
371
- if (!fs.lstatSync(root).isDirectory())
372
- return null;
373
- }
374
- catch (error) {
375
- if (isErrnoException(error) && error.code === "ENOENT")
376
- return null;
377
- throw error;
378
- }
379
- let entries;
380
- try {
381
- entries = fs.readdirSync(root);
382
- }
383
- catch (error) {
384
- if (isErrnoException(error) && error.code === "ENOENT")
385
- return null;
386
- throw error;
387
- }
397
+ async function readProjectionStatusAliases(cwd, root) {
388
398
  const aliases = new Map();
389
399
  const aliasRoot = path.join(path.dirname(path.dirname(root)), ".keiyaku", "projection-alias");
390
400
  try {
@@ -400,34 +410,112 @@ export async function readProjectionStatusBoard(cwd, nowMs, options = {}) {
400
410
  if (!(isErrnoException(error) && error.code === "ENOENT"))
401
411
  throw error;
402
412
  }
403
- const rows = [];
404
- for (const akuma of entries.sort()) {
405
- if (akuma.startsWith("."))
413
+ return aliases;
414
+ }
415
+ function readProjectionTopologyEntries(directory) {
416
+ try {
417
+ if (!fs.lstatSync(directory).isDirectory())
418
+ return null;
419
+ return fs.readdirSync(directory).sort();
420
+ }
421
+ catch (error) {
422
+ if (isErrnoException(error) && error.code === "ENOENT")
423
+ return null;
424
+ throw error;
425
+ }
426
+ }
427
+ async function readProjectionRootTopology(cwd) {
428
+ const root = await projectionRoot(cwd);
429
+ const akumas = readProjectionTopologyEntries(root);
430
+ return akumas ? { root, akumas } : null;
431
+ }
432
+ function readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs) {
433
+ const akumaDir = path.join(root, akuma);
434
+ const hexes = readProjectionTopologyEntries(akumaDir);
435
+ if (!hexes)
436
+ return [];
437
+ const summaries = [];
438
+ for (const hex of hexes) {
439
+ const id = `${akuma}/${hex}`;
440
+ const dir = path.join(akumaDir, hex);
441
+ if (!isProjectionId(id))
406
442
  continue;
407
- const akumaDir = path.join(root, akuma);
408
443
  try {
409
- if (!fs.lstatSync(akumaDir).isDirectory())
444
+ if (!fs.lstatSync(dir).isDirectory())
410
445
  continue;
411
446
  }
412
447
  catch {
413
448
  continue;
414
449
  }
415
- for (const hex of fs.readdirSync(akumaDir).sort()) {
416
- const id = `${akuma}/${hex}`;
417
- const dir = path.join(akumaDir, hex);
418
- if (!isProjectionId(id))
419
- continue;
420
- try {
421
- if (!fs.lstatSync(dir).isDirectory())
422
- continue;
423
- }
424
- catch {
425
- continue;
426
- }
427
- rows.push(readProjectionRow(dir, id, aliases.get(id), nowMs));
428
- }
450
+ summaries.push(readProjectionSummary(dir, id, aliases.get(id), nowMs));
451
+ }
452
+ return summaries;
453
+ }
454
+ function isNonterminalProjectionStatusSummary(summary) {
455
+ return summary.snapshot.compatibility === "current"
456
+ && requiresProjectionStatusDetails(summary.state);
457
+ }
458
+ export async function readOperationalProjectionStatusBoard(cwd, nowMs) {
459
+ const topology = await readProjectionRootTopology(cwd);
460
+ if (!topology)
461
+ return null;
462
+ const { root, akumas } = topology;
463
+ const aliases = await readProjectionStatusAliases(cwd, root);
464
+ const summaries = [];
465
+ for (const akuma of akumas) {
466
+ if (akuma.startsWith("."))
467
+ continue;
468
+ summaries.push(...readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs));
469
+ }
470
+ const rows = summaries.map((summary) => readProjectionRow(summary, nowMs, requiresProjectionStatusDetails(summary.state)));
471
+ return rows.length > 0 ? prioritizeProjectionStatusRows(rows) : null;
472
+ }
473
+ export async function readExactProjectionStatus(cwd, nowMs, projectionId) {
474
+ if (!isProjectionId(projectionId))
475
+ return null;
476
+ const root = await projectionRoot(cwd);
477
+ try {
478
+ if (!fs.lstatSync(root).isDirectory())
479
+ return null;
480
+ }
481
+ catch (error) {
482
+ if (isErrnoException(error) && error.code === "ENOENT")
483
+ return null;
484
+ throw error;
485
+ }
486
+ const [akuma, hex] = projectionId.split("/");
487
+ const dir = path.join(root, akuma, hex);
488
+ try {
489
+ if (!fs.lstatSync(dir).isDirectory())
490
+ return null;
491
+ }
492
+ catch (error) {
493
+ if (isErrnoException(error) && error.code === "ENOENT")
494
+ return null;
495
+ throw error;
496
+ }
497
+ const aliases = await readProjectionStatusAliases(cwd, root);
498
+ return readProjectionRow(readProjectionSummary(dir, projectionId, aliases.get(projectionId), nowMs), nowMs, true);
499
+ }
500
+ export async function readAkumaNonterminalProjectionStatus(cwd, nowMs, akuma) {
501
+ if (!isProjectionId(`${akuma}/00000000`))
502
+ return null;
503
+ const root = await projectionRoot(cwd);
504
+ try {
505
+ if (!fs.lstatSync(root).isDirectory())
506
+ return null;
507
+ }
508
+ catch (error) {
509
+ if (isErrnoException(error) && error.code === "ENOENT")
510
+ return null;
511
+ throw error;
429
512
  }
430
- return rows.length > 0 ? prioritizeProjectionStatusRows(rows, options.showAll === true) : null;
513
+ const aliases = await readProjectionStatusAliases(cwd, root);
514
+ const rows = readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs)
515
+ .filter(isNonterminalProjectionStatusSummary)
516
+ .map((summary) => readProjectionRow(summary, nowMs, true))
517
+ .sort(compareProjectionStatusRows);
518
+ return rows.length > 0 ? { rows } : null;
431
519
  }
432
520
  /**
433
521
  * Renew-only lifecycle guard. Immutable identity is checked before lifecycle,