@ericrisco/rsc 2.1.0 → 3.0.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 +141 -15
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/install-apply.js +21 -0
- package/scripts/rsc.js +67 -1
- package/skills/ftd/SKILL.md +10 -4
- package/skills/ftd/evals/cases.yaml +1 -1
- package/skills/ftd/evals/routing.json +2 -2
- package/skills/init/SKILL.md +10 -1
- package/skills/ship/SKILL.md +6 -0
- package/skills/suggest/SKILL.md +9 -9
- package/skills/worktrees/SKILL.md +1 -0
- package/targets/_md-block.js +7 -1
- package/targets/auto-update.mjs +200 -0
- package/targets/branch-guard.mjs +191 -0
- package/targets/claude.js +22 -3
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/hook-once.mjs +5 -6
- package/targets/knowledge-sync.mjs +544 -0
- package/targets/knowledge-wiring.js +172 -0
- package/targets/opt-outs.js +4 -0
- package/targets/session-memory-adapter.mjs +20 -2
- package/targets/session-memory-core.mjs +33 -0
- package/targets/session-start.mjs +12 -74
- package/targets/team-safe-start.mjs +115 -0
- package/targets/trunk-policy.mjs +69 -0
- package/targets/update-wiring.js +153 -0
package/README.md
CHANGED
|
@@ -56,6 +56,58 @@ not a pile of whatever an agent happened to install.
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
+
## 🛡️ New in 3.0: safe for teams by default
|
|
60
|
+
|
|
61
|
+
Day one with agents is easy. Day ninety is where harnesses break, especially with several people,
|
|
62
|
+
several assistants and several sessions on one project:
|
|
63
|
+
|
|
64
|
+
- every machine drifts, and what one person teaches their agent never reaches anyone else;
|
|
65
|
+
- two agents end up working in the same folder and switch branches under each other;
|
|
66
|
+
- someone's agent commits straight to `main` on a project in production;
|
|
67
|
+
- every request gets the same process, whether it is a typo or a new subsystem.
|
|
68
|
+
|
|
69
|
+
rsc 3.0 fixes all four, by default, with no new commands to learn:
|
|
70
|
+
|
|
71
|
+
| | What happens | Turn it off |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| **Main is closed where it matters** | In a project that is complex or in production, the agent never commits on the default branch. It opens a branch on its own (`feat/…`, `fix/…`) and finishes with a pull request. In a project of scripts or research it works on `main`, because a branch there is only ceremony. | Say **«unlock main»**, or `rsc main unlock` |
|
|
74
|
+
| **One workspace per agent** | If another assistant session is working in the same folder, new work goes to a worktree under `.worktrees/<branch>/`. It stays inside the project, is never committed, and is removed once that branch is merged. | `rsc isolation off` |
|
|
75
|
+
| **The agent picks the method** | A simple change takes Fast-Track (one document, proof for every task). Something big or complex takes the spec-driven chain. The agent decides and says why. You still approve *what* gets built: the spec and the clarifications, then step by step or autopilot. | Ask for the other lane |
|
|
76
|
+
| **Knowledge reaches everyone** | `01-TOOLS/` and `02-DOCS/` travel through one exchange branch, `rsc/knowledge`. They go up when your turn ends and come down into whatever branch each person is on. They reach `main` inside your normal pull requests. | `rsc knowledge-sync off` |
|
|
77
|
+
|
|
78
|
+
**How does it know a project is "complex or in production"?** There are two layers. A hook counts
|
|
79
|
+
what anyone can check: a CI setup, a deployment file (`Dockerfile`, `vercel.json`, `fly.toml`…) or
|
|
80
|
+
at least two people in the last 50 commits. If any of those is there, a commit on the default branch
|
|
81
|
+
is refused before it runs. If none is, the agent decides change by change, using the same rule from
|
|
82
|
+
its skills. If the hook gets it wrong and the project really is simple, «unlock main» opens it for
|
|
83
|
+
the project. That decision is saved in `.rsc.json`, so the whole team gets it.
|
|
84
|
+
|
|
85
|
+
Every refusal says how to carry on: which branch to open, how to create the worktree, or how to
|
|
86
|
+
unlock. An agent is never left stuck. The rules only see what the **agent** runs; a person
|
|
87
|
+
committing in their own terminal is never touched.
|
|
88
|
+
|
|
89
|
+
**The first session after upgrading** does three things, once:
|
|
90
|
+
|
|
91
|
+
- It explains these changes in a few lines.
|
|
92
|
+
- If an agent left commits on a closed `main` that never reached the remote, it moves them to a
|
|
93
|
+
`rescue/main-<date>` branch and puts `main` back to what the remote has. Nothing is lost.
|
|
94
|
+
- It moves worktrees older versions created next to the project into `.worktrees/`.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
rsc main status # open or closed for the agent, and the signals behind it
|
|
98
|
+
rsc main unlock # this project is simple: the agent may work on main (saved in .rsc.json)
|
|
99
|
+
rsc main lock # back to the default
|
|
100
|
+
rsc isolation status # whether another session would send new work to a worktree
|
|
101
|
+
rsc isolation off # never use worktrees for parallel sessions in this project
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Limit, stated plainly.** The hook that refuses the commit or the branch switch runs in
|
|
105
|
+
> **Claude Code**, like the other rsc guards. In Codex, Gemini, Cursor and OpenCode the same rules
|
|
106
|
+
> are carried by the skills and by the session memory, which tells the agent that another session is
|
|
107
|
+
> active. They are followed, but no hook enforces them. Knowledge sync works in all five.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
59
111
|
## 🤖 For agents: "install this harness"
|
|
60
112
|
|
|
61
113
|
If you're an AI coding agent (Claude Code, Cursor, Copilot, Codex, Gemini…) and your user
|
|
@@ -98,9 +150,9 @@ before anything is written.
|
|
|
98
150
|
|
|
99
151
|
---
|
|
100
152
|
|
|
101
|
-
## 🛣️ Three lanes, and the
|
|
153
|
+
## 🛣️ Three lanes, and the agent picks the right one for you
|
|
102
154
|
|
|
103
|
-
<img src="https://raw.githubusercontent.com/ericrisco/rsc-harness/main/site/lanes.svg" alt="The rsc lane decisor:
|
|
155
|
+
<img src="https://raw.githubusercontent.com/ericrisco/rsc-harness/main/site/lanes.svg" alt="The rsc lane decisor: the agent classifies each request into answer (read-only), FTD (simple change, one feature document) or SDD (the ten-phase chain for big or complex work, entered by the agent; you approve the spec)." width="960">
|
|
104
156
|
|
|
105
157
|
Every turn takes exactly one lane, and your agent names the one it took in a line.
|
|
106
158
|
|
|
@@ -111,11 +163,14 @@ Every turn takes exactly one lane, and your agent names the one it took in a lin
|
|
|
111
163
|
2. **FTD — Fast-Track Development.** The request authorises a change. This is the default for
|
|
112
164
|
ordinary work and needs no ceremony. One feature document per feature holds intent, scope, a
|
|
113
165
|
checklist, the evidence and the next step. Tasks are checked off against observed proof, never
|
|
114
|
-
against intention.
|
|
115
|
-
3. **SDD — the ten-phase chain.**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
against intention. For the simple changes.
|
|
167
|
+
3. **SDD — the ten-phase chain.** For the big or complex: several decisions that affect each other,
|
|
168
|
+
or doubts that change the result. **The agent enters it on its own** — you never have to decide
|
|
169
|
+
which method to use. Inside, you still decide *what* gets built: you approve the spec and answer
|
|
170
|
+
the clarifying questions, then choose to review every phase yourself or let it run on autopilot
|
|
171
|
+
to verified code. Publishing always asks.
|
|
172
|
+
|
|
173
|
+
The agent says which lane it took and why, in one line. Ask for the other one and it switches.
|
|
119
174
|
|
|
120
175
|
**Isolation cleans itself up.** When a lane opens a branch or a worktree, you no longer have to
|
|
121
176
|
remember to retire it: a `post-merge` hook does it the moment the work lands on the trunk, on both
|
|
@@ -124,10 +179,9 @@ landed and holds nothing unsaved; anything it refuses tells you why. The previou
|
|
|
124
179
|
agent to run the cleanup at the end of a long phase, and that step was skipped on both features that
|
|
125
180
|
reached it.
|
|
126
181
|
|
|
127
|
-
Earlier versions
|
|
128
|
-
|
|
129
|
-
that is
|
|
130
|
-
is which one you land in by default, and who decides.
|
|
182
|
+
Earlier versions asked you to accept a proposal before entering the chain. People do not want to
|
|
183
|
+
choose a method; they want the work done right. Since 3.0 the agent chooses, and you keep the part
|
|
184
|
+
that is genuinely yours: deciding what is built.
|
|
131
185
|
|
|
132
186
|
---
|
|
133
187
|
|
|
@@ -355,8 +409,9 @@ pinned version. It asks; it does not install anything on its own and it does not
|
|
|
355
409
|
whatever you sat down to do. Say no and it stops asking on that machine.
|
|
356
410
|
|
|
357
411
|
Not `@latest`, and the difference is the whole point of sharing: a teammate who clones in three
|
|
358
|
-
months gets what you had, not what shipped since.
|
|
359
|
-
|
|
412
|
+
months gets what you had, not what shipped since. Once it is built, the same rules as any project
|
|
413
|
+
apply: releases in the same major install themselves (see [Update](#update)), and the new
|
|
414
|
+
`catalogVersion` in `.rsc.json` reaches everyone when somebody commits it.
|
|
360
415
|
|
|
361
416
|
When someone changes the harness and you `git pull`, `rsc doctor` tells you what no longer
|
|
362
417
|
matches. **Nothing is ever written to your machine by a pull** — you are told, and you decide.
|
|
@@ -369,6 +424,59 @@ list somebody has to remember to update is a list that goes quietly stale. Decla
|
|
|
369
424
|
`ownSkills` still works and still reports when someone is missing it.
|
|
370
425
|
|
|
371
426
|
|
|
427
|
+
### Your wiki and tools stay in sync — on by default
|
|
428
|
+
|
|
429
|
+
`01-TOOLS/` and `02-DOCS/` are the team's knowledge, so rsc keeps them the same on every machine
|
|
430
|
+
without anybody thinking about git, **and without ever pushing to `main`**. Your `main` is
|
|
431
|
+
protected, or should be; rsc assumes it is.
|
|
432
|
+
|
|
433
|
+
The knowledge travels through one exchange branch, `rsc/knowledge`, that nobody works on by hand:
|
|
434
|
+
|
|
435
|
+
```text
|
|
436
|
+
ana · feat/login edits 02-DOCS/wiki/api.md ──► rsc/knowledge (when her turn ends)
|
|
437
|
+
eric · feat/payments 📥 1 change from Ana ◄── rsc/knowledge (before his next message)
|
|
438
|
+
main · protected untouched — gets it inside the next merged pull request
|
|
439
|
+
new · clones main has it all after the first message
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **When a turn ends**, your changes in those folders are committed as `📝 docs(auto): … [skip ci]`
|
|
443
|
+
and sent to `rsc/knowledge` on `origin`, from whatever branch you are on. If you are on a closed
|
|
444
|
+
`main`, nothing is committed locally: the snapshot is built aside and only goes to `rsc/knowledge`.
|
|
445
|
+
- **Before each message**, what teammates sent is brought into **the branch you are on**, and you are
|
|
446
|
+
told in one line. Only the knowledge folders are touched.
|
|
447
|
+
- **It reaches `main` the normal way.** Your feature branch now carries the team's knowledge, so it
|
|
448
|
+
arrives in `main` inside the pull request you were going to open anyway. There is no extra pull
|
|
449
|
+
request from `rsc/knowledge`, and nobody has to merge it.
|
|
450
|
+
- The first time, `rsc/knowledge` is created from the remote's default branch. If someone deletes it,
|
|
451
|
+
it is created again.
|
|
452
|
+
|
|
453
|
+
What it will not do, by design:
|
|
454
|
+
|
|
455
|
+
| | |
|
|
456
|
+
| --- | --- |
|
|
457
|
+
| Push to `main` | Never. Only `rsc/knowledge` receives anything. |
|
|
458
|
+
| Push your code or your unpushed commits | Only its own snapshot of the knowledge folders goes up. Your code waits for you. |
|
|
459
|
+
| Trigger CI or a deploy | Every automatic commit says `[skip ci]`. |
|
|
460
|
+
| Bring in anybody else's code | Only knowledge paths come down. Code, config and `.claude/` are never pulled. |
|
|
461
|
+
| Sync `02-DOCS/wiki/harness/user-profile.md` | Those are one person's dials. |
|
|
462
|
+
| Overwrite a file you are editing | It tells you, and your version stays. |
|
|
463
|
+
| Run where it has no business | No `origin`, no knowledge folders, a rebase in progress, a cloud agent: it stays quiet. |
|
|
464
|
+
|
|
465
|
+
It is wired into the assistant's own turn hooks (end of turn, new message) in Claude Code, Codex,
|
|
466
|
+
Gemini, Cursor and OpenCode (tested end to end in Claude Code, Codex and OpenCode). The network part runs in the background, so a turn never waits for
|
|
467
|
+
it. It is kept apart from the [local session memory](#new-sessions-pick-up-the-latest-local-work),
|
|
468
|
+
which promises never to touch the network. The first turn says it is on. To turn it off for the
|
|
469
|
+
project:
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
rsc knowledge-sync off # writes .rsc/.no-knowledge-sync and records it in .rsc.json — commit that
|
|
473
|
+
rsc knowledge-sync status # active or not, and why
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
It is a **project** switch, not a personal one: if one person stopped sending, the rest of the team
|
|
477
|
+
would stop seeing their work. For the rest of the 3.0 team defaults, see
|
|
478
|
+
[New in 3.0](#️-new-in-30-safe-for-teams-by-default).
|
|
479
|
+
|
|
372
480
|
## 🩹 Something's off? One command
|
|
373
481
|
|
|
374
482
|
Recognise any of these? They are all the same fix.
|
|
@@ -403,8 +511,26 @@ so rsc does not repair, move or delete them — not even when rebuilding from sc
|
|
|
403
511
|
|
|
404
512
|
## Update
|
|
405
513
|
|
|
406
|
-
|
|
407
|
-
|
|
514
|
+
**rsc updates itself.** When a session starts, rsc checks npm for a newer version:
|
|
515
|
+
|
|
516
|
+
- **Same major** (`2.1.0 → 2.1.1` or `2.2.0`): it installs that exact version in the background.
|
|
517
|
+
It takes effect in the next session, and that session tells you it happened.
|
|
518
|
+
- **New major** (`2.x → 3.0.0`): it can change how the harness works, so the assistant asks first.
|
|
519
|
+
- **Failed:** the assistant asks instead, and rsc retries once a day. The output is in
|
|
520
|
+
`.rsc/auto-update.log`.
|
|
521
|
+
|
|
522
|
+
A new install asks whether you want this (yes by default). To turn it off later, create
|
|
523
|
+
`.rsc/.no-auto-update`; every release then asks. It is a project decision: it travels in
|
|
524
|
+
`.rsc.json` like the other switches. The update can leave harness files changed in git; rsc never
|
|
525
|
+
commits them for you.
|
|
526
|
+
|
|
527
|
+
Where it runs on its own: **Claude Code, Codex, Gemini CLI, Cursor and OpenCode**, from a
|
|
528
|
+
session-start hook. Every other assistant has no such hook, so the always-on instructions ask the
|
|
529
|
+
agent to run `node .rsc/auto-update.mjs` on its first turn — same rules, but it depends on the agent
|
|
530
|
+
doing it (and some assistants ask your permission before running a command). Codex asks you to
|
|
531
|
+
trust new hooks once.
|
|
532
|
+
|
|
533
|
+
To update by hand, bump the package, then re-sync what's already wired into your project:
|
|
408
534
|
|
|
409
535
|
```bash
|
|
410
536
|
npm install -g @ericrisco/rsc@latest # global install: pull the newest catalog
|
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/install-apply.js
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
wireMemory, unwireMemory, memoryManagedPaths, memoryModeFor, memoryEnabledForProject, memoryArtifactsPresent,
|
|
24
24
|
} from '../targets/memory.js';
|
|
25
|
+
import { wireUpdate, unwireUpdate, updateManagedPaths, updateArtifactsPresent } from '../targets/update-wiring.js';
|
|
26
|
+
import { wireKnowledge, unwireKnowledge, knowledgeManagedPaths, knowledgeArtifactsPresent } from '../targets/knowledge-wiring.js';
|
|
25
27
|
|
|
26
28
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
29
|
const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
@@ -78,6 +80,10 @@ export function generatedHookFiles({ target, cwd, policy }) {
|
|
|
78
80
|
join(cwd, '.rsc', 'worklog-checkpoint.mjs'),
|
|
79
81
|
join(cwd, '.rsc', 'hook-once.mjs'),
|
|
80
82
|
join(cwd, '.rsc', 'worktree-reaper.mjs'),
|
|
83
|
+
join(cwd, '.rsc', 'auto-update.mjs'),
|
|
84
|
+
// team-safe-default: the 3.0 start-up duties and the trunk policy (also read by branch-guard).
|
|
85
|
+
join(cwd, '.rsc', 'team-safe-start.mjs'),
|
|
86
|
+
join(cwd, '.rsc', 'trunk-policy.mjs'),
|
|
81
87
|
];
|
|
82
88
|
// The danger guard is declared on its own terms (#273): present unless the plan says otherwise,
|
|
83
89
|
// whether or not the code guards are.
|
|
@@ -85,6 +91,7 @@ export function generatedHookFiles({ target, cwd, policy }) {
|
|
|
85
91
|
if (policy?.codeHooks === false) return [...lifecycle, ...danger, join(cwd, '.rsc', 'suggest-always-on.md')];
|
|
86
92
|
return [...lifecycle, ...danger,
|
|
87
93
|
join(cwd, '.rsc', 'ship-guard.mjs'),
|
|
94
|
+
join(cwd, '.rsc', 'branch-guard.mjs'),
|
|
88
95
|
join(cwd, '.rsc', 'gitmoji-guard.mjs'), join(cwd, '.rsc', 'userprompt-gate.mjs'),
|
|
89
96
|
join(cwd, '.rsc', 'sello.mjs')];
|
|
90
97
|
}
|
|
@@ -95,6 +102,8 @@ export function managedPathsForInstall({ skillIds, agentIds = [], target, home,
|
|
|
95
102
|
const out = [paths.stateFile, versionFile(cwd), baseVersionsFile(cwd)];
|
|
96
103
|
if (policy?.context7 === false) out.push(join(cwd, '.rsc', '.no-context7'));
|
|
97
104
|
if (policy?.memory !== false) out.push(...memoryManagedPaths(target, cwd));
|
|
105
|
+
out.push(...updateManagedPaths(target, cwd));
|
|
106
|
+
out.push(...knowledgeManagedPaths(target, cwd));
|
|
98
107
|
if (targetHasAgents(target)) {
|
|
99
108
|
const state = readState(paths.stateFile);
|
|
100
109
|
const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
|
|
@@ -297,6 +306,12 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
297
306
|
? { mode: 'disabled', reason: 'onboarding-policy', paths: unwireMemory(target, cwd) }
|
|
298
307
|
: wireMemory(target, cwd);
|
|
299
308
|
state.memory = { mode: memoryResult.mode, reason: memoryResult.reason, paths: memoryResult.paths };
|
|
309
|
+
const updateResult = wireUpdate(target, cwd);
|
|
310
|
+
state.update = { mode: updateResult.mode, ...(updateResult.reason ? { reason: updateResult.reason } : {}) };
|
|
311
|
+
// Knowledge sync is its own decision (`.no-knowledge-sync`, checked at runtime) and never part of
|
|
312
|
+
// the memory, so the memory's policy does not decide it: wired wherever the assistant can run it.
|
|
313
|
+
const knowledgeResult = wireKnowledge(target, cwd);
|
|
314
|
+
state.knowledge = { mode: knowledgeResult.mode, reason: knowledgeResult.reason || null, paths: knowledgeResult.paths };
|
|
300
315
|
const context7OptOut = join(cwd, '.rsc', '.no-context7');
|
|
301
316
|
if (policy?.context7 === false) {
|
|
302
317
|
mkdirSync(dirname(context7OptOut), { recursive: true });
|
|
@@ -596,6 +611,8 @@ export function removeTargetInstall({ target, home, cwd = process.cwd() }) {
|
|
|
596
611
|
}
|
|
597
612
|
unwireHook(target, paths);
|
|
598
613
|
unwireMemory(target, cwd);
|
|
614
|
+
unwireUpdate(target, cwd);
|
|
615
|
+
unwireKnowledge(target, cwd);
|
|
599
616
|
rmSync(paths.stateFile, { force: true });
|
|
600
617
|
const gitignore = join(cwd, '.gitignore');
|
|
601
618
|
if (existsSync(gitignore)) {
|
|
@@ -704,8 +721,12 @@ export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRu
|
|
|
704
721
|
if (!dryRun) {
|
|
705
722
|
removed.push(...unwireHook(target, paths));
|
|
706
723
|
removed.push(...unwireMemory(target, cwd));
|
|
724
|
+
removed.push(...unwireUpdate(target, cwd));
|
|
725
|
+
removed.push(...unwireKnowledge(target, cwd));
|
|
707
726
|
} else {
|
|
708
727
|
removed.push(...memoryArtifactsPresent(target, cwd));
|
|
728
|
+
removed.push(...updateArtifactsPresent(target, cwd));
|
|
729
|
+
removed.push(...knowledgeArtifactsPresent(target, cwd));
|
|
709
730
|
}
|
|
710
731
|
// A lost state file also loses proof of ownership. Leave same-named user files
|
|
711
732
|
// behind rather than guessing from a catalog id and deleting their work.
|
package/scripts/rsc.js
CHANGED
|
@@ -44,7 +44,7 @@ if (['--version', '-v', 'version'].includes(rawArgv[0])) {
|
|
|
44
44
|
const GLOBAL_VALUE_FLAGS = new Set([
|
|
45
45
|
'--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
|
|
46
46
|
]);
|
|
47
|
-
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'sello', 'repair', 'uninstall', 'purge']);
|
|
47
|
+
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'knowledge-sync', 'main', 'isolation', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'sello', 'repair', 'uninstall', 'purge']);
|
|
48
48
|
function positionalTokens(input) {
|
|
49
49
|
const out = [];
|
|
50
50
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -766,6 +766,72 @@ async function main() {
|
|
|
766
766
|
printContextBudget(report.contextBudget);
|
|
767
767
|
return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));
|
|
768
768
|
}
|
|
769
|
+
case 'main':
|
|
770
|
+
case 'isolation': {
|
|
771
|
+
// team-safe-default: the two switches a person asks the agent for in plain words —
|
|
772
|
+
// «desbloquea main» / «bloquea main», «no uses worktrees» / «usa worktrees». Both are PROJECT
|
|
773
|
+
// decisions (recorded in .rsc.json optOuts by the sync below) because one person's answer is
|
|
774
|
+
// the team's: the default branch is open or closed for every agent working on the repo.
|
|
775
|
+
const isMain = cmd === 'main';
|
|
776
|
+
const sub = argv[1] || 'status';
|
|
777
|
+
const root = process.cwd();
|
|
778
|
+
const { existsSync, mkdirSync, writeFileSync, rmSync } = await import('node:fs');
|
|
779
|
+
const { join } = await import('node:path');
|
|
780
|
+
const marker = join(root, '.rsc', isMain ? '.no-trunk-guard' : '.no-worktree-isolation');
|
|
781
|
+
const OFF = isMain ? 'unlock' : 'off';
|
|
782
|
+
const ON = isMain ? 'lock' : 'on';
|
|
783
|
+
if (sub === OFF || sub === ON) {
|
|
784
|
+
mkdirSync(join(root, '.rsc'), { recursive: true });
|
|
785
|
+
if (sub === OFF) writeFileSync(marker, '');
|
|
786
|
+
else rmSync(marker, { force: true });
|
|
787
|
+
for (const t of targets) await syncInstalled({ target: t, cwd: root });
|
|
788
|
+
say(isMain
|
|
789
|
+
? (sub === OFF
|
|
790
|
+
? 'rsc main unlock: the agent may now work on the default branch in this project. Commit .rsc.json so the team gets the same decision.'
|
|
791
|
+
: 'rsc main lock: the default branch is closed for the agent again where the project looks complex or in production. Commit .rsc.json so the team gets the same decision.')
|
|
792
|
+
: (sub === OFF
|
|
793
|
+
? 'rsc isolation off: sessions may share this folder without a worktree. Commit .rsc.json so the team gets the same decision.'
|
|
794
|
+
: 'rsc isolation on: with another session active in this folder, new work goes to .worktrees/<branch>/. Commit .rsc.json so the team gets the same decision.'));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (sub === 'status') {
|
|
798
|
+
if (isMain) {
|
|
799
|
+
const { trunkPolicy } = await import('../targets/trunk-policy.mjs');
|
|
800
|
+
return void say(JSON.stringify(trunkPolicy(root), null, 2));
|
|
801
|
+
}
|
|
802
|
+
const { otherActiveSessions } = await import('../targets/session-memory-core.mjs');
|
|
803
|
+
return void say(JSON.stringify({ isolation: existsSync(marker) ? 'off' : 'on', activeSessions: otherActiveSessions({ cwd: root }) }, null, 2));
|
|
804
|
+
}
|
|
805
|
+
say(`Use: npx @ericrisco/rsc ${cmd} ${ON}|${OFF}|status`);
|
|
806
|
+
process.exitCode = 2;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
case 'knowledge-sync': {
|
|
810
|
+
// On by default; the switch is a PROJECT decision (`.no-knowledge-sync`, recorded in `.rsc.json`
|
|
811
|
+
// optOuts by the sync below), because one person off means the team stops seeing their work.
|
|
812
|
+
const sub = argv[1] || 'status';
|
|
813
|
+
const root = process.cwd();
|
|
814
|
+
const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
|
|
815
|
+
const { join } = await import('node:path');
|
|
816
|
+
const marker = join(root, '.rsc', '.no-knowledge-sync');
|
|
817
|
+
if (sub === 'on' || sub === 'off') {
|
|
818
|
+
mkdirSync(join(root, '.rsc'), { recursive: true });
|
|
819
|
+
if (sub === 'off') writeFileSync(marker, '');
|
|
820
|
+
else rmSync(marker, { force: true });
|
|
821
|
+
for (const t of targets) await syncInstalled({ target: t, cwd: root });
|
|
822
|
+
say(sub === 'off'
|
|
823
|
+
? 'rsc knowledge-sync off: 01-TOOLS/ and 02-DOCS/ no longer sync on their own in this project. Commit .rsc.json so the team gets the same decision.'
|
|
824
|
+
: 'rsc knowledge-sync on: 01-TOOLS/ and 02-DOCS/ go up when a turn ends and come down before each message. Commit .rsc.json so the team gets the same decision.');
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (sub === 'status') {
|
|
828
|
+
const { knowledgeStatus } = await import('../targets/knowledge-sync.mjs');
|
|
829
|
+
return void say(JSON.stringify(knowledgeStatus(root), null, 2));
|
|
830
|
+
}
|
|
831
|
+
say('Use: npx @ericrisco/rsc knowledge-sync on|off|status');
|
|
832
|
+
process.exitCode = 2;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
769
835
|
case 'memory': {
|
|
770
836
|
const sub = argv[1] || 'status';
|
|
771
837
|
const root = process.cwd();
|
package/skills/ftd/SKILL.md
CHANGED
|
@@ -53,10 +53,16 @@ the chain: a result asserted without evidence is treated as not done.
|
|
|
53
53
|
|
|
54
54
|
## Isolation
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
branch
|
|
56
|
+
Where the work runs depends on the project, judged change by change (team-safe-default):
|
|
57
|
+
|
|
58
|
+
- **Complex or in production** (deploys, CI, several people committing, a protected default branch)
|
|
59
|
+
→ a branch, never the default branch. Open it yourself, named by intent, and close it with a pull
|
|
60
|
+
request. Something unrelated to the current branch gets its own branch from the default one.
|
|
61
|
+
- **Simple** (scripts, research, personal) → the default branch is fine.
|
|
62
|
+
- **Another session working in this same folder** → a worktree in `.worktrees/<branch>/`, never a
|
|
63
|
+
branch switch under the other session. rsc tells you when that is the case.
|
|
64
|
+
|
|
65
|
+
A worktree only when another session is there: alone, setting one up costs more than it protects.
|
|
60
66
|
|
|
61
67
|
`../worktrees/SKILL.md` owns the isolation mechanics if you need them. Once the branch lands, the
|
|
62
68
|
cleanup is automatic and you do not run anything.
|
|
@@ -34,7 +34,7 @@ should_not_trigger:
|
|
|
34
34
|
|
|
35
35
|
- prompt: "No tengo claro si los permisos van por rol o por recurso, y afecta al esquema, a la API y a la UI."
|
|
36
36
|
route_to: "specify"
|
|
37
|
-
why: "A substantial unresolved ambiguity that durable spec/plan/tasks would settle.
|
|
37
|
+
why: "A substantial unresolved ambiguity that durable spec/plan/tasks would settle. Several interlocking decisions: the agent enters the chain itself and says why (team-safe-default)."
|
|
38
38
|
|
|
39
39
|
- prompt: "Quiero especificar bien el sistema de permisos antes de tocar nada."
|
|
40
40
|
route_to: "specify"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"lanes": {
|
|
4
4
|
"answer": "information only; writes nothing, creates no artifact",
|
|
5
5
|
"ftd": "authorised change, ordinary; one feature document",
|
|
6
|
-
"sdd": "the ten-phase chain, by
|
|
6
|
+
"sdd": "the ten-phase chain, entered by the agent for big or complex work"
|
|
7
7
|
},
|
|
8
8
|
"cases": [
|
|
9
9
|
{ "request": "¿Por qué el login tarda tanto en producción?", "expect": "answer", "why": "investigation; nothing authorises a change" },
|
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
{ "request": "Migra los tres endpoints al nuevo formato de respuesta", "expect": "ftd", "why": "many files, but nothing a spec would settle that the code does not" },
|
|
20
20
|
{ "request": "Quiero especificar bien el sistema de permisos antes de tocar nada", "expect": "sdd", "why": "explicit request for the chain" },
|
|
21
21
|
{ "request": "Vamos a hacer una spec de esto", "expect": "sdd", "why": "explicit request, stated in the user's own words" },
|
|
22
|
-
{ "request": "No tengo claro si los permisos van por rol o por recurso, y afecta al esquema, a la API y a la UI", "expect": "sdd", "why": "
|
|
22
|
+
{ "request": "No tengo claro si los permisos van por rol o por recurso, y afecta al esquema, a la API y a la UI", "expect": "sdd", "why": "enter the chain: several interlocking decisions that durable artifacts would settle; the agent enters it and says why" }
|
|
23
23
|
]
|
|
24
24
|
}
|
package/skills/init/SKILL.md
CHANGED
|
@@ -137,7 +137,7 @@ cargan al arrancar la sesión."* Full skill map and sample printouts → `refere
|
|
|
137
137
|
|
|
138
138
|
### Phase 4 — GROUND
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Five checks once the skills are in. The SessionStart hook nudges each of these too; doing them here
|
|
141
141
|
means the user starts clean.
|
|
142
142
|
|
|
143
143
|
1. **Version control.** No `.git/` → offer `git init`; the SDD chain and the ship guard assume it.
|
|
@@ -154,6 +154,15 @@ means the user starts clean.
|
|
|
154
154
|
`WHERE`, `dd` to a device, `curl | bash` — and asks for a safer alternative. A fully `technical`
|
|
155
155
|
user is never guarded. It turns off only if the user explicitly asks: `.rsc/.no-danger-guard`.
|
|
156
156
|
Mention it when you record `non-technical`, so a later block is not a surprise.
|
|
157
|
+
5. **Automatic updates.** Ask once, yes by default:
|
|
158
|
+
> *"Cuando salga una versión nueva de rsc, ¿la instalo sola? Las que cambian mucho (major) te las
|
|
159
|
+
> preguntaré siempre. (sí/no)"*
|
|
160
|
+
|
|
161
|
+
> *"When a new rsc version comes out, should I install it on my own? Big ones (major) I will
|
|
162
|
+
> always ask about. (yes/no)"*
|
|
163
|
+
|
|
164
|
+
Yes, or no clear answer → do nothing; it is on. No → create `.rsc/.no-auto-update`: every
|
|
165
|
+
release then asks. It is a project switch, recorded in `.rsc.json` on the next sync.
|
|
157
166
|
|
|
158
167
|
### Phase 5 — HANDOFF
|
|
159
168
|
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -93,6 +93,12 @@ This mirrors the harness "siempre 3 opciones" pattern. Gather the one fact that
|
|
|
93
93
|
| **2. Pull request** | Push the branch, open a PR with a spec-linked body, let CI / a human gate the merge | `main` is protected; a team or CI must sign off; you want the change reviewable in the forge even if you self-merge |
|
|
94
94
|
| **3. Park or discard** | Keep the branch un-merged (park) or delete it (discard) | The approach was superseded, the spike answered its question, or the work is paused — it should not land |
|
|
95
95
|
|
|
96
|
+
**When the default branch is closed for the agent** (team-safe-default: the project is complex or in
|
|
97
|
+
production — `npx @ericrisco/rsc main status` says `closed: true`), **option 1 is not offered**: a
|
|
98
|
+
local merge is a commit on the default branch. Present option 2 (the recommendation) and option 3,
|
|
99
|
+
and say in one line why option 1 is absent and that «desbloquea main» (`npx @ericrisco/rsc main
|
|
100
|
+
unlock`) would bring it back. The branch guard denies the local merge anyway.
|
|
101
|
+
|
|
96
102
|
Recommend based on repo signals: protected `main` or an existing PR culture (look for `.github/`, prior PRs via `gh pr list`) → recommend **option 2**. A solo project with no protection and a passed review → **option 1** is honest and faster. Never default to a PR ceremony the repo doesn't use, and never force-merge a repo that gates `main`.
|
|
97
103
|
|
|
98
104
|
### Delivery strategy from SDD config
|
package/skills/suggest/SKILL.md
CHANGED
|
@@ -26,15 +26,15 @@ recommend. Read-only: write nothing, create no artifact, delegate no writer. Ask
|
|
|
26
26
|
about* building something is still this lane; only asking to build authorises it. When change
|
|
27
27
|
intent is ambiguous, ask one question and stay here — ambiguity slows the lane, never raises it.
|
|
28
28
|
|
|
29
|
-
**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
**You choose the lane; never hand that choice to the person.** Simple → **FTD**: one feature document
|
|
30
|
+
in `02-DOCS` (intent, scope, checklist, evidence, next step), tasks checked off against observed proof.
|
|
31
|
+
Big, or decisions that affect each other → **SDD**: enter the chain; the person approves the spec and
|
|
32
|
+
clarify, then picks manual or autopilot. Say which and why in one line; switch if asked.
|
|
33
|
+
→ `../ftd/SKILL.md` · `../sdd/SKILL.md`.
|
|
34
|
+
|
|
35
|
+
**Where.** Complex or in production → never commit on the default branch: branch yourself, close with
|
|
36
|
+
a PR. Simple → the default branch. Another session in this checkout → `.worktrees/<branch>`. «desbloquea
|
|
37
|
+
main» = `rsc main unlock`; «no uses worktrees» = `rsc isolation off`.
|
|
38
38
|
|
|
39
39
|
Judge the **meaning**, not the wording: the trigger is semantic in any language. A bug fix restoring
|
|
40
40
|
intended behaviour is `debug`. Autopilot consent covers a whole run — advance without re-asking.
|
|
@@ -44,6 +44,7 @@ Both isolate history. A worktree also isolates the *filesystem*. Pick by what's
|
|
|
44
44
|
| You'll run two streams of work at once (see the parallel pattern) | **worktree** | Each stream gets its own directory; no stash-juggling, no checkout thrash. |
|
|
45
45
|
| The current tree is dirty and the user wants both the WIP and the new work | **worktree** | Stash-and-switch risks losing the WIP; a worktree sidesteps the stash entirely. |
|
|
46
46
|
| You're an agent that may keep the main session running elsewhere | **worktree** | Filesystem isolation is the whole point — the parent session keeps its files. |
|
|
47
|
+
| **Another session is working in this same folder** (rsc says so on each message) | **worktree — mandatory** | Switching branches would change its files under it; rsc's branch guard denies the switch. Tell the person that `.worktrees/<branch>/` is where the project now runs. Off for the project: `npx @ericrisco/rsc isolation off`. |
|
|
47
48
|
|
|
48
49
|
When in doubt, prefer a **worktree**: it is the strictly stronger isolation and the cost is one
|
|
49
50
|
extra directory. The rest of this skill assumes a worktree; the branch path is the degenerate case
|
package/targets/_md-block.js
CHANGED
|
@@ -13,6 +13,12 @@ const MARK_END = '<!-- rsc-suggest:end -->';
|
|
|
13
13
|
// The exact separator wireHook writes before an appended block, so unwireHook can take
|
|
14
14
|
// back precisely what it gave and leave the rest of the file untouched.
|
|
15
15
|
const SEAM = '\n\n';
|
|
16
|
+
// Most assistants that read a rules file have no session-start hook, so nothing checks for a new rsc
|
|
17
|
+
// on its own. The agent does it instead. Here and not in suggest's body: the body is paid on every
|
|
18
|
+
// turn by every assistant, Claude Code included, which already checks from its hook.
|
|
19
|
+
export const UPDATE_NOTE = '**rsc updates.** Claude Code, Codex, Gemini CLI, Cursor and OpenCode check for a new rsc on their '
|
|
20
|
+
+ 'own. In any other assistant, run `node .rsc/auto-update.mjs` once on the first turn of a session and relay any '
|
|
21
|
+
+ 'notice it prints in one line; "up to date" needs no mention.\n';
|
|
16
22
|
|
|
17
23
|
export function writeSkill(id, fromDir, toPath) {
|
|
18
24
|
return linkOrCopy(fromDir, toPath);
|
|
@@ -23,7 +29,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
|
|
|
23
29
|
const body = policy.codeHooks === false
|
|
24
30
|
? `# rsc-suggest — always-on operations layer\n\nRead \`02-DOCS/wiki/harness/user-profile.md\` before acting. Use \`orient\` to keep the user situated and \`suggest\` to offer a missing skill only when the current task needs it. Close with the configured orientation block.\n`
|
|
25
31
|
: full;
|
|
26
|
-
const block = `${MARK_START}\n${body}\n${MARK_END}`;
|
|
32
|
+
const block = `${MARK_START}\n${body}\n${UPDATE_NOTE}\n${MARK_END}`;
|
|
27
33
|
let doc = existsSync(paths.hookTarget) ? readFileSync(paths.hookTarget, 'utf8') : '';
|
|
28
34
|
if (doc.includes(MARK_START)) {
|
|
29
35
|
doc = doc.replace(new RegExp(`${MARK_START}[\\s\\S]*?${MARK_END}`), block);
|