@open-tomato/rafa 0.16.0 → 0.18.0

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.
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  describeValue
4
- } from "./index-n3rw8rhr.js";
4
+ } from "./index-944pncd8.js";
5
5
 
6
6
  // src/cli/spends.ts
7
7
  var SPENDS_GLYPH = "\uD83E\uDE99";
@@ -9,12 +9,14 @@ import {
9
9
  PR_PROVIDERS,
10
10
  RELEASE_AUTO,
11
11
  REQUIRED_ITEM_KEYS,
12
+ SKILL_TIERS,
12
13
  STORE_BACKENDS,
13
14
  dayCount,
14
15
  describeValue,
15
16
  flag,
16
17
  githubLogin,
17
18
  isMapping,
19
+ isSkillTier,
18
20
  issueNumber,
19
21
  listOf,
20
22
  mergeMethod,
@@ -24,14 +26,209 @@ import {
24
26
  optionalPrerequisite,
25
27
  releaseEnabled,
26
28
  requiredPrerequisite,
29
+ routeTarget,
27
30
  subsetOf,
28
31
  text,
32
+ tierPin,
33
+ tierSwitch,
29
34
  usdAmount
30
- } from "./index-n3rw8rhr.js";
35
+ } from "./index-944pncd8.js";
31
36
 
32
37
  // src/config-schema.ts
33
38
  import { join } from "path";
