@astrosheep/keiyaku 2.9.11 → 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 +62 -72
  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
@@ -5,6 +5,12 @@ import { runGitProcess } from "../../git/core.js";
5
5
  import { openGitStreamingBatch } from "../../git/streaming-batch.js";
6
6
  import { parseTaskDocument } from "./task.js";
7
7
  import { parseTaskCoordinate } from "./coordinate.js";
8
+ function observePublicationEffect(observer, effect) {
9
+ try {
10
+ observer?.(effect);
11
+ }
12
+ catch { /* observation cannot participate in the Git transaction */ }
13
+ }
8
14
  async function gitResult(cwd, args, input, env, signal) {
9
15
  return await runGitProcess(cwd, args, { input, env, signal });
10
16
  }
@@ -78,9 +84,11 @@ export async function createTaskMutationCommit(input) {
78
84
  ], undefined, env, input.signal);
79
85
  }
80
86
  const tree = (await git(input.cwd, ["write-tree"], undefined, env, input.signal)).trim();
81
- const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent], undefined, undefined, input.signal)).trim();
82
- if (tree === parentTree)
83
- return input.parent;
87
+ if (!input.requireCommit) {
88
+ const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent], undefined, undefined, input.signal)).trim();
89
+ if (tree === parentTree)
90
+ return input.parent;
91
+ }
84
92
  return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject], undefined, undefined, input.signal)).trim();
85
93
  }
86
94
  finally {
@@ -88,15 +96,37 @@ export async function createTaskMutationCommit(input) {
88
96
  }
89
97
  }
90
98
  /** Stage only the mutated task paths into the real index; leave unrelated entries alone. */
91
- export async function reconcileTaskMutationIndex(cwd, writes, signal) {
99
+ async function reconcileTaskMutationIndexPath(cwd, write, signal) {
100
+ const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, signal)).trim();
101
+ await git(cwd, [
102
+ "update-index",
103
+ "--add",
104
+ "--cacheinfo",
105
+ `100644,${oid},${taskMutationPath(write)}`,
106
+ ], undefined, undefined, signal);
107
+ }
108
+ export async function reconcileTaskMutationIndex(cwd, writes, signal, reconcilePath = async (input) => {
109
+ await reconcileTaskMutationIndexPath(input.cwd, input.write, input.signal);
110
+ }) {
92
111
  for (const write of writes) {
93
- const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, signal)).trim();
94
- await git(cwd, [
95
- "update-index",
96
- "--add",
97
- "--cacheinfo",
98
- `100644,${oid},${taskMutationPath(write)}`,
99
- ], undefined, undefined, signal);
112
+ await reconcilePath({ cwd, write, signal });
113
+ }
114
+ }
115
+ async function snapshotTaskMutationIndex(cwd, writes, signal) {
116
+ const snapshot = [];
117
+ for (const write of writes) {
118
+ const taskPath = taskMutationPath(write);
119
+ const output = await git(cwd, ["ls-files", "--stage", "-z", "--", taskPath], undefined, undefined, signal);
120
+ snapshot.push({ path: taskPath, entries: output.split("\0").filter(Boolean) });
121
+ }
122
+ return snapshot;
123
+ }
124
+ async function restoreTaskMutationIndex(cwd, snapshot, signal) {
125
+ for (const item of snapshot) {
126
+ await git(cwd, ["update-index", "--force-remove", "--", item.path], undefined, undefined, signal);
127
+ if (item.entries.length > 0) {
128
+ await git(cwd, ["update-index", "-z", "--index-info"], `${item.entries.join("\0")}\0`, undefined, signal);
129
+ }
100
130
  }
101
131
  }
