@astrosheep/keiyaku 2.9.10 → 2.9.12

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 (48) hide show
  1. package/README.md +86 -45
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/providers/claude-agent-sdk/adapter.js +15 -2
  4. package/build/cli/commands/contract/bind/meta.js +4 -4
  5. package/build/cli/commands/metadata.js +3 -2
  6. package/build/cli/commands/projection/kill/handler.js +4 -5
  7. package/build/cli/commands/shared.js +1 -1
  8. package/build/cli/commands/task/catalog.js +2 -0
  9. package/build/cli/commands/task/compose/handler.js +18 -0
  10. package/build/cli/commands/task/compose/meta.js +10 -0
  11. package/build/cli/flags.js +5 -1
  12. package/build/cli/index.js +11 -6
  13. package/build/cli/parse-flags.js +21 -21
  14. package/build/cli/parse-metadata.js +5 -2
  15. package/build/cli/parse-selectors.js +4 -4
  16. package/build/cli/parse.js +23 -23
  17. package/build/cli/projection-address.js +7 -7
  18. package/build/cli/render/task.js +27 -0
  19. package/build/cli/types.js +1 -1
  20. package/build/core/addressing.js +3 -3
  21. package/build/core/call/context.js +3 -3
  22. package/build/core/call/prompt.js +1 -1
  23. package/build/core/contract-view.js +1 -1
  24. package/build/core/draft.js +51 -22
  25. package/build/core/projection/generation/model.js +1 -1
  26. package/build/core/projection/generation/projection-generation-runner.js +13 -3
  27. package/build/core/projection/generation/transitions.js +10 -0
  28. package/build/core/projection/projection-history.js +1 -1
  29. package/build/core/projection/projection-kill.js +5 -4
  30. package/build/core/projection/projection-runner-lock.js +58 -229
  31. package/build/core/projection/projection-wait.js +2 -2
  32. package/build/core/projection/projection-wake.js +2 -0
  33. package/build/core/repository-ledger/claim-evidence.js +1 -1
  34. package/build/core/repository-ledger/current-state-store.js +455 -337
  35. package/build/core/repository-ledger/read-model.js +3 -3
  36. package/build/core/repository-ledger/write-transaction.js +15 -13
  37. package/build/core/sqlite-process-lifetime-lock.js +194 -0
  38. package/build/core/task/compose.js +327 -0
  39. package/build/core/task/index.js +1 -0
  40. package/build/core/task/settlement-git.js +69 -26
  41. package/build/core/task/task-git-store.js +1 -0
  42. package/build/core/task/task-store-repository.js +1 -0
  43. package/build/flow-error.js +6 -1
  44. package/build/generated/version.js +2 -2
  45. package/package.json +2 -2
  46. package/skills/keiyaku-akuma/SKILL.md +6 -1
  47. package/skills/keiyaku-task/SKILL.md +23 -1
  48. package/skills/keiyaku-workflow/SKILL.md +33 -18
@@ -3,7 +3,7 @@ import { SUBAGENT_SESSION_VERSION } from "../../session.js";
3
3
  import { createProjectionDriver } from "../../harness/execution-handle.js";
4
4
  import { exactLaunchInputReceipts } from "../../harness/control-types.js";
5
5
  import { createProviderFailure } from "../../harness/provider-adapter.js";
6
- import { buildSubagentEnv, createAbortError, emitProviderEvidence, linkAbortSignal, sha256Utf8 } from "../../harness/runtime.js";
6
+ import { awaitProviderAbortSettlement, buildSubagentEnv, createAbortError, emitProviderEvidence, linkAbortSignal, sha256Utf8 } from "../../harness/runtime.js";
7
7
  import { claudeIngressDisposition, emitClaudeThoughts, emitClaudeToolResults, emitClaudeToolStarts, emitClaudeUsage, extractAssistantMessage, extractAssistantMessageId, extractAssistantMessageModel, extractOverloadFallbackWarning, extractResultText, extractResumeToken, isClaudeApiRetryEvent, isClaudeModelRefusalFallbackEvent, isClaudeModelRefusalNoFallbackEvent, mapClaudeApiRetryWarning, } from "./events.js";
8
8
  import { CLAUDE_AGENT_SDK_PROVIDER, createPushableUserMessageStream, toClaudeUserMessage, } from "./session.js";
