chati-dev 4.2.2 → 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 (220) 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 +7 -6
  23. package/framework/constitution.md +127 -44
  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 +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. 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,9 +72,25 @@ 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 || '';
@@ -92,13 +108,16 @@ async function main() {
92
108
  if (toolName === 'Bash') {
93
109
  const command = toolInput.command || '';
94
110
  if (isDestructiveCommand(command)) {
95
- process.stdout.write(JSON.stringify(denyOutput(
96
- `[Article IV] Destructive command detected: "${command.slice(0, 60)}...". This requires explicit user confirmation.`
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.`
97
116
  )));
98
117
  return;
99
118
  }
100
119
 
101
- // Run 23-point shell injection security checks
120
+ // Run 21-point shell injection security checks
102
121
  const injectionFindings = runShellInjectionChecks(command);
103
122
  if (injectionFindings.length > 0) {
104
123
  const critical = injectionFindings.filter(f => f.severity === 'critical');
@@ -147,11 +166,11 @@ function denyOutput(reason) {
147
166
  }
148
167
 
149
168
  /**
150
- * 23-point shell injection security checks.
169
+ * 21-point shell injection security checks.
151
170
  * Inline implementation (hooks are standalone — no imports from src/).
152
171
  * Based on Claude Code's bashSecurity.ts patterns.
153
172
  */
154
- // 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
155
174
  // Hooks are standalone (no imports from src/), so duplication is necessary.
156
175
  // If you update here, update bash-security.js too. Run: npm test to verify both.
157
176
  // Severity tiers:
@@ -3,15 +3,18 @@
3
3
  * License Guard Hook — UserPromptSubmit
4
4
  *
5
5
  * Validates license status before each user prompt turn.
6
- * Uses a 4-hour cache stored in ~/.chati-dev/license.yaml (global, all projects).
6
+ * Once activated, the license is trusted until explicitly revoked by the API.
7
7
  *
8
8
  * Behavior:
9
9
  * - No license file → block with activation instructions
10
- * - Cache VALID < 4h → allow silently
10
+ * - Cache VALID → allow silently (always trust cached VALID status)
11
11
  * - Cache EXPIRED/INVALID → block with renewal message
12
- * - Cache stale (> 4h) call API, refresh cache, then allow or block
13
- * - API unreachable + cache VALID < 24h grace allow
14
- * - API unreachable + cache stale → block (cannot verify)
12
+ * - Cache stale (> 24h) + API reachable → refresh cache in background
13
+ * - API unreachable use cached status (no blocking on network issues)
14
+ *
15
+ * Rationale: users activate once at install time. After that, the hook should
16
+ * never block due to cache expiry or network issues. Only explicit server-side
17
+ * revocation (EXPIRED/INVALID status returned by API) blocks the user.
15
18
  *
16
19
  * Telemetry pings are owned by session-digest.js (PreCompact event), not here.
17
20
  */
@@ -47,19 +50,24 @@ export async function checkLicense() {
47
50
  return { valid: false, reason: 'No license key configured. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
48
51
  }
49
52
 
50
- // Check cache validity
51
53
  const status = readYamlField(licenseRaw, 'status');
52
54
  const checkedAt = readYamlField(licenseRaw, 'checked_at');
53
55
  const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
54
56
 
55
- if (age < CACHE_TTL_MS) {
56
- if (status === 'VALID') return { valid: true, status: 'VALID' };
57
- if (status === 'EXPIRED' || status === 'INVALID') {
58
- return { valid: false, reason: buildMessage(status, readYamlField(licenseRaw, 'reason')), status };
59
- }
57
+ // EXPLICIT REVOCATION block immediately
58
+ // Server explicitly said this license is no longer valid.
59
+ if (status === 'EXPIRED' || status === 'INVALID' || status === 'REVOKED') {
60
+ return { valid: false, reason: buildMessage(status, readYamlField(licenseRaw, 'reason')), status };
60
61
  }
61
62
 
62
- // Cache stalecall API
63
+ // Fresh cache (< CACHE_TTL_MS) trust without API call
64
+ if (status === 'VALID' && age < CACHE_TTL_MS) {
65
+ return { valid: true, status: 'VALID' };
66
+ }
67
+
68
+ // Stale cache — try background refresh.
69
+ // CRITICAL: cached VALID is trusted indefinitely. API call only updates the
70
+ // cache or detects revocation. Network failures NEVER block the user.
63
71
  try {
64
72
  const machineId = await computeMachineId();
65
73
  const res = await fetch(
@@ -85,11 +93,13 @@ export async function checkLicense() {
85
93
  if (data.status === 'VALID') return { valid: true, status: 'VALID' };
86
94
  return { valid: false, reason: buildMessage(data.status, data.reason), status: data.status };
87
95
  } catch {
88
- // API unreachable — grace period if cache is recent and was VALID
89
- if (age < 24 * 60 * 60 * 1000 && status === 'VALID') {
96
+ // API unreachable — trust the last known status indefinitely.
97
+ // The user activated once; we never block due to network issues.
98
+ if (status === 'VALID') {
90
99
  return { valid: true, status: 'VALID', graced: true };
91
100
  }
92
- return { valid: false, reason: 'Unable to verify license (network issue). If this persists, check your connection.\nRun: npx chati-dev activate --key=YOUR-KEY', status: 'NETWORK_ERROR' };
101
+ // Status is unknown/null and API is unreachable only now we ask user to retry.
102
+ return { valid: false, reason: 'License status unknown and unable to reach validation server. Try again when online.\nRun: npx chati-dev activate --key=YOUR-KEY', status: 'NETWORK_ERROR' };
93
103
  }
94
104
  }
95
105
 
@@ -97,30 +107,55 @@ async function main() {
97
107
  let input = '';
98
108
  for await (const chunk of process.stdin) input += chunk;
99
109
 
110
+ // Detect hook event from stdin to output the correct schema.
111
+ // PreToolUse needs hookSpecificOutput.permissionDecision.
112
+ // UserPromptSubmit needs exit 2 + stderr (silent block).
113
+ let event = null;
114
+ try {
115
+ const parsed = JSON.parse(input);
116
+ event = parsed.hook_event_name || null;
117
+ } catch { /* expected: may not be JSON */ }
118
+
119
+ const isPreToolUse = event === 'PreToolUse';
120
+
100
121
  try {
101
122
  const result = await checkLicense();
102
123
  if (result.valid) {
103
- allow();
124
+ process.stdout.write(isPreToolUse
125
+ ? JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
126
+ : '{}');
104
127
  } else {
105
- block(result.reason);
128
+ if (isPreToolUse) {
129
+ process.stdout.write(JSON.stringify({
130
+ hookSpecificOutput: {
131
+ hookEventName: 'PreToolUse',
132
+ permissionDecision: 'deny',
133
+ permissionDecisionReason: result.reason,
134
+ },
135
+ }));
136
+ } else {
137
+ process.stderr.write(result.reason);
138
+ process.exit(2);
139
+ }
106
140
  }
107
- } catch { /* expected: stdin parse may fail — fail open */
108
- allow();
141
+ } catch { /* fail open */
142
+ process.stdout.write(isPreToolUse
143
+ ? JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
144
+ : '{}');
109
145
  }
110
146
  }
111
147
 
112
148
  // ---------------------------------------------------------------------------
113
- // Decision helpers
149
+ // Decision helpers (used only for non-hook Bash invocations via checkLicense)
114
150
  // ---------------------------------------------------------------------------
115
151
 
116
- function allow() {
117
- // UserPromptSubmit canonical "allow" is empty object — omit decision.
118
- process.stdout.write('{}');
119
- }
120
-
121
152
  function block(reason) {
122
- // UserPromptSubmit blocking schema: decision + reason at top level.
123
- process.stdout.write(JSON.stringify({ decision: 'block', reason }));
153
+ // v4.2.2 v4.2.3 hotfix test: try exit code 2 + stderr instead of
154
+ // {decision:block} JSON. The JSON form was silently consumed as context
155
+ // injection in production. Per docs (variant 2), exit 2 + stderr is the
156
+ // canonical UserPromptSubmit block mechanism.
157
+ process.stderr.write(reason);
158
+ process.exit(2);
124
159
  }
125
160
 
126
161
  function buildMessage(status, reason) {
@@ -15,8 +15,8 @@ import { join, relative, isAbsolute } from 'path';
15
15
 
16
16
  const MODE_SCOPES = {
17
17
  planning: {
18
- allowed: ['chati.dev/', '.chati/', 'chati.dev/artifacts/'],
19
- description: 'Planning mode: write only to chati.dev/ and .chati/',
18
+ allowed: ['chati.dev/', '.chati.dev/', '.chati/', 'artifacts/', 'CLAUDE.local.md', 'CLAUDE.md', '.claude/'],
19
+ description: 'Planning mode: write only to framework dir, .chati/, artifacts/, and Claude config files',
20
20
  },
21
21
  build: {
22
22
  allowed: ['*'],
@@ -91,6 +91,17 @@ async function main() {
91
91
  return;
92
92
  }
93
93
 
94
+ // Block direct edits to session.yaml — it must be managed by CLI only.
95
+ // CLI uses fs.writeFileSync (not intercepted by hooks), so this only blocks
96
+ // Claude's Edit/Write tool calls, which is exactly what we want.
97
+ const rel = isAbsolute(filePath) ? relative(projectDir, filePath) : filePath;
98
+ if (rel === join('.chati', 'session.yaml') || rel === '.chati/session.yaml') {
99
+ process.stdout.write(JSON.stringify(denyOutput(
100
+ '[Article XV] session.yaml is managed by the CLI. Use the router: node chati.dev/orchestrator/chati-router.js advance --agent {name} --score {score}'
101
+ )));
102
+ return;
103
+ }
104
+
94
105
  const mode = getCurrentMode(projectDir);
95
106
 
96
107
  if (isPathAllowed(filePath, projectDir, mode)) {
@@ -38,6 +38,7 @@ const AGENT_MODELS = {
38
38
  tasks: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
39
39
  'qa-planning': { provider: 'claude', model: 'opus', tier: 'opus' },
40
40
  'qa-implementation': { provider: 'claude', model: 'opus', tier: 'opus' },
41
+ 'qa-visual': { provider: 'claude', model: 'opus', tier: 'opus' },
41
42
  dev: { provider: 'claude', model: 'opus', tier: 'opus' },
42
43
  devops: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
43
44
  };
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-Dev Auto-QA Hook — PostToolUse (runs after Write tool calls)
4
+ *
5
+ * Autonomous-completion v1 (ADR-AUTO-06). When dev writes its handoff file
6
+ * with status=completed and score >= threshold, this hook spawns
7
+ * visual-qa.js detached — 6 viewports + reduced-motion pass — so the next
8
+ * `/chati` turn finds .chati/visual-qa/report.json already populated.
9
+ *
10
+ * The user never types `pnpm build && pnpm start`. visual-qa.js owns its
11
+ * own dev-server (`npx next dev -p <port>` unless --no-server); this hook
12
+ * just spawns it with the right arguments and exits.
13
+ *
14
+ * Opt-out: set `auto_visual_qa: false` in `chati.dev/config.yaml` (or
15
+ * `.chati.dev/config.yaml` for installed projects). Default is true — the
16
+ * zero-commands thesis.
17
+ *
18
+ * Trigger condition:
19
+ * Write tool wrote `artifacts/handoffs/dev-handoff.md` with frontmatter
20
+ * `status: completed` AND `score: >= 90`.
21
+ *
22
+ * Observational: always returns `{}` — never blocks the Write call. The
23
+ * spawn is detached with stdio ignored so it outlives this hook process.
24
+ */
25
+
26
+ import { existsSync, readFileSync } from 'fs';
27
+ import { join } from 'path';
28
+ import { spawn } from 'child_process';
29
+
30
+ export const DEV_SCORE_THRESHOLD = 90;
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Config loader — checks both installed (.chati.dev) and dev (chati.dev).
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export function readAutoVisualQaConfig(projectDir) {
37
+ const candidates = [
38
+ join(projectDir, '.chati.dev', 'config.yaml'),
39
+ join(projectDir, 'chati.dev', 'config.yaml'),
40
+ ];
41
+ for (const p of candidates) {
42
+ if (!existsSync(p)) continue;
43
+ try {
44
+ const raw = readFileSync(p, 'utf-8');
45
+ const match = raw.match(/^\s*auto_visual_qa:\s*(true|false)\s*$/m);
46
+ if (match) return match[1] === 'true';
47
+ } catch { /* fall through — config unreadable, use default */ }
48
+ }
49
+ return true; // default ON per ADR-AUTO-06
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Handoff frontmatter extraction.
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export function extractHandoffMeta(markdown) {
57
+ if (!markdown || !markdown.startsWith('---')) return null;
58
+ const end = markdown.indexOf('\n---', 3);
59
+ if (end === -1) return null;
60
+ const fm = markdown.slice(3, end + 1);
61
+ const meta = {};
62
+ for (const line of fm.split('\n')) {
63
+ const m = line.match(/^([a-zA-Z_][\w-]*):\s*(.+)$/);
64
+ if (m) meta[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
65
+ }
66
+ return meta;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Route discovery (duplicate of cli.js discoverAppRoutes — hooks cannot
71
+ // import package modules).
72
+ // ---------------------------------------------------------------------------
73
+
74
+ import { readdirSync } from 'fs';
75
+
76
+ export function discoverRoutes(projectDir) {
77
+ const candidates = [join(projectDir, 'app'), join(projectDir, 'src', 'app')];
78
+ const appDir = candidates.find(d => existsSync(d));
79
+ if (!appDir) return ['/'];
80
+ const routes = [];
81
+ const PAGE_RE = /^page\.(tsx|jsx|ts|js|mdx)$/;
82
+ function walk(dir, segments) {
83
+ let entries;
84
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
85
+ for (const e of entries) {
86
+ if (e.isDirectory()) {
87
+ const name = e.name;
88
+ if (name.startsWith('_')) continue;
89
+ if (name.startsWith('[')) continue;
90
+ const isGroup = name.startsWith('(') && name.endsWith(')');
91
+ const nextSegments = isGroup ? segments : [...segments, name];
92
+ walk(join(dir, e.name), nextSegments);
93
+ } else if (e.isFile() && PAGE_RE.test(e.name)) {
94
+ routes.push(segments.length === 0 ? '/' : '/' + segments.join('/'));
95
+ }
96
+ }
97
+ }
98
+ walk(appDir, []);
99
+ const unique = [...new Set(routes)].sort();
100
+ return unique.length > 0 ? unique : ['/'];
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Spawn decision — returns the command + args to run, or null when auto-QA
105
+ // should not fire. Pure for testability.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * @param {{ projectDir: string, toolName: string, filePath: string }} ctx
110
+ * @returns {{ command: string, args: string[] } | null}
111
+ */
112
+ export function decideSpawn(ctx) {
113
+ if (ctx.toolName !== 'Write') return null;
114
+ if (!ctx.filePath) return null;
115
+
116
+ // Match only dev-handoff.md in the canonical paths.
117
+ const handoffMatch = ctx.filePath.match(/artifacts\/handoffs\/dev-handoff\.md$/);
118
+ if (!handoffMatch) return null;
119
+
120
+ // Read the handoff to confirm status=completed and score passes threshold.
121
+ let meta = null;
122
+ try {
123
+ const raw = readFileSync(ctx.filePath, 'utf-8');
124
+ meta = extractHandoffMeta(raw);
125
+ } catch { return null; }
126
+ if (!meta) return null;
127
+ if (meta.status !== 'completed') return null;
128
+ const score = parseInt(meta.score, 10);
129
+ if (!Number.isFinite(score) || score < DEV_SCORE_THRESHOLD) return null;
130
+
131
+ // Config opt-out wins.
132
+ if (!readAutoVisualQaConfig(ctx.projectDir)) return null;
133
+
134
+ // Resolve script path.
135
+ const scriptCandidates = [
136
+ join(ctx.projectDir, '.chati.dev', 'scripts', 'visual-qa.js'),
137
+ join(ctx.projectDir, 'chati.dev', 'scripts', 'visual-qa.js'),
138
+ ];
139
+ const script = scriptCandidates.find(existsSync);
140
+ if (!script) return null;
141
+
142
+ const routes = discoverRoutes(ctx.projectDir);
143
+ const output = join(ctx.projectDir, '.chati', 'visual-qa');
144
+
145
+ return {
146
+ command: 'node',
147
+ args: [
148
+ script,
149
+ '--pages', ...routes,
150
+ '--output', output,
151
+ '--reduced-motion', 'on', // accessibility pass; gate in P6 reads this
152
+ ],
153
+ };
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Hook entry point.
158
+ // ---------------------------------------------------------------------------
159
+
160
+ async function readStdin() {
161
+ let data = '';
162
+ for await (const chunk of process.stdin) data += chunk;
163
+ return data;
164
+ }
165
+
166
+ async function main() {
167
+ let input;
168
+ try {
169
+ input = JSON.parse(await readStdin() || '{}');
170
+ } catch {
171
+ process.stdout.write('{}\n');
172
+ return;
173
+ }
174
+
175
+ const ctx = {
176
+ projectDir: input.cwd || process.cwd(),
177
+ toolName: input.tool_name || '',
178
+ filePath: (input.tool_input && input.tool_input.file_path) || '',
179
+ };
180
+
181
+ // Observational default — the hook never blocks the Write call.
182
+ process.stdout.write('{}\n');
183
+
184
+ const plan = decideSpawn(ctx);
185
+ if (!plan) return;
186
+
187
+ try {
188
+ const child = spawn(plan.command, plan.args, {
189
+ cwd: ctx.projectDir,
190
+ detached: true,
191
+ stdio: 'ignore',
192
+ });
193
+ child.unref();
194
+ } catch (err) {
195
+ process.stderr.write(`[chati] post-dev spawn failed: ${err.message}\n`);
196
+ }
197
+ }
198
+
199
+ import { fileURLToPath } from 'url';
200
+ import { realpathSync } from 'fs';
201
+ if (process.argv[1]) {
202
+ try {
203
+ if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
204
+ main();
205
+ }
206
+ } catch { /* not invoked as a script */ }
207
+ }