@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.
- 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/harness/projection.js +8 -6
- 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 +238 -80
- package/build/cli/render/path-prefix-compaction.js +119 -76
- package/build/cli/render/projection-activity.js +65 -21
- package/build/cli/render/shared.js +8 -7
- package/build/cli/render/status.js +11 -23
- package/build/cli/render/wait.js +42 -64
- package/build/config/env-keys.js +1 -1
- package/build/config/env.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 +52 -38
- 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-activity.js +2 -0
- 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 +183 -60
- package/build/core/projection/projection-wake.js +171 -72
- package/build/core/status/board.js +55 -14
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/settlement-git.js +2 -2
- 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);
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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:
|
|
49
|
+
total: eligible.length,
|
|
50
50
|
inProgress,
|
|
51
51
|
ready,
|
|
52
52
|
blocked,
|
|
53
|
-
omitted:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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 {
|
|
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";
|
|
@@ -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, ["
|
|
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, ["
|
|
357
|
+
const actualTree = (await git(input.cwd, ["show", "-s", "--format=%T", commit])).trim();
|
|
358
358
|
if (actualTree === expectedTree)
|
|
359
359
|
return commit;
|
|
360
360
|
}
|