@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
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: keiyaku-akuma
3
+ description: Use when you need to dispatch an Akuma to do work for you — call a demon, check on it, yell more instructions, or continue from a prior turn.
4
+ allowed-tools: Bash(keiyaku *)
5
+ ---
6
+
7
+ # Keiyaku Akuma
8
+
9
+ ```bash
10
+ keiyaku akuma list
11
+ keiyaku <command> --help
12
+ ```
13
+
14
+ Contract work = `keiyaku` skill. This skill = helpers only.
15
+
16
+ ## Two ids
17
+
18
+ | Id | Means | Use with |
19
+ | --- | --- | --- |
20
+ | projection id | live who | `wait`, `tell`, `status` |
21
+ | artifact id | saved answer | `revive` |
22
+
23
+ ## Daily path
24
+
25
+ ```bash
26
+ keiyaku call NAME "task..." # launch + foreground join (no CLI --timeout)
27
+ keiyaku call NAME --detach "task..." # return after launch; keep projection id
28
+ keiyaku wait <projection-id> # join until terminal; prints activity list
29
+ keiyaku wait <projection-id> --timeout 5m # bounded join; timeout keeps projection alive
30
+ keiyaku tell <projection-id> "more..." # mailbox only; does not wait
31
+ keiyaku wait <projection-id> --timeout 10m # after tell, rejoin to see progress/outcome
32
+ keiyaku status # board + compact activity summary; rediscover ids
33
+ keiyaku revive ARTIFACT_ID # new life from saved answer
34
+ ```
35
+
36
+ Long prompt: `keiyaku call NAME - < prompt.md`
37
+
38
+ ## Timeouts (do not confuse)
39
+
40
+ | Surface | Behavior |
41
+ | --- | --- |
42
+ | `call` / `revive` | **No** CLI `--timeout`. Foreground call joins until terminal or signal. |
43
+ | `call --detach` | Returns after launch. Bounded watching is done with `wait --timeout`. |
44
+ | `wait --timeout DURATION` | Only wait command. Forms: `45s`, `5m`, `2h` (max `24h`). Omit = wait until terminal. Timeout exit is a snapshot, not death. |
45
+ | Worker kill (env) | `KEIYAKU_SUBAGENT_EXEC_TIMEOUT_MS` (default 2700000), `KEIYAKU_SUBAGENT_EXEC_IDLE_TIMEOUT_MS` (default 600000). Not CLI flags. |
46
+ | Startup stall | Adoption timeout → wait/status can show `startup timed out` (launch never adopted). |
47
+
48
+ ## Tell vs wait
49
+
50
+ - `tell` writes mailbox intent and may wake when eligible. It does **not** join.
51
+ - After tell: run `wait` (optionally `--timeout`) for outcome and activity.
52
+ - While pending tells exist, wait header can show `N tells waiting`.
53
+
54
+ ## Activity list
55
+
56
+ No separate `activity` command.
57
+
58
+ - **`wait`**: full activity timeline (tools/turns) under the projection header.
59
+ - **`status`**: under `akuma` section, compact per-projection activity summary + tell counts.
60
+
61
+ Lost the outer handle → `status` to find projection id → `wait` for the list.
62
+
63
+ ## Infrastructure
64
+
65
+ - `-C` / `--cwd DIR` — effective command directory (all akuma commands)
66
+
67
+ ## Other flags
68
+
69
+ - `--akuma NAME` — start a session with the named agent (call only)
70
+ - `--incognito` — no response artifact, no later revive (call only)
71
+ - `@addr` / `--contract` — attach commission/arc context when present (call, wait, tell)
72
+ - `--bare` — skip contract attachment only (call only)
73
+ - `--effort LEVEL` — override opaque provider-native effort value (call, revive, tell); must match profile allowlist when set
74
+
75
+ Profiles: `keiyaku akuma list|show`; user `<KEIYAKU_HOME>/akuma/`, project `.keiyaku/akuma/`.
@@ -1,32 +0,0 @@
1
- import { FlowError } from "../flow-error.js";
2
- function deepOverlay(base, extra) {
3
- const next = {};
4
- for (const key of Object.keys(base))
5
- Object.defineProperty(next, key, { value: base[key], enumerable: true, configurable: true, writable: true });
6
- for (const key of Object.keys(extra)) {
7
- const value = extra[key];
8
- const prior = Object.prototype.hasOwnProperty.call(next, key) ? next[key] : undefined;
9
- const merged = value && typeof value === "object" && !Array.isArray(value)
10
- ? prior && typeof prior === "object" && !Array.isArray(prior)
11
- ? deepOverlay(prior, value)
12
- : deepOverlay({}, value)
13
- : value;
14
- Object.defineProperty(next, key, { value: merged, enumerable: true, configurable: true, writable: true });
15
- }
16
- return next;
17
- }
18
- export function resolveEffectiveCodexSdkProfile(profile, cwd, overrides = {}) {
19
- if (profile.webSearchPolicy === "cached" || profile.webSearchPolicy === "live")
20
- throw new FlowError("UNSUPPORTED_POLICY", `Codex SDK unified webSearchPolicy '${profile.webSearchPolicy}' is unsupported because native web-search capability was not proven with either networkAccessEnabled=false or networkAccessEnabled=true; this restriction can be lifted only by future real native tool-provisioning evidence.`);
21
- const unified = {
22
- sandboxMode: profile.accessPolicy === "read" ? "read-only" : "workspace-write",
23
- networkAccessEnabled: profile.networkPolicy === "enabled",
24
- webSearchMode: profile.webSearchPolicy ?? "disabled",
25
- ...(profile.model ? { model: profile.model } : {}),
26
- ...(profile.effort ? { modelReasoningEffort: profile.effort === "max" ? "xhigh" : profile.effort } : {}),
27
- };
28
- const native = deepOverlay(unified, profile.threadOptions ?? {});
29
- const threadOptions = { ...native, ...(overrides.model ? { model: overrides.model } : {}), ...(overrides.effort ? { modelReasoningEffort: overrides.effort === "max" ? "xhigh" : overrides.effort } : {}), workingDirectory: cwd, skipGitRepoCheck: true };
30
- const policy = { ...(threadOptions.sandboxMode === "read-only" ? { accessPolicy: "read" } : threadOptions.sandboxMode === "workspace-write" || threadOptions.sandboxMode === "danger-full-access" ? { accessPolicy: "write" } : {}), ...(typeof threadOptions.networkAccessEnabled === "boolean" ? { networkPolicy: threadOptions.networkAccessEnabled ? "enabled" : "disabled" } : {}), ...(threadOptions.webSearchMode === "disabled" || threadOptions.webSearchMode === "cached" || threadOptions.webSearchMode === "live" ? { webSearchPolicy: threadOptions.webSearchMode } : {}) };
31
- return Object.freeze({ provider: "codex-sdk", executable: profile.executable, ...(profile.profile ? { profile: profile.profile } : {}), ...(profile.config ? { config: profile.config } : {}), ...(typeof threadOptions.model === "string" ? { model: threadOptions.model } : {}), threadOptions: Object.freeze(threadOptions), effectivePolicy: Object.freeze(policy) });
32
- }
@@ -1,114 +0,0 @@
1
- import { FlowError } from "../flow-error.js";
2
- import { EFFORT_LEVELS } from "../keiyaku.js";
3
- import { resolveEffectiveCodexSdkProfile } from "./effective-policy.js";
4
- import { getConfig } from "../config/env.js";
5
- import { selectSubagent } from "./selector.js";
6
- export { formatAvailableAgentNames } from "./selector.js";
7
- export function createAbortError(message) {
8
- const error = new Error(message);
9
- error.name = "AbortError";
10
- return error;
11
- }
12
- export function coerceString(value) {
13
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
14
- }
15
- function codexEffort(config) {
16
- const value = config?.model_reasoning_effort;
17
- return typeof value === "string" && EFFORT_LEVELS.includes(value) ? value : undefined;
18
- }
19
- export async function resolveSubagentConfig(agentName, cwd) {
20
- return (await selectSubagent(agentName, cwd)).profile;
21
- }
22
- function maxCaptureCharsForProvider(provider) {
23
- const config = getConfig();
24
- switch (provider) {
25
- case "codex-sdk":
26
- return config.codex.execMaxCaptureChars;
27
- case "codex-app-server":
28
- return config.codex.execMaxCaptureChars;
29
- case "claude-agent-sdk":
30
- return config.claude.execMaxCaptureChars;
31
- case "opencode-sdk":
32
- return config.opencode.execMaxCaptureChars;
33
- case "pi":
34
- return config.codex.execMaxCaptureChars;
35
- }
36
- }
37
- export function buildSummonTerms(config, prompt, cwd, options) {
38
- const runtimeConfig = getConfig();
39
- const baseTerms = {
40
- workdir: cwd,
41
- prompt,
42
- timeoutMs: options.timeoutMs ?? runtimeConfig.subagent.execTimeoutMs,
43
- idleTimeoutMs: options.idleTimeoutMs ?? runtimeConfig.subagent.execIdleTimeoutMs,
44
- maxCaptureChars: maxCaptureCharsForProvider(config.provider),
45
- signal: options.signal,
46
- projectionDir: options.projectionDir,
47
- projectionExecutionId: options.projectionExecutionId,
48
- };
49
- const body = options.akumaBody;
50
- const replayPrompt = body ? `[Akuma system prompt]\n${body}\n\n[Operator prompt]\n${prompt}` : prompt;
51
- switch (config.provider) {
52
- case "codex-sdk":
53
- {
54
- const effective = "effectivePolicy" in config && config.threadOptions
55
- ? config
56
- : resolveEffectiveCodexSdkProfile(config, cwd, {});
57
- return {
58
- ...baseTerms, prompt: replayPrompt,
59
- provider: "codex-sdk",
60
- model: effective.model,
61
- profile: effective.profile,
62
- config: effective.config,
63
- threadOptions: effective.threadOptions,
64
- effectivePolicy: effective.effectivePolicy,
65
- executable: effective.executable,
66
- };
67
- }
68
- case "codex-app-server":
69
- return {
70
- ...baseTerms,
71
- provider: "codex-app-server",
72
- model: config.model,
73
- effort: codexEffort(config.config),
74
- profile: config.profile,
75
- config: config.config,
76
- executable: config.executable,
77
- developerInstructions: body || undefined,
78
- onCheckpoint() { },
79
- };
80
- case "claude-agent-sdk":
81
- return {
82
- ...baseTerms,
83
- provider: "claude-agent-sdk",
84
- model: config.model,
85
- executable: config.executable,
86
- effort: config.effort,
87
- permissionMode: config.permissionMode,
88
- settingSources: config.settingSources,
89
- systemPrompt: body ? { type: "preset", preset: "claude_code", append: body } : undefined,
90
- onCheckpoint() { },
91
- };
92
- case "opencode-sdk":
93
- return {
94
- ...baseTerms, prompt: replayPrompt,
95
- provider: "opencode-sdk",
96
- model: config.model,
97
- executable: config.executable,
98
- };
99
- case "pi":
100
- return {
101
- ...baseTerms,
102
- provider: "pi",
103
- model: config.model,
104
- thinkingLevel: config.thinkingLevel,
105
- appendSystemPromptOverride: body ? () => [body] : undefined,
106
- onCheckpoint() { },
107
- };
108
- default:
109
- return assertNever(config);
110
- }
111
- }
112
- function assertNever(_value) {
113
- throw new FlowError("INVALID_SETTINGS", "Unsupported subagent provider.");
114
- }
@@ -1,28 +0,0 @@
1
- import { getConfig } from "../config/env.js";
2
- import { assertSettingsKnobUsable, loadKeiyakuSettings, resolveDefaultAgentName, } from "../config/settings.js";
3
- import { loadAkumaCatalog, normalizeAkumaProfile } from "../config/akuma-loader.js";
4
- import { FlowError } from "../flow-error.js";
5
- export function formatAvailableAgentNames(names) {
6
- return names.length > 0 ? names.join(", ") : "(none configured)";
7
- }
8
- export async function selectSubagent(agentName, cwd) {
9
- const loaded = await loadKeiyakuSettings(cwd);
10
- assertSettingsKnobUsable(loaded, "agents");
11
- const settings = loaded.knobs;
12
- const requested = agentName?.trim();
13
- if (!requested && !getConfig().subagent.nameOverride) {
14
- assertSettingsKnobUsable(loaded, "default");
15
- }
16
- const selected = requested || getConfig().subagent.nameOverride || resolveDefaultAgentName(settings);
17
- const catalog = await loadAkumaCatalog(cwd);
18
- const availableNames = Object.keys(catalog.definitions);
19
- if (!availableNames.includes(selected)) {
20
- throw new FlowError("UNKNOWN_SUBAGENT", `Unknown agent '${selected}'. Expected one of: ${formatAvailableAgentNames(availableNames)}`);
21
- }
22
- const definition = catalog.definitions[selected];
23
- return {
24
- name: selected,
25
- definition,
26
- profile: normalizeAkumaProfile(definition.profile),
27
- };
28
- }
@@ -1,17 +0,0 @@
1
- import { assembleResponse, buildPayload, buildSection, formatMaybe, formatWarnings } from "./format.js";
2
- import { WARNING_SECTION_TITLE } from "./response-style.js";
3
- import { CONTENT_SECTION_TITLE, formatSummonPersistenceSummary, INFO_AGENT_MAX_CHARS, SUMMON_RESPONSE_HEADING, } from "./shared.js";
4
- export function buildSummonResponse(result, input) {
5
- const infoLines = [
6
- ...formatMaybe("Agent", result.agent ?? input.agent, INFO_AGENT_MAX_CHARS),
7
- ];
8
- const summarySection = buildSection(CONTENT_SECTION_TITLE, result.summary);
9
- const warningSection = buildSection(WARNING_SECTION_TITLE, formatWarnings(result.meta?.warnings ?? []));
10
- const persistedSummary = formatSummonPersistenceSummary(input.responsePath);
11
- const text = assembleResponse(SUMMON_RESPONSE_HEADING, persistedSummary, [summarySection, warningSection].filter((s) => s !== null), infoLines);
12
- const { session: _session, ...responseData } = result;
13
- return {
14
- text,
15
- payload: buildPayload(responseData, input.responsePath),
16
- };
17
- }
@@ -1,11 +0,0 @@
1
- export function renderTellReceipt(id, state) {
2
- if (state === "active")
3
- return `Tell ${id} delivered to the active generation.\n`;
4
- if (state === "woken")
5
- return `Tell ${id} accepted; waking the projection.\n`;
6
- if (state === "lost")
7
- return `Tell ${id} remains durable in the mailbox.\ndelivery: next-generation-replay\nnextAction: Run keiyaku resume --projection ${id}.\n`;
8
- if (state === "terminal")
9
- return `Tell ${id} remains in the mailbox. Run keiyaku resume --projection ${id}.\n`;
10
- return `Tell ${id} accepted in the mailbox.\n`;
11
- }
@@ -1,268 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { z } from "zod";
4
- import { parseToAST } from "../core/markdown/parser.js";
5
- import { BUILTIN_AGENT_PROFILES, CLAUDE_DEFAULT_PERMISSION_MODE, CLAUDE_DEFAULT_SETTING_SOURCES, CLAUDE_EFFORT_LEVELS, CLAUDE_PERMISSION_MODES, CLAUDE_SETTING_SOURCES, } from "../keiyaku.js";
6
- import { getKeiyakuHome } from "./env.js";
7
- import { agentNameSchema } from "./settings.js";
8
- function defaultExecutable(provider) {
9
- if (provider === "codex-sdk" || provider === "codex-app-server")
10
- return "codex";
11
- if (provider === "claude-agent-sdk")
12
- return "claude";
13
- if (provider === "pi")
14
- return "pi";
15
- return "opencode";
16
- }
17
- export function normalizeAkumaProfile(profile) {
18
- const executable = "executable" in profile && profile.executable ? profile.executable : defaultExecutable(profile.provider);
19
- if (profile.provider === "codex-sdk" || profile.provider === "codex-app-server") {
20
- return Object.freeze({ ...profile, executable, config: profile.config });
21
- }
22
- if (profile.provider === "claude-agent-sdk") {
23
- return Object.freeze({ ...profile, executable, permissionMode: profile.permissionMode ?? CLAUDE_DEFAULT_PERMISSION_MODE, settingSources: Object.freeze([...(profile.settingSources ?? CLAUDE_DEFAULT_SETTING_SOURCES)]) });
24
- }
25
- return Object.freeze({ ...profile, executable });
26
- }
27
- const executableSchema = z.string().superRefine((value, ctx) => {
28
- if (/\s/.test(value) || value.includes("\"") || value.includes("'")) {
29
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must be a single program name token with no spaces or quotes." });
30
- }
31
- });
32
- const codexFields = {
33
- description: z.string().optional(),
34
- model: z.string().optional(),
35
- profile: z.string().optional(),
36
- executable: executableSchema.optional(),
37
- config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
38
- };
39
- const policyFields = {
40
- accessPolicy: z.enum(["read", "write"]).optional(),
41
- networkPolicy: z.enum(["disabled", "enabled"]).optional(),
42
- webSearchPolicy: z.enum(["disabled", "cached", "live"]).optional(),
43
- };
44
- const codexSdkFields = {
45
- ...codexFields,
46
- ...policyFields,
47
- effort: z.enum(CLAUDE_EFFORT_LEVELS).optional(),
48
- threadOptions: z.record(z.string(), z.unknown()).optional(),
49
- };
50
- const profileSchema = z.discriminatedUnion("provider", [
51
- z.object({ provider: z.literal("codex-sdk"), ...codexSdkFields }).strict(),
52
- z.object({ provider: z.literal("codex-app-server"), ...codexFields, ...policyFields }).strict(),
53
- z.object({ provider: z.literal("claude-agent-sdk"), description: z.string().optional(), model: z.string().optional(), executable: executableSchema.optional(), effort: z.enum(CLAUDE_EFFORT_LEVELS).optional(), permissionMode: z.enum(CLAUDE_PERMISSION_MODES).optional(), settingSources: z.array(z.enum(CLAUDE_SETTING_SOURCES)).optional(), ...policyFields }).strict(),
54
- z.object({ provider: z.literal("opencode-sdk"), description: z.string().optional(), model: z.string().optional(), executable: executableSchema.optional(), ...policyFields }).strict(),
55
- z.object({ provider: z.literal("pi"), description: z.string().optional(), model: z.string().optional(), ...policyFields }).strict(),
56
- ]);
57
- function formatIssues(error) {
58
- return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
59
- }
60
- function decodeConfigValue(value) {
61
- if (value === "true")
62
- return true;
63
- if (value === "false")
64
- return false;
65
- if (/^-?\d+$/.test(value))
66
- return Number.parseInt(value, 10);
67
- if (/^-?\d+\.\d+$/.test(value))
68
- return Number.parseFloat(value);
69
- return value;
70
- }
71
- function stripFrontMatterQuote(rawValue) {
72
- const value = rawValue.trim();
73
- if (value.length < 2)
74
- return value;
75
- const quote = value[0];
76
- return (quote === "'" || quote === '"') && value.at(-1) === quote ? value.slice(1, -1) : value;
77
- }
78
- function splitFrontMatter(content) {
79
- const ast = parseToAST(content, { allowSections: false });
80
- if (ast.frontmatter === undefined) {
81
- throw new Error("missing leading YAML frontmatter");
82
- }
83
- const lines = content.split(/\r?\n/);
84
- let closingIndex = -1;
85
- for (let index = 1; index < lines.length; index += 1) {
86
- if ((lines[index] ?? "").trim() === "---") {
87
- closingIndex = index;
88
- break;
89
- }
90
- }
91
- if (closingIndex === -1)
92
- throw new Error("frontmatter is not closed");
93
- const entries = [];
94
- for (const line of lines.slice(1, closingIndex)) {
95
- const match = line.trim().match(/^([A-Za-z0-9_.-]+)\s*:\s*(.*)$/);
96
- if (match)
97
- entries.push([match[1] ?? "", stripFrontMatterQuote(match[2] ?? "")]);
98
- }
99
- return { entries, body: lines.slice(closingIndex + 1).join("\n") };
100
- }
101
- function isNativeObject(value) {
102
- return value !== null && typeof value === "object" && !Array.isArray(value);
103
- }
104
- function hasOwn(record, key) {
105
- return Object.prototype.hasOwnProperty.call(record, key);
106
- }
107
- function defineOwn(record, key, value) {
108
- Object.defineProperty(record, key, { value, enumerable: true, configurable: true, writable: true });
109
- }
110
- function nativeAssemblyValue(value) {
111
- if (!isNativeObject(value))
112
- return value;
113
- const record = Object.create(null);
114
- for (const key of Object.keys(value))
115
- defineOwn(record, key, nativeAssemblyValue(value[key]));
116
- return record;
117
- }
118
- function materializeNativeValue(value) {
119
- if (!isNativeObject(value))
120
- return value;
121
- const record = Object.create(null);
122
- for (const key of Object.keys(value))
123
- defineOwn(record, key, materializeNativeValue(value[key]));
124
- Object.setPrototypeOf(record, Object.prototype);
125
- return record;
126
- }
127
- function mergeNativeValue(existing, incoming, path) {
128
- const existingObject = isNativeObject(existing);
129
- const incomingObject = isNativeObject(incoming);
130
- if (existingObject !== incomingObject)
131
- throw new Error(`${path} has a scalar/object collision`);
132
- if (!existingObject || !incomingObject) {
133
- if (!Object.is(existing, incoming))
134
- throw new Error(`${path} has contradictory scalar values`);
135
- return existing;
136
- }
137
- for (const key of Object.keys(incoming)) {
138
- const childPath = `${path}.${key}`;
139
- if (hasOwn(existing, key))
140
- defineOwn(existing, key, mergeNativeValue(existing[key], incoming[key], childPath));
141
- else
142
- defineOwn(existing, key, nativeAssemblyValue(incoming[key]));
143
- }
144
- return existing;
145
- }
146
- function decodeNativeValue(value) {
147
- try {
148
- return JSON.parse(value);
149
- }
150
- catch {
151
- return value;
152
- }
153
- }
154
- function insertNativePath(root, segments, decoded) {
155
- let target = root;
156
- const fullPath = `threadOptions.${segments.join(".")}`;
157
- for (const segment of segments.slice(0, -1)) {
158
- if (!hasOwn(target, segment))
159
- defineOwn(target, segment, Object.create(null));
160
- const existing = target[segment];
161
- if (!isNativeObject(existing))
162
- throw new Error(`${fullPath} has a scalar/object collision`);
163
- target = existing;
164
- }
165
- const leaf = segments.at(-1);
166
- const incoming = nativeAssemblyValue(decoded);
167
- if (hasOwn(target, leaf))
168
- defineOwn(target, leaf, mergeNativeValue(target[leaf], incoming, fullPath));
169
- else
170
- defineOwn(target, leaf, incoming);
171
- }
172
- function decodeProfile(entries) {
173
- const raw = {};
174
- let nativeThreadOptions;
175
- for (const [key, value] of entries) {
176
- if (key.startsWith("threadOptions.")) {
177
- const segments = key.slice("threadOptions.".length).split(".");
178
- if (segments.some((segment) => !segment))
179
- throw new Error("threadOptions.* keys must name a path");
180
- if (segments[0] === "workingDirectory" || segments[0] === "skipGitRepoCheck")
181
- throw new Error(`threadOptions.${segments[0]} is reserved by Keiyaku`);
182
- nativeThreadOptions ??= Object.create(null);
183
- insertNativePath(nativeThreadOptions, segments, decodeNativeValue(value));
184
- continue;
185
- }
186
- if (key.startsWith("config.")) {
187
- const configKey = key.slice("config.".length);
188
- if (configKey.length === 0)
189
- throw new Error("config.* keys must name a config field");
190
- if (/^[\[{]/.test(value.trim()))
191
- throw new Error(`config.${configKey} cannot express arrays or nested tables`);
192
- const config = (raw.config ??= {});
193
- config[configKey] = decodeConfigValue(value);
194
- continue;
195
- }
196
- if (key === "settingSources") {
197
- const values = value.split(",").map((token) => token.trim());
198
- if (values.some((token) => token.length === 0))
199
- throw new Error("settingSources must not contain an empty token");
200
- raw[key] = values;
201
- continue;
202
- }
203
- raw[key] = value;
204
- }
205
- const parsed = profileSchema.safeParse(nativeThreadOptions ? { ...raw, threadOptions: {} } : raw);
206
- if (!parsed.success)
207
- throw new Error(formatIssues(parsed.error));
208
- if (parsed.data.provider === "codex-sdk") {
209
- const forbidden = Object.keys(parsed.data.config ?? {}).find((key) => key === "model" || key === "model_reasoning_effort" || key === "sandbox_mode" || key === "sandbox_workspace_write" || key.startsWith("sandbox_workspace_write.") || key === "web_search" || key === "approval_policy");
210
- if (forbidden)
211
- throw new Error(`config.${forbidden} is forbidden for codex-sdk; use ${forbidden === "model_reasoning_effort" ? "effort" : "native profile fields"} instead`);
212
- }
213
- return nativeThreadOptions
214
- ? { ...parsed.data, threadOptions: materializeNativeValue(nativeThreadOptions) }
215
- : parsed.data;
216
- }
217
- async function readLayer(directory, source, invalid) {
218
- let entries;
219
- try {
220
- entries = await fs.readdir(directory, { withFileTypes: true });
221
- }
222
- catch (error) {
223
- if (error.code === "ENOENT")
224
- return [];
225
- invalid.push({ name: `${source}-akuma-directory`, file: directory, reason: `unable to read akuma directory (${error instanceof Error ? error.message : String(error)})` });
226
- return [];
227
- }
228
- const definitions = [];
229
- for (const entry of entries.filter((candidate) => candidate.isFile() && candidate.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
230
- const file = path.join(directory, entry.name);
231
- const name = path.basename(entry.name, ".md");
232
- const nameResult = agentNameSchema.safeParse(name);
233
- if (!nameResult.success) {
234
- invalid.push({ name, file, reason: `invalid akuma name: ${formatIssues(nameResult.error)}` });
235
- continue;
236
- }
237
- try {
238
- const content = await fs.readFile(file, "utf-8");
239
- const { entries: frontmatter, body } = splitFrontMatter(content);
240
- definitions.push({ name, source, path: file, body, profile: decodeProfile(frontmatter) });
241
- }
242
- catch (error) {
243
- invalid.push({ name, file, reason: error instanceof Error ? error.message : String(error) });
244
- }
245
- }
246
- return definitions;
247
- }
248
- export async function loadAkumaCatalog(cwd) {
249
- const invalid = [];
250
- const definitions = {};
251
- const shadows = [];
252
- for (const [name, profile] of Object.entries(BUILTIN_AGENT_PROFILES)) {
253
- definitions[name] = { name, source: "builtin", path: null, body: "", profile };
254
- }
255
- for (const definition of await readLayer(path.join(getKeiyakuHome(), "akuma"), "user", invalid)) {
256
- const prior = definitions[definition.name];
257
- if (prior !== undefined)
258
- shadows.push({ name: prior.name, source: prior.source, path: prior.path, shadowedBy: definition.source });
259
- definitions[definition.name] = definition;
260
- }
261
- for (const definition of await readLayer(path.join(cwd, ".keiyaku", "akuma"), "project", invalid)) {
262
- const prior = definitions[definition.name];
263
- if (prior !== undefined)
264
- shadows.push({ name: prior.name, source: prior.source, path: prior.path, shadowedBy: definition.source });
265
- definitions[definition.name] = definition;
266
- }
267
- return { definitions, shadows, invalid };
268
- }
@@ -1,73 +0,0 @@
1
- import { FlowError } from "../flow-error.js";
2
- import { appendRefLogWithRetry, readRefLog } from "./ref-log.js";
3
- export const QUEUE_REF = "refs/keiyaku/queue";
4
- function parseQueueEntry(message) {
5
- const body = message.slice(message.indexOf("\n\n") + 2).trim();
6
- const parsed = JSON.parse(body);
7
- if (parsed.v !== 1 || (parsed.kind !== "enqueue" && parsed.kind !== "dequeue") || !parsed.contract) {
8
- throw new FlowError("INTERNAL_STATE", `invalid queue entry for ${QUEUE_REF}`);
9
- }
10
- return parsed;
11
- }
12
- function queueSubject(entry) {
13
- return `keiyaku queue: ${entry.kind} ${entry.contract}`;
14
- }
15
- export function queuedContracts(entries) {
16
- const queued = new Set();
17
- const ordered = [];
18
- for (const entry of entries) {
19
- if (entry.kind === "enqueue") {
20
- if (queued.has(entry.contract))
21
- continue;
22
- queued.add(entry.contract);
23
- ordered.push(entry.contract);
24
- continue;
25
- }
26
- if (!queued.delete(entry.contract))
27
- continue;
28
- const index = ordered.indexOf(entry.contract);
29
- if (index >= 0)
30
- ordered.splice(index, 1);
31
- }
32
- return ordered;
33
- }
34
- export function queuePositionFromEntries(entries, contractId) {
35
- const position = queuedContracts(entries).findIndex((id) => id === contractId);
36
- return position < 0 ? null : position + 1;
37
- }
38
- export function queuePositionsFromEntries(entries) {
39
- return new Map(queuedContracts(entries).map((id, index) => [id, index + 1]));
40
- }
41
- export async function readQueue(cwd) {
42
- return readRefLog(cwd, QUEUE_REF, parseQueueEntry);
43
- }
44
- export async function queuePosition(cwd, contractId) {
45
- return queuePositionFromEntries(await readQueue(cwd), contractId);
46
- }
47
- export async function enqueueContract(cwd, input) {
48
- const result = await appendRefLogWithRetry(cwd, QUEUE_REF, queueSubject, parseQueueEntry, (entries) => {
49
- if (queuedContracts(entries).includes(input.contractId))
50
- return null;
51
- return { v: 1, kind: "enqueue", contract: input.contractId, at: input.at, actor: input.actor };
52
- });
53
- if (!result.ok) {
54
- if (result.reason === "aborted") {
55
- return { head: result.currentHead ?? "", position: (await queuePosition(cwd, input.contractId)) ?? 0 };
56
- }
57
- throw new FlowError("INTERNAL_STATE", `failed to enqueue contract ${input.contractId}: ${result.reason}`);
58
- }
59
- return { head: result.head, position: (await queuePosition(cwd, input.contractId)) ?? 0 };
60
- }
61
- export async function dequeueContract(cwd, input) {
62
- const result = await appendRefLogWithRetry(cwd, QUEUE_REF, queueSubject, parseQueueEntry, (entries) => {
63
- if (!queuedContracts(entries).includes(input.contractId))
64
- return null;
65
- return { v: 1, kind: "dequeue", contract: input.contractId, at: input.at, actor: input.actor };
66
- });
67
- if (!result.ok) {
68
- if (result.reason === "aborted")
69
- return { head: result.currentHead };
70
- throw new FlowError("INTERNAL_STATE", `failed to dequeue contract ${input.contractId}: ${result.reason}`);
71
- }
72
- return { head: result.head };
73
- }