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.
- 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 +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 +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +294 -94
- 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 +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 +422 -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 +1869 -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 +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 -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,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reasoning Escalator Hook — Article XXIII enforcement.
|
|
4
|
+
*
|
|
5
|
+
* Two entry points via event-type branching:
|
|
6
|
+
*
|
|
7
|
+
* UserPromptSubmit:
|
|
8
|
+
* Read the user message, evaluate escalation triggers, update
|
|
9
|
+
* session.reasoning_tier. Non-blocking — always allows the prompt
|
|
10
|
+
* through. Side effect only.
|
|
11
|
+
*
|
|
12
|
+
* PreToolUse (matcher Write|Edit|NotebookEdit):
|
|
13
|
+
* If current tier is 'deliberate' AND the target is a source file
|
|
14
|
+
* AND no recent plan exists in .planning/, DENY the tool call with
|
|
15
|
+
* a message telling the agent to write a plan first. Otherwise allow.
|
|
16
|
+
*
|
|
17
|
+
* Pure evaluation logic is exported for unit tests. The main() entry
|
|
18
|
+
* point handles stdin/stdout and session persistence (regex-based YAML
|
|
19
|
+
* mutation — no js-yaml dependency).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
23
|
+
import { join, isAbsolute, relative } from 'path';
|
|
24
|
+
import { execSync } from 'child_process';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Pure helpers (exported for testing)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const TIERS = ['standard', 'deep', 'deliberate'];
|
|
31
|
+
export const TIER_RANK = { standard: 0, deep: 1, deliberate: 2 };
|
|
32
|
+
|
|
33
|
+
// English-canonical friction keywords — always active regardless of
|
|
34
|
+
// session.language. Article VII mandates English for all framework source
|
|
35
|
+
// material; per-language patterns live in FRICTION_KEYWORDS_LOCALES and are
|
|
36
|
+
// merged on top when session.language selects them.
|
|
37
|
+
export const FRICTION_KEYWORDS_EN = [
|
|
38
|
+
/\bredesign\b/i,
|
|
39
|
+
/\brefactor(?:ing|s)?\b/i,
|
|
40
|
+
/\bstuck\b/i,
|
|
41
|
+
/\b(?:in a |a )?loop\b/i,
|
|
42
|
+
/\bnot working\b/i,
|
|
43
|
+
/\bbreak(?:ing|s)?\b/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Locale-specific patterns merged on top of EN when session.language matches.
|
|
47
|
+
// Each array MUST cover only that locale's phrasings of the same friction
|
|
48
|
+
// concepts (refactoring / not working / stuck). Adding a locale does not
|
|
49
|
+
// mutate the EN set.
|
|
50
|
+
export const FRICTION_KEYWORDS_LOCALES = {
|
|
51
|
+
pt: [
|
|
52
|
+
/\brefatora[çc][aã]o\b/i,
|
|
53
|
+
/\bn[aã]o funciona\b/i,
|
|
54
|
+
/\btravad[oa]\b/i,
|
|
55
|
+
],
|
|
56
|
+
es: [
|
|
57
|
+
/\brefactorizaci[oó]n\b/i,
|
|
58
|
+
/\bno funciona\b/i,
|
|
59
|
+
/\batascad[oa]\b/i,
|
|
60
|
+
],
|
|
61
|
+
fr: [
|
|
62
|
+
/\brefactorisation\b/i,
|
|
63
|
+
/\bne marche pas\b/i,
|
|
64
|
+
/\bbloqu[eé]\b/i,
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the active friction-keyword set for a given language.
|
|
70
|
+
* EN is always included; the locale layer is appended if recognised.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} [language='en'] ISO 639-1 language code from session.language
|
|
73
|
+
* @returns {RegExp[]}
|
|
74
|
+
*/
|
|
75
|
+
export function getFrictionKeywords(language = 'en') {
|
|
76
|
+
const locale = FRICTION_KEYWORDS_LOCALES[language] ?? [];
|
|
77
|
+
return [...FRICTION_KEYWORDS_EN, ...locale];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Backwards-compat default export: English-only set. Callers that care about
|
|
81
|
+
// locale coverage should use getFrictionKeywords(session.language) instead.
|
|
82
|
+
export const FRICTION_KEYWORDS = getFrictionKeywords('en');
|
|
83
|
+
|
|
84
|
+
export const SOURCE_FILE_EXT = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|rb|go|rs|java|cs|php|swift|kt|scala|c|cc|cpp|h|hpp|css|scss|sass|html|vue|svelte|astro|sql)$/i;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse slash-command style explicit tier commands from a user message.
|
|
88
|
+
* Returns one of 'ultraplan' | 'deliberate' | 'quick' | null.
|
|
89
|
+
*/
|
|
90
|
+
export function detectExplicitCommand(msg) {
|
|
91
|
+
if (!msg || typeof msg !== 'string') return null;
|
|
92
|
+
const m = msg.match(/(^|\s)\/(ultraplan|deliberate|quick)\b/i);
|
|
93
|
+
return m ? m[2].toLowerCase() : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pure tier-escalation evaluator. All inputs are explicit so the function
|
|
98
|
+
* is fully deterministic and testable.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} input
|
|
101
|
+
* @param {'standard'|'deep'|'deliberate'} [input.currentTier]
|
|
102
|
+
* @param {number} [input.correctionCycles]
|
|
103
|
+
* @param {number} [input.decisionTrailOpen]
|
|
104
|
+
* @param {number} [input.filesModifiedCount]
|
|
105
|
+
* @param {number} [input.linesModified]
|
|
106
|
+
* @param {string} [input.userMessage]
|
|
107
|
+
* @param {boolean} [input.crossViewportRegression]
|
|
108
|
+
* @param {boolean} [input.scopeEscalation]
|
|
109
|
+
* @param {string} [input.language='en'] ISO 639-1 session language; selects the
|
|
110
|
+
* locale-specific friction-keyword layer on top of the English canonical set.
|
|
111
|
+
* @returns {{ tier: string, triggers: string[], reason: string }}
|
|
112
|
+
*/
|
|
113
|
+
export function evaluateTier(input = {}) {
|
|
114
|
+
const {
|
|
115
|
+
currentTier = 'standard',
|
|
116
|
+
correctionCycles = 0,
|
|
117
|
+
decisionTrailOpen = 0,
|
|
118
|
+
filesModifiedCount = 0,
|
|
119
|
+
linesModified = 0,
|
|
120
|
+
userMessage = '',
|
|
121
|
+
crossViewportRegression = false,
|
|
122
|
+
scopeEscalation = false,
|
|
123
|
+
language = 'en',
|
|
124
|
+
} = input;
|
|
125
|
+
|
|
126
|
+
// Explicit user command always wins.
|
|
127
|
+
const explicit = detectExplicitCommand(userMessage);
|
|
128
|
+
if (explicit === 'quick') {
|
|
129
|
+
return { tier: 'standard', triggers: ['user:/quick'], reason: 'User requested quick mode; tier forced to standard.' };
|
|
130
|
+
}
|
|
131
|
+
if (explicit === 'ultraplan' || explicit === 'deliberate') {
|
|
132
|
+
return { tier: 'deliberate', triggers: [`user:/${explicit}`], reason: 'User requested deliberate mode.' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const triggers = [];
|
|
136
|
+
let targetTier = 'standard';
|
|
137
|
+
const promote = (toTier, label) => {
|
|
138
|
+
triggers.push(label);
|
|
139
|
+
if (TIER_RANK[toTier] > TIER_RANK[targetTier]) targetTier = toTier;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (correctionCycles >= 2) promote('deliberate', `correction_cycles=${correctionCycles}`);
|
|
143
|
+
if (scopeEscalation) promote('deliberate', 'scope_escalation');
|
|
144
|
+
if (filesModifiedCount > 5) promote('deep', `files_modified=${filesModifiedCount}`);
|
|
145
|
+
if (linesModified > 200) promote('deep', `lines_modified=${linesModified}`);
|
|
146
|
+
if (decisionTrailOpen > 2) promote('deep', `decision_trail_open=${decisionTrailOpen}`);
|
|
147
|
+
if (crossViewportRegression) promote('deep', 'cross_viewport_regression');
|
|
148
|
+
const frictionSet = getFrictionKeywords(language);
|
|
149
|
+
if (userMessage && frictionSet.some(r => r.test(userMessage))) {
|
|
150
|
+
const matched = frictionSet.filter(r => r.test(userMessage)).length;
|
|
151
|
+
promote('deep', `friction_keywords=${matched}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Monotonic: never auto-demote within a task.
|
|
155
|
+
const finalTier = TIER_RANK[targetTier] > TIER_RANK[currentTier] ? targetTier : currentTier;
|
|
156
|
+
const reason = triggers.length === 0
|
|
157
|
+
? `No escalation signals. Keeping tier "${finalTier}".`
|
|
158
|
+
: `Triggers (${triggers.length}): ${triggers.join(', ')}`;
|
|
159
|
+
return { tier: finalTier, triggers, reason };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check whether a deliberate-tier tool call must be blocked pending a plan.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} input
|
|
166
|
+
* @param {string} input.tier
|
|
167
|
+
* @param {string} input.toolName
|
|
168
|
+
* @param {object} input.toolInput
|
|
169
|
+
* @param {string} input.projectDir
|
|
170
|
+
* @param {number} [input.sessionStartedAt] - ms epoch; plans older than this do not count
|
|
171
|
+
* @returns {{ block: boolean, reason?: string, message?: string }}
|
|
172
|
+
*/
|
|
173
|
+
export function checkDeliberatePlanRequirement(input) {
|
|
174
|
+
const { tier, toolName, toolInput, projectDir, sessionStartedAt = 0 } = input || {};
|
|
175
|
+
if (tier !== 'deliberate') return { block: false };
|
|
176
|
+
if (!['Edit', 'Write', 'NotebookEdit'].includes(toolName)) return { block: false };
|
|
177
|
+
|
|
178
|
+
const rawTarget = (toolInput && (toolInput.file_path || toolInput.path)) || '';
|
|
179
|
+
if (!rawTarget) return { block: false };
|
|
180
|
+
|
|
181
|
+
const target = String(rawTarget);
|
|
182
|
+
|
|
183
|
+
// Writing TO a plan file is permitted (that's how the agent satisfies the gate).
|
|
184
|
+
if (/(^|\/)\.planning\//.test(target)) return { block: false };
|
|
185
|
+
// Writing to non-source files (configs, metadata) is permitted.
|
|
186
|
+
if (!SOURCE_FILE_EXT.test(target)) return { block: false };
|
|
187
|
+
|
|
188
|
+
const planningDir = projectDir ? join(projectDir, '.planning') : null;
|
|
189
|
+
if (!planningDir || !existsSync(planningDir)) {
|
|
190
|
+
return {
|
|
191
|
+
block: true,
|
|
192
|
+
reason: 'deliberate_tier_requires_plan',
|
|
193
|
+
message: 'Reasoning tier is DELIBERATE (Article XXIII). Create .planning/<task>-plan.md with approach, alternatives, risks, and acceptance criteria BEFORE editing code.',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let entries = [];
|
|
198
|
+
try { entries = readdirSync(planningDir); } catch { /* ignore */ }
|
|
199
|
+
const hasRecentPlan = entries.some(f => {
|
|
200
|
+
if (!f.endsWith('.md')) return false;
|
|
201
|
+
try {
|
|
202
|
+
const s = statSync(join(planningDir, f));
|
|
203
|
+
return s.mtimeMs >= sessionStartedAt;
|
|
204
|
+
} catch { return false; }
|
|
205
|
+
});
|
|
206
|
+
if (hasRecentPlan) return { block: false };
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
block: true,
|
|
210
|
+
reason: 'deliberate_tier_requires_plan',
|
|
211
|
+
message: 'Reasoning tier is DELIBERATE (Article XXIII). No plan file in .planning/ newer than the session start. Write the plan before editing code.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// I/O wrappers (not exercised in unit tests; covered by integration)
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse the reasoning_tier and started_at fields out of session.yaml using
|
|
221
|
+
* regex. Stays dependency-free (hooks do not import js-yaml).
|
|
222
|
+
*/
|
|
223
|
+
export function readSessionTier(projectDir) {
|
|
224
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
225
|
+
if (!existsSync(sessionPath)) return { tier: 'standard', startedAt: 0, raw: null };
|
|
226
|
+
let raw = '';
|
|
227
|
+
try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return { tier: 'standard', startedAt: 0, raw: null }; }
|
|
228
|
+
const tierMatch = raw.match(/^\s*reasoning_tier:\s*(["']?)(\w+)\1\s*$/m);
|
|
229
|
+
const startedMatch = raw.match(/^\s*started_at:\s*(["']?)([^"'\n]+)\1\s*$/m);
|
|
230
|
+
let startedAt = 0;
|
|
231
|
+
if (startedMatch) {
|
|
232
|
+
const t = Date.parse(startedMatch[2]);
|
|
233
|
+
if (!Number.isNaN(t)) startedAt = t;
|
|
234
|
+
}
|
|
235
|
+
const tier = tierMatch ? tierMatch[2] : 'standard';
|
|
236
|
+
return { tier: TIERS.includes(tier) ? tier : 'standard', startedAt, raw };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse session.language out of session.yaml. Defaults to 'en' if the key is
|
|
241
|
+
* missing or unreadable. Kept regex-based to avoid a js-yaml dep in hooks.
|
|
242
|
+
*/
|
|
243
|
+
export function readSessionLanguage(projectDir) {
|
|
244
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
245
|
+
if (!existsSync(sessionPath)) return 'en';
|
|
246
|
+
let raw = '';
|
|
247
|
+
try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return 'en'; }
|
|
248
|
+
const m = raw.match(/^\s*language:\s*(["']?)([a-z]{2})\1\s*$/m);
|
|
249
|
+
return m ? m[2] : 'en';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Update session.yaml with a new reasoning_tier value. Append `reasoning_tier:`
|
|
254
|
+
* if missing; replace in place if present. Silent on failure.
|
|
255
|
+
*/
|
|
256
|
+
export function writeSessionTier(projectDir, tier) {
|
|
257
|
+
if (!TIERS.includes(tier)) return false;
|
|
258
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
259
|
+
if (!existsSync(sessionPath)) return false;
|
|
260
|
+
let raw = '';
|
|
261
|
+
try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return false; }
|
|
262
|
+
const line = `reasoning_tier: ${tier}`;
|
|
263
|
+
let next;
|
|
264
|
+
if (/^\s*reasoning_tier:\s*\w+\s*$/m.test(raw)) {
|
|
265
|
+
next = raw.replace(/^\s*reasoning_tier:\s*\w+\s*$/m, line);
|
|
266
|
+
} else {
|
|
267
|
+
next = raw.trimEnd() + '\n' + line + '\n';
|
|
268
|
+
}
|
|
269
|
+
try { writeFileSync(sessionPath, next, 'utf-8'); return true; } catch { return false; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Count files modified since HEAD via git. Returns -1 when git is unavailable.
|
|
274
|
+
*/
|
|
275
|
+
export function gitDiffStats(projectDir) {
|
|
276
|
+
if (!projectDir) return { files: -1, lines: -1 };
|
|
277
|
+
try {
|
|
278
|
+
const files = execSync('git diff --name-only HEAD 2>/dev/null', {
|
|
279
|
+
cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
280
|
+
}).trim().split('\n').filter(Boolean);
|
|
281
|
+
let lines = 0;
|
|
282
|
+
try {
|
|
283
|
+
const stat = execSync('git diff --shortstat HEAD 2>/dev/null', {
|
|
284
|
+
cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
285
|
+
}).trim();
|
|
286
|
+
const m = stat.match(/(\d+)\s+insertion.*?(\d+)\s+deletion/);
|
|
287
|
+
if (m) lines = parseInt(m[1], 10) + parseInt(m[2], 10);
|
|
288
|
+
else {
|
|
289
|
+
const ins = stat.match(/(\d+)\s+insertion/);
|
|
290
|
+
if (ins) lines = parseInt(ins[1], 10);
|
|
291
|
+
}
|
|
292
|
+
} catch { /* ignore */ }
|
|
293
|
+
return { files: files.length, lines };
|
|
294
|
+
} catch {
|
|
295
|
+
return { files: -1, lines: -1 };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function allowOutput() {
|
|
300
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } };
|
|
301
|
+
}
|
|
302
|
+
function denyOutput(reason) {
|
|
303
|
+
return {
|
|
304
|
+
hookSpecificOutput: {
|
|
305
|
+
hookEventName: 'PreToolUse',
|
|
306
|
+
permissionDecision: 'deny',
|
|
307
|
+
permissionDecisionReason: reason,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function userPromptPassthrough() {
|
|
312
|
+
// UserPromptSubmit hooks emit a different shape; nothing to block here.
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function main() {
|
|
317
|
+
let input = '';
|
|
318
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const event = JSON.parse(input || '{}');
|
|
322
|
+
const projectDir = event.cwd || process.cwd();
|
|
323
|
+
const hookEventName = event.hook_event_name || event.hookEventName || '';
|
|
324
|
+
|
|
325
|
+
if (hookEventName === 'UserPromptSubmit') {
|
|
326
|
+
const userMessage = event.user_message || event.prompt || '';
|
|
327
|
+
const { tier: currentTier, startedAt: _s } = readSessionTier(projectDir);
|
|
328
|
+
const language = readSessionLanguage(projectDir);
|
|
329
|
+
const { files: filesModifiedCount, lines: linesModified } = gitDiffStats(projectDir);
|
|
330
|
+
const result = evaluateTier({
|
|
331
|
+
currentTier,
|
|
332
|
+
userMessage,
|
|
333
|
+
filesModifiedCount: Math.max(0, filesModifiedCount),
|
|
334
|
+
linesModified: Math.max(0, linesModified),
|
|
335
|
+
language,
|
|
336
|
+
});
|
|
337
|
+
if (result.tier !== currentTier) {
|
|
338
|
+
writeSessionTier(projectDir, result.tier);
|
|
339
|
+
}
|
|
340
|
+
process.stdout.write(JSON.stringify(userPromptPassthrough()));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (hookEventName === 'PreToolUse') {
|
|
345
|
+
const toolName = event.tool_name || '';
|
|
346
|
+
const toolInput = event.tool_input || {};
|
|
347
|
+
const { tier, startedAt } = readSessionTier(projectDir);
|
|
348
|
+
const verdict = checkDeliberatePlanRequirement({
|
|
349
|
+
tier, toolName, toolInput, projectDir, sessionStartedAt: startedAt,
|
|
350
|
+
});
|
|
351
|
+
if (verdict.block) {
|
|
352
|
+
process.stdout.write(JSON.stringify(denyOutput(`[Article XXIII] ${verdict.message}`)));
|
|
353
|
+
} else {
|
|
354
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Unknown event — pass through.
|
|
360
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
361
|
+
} catch (err) {
|
|
362
|
+
// Fail-OPEN: hook errors must not block user work.
|
|
363
|
+
process.stderr.write(`[chati-hook-error] reasoning-escalator: ${err?.message || 'unknown'}\n`);
|
|
364
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Only run main() when invoked as a script (tests can import pure helpers).
|
|
369
|
+
if (import.meta.url === `file://${process.argv[1] || ''}`) {
|
|
370
|
+
main();
|
|
371
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reference Trigger Hook — PostToolUse (runs after Write tool)
|
|
4
|
+
*
|
|
5
|
+
* When the Brief agent writes brief-report.md, this hook:
|
|
6
|
+
* 1. Extracts visual_references URLs
|
|
7
|
+
* 2. Launches reference-capture.js in BACKGROUND (detached)
|
|
8
|
+
* 3. Appends a "Visual Reference Captures" note to brief-report.md
|
|
9
|
+
*
|
|
10
|
+
* The capture runs in background so Claude doesn't wait ~9 min.
|
|
11
|
+
* The script writes capture-complete.json when ALL sites finish.
|
|
12
|
+
* Brand Architect polls for this file before spawning Visualizer.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
|
|
19
|
+
async function readStdin() {
|
|
20
|
+
let data = '';
|
|
21
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
let input;
|
|
27
|
+
try {
|
|
28
|
+
input = JSON.parse(await readStdin() || '{}');
|
|
29
|
+
} catch {
|
|
30
|
+
process.stdout.write('{}\n');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toolName = input.tool_name || '';
|
|
35
|
+
const toolInput = input.tool_input || {};
|
|
36
|
+
const filePath = toolInput.file_path || '';
|
|
37
|
+
const cwd = input.cwd || process.cwd();
|
|
38
|
+
|
|
39
|
+
process.stdout.write('{}\n');
|
|
40
|
+
|
|
41
|
+
if (toolName !== 'Write') return;
|
|
42
|
+
if (!filePath.endsWith('1-Brief/brief-report.md')) return;
|
|
43
|
+
if (!existsSync(filePath)) return;
|
|
44
|
+
|
|
45
|
+
let content;
|
|
46
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
|
|
47
|
+
|
|
48
|
+
// Don't re-run if already triggered (idempotency)
|
|
49
|
+
if (content.includes('## Visual Reference Captures')) return;
|
|
50
|
+
|
|
51
|
+
// Extract URLs — support both `url: "https://..."` YAML format and bare `- https://...` list items
|
|
52
|
+
const urlPatterns = [
|
|
53
|
+
/url:\s*["']?(https?:\/\/[^\s"']+)["']?/g,
|
|
54
|
+
/^\s*-\s+(https?:\/\/[^\s"']+)/gm,
|
|
55
|
+
];
|
|
56
|
+
const urlSet = new Set();
|
|
57
|
+
for (const pattern of urlPatterns) {
|
|
58
|
+
for (const match of content.matchAll(pattern)) {
|
|
59
|
+
urlSet.add(match[1]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const urls = [...urlSet];
|
|
63
|
+
if (urls.length === 0) return;
|
|
64
|
+
|
|
65
|
+
// Find script + playwright
|
|
66
|
+
const fwCandidates = ['.chati.dev', 'chati.dev'];
|
|
67
|
+
let scriptPath = null;
|
|
68
|
+
let nodePath = process.env.NODE_PATH || '';
|
|
69
|
+
for (const fw of fwCandidates) {
|
|
70
|
+
const p = join(cwd, fw, 'scripts', 'reference-capture.js');
|
|
71
|
+
if (existsSync(p)) { scriptPath = p; break; }
|
|
72
|
+
}
|
|
73
|
+
if (!scriptPath) return;
|
|
74
|
+
for (const fw of fwCandidates) {
|
|
75
|
+
const cliModules = join(cwd, fw, '_cli', 'node_modules');
|
|
76
|
+
if (existsSync(join(cliModules, 'playwright'))) {
|
|
77
|
+
nodePath = nodePath ? `${cliModules}:${nodePath}` : cliModules;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const refOutput = join(cwd, 'artifacts', '4-UX', 'references');
|
|
83
|
+
mkdirSync(refOutput, { recursive: true });
|
|
84
|
+
|
|
85
|
+
// Launch capture in BACKGROUND — don't block Claude.
|
|
86
|
+
// The script writes capture-complete.json when done.
|
|
87
|
+
const child = spawn('node', [scriptPath, '--urls', ...urls, '--output', refOutput], {
|
|
88
|
+
cwd,
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: 'ignore',
|
|
91
|
+
env: { ...process.env, NODE_PATH: nodePath },
|
|
92
|
+
});
|
|
93
|
+
child.unref();
|
|
94
|
+
|
|
95
|
+
// Append note to brief-report.md immediately (capture runs in background)
|
|
96
|
+
const captureSection = `\n\n## Visual Reference Captures
|
|
97
|
+
|
|
98
|
+
> Visual reference capture is running in background for ${urls.length} site(s).
|
|
99
|
+
> Screenshots, scroll videos, and CSS tokens are being captured via Playwright.
|
|
100
|
+
>
|
|
101
|
+
> **IMPORTANT for planning agents:** This section contains preliminary data.
|
|
102
|
+
> A deeper visual analysis of the scroll experience, animation patterns, and design DNA
|
|
103
|
+
> will be performed by the **UX Brand Architect** using the Visualizer agent during planning.
|
|
104
|
+
> Do NOT base final design decisions solely on this summary — wait for the full
|
|
105
|
+
> Visual Experience Analysis in \`artifacts/4-UX/reference-analysis.md\`.
|
|
106
|
+
>
|
|
107
|
+
> **Sites being captured:** ${urls.map(u => `\`${u}\``).join(', ')}
|
|
108
|
+
> **Output:** \`artifacts/4-UX/references/\`
|
|
109
|
+
> **Status:** Check \`artifacts/4-UX/references/capture-complete.json\` for completion status.
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(filePath, content + captureSection, 'utf-8');
|
|
114
|
+
} catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main().catch(() => process.stdout.write('{}\n'));
|
|
@@ -96,6 +96,55 @@ async function main() {
|
|
|
96
96
|
|
|
97
97
|
writeFileSync(join(digestDir, fileName), lines.join('\n') + '\n', 'utf-8');
|
|
98
98
|
|
|
99
|
+
// Memory-bootstrap-v1 (F-A5-006): when the current agent has no
|
|
100
|
+
// MEMORY.md yet (or it is empty), write a minimal one from this digest
|
|
101
|
+
// so prism-engine's L4 memory layer is never empty in early sessions.
|
|
102
|
+
// Inline here instead of importing from src/memory/agent-memory.js
|
|
103
|
+
// because hooks run via raw Node and cannot reliably import the
|
|
104
|
+
// package source on installed projects. The shape mirrors the
|
|
105
|
+
// writeBootstrapMemory() contract in agent-memory.js.
|
|
106
|
+
try {
|
|
107
|
+
const agent = digest.currentAgent;
|
|
108
|
+
if (agent && agent !== 'none') {
|
|
109
|
+
const memDir = join(projectDir, '.chati', 'memories', agent);
|
|
110
|
+
const memPath = join(memDir, 'MEMORY.md');
|
|
111
|
+
let isEmpty = true;
|
|
112
|
+
if (existsSync(memPath)) {
|
|
113
|
+
try { isEmpty = readFileSync(memPath, 'utf-8').trim().length === 0; }
|
|
114
|
+
catch { /* treat as empty */ }
|
|
115
|
+
}
|
|
116
|
+
if (isEmpty) {
|
|
117
|
+
const bootstrap = [];
|
|
118
|
+
bootstrap.push('<!-- bootstrap-write: true -->');
|
|
119
|
+
bootstrap.push('<!-- source: session-digest bootstrap (memory-bootstrap-v1) -->');
|
|
120
|
+
bootstrap.push('');
|
|
121
|
+
bootstrap.push('## Episodic');
|
|
122
|
+
bootstrap.push('');
|
|
123
|
+
bootstrap.push(`- Resumed at mode=${digest.mode}, pipeline_position=${digest.pipelinePosition}, workflow=${digest.workflow || 'unknown'}. (low) [source:bootstrap]`);
|
|
124
|
+
if (digest.active_task) {
|
|
125
|
+
bootstrap.push('');
|
|
126
|
+
bootstrap.push('## Semantic');
|
|
127
|
+
bootstrap.push('');
|
|
128
|
+
bootstrap.push(`- Active task on resume: ${String(digest.active_task).slice(0, 500)} (low) [source:bootstrap]`);
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(digest.gotchas_found) && digest.gotchas_found.length > 0) {
|
|
131
|
+
bootstrap.push('');
|
|
132
|
+
bootstrap.push('## Procedural');
|
|
133
|
+
bootstrap.push('');
|
|
134
|
+
for (const g of digest.gotchas_found.slice(0, 5)) {
|
|
135
|
+
if (!g?.message) continue;
|
|
136
|
+
bootstrap.push(`- Known gotcha [${g.severity || 'info'}] ${g.id || ''}: ${String(g.message).slice(0, 200)} (low) [source:bootstrap, severity:${g.severity || 'info'}]`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
bootstrap.push('');
|
|
140
|
+
mkdirSync(memDir, { recursive: true });
|
|
141
|
+
writeFileSync(memPath, bootstrap.join('\n'), 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
process.stderr.write(`[chati] session-digest bootstrap: ${err.message}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
// Auto daily digest: append session entry on every PreCompact
|
|
100
149
|
try {
|
|
101
150
|
const dailyDir = join(projectDir, '.chati', 'memories', 'shared', 'daily');
|
|
@@ -139,7 +188,7 @@ async function main() {
|
|
|
139
188
|
try {
|
|
140
189
|
const child = spawn('node', [
|
|
141
190
|
'-e',
|
|
142
|
-
`import('
|
|
191
|
+
`import(${JSON.stringify(projectDir + '/node_modules/chati-dev/src/memory/dream.js')}).then(m => m.runDreamConsolidation(${JSON.stringify(projectDir)}))`,
|
|
143
192
|
], {
|
|
144
193
|
cwd: projectDir,
|
|
145
194
|
detached: true,
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
{
|
|
17
17
|
"type": "command",
|
|
18
18
|
"command": "node chati.dev/hooks/model-governance.js"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node chati.dev/hooks/reasoning-escalator.js"
|
|
19
23
|
}
|
|
20
24
|
]
|
|
21
25
|
}
|
|
@@ -31,6 +35,29 @@
|
|
|
31
35
|
]
|
|
32
36
|
}
|
|
33
37
|
],
|
|
38
|
+
"PostToolUse": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Write",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "node chati.dev/hooks/advance-trigger.js"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "node chati.dev/hooks/reference-trigger.js"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "node chati.dev/hooks/brief-validator.js"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "node chati.dev/hooks/post-dev.js"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
],
|
|
34
61
|
"PreToolUse": [
|
|
35
62
|
{
|
|
36
63
|
"matcher": "Read",
|
|
@@ -42,7 +69,7 @@
|
|
|
42
69
|
]
|
|
43
70
|
},
|
|
44
71
|
{
|
|
45
|
-
"matcher": "Write|Edit",
|
|
72
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
46
73
|
"hooks": [
|
|
47
74
|
{
|
|
48
75
|
"type": "command",
|
|
@@ -63,6 +90,10 @@
|
|
|
63
90
|
{
|
|
64
91
|
"type": "command",
|
|
65
92
|
"command": "node chati.dev/hooks/team-quality-gate.js"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "node chati.dev/hooks/reasoning-escalator.js"
|
|
66
97
|
}
|
|
67
98
|
]
|
|
68
99
|
},
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - No em-dashes (U+2013 EN DASH, U+2014 EM DASH)
|
|
7
7
|
* - No emojis in generated content
|
|
8
8
|
*
|
|
9
|
-
* Mode:
|
|
9
|
+
* Mode: Em-dashes are HARD BLOCKED (deny). Emojis are advisory (warn).
|
|
10
10
|
* Feature-flag gated: respects anti_dash and no_emojis in config.yaml.
|
|
11
11
|
*
|
|
12
12
|
* Exempt paths: framework files (chati.dev/, .chati/), YAML configs, CLAUDE.md
|
|
@@ -35,8 +35,9 @@ const EXEMPT_PATTERNS = [
|
|
|
35
35
|
/AGENTS\.md$/,
|
|
36
36
|
/\.yaml$/,
|
|
37
37
|
/\.yml$/,
|
|
38
|
-
/\.json$/,
|
|
39
38
|
/package\.json$/,
|
|
39
|
+
/package-lock\.json$/,
|
|
40
|
+
/tsconfig\.json$/,
|
|
40
41
|
/\.lock$/,
|
|
41
42
|
];
|
|
42
43
|
|
|
@@ -111,11 +112,19 @@ async function main() {
|
|
|
111
112
|
result = checkBashStyle(command);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
if (result.
|
|
115
|
-
//
|
|
116
|
-
//
|
|
115
|
+
if (result.hasDash) {
|
|
116
|
+
// Em-dashes are HARD BLOCKED — they are never acceptable in project output.
|
|
117
|
+
// Use commas, semicolons, colons, or periods instead.
|
|
118
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
119
|
+
`[Style Guard] Em-dash or en-dash detected. REWRITE without em-dashes. Use commas, semicolons, colons, or periods instead. This is non-negotiable.`
|
|
120
|
+
)));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (result.hasEmoji) {
|
|
125
|
+
// Emojis are advisory — warn but allow
|
|
117
126
|
process.stdout.write(JSON.stringify(allowOutput(
|
|
118
|
-
`[Style Guard]
|
|
127
|
+
`[Style Guard] Emoji detected. Consider using icons from approved libraries instead.`
|
|
119
128
|
)));
|
|
120
129
|
return;
|
|
121
130
|
}
|
|
@@ -127,6 +136,16 @@ async function main() {
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|
|
139
|
+
function denyOutput(reason) {
|
|
140
|
+
return {
|
|
141
|
+
hookSpecificOutput: {
|
|
142
|
+
hookEventName: 'PreToolUse',
|
|
143
|
+
permissionDecision: 'deny',
|
|
144
|
+
permissionDecisionReason: reason,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
function allowOutput(reason) {
|
|
131
150
|
const out = {
|
|
132
151
|
hookSpecificOutput: {
|