@lifeaitools/rdc-skills 0.30.2 → 0.31.0
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/.rdc/lessons/2026-08-17-fixit-codex-mcp-only-skills.md +24 -0
- package/README.md +8 -0
- package/commands/build.md +1 -1
- package/commands/collab.md +1 -1
- package/commands/deploy.md +1 -1
- package/commands/design.md +1 -1
- package/commands/edit.md +1 -1
- package/commands/fixit.md +1 -1
- package/commands/handoff.md +1 -1
- package/commands/help.md +1 -1
- package/commands/mode.md +122 -0
- package/commands/overnight.md +1 -1
- package/commands/plan.md +1 -1
- package/commands/preplan.md +1 -1
- package/commands/prototype.md +1 -1
- package/commands/report.md +1 -1
- package/commands/review.md +1 -1
- package/commands/self-test.md +1 -1
- package/commands/status.md +1 -1
- package/commands/watch.md +1 -1
- package/commands/workitems.md +1 -1
- package/hooks/require-work-item-on-commit.js +55 -10
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +150 -41
- package/scripts/verify-live-install.mjs +239 -0
- package/tests/install-rdc-skills.test.mjs +41 -1
- package/tests/require-work-item-on-commit.test.mjs +24 -8
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 2026-08-17-fixit-codex-mcp-only-skills
|
|
3
|
+
date: "2026-08-17"
|
|
4
|
+
skill: fixit
|
|
5
|
+
session: codex-mcp-startup-20260817
|
|
6
|
+
scope: simple
|
|
7
|
+
status: applied
|
|
8
|
+
area: skill
|
|
9
|
+
links:
|
|
10
|
+
commits:
|
|
11
|
+
- 263231a
|
|
12
|
+
memory: []
|
|
13
|
+
work_items:
|
|
14
|
+
- dc076f35-236b-4f8d-be3f-646a66514ba5
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What happened
|
|
18
|
+
Codex's real numbered-lane startup completed MCP initialization but emitted a red skill-budget error on every launch. The machine had 36 RDC skill directories in each of the global `.codex/skills` and `.agents/skills` roots while the same 36-skill catalog was already registered through the RDC-Skills MCP endpoint.
|
|
19
|
+
|
|
20
|
+
## Root cause
|
|
21
|
+
`scripts/install-rdc-skills.js` registered the MCP endpoint and then copied the full RDC catalog into every detected Codex skill directory. Codex loaded those duplicate surfaces together, inflating startup metadata to 113 skills. The same install also left a legacy RDC marketplace block that Codex rejected as an unsupported plugin source.
|
|
22
|
+
|
|
23
|
+
## The fix / rule
|
|
24
|
+
Codex receives RDC skills exclusively through MCP. The installer now verifies the MCP config before purging legacy file copies, fails closed if the replacement cannot be established, removes the obsolete marketplace/plugin blocks with CRLF coverage, and verifies that live Codex skill roots contain zero RDC duplicates. Implemented in commit `263231a`.
|
package/README.md
CHANGED
|
@@ -209,6 +209,14 @@ Located in `guides/agents/` — plain markdown playbooks spawned as sub-agents:
|
|
|
209
209
|
### Reporting
|
|
210
210
|
- rdc:help, rdc:housekeeping, rdc:report, rdc:status, rdc:watch
|
|
211
211
|
|
|
212
|
+
### Operating mode
|
|
213
|
+
- rdc:mode — show or set the operating mode. `HotFix` relaxes ceremony gates
|
|
214
|
+
(full-build, polling-loops, persistent-cd, truth-gate, completion-gate, …) for
|
|
215
|
+
a bounded, reasoned window. It **cannot** relax a safety guard: push-main,
|
|
216
|
+
push-force, rm-rf, credential exposure, cross-lane writes and the authority
|
|
217
|
+
check stay armed in every mode, enforced as an allowlist so a guard added
|
|
218
|
+
tomorrow is essential by default.
|
|
219
|
+
|
|
212
220
|
### Tooling
|
|
213
221
|
- rdc:channel-formatter — channel-native formatting and content repurposing for LinkedIn, Twitter/X, Slack/Teams, email, decks, Word/PDF structure, web copy, and content packs
|
|
214
222
|
- rdc:brochure — HTML/folder/zip/URL/Markdown to print-quality PDF
|
package/commands/build.md
CHANGED
package/commands/collab.md
CHANGED
package/commands/deploy.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: deploy
|
|
3
3
|
description: >-
|
|
4
4
|
Coolify ops. Usage `rdc:deploy <slug> [build-id]`, `rdc:deploy new <slug>`, `rdc:deploy maintenance <service>`, `rdc:deploy diagnose <slug>`, or `rdc:deploy audit [--fix]` — typed checklists, private-service controls, DNS decision tree, and mandatory post-deploy gates. Checklist-only output.
|
|
5
5
|
---
|
package/commands/design.md
CHANGED
package/commands/edit.md
CHANGED
package/commands/fixit.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: fixit
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:fixit <description>` — sanctioned bypass for quick fixes under 5 files / 30 min. Creates minimal work item, makes fix, commits, DELIVERS it to where it is consumed (publish/deploy/land) and verifies, then closes. The ONLY alternative to rdc:build. For typos, config patches, hotfixes, dep bumps.
|
|
5
5
|
---
|
package/commands/handoff.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: handoff
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:handoff <topic>` — structured handoff from planning session to CLI agents. Produces plan doc in .rdc/plans/, work items in DB, prototype registry entry if applicable. Use when plan/prototype is finalized and ready for implementation.
|
|
5
5
|
---
|
package/commands/help.md
CHANGED
package/commands/mode.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mode
|
|
3
|
+
description: >-
|
|
4
|
+
Usage `rdc:mode [status|hotfix "<reason>" [minutes]|normal]` — show or set the operating mode. HotFix relaxes ceremony gates for a bounded window; it never relaxes a safety guard.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
8
|
+
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
9
|
+
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
10
|
+
|
|
11
|
+
# rdc:mode — operating mode
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
- A production incident where process ceremony is costing minutes that matter
|
|
15
|
+
- Checking whether a permissive window is currently open (and when it closes)
|
|
16
|
+
- Closing one early, the moment the incident is over
|
|
17
|
+
|
|
18
|
+
## The one thing to understand first
|
|
19
|
+
|
|
20
|
+
**HotFix relaxes CEREMONY. It cannot relax SAFETY.**
|
|
21
|
+
|
|
22
|
+
That is enforced in code as an allowlist, not a promise in a doc: only the eight
|
|
23
|
+
ceremony rules named in `$LIFEAI_ENV/hooks/lib/rdc-mode.mjs` (`HOTFIX_DISABLES`)
|
|
24
|
+
can ever be skipped, and the filtering happens at one place —
|
|
25
|
+
`ALL_RULES()` in `guard-rules.mjs` — which all four of `evaluateGuard`'s passes
|
|
26
|
+
iterate. A guard added tomorrow is **essential by default**.
|
|
27
|
+
|
|
28
|
+
| relaxed in HotFix | never relaxed, in any mode |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `full-build` · `polling-loops` · `persistent-cd` · `powershell-5.1` · `build-shortcircuit` · `codex-work-item-wrapper` · `codex-supabase-rpc-wrapper` · `codex-ssh-preflight` | `push-main` · `push-force` · `no-verify` · `rm-rf-danger` · `remove-item-danger` · `reset-hard-pool` · `clauth-exposure` · `ssh-key-raw-fetch` · `cross-tree-bash` · `lane-push-develop` · `git-add-all-standalone` · `coolify-direct` · `execution-scope` · `env-repo-maintainer-only` · `settings-json-direct-edit` · `node-modules-write` · `neo4j-outside-codeflow` · `resurrect-deleted-path` · `onramp-*` · `terminal-window-mutation` |
|
|
31
|
+
|
|
32
|
+
Everything in the left column costs **time or tidiness**, and fails immediately,
|
|
33
|
+
locally and visibly. Everything in the right column is irreversible, destroys
|
|
34
|
+
another session's work, leaks a secret into a transcript forever, or fabricates
|
|
35
|
+
governance evidence.
|
|
36
|
+
|
|
37
|
+
Ceremony **gates** are relaxed alongside the guards: `truth-gate`,
|
|
38
|
+
`completion-gate`, `stop-test-nudge`, `rdc-commit-gate`.
|
|
39
|
+
|
|
40
|
+
## Why this exists
|
|
41
|
+
|
|
42
|
+
Five unrelated escape hatches had already accumulated — `ALLOW_RESURRECT`,
|
|
43
|
+
`LIFEAI_ENV_MAINTAINER`, `RDC_TEST`, the `RDC-Bypass:` commit trailer, and a
|
|
44
|
+
CodeFlow break-glass file. Each was added when one guard got in the way, each has
|
|
45
|
+
its own spelling, **none expires**, and none is audited with the others. Somebody
|
|
46
|
+
in a hurry cannot find the right one, so they reach for the one they remember —
|
|
47
|
+
usually the biggest.
|
|
48
|
+
|
|
49
|
+
One named state, with a reason and a deadline, is easier to find *and* smaller in
|
|
50
|
+
blast radius than any of them.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
rdc:mode # status — the default
|
|
56
|
+
rdc:mode status
|
|
57
|
+
rdc:mode hotfix "prod checkout 500s" # open a 60-minute window
|
|
58
|
+
rdc:mode hotfix "prod down" 120 # explicit minutes, capped at 240
|
|
59
|
+
rdc:mode normal # close it early
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Steps
|
|
63
|
+
|
|
64
|
+
### 1. Read the current mode
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node -e "import('file:///C:/Dev/lifeai-env/hooks/lib/rdc-mode.mjs').then(m=>console.log(JSON.stringify(m.currentMode(),null,2)))"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Report `mode`, and when not normal also `reason`, `setBy`, and `minutesLeft`.
|
|
71
|
+
A mode with no minutes left is already normal — the state expires on read, not on
|
|
72
|
+
a timer, so there is nothing to clean up.
|
|
73
|
+
|
|
74
|
+
### 2. Setting hotfix — a reason is REQUIRED
|
|
75
|
+
|
|
76
|
+
Refuse to proceed without one. `setMode` throws on an empty reason by design: an
|
|
77
|
+
unexplained disarm is how a temporary state becomes permanent, and the reason is
|
|
78
|
+
the entire content of the audit line.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node -e "import('file:///C:/Dev/lifeai-env/hooks/lib/rdc-mode.mjs').then(m=>console.log(JSON.stringify(m.setMode('hotfix',{reason:process.argv[1],minutes:Number(process.argv[2]||60),setBy:process.argv[3]}),null,2)))" "<reason>" "<minutes>" "<session-id>"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then state plainly, in the checklist: **which guards are now off, that the safety
|
|
85
|
+
set is still armed, and the exact wall-clock time the window closes.**
|
|
86
|
+
|
|
87
|
+
### 3. Returning to normal
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
node -e "import('file:///C:/Dev/lifeai-env/hooks/lib/rdc-mode.mjs').then(m=>{m.clearMode();console.log('normal')})"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Do this **as soon as the incident is over**. The window expiring on its own is
|
|
94
|
+
the backstop, not the plan.
|
|
95
|
+
|
|
96
|
+
## Rules
|
|
97
|
+
|
|
98
|
+
- **A reason is mandatory.** No reason, no mode change.
|
|
99
|
+
- **Bounded.** Default 60 minutes, hard cap 240. A hotfix is an incident, not a
|
|
100
|
+
working style. If the work needs longer than 240 minutes it is not a hotfix and
|
|
101
|
+
wants `rdc:fixit` or `rdc:build`.
|
|
102
|
+
- **Fail-closed.** Absent, malformed, unknown-mode or expired state all read as
|
|
103
|
+
`normal`. The hurry that motivates a hotfix is exactly the state in which a
|
|
104
|
+
silently-stuck permissive mode does the most damage.
|
|
105
|
+
- **Never widen the disable set to unblock yourself.** A guard blocking a hotfix
|
|
106
|
+
is information. If it is genuinely wrong, that is its own change with its own
|
|
107
|
+
review — editing `HOTFIX_DISABLES` to get past a red guard is the same
|
|
108
|
+
violation as editing code to satisfy a failing gate
|
|
109
|
+
(`.claude/rules/debugging-protocol.md` Rule 10). The safety list is asserted by
|
|
110
|
+
name in `tests/rdc-mode.test.mjs`, so that edit fails a test.
|
|
111
|
+
- **HotFix is not a bypass of review.** Work done in a hotfix window still needs
|
|
112
|
+
its work item, its evidence, and its close. The window buys speed on ceremony,
|
|
113
|
+
not permission to skip the record.
|
|
114
|
+
|
|
115
|
+
## Verification
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node --test $LIFEAI_ENV/tests/rdc-mode.test.mjs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Seven cases, including the load-bearing one: every safety guard is asserted **by
|
|
122
|
+
name** to remain armed while a hotfix window is open.
|
package/commands/overnight.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: overnight
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:overnight [epic-id|label=X]` — unattended overnight supervisor, chains preplan → plan → build → review → report across all high-priority epics in --unattended mode. Use for "run overnight", "build while I sleep".
|
|
5
5
|
---
|
package/commands/plan.md
CHANGED
package/commands/preplan.md
CHANGED
package/commands/prototype.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: prototype
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:prototype <description>` — build JSX/TSX prototype for review before CLI handoff. Saves to docs/source/, registers in prototype_registry. Use for "mock this up", "show me what it looks like". Reference material only, not production.
|
|
5
5
|
---
|
package/commands/report.md
CHANGED
package/commands/review.md
CHANGED
package/commands/self-test.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: self-test
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:self-test [--strict] [--skill <name>] [--json] [--fix] [--tier2] [--parallel <n>] [--quick]` — validates every rdc-*.md skill: frontmatter, Usage marker, name↔filename match, referenced guides/rules/hooks exist, output contract banner, plugin manifest, duplicate-name + collision checks. Tier 1 static lint; Tier 2 behavioral runs via headless Claude or Codex in isolated worktrees + Supabase branch. Run before every release.
|
|
5
5
|
---
|
package/commands/status.md
CHANGED
package/commands/watch.md
CHANGED
package/commands/workitems.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: workitems
|
|
3
3
|
description: >-
|
|
4
4
|
Usage `rdc:workitems <action> [args]` — create, update, query work items via Supabase RPCs. Actions: add, update, done, list, epics. Use for "add to backlog", "mark done", "show open epics", any work item CRUD.
|
|
5
5
|
---
|
|
@@ -17,10 +17,23 @@
|
|
|
17
17
|
* fabrication class (FMEA #1) at the source — WP-2's closure gate then asserts a
|
|
18
18
|
* report's codeflow_post.commit is one of these captured, session-authored SHAs.
|
|
19
19
|
*
|
|
20
|
-
* PreToolUse (intent) behavior
|
|
20
|
+
* PreToolUse (intent) behavior (2026-08-16 — real teeth, flow-gated):
|
|
21
|
+
* - flow does NOT require a work item (rdc-flow.mjs: plan/design/collab,
|
|
22
|
+
* or a relaxed mode — hotfix/maintenance) -> pass silently, no nag
|
|
21
23
|
* - fixit.marker present -> pass
|
|
22
24
|
* - conventional type / UUID / #issue ref -> pass
|
|
23
|
-
* - otherwise
|
|
25
|
+
* - otherwise, flow DOES require one (build/refactor/overnight, or no
|
|
26
|
+
* flow declared at all — fail-closed default)
|
|
27
|
+
* -> BLOCK (was: warn, proceed anyway)
|
|
28
|
+
*
|
|
29
|
+
* Was advisory-only unconditionally ("never hard-block commits... proceeding
|
|
30
|
+
* anyway") — Dave, 2026-08-16, on seeing that exact text fire with no
|
|
31
|
+
* enforcement behind it: the model declares its own flow (rdc-flow.mjs,
|
|
32
|
+
* lifeai-env), and THIS is where that declaration gets teeth. A
|
|
33
|
+
* conversational flow (plan/design/collab) never needed a work item and
|
|
34
|
+
* still doesn't — silently, not even a warning. A code-shipping flow
|
|
35
|
+
* (build/refactor/overnight) now actually stops the commit instead of
|
|
36
|
+
* printing a sentence nobody reads.
|
|
24
37
|
*
|
|
25
38
|
* PostToolUse (capture) behavior:
|
|
26
39
|
* - only fires on a `git commit` whose tool result indicates success
|
|
@@ -220,14 +233,42 @@ async function captureCommit(raw) {
|
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
// ---------------------------------------------------------------------------
|
|
223
|
-
// PreToolUse intent check
|
|
236
|
+
// PreToolUse intent check — flow-gated (2026-08-16)
|
|
224
237
|
// ---------------------------------------------------------------------------
|
|
225
238
|
|
|
226
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Does the CURRENT model-declared flow (rdc-flow.mjs, lifeai-env) require a
|
|
241
|
+
* work item right now? Dynamic import because this file is CommonJS and
|
|
242
|
+
* rdc-flow.mjs is ESM; resolved via $LIFEAI_ENV so this works identically on
|
|
243
|
+
* whichever machine/lane the hook actually runs from.
|
|
244
|
+
*
|
|
245
|
+
* FAILS CLOSED: any error (lifeai-env not on this box, module not found,
|
|
246
|
+
* whatever) returns true — "requires a work item" — the exact behavior this
|
|
247
|
+
* file had before flow-state existed. Introducing this mechanism can only
|
|
248
|
+
* EXEMPT a case that used to warn, never silently add a requirement that
|
|
249
|
+
* was not already effectively there.
|
|
250
|
+
*/
|
|
251
|
+
async function requiresWorkItemNow() {
|
|
252
|
+
const hub = process.env.LIFEAI_ENV || 'C:/Dev/lifeai-env';
|
|
253
|
+
try {
|
|
254
|
+
const mod = await import(`file://${path.join(hub, 'hooks', 'lib', 'rdc-flow.mjs').replace(/\\/g, '/')}`);
|
|
255
|
+
return mod.requiresWorkItem();
|
|
256
|
+
} catch (e) {
|
|
257
|
+
hookLog('require-work-item', 'PreToolUse', 'flow-check-error-fail-closed', { error: e.message });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function preToolUse(raw) {
|
|
227
263
|
if (raw.tool_name !== 'Bash') return process.exit(0);
|
|
228
264
|
const command = raw.tool_input?.command || '';
|
|
229
265
|
if (!command.includes('git commit')) return process.exit(0);
|
|
230
266
|
|
|
267
|
+
if (!(await requiresWorkItemNow())) {
|
|
268
|
+
hookLog('require-work-item', 'PreToolUse', 'pass-flow-exempt', {});
|
|
269
|
+
return process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
231
272
|
if (fs.existsSync(MARKER_FILE)) {
|
|
232
273
|
hookLog('require-work-item', 'PreToolUse', 'pass-fixit', {});
|
|
233
274
|
return process.exit(0);
|
|
@@ -239,12 +280,16 @@ function preToolUse(raw) {
|
|
|
239
280
|
return process.exit(0);
|
|
240
281
|
}
|
|
241
282
|
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
//
|
|
283
|
+
// BLOCK, not warn (2026-08-16). The current flow (build/refactor/overnight,
|
|
284
|
+
// or none declared at all) requires a work item and none was referenced —
|
|
285
|
+
// this used to print a sentence and let the commit through regardless.
|
|
286
|
+
hookLog('require-work-item', 'PreToolUse', 'block', { msg: msg.slice(0, 80) });
|
|
245
287
|
process.stdout.write(JSON.stringify({
|
|
246
|
-
|
|
247
|
-
|
|
288
|
+
decision: 'block',
|
|
289
|
+
reason: '⛔ [require-work-item] No work item reference or conventional commit type, and the current '
|
|
290
|
+
+ 'flow requires one (build/refactor/overnight, or no flow declared). Add a Work-Item UUID or '
|
|
291
|
+
+ '#issue ref to the message, use fix(<scope>): ..., or declare a flow that does not need one '
|
|
292
|
+
+ '(rdc-flow.mjs: plan/design/collab) if this genuinely ships no trackable work.',
|
|
248
293
|
}));
|
|
249
294
|
return process.exit(0);
|
|
250
295
|
}
|
|
@@ -276,7 +321,7 @@ async function main() {
|
|
|
276
321
|
}
|
|
277
322
|
|
|
278
323
|
// Default / PreToolUse intent path.
|
|
279
|
-
preToolUse(raw);
|
|
324
|
+
await preToolUse(raw);
|
|
280
325
|
}
|
|
281
326
|
|
|
282
327
|
// Run when executed as a hook; export pure pieces when required by a test.
|
package/package.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* node scripts/install-rdc-skills.js --profile core ← clean-box portable hooks
|
|
9
9
|
* node scripts/install-rdc-skills.js --profile lifeai ← LIFEAI/regen-root hooks
|
|
10
10
|
* node scripts/install-rdc-skills.js --claude-home <path> ← custom CLI home
|
|
11
|
-
* node scripts/install-rdc-skills.js --codex-root <path> ←
|
|
12
|
-
* node scripts/install-rdc-skills.js --codex-skill-dir <path> ←
|
|
11
|
+
* node scripts/install-rdc-skills.js --codex-root <path> ← purge legacy project-local rdc skill copies
|
|
12
|
+
* node scripts/install-rdc-skills.js --codex-skill-dir <path> ← purge one legacy Codex skill dir
|
|
13
13
|
* node scripts/install-rdc-skills.js --project-root <path> --write-startup-blocks
|
|
14
14
|
* node scripts/install-rdc-skills.js --migrate <path> ← migrate docs/ → .rdc/
|
|
15
15
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 1. git pull (latest commands + guides)
|
|
18
18
|
* 2. CLI plugin — registers in ~/.claude/plugins/ + settings.json
|
|
19
19
|
* 3. Cowork — registers in Desktop cowork_plugins/ + cowork_settings.json
|
|
20
|
-
* 3.5 Codex —
|
|
20
|
+
* 3.5 Codex — removes stale file-based copies; Codex loads RDC skills through MCP
|
|
21
21
|
* 4. Hook files — copies hooks/*.js → ~/.claude/hooks/
|
|
22
22
|
* 5. Hook wiring — wires hooks into ~/.claude/settings.json
|
|
23
23
|
* 6. Zip — builds dist/rdc-skills-plugin.zip for claude.ai / distribution
|
|
@@ -101,26 +101,36 @@ function run(cmd, options = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function updateCodexMcpToml(toml, mcpUrl) {
|
|
104
|
+
const eol = toml.includes('\r\n') ? '\r\n' : '\n';
|
|
105
|
+
toml = toml.replace(/\r\n/g, '\n');
|
|
106
|
+
// RDC skills are MCP-only in Codex. Older installs also registered an RDC
|
|
107
|
+
// marketplace/plugin, which Codex now rejects as an unsupported source and
|
|
108
|
+
// reports during every startup. Remove only those exact legacy blocks.
|
|
109
|
+
toml = toml
|
|
110
|
+
.replace(/(^|\n)\[marketplaces\.rdc-skills\]\n[\s\S]*?(?=\n\[|\s*$)/g, '$1')
|
|
111
|
+
.replace(/(^|\n)\[plugins\."rdc-skills@rdc-skills"\]\n[\s\S]*?(?=\n\[|\s*$)/g, '$1');
|
|
104
112
|
const blockRe = /(^|\n)(\[mcp_servers\.rdc-skills\]\n)([\s\S]*?)(?=\n\[|\s*$)/;
|
|
105
113
|
const desiredLine = `url = '${mcpUrl}'`;
|
|
106
114
|
if (blockRe.test(toml)) {
|
|
107
|
-
|
|
115
|
+
const updated = toml.replace(blockRe, (match, prefix, header, body) => {
|
|
108
116
|
if (/^url\s*=.*$/m.test(body)) {
|
|
109
117
|
return `${prefix}${header}${body.replace(/^url\s*=.*$/m, desiredLine)}`;
|
|
110
118
|
}
|
|
111
119
|
const trimmed = body.replace(/\s*$/, '');
|
|
112
120
|
return `${prefix}${header}${trimmed}${trimmed ? '\n' : ''}${desiredLine}\n`;
|
|
113
121
|
});
|
|
122
|
+
return updated.replace(/\n/g, eol);
|
|
114
123
|
}
|
|
115
|
-
return toml.replace(/\s*$/, '\n') + `\n[mcp_servers.rdc-skills]\n${desiredLine}\n
|
|
124
|
+
return (toml.replace(/\s*$/, '\n') + `\n[mcp_servers.rdc-skills]\n${desiredLine}\n`).replace(/\n/g, eol);
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
function selfTestCodexMcpToml() {
|
|
119
128
|
const url = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
120
|
-
const stale = "[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n\n[mcp_servers.rdc-skills]\nurl = 'https://rdc-skills.dev.regendevcorp.com/mcp'\n\n[mcp_servers.web-research]\nurl = 'https://research.regendevcorp.com/mcp'\n";
|
|
129
|
+
const stale = "[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n\n[mcp_servers.rdc-skills]\nurl = 'https://rdc-skills.dev.regendevcorp.com/mcp'\n\n[marketplaces.rdc-skills]\nsource_type = 'git'\nsource = 'https://github.com/LIFEAI/rdc-skills.git'\n\n[plugins.\"rdc-skills@rdc-skills\"]\nenabled = true\n\n[mcp_servers.web-research]\nurl = 'https://research.regendevcorp.com/mcp'\n";
|
|
121
130
|
const updated = updateCodexMcpToml(stale, url);
|
|
122
131
|
if (!updated.includes(`url = '${url}'`)) throw new Error('did not write production rdc-skills URL');
|
|
123
132
|
if (updated.includes('rdc-skills.dev.regendevcorp.com')) throw new Error('stale dev URL survived');
|
|
133
|
+
if (updated.includes('[marketplaces.rdc-skills]') || updated.includes('[plugins."rdc-skills@rdc-skills"]')) throw new Error('legacy Codex plugin state survived');
|
|
124
134
|
if (!updated.includes('[mcp_servers.clauth]') || !updated.includes('[mcp_servers.web-research]')) throw new Error('neighbor MCP blocks were damaged');
|
|
125
135
|
|
|
126
136
|
const missing = updateCodexMcpToml("[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n", url);
|
|
@@ -129,6 +139,10 @@ function selfTestCodexMcpToml() {
|
|
|
129
139
|
const noUrl = updateCodexMcpToml('[mcp_servers.rdc-skills]\nstartup_timeout_sec = 30\n', url);
|
|
130
140
|
if (!noUrl.includes("startup_timeout_sec = 30") || !noUrl.includes(`url = '${url}'`)) throw new Error('url-less block was not repaired');
|
|
131
141
|
|
|
142
|
+
const crlf = updateCodexMcpToml(stale.replace(/\n/g, '\r\n'), url);
|
|
143
|
+
if (crlf.includes('[marketplaces.rdc-skills]') || crlf.includes('[plugins."rdc-skills@rdc-skills"]')) throw new Error('CRLF legacy Codex plugin state survived');
|
|
144
|
+
if (!crlf.includes(`url = '${url}'`) || !crlf.includes('\r\n')) throw new Error('CRLF MCP config was not preserved');
|
|
145
|
+
|
|
132
146
|
console.log('install-rdc-skills codex MCP TOML self-test — PASS');
|
|
133
147
|
}
|
|
134
148
|
|
|
@@ -474,6 +488,42 @@ function flushOldCaches(cacheBase /* keepVersion intentionally unused */) {
|
|
|
474
488
|
return flushed;
|
|
475
489
|
}
|
|
476
490
|
|
|
491
|
+
// ── Marketplace checkout sync ────────────────────────────────────────────────
|
|
492
|
+
// The marketplace directory is a git CLONE, and this installer historically only
|
|
493
|
+
// rewrote `.claude-plugin/marketplace.json` inside it. The rest of the working
|
|
494
|
+
// tree stayed at whatever commit it was cloned at — and since the plugin loader
|
|
495
|
+
// registers every directory it finds, that stale tree is a second, older copy of
|
|
496
|
+
// every command sitting in the menu beside the current one.
|
|
497
|
+
//
|
|
498
|
+
// Fetching is not installing. On 2026-08-16 this clone had a same-day FETCH_HEAD
|
|
499
|
+
// and a working tree 77 commits behind, still serving the pre-rename
|
|
500
|
+
// `name: rdc:<x>` commands that rendered /rdc:rdc:plan next to /rdc:plan.
|
|
501
|
+
//
|
|
502
|
+
// PRESERVE-DIRTY: a clone with local modifications is never reset. It is a
|
|
503
|
+
// consumer copy, so uncommitted content there is unexpected — which is exactly
|
|
504
|
+
// why it gets reported rather than destroyed.
|
|
505
|
+
function syncMarketplaceCheckout(mktDir) {
|
|
506
|
+
const gitDir = path.join(mktDir, '.git');
|
|
507
|
+
if (!fs.existsSync(gitDir)) return; // manifest-only directory; nothing to sync
|
|
508
|
+
const git = (...args) =>
|
|
509
|
+
execSync(`git -C "${mktDir}" ${args.join(' ')}`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
510
|
+
try {
|
|
511
|
+
if (git('status', '--porcelain')) {
|
|
512
|
+
warn(`marketplace clone has local changes at ${mktDir} — left untouched; it may serve stale commands`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const branch = git('rev-parse', '--abbrev-ref', 'HEAD') || 'master';
|
|
516
|
+
const ref = branch === 'HEAD' ? 'master' : branch;
|
|
517
|
+
git('fetch', 'origin', ref, '--quiet');
|
|
518
|
+
const behind = Number(git('rev-list', '--count', `HEAD..origin/${ref}`)) || 0;
|
|
519
|
+
if (behind === 0) return;
|
|
520
|
+
git('reset', '--hard', `origin/${ref}`, '--quiet');
|
|
521
|
+
info(` marketplace: advanced ${behind} commit(s) -> ${git('rev-parse', '--short', 'HEAD')}`);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
warn(`marketplace clone could not be synced (${String(e.message).split('\n')[0]}) — it may serve stale commands`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
477
527
|
// ── Step 2: CLI plugin registration (→ ~/.claude/plugins/) ───────────────────
|
|
478
528
|
function registerCLI(version, gitSha) {
|
|
479
529
|
const pluginDir = path.join(claudeHome, 'plugins');
|
|
@@ -486,6 +536,24 @@ function registerCLI(version, gitSha) {
|
|
|
486
536
|
fs.mkdirSync(mktPlugDir, { recursive: true });
|
|
487
537
|
fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
|
|
488
538
|
|
|
539
|
+
// 1b. The marketplace dir is a CLONE, and this installer only ever rewrote the
|
|
540
|
+
// manifest inside it. Everything else — commands/, skills/, hooks/ — stayed at
|
|
541
|
+
// whatever commit it was cloned at, and the plugin loader registers every dir
|
|
542
|
+
// it finds (see the note on cache flushing below). So a stale clone is a
|
|
543
|
+
// second, older copy of every command, live in the menu next to the current
|
|
544
|
+
// one.
|
|
545
|
+
//
|
|
546
|
+
// Observed 2026-08-16: this clone sat at v0.24.36, SEVENTY-SEVEN commits
|
|
547
|
+
// behind master, with a FETCH_HEAD from the same day. Fetched and never
|
|
548
|
+
// checked out — the identical failure the environment harness had, where a
|
|
549
|
+
// bare hub advanced its ref while the files consumers actually execute never
|
|
550
|
+
// moved. Its stale commands still declared the old `name: rdc:<x>`, so the
|
|
551
|
+
// menu showed /rdc:rdc:plan beside /rdc:plan, one entry per source.
|
|
552
|
+
//
|
|
553
|
+
// A fetch is not an install. Bring the working tree to the ref, or do not
|
|
554
|
+
// treat the directory as installed.
|
|
555
|
+
syncMarketplaceCheckout(mktDir);
|
|
556
|
+
|
|
489
557
|
// 2. known_marketplaces.json
|
|
490
558
|
const kmpPath = path.join(pluginDir, 'known_marketplaces.json');
|
|
491
559
|
const knownMp = readJson(kmpPath);
|
|
@@ -637,6 +705,8 @@ function addCodexTarget(targets, label, targetDir) {
|
|
|
637
705
|
// connector add in its UI (no programmatic API).
|
|
638
706
|
function registerMcpEndpoints() {
|
|
639
707
|
const out = [];
|
|
708
|
+
let codexReady = false;
|
|
709
|
+
let codexError = null;
|
|
640
710
|
|
|
641
711
|
// Claude Code — user-level ~/.claude.json mcpServers (covers EVERY project).
|
|
642
712
|
try {
|
|
@@ -656,17 +726,33 @@ function registerMcpEndpoints() {
|
|
|
656
726
|
// Codex — ensure [mcp_servers.rdc-skills] exists and points at the live shared endpoint.
|
|
657
727
|
try {
|
|
658
728
|
const codexToml = path.join(os.homedir(), '.codex', 'config.toml');
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
729
|
+
fs.mkdirSync(path.dirname(codexToml), { recursive: true });
|
|
730
|
+
const toml = fs.existsSync(codexToml) ? fs.readFileSync(codexToml, 'utf8') : '';
|
|
731
|
+
const next = updateCodexMcpToml(toml, PUBLIC_MCP_URL);
|
|
732
|
+
if (next !== toml) {
|
|
733
|
+
const tmp = `${codexToml}.tmp-${process.pid}`;
|
|
734
|
+
const mode = fs.existsSync(codexToml) ? (fs.statSync(codexToml).mode & 0o777) : 0o600;
|
|
735
|
+
try {
|
|
736
|
+
fs.writeFileSync(tmp, next, { mode });
|
|
737
|
+
fs.chmodSync(tmp, mode);
|
|
738
|
+
fs.renameSync(tmp, codexToml);
|
|
739
|
+
} finally {
|
|
740
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
665
741
|
}
|
|
742
|
+
out.push('codex(~/.codex/config.toml)');
|
|
666
743
|
}
|
|
667
|
-
|
|
744
|
+
const verified = fs.readFileSync(codexToml, 'utf8').replace(/\r\n/g, '\n');
|
|
745
|
+
const block = verified.match(/\[mcp_servers\.rdc-skills\]\n([\s\S]*?)(?=\n\[|\s*$)/)?.[1] || '';
|
|
746
|
+
codexReady = new RegExp(`^url\\s*=\\s*['\"]${PUBLIC_MCP_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['\"]$`, 'm').test(block)
|
|
747
|
+
&& !verified.includes('[marketplaces.rdc-skills]')
|
|
748
|
+
&& !verified.includes('[plugins."rdc-skills@rdc-skills"]');
|
|
749
|
+
if (!codexReady) throw new Error('Codex MCP endpoint did not verify after write');
|
|
750
|
+
} catch (e) {
|
|
751
|
+
codexError = e.message;
|
|
752
|
+
out.push(`codex ERROR:${e.message}`);
|
|
753
|
+
}
|
|
668
754
|
|
|
669
|
-
return out;
|
|
755
|
+
return { updates: out, codexReady, codexError };
|
|
670
756
|
}
|
|
671
757
|
|
|
672
758
|
function findCodexTargets() {
|
|
@@ -711,19 +797,11 @@ function registerCodexTarget(targetDir) {
|
|
|
711
797
|
}
|
|
712
798
|
}
|
|
713
799
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const skillFile = path.join(skillsSrc, entry.name, 'SKILL.md');
|
|
720
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
721
|
-
const dst = path.join(targetDir, `rdc-${entry.name}`);
|
|
722
|
-
copyDirRecursive(path.join(skillsSrc, entry.name), dst);
|
|
723
|
-
copied++;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return { removed, copied };
|
|
800
|
+
// Do not reinstall file-based copies. Codex discovers the live catalog via
|
|
801
|
+
// [mcp_servers.rdc-skills]. Keeping the same skills in ~/.codex and ~/.agents
|
|
802
|
+
// registers every skill two or three times, exhausts Codex's skill metadata
|
|
803
|
+
// budget, and emits a red startup error on every session.
|
|
804
|
+
return { removed, copied: 0 };
|
|
727
805
|
}
|
|
728
806
|
|
|
729
807
|
// ── Step 6: Zip for claude.ai / distribution ─────────────────────────────────
|
|
@@ -874,6 +952,32 @@ function reportMcpConnector() {
|
|
|
874
952
|
info(` claude.ai / MCP clients: ${PUBLIC_MCP_URL} (Auth: none)`);
|
|
875
953
|
}
|
|
876
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Live cross-surface proof, run at the end of every install.
|
|
957
|
+
*
|
|
958
|
+
* Everything above this line WRITES config files and TRUSTS they took effect.
|
|
959
|
+
* On 2026-08-16 that trust was wrong for one whole surface — the production
|
|
960
|
+
* MCP endpoint answered {"skills":0} while origin/master carried 36 — and
|
|
961
|
+
* nothing in this installer had ever asked it. This is the ask: ADVISORY
|
|
962
|
+
* only (never blocks a Claude CLI / Codex install that IS correct just
|
|
963
|
+
* because a shared remote service is stale), but it prints in every run so
|
|
964
|
+
* the gap cannot go quiet again.
|
|
965
|
+
*/
|
|
966
|
+
function runLiveProof() {
|
|
967
|
+
const script = path.join(repoRoot, 'scripts', 'verify-live-install.mjs');
|
|
968
|
+
if (!fs.existsSync(script)) return;
|
|
969
|
+
console.log('');
|
|
970
|
+
console.log(' \x1b[36mLive cross-surface proof:\x1b[0m');
|
|
971
|
+
try {
|
|
972
|
+
const codexRootArg = codexRoot ? ` --codex-root "${codexRoot}"` : '';
|
|
973
|
+
execSync(`node "${script}"${codexRootArg}`, { stdio: 'inherit', cwd: repoRoot });
|
|
974
|
+
} catch {
|
|
975
|
+
// Non-zero = a surface is stale. Already printed by the script itself;
|
|
976
|
+
// advisory here on purpose — see docstring.
|
|
977
|
+
warn(' one or more live surfaces do not match origin/master — see table above');
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
877
981
|
// ── Preflight ─────────────────────────────────────────────────────────────────
|
|
878
982
|
function runPreflight() {
|
|
879
983
|
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
@@ -1051,7 +1155,18 @@ async function main() {
|
|
|
1051
1155
|
warn('[2/6] Cowork — no Desktop workspaces found (open Claude Desktop once to create them)');
|
|
1052
1156
|
}
|
|
1053
1157
|
|
|
1054
|
-
// 2.5.
|
|
1158
|
+
// 2.5. Establish and verify the MCP replacement BEFORE purging file copies.
|
|
1159
|
+
const mcpReg = registerMcpEndpoints();
|
|
1160
|
+
if (!mcpReg.codexReady) {
|
|
1161
|
+
throw new Error(`Codex MCP registration failed; retaining file-based skills: ${mcpReg.codexError || 'unknown error'}`);
|
|
1162
|
+
}
|
|
1163
|
+
if (mcpReg.updates.length > 0) {
|
|
1164
|
+
ok(`[2.5] MCP — registered rdc-skills endpoint: ${mcpReg.updates.join(', ')}`);
|
|
1165
|
+
} else {
|
|
1166
|
+
info('[2.5] MCP — rdc-skills endpoint verified (claude + codex)');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 2.6. Codex migration: MCP is authoritative; purge legacy file copies.
|
|
1055
1170
|
const codexTargets = findCodexTargets();
|
|
1056
1171
|
if (codexTargets.length > 0) {
|
|
1057
1172
|
let copiedTotal = 0;
|
|
@@ -1060,20 +1175,11 @@ async function main() {
|
|
|
1060
1175
|
const { removed, copied } = registerCodexTarget(target.targetDir);
|
|
1061
1176
|
copiedTotal += copied;
|
|
1062
1177
|
removedTotal += removed;
|
|
1063
|
-
info(` ${target.label.padEnd(15)}: ${target.targetDir} (${
|
|
1178
|
+
info(` ${target.label.padEnd(15)}: ${target.targetDir} (${removed} stale removed; MCP authoritative)`);
|
|
1064
1179
|
}
|
|
1065
|
-
ok(`[2.
|
|
1066
|
-
} else {
|
|
1067
|
-
info('[2.5] Codex — skipped (no Codex skill dirs found; use --codex-root or --codex-skill-dir)');
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// 2.6. Register the rdc-skills MCP endpoint globally (Claude Code + Codex) so
|
|
1071
|
-
// EVERY agent can reach the skills via MCP, not only where a project .mcp.json exists.
|
|
1072
|
-
const mcpReg = registerMcpEndpoints();
|
|
1073
|
-
if (mcpReg.length > 0) {
|
|
1074
|
-
ok(`[2.6] MCP — registered rdc-skills endpoint: ${mcpReg.join(', ')}`);
|
|
1180
|
+
ok(`[2.6] Codex — ${removedTotal} stale file-based skill(s) removed across ${codexTargets.length} target(s); MCP authoritative`);
|
|
1075
1181
|
} else {
|
|
1076
|
-
info('[2.6]
|
|
1182
|
+
info('[2.6] Codex — skipped (no Codex skill dirs found; use --codex-root or --codex-skill-dir)');
|
|
1077
1183
|
}
|
|
1078
1184
|
|
|
1079
1185
|
// 2.7. Symlinks in regen-root/.claude/skills/ (FS MCP + claude.ai access)
|
|
@@ -1209,6 +1315,9 @@ async function main() {
|
|
|
1209
1315
|
console.log(' \x1b[36mMCP connector:\x1b[0m');
|
|
1210
1316
|
reportMcpConnector();
|
|
1211
1317
|
|
|
1318
|
+
// 7.5 Live cross-surface proof — advisory, never blocks install exit code
|
|
1319
|
+
runLiveProof();
|
|
1320
|
+
|
|
1212
1321
|
// Done
|
|
1213
1322
|
console.log('');
|
|
1214
1323
|
console.log(' \x1b[32mDone!\x1b[0m');
|
|
@@ -1238,7 +1347,7 @@ async function main() {
|
|
|
1238
1347
|
// scripts/probe-installed-hooks.mjs verify the real install path instead of
|
|
1239
1348
|
// re-implementing it — a probe that copies its own way proves nothing about what
|
|
1240
1349
|
// ships. Without this guard, requiring the module would run a full install.
|
|
1241
|
-
module.exports = { copyHookFiles, assertHooksLoadable, RDC_ENV_HOOK_TIMEOUT_SEC };
|
|
1350
|
+
module.exports = { copyHookFiles, assertHooksLoadable, registerCodexTarget, RDC_ENV_HOOK_TIMEOUT_SEC };
|
|
1242
1351
|
|
|
1243
1352
|
if (require.main === module) {
|
|
1244
1353
|
main().catch(e => { fail(e.message); process.exit(1); });
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-live-install.mjs — proof, not hope.
|
|
4
|
+
*
|
|
5
|
+
* install-rdc-skills.js's "post-install verification" step only checks LOCAL
|
|
6
|
+
* structural counts (cache dir count, installed_plugins entry count, orphan
|
|
7
|
+
* cleanup) — it never asks any of the four surfaces a Claude/Codex session
|
|
8
|
+
* actually reads from whether they are serving the CURRENT catalog. On
|
|
9
|
+
* 2026-08-16 that gap was live: the production MCP endpoint answered
|
|
10
|
+
* `{"skills":0,"version":"0.0.0"}` while origin/master carried 37 skills, and
|
|
11
|
+
* nothing had ever noticed. This script is the missing check: it names a
|
|
12
|
+
* SOURCE OF TRUTH (origin/master, fetched fresh — never local disk, which is
|
|
13
|
+
* exactly what was stale) and asks each live surface to prove it matches.
|
|
14
|
+
*
|
|
15
|
+
* Surfaces checked (all four the installer registers):
|
|
16
|
+
* 1. Claude CLI — ~/.claude/plugins/installed_plugins.json + cache/latest/skills/
|
|
17
|
+
* 2. Codex — every skill dir the installer's findCodexTargets() would touch
|
|
18
|
+
* 3. Claude MCP — live GET <mcp-url>/health (shared endpoint, also what claude.ai uses)
|
|
19
|
+
* 4. Codex MCP — ~/.codex/config.toml points at the SAME endpoint checked in (3)
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node scripts/verify-live-install.mjs human table, exit 1 on any FAIL
|
|
23
|
+
* node scripts/verify-live-install.mjs --json machine-readable
|
|
24
|
+
* node scripts/verify-live-install.mjs --mcp-url <url> override (default: production)
|
|
25
|
+
* node scripts/verify-live-install.mjs --codex-root <dir> consuming project root
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
import { execSync } from 'node:child_process';
|
|
31
|
+
import https from 'node:https';
|
|
32
|
+
import http from 'node:http';
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const asJson = args.includes('--json');
|
|
36
|
+
const mcpUrlIdx = args.indexOf('--mcp-url');
|
|
37
|
+
const MCP_URL = mcpUrlIdx >= 0 ? args[mcpUrlIdx + 1] : 'https://rdc-skills.regendevcorp.com';
|
|
38
|
+
const codexRootIdx = args.indexOf('--codex-root');
|
|
39
|
+
const CODEX_ROOT = codexRootIdx >= 0 ? path.resolve(args[codexRootIdx + 1]) : null;
|
|
40
|
+
|
|
41
|
+
const repoRoot = path.resolve(new URL('.', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'), '..');
|
|
42
|
+
const claudeHome = path.join(os.homedir(), '.claude');
|
|
43
|
+
const PLUGIN_KEY = 'rdc-skills@rdc-skills';
|
|
44
|
+
|
|
45
|
+
function readJson(p, fallback = {}) {
|
|
46
|
+
if (!fs.existsSync(p)) return fallback;
|
|
47
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFrontmatterName(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
53
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const nm = m[1].match(/^name:\s*(.+)$/m);
|
|
56
|
+
return nm ? nm[1].trim() : null;
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function httpGetJson(url, timeoutMs = 8000) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const lib = url.startsWith('https') ? https : http;
|
|
63
|
+
const req = lib.get(url, { timeout: timeoutMs }, (res) => {
|
|
64
|
+
let body = '';
|
|
65
|
+
res.on('data', (c) => { body += c; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try { resolve({ ok: true, status: res.statusCode, json: JSON.parse(body) }); }
|
|
68
|
+
catch (e) { resolve({ ok: false, error: `bad JSON: ${e.message}`, raw: body.slice(0, 200) }); }
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
|
|
72
|
+
req.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Source of truth — origin/master, FRESHLY FETCHED. Never local disk: a
|
|
77
|
+
// checkout can silently sit behind origin (this is the exact failure the
|
|
78
|
+
// "canonical fallback" repo had this session — 31 commits behind while being
|
|
79
|
+
// treated as the source of truth). ──────────────────────────────────────────
|
|
80
|
+
function sourceOfTruth() {
|
|
81
|
+
execSync('git fetch origin master', { cwd: repoRoot, stdio: 'pipe' });
|
|
82
|
+
const sha = execSync('git rev-parse origin/master', { cwd: repoRoot, encoding: 'utf8' }).trim();
|
|
83
|
+
const pkgRaw = execSync('git show origin/master:package.json', { cwd: repoRoot, encoding: 'utf8' });
|
|
84
|
+
const version = JSON.parse(pkgRaw).version;
|
|
85
|
+
const skillDirs = execSync('git ls-tree -d --name-only origin/master:skills', { cwd: repoRoot, encoding: 'utf8' })
|
|
86
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
87
|
+
// Only count dirs that actually carry a SKILL.md at that ref — matches what
|
|
88
|
+
// the installer/MCP catalog treat as a real skill, not a stray directory.
|
|
89
|
+
let skillCount = 0;
|
|
90
|
+
for (const d of skillDirs) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(`git cat-file -e origin/master:skills/${d}/SKILL.md`, { cwd: repoRoot, stdio: 'pipe' });
|
|
93
|
+
skillCount++;
|
|
94
|
+
} catch { /* not a real skill dir */ }
|
|
95
|
+
}
|
|
96
|
+
return { sha, shortSha: sha.slice(0, 7), version, skillCount };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Surface 1: Claude CLI ────────────────────────────────────────────────────
|
|
100
|
+
function checkClaudeCli(truth) {
|
|
101
|
+
const ipPath = path.join(claudeHome, 'plugins', 'installed_plugins.json');
|
|
102
|
+
const installed = readJson(ipPath, { plugins: {} });
|
|
103
|
+
const entries = installed.plugins[PLUGIN_KEY] || [];
|
|
104
|
+
if (entries.length !== 1) {
|
|
105
|
+
return { surface: 'Claude CLI', pass: false, detail: `expected 1 installed_plugins entry, found ${entries.length}` };
|
|
106
|
+
}
|
|
107
|
+
const entry = entries[0];
|
|
108
|
+
const cacheSkillsDir = entry.installPath ? path.join(entry.installPath, 'skills') : null;
|
|
109
|
+
const cacheCount = cacheSkillsDir && fs.existsSync(cacheSkillsDir)
|
|
110
|
+
? fs.readdirSync(cacheSkillsDir, { withFileTypes: true })
|
|
111
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(cacheSkillsDir, e.name, 'SKILL.md'))).length
|
|
112
|
+
: -1;
|
|
113
|
+
const shaMatch = entry.gitCommitSha === truth.sha;
|
|
114
|
+
const versionMatch = entry.version === truth.version;
|
|
115
|
+
const countMatch = cacheCount === truth.skillCount;
|
|
116
|
+
const pass = shaMatch && versionMatch && countMatch;
|
|
117
|
+
return {
|
|
118
|
+
surface: 'Claude CLI',
|
|
119
|
+
pass,
|
|
120
|
+
detail: pass
|
|
121
|
+
? `v${entry.version} @ ${truth.shortSha}, ${cacheCount} skills — matches origin/master`
|
|
122
|
+
: `v${entry.version || '?'} @ ${(entry.gitCommitSha || '?').slice(0, 7)}, ${cacheCount} skills`
|
|
123
|
+
+ ` — expected v${truth.version} @ ${truth.shortSha}, ${truth.skillCount} skills`
|
|
124
|
+
+ `${shaMatch ? '' : ' [SHA MISMATCH]'}${versionMatch ? '' : ' [VERSION MISMATCH]'}${countMatch ? '' : ' [COUNT MISMATCH]'}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Surface 2: Codex (MCP-only; no stale file-based skill dirs) ─────────────
|
|
129
|
+
function findCodexTargets(codexRoot) {
|
|
130
|
+
const targets = [];
|
|
131
|
+
const add = (label, dir) => {
|
|
132
|
+
if (dir && fs.existsSync(dir) && !targets.some((t) => t.dir.toLowerCase() === dir.toLowerCase())) {
|
|
133
|
+
targets.push({ label, dir });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
if (codexRoot) add('project .agents', path.join(codexRoot, '.agents', 'skills', 'user'));
|
|
137
|
+
add('global .codex', path.join(os.homedir(), '.codex', 'skills'));
|
|
138
|
+
add('global .agents', path.join(os.homedir(), '.agents', 'skills'));
|
|
139
|
+
return targets;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkCodex(truth) {
|
|
143
|
+
const codexRootCandidates = [
|
|
144
|
+
CODEX_ROOT,
|
|
145
|
+
process.env.REGEN_ROOT ? path.resolve(process.env.REGEN_ROOT) : null,
|
|
146
|
+
path.resolve(repoRoot, '..', 'regen-root'),
|
|
147
|
+
process.cwd(),
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
const codexRoot = codexRootCandidates.find((c) => fs.existsSync(path.join(c, '.agents')));
|
|
150
|
+
const targets = findCodexTargets(codexRoot);
|
|
151
|
+
if (targets.length === 0) {
|
|
152
|
+
return { surface: 'Codex', pass: false, detail: 'no Codex skill directories found on this machine — install never ran with a Codex target' };
|
|
153
|
+
}
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const t of targets) {
|
|
156
|
+
const dirs = fs.readdirSync(t.dir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
157
|
+
const rdcDirs = dirs.filter((e) => /^rdc-/.test(e.name) || readFrontmatterName(path.join(t.dir, e.name, 'SKILL.md'))?.startsWith('rdc:'));
|
|
158
|
+
results.push({ label: t.label, dir: t.dir, count: rdcDirs.length });
|
|
159
|
+
}
|
|
160
|
+
const pass = results.every((r) => r.count === 0);
|
|
161
|
+
const detail = results.map((r) =>
|
|
162
|
+
`${r.label}: ${r.count} legacy file-based rdc skill(s)${r.count === 0 ? '' : ' [MCP DUPLICATES PRESENT]'}`
|
|
163
|
+
).join('; ');
|
|
164
|
+
return { surface: 'Codex', pass, detail };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Surface 3: Claude MCP (live) ─────────────────────────────────────────────
|
|
168
|
+
async function checkClaudeMcp(truth) {
|
|
169
|
+
const res = await httpGetJson(`${MCP_URL}/health`);
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
return { surface: 'Claude MCP', pass: false, detail: `${MCP_URL}/health unreachable — ${res.error}` };
|
|
172
|
+
}
|
|
173
|
+
const { skills, git_sha, version, status } = res.json;
|
|
174
|
+
const shaMatch = git_sha === truth.sha;
|
|
175
|
+
const countMatch = skills === truth.skillCount;
|
|
176
|
+
const pass = status === 'ok' && shaMatch && countMatch;
|
|
177
|
+
return {
|
|
178
|
+
surface: 'Claude MCP',
|
|
179
|
+
pass,
|
|
180
|
+
detail: pass
|
|
181
|
+
? `${MCP_URL}/health — v${version}, ${skills} skills @ ${truth.shortSha} — matches origin/master`
|
|
182
|
+
: `${MCP_URL}/health — status=${status} v${version} skills=${skills} sha=${(git_sha || '?').slice(0, 7)}`
|
|
183
|
+
+ ` — expected ${truth.skillCount} skills @ ${truth.shortSha}`
|
|
184
|
+
+ `${shaMatch ? '' : ' [SHA MISMATCH]'}${countMatch ? '' : ' [COUNT MISMATCH]'}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Surface 4: Codex MCP config points at the same live endpoint ───────────
|
|
189
|
+
function checkCodexMcpConfig() {
|
|
190
|
+
const codexToml = path.join(os.homedir(), '.codex', 'config.toml');
|
|
191
|
+
if (!fs.existsSync(codexToml)) {
|
|
192
|
+
return { surface: 'Codex MCP', pass: false, detail: `${codexToml} does not exist` };
|
|
193
|
+
}
|
|
194
|
+
const toml = fs.readFileSync(codexToml, 'utf8').replace(/\r\n/g, '\n');
|
|
195
|
+
const blockRe = /\[mcp_servers\.rdc-skills\]\n([\s\S]*?)(?=\n\[|\s*$)/;
|
|
196
|
+
const m = toml.match(blockRe);
|
|
197
|
+
if (!m) return { surface: 'Codex MCP', pass: false, detail: 'no [mcp_servers.rdc-skills] block in config.toml' };
|
|
198
|
+
const urlM = m[1].match(/^url\s*=\s*'([^']+)'/m);
|
|
199
|
+
const configuredUrl = urlM ? urlM[1] : null;
|
|
200
|
+
const pass = configuredUrl === `${MCP_URL}/mcp`;
|
|
201
|
+
return {
|
|
202
|
+
surface: 'Codex MCP',
|
|
203
|
+
pass,
|
|
204
|
+
detail: pass
|
|
205
|
+
? `config.toml points at ${configuredUrl} — same live endpoint verified above`
|
|
206
|
+
: `config.toml url=${configuredUrl || 'MISSING'} — expected ${MCP_URL}/mcp`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function main() {
|
|
211
|
+
const truth = sourceOfTruth();
|
|
212
|
+
const rows = [];
|
|
213
|
+
rows.push(checkClaudeCli(truth));
|
|
214
|
+
rows.push(checkCodex(truth));
|
|
215
|
+
rows.push(await checkClaudeMcp(truth));
|
|
216
|
+
rows.push(checkCodexMcpConfig());
|
|
217
|
+
|
|
218
|
+
const allPass = rows.every((r) => r.pass);
|
|
219
|
+
|
|
220
|
+
if (asJson) {
|
|
221
|
+
console.log(JSON.stringify({ truth, rows, allPass }, null, 2));
|
|
222
|
+
} else {
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(` Source of truth: origin/master @ ${truth.shortSha}, v${truth.version}, ${truth.skillCount} skills`);
|
|
225
|
+
console.log('');
|
|
226
|
+
for (const r of rows) {
|
|
227
|
+
const mark = r.pass ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
228
|
+
console.log(` ${mark} ${r.surface.padEnd(12)} ${r.detail}`);
|
|
229
|
+
}
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log(allPass
|
|
232
|
+
? ' \x1b[32mAll surfaces verified live — proof, not assumption.\x1b[0m'
|
|
233
|
+
: ' \x1b[31mAt least one surface does NOT match origin/master. This is the exact blind spot install-rdc-skills.js does not check.\x1b[0m');
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
process.exit(allPass ? 0 : 1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
main().catch((e) => { console.error(e.stack || e.message); process.exit(2); });
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import {
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
5
7
|
import { dirname, join, resolve } from 'node:path';
|
|
6
8
|
import { fileURLToPath } from 'node:url';
|
|
7
9
|
|
|
8
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const REPO_ROOT = resolve(__dirname, '..');
|
|
10
12
|
const script = join(REPO_ROOT, 'scripts', 'install-rdc-skills.js');
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
11
14
|
|
|
12
15
|
const syntax = spawnSync(process.execPath, ['--check', script], { encoding: 'utf8' });
|
|
13
16
|
assert.equal(syntax.status, 0, syntax.stderr);
|
|
@@ -21,6 +24,12 @@ assert.match(toml.stdout, /PASS/);
|
|
|
21
24
|
|
|
22
25
|
const source = readFileSync(script, 'utf8');
|
|
23
26
|
const plugin = JSON.parse(readFileSync(join(REPO_ROOT, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
27
|
+
const packageJson = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
28
|
+
assert.equal(
|
|
29
|
+
plugin.version,
|
|
30
|
+
packageJson.version,
|
|
31
|
+
'plugin manifest version must match the published package version',
|
|
32
|
+
);
|
|
24
33
|
const skillCount = Array.isArray(plugin.skills_meta)
|
|
25
34
|
? plugin.skills_meta.length
|
|
26
35
|
: Object.keys(plugin.skills_meta || {}).length;
|
|
@@ -45,5 +54,36 @@ assert.match(
|
|
|
45
54
|
/no plugin upload needed for MCP/,
|
|
46
55
|
'installer should not imply claude.ai MCP usage requires uploading an artifact',
|
|
47
56
|
);
|
|
57
|
+
assert.ok(
|
|
58
|
+
source.indexOf('const mcpReg = registerMcpEndpoints();') < source.indexOf('const codexTargets = findCodexTargets();'),
|
|
59
|
+
'installer must establish the Codex MCP endpoint before removing file-based skills',
|
|
60
|
+
);
|
|
61
|
+
assert.match(
|
|
62
|
+
source,
|
|
63
|
+
/if \(!mcpReg\.codexReady\)[\s\S]*retaining file-based skills/,
|
|
64
|
+
'installer must fail closed and retain file-based skills when MCP registration fails',
|
|
65
|
+
);
|
|
66
|
+
assert.match(
|
|
67
|
+
source,
|
|
68
|
+
/const mode = fs\.existsSync\(codexToml\)[\s\S]*fs\.chmodSync\(tmp, mode\)[\s\S]*finally[\s\S]*fs\.unlinkSync\(tmp\)/,
|
|
69
|
+
'Codex config replacement must preserve permissions and remove failed temporary copies',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { registerCodexTarget } = require(script);
|
|
73
|
+
const codexSkills = mkdtempSync(join(tmpdir(), 'rdc-codex-skills-'));
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(join(codexSkills, 'rdc-build'), { recursive: true });
|
|
76
|
+
writeFileSync(join(codexSkills, 'rdc-build', 'SKILL.md'), '---\nname: rdc:build\n---\n');
|
|
77
|
+
mkdirSync(join(codexSkills, 'legacy-name'), { recursive: true });
|
|
78
|
+
writeFileSync(join(codexSkills, 'legacy-name', 'SKILL.md'), '---\nname: rdc:plan\n---\n');
|
|
79
|
+
mkdirSync(join(codexSkills, 'keep-me'), { recursive: true });
|
|
80
|
+
writeFileSync(join(codexSkills, 'keep-me', 'SKILL.md'), '---\nname: local:keep\n---\n');
|
|
81
|
+
|
|
82
|
+
const migrated = registerCodexTarget(codexSkills);
|
|
83
|
+
assert.deepEqual(migrated, { removed: 2, copied: 0 });
|
|
84
|
+
assert.equal(readFileSync(join(codexSkills, 'keep-me', 'SKILL.md'), 'utf8').includes('local:keep'), true);
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(codexSkills, { recursive: true, force: true });
|
|
87
|
+
}
|
|
48
88
|
|
|
49
89
|
console.log('install-rdc-skills tests — PASS');
|
|
@@ -125,29 +125,45 @@ function runHook(payload, extraEnv = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
|
-
// 4. PreToolUse
|
|
129
|
-
//
|
|
130
|
-
//
|
|
128
|
+
// 4. PreToolUse — flow-gated (2026-08-16): BLOCKS when the current flow
|
|
129
|
+
// requires a work item, exempt SILENTLY when it doesn't. Uses a fresh temp
|
|
130
|
+
// dir as HOME/USERPROFILE so no fixit.marker is visible regardless of real
|
|
131
|
+
// machine state, and real RDC_FLOW_STATE/RDC_MODE_STATE temp files so the
|
|
132
|
+
// test exercises the ACTUAL cross-repo dynamic-import path against
|
|
133
|
+
// whatever $LIFEAI_ENV resolves to on this box — not a mock.
|
|
131
134
|
// ---------------------------------------------------------------------------
|
|
132
135
|
{
|
|
133
136
|
const fakeHome = mkdtempSync(join(tmpdir(), 'wic-home-'));
|
|
137
|
+
const flowState = join(fakeHome, 'flow.json');
|
|
138
|
+
const modeState = join(fakeHome, 'mode.json');
|
|
134
139
|
try {
|
|
135
|
-
const preEnv = { HOME: fakeHome, USERPROFILE: fakeHome };
|
|
140
|
+
const preEnv = { HOME: fakeHome, USERPROFILE: fakeHome, RDC_FLOW_STATE: flowState, RDC_MODE_STATE: modeState };
|
|
136
141
|
|
|
137
|
-
|
|
142
|
+
// No flow declared -> fail-closed default -> requires a work item -> BLOCK.
|
|
143
|
+
const blocked = runHook({
|
|
138
144
|
hook_event_name: 'PreToolUse',
|
|
139
145
|
tool_name: 'Bash',
|
|
140
146
|
tool_input: { command: 'git commit -m "no convention and no uuid"' },
|
|
141
147
|
}, preEnv);
|
|
142
|
-
assert('pre:
|
|
143
|
-
|
|
148
|
+
assert('pre: no-flow-declared BLOCKS (was: warn, never blocked)',
|
|
149
|
+
JSON.parse(blocked.stdout || '{}').decision === 'block', blocked.stdout || blocked.stderr);
|
|
144
150
|
|
|
151
|
+
// Conventional format still passes even when a work item IS required.
|
|
145
152
|
const ok = runHook({
|
|
146
153
|
hook_event_name: 'PreToolUse',
|
|
147
154
|
tool_name: 'Bash',
|
|
148
155
|
tool_input: { command: 'git commit -m "feat(x): conventional"' },
|
|
149
156
|
}, preEnv);
|
|
150
|
-
assert('pre: conventional passes silently', ok.status === 0 && ok.stdout.trim() === '', ok.stdout);
|
|
157
|
+
assert('pre: conventional passes silently even when required', ok.status === 0 && ok.stdout.trim() === '', ok.stdout);
|
|
158
|
+
|
|
159
|
+
// A conversational flow (plan) exempts — silently, no message at all.
|
|
160
|
+
writeFileSync(flowState, JSON.stringify({ flow: 'plan', setBy: 'test', setAt: new Date().toISOString() }));
|
|
161
|
+
const exempt = runHook({
|
|
162
|
+
hook_event_name: 'PreToolUse',
|
|
163
|
+
tool_name: 'Bash',
|
|
164
|
+
tool_input: { command: 'git commit -m "no convention and no uuid"' },
|
|
165
|
+
}, preEnv);
|
|
166
|
+
assert('pre: plan flow exempts silently', exempt.status === 0 && exempt.stdout.trim() === '', exempt.stdout);
|
|
151
167
|
} finally {
|
|
152
168
|
rmSync(fakeHome, { recursive: true, force: true });
|
|
153
169
|
}
|