102
132
  /**
@@ -114,20 +144,33 @@ export async function publishTaskMutationCommit(input, deps = {}) {
114
144
  parent: branch.head,
115
145
  writes: input.writes,
116
146
  subject: input.subject,
147
+ requireCommit: input.requireCommit,
117
148
  signal: input.signal,
118
149
  });
119
150
  if (commit === branch.head) {
120
- input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "current" });
151
+ const indexBefore = await snapshotTaskMutationIndex(input.cwd, input.writes, input.signal);
121
152
  try {
122
- await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
123
- input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
153
+ await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal, deps.reconcileIndexPath);
154
+ observePublicationEffect(input.observeEffect, { effect: "task-reconciliation", status: "succeeded", commit, detail: "current" });
155
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "succeeded", commit });
156
+ return commit;
124
157
  }
125
158
  catch (error) {
126
- input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
159
+ try {
160
+ await restoreTaskMutationIndex(input.cwd, indexBefore, input.signal);
161
+ }
162
+ catch (restoreError) {
163
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "failed", commit, error: restoreError });
164
+ throw new FlowError("INTERNAL_STATE", "cannot restore task index after reconciliation failure", { cause: restoreError });
165
+ }
166
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "failed", commit, error });
167
+ throw error;
127
168
  }
128
- return commit;
129
169
  }
170
+ const indexBefore = await snapshotTaskMutationIndex(input.cwd, input.writes, input.signal);
130
171
  try {
172
+ await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal, deps.reconcileIndexPath);
173
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "succeeded", commit });
131
174
  await updateBranch({
132
175
  cwd: input.cwd,
133
176
  ref: branch.ref,
@@ -135,22 +178,22 @@ export async function publishTaskMutationCommit(input, deps = {}) {
135
178
  expected: branch.head,
136
179
  signal: input.signal,
137
180
  }, deps);
138
- input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "published" });
139
- try {
140
- await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
141
- input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
142
- }
143
- catch (error) {
144
- // Branch fact remains authoritative; index may be repaired by a later mutation.
145
- input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
146
- }
181
+ observePublicationEffect(input.observeEffect, { effect: "task-reconciliation", status: "succeeded", commit, detail: "published" });
147
182
  return commit;
148
183
  }
149
184
  catch (error) {
150
185
  lastError = error;
186
+ try {
187
+ await restoreTaskMutationIndex(input.cwd, indexBefore, input.signal);
188
+ }
189
+ catch (restoreError) {
190
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "failed", commit, error: restoreError });
191
+ throw new FlowError("INTERNAL_STATE", "cannot restore task index after publication failure", { cause: restoreError });
192
+ }
193
+ observePublicationEffect(input.observeEffect, { effect: "task-index-reconciliation", status: "failed", commit, error });
151
194
  }
152
195
  }
153
- input.observeEffect?.({ effect: "task-reconciliation", status: "failed", error: lastError });
196
+ observePublicationEffect(input.observeEffect, { effect: "task-reconciliation", status: "failed", error: lastError });
154
197
  throw lastError;
155
198
  }
156
199
  async function taskTreeEntries(cwd, commit) {
@@ -337,6 +337,7 @@ export function openTaskStoreAt(board, deps = {}) {
337
337
  gitDir: transaction.location.gitDir,
338
338
  writes: plan.writes,
339
339
  subject: subject,
340
+ requireCommit: plan.requireCommit,
340
341
  signal: deps.signal,
341
342
  });
342
343
  }
@@ -75,6 +75,7 @@ function mutationPlan(sources, mutation) {
75
75
  writes: prepared.writes,
76
76
  value: mutation.select(prepared.board),
77
77
  ...(mutation.subject === undefined ? {} : { subject: mutation.subject }),
78
+ requireCommit: mutation.requireCommit ?? true,
78
79
  };
79
80
  }
80
81
  export function taskRepositoryFromStore(store) {
@@ -68,6 +68,11 @@ export function resolveFlowErrorHints(code, context, facts) {
68
68
  return ["Pass one complete task coordinate from the listed candidates."];
69
69
  case "UNKNOWN_COMMAND":
70
70
  return ["Run `keiyaku --help` to inspect available commands."];
71
+ case "UNKNOWN_OPTION":
72
+ case "UNKNOWN_ADDRESS":
73
+ case "INVALID_USAGE":
74
+ case "STDIN_EMPTY":
75
+ return [];
71
76
  case "EMPTY_PARAM":
72
77
  return ["One or more required parameters are empty."];
73
78
  case "DIRTY_WORKTREE":
@@ -95,7 +100,7 @@ export function resolveFlowErrorHints(code, context, facts) {
95
100
  case "FROM_DRAFT_NOT_FOUND":
96
101
  return ["The path in `from_draft` does not exist. `from_draft` must reference an existing file path (absolute or relative to `cwd`)."];
97
102
  case "INVALID_KEIYAKU_DRAFT":
98
- return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Context`, `## Objective`, `## Design`, `## Scope`, `## Criteria`, and `## Verification` sections (or the equivalent shape in `from_draft`)."];
103
+ return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Context`, `## Objective`, `## Design & Approach`, `## Scope`, `## Criteria`, and `## Verification` sections (or the equivalent shape in `from_draft`)."];
99
104
  case "BIND_TASK_INCOMPLETE":
100
105
  return ["Task supplies title/objective defaults; pipe only the missing sections as partial contract Markdown on stdin."];
101
106
  case "KEIYAKU_FILE_EXISTS":
@@ -1,3 +1,3 @@
1
1
  // Generated into build output by scripts/build.mjs
2
- export const VERSION = "2.9.11";
3
- export const GIT_HASH = "4384d75";
2
+ export const VERSION = "2.9.12";
3
+ export const GIT_HASH = "25d9570";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "2.9.11",
3
+ "version": "2.9.12",
4
4
  "description": "CLI for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "start": "node build/index.js",
34
34
  "test:unit": "node scripts/run-unit-tests.mjs",
35
35
  "test:boundaries": "node scripts/check-import-boundaries.mjs",
36
- "test:integration": "node --import ./tests/support/real-provider-path-sentinel.mjs --import tsx --test tests/integration/integration.test.ts",
36
+ "test:integration": "node --import ./tests/support/real-provider-path-sentinel.mjs --import tsx --test --test-concurrency=4 tests/integration/*.test.ts",
37
37
  "pretest:cli-expect": "npm run build",
38
38
  "test:cli-expect": "node --import ./tests/support/real-provider-path-sentinel.mjs --test --test-concurrency=8 tests/cli-expect/suite/*.test.mjs",
39
39
  "test:real-providers": "KEIYAKU_REAL_PROVIDERS=1 node --import tsx --test --test-concurrency=1 tests/real-provider/matrix.test.ts",
@@ -52,7 +52,8 @@ keiyaku wait <proj-a> <proj-b> --any # return when the first member fin
52
52
  keiyaku wait <proj-a> <proj-b> --all # return when every member finishes
53
53
  keiyaku tell <proj-id> [--wait DURATION] <BODY|-> # optionally tell, then bounded-join
54
54
  keiyaku revive rsp_XXXX "continue..." # new run continuing from a record
55
- keiyaku kill <proj-id> # stop it; record survives, revive still works
55
+ keiyaku kill <proj-id> # request a stop; returns an acceptance receipt
56
+ keiyaku kill <proj-id> --wait 5m # request a stop, then observe the result for 5m
56
57
  keiyaku status # forgot an id? it's on the board
57
58
  ```
58
59
 
@@ -74,6 +75,10 @@ Use `keiyaku akuma show NAME` to inspect the resolved source and configuration.
74
75
  success means the tell and interrupt intent are durable; provider stop and
75
76
  successor wake continue asynchronously under the same projection id. Idle or
76
77
  terminal state uses ordinary tell plus wake and reports its typed wake error.
78
+ - `kill` returns an acceptance receipt; it does not include the final result.
79
+ Use `kill --wait DURATION` when you need to observe the ordinary final result.
80
+ `DURATION` is required. A timeout ends observation only and does not undo the
81
+ stop request.
77
82
  - Body is required for `call`/`tell`. Literal `-` selects stdin; unselected
78
83
  piped bytes are ignored and cannot change an argv body.
79
84
  - A `--wait`/`--timeout` expiry prints a live snapshot and exits 0 — the run continues; rejoin with `wait` anytime.
@@ -38,6 +38,28 @@ keiyaku task update k-xxxx --pri 0 # also --title, --body TEXT|-, -
38
38
  keiyaku task doctor # check board integrity
39
39
  ```
40
40
 
41
+ ## Atomic reorganization
42
+
43
+ Use `task compose` when the final tree and its complete outgoing relation sets
44
+ need review as one operation. Pass the compact sketch through stdin and check
45
+ it before applying it:
46
+
47
+ ```bash
48
+ keiyaku task compose --check - <<'EOF'
49
+ + Project grouping
50
+ @parser needs=@types
51
+ @renderer relates=
52
+ @release parent=
53
+ EOF
54
+ keiyaku task compose - < composition.md
55
+ ```
56
+
57
+ An indented line assigns its parent. A top-level `@reference` keeps its parent;
58
+ `parent=` explicitly clears it. `needs=`, `supersedes=`, and `relates=` replace
59
+ only their named complete relation sets, and an explicit empty assignment clears
60
+ that set. Omit a relation to preserve it. Use the command help for the current
61
+ sketch grammar and receipt fields; this skill does not redefine task policy.
62
+
41
63
  ## Dependencies
42
64
 
43
65
  `--needs` stores the forward edge; `blocks` on the other task is derived automatically. Cycles are rejected at write time. Use needs to express "don't start B before A lands" — nothing more.
@@ -54,7 +76,7 @@ intent, share a coherent write surface, and should settle together; otherwise
54
76
  keep separate contracts and express order with task `needs` or contract
55
77
  `--after`. The first task supplies title/objective defaults; provide a partial
56
78
  bind document for every remaining resolved section. Final bind admission always
57
- requires Title, Context, Objective, Design, Scope, and Criteria; Verification
79
+ requires Title, Context, Objective, Design & Approach, Scope, and Criteria; Verification
58
80
  is optional.
59
81
 
60
82
  ## Gotcha
@@ -17,12 +17,33 @@ allowed-tools: Bash(keiyaku *)
17
17
  `arc`, `renew`, or `petition`.
18
18
  - An arc is a coherent chapter of the same delivery. It keeps the contract,
19
19
  worktree, branch, and final settlement.
20
- - `audit` reports evidence for one pinned candidate. `petition` asks to settle.
20
+ - `audit` is the primary integrated review of one pinned candidate; `petition`
21
+ asks to settle that candidate.
21
22
 
22
23
  ```text
23
24
  bound -> active -> petitioned -> claimed | forfeited
24
25
  ```
25
26
 
27
+ ## Choosing Scope
28
+
29
+ Scope names the files this contract is currently expected to write. Use exact
30
+ repository-relative paths when known, a one-component wildcard for unknown
31
+ files in one directory, and a recursive wildcard only when the Objective calls
32
+ for changing the whole subtree.
33
+
34
+ ## Scope
35
+ ~~~
36
+ src/pump.ts
37
+ ~~~
38
+
39
+ Scope overlap means contracts may write the same paths; it does not prove their
40
+ changes conflict or forbid parallel work. If intents are independent, proceed
41
+ in parallel and use `--after` to order settlement; use `--exclusive` only when
42
+ concurrent work is unsafe.
43
+
44
+ Amend is cheap. Broad Scope consumes parallel coordination and weakens audit
45
+ signal, so broader patterns need a reason while narrower terms do not.
46
+
26
47
  ## Bind
27
48
 
28
49
  Bind when one implementation journey is decided: motivating facts, owning
@@ -30,19 +51,12 @@ modules, inputs and outputs, consequences, failure behavior, invariants,
30
51
  forbidden expansion, write set, acceptance criteria, and decisive checks.
31
52
  Use a task only when planning or dependencies need a durable place.
32
53
 
33
- Declare the smallest useful Scope:
34
-
35
- 1. exact paths when known;
36
- 2. a one-component wildcard when names inside one directory are still open;
37
- 3. a recursive wildcard only for a real subtree rewrite, migration, or
38
- generated output.
39
-
40
54
  ```bash
41
55
  keiyaku bind - < contract.md
42
56
  keiyaku bind --task TASK_ID - < contract.md
43
57
  ```
44
58
 
45
- The contract document carries `Title`, `Context`, `Objective`, `Design`,
59
+ The contract document carries `Title`, `Context`, `Objective`, `Design & Approach`,
46
60
  `Scope`, `Criteria`, and optional `Verification`. Add newly expected exact paths
47
61
  with `amend`; do not pre-authorize a broad tree for convenience.
48
62
 
@@ -81,16 +95,14 @@ keiyaku status
81
95
  - `amend` records a changed intent, invariant, Scope, Criteria, or Verification
82
96
  before implementation relies on that change.
83
97
 
84
- Scope changes are ordered terms. Use flags for simple additions or one
85
- `Scope Append` block for an ordered delta:
98
+ Scope changes are ordered terms. Use `--append-scope` for a simple addition or
99
+ one `Scope Append` block for an ordered delta:
86
100
 
87
- ````markdown
88
101
  ## Scope Append
89
- ```
102
+ ~~~
90
103
  src/new-owner.ts
91
104
  !src/retired/**
92
- ```
93
- ````
105
+ ~~~
94
106
 
95
107
  Later `!pattern` terms narrow earlier matches. Do not mix the block with
96
108
  `--append-scope` flags.
@@ -113,9 +125,12 @@ been reviewed again.
113
125
 
114
126
  ## Settle
115
127
 
116
- Before settlement, renew if the target moved, then rerun the declared evidence.
117
- `audit` is read-only; exit 0 means it produced a report, not that the candidate
118
- passed. Petition requires an oath and runs the settlement gates:
128
+ Commit, renew if stale, then run `audit` first. Audit already combines the
129
+ candidate diff, Scope, Criteria, Verification, and delivery context.
130
+
131
+ If the evidence is sufficient, petition. Otherwise, run only the check needed
132
+ for a specific gap. `not configured` is not itself a failure. Audit exit 0 means
133
+ a report was produced, not that the candidate passed.
119
134
 
120
135
  ```bash
121
136
  keiyaku audit