@ericrisco/rsc 0.1.16 → 0.1.17

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
@@ -273,7 +273,7 @@ duplicated. The wizard asks which ones; `--target a,b` does it non-interactively
273
273
 
274
274
  | Target | Skill destination (→ `.rsc/skills/<id>/`) | Always-on detector |
275
275
  | --- | --- | --- |
276
- | `claude` | `.claude/skills/rsc/<id>/` → symlink | SessionStart hook in `.claude/settings.json` |
276
+ | `claude` | `.claude/skills/<id>/` → symlink (copy on Windows) | SessionStart hook in `.claude/settings.json` |
277
277
  | `codex` | `.codex/rsc/<id>/` → symlink | block in `AGENTS.md` |
278
278
  | `copilot` | `.github/rsc/<id>/` → symlink | block in `.github/copilot-instructions.md` |
279
279
  | `cursor` | `.cursor/rules/<id>.mdc` (converted) | always-apply rule |
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.16",
2
+ "version": "0.1.17",
3
3
  "counts": {
4
4
  "skills": 231
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,10 +49,14 @@ profiles: [core, full] # optional: named-profile membership
49
49
 
50
50
  ## Invocation
51
51
 
52
- There are no bundles and no `/<bundle>:<id>` namespacing. A skill installs under
53
- the target's rsc namespace (e.g. `~/.claude/skills/rsc/<id>/`) and is invoked by
54
- its `name`. The `suggest` detector is always installed (the floor) and proposes
55
- installing any skill a task needs via `npx @ericrisco/rsc add <id>`.
52
+ There are no bundles and no `/<bundle>:<id>` namespacing. Most targets install a
53
+ skill under their own rsc folder (e.g. `.codex/rsc/<id>/`), reached from that
54
+ assistant's instructions file. **Claude is the exception:** Claude Code only
55
+ discovers project skills one level under `.claude/skills/`, so rsc skills install
56
+ **flat** at `.claude/skills/<id>/SKILL.md` (a nested `.claude/skills/rsc/<id>/`
57
+ is never discovered). A skill is invoked by its `name`. The `suggest` detector is
58
+ always installed (the floor) and proposes installing any skill a task needs via
59
+ `npx @ericrisco/rsc add <id>`.
56
60
 
57
61
  ## Wiring steps for a new skill
58
62
 
@@ -388,7 +388,8 @@ generalizes the SDD session-summary convention to *any* work.
388
388
 
389
389
  - **Hook — `PreCompact` / `SessionEnd`** (Claude Code): fires right before context
390
390
  is lost or the session ends. The hook only *reminds*; the agent writes the
391
- worklog (Karpathy: the LLM writes). Wired by `targets/` → `.rsc/worklog-checkpoint.sh`.
391
+ worklog (Karpathy: the LLM writes). Wired by `targets/` → `.rsc/worklog-checkpoint.mjs`
392
+ (run via `node`, so it works on Windows too).
392
393
  - **Explicit milestone**: after a commit or a shipped feature, capture a worklog.
393
394
  - **Daily curation automation**: distills any pending worklog raw on a timer (see
394
395
  Continuous Improvement and `daily-curation-automation.md`).
package/targets/claude.js CHANGED
@@ -1,27 +1,34 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { linkOrCopy } from './index.js';
5
5
 
6
+ const HERE = dirname(fileURLToPath(import.meta.url));
7
+
6
8
  export function writeSkill(id, fromDir, toPath) {
9
+ // Migrate away from the legacy nested layout (.claude/skills/rsc/<id>) that
10
+ // Claude Code never discovered — it only reads .claude/skills/<name>/SKILL.md.
11
+ // toPath is now .claude/skills/<id>; drop the stale rsc/ sibling if present.
12
+ const legacy = join(dirname(toPath), 'rsc');
13
+ if (existsSync(legacy)) rmSync(legacy, { recursive: true, force: true });
7
14
  return linkOrCopy(fromDir, toPath);
8
15
  }
9
16
 
10
- // SessionStart runs a project-local session-start.sh: it cats suggest's always-on
11
- // body (preserving prior behavior) and appends an onboarding banner when the
12
- // workspace has no harness profile yet. We materialize the script next to the
13
- // shared base and point the hook at it. Any prior rsc SessionStart entry (the old
14
- // `cat …/suggest/SKILL.md` form or a previous script form) is dropped before we
15
- // add the current one idempotent, and it migrates legacy hooks in place. Other
16
- // (non-rsc) SessionStart hooks are preserved.
17
+ // SessionStart runs a project-local session-start.mjs via `node`: it prints
18
+ // suggest's always-on body, an onboarding banner when the workspace has no harness
19
+ // profile yet, and an auto-ingest nudge when the inbox has un-ingested material. We
20
+ // invoke with `node` (not bash) so the hook runs on Windows too. We materialize the
21
+ // script next to the shared base and point the hook at it. Any prior rsc SessionStart
22
+ // entry (the old `cat …/suggest/SKILL.md` form, or a previous `.sh`/`.mjs` script
23
+ // form) is dropped before we add the current one — idempotent, migrating legacy and
24
+ // bash-era hooks in place. Other (non-rsc) SessionStart hooks are preserved.
17
25
  export function wireHook(paths) {
18
- const scriptDest = join(paths.projectRoot, '.rsc', 'session-start.sh');
26
+ const scriptDest = join(paths.projectRoot, '.rsc', 'session-start.mjs');
19
27
  mkdirSync(dirname(scriptDest), { recursive: true });
20
- copyFileSync(join(dirname(fileURLToPath(import.meta.url)), 'session-start.sh'), scriptDest);
21
- chmodSync(scriptDest, 0o755);
28
+ copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
22
29
 
23
30
  const suggestMd = `${paths.skillDir('suggest')}/SKILL.md`;
24
- const cmd = `bash "${scriptDest}" "${suggestMd}" "${paths.projectRoot}"`;
31
+ const cmd = `node "${scriptDest}" "${suggestMd}" "${paths.projectRoot}"`;
25
32
 
26
33
  const file = paths.hookTarget;
27
34
  const settings = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
@@ -29,23 +36,23 @@ export function wireHook(paths) {
29
36
  settings.hooks.SessionStart ||= [];
30
37
  settings.hooks.SessionStart = settings.hooks.SessionStart.filter((e) => {
31
38
  const s = JSON.stringify(e);
32
- return !s.includes('skills/rsc/suggest') && !s.includes('.rsc/session-start.sh');
39
+ // drop legacy cat-form and any prior session-start script (.sh from the bash era, or .mjs)
40
+ return !s.includes('skills/rsc/suggest') && !s.includes('.rsc/session-start.');
33
41
  });
34
42
  settings.hooks.SessionStart.push({ hooks: [{ type: 'command', command: cmd }] });
35
43
 
36
44
  // Worklog checkpoint: PreCompact + SessionEnd run a project-local
37
- // worklog-checkpoint.sh that reminds the agent to capture what we did this
38
- // session into 02-DOCS/raw/worklog/ (the work-driven on-ramp). Silent when the
39
- // workspace has no harness wiki. Registered idempotently on both events, with
40
- // any prior rsc worklog-checkpoint entry dropped first.
41
- const wlDest = join(paths.projectRoot, '.rsc', 'worklog-checkpoint.sh');
42
- copyFileSync(join(dirname(fileURLToPath(import.meta.url)), 'worklog-checkpoint.sh'), wlDest);
43
- chmodSync(wlDest, 0o755);
44
- const wlCmd = `bash "${wlDest}" "${paths.projectRoot}"`;
45
+ // worklog-checkpoint.mjs via `node` that reminds the agent to capture what we did
46
+ // this session into 02-DOCS/raw/worklog/ (the work-driven on-ramp). Silent when
47
+ // the workspace has no harness wiki. Registered idempotently on both events, with
48
+ // any prior rsc worklog-checkpoint entry (.sh or .mjs) dropped first.
49
+ const wlDest = join(paths.projectRoot, '.rsc', 'worklog-checkpoint.mjs');
50
+ copyFileSync(join(HERE, 'worklog-checkpoint.mjs'), wlDest);
51
+ const wlCmd = `node "${wlDest}" "${paths.projectRoot}"`;
45
52
  for (const event of ['PreCompact', 'SessionEnd']) {
46
53
  settings.hooks[event] ||= [];
47
54
  settings.hooks[event] = settings.hooks[event].filter(
48
- (e) => !JSON.stringify(e).includes('.rsc/worklog-checkpoint.sh'),
55
+ (e) => !JSON.stringify(e).includes('.rsc/worklog-checkpoint.'),
49
56
  );
50
57
  settings.hooks[event].push({ hooks: [{ type: 'command', command: wlCmd }] });
51
58
  }
package/targets/index.js CHANGED
@@ -11,16 +11,22 @@ export function baseDir(id, cwd = process.cwd()) {
11
11
  return join(cwd, '.rsc', 'skills', id);
12
12
  }
13
13
 
14
- // Point an assistant's skill folder at the shared base via a relative symlink.
15
- // Falls back to a real copy when the filesystem rejects symlinks (e.g. Windows
16
- // without privileges). Idempotent: replaces any existing link/dir at toPath.
14
+ // Point an assistant's skill folder at the shared base. On macOS/Linux a relative
15
+ // symlink avoids duplication. On Windows we copy real files: relative `dir`
16
+ // symlinks require Developer Mode/admin and are not reliably followed by skill
17
+ // discovery, so correctness wins over de-duplication. Idempotent: replaces any
18
+ // existing link/dir at toPath.
17
19
  export function linkOrCopy(fromDir, toPath) {
18
20
  mkdirSync(dirname(toPath), { recursive: true });
19
21
  try { lstatSync(toPath); rmSync(toPath, { recursive: true, force: true }); } catch { /* nothing there */ }
20
- try {
21
- symlinkSync(relative(dirname(toPath), fromDir), toPath, 'dir');
22
- } catch {
22
+ if (process.platform === 'win32') {
23
23
  cpSync(fromDir, toPath, { recursive: true });
24
+ } else {
25
+ try {
26
+ symlinkSync(relative(dirname(toPath), fromDir), toPath, 'dir');
27
+ } catch {
28
+ cpSync(fromDir, toPath, { recursive: true });
29
+ }
24
30
  }
25
31
  return [toPath];
26
32
  }
@@ -31,7 +37,7 @@ export function linkOrCopy(fromDir, toPath) {
31
37
  // is a single converted file, not a linked directory.
32
38
  const SPEC = {
33
39
  // JSON-hook + linked skill dirs
34
- claude: { root: '.claude/skills/rsc', hook: '.claude/settings.json', adapter: 'claude' },
40
+ claude: { root: '.claude/skills', hook: '.claude/settings.json', adapter: 'claude' },
35
41
  // Converted .mdc rules
36
42
  cursor: { root: '.cursor/rules', hook: '.cursor/rules/rsc-suggest.mdc', adapter: 'cursor', skillExt: '.mdc' },
37
43
  // AGENTS.md family — all read the same root AGENTS.md
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // rsc SessionStart payload (claude). Wired by targets/claude.js as `node ...` so
3
+ // it runs on every platform including Windows (no bash dependency).
4
+ // argv[2] = absolute path to suggest's SKILL.md argv[3] = absolute project root
5
+ // Always emits suggest's always-on body; appends an onboarding banner when the
6
+ // workspace has no harness profile yet, and an auto-ingest nudge when there is
7
+ // un-ingested material waiting in the inbox.
8
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ const suggestMd = process.argv[2];
12
+ const root = process.argv[3] || process.cwd();
13
+
14
+ try { process.stdout.write(readFileSync(suggestMd, 'utf8')); } catch { /* missing → emit nothing */ }
15
+
16
+ const profile = join(root, '02-DOCS', 'wiki', 'harness', 'user-profile.md');
17
+ const optout = join(root, '.rsc', '.no-harness');
18
+ if (!existsSync(profile) && !existsSync(optout)) {
19
+ process.stdout.write(`
20
+ ===== rsc onboarding =====
21
+ Fresh setup: 02-DOCS/wiki/harness/user-profile.md is missing.
22
+ ACTION: invoke \`init\` now (first contact: technical level + accompaniment dial) before the task.
23
+ If the user does not want a harness here: create .rsc/.no-harness
24
+ ==========================
25
+ `);
26
+ }
27
+
28
+ // Auto-Ingest nudge: when a harness wiki exists and the inbox holds a real file
29
+ // (anything other than README.md / dotfiles / the _processed archive), tell the
30
+ // agent to run the Auto-Ingest Sweep. The hook only reminds; the agent does the
31
+ // scan + ingest. Cheap signal here; the thorough workspace scan lives in the sweep.
32
+ const inbox = join(root, '02-DOCS', 'inbox');
33
+ if (existsSync(join(root, '02-DOCS', 'wiki')) && existsSync(inbox)) {
34
+ let pending = false;
35
+ try {
36
+ pending = readdirSync(inbox, { withFileTypes: true })
37
+ .some((e) => e.isFile() && e.name !== 'README.md' && !e.name.startsWith('.'));
38
+ } catch { /* unreadable → no nudge */ }
39
+ if (pending) {
40
+ process.stdout.write(`
41
+ ===== rsc auto-ingest =====
42
+ Un-ingested material is waiting in 02-DOCS/inbox/.
43
+ ACTION: run the Auto-Ingest Sweep now — ingest inbox/, then scan the workspace
44
+ (minus .rscignore) for un-ingested documents, recording them in wiki/.ingested.json.
45
+ Originals are copied, never moved; deleting an emptied folder needs explicit consent.
46
+ ===========================
47
+ `);
48
+ }
49
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // rsc Worklog checkpoint (claude). Wired by targets/claude.js onto PreCompact and
3
+ // SessionEnd as `node ...` so it runs on every platform including Windows.
4
+ // argv[2] = absolute project root
5
+ // Reminds the agent to run a Worklog Sweep (capture what we did this session into
6
+ // 02-DOCS/raw/worklog/). The hook only reminds; the agent writes the worklog.
7
+ // Silent when this workspace has no harness wiki yet.
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ const root = process.argv[2] || process.cwd();
12
+
13
+ // No harness wiki here → nothing to document into. Stay silent.
14
+ if (!existsSync(join(root, '02-DOCS', 'wiki'))) process.exit(0);
15
+
16
+ process.stdout.write(`
17
+ ===== rsc worklog checkpoint =====
18
+ If this session did meaningful work (files changed, a decision made, a commit),
19
+ run a WORKLOG SWEEP before context is lost:
20
+ 1. Write 02-DOCS/raw/worklog/<YYYY-MM-DD>-<slug>.md using the harness
21
+ wiki-worklog-template.md (what we did · why · files touched · outcome · next).
22
+ 2. Compile it into wiki/ (update existing articles first; wikilinks + Related);
23
+ append significant decisions to 02-DOCS/wiki/harness/decisions.md.
24
+ Skip entirely if this was a pure read/answer turn with no changes.
25
+ ==================================
26
+ `);
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env bash
2
- # rsc SessionStart payload (claude). Wired by targets/claude.js.
3
- # $1 = absolute path to suggest's SKILL.md $2 = absolute project root
4
- # Always emits suggest's always-on body; appends an onboarding banner when the
5
- # workspace has no harness profile yet, and an auto-ingest nudge when there is
6
- # un-ingested material waiting in the inbox.
7
- set -u
8
-
9
- cat "$1" 2>/dev/null
10
-
11
- profile="$2/02-DOCS/wiki/harness/user-profile.md"
12
- optout="$2/.rsc/.no-harness"
13
-
14
- if [ ! -f "$profile" ] && [ ! -f "$optout" ]; then
15
- cat <<'BANNER'
16
-
17
- ===== rsc onboarding =====
18
- Fresh setup: 02-DOCS/wiki/harness/user-profile.md is missing.
19
- ACTION: invoke `init` now (first contact: technical level + accompaniment dial) before the task.
20
- If the user does not want a harness here: create .rsc/.no-harness
21
- ==========================
22
- BANNER
23
- fi
24
-
25
- # Auto-Ingest nudge: when a harness wiki exists and the inbox holds a real file
26
- # (anything other than README.md / dotfiles / the _processed archive), tell the
27
- # agent to run the Auto-Ingest Sweep. The hook only reminds; the agent does the
28
- # scan + ingest. Cheap signal here; the thorough workspace scan lives in the sweep.
29
- inbox="$2/02-DOCS/inbox"
30
- if [ -d "$2/02-DOCS/wiki" ] && [ -d "$inbox" ]; then
31
- pending=$(find "$inbox" -maxdepth 1 -type f ! -name 'README.md' ! -name '.*' 2>/dev/null | head -1)
32
- if [ -n "$pending" ]; then
33
- cat <<'BANNER'
34
-
35
- ===== rsc auto-ingest =====
36
- Un-ingested material is waiting in 02-DOCS/inbox/.
37
- ACTION: run the Auto-Ingest Sweep now — ingest inbox/, then scan the workspace
38
- (minus .rscignore) for un-ingested documents, recording them in wiki/.ingested.json.
39
- Originals are copied, never moved; deleting an emptied folder needs explicit consent.
40
- ===========================
41
- BANNER
42
- fi
43
- fi
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env bash
2
- # rsc Worklog checkpoint payload (claude). Wired by targets/claude.js onto the
3
- # PreCompact and SessionEnd hooks.
4
- # $1 = absolute project root
5
- # Reminds the agent to run a Worklog Sweep (capture what we did this session into
6
- # 02-DOCS/raw/worklog/ so the harness can compile it into the wiki). The hook only
7
- # REMINDS — the agent writes the worklog (Karpathy: the LLM writes the wiki).
8
- # Silent when this workspace has no harness wiki yet (nothing to document into).
9
- set -u
10
-
11
- root="${1:-$PWD}"
12
-
13
- # No harness wiki here → nothing to do. Stay silent.
14
- [ -d "$root/02-DOCS/wiki" ] || exit 0
15
-
16
- cat <<'BANNER'
17
-
18
- ===== rsc worklog checkpoint =====
19
- If this session did meaningful work (files changed, a decision made, a commit),
20
- run a WORKLOG SWEEP before context is lost:
21
- 1. Write 02-DOCS/raw/worklog/<YYYY-MM-DD>-<slug>.md using the harness
22
- wiki-worklog-template.md (what we did · why · files touched · outcome · next).
23
- 2. Compile it into wiki/ (update existing articles first; wikilinks + Related);
24
- append significant decisions to 02-DOCS/wiki/harness/decisions.md.
25
- Skip entirely if this was a pure read/answer turn with no changes.
26
- ==================================
27
- BANNER