@astrosheep/keiyaku 4.6.1 → 4.6.3

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 (55) hide show
  1. package/README.md +93 -45
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +3 -2
  3. package/build/src/akuma/providers/pi/events.js +3 -2
  4. package/build/src/akuma/providers/pi/index.d.ts +3 -4
  5. package/build/src/akuma/providers/pi/index.js +30 -26
  6. package/build/src/body/render.d.ts +9 -1
  7. package/build/src/body/render.js +11 -0
  8. package/build/src/cli/commands/akuma-grammar.js +12 -3
  9. package/build/src/cli/commands/akuma.js +6 -0
  10. package/build/src/cli/commands/contract-grammar.js +7 -0
  11. package/build/src/cli/commands/contract-help.d.ts +1 -1
  12. package/build/src/cli/commands/contract-help.js +21 -9
  13. package/build/src/cli/commands/contract.js +3 -1
  14. package/build/src/cli/commands/task-grammar.js +9 -7
  15. package/build/src/cli/parse.js +5 -0
  16. package/build/src/cli/render/akuma-activity.d.ts +26 -6
  17. package/build/src/cli/render/akuma-activity.js +128 -118
  18. package/build/src/cli/render/akuma-tool.js +1 -1
  19. package/build/src/cli/render/audit.js +4 -6
  20. package/build/src/cli/render/catalog.js +7 -4
  21. package/build/src/cli/render/contract-history.js +8 -18
  22. package/build/src/cli/render/contract.js +5 -10
  23. package/build/src/cli/render/kanshi-akuma.js +1 -1
  24. package/build/src/cli/render/kanshi.js +24 -30
  25. package/build/src/cli/render/receipt.d.ts +1 -2
  26. package/build/src/cli/render/receipt.js +1 -6
  27. package/build/src/cli/render/refusal.js +2 -1
  28. package/build/src/cli/render/task.js +2 -1
  29. package/build/src/core/facts/codec.js +0 -6
  30. package/build/src/core/facts/fold.js +0 -7
  31. package/build/src/core/facts/types.d.ts +1 -4
  32. package/build/src/core/verbs/deliver.js +2 -14
  33. package/build/src/library/bind.d.ts +20 -20
  34. package/build/src/library/bind.js +2 -1
  35. package/build/src/library/contract-composition.d.ts +5 -2
  36. package/build/src/library/contract-composition.js +5 -3
  37. package/build/src/library/contract-creation.d.ts +7 -1
  38. package/build/src/library/contract-creation.js +19 -9
  39. package/build/src/library/contract-execution.d.ts +1 -0
  40. package/build/src/library/contract-execution.js +12 -4
  41. package/build/src/library/contract-settings.d.ts +2 -2
  42. package/build/src/library/contract-settings.js +21 -12
  43. package/build/src/library/outcome.d.ts +476 -652
  44. package/build/src/library/refusal.d.ts +2 -2
  45. package/build/src/protocol/bind.d.ts +1 -1
  46. package/build/src/protocol/deliver.js +1 -1
  47. package/build/src/protocol/execution-observation.d.ts +0 -8
  48. package/build/src/protocol/intent.js +2 -2
  49. package/build/src/protocol/operations.d.ts +1 -1
  50. package/build/src/protocol/read/status.d.ts +1 -1
  51. package/build/src/protocol/read/status.js +0 -2
  52. package/build/src/runtime/proc/windows-launch.exe +0 -0
  53. package/build/src/world.d.ts +1 -0
  54. package/build/src/world.js +14 -0
  55. package/package.json +3 -3
package/README.md CHANGED
@@ -52,7 +52,7 @@ The CLI parses a predicate; the evaluator never sees a raw shell string.
52
52
  Query reads only Task facts. No Contract. No Akuma.
53
53
 
54
54
  ## Verification
55
- ```bash
55
+ ```bash timeout=2m
56
56
  npm test
57
57
  ```
58
58
  ```
@@ -60,60 +60,60 @@ npm test
60
60
  ## What a deal looks like
61
61
 
62
62
  ```bash
63
- keiyaku bind - < keiyaku.md # terms written; an isolated worktree appears
64
- keiyaku call worker - # a worker goes in
65
- keiyaku deliver # tendered; Verification runs; gates judge
66
- keiyaku review --satisfied # attested; main moves with a commit receipt
63
+ keiyaku bind - < contract.md # terms written; an isolated worktree appears
64
+ keiyaku call worker - # a worker goes in
65
+ keiyaku deliver <contract> # delivered; Verification runs; gates judge
66
+ keiyaku review <contract> --satisfied --summary - # attested; main moves with a commit receipt
67
67
  ```
68
68
 
69
69
  ## What the journal records
70
70
 
71
- One real deal, pulled from this repo's own journal:
71
+ One real deal, read back with `keiyaku history`:
72
72
 
