@haposoft/cafekit 0.15.1 → 0.16.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +6 -2
  2. package/bin/__tests__/codex-hooks.test.js +140 -0
  3. package/bin/__tests__/codex-native.test.js +230 -2
  4. package/bin/__tests__/develop-contract.test.js +1120 -0
  5. package/bin/__tests__/installer-safety.test.js +553 -2
  6. package/bin/__tests__/package-inventory.test.js +258 -0
  7. package/bin/__tests__/secret-scanner.test.js +157 -0
  8. package/bin/__tests__/validator-grounding.test.js +713 -0
  9. package/bin/install.js +5 -1
  10. package/bin/lib/codex-install.js +21 -6
  11. package/bin/lib/context.js +17 -6
  12. package/bin/lib/copy-utils.js +31 -0
  13. package/bin/lib/i18n.js +12 -3
  14. package/bin/lib/instruction-blocks.js +85 -0
  15. package/bin/lib/managed-writer.js +4 -1
  16. package/bin/lib/opencode-install.js +75 -30
  17. package/bin/lib/skill-deps.js +45 -7
  18. package/bin/phases/claude-runtime.js +103 -4
  19. package/bin/phases/codex-runtime.js +41 -7
  20. package/bin/phases/post-install.js +29 -22
  21. package/bin/phases/root-config.js +89 -10
  22. package/bin/phases/select-platform.js +23 -0
  23. package/bin/phases/skills-setup.js +7 -3
  24. package/package.json +15 -5
  25. package/src/claude/CLAUDE.md +6 -122
  26. package/src/claude/agents/code-auditor.md +21 -22
  27. package/src/claude/agents/debugger.md +3 -3
  28. package/src/claude/agents/deployer.md +1 -1
  29. package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
  30. package/src/claude/agents/project-manager.md +1 -1
  31. package/src/claude/agents/spec-maker.md +2 -2
  32. package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
  33. package/src/claude/hooks/agent.cjs +7 -14
  34. package/src/claude/hooks/rules.cjs +33 -53
  35. package/src/claude/hooks/spec-gate.cjs +66 -21
  36. package/src/claude/hooks/spec-state.cjs +5 -0
  37. package/src/claude/migration-manifest.json +9 -2
  38. package/src/claude/runtime.json +0 -3
  39. package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
  40. package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
  41. package/src/claude/scripts/spec-ground.cjs +189 -83
  42. package/src/claude/scripts/validate-spec-output.cjs +294 -30
  43. package/src/claude/scripts/workflow-policy.cjs +609 -0
  44. package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
  45. package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
  46. package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
  47. package/src/claude/skills/backend-development/SKILL.md +4 -4
  48. package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
  49. package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
  50. package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
  51. package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
  52. package/src/claude/skills/backend-development/references/backend-security.md +3 -3
  53. package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
  54. package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
  55. package/src/claude/skills/brainstorm/SKILL.md +2 -3
  56. package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
  57. package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
  58. package/src/claude/skills/code-review/SKILL.md +12 -8
  59. package/src/claude/skills/code-review/references/verification-gate.md +4 -4
  60. package/src/claude/skills/debug/SKILL.md +2 -0
  61. package/src/claude/skills/develop/SKILL.md +100 -52
  62. package/src/claude/skills/develop/references/parallel-waves.md +105 -26
  63. package/src/claude/skills/develop/references/quality-gate.md +58 -38
  64. package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
  65. package/src/claude/skills/git/SKILL.md +44 -50
  66. package/src/claude/skills/hotfix/SKILL.md +10 -8
  67. package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
  68. package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
  69. package/src/claude/skills/inspect/SKILL.md +12 -30
  70. package/src/claude/skills/mobile-development/SKILL.md +12 -12
  71. package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
  72. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
  73. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
  74. package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
  75. package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
  76. package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
  77. package/src/claude/skills/pptx/scripts/package.json +1 -1
  78. package/src/claude/skills/question/SKILL.md +13 -13
  79. package/src/claude/skills/research/SKILL.md +2 -2
  80. package/src/claude/skills/specs/SKILL.md +32 -15
  81. package/src/claude/skills/specs/references/review.md +3 -3
  82. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  83. package/src/claude/skills/specs/templates/spec-state.json +7 -3
  84. package/src/claude/skills/sync/SKILL.md +26 -1
  85. package/src/claude/skills/sync/references/sync-protocols.md +3 -3
  86. package/src/claude/skills/test/SKILL.md +12 -4
  87. package/src/claude/skills/test/references/failure-triage.md +17 -17
  88. package/src/claude/skills/test/references/test-memory.md +2 -2
  89. package/src/codex/AGENTS.md +16 -24
  90. package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
  91. package/src/codex/hooks/rules.cjs +27 -26
  92. package/src/codex/hooks/spec-gate.cjs +19 -8
  93. package/src/codex/hooks/spec-state.cjs +10 -1
  94. package/src/codex/hooks.json +14 -14
  95. package/src/common/AGENTS.md +43 -0
  96. package/src/opencode/AGENTS.md +21 -121
  97. package/src/opencode/plugins/rules.ts +26 -34
  98. package/src/opencode/plugins/spec-gate.ts +283 -0
  99. package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
