@ericrisco/rsc 2.0.11 → 2.0.12

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.11",
2
+ "version": "2.0.12",
3
3
  "counts": {
4
4
  "skills": 273,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
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": {
@@ -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
- const project = nearestHarness(resolve(cwd || native.cwd || process.env.RSC_PROJECT_CWD || process.cwd()));
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(cwd, null);
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(cwd, anchor.baselineHead);
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 current = snapshot(cwd, null);
423
- const records = readRecords(store.root).filter((record) => validAnchor(cwd, record))
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) || records[0] || null;
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);