@astrosheep/keiyaku 1.0.0 → 1.0.2
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 +73 -56
- 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 +42 -26
- 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/config/settings.js
CHANGED
|
@@ -3,8 +3,181 @@ import * as path from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { isErrnoException } from "../errno.js";
|
|
5
5
|
import { FlowError } from "../flow-error.js";
|
|
6
|
-
import { SETTINGS_FILE, } from "../keiyaku.js";
|
|
6
|
+
import { CLAUDE_SETTING_SOURCES, SETTINGS_FILE, } from "../keiyaku.js";
|
|
7
7
|
import { getKeiyakuHome } from "./env.js";
|
|
8
|
+
const tomlValueSchema = z.lazy(() => z.union([
|
|
9
|
+
z.string(),
|
|
10
|
+
z.number(),
|
|
11
|
+
z.boolean(),
|
|
12
|
+
z.array(tomlValueSchema),
|
|
13
|
+
z.record(z.string(), tomlValueSchema),
|
|
14
|
+
]));
|
|
15
|
+
const structuredTomlValueSchema = z.record(z.string(), tomlValueSchema);
|
|
16
|
+
const executableSchema = z.string().superRefine((value, ctx) => {
|
|
17
|
+
if (value.length === 0 || /\s/.test(value) || value.includes("\"") || value.includes("'")) {
|
|
18
|
+
ctx.addIssue({
|
|
19
|
+
code: z.ZodIssueCode.custom,
|
|
20
|
+
message: "must be a single program name token with no spaces or quotes.",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const nonblankOptionalString = z
|
|
25
|
+
.string()
|
|
26
|
+
.refine((value) => value.trim().length > 0, { message: "must not be blank." })
|
|
27
|
+
.optional();
|
|
28
|
+
const providerCommonFields = {
|
|
29
|
+
description: nonblankOptionalString,
|
|
30
|
+
};
|
|
31
|
+
const CODEX_SDK_FORBIDDEN_THREAD_OPTIONS_KEYS = [
|
|
32
|
+
"model",
|
|
33
|
+
"modelReasoningEffort",
|
|
34
|
+
"sandboxMode",
|
|
35
|
+
"networkAccessEnabled",
|
|
36
|
+
"webSearchMode",
|
|
37
|
+
"approvalPolicy",
|
|
38
|
+
"workingDirectory",
|
|
39
|
+
"skipGitRepoCheck",
|
|
40
|
+
];
|
|
41
|
+
const FORBIDDEN_CONFIG_KEYS = [
|
|
42
|
+
"model",
|
|
43
|
+
"model_reasoning_effort",
|
|
44
|
+
"sandbox_mode",
|
|
45
|
+
"sandbox_workspace_write",
|
|
46
|
+
"web_search",
|
|
47
|
+
"approval_policy",
|
|
48
|
+
];
|
|
49
|
+
function isConfigKeyForbidden(key) {
|
|
50
|
+
// Exact match for model, model_reasoning_effort, sandbox_mode, web_search, approval_policy
|
|
51
|
+
if (FORBIDDEN_CONFIG_KEYS.includes(key))
|
|
52
|
+
return true;
|
|
53
|
+
// sandbox_workspace_write also rejects dotted subkeys from adapter-native TOML flattening
|
|
54
|
+
if (key.startsWith("sandbox_workspace_write."))
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const claudeSettingSourceSchema = z.enum(CLAUDE_SETTING_SOURCES);
|
|
59
|
+
const writableRootItemSchema = z.string().superRefine((value, ctx) => {
|
|
60
|
+
if (value.length === 0) {
|
|
61
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must not be empty." });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (value.startsWith("~")) {
|
|
65
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must not begin with tilde." });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (value.includes("\0")) {
|
|
69
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must not contain NUL byte." });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (/\$[A-Za-z_]/.test(value) || /\$\{/.test(value)) {
|
|
73
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must not contain environment variable references." });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
export const providerInstanceSchema = z.discriminatedUnion("kind", [
|
|
78
|
+
z
|
|
79
|
+
.object({
|
|
80
|
+
kind: z.literal("codex-sdk"),
|
|
81
|
+
...providerCommonFields,
|
|
82
|
+
executable: executableSchema.optional(),
|
|
83
|
+
profile: z.string().optional(),
|
|
84
|
+
config: structuredTomlValueSchema.optional(),
|
|
85
|
+
threadOptions: structuredTomlValueSchema.optional(),
|
|
86
|
+
})
|
|
87
|
+
.strict()
|
|
88
|
+
.superRefine((data, ctx) => {
|
|
89
|
+
if (data.threadOptions) {
|
|
90
|
+
for (const key of CODEX_SDK_FORBIDDEN_THREAD_OPTIONS_KEYS) {
|
|
91
|
+
if (key in data.threadOptions) {
|
|
92
|
+
ctx.addIssue({
|
|
93
|
+
code: z.ZodIssueCode.custom,
|
|
94
|
+
path: ["threadOptions", key],
|
|
95
|
+
message: `"${key}" is owned by the Akuma or Keiyaku execution, not this provider instance`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (data.config) {
|
|
101
|
+
for (const key of Object.keys(data.config)) {
|
|
102
|
+
if (isConfigKeyForbidden(key)) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: z.ZodIssueCode.custom,
|
|
105
|
+
path: ["config", key],
|
|
106
|
+
message: `"${key}" is owned by the Akuma or Keiyaku execution, not this provider instance`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
z
|
|
113
|
+
.object({
|
|
114
|
+
kind: z.literal("codex-app-server"),
|
|
115
|
+
...providerCommonFields,
|
|
116
|
+
executable: executableSchema.optional(),
|
|
117
|
+
profile: z.string().optional(),
|
|
118
|
+
config: structuredTomlValueSchema.optional(),
|
|
119
|
+
writableRoots: z.array(writableRootItemSchema).optional(),
|
|
120
|
+
})
|
|
121
|
+
.strict()
|
|
122
|
+
.superRefine((data, ctx) => {
|
|
123
|
+
if (data.config) {
|
|
124
|
+
for (const key of Object.keys(data.config)) {
|
|
125
|
+
if (isConfigKeyForbidden(key)) {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: z.ZodIssueCode.custom,
|
|
128
|
+
path: ["config", key],
|
|
129
|
+
message: `"${key}" is owned by the Akuma or Keiyaku execution, not this provider instance`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
z
|
|
136
|
+
.object({
|
|
137
|
+
kind: z.literal("claude-agent-sdk"),
|
|
138
|
+
...providerCommonFields,
|
|
139
|
+
executable: executableSchema.optional(),
|
|
140
|
+
settingSources: z.array(claudeSettingSourceSchema).optional(),
|
|
141
|
+
})
|
|
142
|
+
.strict(),
|
|
143
|
+
z
|
|
144
|
+
.object({
|
|
145
|
+
kind: z.literal("opencode-sdk"),
|
|
146
|
+
...providerCommonFields,
|
|
147
|
+
executable: executableSchema.optional(),
|
|
148
|
+
})
|
|
149
|
+
.strict(),
|
|
150
|
+
z
|
|
151
|
+
.object({
|
|
152
|
+
kind: z.literal("pi"),
|
|
153
|
+
...providerCommonFields,
|
|
154
|
+
})
|
|
155
|
+
.strict(),
|
|
156
|
+
]);
|
|
157
|
+
const PROVIDER_INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
158
|
+
export const providerInstanceNameSchema = z
|
|
159
|
+
.string()
|
|
160
|
+
.max(64, "must contain at most 64 characters.")
|
|
161
|
+
.regex(PROVIDER_INSTANCE_NAME_PATTERN, "must match ^[a-z0-9][a-z0-9-]*$.");
|
|
162
|
+
export const BUILTIN_PROVIDER_INSTANCE_CANDIDATES = {
|
|
163
|
+
"codex-sdk": { name: "codex-sdk", source: "builtin", instance: { kind: "codex-sdk", executable: "codex" } },
|
|
164
|
+
"codex-app-server": {
|
|
165
|
+
name: "codex-app-server",
|
|
166
|
+
source: "builtin",
|
|
167
|
+
instance: { kind: "codex-app-server", executable: "codex" },
|
|
168
|
+
},
|
|
169
|
+
"claude-agent-sdk": {
|
|
170
|
+
name: "claude-agent-sdk",
|
|
171
|
+
source: "builtin",
|
|
172
|
+
instance: { kind: "claude-agent-sdk", executable: "claude", settingSources: ["project"] },
|
|
173
|
+
},
|
|
174
|
+
"opencode-sdk": {
|
|
175
|
+
name: "opencode-sdk",
|
|
176
|
+
source: "builtin",
|
|
177
|
+
instance: { kind: "opencode-sdk", executable: "opencode" },
|
|
178
|
+
},
|
|
179
|
+
pi: { name: "pi", source: "builtin", instance: { kind: "pi" } },
|
|
180
|
+
};
|
|
8
181
|
const AGENT_NAME_PATTERN = /^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*$/;
|
|
9
182
|
export const agentNameSchema = z
|
|
10
183
|
.string()
|
|
@@ -52,13 +225,46 @@ const gateSchema = z
|
|
|
52
225
|
commands: z.array(gateCommandSchema).optional(),
|
|
53
226
|
})
|
|
54
227
|
.strict();
|
|
55
|
-
const KNOWN_SETTINGS_KNOBS = ["default", "reviewer", "gate", "defaultBranch"];
|
|
228
|
+
const KNOWN_SETTINGS_KNOBS = ["default", "reviewer", "gate", "defaultBranch", "worktreeBootstrap"];
|
|
229
|
+
const KNOWN_SETTINGS_ROOT_KEYS = [...KNOWN_SETTINGS_KNOBS, "providers"];
|
|
230
|
+
const WORKTREE_BOOTSTRAP_DEFAULT_TIMEOUT_MS = 300_000;
|
|
231
|
+
const WORKTREE_BOOTSTRAP_MIN_TIMEOUT_MS = 1;
|
|
232
|
+
const WORKTREE_BOOTSTRAP_MAX_TIMEOUT_MS = 3_600_000;
|
|
233
|
+
export const worktreeBootstrapSchema = z
|
|
234
|
+
.object({
|
|
235
|
+
command: z
|
|
236
|
+
.array(z.string())
|
|
237
|
+
.min(1, "command must be a nonempty argv array.")
|
|
238
|
+
.superRefine((items, ctx) => {
|
|
239
|
+
items.forEach((item, index) => {
|
|
240
|
+
if (item.trim().length === 0) {
|
|
241
|
+
ctx.addIssue({
|
|
242
|
+
code: z.ZodIssueCode.custom,
|
|
243
|
+
path: [index],
|
|
244
|
+
message: "command items must be nonempty after trimming.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}),
|
|
249
|
+
timeoutMs: z
|
|
250
|
+
.number()
|
|
251
|
+
.int("timeoutMs must be an integer.")
|
|
252
|
+
.min(WORKTREE_BOOTSTRAP_MIN_TIMEOUT_MS, `timeoutMs must be >= ${WORKTREE_BOOTSTRAP_MIN_TIMEOUT_MS}.`)
|
|
253
|
+
.max(WORKTREE_BOOTSTRAP_MAX_TIMEOUT_MS, `timeoutMs must be <= ${WORKTREE_BOOTSTRAP_MAX_TIMEOUT_MS}.`)
|
|
254
|
+
.optional(),
|
|
255
|
+
})
|
|
256
|
+
.strict()
|
|
257
|
+
.transform((value) => ({
|
|
258
|
+
command: value.command,
|
|
259
|
+
timeoutMs: value.timeoutMs ?? WORKTREE_BOOTSTRAP_DEFAULT_TIMEOUT_MS,
|
|
260
|
+
}));
|
|
56
261
|
const settingsSchema = z
|
|
57
262
|
.object({
|
|
58
263
|
default: agentNameSchema.optional(),
|
|
59
264
|
reviewer: agentNameSchema.optional(),
|
|
60
265
|
gate: gateSchema.optional(),
|
|
61
266
|
defaultBranch: optionalTrimmedString(),
|
|
267
|
+
worktreeBootstrap: worktreeBootstrapSchema.optional(),
|
|
62
268
|
})
|
|
63
269
|
.strict();
|
|
64
270
|
const knobSchemas = {
|
|
@@ -66,6 +272,7 @@ const knobSchemas = {
|
|
|
66
272
|
reviewer: agentNameSchema,
|
|
67
273
|
gate: gateSchema,
|
|
68
274
|
defaultBranch: optionalTrimmedString(),
|
|
275
|
+
worktreeBootstrap: worktreeBootstrapSchema,
|
|
69
276
|
};
|
|
70
277
|
function isPlainObject(value) {
|
|
71
278
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -108,27 +315,102 @@ async function readSettingsSource(filePath) {
|
|
|
108
315
|
return { found: true, raw: undefined, parseError: reason };
|
|
109
316
|
}
|
|
110
317
|
}
|
|
111
|
-
function
|
|
318
|
+
function builtinProviderInstanceCatalog() {
|
|
319
|
+
return { selected: { ...BUILTIN_PROVIDER_INSTANCE_CANDIDATES }, shadows: [] };
|
|
320
|
+
}
|
|
321
|
+
function formatProviderInstanceIssues(name, error) {
|
|
322
|
+
return error.issues
|
|
323
|
+
.map((issue) => {
|
|
324
|
+
const suffix = issue.path.length === 0 ? "" : `.${issue.path.join(".")}`;
|
|
325
|
+
return `providers.${name}${suffix}: ${issue.message}`;
|
|
326
|
+
})
|
|
327
|
+
.join("; ");
|
|
328
|
+
}
|
|
329
|
+
function collectProviderCandidates(source, rawProviders) {
|
|
330
|
+
if (!isPlainObject(rawProviders)) {
|
|
331
|
+
return {
|
|
332
|
+
candidates: [],
|
|
333
|
+
disease: { source: source === "project" ? "local" : "global", knob: "providers", reason: "providers: must be a JSON object" },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const candidates = [];
|
|
337
|
+
for (const [name, rawInstance] of Object.entries(rawProviders)) {
|
|
338
|
+
const nameResult = providerInstanceNameSchema.safeParse(name);
|
|
339
|
+
if (!nameResult.success) {
|
|
340
|
+
candidates.push({
|
|
341
|
+
name,
|
|
342
|
+
source,
|
|
343
|
+
reason: formatProviderInstanceIssues(name, nameResult.error),
|
|
344
|
+
});
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const instanceResult = providerInstanceSchema.safeParse(rawInstance);
|
|
348
|
+
if (!instanceResult.success) {
|
|
349
|
+
candidates.push({
|
|
350
|
+
name,
|
|
351
|
+
source,
|
|
352
|
+
reason: formatProviderInstanceIssues(name, instanceResult.error),
|
|
353
|
+
});
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const instance = instanceResult.data;
|
|
357
|
+
if (instance.kind === "codex-app-server" && instance.writableRoots !== undefined && source === "global") {
|
|
358
|
+
let relativeIndex;
|
|
359
|
+
for (let i = 0; i < instance.writableRoots.length; i++) {
|
|
360
|
+
if (!path.posix.isAbsolute(instance.writableRoots[i])) {
|
|
361
|
+
relativeIndex = i;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (relativeIndex !== undefined) {
|
|
366
|
+
candidates.push({
|
|
367
|
+
name,
|
|
368
|
+
source,
|
|
369
|
+
reason: `providers.${name}.writableRoots.${relativeIndex}: writableRoots item must be an absolute path for global provider instances`,
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
candidates.push({ name, source, instance: instanceResult.data });
|
|
375
|
+
}
|
|
376
|
+
return { candidates };
|
|
377
|
+
}
|
|
378
|
+
function mergeProviderCandidates(catalog, candidates) {
|
|
379
|
+
const selected = { ...catalog.selected };
|
|
380
|
+
const shadows = [...catalog.shadows];
|
|
381
|
+
for (const candidate of candidates) {
|
|
382
|
+
const prior = selected[candidate.name];
|
|
383
|
+
if (prior !== undefined) {
|
|
384
|
+
shadows.push({ name: candidate.name, source: prior.source, shadowedBy: candidate.source });
|
|
385
|
+
}
|
|
386
|
+
selected[candidate.name] = candidate;
|
|
387
|
+
}
|
|
388
|
+
return { selected, shadows };
|
|
389
|
+
}
|
|
390
|
+
function collectSourceDiseases(source, providerSource, read) {
|
|
112
391
|
if (!read.found) {
|
|
113
|
-
return { partial: {}, diseases: [] };
|
|
392
|
+
return { partial: {}, diseases: [], providerCandidates: [] };
|
|
114
393
|
}
|
|
115
394
|
if (read.parseError !== undefined) {
|
|
116
395
|
return {
|
|
117
396
|
partial: {},
|
|
118
397
|
diseases: [{ source, reason: `settings root is unreadable (${read.parseError})` }],
|
|
398
|
+
providerCandidates: [],
|
|
119
399
|
};
|
|
120
400
|
}
|
|
121
401
|
if (!isPlainObject(read.raw)) {
|
|
122
402
|
return {
|
|
123
403
|
partial: {},
|
|
124
404
|
diseases: [{ source, reason: "settings root must be a JSON object" }],
|
|
405
|
+
providerCandidates: [],
|
|
125
406
|
};
|
|
126
407
|
}
|
|
127
408
|
const diseases = [];
|
|
128
409
|
const partial = {};
|
|
410
|
+
let providerCandidates = [];
|
|
129
411
|
const raw = read.raw;
|
|
130
412
|
for (const key of Object.keys(raw)) {
|
|
131
|
-
if (!
|
|
413
|
+
if (!KNOWN_SETTINGS_ROOT_KEYS.includes(key)) {
|
|
132
414
|
diseases.push({
|
|
133
415
|
source,
|
|
134
416
|
knob: key,
|
|
@@ -138,9 +420,25 @@ function collectSourceDiseases(source, read) {
|
|
|
138
420
|
});
|
|
139
421
|
}
|
|
140
422
|
}
|
|
423
|
+
if ("providers" in raw) {
|
|
424
|
+
const collectedProviders = collectProviderCandidates(providerSource, raw.providers);
|
|
425
|
+
providerCandidates = collectedProviders.candidates;
|
|
426
|
+
if (collectedProviders.disease !== undefined)
|
|
427
|
+
diseases.push(collectedProviders.disease);
|
|
428
|
+
}
|
|
141
429
|
for (const knob of KNOWN_SETTINGS_KNOBS) {
|
|
142
430
|
if (!(knob in raw))
|
|
143
431
|
continue;
|
|
432
|
+
// worktreeBootstrap is project-layer only. Global/user presence is a named
|
|
433
|
+
// disease and never contributes execution authority.
|
|
434
|
+
if (knob === "worktreeBootstrap" && source === "global") {
|
|
435
|
+
diseases.push({
|
|
436
|
+
source,
|
|
437
|
+
knob,
|
|
438
|
+
reason: "worktreeBootstrap is project-only and never supplies a value from global settings",
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
144
442
|
const schema = knobSchemas[knob];
|
|
145
443
|
const result = schema.safeParse(raw[knob]);
|
|
146
444
|
if (!result.success) {
|
|
@@ -155,18 +453,29 @@ function collectSourceDiseases(source, read) {
|
|
|
155
453
|
continue;
|
|
156
454
|
partial[knob] = result.data;
|
|
157
455
|
}
|
|
158
|
-
return { partial, diseases };
|
|
456
|
+
return { partial, diseases, providerCandidates };
|
|
159
457
|
}
|
|
160
458
|
function mergeSettingsKnobs(base, override) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
459
|
+
// worktreeBootstrap is whole-value project authority; never field-merge it.
|
|
460
|
+
const { worktreeBootstrap: baseBootstrap, ...baseRest } = base;
|
|
461
|
+
const { worktreeBootstrap: overrideBootstrap, ...overrideRest } = override;
|
|
462
|
+
const mergedRaw = deepMerge(baseRest, overrideRest);
|
|
463
|
+
if (!isPlainObject(mergedRaw)) {
|
|
464
|
+
return overrideBootstrap !== undefined
|
|
465
|
+
? { ...override, worktreeBootstrap: overrideBootstrap }
|
|
466
|
+
: { ...override };
|
|
467
|
+
}
|
|
164
468
|
const merged = {};
|
|
165
469
|
for (const knob of KNOWN_SETTINGS_KNOBS) {
|
|
470
|
+
if (knob === "worktreeBootstrap")
|
|
471
|
+
continue;
|
|
166
472
|
if (knob in mergedRaw) {
|
|
167
473
|
merged[knob] = mergedRaw[knob];
|
|
168
474
|
}
|
|
169
475
|
}
|
|
476
|
+
const bootstrap = overrideBootstrap ?? baseBootstrap;
|
|
477
|
+
if (bootstrap !== undefined)
|
|
478
|
+
merged.worktreeBootstrap = bootstrap;
|
|
170
479
|
return merged;
|
|
171
480
|
}
|
|
172
481
|
export function findSettingsDiseasesForKnob(diseases, knob) {
|
|
@@ -210,17 +519,21 @@ export async function loadKeiyakuSettings(cwd) {
|
|
|
210
519
|
return {
|
|
211
520
|
knobs: null,
|
|
212
521
|
diseases: [{ source: "local", reason: `settings root is unreadable (${reason})` }],
|
|
522
|
+
providerInstances: builtinProviderInstanceCatalog(),
|
|
213
523
|
};
|
|
214
524
|
}
|
|
215
|
-
const globalCollected = collectSourceDiseases("global", globalRead);
|
|
216
|
-
const localCollected = collectSourceDiseases("local", localRead);
|
|
525
|
+
const globalCollected = collectSourceDiseases("global", "global", globalRead);
|
|
526
|
+
const localCollected = collectSourceDiseases("local", "project", localRead);
|
|
217
527
|
const diseases = [...globalCollected.diseases, ...localCollected.diseases];
|
|
528
|
+
let providerInstances = builtinProviderInstanceCatalog();
|
|
529
|
+
providerInstances = mergeProviderCandidates(providerInstances, globalCollected.providerCandidates);
|
|
530
|
+
providerInstances = mergeProviderCandidates(providerInstances, localCollected.providerCandidates);
|
|
218
531
|
let knobs = null;
|
|
219
532
|
if (globalRead.found || localRead.found) {
|
|
220
533
|
knobs = mergeSettingsKnobs(globalCollected.partial, localCollected.partial);
|
|
221
534
|
}
|
|
222
535
|
// Empty object still means "settings files were present"; callers treat null as absent files.
|
|
223
|
-
return { knobs, diseases };
|
|
536
|
+
return { knobs, diseases, providerInstances };
|
|
224
537
|
}
|
|
225
538
|
export function resolveDefaultAgentName(settings) {
|
|
226
539
|
return settings?.default ?? "akuma-codex";
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { FlowError } from "../flow-error.js";
|
|
3
|
+
import { listContractIds, readLedger } from "./ledger.js";
|
|
4
|
+
import { stableRepoRoot } from "./worktree-path.js";
|
|
5
|
+
import { deriveContractState, isTerminalState, } from "./status.js";
|
|
6
|
+
function normalizeAddressQuery(raw) {
|
|
7
|
+
const value = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
8
|
+
if (!value.trim()) {
|
|
9
|
+
throw new FlowError("EMPTY_PARAM", "contract address cannot be empty");
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
const ADDRESS_CARRIER = Symbol.for("keiyaku.address-carrier");
|
|
14
|
+
class AddressedFlowError extends FlowError {
|
|
15
|
+
[ADDRESS_CARRIER];
|
|
16
|
+
constructor(error, carrier) {
|
|
17
|
+
super(error.code, error.message, {
|
|
18
|
+
cause: error,
|
|
19
|
+
hints: error.hints,
|
|
20
|
+
review: error.review,
|
|
21
|
+
});
|
|
22
|
+
this.name = error.name;
|
|
23
|
+
this[ADDRESS_CARRIER] = carrier;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class AddressedError extends Error {
|
|
27
|
+
[ADDRESS_CARRIER];
|
|
28
|
+
constructor(error, carrier) {
|
|
29
|
+
super(error instanceof Error ? error.message : String(error), { cause: error });
|
|
30
|
+
this.name = error instanceof Error ? error.name : "Error";
|
|
31
|
+
this[ADDRESS_CARRIER] = carrier;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function attachAddressCarrier(error, carrier) {
|
|
35
|
+
if (addressCarrierFromError(error))
|
|
36
|
+
return error;
|
|
37
|
+
return error instanceof FlowError
|
|
38
|
+
? new AddressedFlowError(error, carrier)
|
|
39
|
+
: new AddressedError(error, carrier);
|
|
40
|
+
}
|
|
41
|
+
export function addressCarrierFromError(error) {
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
let current = error;
|
|
44
|
+
while (((typeof current === "object" && current !== null) || typeof current === "function") && !seen.has(current)) {
|
|
45
|
+
seen.add(current);
|
|
46
|
+
if (current instanceof AddressResolutionError)
|
|
47
|
+
return current.attempt;
|
|
48
|
+
const addressed = current;
|
|
49
|
+
if (addressed[ADDRESS_CARRIER])
|
|
50
|
+
return addressed[ADDRESS_CARRIER];
|
|
51
|
+
current = addressed.cause;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export async function withResolvedAddress(address, operation) {
|
|
56
|
+
try {
|
|
57
|
+
return await operation();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw attachAddressCarrier(error, address);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function buildResolutionAttempt(input) {
|
|
64
|
+
return {
|
|
65
|
+
repo: input.repo,
|
|
66
|
+
workdir: path.resolve(input.workdir),
|
|
67
|
+
expected: "commission",
|
|
68
|
+
...(input.spelling ? { spelling: input.spelling } : {}),
|
|
69
|
+
candidates: [...(input.candidates ?? [])],
|
|
70
|
+
inference: [...(input.inference ?? [])],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export class AddressResolutionError extends FlowError {
|
|
74
|
+
attempt;
|
|
75
|
+
constructor(code, message, attempt, options) {
|
|
76
|
+
super(code, message, options);
|
|
77
|
+
this.name = "AddressResolutionError";
|
|
78
|
+
this.attempt = attempt;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function contractSlug(contractId) {
|
|
82
|
+
const separator = contractId.indexOf(".");
|
|
83
|
+
return separator > 0 ? contractId.slice(0, separator) : contractId;
|
|
84
|
+
}
|
|
85
|
+
async function commissionAddressCandidates(cwd) {
|
|
86
|
+
const ids = await listContractIds(cwd);
|
|
87
|
+
const ledgers = await Promise.all(ids.map(async (contractId) => readLedger(cwd, contractId)));
|
|
88
|
+
return ledgers.flatMap((ledger) => {
|
|
89
|
+
if (!ledger)
|
|
90
|
+
return [];
|
|
91
|
+
const bind = ledger.entries.find((entry) => entry.kind === "bind");
|
|
92
|
+
if (!bind || bind.contract !== ledger.contractId)
|
|
93
|
+
return [];
|
|
94
|
+
const lifecycle = deriveContractState(ledger.entries);
|
|
95
|
+
return [{
|
|
96
|
+
contractId: ledger.contractId,
|
|
97
|
+
bind,
|
|
98
|
+
active: lifecycle.state !== "corrupt" && !isTerminalState(lifecycle),
|
|
99
|
+
lifecycle,
|
|
100
|
+
}];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function matchCommissionAddress(query, candidates) {
|
|
104
|
+
return candidates.flatMap((candidate) => {
|
|
105
|
+
const matches = [];
|
|
106
|
+
if (candidate.contractId === query)
|
|
107
|
+
matches.push({ ...candidate, spelling: "id" });
|
|
108
|
+
if (contractSlug(candidate.contractId) === query)
|
|
109
|
+
matches.push({ ...candidate, spelling: "slug" });
|
|
110
|
+
if (candidate.active && candidate.bind.data.place === query) {
|
|
111
|
+
matches.push({ ...candidate, spelling: "place" });
|
|
112
|
+
}
|
|
113
|
+
return matches;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function attemptCandidates(candidates) {
|
|
117
|
+
return candidates.map((candidate) => ({
|
|
118
|
+
commissionId: candidate.contractId,
|
|
119
|
+
place: candidate.bind.data.place ?? null,
|
|
120
|
+
name: candidate.bind.data.name,
|
|
121
|
+
objective: candidate.bind.data.objective,
|
|
122
|
+
lifecycle: candidate.lifecycle,
|
|
123
|
+
target: { branch: candidate.bind.data.target },
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
function formatAmbiguousCandidate(query, candidate) {
|
|
127
|
+
const label = candidate.matchedBy ?? "candidate";
|
|
128
|
+
return `${label} @${query}: ${candidate.name} · ${candidate.objective} · commission ${candidate.commissionId}`;
|
|
129
|
+
}
|
|
130
|
+
async function resolvedRepository(cwd) {
|
|
131
|
+
return { kind: "repository", stableRoot: await stableRepoRoot(cwd) };
|
|
132
|
+
}
|
|
133
|
+
function buildResolvedAddress(input) {
|
|
134
|
+
return {
|
|
135
|
+
repo: input.repo,
|
|
136
|
+
workdir: input.workdir,
|
|
137
|
+
binding: { kind: "commission", commissionId: input.candidate.contractId },
|
|
138
|
+
spelling: { asTyped: input.asTyped, source: input.source, matchedBy: input.matchedBy },
|
|
139
|
+
place: { display: input.candidate.bind.data.place ?? null },
|
|
140
|
+
lifecycle: input.candidate.lifecycle,
|
|
141
|
+
target: { branch: input.candidate.bind.data.target },
|
|
142
|
+
inference: input.inference,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function buildUncommissionedAddress(input) {
|
|
146
|
+
return {
|
|
147
|
+
repo: input.repo,
|
|
148
|
+
workdir: path.resolve(input.workdir),
|
|
149
|
+
binding: { kind: input.binding },
|
|
150
|
+
inference: [...(input.inference ?? [])],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export async function resolveUncommissionedRepositoryAddress(input) {
|
|
154
|
+
const repo = await resolvedRepository(input.repoCwd);
|
|
155
|
+
return buildUncommissionedAddress({
|
|
156
|
+
repo,
|
|
157
|
+
workdir: input.workdir,
|
|
158
|
+
binding: input.binding,
|
|
159
|
+
inference: input.inference,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function resolveContractAddress(cwd, raw, source = raw?.startsWith("@") ? "at-address" : "explicit-flag") {
|
|
163
|
+
const workdir = path.resolve(cwd);
|
|
164
|
+
const repo = await resolvedRepository(cwd);
|
|
165
|
+
const candidates = await commissionAddressCandidates(repo.stableRoot);
|
|
166
|
+
if (raw === undefined) {
|
|
167
|
+
const operable = candidates.filter((candidate) => candidate.active);
|
|
168
|
+
const attempt = {
|
|
169
|
+
repo,
|
|
170
|
+
workdir,
|
|
171
|
+
expected: "commission",
|
|
172
|
+
candidates: attemptCandidates(operable),
|
|
173
|
+
inference: ["consulted active commission ledgers"],
|
|
174
|
+
};
|
|
175
|
+
if (operable.length === 1) {
|
|
176
|
+
const candidate = operable[0];
|
|
177
|
+
return buildResolvedAddress({
|
|
178
|
+
repo,
|
|
179
|
+
workdir,
|
|
180
|
+
candidate,
|
|
181
|
+
matchedBy: "id",
|
|
182
|
+
asTyped: `@${candidate.contractId}`,
|
|
183
|
+
source: "sole-active",
|
|
184
|
+
inference: attempt.inference,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
throw new AddressResolutionError(operable.length === 0 ? "EMPTY_PARAM" : "CONTRACT_AMBIGUOUS", operable.length === 0
|
|
188
|
+
? "no active commission found; pass @place, @slug, full @commission-id, or --contract <addr>"
|
|
189
|
+
: "multiple active commissions found; pass @place, @slug, full @commission-id, or --contract <addr>", attempt);
|
|
190
|
+
}
|
|
191
|
+
const query = normalizeAddressQuery(raw);
|
|
192
|
+
const matches = matchCommissionAddress(query, candidates);
|
|
193
|
+
const attempt = {
|
|
194
|
+
repo,
|
|
195
|
+
workdir,
|
|
196
|
+
expected: "commission",
|
|
197
|
+
spelling: { asTyped: source === "at-address" ? `@${query}` : query, source },
|
|
198
|
+
candidates: matches.map((candidate) => ({
|
|
199
|
+
commissionId: candidate.contractId,
|
|
200
|
+
matchedBy: candidate.spelling,
|
|
201
|
+
place: candidate.bind.data.place ?? null,
|
|
202
|
+
name: candidate.bind.data.name,
|
|
203
|
+
objective: candidate.bind.data.objective,
|
|
204
|
+
lifecycle: candidate.lifecycle,
|
|
205
|
+
target: { branch: candidate.bind.data.target },
|
|
206
|
+
})),
|
|
207
|
+
inference: [],
|
|
208
|
+
};
|
|
209
|
+
if (matches.length === 1) {
|
|
210
|
+
return buildResolvedAddress({
|
|
211
|
+
repo,
|
|
212
|
+
workdir,
|
|
213
|
+
candidate: matches[0],
|
|
214
|
+
matchedBy: matches[0].spelling,
|
|
215
|
+
asTyped: attempt.spelling.asTyped,
|
|
216
|
+
source,
|
|
217
|
+
inference: [],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (matches.length === 0) {
|
|
221
|
+
throw new AddressResolutionError("EMPTY_PARAM", `unknown contract address @${query}`, attempt);
|
|
222
|
+
}
|
|
223
|
+
throw new AddressResolutionError("CONTRACT_AMBIGUOUS", `contract address @${query} is ambiguous: ${attempt.candidates.map((candidate) => formatAmbiguousCandidate(query, candidate)).join("; ")}; use a full @commission-id`, attempt);
|
|
224
|
+
}
|