@lifeaitools/rdc-skills 0.37.4 → 0.37.7

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.
@@ -93,6 +93,14 @@ launch if it is dead.
93
93
  ```
94
94
 
95
95
  ### 4. Open the page
96
+ - **Probe the resolved `targetUrl` before calling the preview open** (skip for
97
+ `brandSlug=test`). The editor host on 3015 can be up while the iframe target
98
+ returns a 500 (e.g. a Supabase-gated app with no local `.env.local`):
99
+ ```bash
100
+ curl -s -o /dev/null -w "%{http_code}" <targetUrl>/
101
+ ```
102
+ Non-2xx: do not report the preview as open. Report the status code and the first
103
+ error line from the target's dev-server log, and stop.
96
104
  - Normal use: open the URL in the browser and confirm the editor loaded.
97
105
  Open via PowerShell `Start-Process`, NOT `cmd start` — `cmd` treats the `&`
98
106
  between query params as a command separator and truncates the URL:
@@ -50,11 +50,18 @@ SELECT insert_work_item(
50
50
  p_item_type := 'bug',
51
51
  p_priority := 'urgent',
52
52
  p_status := 'in_progress',
53
- p_source := 'fixit'
53
+ p_source := 'fixit',
54
+ p_session_id := '<agent-session-id>',
55
+ p_checklist := '[
56
+ {"id":"decomp-fix","text":"<the fix, one line>","required":true,"checked":false},
57
+ {"id":"test-fix-proof","text":"<command that proves the defect is gone> exits 0","required":true,"checked":false}
58
+ ]'::jsonb
54
59
  );
55
60
  ```
56
61
 
57
- Note the returned `id`.
62
+ Note the returned `id`. The two rows are not optional: `update_work_item_status(...,'done')`
63
+ refuses any non-epic item without a required `decomp-*` AND a required `test-*` row.
64
+ `p_session_id` must be this session — only it may tick them.
58
65
 
59
66
  ### 2a. Declare the fix and its proof (before touching any code)
60
67
 
@@ -99,6 +106,7 @@ files, and verify behavior before moving the work item to `review`.
99
106
 
100
107
  ```bash
101
108
  git add <specific files only — never git add -A for a fixit>
109
+ git diff --cached --stat # every staged file and hunk must be yours
102
110
  git commit -m "fix(<scope>): <description>"
103
111
  if [ "$RDC_TEST" != "1" ]; then
104
112
  git push origin {development-branch}
@@ -107,6 +115,10 @@ else
107
115
  fi
108
116
  ```
109
117
 
118
+ `git add <file>` stages every hunk in that file, including another session's
119
+ uncommitted edit. If a file was already dirty before you touched it, stage with
120
+ `git add -p <file>` and take only your hunks.
121
+
110
122
  ### 5.5 Mandatory code-review gate (before submitting implementation report)
111
123
 
112
124
  ⛔ **No fixit closes without a code-review pass.** Even single-file changes go through review.
@@ -144,7 +156,7 @@ complete it:
144
156
 
145
157
  | Target shape | "Done" = delivered means | How + structural proof |
146
158
  |---|---|---|
147
- | Published package (npm / PyPI) | new version live on the registry | run the repo's release/publish path (e.g. `npm publish --access public`); verify `npm view <pkg> version` == new version |
159
+ | Published package (npm / PyPI) | new version live on the registry | run the repo's release/publish path (e.g. `npm publish --access public`); verify `curl -fsS https://registry.npmjs.org/<pkg>/<version>` returns 200 (exact version; `npm view` lags the registry) |
148
160
  | Deployed app (`apps/*`, sites) | change live on the running host | `/rdc:deploy <slug>` (dev) / `promote` (prod); verify HTTP 200 + content probe |
149
161
  | Shared lib consumed in-repo | landed on the integration branch | `node scripts/land.mjs` (or repo equivalent); verify branch contains the SHA |
150
162
  | Standalone repo | its own release ritual completed | follow the repo's release script/tag; verify the artifact/tag exists |
@@ -172,11 +184,15 @@ is fleet-wide yet — its absence is not itself a fixit failure.
172
184
 
173
185
  ### 6. Close and clean up
174
186
 
