@lifeaitools/rdc-skills 0.24.38 → 0.24.39

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.
Files changed (191) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1371 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +377 -377
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +124 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +153 -153
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +56 -56
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +464 -464
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +424 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +533 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +140 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +165 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -200
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +189 -189
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/overnight/SKILL.md +251 -251
  131. package/skills/plan/SKILL.md +345 -345
  132. package/skills/preplan/SKILL.md +90 -90
  133. package/skills/prototype/SKILL.md +150 -150
  134. package/skills/rdc-brochurify/SKILL.md +245 -245
  135. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  136. package/skills/release/SKILL.md +140 -140
  137. package/skills/report/SKILL.md +100 -100
  138. package/skills/review/SKILL.md +152 -152
  139. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  140. package/skills/rpms-filemap/SKILL.md +111 -111
  141. package/skills/self-test/SKILL.md +132 -132
  142. package/skills/status/SKILL.md +99 -99
  143. package/skills/terminal-config/SKILL.md +62 -62
  144. package/skills/tests/MATRIX.md +54 -54
  145. package/skills/tests/README.md +47 -47
  146. package/skills/tests/rdc-brochure.test.json +34 -34
  147. package/skills/tests/rdc-build.test.json +36 -36
  148. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  149. package/skills/tests/rdc-co-develop.test.json +29 -29
  150. package/skills/tests/rdc-collab.test.json +29 -29
  151. package/skills/tests/rdc-convert.test.json +35 -35
  152. package/skills/tests/rdc-deploy.test.json +30 -30
  153. package/skills/tests/rdc-design.test.json +27 -27
  154. package/skills/tests/rdc-edit.test.json +29 -29
  155. package/skills/tests/rdc-fixit.test.json +36 -36
  156. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  157. package/skills/tests/rdc-handoff.test.json +28 -28
  158. package/skills/tests/rdc-help.test.json +29 -29
  159. package/skills/tests/rdc-housekeeping.test.json +31 -31
  160. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  161. package/skills/tests/rdc-overnight.test.json +37 -37
  162. package/skills/tests/rdc-plan.test.json +27 -27
  163. package/skills/tests/rdc-preplan.test.json +31 -31
  164. package/skills/tests/rdc-prototype.test.json +28 -28
  165. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  166. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  167. package/skills/tests/rdc-release.test.json +29 -29
  168. package/skills/tests/rdc-report.test.json +28 -28
  169. package/skills/tests/rdc-review.test.json +29 -29
  170. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  171. package/skills/tests/rdc-self-test.test.json +24 -24
  172. package/skills/tests/rdc-status.test.json +29 -29
  173. package/skills/tests/rdc-terminal-config.test.json +29 -29
  174. package/skills/tests/rdc-watch.test.json +24 -24
  175. package/skills/tests/rdc-workitems.test.json +27 -27
  176. package/skills/watch/SKILL.md +97 -97
  177. package/skills/workitems/SKILL.md +151 -151
  178. package/tests/acceptance.test.mjs +59 -59
  179. package/tests/channel-formatter.contract.test.mjs +251 -251
  180. package/tests/curl-surface.test.mjs +289 -289
  181. package/tests/harness-gates.test.mjs +325 -325
  182. package/tests/help-surface.test.mjs +61 -61
  183. package/tests/install-rdc-skills.test.mjs +49 -49
  184. package/tests/manifest-contract-fields.test.mjs +78 -78
  185. package/tests/mcp.test.mjs +271 -271
  186. package/tests/require-work-item-on-commit.test.mjs +162 -162
  187. package/tests/run-evidence-gate.test.mjs +82 -82
  188. package/tests/skill-test-matrix.test.mjs +66 -66
  189. package/tests/validate-skills.js +27 -27
  190. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  191. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