39
+
40
+ // src/tiers/resolve.ts
41
+ import { readFileSync } from "fs";
42
+ var VENDORED_HEADER = /^<!-- vendored by rafa from .* on \d{4}-\d{2}-\d{2} -->$/;
43
+ var CLAUDE_SKILL_ORDER = ["user", "project", "rafa"];
44
+ var KIND_ORDER = ["skill", "agent"];
45
+ var PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
46
+ function loadedTiers(settings) {
47
+ return SKILL_TIERS.filter((tier) => tier === "project" || tier === "rafa" && settings.tiersRafa !== "off" || tier === "user" && settings.settingSources.includes("user"));
48
+ }
49
+ function pinKey(kind) {
50
+ return kind === "skill" ? "tiers.skills" : "tiers.agents";
51
+ }
52
+ function pinLine(kind, name, tier) {
53
+ const key = PLAIN_NAME.test(name) ? name : JSON.stringify(name);
54
+ return `${pinKey(kind)}: { ${key}: ${tier} }`;
55
+ }
56
+ function collisionMessage(collision) {
57
+ const places = collision.holders.map((holder) => `${holder.source} ${holder.path}`).join(" and ");
58
+ const held = `${collision.kind} ${collision.name} is held by ${collision.holders.length} loaded tiers ` + `with different contents: ${places}; `;
59
+ const tier = collision.setAsidePin;
60
+ if (tier === null)
61
+ return `${held}pin the tier that serves it: ${collision.pinLine}`;
62
+ const over = collision.outrankedBy;
63
+ const tiers = over.map((holder) => holder.source).join(" and ");
64
+ const copies = over.length === 1 ? "copy" : "copies";
65
+ const paths = over.map((holder) => holder.path).join(" and ");
66
+ return `${held}${pinLine(collision.kind, collision.name, tier)} has no effect, because Claude Code loads ` + `the ${tiers} ${copies} over the ${tier} copy: pin the copy it loads (${collision.pinLine}), ` + `or delete or rename ${paths} to let the ${tier} copy serve`;
67
+ }
68
+ function readItemBytes(path) {
69
+ try {
70
+ return readFileSync(path);
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function comparableText(bytes) {
76
+ return Buffer.from(bytes).toString("latin1").split(`
77
+ `).filter((line) => !VENDORED_HEADER.test(line)).join(`
78
+ `);
79
+ }
80
+ function tierIndex(row) {
81
+ return SKILL_TIERS.indexOf(row.source);
82
+ }
83
+ function groupByName(rows) {
84
+ const groups = new Map;
85
+ for (const row of rows) {
86
+ if (!isSkillTier(row.source))
87
+ continue;
88
+ const key = `${row.kind}\x00${row.name}`;
89
+ groups.set(key, [...groups.get(key) ?? [], row]);
90
+ }
91
+ return new Map([...groups].map(([key, group]) => [
92
+ key,
93
+ [...group].sort((a, b) => tierIndex(a) - tierIndex(b))
94
+ ]));
95
+ }
96
+ function loadedHolders(holders, tiers) {
97
+ return tiers.flatMap((tier) => {
98
+ const holder = holders.find((row) => row.source === tier);
99
+ return holder === undefined ? [] : [holder];
100
+ });
101
+ }
102
+ function sameItemGroups(holders, read) {
103
+ if (holders.length < 2)
104
+ return [holders];
105
+ const groups = [];
106
+ for (const row of holders) {
107
+ const bytes = read(row.path);
108
+ const text2 = bytes === null ? null : comparableText(bytes);
109
+ const group = text2 === null ? undefined : groups.find((candidate) => candidate.text === text2);
110
+ if (group === undefined)
111
+ groups.push({ text: text2, rows: [row] });
112
+ else
113
+ group.rows.push(row);
114
+ }
115
+ return groups.map((group) => group.rows);
116
+ }
117
+ function served(base, winner, decidedBy, groups) {
118
+ const same = groups.find((group) => group.includes(winner)) ?? [winner];
119
+ return {
120
+ ...base,
121
+ state: "served",
122
+ winner,
123
+ decidedBy,
124
+ copies: same.filter((row) => row !== winner),
125
+ overridden: base.loaded.filter((row) => !same.includes(row))
126
+ };
127
+ }
128
+ function claudeRank(row) {
129
+ return CLAUDE_SKILL_ORDER.indexOf(row.source);
130
+ }
131
+ function outrankingHolders(pinned, loaded, groups) {
132
+ if (pinned.kind !== "skill")
133
+ return [];
134
+ const same = groups.find((group) => group.includes(pinned)) ?? [pinned];
135
+ return loaded.filter((row) => claudeRank(row) < claudeRank(pinned) && !same.includes(row)).sort((a, b) => claudeRank(a) - claudeRank(b));
136
+ }
137
+ function workingPinTier(kind, loaded) {
138
+ const [first] = kind === "skill" ? [...loaded].sort((a, b) => claudeRank(a) - claudeRank(b)) : loaded;
139
+ return first !== undefined && isSkillTier(first.source) ? first.source : "project";
140
+ }
141
+ function resolveItem(holders, settings, tiers, read) {
142
+ const [first] = holders;
143
+ if (first === undefined)
144
+ throw new Error("resolveItem: a name with no holder");
145
+ const { kind, name } = first;
146
+ const pins = kind === "skill" ? settings.tiersSkills : settings.tiersAgents;
147
+ const base = {
148
+ kind,
149
+ name,
150
+ holders,
151
+ loaded: loadedHolders(holders, tiers),
152
+ pin: pins.get(name) ?? null
153
+ };
154
+ if (base.pin === false)
155
+ return { ...base, state: "off" };
156
+ const [nearest] = base.loaded;
157
+ if (nearest === undefined)
158
+ return { ...base, state: "unloaded" };
159
+ const groups = sameItemGroups(base.loaded, read);
160
+ const pinned = base.loaded.find((row) => row.source === base.pin);
161
+ const outrankedBy = pinned === undefined ? [] : outrankingHolders(pinned, base.loaded, groups);
162
+ if (pinned !== undefined && outrankedBy.length === 0)
163
+ return served(base, pinned, "pin", groups);
164
+ if (groups.length === 1)
165
+ return served(base, nearest, "order", groups);
166
+ const distinct = groups.flatMap((group) => group.slice(0, 1));
167
+ const setAsidePin = pinned !== undefined && isSkillTier(pinned.source) ? pinned.source : null;
168
+ return {
169
+ ...base,
170
+ state: "collision",
171
+ collision: {
172
+ kind,
173
+ name,
174
+ holders: distinct,
175
+ pinLine: pinLine(kind, name, workingPinTier(kind, base.loaded)),
176
+ setAsidePin,
177
+ outrankedBy
178
+ }
179
+ };
180
+ }
181
+ function resolveTiers(rows, settings, read) {
182
+ const tiers = loadedTiers(settings);
183
+ const items = [...groupByName(rows).values()].map((holders) => resolveItem(holders, settings, tiers, read)).sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) || a.name.localeCompare(b.name));
184
+ return {
185
+ loadedTiers: tiers,
186
+ items,
187
+ collisions: items.flatMap((item) => item.state === "collision" ? [item.collision] : [])
188
+ };
189
+ }
190
+ function findTierItem(resolution, kind, name) {
191
+ return resolution.items.find((item) => item.kind === kind && item.name === name);
192
+ }
193
+
194
+ // src/tiers/routing.ts
195
+ var DEFAULT_ROUTES = Object.freeze([
196
+ Object.freeze(["prose", "doc-updater"]),
197
+ Object.freeze(["tests", "tdd-guide"]),
198
+ Object.freeze(["repair", "build-error-resolver"]),
199
+ Object.freeze(["review", "code-reviewer"]),
200
+ Object.freeze(["implementation", "loop-implementer"])
201
+ ]);
202
+ var DEFAULT_ROUTING = new Map(DEFAULT_ROUTES);
203
+
204
+ // src/config-schema.ts
34
205
  var CONFIG_FILE = join(".rafa", "config.yaml");