175
- Submit implementation report first, move to review, then close as validator.
187
+ Tick the two checklist rows from Step 2 as the originating session (after the proof
188
+ command passes), submit the implementation report, move to review, then close as validator.
176
189
  **Precondition: Step 5.7 delivery is verified.** If delivery is blocked, set status
177
190
  `blocked` (not `done`) with the remaining step — never mark `done` on an undelivered change:
178
191
 
179
192
  ```sql
193
+ SELECT update_checklist_item('<id>'::uuid, 'decomp-fix', true, '<agent-session-id>', 'agent');
194
+ SELECT update_checklist_item('<id>'::uuid, 'test-fix-proof', true, '<agent-session-id>', 'agent');
195
+
180
196
  SELECT submit_implementation_report('<id>'::uuid,
181
197
  '{"tldr":"<one sentence>","assumptions":[],"deviations":[],"uncertainty":[],"detail":"<what was fixed>","flags":[],"transactional":false,"memory_records":[],"codeflow_post":{"agent_session_id":"<agent-session-id>","summary":"<what changed and why>","files_changed":["<path>"],"verification":["<command/evidence>"],"commit":"<hash optional>"}}'::jsonb
182
198
  );
@@ -194,6 +210,10 @@ SELECT update_work_item_status('<id>'::uuid, 'done',
194
210
  );
195
211
  ```
196
212
 
213
+ If `done` is rejected as `close_blocked_uninstrumented` (an item created without the
214
+ rows), add them and tick them, then retry:
215
+ `SELECT append_checklist_item('<id>'::uuid, 'test-fix-proof', '<proof>', true, '<agent-session-id>', 'agent');`
216
+
197
217
  If the fix touched a transactional flow, API boundary, or package contract, set `"transactional": true` and populate `memory_records` (see `agent-bootstrap.md`), then run:
