@bluewombat/slots 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,23 +58,23 @@ these all use it. How Host wires a slot into a run:
58
58
 
59
59
  ## Catalogue
60
60
 
61
- | Slot | Kind | Answers |
62
- | --------------------------------- | ------- | -------------------------------------------------------------------- |
63
- | `agents/cursor.mjs` | Agent | Runs Cursor CLI with a filled prompt |
64
- | `agents/claude.mjs` | Agent | Runs Claude Code with a filled prompt |
65
- | `builders/producer.mjs` | Builder | First pass of a Subtask: fill `{{task}}`, then spawn the agent |
66
- | `builders/repair.mjs` | Builder | A Gate refused the Subtask: fill `{{task}}` and `{{report}}` |
67
- | `assembly/fix.mjs` | Builder | A judgement refused the assembled feature: fill `{{task}}` and `{{report}}` |
68
- | `gates/workspace-changed.mjs` | Gate | The Attempt left an uncommitted change |
69
- | `gates/parent-clean.mjs` | Gate | Isolator's Parent working files did not move |
70
- | `gates/sensitive-path.mjs` | Gate | No path matching a glob was touched |
71
- | `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
72
- | `planners/one-subtask.mjs` | Planner | Bootstrap: one Subtask that is the FeatureStandard itself |
73
- | `publishers/git.mjs` | Publisher | Pushes the feature's branch to the work line's own remote |
74
- | `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
75
- | `planners/producer.mjs` | Planner | An agent CLI splits the FeatureStandard |
76
- | `messages/conventional.mjs` | Message | `<type>: <title>` for the feature's fold, no agent; `--scope`, `--type` |
77
- | `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the feature's subject and body under `--rules-file` |
61
+ | Slot | Kind | Answers |
62
+ | ----------------------------- | --------- | ------------------------------------------------------------------------------------------ |
63
+ | `agents/cursor.mjs` | Agent | Runs Cursor CLI with a filled prompt |
64
+ | `agents/claude.mjs` | Agent | Runs Claude Code with a filled prompt |
65
+ | `builders/producer.mjs` | Builder | First pass of a Subtask: fill `{{task}}`, then spawn the agent |
66
+ | `builders/repair.mjs` | Builder | A Gate refused the Subtask: fill `{{task}}` and `{{report}}` |
67
+ | `assembly/fix.mjs` | Builder | A judgement refused the assembled feature: fill `{{task}}` and `{{report}}` |
68
+ | `gates/workspace-changed.mjs` | Gate | The Attempt left an uncommitted change |
69
+ | `gates/parent-clean.mjs` | Gate | Isolator's Parent working files did not move |
70
+ | `gates/sensitive-path.mjs` | Gate | No path matching a glob was touched |
71
+ | `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
72
+ | `planners/one-subtask.mjs` | Planner | Bootstrap: one Subtask that is the FeatureStandard itself |
73
+ | `publishers/git.mjs` | Publisher | Pushes the feature's branch to the work line's own remote |
74
+ | `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
75
+ | `planners/producer.mjs` | Planner | An agent CLI splits the FeatureStandard |
76
+ | `messages/conventional.mjs` | Message | `<type>: <title>` for the feature's fold, no agent; `--scope`, `--type` |
77
+ | `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the feature's subject and body under `--rules-file` |
78
78
 
79
79
  ## Agents
80
80
 
@@ -92,18 +92,36 @@ and print the serialized run on stdout. They do not know `--intention` or
92
92
  `cursor.mjs` needs `cursor-agent` on PATH (`cursor-agent login`); `claude.mjs`
93
93
  needs `claude`, signed in once.
94
94
 
