@astrosheep/keiyaku 1.0.1 → 2.8.0
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 +5 -7
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +29 -24
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- package/build/core/summon.js +0 -377
package/build/core/verdict.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FlowError } from "../flow-error.js";
|
|
2
2
|
import { claimContract } from "./claim.js";
|
|
3
3
|
import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assertClaimQueueHead, queueMembership, readQueueReadModel } from "./queue_v2.js";
|
|
5
5
|
import { deriveContractState } from "./status.js";
|
|
6
6
|
function latestVerdict(entries) {
|
|
7
7
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
@@ -13,12 +13,6 @@ function latestVerdict(entries) {
|
|
|
13
13
|
}
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
|
-
async function assertQueueHead(cwd, contractId) {
|
|
17
|
-
const position = queuePositionFromEntries(await readQueue(cwd), contractId);
|
|
18
|
-
if (position !== 1) {
|
|
19
|
-
throw new FlowError("INTERNAL_STATE", `contract ${contractId} is not queue head; position=${position ?? "not queued"}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
16
|
function assertPetitioned(contractId, entries) {
|
|
23
17
|
const state = deriveContractState(entries);
|
|
24
18
|
if (state.state !== "petitioned") {
|
|
@@ -32,13 +26,22 @@ export async function verdictContract(input) {
|
|
|
32
26
|
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
33
27
|
}
|
|
34
28
|
assertPetitioned(input.contractId, current.entries);
|
|
35
|
-
|
|
29
|
+
// Rejected verdict is allowed for any petitioned ledger, including legacy and
|
|
30
|
+
// target mismatch, without a queue-head gate.
|
|
31
|
+
if (input.result === "approved") {
|
|
32
|
+
const model = await readQueueReadModel(input.cwd);
|
|
33
|
+
assertClaimQueueHead(model, input.contractId);
|
|
34
|
+
}
|
|
36
35
|
const ref = activeLedgerRef(input.contractId);
|
|
37
36
|
const appendResult = await appendWithRetry(input.cwd, ref, async (_head, entries) => {
|
|
38
37
|
const existingVerdict = latestVerdict(entries);
|
|
39
38
|
if (existingVerdict)
|
|
40
39
|
return null;
|
|
41
40
|
assertPetitioned(input.contractId, entries);
|
|
41
|
+
if (input.result === "approved") {
|
|
42
|
+
const model = await readQueueReadModel(input.cwd);
|
|
43
|
+
assertClaimQueueHead(model, input.contractId);
|
|
44
|
+
}
|
|
42
45
|
const entry = {
|
|
43
46
|
v: 1,
|
|
44
47
|
kind: "verdict",
|
|
@@ -67,7 +70,7 @@ export async function verdictContract(input) {
|
|
|
67
70
|
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} already has ${verdict.data.result} verdict`);
|
|
68
71
|
}
|
|
69
72
|
if (verdict.data.result === "rejected") {
|
|
70
|
-
|
|
73
|
+
// Rejected verdict naturally returns state to active and clears membership.
|
|
71
74
|
return {
|
|
72
75
|
contractId: input.contractId,
|
|
73
76
|
result: "rejected",
|
|
@@ -76,21 +79,30 @@ export async function verdictContract(input) {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
const claimOnApproval = input.claimOnApproval ?? true;
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (claimOnApproval) {
|
|
83
|
+
const claimed = await claimContract({
|
|
81
84
|
cwd: input.cwd,
|
|
82
85
|
contractId: input.contractId,
|
|
83
86
|
actor: "keiyaku",
|
|
84
87
|
nowMs: input.nowMs,
|
|
85
88
|
mergeDelivery: input.mergeDelivery,
|
|
86
89
|
afterMerge: input.afterMerge,
|
|
87
|
-
})
|
|
88
|
-
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
contractId: input.contractId,
|
|
93
|
+
result: "approved",
|
|
94
|
+
summary: verdict.data.summary,
|
|
95
|
+
claimed,
|
|
96
|
+
queuePosition: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// claimOnApproval false remains petitioned at target-local position 1.
|
|
100
|
+
const model = await readQueueReadModel(input.cwd);
|
|
101
|
+
const membership = queueMembership(model, input.contractId);
|
|
89
102
|
return {
|
|
90
103
|
contractId: input.contractId,
|
|
91
104
|
result: "approved",
|
|
92
105
|
summary: verdict.data.summary,
|
|
93
|
-
|
|
94
|
-
queuePosition: claimed ? null : 1,
|
|
106
|
+
queuePosition: membership?.position ?? 1,
|
|
95
107
|
};
|
|
96
108
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
export const WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES = 2048;
|
|
3
|
+
export const WORKTREE_BOOTSTRAP_KILL_GRACE_MS = 10_000;
|
|
4
|
+
function defaultWait(ms) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const timer = setTimeout(resolve, ms);
|
|
7
|
+
timer.unref?.();
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
/** Retain the last 2048 UTF-8 bytes and decode lossily (partial leading code point → U+FFFD). */
|
|
11
|
+
export function tailUtf8Bytes(bytes, maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES) {
|
|
12
|
+
const truncated = bytes.byteLength > maxBytes;
|
|
13
|
+
const slice = truncated ? bytes.subarray(bytes.byteLength - maxBytes) : bytes;
|
|
14
|
+
return { text: slice.toString("utf8"), truncated };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rolling stderr accumulator: holds at most `maxBytes` across every chunk and
|
|
18
|
+
* remembers whether any earlier prefix was dropped. Never retains unbounded stderr.
|
|
19
|
+
*/
|
|
20
|
+
function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES) {
|
|
21
|
+
let tail = Buffer.alloc(0);
|
|
22
|
+
let truncated = false;
|
|
23
|
+
return {
|
|
24
|
+
append(chunk) {
|
|
25
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
26
|
+
if (bytes.byteLength === 0)
|
|
27
|
+
return;
|
|
28
|
+
if (bytes.byteLength >= maxBytes) {
|
|
29
|
+
// Keep last maxBytes. Mark truncated only when this chunk itself exceeds
|
|
30
|
+
// maxBytes or when any already-retained prefix is displaced. Prior true sticks.
|
|
31
|
+
if (bytes.byteLength > maxBytes || tail.byteLength > 0) {
|
|
32
|
+
truncated = true;
|
|
33
|
+
}
|
|
34
|
+
tail = Buffer.from(bytes.subarray(bytes.byteLength - maxBytes));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const combinedLength = tail.byteLength + bytes.byteLength;
|
|
38
|
+
if (combinedLength <= maxBytes) {
|
|
39
|
+
const next = Buffer.allocUnsafe(combinedLength);
|
|
40
|
+
tail.copy(next, 0);
|
|
41
|
+
bytes.copy(next, tail.byteLength);
|
|
42
|
+
tail = next;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
truncated = true;
|
|
46
|
+
const keepFromTail = maxBytes - bytes.byteLength;
|
|
47
|
+
const next = Buffer.allocUnsafe(maxBytes);
|
|
48
|
+
tail.copy(next, 0, tail.byteLength - keepFromTail);
|
|
49
|
+
bytes.copy(next, keepFromTail);
|
|
50
|
+
tail = next;
|
|
51
|
+
},
|
|
52
|
+
snapshot() {
|
|
53
|
+
if (tail.byteLength === 0)
|
|
54
|
+
return { bytes: null, truncated: false };
|
|
55
|
+
return { bytes: tail, truncated };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function observationFrom(result, durationMs, stderr) {
|
|
60
|
+
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
|
+
return {
|
|
66
|
+
result,
|
|
67
|
+
durationMs: safeDuration,
|
|
68
|
+
stderrTail: stderr.bytes.toString("utf8"),
|
|
69
|
+
...(stderr.truncated ? { stderrTruncated: true } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function spawnFailureObservation(message, durationMs) {
|
|
73
|
+
const tail = tailUtf8Bytes(Buffer.from(message, "utf8"));
|
|
74
|
+
const safeDuration = Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(durationMs)));
|
|
75
|
+
return {
|
|
76
|
+
result: { kind: "spawn", message: tail.text },
|
|
77
|
+
durationMs: safeDuration,
|
|
78
|
+
...(tail.truncated ? { stderrTruncated: true } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
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
|
+
/**
|
|
90
|
+
* Run the project-declared worktree bootstrap once.
|
|
91
|
+
* argv only (shell: false), cwd = new worktree, inherited env, stdin/stdout ignored, stderr piped.
|
|
92
|
+
*/
|
|
93
|
+
export async function runWorktreeBootstrap(input, dependencies = {}) {
|
|
94
|
+
const spawn = dependencies.spawn ?? nodeSpawn;
|
|
95
|
+
const nowMs = dependencies.nowMs ?? Date.now;
|
|
96
|
+
const wait = dependencies.wait ?? defaultWait;
|
|
97
|
+
const [program, ...args] = input.plan.command;
|
|
98
|
+
if (!program) {
|
|
99
|
+
return {
|
|
100
|
+
result: { kind: "spawn", message: "bootstrap command is empty" },
|
|
101
|
+
durationMs: 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const startedAt = nowMs();
|
|
105
|
+
const stderrTail = createRollingByteTail();
|
|
106
|
+
let child;
|
|
107
|
+
try {
|
|
108
|
+
child = spawn(program, args, {
|
|
109
|
+
cwd: input.cwd,
|
|
110
|
+
env: process.env,
|
|
111
|
+
shell: false,
|
|
112
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return spawnFailureObservation(message, Math.max(0, nowMs() - startedAt));
|
|
118
|
+
}
|
|
119
|
+
return await new Promise((resolve) => {
|
|
120
|
+
let settled = false;
|
|
121
|
+
let timedOut = false;
|
|
122
|
+
let exited = false;
|
|
123
|
+
let timeoutHandle;
|
|
124
|
+
const finish = (observation) => {
|
|
125
|
+
if (settled)
|
|
126
|
+
return;
|
|
127
|
+
settled = true;
|
|
128
|
+
if (timeoutHandle)
|
|
129
|
+
clearTimeout(timeoutHandle);
|
|
130
|
+
resolve(observation);
|
|
131
|
+
};
|
|
132
|
+
child.stderr?.on("data", (chunk) => {
|
|
133
|
+
stderrTail.append(chunk);
|
|
134
|
+
});
|
|
135
|
+
child.on("error", (error) => {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
finish(spawnFailureObservation(message, Math.max(0, nowMs() - startedAt)));
|
|
138
|
+
});
|
|
139
|
+
child.on("close", (code, signal) => {
|
|
140
|
+
exited = true;
|
|
141
|
+
const durationMs = Math.max(0, nowMs() - startedAt);
|
|
142
|
+
const snap = stderrTail.snapshot();
|
|
143
|
+
const stderr = snap.bytes ? { bytes: snap.bytes, truncated: snap.truncated } : null;
|
|
144
|
+
if (timedOut) {
|
|
145
|
+
finish(observationFrom({ kind: "timeout" }, durationMs, stderr));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (signal) {
|
|
149
|
+
finish(observationFrom({ kind: "signal", signal }, durationMs, stderr));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const exitCode = typeof code === "number" ? code : 1;
|
|
153
|
+
const normalized = Math.max(0, Math.min(255, exitCode));
|
|
154
|
+
if (normalized === 0) {
|
|
155
|
+
// Success discards stderr but still clamps durationMs.
|
|
156
|
+
finish(observationFrom({ kind: "exit", code: 0 }, durationMs, null));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
finish(observationFrom({ kind: "exit", code: normalized }, durationMs, stderr));
|
|
160
|
+
});
|
|
161
|
+
timeoutHandle = setTimeout(() => {
|
|
162
|
+
void (async () => {
|
|
163
|
+
if (settled || exited)
|
|
164
|
+
return;
|
|
165
|
+
timedOut = true;
|
|
166
|
+
killChild(child, "SIGTERM");
|
|
167
|
+
await wait(WORKTREE_BOOTSTRAP_KILL_GRACE_MS);
|
|
168
|
+
// SIGKILL only if the child has not reached a terminal close yet.
|
|
169
|
+
if (!exited && !settled) {
|
|
170
|
+
killChild(child, "SIGKILL");
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
}, input.plan.timeoutMs);
|
|
174
|
+
timeoutHandle.unref?.();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function bootstrapEntryDataFromObservation(observation) {
|
|
178
|
+
return {
|
|
179
|
+
result: observation.result,
|
|
180
|
+
durationMs: observation.durationMs,
|
|
181
|
+
...(observation.stderrTail !== undefined ? { stderrTail: observation.stderrTail } : {}),
|
|
182
|
+
...(observation.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function isBootstrapSuccess(observation) {
|
|
186
|
+
return observation.result.kind === "exit" && observation.result.code === 0;
|
|
187
|
+
}
|
|
@@ -3,10 +3,42 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import { FlowError } from "../flow-error.js";
|
|
4
4
|
import { createGit, wrapGitError } from "../git/core.js";
|
|
5
5
|
import { readLedger } from "./ledger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { listContractIds } from "./ledger.js";
|
|
7
|
+
import { deriveContractState, isTerminalState } from "./status.js";
|
|
7
8
|
export function findBindEntry(entries) {
|
|
8
9
|
return entries.find((entry) => entry.kind === "bind");
|
|
9
10
|
}
|
|
11
|
+
function parseWorktreeListPorcelainZ(output) {
|
|
12
|
+
const listings = [];
|
|
13
|
+
let current;
|
|
14
|
+
for (const field of output.split("\0")) {
|
|
15
|
+
if (field.startsWith("worktree ")) {
|
|
16
|
+
if (current)
|
|
17
|
+
listings.push(current);
|
|
18
|
+
current = { path: field.slice("worktree ".length) };
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (field.startsWith("branch ") && current) {
|
|
22
|
+
current.branch = field.slice("branch ".length);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (current)
|
|
26
|
+
listings.push(current);
|
|
27
|
+
return listings;
|
|
28
|
+
}
|
|
29
|
+
async function listWorktrees(cwd) {
|
|
30
|
+
const git = createGit(cwd);
|
|
31
|
+
try {
|
|
32
|
+
return parseWorktreeListPorcelainZ(await git.raw(["worktree", "list", "--porcelain", "-z"]));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw wrapGitError("worktree list --porcelain -z", error, cwd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function pathContains(root, candidate) {
|
|
39
|
+
const relative = path.relative(root, candidate);
|
|
40
|
+
return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
41
|
+
}
|
|
10
42
|
export async function stableRepoRoot(cwd) {
|
|
11
43
|
const git = createGit(cwd);
|
|
12
44
|
let bare;
|
|
@@ -47,25 +79,12 @@ export async function requireContractWorktreePath(cwd, contractId) {
|
|
|
47
79
|
return await contractWorktreePathFromBind(cwd, contractId, findBindEntry(ledger.entries));
|
|
48
80
|
}
|
|
49
81
|
async function findContractWorktree(cwd, contractId) {
|
|
50
|
-
const git = createGit(await stableRepoRoot(cwd));
|
|
51
|
-
let output;
|
|
52
|
-
try {
|
|
53
|
-
output = await git.raw(["worktree", "list", "--porcelain", "-z"]);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
throw wrapGitError("worktree list --porcelain -z", error, cwd);
|
|
57
|
-
}
|
|
58
82
|
const expectedBranch = `refs/heads/keiyaku/${contractId}`;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
currentPath = field.slice("worktree ".length);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (field === `branch ${expectedBranch}` && currentPath) {
|
|
66
|
-
const dotGit = await fs.stat(path.join(currentPath, ".git"));
|
|
83
|
+
for (const worktree of await listWorktrees(await stableRepoRoot(cwd))) {
|
|
84
|
+
if (worktree.branch === expectedBranch) {
|
|
85
|
+
const dotGit = await fs.stat(path.join(worktree.path, ".git"));
|
|
67
86
|
if (!dotGit.isDirectory())
|
|
68
|
-
return { path:
|
|
87
|
+
return { path: worktree.path, primary: false };
|
|
69
88
|
return { primary: true };
|
|
70
89
|
}
|
|
71
90
|
}
|
|
@@ -74,6 +93,41 @@ async function findContractWorktree(cwd, contractId) {
|
|
|
74
93
|
export async function findContractWorktreePath(cwd, contractId) {
|
|
75
94
|
return (await findContractWorktree(cwd, contractId)).path;
|
|
76
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the active commission presented by an engine-owned contract
|
|
98
|
+
* worktree containing cwd. Ordinary repository directories and arbitrary
|
|
99
|
+
* worktrees never present a commission address.
|
|
100
|
+
*/
|
|
101
|
+
export async function contractPresentedByWorktree(cwd) {
|
|
102
|
+
const stableRoot = await stableRepoRoot(cwd);
|
|
103
|
+
const canonicalCwd = await fs.realpath(cwd);
|
|
104
|
+
for (const worktree of await listWorktrees(stableRoot)) {
|
|
105
|
+
const branchPrefix = "refs/heads/keiyaku/";
|
|
106
|
+
if (!worktree.branch?.startsWith(branchPrefix))
|
|
107
|
+
continue;
|
|
108
|
+
const contractId = worktree.branch.slice(branchPrefix.length);
|
|
109
|
+
const ledger = await readLedger(stableRoot, contractId);
|
|
110
|
+
if (!ledger || isTerminalState(deriveContractState(ledger.entries)))
|
|
111
|
+
continue;
|
|
112
|
+
const bind = findBindEntry(ledger.entries);
|
|
113
|
+
if (!bind || bind.data.workspace !== "worktree")
|
|
114
|
+
continue;
|
|
115
|
+
let canonicalWorktree;
|
|
116
|
+
let expectedWorktree;
|
|
117
|
+
try {
|
|
118
|
+
canonicalWorktree = await fs.realpath(worktree.path);
|
|
119
|
+
expectedWorktree = await fs.realpath(await contractWorktreePathFromBind(stableRoot, contractId, bind));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (canonicalWorktree !== expectedWorktree)
|
|
125
|
+
continue;
|
|
126
|
+
if (pathContains(canonicalWorktree, canonicalCwd))
|
|
127
|
+
return contractId;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
77
131
|
export async function cleanupContractWorkspace(cwd, contractId) {
|
|
78
132
|
const stableRoot = await stableRepoRoot(cwd);
|
|
79
133
|
const git = createGit(stableRoot);
|
package/build/flow-error.js
CHANGED
|
@@ -23,7 +23,7 @@ function resolveFlowErrorHint(code, context) {
|
|
|
23
23
|
case "ACTIVE_KEIYAKU_EXISTS":
|
|
24
24
|
return `Next: open an arc with \`${context.arcToolName}\`, then use \`${context.renewToolName}\` when delivery must follow the current base, or \`${context.closeToolName}\` to settle it.`;
|
|
25
25
|
case "CONTRACT_AMBIGUOUS":
|
|
26
|
-
return "Address a specific contract workspace, or use `
|
|
26
|
+
return "Address a specific contract workspace, or use `call --bare` to run without contract attachment.";
|
|
27
27
|
case "UNKNOWN_COMMAND":
|
|
28
28
|
return "Run `keiyaku --help` to inspect available commands.";
|
|
29
29
|
case "EMPTY_PARAM":
|
|
@@ -53,21 +53,19 @@ function resolveFlowErrorHint(code, context) {
|
|
|
53
53
|
case "FROM_DRAFT_NOT_FOUND":
|
|
54
54
|
return "The path in `from_draft` does not exist. `from_draft` must reference an existing file path (absolute or relative to `cwd`).";
|
|
55
55
|
case "INVALID_KEIYAKU_DRAFT":
|
|
56
|
-
return "Invalid draft/input.
|
|
56
|
+
return "Invalid draft/input. Use a `# <title>` heading with non-empty `## Objective` and `## Scope` sections (or the equivalent shape in `from_draft`).";
|
|
57
57
|
case "KEIYAKU_FILE_EXISTS":
|
|
58
58
|
return `\`${context.bindToolName}\` requires a clean slate. \`KEIYAKU.md\` must be removed before starting.`;
|
|
59
59
|
case "DETACHED_HEAD":
|
|
60
60
|
return "Repository is in detached HEAD state. Address an active contract workspace before retrying.";
|
|
61
61
|
case "INVALID_RESPONSE_PATH":
|
|
62
|
-
return "`keiyaku
|
|
62
|
+
return "`keiyaku revive ARTIFACT_ID` needs a valid previous Keiyaku response artifact that includes complete provenance plus `session` and `subagent_name`.";
|
|
63
63
|
case "INVALID_CLOSE_INTENT":
|
|
64
64
|
return "Invalid close intent. Valid values are `CLAIM` and `FORFEIT`.";
|
|
65
65
|
case "INVALID_SETTINGS":
|
|
66
66
|
return `Fix invalid configuration in \`${SETTINGS_FILE}\` and retry.`;
|
|
67
67
|
case "INVALID_EFFORT_OVERRIDE":
|
|
68
|
-
return "Choose
|
|
69
|
-
case "UNSUPPORTED_EFFORT_OVERRIDE":
|
|
70
|
-
return "Choose a provider with a native effort control, or omit the generation override.";
|
|
68
|
+
return "Choose an effort value declared by the selected Akuma's effortOptions.";
|
|
71
69
|
case "CONCURRENT_MODIFICATION":
|
|
72
70
|
return `Concurrent repository changes were detected during \`${context.arcToolName}\` or \`${context.renewToolName}\`. Retry on the latest branch state.`;
|
|
73
71
|
case "INTERNAL_STATE":
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.mjs
|
|
2
|
-
export const VERSION = "
|
|
2
|
+
export const VERSION = "2.8.0";
|
package/build/git/branches.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { simpleGit } from "simple-git";
|
|
1
2
|
import { FlowError } from "../flow-error.js";
|
|
2
3
|
import { createGit, wrapGitError } from "./core.js";
|
|
3
4
|
export async function isGitRepo(cwd) {
|
|
4
|
-
const git =
|
|
5
|
+
const git = simpleGit(cwd);
|
|
6
|
+
git.env("GIT_TERMINAL_PROMPT", "0");
|
|
7
|
+
git.env("GCM_INTERACTIVE", "Never");
|
|
8
|
+
git.env("GIT_MERGE_AUTOEDIT", "no");
|
|
9
|
+
git.env("LC_ALL", "C");
|
|
5
10
|
try {
|
|
6
11
|
return await git.checkIsRepo();
|
|
7
12
|
}
|
package/build/index.js
CHANGED
|
@@ -4,16 +4,19 @@ import * as fs from "node:fs/promises";
|
|
|
4
4
|
import { getConfig, initializeConfig } from "./config/env.js";
|
|
5
5
|
import { ENV_KEYS } from "./config/env-keys.js";
|
|
6
6
|
import { logErrorWithCause } from "./telemetry/logger.js";
|
|
7
|
-
import {
|
|
7
|
+
import { runProjectionGenerationRuntime } from "./core/projection-generation-runtime.js";
|
|
8
8
|
import { renderCliHelp, runCliCommand } from "./cli/index.js";
|
|
9
9
|
import { assertSubagentCliCommandAllowed } from "./cli/subagent-guard.js";
|
|
10
10
|
const packageVersion = VERSION;
|
|
11
11
|
function findCliCwd(args) {
|
|
12
12
|
for (let index = 0; index < args.length; index += 1) {
|
|
13
13
|
const arg = args[index];
|
|
14
|
-
|
|
14
|
+
// Tokens after the literal -- boundary are payload, never directory selectors.
|
|
15
|
+
if (arg === "--")
|
|
16
|
+
return undefined;
|
|
17
|
+
if (arg === "--cwd" || arg === "-C") {
|
|
15
18
|
const value = args[index + 1];
|
|
16
|
-
return value && !value.startsWith("--") ? value : undefined;
|
|
19
|
+
return value && value !== "--" && !value.startsWith("--") ? value : undefined;
|
|
17
20
|
}
|
|
18
21
|
if (arg?.startsWith("--cwd=")) {
|
|
19
22
|
const value = arg.slice("--cwd=".length).trim();
|
|
@@ -25,11 +28,11 @@ function findCliCwd(args) {
|
|
|
25
28
|
async function main() {
|
|
26
29
|
const cliArgs = process.argv.slice(2);
|
|
27
30
|
if (cliArgs[0] === "__keiyaku-wake") {
|
|
28
|
-
if (cliArgs.length !==
|
|
31
|
+
if (cliArgs.length !== 3 || !cliArgs[1] || !cliArgs[2])
|
|
29
32
|
throw new Error("invalid internal wake invocation");
|
|
30
33
|
// Internal only: canonicalize once at the boundary; no public command can reach here.
|
|
31
34
|
const projectionDirectory = await fs.realpath(cliArgs[1]);
|
|
32
|
-
await
|
|
35
|
+
await runProjectionGenerationRuntime(projectionDirectory, cliArgs[2]);
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
initializeConfig(process.env, findCliCwd(cliArgs) ?? process.cwd());
|
package/build/keiyaku.js
CHANGED
|
@@ -7,7 +7,7 @@ export const TOOL_NAMES = {
|
|
|
7
7
|
bind: "bind",
|
|
8
8
|
arc: "arc",
|
|
9
9
|
renew: "renew",
|
|
10
|
-
|
|
10
|
+
call: "call",
|
|
11
11
|
amend: "amend",
|
|
12
12
|
close: "petition",
|
|
13
13
|
status: "status",
|
|
@@ -54,8 +54,6 @@ export const BUILTIN_AGENT_PROFILES = {
|
|
|
54
54
|
"akuma-claude": { provider: "claude-agent-sdk", description: "Default Claude Agent SDK Akuma." },
|
|
55
55
|
};
|
|
56
56
|
export const CLAUDE_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
|
|
57
|
-
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "max"];
|
|
58
|
-
export const EFFORT_LEVELS = CLAUDE_EFFORT_LEVELS;
|
|
59
57
|
export const CLAUDE_SETTING_SOURCES = ["project", "user", "local"];
|
|
60
58
|
export const CLAUDE_DEFAULT_SETTING_SOURCES = ["project"];
|
|
61
59
|
export const CLAUDE_DEFAULT_PERMISSION_MODE = "bypassPermissions";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/keiyaku",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "CLI for running iterative keiyaku workflows with Codex subagents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"test:typecheck": "npx tsc --project tsconfig.tests.json",
|
|
38
38
|
"prepublishOnly": "npm run build",
|
|
39
39
|
"start": "node build/index.js",
|
|
40
|
-
"test:unit": "node --import ./tests/support/real-provider-path-sentinel.mjs --import tsx --test --test-concurrency=
|
|
41
|
-
"test:integration": "
|
|
40
|
+
"test:unit": "node --import ./tests/support/real-provider-path-sentinel.mjs --import tsx --test --test-concurrency=8 tests/unit/*.test.ts",
|
|
41
|
+
"test:integration": "node --import ./tests/support/real-provider-path-sentinel.mjs --import tsx --test tests/integration/integration.test.ts",
|
|
42
42
|
"pretest:cli-expect": "npm run build",
|
|
43
|
-
"test:cli-expect": "node --import ./tests/support/real-provider-path-sentinel.mjs --test --test-concurrency=
|
|
43
|
+
"test:cli-expect": "node --import ./tests/support/real-provider-path-sentinel.mjs --test --test-concurrency=8 tests/cli-expect/suite/*.test.mjs",
|
|
44
44
|
"test:real-providers": "KEIYAKU_REAL_PROVIDERS=1 node --import tsx --test --test-concurrency=1 tests/real-provider/matrix.test.ts",
|
|
45
45
|
"test": "npm run test:unit && npm run test:typecheck && npm run test:integration && npm run test:cli-expect",
|
|
46
46
|
"release": "npm version patch && npm publish"
|
|
@@ -48,8 +48,9 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@anthropic-ai/claude-agent-sdk": "^0.3.204",
|
|
50
50
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
51
|
-
"@earendil-works/pi-coding-agent": "
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.80.2",
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
+
"@opencode-ai/sdk": "1.17.20",
|
|
53
54
|
"@openai/codex-sdk": "^0.113.0",
|
|
54
55
|
"simple-git": "^3.21.0",
|
|
55
56
|
"zod": "4.3.6"
|
package/skills/keiyaku/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: keiyaku
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use when you need Keiyaku tools to coordinate Akuma work, organize parallel operations, review diffs, and create or manage worktrees.
|
|
4
4
|
allowed-tools: Bash(keiyaku *)
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -12,58 +12,17 @@ Prefer the CLI as the source of truth:
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
keiyaku guide
|
|
15
|
-
keiyaku akuma list
|
|
16
15
|
keiyaku <command> --help
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Use this shape for committed delivery work:
|
|
22
|
-
|
|
23
|
-
```text
|
|
24
|
-
bind -> round open -> [summon/ask | amend | delivery work]* -> delivery commits -> round close -> petition
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- `bind`: create the active contract ledger and workspace files from Markdown stdin.
|
|
28
|
-
- `round open`: record the scope/objective/brief before delivery work starts.
|
|
29
|
-
- `summon` / `ask`: summon an Akuma helper for bounded work with `keiyaku summon NAME [prompt|-]` or `keiyaku ask NAME [prompt|-]`.
|
|
30
|
-
- `resume`: continue a prior summon session with `keiyaku resume RESPONSE_PATH [prompt|-]`.
|
|
31
|
-
- `akuma list` / `akuma show <name>`: inspect available Akuma profiles before choosing `keiyaku summon NAME`.
|
|
32
|
-
- `round close`: after delivery commits exist, record the report and non-empty commit list.
|
|
33
|
-
- `amend`: update the contract when scope, rules, or criteria changed.
|
|
34
|
-
- `petition`: run the gated petition -> verdict -> engine-claim settlement pipeline.
|
|
35
|
-
- `claim`: engine-only tombstone; use `keiyaku petition`.
|
|
36
|
-
- `forfeit`: archive an addressed contract as abandoned, optionally with `--reason TEXT`.
|
|
37
|
-
- `status`: inspect the Kanshi board: 現世 head, round sparkline, age, drift, stall, archive count.
|
|
38
|
-
|
|
39
|
-
## Temporary Work
|
|
40
|
-
|
|
41
|
-
For temporary tasks, quick inspection, or bounded helper work, use `summon` or `ask` directly. It does not require an active Keiyaku. If a round is open, the Akuma receives the round scope automatically; otherwise it receives only the task, context, and reference files.
|
|
18
|
+
Akuma call/tell/wait/revive lifecycle is owned by the `keiyaku-akuma` skill. Load that skill for helper execution.
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```markdown
|
|
46
|
-
# <short title>
|
|
20
|
+
## Basic Flow
|
|
47
21
|
|
|
48
|
-
|
|
49
|
-
<what the Akuma should do>
|
|
22
|
+
For committed delivery work, bind the contract, open an arc before implementation, commit delivery work, amend when the governing contract changes, renew when the base drifts, and petition only after the delivery is ready for settlement. Opening another arc seals the current one; petition also seals an open arc, so no extra closing arc is needed.
|
|
50
23
|
|
|
51
|
-
|
|
52
|
-
<optional background, file paths, errors, constraints>
|
|
53
|
-
```
|
|
24
|
+
Use `status` to inspect the Kanshi board and `forfeit` to archive abandoned work. Consult command help for their syntax and flags.
|
|
54
25
|
|
|
55
26
|
## Help
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
keiyaku guide
|
|
61
|
-
keiyaku akuma list
|
|
62
|
-
keiyaku round --help
|
|
63
|
-
keiyaku summon --help
|
|
64
|
-
keiyaku ask --help
|
|
65
|
-
keiyaku resume --help
|
|
66
|
-
keiyaku petition --help
|
|
67
|
-
keiyaku claim --help
|
|
68
|
-
keiyaku forfeit --help
|
|
69
|
-
```
|
|
28
|
+
Use `keiyaku <command> --help` for command syntax and flags, and `keiyaku guide` for long-form mechanics. Do not guess Markdown input sections or option defaults.
|