@ericrisco/rsc 3.0.0 → 3.0.1

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
@@ -442,6 +442,8 @@ new · clones main has it all after the first message
442
442
  - **When a turn ends**, your changes in those folders are committed as `📝 docs(auto): … [skip ci]`
443
443
  and sent to `rsc/knowledge` on `origin`, from whatever branch you are on. If you are on a closed
444
444
  `main`, nothing is committed locally: the snapshot is built aside and only goes to `rsc/knowledge`.
445
+ Knowledge that you or the agent already **committed** on the branch during the turn goes up too —
446
+ only its `01-TOOLS/` and `02-DOCS/` part, under a `docs(auto) … [skip ci]` message.
445
447
  - **Before each message**, what teammates sent is brought into **the branch you are on**, and you are
446
448
  told in one line. Only the knowledge folders are touched.
447
449
  - **It reaches `main` the normal way.** Your feature branch now carries the team's knowledge, so it
@@ -455,7 +457,7 @@ What it will not do, by design:
455
457
  | | |
456
458
  | --- | --- |
457
459
  | Push to `main` | Never. Only `rsc/knowledge` receives anything. |
458
- | Push your code or your unpushed commits | Only its own snapshot of the knowledge folders goes up. Your code waits for you. |
460
+ | Push your code or your unpushed commits | Only the knowledge folders go up, even from a commit that also has code. Your code waits for you. |
459
461
  | Trigger CI or a deploy | Every automatic commit says `[skip ci]`. |
460
462
  | Bring in anybody else's code | Only knowledge paths come down. Code, config and `.claude/` are never pulled. |