73
73
  ```text
74
- $ keiyaku audit kei/add-acp-provider-and-grok-build-profile
75
-
76
- accepted audit kei/add-acp-provider-and-grok-build-profile head=0508f7eb9cb738bcab06060b13ff1a96d36e3754
77
- report {"reworks":1,"reviews":2,"timeline":[
78
- {"kind":"bind","at":"2026-08-14T08:18:36.224Z"},
79
- {"kind":"deliver","at":"2026-08-14T13:02:08.339Z"},
80
- {"kind":"attestation","gate":"reviewed","verdict":"unsatisfied","summary":
81
- "Implementation review found no blocking code issues; authenticated official
82
- Grok smoke remains unavailable because this host has no grok binary."},
83
- {"kind":"attestation","gate":"verified","verdict":"satisfied","summary":"[1 bash exit 0] …"}]}
74
+ history kei/ship-typed-task-query-b186 · accepted · 9 entries
75
+
76
+ 2026-10-04
77
+
78
+ 16:53 bound to targetless @ 3c5d8c0 · gate review
79
+ 16:53 delivered 210eeed · ✓ verification · 10 lines
80
+ 16:53 × review · 6 lines
81
+ 16:53 delivered aebef87 · ✓ verification · 10 lines
82
+ 16:53 ✓ review · 6 lines
83
+ 16:53 accepted
84
84
  ```
85
85
 
86
- The review said no — with a reason, in bytes, on the journal. The deal
87
- did not land until the gates had current evidence.
86
+ The review said no, and the deal did not land. The verdict and its reason
87
+ stay on the journal as bytes — `keiyaku history --json` reads them:
88
+
89
+ ```json
90
+ {
91
+ "kind": "attestation",
92
+ "contract": "kei/ship-typed-task-query-b186",
93
+ "data": {
94
+ "gate": "reviewed",
95
+ "verdict": "unsatisfied",
96
+ "summary": "Query evaluator still reaches for Contract facts; reads are not Task-owned."
97
+ }
98
+ }
99
+ ```
88
100
 
89
101
  ## The board
90
102
 
91
- `keiyaku status` is the one screen. A slice of this repository, right now:
103
+ `keiyaku status` is the one screen. Two Contracts and two Tasks in flight:
92
104
 
93
105
  ```text
94
- kanshi ─ 7 keiyaku · 18 akuma · 286 task ─ /Users/astrosheep/Developer/keiyaku-v4 main 9cfdca6017633e51827b9b2eba3c76a7fe08e05f
95
-
96
- keiyaku 7
97
- ! kei/add-acp-provider-and-grok-build-profile tendered
98
- worktree · integration c5cafef6 · -> refs/heads/main
99
- × reviewed
100
- ⧗ kei/align-task-cli-truth-promises waiting
101
- ! reviewed
102
- held by task/align-task-cli-truth-promises-for-ready-compose
103
-
104
- akuma 18
105
- ● aku/expert-akuma/a7aafc9e running
106
- alias @process-custody-lead
107
- keiyaku kei/make-process-custody-capability-honest (active)
108
- ○ aku/design-akuma/cc53ef08 asleep
109
- alias @timeline-design
110
- ! aku/grok/95d90b7d stranded
111
- alias @acp-provider-impl
112
-
113
- task 8 · 5 ready · 2 held
114
- ● task/align-task-cli-truth-promises-for-ready-compose in_progress
115
- Align Task CLI truth promises for ready compose and world absence
116
- P0 · keiyaku kei/align-task-cli-truth-promises (active)
106
+ CONTRACTS // recent
107
+
108
+ ⧗ kei/ship-typed-task-query-c898 · 1s · Ship typed Task query
109
+ [✓] delivery [ ] review
110
+ ⧗ kei/tighten-receipt-vocabulary-4a70 · 18s · Tighten receipt vocabulary
111
+ [ ] delivery [ ] review
112
+
113
+ TASKS // recent
114
+
115
+ ○ task/regenerate-readme-from-real-010b · ready · P1 · Regenerate README from real output
116
+ ○ task/align-task-cli-truth-promises-3af5 · ready · P0 · Align Task CLI truth promises
117
117
  ```
118
118
 
119
119
  Marks accelerate scanning; the words carry the state.
@@ -122,9 +122,8 @@ Marks accelerate scanning; the words carry the state.
122
122
 
