@astrosheep/keiyaku 2.9.7 → 2.9.9

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 (57) 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/harness/projection.js +8 -6
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/projection/status/handler.js +5 -4
  12. package/build/cli/commands/projection/status/meta.js +2 -2
  13. package/build/cli/commands/task/add/meta.js +9 -1
  14. package/build/cli/commands/task/shared.js +2 -1
  15. package/build/cli/completion.js +8 -0
  16. package/build/cli/render/kanshi.js +238 -80
  17. package/build/cli/render/path-prefix-compaction.js +119 -76
  18. package/build/cli/render/projection-activity.js +65 -21
  19. package/build/cli/render/shared.js +8 -7
  20. package/build/cli/render/status.js +11 -23
  21. package/build/cli/render/wait.js +42 -64
  22. package/build/config/env-keys.js +1 -1
  23. package/build/config/env.js +1 -1
  24. package/build/config/settings/disease.js +4 -4
  25. package/build/config/settings/loader.js +44 -21
  26. package/build/core/addressing.js +40 -9
  27. package/build/core/amend.js +21 -5
  28. package/build/core/call/context.js +19 -3
  29. package/build/core/call/execution.js +43 -20
  30. package/build/core/ledger-batch.js +194 -0
  31. package/build/core/projection/generation/database.js +22 -0
  32. package/build/core/projection/generation/projection-generation-execution.js +52 -38
  33. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  34. package/build/core/projection/generation/projection-generation-process.js +3 -1
  35. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  36. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  37. package/build/core/projection/generation/store.js +17 -1
  38. package/build/core/projection/generation/transitions.js +89 -12
  39. package/build/core/projection/index.js +3 -3
  40. package/build/core/projection/projection-activity.js +2 -0
  41. package/build/core/projection/projection-kill.js +22 -10
  42. package/build/core/projection/projection-runner-lock.js +177 -37
  43. package/build/core/projection/projection-status.js +183 -60
  44. package/build/core/projection/projection-wake.js +171 -72
  45. package/build/core/status/board.js +55 -14
  46. package/build/core/status/drift.js +21 -5
  47. package/build/core/status/ledger-batch.js +1 -158
  48. package/build/core/task/settlement-git.js +2 -2
  49. package/build/core/task/task-git-runtime.js +8 -10
  50. package/build/core/task/task-git-store.js +5 -3
  51. package/build/core/worktree-path.js +39 -25
  52. package/build/flow-error.js +1 -1
  53. package/build/generated/version.js +2 -2
  54. package/build/git/refs.js +47 -1
  55. package/package.json +1 -1
  56. package/skills/keiyaku-akuma/SKILL.md +18 -0
  57. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -7,40 +7,12 @@ import { createProjectionExecutionId } from "./generation/projection-generation-
7
7
  import { PROJECTION_ADOPTION_POLL_MS, PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
8
8
  import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
9
9
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
10
- import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
10
+ import { acquireProjectionRunnerLock, acquireProjectionRunnerSettlementLock, ProjectionRunnerLockUnavailableError, } from "./projection-runner-lock.js";
11
11
  import { setTimeout as delay } from "node:timers/promises";
12
- /** Persist an asynchronous interrupt wake failure at the generation boundary. */
13
- export function recordInterruptWakeFailure(projectionDirectory, cause, nowMs = Date.now()) {
14
- const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
15
- const current = continuation.current;
16
- if (!current || !current.verdict || current.verdict.facts.state !== "interrupted")
17
- return;
18
- const snapshot = continuation.successor;
19
- const executionId = createProjectionExecutionId(nowMs);
20
- const facts = successorFacts(current, snapshot, nowMs, false);
21
- const store = openProjectionGenerationStore(projectionDirectory);
22
- try {
23
- const launch = store.launchIfSettled({ executionId, facts });
24
- if (launch.status !== "committed")
25
- return;
26
- store.verdictIfOpen({
27
- executionId,
28
- verdict: "launch-failed",
29
- facts: {
30
- stage: "interrupt-wake",
31
- cause,
32
- tellRetained: true,
33
- terminalAt: new Date(nowMs).toISOString(),
34
- },
35
- });
36
- }
37
- finally {
38
- store.close();
39
- }
40
- }
41
12
  function incompatibleProjectionError(projectionId) {
42
13
  const subject = projectionId ? `Projection '${projectionId}'` : "Projection";
43
- return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; keiyaku status --all can inspect it, but tell cannot change it.`);
14
+ const statusCoordinate = projectionId ?? "<projection-id>";
15
+ return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; inspect it with keiyaku status ${statusCoordinate}, but tell cannot change it.`);
44
16
  }
