@astrosheep/keiyaku 2.9.6 → 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/harness/outcome.js +10 -0
- 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/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/flags.js +8 -0
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +131 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +103 -21
- package/build/cli/render/shared.js +38 -15
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.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/bind.js +111 -6
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/draft.js +1 -1
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +43 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +46 -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 +145 -66
- package/build/core/projection/generation/projection-generation-runtime.js +93 -11
- package/build/core/projection/generation/store.js +26 -1
- package/build/core/projection/generation/transitions.js +193 -14
- package/build/core/projection/index.js +5 -5
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +53 -10
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +177 -46
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- 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/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +55 -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 +28 -0
- package/skills/keiyaku-workflow/SKILL.md +142 -18
package/build/core/bind.js
CHANGED
|
@@ -3,12 +3,12 @@ import { assertSettingsKnobUsable } from "../config/settings/disease.js";
|
|
|
3
3
|
import { loadKeiyakuSettings } from "../config/settings/loader.js";
|
|
4
4
|
import { FlowError } from "../flow-error.js";
|
|
5
5
|
import { activeLedgerRef, appendEntry, appendWithRetry, readLedger } from "./ledger.js";
|
|
6
|
-
import { describeMissingBindSections, materializeBindDraft, mergeBindDraft, missingBindDraftSections, parseBindDraft, parsePartialBindDraft, renderMissingBindSectionsTemplate, } from "./draft.js";
|
|
7
|
-
import { assertValidContractId, buildContractId } from "./ids.js";
|
|
8
|
-
import { registerScope, releaseScope } from "./registry.js";
|
|
6
|
+
import { describeMissingBindSections, materializeBindDraft, mergeBindDraft, missingBindDraftSections, parseBindDraft, parsePartialBindDraft, renderBindDraft, renderMissingBindSectionsTemplate, } from "./draft.js";
|
|
7
|
+
import { assertValidContractId, buildContractId, isCommissionId } from "./ids.js";
|
|
8
|
+
import { previewScopeRegistration, registerScope, releaseScope } from "./registry.js";
|
|
9
9
|
import { resolveDefaultBranch } from "./status/drift.js";
|
|
10
10
|
import { normalizeTargetRef } from "./target-ref.js";
|
|
11
|
-
import { reconcileTaskForfeit, settleTaskBoardForBind } from "./task/index.js";
|
|
11
|
+
import { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind } from "./task/index.js";
|
|
12
12
|
import { bootstrapEntryDataFromObservation, isBootstrapSuccess, runWorktreeBootstrap, } from "./worktree-bootstrap.js";
|
|
13
13
|
import { contractWorktreePathFromBind, stableRepoRoot } from "./worktree-path.js";
|
|
14
14
|
import { allocateWorktreePlace, cleanupCreatedBindResources, createContractBranch, defaultCreateWorktree, materializeContractView, resolveTreeRoot, } from "./bind-workspace.js";
|
|
@@ -138,6 +138,99 @@ export function composeTaskBindDraft(input) {
|
|
|
138
138
|
}
|
|
139
139
|
return materializeBindDraft(merged);
|
|
140
140
|
}
|
|
141
|
+
async function validateBindAfter(cwd, after) {
|
|
142
|
+
const unique = new Set();
|
|
143
|
+
for (const contractId of after) {
|
|
144
|
+
if (unique.has(contractId)) {
|
|
145
|
+
throw new FlowError("INVALID_TARGET", `contract ${contractId} selected more than once by --after`);
|
|
146
|
+
}
|
|
147
|
+
unique.add(contractId);
|
|
148
|
+
if (!isCommissionId(contractId)) {
|
|
149
|
+
throw new FlowError("INVALID_TARGET", `after contract ${contractId} is not a valid contract id`);
|
|
150
|
+
}
|
|
151
|
+
if (!await readLedger(cwd, contractId)) {
|
|
152
|
+
throw new FlowError("INVALID_TARGET", `after contract ${contractId} does not exist`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The common read-only bind admission candidate. Mutation uses this candidate
|
|
158
|
+
* before task settlement; preview stops after it has observed all admission
|
|
159
|
+
* facts and rendered the canonical draft.
|
|
160
|
+
*/
|
|
161
|
+
async function admitBindCandidate(input, draft) {
|
|
162
|
+
const id = buildContractId({ name: draft.name, nowMs: input.nowMs, randomBytes: input.randomBytes });
|
|
163
|
+
await assertValidContractId(input.cwd, id);
|
|
164
|
+
await validateBindAfter(input.cwd, input.after);
|
|
165
|
+
const provisionalEntry = makeBindEntry({
|
|
166
|
+
id,
|
|
167
|
+
at: new Date(input.nowMs).toISOString(),
|
|
168
|
+
actor: input.actor,
|
|
169
|
+
draft,
|
|
170
|
+
// Preview never writes the task-board commit. The current target head is
|
|
171
|
+
// sufficient for its read-only provisional entry and worktree coordinate.
|
|
172
|
+
base: input.base,
|
|
173
|
+
target: input.target,
|
|
174
|
+
after: input.after,
|
|
175
|
+
workspace: input.workspace,
|
|
176
|
+
place: input.place,
|
|
177
|
+
bootstrap: input.bootstrapPlan,
|
|
178
|
+
});
|
|
179
|
+
const tree = await resolveTreeRoot(input.cwd, input.workspace, id, provisionalEntry);
|
|
180
|
+
const scopeOverlaps = await previewScopeRegistration(input.cwd, {
|
|
181
|
+
contractId: id,
|
|
182
|
+
scope: draft.scope,
|
|
183
|
+
tree,
|
|
184
|
+
exclusive: input.exclusive,
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
id,
|
|
188
|
+
draft,
|
|
189
|
+
canonicalDraft: renderBindDraft(draft),
|
|
190
|
+
target: input.target,
|
|
191
|
+
place: input.workspace === "worktree" ? input.place ?? null : null,
|
|
192
|
+
workspace: input.workspace,
|
|
193
|
+
after: input.after,
|
|
194
|
+
scopeOverlaps,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export async function previewBindContract(input) {
|
|
198
|
+
const isTaskBound = input.taskIds !== undefined && input.taskIds.length > 0;
|
|
199
|
+
const ordinaryDraft = !isTaskBound ? parseBindDraft(input.draft) : undefined;
|
|
200
|
+
const workspace = input.workspace ?? "here";
|
|
201
|
+
const defaultBranch = await resolveDefaultBranch(input.cwd);
|
|
202
|
+
if (!defaultBranch) {
|
|
203
|
+
throw new FlowError("INTERNAL_STATE", "cannot bind: no default branch");
|
|
204
|
+
}
|
|
205
|
+
const target = normalizeTargetRef(defaultBranch.branch);
|
|
206
|
+
const loadedSettings = await loadKeiyakuSettings(input.cwd);
|
|
207
|
+
let bootstrapPlan;
|
|
208
|
+
if (workspace === "worktree") {
|
|
209
|
+
assertSettingsKnobUsable(loadedSettings, "worktreeBootstrap");
|
|
210
|
+
bootstrapPlan = resolvedBootstrapPlan(loadedSettings.knobs?.worktreeBootstrap);
|
|
211
|
+
}
|
|
212
|
+
const randomBytes = input.randomBytes?.(0) ?? randomBytesDefault();
|
|
213
|
+
const place = workspace === "worktree" ? await allocateWorktreePlace(input.cwd, randomBytes) : undefined;
|
|
214
|
+
const taskAdmission = isTaskBound
|
|
215
|
+
? await previewTaskBoardForBind({ cwd: input.cwd, taskIds: input.taskIds })
|
|
216
|
+
: { tasks: [] };
|
|
217
|
+
const draft = isTaskBound
|
|
218
|
+
? composeTaskBindDraft({ task: taskAdmission.tasks[0], draft: input.draft })
|
|
219
|
+
: ordinaryDraft;
|
|
220
|
+
return await admitBindCandidate({
|
|
221
|
+
cwd: input.cwd,
|
|
222
|
+
actor: input.actor,
|
|
223
|
+
nowMs: input.nowMs,
|
|
224
|
+
after: input.after ?? [],
|
|
225
|
+
exclusive: input.exclusive,
|
|
226
|
+
workspace,
|
|
227
|
+
randomBytes,
|
|
228
|
+
base: defaultBranch.head,
|
|
229
|
+
target,
|
|
230
|
+
bootstrapPlan,
|
|
231
|
+
place,
|
|
232
|
+
}, draft);
|
|
233
|
+
}
|
|
141
234
|
export async function bindContract(input) {
|
|
142
235
|
const isTaskBound = input.taskIds !== undefined && input.taskIds.length > 0;
|
|
143
236
|
const ordinaryDraft = !isTaskBound ? parseBindDraft(input.draft) : undefined;
|
|
@@ -161,8 +254,20 @@ export async function bindContract(input) {
|
|
|
161
254
|
let settlement;
|
|
162
255
|
try {
|
|
163
256
|
const prepare = async (draft) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
257
|
+
const candidate = await admitBindCandidate({
|
|
258
|
+
cwd: input.cwd,
|
|
259
|
+
actor: input.actor,
|
|
260
|
+
nowMs: input.nowMs,
|
|
261
|
+
after: input.after ?? [],
|
|
262
|
+
exclusive: input.exclusive,
|
|
263
|
+
workspace,
|
|
264
|
+
randomBytes,
|
|
265
|
+
base: defaultBranch.head,
|
|
266
|
+
target,
|
|
267
|
+
bootstrapPlan,
|
|
268
|
+
place,
|
|
269
|
+
}, draft);
|
|
270
|
+
const id = candidate.id;
|
|
166
271
|
const ref = activeLedgerRef(id);
|
|
167
272
|
return {
|
|
168
273
|
contractId: id,
|
|
@@ -2,7 +2,7 @@ import * as fs from "fs/promises";
|
|
|
2
2
|
import { FlowError } from "../../flow-error.js";
|
|
3
3
|
import { assertGitObservation, observeGitRepository } from "../../git/branches.js";
|
|
4
4
|
import { createGit, wrapGitError } from "../../git/core.js";
|
|
5
|
-
import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, withResolvedAddress, } from "../addressing.js";
|
|
5
|
+
import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, resolveCallWorktreeAddress, withResolvedAddress, } from "../addressing.js";
|
|
6
6
|
import { normalizeFilesystemCoordinate } from "../../fs/path-coordinate.js";
|
|
7
7
|
import { findOpenArcView } from "../arc.js";
|
|
8
8
|
import { readLedger } from "../ledger.js";
|
|
@@ -139,7 +139,24 @@ export async function readCallKeiyakuContext(executionCwd, input) {
|
|
|
139
139
|
},
|
|
140
140
|
}));
|
|
141
141
|
}
|
|
142
|
-
if (input.bare
|
|
142
|
+
if (input.bare) {
|
|
143
|
+
return {
|
|
144
|
+
activeKeiyaku: false,
|
|
145
|
+
effectiveCwd: executionCwd,
|
|
146
|
+
historyCwd: ledgerCwd,
|
|
147
|
+
profileCwd: ledgerCwd,
|
|
148
|
+
projectionCwd: ledgerCwd,
|
|
149
|
+
address: buildUncommissionedRepositoryAddress({
|
|
150
|
+
stableRoot: ledgerCwd,
|
|
151
|
+
workdir: executionCwd,
|
|
152
|
+
binding: "bare",
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const address = input.contractId
|
|
157
|
+
? await resolveContractAddress(ledgerCwd, input.contractId, input.contractAddressSource)
|
|
158
|
+
: await resolveCallWorktreeAddress(ledgerCwd, executionCwd);
|
|
159
|
+
if (!address) {
|
|
143
160
|
return {
|
|
144
161
|
activeKeiyaku: false,
|
|
145
162
|
effectiveCwd: executionCwd,
|
|
@@ -153,7 +170,6 @@ export async function readCallKeiyakuContext(executionCwd, input) {
|
|
|
153
170
|
}),
|
|
154
171
|
};
|
|
155
172
|
}
|
|
156
|
-
const address = await resolveContractAddress(ledgerCwd, input.contractId, input.contractAddressSource);
|
|
157
173
|
return await withResolvedAddress(address, async () => {
|
|
158
174
|
const contractId = address.binding.commissionId;
|
|
159
175
|
const ledger = await readLedger(ledgerCwd, contractId);
|
|
@@ -7,7 +7,7 @@ import { assertProjectionAlias, moveProjectionAlias } from "../projection/index.
|
|
|
7
7
|
import { projectionDir } from "../projection/index.js";
|
|
8
8
|
import { createProjectionExecutionId } from "../projection/index.js";
|
|
9
9
|
import { PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration } from "../projection/index.js";
|
|
10
|
-
import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "../projection/index.js";
|
|
10
|
+
import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, acquireProjectionRunnerLock, } from "../projection/index.js";
|
|
11
11
|
import { observeProjectionLife } from "../projection/index.js";
|
|
12
12
|
import { mintProjection, mintRepositoryProjection } from "../projection/index.js";
|
|
13
13
|
import { waitForProjection } from "../projection/index.js";
|
|
@@ -60,13 +60,29 @@ export function outcomeFromGeneration(current) {
|
|
|
60
60
|
return restoreOutcome(record);
|
|
61
61
|
}
|
|
62
62
|
export async function executeSubagent(agentName, prompt, cwd, options) {
|
|
63
|
-
const { projection: projectionCoordinate, launchSnapshot } = await buildCallConfig(agentName, prompt, cwd, options);
|
|
63
|
+
const { projection: projectionCoordinate, launchSnapshot, launchFacts } = await buildCallConfig(agentName, prompt, cwd, options);
|
|
64
64
|
if (!projectionCoordinate) {
|
|
65
65
|
throw new FlowError("INTERNAL_STATE", "durable execution requires a projection coordinate");
|
|
66
66
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const launchOwnership = acquireProjectionRunnerLock(projectionCoordinate.dir);
|
|
68
|
+
const launched = await startProjectionGeneration(projectionCoordinate.dir, projectionCoordinate.executionId, {
|
|
69
|
+
commitLaunch: () => {
|
|
70
|
+
const store = openProjectionGenerationStore(projectionCoordinate.dir);
|
|
71
|
+
try {
|
|
72
|
+
return store.launchIfSettled({ executionId: projectionCoordinate.executionId, facts: launchFacts }).status === "committed";
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
store.close();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
launchOwnership,
|
|
79
|
+
});
|
|
80
|
+
launchOwnership.close();
|
|
81
|
+
if (launched.status !== "adopted") {
|
|
82
|
+
const detail = launched.status === "pending"
|
|
83
|
+
? "runner retained startup ownership without adopting before the bounded launch wait"
|
|
84
|
+
: launched.detail;
|
|
85
|
+
throw new FlowError("SUBAGENT_EXEC_ERROR", detail);
|
|
70
86
|
}
|
|
71
87
|
const joined = await waitForProjection({
|
|
72
88
|
projectionDirectory: projectionCoordinate.dir,
|
|
@@ -111,13 +127,28 @@ export async function executeSubagent(agentName, prompt, cwd, options) {
|
|
|
111
127
|
throw new FlowError("INTERNAL_STATE", "Unhandled subagent outcome state.");
|
|
112
128
|
}
|
|
113
129
|
export async function launchSubagentGeneration(agentName, prompt, cwd, options) {
|
|
114
|
-
const { projection, launchSnapshot } = await buildCallConfig(agentName, prompt, cwd, options);
|
|
130
|
+
const { projection, launchSnapshot, launchFacts } = await buildCallConfig(agentName, prompt, cwd, options);
|
|
115
131
|
if (!projection) {
|
|
116
132
|
throw new FlowError("INTERNAL_STATE", "durable launch requires a projection coordinate");
|
|
117
133
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
const launchOwnership = acquireProjectionRunnerLock(projection.dir);
|
|
135
|
+
const launched = await startProjectionGeneration(projection.dir, projection.executionId, {
|
|
136
|
+
commitLaunch: () => {
|
|
137
|
+
const store = openProjectionGenerationStore(projection.dir);
|
|
138
|
+
try {
|
|
139
|
+
return store.launchIfSettled({ executionId: projection.executionId, facts: launchFacts }).status === "committed";
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
store.close();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
launchOwnership,
|
|
146
|
+
});
|
|
147
|
+
launchOwnership.close();
|
|
148
|
+
if (launched.status !== "adopted") {
|
|
149
|
+
throw new FlowError("SUBAGENT_EXEC_ERROR", launched.status === "pending"
|
|
150
|
+
? "runner retained startup ownership without adopting before the bounded launch wait"
|
|
151
|
+
: launched.detail);
|
|
121
152
|
}
|
|
122
153
|
return { projection, launchSnapshot };
|
|
123
154
|
}
|
|
@@ -173,16 +204,6 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
|
|
|
173
204
|
} : {}),
|
|
174
205
|
},
|
|
175
206
|
}));
|
|
176
|
-
const store = openProjectionGenerationStore(minted.dir);
|
|
177
|
-
try {
|
|
178
|
-
const launch = store.launchIfSettled({ executionId, facts: launchFacts });
|
|
179
|
-
if (launch.status !== "committed") {
|
|
180
|
-
throw new FlowError("INTERNAL_STATE", `initial generation launch rejected: ${launch.reason}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
store.close();
|
|
185
|
-
}
|
|
186
207
|
const previousAliasTarget = options.alias === undefined
|
|
187
208
|
? undefined
|
|
188
209
|
: await moveProjectionAlias(options.projectionCwd ?? cwd, assertProjectionAlias(options.alias), minted.id);
|
|
@@ -202,6 +223,7 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
|
|
|
202
223
|
id: minted.id,
|
|
203
224
|
dir: minted.dir,
|
|
204
225
|
executionId,
|
|
226
|
+
launchFacts,
|
|
205
227
|
...(options.alias ? { alias: options.alias } : {}),
|
|
206
228
|
...(previousAliasTargetStillRunning ? { previousAliasTarget: previousAliasTargetStillRunning } : {}),
|
|
207
229
|
};
|
|
@@ -209,9 +231,10 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
|
|
|
209
231
|
// Every public launch has one durable projection object. Repository-free
|
|
210
232
|
// authority changes the storage owner, not whether the object exists; mint
|
|
211
233
|
// failure is therefore a launch failure rather than an in-process fallback.
|
|
212
|
-
const projectionCoordinate = await publishProjection();
|
|
234
|
+
const { launchFacts, ...projectionCoordinate } = await publishProjection();
|
|
213
235
|
return {
|
|
214
236
|
projection: projectionCoordinate,
|
|
215
237
|
launchSnapshot,
|
|
238
|
+
launchFacts,
|
|
216
239
|
};
|
|
217
240
|
}
|
package/build/core/draft.js
CHANGED
|
@@ -204,7 +204,7 @@ export function renderMissingBindSectionsTemplate(missing) {
|
|
|
204
204
|
continue;
|
|
205
205
|
}
|
|
206
206
|
if (section === "scope") {
|
|
207
|
-
lines.push("## Scope", "
|
|
207
|
+
lines.push("## Scope", "~~~", "<pattern>", "<another-pattern>", "~~~");
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
210
|
lines.push("## Checks", "- <verification>");
|
|
@@ -0,0 +1,194 @@
|
|
|
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, activeLedgerRef, } from "./ledger.js";
|
|
5
|
+
const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
|
|
6
|
+
async function runLedgerGit(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")
|
|
23
|
+
return null;
|
|
24
|
+
if (record.kind === "ambiguous") {
|
|
25
|
+
throw new Error(`[git cat-file ${expression}] object is ambiguous`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
oid: record.record.oid,
|
|
29
|
+
type: record.record.type,
|
|
30
|
+
content: record.record.content,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function parseCommitObject(object) {
|
|
34
|
+
if (object.type !== "commit") {
|
|
35
|
+
throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
|
|
36
|
+
}
|
|
37
|
+
const content = object.content.toString("utf8");
|
|
38
|
+
const messageStart = content.indexOf("\n\n");
|
|
39
|
+
if (messageStart < 0)
|
|
40
|
+
throw new Error(`git commit ${object.oid} has no message separator`);
|
|
41
|
+
const parents = [];
|
|
42
|
+
for (const line of content.slice(0, messageStart).split("\n")) {
|
|
43
|
+
if (!line.startsWith("parent "))
|
|
44
|
+
continue;
|
|
45
|
+
const parent = line.slice("parent ".length);
|
|
46
|
+
if (!/^[0-9a-f]{40,64}$/.test(parent)) {
|
|
47
|
+
throw new Error(`git commit ${object.oid} has an invalid parent header`);
|
|
48
|
+
}
|
|
49
|
+
parents.push(parent);
|
|
50
|
+
}
|
|
51
|
+
return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
|
|
52
|
+
}
|
|
53
|
+
function parseLedgerRefs(output) {
|
|
54
|
+
return output
|
|
55
|
+
.toString("utf8")
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
|
|
58
|
+
.map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
|
|
59
|
+
}
|
|
60
|
+
async function readLedgerFromBatch(batch, ref, commits) {
|
|
61
|
+
const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
|
|
62
|
+
if (!headObject)
|
|
63
|
+
return null;
|
|
64
|
+
const head = parseCommitObject(headObject);
|
|
65
|
+
commits.set(head.oid, Promise.resolve(head));
|
|
66
|
+
const ordered = [];
|
|
67
|
+
const visited = new Set();
|
|
68
|
+
const visiting = new Set();
|
|
69
|
+
const visit = async (oid) => {
|
|
70
|
+
if (visited.has(oid))
|
|
71
|
+
return;
|
|
72
|
+
if (visiting.has(oid))
|
|
73
|
+
throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
|
|
74
|
+
visiting.add(oid);
|
|
75
|
+
let pending = commits.get(oid);
|
|
76
|
+
if (!pending) {
|
|
77
|
+
pending = queryBatchObject(batch, oid).then((object) => {
|
|
78
|
+
if (!object)
|
|
79
|
+
throw new Error(`[git cat-file ${oid}] object is missing`);
|
|
80
|
+
return parseCommitObject(object);
|
|
81
|
+
});
|
|
82
|
+
commits.set(oid, pending);
|
|
83
|
+
}
|
|
84
|
+
const commit = await pending;
|
|
85
|
+
for (const parent of commit.parents)
|
|
86
|
+
await visit(parent);
|
|
87
|
+
visiting.delete(oid);
|
|
88
|
+
visited.add(oid);
|
|
89
|
+
ordered.push(commit);
|
|
90
|
+
};
|
|
91
|
+
await visit(head.oid);
|
|
92
|
+
return {
|
|
93
|
+
contractId: ref.contractId,
|
|
94
|
+
ref: ref.ref,
|
|
95
|
+
head: head.oid,
|
|
96
|
+
entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function closeBatch(cwd, batch) {
|
|
100
|
+
const close = await batch.close();
|
|
101
|
+
if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
|
|
102
|
+
throw wrapGitError("cat-file --batch", {
|
|
103
|
+
message: close.timedOut
|
|
104
|
+
? "git timed out"
|
|
105
|
+
: close.signal
|
|
106
|
+
? `git exited from signal ${close.signal}`
|
|
107
|
+
: `git exited with status ${close.code ?? "unknown"}`,
|
|
108
|
+
stderr: close.stderr.toString("utf8"),
|
|
109
|
+
}, cwd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readLedgerRefs(cwd, refs) {
|
|
113
|
+
if (refs.length === 0)
|
|
114
|
+
return [];
|
|
115
|
+
const batch = openGitStreamingBatch(cwd);
|
|
116
|
+
const commits = new Map();
|
|
117
|
+
const results = [];
|
|
118
|
+
let readError;
|
|
119
|
+
try {
|
|
120
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
121
|
+
const ref = refs[index];
|
|
122
|
+
try {
|
|
123
|
+
const ledger = await readLedgerFromBatch(batch, ref, commits);
|
|
124
|
+
if (ledger)
|
|
125
|
+
results.push(ledger);
|
|
126
|
+
else
|
|
127
|
+
results.push({ id: ref.contractId, error: new Error(`ledger ref disappeared: ${ref.ref}`) });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const diagnostic = error instanceof Error ? error : new Error(String(error));
|
|
131
|
+
results.push({ id: ref.contractId, error: diagnostic });
|
|
132
|
+
if (error instanceof GitStreamingBatchFramingError
|
|
133
|
+
|| error instanceof GitStreamingBatchResponseEndedError) {
|
|
134
|
+
const isolationReason = error instanceof GitStreamingBatchResponseEndedError
|
|
135
|
+
? "response ended"
|
|
136
|
+
: "malformed frame";
|
|
137
|
+
for (const pending of refs.slice(index + 1)) {
|
|
138
|
+
results.push({
|
|
139
|
+
id: pending.contractId,
|
|
140
|
+
error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
readError = error;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await closeBatch(cwd, batch);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!readError)
|
|
156
|
+
readError = error;
|
|
157
|
+
}
|
|
158
|
+
if (readError)
|
|
159
|
+
throw readError;
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
/** Read every ledger with two Git processes regardless of ledger or entry count. */
|
|
163
|
+
export async function readAllLedgerSnapshots(cwd) {
|
|
164
|
+
const refOutput = await runLedgerGit(cwd, [
|
|
165
|
+
"for-each-ref",
|
|
166
|
+
"--sort=refname",
|
|
167
|
+
"--format=%(refname)",
|
|
168
|
+
ACTIVE_LEDGER_REF_PREFIX,
|
|
169
|
+
]);
|
|
170
|
+
return readLedgerRefs(cwd, parseLedgerRefs(refOutput));
|
|
171
|
+
}
|
|
172
|
+
/** Read one exact ledger with one Git process and without enumerating other refs. */
|
|
173
|
+
export async function readLedgerSnapshot(cwd, contractId) {
|
|
174
|
+
const ref = activeLedgerRef(contractId);
|
|
175
|
+
const batch = openGitStreamingBatch(cwd);
|
|
176
|
+
let ledger = null;
|
|
177
|
+
let readError;
|
|
178
|
+
try {
|
|
179
|
+
ledger = await readLedgerFromBatch(batch, { contractId, ref }, new Map());
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
readError = error;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await closeBatch(cwd, batch);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!readError)
|
|
189
|
+
readError = error;
|
|
190
|
+
}
|
|
191
|
+
if (readError)
|
|
192
|
+
throw readError;
|
|
193
|
+
return ledger;
|
|
194
|
+
}
|
|
@@ -4,7 +4,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
import { configureHeartBusyTimeout } from "../heart.js";
|
|
5
5
|
import { ProjectionGenerationStoreError, parseStoredRow, validateHistory, } from "./model.js";
|
|
6
6
|
export const PROJECTION_GENERATION_STORE_FILE = "heart";
|
|
7
|
-
export const PROJECTION_GENERATION_SCHEMA_VERSION =
|
|
7
|
+
export const PROJECTION_GENERATION_SCHEMA_VERSION = 3;
|
|
8
8
|
const SCHEMA_TABLE_SQL = `
|
|
9
9
|
CREATE TABLE generation_schema (
|
|
10
10
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
@@ -14,7 +14,7 @@ const SCHEMA_TABLE_SQL = `
|
|
|
14
14
|
const RECORD_TABLE_SQL = `
|
|
15
15
|
CREATE TABLE generation_records (
|
|
16
16
|
seq INTEGER PRIMARY KEY CHECK (seq > 0),
|
|
17
|
-
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'verdict')),
|
|
17
|
+
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
|
|
18
18
|
execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
|
|
19
19
|
facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
|
|
20
20
|
) STRICT
|
|
@@ -250,6 +250,25 @@ export function openReadOnlyDatabase(projectionDirectory) {
|
|
|
250
250
|
throw projectionGenerationStoreReadError(databasePath, error);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
/** Read the validated heart through one consistent SQLite snapshot. */
|
|
254
|
+
export function withGenerationReadSnapshot(projectionDirectory, read) {
|
|
255
|
+
const { database } = openReadOnlyDatabase(projectionDirectory);
|
|
256
|
+
try {
|
|
257
|
+
database.exec("BEGIN");
|
|
258
|
+
try {
|
|
259
|
+
const result = read(database);
|
|
260
|
+
database.exec("COMMIT");
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
rollbackQuietly(database);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
closeDatabase(database);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
253
272
|
export function assertDatabaseOpen(database) {
|
|
254
273
|
if (closedDatabases.has(database))
|
|
255
274
|
throw new ProjectionGenerationStoreError("projection generation store is closed");
|
|
@@ -276,6 +295,8 @@ export function writeTransition(database, readOnly, run) {
|
|
|
276
295
|
if (readOnly) {
|
|
277
296
|
throw new ProjectionGenerationStoreError("projection generation store is read-only");
|
|
278
297
|
}
|
|
298
|
+
if (database.isTransaction)
|
|
299
|
+
return run();
|
|
279
300
|
database.exec("BEGIN IMMEDIATE");
|
|
280
301
|
try {
|
|
281
302
|
const result = run();
|
|
@@ -287,6 +308,26 @@ export function writeTransition(database, readOnly, run) {
|
|
|
287
308
|
throw error;
|
|
288
309
|
}
|
|
289
310
|
}
|
|
311
|
+
export function beginGenerationWriteTransaction(database, readOnly) {
|
|
312
|
+
assertDatabaseOpen(database);
|
|
313
|
+
if (readOnly)
|
|
314
|
+
throw new ProjectionGenerationStoreError("projection generation store is read-only");
|
|
315
|
+
if (database.isTransaction)
|
|
316
|
+
throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
|
|
317
|
+
database.exec("BEGIN IMMEDIATE");
|
|
318
|
+
}
|
|
319
|
+
export function commitGenerationWriteTransaction(database) {
|
|
320
|
+
assertDatabaseOpen(database);
|
|
321
|
+
if (!database.isTransaction)
|
|
322
|
+
throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
|
|
323
|
+
database.exec("COMMIT");
|
|
324
|
+
}
|
|
325
|
+
export function rollbackGenerationWriteTransaction(database) {
|
|
326
|
+
assertDatabaseOpen(database);
|
|
327
|
+
if (!database.isTransaction)
|
|
328
|
+
return;
|
|
329
|
+
rollbackQuietly(database);
|
|
330
|
+
}
|
|
290
331
|
export function closeDatabase(database) {
|
|
291
332
|
if (closedDatabases.has(database))
|
|
292
333
|
return;
|
|
@@ -133,6 +133,16 @@ export function validateKillIntentFacts(value, location) {
|
|
|
133
133
|
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
134
134
|
return facts;
|
|
135
135
|
}
|
|
136
|
+
export function validateInterruptIntentFacts(value, location) {
|
|
137
|
+
const facts = cloneFacts(value, location);
|
|
138
|
+
facts.interruptedAt = assertIsoTimestamp(facts.interruptedAt, `${location}.interruptedAt`);
|
|
139
|
+
if ("operatorAction" in facts) {
|
|
140
|
+
throw new ProjectionGenerationStoreError(`${location}.operatorAction is not part of interrupt-intent`);
|
|
141
|
+
}
|
|
142
|
+
if ("state" in facts)
|
|
143
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
144
|
+
return facts;
|
|
145
|
+
}
|
|
136
146
|
export function validateVerdictFacts(value, location) {
|
|
137
147
|
const facts = cloneFacts(value, location);
|
|
138
148
|
if (typeof facts.state !== "string" || !VERDICTS.has(facts.state)) {
|
|
@@ -182,6 +192,7 @@ export function parseStoredRow(row) {
|
|
|
182
192
|
if (row.kind !== "launch"
|
|
183
193
|
&& row.kind !== "adoption"
|
|
184
194
|
&& row.kind !== "kill-intent"
|
|
195
|
+
&& row.kind !== "interrupt-intent"
|
|
185
196
|
&& row.kind !== "verdict") {
|
|
186
197
|
throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${row.kind}'`);
|
|
187
198
|
}
|
|
@@ -200,7 +211,9 @@ export function parseStoredRow(row) {
|
|
|
200
211
|
? validateAdoptionFacts(decoded, location)
|
|
201
212
|
: row.kind === "kill-intent"
|
|
202
213
|
? validateKillIntentFacts(decoded, location)
|
|
203
|
-
:
|
|
214
|
+
: row.kind === "interrupt-intent"
|
|
215
|
+
? validateInterruptIntentFacts(decoded, location)
|
|
216
|
+
: validateVerdictFacts(decoded, location);
|
|
204
217
|
return { seq, kind: row.kind, executionId, facts };
|
|
205
218
|
}
|
|
206
219
|
export function validateHistory(records) {
|
|
@@ -237,13 +250,21 @@ export function validateHistory(records) {
|
|
|
237
250
|
continue;
|
|
238
251
|
}
|
|
239
252
|
if (record.kind === "kill-intent") {
|
|
240
|
-
if (current.killIntent)
|
|
241
|
-
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates
|
|
253
|
+
if (current.killIntent || current.interruptIntent)
|
|
254
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates generation stop intent`);
|
|
242
255
|
if (current.verdict)
|
|
243
256
|
throw new ProjectionGenerationStoreError(`generation row ${record.seq} records kill-intent after verdict`);
|
|
244
257
|
current.killIntent = record;
|
|
245
258
|
continue;
|
|
246
259
|
}
|
|
260
|
+
if (record.kind === "interrupt-intent") {
|
|
261
|
+
if (current.killIntent || current.interruptIntent)
|
|
262
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates generation stop intent`);
|
|
263
|
+
if (current.verdict)
|
|
264
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} records interrupt-intent after verdict`);
|
|
265
|
+
current.interruptIntent = record;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
247
268
|
if (current.verdict)
|
|
248
269
|
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates verdict`);
|
|
249
270
|
current.verdict = record;
|