@ericrisco/rsc 3.0.16 → 3.0.18

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": "3.0.16",
2
+ "version": "3.0.18",
3
3
  "counts": {
4
4
  "skills": 271,
5
5
  "agents": 34,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "3.0.16",
3
+ "version": "3.0.18",
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": {
@@ -469,12 +469,24 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
469
469
  }
470
470
 
471
471
  const add = wanted.filter((w) => !present.has(norm(w)));
472
- if (!add.length) return null;
472
+ // Every append used to open a new block with its own header, so a project synced across a dozen
473
+ // versions carried a dozen copies of it. Adjacent rsc blocks are one block: fold them (only our
474
+ // header lines and the blank seams between our blocks go; every entry stays, in order).
475
+ const folded = foldIgnoreBlocks(text);
476
+ if (!add.length) {
477
+ if (folded === text) return null;
478
+ try { writeFileSync(gi, folded); return gi; } catch { return null; }
479
+ }
473
480
 
474
- // Append only. Never rewrite, never reorder: the rest of that file is theirs.
475
- const prefix = text === '' ? '' : (text.endsWith('\n') ? '' : '\n');
481
+ // Never reorder, never touch a line of theirs. When the file already ends with our block, the new
482
+ // entries join it; otherwise they open one.
483
+ const base = folded === '' || folded.endsWith('\n') ? folded : `${folded}\n`;
484
+ const next = endsWithIgnoreBlock(base)
485
+ ? `${base}${add.join('\n')}\n`
486
+ : `${base}${base === '' ? '' : '\n'}${RSC_IGNORE_HEADER}\n${add.join('\n')}\n`;
476
487
  try {
477
- appendFileSync(gi, `${prefix}\n# rsc local state (hooks, seals, logs) and managed skill links — machine-local\n${add.join('\n')}\n`);
488
+ if (folded === text && next.startsWith(text)) appendFileSync(gi, next.slice(text.length));
489
+ else writeFileSync(gi, next);
478
490
  if (target) {
479
491
  const statePath = targetPaths(target, undefined, cwd).stateFile;
480
492
  const state = readState(statePath);
@@ -778,6 +790,41 @@ function emptyConfig(file) {
778
790
 
779
791
  const RSC_IGNORE_HEADER = '# rsc local state (hooks, seals, logs) and managed skill links — machine-local';
780
792
 
793
+ // Merge consecutive rsc blocks into the first: drop a repeated header and the blank seam before it
794
+ // when the block above was ours too. A block of the user's in between ends ours, so nothing of
795
+ // theirs is ever moved or absorbed.
796
+ export function foldIgnoreBlocks(text) {
797
+ const out = [];
798
+ let ours = false;
799
+ let afterBlank = false;
800
+ for (const line of text.split('\n')) {
801
+ if (line === RSC_IGNORE_HEADER) {
802
+ if (ours && afterBlank) {
803
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
804
+ afterBlank = false;
805
+ continue;
806
+ }
807
+ ours = true;
808
+ } else if (line.trim() === '') {
809
+ afterBlank = true;
810
+ out.push(line);
811
+ continue;
812
+ } else if (afterBlank) {
813
+ ours = false;
814
+ }
815
+ afterBlank = false;
816
+ out.push(line);
817
+ }
818
+ return out.join('\n');
819
+ }
820
+
821
+ function endsWithIgnoreBlock(text) {
822
+ const lines = text.replace(/\n+$/, '').split('\n');
823
+ let i = lines.length - 1;
824
+ while (i > 0 && lines[i - 1].trim() !== '') i -= 1;
825
+ return lines[i] === RSC_IGNORE_HEADER;
826
+ }
827
+
781
828
  // Remove the blocks `ignoreLocalState` appended: the header line, the entries under it up to the
782
829
  // next blank line, and the blank seam before it. Lines the user wrote are never part of a block.
783
830
  function stripIgnoreBlocks(text) {