@ericrisco/rsc 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,7 +70,7 @@ rsc 3.0 fixes all four, by default, with no new commands to learn:
70
70
 
71
71
  | | What happens | Turn it off |
72
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` |
73
+ | **You decide where each change goes** | Before every change that writes code, the agent asks: a new branch, or straight on `main`? It never opens a branch on its own. In a project that is complex or in production (CI, a deployment, several people), a commit on `main` is refused, and the agent asks whether to open a branch or unlock `main`. | Say **«unlock main»**, or `rsc main unlock` |
74
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
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
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` |
@@ -99,12 +99,12 @@ rsc git-permissions on # turn it on, also for a project adopted before
99
99
  **How does it know a project is "complex or in production"?** There are two layers. A hook counts
100
100
  what anyone can check: a CI setup, a deployment file (`Dockerfile`, `vercel.json`, `fly.toml`…) or
101
101
  at least two people in the last 50 commits. If any of those is there, a commit on the default branch
102
- is refused before it runs. If none is, the agent decides change by change, using the same rule from
103
- its skills. If the hook gets it wrong and the project really is simple, «unlock main» opens it for
102
+ is refused before it runs, and the agent asks you whether to open a branch or unlock it. If none is,
103
+ `main` is open, and the agent still asks before each change whether you want a branch. If the hook gets it wrong and the project really is simple, «unlock main» opens it for
104
104
  the project. That decision is saved in `.rsc.json`, so the whole team gets it.
105
105
 
106
- Every refusal says how to carry on: which branch to open, how to create the worktree, or how to
107
- unlock. An agent is never left stuck. The rules only see what the **agent** runs; a person
106
+ Every refusal says how to carry on, and the choice between a branch and unlocking goes back to you.
107
+ An agent is never left stuck. The rules only see what the **agent** runs; a person
108
108
  committing in their own terminal is never touched.
109
109
 
110
110
  **The first session after upgrading** does three things, once:
@@ -467,6 +467,8 @@ new · clones main has it all after the first message
467
467
  only its `01-TOOLS/` and `02-DOCS/` part, under a `docs(auto) … [skip ci]` message.
468
468
  - **Before each message**, what teammates sent is brought into **the branch you are on**, and you are
469
469
  told in one line. Only the knowledge folders are touched.
470
+ A branch you open later catches up on its first message: everything on `rsc/knowledge` since it
471
+ left `main` comes down, including what you wrote yourself on another branch.
470
472
  - **It reaches `main` the normal way.** Your feature branch now carries the team's knowledge, so it
471
473
  arrives in `main` inside the pull request you were going to open anyway. There is no extra pull
472
474
  request from `rsc/knowledge`, and nobody has to merge it.
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.3",
2
+ "version": "3.0.4",
3
3
  "counts": {
4
4
  "skills": 270,
5
5
  "agents": 33,
@@ -1911,7 +1911,7 @@
1911
1911
  },
