@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.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/event-persistence.js +7 -5
- package/build/agents/harness/events.js +3 -2
- package/build/agents/providers/codex-app-server/adapter.js +6 -1
- package/build/agents/providers/codex-app-server/session.js +8 -7
- package/build/agents/selector.js +12 -1
- package/build/cli/commands/akuma/view/handler.js +3 -11
- package/build/cli/commands/contract/amend/handler.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/path-prefix-compaction.js +119 -76
- package/build/cli/render/projection-activity.js +10 -1
- package/build/cli/render/shared.js +8 -7
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/wait.js +1 -1
- package/build/config/settings/disease.js +4 -4
- package/build/config/settings/loader.js +44 -21
- package/build/core/addressing.js +40 -9
- package/build/core/amend.js +21 -5
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +22 -0
- package/build/core/projection/generation/projection-generation-execution.js +45 -37
- package/build/core/projection/generation/projection-generation-launcher.js +148 -20
- package/build/core/projection/generation/projection-generation-process.js +3 -1
- package/build/core/projection/generation/projection-generation-runner.js +76 -40
- package/build/core/projection/generation/projection-generation-runtime.js +82 -19
- package/build/core/projection/generation/store.js +17 -1
- package/build/core/projection/generation/transitions.js +89 -12
- package/build/core/projection/index.js +3 -3
- package/build/core/projection/projection-kill.js +22 -10
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wake.js +171 -72
- package/build/core/status/board.js +42 -4
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +5 -3
- package/build/core/worktree-path.js +39 -25
- package/build/flow-error.js +1 -1
- package/build/generated/version.js +2 -2
- package/build/git/refs.js +47 -1
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +18 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error instanceof ProjectionRunnerLockUnavailableError)
|
|
221
|
+
return "pending";
|
|
217
222
|
return "pending";
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error instanceof ProjectionRunnerLockUnavailableError)
|
|
286
|
+
return "active";
|
|
237
287
|
return "pending";
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
?
|
|
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:
|
|
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 {
|
|
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
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
if (
|
|
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
|
|
14
|
+
const board = await locateTaskBoard(cwd);
|
|
15
|
+
const store = openTaskStoreAt(board);
|
|
18
16
|
const repository = taskRepositoryFromStore(store);
|
|
19
|
-
if (
|
|
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
|
|
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(
|
|
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);
|