chati-dev 4.2.1 → 4.3.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.
Files changed (222) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +8 -7
  23. package/framework/constitution.md +127 -43
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +91 -36
  35. package/framework/hooks/license-guard.js +129 -190
  36. package/framework/hooks/mode-governance.js +68 -16
  37. package/framework/hooks/model-governance.js +19 -8
  38. package/framework/hooks/package.json +3 -0
  39. package/framework/hooks/post-dev.js +207 -0
  40. package/framework/hooks/prism-engine.js +288 -105
  41. package/framework/hooks/read-protection.js +37 -9
  42. package/framework/hooks/reasoning-escalator.js +371 -0
  43. package/framework/hooks/reference-trigger.js +117 -0
  44. package/framework/hooks/session-digest.js +94 -20
  45. package/framework/hooks/settings.json +32 -1
  46. package/framework/hooks/style-guard.js +52 -13
  47. package/framework/hooks/team-quality-gate.js +58 -25
  48. package/framework/hooks/undercover-guard.js +34 -13
  49. package/framework/i18n/en.yaml +3 -3
  50. package/framework/i18n/es.yaml +3 -3
  51. package/framework/i18n/fr.yaml +3 -3
  52. package/framework/i18n/pt.yaml +3 -3
  53. package/framework/intelligence/context-engine.md +4 -5
  54. package/framework/intelligence/decision-engine.md +1 -1
  55. package/framework/intelligence/hooks-performance.md +3 -3
  56. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  57. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  58. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  59. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  60. package/framework/orchestrator/chati-router.js +387 -0
  61. package/framework/orchestrator/chati-update.md +40 -40
  62. package/framework/orchestrator/chati.md +318 -115
  63. package/framework/scaffold/motion-premium/README.md +89 -0
  64. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  65. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  66. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  67. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  68. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  69. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  70. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  71. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  72. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  73. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  74. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  77. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  78. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  79. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  80. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  83. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  89. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  91. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  92. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  93. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  94. package/framework/schemas/session.schema.json +109 -21
  95. package/framework/scripts/reference-capture.js +430 -0
  96. package/framework/scripts/visual-qa.js +674 -0
  97. package/framework/tasks/orchestrator-handoff.md +1 -1
  98. package/framework/tasks/orchestrator-resume.md +1 -1
  99. package/framework/tasks/orchestrator-route.md +1 -1
  100. package/framework/tasks/orchestrator-status.md +3 -3
  101. package/framework/tasks/qa-planning-gate-define.md +1 -1
  102. package/framework/templates/brandbook-html-tmpl.md +1 -1
  103. package/framework/templates/brandbook-tmpl.yaml +1 -1
  104. package/framework/templates/component-spec-tmpl.yaml +1 -1
  105. package/framework/templates/design-token-tmpl.yaml +1 -1
  106. package/framework/templates/icon-system-tmpl.yaml +1 -1
  107. package/framework/templates/team-planning-tasks.yaml +6 -5
  108. package/framework/workflows/brownfield-discovery.yaml +2 -2
  109. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  110. package/framework/workflows/brownfield-service.yaml +14 -10
  111. package/framework/workflows/brownfield-ui.yaml +15 -11
  112. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  113. package/framework/workflows/quick-flow.yaml +3 -3
  114. package/framework/workflows/standard-flow.yaml +12 -9
  115. package/package.json +10 -5
  116. package/src/autonomy/autonomous-gate.js +1 -0
  117. package/src/autonomy/build-state.js +1 -2
  118. package/src/autonomy/progress-reporter.js +1 -1
  119. package/src/config/agent-customizer.js +11 -3
  120. package/src/config/claude-settings-generator.js +226 -0
  121. package/src/config/context-file-generator.js +41 -21
  122. package/src/config/framework-adapter.js +1 -0
  123. package/src/config/gemini-hooks-generator.js +76 -6
  124. package/src/config/mcp-configs.js +1 -0
  125. package/src/context/layers/l1-global.js +2 -1
  126. package/src/dashboard/data-reader.js +4 -3
  127. package/src/dashboard/layout.js +2 -1
  128. package/src/decision/analyzer.js +6 -30
  129. package/src/decision/engine.js +4 -28
  130. package/src/decision/registry-healer.js +3 -2
  131. package/src/decision/registry-updater.js +23 -14
  132. package/src/extensions/loader.js +2 -8
  133. package/src/gates/g1-planning-complete.js +2 -1
  134. package/src/gates/g2-qa-planning.js +2 -1
  135. package/src/gates/g3-implementation.js +2 -1
  136. package/src/gates/g4-qa-implementation.js +3 -2
  137. package/src/gates/g5-deploy-ready.js +2 -1
  138. package/src/health/engine.js +4 -3
  139. package/src/installer/core.js +557 -81
  140. package/src/installer/preflight.js +131 -0
  141. package/src/installer/provider-overlay.js +3 -3
  142. package/src/installer/scaffold-applier.js +358 -0
  143. package/src/installer/templates.js +52 -31
  144. package/src/installer/validator.js +17 -12
  145. package/src/intelligence/registry-manager.js +22 -29
  146. package/src/intelligence/timeline.js +11 -6
  147. package/src/license/commands.js +1 -1
  148. package/src/license/wait.js +102 -0
  149. package/src/memory/agent-memory.js +81 -0
  150. package/src/memory/dream.js +32 -1
  151. package/src/merger/replace-merger.js +28 -15
  152. package/src/orchestrator/agent-selector.js +2 -1
  153. package/src/orchestrator/cli.js +1908 -80
  154. package/src/orchestrator/doctor.js +270 -0
  155. package/src/orchestrator/handoff-engine.js +4 -3
  156. package/src/orchestrator/index.js +2 -0
  157. package/src/orchestrator/pipeline-manager.js +306 -15
  158. package/src/orchestrator/session-manager.js +331 -6
  159. package/src/tasks/handoff.js +3 -2
  160. package/src/telemetry/config.js +4 -3
  161. package/src/telemetry/schema.js +1 -0
  162. package/src/terminal/collector.js +3 -2
  163. package/src/terminal/index.js +1 -2
  164. package/src/terminal/isolation.js +52 -18
  165. package/src/terminal/prompt-builder.js +42 -25
  166. package/src/terminal/run-parallel.js +1 -1
  167. package/src/terminal/run-team.js +3 -3
  168. package/src/terminal/team-task-list.js +43 -4
  169. package/src/upgrade/backup.js +3 -2
  170. package/src/upgrade/checker.js +3 -2
  171. package/src/upgrade/migrator.js +65 -7
  172. package/src/upgrade/tracked-files-detector.js +86 -0
  173. package/src/upgrade/user-messages.js +94 -0
  174. package/src/utils/config-parser.js +2 -1
  175. package/src/utils/feature-flags.js +2 -1
  176. package/src/utils/flatten-entities.js +69 -0
  177. package/src/utils/framework-dir.js +16 -0
  178. package/src/utils/model-id.js +85 -0
  179. package/src/utils/provider-limits.js +84 -23
  180. package/src/utils/schema-validator.js +1 -1
  181. package/src/wizard/i18n.js +5 -4
  182. package/src/wizard/index.js +14 -0
  183. package/assets/logo - c/303/263pia.png +0 -0
  184. package/assets/logo.svg +0 -42
  185. package/assets/logo2.png +0 -0
  186. package/assets/social-preview.png +0 -0
  187. package/scripts/bundle-framework.js +0 -69
  188. package/scripts/changelog-generator.js +0 -222
  189. package/scripts/codebase-mapper.js +0 -728
  190. package/scripts/commit-message-generator.js +0 -167
  191. package/scripts/coverage-analyzer.js +0 -260
  192. package/scripts/dependency-analyzer.js +0 -280
  193. package/scripts/doctor/checks/agents.js +0 -77
  194. package/scripts/doctor/checks/constitution.js +0 -41
  195. package/scripts/doctor/checks/domain-alignment.js +0 -58
  196. package/scripts/doctor/checks/prism-layers.js +0 -84
  197. package/scripts/doctor/checks/registry.js +0 -55
  198. package/scripts/doctor/checks/schemas.js +0 -61
  199. package/scripts/doctor/fixes/reference-fix.js +0 -100
  200. package/scripts/doctor/fixes/registry-fix.js +0 -56
  201. package/scripts/doctor/index.js +0 -212
  202. package/scripts/framework-analyzer.js +0 -308
  203. package/scripts/generate-constitution-domain.js +0 -253
  204. package/scripts/generate-signing-key.js +0 -33
  205. package/scripts/health-check.js +0 -481
  206. package/scripts/ide-sync.js +0 -326
  207. package/scripts/performance-analyzer.js +0 -325
  208. package/scripts/plan-tracker.js +0 -278
  209. package/scripts/populate-entity-registry.js +0 -481
  210. package/scripts/pr-review.js +0 -317
  211. package/scripts/rollback-manager.js +0 -310
  212. package/scripts/semantic-lint.js +0 -328
  213. package/scripts/sign-manifest.js +0 -53
  214. package/scripts/stuck-detector.js +0 -343
  215. package/scripts/test-quality-assessment.js +0 -257
  216. package/scripts/validate-agents.js +0 -368
  217. package/scripts/validate-package.js +0 -367
  218. package/scripts/validate-tasks.js +0 -465
  219. package/src/autonomy/worktree-manager.js +0 -250
  220. package/src/intelligence/decision-engine.js +0 -256
  221. package/src/intelligence/document-sharder.js +0 -221
  222. package/src/intelligence/elicitation.js +0 -265
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Advance Trigger Hook — PostToolUse (runs after each Write tool call)
4
+ *
5
+ * Autonomous pipeline advance: when an agent writes its handoff file,
6
+ * this hook detects it and fires `chati-router.js advance` in the background
7
+ * so session.yaml, Magic Docs, and the next agent activation happen
8
+ * deterministically — without depending on Claude remembering to call advance.
9
+ *
10
+ * Trigger condition: the Write tool just wrote a file at
11
+ * artifacts/handoffs/<agent>-handoff.md
12
+ * where the handoff's YAML frontmatter declares `status: completed` and
13
+ * contains `score: <N>`.
14
+ *
15
+ * The hook is observational — it always returns `{}` (no permission decision)
16
+ * and never blocks the tool call. The router is spawned detached with stdio
17
+ * ignored so it outlives the hook process.
18
+ */
19
+
20
+ import { existsSync, readFileSync } from 'fs';
21
+ import { join } from 'path';
22
+ import { spawn } from 'child_process';
23
+
24
+ async function readStdin() {
25
+ let data = '';
26
+ for await (const chunk of process.stdin) data += chunk;
27
+ return data;
28
+ }
29
+
30
+ function extractFrontmatter(markdown) {
31
+ if (!markdown.startsWith('---')) return null;
32
+ const end = markdown.indexOf('\n---', 3);
33
+ if (end === -1) return null;
34
+ const fm = markdown.slice(3, end + 1);
35
+ const fields = {};
36
+ for (const line of fm.split('\n')) {
37
+ const m = line.match(/^([a-zA-Z_][\w-]*):\s*(.+)$/);
38
+ if (m) fields[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
39
+ }
40
+ return fields;
41
+ }
42
+
43
+ async function main() {
44
+ let input;
45
+ try {
46
+ input = JSON.parse(await readStdin() || '{}');
47
+ } catch {
48
+ process.stdout.write('{}\n');
49
+ return;
50
+ }
51
+
52
+ const toolName = input.tool_name || '';
53
+ const toolInput = input.tool_input || {};
54
+ const filePath = toolInput.file_path || '';
55
+ const cwd = input.cwd || process.cwd();
56
+
57
+ // Observational default — always allow the tool call through.
58
+ process.stdout.write('{}\n');
59
+
60
+ if (toolName !== 'Write') return;
61
+ if (!filePath) return;
62
+
63
+ // Match agent handoff files in either layout:
64
+ // <root>/artifacts/handoffs/<agent>-handoff.md
65
+ // <root>/.chati.dev/artifacts/handoffs/<agent>-handoff.md
66
+ const handoffMatch = filePath.match(/artifacts\/handoffs\/([a-z0-9-]+)-handoff\.md$/);
67
+ if (!handoffMatch) return;
68
+ const agent = handoffMatch[1];
69
+
70
+ // Load execution_mode from session to decide firing policy (Article XVII).
71
+ //
72
+ // Firing policy (v4.3.1):
73
+ // - autonomous: always fire (as before)
74
+ // - interactive (default): fire ONLY when the handoff is a QA agent
75
+ // that failed its threshold (score below cutoff) OR verdict is
76
+ // NEEDS_FIX/REJECTED. Correction loops are an INTERNAL quality process
77
+ // (Fault Vector Protocol, Article XXII), not a forward step.
78
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
79
+ if (!existsSync(sessionPath)) return;
80
+ let raw;
81
+ try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return; }
82
+ const modeMatch = raw.match(/^\s*execution_mode:\s*(.+)$/m);
83
+ const profile = modeMatch ? modeMatch[1].trim().replace(/^["']|["']$/g, '') : 'interactive';
84
+
85
+ // Read the written handoff and check its frontmatter declares completion.
86
+ if (!existsSync(filePath)) return;
87
+ let content;
88
+ try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
89
+ const fm = extractFrontmatter(content);
90
+ if (!fm) return;
91
+ if ((fm.status || '').toLowerCase() !== 'completed' && (fm.status || '').toLowerCase() !== 'complete') return;
92
+ const score = parseInt(fm.score, 10);
93
+ if (isNaN(score)) return;
94
+ if ((fm.agent || agent) !== agent) return;
95
+
96
+ // Decide whether to fire under interactive mode.
97
+ if (profile !== 'autonomous') {
98
+ const QA_THRESHOLDS = { 'qa-planning': 95, 'qa-implementation': 95, 'qa-visual': 90 };
99
+ const threshold = QA_THRESHOLDS[agent];
100
+ const isQA = threshold !== undefined;
101
+ const verdict = (fm.verdict || '').toUpperCase();
102
+ const qaFailed = isQA && (
103
+ score < threshold ||
104
+ verdict === 'NEEDS_FIX' ||
105
+ verdict === 'REJECTED'
106
+ );
107
+ if (!qaFailed) return;
108
+ }
109
+
110
+ // Locate the router relative to the project root.
111
+ // Prefer .chati.dev/ layout; fall back to legacy chati.dev/.
112
+ const fwCandidates = ['.chati.dev', 'chati.dev'];
113
+ let routerPath = null;
114
+ for (const fw of fwCandidates) {
115
+ const p = join(cwd, fw, 'orchestrator', 'chati-router.js');
116
+ if (existsSync(p)) { routerPath = p; break; }
117
+ }
118
+ if (!routerPath) return;
119
+
120
+ // Fire and forget: detach so the router outlives this hook process.
121
+ try {
122
+ const child = spawn('node', [routerPath, 'advance', '--agent', agent, '--score', String(score)], {
123
+ cwd,
124
+ detached: true,
125
+ stdio: 'ignore',
126
+ });
127
+ child.unref();
128
+ } catch { /* non-fatal */ }
129
+ }
130
+
131
+ main().catch(() => process.stdout.write('{}\n'));
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Brief Validator Hook — PostToolUse (runs after Write tool)
4
+ *
5
+ * When brief-handoff.md is written, validates that brief-report.md exists
6
+ * and contains required fields. Writes warnings directly into the handoff
7
+ * if validation fails. Deterministic — replaces Brief's self-validation.
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ async function readStdin() {
14
+ let data = '';
15
+ for await (const chunk of process.stdin) data += chunk;
16
+ return data;
17
+ }
18
+
19
+ async function main() {
20
+ let input;
21
+ try {
22
+ input = JSON.parse(await readStdin() || '{}');
23
+ } catch {
24
+ process.stdout.write('{}\n');
25
+ return;
26
+ }
27
+
28
+ const toolName = input.tool_name || '';
29
+ const toolInput = input.tool_input || {};
30
+ const filePath = toolInput.file_path || '';
31
+ const cwd = input.cwd || process.cwd();
32
+
33
+ process.stdout.write('{}\n');
34
+
35
+ if (toolName !== 'Write') return;
36
+ if (!filePath.endsWith('handoffs/brief-handoff.md')) return;
37
+
38
+ // Validate brief-report.md
39
+ const reportCandidates = [
40
+ join(cwd, 'artifacts', '1-Brief', 'brief-report.md'),
41
+ ];
42
+ const reportPath = reportCandidates.find(p => existsSync(p));
43
+
44
+ const warnings = [];
45
+
46
+ if (!reportPath) {
47
+ warnings.push('brief-report.md NOT FOUND — handoff written without artifact');
48
+ } else {
49
+ const content = readFileSync(reportPath, 'utf-8');
50
+
51
+ if (!content.includes('visual_quality')) {
52
+ warnings.push('visual_quality field missing — downstream UX agent needs this to set quality bar');
53
+ }
54
+ if (content.includes('[TODO]') || content.includes('[TBD]')) {
55
+ warnings.push('Placeholders found ([TODO] or [TBD]) — report has incomplete sections');
56
+ }
57
+ if (content.length < 500) {
58
+ warnings.push(`brief-report.md is very short (${content.length} chars) — may be incomplete`);
59
+ }
60
+ // Check if visual_references URLs exist but captures section is missing
61
+ const hasUrls = content.match(/url:\s*["']?https?:\/\//);
62
+ if (hasUrls && !content.includes('Visual Reference Captures')) {
63
+ warnings.push('visual_references URLs found but Visual Reference Captures section missing — reference-trigger hook may not have fired');
64
+ }
65
+ }
66
+
67
+ if (warnings.length === 0) return;
68
+
69
+ // Append warnings to the handoff file
70
+ try {
71
+ let handoff = readFileSync(filePath, 'utf-8');
72
+ if (handoff.includes('## Brief Validation Warnings')) return; // idempotent
73
+
74
+ handoff += `\n\n## Brief Validation Warnings\n\n`;
75
+ handoff += `> Auto-generated by brief-validator hook. These are deterministic checks.\n\n`;
76
+ for (const w of warnings) {
77
+ handoff += `- ⚠ ${w}\n`;
78
+ }
79
+ writeFileSync(filePath, handoff, 'utf-8');
80
+ } catch { /* ignore */ }
81
+ }
82
+
83
+ main().catch(() => process.stdout.write('{}\n'));
@@ -72,19 +72,34 @@ async function main() {
72
72
 
73
73
  try {
74
74
  const event = JSON.parse(input);
75
+ const projectDir = event.cwd || process.cwd();
75
76
  const toolName = event.tool_name || '';
76
77
  const toolInput = event.tool_input || {};
77
78
 
79
+ // Integrity check: verify license-guard.js exists on disk.
80
+ // If someone deletes license-guard.js to bypass license enforcement,
81
+ // constitution-guard (which is a separate hook) catches it.
82
+ // Cross-hook verification = no single point of removal.
83
+ const { existsSync } = await import('fs');
84
+ const { join } = await import('path');
85
+ const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
86
+ const licenseGuardPath = join(projectDir, fwDir, 'hooks', 'license-guard.js');
87
+ if (!existsSync(licenseGuardPath)) {
88
+ process.stdout.write(JSON.stringify(denyOutput(
89
+ 'License enforcement file (license-guard.js) is missing. System integrity compromised. Reinstall with: npx chati-dev init'
90
+ )));
91
+ return;
92
+ }
93
+
78
94
  // Check Write/Edit operations for secrets
79
95
  if (toolName === 'Write' || toolName === 'Edit') {
80
96
  const content = toolInput.content || toolInput.new_string || '';
81
97
  const secrets = containsSecrets(content);
82
98
 
83
99
  if (secrets.length > 0) {
84
- process.stdout.write(JSON.stringify({
85
- decision: 'block',
86
- reason: `[Article IV] Potential secret detected in file content. Pattern: ${secrets[0]}. Use environment variables instead.`,
87
- }));
100
+ process.stdout.write(JSON.stringify(denyOutput(
101
+ `[Article IV] Potential secret detected in file content. Pattern: ${secrets[0]}. Use environment variables instead.`
102
+ )));
88
103
  return;
89
104
  }
90
105
  }
@@ -93,57 +108,89 @@ async function main() {
93
108
  if (toolName === 'Bash') {
94
109
  const command = toolInput.command || '';
95
110
  if (isDestructiveCommand(command)) {
96
- process.stdout.write(JSON.stringify({
97
- decision: 'block',
98
- reason: `[Article IV] Destructive command detected: "${command.slice(0, 60)}...". This requires explicit user confirmation.`,
99
- }));
111
+ // Advisory, not blocking — destructive commands should be confirmed
112
+ // by the user via Claude Code's permission prompt, not hard-blocked
113
+ // by the hook (which runs AFTER user approval and blocks anyway).
114
+ process.stdout.write(JSON.stringify(allowOutput(
115
+ `[Article IV] Destructive command: "${command.slice(0, 60)}..." — proceeding with user approval.`
116
+ )));
100
117
  return;
101
118
  }
102
119
 
103
- // Run 23-point shell injection security checks
120
+ // Run 21-point shell injection security checks
104
121
  const injectionFindings = runShellInjectionChecks(command);
105
122
  if (injectionFindings.length > 0) {
106
123
  const critical = injectionFindings.filter(f => f.severity === 'critical');
107
124
  if (critical.length > 0) {
108
- process.stdout.write(JSON.stringify({
109
- decision: 'block',
110
- reason: `[Article IV] Shell injection risk detected (${critical.length} critical): ${critical.map(f => f.id).join(', ')}. Command: "${command.slice(0, 60)}..."`,
111
- }));
125
+ process.stdout.write(JSON.stringify(denyOutput(
126
+ `[Article IV] Shell injection risk detected (${critical.length} critical): ${critical.map(f => f.id).join(', ')}. Command: "${command.slice(0, 60)}..."`
127
+ )));
112
128
  return;
113
129
  }
114
- // High/medium findings: allow with warning (advisory)
115
- process.stdout.write(JSON.stringify({
116
- decision: 'allow',
117
- reason: `[Security Advisory] ${injectionFindings.length} shell security finding(s): ${injectionFindings.map(f => f.id).join(', ')}`,
118
- }));
130
+ // High/medium findings: allow (advisory only, surfaced via reason)
131
+ process.stdout.write(JSON.stringify(allowOutput(
132
+ `[Security Advisory] ${injectionFindings.length} shell security finding(s): ${injectionFindings.map(f => f.id).join(', ')}`
133
+ )));
119
134
  return;
120
135
  }
121
136
  }
122
137
 
123
- process.stdout.write(JSON.stringify({ decision: 'allow' }));
138
+ process.stdout.write(JSON.stringify(allowOutput()));
124
139
  } catch (err) {
125
140
  process.stderr.write(`[chati-hook-error] constitution-guard: ${err?.message || 'unknown'}\n`);
126
- process.stdout.write(JSON.stringify({ decision: 'block', reason: 'Hook error fail-closed for safety' }));
141
+ // Fail-open on hook errors: a broken hook should not block legitimate work.
142
+ // Real security checks above are explicit and run before this catch.
143
+ process.stdout.write(JSON.stringify(allowOutput()));
127
144
  }
128
145
  }
129
146
 
147
+ function allowOutput(reason) {
148
+ const out = {
149
+ hookSpecificOutput: {
150
+ hookEventName: 'PreToolUse',
151
+ permissionDecision: 'allow',
152
+ },
153
+ };
154
+ if (reason) out.hookSpecificOutput.permissionDecisionReason = reason;
155
+ return out;
156
+ }
157
+
158
+ function denyOutput(reason) {
159
+ return {
160
+ hookSpecificOutput: {
161
+ hookEventName: 'PreToolUse',
162
+ permissionDecision: 'deny',
163
+ permissionDecisionReason: reason,
164
+ },
165
+ };
166
+ }
167
+
130
168
  /**
131
- * 23-point shell injection security checks.
169
+ * 21-point shell injection security checks.
132
170
  * Inline implementation (hooks are standalone — no imports from src/).
133
171
  * Based on Claude Code's bashSecurity.ts patterns.
134
172
  */
135
- // SYNC: These 23 check IDs MUST match packages/chati-dev/src/security/bash-security.js
173
+ // SYNC: These 21 check IDs MUST match packages/chati-dev/src/security/bash-security.js
136
174
  // Hooks are standalone (no imports from src/), so duplication is necessary.
137
175
  // If you update here, update bash-security.js too. Run: npm test to verify both.
176
+ // Severity tiers:
177
+ // - critical = block immediately (clearly malicious, no legitimate use)
178
+ // - high/medium = advisory only (surfaced as reason, allowed). Used to be
179
+ // blocking, but several patterns produced too many false positives
180
+ // (e.g. SHELL_METACHARACTERS firing on every $(date), GIT_COMMIT_SUBSTITUTION
181
+ // firing on git commit -m "$(date)"). Critical patterns below are narrowly
182
+ // scoped to actually-dangerous shell injection vectors.
138
183
  const SHELL_INJECTION_CHECKS = [
139
- { id: 'INCOMPLETE_COMMANDS', pattern: /[|&;]\s*$/, severity: 'high' },
184
+ { id: 'INCOMPLETE_COMMANDS', pattern: /[|&;]\s*$/, severity: 'medium' },
140
185
  { id: 'JQ_SYSTEM_FUNCTION', pattern: /jq\b.*\bsystem\s*\(/i, severity: 'critical' },
141
- { id: 'JQ_FILE_ARGUMENTS', pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i, severity: 'high' },
142
- { id: 'OBFUSCATED_FLAGS', pattern: /\$[({].*[)}].*-/, severity: 'high' },
143
- { id: 'SHELL_METACHARACTERS', pattern: /[`]|\$\(/, severity: 'critical' },
144
- { id: 'DANGEROUS_VARIABLES', pattern: /(?:^|\s)(?:PATH|LD_PRELOAD|LD_LIBRARY_PATH|DYLD_INSERT_LIBRARIES|PYTHONPATH|NODE_PATH|RUBYLIB|PERL5LIB)\s*=/, severity: 'critical' },
145
- { id: 'NEWLINES', pattern: /(?<!\\)\n.*(?:rm|curl|wget|chmod|chown|sudo|eval|exec)/, severity: 'high' },
146
- { id: 'BACKSLASH_ESCAPED_WHITESPACE', pattern: /\\\s+(?:-|\/)/,severity: 'medium' },
186
+ { id: 'JQ_FILE_ARGUMENTS', pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i, severity: 'medium' },
187
+ { id: 'OBFUSCATED_FLAGS', pattern: /\$[({].*[)}].*-/, severity: 'medium' },
188
+ // SHELL_METACHARACTERS removed: $() and backticks are normal substitution.
189
+ // Genuinely dangerous substitutions are caught by DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION below.
190
+ { id: 'DANGEROUS_VARIABLES', pattern: /(?:^|\s)(?:LD_PRELOAD|LD_LIBRARY_PATH|DYLD_INSERT_LIBRARIES|PYTHONPATH|NODE_PATH|RUBYLIB|PERL5LIB)\s*=/, severity: 'critical' },
191
+ // PATH= removed from critical (legitimate `PATH=/foo:$PATH cmd` exists).
192
+ { id: 'NEWLINES', pattern: /(?<!\\)\n.*(?:rm|curl|wget|chmod|chown|sudo|eval|exec)/, severity: 'medium' },
193
+ { id: 'BACKSLASH_ESCAPED_WHITESPACE', pattern: /\\\s+(?:-|\/)/, severity: 'medium' },
147
194
  // eslint-disable-next-line no-control-regex
148
195
  { id: 'CONTROL_CHARACTERS', pattern: /[\x00-\x08\x0e-\x1f\x7f]/, severity: 'critical' },
149
196
  { id: 'UNICODE_WHITESPACE', pattern: /[\u200B-\u200F\u2028-\u202F\uFEFF\u00A0\u2060\u180E]/, severity: 'critical' },
@@ -151,13 +198,13 @@ const SHELL_INJECTION_CHECKS = [
151
198
  { id: 'DANGEROUS_PATTERNS_INPUT_REDIRECTION', pattern: /<\s*(?:\/etc\/(?:passwd|shadow|sudoers)|\/proc\/|~\/\.ssh\/|~\/\.aws\/)/, severity: 'critical' },
152
199
  { id: 'DANGEROUS_PATTERNS_OUTPUT_REDIRECTION', pattern: />\s*(?:\/etc\/|~\/\.ssh\/|~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.gitconfig)/, severity: 'critical' },
153
200
  { id: 'IFS_INJECTION', pattern: /\bIFS\s*=/, severity: 'critical' },
154
- { id: 'BRACE_EXPANSION', pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/, severity: 'high' },
155
- { id: 'GIT_COMMIT_SUBSTITUTION', pattern: /git\s+(?:commit|push|tag).*\$[({]/, severity: 'high' },
201
+ { id: 'BRACE_EXPANSION', pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/, severity: 'medium' },
202
+ // GIT_COMMIT_SUBSTITUTION removed: `git commit -m "$(date)"` is legitimate.
156
203
  { id: 'PROC_ENVIRON_ACCESS', pattern: /\/proc\/(?:self|\d+)\/(?:environ|cmdline|maps|mem)/, severity: 'critical' },
157
- { id: 'MALFORMED_TOKEN_INJECTION', pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i, severity: 'high' },
204
+ { id: 'MALFORMED_TOKEN_INJECTION', pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i, severity: 'medium' },
158
205
  { id: 'MID_WORD_HASH', pattern: /\w#\w/, severity: 'medium' },
159
206
  { id: 'COMMENT_QUOTE_DESYNC', pattern: /#.*['"][^'"]*$/, severity: 'medium' },
160
- { id: 'QUOTED_NEWLINE', pattern: /["'][^"']*\n[^"']*["']/, severity: 'high' },
207
+ { id: 'QUOTED_NEWLINE', pattern: /["'][^"']*\n[^"']*["']/, severity: 'medium' },
161
208
  { id: 'ZSH_DANGEROUS_COMMANDS', pattern: /\b(?:zmodload|sysopen|sysread|syswrite|zsystem|zselect|ztcp)\b/, severity: 'critical' },
162
209
  { id: 'BACKSLASH_ESCAPED_OPERATORS', pattern: /\\[|;&]/, severity: 'medium' },
163
210
  ];
@@ -176,8 +223,16 @@ function runShellInjectionChecks(command) {
176
223
 
177
224
  export { containsSecrets, isDestructiveCommand, runShellInjectionChecks, SECRET_PATTERNS, DESTRUCTIVE_COMMANDS, SHELL_INJECTION_CHECKS };
178
225
 
179
- // Only run main when executed directly (not imported by tests)
226
+ // Only run main when executed directly (not imported by tests).
227
+ // realpathSync resolves symlinks on both sides — important on macOS where
228
+ // /tmp is a symlink to /private/tmp. Without realpath the comparison fails
229
+ // silently and main() never runs.
180
230
  import { fileURLToPath } from 'url';
181
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
182
- main();
231
+ import { realpathSync } from 'fs';
232
+ if (process.argv[1]) {
233
+ try {
234
+ if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
235
+ main();
236
+ }
237
+ } catch { /* path resolution failed — not invoked as a script */ }
183
238
  }