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,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');
|
|
@@ -115,7 +164,9 @@ async function main() {
|
|
|
115
164
|
process.stderr.write(`[chati] session-digest daily-append: ${err.message}\n`);
|
|
116
165
|
}
|
|
117
166
|
|
|
118
|
-
// Auto memory consolidation: trigger when memories accumulate
|
|
167
|
+
// Auto memory consolidation: trigger when memories accumulate.
|
|
168
|
+
// CRITICAL: must be fully async (detached spawn) — PreCompact runs on the
|
|
169
|
+
// hot path and execSync would add up to 10s of latency to every compact.
|
|
119
170
|
try {
|
|
120
171
|
const memBase = join(projectDir, '.chati', 'memories');
|
|
121
172
|
if (existsSync(memBase)) {
|
|
@@ -132,16 +183,19 @@ async function main() {
|
|
|
132
183
|
// Consolidate when > 100 entries (50% of 200 cap)
|
|
133
184
|
if (totalEntries > 100) {
|
|
134
185
|
process.stderr.write(`[chati] auto-dream: ${totalEntries} entries, triggering consolidation\n`);
|
|
135
|
-
// Fire-and-forget
|
|
136
|
-
|
|
137
|
-
const { execSync } = await import('child_process');
|
|
186
|
+
// Fire-and-forget detached spawn — returns immediately, child outlives parent.
|
|
187
|
+
const { spawn } = await import('child_process');
|
|
138
188
|
try {
|
|
139
|
-
|
|
189
|
+
const child = spawn('node', [
|
|
190
|
+
'-e',
|
|
191
|
+
`import(${JSON.stringify(projectDir + '/node_modules/chati-dev/src/memory/dream.js')}).then(m => m.runDreamConsolidation(${JSON.stringify(projectDir)}))`,
|
|
192
|
+
], {
|
|
140
193
|
cwd: projectDir,
|
|
141
|
-
|
|
194
|
+
detached: true,
|
|
142
195
|
stdio: 'ignore',
|
|
143
196
|
});
|
|
144
|
-
|
|
197
|
+
child.unref();
|
|
198
|
+
} catch { /* expected: spawn may fail in restricted env */ }
|
|
145
199
|
}
|
|
146
200
|
}
|
|
147
201
|
} catch (err) {
|
|
@@ -149,8 +203,9 @@ async function main() {
|
|
|
149
203
|
}
|
|
150
204
|
}
|
|
151
205
|
|
|
152
|
-
// Session telemetry ping (moved from license-guard
|
|
153
|
-
// 5-minute throttle with phase/agent change detection
|
|
206
|
+
// Session telemetry ping (owned by session-digest, was moved here from license-guard).
|
|
207
|
+
// 5-minute throttle with phase/agent change detection.
|
|
208
|
+
// Pings.yaml format: '/path/to/project': '<ISO8601>|<phase>|<agent>'
|
|
154
209
|
try {
|
|
155
210
|
const homedir = process.env.HOME || '';
|
|
156
211
|
const pingsPath = join(homedir, '.chati-dev', 'pings.yaml');
|
|
@@ -163,15 +218,17 @@ async function main() {
|
|
|
163
218
|
|
|
164
219
|
if (licenseKey && digest) {
|
|
165
220
|
let shouldPing = false;
|
|
166
|
-
const
|
|
221
|
+
const nowIso = new Date().toISOString();
|
|
167
222
|
const currentState = `${digest.mode}|${digest.currentAgent}`;
|
|
168
223
|
|
|
169
224
|
if (existsSync(pingsPath)) {
|
|
170
225
|
const pingsRaw = readFileSync(pingsPath, 'utf-8');
|
|
171
|
-
const
|
|
226
|
+
const escaped = projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
227
|
+
const entryMatch = pingsRaw.match(new RegExp(`^${escaped}:\\s*'([^']+)'`, 'm'));
|
|
172
228
|
if (entryMatch) {
|
|
173
229
|
const [ts, phase, agent] = entryMatch[1].split('|');
|
|
174
|
-
const
|
|
230
|
+
const lastTime = new Date(ts).getTime();
|
|
231
|
+
const elapsed = isNaN(lastTime) ? Infinity : Date.now() - lastTime;
|
|
175
232
|
const stateChanged = `${phase}|${agent}` !== currentState;
|
|
176
233
|
shouldPing = elapsed > 300000 || stateChanged; // 5 min or state change
|
|
177
234
|
} else {
|
|
@@ -182,16 +239,24 @@ async function main() {
|
|
|
182
239
|
}
|
|
183
240
|
|
|
184
241
|
if (shouldPing) {
|
|
185
|
-
// Update ping
|
|
242
|
+
// Update ping entry — preserve other projects in the file.
|
|
186
243
|
mkdirSync(join(homedir, '.chati-dev'), { recursive: true });
|
|
187
|
-
|
|
244
|
+
const newEntry = `${projectDir}: '${nowIso}|${currentState}'\n`;
|
|
245
|
+
let content = existsSync(pingsPath) ? readFileSync(pingsPath, 'utf-8') : '';
|
|
246
|
+
const escaped = projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
247
|
+
if (new RegExp(`^${escaped}:`, 'm').test(content)) {
|
|
248
|
+
content = content.replace(new RegExp(`^${escaped}:.*$`, 'm'), newEntry.trim());
|
|
249
|
+
} else {
|
|
250
|
+
content += newEntry;
|
|
251
|
+
}
|
|
252
|
+
writeFileSync(pingsPath, content, 'utf-8');
|
|
188
253
|
|
|
189
254
|
// Fire-and-forget telemetry (3s timeout, never blocks)
|
|
190
255
|
try {
|
|
191
256
|
const body = JSON.stringify({
|
|
192
257
|
license_key: licenseKey,
|
|
193
258
|
project_name: digest.currentAgent || 'unknown',
|
|
194
|
-
events: [{ type: 'session_active', timestamp:
|
|
259
|
+
events: [{ type: 'session_active', timestamp: nowIso, properties: { pipeline_phase: digest.mode, current_agent: digest.currentAgent } }],
|
|
195
260
|
});
|
|
196
261
|
fetch('https://chati.dev/api/telemetry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
197
262
|
} catch { /* expected: network may be unavailable */ }
|
|
@@ -202,15 +267,24 @@ async function main() {
|
|
|
202
267
|
process.stderr.write(`[chati] session-digest telemetry-ping: ${err.message}\n`);
|
|
203
268
|
}
|
|
204
269
|
|
|
205
|
-
|
|
270
|
+
// PreCompact is observational only — empty output is the canonical "no-op".
|
|
271
|
+
process.stdout.write('{}');
|
|
206
272
|
} catch (err) {
|
|
207
273
|
process.stderr.write(`[chati] session-digest: ${err.message}\n`);
|
|
208
|
-
process.stdout.write(
|
|
274
|
+
process.stdout.write('{}');
|
|
209
275
|
}
|
|
210
276
|
}
|
|
211
277
|
|
|
212
|
-
// Only run main when executed directly (not imported by tests)
|
|
278
|
+
// Only run main when executed directly (not imported by tests).
|
|
279
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
280
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
281
|
+
// silently and main() never runs.
|
|
213
282
|
import { fileURLToPath } from 'url';
|
|
214
|
-
|
|
215
|
-
|
|
283
|
+
import { realpathSync } from 'fs';
|
|
284
|
+
if (process.argv[1]) {
|
|
285
|
+
try {
|
|
286
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
287
|
+
main();
|
|
288
|
+
}
|
|
289
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
216
290
|
}
|