198
218
  ```bash
199
219
  node scripts/work-item-memory.mjs <work-item-id>
@@ -317,6 +317,8 @@ choose the most conservative/reversible approach and document the decision.
317
317
  - Interactive: ALWAYS get approval before proceeding to build
318
318
  - Unattended: proceed immediately without approval
319
319
  - Plan doc goes in `.rdc/plans/` (fallback: `.rdc/plans/` if `.rdc/` does not exist) — not `.planning/`
320
+ - An architecture or spec draft that has not passed its approval interview goes in
321
+ `.rdc/research/<topic>-draft.md`. Nothing is written under `docs/systems/**` until it is approved.
320
322
  - Each work package must be independently executable by an agent
321
323
  - No file overlap between work packages
322
324
  - Include test requirements in every work package
@@ -34,6 +34,7 @@ If `<repo>` is not resolvable from the current workspace, ask for its local path
34
34
  rdc:release: <repo> vX.Y.Z -> vA.B.C
35
35
  [ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
36
36
  [ ] Source path resolved
37
+ [ ] <repo>/RELEASE.md read (if present) — its steps override the generic flow below
37
38
  [ ] Release metadata read
38
39
  [ ] Working tree clean or user-approved dirty scope identified
39
40
  [ ] Current version detected
@@ -81,8 +82,13 @@ these was in the command and NOT in this file, which is precisely the drift that
81
82
  shipping one verb as two documents produces.
82
83
 
83
84
  - Do not release without explicit user authorization.
84
- - Prefer repo-local release instructions in `.rdc/release.json`, README, package
85
- scripts, or CI workflows.
85
+ - **Read `<repo>/RELEASE.md` first.** When it exists it is the release procedure
86
+ and overrides everything generic in this skill (branch, who publishes — a tag
87
+ workflow, a push hook or you — and post-publish gates). Otherwise prefer
88
+ `.rdc/release.json`, README, package scripts, or CI workflows.
89
+ - **`E403`/`E409` "cannot publish over the previously published version" right
90
+ after a push or tag usually means CI already published it.** Verify the exact
91
+ version on the registry; never bump again to get past it.
86
92
  - **Never force push or bypass hooks.**
87
93
  - **Never declare success without verifying the installed or deployed version.**
88
94
  A publish that exits 0 is not a published package — the registry can lag
@@ -123,7 +129,10 @@ node scripts/rdc-release.mjs --repo <repo-path> \
123
129
  --message "chore(release): vA.B.C" --tag vA.B.C
124
130
  git push origin HEAD
125
131
  git push origin vA.B.C
126
- npm view <package-name> version
132
+ # exact-version fetch: 200 = published. `npm view` reads a cache that can 404
133
+ # for minutes after a successful publish.
134
+ curl -fsS -o /dev/null -w "%{http_code}
135
+ " https://registry.npmjs.org/<package-name>/A.B.C
127
136
  ```
128
137
 
129
138
  The wrapper owns this documented commit-and-tag step. It compares the exact
@@ -57,12 +57,10 @@ description: rdc:status () — snapshot open epics, health, blockers and next ac
57
57
  ```
58
58
 
59
59
  6. **Dev server health (PM2):**
60
- SSH into the Coolify server and check PM2 process state:
60
+ Read PM2 process state on the dev host through the sanctioned wrapper (it loads
61
+ the clauth-held key into ssh-agent; a raw key fetch is guard-blocked):
61
62
  ```bash
62
- # Note: clauth key name for the SSH key may vary by project. Common names: ssh-key-path, vultr-ssh-key, dev-ssh-key.
63
- # Check available keys with: curl -s http://127.0.0.1:52437/list-services
64
- SSH_KEY=$(curl -s http://127.0.0.1:52437/v/ssh-key-path)
65
- ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no root@64.237.54.189 "pm2 jlist 2>/dev/null"
63
+ bash "$LIFEAI_ENV/services/ssh-clauth.sh" "pm2 jlist 2>/dev/null"
66
64
  ```
67
65
  Parse the JSON array — for each process note `name`, `pm2_env.status` (online/stopped/errored), and `pm2_env.pm_uptime`.
68
66
  If SSH fails or PM2 returns nothing, note "PM2 unreachable" and continue.
@@ -58,6 +58,11 @@ rdc:terminal-config: <task>
58
58
  Get-Content "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" | ConvertFrom-Json
59
59
  ```
60
60
 
61
+ - **A `%VAR%` in `commandline`/`startingDirectory` that does not expand has two causes; check which before editing:**
62
+ - **Stale inherited environment** (most common): the variable was set after `explorer.exe`, Terminal's parent, last started. Confirm it is set (`[Environment]::GetEnvironmentVariable('VAR','User')`), then `Stop-Process -Name explorer -Force` (it relaunches) and restart Windows Terminal fully — a new tab in a running instance keeps the old block.
63
+ - **Hardcoded path**: the config never used a variable. Only this case is fixed by editing the config.
64
+ - Never replace a `%VAR%` with a literal path to fix a stale environment; a restart fixes it and the literal breaks portability.
65
+
61
66
  ## Startup Scripts
62
67
 
63
68
  If a project uses role/cell startup scripts, keep those scripts under the project root and commit them with the project. Do not bake one user's absolute machine paths into shared RDC skill files.
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Truth Gate 3.0 — Layers 5+6 harness-gate tests (WP-5).
3
+ * Truth Gate 3.0 — Layer 6 harness-gate tests (WP-5).
4
4
  *
5
- * Covers the three new hooks, fully offline (no DB, no brain, no clauth):
6
- * task-completed-gate.js — flag OFF no-op; flag ON + no/unsound closure BLOCK;
7
- * flag ON + sound closure PASS.
8
- * post-tool-batch-gate.js — flag OFF no-op; flag ON + stale worktree base FLAG;
9
- * flag ON + current base PASS.
5
+ * Fully offline (no DB, no brain, no clauth). task-completed-gate.js and
6
+ * post-tool-batch-gate.js were deleted in 0.37.5 (LIFEAI/regen-root#237): both
7
+ * sat behind env flags nothing ever set.
10
8
  * gate-watchdog-selfcheck — STOP-banner when a gate file/registration absent;
11
9
  * silent when all present; checks the LIVE hookify
12
10
  * hooks.json (plugin manifest), not the dead wrappers.
@@ -17,7 +15,6 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
17
15
  import { tmpdir } from 'node:os';
18
16
  import { join, resolve, dirname } from 'node:path';
19
17
  import { fileURLToPath } from 'node:url';
20
- import { spawnSync } from 'node:child_process';
21
18
  import { createRequire } from 'node:module';
22
19
 
23
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -31,120 +28,6 @@ function assert(name, condition, detail = '') {
31
28
  else process.stdout.write(` ok ${name}\n`);
32
29
  }
33
30
 
34
- function runHook(file, payload, extraEnv = {}) {
35
- return spawnSync(process.execPath, [join(HOOKS, file)], {
36
- input: JSON.stringify(payload),
37
- encoding: 'utf8',
38
- env: { ...process.env, ...extraEnv },
39
- });
40
- }
41
-
42
- const WI = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
43
-
44
- // ===========================================================================
45
- // 1. task-completed-gate.js
46
- // ===========================================================================
47
- {
48
- const tcg = require(join(HOOKS, 'task-completed-gate.js'));
49
-
50
- // pure: closureIsSound branches
51
- assert('TCG closureIsSound: null row → not ok', tcg.closureIsSound(null).ok === false);
52
- assert('TCG closureIsSound: not-done task → not ok',
53
- tcg.closureIsSound({ id: WI, item_type: 'task', status: 'review' }).ok === false);
54
- assert('TCG closureIsSound: done but witness:agent → not ok',
55
- tcg.closureIsSound({ id: WI, item_type: 'task', status: 'done', implementation_report: { codeflow_post: { witness: 'agent', commit: 'x', files_changed: ['f'], verification: [{ exit_code: 0 }] } } }).ok === false);
56
- const soundRow = {
57
- id: WI, item_type: 'task', status: 'done',
58
- implementation_report: { codeflow_post: { witness: 'validator-rerun', commit: 'c3189c9d58a37d648e9de4a6bcd7d46772053eea', files_changed: ['hooks/x.js'], verification: [{ exit_code: 0 }] } },
59
- };
60
- assert('TCG closureIsSound: full sound closure → ok', tcg.closureIsSound(soundRow).ok === true,
61
- tcg.closureIsSound(soundRow).reason || '');
62
- assert('TCG closureIsSound: epic done → ok',
63
- tcg.closureIsSound({ id: WI, item_type: 'epic', status: 'done' }).ok === true);
64
-
65
- // pure: extractWorkItemId
66
- assert('TCG extractWorkItemId: from field', tcg.extractWorkItemId({ work_item_id: WI }) === WI);
67
- assert('TCG extractWorkItemId: from blob', tcg.extractWorkItemId({ task: { note: `closes ${WI}` } }) === WI);
68
- assert('TCG extractWorkItemId: none → null', tcg.extractWorkItemId({ task: { note: 'no uuid' } }) === null);
69
-
70
- // pure: flagEnabledEnv default OFF
71
- {
72
- const prev = process.env.RDC_TRUTHGATE_TASKCOMPLETED;
73
- delete process.env.RDC_TRUTHGATE_TASKCOMPLETED;
74
- assert('TCG flag default OFF', tcg.flagEnabledEnv() === false);
75
- process.env.RDC_TRUTHGATE_TASKCOMPLETED = 'true';
76
- assert('TCG flag ON via env', tcg.flagEnabledEnv() === true);
77
- if (prev === undefined) delete process.env.RDC_TRUTHGATE_TASKCOMPLETED;
78
- else process.env.RDC_TRUTHGATE_TASKCOMPLETED = prev;
79
- }
80
-
81
- // pure: flagDbOptIn default OFF, ON via env
82
- {
83
- const prev = process.env.RDC_TRUTHGATE_TASKCOMPLETED_DB;
84
- delete process.env.RDC_TRUTHGATE_TASKCOMPLETED_DB;
85
- assert('TCG DB-opt-in default OFF', tcg.flagDbOptIn() === false);
86
- process.env.RDC_TRUTHGATE_TASKCOMPLETED_DB = 'on';
87
- assert('TCG DB-opt-in ON via env', tcg.flagDbOptIn() === true);
88
- if (prev === undefined) delete process.env.RDC_TRUTHGATE_TASKCOMPLETED_DB;
89
- else process.env.RDC_TRUTHGATE_TASKCOMPLETED_DB = prev;
90
- }
91
-
92
- // process: flag OFF → no-op (exit 0, no block)
93
- {
94
- const r = runHook('task-completed-gate.js', { work_item_id: WI }, { RDC_TRUTHGATE_TASKCOMPLETED: '' });
95
- assert('TCG flag OFF → exit 0 no-op', r.status === 0, `status=${r.status} ${r.stderr}`);
96
- }
97
-
98
- // process: flag OFF + DB-opt-in unset → ZERO-COST OFF path makes NO DB call.
99
- // Point Supabase + the clauth daemon at a non-routable, slow-to-fail address.
100
- // If the OFF path consulted the DB, the call would stall near the hook's
101
- // Supabase timeout (~3.5s) / clauth timeout (~2s). The run completing far
102
- // under that floor proves no round-trip was attempted.
103
- {
104
- const t0 = Date.now();
105
- const r = runHook('task-completed-gate.js', { work_item_id: WI }, {
106
- RDC_TRUTHGATE_TASKCOMPLETED: '',
107
- RDC_TRUTHGATE_TASKCOMPLETED_DB: '', // DB check NOT opted into
108
- SUPABASE_URL: 'http://10.255.255.1', // non-routable: any call would hang
109
- SUPABASE_SERVICE_ROLE_KEY: 'x', // a key IS present, so only the opt-in gate keeps us off the network
110
- });
111
- const elapsed = Date.now() - t0;
112
- assert('TCG OFF path → exit 0', r.status === 0, `status=${r.status} ${r.stderr}`);
113
- assert('TCG OFF path makes no DB call (returns well under the network-timeout floor)',
114
- elapsed < 1500, `elapsed=${elapsed}ms (a DB round-trip would approach the ~3.5s timeout)`);
115
- }
116
-
117
- // process: flag ON + no closure ref → BLOCK (exit 2)
118
- {
119
- const r = runHook('task-completed-gate.js', { task: { note: 'no uuid here' } },
120
- { RDC_TRUTHGATE_TASKCOMPLETED: '1' });
121
- assert('TCG flag ON + no work-item ref → exit 2 BLOCK', r.status === 2, `status=${r.status}`);
122
- assert('TCG block mentions no work-item reference', /no work-item reference/.test(r.stdout + r.stderr), r.stdout);
123
- }
124
-
125
- // process: flag ON + unsound closure (status review via sink) → BLOCK
126
- {
127
- const dir = mkdtempSync(join(tmpdir(), 'tcg-'));
128
- const sink = join(dir, 'closure.json');
129
- writeFileSync(sink, JSON.stringify({ id: WI, item_type: 'task', status: 'review', implementation_report: null }));
130
- const r = runHook('task-completed-gate.js', { work_item_id: WI },
131
- { RDC_TRUTHGATE_TASKCOMPLETED: '1', RDC_TASKCOMPLETED_CLOSURE_SINK: sink });
132
- assert('TCG flag ON + unsound closure → exit 2 BLOCK', r.status === 2, `status=${r.status} ${r.stdout}`);
133
- rmSync(dir, { recursive: true, force: true });
134
- }
135
-
136
- // process: flag ON + sound closure (via sink) → PASS (exit 0)
137
- {
138
- const dir = mkdtempSync(join(tmpdir(), 'tcg-'));
139
- const sink = join(dir, 'closure.json');
140
- writeFileSync(sink, JSON.stringify(soundRow));
141
- const r = runHook('task-completed-gate.js', { work_item_id: WI },
142
- { RDC_TRUTHGATE_TASKCOMPLETED: '1', RDC_TASKCOMPLETED_CLOSURE_SINK: sink });
143
- assert('TCG flag ON + sound closure → exit 0 PASS', r.status === 0, `status=${r.status} ${r.stdout}${r.stderr}`);
144
- rmSync(dir, { recursive: true, force: true });
145
- }
146
- }
147
-
148
31
  // ===========================================================================
149
32
  // 2. foreground-process-gate.js — moved to its own file, tests/foreground-process-gate.test.mjs
150
33
  //
@@ -157,57 +40,6 @@ const WI = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
157
40
  // section is not a coverage loss — see that file instead.
158
41
  // ===========================================================================
159
42
 
160
- // ===========================================================================
161
- // 3. post-tool-batch-gate.js
162
- // ===========================================================================
163
- {
164
- const ptb = require(join(HOOKS, 'post-tool-batch-gate.js'));
165
- const DEV = 'c3189c9d58a37d648e9de4a6bcd7d46772053eea';
166
- const STALE = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
167
- const FRESH = '1234567812345678123456781234567812345678';
168
-
169
- // pure: evaluateWave — cannotEvaluate when no develop HEAD
170
- {
171
- const v = ptb.evaluateWave({ developHead: null, worktrees: [], isAncestor: () => true });
172
- assert('PTB evaluateWave: no develop HEAD → cannotEvaluate', v.cannotEvaluate === true && v.ok === false);
173
- }
174
- // pure: current base (develop is ancestor of worktree HEAD) → ok
175
- {
176
- const v = ptb.evaluateWave({
177
- developHead: DEV,
178
- worktrees: [{ path: '/wt/a', head: FRESH }],
179
- isAncestor: (a, b) => a === DEV && b === FRESH, // DEV is ancestor of FRESH
180
- });
181
- assert('PTB evaluateWave: current base → ok, no stale', v.ok === true && v.stale.length === 0);
182
- }
183
- // pure: worktree HEAD == develop HEAD → ok
184
- {
185
- const v = ptb.evaluateWave({
186
- developHead: DEV,
187
- worktrees: [{ path: '/wt/a', head: DEV }],
188
- isAncestor: () => false,
189
- });
190
- assert('PTB evaluateWave: head==develop → ok', v.ok === true && v.stale.length === 0);
191
- }
192
- // pure: stale base (develop NOT an ancestor) → flagged
193
- {
194
- const v = ptb.evaluateWave({
195
- developHead: DEV,
196
- worktrees: [{ path: '/wt/stale', head: STALE }],
197
- isAncestor: () => false, // DEV is NOT an ancestor of STALE
198
- });
199
- assert('PTB evaluateWave: stale base → not ok', v.ok === false, JSON.stringify(v));
200
- assert('PTB evaluateWave: stale worktree listed', v.stale.length === 1 && v.stale[0].head === STALE);
201
- }
202
-
203
- // process: flag OFF → no-op (exit 0)
204
- {
205
- const r = runHook('post-tool-batch-gate.js', { hook_event_name: 'PostToolBatch' },
206
- { RDC_TRUTHGATE_POSTTOOLBATCH: '' });
207
- assert('PTB flag OFF → exit 0 no-op', r.status === 0, `status=${r.status} ${r.stderr}`);
208
- }
209
- }
210
-
211
43
  // ===========================================================================
212
44
  // 4. gate-watchdog-selfcheck.js
213
45
  // ===========================================================================
@@ -296,6 +128,26 @@ const WI = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
296
128
  rmSync(home, { recursive: true, force: true });
297
129
  rmSync(repo, { recursive: true, force: true });
298
130
  }
131
+
132
+ // truthGateWiredForEvent (LIFEAI/regen-root#212): since the stage merge the
133
+ // event registers ONE dispatcher; truth-gate.mjs is named only inside it.
134
+ {
135
+ const dir = mkdtempSync(join(tmpdir(), 'wd-stage-'));
136
+ const stop = join(dir, 'stop.mjs');
137
+ const other = join(dir, 'other.mjs');
138
+ writeFileSync(stop, "import './truth-gate.mjs';\n");
139
+ writeFileSync(other, "console.log('unrelated');\n");
140
+ const N = 'truth-gate.mjs';
141
+ assert('WD wired: direct registration → true',
142
+ wd.truthGateWiredForEvent('Stop::node .claude/hooks/truth-gate.mjs', 'Stop', N) === true);
143
+ assert('WD wired: via stage dispatcher → true (positive control)',
144
+ wd.truthGateWiredForEvent(`Stop::node "${stop}"`, 'Stop', N) === true);
145
+ assert('WD wired: dispatcher that does not reference it → false (negative control)',
146
+ wd.truthGateWiredForEvent(`Stop::node "${other}"`, 'Stop', N) === false);
147
+ assert('WD wired: registered on a different event only → false',
148
+ wd.truthGateWiredForEvent(`SubagentStop::node "${stop}"`, 'Stop', N) === false);
149
+ rmSync(dir, { recursive: true, force: true });
150
+ }
299
151
  }
300
152
 
301
153
  // ===========================================================================
@@ -0,0 +1,42 @@
1
+ // The installer warns when it runs from a checkout behind origin/master
2
+ // (LIFEAI/regen-root#230). Builds a throwaway origin + clone; no network.
3
+ import assert from 'node:assert/strict';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { test } from 'node:test';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const { checkoutBehindMaster } = require('../scripts/install-rdc-skills.js');
13
+
14
+ const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
15
+ const commit = (cwd, name) => {
16
+ writeFileSync(join(cwd, name), name);
17
+ git(cwd, 'add', name);
18
+ git(cwd, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', name);
19
+ };
20
+
21
+ test('checkoutBehindMaster', () => {
22
+ const dir = mkdtempSync(join(tmpdir(), 'rdc-stale-'));
23
+ try {
24
+ const origin = join(dir, 'origin');
25
+ const clone = join(dir, 'clone');
26
+ execFileSync('git', ['init', '-q', '-b', 'master', origin]);
27
+ commit(origin, 'a');
28
+ execFileSync('git', ['clone', '-q', origin, clone]);
29
+
30
+ assert.deepEqual(checkoutBehindMaster(clone), { behind: 0, branch: 'master' });
31
+
32
+ git(clone, 'checkout', '-q', '-b', 'old-feature');
33
+ commit(origin, 'b');
34
+ commit(origin, 'c');
35
+ git(clone, 'fetch', '-q', 'origin');
36
+ assert.deepEqual(checkoutBehindMaster(clone), { behind: 2, branch: 'old-feature' });
37
+
38
+ assert.equal(checkoutBehindMaster(join(dir, 'origin-not-a-clone-dir')), null);
39
+ } finally {
40
+ rmSync(dir, { recursive: true, force: true });
41
+ }
42
+ });
@@ -0,0 +1,30 @@
1
+ // staging_gates in a PUBLISH.md DEPLOY block must be a mapping, the shape
2
+ // regen-root scripts/gen-deploy-blocks.mjs emits (LIFEAI/regen-root#210).
3
+ import assert from 'node:assert/strict';
4
+ import { createRequire } from 'node:module';
5
+ import { test } from 'node:test';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { stagingGatesShapeError } = require('../scripts/validate-publish-manifests.js');
9
+
10
+ const block = (gates) => `\nruntime: next\nport: registry\nhealth_path: /\n${gates}\n# slug: x\n`;
11
+
12
+ test('generated mapping passes', () => {
13
+ assert.equal(stagingGatesShapeError(block('staging_gates:\n tsc_clean: true\n health_200: true\n content_probe: true')), null);
14
+ });
15
+
16
+ test('absent key passes', () => {
17
+ assert.equal(stagingGatesShapeError(block('')), null);
18
+ });
19
+
20
+ test('a list fails', () => {
21
+ assert.match(stagingGatesShapeError(block('staging_gates:\n - tsc_clean\n - health_200')), /mapping.*not a list item/);
22
+ });
23
+
24
+ test('an inline value fails', () => {
25
+ assert.match(stagingGatesShapeError(block('staging_gates: [tsc_clean]')), /not inline/);
26
+ });
27
+
28
+ test('a bare key fails', () => {
29
+ assert.match(stagingGatesShapeError(block('staging_gates:\n tsc_clean')), /not 'key: value'/);
30
+ });
@@ -45,6 +45,19 @@ assert('isGitCommit: false for --help',
45
45
  hook.isGitCommit('git commit --help') === false);
46
46
  assert('isGitCommit: false for unrelated',
47
47
  hook.isGitCommit('git status') === false);
48
+ // LIFEAI/regen-root#243: git must be the command word, not a substring anywhere.
49
+ for (const [cmd, want] of [
50
+ ['( cd /repo && git add a.js && git commit -q -F - <<\'EOF\'\nfix(x): y\nEOF\n)', true],
51
+ ['git -C "C:/Dev/my repo" commit -m "fix: x"', true],
52
+ ['GIT_AUTHOR_NAME=x git -c user.email=a@b commit -m "fix: x"', true],
53
+ ['cd a; git --no-pager commit --amend --no-edit', true],
54
+ ['node rdc-lesson.mjs submit --what "the git hook fired before commit ran"', false],
55
+ ['git log --oneline --grep commit', false],
56
+ ['gh issue create --body "run git commit after staging"', false],
57
+ ['echo git && echo commit', false],
58
+ ]) {
59
+ assert(`isGitCommit(${JSON.stringify(cmd).slice(0, 60)}) === ${want}`, hook.isGitCommit(cmd) === want);
60
+ }
48
61
  assert('commitSucceeded: false on nothing-to-commit',
49
62
  hook.commitSucceeded({ stdout: 'nothing to commit, working tree clean' }) === false);
50
63
  assert('commitSucceeded: true on exit 0',