@@ -2,40 +2,31 @@
2
2
  /**
3
3
  * CafeKit spec GROUNDING checker (Specs v2, Layer 2).
4
4
  *
5
- * The deterministic validator (`validate-spec-output.cjs`) checks spec SHAPE
6
- * (headings, registry sync, coverage). It is blind to whether the file paths a
7
- * task cites actually EXIST. This script closes that gap: it parses every task's
8
- * `Related Files` table and verifies, against the real work tree, that each
9
- * Modify/Delete/Read path exists — unless another task in the same spec Creates
10
- * it first (intra-spec resolution).
11
- *
12
- * Why active-grep instead of opt-in: field tests showed opt-in checks get
13
- * skipped by the author. Grounding runs unconditionally over whatever paths the
14
- * tasks already declare, so it cannot be dodged by choosing a different format.
15
- *
16
- * Usage:
17
- * node spec-ground.cjs <specDir> [--root <work-context>]
18
- *
19
- * <specDir> : path to specs/<feature> (relative or absolute)
20
- * --root : work tree the paths resolve against. Default: two levels up from
21
- * specDir (…/<root>/specs/<feature> -> <root>). Override for
22
- * monorepos where the spec lives apart from the code.
23
- *
24
- * Exit: 0 = GROUNDED, 1 = FAIL (missing path), 2 = usage error.
5
+ * Verifies every task Related Files declaration against the real work tree.
6
+ * Paths are relative, actions are explicit, and globs must match at least one
7
+ * filesystem entry. Create on existing paths is an error. Lifecycle ordering
8
+ * (Create before Modify/Delete/Read) is enforced within task and via
9
+ * dependency DAG for cross-task references; cross-task producer/consumer
10
+ * requires a valid spec.json task_registry (fail-closed).
25
11
  */
26
12
 
27
13
  const fs = require('fs');
28
14
  const path = require('path');
29
15
 
16
+ const ACTIONS = new Set(['create', 'modify', 'delete', 'read']);
17
+
30
18
  function usage() {
31
19
  console.error('Usage: node spec-ground.cjs <specDir> [--root <work-context>]');
32
20
  }
33
21
 