206
+ function mapOf(value, expected) {
207
+ return (raw, at) => {
208
+ if (!isMapping(raw)) {
209
+ const problem = `${at.label} is ${describeValue(raw)}, expected a mapping of names to ${expected}`;
210
+ return { value: undefined, problems: [problem], extras: [] };
211
+ }
212
+ const readings = Object.entries(raw).map(([name, entry]) => [
213
+ name,
214
+ name.trim() === "" ? {
215
+ value: undefined,
216
+ problems: [`${at.label} names ${JSON.stringify(name)}, expected a non-empty name`],
217
+ extras: []
218
+ } : value(entry, { label: `${at.label}.${name}`, key: `${at.key}.${name}` })
219
+ ]);
220
+ const problems = readings.flatMap(([, reading]) => reading.problems);
221
+ const extras = readings.flatMap(([, reading]) => reading.extras);
222
+ if (problems.length > 0)
223
+ return { value: undefined, problems, extras };
224
+ const read = new Map;
225
+ for (const [name, reading] of readings) {
226
+ if (reading.value !== undefined)
227
+ read.set(name, reading.value);
228
+ }
229
+ return { value: read, problems, extras };
230
+ };
231
+ }
35
232
  var CONFIG_DEFAULTS = Object.freeze({
36
233
  version: 1,
37
234
  store: "sqlite",
@@ -63,11 +260,17 @@ var CONFIG_DEFAULTS = Object.freeze({
63
260
  cleanupStaleDays: 30,
64
261
  cleanupWorktreeIdleDays: 7,
65
262
  cleanupKeep: Object.freeze([]),
66
- dangerousAcceptStaleRefs: false
263
+ dangerousAcceptStaleRefs: false,
264
+ statusNotice: true,
265
+ tiersRafa: "on",
266
+ tiersSkills: new Map,
267
+ tiersAgents: new Map,
268
+ routing: DEFAULT_ROUTING
67
269
  });
68
270
  var directory = text("a directory path");
69
271
  var trackerKind = text("a tracker kind name");
70
272
  var releaseFile = text("a file path");
273
+ var tierPins = mapOf(tierPin, "false or a tier");
71
274
  var SETTINGS = {
72
275
  version: { key: "version", read: oneOf(CONFIG_VERSIONS), cli: false },
73
276
  store: { key: "store", read: oneOf(STORE_BACKENDS), cli: true },
@@ -154,6 +357,15 @@ var SETTINGS = {
154
357
  key: "dangerous.acceptStaleRefs",
155
358
  read: flag,
156
359
  cli: false
360
+ },
361
+ statusNotice: { key: "status.notice", read: flag, cli: false },
362
+ tiersRafa: { key: "tiers.rafa", read: tierSwitch, cli: false },
363
+ tiersSkills: { key: "tiers.skills", read: tierPins, cli: false },
364
+ tiersAgents: { key: "tiers.agents", read: tierPins, cli: false },
365
+ routing: {
366
+ key: "routing",
367
+ read: mapOf(routeTarget, "false or an agent name"),
368
+ cli: false
157
369
  }
158
370
  };
159
371
  var SETTING_NAMES = Object.keys(SETTINGS);
@@ -252,7 +464,12 @@ function readLayer(rawOf, atOf) {
252
464
  cleanupStaleDays: read("cleanupStaleDays"),
253
465
  cleanupWorktreeIdleDays: read("cleanupWorktreeIdleDays"),
254
466
  cleanupKeep: read("cleanupKeep"),
255
- dangerousAcceptStaleRefs: read("dangerousAcceptStaleRefs")
467
+ dangerousAcceptStaleRefs: read("dangerousAcceptStaleRefs"),
468
+ statusNotice: read("statusNotice"),
469
+ tiersRafa: read("tiersRafa"),
470
+ tiersSkills: read("tiersSkills"),
471
+ tiersAgents: read("tiersAgents"),
472
+ routing: read("routing")
256
473
  };
257
474
  return { layer, problems, extras };
258
475
  }
@@ -321,6 +538,28 @@ function parseConfigText(text2, path) {
321
538
  throw new ConfigError(all);
322
539
  return { path, values: read.layer, extras: [...sorted.extras, ...read.extras] };
323
540
  }
541
+ var MAP_SETTINGS = new Set([
542
+ "tiersSkills",
543
+ "tiersAgents",
544
+ "routing"
545
+ ]);
546
+ function mergeMaps(...layers) {
547
+ const merged = new Map;
548
+ for (const layer of layers) {
549
+ if (!(layer instanceof Map))
550
+ continue;
551
+ for (const [name, value] of layer)
552
+ merged.set(name, value);
553
+ }
554
+ return merged;
555
+ }
556
+ function unloadedPinWarnings(layer) {
557
+ const pins = [
558
+ ["tiers.skills", layer.values.tiersSkills],
559
+ ["tiers.agents", layer.values.tiersAgents]
560
+ ];
561
+ return pins.flatMap(([key, map]) => [...map ?? []].filter(([, pin]) => pin === "rafa").map(([name]) => `rafa config: ${key}.${name} in ${layer.path} pins the rafa tier, ` + "which is not loaded (tiers.rafa is off), so the pin has no effect"));
562
+ }
324
563
  function unknownKeyWarning(key, path) {
325
564
  const quoted = JSON.stringify(key);
326
565
  const [under, names] = knownKeysAbove(key);
@@ -339,15 +578,24 @@ function resolveConfig(layers = {}) {
339
578
  if (fromCli !== undefined)
340
579
  return { value: fromCli, source: "cli" };
341
580
  const fromFile = file?.values[setting];
581
+ const fromUser = user?.values[setting];
582
+ if (MAP_SETTINGS.has(setting)) {
583
+ const merged = mergeMaps(CONFIG_DEFAULTS[setting], fromUser, fromFile);
584
+ const source = fromFile !== undefined ? "file" : fromUser !== undefined ? "user" : "default";
585
+ return { value: merged, source };
586
+ }
342
587
  if (fromFile !== undefined)
343
588
  return { value: fromFile, source: "file" };
344
- const fromUser = user?.values[setting];
345
589
  if (fromUser !== undefined)
346
590
  return { value: fromUser, source: "user" };
347
591
  return { value: CONFIG_DEFAULTS[setting], source: "default" };
348
592
  };
349
593
  const ranked = SETTING_NAMES.map((setting) => [setting, rank(setting)]);
350
- const warnings = [user, file].flatMap((layer) => layer === null ? [] : layer.extras.map((extra) => unknownKeyWarning(extra.key, layer.path)));
594
+ const rafaLoaded = ranked.find(([setting]) => setting === "tiersRafa")?.[1].value !== "off";
595
+ const warnings = [user, file].flatMap((layer) => layer === null ? [] : [
596
+ ...layer.extras.map((extra) => unknownKeyWarning(extra.key, layer.path)),
597
+ ...rafaLoaded ? [] : unloadedPinWarnings(layer)
598
+ ]);
351
599
  return {
352
600
  config: Object.fromEntries(ranked.map(([setting, { value }]) => [setting, value])),
353
601
  sources: Object.fromEntries(ranked.map(([setting, { source }]) => [setting, source])),
@@ -525,4 +773,4 @@ function activeOutputMode() {
525
773
  return activeMode;
526
774
  }
527
775
 
528
- export { CONFIG_FILE, CONFIG_DEFAULTS, SETTINGS, ConfigError, configFilePath, parseConfigText, resolveConfig, createTextOutput, activeOutput, activeOutputMode, RAFA_BLOCK_KINDS, isRafaBlockKind, readRafaBlocks };
776
+ export { pinKey, pinLine, collisionMessage, readItemBytes, resolveTiers, findTierItem, DEFAULT_ROUTES, DEFAULT_ROUTING, CONFIG_FILE, CONFIG_DEFAULTS, SETTINGS, ConfigError, configFilePath, parseConfigText, resolveConfig, createTextOutput, activeOutput, activeOutputMode, RAFA_BLOCK_KINDS, isRafaBlockKind, readRafaBlocks };
@@ -4,10 +4,10 @@ import {
4
4
  commandSpelling,
5
5
  isRoutingWord,
6
6
  isTopLevel
7
- } from "./index-p901zs2p.js";
7
+ } from "./index-ddf85qac.js";
8
8
  import {
9
9
  describeValue
10
- } from "./index-n3rw8rhr.js";
10
+ } from "./index-944pncd8.js";
11
11
 
12
12
  // src/cli/registry.ts
13
13
  var REFUSAL = "command registry";
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  start,
21
21
  usage,
22
22
  validateManifest
23
- } from "./index-1vtd5fr7.js";
23
+ } from "./index-6vkd153j.js";
24
24
  import {
25
25
  FINDING_KINDS,
26
26
  FINDING_SIGNALS,
@@ -38,7 +38,7 @@ import {
38
38
  renderInjection,
39
39
  runPreflight,
40
40
  runShellProbe
41
- } from "./index-krjzghpz.js";
41
+ } from "./index-0pdsq2hy.js";
42
42
  import {
43
43
  CORE_ADAPTER_REGISTRY,
44
44
  EFFORT_KEY_PROJECTIONS,
@@ -47,7 +47,7 @@ import {
47
47
  openNdjsonStore,
48
48
  openSqliteStore,
49
49
  selectEffortStore
50
- } from "./index-q491ekzm.js";
50
+ } from "./index-at9f1z2z.js";
51
51
  import {
52
52
  CONFIG_DEFAULTS,
53
53
  CONFIG_FILE,
@@ -58,9 +58,9 @@ import {
58
58
  parseConfigText,
59
59
  readRafaBlocks,
60
60
  resolveConfig
61
- } from "./index-6cw1n0qy.js";
62
- import"./index-qzhpqmfy.js";
63
- import"./index-p901zs2p.js";
61
+ } from "./index-jva32qe6.js";
62
+ import"./index-kvnggvyk.js";
63
+ import"./index-ddf85qac.js";
64
64
  import {
65
65
  CLAUDE_SETTING_SOURCES,
66
66
  CONFIG_VERSIONS,
@@ -71,7 +71,7 @@ import {
71
71
  PR_PROVIDERS,
72
72
  RELEASE_AUTO,
73
73
  STORE_BACKENDS
74
- } from "./index-n3rw8rhr.js";
74
+ } from "./index-944pncd8.js";
75
75
  export {
76
76
  validateManifest,
77
77
  usage as usageCommand,
@@ -16,15 +16,15 @@ import {
16
16
  renderInjection,
17
17
  runPreflight,
18
18
  runShellProbe
19
- } from "../index-krjzghpz.js";
19
+ } from "../index-0pdsq2hy.js";
20
20
  import {
21
21
  RAFA_BLOCK_KINDS,
22
22
  isRafaBlockKind,
23
23
  readRafaBlocks
24
- } from "../index-6cw1n0qy.js";
24
+ } from "../index-jva32qe6.js";
25
25
  import {
26
26
  INJECT_MODES
27
- } from "../index-n3rw8rhr.js";
27
+ } from "../index-944pncd8.js";
28
28
  export {
29
29
  runShellProbe,
30
30
  runPreflight,
@@ -66,11 +66,15 @@ criteria — **do not execute the plan**.
66
66
  as a checklist in the PREREQUISITES.md format of the dev-planner
67
67
  specification below. Only genuinely non-automatable steps; do not duplicate
68
68
  what `README.md`/`AGENTS.md` already require of every contributor. Link to
69
- it from the plan.
69
+ it from the plan. An `[auto]` item holds exactly one backticked span, and
70
+ that span is its command, ending the item after `: ` and run as written:
71
+ prove a tool with `<tool> --version`, `<tool> --help` or `which <tool>`.
70
72
  * Do not create or touch any tracker file — the loop derives it from the plan.
71
73
 
72
74
  {PLAN_FORMAT}
73
75
 
76
+ {ROUTING}
77
+
74
78
  ## Spec
75
79
 
76
80
  {SPEC_CONTENT}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-tomato/rafa",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Alpha. Runs a plan through the ralph loop: one Claude Code session per task, one commit per task, then a pull request and a CI wait.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "bun": ">=1.3.14"
48
48
  },
