@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/clauth-plugin.json +1 -1
- package/commands/flow.md +12 -3
- package/git-sha.json +1 -1
- package/hooks/gate-watchdog-selfcheck.js +93 -5
- package/hooks/require-work-item-on-commit.js +15 -1
- package/package.json +3 -3
- package/scripts/acceptance.mjs +5 -2
- package/scripts/install-rdc-skills.js +66 -30
- package/scripts/validate-publish-manifests.js +41 -8
- package/scripts/verify-live-install.mjs +10 -4
- package/skills/build/SKILL.md +4 -0
- package/skills/collab/SKILL.md +2 -0
- package/skills/deploy/SKILL.md +20 -5
- package/skills/edit/SKILL.md +8 -0
- package/skills/fixit/SKILL.md +24 -4
- package/skills/plan/SKILL.md +2 -0
- package/skills/release/SKILL.md +12 -3
- package/skills/status/SKILL.md +3 -5
- package/skills/terminal-config/SKILL.md +5 -0
- package/tests/harness-gates.test.mjs +24 -172
- package/tests/install-stale-checkout.test.mjs +42 -0
- package/tests/publish-manifest-staging-gates.test.mjs +30 -0
- package/tests/require-work-item-on-commit.test.mjs +13 -0
- package/hooks/post-tool-batch-gate.js +0 -203
- package/hooks/task-completed-gate.js +0 -274
package/skills/edit/SKILL.md
CHANGED
|
@@ -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:
|
package/skills/fixit/SKILL.md
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
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>
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -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
|
package/skills/release/SKILL.md
CHANGED
|
@@ -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
|
-
-
|
|
85
|
-
|
|
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
|
|
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
|
package/skills/status/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 —
|
|
3
|
+
* Truth Gate 3.0 — Layer 6 harness-gate tests (WP-5).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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',
|