9
9
  function toClaudeSdkEffort(effort) {
@@ -19,6 +19,10 @@ async function loadClaudeAgentSdk(loader) {
19
19
  export async function startClaudeAgentSdk(prompt, cwd, options) {
20
20
  const sessionId = options.sessionId?.trim();
21
21
  let liveQuery;
22
+ let queryAbortController;
23
+ let runStarted = false;
24
+ let settleRun;
25
+ const runSettled = new Promise((resolve) => { settleRun = resolve; });
22
26
  let pendingEvidenceDiagnostics = 0;
23
27
  let builderDiagnostic = () => {
24
28
  pendingEvidenceDiagnostics += 1;
@@ -117,9 +121,13 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
117
121
  queryActive = false;
118
122
  const abortError = createAbortError(`${CLAUDE_AGENT_SDK_PROVIDER} exec cancelled by client`);
119
123
  if (mode === "force") {
124
+ queryAbortController?.abort(abortError);
120
125
  inputStream?.end(abortError);
121
126
  rejectPendingEnqueueWaiters(abortError);
122
127
  liveQuery?.close();
128
+ if (runStarted) {
129
+ await awaitProviderAbortSettlement(CLAUDE_AGENT_SDK_PROVIDER, runSettled, "forced query teardown did not settle");
130
+ }
123
131
  return;
124
132
  }
125
133
  try {
@@ -131,8 +139,12 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
131
139
  liveQuery?.close();
132
140
  inputStream?.end(abortError);
133
141
  rejectPendingEnqueueWaiters(abortError);
142
+ if (runStarted) {
143
+ await awaitProviderAbortSettlement(CLAUDE_AGENT_SDK_PROVIDER, runSettled, "graceful query teardown did not settle");
144
+ }
134
145
  },
135
146
  async run(builder) {
147
+ runStarted = true;
136
148
  builderDiagnostic = (text) => builder.emit({ type: "diagnostic", text });
137
149
  emitTurnStarted = () => builder.emit({ type: "turn", phase: "started" });
138
150
  while (pendingEvidenceDiagnostics > 0) {
@@ -140,7 +152,7 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
140
152
  builderDiagnostic("provider evidence callback failed");
141
153
  }
142
154
  const sdk = await loadClaudeAgentSdk(options.loader);
143
- const queryAbortController = new AbortController();
155
+ queryAbortController = new AbortController();
144
156
  const unlinkBuilderAbort = linkAbortSignal(builder.signal, queryAbortController, () => undefined);
145
157
  const unlinkOuterAbort = linkAbortSignal(options.signal, queryAbortController, () => undefined);
146
158
  try {
@@ -375,6 +387,7 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
375
387
  inputStream.end(endError);
376
388
  }
377
389
  rejectPendingEnqueueWaiters(endError);
390
+ settleRun();
378
391
  }
379
392
  },
380
393
  });
@@ -5,7 +5,7 @@ export const meta = {
5
5
  "keiyaku @<full-contract-id> bind [-C|--cwd DIR] [--repo DIR]",
6
6
  "keiyaku bind --contract <full-contract-id> [-C|--cwd DIR] [--repo DIR]",
7
7
  "keiyaku bind [-C|--cwd DIR] [--repo DIR] [--task ID]... [- < contract.md]",
8
- "keiyaku bind [-C|--cwd DIR] [--repo DIR] [--task ID]... --title TEXT --context TEXT --objective TEXT --design TEXT --scope GLOB --criteria CRITERION [--verification DECLARATION] [--dry-run]",
8
+ "keiyaku bind [-C|--cwd DIR] [--repo DIR] [--task ID]... --title TEXT --context TEXT --objective TEXT --design TEXT --scope GLOB [--scope GLOB]... --criteria CRITERION [--verification DECLARATION] [--dry-run]",
9
9
  ],
10
10
  stdin: { mode: "document", selector: { kind: "positional", index: 0, required: false, rejectBlank: false } },
11
11
  flags: ["cwd", "repo", "contract", "after", "exclusive", "draft-only", "title", "context", "objective", "design", "scope", "criteria", "verification", "task", "dry-run"],
@@ -21,7 +21,7 @@ export const meta = {
21
21
  "## Objective",
22
22
  "<what the commission should accomplish>",
23
23
  "",
24
- "## Design",
24
+ "## Design & Approach",
25
25
  "<settled blueprint>",
26
26
  "",
27
27
  "## Scope",
@@ -54,7 +54,7 @@ export const meta = {
54
54
  "## Objective",
55
55
  "<what the commission should accomplish>",
56
56
  "",
57
- "## Design",
57
+ "## Design & Approach",
58
58
  "<settled blueprint>",
59
59
  "",
60
60
  "## Scope",
@@ -75,7 +75,7 @@ export const meta = {
75
75
  "## Context",
76
76
  "<established facts and constraints>",
77
77
  "",
78
- "## Design",
78
+ "## Design & Approach",
79
79
  "<settled blueprint>",
80
80
  "",
81
81
  "## Scope",
@@ -22,6 +22,7 @@ import { meta as statusMeta } from "./projection/status/meta.js";
22
22
  import { meta as historyMeta } from "./projection/history/meta.js";
23
23
  import { meta as taskMeta } from "./task/task/meta.js";
24
24
  import { meta as taskAddMeta } from "./task/add/meta.js";
25
+ import { meta as taskComposeMeta } from "./task/compose/meta.js";
25
26
  import { meta as taskShowMeta } from "./task/show/meta.js";
26
27
  import { meta as taskLsMeta } from "./task/ls/meta.js";
27
28
  import { meta as taskLogMeta } from "./task/log/meta.js";
@@ -38,14 +39,14 @@ export const COMMAND_METADATA_ORDER = [
38
39
  "bind", "arc", "call", "revive", "wait", "tell", "kill", "log",
39
40
  "akuma", "akuma ls", "akuma show", "skills", "skills install", "amend", "audit",
40
41
  "renew", "petition", "forfeit", "guide", "completion", "status", "history", "task",
41
- "task add", "task show", "task ls", "task log", "task start", "task stop", "task hold", "task resume", "task done",
42
+ "task add", "task compose", "task show", "task ls", "task log", "task start", "task stop", "task hold", "task resume", "task done",
42
43
  "task drop", "task update", "task doctor", "dump-env",
43
44
  ];
44
45
  const COMMAND_METADATA = [
45
46
  bindMeta, arcMeta, callMeta, reviveMeta, waitMeta, tellMeta, killMeta, logMeta,
46
47
  akumaMeta, akumaLsMeta, akumaShowMeta, skillsMeta, skillsInstallMeta, amendMeta, auditMeta,
47
48
  renewMeta, petitionMeta, forfeitMeta, guideMeta, completionMeta, statusMeta, historyMeta,
48
- taskMeta, taskAddMeta, taskShowMeta, taskLsMeta, taskLogMeta, taskStartMeta, taskStopMeta,
49
+ taskMeta, taskAddMeta, taskComposeMeta, taskShowMeta, taskLsMeta, taskLogMeta, taskStartMeta, taskStopMeta,
49
50
  taskHoldMeta, taskResumeMeta,
50
51
  taskDoneMeta, taskDropMeta, taskUpdateMeta, taskDoctorMeta, dumpEnvMeta,
51
52
  ];
@@ -10,12 +10,11 @@ export const handler = async (_stdin, flags, signal, positional) => runProjectio
10
10
  throw new FlowError("EMPTY_PARAM", "kill requires a projection address");
11
11
  const projection = await resolveProjectionCommandTarget(ctx, projectionAddress, "kill");
12
12
  const result = killProjectionGeneration(projection.directory);
13
- if (result.status === "already-closed") {
14
- return buildKillAlreadyClosedResponse(projection.id);
15
- }
16
13
  if (flags.timeoutMs === undefined) {
17
- return buildKillAcceptedResponse(projection.id);
14
+ return result.status === "already-closed"
15
+ ? buildKillAlreadyClosedResponse(projection.id)
16
+ : buildKillAcceptedResponse(projection.id);
18
17
  }
19
- // Accepted with --wait: no acceptance receipt or address preamble.
18
+ // --wait always replays ordinary wait, including already-terminal generations.
20
19
  return buildWaitResponse(projection.id, await waitForProjection({ projectionDirectory: projection.directory, timeoutMs: flags.timeoutMs, signal }), { alias: projection.alias });
21
20
  });
@@ -54,7 +54,7 @@ export async function repositoryDirectory(flags, operation) {
54
54
  const asTyped = flags.contractAddressSource === "at-address"
55
55
  ? `@${flags.contractId}`
56
56
  : `--contract ${flags.contractId}`;
57
- throw new AddressResolutionError("EMPTY_PARAM", `${asTyped} is a commission address, but ${candidate} has no repository ledger; contract addressing requires a ledger`, buildResolutionAttempt({
57
+ throw new AddressResolutionError("UNKNOWN_ADDRESS", `${asTyped} is a commission address, but ${candidate} has no repository ledger; contract addressing requires a ledger`, buildResolutionAttempt({
58
58
  repo: { kind: "user" },
59
59
  workdir: effectiveDirectory(flags),
60
60
  spelling: {
@@ -1,5 +1,6 @@
1
1
  import { meta as taskMeta } from "./task/meta.js";
2
2
  import { meta as addMeta } from "./add/meta.js";
3
+ import { meta as composeMeta } from "./compose/meta.js";
3
4
  import { meta as showMeta } from "./show/meta.js";
4
5
  import { meta as lsMeta } from "./ls/meta.js";
5
6
  import { meta as logMeta } from "./log/meta.js";
@@ -14,6 +15,7 @@ import { meta as doctorMeta } from "./doctor/meta.js";
14
15
  export const taskCatalog = [
15
16
  { meta: taskMeta, load: () => import("./task/handler.js") },
16
17
  { meta: addMeta, load: () => import("./add/handler.js") },
18
+ { meta: composeMeta, load: () => import("./compose/handler.js") },
17
19
  { meta: showMeta, load: () => import("./show/handler.js") },
18
20
  { meta: lsMeta, load: () => import("./ls/handler.js") },
19
21
  { meta: logMeta, load: () => import("./log/handler.js") },
@@ -0,0 +1,18 @@
1
+ import { composeTasks, openTaskMutationContext, planTaskComposition, resolveTaskCreator, } from "../../../../core/task/index.js";
2
+ import { renderTaskComposition, renderTaskCompositionJson } from "../../../render/task.js";
3
+ import { textResponse } from "../../../render/shared.js";
4
+ import { effectiveDirectory } from "../../shared.js";
5
+ export const handler = async (stdin, flags, signal) => {
6
+ const cwd = effectiveDirectory(flags);
7
+ const context = await openTaskMutationContext(cwd, signal);
8
+ const input = {
9
+ sketch: stdin,
10
+ creator: await resolveTaskCreator(cwd),
11
+ namespace: context.namespace,
12
+ now: () => new Date(),
13
+ };
14
+ const plan = flags.check
15
+ ? planTaskComposition(await context.repository.read(), input)
16
+ : await composeTasks(context.repository, input);
17
+ return textResponse(flags.json ? renderTaskCompositionJson(plan, Boolean(flags.check)) : renderTaskComposition(plan, Boolean(flags.check)));
18
+ };
@@ -0,0 +1,10 @@
1
+ export const meta = {
2
+ command: "task compose",
3
+ summary: "Atomically compose tasks from a tree sketch",
4
+ usage: ["keiyaku task compose [-C|--cwd DIR] [--check] - < sketch.md"],
5
+ stdin: { mode: "document", selector: { kind: "positional", index: 0, required: true, rejectBlank: true } },
6
+ flags: ["cwd", "check", "json"],
7
+ purpose: "Atomically create and rewire tasks from one validated tree sketch.",
8
+ input: ["Each task line is two-space indented + title or @COORDINATE, followed by whitespace-separated parent=, needs=@a,@b, supersedes=@c, or relates=@d declarations. Bare prose is allowed only in a nested + task body."],
9
+ notes: ["Nesting assigns parent. Top-level references preserve their parent; parent= clears it. Omitted relation declarations preserve their current complete set, while an explicit empty declaration clears that set."],
10
+ };
@@ -103,7 +103,7 @@ export const FLAG_SPECS = {
103
103
  scope: {
104
104
  kind: "value",
105
105
  placeholder: "GLOB",
106
- description: "Set the single scope pattern for flag-generated bind Markdown.",
106
+ description: "Add an ordered scope pattern to flag-generated bind Markdown. May be repeated.",
107
107
  },
108
108
  criteria: {
109
109
  kind: "value",
@@ -138,6 +138,10 @@ export const FLAG_SPECS = {
138
138
  kind: "boolean",
139
139
  description: "Preview bind input or petition preflight without mutation.",
140
140
  },
141
+ check: {
142
+ kind: "boolean",
143
+ description: "Derive and render the task composition without changing the board or Git state.",
144
+ },
141
145
  "append-scope": {
142
146
  kind: "value",
143
147
  placeholder: "PATTERN",
@@ -37,10 +37,10 @@ function prepareBindInput(stdin, flags, selectsStdin) {
37
37
  const nonblankStdin = stdin.trim().length > 0;
38
38
  const domainFlags = hasBindDomainFlags(flags);
39
39
  if (flags.dryRun && flags.draftOnly) {
40
- throw new FlowError("EMPTY_PARAM", "--dry-run cannot be combined with --draft-only");
40
+ throw new FlowError("INVALID_USAGE", "--dry-run cannot be combined with --draft-only");
41
41
  }
42
42
  if (selectsStdin && domainFlags) {
43
- throw new FlowError("EMPTY_PARAM", "bind accepts either stdin Markdown or domain flags, not both");
43
+ throw new FlowError("INVALID_USAGE", "bind accepts either stdin Markdown or domain flags, not both");
44
44
  }
45
45
  if (domainFlags) {
46
46
  const missing = [];
@@ -74,7 +74,10 @@ function prepareBindInput(stdin, flags, selectsStdin) {
74
74
  if (flags.bindTaskIds !== undefined && flags.bindTaskIds.length > 0) {
75
75
  return "";
76
76
  }
77
- throw new FlowError("EMPTY_PARAM", `No Markdown input received on stdin.\n\nExample:\n ${usageExample("bind")}`);
77
+ const message = selectsStdin
78
+ ? `No Markdown input received on stdin.\n\nExample:\n ${usageExample("bind")}`
79
+ : "bind requires literal '-' for Markdown input or the required domain flags";
80
+ throw new FlowError(selectsStdin ? "STDIN_EMPTY" : "EMPTY_PARAM", message);
78
81
  }
79
82
  return stdin;
80
83
  }
@@ -218,7 +221,7 @@ async function resolveCliInput(parsed) {
218
221
  const token = selected?.token;
219
222
  const selectsStdin = token === "-";
220
223
  if (selectsStdin && process.stdin.isTTY) {
221
- throw new FlowError("EMPTY_PARAM", `${parsed.command} literal '-' requires piped stdin`);
224
+ throw new FlowError("INVALID_USAGE", `${parsed.command} literal '-' requires piped stdin`);
222
225
  }
223
226
  const piped = selectsStdin ? await readStdin() : "";
224
227
  const input = selectsStdin ? piped : token ?? "";
@@ -231,8 +234,10 @@ async function resolveCliInput(parsed) {
231
234
  if (token !== undefined && stdinMeta.selector?.rejectBlank && !input.trim()) {
232
235
  const message = stdinMeta.mode === "document"
233
236
  ? `No Markdown input received on stdin.\n\nExample:\n ${usageExample(parsed.command)}`
234
- : `${parsed.command} body cannot be empty`;
235
- throw new FlowError("EMPTY_PARAM", message);
237
+ : selectsStdin
238
+ ? `${parsed.command} received no body on stdin`
239
+ : `${parsed.command} body cannot be empty`;
240
+ throw new FlowError(selectsStdin ? "STDIN_EMPTY" : "EMPTY_PARAM", message);
236
241
  }
237
242
  return { stdin: input, flags: selected?.replace(input) ?? parsed.flags, selectsStdin };
238
243
  }
@@ -11,7 +11,7 @@ export function parseFlagName(raw) {
11
11
  }
12
12
  export function assertAllowedFlag(command, name) {
13
13
  if (!allowedFlagsForParse(command).includes(name)) {
14
- throw new FlowError("EMPTY_PARAM", `option --${name} is not valid for ${command}`);
14
+ throw new FlowError("UNKNOWN_OPTION", `option --${name} is not valid for ${command}`);
15
15
  }
16
16
  }
17
17
  export function assignFlag(flags, name, value) {
@@ -65,43 +65,40 @@ export function assignFlag(flags, name, value) {
65
65
  return;
66
66
  case "place":
67
67
  if (flags.bindPlace !== undefined) {
68
- throw new FlowError("EMPTY_PARAM", "option --place may only be specified once");
68
+ throw new FlowError("INVALID_USAGE", "option --place may only be specified once");
69
69
  }
70
70
  flags.bindPlace = String(value);
71
71
  return;
72
72
  case "objective":
73
73
  if (flags.bindObjective !== undefined) {
74
- throw new FlowError("EMPTY_PARAM", "option --objective may only be specified once");
74
+ throw new FlowError("INVALID_USAGE", "option --objective may only be specified once");
75
75
  }
76
76
  flags.bindObjective = String(value);
77
77
  return;
78
78
  case "context":
79
79
  if (flags.bindContext !== undefined) {
80
- throw new FlowError("EMPTY_PARAM", "option --context may only be specified once");
80
+ throw new FlowError("INVALID_USAGE", "option --context may only be specified once");
81
81
  }
82
82
  flags.bindContext = String(value);
83
83
  return;
84
84
  case "design":
85
85
  if (flags.bindDesign !== undefined) {
86
- throw new FlowError("EMPTY_PARAM", "option --design may only be specified once");
86
+ throw new FlowError("INVALID_USAGE", "option --design may only be specified once");
87
87
  }
88
88
  flags.bindDesign = String(value);
89
89
  return;
90
90
  case "scope":
91
- if (flags.bindScope !== undefined) {
92
- throw new FlowError("EMPTY_PARAM", "option --scope may only be specified once");
93
- }
94
- flags.bindScope = String(value);
91
+ flags.bindScope = [...(flags.bindScope ?? []), String(value)];
95
92
  return;
96
93
  case "criteria":
97
94
  if (flags.bindCriteria !== undefined) {
98
- throw new FlowError("EMPTY_PARAM", "option --criteria may only be specified once");
95
+ throw new FlowError("INVALID_USAGE", "option --criteria may only be specified once");
99
96
  }
100
97
  flags.bindCriteria = String(value);
101
98
  return;
102
99
  case "verification":
103
100
  if (flags.bindVerification !== undefined) {
104
- throw new FlowError("EMPTY_PARAM", "option --verification may only be specified once");
101
+ throw new FlowError("INVALID_USAGE", "option --verification may only be specified once");
105
102
  }
106
103
  flags.bindVerification = String(value);
107
104
  return;
@@ -120,6 +117,9 @@ export function assignFlag(flags, name, value) {
120
117
  case "dry-run":
121
118
  flags.dryRun = Boolean(value);
122
119
  return;
120
+ case "check":
121
+ flags.check = Boolean(value);
122
+ return;
123
123
  case "append-scope":
124
124
  flags.appendScope = [...(flags.appendScope ?? []), String(value)];
125
125
  return;
@@ -152,7 +152,7 @@ export function assignFlag(flags, name, value) {
152
152
  return;
153
153
  case "target":
154
154
  if (flags.target !== undefined) {
155
- throw new FlowError("EMPTY_PARAM", "option --target may only be specified once");
155
+ throw new FlowError("INVALID_USAGE", "option --target may only be specified once");
156
156
  }
157
157
  flags.target = String(value);
158
158
  return;
@@ -162,7 +162,7 @@ export function assignFlag(flags, name, value) {
162
162
  case "diff-budget": {
163
163
  const parsed = Number(String(value));
164
164
  if (!Number.isSafeInteger(parsed) || parsed < 0) {
165
- throw new FlowError("EMPTY_PARAM", "option --diff-budget requires a non-negative safe integer byte count");
165
+ throw new FlowError("INVALID_USAGE", "option --diff-budget requires a non-negative safe integer byte count");
166
166
  }
167
167
  flags.auditDiffBudgetBytes = parsed;
168
168
  return;
@@ -175,17 +175,17 @@ export function assignFlag(flags, name, value) {
175
175
  return;
176
176
  case "sort":
177
177
  if (flags.taskSort !== undefined) {
178
- throw new FlowError("EMPTY_PARAM", "option --sort may only be specified once");
178
+ throw new FlowError("INVALID_USAGE", "option --sort may only be specified once");
179
179
  }
180
180
  flags.taskSort = String(value);
181
181
  return;
182
182
  case "pri": {
183
183
  const raw = String(value);
184
184
  if (!/^[0-3]$/.test(raw)) {
185
- throw new FlowError("EMPTY_PARAM", "task priority must be an integer from 0 through 3");
185
+ throw new FlowError("INVALID_USAGE", "task priority must be an integer from 0 through 3");
186
186
  }
187
187
  if (flags.taskPri !== undefined) {
188
- throw new FlowError("EMPTY_PARAM", "option --pri may only be specified once");
188
+ throw new FlowError("INVALID_USAGE", "option --pri may only be specified once");
189
189
  }
190
190
  flags.taskPri = Number(raw);
191
191
  return;
@@ -213,7 +213,7 @@ export function assignFlag(flags, name, value) {
213
213
  return;
214
214
  case "parent":
215
215
  if (flags.taskParent !== undefined) {
216
- throw new FlowError("EMPTY_PARAM", "option --parent may only be specified once");
216
+ throw new FlowError("INVALID_USAGE", "option --parent may only be specified once");
217
217
  }
218
218
  flags.taskParent = String(value);
219
219
  return;
@@ -222,24 +222,24 @@ export function assignFlag(flags, name, value) {
222
222
  return;
223
223
  case "title":
224
224
  if (flags.taskTitle !== undefined) {
225
- throw new FlowError("EMPTY_PARAM", "option --title may only be specified once");
225
+ throw new FlowError("INVALID_USAGE", "option --title may only be specified once");
226
226
  }
227
227
  flags.taskTitle = String(value);
228
228
  return;
229
229
  case "body":
230
230
  if (flags.taskBody !== undefined) {
231
- throw new FlowError("EMPTY_PARAM", "option --body may only be specified once");
231
+ throw new FlowError("INVALID_USAGE", "option --body may only be specified once");
232
232
  }
233
233
  flags.taskBody = String(value);
234
234
  return;
235
235
  case "append":
236
236
  if (flags.taskAppend !== undefined) {
237
- throw new FlowError("EMPTY_PARAM", "option --append may only be specified once");
237
+ throw new FlowError("INVALID_USAGE", "option --append may only be specified once");
238
238
  }
239
239
  flags.taskAppend = String(value);
240
240
  return;
241
241
  default:
242
- throw new FlowError("EMPTY_PARAM", `unknown option: --${name}`);
242
+ throw new FlowError("UNKNOWN_OPTION", `unknown option: --${name}`);
243
243
  }
244
244
  }
245
245
  export function readValueFlag(name, token, eq, tokens, index) {
@@ -45,13 +45,13 @@ export function isPetitionDryRunInvocation(tokens) {
45
45
  export function parseWaitTimeout(value) {
46
46
  const matched = /^(?<amount>[1-9][0-9]*)(?<unit>[smh])$/.exec(value);
47
47
  if (!matched?.groups) {
48
- throw new FlowError("EMPTY_PARAM", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
48
+ throw new FlowError("INVALID_USAGE", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
49
49
  }
50
50
  const amount = Number(matched.groups.amount);
51
51
  const multiplier = matched.groups.unit === "s" ? 1_000 : matched.groups.unit === "m" ? 60_000 : 3_600_000;
52
52
  const timeoutMs = amount * multiplier;
53
53
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs > 24 * 3_600_000) {
54
- throw new FlowError("EMPTY_PARAM", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
54
+ throw new FlowError("INVALID_USAGE", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
55
55
  }
56
56
  return timeoutMs;
57
57
  }
@@ -86,6 +86,9 @@ export function commandPositionalRange(command) {
86
86
  if (command === "task add") {
87
87
  return { min: 1, max: 1, label: "<BODY|->" };
88
88
  }
89
+ if (command === "task compose") {
90
+ return { min: 0, max: 0 };
91
+ }
89
92
  if (command === "task show" || command === "task log" || command === "task start" || command === "task hold" || command === "task resume" || command === "task drop") {
90
93
  return { min: 1, max: 1, label: "<ID>" };
91
94
  }
@@ -26,13 +26,13 @@ function formatSelectorList(items) {
26
26
  }
27
27
  export function applyCommissionSelector(command, flags, atTokens, contractFlagRaws) {
28
28
  if (atTokens.length > 0 && contractFlagRaws.length > 0) {
29
- throw new FlowError("EMPTY_PARAM", `commission address supplied twice: ${atTokens[0]} and --contract ${contractFlagRaws[0]}; use exactly one form`);
29
+ throw new FlowError("INVALID_USAGE", `commission address supplied twice: ${atTokens[0]} and --contract ${contractFlagRaws[0]}; use exactly one form`);
30
30
  }
31
31
  if (atTokens.length > 1) {
32
- throw new FlowError("EMPTY_PARAM", `${command} accepts one commission address; got ${formatSelectorList(atTokens)}`);
32
+ throw new FlowError("INVALID_USAGE", `${command} accepts one commission address; got ${formatSelectorList(atTokens)}`);
33
33
  }
34
34
  if (contractFlagRaws.length > 1) {
35
- throw new FlowError("EMPTY_PARAM", `${command} accepts one commission address; got ${formatSelectorList(contractFlagRaws.map((raw) => `--contract ${raw}`))}`);
35
+ throw new FlowError("INVALID_USAGE", `${command} accepts one commission address; got ${formatSelectorList(contractFlagRaws.map((raw) => `--contract ${raw}`))}`);
36
36
  }
37
37
  if (atTokens.length === 1) {
38
38
  flags.contractId = normalizeContractAddress(atTokens[0]);
@@ -46,7 +46,7 @@ export function assertCommissionSelectorAllowed(command, flags) {
46
46
  if (flags.contractId === undefined || COMMISSION_SELECTOR_COMMANDS.has(command)) {
47
47
  return;
48
48
  }
49
- throw new FlowError("EMPTY_PARAM", `contract address @${flags.contractId} is not valid for ${command}`);
49
+ throw new FlowError("INVALID_USAGE", `contract address @${flags.contractId} is not valid for ${command}`);
50
50
  }
51
51
  export function assertProjectionSelectorPresent(command, flags, atTokens, contractFlagRaws, restLength, payloadLength) {
52
52
  if ((command !== "wait" && command !== "tell") || flags.contractId === undefined) {
@@ -31,7 +31,7 @@ export function parseCliArgs(tokens) {
31
31
  const short = token.slice(1);
32
32
  const name = SHORT_FLAG_ALIASES[short];
33
33
  if (!name) {
34
- throw new FlowError("EMPTY_PARAM", `unknown option: ${token}`);
34
+ throw new FlowError("UNKNOWN_OPTION", `unknown option: ${token}`);
35
35
  }
36
36
  seenFlagNames.add(name);
37
37
  if (VALUE_FLAGS.has(name)) {
@@ -52,7 +52,7 @@ export function parseCliArgs(tokens) {
52
52
  assignFlag(flags, name, true);
53
53
  continue;
54
54
  }
55
- throw new FlowError("EMPTY_PARAM", `unknown option: ${token}`);
55
+ throw new FlowError("UNKNOWN_OPTION", `unknown option: ${token}`);
56
56
  }
57
57
  if (!token.startsWith("--")) {
58
58
  free.push(token);
@@ -61,7 +61,7 @@ export function parseCliArgs(tokens) {
61
61
  const eq = token.indexOf("=");
62
62
  const name = parseFlagName(eq === -1 ? token : token.slice(0, eq));
63
63
  if (!CONTROL_FLAGS.has(name)) {
64
- throw new FlowError("EMPTY_PARAM", `unknown option: --${name}`);
64
+ throw new FlowError("UNKNOWN_OPTION", `unknown option: --${name}`);
65
65
  }
66
66
  seenFlagNames.add(name);
67
67
  if (VALUE_FLAGS.has(name)) {
@@ -77,7 +77,7 @@ export function parseCliArgs(tokens) {
77
77
  }
78
78
  if (BOOLEAN_FLAGS.has(name)) {
79
79
  if (eq !== -1) {
80
- throw new FlowError("EMPTY_PARAM", `option --${name} does not accept a value`);
80
+ throw new FlowError("INVALID_USAGE", `option --${name} does not accept a value`);
81
81
  }
82
82
  assignFlag(flags, name, true);
83
83
  }
@@ -113,9 +113,9 @@ export function parseCliArgs(tokens) {
113
113
  }
114
114
  if (seenFlagNames.has("place")) {
115
115
  if (command === "bind") {
116
- throw new FlowError("EMPTY_PARAM", "bind no longer accepts --place; use --title <text>");
116
+ throw new FlowError("UNKNOWN_OPTION", "bind no longer accepts --place; use --title <text>");
117
117
  }
118
- throw new FlowError("EMPTY_PARAM", "unknown option --place; use @addr or --contract <addr>");
118
+ throw new FlowError("UNKNOWN_OPTION", "unknown option --place; use @addr or --contract <addr>");
119
119
  }
120
120
  for (const name of seenFlagNames) {
121
121
  assertAllowedFlag(command, name);
@@ -125,19 +125,19 @@ export function parseCliArgs(tokens) {
125
125
  delete flags.taskTitle;
126
126
  }
127
127
  if (command === "call" && flags.detach && flags.timeoutMs !== undefined) {
128
- throw new FlowError("EMPTY_PARAM", "call cannot combine --detach and --wait");
128
+ throw new FlowError("INVALID_USAGE", "call cannot combine --detach and --wait");
129
129
  }
130
130
  if (command === "task ls" && flags.all && flags.done) {
131
- throw new FlowError("EMPTY_PARAM", "task ls cannot combine --all and --done");
131
+ throw new FlowError("INVALID_USAGE", "task ls cannot combine --all and --done");
132
132
  }
133
133
  if (command === "wait" && flags.any && flags.all) {
134
- throw new FlowError("EMPTY_PARAM", WAIT_SET_SELECTION_ERROR);
134
+ throw new FlowError("INVALID_USAGE", WAIT_SET_SELECTION_ERROR);
135
135
  }
136
136
  if (command === "task update" && flags.taskParent !== undefined && flags.taskNoParent) {
137
- throw new FlowError("EMPTY_PARAM", "task update cannot combine --parent and --no-parent");
137
+ throw new FlowError("INVALID_USAGE", "task update cannot combine --parent and --no-parent");
138
138
  }
139
139
  if (command === "task update" && flags.taskBody !== undefined && flags.taskAppend !== undefined) {
140
- throw new FlowError("EMPTY_PARAM", "task update cannot combine --body and --append");
140
+ throw new FlowError("INVALID_USAGE", "task update cannot combine --body and --append");
141
141
  }
142
142
  if (command === "task update" && flags.taskTitle === undefined && flags.taskBody === undefined && flags.taskAppend === undefined
143
143
  && flags.taskPri === undefined && flags.taskNeeds === undefined && flags.taskDropNeeds === undefined
@@ -147,18 +147,18 @@ export function parseCliArgs(tokens) {
147
147
  throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
148
148
  }
149
149
  if (command === "petition" && flags.json && !flags.dryRun) {
150
- throw new FlowError("EMPTY_PARAM", "petition --json requires --dry-run");
150
+ throw new FlowError("INVALID_USAGE", "petition --json requires --dry-run");
151
151
  }
152
152
  if (command === "petition" && flags.dryRun && (flags.appendScope?.length ?? 0) > 0) {
153
- throw new FlowError("EMPTY_PARAM", "petition --dry-run does not accept --append-scope");
153
+ throw new FlowError("INVALID_USAGE", "petition --dry-run does not accept --append-scope");
154
154
  }
155
155
  if (command === "renew") {
156
156
  const recoveryModes = [flags.renewContinue, flags.renewAbort].filter(Boolean).length;
157
157
  if (recoveryModes > 1) {
158
- throw new FlowError("EMPTY_PARAM", "renew accepts only one of --continue or --abort");
158
+ throw new FlowError("INVALID_USAGE", "renew accepts only one of --continue or --abort");
159
159
  }
160
160
  if (flags.renewAbort && (flags.appendScope?.length ?? 0) > 0) {
161
- throw new FlowError("EMPTY_PARAM", "renew --abort does not accept --append-scope");
161
+ throw new FlowError("INVALID_USAGE", "renew --abort does not accept --append-scope");
162
162
  }
163
163
  }
164
164
  assertCommissionSelectorAllowed(command, flags);
@@ -166,7 +166,7 @@ export function parseCliArgs(tokens) {
166
166
  const positional = [...rest, ...payload];
167
167
  if (command === "bind" && flags.contractId !== undefined) {
168
168
  if (!isCommissionId(flags.contractId)) {
169
- throw new FlowError("EMPTY_PARAM", `bind replay requires a full canonical contract id; got ${flags.contractId}`);
169
+ throw new FlowError("UNKNOWN_ADDRESS", `bind replay requires a full canonical contract id; got ${flags.contractId}`);
170
170
  }
171
171
  const hasReplayInput = positional.length > 0
172
172
  || flags.bindTaskIds !== undefined
@@ -182,7 +182,7 @@ export function parseCliArgs(tokens) {
182
182
  || flags.bindCriteria !== undefined
183
183
  || flags.bindVerification !== undefined;
184
184
  if (hasReplayInput) {
185
- throw new FlowError("EMPTY_PARAM", "addressed bind is replay-only and accepts no draft or creation input");
185
+ throw new FlowError("INVALID_USAGE", "addressed bind is replay-only and accepts no draft or creation input");
186
186
  }
187
187
  }
188
188
  if (command === "wait") {
@@ -190,13 +190,13 @@ export function parseCliArgs(tokens) {
190
190
  throw new FlowError("EMPTY_PARAM", "wait requires <projection-address>");
191
191
  }
192
192
  if (positional.length > 1 && !flags.any && !flags.all) {
193
- throw new FlowError("EMPTY_PARAM", WAIT_SET_SELECTION_ERROR);
193
+ throw new FlowError("INVALID_USAGE", WAIT_SET_SELECTION_ERROR);
194
194
  }
195
195
  }
196
196
  if (isCallCommand(command)) {
197
197
  if (flags.akuma) {
198
198
  if (positional.length > 1) {
199
- throw new FlowError("EMPTY_PARAM", `${command} accepts at most one <BODY|-> when --akuma is used`);
199
+ throw new FlowError("INVALID_USAGE", `${command} accepts at most one <BODY|-> when --akuma is used`);
200
200
  }
201
201
  }
202
202
  else {
@@ -205,16 +205,16 @@ export function parseCliArgs(tokens) {
205
205
  throw new FlowError("EMPTY_PARAM", `${command} requires an agent name; use \`keiyaku ${command} NAME <BODY|->\``);
206
206
  }
207
207
  if (isResponseArtifactId(name)) {
208
- throw new FlowError("EMPTY_PARAM", `artifact ${name} is a response artifact; call expects an Akuma profile name`);
208
+ throw new FlowError("UNKNOWN_ADDRESS", `artifact ${name} is a response artifact; call expects an Akuma profile name`);
209
209
  }
210
210
  const parsedName = agentNameSchema.safeParse(name);
211
211
  if (!parsedName.success) {
212
- throw new FlowError("EMPTY_PARAM", `${command} requires a valid agent name before <BODY|->`);
212
+ throw new FlowError("UNKNOWN_ADDRESS", `${command} requires a valid agent name before <BODY|->`);
213
213
  }
214
214
  flags.akuma = parsedName.data;
215
215
  positional.shift();
216
216
  if (positional.length > 1) {
217
- throw new FlowError("EMPTY_PARAM", `${command} accepts at most one <BODY|-> after the agent name`);
217
+ throw new FlowError("INVALID_USAGE", `${command} accepts at most one <BODY|-> after the agent name`);
218
218
  }
219
219
  }
220
220
  }
@@ -231,7 +231,7 @@ export function parseCliArgs(tokens) {
231
231
  const message = stdin.mode === "document"
232
232
  ? `${command} reads Markdown only through literal '-'; file path arguments are not supported`
233
233
  : `${command} does not accept positional arguments`;
234
- throw new FlowError("EMPTY_PARAM", message);
234
+ throw new FlowError("INVALID_USAGE", message);
235
235
  }
236
236
  return { command, flags, positional };
237
237
  }