123
123
  ```markdown
124
124
  ---
125
- provider: claude-agent-sdk
126
- model: claude-sonnet-4-5
127
- access: write
125
+ provider: pi
126
+ model: kimi-coding/k3-256k
128
127
  description: Repository implementation agent
129
128
  ---
130
129
  Make scoped changes and run relevant tests.
@@ -132,6 +131,55 @@ Make scoped changes and run relevant tests.
132
131
 
133
132
  One Markdown file, one worker. `keiyaku call worker` summons it.
134
133
 
134
+ ## Configuration
135
+
136
+ Settings are JSON at two addresses: `~/.keiyaku/settings.json` for the user,
137
+ `.keiyaku/settings.json` in the project. A project record
138
+ wholly shadows the same-name user record. There is no write command; edit the
139
+ files directly and inspect the merged, provenance-annotated view with
140
+ `keiyaku settings`.
141
+
142
+ Named gate groups provide reusable gate sets. Bind and amend select them with
143
+ `--gates`; the `default` group applies when binding with `--gates` omitted.
144
+ Amending without `--gates` keeps the existing gates:
145
+
146
+ ```json
147
+ {
148
+ "gates": {
149
+ "default": { "kind": "bundle", "gates": ["reviewed"] },
150
+ "strict": { "kind": "bundle", "gates": ["reviewed", "verified"] }
151
+ }
152
+ }
153
+ ```
154
+
155
+ Selections accept the built-in names `reviewed` and `verified`, or configured
156
+ group names. Unknown names are rejected with the known names listed. Custom gates
157
+ must be declared inside a configured group; they stay unsatisfied until a producer
158
+ attests them.
159
+
160
+ Worktree hooks run commands when a Contract's managed worktree is created or
161
+ destroyed — dependency installs are the usual suspect:
162
+
163
+ ```json
164
+ {
165
+ "worktree": {
166
+ "create": [
167
+ { "name": "install", "argv": ["npm", "ci", "--ignore-scripts", "--prefer-offline"], "timeoutMs": 300000 }
168
+ ],
169
+ "destroy": [
170
+ { "name": "teardown", "argv": ["docker", "compose", "down", "-v"], "timeoutMs": 60000 }
171
+ ]
172
+ }
173
+ }
174
+ ```
175
+
176
+ Hooks run as one ordered phase inside the worktree and must be replay-safe: a
177
+ retry reruns the phase from its beginning. Create hooks prepare the worktree;
178
+ destroy hooks release external resources the worktree deletion alone cannot.
179
+ A failing hook retains the worktree
180
+ and reports lag; it never abandons the Contract. `keiyaku settings --help`
181
+ lists every recognized namespace.
182
+
135
183
  ## Install
136
184
 
137
185
  ```bash
@@ -183,9 +183,10 @@ keiyaku -C <repo> bind --gates "" - < CONTRACT.md
183
183
  keiyaku -C <repo> bind --gates reviewed - < CONTRACT.md
