@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.
Files changed (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -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 collectSourceDiseases(source, read) {
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 (!KNOWN_SETTINGS_KNOBS.includes(key)) {
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
- const mergedRaw = deepMerge(base, override);
162
- if (!isPlainObject(mergedRaw))
163
- return { ...override };
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
+ }