461
463
  | Sync `02-DOCS/wiki/harness/user-profile.md` | Those are one person's dials. |
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.0",
2
+ "version": "3.0.1",
3
3
  "counts": {
4
4
  "skills": 270,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
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": {
@@ -104,7 +104,7 @@ const worktreeBlob = (root, path) => (existsSync(join(root, path)) ? line(root,
104
104
  function readState(root) {
105
105
  let s = {};
106
106
  try { s = JSON.parse(readFileSync(join(root, '.rsc', STATE), 'utf8')); } catch { /* first run */ }
107
- return { announced: false, seen: null, queue: [], ours: [], notices: [], lastFetch: 0, snap: null, ...s };
107
+ return { announced: false, seen: null, queue: [], ours: [], notices: [], lastFetch: 0, snap: null, head: null, ...s };
108
108
  }
109
109
 
110
110
  function writeState(root, s) {
@@ -232,6 +232,26 @@ export function commitLocal(root, s) {
232
232
  return sha;
233
233
  }
234
234
 
235
+ /**
236
+ * Knowledge the agent (or the person) committed on the branch since the last turn goes up too. Without
237
+ * this only what was still uncommitted when the turn ended travelled, and an agent that commits its
238
+ * feature document with its code in one long turn left nothing to send (E2E 2026-10-04). The first
239
+ * time, and after a branch switch, it starts where the branch left the remote default branch.
240
+ */
241
+ export function queueCommitted(root, s) {
242
+ const branch = line(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
243
+ if (!branch || branch === 'HEAD') return;
244
+ const head = line(root, ['rev-parse', 'HEAD']);
245
+ const def = defaultBranch(root);
246
+ const remoteDef = def && line(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${def}`]);
247
+ const valid = s.head && line(root, ['rev-parse', '--verify', '--quiet', `${s.head}^{commit}`])
248
+ && run(root, ['merge-base', '--is-ancestor', s.head, head]).ok;
249
+ const from = valid ? s.head : (remoteDef && line(root, ['merge-base', 'HEAD', remoteDef]));
250
+ if (!head || !from || from === head) return;
251
+ const commits = git(root, ['rev-list', '--reverse', '--no-merges', `${from}..${head}`, '--', ...KNOWLEDGE]).split('\n').filter(Boolean);
252
+ for (const c of commits) if (!s.queue.includes(c) && !s.ours.includes(c)) s.queue.push(c);
253
+ }
254
+
235
255
  // ------------------------------------------------------------------ network: up
236
256
 
237
257
  /**
@@ -241,7 +261,9 @@ export function commitLocal(root, s) {
241
261
  */
242
262
  function replay(root, base, sha, ours = []) {
243
263
  if (!line(root, ['rev-parse', '--verify', '--quiet', `${sha}^`])) return null;
244
- const patch = run(root, ['diff-tree', '-p', '--binary', '--full-index', `${sha}^`, sha], { timeout: NET_TIMEOUT_MS });
264
+ // Only the knowledge paths: a commit of the agent's own on a branch also carries code, and code never
265
+ // travels this way.
266
+ const patch = run(root, ['diff-tree', '-p', '--binary', '--full-index', `${sha}^`, sha, '--', ...KNOWLEDGE], { timeout: NET_TIMEOUT_MS });
245
267
  if (!patch.ok) return null;
246
268
  if (!patch.out.trim()) return line(root, ['rev-parse', `${base}^{tree}`]);
247
269
  const env = { GIT_INDEX_FILE: join(root, '.rsc', 'knowledge-sync.index') };
@@ -250,7 +272,7 @@ function replay(root, base, sha, ours = []) {
250
272
  // File by file: one that is already there as it should be is skipped, not read as a clash (a
251
273
  // teammate's change taken uncommitted on a closed trunk comes back up in the next snapshot), and
252
274
  // one real clash does not hold back the rest of the patch's reasoning.
253
- const files = zlist(git(root, ['diff-tree', '--no-commit-id', '--name-only', '--no-renames', '-r', '-z', `${sha}^`, sha]));
275
+ const files = zlist(git(root, ['diff-tree', '--no-commit-id', '--name-only', '--no-renames', '-r', '-z', `${sha}^`, sha, '--', ...KNOWLEDGE])).filter(isKnowledge);
254
276
  for (const f of files) {
255
277
  const next = blob(root, sha, f);
256
278
  const there = blob(root, base, f);
@@ -304,13 +326,19 @@ export function ship(root, s) {
304
326
  if (!line(root, ['rev-parse', '--verify', '--quiet', `${sha}^{commit}`])) continue; // rewritten away
305
327
  const tree = replay(root, base, sha, s.ours);
306
328
  if (tree === null) {
307
- const files = zlist(git(root, ['diff-tree', '--no-commit-id', '--name-only', '-r', '-z', sha]));
329
+ const files = zlist(git(root, ['diff-tree', '--no-commit-id', '--name-only', '-r', '-z', sha, '--', ...KNOWLEDGE])).filter(isKnowledge);
308
330
  note(s, `No he podido subir ${listed(files)}: choca con un cambio que ya está en ${KNOWLEDGE_BRANCH}. ` +
309
331
  'Tu versión sigue aquí; hay que juntarlas a mano.');
310
332
  continue;
311
333
  }
312
334
  if (tree === line(root, ['rev-parse', `${base}^{tree}`])) { kept.push(sha); continue; } // already there
313
- const [name, email, date, ...body] = git(root, ['log', '-1', '--format=%an%n%ae%n%aI%n%B', sha]).split('\n');
335
+ const [name, email, date, ...rest] = git(root, ['log', '-1', '--format=%an%n%ae%n%aI%n%B', sha]).split('\n');
336
+ // A commit of somebody's own (code + docs) goes up as its knowledge part, under a message that
337
+ // says so and carries [skip ci]: its own message would describe code that is not there, and
338
+ // would run CI on the exchange branch.
339
+ const body = rest.join('\n').includes(SKIP_CI) ? rest : [
340
+ `📝 docs(auto): ${summary(zlist(git(root, ['diff-tree', '--no-commit-id', '--name-only', '-r', '-z', sha, '--', ...KNOWLEDGE])).filter(isKnowledge))} ${SKIP_CI}`,
341
+ '', `Desde: ${rest[0] || sha.slice(0, 7)}`];
314
342
  base = git(root, ['commit-tree', tree, '-p', base, '-F', '-'], {
315
343
  input: body.join('\n'),
316
344
  env: { GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_AUTHOR_DATE: date },
@@ -448,7 +476,9 @@ export function onTurn(root, { spawnShip = true } = {}) {
448
476
  try {
449
477
  if (inactiveReason(root) || busy(root)) return;
450
478
  const pending = locked(root, (s) => {
479
+ try { queueCommitted(root, s); } catch { /* the next turn looks again */ }
451
480
  try { commitLocal(root, s); } catch (e) { note(s, `La sincronización del conocimiento no pudo comitear: ${clean(e.message, 160)}`); }
481
+ s.head = line(root, ['rev-parse', '--verify', '--quiet', 'HEAD']) || s.head;
452
482
  return s.queue.length;
453
483
  });
454
484
  if (pending && spawnShip) background(root, 'ship');