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.
- package/README.md +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -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 +36 -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 +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +8 -7
- package/framework/constitution.md +127 -43
- 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 +91 -36
- package/framework/hooks/license-guard.js +129 -190
- package/framework/hooks/mode-governance.js +68 -16
- package/framework/hooks/model-governance.js +19 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +288 -105
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +94 -20
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +52 -13
- package/framework/hooks/team-quality-gate.js +58 -25
- package/framework/hooks/undercover-guard.js +34 -13
- 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 +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +318 -115
- package/framework/scaffold/motion-premium/README.md +89 -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 +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -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 +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -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 +1 -1
- 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 +10 -5
- 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 +226 -0
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +76 -6
- 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 +557 -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 +52 -31
- 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 +1908 -80
- 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 +331 -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 -367
- 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,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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
115
|
-
process.stdout.write(JSON.stringify(
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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: '
|
|
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: '
|
|
142
|
-
{ id: 'OBFUSCATED_FLAGS', pattern: /\$[({].*[)}].*-/, severity: '
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{ id: '
|
|
146
|
-
|
|
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: '
|
|
155
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
182
|
-
|
|
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
|
}
|