@ericrisco/rsc 3.0.15 → 3.0.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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.15",
2
+ "version": "3.0.17",
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.15",
3
+ "version": "3.0.17",
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) {
@@ -27,6 +27,16 @@ You own how the harness speaks to the person. Four rules, a register, a ladder,
27
27
  4. **Short.** Say what the reader needs, then stop. No walls of paragraphs. Rules 3 and 4 meet in one
28
28
  place: give the **minimum** context, never the whole story.
29
29
 
30
+ Three habits make those rules hold (adapted from the `i-have-adhd` skill, ayghri, MIT):
31
+
32
+ - **The answer goes first.** The first line is the answer, the verdict or the action. Context comes
33
+ after, and only if it is needed. A reader who stops after one line must still know the answer.
34
+ - **Five visible items at most.** Group a longer list and show the five that matter now. Keep the
35
+ rest and give it when asked or when it becomes next. Never drop an item that matters.
36
+ - **Check before you send.** Delete a first sentence that only announces what follows. Delete
37
+ closing filler ("hope this helps"); the brújula is the close. Delete a "perhaps" that adds
38
+ nothing, and keep one that carries real doubt.
39
+
30
40
  Full rules and examples → `references/orientation-contract.md`.
31
41
 
32
42
  ## The register