95
- | Option | Meaning |
96
- | --------------------------- | ------------------------------------------------------------------ |
97
- | `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file` |
98
- | `--prompt-file PATH` | The same text, from a file. No interpolation |
99
- | `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH |
100
- | `--model NAME` | Model for that run |
101
- | `--output-format FMT` | `json` (default), `text`, or `stream-json` |
102
- | `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions` |
103
- | `--transcript-dir PATH` | Write one file per turn under here. Off unless given |
104
- | `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all |
105
- | `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable |
106
- | `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role |
95
+ | Option | Meaning |
96
+ | ---------------------------------- | -------------------------------------------------------------------------------- |
97
+ | `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file` |
98
+ | `--prompt-file PATH` | The same text, from a file. No interpolation |
99
+ | `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH |
100
+ | `--model NAME` | Model for that run |
101
+ | `--output-format FMT` | `stream-json` (default), `json`, or `text`. Skill extraction needs `stream-json` |
102
+ | `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions` |
103
+ | `--transcript-dir PATH` | Write one file per turn under here. Off unless given |
104
+ | `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all |
105
+ | `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable |
106
+ | `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role |
107
+
108
+ The agent CLI prints **one JSON line** — a `SerializedRun` from
109
+ `@bluewombat/slot-kit` — with the vendor's raw streams plus two extras the
110
+ wrapper fills when it can:
111
+
112
+ | Field | Meaning |
113
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
114
+ | `skills` | `null` = unknown (not stream-json, or unparseable). `[]` = looked, none used. Otherwise skill names in order |
115
+ | `usage` | `null` = unknown. Otherwise token counts (`input`, `output`, `cacheRead`, `cacheWrite`) and `costUsd` (`null` when the vendor did not report a cost) |
116
+
117
+ Cursor extracts skills from `stream-json` `readToolCall` paths ending in
118
+ `/skills/<name>/SKILL.md`. Claude extracts them from `tool_use` blocks named
119
+ `Skill` (`input.skill`), and adds `--verbose` whenever the format is
120
+ `stream-json`. A future vendor agent (`codex.mjs`, …) uses the same fields:
121
+ leave them `null` when it cannot tell.
122
+
123
+ When `--transcript-dir` is set, the transcript header repeats `skills` and
124
+ `usage` (`unknown`, `(none)`, or the values).
107
125
 
108
126
  A global npm install belongs to one Node version: after `nvm use`, a `claude`
109
127
  installed under another version is off PATH. Give `--bin` the absolute path, or
@@ -159,11 +177,11 @@ never places, is `fail-blocking`. `{{done_when}}` is `Done when:` plus the
159
177
  definition of done, and vanishes whole when there is none — which is every
160
178
  assembly, so `assembly/fix` does not fill it at all.
161
179
 
162
- | Role | Required | Also fills |
163
- | ----------------------- | ------------------------ | ------------------------------------------------------------------ |
164
- | `builders/producer` | `{{task}}`, `{{rules}}` | `{{done_when}}`, `{{id}}`, `{{attempt}}` |
165
- | `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{done_when}}`, `{{id}}`, `{{attempt}}` |
166
- | `assembly/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
180
+ | Role | Required | Also fills |
181
+ | ------------------- | ------------------------------------- | ---------------------------------------------------------- |
182
+ | `builders/producer` | `{{task}}`, `{{rules}}` | `{{done_when}}`, `{{id}}`, `{{attempt}}` |
183
+ | `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{done_when}}`, `{{id}}`, `{{attempt}}` |
184
+ | `assembly/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
167
185
 
168
186
  A first-pass command that receives `--report` is `fail-blocking`: that report
169
187
  belongs to the repair command. A repair or fix command without `--report` is the same.
@@ -206,9 +224,10 @@ but inside the slot, and by the time a result reaches Host it is one word. So a
206
224
  loop that misbehaves cannot be read back, only guessed at.
207
225
 
208
226
  `--transcript-dir` writes one Markdown file per turn: the invocation, the prompt