184
184
  ```
185
185
 
186
- Omitting `--gates` uses `gates.default`, or `reviewed` when no default bundle
186
+ Omitting `--gates` uses `gates.default`, or `reviewed` when no default group
187
187
  exists. `--gates ""` selects no gates. Gates are named acceptance obligations,
188
- not work assignments.
188
+ not work assignments. Named gate groups and worktree hooks live in Settings;
189
+ see the README Configuration section or `keiyaku settings --help`.
189
190
 
190
191
  ## 3. After Binding
191
192
 
@@ -164,10 +164,11 @@ function translateMessage(event, state) {
164
164
  state.assistantSeen = true;
165
165
  state.answer = text;
166
166
  }
167
- // Gemini emits an empty text block alongside every tool-use message. It is
167
+ // Some providers emit a blank text block alongside every tool-use message:
168
+ // empty from Gemini, whitespace from openai-responses-style proxies. It is
168
169
  // transport scaffolding, not narration; an ordinary terminal empty answer
169
170
  // remains public evidence.
170
- if (hasTextContent(message.content) && !(message.stopReason === "toolUse" && text.length === 0)) {
171
+ if (hasTextContent(message.content) && !(message.stopReason === "toolUse" && text.trim().length === 0)) {
171
172
  translated.push({ type: "assistant", text });
172
173
  }
173
174
  return translated;
@@ -1,12 +1,11 @@
1
- import type { createAgentSession, createBashToolDefinition, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
1
+ import type { createAgentSessionFromServices, createAgentSessionServices, createBashToolDefinition, getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
2
2
  import type { ProviderExecution } from "../../provider-recipe.js";
3
3
  import { type ProviderAdapter } from "../../provider.js";
4
4
  export type PiSdk = Readonly<{
5
- createAgentSession(options?: CreateAgentSessionOptions): ReturnType<typeof createAgentSession>;
5
+ createAgentSessionFromServices: typeof createAgentSessionFromServices;
6
+ createAgentSessionServices: typeof createAgentSessionServices;
6
7
  createBashToolDefinition: typeof createBashToolDefinition;
7
- DefaultResourceLoader: typeof DefaultResourceLoader;
8
8
  getAgentDir: typeof getAgentDir;
9
- ModelRuntime: typeof ModelRuntime;
10
9
  SessionManager: typeof SessionManager;
11
10
  }>;
12
11
  export declare function createPiProvider(execution?: ProviderExecution, load?: () => Promise<PiSdk>): ProviderAdapter;
@@ -21,26 +21,14 @@ function admitPiOptions(options) {
21
21
  options: Object.freeze({ ...options }),
22
22
  };
23
23
  }
24
- async function piCreateOptions(sdk, input) {
24
+ function piCreateOptions(sdk, input, services) {
25
25
  let model;
26
- let modelRuntime;
27
26
  if (input.options.model !== undefined) {
28
27
  const slash = input.options.model.indexOf("/");
29
- modelRuntime = await sdk.ModelRuntime.create();
30
- model = modelRuntime.getModel(input.options.model.slice(0, slash), input.options.model.slice(slash + 1));
28
+ model = services.modelRuntime.getModel(input.options.model.slice(0, slash), input.options.model.slice(slash + 1));
31
29
  if (model === undefined)
32
30
  throw new Error(`Pi model '${input.options.model}' is unavailable`);
33
31
  }
34
- const resourceLoader = input.options.systemPrompt === undefined
35
- ? undefined
36
- : new sdk.DefaultResourceLoader({
37
- cwd: input.cwd,
38
- agentDir: sdk.getAgentDir(),
39
- ...(input.options.systemPromptMode === "append"
40
- ? { appendSystemPromptOverride: (base) => [...base, input.options.systemPrompt] }
41
- : { systemPromptOverride: () => input.options.systemPrompt }),
42
- });
43
- await resourceLoader?.reload();
44
32
  const sessionManager = input.session.kind === "fresh"
45
33
  ? sdk.SessionManager.create(input.cwd)
46
34
  : "sessionFile" in input.session.coordinate
@@ -59,22 +47,39 @@ async function piCreateOptions(sdk, input) {
59
47
  }),
60
48
  ];
61
49
  return {
62
- cwd: input.cwd,
50
+ services,
63
51
  sessionManager,
64
- ...(model === undefined || modelRuntime === undefined ? {} : { model, modelRuntime }),
65
- ...(resourceLoader === undefined ? {} : { resourceLoader }),
52
+ ...(model === undefined ? {} : { model }),
66
53
  ...(input.options.effort === undefined ? {} : { thinkingLevel: input.options.effort }),
67
54
  ...(customTools === undefined ? {} : { customTools }),
68
55
  };
69
56
  }
70
57
  async function createPiSession(sdk, execution, input) {
71
- const setup = piCreateOptions(sdk, input).then(async (options) => {
72
- if (execution.config !== undefined) {
73
- throw new TypeError("Pi provider config cannot be consumed by native CreateAgentSessionOptions");
74
- }
75
- return await sdk.createAgentSession(options);
58
+ if (execution.config !== undefined) {
59
+ throw new TypeError("Pi provider config cannot be consumed by native CreateAgentSessionOptions");
60
+ }
61
+ const services = await sdk.createAgentSessionServices({
62
+ cwd: input.cwd,
63
+ agentDir: sdk.getAgentDir(),
64
+ modelRuntimeSignal: input.signal,
65
+ ...(input.options.systemPrompt === undefined
66
+ ? {}
67
+ : {
68
+ resourceLoaderOptions: input.options.systemPromptMode === "append"
69
+ ? { appendSystemPromptOverride: (base) => [...base, input.options.systemPrompt] }
70
+ : { systemPromptOverride: () => input.options.systemPrompt },
71
+ }),
76
72
  });
77
- return await setup;
73
+ try {
74
+ input.signal?.throwIfAborted();
75
+ return await sdk.createAgentSessionFromServices(piCreateOptions(sdk, input, services));
76
+ }
77
+ catch (error) {
78
+ // Before a session owns the extension runtime, failed selection/setup must
79
+ // retire registrations and event-bus subscriptions loaded by the services.
80
+ services.resourceLoader.getExtensions().runtime.invalidate();
81
+ throw error;
82
+ }
78
83
  }
79
84
  async function runPiPrompt(native, input, events, state, settle) {
80
85
  try {
@@ -273,11 +278,10 @@ async function forkPi(sdk, input) {
273
278
  async function loadPiSdk() {
274
279
  const sdk = await import("@earendil-works/pi-coding-agent");
275
280
  return {
276
- createAgentSession: sdk.createAgentSession,
281
+ createAgentSessionFromServices: sdk.createAgentSessionFromServices,
282
+ createAgentSessionServices: sdk.createAgentSessionServices,
277
283
  createBashToolDefinition: sdk.createBashToolDefinition,
278
- DefaultResourceLoader: sdk.DefaultResourceLoader,
279
284
  getAgentDir: sdk.getAgentDir,
280
- ModelRuntime: sdk.ModelRuntime,
281
285
  SessionManager: sdk.SessionManager,
282
286
  };
283
287
  }
@@ -1,4 +1,12 @@
1
1
  import type { ArcData } from "../core/facts/types.js";
2
- import type { ContractBody } from "./types.js";
2
+ import type { ContractBody, DecodedContractDocument } from "./types.js";
3
3
  export declare function renderContractBody(body: ContractBody, currentArc?: ArcData): string;
4
+ /**
5
+ * Binding persists the canonical rendering of decoded terms, never the caller's
6
+ * literal byte stream: layout-only authorship is normalized at admission so it
7
+ * never reappears as phantom changes in later amendment diffs.
8
+ */
9
+ export declare function decodeCanonicalContractDocument(source: string, options?: Readonly<{
10
+ requireTimeout?: boolean;
11
+ }>): DecodedContractDocument;
4
12
  export declare function renderAmendedContractBody(currentSource: string, body: ContractBody, changedSections: ReadonlySet<string>): string;
@@ -1,4 +1,5 @@
1
1
  import { formatDuration } from "../duration.js";
2
+ import { decodeContractDocument } from "./decode.js";
2
3
  import { parseToAST } from "../markdown/parse.js";
3
4
  import { indexDocument, indexedHeadings, normalizeTitle, rawSlice } from "../markdown/query.js";
4
5
  import { CONTRACT_SECTIONS } from "./shape.js";
@@ -54,6 +55,16 @@ export function renderContractBody(body, currentArc) {
54
55
  .join("\n\n")
55
56
  .concat("\n");
56
57
  }
58
+ /**
59
+ * Binding persists the canonical rendering of decoded terms, never the caller's
60
+ * literal byte stream: layout-only authorship is normalized at admission so it
61
+ * never reappears as phantom changes in later amendment diffs.
62
+ */
63
+ export function decodeCanonicalContractDocument(source, options = {}) {
64
+ const decoded = decodeContractDocument(source, options);
65
+ const rendered = renderContractBody(decoded);
66
+ return rendered === decoded.document.bytes ? decoded : decodeContractDocument(rendered, options);
67
+ }
57
68
  function sections(document) {
58
69
  return indexedHeadings(indexDocument(document), { level: 2 }).filter((node) => node.type === "section");
59
70
  }
@@ -304,8 +304,10 @@ function parseAddressed(action, rawSelectors, flags, output, fail) {
304
304
  return { command: action, akuma, at, output };
305
305
  }
306
306
  function parsePrompted(action, positionals, stdin, fail) {
307
- if (positionals.length < 1 || positionals.length > 2)
308
- fail(`${action} has invalid positional arguments`);
307
+ if (positionals.length === 0)
308
+ fail(action === "call" ? "call requires an Akuma name" : `${action} requires an Akuma selector`);
309
+ if (positionals.length > 2)
310
+ fail(`${action} accepts at most ${action === "call" ? "a name" : "a selector"} and one prompt`);
309
311
  const argument = positionals[1];
310
312
  if (stdin && argument !== undefined)
311
313
  fail(`${action} accepts either a prompt argument or stdin, not both`);
@@ -406,7 +408,14 @@ export function parseAkumaCommand(argv) {
406
408
  : parseAsk(flags, parsed.subject, parsed.prompt, output, fail);
407
409
  }
408
410
  if (spec.arity === "one-or-more" ? positionals.length === 0 : positionals.length !== spec.arity) {
409
- fail(`${action} has invalid positional arguments`);
411
+ if (positionals.length === 0)
412
+ fail(spec.arity === "one-or-more"
413
+ ? `${action} requires at least one Akuma selector`
414
+ : `${action} requires an Akuma selector`);
415
+ else
416
+ fail(spec.arity === 1
417
+ ? `${action} accepts one Akuma selector`
418
+ : `${action} accepts exactly ${spec.arity} Akuma selectors`);
410
419
  }
411
420
  if (stdin)
412
421
  fail(`${action} reads no stdin`);
@@ -9,6 +9,7 @@ import { executionChannel } from "../../akuma/requests.js";
9
9
  import { settings } from "../../settings.js";
10
10
  import { AkumaWorldScopeError } from "../../index.js";
11
11
  import { akumasWithExecution } from "../../library/akumas.js";
12
+ import { World } from "../../world.js";
12
13
  import { contractFromInput } from "../selectors.js";
13
14
  import { ActivityDriver } from "../activity.js";
14
15
  import { displayContext, resultContext, writeJson, writeStderr, writeStdout } from "../streams.js";
@@ -67,6 +68,11 @@ async function inputInitiator(input) {
67
68
  catch { }
68
69
  if (initiator === undefined)
69
70
  return {};
71
+ // Plugins coordinate within an established World; a `call` may run where the
72
+ // marker does not exist yet, and activating there would only fail noisily.
73
+ // The initiator still travels with the request either way.
74
+ if (!(await World.established(input.path)))
75
+ return { initiator };
70
76
  try {
71
77
  await emitInitiatingPluginSignal({
72
78
  world: input.path,
@@ -22,6 +22,9 @@ function optionalFlag(flags, name) {
22
22
  function refuse(command, message) {
23
23
  throw new CliUsageError(message, commandGuide(command, CONTRACT_COMMAND_SPECS[command].usage));
24
24
  }
25
+ // The CLI restates the literal gate-word grammar its help prints; the corpus
26
+ // pin in cli-parse.test.ts keeps this shape honest against the owner's gateWord.
27
+ const GATE_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
25
28
  function parseGateNames(parts, command) {
26
29
  const value = optionalFlag(parts.flags, "gates");
27
30
  if (value === undefined)
@@ -32,6 +35,10 @@ function parseGateNames(parts, command) {
32
35
  if (names.some((name) => name.length === 0)) {
33
36
  refuse(command, '--gates requires comma-separated names or "" to clear gates');
34
37
  }
38
+ const invalid = names.find((name) => !GATE_NAME_PATTERN.test(name));
39
+ if (invalid !== undefined) {
40
+ refuse(command, `--gates name must match ^[a-z][a-z0-9-]{0,63}$: ${invalid}`);
41
+ }
35
42
  return names;
36
43
  }
37
44
  function parseBind(parts) {
@@ -106,7 +106,7 @@ export declare const CONTRACT_COMMAND_SPECS: {
106
106
  readonly json: "boolean";
107
107
  };
108
108
  readonly usage: "ls task[/] [--limit <count>]\nls kei[/] [--limit <count>]\nls aku[/] [--limit <count>]\nls aku/<name>[/] [--limit <count>]\nls \"aku/<name>/*\" [--limit <count>]\nls \"aku/*/*\" [--limit <count>]";
109
- readonly purpose: "List Tasks, Contracts, or Akumas.";
109
+ readonly purpose: "List tasks (ls task), contracts (ls kei), or akumas (ls aku).";
110
110
  readonly details: string;
111
111
  };
112
112
  readonly audit: {
@@ -15,11 +15,15 @@ export const CONTRACT_COMMAND_SPECS = {
15
15
  usage: "bind [--task <task/...>] [--target <ref>] [--after <kei/...> ...] [--gates <name,...>] [--actor <actor>] - | bind --fork-of <kei/...> [--target <ref>] [--actor <actor>]",
16
16
  purpose: "Create a Contract from stdin Markdown or copy an existing Contract as a starting point.",
17
17
  details: [
18
- "--gates <name,...> accepts gate words and configured bundle names together.",
19
- "A matching bundle expands; otherwise the name is a literal gate. Duplicates",
20
- "are removed in first-seen order. Gate words match ^[a-z][a-z0-9-]{0,63}$.",
18
+ "Gates are acceptance obligations checked at placement: reviewed awaits a",
19
+ "satisfied review, verified awaits a passing Verification run.",
20
+ "",
21
+ "--gates <name,...> selects reviewed, verified, or named gate groups from Settings",
22
+ "(keiyaku settings). A group expands into its gates; unknown names are rejected",
23
+ "with the known names listed. Custom gates must be declared in a configured group.",
24
+ "Duplicates are removed in first-seen order. Names match ^[a-z][a-z0-9-]{0,63}$.",
21
25
  "Omitting --gates selects gates.default, or reviewed when no default exists.",
22
- '--gates "" explicitly binds without gates; --gates reviewed needs no bundle.',
26
+ '--gates "" explicitly binds without gates; --gates reviewed needs no configuration.',
23
27
  "",
24
28
  "Binding appoints one managed worktree for the Contract and the receipt",
25
29
  "reports its path; the holder and delegates work there. When appointment",
@@ -55,9 +59,14 @@ export const CONTRACT_COMMAND_SPECS = {
55
59
  usage: "amend [<contract>|@<contract>] [--after <kei/...> ... | --clear-after] [--gates <name,...>] [--actor <actor>] [-]",
56
60
  purpose: "Change a Contract's document, prerequisites, or acceptance gates.",
57
61
  details: [
58
- "--gates <name,...> replaces gates using gate words and configured bundle names.",
59
- "A matching bundle expands; otherwise the name is a literal gate. Duplicates",
60
- "are removed in first-seen order. Gate words match ^[a-z][a-z0-9-]{0,63}$.",
62
+ "Gates are acceptance obligations checked at placement: reviewed awaits a",
63
+ "satisfied review, verified awaits a passing Verification run.",
64
+ "",
65
+ "--gates <name,...> replaces gates using reviewed, verified, or named gate groups",
66
+ "from Settings (keiyaku settings). A group expands into its gates; unknown names",
67
+ "are rejected with the known names listed. Custom gates must be declared in a",
68
+ "configured group. Duplicates are removed in first-seen order.",
69
+ "Names match ^[a-z][a-z0-9-]{0,63}$.",
61
70
  'Omitting --gates leaves gates unchanged; --gates "" clears them.',
62
71
  "",
63
72
  " ## Context|Objective|Design|Region|Criteria|Verification|<extension> (replace, or add new extension)",
@@ -88,6 +97,7 @@ export const CONTRACT_COMMAND_SPECS = {
88
97
  "Delivery requests integration. If prerequisites and gates pass, it integrates now and accepts the Contract; otherwise the candidate remains waiting.",
89
98
  "Only changed candidate work invalidates earlier review. If Verification did not finish, repeating delivery resumes that candidate; --overwrite replaces it.",
90
99
  "Delivery never counts as an independent review verdict.",
100
+ "Content identity names the change's content hash, never a commit; it survives rebase and Git cannot look it up.",
91
101
  "",
92
102
  " --materialize-conflict Write the observed integration conflict into the worktree as an uncommitted merge.",
93
103
  " With --include-dirty, current non-ignored changes are preserved first.",
@@ -147,8 +157,9 @@ export const CONTRACT_COMMAND_SPECS = {
147
157
  stdin: "none",
148
158
  flags: { limit: "value", json: "boolean" },
149
159
  usage: 'ls task[/] [--limit <count>]\nls kei[/] [--limit <count>]\nls aku[/] [--limit <count>]\nls aku/<name>[/] [--limit <count>]\nls "aku/<name>/*" [--limit <count>]\nls "aku/*/*" [--limit <count>]',
150
- purpose: "List Tasks, Contracts, or Akumas.",
160
+ purpose: "List tasks (ls task), contracts (ls kei), or akumas (ls aku).",
151
161
  details: [
162
+ "ls kei lists active Contracts; accepted and abandoned ones leave the list, and their record stays in history and Git.",
152
163
  "ls aku/ lists the callable Akuma-name catalog; ls aku/<name> lists the living Akumas under one name.",
153
164
  'ls "aku/<name>/*" is the same living set in glob form; ls "aku/*/*" lists every living Akuma.',
154
165
  ].join("\n"),
@@ -162,6 +173,7 @@ export const CONTRACT_COMMAND_SPECS = {
162
173
  details: [
163
174
  "--include-dirty checks all non-ignored worktree changes, staged or not.",
164
175
  "--show-diff includes the proposed candidate diff when one can be prepared.",
176
+ "Content identity names the change's content hash, never a commit; it survives rebase and Git cannot look it up.",
165
177
  "Progress appears on stderr; stdout contains one final result.",
166
178
  ].join("\n"),
167
179
  },
@@ -210,7 +222,7 @@ export function renderContractHelp(command) {
210
222
  "command; edit the files directly.",
211
223
  "",
212
224
  "Recognized settings:",
213
- " gates gate bundles selected by bind and amend",
225
+ " gates named gate groups selected by bind and amend",
214
226
  " worktree commands run when worktrees are created or destroyed",
215
227
  " git.requireBranchesToBeUpToDate whether deliver and audit require up-to-date target branches",
216
228
  " providers the available Akuma and how each is run",
@@ -148,7 +148,9 @@ async function contractLibrary(command, execution, configuration, runtime) {
148
148
  return composeContractLibrary(execution, createKeiyakuHandle, {
149
149
  ...(configuration === undefined ? {} : { settings: configuration }),
150
150
  ...(actor === undefined ? {} : { actor }),
151
- });
151
+ },
152
+ // The CLI owns this default: an omitted Markdown --target names the invocation branch.
153
+ { kind: "current-branch" });
152
154
  }
153
155
  async function runBind(command, input, library) {
154
156
  const { coordinates, runtime } = input;
@@ -204,14 +204,12 @@ export function renderTaskHelp(action) {
204
204
  const spec = TASK_COMMAND_SPECS[action];
205
205
  return `${spec.purpose}\n\n${renderTaskUsage(action)}${spec.details === undefined ? "" : `\n\n${spec.details}`}`;
206
206
  }
207
+ const nameWidth = Math.max(...Object.keys(TASK_COMMAND_SPECS).map((name) => name.length));
207
208
  return [
208
209
  "usage keiyaku task <command> ...",
210
+ " keiyaku task <command> --help shows that command's complete usage",
209
211
  "",
210
- "commands:",
211
- ...Object.values(TASK_COMMAND_SPECS).flatMap((spec) => spec.usage
212
- .split("\n")
213
- .map((line) => ` ${line}`)
214
- .concat(` ${spec.purpose}`)),
212
+ ...Object.entries(TASK_COMMAND_SPECS).map(([name, spec]) => ` ${name.padEnd(nameWidth)} ${spec.purpose}`),
215
213
  ].join("\n");
216
214
  }
217
215
  function renderTaskUsage(action) {
@@ -387,8 +385,12 @@ function validateListSelector(selector, fail) {
387
385
  }
388
386
  function validateTaskScan(action, scanned, fail) {
389
387
  const spec = TASK_COMMAND_SPECS[action], { positionals, flags, stdin } = scanned;
390
- if (positionals.length < spec.arity[0] || positionals.length > spec.arity[1])
391
- fail(`task ${action} has invalid positional arguments`);
388
+ if (positionals.length < spec.arity[0])
389
+ fail(spec.arity[1] === 1 ? `task ${action} requires a TaskId` : `task ${action} requires at least one TaskId`);
390
+ if (positionals.length > spec.arity[1])
391
+ fail(spec.arity[1] === 0
392
+ ? `task ${action} accepts no positional arguments`
393
+ : `task ${action} accepts at most ${spec.arity[1]} positional argument${spec.arity[1] === 1 ? "" : "s"}`);
392
394
  if (action === "add" && (stdin === "document") === (positionals.length === 1))
393
395
  fail("task add requires either TITLE or '-' input");
394
396
  if (action === "add" &&
@@ -294,6 +294,11 @@ export function parseArgv(argv) {
294
294
  const help = helpCoordinate(invocation.commandArgv);
295
295
  if (help !== null)
296
296
  return { help };
297
+ // A bare invocation asks for orientation: the root help index, not a refusal.
298
+ if (invocation.commandArgv.length === 0)
299
+ return { help: { kind: "root" } };
300
+ if (invocation.commandArgv.length === 1 && invocation.commandArgv[0] === "task")
301
+ return { help: { kind: "task" } };
297
302
  if (invocation.commandArgv.length === 1 && invocation.commandArgv[0] === "--version")
298
303
  return { version: true };
299
304
  const task = invocation.commandArgv[0] === "task" ? parseTaskCommand(invocation.commandArgv.slice(1)) : undefined;
@@ -14,6 +14,10 @@ export declare const DEFAULT_CONTEXT: TextRenderContext;
14
14
  */
15
15
  export declare function frameRule(headLines: readonly string[]): string;
16
16
  type StatusTimeline = AkumaObservation["status"]["timeline"];
17
+ type StatusTimelineEntry = StatusTimeline["entries"][number];
18
+ type RenderRow = ActivityRow | Extract<StatusTimelineEntry, {
19
+ kind: "row";
20
+ }>["row"];
17
21
  type RenderedSnapshot = StatusTimeline;
18
22
  type RenderedActivity = Readonly<{
19
23
  snapshot: RenderedSnapshot;
@@ -56,15 +60,31 @@ export type ActivityStream = ((activity: RenderedActivity) => readonly string[])
56
60
  seed: (activity: RenderedActivity, alreadyRenderedSequence?: number) => readonly string[];
57
61
  /** Current live rows for the redrawable frame; never part of append-only output. */
58
62
  frame: () => readonly string[];
59
- /** Emit the deferred tail exactly once before the command's conclusion. */
63
+ /** Emit the pending omission count and unsettled rows once before the command's conclusion. */
60
64
  flush: () => readonly string[];
65
+ /** The same decisions undrawn, so concurrent streams can merge before rendering. */
66
+ emissions: Readonly<{
67
+ observe: (activity: RenderedActivity) => readonly StreamEmission[];
68
+ seed: (activity: RenderedActivity, alreadyRenderedSequence?: number) => readonly StreamEmission[];
69
+ flush: () => readonly StreamEmission[];
70
+ }>;
71
+ render: (emissions: readonly StreamEmission[]) => readonly string[];
72
+ }>;
73
+ /** One decided piece of append-only output: a settled row, or an untimed omission count. */
74
+ export type StreamEmission = Readonly<{
75
+ kind: "gap";
76
+ count: number;
77
+ }> | Readonly<{
78
+ kind: "row";
79
+ row: RenderRow;
80
+ inFlightSay?: boolean;
81
+ unsettled?: boolean;
61
82
  }>;
62
83
  /**
63
- * Append-only live view over one command's successive settled snapshots. The
64
- * first three tools stream immediately; later tools wait in a two-row tail.
65
- * A say omits the earlier tail, so only the last two extra tools after the
66
- * final say can print at closing. Newer tools displace older tail candidates
67
- * in place. Other narrative rows wait behind undecided tail tools.
84
+ * Append-only live view over one command's successive settled snapshots.
85
+ * After each say or Tell, the first three tools print as they settle; later
86
+ * tools until the next checkpoint fold into one omission marker. Failed tools
87
+ * always print.
68
88
  */
69
89
  export declare function activityStream(context: TextRenderContext, layout?: RowLayout): ActivityStream;
70
90
  /** What a wait conclusion renders over: its observed and unobserved members. */