chati-dev 4.2.2 → 4.3.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.
- package/README.md +80 -53
- package/bin/chati.js +170 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +158 -22
- package/framework/agents/discover/brownfield-wu.md +26 -14
- package/framework/agents/discover/greenfield-wu.md +102 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +38 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +448 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +7 -6
- package/framework/constitution.md +127 -44
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +24 -5
- package/framework/hooks/license-guard.js +62 -27
- package/framework/hooks/mode-governance.js +13 -2
- package/framework/hooks/model-governance.js +1 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +274 -105
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +50 -1
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +25 -6
- package/framework/hooks/team-quality-gate.js +19 -12
- package/framework/hooks/undercover-guard.js +4 -2
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +441 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +295 -94
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +99 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
- package/framework/scaffold/motion-premium-3d/README.md +90 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +686 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +2 -2
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +12 -6
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +27 -7
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +19 -7
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +431 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +46 -29
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1880 -71
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +339 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -505
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- 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
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
10
|
+
* - Cache VALID → allow silently (always trust cached VALID status)
|
|
11
11
|
* - Cache EXPIRED/INVALID → block with renewal message
|
|
12
|
-
* - Cache stale (>
|
|
13
|
-
* - API unreachable
|
|
14
|
-
*
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
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 —
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
process.stdout.write(isPreToolUse
|
|
125
|
+
? JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
|
|
126
|
+
: '{}');
|
|
104
127
|
} else {
|
|
105
|
-
|
|
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 { /*
|
|
108
|
-
|
|
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
|
-
//
|
|
123
|
-
|
|
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
|
|
19
|
-
description: 'Planning mode: write only to 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
|
+
}
|