209
- as sent, stdout, stderr, and how long it took. Files are filed under the Feature
210
- the Task belongs to, named for the Subtask and the Attempt. It belongs on the
211
- **agent** command, after `--`.
227
+ as sent, stdout, stderr, how long it took, and — when the agent wrapper passed
228
+ them — `skills` and `usage` (`unknown`, `(none)`, or the values). Files are
229
+ filed under the Feature the Task belongs to, named for the Subtask and the
230
+ Attempt. It belongs on the **agent** command, after `--`.
212
231
 
213
232
  ```json
214
233
  "builder": {
@@ -345,10 +364,10 @@ on the role; the vendor is named after `--`, the same way a Builder names it.
345
364
  }
346
365
  ```
347
366
 
348
- | Option | Meaning |
349
- | ------------------------ | ---------------------------------------------------------------- |
350
- | `--read PATH` | The project the agent may read before splitting. Default: cwd |
351
- | `--prompt-file PATH` | Prompt template. Must place `{{intention}}` and `{{out}}` |
367
+ | Option | Meaning |
368
+ | -------------------- | ------------------------------------------------------------- |
369
+ | `--read PATH` | The project the agent may read before splitting. Default: cwd |
370
+ | `--prompt-file PATH` | Prompt template. Must place `{{intention}}` and `{{out}}` |
352
371
 
353
372
  `--bin`, `--model`, `--transcript-dir` and `--agent-arg` are the agent's, after
354
373
  `--`. Optional placeholder: `{{max_units}}`.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Extract mason AgentExtras from a Claude Code CLI stdout blob.
3
+ *
4
+ * Skills: stream-json assistant messages whose content has a `tool_use` named
5
+ * `Skill` (Claude tools reference). The skill id is `input.skill` as emitted
6
+ * by Claude Code (e.g. `planner:thin-slice`). Ignore `system/init.skills`
7
+ * (available catalogue, not used).
8
+ *
9
+ * Usage: terminal `result` documents `usage` (`input_tokens`, …) and
10
+ * `total_cost_usd`.
11
+ *
12
+ * `skills` is `null` when stdout is not NDJSON we can parse; `[]` when parsed
13
+ * with no Skill tool_use.
14
+ */
15
+
16
+ import { readResult } from "@bluewombat/slot-kit";
17
+
18
+ /**
19
+ * @param {string} stdout
20
+ * @param {string} outputFormat
21
+ * @returns {{ skills: string[] | null, usage: import("@bluewombat/slot-kit").AgentUsage | null }}
22
+ */
23
+ export function extrasFromClaude(stdout, outputFormat) {
24
+ return {
25
+ skills: outputFormat === "stream-json" ? skillsFromClaude(stdout) : null,
26
+ usage: usageFromClaude(stdout),
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {string} stdout
32
+ * @returns {string[] | null}
33
+ */
34
+ export function skillsFromClaude(stdout) {
35
+ const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
36
+ if (lines.length === 0) {
37
+ return null;
38
+ }
39
+ /** @type {string[]} */
40
+ const skills = [];
41
+ /** @type {Set<string>} */
42
+ const seen = new Set();
43
+ let parsedAny = false;
44
+ for (const line of lines) {
45
+ let event;
46
+ try {
47
+ event = JSON.parse(line);
48
+ } catch {
49
+ continue;
50
+ }
51
+ if (event === null || typeof event !== "object" || Array.isArray(event)) {
52
+ continue;
53
+ }
54
+ parsedAny = true;
55
+ if (event.type !== "assistant") {
56
+ continue;
57
+ }
58
+ const content = event.message?.content;
59
+ if (!Array.isArray(content)) {
60
+ continue;
61
+ }
62
+ for (const block of content) {
63
+ if (block === null || typeof block !== "object" || Array.isArray(block)) {
64
+ continue;
65
+ }
66
+ if (block.type !== "tool_use" || block.name !== "Skill") {
67
+ continue;
68
+ }
69
+ const input = block.input;
70
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
71
+ continue;
72
+ }
73
+ const name = /** @type {Record<string, unknown>} */ (input).skill;
74
+ if (typeof name !== "string" || name.length === 0 || seen.has(name)) {
75
+ continue;
76
+ }
77
+ seen.add(name);
78
+ skills.push(name);
79
+ }
80
+ }
81
+ return parsedAny ? skills : null;
82
+ }
83
+
84
+ /**
85
+ * @param {string} stdout
86
+ * @returns {import("@bluewombat/slot-kit").AgentUsage | null}
87
+ */
88
+ export function usageFromClaude(stdout) {
89
+ const result = readResult(stdout);
90
+ if (result === undefined) {
91
+ return null;
92
+ }
93
+ const usage = result.usage;
94
+ if (usage === null || typeof usage !== "object" || Array.isArray(usage)) {
95
+ return null;
96
+ }
97
+ const record = /** @type {Record<string, unknown>} */ (usage);
98
+ if (
99
+ typeof record.input_tokens !== "number" ||
100
+ typeof record.output_tokens !== "number" ||
101
+ typeof record.cache_creation_input_tokens !== "number" ||
102
+ typeof record.cache_read_input_tokens !== "number"
103
+ ) {
104
+ return null;
105
+ }
106
+ return {
107
+ input: record.input_tokens,
108
+ output: record.output_tokens,
109
+ cacheRead: record.cache_read_input_tokens,
110
+ cacheWrite: record.cache_creation_input_tokens,
111
+ costUsd: typeof result.total_cost_usd === "number" ? result.total_cost_usd : null,
112
+ };
113
+ }
package/agents/claude.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  textOption,
13
13
  transcriptFor,
14
14
  } from "@bluewombat/slot-kit";
