@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
@@ -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);
@@ -165,6 +169,23 @@ function deriveLastActiveAge(entries, now) {
165
169
  lastActiveMs: Math.max(0, now - atMs),
166
170
  };
167
171
  }
172
+ export async function evaluateDistinctBaseDrifts(cwd, bases, defaultBranch, evaluate = evaluateBaseDrift) {
173
+ const uniqueBases = [...new Set(bases)];
174
+ const results = new Map();
175
+ let nextIndex = 0;
176
+ const worker = async () => {
177
+ while (nextIndex < uniqueBases.length) {
178
+ const index = nextIndex++;
179
+ const base = uniqueBases[index];
180
+ if (base === undefined)
181
+ return;
182
+ results.set(base, await evaluate(cwd, base, defaultBranch));
183
+ }
184
+ };
185
+ const workerCount = Math.min(STATUS_DRIFT_CONCURRENCY, uniqueBases.length);
186
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
187
+ return results;
188
+ }
168
189
  export async function readKanshiBoard(cwd, now, opts) {
169
190
  const targetFilter = opts?.target ?? null;
170
191
  const loadedSettings = await loadKeiyakuSettings(cwd);
@@ -173,9 +194,14 @@ export async function readKanshiBoard(cwd, now, opts) {
173
194
  if (repository.capability === "unknown")
174
195
  throw repository.error;
175
196
  if (repository.capability !== "available" || repository.coordinate !== "repository") {
197
+ const exactProjection = opts?.projectionId === undefined
198
+ ? undefined
199
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
176
200
  return {
177
201
  contracts: [],
178
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
202
+ projections: opts?.projectionId === undefined
203
+ ? await readOperationalProjectionStatusBoard(cwd, now)
204
+ : exactProjection ? { rows: [exactProjection] } : null,
179
205
  queueLength: 0,
180
206
  diseases: loadedSettings.diseases,
181
207
  claimReconciliation: null,
@@ -203,6 +229,13 @@ export async function readKanshiBoard(cwd, now, opts) {
203
229
  }
204
230
  const queueModel = deriveQueueReadModel(readableLedgers);
205
231
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
232
+ const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
233
+ const state = deriveContractState(item.entries);
234
+ return isTerminalState(state) ? [] : (() => {
235
+ const base = latestBase(item.entries);
236
+ return base ? [base] : [];
237
+ })();
238
+ }), defaultBranch);
206
239
  const contracts = [];
207
240
  // Readable-ledger commission targets retained for post-assembly filter only.
208
241
  const derivedTargetById = new Map();
@@ -236,7 +269,7 @@ export async function readKanshiBoard(cwd, now, opts) {
236
269
  const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
237
270
  const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
238
271
  const baseDrift = base
239
- ? await evaluateBaseDrift(cwd, base, defaultBranch)
272
+ ? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
240
273
  : { state: "unknown", label: "n/a (no default branch)" };
241
274
  const nextStep = deriveNextStep({
242
275
  id: item.contractId,
@@ -296,9 +329,14 @@ export async function readKanshiBoard(cwd, now, opts) {
296
329
  ? boardQueueLength
297
330
  : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
298
331
  const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
332
+ const exactProjection = opts?.projectionId === undefined
333
+ ? undefined
334
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
299
335
  return {
300
336
  contracts: filteredContracts,
301
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
337
+ projections: opts?.projectionId === undefined
338
+ ? await readOperationalProjectionStatusBoard(cwd, now)
339
+ : exactProjection ? { rows: [exactProjection] } : null,
302
340
  queueLength,
303
341
  defaultBranch: defaultBranch?.branch,
304
342
  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";
@@ -1,24 +1,22 @@
1
- import { observeGitRepository } from "../../git/branches.js";
2
1
  import { listContractIds } from "../ledger.js";
3
2
  import { contractBoundByEngineWorktree, stableRepoRoot } from "../worktree-path.js";
4
- import { inspectTaskLock, openTaskStore } from "./task-git-store.js";
3
+ import { inspectTaskLock, locateTaskBoard, openTaskStore, openTaskStoreAt, } from "./task-git-store.js";
5
4
  import { taskRepositoryFromStore } from "./task-store-repository.js";
6
5
  import { withTaskWorktreePolicy } from "./task-worktree.js";
7
6
  import { inspectTaskDoctor } from "./task-doctor.js";
8
7
  export async function taskCommandContractId(cwd) {
9
- const observation = await observeGitRepository(cwd);
10
- if (observation.capability === "unknown")
11
- throw observation.error;
12
- if (observation.capability !== "available" || observation.coordinate !== "repository")
8
+ const board = await locateTaskBoard(cwd);
9
+ if (board.mode !== "git")
13
10
  return undefined;
14
- return contractBoundByEngineWorktree(cwd);
11
+ return contractBoundByEngineWorktree(cwd, board.root);
15
12
  }
16
13
  export async function openTaskCommandRepository(cwd) {
17
- const store = await openTaskStore(cwd);
14
+ const board = await locateTaskBoard(cwd);
15
+ const store = openTaskStoreAt(board);
18
16
  const repository = taskRepositoryFromStore(store);
19
- if (store.mode !== "git")
17
+ if (board.mode !== "git")
20
18
  return repository;
21
- const contractId = await contractBoundByEngineWorktree(cwd);
19
+ const contractId = await contractBoundByEngineWorktree(cwd, board.root);
22
20
  return contractId ? withTaskWorktreePolicy(repository, contractId) : repository;
23
21
  }
24
22
  export async function taskContractIds(cwd) {
@@ -395,8 +395,7 @@ function assertTaskDirectorySafety(directory) {
395
395
  throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
396
396
  }
397
397
  }
398
- export async function openTaskStore(cwd, deps = {}) {
399
- const board = await locateTaskBoard(cwd, deps.runGit);
398
+ export function openTaskStoreAt(board, deps = {}) {
400
399
  const directory = taskDirectory(board);
401
400
  return {
402
401
  mode: board.mode,
@@ -405,7 +404,7 @@ export async function openTaskStore(cwd, deps = {}) {
405
404
  return readTaskSources(directory);
406
405
  },
407
406
  async mutate(apply) {
408
- return withLockHeldTaskBoard(cwd, async (transaction) => {
407
+ return withLockHeldTaskBoard(board.root, async (transaction) => {
409
408
  const previousSources = transaction.sources;
410
409
  const plan = await apply(transaction.sources);
411
410
  validatePlan(plan);
@@ -437,6 +436,9 @@ export async function openTaskStore(cwd, deps = {}) {
437
436
  },
438
437
  };
439
438
  }
439
+ export async function openTaskStore(cwd, deps = {}) {
440
+ return openTaskStoreAt(await locateTaskBoard(cwd, deps.runGit), deps);
441
+ }
440
442
  export async function withLockHeldTaskBoard(cwd, apply, deps = {}, locatedBoard) {
441
443
  const board = locatedBoard ?? await locateTaskBoard(cwd, deps.runGit);
442
444
  const directory = taskDirectory(board);