34
22
  function parseArgs(argv) {
35
23
  const args = { specDir: null, root: null };
36
- for (let i = 2; i < argv.length; i++) {
37
- if (argv[i] === '--root') args.root = argv[++i];
38
- else if (!args.specDir) args.specDir = argv[i];
24
+ for (let i = 2; i < argv.length; i += 1) {
25
+ if (argv[i] === '--root') {
26
+ args.root = argv[++i];
27
+ if (!args.root) throw new Error('--root requires a value');
28
+ } else if (!args.specDir) args.specDir = argv[i];
29
+ else throw new Error(`Unknown argument: ${argv[i]}`);
39
30
  }
40
31
  return args;
41
32
  }
@@ -45,102 +36,217 @@ function listTaskFiles(specDir) {
45
36
  if (!fs.existsSync(tasksDir)) return [];
46
37
  return fs
47
38
  .readdirSync(tasksDir, { withFileTypes: true })
48
- .filter((e) => e.isFile() && e.name.endsWith('.md'))
49
- .map((e) => path.join(tasksDir, e.name))
39
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
40
+ .map((entry) => path.join(tasksDir, entry.name))
50
41
  .sort();
51
42
  }
52
43
 
53
- /**
54
- * Pull rows out of a markdown `## Related Files` table. Returns
55
- * [{ path, action, taskFile }]. Tables look like:
56
- * | Path | Action | Description |
57
- * | `front/x.vue` | Modify | ... |
58
- */
59
44
  function parseRelatedFiles(content, taskFile) {
60
- const rows = [];
61
45
  const lines = content.split('\n');
62
- let inSection = false;
63
- for (const line of lines) {
64
- if (/^##+\s+Related Files\s*$/i.test(line)) { inSection = true; continue; }
65
- if (inSection && /^##+\s+/.test(line)) break; // next heading ends section
66
- if (!inSection) continue;
46
+ const headingIndex = lines.findIndex((line) => /^##+\s+Related Files\s*$/i.test(line));
47
+ if (headingIndex < 0) return { present: false, rows: [] };
48
+
49
+ const rows = [];
50
+ for (let index = headingIndex + 1; index < lines.length; index += 1) {
51
+ const line = lines[index];
52
+ if (/^##+\s+/.test(line)) break;
67
53
  if (!line.trim().startsWith('|')) continue;
54
+ const cells = line.split('|').map((cell) => cell.trim());
55
+ const filePath = (cells[1] || '').replace(/`/g, '').trim();
56
+ const action = (cells[2] || '').replace(/`/g, '').trim().toLowerCase();
57
+ if (!filePath || filePath.toLowerCase() === 'path' || /^-+$/.test(filePath)) continue;
58
+ rows.push({ path: filePath, action, taskFile: path.basename(taskFile) });
59
+ }
60
+ return { present: true, rows };
61
+ }
62
+
63
+ function unsafePath(filePath) {
64
+ return path.isAbsolute(filePath)
65
+ || /^[A-Za-z]:[\\/]/.test(filePath)
66
+ || filePath.split(/[\\/]+/).includes('..');
67
+ }
68
+
69
+ function expandBraces(pattern) {
70
+ const match = pattern.match(/\{([^{}]*)\}/);
71
+ if (!match) return [pattern];
72
+ return match[1].split(',').flatMap((part) => expandBraces(
73
+ `${pattern.slice(0, match.index)}${part}${pattern.slice(match.index + match[0].length)}`,
74
+ ));
75
+ }
68
76
 
69
- const cells = line.split('|').map((c) => c.trim());
70
- // cells[0] is '' (leading pipe). path=cells[1], action=cells[2].
71
- const rawPath = (cells[1] || '').replace(/`/g, '').trim();
72
- const action = (cells[2] || '').toLowerCase().trim();
73
- if (!rawPath || rawPath === 'path') continue; // header row
74
- if (/^-+$/.test(rawPath)) continue; // separator row
75
- if (!action || !/(create|modify|delete|read)/.test(action)) continue;
76
- rows.push({ path: rawPath, action, taskFile: path.basename(taskFile) });
77
+ function globRegExp(pattern) {
78
+ let source = '^';
79
+ for (let index = 0; index < pattern.length; index += 1) {
80
+ const char = pattern[index];
81
+ if (char === '*') {
82
+ if (pattern[index + 1] === '*') {
83
+ index += 1;
84
+ if (pattern[index + 1] === '/') index += 1;
85
+ source += '.*';
86
+ } else source += '[^/]*';
87
+ } else if (char === '?') source += '[^/]';
88
+ else source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
77
89
  }
78
- return rows;
90
+ return new RegExp(`${source}$`);
79
91
  }
80
92
 
81
- /** Lenient existence check with minimal glob handling. */
82
- function pathExists(root, p) {
83
- // Glob-ish path (e.g. `assets/{x,instagram,facebook}.svg` or `dir/*.ts`):
84
- // verify the parent directory exists rather than the literal name.
85
- if (/[{*]/.test(p)) {
86
- const parent = path.dirname(p.replace(/\{.*$/, '').replace(/\*.*$/, ''));
87
- return fs.existsSync(path.join(root, parent));
93
+ function relativeEntries(root) {
94
+ const entries = [];
95
+ function walk(current, relative) {
96
+ let children;
97
+ try { children = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
98
+ for (const entry of children) {
99
+ const nextRelative = relative ? `${relative}/${entry.name}` : entry.name;
100
+ const next = path.join(current, entry.name);
101
+ entries.push(nextRelative);
102
+ if (entry.isDirectory() && !entry.isSymbolicLink()) walk(next, nextRelative);
103
+ }
88
104
  }
89
- return fs.existsSync(path.join(root, p));
105
+ walk(root, '');
106
+ return entries;
107
+ }
108
+
109
+ function matchingEntries(root, filePath) {
110
+ if (!/[{}*?]/.test(filePath)) return fs.existsSync(path.join(root, filePath)) ? [filePath] : [];
111
+ const candidates = relativeEntries(root);
112
+ const matches = new Set();
113
+ for (const expanded of expandBraces(filePath)) {
114
+ const matcher = globRegExp(expanded.replaceAll('\\', '/'));
115
+ for (const candidate of candidates) if (matcher.test(candidate)) matches.add(candidate);
116
+ }
117
+ return [...matches];
90
118
  }
91
119
 
92
120
  function main() {
93
- const { specDir: specInput, root: rootInput } = parseArgs(process.argv);
94
- if (!specInput) { usage(); process.exit(2); }
121
+ let args;
122
+ try { args = parseArgs(process.argv); } catch (error) {
123
+ usage();
124
+ console.error(`- ${error.message}`);
125
+ process.exit(2);
126
+ }
127
+ if (!args.specDir) { usage(); process.exit(2); }
95
128
 
96
- const specDir = path.resolve(process.cwd(), specInput);
129
+ const specDir = path.resolve(process.cwd(), args.specDir);
97
130
  if (!fs.existsSync(specDir)) {
98
- console.error(`FAIL ${specInput}\n- spec directory does not exist`);
131
+ console.error(`FAIL ${args.specDir}\n- spec directory does not exist`);
99
132
  process.exit(1);
100
133
  }
101
- // Default work-context: two levels up (…/<root>/specs/<feature>).
102
- const root = rootInput
103
- ? path.resolve(process.cwd(), rootInput)
134
+ const root = args.root
135
+ ? path.resolve(process.cwd(), args.root)
104
136
  : path.resolve(specDir, '..', '..');
105
137
 
106
138
  const taskFiles = listTaskFiles(specDir);
107
139
  const allRows = [];
108
- for (const tf of taskFiles) {
109
- allRows.push(...parseRelatedFiles(fs.readFileSync(tf, 'utf8'), tf));
140
+ const rowsByTask = new Map();
141
+ const errors = [];
142
+ const warnings = [];
143
+ for (const taskFile of taskFiles) {
144
+ const section = parseRelatedFiles(fs.readFileSync(taskFile, 'utf8'), taskFile);
145
+ const basename = path.basename(taskFile);
146
+ if (!rowsByTask.has(basename)) rowsByTask.set(basename, []);
147
+ if (!section.present) errors.push(`${basename}: missing Related Files section`);
148
+ else if (section.rows.length === 0) errors.push(`${basename}: Related Files section must not be empty`);
149
+ for (const row of section.rows) {
150
+ if (!ACTIONS.has(row.action)) errors.push(`${row.taskFile}: unsupported Related Files action "${row.action}" for ${row.path}`);
151
+ if (unsafePath(row.path)) errors.push(`${row.taskFile}: Related Files path must be relative and stay within work root: ${row.path}`);
152
+ allRows.push(row);
153
+ rowsByTask.get(basename).push(row);
154
+ }
110
155
  }
111
156
 
112
- // Paths that will exist because a task Creates them (intra-spec resolution).
113
- const createdPaths = new Set(
114
- allRows.filter((r) => r.action.includes('create')).map((r) => r.path),
115
- );
157
+ // Lifecycle ordering: Create must precede Modify/Delete/Read within same task
158
+ for (const [taskFile, rows] of rowsByTask.entries()) {
159
+ const createIndexByPath = new Map();
160
+ rows.forEach((row, idx) => {
161
+ if (row.action === 'create' && !createIndexByPath.has(row.path)) createIndexByPath.set(row.path, idx);
162
+ });
163
+ rows.forEach((row, idx) => {
164
+ if (!['modify', 'delete', 'read'].includes(row.action)) return;
165
+ if (!createIndexByPath.has(row.path)) return;
166
+ const createIdx = createIndexByPath.get(row.path);
167
+ if (createIdx > idx) {
168
+ errors.push(`${taskFile}: Create must precede ${row.action} for ${row.path} within Related Files order`);
169
+ }
170
+ });
171
+ }
116
172
 
117
- const errors = [];
118
- const warnings = [];
119
- let checked = 0;
173
+ // Cross-task lifecycle via spec.json dependency DAG (fail-closed when registry missing)
174
+ let taskRegistry = null;
175
+ try {
176
+ const specJson = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
177
+ if (specJson.task_registry && typeof specJson.task_registry === 'object' && !Array.isArray(specJson.task_registry)) taskRegistry = specJson.task_registry;
178
+ } catch {}
179
+ const producers = new Map();
180
+ for (const [taskFile, rows] of rowsByTask.entries()) {
181
+ for (const row of rows) {
182
+ if (row.action !== 'create') continue;
183
+ if (!producers.has(row.path)) producers.set(row.path, { taskFile });
184
+ }
185
+ }
186
+ const crossTaskConsumers = [];
187
+ for (const [taskFile, rows] of rowsByTask.entries()) {
188
+ for (const row of rows) {
189
+ if (!['modify', 'delete', 'read'].includes(row.action)) continue;
190
+ const producer = producers.get(row.path);
191
+ if (!producer) continue;
192
+ if (producer.taskFile === taskFile) continue;
193
+ crossTaskConsumers.push({ taskFile, row, producer });
194
+ }
195
+ }
196
+ if (crossTaskConsumers.length > 0 && !taskRegistry) {
197
+ for (const { taskFile, row, producer } of crossTaskConsumers) {
198
+ errors.push(`${taskFile}: ${row.action} of ${row.path} has producer ${producer.taskFile} but spec.json task_registry is missing or invalid — cannot verify cross-task dependency (fail-closed)`);
199
+ }
200
+ } else if (taskRegistry) {
201
+ // Build basename-indexed dependencies
202
+ const dependencies = new Map();
203
+ for (const [key, entry] of Object.entries(taskRegistry)) {
204
+ const base = path.basename(key);
205
+ const deps = new Set((entry.dependencies || []).map((d) => path.basename(d)));
206
+ dependencies.set(base, deps);
207
+ }
208
+ function dependsOn(taskBase, ancestorBase, seen = new Set()) {
209
+ if (taskBase === ancestorBase) return true;
210
+ if (seen.has(taskBase)) return false;
211
+ seen.add(taskBase);
212
+ const deps = dependencies.get(taskBase) || new Set();
213
+ for (const dep of deps) if (dependsOn(dep, ancestorBase, seen)) return true;
214
+ return false;
215
+ }
216
+ for (const { taskFile, row, producer } of crossTaskConsumers) {
217
+ if (!dependsOn(taskFile, producer.taskFile)) {
218
+ errors.push(`${taskFile}: ${row.action} of ${row.path} must depend on creator ${producer.taskFile}`);
219
+ }
220
+ }
221
+ }
120
222
 
223
+ const createdPaths = new Set(allRows.filter((row) => row.action === 'create').map((row) => row.path));
224
+ let checked = 0;
121
225
  for (const row of allRows) {
122
- const onDisk = pathExists(root, row.path);
123
- if (row.action.includes('create')) {
124
- if (onDisk) warnings.push(`${row.taskFile}: Create path already exists (will overwrite): ${row.path}`);
226
+ if (!ACTIONS.has(row.action) || unsafePath(row.path)) continue;
227
+ const matches = matchingEntries(root, row.path);
228
+ if (/[{}*?]/.test(row.path) && matches.length === 0) {
229
+ errors.push(`${row.taskFile}: glob matches no paths in work tree: ${row.path}`);
125
230
  continue;
126
231
  }
127
- // modify / delete / read must exist OR be created by another task in-spec.
128
- checked++;
129
- if (!onDisk && !createdPaths.has(row.path)) {
232
+ if (row.action === 'create') {
233
+ if (matches.length > 0) errors.push(`${row.taskFile}: Create path already exists in work tree (would overwrite): ${row.path} — use Modify instead of Create for existing files`);
234
+ continue;
235
+ }
236
+ checked += 1;
237
+ if (matches.length === 0 && !createdPaths.has(row.path)) {
130
238
  errors.push(`${row.taskFile}: ${row.action} path not found in work tree: ${row.path}`);
131
239
  }
132
240
  }
133
241
 
134
- for (const w of warnings) console.warn(`[WARN] ${w}`);
135
-
136
- const rel = path.relative(process.cwd(), specDir) || specDir;
242
+ for (const warning of warnings) console.warn(`[WARN] ${warning}`);
243
+ const relativeSpec = path.relative(process.cwd(), specDir) || specDir;
137
244
  if (errors.length > 0) {
138
- console.error(`FAIL ${rel} (root: ${root})`);
139
- for (const e of errors) console.error(`- ${e}`);
140
- console.error(`\n${errors.length} missing / ${checked} checked path(s).`);
245
+ console.error(`FAIL ${relativeSpec} (root: ${root})`);
246
+ for (const error of errors) console.error(`- ${error}`);
141
247
  process.exit(1);
142
248
  }
143
- console.log(`GROUNDED ${rel} (${checked} path(s) verified against ${root})`);
249
+ console.log(`GROUNDED ${relativeSpec} (${checked} path(s) verified against ${root})`);
144
250
  }
145
251
 
146
252
  main();