15
+ import { extrasFromClaude } from "./claude-extras.mjs";
15
16
 
16
17
  // Claude Code as a vendor agent. The prompt is already filled: this command
17
18
  // does not interpolate. Own options only — a role slot strips Implementer's
@@ -19,7 +20,7 @@ import {
19
20
  // --prompt TEXT / --prompt-file PATH the filled prompt. One of the two
20
21
  // --bin PATH the Claude CLI. Default: claude, found on PATH
21
22
  // --model NAME alias (opus, sonnet) or full name
22
- // --output-format FMT json (default), text, or stream-json
23
+ // --output-format FMT stream-json (default), json, or text
23
24
  // --permission-mode MODE bypassPermissions (default), acceptEdits, …
24
25
  // --transcript-dir PATH write one file per turn. Off by default
25
26
  // --transcript-part NAME prompt, stdout, stderr, timing. Repeatable
@@ -59,12 +60,16 @@ if (bin === undefined) {
59
60
  }
60
61
 
61
62
  const model = textOption(options, "model");
63
+ /** @type {string} */
64
+ const outputFormat = /** @type {string} */ (options.outputFormat);
62
65
  const args = [
63
66
  "-p",
64
67
  "--permission-mode",
65
68
  /** @type {string} */ (options.permissionMode),
66
69
  "--output-format",
67
- /** @type {string} */ (options.outputFormat),
70
+ outputFormat,
71
+ // Claude's stream-json only emits tool events with --verbose (headless docs).
72
+ ...(outputFormat === "stream-json" ? ["--verbose"] : []),
68
73
  ...(model === undefined ? [] : ["--model", model]),
69
74
  ...listOption(options, "agentArg"),
70
75
  prompt,
@@ -77,8 +82,9 @@ const transcript = transcriptFor(process.argv, options, {
77
82
  prompt,
78
83
  });
79
84
  const run = await runAgent({ file: bin, args, cwd });
80
- transcript.write(run);
81
- answer(run, bin);
85
+ const extras = extrasFromClaude(run.stdout, outputFormat);
86
+ transcript.write(run, extras);
87
+ answer(run, bin, extras);
82
88
 
83
89
  /** @param {string} reason @returns {never} */
84
90
  function refuse(reason) {
@@ -89,9 +95,14 @@ function refuse(reason) {
89
95
  /**
90
96
  * @param {import("@bluewombat/slot-kit").AgentRun} run
91
97
  * @param {string} binPath
98
+ * @param {import("@bluewombat/slot-kit").AgentExtras} extras
92
99
  * @returns {never}
93
100
  */
94
- function answer(run, binPath) {
95
- process.stdout.write(`${JSON.stringify(serializeRun(run, { name: NAME, bin: binPath }))}\n`);
101
+ function answer(run, binPath, extras) {
102
+ process.stdout.write(
103
+ `${JSON.stringify(
104
+ serializeRun(run, { name: NAME, bin: binPath, skills: extras.skills, usage: extras.usage }),
105
+ )}\n`,
106
+ );
96
107
  process.exit(0);
97
108
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Extract mason AgentExtras from a Cursor CLI stdout blob.
3
+ *
4
+ * Skills: documented stream-json `tool_call` / `readToolCall` with a path that
5
+ * ends in `/skills/<name>/SKILL.md` (Cursor skills are SKILL.md files the
6
+ * agent reads). Count each `call_id` once, on `started`.
7
+ *
8
+ * Usage: last JSON object (the terminal `result`) may carry a `usage` object
9
+ * with camelCase token fields (observed; not in the published json schema).
10
+ * No cost field is documented for Cursor — `costUsd` stays null.
11
+ *
12
+ * `skills` is `null` when the stdout is not NDJSON we can parse (unknown),
13
+ * `[]` when we parsed and found no skill reads.
14
+ */
15
+
16
+ import { readResult } from "@bluewombat/slot-kit";
17
+
18
+ /**
19
+ * @param {string} stdout
20
+ * @param {string} outputFormat
21
+ * @returns {{ skills: string[] | null, usage: import("@bluewombat/slot-kit").AgentUsage | null }}
22
+ */
23
+ export function extrasFromCursor(stdout, outputFormat) {
24
+ return {
25
+ skills: outputFormat === "stream-json" ? skillsFromCursor(stdout) : null,
26
+ usage: usageFromCursor(stdout),
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {string} stdout
32
+ * @returns {string[] | null}
33
+ */
34
+ export function skillsFromCursor(stdout) {
35
+ const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
36
+ if (lines.length === 0) {
37
+ return null;
38
+ }
39
+ /** @type {string[]} */
40
+ const skills = [];
41
+ /** @type {Set<string>} */
42
+ const seenCalls = new Set();
43
+ /** @type {Set<string>} */
44
+ const seenNames = new Set();
45
+ let parsedAny = false;
46
+ for (const line of lines) {
47
+ let event;
48
+ try {
49
+ event = JSON.parse(line);
50
+ } catch {
51
+ continue;
52
+ }
53
+ if (event === null || typeof event !== "object" || Array.isArray(event)) {
54
+ continue;
55
+ }
56
+ parsedAny = true;
57
+ if (event.type !== "tool_call" || event.subtype !== "started") {
58
+ continue;
59
+ }
60
+ const callId = typeof event.call_id === "string" ? event.call_id : "";
61
+ if (callId.length > 0 && seenCalls.has(callId)) {
62
+ continue;
63
+ }
64
+ const path = readToolPath(event.tool_call);
65
+ const name = skillNameFromPath(path);
66
+ if (name === undefined) {
67
+ continue;
68
+ }
69
+ if (callId.length > 0) {
70
+ seenCalls.add(callId);
71
+ }
72
+ if (seenNames.has(name)) {
73
+ continue;
74
+ }
75
+ seenNames.add(name);
76
+ skills.push(name);
77
+ }
78
+ return parsedAny ? skills : null;
79
+ }
80
+
81
+ /**
82
+ * @param {unknown} toolCall
83
+ * @returns {string | undefined}
84
+ */
85
+ function readToolPath(toolCall) {
86
+ if (toolCall === null || typeof toolCall !== "object" || Array.isArray(toolCall)) {
87
+ return undefined;
88
+ }
89
+ const read = /** @type {Record<string, unknown>} */ (toolCall).readToolCall;
90
+ if (read === null || typeof read !== "object" || Array.isArray(read)) {
91
+ return undefined;
92
+ }
93
+ const args = /** @type {Record<string, unknown>} */ (read).args;
94
+ if (args === null || typeof args !== "object" || Array.isArray(args)) {
95
+ return undefined;
96
+ }
97
+ const path = /** @type {Record<string, unknown>} */ (args).path;
98
+ return typeof path === "string" ? path : undefined;
99
+ }
100
+
101
+ /**
102
+ * @param {string | undefined} path
103
+ * @returns {string | undefined}
104
+ */
105
+ export function skillNameFromPath(path) {
106
+ if (path === undefined) {
107
+ return undefined;
108
+ }
109
+ const match = path.match(/(?:^|[/\\])skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i);
110
+ return match?.[1];
111
+ }
112
+
113
+ /**
114
+ * @param {string} stdout
115
+ * @returns {import("@bluewombat/slot-kit").AgentUsage | null}
116
+ */
117
+ export function usageFromCursor(stdout) {
118
+ const result = readResult(stdout);
119
+ if (result === undefined) {
120
+ return null;
121
+ }
122
+ const usage = result.usage;
123
+ if (usage === null || typeof usage !== "object" || Array.isArray(usage)) {
124
+ return null;
125
+ }
126
+ const record = /** @type {Record<string, unknown>} */ (usage);
127
+ if (
128
+ typeof record.inputTokens !== "number" ||
129
+ typeof record.outputTokens !== "number" ||
130
+ typeof record.cacheReadTokens !== "number" ||
131
+ typeof record.cacheWriteTokens !== "number"
132
+ ) {
133
+ return null;
134
+ }
135
+ return {
136
+ input: record.inputTokens,
137
+ output: record.outputTokens,
138
+ cacheRead: record.cacheReadTokens,
139
+ cacheWrite: record.cacheWriteTokens,
140
+ costUsd: null,
141
+ };
142
+ }
package/agents/cursor.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  textOption,
13
13
  transcriptFor,
14
14
  } from "@bluewombat/slot-kit";
15
+ import { extrasFromCursor } from "./cursor-extras.mjs";
15
16
 
16
17
  // Cursor CLI as a vendor agent. The prompt is already filled: this command
17
18
  // does not interpolate. Own options only — a role slot strips Implementer's
@@ -19,7 +20,7 @@ import {
19
20
  // --prompt TEXT / --prompt-file PATH the filled prompt. One of the two
20
21
  // --bin PATH the Cursor CLI. Default: cursor-agent, found
21
22
  // --model NAME
22
- // --output-format FMT json (default), text, or stream-json
23
+ // --output-format FMT stream-json (default), json, or text
23
24
  // --transcript-dir PATH write one file per turn. Off by default
24
25
  // --transcript-part NAME prompt, stdout, stderr, timing. Repeatable
25
26
  // --agent-arg VALUE appended before the prompt. Repeatable
@@ -51,6 +52,8 @@ if (bin === undefined) {
51
52
  }
52
53
 
53
54
  const model = textOption(options, "model");
55
+ /** @type {string} */
56
+ const outputFormat = /** @type {string} */ (options.outputFormat);
54
57
  const args = [
55
58
  "-p",
56
59
  "--force",
@@ -60,7 +63,7 @@ const args = [
60
63
  "--sandbox",
61
64
  "disabled",
62
65
  "--output-format",
63
- /** @type {string} */ (options.outputFormat),
66
+ outputFormat,
64
67
  ...(model === undefined ? [] : ["--model", model]),
65
68
  ...listOption(options, "agentArg"),
66
69
  prompt,
@@ -73,8 +76,9 @@ const transcript = transcriptFor(process.argv, options, {
73
76
  prompt,
74
77
  });
75
78
  const run = await runAgent({ file: bin, args, cwd });
76
- transcript.write(run);
77
- answer(run, bin);
79
+ const extras = extrasFromCursor(run.stdout, outputFormat);
80
+ transcript.write(run, extras);
81
+ answer(run, bin, extras);
78
82
 
79
83
  /** @param {string} reason @returns {never} */
80
84
  function refuse(reason) {
@@ -85,9 +89,14 @@ function refuse(reason) {
85
89
  /**
86
90
  * @param {import("@bluewombat/slot-kit").AgentRun} run
87
91
  * @param {string} binPath
92
+ * @param {import("@bluewombat/slot-kit").AgentExtras} extras
88
93
  * @returns {never}
89
94
  */
90
- function answer(run, binPath) {
91
- process.stdout.write(`${JSON.stringify(serializeRun(run, { name: NAME, bin: binPath }))}\n`);
95
+ function answer(run, binPath, extras) {
96
+ process.stdout.write(
97
+ `${JSON.stringify(
98
+ serializeRun(run, { name: NAME, bin: binPath, skills: extras.skills, usage: extras.usage }),
99
+ )}\n`,
100
+ );
92
101
  process.exit(0);
93
102
  }
@@ -24,6 +24,10 @@ Task:
24
24
 
25
25
  {{done_when}}
26
26
 
27
+ This checkout already includes any earlier subtasks that were folded into the
28
+ feature. Do not rewrite that work. Add or change only what this task names, and
29
+ leave the project's own checks green on the whole tree.
30
+
27
31
  Rules of this run:
28
32
  {{rules}}
29
33
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluewombat/slots",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Shipped mason slots: Planners, Builders, Gates, publishers, and refreshers a Project names in its config.",
6
6
  "keywords": [
@@ -53,6 +53,6 @@
53
53
  "test": "tsx --test $(find test -name '*.test.ts')"
54
54
  },
55
55
  "dependencies": {
56
- "@bluewombat/slot-kit": "^0.1.1"
56
+ "@bluewombat/slot-kit": "^0.1.2"
57
57
  }
58
58
  }
@@ -36,7 +36,11 @@ How the subtasks will be run, which is what makes a good split:
36
36
 
37
37
  - One at a time, never in parallel. Each runs in its own copy of this project,
38
38
  and its result is folded back before the next one starts. So a later subtask
39
- sees the earlier ones' work.
39
+ sees the earlier ones' work. If two slices would both change the same path
40
+ (a README, an index or barrel file, a changelog, package.json, or any other
41
+ shared file), make them **one** subtask, or give the second a \`depends_on\` on
42
+ the first and let **only** that second one write the shared path. Do not invent
43
+ a docs-only subtask unless the work asked for documentation.
40
44
  - Every subtask is checked by the project's own checks — its tests, its lint —
41
45
  on its own. **A subtask must leave the project working.** This is the rule that
42
46
  decides your split: never cut by layer. "Add the types", then "add the code",
@@ -58,9 +62,11 @@ How the subtasks will be run, which is what makes a good split:
58
62
  mind, and preferably by a test. It describes this subtask's observable result,
59
63
  not the whole feature and not an intention.
60
64
  - \`depends_on\` means "cannot start before this one is done", and nothing else.
61
- List only what a subtask genuinely cannot begin without. Chaining every
62
- subtask to the one before it, out of habit, describes an order you were never
63
- asked for and hides which ones were actually independent.
65
+ Sharing a file is a reason not to start side by side: pack those slices into
66
+ one subtask, or make the later one depend on the earlier. The graph describes
67
+ what must land before a merge is safe, not the order you happened to think in.
68
+ Chaining every subtask to the one before it when their files do not overlap
69
+ hides which ones were actually independent.
64
70
 
65
71
  You are choosing between two costs, and both are real.
66
72