@open-tomato/rafa 0.9.1 → 0.10.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.
- package/README.md +3 -0
- package/dist/SKILL.md +21 -0
- package/dist/cli.js +171 -34
- package/dist/effort/store/index.js +1 -1
- package/dist/{index-eqsm86n7.js → index-srdtw7w1.js} +68 -2
- package/dist/index.js +12 -26
- package/dist/plan-prompt.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,8 @@ Read this once; rafa also says it the first time you start a run.
|
|
|
54
54
|
| `rafa skill backfill --propose` | 🪙 one session per batch of skills; without `--propose`, nothing |
|
|
55
55
|
| `rafa next` | 🪙 when the step it runs is one of the above; it asks before each step |
|
|
56
56
|
|
|
57
|
+
The same 🪙 marks appear in `rafa --help` at all levels and in `rafa describe` output.
|
|
58
|
+
|
|
57
59
|
Everything else reads files, git and GitHub and spends nothing,
|
|
58
60
|
`rafa usage` included (it reads `CLAUDE_USAGE_PERCENT` and asks
|
|
59
61
|
nobody). `rafa loop resume` starts no session itself, but it lets a
|
|
@@ -330,6 +332,7 @@ built. A box is ticked by the change that finishes the feature.
|
|
|
330
332
|
- [x] Start a plan from the main branch and rafa makes the branch for you
|
|
331
333
|
- [x] One command takes you to the next step: merge, clean up, plan,
|
|
332
334
|
branch, start
|
|
335
|
+
- [x] Every command that spends Claude usage says so in its help
|
|
333
336
|
- [ ] The right skills reach the right task, chosen when the plan is
|
|
334
337
|
written
|
|
335
338
|
- [ ] Know which skills earn their place and which are ignored
|
package/dist/SKILL.md
CHANGED
|
@@ -471,6 +471,27 @@ The report rules in this skill restate `src/report/parse.ts`, which is the autho
|
|
|
471
471
|
|
|
472
472
|
---
|
|
473
473
|
|
|
474
|
+
## Command-development rule: Claude session declaration
|
|
475
|
+
|
|
476
|
+
When planning work on rafa commands: every command that can start a Claude
|
|
477
|
+
Code session must declare its `spends` field on its `RafaCommand` definition.
|
|
478
|
+
The field tells the help system, `describe` output, and the spawn guard in
|
|
479
|
+
`src/utils/claude.ts` what Claude usage the command declares. It is optional,
|
|
480
|
+
and takes one of four shapes:
|
|
481
|
+
|
|
482
|
+
- `{ when: 'always'; what: string }` — the command always starts a session
|
|
483
|
+
- `{ when: 'with'; flag: string; what: string }` — starts a session only when
|
|
484
|
+
the named flag is passed (flag written as `--resolve`, matched by name)
|
|
485
|
+
- `{ when: 'unless'; flag: string; what: string }` — starts a session unless
|
|
486
|
+
the named flag is passed
|
|
487
|
+
- `{ when: 'through'; what: string }` — may start a session through a
|
|
488
|
+
sub-command it calls
|
|
489
|
+
|
|
490
|
+
The `what` field is a short phrase describing the session (e.g., "one planning
|
|
491
|
+
session"). See `context/cli.md` for the command architecture and examples.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
474
495
|
## Examples
|
|
475
496
|
|
|
476
497
|
Each module task below carries its own unit tests, and each stage closes on a test task over what its modules do together.
|
package/dist/cli.js
CHANGED
|
@@ -877,6 +877,47 @@ function resolveScope(start, seams) {
|
|
|
877
877
|
return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
// src/cli/spends.ts
|
|
881
|
+
var SPENDS_GLYPH = "\uD83E\uDE99";
|
|
882
|
+
var SPENDS_WHEN = ["always", "with", "unless", "through"];
|
|
883
|
+
var FLAGGED = ["with", "unless"];
|
|
884
|
+
var FLAG_SPELLING = /^--[^\s-]\S*$/;
|
|
885
|
+
function flagProblem(when, flag2) {
|
|
886
|
+
if (FLAGGED.includes(when)) {
|
|
887
|
+
return typeof flag2 === "string" && FLAG_SPELLING.test(flag2) ? null : `spends.flag is ${describeValue(flag2)}, expected a flag as typed, starting with --`;
|
|
888
|
+
}
|
|
889
|
+
return flag2 === undefined ? null : `spends.flag is ${describeValue(flag2)}, expected absent when spends.when is "${when}"`;
|
|
890
|
+
}
|
|
891
|
+
function spendsProblem(value) {
|
|
892
|
+
if (value === undefined)
|
|
893
|
+
return null;
|
|
894
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
895
|
+
return `spends is ${describeValue(value)}, expected absent or a mapping`;
|
|
896
|
+
}
|
|
897
|
+
const { when, what, flag: flag2 } = value;
|
|
898
|
+
if (!SPENDS_WHEN.includes(when)) {
|
|
899
|
+
return `spends.when is ${describeValue(when)}, expected one of ${SPENDS_WHEN.join(", ")}`;
|
|
900
|
+
}
|
|
901
|
+
if (typeof what !== "string") {
|
|
902
|
+
return `spends.what is ${describeValue(what)}, expected a string`;
|
|
903
|
+
}
|
|
904
|
+
return flagProblem(when, flag2);
|
|
905
|
+
}
|
|
906
|
+
function spendsCondition(spend) {
|
|
907
|
+
switch (spend.when) {
|
|
908
|
+
case "with":
|
|
909
|
+
case "unless":
|
|
910
|
+
return `${spend.when} ${spend.flag}`;
|
|
911
|
+
case "always":
|
|
912
|
+
case "through":
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function spendsMark(spend) {
|
|
917
|
+
const condition = spendsCondition(spend);
|
|
918
|
+
return condition === null ? SPENDS_GLYPH : `${SPENDS_GLYPH} ${condition}`;
|
|
919
|
+
}
|
|
920
|
+
|
|
880
921
|
// src/cli/command.ts
|
|
881
922
|
var COMMAND_OUTPUTS = ["text", "json", "tui"];
|
|
882
923
|
var MAX_EXIT_CODE = 255;
|
|
@@ -955,7 +996,7 @@ function commandProblem(value) {
|
|
|
955
996
|
return `a command is ${describeValue(value)}, expected a mapping`;
|
|
956
997
|
}
|
|
957
998
|
const fields = value;
|
|
958
|
-
const problem = fieldProblem(fields);
|
|
999
|
+
const problem = fieldProblem(fields) ?? spendsProblem(fields.spends);
|
|
959
1000
|
if (problem === null)
|
|
960
1001
|
return null;
|
|
961
1002
|
const named = isRoutingWord(fields.subject) && isRoutingWord(fields.action) ? `command "${commandSpelling(fields)}"` : "a command";
|
|
@@ -1532,9 +1573,23 @@ function routeLine(registry, argv) {
|
|
|
1532
1573
|
};
|
|
1533
1574
|
}
|
|
1534
1575
|
|
|
1576
|
+
// src/cli/running.ts
|
|
1577
|
+
var running = null;
|
|
1578
|
+
function setRunningCommand(command, flags) {
|
|
1579
|
+
const previous = running;
|
|
1580
|
+
running = Object.freeze({ command, flags: Object.freeze({ ...flags }) });
|
|
1581
|
+
return previous;
|
|
1582
|
+
}
|
|
1583
|
+
function runningCommand() {
|
|
1584
|
+
return running;
|
|
1585
|
+
}
|
|
1586
|
+
function restoreRunningCommand(previous) {
|
|
1587
|
+
running = previous;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1535
1590
|
// package.json
|
|
1536
1591
|
var name = "@open-tomato/rafa";
|
|
1537
|
-
var version = "0.
|
|
1592
|
+
var version = "0.10.0";
|
|
1538
1593
|
|
|
1539
1594
|
// src/cli/version.ts
|
|
1540
1595
|
var RAFA_VERSION = version;
|
|
@@ -1674,6 +1729,7 @@ async function runCommand(route, base, registry, project, settings) {
|
|
|
1674
1729
|
const previous = activeOutput();
|
|
1675
1730
|
const previousMode = activeOutputMode();
|
|
1676
1731
|
setActiveOutput(guarded.output, base.outputMode);
|
|
1732
|
+
const previousRunning = setRunningCommand(route.command, base.flags);
|
|
1677
1733
|
try {
|
|
1678
1734
|
await route.command.run(context);
|
|
1679
1735
|
return success(guarded.payload());
|
|
@@ -1698,6 +1754,7 @@ async function runCommand(route, base, registry, project, settings) {
|
|
|
1698
1754
|
stderrLine: error.message === "" ? null : error.message
|
|
1699
1755
|
};
|
|
1700
1756
|
} finally {
|
|
1757
|
+
restoreRunningCommand(previousRunning);
|
|
1701
1758
|
setActiveOutput(previous, previousMode);
|
|
1702
1759
|
}
|
|
1703
1760
|
}
|
|
@@ -1800,15 +1857,19 @@ var GLOBAL_FLAGS = Object.freeze([
|
|
|
1800
1857
|
{ spelling: "-v, --verbose", note: "repeat for more, up to 3; --verbose=N (also RAFA_VERBOSITY=N)" },
|
|
1801
1858
|
{ spelling: "--version", note: 'print "rafa <version>" and exit; typed alone, no short form' }
|
|
1802
1859
|
]);
|
|
1860
|
+
var SPENDS_LEGEND = `${SPENDS_GLYPH} starts Claude Code sessions, which spend your Claude usage`;
|
|
1803
1861
|
var INDENT = " ";
|
|
1804
1862
|
var NOTE_INDENT = " ";
|
|
1805
1863
|
var USAGE_CONTINUATION = " ";
|
|
1806
1864
|
var GAP = 3;
|
|
1807
1865
|
var MIN_TEXT_WIDTH = 20;
|
|
1808
|
-
function
|
|
1866
|
+
function wordsOf(text2) {
|
|
1867
|
+
return text2.split(/\s+/).filter((part) => part !== "");
|
|
1868
|
+
}
|
|
1869
|
+
function pack(words, first, rest) {
|
|
1809
1870
|
const lines = [];
|
|
1810
1871
|
let line = "";
|
|
1811
|
-
for (const word of
|
|
1872
|
+
for (const word of words) {
|
|
1812
1873
|
const width = lines.length === 0 ? first : rest;
|
|
1813
1874
|
if (line === "") {
|
|
1814
1875
|
line = word;
|
|
@@ -1821,17 +1882,38 @@ function pack(text2, first, rest) {
|
|
|
1821
1882
|
}
|
|
1822
1883
|
return line === "" ? lines : [...lines, line];
|
|
1823
1884
|
}
|
|
1824
|
-
function
|
|
1885
|
+
function hangWords(prefix, words, indent = prefix) {
|
|
1825
1886
|
const width = (used) => Math.max(HELP_WIDTH - used.length, MIN_TEXT_WIDTH);
|
|
1826
|
-
return pack(
|
|
1887
|
+
return pack(words, width(prefix), width(indent)).map((line, index) => index === 0 ? `${prefix}${line}` : `${indent}${line}`);
|
|
1888
|
+
}
|
|
1889
|
+
function hang(prefix, text2, indent = prefix) {
|
|
1890
|
+
return hangWords(prefix, wordsOf(text2), indent);
|
|
1827
1891
|
}
|
|
1828
1892
|
function columnOf(rows) {
|
|
1829
1893
|
const lefts = rows.filter(([, right]) => right !== "").map(([left]) => left.length);
|
|
1830
1894
|
return INDENT.length + Math.max(0, ...lefts) + GAP;
|
|
1831
1895
|
}
|
|
1832
|
-
function row([left, right], column) {
|
|
1896
|
+
function row([left, right, mark = null], column) {
|
|
1833
1897
|
const head = `${INDENT}${left}`;
|
|
1834
|
-
|
|
1898
|
+
const words = mark === null ? wordsOf(right) : [...wordsOf(right), mark];
|
|
1899
|
+
return words.length === 0 ? [head] : hangWords(head.padEnd(column), words, " ".repeat(column));
|
|
1900
|
+
}
|
|
1901
|
+
function markOf(command) {
|
|
1902
|
+
return command.spends === undefined ? null : spendsMark(command.spends);
|
|
1903
|
+
}
|
|
1904
|
+
function subjectMark(subject, registry) {
|
|
1905
|
+
return registry.actionsOf(subject.name).some((action) => action.spends !== undefined) ? SPENDS_GLYPH : null;
|
|
1906
|
+
}
|
|
1907
|
+
function commandWord(command) {
|
|
1908
|
+
const mark = markOf(command);
|
|
1909
|
+
return mark === null ? command.subject : `${command.subject} ${mark}`;
|
|
1910
|
+
}
|
|
1911
|
+
function spendsLines(command) {
|
|
1912
|
+
const { spends } = command;
|
|
1913
|
+
if (spends === undefined)
|
|
1914
|
+
return [];
|
|
1915
|
+
const mark = spendsCondition(spends) === null ? SPENDS_GLYPH : `${spendsMark(spends)}:`;
|
|
1916
|
+
return hangWords(INDENT, [mark, ...wordsOf(spends.what)]);
|
|
1835
1917
|
}
|
|
1836
1918
|
function rows(entries) {
|
|
1837
1919
|
const column = columnOf(entries);
|
|
@@ -1879,9 +1961,10 @@ function renderRoot(registry) {
|
|
|
1879
1961
|
["rafa <subject> <action> --help", "arguments, flags, examples"]
|
|
1880
1962
|
])),
|
|
1881
1963
|
block("Quick start", quickStart.map((example) => `${INDENT}${example.cmd}`)),
|
|
1882
|
-
block("Subjects", rows(subjects.map((subject) => [subject.name, subject.summary]))),
|
|
1883
|
-
block("Commands",
|
|
1884
|
-
block("Global flags", rows(GLOBAL_FLAGS.map((flag2) => [flag2.spelling, flag2.note])))
|
|
1964
|
+
block("Subjects", rows(subjects.map((subject) => [subject.name, subject.summary, subjectMark(subject, registry)]))),
|
|
1965
|
+
block("Commands", hangWords(INDENT, topLevel.map((command, index) => index === topLevel.length - 1 ? commandWord(command) : `${commandWord(command)},`))),
|
|
1966
|
+
block("Global flags", rows(GLOBAL_FLAGS.map((flag2) => [flag2.spelling, flag2.note]))),
|
|
1967
|
+
registry.commands().some((command) => command.spends !== undefined) ? [SPENDS_LEGEND] : []
|
|
1885
1968
|
]);
|
|
1886
1969
|
}
|
|
1887
1970
|
function examplesAcross(actions, count) {
|
|
@@ -1898,7 +1981,7 @@ function renderSubject(subject, registry) {
|
|
|
1898
1981
|
[`${spelling} <action> [args] [flags]`, ""],
|
|
1899
1982
|
[`${spelling} <action> --help`, "arguments, flags, examples"]
|
|
1900
1983
|
])),
|
|
1901
|
-
block("Actions", rows(actions.map((action) => [action.action, action.summary]))),
|
|
1984
|
+
block("Actions", rows(actions.map((action) => [action.action, action.summary, markOf(action)]))),
|
|
1902
1985
|
block("Examples", examplesAcross(actions, SUBJECT_EXAMPLES).flatMap(exampleLines))
|
|
1903
1986
|
]);
|
|
1904
1987
|
}
|
|
@@ -1948,6 +2031,7 @@ function renderAction(request, registry) {
|
|
|
1948
2031
|
block("Deprecated", deprecated === undefined ? [] : hang(INDENT, `since ${deprecated.since}; use "rafa ${deprecated.use}"`)),
|
|
1949
2032
|
block("Usage", hang(INDENT, usage, USAGE_CONTINUATION)),
|
|
1950
2033
|
block("Description", hang(INDENT, command.description)),
|
|
2034
|
+
block("Spends", spendsLines(command)),
|
|
1951
2035
|
block("Arguments", table(command.args.map(argRow))),
|
|
1952
2036
|
block("Flags", table(command.flags.map(flagRow))),
|
|
1953
2037
|
block("Examples", command.examples.flatMap(exampleLines)),
|
|
@@ -3076,7 +3160,7 @@ function typedAlias(alias) {
|
|
|
3076
3160
|
return alias.trim().split(/\s+/).join(" ");
|
|
3077
3161
|
}
|
|
3078
3162
|
function describeCommand(command, name2, module) {
|
|
3079
|
-
const { deprecated } = command;
|
|
3163
|
+
const { deprecated, spends } = command;
|
|
3080
3164
|
return {
|
|
3081
3165
|
name: name2,
|
|
3082
3166
|
summary: command.summary,
|
|
@@ -3087,7 +3171,8 @@ function describeCommand(command, name2, module) {
|
|
|
3087
3171
|
outputs: [...command.outputs],
|
|
3088
3172
|
aliases: module === null ? (command.aliases ?? []).map(typedAlias) : [],
|
|
3089
3173
|
deprecated: deprecated === undefined ? null : { since: deprecated.since, use: deprecated.use },
|
|
3090
|
-
module
|
|
3174
|
+
module,
|
|
3175
|
+
spends: spends === undefined ? null : { ...spends }
|
|
3091
3176
|
};
|
|
3092
3177
|
}
|
|
3093
3178
|
function mountedThrough(exec, registry) {
|
|
@@ -3553,7 +3638,7 @@ function requireIssueNumber(issue) {
|
|
|
3553
3638
|
}
|
|
3554
3639
|
return issue;
|
|
3555
3640
|
}
|
|
3556
|
-
function
|
|
3641
|
+
function wordsOf2(title2) {
|
|
3557
3642
|
return title2.toLowerCase().replace(NON_WORD, " ").split(" ").filter((word) => word !== "");
|
|
3558
3643
|
}
|
|
3559
3644
|
function withoutStopWords(words) {
|
|
@@ -3563,8 +3648,8 @@ function withoutStopWords(words) {
|
|
|
3563
3648
|
function slugFromTitle(title2) {
|
|
3564
3649
|
const withoutId = title2.replace(LEADING_ID, "");
|
|
3565
3650
|
const [firstClause = ""] = withoutId.split(CLAUSE_BREAK);
|
|
3566
|
-
const whole = withoutStopWords(
|
|
3567
|
-
const clause = withoutStopWords(
|
|
3651
|
+
const whole = withoutStopWords(wordsOf2(withoutId));
|
|
3652
|
+
const clause = withoutStopWords(wordsOf2(firstClause));
|
|
3568
3653
|
const chosen = clause.length >= MIN_SLUG_WORDS ? clause : whole;
|
|
3569
3654
|
if (chosen.length === 0)
|
|
3570
3655
|
return FALLBACK_SLUG;
|
|
@@ -8048,7 +8133,7 @@ function payloadProblem(payload) {
|
|
|
8048
8133
|
function heldInstinctProblem(value) {
|
|
8049
8134
|
return fieldsProblem(value, HELD_INSTINCT_CHECKS, "the line") ?? fieldsProblem(value.instinct, INSTINCT_CHECKS, "the instinct");
|
|
8050
8135
|
}
|
|
8051
|
-
function
|
|
8136
|
+
function flagProblem2(value) {
|
|
8052
8137
|
return fieldsProblem(value, FLAG_CHECKS, "the flag");
|
|
8053
8138
|
}
|
|
8054
8139
|
function copyInstinct(record) {
|
|
@@ -8127,7 +8212,7 @@ function createLocalLearning(options) {
|
|
|
8127
8212
|
async function flaggedIds() {
|
|
8128
8213
|
const lines = numberedLines(await readIfPresent2(flagsPath));
|
|
8129
8214
|
return new Set(lines.map(([number, line]) => {
|
|
8130
|
-
const reading2 = readLine2(line,
|
|
8215
|
+
const reading2 = readLine2(line, flagProblem2);
|
|
8131
8216
|
if (reading2.ok)
|
|
8132
8217
|
return reading2.value.id;
|
|
8133
8218
|
throw new Error(`${PREFIX7}: line ${number} of ${flagsPath} is not a flag,` + ` and a bundle skipping it could hold an instinct it flags: ${reading2.problem}`);
|
|
@@ -8161,7 +8246,7 @@ function createLocalLearning(options) {
|
|
|
8161
8246
|
},
|
|
8162
8247
|
flag: async (id, reason) => {
|
|
8163
8248
|
const flag2 = { id, reason, flagged_at: now() };
|
|
8164
|
-
const problem =
|
|
8249
|
+
const problem = flagProblem2(flag2);
|
|
8165
8250
|
if (problem !== null) {
|
|
8166
8251
|
throw new TypeError(`${PREFIX7}: refused to write an invalid flag: ${problem}`);
|
|
8167
8252
|
}
|
|
@@ -8394,9 +8479,44 @@ function claudeSessionEnv() {
|
|
|
8394
8479
|
CLAUDE_CODE_ENTRYPOINT: "cli"
|
|
8395
8480
|
};
|
|
8396
8481
|
}
|
|
8482
|
+
|
|
8483
|
+
class UndeclaredSpendError extends Error {
|
|
8484
|
+
name = "UndeclaredSpendError";
|
|
8485
|
+
}
|
|
8486
|
+
function carriesFlag(flags, flag2) {
|
|
8487
|
+
const name2 = flag2.replace(/^--/, "");
|
|
8488
|
+
const value = flags[name2];
|
|
8489
|
+
if (value !== undefined && value !== false)
|
|
8490
|
+
return true;
|
|
8491
|
+
return name2.startsWith("no-") && flags[name2.slice(3)] === false;
|
|
8492
|
+
}
|
|
8493
|
+
function spendRefusal(running2) {
|
|
8494
|
+
if (running2 === null)
|
|
8495
|
+
return null;
|
|
8496
|
+
const { command, flags } = running2;
|
|
8497
|
+
const spend = command.spends;
|
|
8498
|
+
const named = `rafa ${commandSpelling(command)}`;
|
|
8499
|
+
const remedy = "declare spends on the command to cover this run";
|
|
8500
|
+
if (spend === undefined) {
|
|
8501
|
+
return `${named} started a Claude session but declares no spends: ${remedy}`;
|
|
8502
|
+
}
|
|
8503
|
+
if (spend.when === "with" && !carriesFlag(flags, spend.flag)) {
|
|
8504
|
+
return `${named} started a Claude session without ${spend.flag}, and its spends covers only a run with ${spend.flag}: ${remedy}`;
|
|
8505
|
+
}
|
|
8506
|
+
if (spend.when === "unless" && carriesFlag(flags, spend.flag)) {
|
|
8507
|
+
return `${named} started a Claude session with ${spend.flag}, and its spends covers no run with ${spend.flag}: ${remedy}`;
|
|
8508
|
+
}
|
|
8509
|
+
return null;
|
|
8510
|
+
}
|
|
8511
|
+
function guardSpend() {
|
|
8512
|
+
const refusal3 = spendRefusal(runningCommand());
|
|
8513
|
+
if (refusal3 !== null)
|
|
8514
|
+
throw new UndeclaredSpendError(refusal3);
|
|
8515
|
+
}
|
|
8397
8516
|
async function spawnClaude(args, prompt) {
|
|
8517
|
+
guardSpend();
|
|
8398
8518
|
if (activeOutputMode() === "json") {
|
|
8399
|
-
const { exitCode } = await
|
|
8519
|
+
const { exitCode } = await spawnCaptured(args, prompt);
|
|
8400
8520
|
return exitCode;
|
|
8401
8521
|
}
|
|
8402
8522
|
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
@@ -8448,6 +8568,10 @@ async function teeToOperator(stream) {
|
|
|
8448
8568
|
return text2 + tail;
|
|
8449
8569
|
}
|
|
8450
8570
|
async function spawnClaudeCaptured(args, prompt) {
|
|
8571
|
+
guardSpend();
|
|
8572
|
+
return spawnCaptured(args, prompt);
|
|
8573
|
+
}
|
|
8574
|
+
async function spawnCaptured(args, prompt) {
|
|
8451
8575
|
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
8452
8576
|
stdin: new TextEncoder().encode(prompt),
|
|
8453
8577
|
stdout: "pipe",
|
|
@@ -16390,13 +16514,18 @@ function openNextSources(context, seams = DEFAULT_NEXT_SOURCE_SEAMS) {
|
|
|
16390
16514
|
const git = (seams.openGit ?? createGitRunner)(project.root);
|
|
16391
16515
|
const gh = (seams.openGh ?? ((root) => createGhRunner({ cwd: root })))(project.root);
|
|
16392
16516
|
const loop = resolveLoopSeams({ isAlive: seams.isAlive });
|
|
16393
|
-
|
|
16517
|
+
const board = () => ghNextBoard({ gh, git, configured: config.roadmapIssue });
|
|
16518
|
+
const held = {
|
|
16394
16519
|
base: config.prBase ?? DEFAULT_BASE_BRANCH,
|
|
16395
16520
|
plans: plansDirAt(project.root, config.planDir),
|
|
16396
16521
|
runs: () => readRecords(project.root, loop),
|
|
16397
16522
|
git,
|
|
16398
|
-
pulls: (seams.pullRequests ?? ghPullRequestsIn)(project.root)
|
|
16399
|
-
|
|
16523
|
+
pulls: (seams.pullRequests ?? ghPullRequestsIn)(project.root)
|
|
16524
|
+
};
|
|
16525
|
+
return Object.freeze({
|
|
16526
|
+
...held,
|
|
16527
|
+
board: board(),
|
|
16528
|
+
answer: () => Object.freeze({ ...held, board: board() })
|
|
16400
16529
|
});
|
|
16401
16530
|
}
|
|
16402
16531
|
|
|
@@ -18012,16 +18141,16 @@ function conflictLine(repoRoot, conflict) {
|
|
|
18012
18141
|
}
|
|
18013
18142
|
function sessionRefusal(repoRoot, draft, conflicts) {
|
|
18014
18143
|
const onOtherBranch = conflicts.some((conflict) => conflict.reason === "branch");
|
|
18015
|
-
const
|
|
18144
|
+
const running2 = conflicts.some((conflict) => conflict.reason === "live");
|
|
18016
18145
|
const why = [
|
|
18017
18146
|
onOtherBranch ? "it already has a branch" : null,
|
|
18018
|
-
|
|
18147
|
+
running2 ? "a session is already running it" : null
|
|
18019
18148
|
].filter((reason) => reason !== null);
|
|
18020
18149
|
return [
|
|
18021
18150
|
`\u274C Refusing to run plan \`${draft.planStub ?? draft.plan}\` on \`${draft.branch}\`: ${why.join(", and ")}.`,
|
|
18022
18151
|
...conflicts.map((conflict) => conflictLine(repoRoot, conflict)),
|
|
18023
18152
|
...onOtherBranch ? [" A plan runs on one branch: check out that branch to run it there, or revise the plan under a new stub."] : [],
|
|
18024
|
-
...
|
|
18153
|
+
...running2 ? [" One session runs a plan at a time: let that run end, or interrupt it, then run again."] : [],
|
|
18025
18154
|
NOTHING_DISPATCHED
|
|
18026
18155
|
].join(`
|
|
18027
18156
|
`);
|
|
@@ -21099,7 +21228,11 @@ var wrapped = wrapPhaseZeroCommand({
|
|
|
21099
21228
|
}
|
|
21100
21229
|
],
|
|
21101
21230
|
aliases: ["start"],
|
|
21102
|
-
outputs: ["text", "json"]
|
|
21231
|
+
outputs: ["text", "json"],
|
|
21232
|
+
spends: {
|
|
21233
|
+
when: "always",
|
|
21234
|
+
what: "one session per task, one for the wrap-up, and repair sessions while CI is red"
|
|
21235
|
+
}
|
|
21103
21236
|
}, start);
|
|
21104
21237
|
var loopStart = Object.freeze({
|
|
21105
21238
|
...wrapped,
|
|
@@ -21507,7 +21640,7 @@ function readProvides(raw, label, declared) {
|
|
|
21507
21640
|
}
|
|
21508
21641
|
return all.length > 0 ? failed2(...all) : passed(Object.freeze(Object.fromEntries(provided)));
|
|
21509
21642
|
}
|
|
21510
|
-
function readRange(raw, label,
|
|
21643
|
+
function readRange(raw, label, running2) {
|
|
21511
21644
|
const read = readText4(raw, label, RANGE_EXPECTED);
|
|
21512
21645
|
if (read.value === undefined)
|
|
21513
21646
|
return read;
|
|
@@ -21515,7 +21648,7 @@ function readRange(raw, label, running) {
|
|
|
21515
21648
|
if (!range.split("||").every((alternative) => ALTERNATIVE_PATTERN.test(alternative))) {
|
|
21516
21649
|
return failed2(expected(label, raw, RANGE_EXPECTED));
|
|
21517
21650
|
}
|
|
21518
|
-
return Bun.semver.satisfies(
|
|
21651
|
+
return Bun.semver.satisfies(running2, range) ? passed(range) : failed2(`${label} is ${JSON.stringify(range)}, which the running rafa ${running2} does not satisfy`);
|
|
21519
21652
|
}
|
|
21520
21653
|
function readPorts(raw, label, declared, served) {
|
|
21521
21654
|
const versions = new Map(Object.entries(served));
|
|
@@ -22130,7 +22263,7 @@ async function runNext(context, seams) {
|
|
|
22130
22263
|
const prompter = lazyPrompter(openPrompter);
|
|
22131
22264
|
try {
|
|
22132
22265
|
return await runNextChain({
|
|
22133
|
-
read: () => readNextState(sources),
|
|
22266
|
+
read: () => readNextState(sources.answer()),
|
|
22134
22267
|
run: (state) => runStateAction(context, sources, state),
|
|
22135
22268
|
ask: prompter.ask,
|
|
22136
22269
|
handOver: prompter.close,
|
|
@@ -22184,6 +22317,7 @@ function createNextCommand(seams = DEFAULT_NEXT_SEAMS) {
|
|
|
22184
22317
|
}
|
|
22185
22318
|
],
|
|
22186
22319
|
outputs: ["text", "json"],
|
|
22320
|
+
spends: { when: "through", what: "when the step it runs is one of the above" },
|
|
22187
22321
|
run: async (context) => {
|
|
22188
22322
|
const report2 = await runNext(context, seams);
|
|
22189
22323
|
if (context.outputMode === "json")
|
|
@@ -22622,7 +22756,8 @@ var wrapped2 = wrapPhaseZeroCommand({
|
|
|
22622
22756
|
}
|
|
22623
22757
|
],
|
|
22624
22758
|
aliases: ["plan"],
|
|
22625
|
-
outputs: ["text", "json"]
|
|
22759
|
+
outputs: ["text", "json"],
|
|
22760
|
+
spends: { when: "always", what: "one planning session" }
|
|
22626
22761
|
}, plan);
|
|
22627
22762
|
var create_default2 = endingWith(wrapped2);
|
|
22628
22763
|
|
|
@@ -24905,9 +25040,9 @@ function alreadyHeadline(at, block2) {
|
|
|
24905
25040
|
return `already assessed at ${when}, as ${what}; the head has not moved since`;
|
|
24906
25041
|
}
|
|
24907
25042
|
function pendingHeadline(head, rows2, at) {
|
|
24908
|
-
const
|
|
25043
|
+
const running2 = rows2.filter((row2) => row2.outcome === "pending").length;
|
|
24909
25044
|
const when = at ?? "an unrecorded time";
|
|
24910
|
-
return `the head moved to ${shortHead(head)} and ${checkCount2(
|
|
25045
|
+
return `the head moved to ${shortHead(head)} and ${checkCount2(running2)} of ${rows2.length} ` + `still running, so the triage from ${when} stands`;
|
|
24911
25046
|
}
|
|
24912
25047
|
function assessHeadline(head, verdict, write) {
|
|
24913
25048
|
const state = verdict === "none" ? "reports no checks at all" : "is not green";
|
|
@@ -26293,6 +26428,7 @@ function createPrTriageCommand(seams = DEFAULT_TRIAGE_SEAMS) {
|
|
|
26293
26428
|
}
|
|
26294
26429
|
],
|
|
26295
26430
|
outputs: ["text", "json"],
|
|
26431
|
+
spends: { when: "with", flag: "--resolve", what: "runs a small fixed plan through the loop" },
|
|
26296
26432
|
run: async (context) => {
|
|
26297
26433
|
const triaged = await runTriage(context, seams);
|
|
26298
26434
|
if (context.outputMode === "json") {
|
|
@@ -28942,6 +29078,7 @@ function createSkillBackfillCommand(seams = DEFAULT_BACKFILL_SEAMS) {
|
|
|
28942
29078
|
}
|
|
28943
29079
|
],
|
|
28944
29080
|
outputs: ["text", "json"],
|
|
29081
|
+
spends: { when: "with", flag: "--propose", what: "one session per batch of skills" },
|
|
28945
29082
|
run: async (context) => {
|
|
28946
29083
|
await runBackfill(context, seams);
|
|
28947
29084
|
}
|
|
@@ -840,6 +840,33 @@ function parseSpecReview(output) {
|
|
|
840
840
|
return judged(verdict, readGaps(document, issues), block, issues);
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
// src/cli/command.ts
|
|
844
|
+
var MAX_EXIT_CODE = 255;
|
|
845
|
+
|
|
846
|
+
class CommandExit extends Error {
|
|
847
|
+
exitCode;
|
|
848
|
+
constructor(exitCode, message = "") {
|
|
849
|
+
super(message);
|
|
850
|
+
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
|
|
851
|
+
throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
|
|
852
|
+
}
|
|
853
|
+
this.name = "CommandExit";
|
|
854
|
+
this.exitCode = exitCode;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function isTopLevel(command) {
|
|
858
|
+
return command.subject === command.action;
|
|
859
|
+
}
|
|
860
|
+
function commandSpelling(command) {
|
|
861
|
+
return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/cli/running.ts
|
|
865
|
+
var running = null;
|
|
866
|
+
function runningCommand() {
|
|
867
|
+
return running;
|
|
868
|
+
}
|
|
869
|
+
|
|
843
870
|
// src/utils/claude.ts
|
|
844
871
|
async function getClaudeUsagePercent() {
|
|
845
872
|
const envPct = process.env["CLAUDE_USAGE_PERCENT"];
|
|
@@ -891,9 +918,44 @@ function claudeSessionEnv() {
|
|
|
891
918
|
CLAUDE_CODE_ENTRYPOINT: "cli"
|
|
892
919
|
};
|
|
893
920
|
}
|
|
921
|
+
|
|
922
|
+
class UndeclaredSpendError extends Error {
|
|
923
|
+
name = "UndeclaredSpendError";
|
|
924
|
+
}
|
|
925
|
+
function carriesFlag(flags, flag) {
|
|
926
|
+
const name = flag.replace(/^--/, "");
|
|
927
|
+
const value = flags[name];
|
|
928
|
+
if (value !== undefined && value !== false)
|
|
929
|
+
return true;
|
|
930
|
+
return name.startsWith("no-") && flags[name.slice(3)] === false;
|
|
931
|
+
}
|
|
932
|
+
function spendRefusal(running2) {
|
|
933
|
+
if (running2 === null)
|
|
934
|
+
return null;
|
|
935
|
+
const { command, flags } = running2;
|
|
936
|
+
const spend = command.spends;
|
|
937
|
+
const named = `rafa ${commandSpelling(command)}`;
|
|
938
|
+
const remedy = "declare spends on the command to cover this run";
|
|
939
|
+
if (spend === undefined) {
|
|
940
|
+
return `${named} started a Claude session but declares no spends: ${remedy}`;
|
|
941
|
+
}
|
|
942
|
+
if (spend.when === "with" && !carriesFlag(flags, spend.flag)) {
|
|
943
|
+
return `${named} started a Claude session without ${spend.flag}, and its spends covers only a run with ${spend.flag}: ${remedy}`;
|
|
944
|
+
}
|
|
945
|
+
if (spend.when === "unless" && carriesFlag(flags, spend.flag)) {
|
|
946
|
+
return `${named} started a Claude session with ${spend.flag}, and its spends covers no run with ${spend.flag}: ${remedy}`;
|
|
947
|
+
}
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
function guardSpend() {
|
|
951
|
+
const refusal = spendRefusal(runningCommand());
|
|
952
|
+
if (refusal !== null)
|
|
953
|
+
throw new UndeclaredSpendError(refusal);
|
|
954
|
+
}
|
|
894
955
|
async function spawnClaude(args, prompt) {
|
|
956
|
+
guardSpend();
|
|
895
957
|
if (activeOutputMode() === "json") {
|
|
896
|
-
const { exitCode } = await
|
|
958
|
+
const { exitCode } = await spawnCaptured(args, prompt);
|
|
897
959
|
return exitCode;
|
|
898
960
|
}
|
|
899
961
|
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
@@ -945,6 +1007,10 @@ async function teeToOperator(stream) {
|
|
|
945
1007
|
return text + tail;
|
|
946
1008
|
}
|
|
947
1009
|
async function spawnClaudeCaptured(args, prompt) {
|
|
1010
|
+
guardSpend();
|
|
1011
|
+
return spawnCaptured(args, prompt);
|
|
1012
|
+
}
|
|
1013
|
+
async function spawnCaptured(args, prompt) {
|
|
948
1014
|
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
949
1015
|
stdin: new TextEncoder().encode(prompt),
|
|
950
1016
|
stdout: "pipe",
|
|
@@ -1771,4 +1837,4 @@ function selectEffortStore(repoRoot, config) {
|
|
|
1771
1837
|
return adapter.create({ repoRoot });
|
|
1772
1838
|
}
|
|
1773
1839
|
|
|
1774
|
-
export { EFFORT_KEY_PROJECTIONS, openNdjsonStore, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, getClaudeUsagePercent, checkUsage, interruptClaudeSessions, runClaude, runClaudeCaptured, planFilePath, ClaudePlannerError, createGhRunner, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|
|
1840
|
+
export { EFFORT_KEY_PROJECTIONS, openNdjsonStore, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, CommandExit, commandSpelling, getClaudeUsagePercent, checkUsage, interruptClaudeSessions, runClaude, runClaudeCaptured, planFilePath, ClaudePlannerError, createGhRunner, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|
package/dist/index.js
CHANGED
|
@@ -30,10 +30,12 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
CORE_ADAPTER_REGISTRY,
|
|
32
32
|
ClaudePlannerError,
|
|
33
|
+
CommandExit,
|
|
33
34
|
EFFORT_KEY_PROJECTIONS,
|
|
34
35
|
LONE_SURROGATE,
|
|
35
36
|
PORT_VERSIONS,
|
|
36
37
|
checkUsage,
|
|
38
|
+
commandSpelling,
|
|
37
39
|
createAdapterRegistry,
|
|
38
40
|
createGhRunner,
|
|
39
41
|
createLocalTracker,
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
sqliteStorePath,
|
|
49
51
|
withSqliteStore,
|
|
50
52
|
writeSqliteStore
|
|
51
|
-
} from "./index-
|
|
53
|
+
} from "./index-srdtw7w1.js";
|
|
52
54
|
import {
|
|
53
55
|
BUDGET_FLAG,
|
|
54
56
|
CLAUDE_SETTING_SOURCES,
|
|
@@ -132,27 +134,6 @@ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
|
132
134
|
import { homedir } from "os";
|
|
133
135
|
import { join, resolve } from "path";
|
|
134
136
|
|
|
135
|
-
// src/cli/command.ts
|
|
136
|
-
var MAX_EXIT_CODE = 255;
|
|
137
|
-
|
|
138
|
-
class CommandExit extends Error {
|
|
139
|
-
exitCode;
|
|
140
|
-
constructor(exitCode, message = "") {
|
|
141
|
-
super(message);
|
|
142
|
-
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
|
|
143
|
-
throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
|
|
144
|
-
}
|
|
145
|
-
this.name = "CommandExit";
|
|
146
|
-
this.exitCode = exitCode;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function isTopLevel(command) {
|
|
150
|
-
return command.subject === command.action;
|
|
151
|
-
}
|
|
152
|
-
function commandSpelling(command) {
|
|
153
|
-
return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
137
|
// src/effort/session-log.ts
|
|
157
138
|
import { createReadStream } from "fs";
|
|
158
139
|
import { basename } from "path";
|
|
@@ -1914,7 +1895,7 @@ async function report(args, repoRoot) {
|
|
|
1914
1895
|
}
|
|
1915
1896
|
}
|
|
1916
1897
|
// package.json
|
|
1917
|
-
var version = "0.
|
|
1898
|
+
var version = "0.10.0";
|
|
1918
1899
|
|
|
1919
1900
|
// src/modules/manifest.ts
|
|
1920
1901
|
var MANIFEST_VERSION = 1;
|
|
@@ -6006,13 +5987,18 @@ function openNextSources(context, seams = DEFAULT_NEXT_SOURCE_SEAMS) {
|
|
|
6006
5987
|
const git = (seams.openGit ?? createGitRunner)(project.root);
|
|
6007
5988
|
const gh = (seams.openGh ?? ((root) => createGhRunner({ cwd: root })))(project.root);
|
|
6008
5989
|
const loop = resolveLoopSeams({ isAlive: seams.isAlive });
|
|
6009
|
-
|
|
5990
|
+
const board = () => ghNextBoard({ gh, git, configured: config.roadmapIssue });
|
|
5991
|
+
const held = {
|
|
6010
5992
|
base: config.prBase ?? DEFAULT_BASE_BRANCH,
|
|
6011
5993
|
plans: plansDirAt(project.root, config.planDir),
|
|
6012
5994
|
runs: () => readRecords(project.root, loop),
|
|
6013
5995
|
git,
|
|
6014
|
-
pulls: (seams.pullRequests ?? ghPullRequestsIn)(project.root)
|
|
6015
|
-
|
|
5996
|
+
pulls: (seams.pullRequests ?? ghPullRequestsIn)(project.root)
|
|
5997
|
+
};
|
|
5998
|
+
return Object.freeze({
|
|
5999
|
+
...held,
|
|
6000
|
+
board: board(),
|
|
6001
|
+
answer: () => Object.freeze({ ...held, board: board() })
|
|
6016
6002
|
});
|
|
6017
6003
|
}
|
|
6018
6004
|
|
package/dist/plan-prompt.md
CHANGED
|
@@ -20,6 +20,10 @@ The slug summarizes what the user gets, using two to four words joined by
|
|
|
20
20
|
hyphens. The order of work lives in ONE place: the pinned "Roadmap" issue
|
|
21
21
|
on the `open-tomato/rafa` board.
|
|
22
22
|
|
|
23
|
+
* Commands that start Claude sessions must declare `spends` on their
|
|
24
|
+
`RafaCommand` — see the dev-planner skill's "Command-development rule" for
|
|
25
|
+
guidance.
|
|
26
|
+
|
|
23
27
|
* Before planning, review the spec as written, and report that review at the
|
|
24
28
|
END of your final message as a `rafa:spec-review` block carrying
|
|
25
29
|
`verdict: ready` or `verdict: not-ready`, with empty `gaps:` for ready, or a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-tomato/rafa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|