@ericrisco/rsc 2.0.11 → 2.0.13
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/manifest.json +1 -1
- package/package.json +24 -2
- package/scripts/doctor.js +4 -0
- package/scripts/lib/versions.js +31 -0
- package/scripts/rsc.js +15 -0
- package/targets/session-memory-adapter.mjs +7 -3
- package/targets/session-memory-core.mjs +17 -5
- package/targets/session-start.mjs +44 -4
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericrisco/rsc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
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": {
|
|
@@ -64,5 +64,27 @@
|
|
|
64
64
|
},
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
67
|
-
}
|
|
67
|
+
},
|
|
68
|
+
"rscFixes": [
|
|
69
|
+
{
|
|
70
|
+
"fixedIn": "2.0.6",
|
|
71
|
+
"symptom": "A guard your team disarmed (.rsc/.no-gitmoji and similar) comes back armed after cloning the repo"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"fixedIn": "2.0.8",
|
|
75
|
+
"symptom": "rsc doctor reports commandOrphans for skills that are installed, and rsc capabilities shows no skills (macOS/Linux)"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"fixedIn": "2.0.10",
|
|
79
|
+
"symptom": "onboard fails with RSC_ONBOARDING_INCOMPLETE: installed agents differ from accepted policy (antigravity, windsurf, cline, roo, continue, amp, jules, zed, aider)"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"fixedIn": "2.0.11",
|
|
83
|
+
"symptom": "Stop or PreCompact hook error: Hook JSON output validation failed, and a 'consider compacting' notice repeating after every turn"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"fixedIn": "2.0.12",
|
|
87
|
+
"symptom": "Security: session continuation context could come from a different git worktree"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
68
90
|
}
|
package/scripts/doctor.js
CHANGED
|
@@ -18,6 +18,7 @@ import { inspectMemoryWiring } from '../targets/memory.js';
|
|
|
18
18
|
import { metricsSummary } from '../targets/session-memory-core.mjs';
|
|
19
19
|
import { agentPath } from '../targets/agents.js';
|
|
20
20
|
import { projectOptOuts } from '../targets/opt-outs.js';
|
|
21
|
+
import { versionReport } from './lib/versions.js';
|
|
21
22
|
|
|
22
23
|
// Gates the committed manifest says the team disarmed, still armed here. Read through the same
|
|
23
24
|
// partition the installer uses, so a machine-only switch somebody's older rsc wrote into the
|
|
@@ -238,6 +239,9 @@ export function doctor({ target, home, cwd }) {
|
|
|
238
239
|
// never rewrites somebody's machine — so the only thing owed is saying it out loud, and
|
|
239
240
|
// saying it every run, because a notice offered once per session is a notice missed.
|
|
240
241
|
optOutsNotApplied: optOutsNotApplied(root),
|
|
242
|
+
// First thing to read on any bug report: a project behind its CLI runs old hooks, and "it is
|
|
243
|
+
// already fixed" only helps once the fix is in `.rsc/`.
|
|
244
|
+
versions: versionReport(root),
|
|
241
245
|
// Counted, never interpreted — by spec, the gap log's reader is the user.
|
|
242
246
|
automationGaps: countGaps(root),
|
|
243
247
|
memory,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Which rsc is running, and which rsc this project was last built with. Two different numbers, and
|
|
6
|
+
// the gap between them is where a published fix fails to arrive: hooks are materialized as copies
|
|
7
|
+
// under `.rsc/`, so a release changes nothing in a project until someone syncs it. Nobody could see
|
|
8
|
+
// that gap before — there was no `--version`, and bug reports arrived without one.
|
|
9
|
+
|
|
10
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
|
+
export const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
12
|
+
|
|
13
|
+
/** `.rsc/.version` is written at install and at every sync — the version this project's hooks came from. */
|
|
14
|
+
export function installedVersion(root) {
|
|
15
|
+
try { return readFileSync(join(root, '.rsc', '.version'), 'utf8').trim() || null; } catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isNewer(a, b) {
|
|
19
|
+
const pa = String(a).split('.').map(Number);
|
|
20
|
+
const pb = String(b).split('.').map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
23
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function versionReport(root) {
|
|
29
|
+
const installed = installedVersion(root);
|
|
30
|
+
return { cli: CLI_VERSION, installed, behind: Boolean(installed && isNewer(CLI_VERSION, installed)) };
|
|
31
|
+
}
|
package/scripts/rsc.js
CHANGED
|
@@ -16,6 +16,7 @@ import { runUpgrade } from './lib/upgrade.js';
|
|
|
16
16
|
import { diagnose, repair } from './lib/repair.js';
|
|
17
17
|
import { DEFAULT_SKILL_FLOOR, withDefaultSkillFloor } from './lib/default-skill-floor.js';
|
|
18
18
|
import { readManifest, writeManifest } from './lib/manifest-file.js';
|
|
19
|
+
import { versionReport } from './lib/versions.js';
|
|
19
20
|
import {
|
|
20
21
|
normalizeOnboarding, missingOnboardingFields, scanProject,
|
|
21
22
|
buildOnboardingPlan, decodeGoal, encodeGoal, identifyPlan, recommendDeferredComponents,
|
|
@@ -23,6 +24,20 @@ import {
|
|
|
23
24
|
import { applyAcceptedOnboarding, verifyOnboarding, ensureHarnessSkeleton, harnessReadiness } from './lib/onboarding-apply.js';
|
|
24
25
|
|
|
25
26
|
const rawArgv = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
// `rsc --version` — answered before anything else runs, offline, and in a shape a bug report can
|
|
29
|
+
// paste. Two numbers because there are two things: the CLI you just ran, and the harness this
|
|
30
|
+
// project was built with. When they differ the project is running old hooks, and a fix that was
|
|
31
|
+
// published may simply not be here yet.
|
|
32
|
+
if (['--version', '-v', 'version'].includes(rawArgv[0])) {
|
|
33
|
+
const v = versionReport(process.cwd());
|
|
34
|
+
const lines = [`rsc ${v.cli}`];
|
|
35
|
+
if (v.installed) lines.push(`installed in this project: ${v.installed}${v.behind ? ' (behind)' : ''}`);
|
|
36
|
+
else lines.push('installed in this project: none');
|
|
37
|
+
if (v.behind) lines.push('This project runs older hooks than this CLI. To catch up, run here:', ' npx @ericrisco/rsc@latest');
|
|
38
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
26
41
|
const GLOBAL_VALUE_FLAGS = new Set([
|
|
27
42
|
'--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
|
|
28
43
|
]);
|
|
@@ -98,15 +98,18 @@ export function handleLifecycle({ target, event, native = {}, cwd, settings } =
|
|
|
98
98
|
try {
|
|
99
99
|
if (!LOCAL_TARGETS.has(target)) throw new Error(`unsupported memory target: ${target}`);
|
|
100
100
|
if (isRemote(target, native)) return { output: {}, capture: null, remote: true, degraded: false };
|
|
101
|
-
|
|
101
|
+
// Two answers from one path, kept apart on purpose: `project` is where the journal is KEPT (the
|
|
102
|
+
// nearest harness), `here` is where the agent WORKS and therefore who the session is.
|
|
103
|
+
const here = resolve(cwd || native.cwd || process.env.RSC_PROJECT_CWD || process.cwd());
|
|
104
|
+
const project = nearestHarness(here);
|
|
102
105
|
if (!existsSync(project)) throw new Error('project directory unavailable');
|
|
103
106
|
const config = settings || projectSettings(project);
|
|
104
107
|
if (config.enabled === false) return { output: {}, capture: null, remote: false, degraded: false };
|
|
105
108
|
const id = sessionId(native, target);
|
|
106
109
|
const eventName = hookEventName(target, event, native);
|
|
107
110
|
if (event === 'start') {
|
|
108
|
-
const started = capture({ cwd: project, sessionId: id, target, event: 'start', settings: config });
|
|
109
|
-
const resumed = resume({ cwd: project, target, settings: config });
|
|
111
|
+
const started = capture({ cwd: project, worktreeCwd: here, sessionId: id, target, event: 'start', settings: config });
|
|
112
|
+
const resumed = resume({ cwd: project, worktreeCwd: here, target, settings: config });
|
|
110
113
|
return {
|
|
111
114
|
output: nativeOutput(target, eventName, resumed.context),
|
|
112
115
|
capture: started,
|
|
@@ -117,6 +120,7 @@ export function handleLifecycle({ target, event, native = {}, cwd, settings } =
|
|
|
117
120
|
}
|
|
118
121
|
const captureInput = {
|
|
119
122
|
cwd: project,
|
|
123
|
+
worktreeCwd: here,
|
|
120
124
|
sessionId: id,
|
|
121
125
|
target,
|
|
122
126
|
event: event === 'end' ? 'sessionEnd' : event,
|
|
@@ -284,6 +284,12 @@ function readRecords(root) {
|
|
|
284
284
|
|
|
285
285
|
export function capture(input = {}) {
|
|
286
286
|
const cwd = resolve(input.cwd || process.cwd());
|
|
287
|
+
// WHERE the journal lives and WHO the session is are two different questions. `cwd` answers the
|
|
288
|
+
// first — the nearest harness, so a child with no manifest of its own does not scatter journals.
|
|
289
|
+
// The second has to be asked of git from where the agent actually works: asked from the harness,
|
|
290
|
+
// every worktree nested under it came back as the harness itself, and a sibling's record then
|
|
291
|
+
// "matched exactly". See GHSA-8gjp-3r7c-33f4.
|
|
292
|
+
const here = resolve(input.worktreeCwd || cwd);
|
|
287
293
|
const config = settings(input.settings);
|
|
288
294
|
if (!config.enabled) return { record: null, path: null, notice: null, compactionHint: false };
|
|
289
295
|
const store = chooseMemoryRoot(cwd);
|
|
@@ -299,7 +305,7 @@ export function capture(input = {}) {
|
|
|
299
305
|
const recordPath = join(store.root, 'sessions', `${storageId}.json`);
|
|
300
306
|
let anchor = readJson(anchorPath);
|
|
301
307
|
let existing = readJson(recordPath);
|
|
302
|
-
const firstSnapshot = snapshot(
|
|
308
|
+
const firstSnapshot = snapshot(here, null);
|
|
303
309
|
if (!anchor) {
|
|
304
310
|
anchor = {
|
|
305
311
|
sessionId,
|
|
@@ -312,7 +318,7 @@ export function capture(input = {}) {
|
|
|
312
318
|
}
|
|
313
319
|
if (input.event === 'start' && !existing) return { record: null, path: null, notice: null, compactionHint: false };
|
|
314
320
|
|
|
315
|
-
const repo = snapshot(
|
|
321
|
+
const repo = snapshot(here, anchor.baselineHead);
|
|
316
322
|
const editCount = Math.max(0, (existing?.editCount || 0) + (finiteOrNull(input.editDelta, true) || 0));
|
|
317
323
|
const baselineFiles = new Set(anchor.baselineFiles || []);
|
|
318
324
|
const newDirtyPath = repo.files.some((path) => !baselineFiles.has(path));
|
|
@@ -419,13 +425,19 @@ export function resume(input = {}) {
|
|
|
419
425
|
ensureStore(store.root);
|
|
420
426
|
const now = iso(input.now);
|
|
421
427
|
prune(store.root, now, config);
|
|
422
|
-
const
|
|
423
|
-
const
|
|
428
|
+
const here = resolve(input.worktreeCwd || cwd);
|
|
429
|
+
const current = snapshot(here, null);
|
|
430
|
+
const records = readRecords(store.root).filter((record) => validAnchor(here, record))
|
|
424
431
|
.sort((a, b) => b.timestamps.updatedAt.localeCompare(a.timestamps.updatedAt));
|
|
425
432
|
let record = records.find((candidate) => candidate.branch === current.branch && candidate.worktree === current.worktree) || null;
|
|
426
433
|
let match = record ? 'exact' : 'none';
|
|
434
|
+
// Only ever this worktree's own records. The chain used to end in `|| records[0]` — the newest
|
|
435
|
+
// record in the store, whoever wrote it — which a moved or renamed checkout reached every time,
|
|
436
|
+
// because its stale records match nothing. A shared store is a place to KEEP several worktrees'
|
|
437
|
+
// journals, never a reason to hand one of them to another. No continuation is the honest answer
|
|
438
|
+
// when there is nothing of yours to continue.
|
|
427
439
|
if (!record) {
|
|
428
|
-
record = records.find((candidate) => candidate.worktree === current.worktree) ||
|
|
440
|
+
record = records.find((candidate) => candidate.worktree === current.worktree) || null;
|
|
429
441
|
if (record) match = 'nearby';
|
|
430
442
|
}
|
|
431
443
|
const lessons = selectedLessons(store.root, config);
|
|
@@ -266,19 +266,58 @@ function isNewer(a, b) {
|
|
|
266
266
|
return false;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// "rsc X is out" was being ignored — the maintainer's own workspace sat on 2.0.4 for days with this
|
|
270
|
+
// notice on every session, and two people in two days reported bugs that were already fixed. A
|
|
271
|
+
// version number names nothing anyone recognises. A symptom does: someone who sees a red error after
|
|
272
|
+
// every turn updates the moment they are told that is what the update fixes (`suggest` §3: say what
|
|
273
|
+
// is wrong as a symptom, not as a cause).
|
|
274
|
+
//
|
|
275
|
+
// The symptoms travel for free: the registry's `/latest` document is the published package.json, so
|
|
276
|
+
// its `rscFixes` field — `{ fixedIn, symptom }` — arrives in the response this check already makes.
|
|
277
|
+
// It is text a model will read, so every entry is reduced to one short plain line or dropped.
|
|
278
|
+
const SEMVER_ONLY = /^\d+\.\d+\.\d+$/;
|
|
279
|
+
function fixesSince(list, installed, latest) {
|
|
280
|
+
if (!Array.isArray(list)) return [];
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const item of list) {
|
|
283
|
+
if (!item || typeof item !== 'object' || typeof item.symptom !== 'string') continue;
|
|
284
|
+
if (!SEMVER_ONLY.test(String(item.fixedIn))) continue;
|
|
285
|
+
if (!isNewer(item.fixedIn, installed) || isNewer(item.fixedIn, latest)) continue;
|
|
286
|
+
const symptom = item.symptom.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
287
|
+
if (symptom) out.push({ fixedIn: item.fixedIn, symptom });
|
|
288
|
+
}
|
|
289
|
+
return out.slice(0, 8);
|
|
290
|
+
}
|
|
291
|
+
|
|
269
292
|
if (!process.env.RSC_NO_UPDATE_CHECK) {
|
|
270
293
|
try {
|
|
271
294
|
const installed = readFileSync(join(root, '.rsc', '.version'), 'utf8').trim();
|
|
272
|
-
let
|
|
273
|
-
if (
|
|
295
|
+
let doc = null;
|
|
296
|
+
if (process.env.RSC_LATEST_JSON) doc = JSON.parse(process.env.RSC_LATEST_JSON);
|
|
297
|
+
else if (process.env.RSC_LATEST) doc = { version: process.env.RSC_LATEST };
|
|
298
|
+
else {
|
|
274
299
|
const ctrl = new AbortController();
|
|
275
300
|
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
276
301
|
const res = await fetch('https://registry.npmjs.org/@ericrisco%2frsc/latest', { signal: ctrl.signal });
|
|
277
302
|
clearTimeout(timer);
|
|
278
|
-
|
|
303
|
+
doc = await res.json();
|
|
279
304
|
}
|
|
305
|
+
const latest = doc?.version;
|
|
280
306
|
if (installed && latest && isNewer(latest, installed)) {
|
|
281
|
-
|
|
307
|
+
let fixes = [];
|
|
308
|
+
try { fixes = fixesSince(doc.rscFixes, installed, latest); } catch { fixes = []; }
|
|
309
|
+
if (fixes.length) {
|
|
310
|
+
process.stdout.write(`
|
|
311
|
+
===== rsc update available =====
|
|
312
|
+
rsc ${latest} is out — you have ${installed}. It fixes problems this install still has:
|
|
313
|
+
${fixes.map((f) => ` - ${f.symptom} (fixed in ${f.fixedIn})`).join('\n')}
|
|
314
|
+
ACTION: before starting the task, tell the user in one line that the update fixes what they
|
|
315
|
+
may be seeing, and if they say yes, run it in this project:
|
|
316
|
+
npx @ericrisco/rsc@latest
|
|
317
|
+
================================
|
|
318
|
+
`);
|
|
319
|
+
} else {
|
|
320
|
+
process.stdout.write(`
|
|
282
321
|
===== rsc update available =====
|
|
283
322
|
rsc ${latest} is out — you have ${installed}.
|
|
284
323
|
ACTION: tell the user a new version is available and, if they say yes, run:
|
|
@@ -286,6 +325,7 @@ ACTION: tell the user a new version is available and, if they say yes, run:
|
|
|
286
325
|
(That reinstalls and refreshes the skill content to the latest.)
|
|
287
326
|
================================
|
|
288
327
|
`);
|
|
328
|
+
}
|
|
289
329
|
}
|
|
290
330
|
} catch { /* offline / no baseline / parse error → stay silent */ }
|
|
291
331
|
}
|