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
|
@@ -3,229 +3,159 @@
|
|
|
3
3
|
* License Guard Hook — UserPromptSubmit
|
|
4
4
|
*
|
|
5
5
|
* Validates license status before each user prompt turn.
|
|
6
|
-
*
|
|
6
|
+
* Once activated, the license is trusted until explicitly revoked by the API.
|
|
7
7
|
*
|
|
8
8
|
* Behavior:
|
|
9
|
-
* - No
|
|
10
|
-
* - Cache VALID
|
|
11
|
-
* - Cache EXPIRED/INVALID
|
|
12
|
-
* - Cache stale (> 24h)
|
|
13
|
-
* - API unreachable
|
|
9
|
+
* - No license file → block with activation instructions
|
|
10
|
+
* - Cache VALID → allow silently (always trust cached VALID status)
|
|
11
|
+
* - Cache EXPIRED/INVALID → block with renewal message
|
|
12
|
+
* - Cache stale (> 24h) + API reachable → refresh cache in background
|
|
13
|
+
* - API unreachable → use cached status (no blocking on network issues)
|
|
14
|
+
*
|
|
15
|
+
* Rationale: users activate once at install time. After that, the hook should
|
|
16
|
+
* never block due to cache expiry or network issues. Only explicit server-side
|
|
17
|
+
* revocation (EXPIRED/INVALID status returned by API) blocks the user.
|
|
18
|
+
*
|
|
19
|
+
* Telemetry pings are owned by session-digest.js (PreCompact event), not here.
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
17
|
-
import { join
|
|
23
|
+
import { join } from 'path';
|
|
18
24
|
import { homedir } from 'os';
|
|
19
25
|
|
|
20
26
|
const API_BASE = 'https://chati.dev/api';
|
|
21
|
-
const
|
|
22
|
-
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — shorter window to catch expirations faster
|
|
23
|
-
const PING_THROTTLE_MS = 5 * 60 * 1000; // 5 minutes
|
|
27
|
+
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
24
28
|
|
|
25
29
|
const GLOBAL_DIR = join(homedir(), '.chati-dev');
|
|
26
30
|
const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
|
|
27
|
-
const PINGS_PATH = join(GLOBAL_DIR, 'pings.yaml');
|
|
28
|
-
|
|
29
|
-
async function main() {
|
|
30
|
-
let input = '';
|
|
31
|
-
for await (const chunk of process.stdin) input += chunk;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// Read global license file — REQUIRED for operation
|
|
35
|
-
if (!existsSync(LICENSE_PATH)) {
|
|
36
|
-
block('No license found. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const licenseRaw = readFileSync(LICENSE_PATH, 'utf-8');
|
|
41
|
-
const licenseKey = readYamlField(licenseRaw, 'key');
|
|
42
|
-
|
|
43
|
-
if (!licenseKey || licenseKey === 'null') {
|
|
44
|
-
block('No license key configured. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Cache stale — call API
|
|
65
|
-
try {
|
|
66
|
-
const machineId = await computeMachineId();
|
|
67
|
-
const res = await fetch(
|
|
68
|
-
`${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
69
|
-
{ signal: AbortSignal.timeout(5000) }
|
|
70
|
-
);
|
|
71
|
-
const data = await res.json();
|
|
32
|
+
/**
|
|
33
|
+
* Pure license validation function — provider-agnostic.
|
|
34
|
+
* Returns a verdict object that hook wrappers can translate to their
|
|
35
|
+
* provider's blocking schema (Claude UserPromptSubmit, Gemini BeforeModel,
|
|
36
|
+
* Codex UserPromptSubmit).
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<{valid: boolean, reason?: string, status?: string}>}
|
|
39
|
+
*/
|
|
40
|
+
export async function checkLicense() {
|
|
41
|
+
// Read global license file — REQUIRED for operation
|
|
42
|
+
if (!existsSync(LICENSE_PATH)) {
|
|
43
|
+
return { valid: false, reason: 'No license found. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
|
|
44
|
+
}
|
|
72
45
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const updated = {
|
|
76
|
-
...existing,
|
|
77
|
-
status: data.status,
|
|
78
|
-
plan: data.plan ?? '',
|
|
79
|
-
days_remaining: data.days_remaining ?? '',
|
|
80
|
-
expires_at: data.expires_at ?? '',
|
|
81
|
-
reason: data.reason ?? '',
|
|
82
|
-
checked_at: new Date().toISOString(),
|
|
83
|
-
};
|
|
84
|
-
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
85
|
-
writeFileSync(LICENSE_PATH, dumpYaml(updated));
|
|
46
|
+
const licenseRaw = readFileSync(LICENSE_PATH, 'utf-8');
|
|
47
|
+
const licenseKey = readYamlField(licenseRaw, 'key');
|
|
86
48
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
block(buildMessage(data.status, data.reason));
|
|
92
|
-
} catch {
|
|
93
|
-
// API unreachable — check if we have a recent VALID cache to fall back on
|
|
94
|
-
// If cache is less than 24h old AND was VALID, allow (grace period for network issues)
|
|
95
|
-
// Otherwise block — we cannot verify the license
|
|
96
|
-
if (age < 24 * 60 * 60 * 1000 && status === 'VALID') {
|
|
97
|
-
allow(); // grace: last check was recent and valid, allow despite network issue
|
|
98
|
-
} else {
|
|
99
|
-
block('Unable to verify license (network issue). If this persists, check your connection.\nRun: npx chati-dev activate --key=YOUR-KEY');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} catch { /* expected: stdin parse may fail — fail open */
|
|
103
|
-
allow();
|
|
49
|
+
if (!licenseKey || licenseKey === 'null') {
|
|
50
|
+
return { valid: false, reason: 'No license key configured. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
|
|
104
51
|
}
|
|
105
|
-
}
|
|
106
52
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
53
|
+
const status = readYamlField(licenseRaw, 'status');
|
|
54
|
+
const checkedAt = readYamlField(licenseRaw, 'checked_at');
|
|
55
|
+
const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
|
|
110
56
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
57
|
+
// EXPLICIT REVOCATION — block immediately
|
|
58
|
+
// Server explicitly said this license is no longer valid.
|
|
59
|
+
if (status === 'EXPIRED' || status === 'INVALID' || status === 'REVOKED') {
|
|
60
|
+
return { valid: false, reason: buildMessage(status, readYamlField(licenseRaw, 'reason')), status };
|
|
61
|
+
}
|
|
115
62
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
allow();
|
|
120
|
-
await pingPromise; // keep process alive until ping completes (max 3s)
|
|
121
|
-
} else {
|
|
122
|
-
allow();
|
|
63
|
+
// Fresh cache (< CACHE_TTL_MS) — trust without API call
|
|
64
|
+
if (status === 'VALID' && age < CACHE_TTL_MS) {
|
|
65
|
+
return { valid: true, status: 'VALID' };
|
|
123
66
|
}
|
|
124
|
-
}
|
|
125
67
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*/
|
|
130
|
-
function readSessionContext() {
|
|
68
|
+
// Stale cache — try background refresh.
|
|
69
|
+
// CRITICAL: cached VALID is trusted indefinitely. API call only updates the
|
|
70
|
+
// cache or detects revocation. Network failures NEVER block the user.
|
|
131
71
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
72
|
+
const machineId = await computeMachineId();
|
|
73
|
+
const res = await fetch(
|
|
74
|
+
`${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
75
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
76
|
+
);
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
|
|
79
|
+
// Persist updated status (keep existing key)
|
|
80
|
+
const existing = parseYaml(licenseRaw);
|
|
81
|
+
const updated = {
|
|
82
|
+
...existing,
|
|
83
|
+
status: data.status,
|
|
84
|
+
plan: data.plan ?? '',
|
|
85
|
+
days_remaining: data.days_remaining ?? '',
|
|
86
|
+
expires_at: data.expires_at ?? '',
|
|
87
|
+
reason: data.reason ?? '',
|
|
88
|
+
checked_at: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
91
|
+
writeFileSync(LICENSE_PATH, dumpYaml(updated));
|
|
92
|
+
|
|
93
|
+
if (data.status === 'VALID') return { valid: true, status: 'VALID' };
|
|
94
|
+
return { valid: false, reason: buildMessage(data.status, data.reason), status: data.status };
|
|
95
|
+
} catch {
|
|
96
|
+
// API unreachable — trust the last known status indefinitely.
|
|
97
|
+
// The user activated once; we never block due to network issues.
|
|
98
|
+
if (status === 'VALID') {
|
|
99
|
+
return { valid: true, status: 'VALID', graced: true };
|
|
100
|
+
}
|
|
101
|
+
// Status is unknown/null and API is unreachable — only now we ask user to retry.
|
|
102
|
+
return { valid: false, reason: 'License status unknown and unable to reach validation server. Try again when online.\nRun: npx chati-dev activate --key=YOUR-KEY', status: 'NETWORK_ERROR' };
|
|
103
|
+
}
|
|
150
104
|
}
|
|
151
105
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*/
|
|
156
|
-
function shouldPing(projectPath, session = {}) {
|
|
157
|
-
try {
|
|
158
|
-
if (!existsSync(PINGS_PATH)) return true;
|
|
159
|
-
const content = readFileSync(PINGS_PATH, 'utf-8');
|
|
160
|
-
const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
161
|
-
const match = content.match(new RegExp(`^${escaped}:\\s*'?([^'\\n]+)'?$`, 'm'));
|
|
162
|
-
if (!match) return true;
|
|
163
|
-
|
|
164
|
-
const [timestamp, lastPhase, lastAgent] = match[1].trim().split('|');
|
|
165
|
-
const age = Date.now() - new Date(timestamp).getTime();
|
|
166
|
-
if (age > PING_THROTTLE_MS) return true;
|
|
106
|
+
async function main() {
|
|
107
|
+
let input = '';
|
|
108
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
167
109
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
110
|
+
// Detect hook event from stdin to output the correct schema.
|
|
111
|
+
// PreToolUse needs hookSpecificOutput.permissionDecision.
|
|
112
|
+
// UserPromptSubmit needs exit 2 + stderr (silent block).
|
|
113
|
+
let event = null;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(input);
|
|
116
|
+
event = parsed.hook_event_name || null;
|
|
117
|
+
} catch { /* expected: may not be JSON */ }
|
|
171
118
|
|
|
172
|
-
|
|
173
|
-
} catch { return false; } // fail closed — don't add latency on parse error
|
|
174
|
-
}
|
|
119
|
+
const isPreToolUse = event === 'PreToolUse';
|
|
175
120
|
|
|
176
|
-
function recordPing(projectPath, session = {}) {
|
|
177
121
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let content = existsSync(PINGS_PATH) ? readFileSync(PINGS_PATH, 'utf-8') : '';
|
|
185
|
-
const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
-
if (new RegExp(`^${escaped}:`, 'm').test(content)) {
|
|
187
|
-
content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 '${value}'`);
|
|
122
|
+
const result = await checkLicense();
|
|
123
|
+
if (result.valid) {
|
|
124
|
+
process.stdout.write(isPreToolUse
|
|
125
|
+
? JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
|
|
126
|
+
: '{}');
|
|
188
127
|
} else {
|
|
189
|
-
|
|
128
|
+
if (isPreToolUse) {
|
|
129
|
+
process.stdout.write(JSON.stringify({
|
|
130
|
+
hookSpecificOutput: {
|
|
131
|
+
hookEventName: 'PreToolUse',
|
|
132
|
+
permissionDecision: 'deny',
|
|
133
|
+
permissionDecisionReason: result.reason,
|
|
134
|
+
},
|
|
135
|
+
}));
|
|
136
|
+
} else {
|
|
137
|
+
process.stderr.write(result.reason);
|
|
138
|
+
process.exit(2);
|
|
139
|
+
}
|
|
190
140
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const properties = {};
|
|
198
|
-
if (session.pipeline_phase) properties.pipeline_phase = session.pipeline_phase;
|
|
199
|
-
if (session.current_agent) properties.current_agent = session.current_agent;
|
|
200
|
-
if (session.project_type) properties.project_type = session.project_type;
|
|
201
|
-
|
|
202
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
203
|
-
method: 'POST',
|
|
204
|
-
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
body: JSON.stringify({
|
|
206
|
-
license_key: licenseKey,
|
|
207
|
-
project_name: projectName,
|
|
208
|
-
events: [{
|
|
209
|
-
type: 'session_active',
|
|
210
|
-
timestamp: new Date().toISOString(),
|
|
211
|
-
properties,
|
|
212
|
-
}],
|
|
213
|
-
}),
|
|
214
|
-
signal: AbortSignal.timeout(3000),
|
|
215
|
-
});
|
|
216
|
-
} catch {} // fire-and-forget, always fail silently
|
|
141
|
+
} catch { /* fail open */
|
|
142
|
+
process.stdout.write(isPreToolUse
|
|
143
|
+
? JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
|
|
144
|
+
: '{}');
|
|
145
|
+
}
|
|
217
146
|
}
|
|
218
147
|
|
|
219
148
|
// ---------------------------------------------------------------------------
|
|
220
|
-
// Decision helpers
|
|
149
|
+
// Decision helpers (used only for non-hook Bash invocations via checkLicense)
|
|
221
150
|
// ---------------------------------------------------------------------------
|
|
222
151
|
|
|
223
|
-
function allow() {
|
|
224
|
-
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
152
|
function block(reason) {
|
|
228
|
-
|
|
153
|
+
// v4.2.2 → v4.2.3 hotfix test: try exit code 2 + stderr instead of
|
|
154
|
+
// {decision:block} JSON. The JSON form was silently consumed as context
|
|
155
|
+
// injection in production. Per docs (variant 2), exit 2 + stderr is the
|
|
156
|
+
// canonical UserPromptSubmit block mechanism.
|
|
157
|
+
process.stderr.write(reason);
|
|
158
|
+
process.exit(2);
|
|
229
159
|
}
|
|
230
160
|
|
|
231
161
|
function buildMessage(status, reason) {
|
|
@@ -274,7 +204,16 @@ async function computeMachineId() {
|
|
|
274
204
|
return createHash('sha256').update(raw).digest('hex').substring(0, 16);
|
|
275
205
|
}
|
|
276
206
|
|
|
207
|
+
// Only run main when executed directly (not imported by tests).
|
|
208
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
209
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
210
|
+
// silently and main() never runs.
|
|
277
211
|
import { fileURLToPath } from 'url';
|
|
278
|
-
|
|
279
|
-
|
|
212
|
+
import { realpathSync } from 'fs';
|
|
213
|
+
if (process.argv[1]) {
|
|
214
|
+
try {
|
|
215
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
216
|
+
main();
|
|
217
|
+
}
|
|
218
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
280
219
|
}
|
|
@@ -15,8 +15,8 @@ import { join, relative, isAbsolute } from 'path';
|
|
|
15
15
|
|
|
16
16
|
const MODE_SCOPES = {
|
|
17
17
|
planning: {
|
|
18
|
-
allowed: ['chati.dev/', '.chati/', 'chati
|
|
19
|
-
description: 'Planning mode: write only to chati
|
|
18
|
+
allowed: ['chati.dev/', '.chati.dev/', '.chati/', 'artifacts/', 'CLAUDE.local.md', 'CLAUDE.md', '.claude/'],
|
|
19
|
+
description: 'Planning mode: write only to framework dir, .chati/, artifacts/, and Claude config files',
|
|
20
20
|
},
|
|
21
21
|
build: {
|
|
22
22
|
allowed: ['*'],
|
|
@@ -41,12 +41,25 @@ const STATE_TO_GOVERNANCE_MODE = {
|
|
|
41
41
|
|
|
42
42
|
function getCurrentMode(projectDir) {
|
|
43
43
|
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
44
|
-
|
|
44
|
+
// No session = no enforcement yet (orchestrator will set mode on first run).
|
|
45
|
+
// Defaulting to planning here would deadlock fresh installs (planning blocks
|
|
46
|
+
// session.yaml writes, so the orchestrator could never bootstrap).
|
|
47
|
+
if (!existsSync(sessionPath)) return 'build';
|
|
45
48
|
|
|
46
49
|
const raw = readFileSync(sessionPath, 'utf-8');
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
+
// Prefer explicit mode field; fall back to project.state mapping.
|
|
51
|
+
const modeMatch = raw.match(/^\s*mode:\s*(.+)$/m);
|
|
52
|
+
if (modeMatch) {
|
|
53
|
+
const mode = modeMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
54
|
+
if (MODE_SCOPES[mode]) return mode;
|
|
55
|
+
}
|
|
56
|
+
const stateMatch = raw.match(/^\s+state:\s*(.+)$/m);
|
|
57
|
+
if (stateMatch) {
|
|
58
|
+
const state = stateMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
59
|
+
return STATE_TO_GOVERNANCE_MODE[state] || 'build';
|
|
60
|
+
}
|
|
61
|
+
// Session exists but no mode/state field — be permissive.
|
|
62
|
+
return 'build';
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function isPathAllowed(filePath, projectDir, mode) {
|
|
@@ -74,31 +87,70 @@ async function main() {
|
|
|
74
87
|
const filePath = toolInput.file_path || toolInput.path || '';
|
|
75
88
|
|
|
76
89
|
if (!filePath) {
|
|
77
|
-
process.stdout.write(JSON.stringify(
|
|
90
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Block direct edits to session.yaml — it must be managed by CLI only.
|
|
95
|
+
// CLI uses fs.writeFileSync (not intercepted by hooks), so this only blocks
|
|
96
|
+
// Claude's Edit/Write tool calls, which is exactly what we want.
|
|
97
|
+
const rel = isAbsolute(filePath) ? relative(projectDir, filePath) : filePath;
|
|
98
|
+
if (rel === join('.chati', 'session.yaml') || rel === '.chati/session.yaml') {
|
|
99
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
100
|
+
'[Article XV] session.yaml is managed by the CLI. Use the router: node chati.dev/orchestrator/chati-router.js advance --agent {name} --score {score}'
|
|
101
|
+
)));
|
|
78
102
|
return;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
const mode = getCurrentMode(projectDir);
|
|
82
106
|
|
|
83
107
|
if (isPathAllowed(filePath, projectDir, mode)) {
|
|
84
|
-
process.stdout.write(JSON.stringify(
|
|
108
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
85
109
|
} else {
|
|
86
110
|
const scope = MODE_SCOPES[mode];
|
|
87
|
-
process.stdout.write(JSON.stringify(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}));
|
|
111
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
112
|
+
`[Article XI] ${scope.description}. Cannot write to "${filePath}" in ${mode} mode.`
|
|
113
|
+
)));
|
|
91
114
|
}
|
|
92
115
|
} catch (err) {
|
|
93
116
|
process.stderr.write(`[chati-hook-error] mode-governance: ${err?.message || 'unknown'}\n`);
|
|
94
|
-
|
|
117
|
+
// Hook errors are fail-OPEN: a broken hook should not block the user.
|
|
118
|
+
// Security-critical denials are handled by explicit logic above.
|
|
119
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
95
120
|
}
|
|
96
121
|
}
|
|
97
122
|
|
|
123
|
+
function allowOutput() {
|
|
124
|
+
return {
|
|
125
|
+
hookSpecificOutput: {
|
|
126
|
+
hookEventName: 'PreToolUse',
|
|
127
|
+
permissionDecision: 'allow',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function denyOutput(reason) {
|
|
133
|
+
return {
|
|
134
|
+
hookSpecificOutput: {
|
|
135
|
+
hookEventName: 'PreToolUse',
|
|
136
|
+
permissionDecision: 'deny',
|
|
137
|
+
permissionDecisionReason: reason,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
export { getCurrentMode, isPathAllowed, MODE_SCOPES, STATE_TO_GOVERNANCE_MODE };
|
|
99
143
|
|
|
100
|
-
// Only run main when executed directly (not imported by tests)
|
|
144
|
+
// Only run main when executed directly (not imported by tests).
|
|
145
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
146
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
147
|
+
// silently and main() never runs.
|
|
101
148
|
import { fileURLToPath } from 'url';
|
|
102
|
-
|
|
103
|
-
|
|
149
|
+
import { realpathSync } from 'fs';
|
|
150
|
+
if (process.argv[1]) {
|
|
151
|
+
try {
|
|
152
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
153
|
+
main();
|
|
154
|
+
}
|
|
155
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
104
156
|
}
|
|
@@ -38,6 +38,7 @@ const AGENT_MODELS = {
|
|
|
38
38
|
tasks: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
39
39
|
'qa-planning': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
40
40
|
'qa-implementation': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
41
|
+
'qa-visual': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
41
42
|
dev: { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
42
43
|
devops: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
43
44
|
};
|
|
@@ -80,24 +81,34 @@ async function main() {
|
|
|
80
81
|
const assignment = AGENT_MODELS[agent];
|
|
81
82
|
const expected = assignment.model || assignment;
|
|
82
83
|
const provider = assignment.provider || 'claude';
|
|
83
|
-
// Advisory
|
|
84
|
+
// Advisory context injection via canonical UserPromptSubmit schema.
|
|
84
85
|
process.stdout.write(JSON.stringify({
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
hookSpecificOutput: {
|
|
87
|
+
hookEventName: 'UserPromptSubmit',
|
|
88
|
+
additionalContext: `[Article XVI] Agent "${agent}" assigned model: ${expected} (provider: ${provider})`,
|
|
89
|
+
},
|
|
87
90
|
}));
|
|
88
91
|
} else {
|
|
89
|
-
process.stdout.write(
|
|
92
|
+
process.stdout.write('{}');
|
|
90
93
|
}
|
|
91
94
|
} catch (err) {
|
|
92
95
|
process.stderr.write(`[chati] model-governance: ${err.message}\n`);
|
|
93
|
-
process.stdout.write(
|
|
96
|
+
process.stdout.write('{}');
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
export { AGENT_MODELS, UPGRADE_CONDITIONS, getCurrentAgent };
|
|
98
101
|
|
|
99
|
-
// Only run main when executed directly (not imported by tests)
|
|
102
|
+
// Only run main when executed directly (not imported by tests).
|
|
103
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
104
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
105
|
+
// silently and main() never runs.
|
|
100
106
|
import { fileURLToPath } from 'url';
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
import { realpathSync } from 'fs';
|
|
108
|
+
if (process.argv[1]) {
|
|
109
|
+
try {
|
|
110
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
111
|
+
main();
|
|
112
|
+
}
|
|
113
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
103
114
|
}
|