@open-tomato/rafa 0.17.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",
@@ -64,11 +261,16 @@ var CONFIG_DEFAULTS = Object.freeze({
64
261
  cleanupWorktreeIdleDays: 7,
65
262
  cleanupKeep: Object.freeze([]),
66
263
  dangerousAcceptStaleRefs: false,
67
- statusNotice: true
264
+ statusNotice: true,
265
+ tiersRafa: "on",
266
+ tiersSkills: new Map,
267
+ tiersAgents: new Map,
268
+ routing: DEFAULT_ROUTING
68
269
  });
69
270
  var directory = text("a directory path");
70
271
  var trackerKind = text("a tracker kind name");
71
272
  var releaseFile = text("a file path");
273
+ var tierPins = mapOf(tierPin, "false or a tier");
72
274
  var SETTINGS = {
73
275
  version: { key: "version", read: oneOf(CONFIG_VERSIONS), cli: false },
74
276
  store: { key: "store", read: oneOf(STORE_BACKENDS), cli: true },
@@ -156,7 +358,15 @@ var SETTINGS = {
156
358
  read: flag,
157
359
  cli: false
158
360
  },
159
- statusNotice: { key: "status.notice", read: flag, cli: false }
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
369
+ }
160
370
  };
161
371
  var SETTING_NAMES = Object.keys(SETTINGS);
162
372
  var SETTING_BY_KEY = new Map(SETTING_NAMES.map((setting) => [
@@ -255,7 +465,11 @@ function readLayer(rawOf, atOf) {
255
465
  cleanupWorktreeIdleDays: read("cleanupWorktreeIdleDays"),
256
466
  cleanupKeep: read("cleanupKeep"),
257
467
  dangerousAcceptStaleRefs: read("dangerousAcceptStaleRefs"),
258
- statusNotice: read("statusNotice")
468
+ statusNotice: read("statusNotice"),
469
+ tiersRafa: read("tiersRafa"),
470
+ tiersSkills: read("tiersSkills"),
471
+ tiersAgents: read("tiersAgents"),
472
+ routing: read("routing")
259
473
  };
260
474
  return { layer, problems, extras };
261
475
  }
@@ -324,6 +538,28 @@ function parseConfigText(text2, path) {
324
538
  throw new ConfigError(all);
325
539
  return { path, values: read.layer, extras: [...sorted.extras, ...read.extras] };
326
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
+ }
327
563
  function unknownKeyWarning(key, path) {
328
564
  const quoted = JSON.stringify(key);
329
565
  const [under, names] = knownKeysAbove(key);
@@ -342,15 +578,24 @@ function resolveConfig(layers = {}) {
342
578
  if (fromCli !== undefined)
343
579
  return { value: fromCli, source: "cli" };
344
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
+ }
345
587
  if (fromFile !== undefined)
346
588
  return { value: fromFile, source: "file" };
347
- const fromUser = user?.values[setting];
348
589
  if (fromUser !== undefined)
349
590
  return { value: fromUser, source: "user" };
350
591
  return { value: CONFIG_DEFAULTS[setting], source: "default" };
351
592
  };
352
593
  const ranked = SETTING_NAMES.map((setting) => [setting, rank(setting)]);
353
- 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
+ ]);
354
599
  return {
355
600
  config: Object.fromEntries(ranked.map(([setting, { value }]) => [setting, value])),
356
601
  sources: Object.fromEntries(ranked.map(([setting, { source }]) => [setting, source])),
@@ -528,4 +773,4 @@ function activeOutputMode() {
528
773
  return activeMode;
529
774
  }
530
775
 
531
- 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-4qd537z6.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-64wec0k7.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-7t0qz29r.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-ps5sac9p.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-64wec0k7.js";
19
+ } from "../index-0pdsq2hy.js";
20
20
  import {
21
21
  RAFA_BLOCK_KINDS,
22
22
  isRafaBlockKind,
23
23
  readRafaBlocks
24
- } from "../index-ps5sac9p.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,
@@ -73,6 +73,8 @@ criteria — **do not execute the plan**.
73
73
 
74
74
  {PLAN_FORMAT}
75
75
 
76
+ {ROUTING}
77
+
76
78
  ## Spec
77
79
 
78
80
  {SPEC_CONTENT}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-tomato/rafa",
3
- "version": "0.17.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 .",