@@ -1,294 +1,294 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Work-item commit hook — Truth Gate 3.0 Layer 1 (commit capture).
4
- *
5
- * Registered on BOTH:
6
- * - PreToolUse(Bash): intent check (preserved legacy behavior — WARN only,
7
- * never hard-blocks a commit).
8
- * - PostToolUse(Bash): commit capture — after a `git commit` lands, the hook
9
- * runs `git rev-parse HEAD` and records the REAL SHA +
10
- * session against the active work item in the
11
- * `work_item_commits` side table.
12
- *
13
- * ⛔ THE AGENT NEVER WRITES THE COMMIT FIELD. The system does. The work item is
14
- * identified from the work-item UUID the agent referenced in the commit message
15
- * (its "active work-item ref"); the SHA is taken from `git rev-parse HEAD`, not
16
- * from anything the agent typed. This kills the "real-but-wrong commit SHA"
17
- * fabrication class (FMEA #1) at the source — WP-2's closure gate then asserts a
18
- * report's codeflow_post.commit is one of these captured, session-authored SHAs.
19
- *
20
- * PreToolUse (intent) behavior is unchanged:
21
- * - fixit.marker present -> pass
22
- * - conventional type / UUID / #issue ref -> pass
23
- * - otherwise -> WARN (proceed anyway)
24
- *
25
- * PostToolUse (capture) behavior:
26
- * - only fires on a `git commit` whose tool result indicates success
27
- * - extracts the work-item UUID from the commit message (active ref)
28
- * - no UUID -> NO-OP (no orphan row), logs `capture-no-item`
29
- * - UUID -> `git rev-parse HEAD` -> INSERT { work_item_id, sha, session_id }
30
- *
31
- * Capture sink (for tests / offline): if `RDC_COMMIT_CAPTURE_SINK` is set to a
32
- * file path, the capture payload is appended there as JSONL in addition to the
33
- * DB write. This lets the hook be verified deterministically without a live DB.
34
- */
35
- 'use strict';
36
-
37
- const fs = require('fs');
38
- const path = require('path');
39
- const os = require('os');
40
- const { execFileSync } = require('child_process');
41
- const hookLog = require('./hook-logger');
42
-
43
- const MARKER_FILE = path.join(
44
- process.env.USERPROFILE || process.env.HOME || os.homedir(),
45
- '.claude',
46
- 'fixit.marker'
47
- );
48
-
49
- const CONVENTIONAL_TYPES = /^(feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert)(\(.+\))?:/i;
50
- const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
51
- const ISSUE_REF = /#[a-zA-Z0-9-]+/;
52
- const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
53
-
54
- const DEFAULT_SUPABASE_URL = 'https://uvojezuorjgqzmhhgluu.supabase.co';
55
-
56
- // ---------------------------------------------------------------------------
57
- // Pure helpers (exported for unit tests)
58
- // ---------------------------------------------------------------------------
59
-
60
- /** True when the Bash command is a real `git commit` (not e.g. `git commit --help`). */
61
- function isGitCommit(command) {
62
- if (typeof command !== 'string') return false;
63
- if (!/\bgit\b[^\n]*\bcommit\b/.test(command)) return false;
64
- if (/\bcommit\b[^\n]*--help/.test(command) || /\bcommit\b[^\n]*\s-h\b/.test(command)) return false;
65
- return true;
66
- }
67
-
68
- /** Extract the -m "..." message (or fall back to the whole command). */
69
- function extractCommitMessage(command) {
70
- const cmd = String(command || '');
71
- const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/s) ||
72
- cmd.match(/-m\s+"([\s\S]+?)"\s*(?:&&|$)/);
73
- return msgMatch ? msgMatch[1] : cmd;
74
- }
75
-
76
- /**
77
- * Find the work-item UUID the agent referenced in the commit message — the
78
- * "active work-item ref". Returns the first UUID, or null when none is present.
79
- * A null result means "no active work item" -> capture must NO-OP.
80
- */
81
- function parseCommitMessageWorkItem(message) {
82
- const m = String(message || '').match(UUID_PATTERN);
83
- return m ? m[0].toLowerCase() : null;
84
- }
85
-
86
- /**
87
- * Did the committed tool call actually succeed? PostToolUse provides the tool
88
- * result. A failed commit (nothing to commit / hook reject / non-zero exit)
89
- * must NOT capture a row. We treat it as success unless we have positive
90
- * evidence of failure, OR there is positive evidence of success in stdout.
91
- */
92
- function commitSucceeded(toolResult) {
93
- if (!toolResult || typeof toolResult !== 'object') return true; // no signal -> trust HEAD check below
94
- const code = toolResult.exit_code ?? toolResult.exitCode ?? toolResult.code;
95
- if (typeof code === 'number') return code === 0;
96
- const out = `${toolResult.stdout || ''}\n${toolResult.stderr || ''}\n${toolResult.output || ''}`;
97
- if (/nothing to commit|no changes added|did not match any files|commit failed|error:/i.test(out)) return false;
98
- return true;
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Capture side effects
103
- // ---------------------------------------------------------------------------
104
-
105
- /** Real `git rev-parse HEAD` in the given cwd. Returns the SHA, or null on error. */
106
- function gitHead(cwd) {
107
- try {
108
- const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
109
- cwd: cwd || process.cwd(),
110
- encoding: 'utf8',
111
- stdio: ['ignore', 'pipe', 'ignore'],
112
- }).trim();
113
- return SHA_PATTERN.test(sha) ? sha : null;
114
- } catch {
115
- return null;
116
- }
117
- }
118
-
119
- async function getServiceKey() {
120
- if (process.env.SUPABASE_SERVICE_ROLE_KEY) return process.env.SUPABASE_SERVICE_ROLE_KEY;
121
- if (process.env.SUPABASE_SERVICE_KEY) return process.env.SUPABASE_SERVICE_KEY;
122
- for (const endpoint of ['supabase-service', 'supabase-service-role']) {
123
- try {
124
- const res = await fetch(`http://127.0.0.1:52437/v/${endpoint}`, { signal: AbortSignal.timeout(2000) });
125
- if (res.ok) {
126
- const text = (await res.text()).trim();
127
- if (text && !text.startsWith('{')) return text;
128
- }
129
- } catch (_) {}
130
- }
131
- return null;
132
- }
133
-
134
- /** Append the capture payload to the test/offline sink, if configured. */
135
- function writeSink(payload) {
136
- const sink = process.env.RDC_COMMIT_CAPTURE_SINK;
137
- if (!sink) return;
138
- try {
139
- fs.mkdirSync(path.dirname(sink), { recursive: true });
140
- fs.appendFileSync(sink, JSON.stringify(payload) + '\n');
141
- } catch (_) {}
142
- }
143
-
144
- /**
145
- * INSERT the captured row into work_item_commits via the Supabase REST API
146
- * using the service-role key (bypasses RLS — capture is system-only). The SHA
147
- * is the live `git rev-parse HEAD`, never an agent-supplied value.
148
- */
149
- async function insertCommitRow(payload) {
150
- const key = await getServiceKey();
151
- if (!key) return { ok: false, reason: 'no-service-key' };
152
- const base = (process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL).replace(/\/$/, '');
153
- try {
154
- const res = await fetch(`${base}/rest/v1/work_item_commits`, {
155
- method: 'POST',
156
- headers: {
157
- apikey: key,
158
- Authorization: `Bearer ${key}`,
159
- 'Content-Type': 'application/json',
160
- Prefer: 'return=minimal',
161
- },
162
- body: JSON.stringify({
163
- work_item_id: payload.work_item_id,
164
- sha: payload.sha,
165
- session_id: payload.session_id,
166
- source: 'commit-hook',
167
- }),
168
- signal: AbortSignal.timeout(3500),
169
- });
170
- if (!res.ok) return { ok: false, reason: `http-${res.status}` };
171
- return { ok: true };
172
- } catch (e) {
173
- return { ok: false, reason: e.message };
174
- }
175
- }
176
-
177
- /**
178
- * Capture orchestration for a single PostToolUse(git commit) event.
179
- * Returns a result object (also used by tests). NEVER throws.
180
- */
181
- async function captureCommit(raw) {
182
- const command = raw?.tool_input?.command || '';
183
- if (!isGitCommit(command)) return { captured: false, reason: 'not-git-commit' };
184
-
185
- const toolResult = raw.tool_response || raw.tool_result || raw.result || null;
186
- if (!commitSucceeded(toolResult)) {
187
- hookLog('require-work-item', 'PostToolUse', 'capture-skip-failed-commit', {});
188
- return { captured: false, reason: 'commit-not-successful' };
189
- }
190
-
191
- const message = extractCommitMessage(command);
192
- const workItemId = parseCommitMessageWorkItem(message);
193
- if (!workItemId) {
194
- // No active work item -> NO-OP. No orphan row is ever written.
195
- hookLog('require-work-item', 'PostToolUse', 'capture-no-item', { msg: String(message).slice(0, 80) });
196
- return { captured: false, reason: 'no-active-work-item' };
197
- }
198
-
199
- const cwd = raw.cwd || process.cwd();
200
- const sha = gitHead(cwd);
201
- if (!sha) {
202
- hookLog('require-work-item', 'PostToolUse', 'capture-no-head', { work_item_id: workItemId });
203
- return { captured: false, reason: 'no-head-sha', work_item_id: workItemId };
204
- }
205
-
206
- const payload = {
207
- work_item_id: workItemId,
208
- sha, // <-- system-derived HEAD, never agent text
209
- session_id: raw.session_id || null,
210
- };
211
-
212
- writeSink(payload);
213
- const insert = await insertCommitRow(payload);
214
- hookLog('require-work-item', 'PostToolUse', insert.ok ? 'capture-recorded' : 'capture-deferred', {
215
- work_item_id: workItemId,
216
- sha,
217
- insert_reason: insert.reason || null,
218
- });
219
- return { captured: true, sha, work_item_id: workItemId, db: insert };
220
- }
221
-
222
- // ---------------------------------------------------------------------------
223
- // PreToolUse intent check (unchanged legacy behavior)
224
- // ---------------------------------------------------------------------------
225
-
226
- function preToolUse(raw) {
227
- if (raw.tool_name !== 'Bash') return process.exit(0);
228
- const command = raw.tool_input?.command || '';
229
- if (!command.includes('git commit')) return process.exit(0);
230
-
231
- if (fs.existsSync(MARKER_FILE)) {
232
- hookLog('require-work-item', 'PreToolUse', 'pass-fixit', {});
233
- return process.exit(0);
234
- }
235
-
236
- const msg = extractCommitMessage(command);
237
- if (CONVENTIONAL_TYPES.test(msg.trim()) || UUID_PATTERN.test(msg) || ISSUE_REF.test(msg)) {
238
- hookLog('require-work-item', 'PreToolUse', 'pass', { msg: msg.slice(0, 80) });
239
- return process.exit(0);
240
- }
241
-
242
- hookLog('require-work-item', 'PreToolUse', 'warn', { msg: msg.slice(0, 80) });
243
- // Warn only — never hard-block commits. Conventional commit format is
244
- // sufficient self-documentation.
245
- process.stdout.write(JSON.stringify({
246
- systemMessage: `⚠️ Commit has no work item reference or conventional commit type.\n` +
247
- `Preferred format: fix(<scope>): <message> — proceeding anyway.`,
248
- }));
249
- return process.exit(0);
250
- }
251
-
252
- // ---------------------------------------------------------------------------
253
- // Entry point — event-aware
254
- // ---------------------------------------------------------------------------
255
-
256
- async function main() {
257
- let input = '';
258
- await new Promise((resolve) => {
259
- process.stdin.resume();
260
- process.stdin.setEncoding('utf8');
261
- process.stdin.on('data', (chunk) => { input += chunk; });
262
- process.stdin.on('end', resolve);
263
- });
264
-
265
- let raw;
266
- try { raw = JSON.parse(input); } catch { return process.exit(0); }
267
-
268
- const event = raw.hook_event_name || raw.hookEventName || 'PreToolUse';
269
-
270
- if (event === 'PostToolUse') {
271
- if (raw.tool_name && raw.tool_name !== 'Bash') return process.exit(0);
272
- try { await captureCommit(raw); } catch (e) {
273
- hookLog('require-work-item', 'PostToolUse', 'capture-error', { error: e.message });
274
- }
275
- return process.exit(0); // capture is observe-only; never blocks the loop
276
- }
277
-
278
- // Default / PreToolUse intent path.
279
- preToolUse(raw);
280
- }
281
-
282
- // Run when executed as a hook; export pure pieces when required by a test.
283
- if (require.main === module) {
284
- main();
285
- } else {
286
- module.exports = {
287
- isGitCommit,
288
- extractCommitMessage,
289
- parseCommitMessageWorkItem,
290
- commitSucceeded,
291
- gitHead,
292
- captureCommit,
293
- };
294
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Work-item commit hook — Truth Gate 3.0 Layer 1 (commit capture).
4
+ *
5
+ * Registered on BOTH:
6
+ * - PreToolUse(Bash): intent check (preserved legacy behavior — WARN only,
7
+ * never hard-blocks a commit).
8
+ * - PostToolUse(Bash): commit capture — after a `git commit` lands, the hook
9
+ * runs `git rev-parse HEAD` and records the REAL SHA +
10
+ * session against the active work item in the
11
+ * `work_item_commits` side table.
12
+ *
13
+ * ⛔ THE AGENT NEVER WRITES THE COMMIT FIELD. The system does. The work item is
14
+ * identified from the work-item UUID the agent referenced in the commit message
15
+ * (its "active work-item ref"); the SHA is taken from `git rev-parse HEAD`, not
16
+ * from anything the agent typed. This kills the "real-but-wrong commit SHA"
17
+ * fabrication class (FMEA #1) at the source — WP-2's closure gate then asserts a
18
+ * report's codeflow_post.commit is one of these captured, session-authored SHAs.
19
+ *
20
+ * PreToolUse (intent) behavior is unchanged:
21
+ * - fixit.marker present -> pass
22
+ * - conventional type / UUID / #issue ref -> pass
23
+ * - otherwise -> WARN (proceed anyway)
24
+ *
25
+ * PostToolUse (capture) behavior:
26
+ * - only fires on a `git commit` whose tool result indicates success
27
+ * - extracts the work-item UUID from the commit message (active ref)
28
+ * - no UUID -> NO-OP (no orphan row), logs `capture-no-item`
29
+ * - UUID -> `git rev-parse HEAD` -> INSERT { work_item_id, sha, session_id }
30
+ *
31
+ * Capture sink (for tests / offline): if `RDC_COMMIT_CAPTURE_SINK` is set to a
32
+ * file path, the capture payload is appended there as JSONL in addition to the
33
+ * DB write. This lets the hook be verified deterministically without a live DB.
34
+ */
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const os = require('os');
40
+ const { execFileSync } = require('child_process');
41
+ const hookLog = require('./hook-logger');
42
+
43
+ const MARKER_FILE = path.join(
44
+ process.env.USERPROFILE || process.env.HOME || os.homedir(),
45
+ '.claude',
46
+ 'fixit.marker'
47
+ );
48
+
49
+ const CONVENTIONAL_TYPES = /^(feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert)(\(.+\))?:/i;
50
+ const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
51
+ const ISSUE_REF = /#[a-zA-Z0-9-]+/;
52
+ const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
53
+
54
+ const DEFAULT_SUPABASE_URL = 'https://uvojezuorjgqzmhhgluu.supabase.co';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Pure helpers (exported for unit tests)
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /** True when the Bash command is a real `git commit` (not e.g. `git commit --help`). */
61
+ function isGitCommit(command) {
62
+ if (typeof command !== 'string') return false;
63
+ if (!/\bgit\b[^\n]*\bcommit\b/.test(command)) return false;
64
+ if (/\bcommit\b[^\n]*--help/.test(command) || /\bcommit\b[^\n]*\s-h\b/.test(command)) return false;
65
+ return true;
66
+ }
67
+
68
+ /** Extract the -m "..." message (or fall back to the whole command). */
69
+ function extractCommitMessage(command) {
70
+ const cmd = String(command || '');
71
+ const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/s) ||
72
+ cmd.match(/-m\s+"([\s\S]+?)"\s*(?:&&|$)/);
73
+ return msgMatch ? msgMatch[1] : cmd;
74
+ }
75
+
76
+ /**
77
+ * Find the work-item UUID the agent referenced in the commit message — the
78
+ * "active work-item ref". Returns the first UUID, or null when none is present.
79
+ * A null result means "no active work item" -> capture must NO-OP.
80
+ */
81
+ function parseCommitMessageWorkItem(message) {
82
+ const m = String(message || '').match(UUID_PATTERN);
83
+ return m ? m[0].toLowerCase() : null;
84
+ }
85
+
86
+ /**
87
+ * Did the committed tool call actually succeed? PostToolUse provides the tool
88
+ * result. A failed commit (nothing to commit / hook reject / non-zero exit)
89
+ * must NOT capture a row. We treat it as success unless we have positive
90
+ * evidence of failure, OR there is positive evidence of success in stdout.
91
+ */
92
+ function commitSucceeded(toolResult) {
93
+ if (!toolResult || typeof toolResult !== 'object') return true; // no signal -> trust HEAD check below
94
+ const code = toolResult.exit_code ?? toolResult.exitCode ?? toolResult.code;
95
+ if (typeof code === 'number') return code === 0;
96
+ const out = `${toolResult.stdout || ''}\n${toolResult.stderr || ''}\n${toolResult.output || ''}`;
97
+ if (/nothing to commit|no changes added|did not match any files|commit failed|error:/i.test(out)) return false;
98
+ return true;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Capture side effects
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /** Real `git rev-parse HEAD` in the given cwd. Returns the SHA, or null on error. */
106
+ function gitHead(cwd) {
107
+ try {
108
+ const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
109
+ cwd: cwd || process.cwd(),
110
+ encoding: 'utf8',
111
+ stdio: ['ignore', 'pipe', 'ignore'],
112
+ }).trim();
113
+ return SHA_PATTERN.test(sha) ? sha : null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ async function getServiceKey() {
120
+ if (process.env.SUPABASE_SERVICE_ROLE_KEY) return process.env.SUPABASE_SERVICE_ROLE_KEY;
121
+ if (process.env.SUPABASE_SERVICE_KEY) return process.env.SUPABASE_SERVICE_KEY;
122
+ for (const endpoint of ['supabase-service', 'supabase-service-role']) {
123
+ try {
124
+ const res = await fetch(`http://127.0.0.1:52437/v/${endpoint}`, { signal: AbortSignal.timeout(2000) });
125
+ if (res.ok) {
126
+ const text = (await res.text()).trim();
127
+ if (text && !text.startsWith('{')) return text;
128
+ }
129
+ } catch (_) {}
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /** Append the capture payload to the test/offline sink, if configured. */
135
+ function writeSink(payload) {
136
+ const sink = process.env.RDC_COMMIT_CAPTURE_SINK;
137
+ if (!sink) return;
138
+ try {
139
+ fs.mkdirSync(path.dirname(sink), { recursive: true });
140
+ fs.appendFileSync(sink, JSON.stringify(payload) + '\n');
141
+ } catch (_) {}
142
+ }
143
+
144
+ /**
145
+ * INSERT the captured row into work_item_commits via the Supabase REST API
146
+ * using the service-role key (bypasses RLS — capture is system-only). The SHA
147
+ * is the live `git rev-parse HEAD`, never an agent-supplied value.
148
+ */
149
+ async function insertCommitRow(payload) {
150
+ const key = await getServiceKey();
151
+ if (!key) return { ok: false, reason: 'no-service-key' };
152
+ const base = (process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL).replace(/\/$/, '');
153
+ try {
154
+ const res = await fetch(`${base}/rest/v1/work_item_commits`, {
155
+ method: 'POST',
156
+ headers: {
157
+ apikey: key,
158
+ Authorization: `Bearer ${key}`,
159
+ 'Content-Type': 'application/json',
160
+ Prefer: 'return=minimal',
161
+ },
162
+ body: JSON.stringify({
163
+ work_item_id: payload.work_item_id,
164
+ sha: payload.sha,
165
+ session_id: payload.session_id,
166
+ source: 'commit-hook',
167
+ }),
168
+ signal: AbortSignal.timeout(3500),
169
+ });
170
+ if (!res.ok) return { ok: false, reason: `http-${res.status}` };
171
+ return { ok: true };
172
+ } catch (e) {
173
+ return { ok: false, reason: e.message };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Capture orchestration for a single PostToolUse(git commit) event.
179
+ * Returns a result object (also used by tests). NEVER throws.
180
+ */
181
+ async function captureCommit(raw) {
182
+ const command = raw?.tool_input?.command || '';
183
+ if (!isGitCommit(command)) return { captured: false, reason: 'not-git-commit' };
184
+
185
+ const toolResult = raw.tool_response || raw.tool_result || raw.result || null;
186
+ if (!commitSucceeded(toolResult)) {
187
+ hookLog('require-work-item', 'PostToolUse', 'capture-skip-failed-commit', {});
188
+ return { captured: false, reason: 'commit-not-successful' };
189
+ }
190
+
191
+ const message = extractCommitMessage(command);
192
+ const workItemId = parseCommitMessageWorkItem(message);
193
+ if (!workItemId) {
194
+ // No active work item -> NO-OP. No orphan row is ever written.
195
+ hookLog('require-work-item', 'PostToolUse', 'capture-no-item', { msg: String(message).slice(0, 80) });
196
+ return { captured: false, reason: 'no-active-work-item' };
197
+ }
198
+
199
+ const cwd = raw.cwd || process.cwd();
200
+ const sha = gitHead(cwd);
201
+ if (!sha) {
202
+ hookLog('require-work-item', 'PostToolUse', 'capture-no-head', { work_item_id: workItemId });
203
+ return { captured: false, reason: 'no-head-sha', work_item_id: workItemId };
204
+ }
205
+
206
+ const payload = {
207
+ work_item_id: workItemId,
208
+ sha, // <-- system-derived HEAD, never agent text
209
+ session_id: raw.session_id || null,
210
+ };
211
+
212
+ writeSink(payload);
213
+ const insert = await insertCommitRow(payload);
214
+ hookLog('require-work-item', 'PostToolUse', insert.ok ? 'capture-recorded' : 'capture-deferred', {
215
+ work_item_id: workItemId,
216
+ sha,
217
+ insert_reason: insert.reason || null,
218
+ });
219
+ return { captured: true, sha, work_item_id: workItemId, db: insert };
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // PreToolUse intent check (unchanged legacy behavior)
224
+ // ---------------------------------------------------------------------------
225
+
226
+ function preToolUse(raw) {
227
+ if (raw.tool_name !== 'Bash') return process.exit(0);
228
+ const command = raw.tool_input?.command || '';
229
+ if (!command.includes('git commit')) return process.exit(0);
230
+
231
+ if (fs.existsSync(MARKER_FILE)) {
232
+ hookLog('require-work-item', 'PreToolUse', 'pass-fixit', {});
233
+ return process.exit(0);
234
+ }
235
+
236
+ const msg = extractCommitMessage(command);
237
+ if (CONVENTIONAL_TYPES.test(msg.trim()) || UUID_PATTERN.test(msg) || ISSUE_REF.test(msg)) {
238
+ hookLog('require-work-item', 'PreToolUse', 'pass', { msg: msg.slice(0, 80) });
239
+ return process.exit(0);
240
+ }
241
+
242
+ hookLog('require-work-item', 'PreToolUse', 'warn', { msg: msg.slice(0, 80) });
243
+ // Warn only — never hard-block commits. Conventional commit format is
244
+ // sufficient self-documentation.
245
+ process.stdout.write(JSON.stringify({
246
+ systemMessage: `⚠️ Commit has no work item reference or conventional commit type.\n` +
247
+ `Preferred format: fix(<scope>): <message> — proceeding anyway.`,
248
+ }));
249
+ return process.exit(0);
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Entry point — event-aware
254
+ // ---------------------------------------------------------------------------
255
+
256
+ async function main() {
257
+ let input = '';
258
+ await new Promise((resolve) => {
259
+ process.stdin.resume();
260
+ process.stdin.setEncoding('utf8');
261
+ process.stdin.on('data', (chunk) => { input += chunk; });
262
+ process.stdin.on('end', resolve);
263
+ });
264
+
265
+ let raw;
266
+ try { raw = JSON.parse(input); } catch { return process.exit(0); }
267
+
268
+ const event = raw.hook_event_name || raw.hookEventName || 'PreToolUse';
269
+
270
+ if (event === 'PostToolUse') {
271
+ if (raw.tool_name && raw.tool_name !== 'Bash') return process.exit(0);
272
+ try { await captureCommit(raw); } catch (e) {
273
+ hookLog('require-work-item', 'PostToolUse', 'capture-error', { error: e.message });
274
+ }
275
+ return process.exit(0); // capture is observe-only; never blocks the loop
276
+ }
277
+
278
+ // Default / PreToolUse intent path.
279
+ preToolUse(raw);
280
+ }
281
+
282
+ // Run when executed as a hook; export pure pieces when required by a test.
283
+ if (require.main === module) {
284
+ main();
285
+ } else {
286
+ module.exports = {
287
+ isGitCommit,
288
+ extractCommitMessage,
289
+ parseCommitMessageWorkItem,
290
+ commitSucceeded,
291
+ gitHead,
292
+ captureCommit,
293
+ };
294
+ }
@@ -1,19 +1,19 @@
1
- #!/usr/bin/env node
2
- // PostCompact hook — write a brief for session restart context.
3
- 'use strict';
4
- const fs = require('fs');
5
- const path = require('path');
6
- const HOME = process.env.USERPROFILE || process.env.HOME;
7
- const BRIEF = path.join(HOME, '.claude', 'restart-brief.md');
8
-
9
- let input = '';
10
- process.stdin.resume();
11
- process.stdin.setEncoding('utf8');
12
- process.stdin.on('data', c => { input += c; });
13
- process.stdin.on('end', () => {
14
- try {
15
- const ts = new Date().toISOString();
16
- fs.writeFileSync(BRIEF, `# Restart Brief\nGenerated: ${ts}\n\nContext compacted. Resume from work items queue.\n`);
17
- } catch (_) {}
18
- process.exit(0);
19
- });
1
+ #!/usr/bin/env node
2
+ // PostCompact hook — write a brief for session restart context.
3
+ 'use strict';
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const HOME = process.env.USERPROFILE || process.env.HOME;
7
+ const BRIEF = path.join(HOME, '.claude', 'restart-brief.md');
8
+
9
+ let input = '';
10
+ process.stdin.resume();
11
+ process.stdin.setEncoding('utf8');
12
+ process.stdin.on('data', c => { input += c; });
13
+ process.stdin.on('end', () => {
14
+ try {
15
+ const ts = new Date().toISOString();
16
+ fs.writeFileSync(BRIEF, `# Restart Brief\nGenerated: ${ts}\n\nContext compacted. Resume from work items queue.\n`);
17
+ } catch (_) {}
18
+ process.exit(0);
19
+ });