49
49
  "scripts": {
50
- "build": "rm -rf dist && bun build src/rafa.ts --target=bun --outfile=dist/cli.js && bun build src/index.ts src/plan/index.ts src/effort/store/index.ts src/ports/index.ts --target=bun --splitting --root=src --outdir=dist && cp src/PROMPT.md src/plan-prompt.md .claude/skills/dev-planner/SKILL.md dist/ && for tree in pr/plans:plans board/templates:templates; do src=src/${tree%:*}; out=dist/${tree#*:}; [ -d \"$src\" ] || continue; mkdir -p \"$out\" && cp \"$src\"/*.md \"$out\"/ || exit 1; done",
50
+ "build": "rm -rf dist && bun build src/rafa.ts --target=bun --outfile=dist/cli.js && bun build src/index.ts src/plan/index.ts src/effort/store/index.ts src/ports/index.ts --target=bun --splitting --root=src --outdir=dist && cp src/PROMPT.md src/plan-prompt.md dist/ && cp -R src/bundled dist/bundled && bun build src/tools/ts-symbols/cli.ts --target=bun --outfile=dist/bundled/bin/ts-symbols && for tree in pr/plans:plans board/templates:templates; do src=src/${tree%:*}; out=dist/${tree#*:}; [ -d \"$src\" ] || continue; mkdir -p \"$out\" && cp \"$src\"/*.md \"$out\"/ || exit 1; done",
51
51
  "prepack": "bun run build",
52
52
  "test": "bun test",
53
53
  "lint": "eslint .",