@astrosheep/keiyaku 2.9.8 → 2.9.10
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/README.md +50 -182
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +10 -7
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +242 -79
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +35 -337
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +125 -127
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { parseResolvedAkumaLaunchSnapshot, } from "../agents/launch-snapshot/model.js";
|
|
5
5
|
import { resolvePersistedSubagentSession } from "../agents/session.js";
|
|
6
|
+
import { OutcomeErrorValidationError, parseCurrentOutcomeError, } from "../agents/harness/index.js";
|
|
6
7
|
import { isErrnoException } from "../errno.js";
|
|
7
8
|
import { FlowError, requireText } from "../flow-error.js";
|
|
8
9
|
import { getCurrentBranch } from "../git/branches.js";
|
|
@@ -19,6 +20,7 @@ export { isResponseArtifactId, RESPONSE_ARTIFACT_ID_RE } from "./response-artifa
|
|
|
19
20
|
const RESPONSE_HISTORY_SLUG_MAX_LENGTH = 24;
|
|
20
21
|
const RESPONSE_HISTORY_SESSION_KEY = "session";
|
|
21
22
|
const RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY = "launch_snapshot";
|
|
23
|
+
const RESPONSE_HISTORY_TERMINAL_FAILURE_KEY = "terminal_failure";
|
|
22
24
|
const RESPONSE_HISTORY_PROVENANCE_KEY = "provenance";
|
|
23
25
|
const REVIEW_HISTORY_FILE_RE = /^\d{8}-\d{9}_REVIEW_(\d+)\.md$/;
|
|
24
26
|
/** Artifact-owner rendering of seal receipt/diffstat facts into the durable Diff section. */
|
|
@@ -99,6 +101,7 @@ function buildMarkdown(input, iso, provenance) {
|
|
|
99
101
|
`${RESPONSE_HISTORY_PROVENANCE_KEY}: ${yamlQuote(JSON.stringify(provenance))}`,
|
|
100
102
|
...(input.session ? [`${RESPONSE_HISTORY_SESSION_KEY}: ${yamlQuote(JSON.stringify(input.session))}`] : []),
|
|
101
103
|
...(input.launchSnapshot ? [`${RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY}: ${yamlQuote(JSON.stringify(input.launchSnapshot))}`] : []),
|
|
104
|
+
...(input.tool === "call" && input.terminalFailure ? [`${RESPONSE_HISTORY_TERMINAL_FAILURE_KEY}: ${yamlQuote(JSON.stringify(input.terminalFailure))}`] : []),
|
|
102
105
|
...(input.subagentName?.trim() ? [`subagent_name: ${yamlQuote(input.subagentName.trim())}`] : []),
|
|
103
106
|
...(input.effectivePolicy ? [`effective_policy: ${yamlQuote(JSON.stringify(input.effectivePolicy))}`] : []),
|
|
104
107
|
`time: ${yamlQuote(iso)}`,
|
|
@@ -172,6 +175,10 @@ export async function readResponseArtifact(input) {
|
|
|
172
175
|
throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact not found: '${locator}'.`);
|
|
173
176
|
throw error;
|
|
174
177
|
}
|
|
178
|
+
return parseResponseArtifactDocument(input, locator, absolutePath, document);
|
|
179
|
+
}
|
|
180
|
+
/** Parse one already-read response document without a second filesystem read. */
|
|
181
|
+
async function parseResponseArtifactDocument(input, locator, absolutePath, document) {
|
|
175
182
|
const ast = parseToAST(document);
|
|
176
183
|
const frontmatter = ast.frontmatter?.entries;
|
|
177
184
|
if (!frontmatter)
|
|
@@ -215,9 +222,94 @@ export async function readResponseArtifact(input) {
|
|
|
215
222
|
throw artifactError(locator, RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY, `is invalid${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
216
223
|
}
|
|
217
224
|
}
|
|
225
|
+
let terminalFailure;
|
|
226
|
+
const rawTerminalFailure = frontmatter[RESPONSE_HISTORY_TERMINAL_FAILURE_KEY];
|
|
227
|
+
if (rawTerminalFailure !== undefined) {
|
|
228
|
+
let terminalFailureValue;
|
|
229
|
+
try {
|
|
230
|
+
terminalFailureValue = JSON.parse(rawTerminalFailure);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw artifactError(locator, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY, `contains malformed JSON${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
terminalFailure = parseCurrentOutcomeError(terminalFailureValue, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (error instanceof OutcomeErrorValidationError) {
|
|
240
|
+
throw artifactError(locator, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY, `is invalid (${error.message})`);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
218
245
|
const subagentName = frontmatter.subagent_name?.trim() || undefined;
|
|
219
246
|
const storageRoot = await fs.realpath(path.resolve(path.dirname(absolutePath), "..", ".."));
|
|
220
|
-
return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(subagentName ? { subagentName } : {}) };
|
|
247
|
+
return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(terminalFailure ? { terminalFailure } : {}), ...(subagentName ? { subagentName } : {}) };
|
|
248
|
+
}
|
|
249
|
+
function artifactIdFromDocument(document) {
|
|
250
|
+
const encoded = parseToAST(document).frontmatter?.entries?.[RESPONSE_HISTORY_PROVENANCE_KEY];
|
|
251
|
+
if (encoded === undefined)
|
|
252
|
+
return undefined;
|
|
253
|
+
try {
|
|
254
|
+
const provenance = JSON.parse(encoded);
|
|
255
|
+
return isPlainRecord(provenance) && typeof provenance.artifactId === "string" && isResponseArtifactId(provenance.artifactId)
|
|
256
|
+
? provenance.artifactId
|
|
257
|
+
: undefined;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Read each response artifact file at most once to build a query-local lookup. */
|
|
264
|
+
export async function indexResponseArtifacts(input) {
|
|
265
|
+
let stableRoot;
|
|
266
|
+
try {
|
|
267
|
+
stableRoot = await fs.realpath(input.cwd);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (!isErrnoException(error) || error.code !== "ENOENT")
|
|
271
|
+
throw error;
|
|
272
|
+
stableRoot = path.resolve(input.cwd);
|
|
273
|
+
}
|
|
274
|
+
const historyDir = path.join(stableRoot, RESPONSE_HISTORY_DIR);
|
|
275
|
+
let names;
|
|
276
|
+
try {
|
|
277
|
+
names = await (input.readDirectory ?? fs.readdir)(historyDir);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
281
|
+
return { coverage: "complete", lookup: () => undefined };
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const entries = new Map();
|
|
285
|
+
let coverage = "complete";
|
|
286
|
+
for (const name of names.filter((candidate) => candidate.endsWith(".md")).sort()) {
|
|
287
|
+
const responsePath = path.join(RESPONSE_HISTORY_DIR, name);
|
|
288
|
+
let document;
|
|
289
|
+
try {
|
|
290
|
+
document = await fs.readFile(path.join(stableRoot, responsePath), "utf8");
|
|
291
|
+
const artifact = await parseResponseArtifactDocument({ cwd: stableRoot, responsePath }, responsePath, path.join(stableRoot, responsePath), document);
|
|
292
|
+
if (!artifact)
|
|
293
|
+
continue;
|
|
294
|
+
if (entries.has(artifact.artifactId))
|
|
295
|
+
entries.set(artifact.artifactId, { state: "incompatible" });
|
|
296
|
+
else
|
|
297
|
+
entries.set(artifact.artifactId, { state: "available", artifact });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
const artifactId = document === undefined ? undefined : artifactIdFromDocument(document);
|
|
301
|
+
if (isErrnoException(error)) {
|
|
302
|
+
if (artifactId && error.code !== "ENOENT")
|
|
303
|
+
entries.set(artifactId, { state: "unavailable" });
|
|
304
|
+
if (error.code !== "ENOENT")
|
|
305
|
+
coverage = "incomplete";
|
|
306
|
+
}
|
|
307
|
+
else if (artifactId) {
|
|
308
|
+
entries.set(artifactId, { state: "incompatible" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { coverage, lookup: (artifactId) => entries.get(artifactId) };
|
|
221
313
|
}
|
|
222
314
|
async function findResponseArtifactAtRoot(input) {
|
|
223
315
|
const physicalHistoryDir = path.join(input.stableRoot, RESPONSE_HISTORY_DIR);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
-
|
|
2
|
+
import { terminateProcessGroup } from "./process-group.js";
|
|
3
|
+
export const WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES = 2048;
|
|
3
4
|
export const WORKTREE_BOOTSTRAP_KILL_GRACE_MS = 10_000;
|
|
4
5
|
function defaultWait(ms) {
|
|
5
6
|
return new Promise((resolve) => {
|
|
@@ -8,16 +9,16 @@ function defaultWait(ms) {
|
|
|
8
9
|
});
|
|
9
10
|
}
|
|
10
11
|
/** Retain the last 2048 UTF-8 bytes and decode lossily (partial leading code point → U+FFFD). */
|
|
11
|
-
export function tailUtf8Bytes(bytes, maxBytes =
|
|
12
|
+
export function tailUtf8Bytes(bytes, maxBytes = WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES) {
|
|
12
13
|
const truncated = bytes.byteLength > maxBytes;
|
|
13
14
|
const slice = truncated ? bytes.subarray(bytes.byteLength - maxBytes) : bytes;
|
|
14
15
|
return { text: slice.toString("utf8"), truncated };
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
* Rolling
|
|
18
|
-
* remembers whether any earlier prefix was dropped.
|
|
18
|
+
* Rolling output accumulator: holds at most `maxBytes` across every chunk and
|
|
19
|
+
* remembers whether any earlier prefix was dropped.
|
|
19
20
|
*/
|
|
20
|
-
function createRollingByteTail(maxBytes =
|
|
21
|
+
function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES) {
|
|
21
22
|
let tail = Buffer.alloc(0);
|
|
22
23
|
let truncated = false;
|
|
23
24
|
return {
|
|
@@ -56,17 +57,15 @@ function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES)
|
|
|
56
57
|
},
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
|
-
function observationFrom(result, durationMs,
|
|
60
|
+
function observationFrom(result, durationMs, output) {
|
|
60
61
|
const safeDuration = Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(durationMs)));
|
|
61
|
-
if (!stderr || stderr.bytes.byteLength === 0) {
|
|
62
|
-
return { result, durationMs: safeDuration };
|
|
63
|
-
}
|
|
64
|
-
// Buffer is already the bounded tail; decode lossily (partial leading code point → U+FFFD).
|
|
65
62
|
return {
|
|
66
63
|
result,
|
|
67
64
|
durationMs: safeDuration,
|
|
68
|
-
|
|
69
|
-
...(
|
|
65
|
+
...(output.stdout ? { stdoutTail: output.stdout.bytes.toString("utf8") } : {}),
|
|
66
|
+
...(output.stdout?.truncated ? { stdoutTruncated: true } : {}),
|
|
67
|
+
...(output.stderr ? { stderrTail: output.stderr.bytes.toString("utf8") } : {}),
|
|
68
|
+
...(output.stderr?.truncated ? { stderrTruncated: true } : {}),
|
|
70
69
|
};
|
|
71
70
|
}
|
|
72
71
|
function spawnFailureObservation(message, durationMs) {
|
|
@@ -78,17 +77,10 @@ function spawnFailureObservation(message, durationMs) {
|
|
|
78
77
|
...(tail.truncated ? { stderrTruncated: true } : {}),
|
|
79
78
|
};
|
|
80
79
|
}
|
|
81
|
-
function killChild(child, signal) {
|
|
82
|
-
try {
|
|
83
|
-
child.kill(signal);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// Process may have raced out between the liveness check and kill.
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
80
|
/**
|
|
90
81
|
* Run the project-declared worktree bootstrap once.
|
|
91
|
-
* argv only (shell: false), cwd = new worktree, inherited env, stdin
|
|
82
|
+
* argv only (shell: false), cwd = new worktree, inherited env, stdin ignored.
|
|
83
|
+
* Output capture is opt-in so bind retains its established stdio and persistence behavior.
|
|
92
84
|
*/
|
|
93
85
|
export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
94
86
|
const spawn = dependencies.spawn ?? nodeSpawn;
|
|
@@ -102,6 +94,7 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
|
102
94
|
};
|
|
103
95
|
}
|
|
104
96
|
const startedAt = nowMs();
|
|
97
|
+
const stdoutTail = input.captureOutput ? createRollingByteTail() : null;
|
|
105
98
|
const stderrTail = createRollingByteTail();
|
|
106
99
|
let child;
|
|
107
100
|
try {
|
|
@@ -109,7 +102,8 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
|
109
102
|
cwd: input.cwd,
|
|
110
103
|
env: process.env,
|
|
111
104
|
shell: false,
|
|
112
|
-
|
|
105
|
+
detached: process.platform !== "win32",
|
|
106
|
+
stdio: ["ignore", input.captureOutput ? "pipe" : "ignore", "pipe"],
|
|
113
107
|
});
|
|
114
108
|
}
|
|
115
109
|
catch (error) {
|
|
@@ -129,6 +123,9 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
|
129
123
|
clearTimeout(timeoutHandle);
|
|
130
124
|
resolve(observation);
|
|
131
125
|
};
|
|
126
|
+
child.stdout?.on("data", (chunk) => {
|
|
127
|
+
stdoutTail?.append(chunk);
|
|
128
|
+
});
|
|
132
129
|
child.stderr?.on("data", (chunk) => {
|
|
133
130
|
stderrTail.append(chunk);
|
|
134
131
|
});
|
|
@@ -139,35 +136,45 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
|
139
136
|
child.on("close", (code, signal) => {
|
|
140
137
|
exited = true;
|
|
141
138
|
const durationMs = Math.max(0, nowMs() - startedAt);
|
|
142
|
-
const
|
|
143
|
-
const
|
|
139
|
+
const stdoutSnapshot = stdoutTail?.snapshot();
|
|
140
|
+
const stderrSnapshot = stderrTail.snapshot();
|
|
141
|
+
const capturedOutput = {
|
|
142
|
+
stdout: stdoutSnapshot?.bytes ? { bytes: stdoutSnapshot.bytes, truncated: stdoutSnapshot.truncated } : null,
|
|
143
|
+
stderr: stderrSnapshot.bytes ? { bytes: stderrSnapshot.bytes, truncated: stderrSnapshot.truncated } : null,
|
|
144
|
+
};
|
|
145
|
+
const failureOutput = { stdout: capturedOutput.stdout, stderr: capturedOutput.stderr };
|
|
144
146
|
if (timedOut) {
|
|
145
|
-
finish(observationFrom({ kind: "timeout" }, durationMs,
|
|
147
|
+
finish(observationFrom({ kind: "timeout" }, durationMs, failureOutput));
|
|
146
148
|
return;
|
|
147
149
|
}
|
|
148
150
|
if (signal) {
|
|
149
|
-
finish(observationFrom({ kind: "signal", signal }, durationMs,
|
|
151
|
+
finish(observationFrom({ kind: "signal", signal }, durationMs, failureOutput));
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
154
|
const exitCode = typeof code === "number" ? code : 1;
|
|
153
155
|
const normalized = Math.max(0, Math.min(255, exitCode));
|
|
154
156
|
if (normalized === 0) {
|
|
155
|
-
|
|
156
|
-
finish(observationFrom({ kind: "exit", code: 0 }, durationMs,
|
|
157
|
+
const successOutput = input.captureOutput ? capturedOutput : { stdout: null, stderr: null };
|
|
158
|
+
finish(observationFrom({ kind: "exit", code: 0 }, durationMs, successOutput));
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
|
-
finish(observationFrom({ kind: "exit", code: normalized }, durationMs,
|
|
161
|
+
finish(observationFrom({ kind: "exit", code: normalized }, durationMs, failureOutput));
|
|
160
162
|
});
|
|
161
163
|
timeoutHandle = setTimeout(() => {
|
|
162
164
|
void (async () => {
|
|
163
165
|
if (settled || exited)
|
|
164
166
|
return;
|
|
165
167
|
timedOut = true;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
try {
|
|
169
|
+
await terminateProcessGroup(child, { graceMs: WORKTREE_BOOTSTRAP_KILL_GRACE_MS, wait });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
const stdoutSnapshot = stdoutTail?.snapshot();
|
|
173
|
+
const stderrSnapshot = stderrTail.snapshot();
|
|
174
|
+
finish(observationFrom({ kind: "timeout" }, Math.max(0, nowMs() - startedAt), {
|
|
175
|
+
stdout: stdoutSnapshot?.bytes ? { bytes: stdoutSnapshot.bytes, truncated: stdoutSnapshot.truncated } : null,
|
|
176
|
+
stderr: stderrSnapshot.bytes ? { bytes: stderrSnapshot.bytes, truncated: stderrSnapshot.truncated } : null,
|
|
177
|
+
}));
|
|
171
178
|
}
|
|
172
179
|
})();
|
|
173
180
|
}, input.plan.timeoutMs);
|
|
@@ -175,11 +182,12 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
|
175
182
|
});
|
|
176
183
|
}
|
|
177
184
|
export function bootstrapEntryDataFromObservation(observation) {
|
|
185
|
+
const retainFailureStderr = !isBootstrapSuccess(observation);
|
|
178
186
|
return {
|
|
179
187
|
result: observation.result,
|
|
180
188
|
durationMs: observation.durationMs,
|
|
181
|
-
...(observation.stderrTail !== undefined ? { stderrTail: observation.stderrTail } : {}),
|
|
182
|
-
...(observation.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
189
|
+
...(retainFailureStderr && observation.stderrTail !== undefined ? { stderrTail: observation.stderrTail } : {}),
|
|
190
|
+
...(retainFailureStderr && observation.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
183
191
|
};
|
|
184
192
|
}
|
|
185
193
|
export function isBootstrapSuccess(observation) {
|
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { FlowError } from "../flow-error.js";
|
|
4
|
-
import { createGit, wrapGitError } from "../git/core.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
|
|
4
|
+
import { createGit, runGitProcess, wrapGitError } from "../git/core.js";
|
|
5
|
+
import { resolveLocalBranchHead } from "../git/refs.js";
|
|
6
|
+
import { readCurrentContractLifecycle } from "./repository-ledger/read-model.js";
|
|
8
7
|
import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
|
|
9
8
|
import { gitCommonRootPath, resolveGitCommonRoot } from "../git/path-coordinate.js";
|
|
10
|
-
|
|
11
|
-
return
|
|
9
|
+
function record(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Reconstruct the public terminal-cleanup fact from its independent replay receipts. */
|
|
15
|
+
export function terminalCleanupFacts(worktreeValue, deliveryRefValue) {
|
|
16
|
+
const worktree = record(worktreeValue);
|
|
17
|
+
const delivery = record(deliveryRefValue);
|
|
18
|
+
if (!worktree || !Array.isArray(worktree.absent) || !delivery)
|
|
19
|
+
return undefined;
|
|
20
|
+
const absent = worktree.absent.filter((item) => item === "worktree");
|
|
21
|
+
const base = {
|
|
22
|
+
absent,
|
|
23
|
+
...(typeof worktree.removedWorktreePath === "string" ? { removedWorktreePath: worktree.removedWorktreePath } : {}),
|
|
24
|
+
};
|
|
25
|
+
const failure = record(worktree.worktreeRemoveFailure);
|
|
26
|
+
if (failure && typeof failure.path === "string" && typeof failure.reason === "string") {
|
|
27
|
+
base.worktreeRemoveFailure = { path: failure.path, reason: failure.reason };
|
|
28
|
+
}
|
|
29
|
+
if (delivery.status === "aligned"
|
|
30
|
+
&& typeof delivery.ref === "string"
|
|
31
|
+
&& delivery.expected === "absent"
|
|
32
|
+
&& delivery.observed === "absent") {
|
|
33
|
+
return { ...base, deliveryRef: { status: "aligned", ref: delivery.ref, expected: "absent", observed: "absent" } };
|
|
34
|
+
}
|
|
35
|
+
const mismatch = record(delivery.mismatch);
|
|
36
|
+
const observed = record(mismatch?.observed);
|
|
37
|
+
if (delivery.status === "mismatch"
|
|
38
|
+
&& mismatch?.kind === "projected-surface-mismatch"
|
|
39
|
+
&& mismatch.surface === "delivery-ref"
|
|
40
|
+
&& typeof mismatch.ref === "string"
|
|
41
|
+
&& mismatch.expected === "absent"
|
|
42
|
+
&& observed?.state === "present"
|
|
43
|
+
&& typeof observed.head === "string") {
|
|
44
|
+
return {
|
|
45
|
+
...base,
|
|
46
|
+
deliveryRef: {
|
|
47
|
+
status: "mismatch",
|
|
48
|
+
mismatch: {
|
|
49
|
+
kind: "projected-surface-mismatch",
|
|
50
|
+
surface: "delivery-ref",
|
|
51
|
+
ref: mismatch.ref,
|
|
52
|
+
expected: "absent",
|
|
53
|
+
observed: { state: "present", head: observed.head },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
12
59
|
}
|
|
13
60
|
function parseWorktreeListPorcelainZ(output) {
|
|
14
61
|
const listings = [];
|
|
@@ -28,7 +75,16 @@ function parseWorktreeListPorcelainZ(output) {
|
|
|
28
75
|
listings.push(current);
|
|
29
76
|
return listings;
|
|
30
77
|
}
|
|
31
|
-
async function listWorktrees(cwd) {
|
|
78
|
+
async function listWorktrees(cwd, signal) {
|
|
79
|
+
if (signal) {
|
|
80
|
+
const result = await runGitProcess(cwd, ["worktree", "list", "--porcelain", "-z"], { signal });
|
|
81
|
+
if (result.error || result.status !== 0) {
|
|
82
|
+
if (result.error?.name === "AbortError")
|
|
83
|
+
throw result.error;
|
|
84
|
+
throw wrapGitError("worktree list --porcelain -z", result.error ?? new Error(result.stderr || `exit ${String(result.status)}`), cwd);
|
|
85
|
+
}
|
|
86
|
+
return parseWorktreeListPorcelainZ(result.stdout);
|
|
87
|
+
}
|
|
32
88
|
const git = createGit(cwd);
|
|
33
89
|
try {
|
|
34
90
|
return parseWorktreeListPorcelainZ(await git.raw(["worktree", "list", "--porcelain", "-z"]));
|
|
@@ -80,16 +136,19 @@ function contractWorktreePathFromStableRoot(stableRoot, contractId, bind) {
|
|
|
80
136
|
return path.join(stableRoot, ".keiyaku", "wt", bind?.data.place ?? contractId);
|
|
81
137
|
}
|
|
82
138
|
export async function contractWorktreePath(cwd, contractId) {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
return
|
|
139
|
+
const stableRoot = await stableRepoRoot(cwd);
|
|
140
|
+
const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
|
|
141
|
+
return path.join(stableRoot, ".keiyaku", "wt", outcome.status === "healthy" || outcome.status === "diseased"
|
|
142
|
+
? outcome.model.resources.worktree?.place ?? contractId
|
|
143
|
+
: contractId);
|
|
86
144
|
}
|
|
87
145
|
export async function requireContractWorktreePath(cwd, contractId) {
|
|
88
|
-
const
|
|
89
|
-
|
|
146
|
+
const stableRoot = await stableRepoRoot(cwd);
|
|
147
|
+
const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
|
|
148
|
+
if (outcome.status !== "healthy" || !outcome.model.resources.worktree) {
|
|
90
149
|
throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found`);
|
|
91
150
|
}
|
|
92
|
-
return
|
|
151
|
+
return path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place);
|
|
93
152
|
}
|
|
94
153
|
async function findContractWorktree(cwd, contractId) {
|
|
95
154
|
const expectedBranch = `refs/heads/keiyaku/${contractId}`;
|
|
@@ -108,21 +167,53 @@ export async function findContractWorktreePath(cwd, contractId) {
|
|
|
108
167
|
}
|
|
109
168
|
export async function findRegisteredContractWorktreePath(cwd, contractId) {
|
|
110
169
|
const stableRoot = await stableRepoRoot(cwd);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (!bind || bind.data.workspace !== "worktree")
|
|
170
|
+
const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
|
|
171
|
+
if (outcome.status !== "healthy" || outcome.model.resources.worktree?.state !== "held")
|
|
114
172
|
return undefined;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
173
|
+
return findRegisteredContractWorktreePathFromModel(stableRoot, outcome.model);
|
|
174
|
+
}
|
|
175
|
+
export async function findRegisteredContractWorktreePathFromModel(stableRoot, model) {
|
|
176
|
+
const worktree = model.resources.worktree;
|
|
177
|
+
if (!worktree)
|
|
120
178
|
return undefined;
|
|
179
|
+
const expected = path.join(stableRoot, ".keiyaku", "wt", worktree.place);
|
|
180
|
+
for (const registered of await listWorktrees(stableRoot)) {
|
|
181
|
+
if (registered.branch !== model.resources.deliveryRef)
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
if (normalizeFilesystemCoordinate(await fs.realpath(registered.path)) === normalizeFilesystemCoordinate(await fs.realpath(expected))) {
|
|
185
|
+
return await fs.realpath(registered.path);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { /* stale registration is not lifecycle evidence */ }
|
|
121
189
|
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
async function findRegisteredContractWorktreePathFromBind(stableRoot, contractId, _bind) {
|
|
193
|
+
const expectedBranch = `refs/heads/keiyaku/${contractId}`;
|
|
122
194
|
for (const worktree of await listWorktrees(stableRoot)) {
|
|
195
|
+
let ownsBranch = worktree.branch === expectedBranch;
|
|
196
|
+
if (!ownsBranch && !worktree.branch) {
|
|
197
|
+
for (const statePath of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
|
|
198
|
+
try {
|
|
199
|
+
const gitPath = (await createGit(worktree.path).raw(["rev-parse", "--git-path", statePath])).trim();
|
|
200
|
+
if ((await fs.readFile(gitPath, "utf8")).trim() === expectedBranch) {
|
|
201
|
+
ownsBranch = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// No owned rebase coordinate at this registered worktree.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!ownsBranch)
|
|
211
|
+
continue;
|
|
123
212
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
213
|
+
const dotGit = await fs.stat(path.join(worktree.path, ".git"));
|
|
214
|
+
if (dotGit.isDirectory())
|
|
215
|
+
continue;
|
|
216
|
+
return await fs.realpath(worktree.path);
|
|
126
217
|
}
|
|
127
218
|
catch {
|
|
128
219
|
// A stale Git registration is not an engine-owned worktree fact.
|
|
@@ -130,12 +221,21 @@ export async function findRegisteredContractWorktreePath(cwd, contractId) {
|
|
|
130
221
|
}
|
|
131
222
|
return undefined;
|
|
132
223
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
224
|
+
/** Prove the sole carrier target from stable-root, bind, and Git registration facts. */
|
|
225
|
+
export async function resolveContractCarrierTarget(stableRoot, contractId, bind) {
|
|
226
|
+
if (bind.data.workspace === "here") {
|
|
227
|
+
const root = await fs.realpath(stableRoot);
|
|
228
|
+
return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "here" };
|
|
229
|
+
}
|
|
230
|
+
const root = await findRegisteredContractWorktreePathFromBind(stableRoot, contractId, bind);
|
|
231
|
+
if (!root) {
|
|
232
|
+
return {
|
|
233
|
+
status: "unavailable",
|
|
234
|
+
workspace: "worktree",
|
|
235
|
+
reason: `contract ${contractId} has no verified registered engine worktree`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "worktree" };
|
|
139
239
|
}
|
|
140
240
|
/**
|
|
141
241
|
* Resolve the contract id for an engine-owned bound worktree containing cwd.
|
|
@@ -144,10 +244,29 @@ export async function resolveContractDeliveryCwd(cwd, contractId) {
|
|
|
144
244
|
*/
|
|
145
245
|
async function contractIdForEngineOwnedWorktree(cwd, options) {
|
|
146
246
|
const stableRoot = options.stableRoot ?? await stableRepoRoot(cwd);
|
|
247
|
+
const candidate = await contractIdentityFromWorktreeWitness(cwd, stableRoot);
|
|
248
|
+
if (!candidate)
|
|
249
|
+
return undefined;
|
|
250
|
+
const outcome = await readCurrentContractLifecycle(stableRoot, candidate.contractId);
|
|
251
|
+
if (outcome.status !== "healthy" || !outcome.model.resources.worktree)
|
|
252
|
+
return undefined;
|
|
253
|
+
if (options.requireActive && ["claimed", "forfeited"].includes(outcome.model.phase))
|
|
254
|
+
return undefined;
|
|
255
|
+
let expectedWorktree;
|
|
256
|
+
try {
|
|
257
|
+
expectedWorktree = await fs.realpath(path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return candidate.path === expectedWorktree ? candidate.contractId : undefined;
|
|
263
|
+
}
|
|
264
|
+
/** Resolve only Git's local registered-worktree witness; lifecycle facts must verify it separately. */
|
|
265
|
+
export async function contractIdentityFromWorktreeWitness(cwd, stableRoot, signal) {
|
|
147
266
|
const canonicalCwd = await fs.realpath(cwd);
|
|
267
|
+
const branchPrefix = "refs/heads/keiyaku/";
|
|
148
268
|
const candidates = [];
|
|
149
|
-
for (const worktree of await listWorktrees(stableRoot)) {
|
|
150
|
-
const branchPrefix = "refs/heads/keiyaku/";
|
|
269
|
+
for (const worktree of await listWorktrees(stableRoot, signal)) {
|
|
151
270
|
if (!worktree.branch?.startsWith(branchPrefix))
|
|
152
271
|
continue;
|
|
153
272
|
let canonicalWorktree;
|
|
@@ -162,27 +281,10 @@ async function contractIdForEngineOwnedWorktree(cwd, options) {
|
|
|
162
281
|
candidates.push({
|
|
163
282
|
contractId: worktree.branch.slice(branchPrefix.length),
|
|
164
283
|
path: canonicalWorktree,
|
|
284
|
+
branch: worktree.branch,
|
|
165
285
|
});
|
|
166
286
|
}
|
|
167
|
-
|
|
168
|
-
if (!candidate)
|
|
169
|
-
return undefined;
|
|
170
|
-
const ledger = await readLedgerSnapshot(stableRoot, candidate.contractId);
|
|
171
|
-
if (!ledger)
|
|
172
|
-
return undefined;
|
|
173
|
-
if (options.requireActive && isTerminalState(deriveContractState(ledger.entries)))
|
|
174
|
-
return undefined;
|
|
175
|
-
const bind = findBindEntry(ledger.entries);
|
|
176
|
-
if (!bind || bind.data.workspace !== "worktree")
|
|
177
|
-
return undefined;
|
|
178
|
-
let expectedWorktree;
|
|
179
|
-
try {
|
|
180
|
-
expectedWorktree = await fs.realpath(contractWorktreePathFromStableRoot(stableRoot, candidate.contractId, bind));
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
return candidate.path === expectedWorktree ? candidate.contractId : undefined;
|
|
287
|
+
return candidates.sort((left, right) => right.path.length - left.path.length || left.contractId.localeCompare(right.contractId))[0];
|
|
186
288
|
}
|
|
187
289
|
/**
|
|
188
290
|
* Resolve the contract bound by an engine-owned worktree containing cwd,
|
|
@@ -196,12 +298,48 @@ export async function contractBoundByEngineWorktree(cwd, stableRoot) {
|
|
|
196
298
|
export async function activeContractBoundByEngineWorktree(cwd, stableRoot) {
|
|
197
299
|
return contractIdForEngineOwnedWorktree(cwd, { requireActive: true, stableRoot });
|
|
198
300
|
}
|
|
199
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Verify a worktree binding against a folded current-ledger model. Git's
|
|
303
|
+
* registration is only a physical witness here; it cannot create lifecycle
|
|
304
|
+
* state or select a contract absent from the supplied read model.
|
|
305
|
+
*/
|
|
306
|
+
export async function currentContractBoundByWorktreeWitness(input) {
|
|
307
|
+
let canonicalCwd;
|
|
308
|
+
try {
|
|
309
|
+
canonicalCwd = await fs.realpath(input.cwd);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const registeredWorktrees = await listWorktrees(input.stableRoot);
|
|
315
|
+
const candidates = [];
|
|
316
|
+
for (const model of input.contracts) {
|
|
317
|
+
const worktree = model.resources.worktree;
|
|
318
|
+
if (!worktree || worktree.state !== "held")
|
|
319
|
+
continue;
|
|
320
|
+
const expectedPath = path.join(input.stableRoot, ".keiyaku", "wt", worktree.place);
|
|
321
|
+
let canonicalExpectedPath;
|
|
322
|
+
try {
|
|
323
|
+
canonicalExpectedPath = await fs.realpath(expectedPath);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!worktreeContainsCoordinate(canonicalExpectedPath, canonicalCwd))
|
|
329
|
+
continue;
|
|
330
|
+
const registered = registeredWorktrees.some((candidate) => candidate.branch === model.resources.deliveryRef &&
|
|
331
|
+
normalizeFilesystemCoordinate(candidate.path) === normalizeFilesystemCoordinate(canonicalExpectedPath));
|
|
332
|
+
if (registered)
|
|
333
|
+
candidates.push({ contract: model.contract, path: canonicalExpectedPath });
|
|
334
|
+
}
|
|
335
|
+
candidates.sort((left, right) => right.path.length - left.path.length || left.contract.localeCompare(right.contract));
|
|
336
|
+
return candidates[0]?.contract;
|
|
337
|
+
}
|
|
338
|
+
/** Remove only the derived linked worktree. Authoritative refs are outside this adapter. */
|
|
339
|
+
export async function cleanupContractWorkspace(cwd, lifecycle) {
|
|
200
340
|
const stableRoot = await stableRepoRoot(cwd);
|
|
201
341
|
const git = createGit(stableRoot);
|
|
202
|
-
const
|
|
203
|
-
const worktree = await findContractWorktree(stableRoot, contractId);
|
|
204
|
-
const worktreePath = worktree.path;
|
|
342
|
+
const worktreePath = await findRegisteredContractWorktreePathFromModel(stableRoot, lifecycle);
|
|
205
343
|
const facts = { absent: [] };
|
|
206
344
|
if (worktreePath) {
|
|
207
345
|
try {
|
|
@@ -215,28 +353,23 @@ export async function cleanupContractWorkspace(cwd, contractId) {
|
|
|
215
353
|
else {
|
|
216
354
|
facts.absent.push("worktree");
|
|
217
355
|
}
|
|
218
|
-
if (worktree.primary) {
|
|
219
|
-
facts.absent.push("branch");
|
|
220
|
-
return facts;
|
|
221
|
-
}
|
|
222
|
-
let branchRef;
|
|
223
|
-
try {
|
|
224
|
-
branchRef = (await git.raw(["for-each-ref", "--format=%(refname)", `refs/heads/${branch}`])).trim();
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
throw wrapGitError(`for-each-ref refs/heads/${branch}`, error, stableRoot);
|
|
228
|
-
}
|
|
229
|
-
if (branchRef !== `refs/heads/${branch}`) {
|
|
230
|
-
facts.absent.push("branch");
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
try {
|
|
234
|
-
await git.raw(["branch", "-D", branch]);
|
|
235
|
-
facts.deletedBranch = branch;
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
facts.branchDeleteFailure = { name: branch, reason: wrapGitError(`branch -D ${branch}`, error, stableRoot).message };
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
356
|
return facts;
|
|
242
357
|
}
|
|
358
|
+
/** Observe the terminal transaction's expected delivery-ref absence without reconciling it. */
|
|
359
|
+
export async function observeTerminalDeliveryRef(cwd, contractId) {
|
|
360
|
+
const stableRoot = await stableRepoRoot(cwd);
|
|
361
|
+
const ref = `refs/heads/keiyaku/${contractId}`;
|
|
362
|
+
const observed = await resolveLocalBranchHead(stableRoot, ref);
|
|
363
|
+
if (!observed)
|
|
364
|
+
return { status: "aligned", ref, expected: "absent", observed: "absent" };
|
|
365
|
+
return {
|
|
366
|
+
status: "mismatch",
|
|
367
|
+
mismatch: {
|
|
368
|
+
kind: "projected-surface-mismatch",
|
|
369
|
+
surface: "delivery-ref",
|
|
370
|
+
ref,
|
|
371
|
+
expected: "absent",
|
|
372
|
+
observed: { state: "present", head: observed.head },
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|