1912
1912
  {
1913
1913
  "id": "ftd",
1914
- "description": "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof, and branches when the work writes code. NOT the ten-phase chain (that is `sdd`), NOT the classification itself (that is the decisor in `suggest`), NOT a request for information, which writes nothing.",
1914
+ "description": "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof, and asks whether to branch before the work writes code. NOT the ten-phase chain (that is `sdd`), NOT the classification itself (that is the decisor in `suggest`), NOT a request for information, which writes nothing.",
1915
1915
  "tags": [
1916
1916
  "ftd",
1917
1917
  "lane",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ftd
3
- description: "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof, and branches when the work writes code. NOT the ten-phase chain (that is `sdd`), NOT the classification itself (that is the decisor in `suggest`), NOT a request for information, which writes nothing."
3
+ description: "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof, and asks whether to branch before the work writes code. NOT the ten-phase chain (that is `sdd`), NOT the classification itself (that is the decisor in `suggest`), NOT a request for information, which writes nothing."
4
4
  tags: [ftd, lane, default, feature-document, evidence]
5
5
  recommends: [worktrees, debug, verify]
6
6
  profiles: [minimal, core, full]
@@ -53,14 +53,16 @@ the chain: a result asserted without evidence is treated as not done.
53
53
 
54
54
  ## Isolation
55
55
 
56
- Where the work runs depends on the project, judged change by change (team-safe-default):
56
+ Where the work runs is the person's call, asked change by change — never a branch opened on your own:
57
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.
58
+ - **Before each change that writes code**, ask in one line: «¿Lo hago en una rama nueva o directamente
59
+ en `<default branch>`?». A branch is named by intent and closes with a pull request.
60
+ - **Default branch closed for the agent** (rsc refuses the commit there: CI, a deployment or several
61
+ people committing) → ask: «¿Abro una rama o desbloqueo `<default branch>`?». Unlocking is
62
+ `rsc main unlock`, a project decision; never run it without that answer.
63
+ - **Docs, wiki or config only** → no question, no branch.
62
64
  - **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.
65
+ branch switch under the other session. Not a question: rsc tells you when that is the case.
64
66
 
65
67
  A worktree only when another session is there: alone, setting one up costs more than it protects.
66
68
 
@@ -45,9 +45,9 @@ back — you do not write feature code anyway.
45
45
  4. **Read the register.** Open `02-DOCS/wiki/harness/user-profile.md` and read `technical_level`.
46
46
  It picks technical terms or plain words with analogies at each checkpoint (see "Checkpoints"
47
47
  below). No profile yet → use analogies, and ask before any irreversible step.
48
- 5. **Confirm isolation.** Implementation happens on a feature branch or worktree, never directly on
49
- the default branch. If you are on `main`/`master`, stop and hand to `worktrees` before the first
50
- commit.
48
+ 5. **Confirm isolation.** On `main`/`master`, stop before the first commit and ask the person: a
49
+ feature branch, or straight on the default branch (`worktrees`)? Never branch on your own; another
50
+ session in this folder makes the worktree mandatory.
51
51
  6. **Scan the tasks for independence.** Mark which tasks share files/state and which are disjoint.
52
52
  Disjoint clusters are candidates for `parallel`; everything else runs in order.
53
53
 
@@ -314,7 +314,7 @@ outranks the plan, and the plan outranks your in-the-moment preference.
314
314
  - **Unresolved `analyze` findings** (contradiction between constitution ↔ spec ↔ plan ↔ tasks) →
315
315
  resolve them before any code; they will only get more expensive after the diff exists.
316
316
  - **A task forces a constitution violation** → surface it, stop, kick it back to `analyze`/`plan`.
317
- - **You're on the default branch** → stop before the first commit; hand to `worktrees`.
317
+ - **You're on the default branch** → stop before the first commit and ask: branch or default branch (`worktrees`).
318
318
  - **A test won't go red** (wrong reason — import error, syntax) → fix the test before trusting it.
319
319
  - **The combined suite is red after a parallel merge** → do not checkpoint as done; debug with
320
320
  `debug` before continuing.
@@ -32,9 +32,9 @@ Big, or decisions that affect each other → **SDD**: enter the chain; the perso
32
32
  clarify, then picks manual or autopilot. Say which and why in one line; switch if asked.
33
33
  → `../ftd/SKILL.md` · `../sdd/SKILL.md`.
34
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`.
35
+ **Where.** Before each change that writes code, ask: a branch, or straight on the default branch? Never
36
+ branch on your own. Default branch closed (CI, deploy, team) → ask: a branch, or `rsc main unlock`?
37
+ Another session in this checkout → `.worktrees/<branch>`; «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.
@@ -27,11 +27,12 @@ constitution → specify → clarify → plan → tasks → analyze → [ worktr
27
27
 
28
28
  ## The one rule that defines this step
29
29
 
30
- **Never run `implement` on the default branch.** The moment a plan is about to become commits, the
31
- work belongs in isolation. If `git rev-parse --abbrev-ref HEAD` says `main` or `master`, you stop
32
- and create the isolated workspace *first* — before the first edit, not after the diff already exists
33
- on the wrong branch. Recovering a half-built feature off `main` is strictly more expensive than
34
- branching one command earlier.
30
+ **Never run `implement` on the default branch without asking.** The moment a plan is about to become
31
+ commits, if `git rev-parse --abbrev-ref HEAD` says `main` or `master`, you stop and ask the person
32
+ *first* — «¿rama nueva o directamente en `main`?» — before the first edit, not after the diff already
33
+ exists. Never branch on your own; if rsc refuses commits there (closed default branch), the question
34
+ is «¿abro una rama o desbloqueo `main`?». The one exception is another session in this folder: then
35
+ the worktree is mandatory, not a question.
35
36
 
36
37
  ## The decision: branch vs. worktree
37
38
 
@@ -152,14 +152,16 @@ async function othersHere({ root, here, sessionId, self }) {
152
152
  }
153
153
 
154
154
  async function trunkDenial({ root, here, sessionId, self, trunk, policy }) {
155
- // Where the branch goes depends on rule B: alone → this same folder; with company → .worktrees/.
155
+ // Where a branch would go depends on rule B: alone → this same folder; with company → .worktrees/.
156
+ // Whether to branch at all is the person's call: the agent asks, it never branches on its own.
156
157
  const others = existsSync(join(root, '.rsc', '.no-worktree-isolation')) ? [] : await othersHere({ root, here, sessionId, self });
157
- const where = others.length
158
- ? 'Another session is working in this folder, so open it as a worktree inside the project: `git worktree add .worktrees/<branch> -b feat/<what-you-are-doing>` and commit inside `.worktrees/<branch>/`'
159
- : 'You are the only session here, so stay in this same folder, no worktree: `git switch -c feat/<what-you-are-doing>` (or fix/…, docs/…) and commit there';
160
- return `This project keeps its default branch "${trunk}" closed for the agent (it looks complex or in production: ${policy.signals.join(', ')}). ` +
161
- `Do this work on a branch instead. ${where}; it reaches "${trunk}" through a pull request. ` +
162
- 'If this project is actually simple and the person asks to unlock it, run `npx @ericrisco/rsc main unlock` (a project decision, saved in .rsc.json).';
158
+ const branch = others.length
159
+ ? '`git worktree add .worktrees/<branch> -b feat/<what>` and commit inside `.worktrees/<branch>/` (another session is working in this folder)'
160
+ : '`git switch -c feat/<what>` in this same folder, no worktree';
161
+ return `This project keeps its default branch "${trunk}" closed for the agent (it looks complex or in production: ${policy.signals.join(', ')}), so this commit was not made. ` +
162
+ `Do not choose for the person: ask them in one line whether to open a branch for this change or to unlock "${trunk}". ` +
163
+ `Branch → ${branch}; it reaches "${trunk}" through a pull request. ` +
164
+ 'Unlock → only on their explicit answer, `npx @ericrisco/rsc main unlock` (a project decision, saved in .rsc.json), then commit again.';
163
165
  }
164
166
 
165
167
  async function isolationDenial({ root, here, sessionId, self }) {
@@ -97,8 +97,8 @@ Classify this turn before acting, and name the lane in one line.
97
97
  - Authorises a change? YOU pick the lane, say it and why: simple -> FTD (one
98
98
  feature document, tasks checked off against observed proof); big or
99
99
  interlocking decisions -> SDD (spec + clarify approved, then autopilot or not).
100
- - Complex/production: no commits on the default branch, branch + PR. Simple:
101
- default branch. Another session here: .worktrees/<branch>.
100
+ - Code change? Ask first, every time: a branch or the default branch? Never
101
+ branch on your own. Another session here: .worktrees/<branch>.
102
102
  Method: \`ftd\` · chain: \`sdd\` · full decisor in the always-on \`suggest\` body.
103
103
  =================================================
104
104
  `;
@@ -104,7 +104,7 @@ const worktreeBlob = (root, path) => (existsSync(join(root, path)) ? line(root,
104
104
  function readState(root) {
105
105
  let s = {};
106
106
  try { s = JSON.parse(readFileSync(join(root, '.rsc', STATE), 'utf8')); } catch { /* first run */ }
107
- return { announced: false, seen: null, queue: [], ours: [], notices: [], lastFetch: 0, snap: null, head: null, ...s };
107
+ return { announced: false, seenBy: {}, queue: [], ours: [], notices: [], lastFetch: 0, snap: null, head: null, ...s };
108
108
  }
109
109
 
110
110
  function writeState(root, s) {
@@ -375,30 +375,35 @@ export function applyIncoming(root, s) {
375
375
  if (!branch || branch === 'HEAD') return said;
376
376
  const tip = line(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${KNOWLEDGE_BRANCH}`]);
377
377
  if (!tip) return said;
378
- if (!s.seen || !line(root, ['rev-parse', '--verify', '--quiet', `${s.seen}^{commit}`])) {
379
- s.seen = line(root, ['merge-base', 'HEAD', tip]) || tip;
378
+ // Per branch, not per repo: a branch opened from main after the last sync starts behind what
379
+ // rsc/knowledge already has, and a single "last seen" made it look up to date (2026-10-06). A branch
380
+ // seen for the first time catches up from where it left rsc/knowledge.
381
+ s.seenBy = s.seenBy && typeof s.seenBy === 'object' ? s.seenBy : {};
382
+ let seen = s.seenBy[branch];
383
+ if (!seen || !line(root, ['rev-parse', '--verify', '--quiet', `${seen}^{commit}`])) {
384
+ seen = line(root, ['merge-base', 'HEAD', tip]) || tip;
380
385
  }
381
- if (s.seen === tip) return said;
386
+ if (seen === tip) { s.seenBy[branch] = tip; return said; }
382
387
  const paths = [...KNOWLEDGE];
383
- const theirs = git(root, ['rev-list', `${s.seen}..${tip}`, '--', ...paths]).split('\n').filter(Boolean)
384
- .filter((c) => !s.ours.includes(c));
385
- if (!theirs.length) { s.seen = tip; return said; }
388
+ const commits = git(root, ['rev-list', `${seen}..${tip}`, '--', ...paths]).split('\n').filter(Boolean);
389
+ if (!commits.length) { s.seenBy[branch] = tip; return said; }
386
390
 
387
- const know = zlist(git(root, ['diff', '--name-only', '--no-renames', '-z', s.seen, tip, '--', ...paths]))
391
+ const know = zlist(git(root, ['diff', '--name-only', '--no-renames', '-z', seen, tip, '--', ...paths]))
388
392
  .filter(isKnowledge).slice(0, MAX_FILES);
389
- const who = authorsOf(root, theirs);
390
393
  const closed = branch === defaultBranch(root) && trunkClosed(root);
394
+ const touchedBy = (f) => git(root, ['rev-list', `${seen}..${tip}`, '--', f]).split('\n').filter(Boolean);
391
395
  const take = [];
392
396
  const clash = [];
393
397
  for (const f of know) {
394
398
  const incoming = blob(root, tip, f);
395
399
  const mine = worktreeBlob(root, f);
396
400
  if (mine === incoming) continue;
397
- const base = blob(root, s.seen, f);
401
+ const base = blob(root, seen, f);
398
402
  // On a closed trunk HEAD lags behind on purpose (what came in was not committed), so only the
399
403
  // working tree says whether you touched it (review H2).
400
404
  if (mine === base && (closed || blob(root, 'HEAD', f) === base)) take.push(f);
401
- else clash.push(f);
405
+ // Touched here and changed only by your own uploads: your newer version, not a clash.
406
+ else if (touchedBy(f).some((c) => !s.ours.includes(c))) clash.push(f);
402
407
  }
403
408
 
404
409
  if (take.length) {
@@ -414,12 +419,16 @@ export function applyIncoming(root, s) {
414
419
  } else {
415
420
  git(root, ['--literal-pathspecs', 'reset', '--quiet', '--', ...take]); // updated, not staged
416
421
  }
417
- said.push(`📥 Traídos ${theirs.length} cambio(s) de conocimiento de ${who}: ${listed(take)}.`);
422
+ const from = [...new Set(take.flatMap(touchedBy))];
423
+ const theirs = from.filter((c) => !s.ours.includes(c));
424
+ const who = theirs.length ? authorsOf(root, theirs) : 'ti, desde otra rama';
425
+ said.push(`📥 Traídos ${from.length} cambio(s) de conocimiento de ${who}: ${listed(take)}.`);
418
426
  }
419
427
  if (clash.length) {
428
+ const who = authorsOf(root, [...new Set(clash.flatMap(touchedBy))].filter((c) => !s.ours.includes(c)));
420
429
  said.push(`${who} cambió ${listed(clash)}, que tú también has tocado. No lo traigo solo: tu versión sigue intacta.`);
421
430
  }
422
- s.seen = tip;
431
+ s.seenBy[branch] = tip;
423
432
  return said;
424
433
  }
425
434
 
@@ -101,8 +101,9 @@ export function teamSafeAnnouncement(root) {
101
101
  return `
102
102
  ===== rsc 3.0 · equipo seguro por defecto =====
103
103
  ACTION: díselo a la persona en pocas líneas, una vez, antes de su petición:
104
- 1. En proyectos complejos o con producción, el agente no trabaja en la rama principal: abre ramas y
105
- cierra con PR. En proyectos sencillos trabaja en la principal. Para abrirla: «desbloquea main».
104
+ 1. Antes de cada cambio de código, el agente pregunta: ¿rama nueva o directamente en la principal?
105
+ Nunca abre una rama por su cuenta. Si la principal está cerrada (CI, despliegue o equipo), pregunta
106
+ si abre una rama o la desbloquea («desbloquea main»).
106
107
  2. Si otra sesión de un asistente está trabajando en esta misma carpeta, el trabajo nuevo va a un
107
108
  worktree en .worktrees/<rama>/. Para no hacerlo: «no uses worktrees».
108
109
  3. El agente elige solo entre FTD (lo sencillo) y SDD (lo grande o complejo) y lo dice; se puede