@ericrisco/rsc 3.0.6 → 3.0.8
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 +61 -17
- package/manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/audit.js +11 -0
- package/scripts/doctor.js +63 -11
- package/scripts/install-apply.js +224 -18
- package/scripts/lib/onboarding-apply.js +27 -24
- package/scripts/lib/onboarding.js +19 -4
- package/scripts/lib/tools-skeleton.js +76 -0
- package/scripts/rsc.js +93 -13
- package/skills/suggest/SKILL.md +5 -4
- package/targets/branch-guard.mjs +94 -26
- package/targets/claude.js +36 -6
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/danger-guard.mjs +80 -14
- package/targets/deepseek-bridge.js +86 -0
- package/targets/deepseek-bridge.mjs +90 -0
- package/targets/ftd-nudge.mjs +137 -0
- package/targets/gitmoji-guard.mjs +14 -1
- package/targets/index.js +4 -0
- package/targets/knowledge-sync.mjs +203 -45
- package/targets/knowledge-wiring.js +2 -0
- package/targets/memory.js +7 -0
- package/targets/opt-outs.js +1 -0
- package/targets/sello.mjs +9 -2
- package/targets/session-memory-adapter.mjs +28 -4
- package/targets/session-memory-core.mjs +211 -36
- package/targets/session-start.mjs +40 -5
- package/targets/shell-unwrap.mjs +145 -0
- package/targets/ship-guard.mjs +40 -9
- package/targets/team-safe-start.mjs +76 -10
- package/targets/update-wiring.js +2 -1
- package/targets/worktree-reaper.mjs +109 -3
package/README.md
CHANGED
|
@@ -31,6 +31,27 @@ npx @ericrisco/rsc@latest onboard
|
|
|
31
31
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
+
## 🐋 New: DeepSeek Harness
|
|
35
|
+
|
|
36
|
+
[DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh) (`dsh`) is now a supported
|
|
37
|
+
assistant, the 18th. Pick it in the wizard or pass `--target deepseek`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @ericrisco/rsc@latest onboard --target deepseek
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Skills** go to `.dsh/skills/`, where dsh finds them on its own.
|
|
44
|
+
- **Instructions** go in the rsc block of `AGENTS.md`, which dsh already reads.
|
|
45
|
+
- **Session memory, knowledge sync and the update notice** run from dsh's own hooks. dsh has no
|
|
46
|
+
hooks per project, so rsc adds one marked block to `~/.dsh/cordis.patch.yml` once per machine. That
|
|
47
|
+
block runs the hooks of whichever project the session is in. It works in `dsh web` with several
|
|
48
|
+
projects open, and in a project without rsc it does nothing. Restart dsh once after the first
|
|
49
|
+
install. To turn it off, delete the block.
|
|
50
|
+
|
|
51
|
+
Details: [targets table](#multi-target).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
34
55
|
## Your agent needs a harness. It should not improvise one.
|
|
35
56
|
|
|
36
57
|
A model is only the brain. Real work also needs project memory, tools, domain knowledge, rules and
|
|
@@ -73,7 +94,7 @@ rsc 3.0 fixes all four, by default, with no new commands to learn:
|
|
|
73
94
|
| **You choose how the project works** | The install asks once: straight on `main`, or branches and pull requests? Branches are recommended only for long-lived, complex code. With `main` the agent works on it and never asks about branches. With branches a commit on `main` is refused, and before each code change the agent asks: this branch, a new one, or unlock `main`? It never opens a branch on its own. | Say **«unlock main»** / **«lock main»**, or `rsc main unlock` / `lock` |
|
|
74
95
|
| **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
96
|
| **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` |
|
|
97
|
+
| **Knowledge reaches everyone** | `01-TOOLS/`, `02-DOCS/wiki/` and `02-DOCS/attachments/` 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
98
|
|
|
78
99
|
**Commit, push and pull request without a prompt each time.** A harness installed from scratch lets
|
|
79
100
|
the agent run `git commit`, `git push` and `gh pr create` without asking, because those are the steps
|
|
@@ -89,6 +110,7 @@ someone runs `rsc git-permissions on`.
|
|
|
89
110
|
| Gemini | `.gemini/settings.json` → `tools.allowed` |
|
|
90
111
|
| OpenCode | `opencode.json` → `permission.bash` (force-push → `ask`) |
|
|
91
112
|
| Cursor | Not covered: its CLI matches only the first word, so allowing a push would allow every git command. |
|
|
113
|
+
| DeepSeek Harness | Not covered: dsh has no per-command allow list. Its own Permissions selector (`/permission`) sets approval for every command at once. |
|
|
92
114
|
|
|
93
115
|
```bash
|
|
94
116
|
rsc git-permissions status # decided or not, and wired per assistant
|
|
@@ -241,8 +263,8 @@ no method deciding whether each piece belongs. rsc-harness keeps that constructi
|
|
|
241
263
|
- **Specialists follow the stack.** The four base agents stay small; installing a
|
|
242
264
|
supported stack adds only its reviewer and build resolver. `rsc add go`, for
|
|
243
265
|
example, adds the Go pair without pulling reviewers for every other language.
|
|
244
|
-
- **A new local session continues the old one.** Claude Code, Codex, Gemini CLI
|
|
245
|
-
and
|
|
266
|
+
- **A new local session continues the old one.** Claude Code, Codex, Gemini CLI,
|
|
267
|
+
OpenCode and DeepSeek Harness load a bounded checkpoint for the current branch and worktree at
|
|
246
268
|
session start. Cursor desktop uses an assisted read-before-action fallback.
|
|
247
269
|
- **Honestly good.** Every skill was built by a research → spec → implement →
|
|
248
270
|
*adversarial review* pipeline and had to clear an objective rubric
|
|
@@ -262,7 +284,8 @@ When a new session opens in the same checkout, that state is injected before the
|
|
|
262
284
|
first agent action. A completed edit is preserved even if the previous client
|
|
263
285
|
closed before its normal session-end event.
|
|
264
286
|
|
|
265
|
-
- **Full:** Claude Code, Codex, Gemini CLI and
|
|
287
|
+
- **Full:** Claude Code, Codex, Gemini CLI, OpenCode and DeepSeek Harness (through rsc's machine
|
|
288
|
+
bridge, see [targets](#multi-target)). Codex asks you to inspect
|
|
266
289
|
and trust the project hook once with `/hooks`; until then `doctor` reports that
|
|
267
290
|
trust is still required.
|
|
268
291
|
- **Assisted:** Cursor desktop. Its start hook is fire-and-forget, so rsc also
|
|
@@ -448,8 +471,10 @@ list somebody has to remember to update is a list that goes quietly stale. Decla
|
|
|
448
471
|
|
|
449
472
|
### Your wiki and tools stay in sync — on by default
|
|
450
473
|
|
|
451
|
-
`01-TOOLS/` and `02-DOCS/` are the team's knowledge, so rsc keeps them
|
|
452
|
-
without anybody thinking about git, **and without ever pushing to `main`**.
|
|
474
|
+
`01-TOOLS/`, `02-DOCS/wiki/` and `02-DOCS/attachments/` are the team's knowledge, so rsc keeps them
|
|
475
|
+
the same on every machine without anybody thinking about git, **and without ever pushing to `main`**.
|
|
476
|
+
Exactly those three folders, minus rsc's own `01-TOOLS/_TEMPLATE/` and your personal
|
|
477
|
+
`02-DOCS/wiki/harness/user-profile.md`. Your `main` is
|
|
453
478
|
protected, or should be; rsc assumes it is.
|
|
454
479
|
|
|
455
480
|
The knowledge travels through one exchange branch, `rsc/knowledge`, that nobody works on by hand:
|
|
@@ -461,13 +486,22 @@ main · protected untouched — gets it inside the next merged pull reque
|
|
|
461
486
|
new · clones main has it all after the first message
|
|
462
487
|
```
|
|
463
488
|
|
|
464
|
-
- **When a turn ends**, your changes in those folders are committed
|
|
465
|
-
and sent to `rsc/knowledge` on `origin`, from whatever branch you are on.
|
|
466
|
-
`
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
- **When a turn ends**, your changes in those folders are committed on your branch as
|
|
490
|
+
`📝 docs(auto): …` and sent to `rsc/knowledge` on `origin`, from whatever branch you are on. Only
|
|
491
|
+
the copy on `rsc/knowledge` carries `[skip ci]`; the commit on your branch does not, so your next
|
|
492
|
+
ordinary push still runs CI. Two cases commit nothing on your branch: a closed `main`, and code of
|
|
493
|
+
yours still uncommitted (a docs commit would land ahead of the code it describes). Then the snapshot
|
|
494
|
+
is built aside, only goes to `rsc/knowledge`, and the files stay modified for you to commit with
|
|
495
|
+
your work. Knowledge that you or the agent already **committed** on the branch goes up too — only
|
|
496
|
+
its knowledge part, under a `docs(auto) … [skip ci]` message. Commits that came in by merging
|
|
497
|
+
`main` are your teammates', not yours, and are not sent again.
|
|
498
|
+
- **Before each message**, what teammates sent is brought into **the branch you are on** as a
|
|
499
|
+
`📥 docs(auto): sync` commit, and you are told in one line. Only the knowledge folders are touched.
|
|
500
|
+
A doc that `main` on `origin` already has, identical, is left for your ordinary merge of `main`. On
|
|
501
|
+
a closed `main` nothing is written at all — an uncommitted file there would make your next
|
|
502
|
+
`git pull` refuse to run — you are told what is waiting, and it arrives with that pull or in the
|
|
503
|
+
next branch you open. When the last fetch is more than ten minutes old, the message fetches first
|
|
504
|
+
(three seconds at most), so even a one-message session sees fresh docs.
|
|
471
505
|
A branch you open later catches up on its first message: everything on `rsc/knowledge` since it
|
|
472
506
|
left `main` comes down, including what you wrote yourself on another branch.
|
|
473
507
|
- **It reaches `main` the normal way.** Your feature branch now carries the team's knowledge, so it
|
|
@@ -482,14 +516,14 @@ What it will not do, by design:
|
|
|
482
516
|
| --- | --- |
|
|
483
517
|
| Push to `main` | Never. Only `rsc/knowledge` receives anything. |
|
|
484
518
|
| Push your code or your unpushed commits | Only the knowledge folders go up, even from a commit that also has code. Your code waits for you. |
|
|
485
|
-
| Trigger CI or a deploy | Every
|
|
519
|
+
| Trigger CI or a deploy | Every commit on `rsc/knowledge` says `[skip ci]`; the one on your branch does not, so it never skips the CI of your own push. |
|
|
486
520
|
| Bring in anybody else's code | Only knowledge paths come down. Code, config and `.claude/` are never pulled. |
|
|
487
521
|
| Sync `02-DOCS/wiki/harness/user-profile.md` | Those are one person's dials. |
|
|
488
522
|
| Overwrite a file you are editing | It tells you, and your version stays. |
|
|
489
523
|
| Run where it has no business | No `origin`, no knowledge folders, a rebase in progress, a cloud agent: it stays quiet. |
|
|
490
524
|
|
|
491
525
|
It is wired into the assistant's own turn hooks (end of turn, new message) in Claude Code, Codex,
|
|
492
|
-
Gemini, Cursor and
|
|
526
|
+
Gemini, Cursor, OpenCode and DeepSeek Harness (tested end to end in Claude Code, Codex and OpenCode). The network part runs in the background, so a turn never waits for
|
|
493
527
|
it. It is kept apart from the [local session memory](#new-sessions-pick-up-the-latest-local-work),
|
|
494
528
|
which promises never to touch the network. The first turn says it is on. To turn it off for the
|
|
495
529
|
project:
|
|
@@ -550,7 +584,7 @@ A new install asks whether you want this (yes by default). To turn it off later,
|
|
|
550
584
|
`.rsc.json` like the other switches. The update can leave harness files changed in git; rsc never
|
|
551
585
|
commits them for you.
|
|
552
586
|
|
|
553
|
-
Where it runs on its own: **Claude Code, Codex, Gemini CLI, Cursor and
|
|
587
|
+
Where it runs on its own: **Claude Code, Codex, Gemini CLI, Cursor, OpenCode and DeepSeek Harness**, from a
|
|
554
588
|
session-start hook. Every other assistant has no such hook, so the always-on instructions ask the
|
|
555
589
|
agent to run `node .rsc/auto-update.mjs` on its first turn — same rules, but it depends on the agent
|
|
556
590
|
doing it (and some assistants ask your permission before running a command). Codex asks you to
|
|
@@ -772,14 +806,23 @@ duplicated. The wizard asks which ones; `--target a,b` does it non-interactively
|
|
|
772
806
|
| `roo` | `.roo/rsc/<id>/` → symlink | rule in `.roo/rules/rsc-suggest.md` |
|
|
773
807
|
| `amp` | `.amp/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
774
808
|
| `opencode` | `.opencode/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
809
|
+
| `deepseek` | `.dsh/skills/<id>/` → symlink (DeepSeek Harness discovers it natively) | block in `AGENTS.md` |
|
|
775
810
|
| `jules` | `.jules/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
776
811
|
| `junie` | `.junie/rsc/<id>/` → symlink | block in `.junie/guidelines.md` |
|
|
777
812
|
| `kiro` | `.kiro/rsc/<id>/` → symlink | doc in `.kiro/steering/rsc-suggest.md` |
|
|
778
813
|
| `aider` | `.aider/rsc/<id>/` → symlink | block in `CONVENTIONS.md` |
|
|
779
814
|
|
|
780
|
-
> `codex`, `zed`, `amp`, `opencode` and `jules` all share the one root
|
|
815
|
+
> `codex`, `zed`, `amp`, `opencode`, `deepseek` and `jules` all share the one root
|
|
781
816
|
> `AGENTS.md`; the block is idempotent, so picking several writes it once.
|
|
782
817
|
|
|
818
|
+
> **DeepSeek Harness** (`dsh`) has no project-level hook config: its plugins are per machine. So
|
|
819
|
+
> the `deepseek` target writes the project hooks to `.dsh/hooks.json` (Codex format, untracked) and,
|
|
820
|
+
> once per machine, adds one marked block to `~/.dsh/cordis.patch.yml` (or `$DSH_HOME`) that mounts
|
|
821
|
+
> dsh's Codex-hooks bridge on rsc's dispatcher in `~/.dsh/rsc/`. The dispatcher runs the hooks of
|
|
822
|
+
> whichever project the session is in, so `dsh web` serving several workspaces works too, and a
|
|
823
|
+
> project without rsc runs nothing. Restart dsh once after the first install. Delete the block to
|
|
824
|
+
> turn it off.
|
|
825
|
+
|
|
783
826
|
The richer surfaces are intentionally narrower than skill support:
|
|
784
827
|
|
|
785
828
|
| Targets | Stack agents | Native commands | Local session continuation |
|
|
@@ -788,6 +831,7 @@ The richer surfaces are intentionally narrower than skill support:
|
|
|
788
831
|
| Codex | yes | no separate project-command surface | full after `/hooks` trust |
|
|
789
832
|
| Cursor desktop | yes | yes | assisted |
|
|
790
833
|
| Gemini CLI, OpenCode | yes | yes | full |
|
|
834
|
+
| DeepSeek Harness | unsupported | unsupported | full (through the machine bridge) |
|
|
791
835
|
| GitHub Copilot | yes | yes | unsupported |
|
|
792
836
|
| Junie, Kiro | yes | unsupported | unsupported |
|
|
793
837
|
| Windsurf, Cline, Roo | unsupported | yes | unsupported |
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericrisco/rsc",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
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": {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"claude-code",
|
|
47
47
|
"cursor",
|
|
48
48
|
"codex",
|
|
49
|
+
"deepseek",
|
|
50
|
+
"deepseek-harness",
|
|
49
51
|
"gemini",
|
|
50
52
|
"antigravity",
|
|
51
53
|
"cli",
|
package/scripts/audit.js
CHANGED
|
@@ -207,6 +207,17 @@ export function stampAudit(cwd, date = new Date().toISOString()) {
|
|
|
207
207
|
writeFileSync(join(dir, 'audit.json'), JSON.stringify({ lastRun: date }, null, 2) + '\n');
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// The start of the cadence for a harness that has never been audited: written by onboarding so the
|
|
211
|
+
// first audit falls due STALE_DAYS after install, not in the first session. `baseline` and not
|
|
212
|
+
// `lastRun`, because no audit ran — the SessionStart hook reads either. Leaves an existing file alone.
|
|
213
|
+
export function recordAuditBaseline(cwd, date = new Date().toISOString()) {
|
|
214
|
+
const file = join(cwd, '.rsc', 'audit.json');
|
|
215
|
+
if (existsSync(file)) return false;
|
|
216
|
+
mkdirSync(join(cwd, '.rsc'), { recursive: true });
|
|
217
|
+
writeFileSync(file, JSON.stringify({ baseline: date }, null, 2) + '\n');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
// Write the report into the harness wiki when one exists; always stamp .rsc/audit.json.
|
|
211
222
|
export function writeAuditReport(report, cwd = process.cwd()) {
|
|
212
223
|
const written = [];
|
package/scripts/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { hooksDir, hooksDirIsOurs } from '../targets/worktree-reaper.mjs';
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
1
|
+
import { hooksDir, hooksDirIsOurs, classifyWorktrees } from '../targets/worktree-reaper.mjs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, relative, sep } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import { targetPaths, TARGET_IDS } from '../targets/index.js';
|
|
6
7
|
import { readState } from './lib/state.js';
|
|
@@ -101,7 +102,18 @@ export function missingHookScripts({ target, home = homedir(), cwd = process.cwd
|
|
|
101
102
|
// Is the post-merge trigger actually armed in this clone? Three states, because they need three
|
|
102
103
|
// different answers: armed, absent (repair it), or someone else's (leave it — chaining is our job
|
|
103
104
|
// at install time, and overwriting a husky hook to fix a convenience would be indefensible).
|
|
104
|
-
|
|
105
|
+
// The skills that read or write a design identity (02-DOCS/wiki/brand/) or a starting point. The
|
|
106
|
+
// state is always reported; the FIX — "run design-loop, then design-dna" — is advice, and advice needs
|
|
107
|
+
// evidence. A todo web app with none of these installed was told to run two skills it does not have
|
|
108
|
+
// and never asked for (E2E 2026-10-07).
|
|
109
|
+
const DESIGN_SKILLS = new Set(['design', 'design-loop', 'design-dna', 'brand-voice', 'content-engine', 'marketing', 'presentations', 'make']);
|
|
110
|
+
function withoutUnfoundedFix(summary, inPlay) {
|
|
111
|
+
if (inPlay || !summary || !summary.fix) return summary;
|
|
112
|
+
const { fix, ...rest } = summary;
|
|
113
|
+
return { ...rest, note: 'no design skill is installed here, so nothing is recommended' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function worktreeCleanupStatus(root, policy) {
|
|
105
117
|
if (!existsSync(join(root, '.git'))) return { state: 'not-a-repo' };
|
|
106
118
|
if (existsSync(join(root, '.rsc', '.no-worktree-cleanup'))) return { state: 'off-by-choice' };
|
|
107
119
|
// Ask git where hooks live instead of assuming `.git/hooks`. Until 2.0.1 this read the assumed
|
|
@@ -118,6 +130,13 @@ function worktreeCleanupStatus(root) {
|
|
|
118
130
|
}
|
|
119
131
|
const hook = join(dir, 'post-merge');
|
|
120
132
|
if (!existsSync(hook)) {
|
|
133
|
+
// The merge hook is one of the code hooks, and the accepted plan can defer those. Recommending
|
|
134
|
+
// `repair` then is a loop with no exit — repair re-runs the same install, which honours the same
|
|
135
|
+
// plan and skips the hook again — and it contradicted `healthy: true` on every fresh small
|
|
136
|
+
// software install (E2E 2026-10-07). Said as what it is, with nothing to do.
|
|
137
|
+
if (policy?.codeHooks === false) {
|
|
138
|
+
return { state: 'not-in-plan', reason: 'the accepted plan defers code hooks, and the post-merge cleanup is one of them; `rsc reassess` revisits that.' };
|
|
139
|
+
}
|
|
121
140
|
return { state: 'absent', action: 'Run `npx @ericrisco/rsc repair` to arm the post-merge cleanup in this clone.' };
|
|
122
141
|
}
|
|
123
142
|
try {
|
|
@@ -128,6 +147,27 @@ function worktreeCleanupStatus(root) {
|
|
|
128
147
|
} catch { return { state: 'unreadable' }; }
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
// The two things a team trips over with worktrees (team simulation D9): a `.worktrees/` git does not
|
|
151
|
+
// ignore — one `git add -A` away from committing a whole checkout — and landed worktrees still on
|
|
152
|
+
// disk, which look exactly like live ones. Reported with the way out, never blocking `healthy`: both
|
|
153
|
+
// cost tidiness, not work.
|
|
154
|
+
const realOf = (p) => { try { return realpathSync.native(p); } catch { return p; } };
|
|
155
|
+
function worktreeHygiene(root) {
|
|
156
|
+
if (!existsSync(join(root, '.git'))) return { state: 'not-a-repo' };
|
|
157
|
+
const probe = spawnSync('git', ['-C', root, 'check-ignore', '-q', '--no-index', '.worktrees/rsc-probe/file'], { encoding: 'utf8' });
|
|
158
|
+
const ignored = probe.status === 0;
|
|
159
|
+
let leftovers = [];
|
|
160
|
+
try {
|
|
161
|
+
leftovers = classifyWorktrees(root)
|
|
162
|
+
.filter((c) => c.verdict === 'safe' || c.verdict === 'ask')
|
|
163
|
+
.map((c) => ({ path: relative(realOf(root), c.path).split(sep).join('/') || c.path, branch: c.branch, verdict: c.verdict }));
|
|
164
|
+
} catch { /* unanswerable is not a finding */ }
|
|
165
|
+
const actions = [];
|
|
166
|
+
if (!ignored) actions.push('`.worktrees/` is not ignored by git here: run `npx @ericrisco/rsc sync` (it adds it to the rsc block of .gitignore) and commit .gitignore so every clone gets it.');
|
|
167
|
+
if (leftovers.length) actions.push(`${leftovers.length} landed worktree(s) are still on disk: \`npx @ericrisco/rsc worktrees\` lists them, \`npx @ericrisco/rsc worktrees reap\` removes the safe ones.`);
|
|
168
|
+
return { ignored, leftovers, ...(actions.length ? { action: actions.join(' ') } : {}) };
|
|
169
|
+
}
|
|
170
|
+
|
|
131
171
|
// Every skill this catalog ships declares `origin: risco` in its own frontmatter — all 272 of them,
|
|
132
172
|
// uniformly. So "is this ours?" is answerable from the file itself, and does not need the list in
|
|
133
173
|
// `.rsc.json` that nobody ever filled: that list is parallel accounting (P3), it has been empty in
|
|
@@ -184,8 +224,8 @@ export function doctor({ target, home, cwd }) {
|
|
|
184
224
|
try {
|
|
185
225
|
memory.metrics = hasStore
|
|
186
226
|
? metricsSummary({ cwd: root })
|
|
187
|
-
: { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost:
|
|
188
|
-
} catch { memory.metrics = { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost:
|
|
227
|
+
: { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost: null, toolCalls: null }, unknown: { cost: 0, toolCalls: 0 } };
|
|
228
|
+
} catch { memory.metrics = { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost: null, toolCalls: null }, unknown: { cost: 0, toolCalls: 0 } }; }
|
|
189
229
|
}
|
|
190
230
|
const actualSkills = listSkills({ target, home, cwd: root }).map((entry) => entry.id);
|
|
191
231
|
const actualAgents = listAgents({ target, home, cwd: root }).agents.map((entry) => entry.id);
|
|
@@ -208,6 +248,9 @@ export function doctor({ target, home, cwd }) {
|
|
|
208
248
|
const path = agentPath(target, root, id);
|
|
209
249
|
return path && !existsSync(path);
|
|
210
250
|
}).map((id) => ({ id, path: agentPath(target, root, id), action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
|
|
251
|
+
// What THIS harness installed, not every skill visible from here: a design skill in the user's
|
|
252
|
+
// global scope is how a todo app's doctor came to recommend design-dna (E2E 2026-10-07).
|
|
253
|
+
const designInPlay = Object.keys(state.skills || {}).some((id) => DESIGN_SKILLS.has(id));
|
|
211
254
|
const report = {
|
|
212
255
|
target,
|
|
213
256
|
installed: Object.keys(state.skills),
|
|
@@ -224,17 +267,18 @@ export function doctor({ target, home, cwd }) {
|
|
|
224
267
|
// The worktree cleanup's trigger is a git hook, and `.git/hooks/` is not cloned. So a repo can
|
|
225
268
|
// carry a perfectly healthy harness and still never clean up, on every machine but the one that
|
|
226
269
|
// ran the installer. Reported, never nagged about: `repair` is what puts it back (P6).
|
|
227
|
-
worktreeCleanup: worktreeCleanupStatus(root),
|
|
270
|
+
worktreeCleanup: worktreeCleanupStatus(root, state.policy),
|
|
271
|
+
worktreeHygiene: worktreeHygiene(root),
|
|
228
272
|
ownSkills: ownSkillsIn(paths, readProjectManifest(root)?.ownSkills || []),
|
|
229
273
|
sello: selloStatus(root),
|
|
230
274
|
// Whether this harness has a design identity at all. `design` has always DECLARED that it
|
|
231
275
|
// stops without one; until lib/design-identity.js nothing checked it (P2). Reported here,
|
|
232
276
|
// never nagged about and never blocking: a missing identity is low risk (P7).
|
|
233
|
-
designIdentity: designIdentity(root),
|
|
277
|
+
designIdentity: withoutUnfoundedFix(designIdentity(root), designInPlay),
|
|
234
278
|
// The other half of the same question: an identity is what this project settled on, a starting
|
|
235
279
|
// point is what it has to settle FROM. Three skills promise to propose one and none could look;
|
|
236
280
|
// this is the look. One field, not a section — a report grows for every user who runs it (P5).
|
|
237
|
-
designStartingPoint: startingPointSummary(root),
|
|
281
|
+
designStartingPoint: withoutUnfoundedFix(startingPointSummary(root), designInPlay),
|
|
238
282
|
// An inert gate must never read as an armed one (the same rule the sello follows):
|
|
239
283
|
// the gitmoji guard has a per-project kill switch, so say which state it is in.
|
|
240
284
|
gitmojiGuard: state.policy?.gitmojiGuard === false
|
|
@@ -306,9 +350,17 @@ const bytesOf = (p) => { try { return statSync(p).size; } catch { return 0; } };
|
|
|
306
350
|
|
|
307
351
|
// The always-on body a wired scope injects: the hook command embeds the exact SKILL.md path,
|
|
308
352
|
// so read it from there rather than guessing the layout.
|
|
353
|
+
// The body the SessionStart hook prints is the `.md` file its command names — today the short
|
|
354
|
+
// `.rsc/suggest-always-on.md`, not the whole `suggest` SKILL.md. Reading only `…SKILL.md` out of the
|
|
355
|
+
// command meant the current wiring never matched and the fallback counted the full skill: doctor said
|
|
356
|
+
// 7.1 KB for a hook that injects 0.3 KB of body (E2E 2026-10-07). The command names the project
|
|
357
|
+
// through ${CLAUDE_PROJECT_DIR}, so that is resolved against the scope before measuring.
|
|
309
358
|
function alwaysOnBytesFor(scopeRoot, settingsRaw) {
|
|
310
|
-
const
|
|
311
|
-
|
|
359
|
+
const raw = (settingsRaw || '').replace(/\\\\/g, '/');
|
|
360
|
+
const candidates = [...raw.matchAll(/\\?"([^"\\]*suggest[^"\\]*\.md)\\?"/g)].map((m) => m[1]
|
|
361
|
+
.replace(/\$\{CLAUDE_PROJECT_DIR\}|\$CLAUDE_PROJECT_DIR/g, scopeRoot)
|
|
362
|
+
.replace(/^~(?=\/)/, homedir()));
|
|
363
|
+
for (const file of candidates) if (bytesOf(file)) return bytesOf(file);
|
|
312
364
|
return bytesOf(join(scopeRoot, '.rsc', 'skills', 'suggest', 'SKILL.md'));
|
|
313
365
|
}
|
|
314
366
|
|