45
17
  /**
46
18
  * Tell is a current-schema mutation only. This gate runs before the handler
@@ -127,10 +99,24 @@ export function assertTellEffortSupported(projectionDirectory, effort) {
127
99
  throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' is not permitted; legal values: ${effortOptions.join(", ")}.`);
128
100
  }
129
101
  }
130
- async function startCommittedSuccessor(projectionDirectory, projectionId, executionId, startGeneration) {
131
- const started = await startGeneration(projectionDirectory, executionId);
102
+ async function startCommittedSuccessor(projectionDirectory, projectionId, executionId, startGeneration, dependencies = {}) {
103
+ let started;
104
+ try {
105
+ started = await startGeneration(projectionDirectory, executionId, dependencies);
106
+ }
107
+ finally {
108
+ dependencies.launchOwnership?.close();
109
+ }
132
110
  if (started.status === "adopted")
133
111
  return "woken";
112
+ if (started.status === "pending")
113
+ return "pending";
114
+ const reconciled = readCurrentGeneration(projectionDirectory);
115
+ if (reconciled?.launch.executionId === executionId && reconciled.adoption)
116
+ return "woken";
117
+ if (reconciled && !reconciled.verdict && reconciled.launch.executionId !== executionId) {
118
+ return reconciled.adoption ? "active" : "pending";
119
+ }
134
120
  throw new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started (cause: ${started.cause}).`, {
135
121
  facts: {
136
122
  kind: "projection_wake_failure",
@@ -151,6 +137,31 @@ function sessionWakeFailure(projectionId, cause) {
151
137
  },
152
138
  });
153
139
  }
140
+ export function persistInterruptWakeFailure(projectionDirectory, error, nowMs = Date.now()) {
141
+ if (!(error instanceof FlowError)
142
+ || error.code !== "PROJECTION_WAKE_FAILED"
143
+ || error.facts?.kind !== "projection_wake_failure")
144
+ return;
145
+ const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
146
+ const current = continuation.current;
147
+ if (current?.verdict?.facts.state !== "interrupted" || !continuation.requiresWake)
148
+ return;
149
+ const successorExecutionId = createProjectionExecutionId(nowMs);
150
+ const facts = successorFacts(current, continuation.successor, nowMs, false);
151
+ const store = openProjectionGenerationStore(projectionDirectory);
152
+ try {
153
+ store.failInterruptWakeIfSettled({
154
+ predecessorExecutionId: current.launch.executionId,
155
+ successorExecutionId,
156
+ successorFacts: facts,
157
+ cause: error.facts.cause,
158
+ terminalAt: new Date(nowMs).toISOString(),
159
+ });
160
+ }
161
+ finally {
162
+ store.close();
163
+ }
164
+ }
154
165
  export async function tellProjection(projectionDirectory, projectionId, effort, dependencies = {}) {
155
166
  const startGeneration = dependencies.startGeneration ?? startProjectionGeneration;
156
167
  const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
@@ -201,56 +212,144 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
201
212
  try {
202
213
  if (life.phase === "startup-timeout") {
203
214
  const terminalAt = new Date(nowMs).toISOString();
204
- const transition = store.supersedeStartupTimeout({
205
- executionId: current.launch.executionId,
206
- observedAtMs: nowMs,
207
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
208
- diagnostic: { detail: "runner did not adopt before startup timeout" },
209
- successorExecutionId,
210
- successorFacts: facts,
211
- terminalAt,
212
- });
213
- if (transition.status === "committed") {
214
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
215
+ let deathEvidence;
216
+ try {
217
+ deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
215
218
  }
216
- if (transition.reason === "kill-requested")
219
+ catch (error) {
220
+ if (error instanceof ProjectionRunnerLockUnavailableError)
221
+ return "pending";
217
222
  return "pending";
218
- await sleep(PROJECTION_ADOPTION_POLL_MS);
219
- continue;
223
+ }
224
+ try {
225
+ store.beginWriteTransaction();
226
+ const protectedCurrent = store.readCurrentGeneration();
227
+ if (protectedCurrent?.launch.executionId !== current.launch.executionId
228
+ || protectedCurrent.adoption
229
+ || protectedCurrent.verdict) {
230
+ store.rollbackWriteTransaction();
231
+ return "pending";
232
+ }
233
+ if (protectedCurrent.killIntent) {
234
+ const settled = store.settleKillIfRunnerDead({
235
+ executionId: current.launch.executionId,
236
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
237
+ });
238
+ if (settled.status === "committed")
239
+ store.commitWriteTransaction();
240
+ else
241
+ store.rollbackWriteTransaction();
242
+ return "pending";
243
+ }
244
+ deathEvidence.close();
245
+ let launchOwnership;
246
+ try {
247
+ launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
248
+ }
249
+ catch (error) {
250
+ store.rollbackWriteTransaction();
251
+ if (error instanceof ProjectionRunnerLockUnavailableError)
252
+ return "pending";
253
+ return "pending";
254
+ }
255
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
256
+ commitLaunch: () => {
257
+ const transition = store.supersedeStartupTimeout({
258
+ executionId: current.launch.executionId,
259
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
260
+ diagnostic: { detail: "runner did not adopt before startup timeout" },
261
+ successorExecutionId,
262
+ successorFacts: facts,
263
+ terminalAt,
264
+ });
265
+ if (transition.status === "committed")
266
+ store.commitWriteTransaction();
267
+ else
268
+ store.rollbackWriteTransaction();
269
+ return transition.status === "committed";
270
+ },
271
+ launchOwnership,
272
+ });
273
+ }
274
+ finally {
275
+ store.rollbackWriteTransaction();
276
+ deathEvidence.close();
277
+ }
220
278
  }
221
279
  if (life.phase === "lost" && !current.verdict) {
222
- const lock = observeProjectionRunnerLock(projectionDirectory);
223
- if (lock.state !== "released")
224
- return lock.state === "held" ? "active" : "pending";
225
- const terminalAt = new Date(nowMs).toISOString();
226
- const transition = store.replaceDeadRunner({
227
- executionId: current.launch.executionId,
228
- releasedLockEvidence: { observedAt: terminalAt },
229
- successorExecutionId,
230
- successorFacts: facts,
231
- terminalAt,
232
- });
233
- if (transition.status === "committed") {
234
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
280
+ let deathEvidence;
281
+ try {
282
+ deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
235
283
  }
236
- if (transition.reason === "kill-requested")
284
+ catch (error) {
285
+ if (error instanceof ProjectionRunnerLockUnavailableError)
286
+ return "active";
237
287
  return "pending";
238
- await sleep(PROJECTION_ADOPTION_POLL_MS);
239
- continue;
288
+ }
289
+ try {
290
+ store.beginWriteTransaction();
291
+ const protectedCurrent = store.readCurrentGeneration();
292
+ if (protectedCurrent?.launch.executionId !== current.launch.executionId
293
+ || !protectedCurrent.adoption
294
+ || protectedCurrent.verdict) {
295
+ store.rollbackWriteTransaction();
296
+ return "pending";
297
+ }
298
+ const terminalAt = new Date(nowMs).toISOString();
299
+ if (protectedCurrent.killIntent) {
300
+ const settled = store.settleKillIfRunnerDead({
301
+ executionId: current.launch.executionId,
302
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
303
+ });
304
+ if (settled.status === "committed")
305
+ store.commitWriteTransaction();
306
+ else
307
+ store.rollbackWriteTransaction();
308
+ return "pending";
309
+ }
310
+ deathEvidence.close();
311
+ let launchOwnership;
312
+ try {
313
+ launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
314
+ }
315
+ catch (error) {
316
+ store.rollbackWriteTransaction();
317
+ if (error instanceof ProjectionRunnerLockUnavailableError)
318
+ return "active";
319
+ return "pending";
320
+ }
321
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
322
+ commitLaunch: () => {
323
+ const transition = store.replaceDeadRunner({
324
+ executionId: current.launch.executionId,
325
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
326
+ successorExecutionId,
327
+ successorFacts: facts,
328
+ terminalAt,
329
+ });
330
+ if (transition.status === "committed")
331
+ store.commitWriteTransaction();
332
+ else
333
+ store.rollbackWriteTransaction();
334
+ return transition.status === "committed";
335
+ },
336
+ launchOwnership,
337
+ });
338
+ }
339
+ finally {
340
+ store.rollbackWriteTransaction();
341
+ deathEvidence.close();
342
+ }
240
343
  }
241
344
  if (!["done", "failed", "lost", "dismissed", "killed"].includes(life.phase)) {
242
345
  return "pending";
243
346
  }
244
- const transition = store.launchIfSettled({
245
- executionId: successorExecutionId,
246
- facts,
347
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
348
+ commitLaunch: () => store.launchIfSettled({
349
+ executionId: successorExecutionId,
350
+ facts,
351
+ }).status === "committed",
247
352
  });
248
- if (transition.status === "committed") {
249
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
250
- }
251
- if (transition.reason === "kill-requested")
252
- return "pending";
253
- await sleep(PROJECTION_ADOPTION_POLL_MS);
254
353
  }
255
354
  finally {
256
355
  store.close();
@@ -1,6 +1,7 @@
1
+ import { availableParallelism } from "node:os";
1
2
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
2
3
  import { observeGitRepository } from "../../git/branches.js";
3
- import { readProjectionStatusBoard } from "../projection/index.js";
4
+ import { readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "../projection/index.js";
4
5
  import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
5
6
  import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
6
7
  import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
@@ -8,6 +9,9 @@ import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerd
8
9
  import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
9
10
  import { computeClaimReconciliation, } from "./reconciliation.js";
10
11
  import { readStatusLedgers } from "./ledger-batch.js";
12
+ // Git object reads benefit from overlap, but a hard cap avoids process and disk contention.
13
+ const STATUS_DRIFT_MAX_CONCURRENCY = 8;
14
+ const STATUS_DRIFT_CONCURRENCY = Math.min(STATUS_DRIFT_MAX_CONCURRENCY, availableParallelism());
11
15
  function compareTaskStatusRows(left, right) {
12
16
  const leftUpdated = taskUpdatedAt(left);
13
17
  const rightUpdated = taskUpdatedAt(right);
@@ -22,11 +26,9 @@ function timestampAge(timestamp, now) {
22
26
  export function deriveTaskStatusSummary(board, now) {
23
27
  const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress"));
24
28
  const inProgress = eligible.filter((task) => task.state === "in_progress").length;
25
- const topTasks = [
26
- ...eligible.filter((task) => task.state === "in_progress").sort(compareTaskStatusRows),
27
- ...eligible.filter((task) => task.state === "open" && !task.blocked).sort(compareTaskStatusRows),
28
- ]
29
- .slice(0, 5)
29
+ const topTasks = eligible
30
+ .filter((task) => task.state === "in_progress")
31
+ .sort(compareTaskStatusRows)
30
32
  .map((task) => {
31
33
  const updatedAt = taskUpdatedAt(task);
32
34
  return {
@@ -43,14 +45,12 @@ export function deriveTaskStatusSummary(board, now) {
43
45
  });
44
46
  const ready = eligible.filter((task) => task.state === "open" && !task.blocked).length;
45
47
  const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
46
- if (board.tasks.length === 0 && board.diseases.length === 0)
47
- return undefined;
48
48
  return {
49
- total: board.tasks.length,
49
+ total: eligible.length,
50
50
  inProgress,
51
51
  ready,
52
52
  blocked,
53
- omitted: inProgress + ready - topTasks.length,
53
+ omitted: ready + blocked,
54
54
  topTasks,
55
55
  diseases: board.diseases,
56
56
  };
@@ -165,6 +165,23 @@ function deriveLastActiveAge(entries, now) {
165
165
  lastActiveMs: Math.max(0, now - atMs),
166
166
  };
167
167
  }
168
+ export async function evaluateDistinctBaseDrifts(cwd, bases, defaultBranch, evaluate = evaluateBaseDrift) {
169
+ const uniqueBases = [...new Set(bases)];
170
+ const results = new Map();
171
+ let nextIndex = 0;
172
+ const worker = async () => {
173
+ while (nextIndex < uniqueBases.length) {
174
+ const index = nextIndex++;
175
+ const base = uniqueBases[index];
176
+ if (base === undefined)
177
+ return;
178
+ results.set(base, await evaluate(cwd, base, defaultBranch));
179
+ }
180
+ };
181
+ const workerCount = Math.min(STATUS_DRIFT_CONCURRENCY, uniqueBases.length);
182
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
183
+ return results;
184
+ }
168
185
  export async function readKanshiBoard(cwd, now, opts) {
169
186
  const targetFilter = opts?.target ?? null;
170
187
  const loadedSettings = await loadKeiyakuSettings(cwd);
@@ -173,14 +190,22 @@ export async function readKanshiBoard(cwd, now, opts) {
173
190
  if (repository.capability === "unknown")
174
191
  throw repository.error;
175
192
  if (repository.capability !== "available" || repository.coordinate !== "repository") {
193
+ const exactProjection = opts?.projectionId === undefined
194
+ ? undefined
195
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
176
196
  return {
177
197
  contracts: [],
178
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
198
+ contractsObservable: false,
199
+ commissionPlaces: new Map(),
200
+ projections: opts?.projectionId === undefined
201
+ ? await readOperationalProjectionStatusBoard(cwd, now)
202
+ : exactProjection ? { rows: [exactProjection] } : null,
179
203
  queueLength: 0,
180
204
  diseases: loadedSettings.diseases,
181
205
  claimReconciliation: null,
182
206
  targetFilter,
183
- ...(tasks ? { tasks } : {}),
207
+ // Task and contract boards are repository organs; a non-repository cwd
208
+ // cannot truthfully turn their absence into a zero count.
184
209
  };
185
210
  }
186
211
  // Status is a pure reader: use the same resolver, but retain its ability to
@@ -203,7 +228,15 @@ export async function readKanshiBoard(cwd, now, opts) {
203
228
  }
204
229
  const queueModel = deriveQueueReadModel(readableLedgers);
205
230
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
231
+ const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
232
+ const state = deriveContractState(item.entries);
233
+ return isTerminalState(state) ? [] : (() => {
234
+ const base = latestBase(item.entries);
235
+ return base ? [base] : [];
236
+ })();
237
+ }), defaultBranch);
206
238
  const contracts = [];
239
+ const commissionPlaces = new Map();
207
240
  // Readable-ledger commission targets retained for post-assembly filter only.
208
241
  const derivedTargetById = new Map();
209
242
  for (const item of ledgers) {
@@ -228,6 +261,7 @@ export async function readKanshiBoard(cwd, now, opts) {
228
261
  continue;
229
262
  }
230
263
  const bind = findBindEntry(item.entries);
264
+ commissionPlaces.set(item.contractId, bind?.data.place ?? null);
231
265
  derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
232
266
  const base = latestBase(item.entries);
233
267
  const state = deriveContractState(item.entries);
@@ -236,7 +270,7 @@ export async function readKanshiBoard(cwd, now, opts) {
236
270
  const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
237
271
  const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
238
272
  const baseDrift = base
239
- ? await evaluateBaseDrift(cwd, base, defaultBranch)
273
+ ? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
240
274
  : { state: "unknown", label: "n/a (no default branch)" };
241
275
  const nextStep = deriveNextStep({
242
276
  id: item.contractId,
@@ -296,9 +330,16 @@ export async function readKanshiBoard(cwd, now, opts) {
296
330
  ? boardQueueLength
297
331
  : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
298
332
  const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
333
+ const exactProjection = opts?.projectionId === undefined
334
+ ? undefined
335
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
299
336
  return {
300
337
  contracts: filteredContracts,
301
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
338
+ contractsObservable: true,
339
+ commissionPlaces,
340
+ projections: opts?.projectionId === undefined
341
+ ? await readOperationalProjectionStatusBoard(cwd, now)
342
+ : exactProjection ? { rows: [exactProjection] } : null,
302
343
  queueLength,
303
344
  defaultBranch: defaultBranch?.branch,
304
345
  defaultHead: defaultBranch?.head,
@@ -1,7 +1,7 @@
1
1
  import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
2
2
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
3
3
  import { FlowError } from "../../flow-error.js";
4
- import { SETTINGS_FILE } from "../../keiyaku.js";
4
+ import { resolveLocalBranchHead } from "../../git/refs.js";
5
5
  import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../../git/core.js";
6
6
  function findBindEntry(entries) {
7
7
  return entries.find((entry) => entry.kind === "bind");
@@ -49,10 +49,26 @@ export async function resolveDefaultBranchFromSettings(cwd, loaded, rejectDiseas
49
49
  assertSettingsKnobUsable(loaded, "defaultBranch");
50
50
  const override = loaded.knobs?.defaultBranch;
51
51
  if (override) {
52
- const head = await resolveCommit(cwd, override);
53
- if (!head)
54
- throw new FlowError("INVALID_SETTINGS", `${SETTINGS_FILE} defaultBranch '${override}' does not resolve`);
55
- return { branch: override, head };
52
+ const resolved = await resolveLocalBranchHead(cwd, override);
53
+ if (!resolved) {
54
+ const source = loaded.knobSources.defaultBranch;
55
+ if (source === undefined) {
56
+ throw new Error("admitted defaultBranch has no settings source coordinate");
57
+ }
58
+ throw new FlowError("INVALID_SETTINGS", `${source.coordinate} defaultBranch '${override}' does not resolve`, {
59
+ facts: {
60
+ kind: "invalid_settings",
61
+ diseases: [{
62
+ source: source.source,
63
+ coordinate: source.coordinate,
64
+ knob: "defaultBranch",
65
+ reason: `defaultBranch '${override}' does not resolve as a local branch`,
66
+ }],
67
+ },
68
+ });
69
+ }
70
+ const branch = resolved.ref.slice("refs/heads/".length);
71
+ return { branch, head: resolved.head };
56
72
  }
57
73
  const git = createGit(cwd);
58
74
  for (const branch of ["main", "master"]) {
@@ -1,158 +1 @@
1
- import { wrapGitError, runGitProcess } from "../../git/core.js";
2
- import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../../git/streaming-batch.js";
3
- import { parseLedgerEntryCommitMessage } from "../entry.js";
4
- import { ACTIVE_LEDGER_REF_PREFIX, } from "../ledger.js";
5
- const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
6
- async function runStatusGit(cwd, args) {
7
- const result = await runGitProcess(cwd, args);
8
- if (result.error || result.status !== 0) {
9
- throw wrapGitError(args.join(" "), {
10
- message: result.error?.message
11
- ?? `git exited with status ${result.status ?? "unknown"}`,
12
- stderr: result.stderr,
13
- stdout: result.stdout,
14
- }, cwd);
15
- }
16
- return Buffer.from(result.stdout, "utf8");
17
- }
18
- async function queryBatchObject(batch, expression) {
19
- const record = await batch.request(expression, {
20
- maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
21
- });
22
- if (record.kind === "missing" || record.kind === "ambiguous") {
23
- throw new Error(`[git cat-file ${expression}] object is ${record.kind === "missing" ? "missing" : "ambiguous"}`);
24
- }
25
- return {
26
- oid: record.record.oid,
27
- type: record.record.type,
28
- content: record.record.content,
29
- };
30
- }
31
- function parseCommitObject(object) {
32
- if (object.type !== "commit") {
33
- throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
34
- }
35
- const content = object.content.toString("utf8");
36
- const messageStart = content.indexOf("\n\n");
37
- if (messageStart < 0)
38
- throw new Error(`git commit ${object.oid} has no message separator`);
39
- const parents = [];
40
- for (const line of content.slice(0, messageStart).split("\n")) {
41
- if (!line.startsWith("parent "))
42
- continue;
43
- const parent = line.slice("parent ".length);
44
- if (!/^[0-9a-f]{40,64}$/.test(parent)) {
45
- throw new Error(`git commit ${object.oid} has an invalid parent header`);
46
- }
47
- parents.push(parent);
48
- }
49
- return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
50
- }
51
- function parseLedgerRefs(output) {
52
- return output
53
- .toString("utf8")
54
- .split(/\r?\n/)
55
- .filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
56
- .map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
57
- }
58
- async function readLedgerFromBatch(batch, ref, commits) {
59
- const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
60
- const head = parseCommitObject(headObject);
61
- commits.set(head.oid, Promise.resolve(head));
62
- const ordered = [];
63
- const visited = new Set();
64
- const visiting = new Set();
65
- const visit = async (oid) => {
66
- if (visited.has(oid))
67
- return;
68
- if (visiting.has(oid))
69
- throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
70
- visiting.add(oid);
71
- let pending = commits.get(oid);
72
- if (!pending) {
73
- pending = queryBatchObject(batch, oid).then(parseCommitObject);
74
- commits.set(oid, pending);
75
- }
76
- const commit = await pending;
77
- for (const parent of commit.parents)
78
- await visit(parent);
79
- visiting.delete(oid);
80
- visited.add(oid);
81
- ordered.push(commit);
82
- };
83
- await visit(head.oid);
84
- return {
85
- contractId: ref.contractId,
86
- ref: ref.ref,
87
- head: head.oid,
88
- entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
89
- };
90
- }
91
- /**
92
- * Read the aggregate status ledger snapshot with at most two Git processes:
93
- * ref-name discovery and one framed, streaming object session.
94
- */
95
- export async function readStatusLedgers(cwd) {
96
- const refOutput = await runStatusGit(cwd, [
97
- "for-each-ref",
98
- "--sort=refname",
99
- "--format=%(refname)",
100
- ACTIVE_LEDGER_REF_PREFIX,
101
- ]);
102
- const refs = parseLedgerRefs(refOutput);
103
- if (refs.length === 0)
104
- return [];
105
- const batch = openGitStreamingBatch(cwd);
106
- const commits = new Map();
107
- const results = [];
108
- let readError;
109
- try {
110
- for (let index = 0; index < refs.length; index += 1) {
111
- const ref = refs[index];
112
- try {
113
- results.push(await readLedgerFromBatch(batch, ref, commits));
114
- }
115
- catch (error) {
116
- const diagnostic = error instanceof Error ? error : new Error(String(error));
117
- results.push({ id: ref.contractId, error: diagnostic });
118
- // Framing and mid-frame EOF both destroy the batch channel; isolate remaining refs.
119
- if (error instanceof GitStreamingBatchFramingError
120
- || error instanceof GitStreamingBatchResponseEndedError) {
121
- const isolationReason = error instanceof GitStreamingBatchResponseEndedError
122
- ? "response ended"
123
- : "malformed frame";
124
- for (const pending of refs.slice(index + 1)) {
125
- results.push({
126
- id: pending.contractId,
127
- error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
128
- });
129
- }
130
- break;
131
- }
132
- }
133
- }
134
- }
135
- catch (error) {
136
- readError = error;
137
- }
138
- try {
139
- const close = await batch.close();
140
- if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
141
- throw wrapGitError("cat-file --batch", {
142
- message: close.timedOut
143
- ? "git timed out"
144
- : close.signal
145
- ? `git exited from signal ${close.signal}`
146
- : `git exited with status ${close.code ?? "unknown"}`,
147
- stderr: close.stderr.toString("utf8"),
148
- }, cwd);
149
- }
150
- }
151
- catch (error) {
152
- if (!readError)
153
- readError = error;
154
- }
155
- if (readError)
156
- throw readError;
157
- return results;
158
- }
1
+ export { readAllLedgerSnapshots as readStatusLedgers, } from "../ledger-batch.js";
@@ -72,7 +72,7 @@ export async function createTaskMutationCommit(input) {
72
72
  ], undefined, env);
73
73
  }
74
74
  const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
75
- const parentTree = (await git(input.cwd, ["rev-parse", `${input.parent}^{tree}`])).trim();
75
+ const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent])).trim();
76
76
  if (tree === parentTree)
77
77
  return input.parent;
78
78
  return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
@@ -354,7 +354,7 @@ export async function findAcceptedLandingCommit(input) {
354
354
  delivery: input.delivery,
355
355
  sources,
356
356
  });
357
- const actualTree = (await git(input.cwd, ["rev-parse", `${commit}^{tree}`])).trim();
357
+ const actualTree = (await git(input.cwd, ["show", "-s", "--format=%T", commit])).trim();
358
358
  if (actualTree === expectedTree)
359
359
  return commit;
360
360
  }