chati-dev 4.3.0 → 4.4.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 +18 -13
- package/bin/chati.js +32 -4
- package/framework/agents/build/dev.md +9 -5
- package/framework/agents/discover/brief.md +2 -0
- package/framework/agents/discover/brownfield-wu.md +2 -0
- package/framework/agents/discover/greenfield-wu.md +18 -1
- package/framework/agents/plan/detail.md +2 -0
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +33 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- 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/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/orchestrator/chati-router.js +59 -5
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +20 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +12 -2
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
- package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
- package/framework/scaffold/motion-premium-3d/README.md +10 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +101 -3
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -3
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +13 -3
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +186 -15
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +64 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Execution-kind runner for deterministic, zero-LLM script tasks.
|
|
3
|
+
*
|
|
4
|
+
* When a task declares `execution_kind: script` (and the execution_kinds feature
|
|
5
|
+
* flag is on), the orchestrator runs it here instead of spawning an agent. The
|
|
6
|
+
* runner resolves the executor from chati.dev/executors/registry.yaml, spawns it
|
|
7
|
+
* as a child process under the executor contract (JSON request on stdin, JSON
|
|
8
|
+
* result on stdout), enforces a timeout, and shapes a synthetic handoff so the
|
|
9
|
+
* pipeline continues exactly as it would after an agent.
|
|
10
|
+
*
|
|
11
|
+
* Contract result: { status: 'passed'|'failed'|'error', summary, details }.
|
|
12
|
+
* passed -> the task is done; advance.
|
|
13
|
+
* failed -> the deterministic check is negative; route to dev (CODE fault).
|
|
14
|
+
* error -> the executor could not run; surface to the human.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { existsSync, readFileSync } from 'fs';
|
|
19
|
+
import { join, resolve, sep } from 'path';
|
|
20
|
+
import yaml from 'js-yaml';
|
|
21
|
+
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Split an execution_ref into executor id and argument. The split is on the
|
|
25
|
+
* FIRST colon, so an argument may itself contain colons.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} ref - e.g. "npm-script:test" or "path-check:dist,build"
|
|
28
|
+
* @returns {{ id: string, arg: string }}
|
|
29
|
+
*/
|
|
30
|
+
export function parseExecutionRef(ref) {
|
|
31
|
+
const s = String(ref || '').trim();
|
|
32
|
+
if (!s) return { id: '', arg: '' };
|
|
33
|
+
const i = s.indexOf(':');
|
|
34
|
+
if (i === -1) return { id: s, arg: '' };
|
|
35
|
+
return { id: s.slice(0, i).trim(), arg: s.slice(i + 1).trim() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load and parse the executor registry for a project.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectDir
|
|
42
|
+
* @returns {{ version?: string, executors: Record<string, {path:string, description?:string, timeout_ms?:number}> }}
|
|
43
|
+
*/
|
|
44
|
+
export function loadExecutorRegistry(projectDir) {
|
|
45
|
+
const path = join(projectDir, resolveFrameworkDir(projectDir), 'executors', 'registry.yaml');
|
|
46
|
+
if (!existsSync(path)) return { executors: {} };
|
|
47
|
+
try {
|
|
48
|
+
const parsed = yaml.load(readFileSync(path, 'utf-8'));
|
|
49
|
+
return parsed && typeof parsed === 'object' ? { executors: {}, ...parsed } : { executors: {} };
|
|
50
|
+
} catch {
|
|
51
|
+
return { executors: {} };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spawn an executor script, feed it the JSON request, and resolve its JSON
|
|
57
|
+
* result. Never rejects: a spawn failure, a timeout, or non-JSON output all
|
|
58
|
+
* resolve to a structured `error` result.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} execPath - absolute path to the executor script
|
|
61
|
+
* @param {string} request - JSON string written to the executor's stdin
|
|
62
|
+
* @param {number} timeoutMs
|
|
63
|
+
* @returns {Promise<{status:string, summary:string, details?:object}>}
|
|
64
|
+
*/
|
|
65
|
+
function spawnExecutor(execPath, request, timeoutMs) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
const done = (r) => { if (!settled) { settled = true; resolve(r); } };
|
|
69
|
+
|
|
70
|
+
const child = spawn(process.execPath, [execPath], { shell: false });
|
|
71
|
+
let out = '';
|
|
72
|
+
let err = '';
|
|
73
|
+
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
child.kill('SIGKILL');
|
|
76
|
+
done({ status: 'error', summary: `Executor timed out after ${timeoutMs}ms.` });
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
|
|
79
|
+
child.stdout.on('data', (c) => { out += c; });
|
|
80
|
+
child.stderr.on('data', (c) => { err += c; });
|
|
81
|
+
child.on('error', (e) => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
done({ status: 'error', summary: `Executor could not be spawned: ${e.message}.` });
|
|
84
|
+
});
|
|
85
|
+
child.on('close', () => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(out.trim());
|
|
89
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.status) {
|
|
90
|
+
done({ status: 'error', summary: 'Executor returned JSON without a status field.', details: { stdout: out.slice(0, 500) } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
done(parsed);
|
|
94
|
+
} catch {
|
|
95
|
+
done({ status: 'error', summary: 'Executor did not return valid JSON.', details: { stdout: out.slice(0, 500), stderr: err.slice(0, 500) } });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child.stdin.write(request);
|
|
100
|
+
child.stdin.end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run a `script` task through its executor.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} projectDir
|
|
108
|
+
* @param {{id?:string, title?:string, execution_ref?:string}} task
|
|
109
|
+
* @returns {Promise<{executor:string, arg:string, status:string, summary:string, details?:object, durationMs:number}>}
|
|
110
|
+
*/
|
|
111
|
+
export async function runScriptTask(projectDir, task) {
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
const ref = task?.execution_ref || '';
|
|
114
|
+
const { id, arg } = parseExecutionRef(ref);
|
|
115
|
+
|
|
116
|
+
const wrap = (r) => ({ executor: id, arg, durationMs: Date.now() - startedAt, ...r });
|
|
117
|
+
|
|
118
|
+
if (!id) {
|
|
119
|
+
return wrap({ status: 'error', summary: 'Script task has no execution_ref (expected "executor-id:arg").' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const registry = loadExecutorRegistry(projectDir);
|
|
123
|
+
const entry = registry.executors?.[id];
|
|
124
|
+
if (!entry) {
|
|
125
|
+
const known = Object.keys(registry.executors || {}).join(', ') || '(none)';
|
|
126
|
+
return wrap({ status: 'error', summary: `No executor "${id}" in the registry. Known: ${known}.` });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const execRoot = join(projectDir, resolveFrameworkDir(projectDir), 'executors');
|
|
130
|
+
const execPath = join(projectDir, resolveFrameworkDir(projectDir), entry.path);
|
|
131
|
+
// Containment: a tampered or supply-chain-compromised registry must not point
|
|
132
|
+
// the runner at a file outside the framework's executors directory.
|
|
133
|
+
const resolvedExec = resolve(execPath);
|
|
134
|
+
const resolvedRoot = resolve(execRoot);
|
|
135
|
+
if (resolvedExec !== resolvedRoot && !resolvedExec.startsWith(resolvedRoot + sep)) {
|
|
136
|
+
return wrap({ status: 'error', summary: `Executor path "${entry.path}" is outside the executors directory and was refused.` });
|
|
137
|
+
}
|
|
138
|
+
if (!existsSync(execPath)) {
|
|
139
|
+
return wrap({ status: 'error', summary: `Executor file is missing: ${entry.path}.` });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const request = JSON.stringify({
|
|
143
|
+
task: { id: task.id, title: task.title, execution_ref: ref },
|
|
144
|
+
projectDir,
|
|
145
|
+
arg,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await spawnExecutor(execPath, request, entry.timeout_ms || 60000);
|
|
149
|
+
return wrap(result);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shape a synthetic handoff for a completed script task so the pipeline reads it
|
|
154
|
+
* the same way it reads an agent handoff.
|
|
155
|
+
*
|
|
156
|
+
* @param {{id?:string, title?:string}} task
|
|
157
|
+
* @param {{executor:string, arg:string, status:string, summary:string, details?:object, durationMs:number}} result
|
|
158
|
+
* @param {string} [timestamp] - ISO timestamp (caller supplies; the runner does not read the clock for handoff text)
|
|
159
|
+
* @returns {string} handoff markdown
|
|
160
|
+
*/
|
|
161
|
+
export function formatScriptHandoff(task, result, timestamp) {
|
|
162
|
+
const status = result.status === 'passed' ? 'completed'
|
|
163
|
+
: result.status === 'failed' ? 'blocked'
|
|
164
|
+
: 'error';
|
|
165
|
+
const score = result.status === 'passed' ? 100 : 0;
|
|
166
|
+
// failed is a real negative result routed to dev (CODE). An error means the
|
|
167
|
+
// executor could not run; that is surfaced to the human, not backlogged, so it
|
|
168
|
+
// carries no fault_origin (DEFER would wrongly signal "out of scope").
|
|
169
|
+
const faultOrigin = result.status === 'failed' ? 'CODE' : null;
|
|
170
|
+
|
|
171
|
+
const lines = [];
|
|
172
|
+
lines.push('---');
|
|
173
|
+
lines.push(`task: ${task?.id || '(unknown)'}`);
|
|
174
|
+
lines.push('execution_kind: script');
|
|
175
|
+
lines.push(`executor: ${result.executor}${result.arg ? `:${result.arg}` : ''}`);
|
|
176
|
+
lines.push(`status: ${status}`);
|
|
177
|
+
lines.push(`score: ${score}`);
|
|
178
|
+
if (faultOrigin) lines.push(`fault_origin: ${faultOrigin}`);
|
|
179
|
+
if (timestamp) lines.push(`completed_at: ${timestamp}`);
|
|
180
|
+
lines.push('---');
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push(`# Script task ${task?.id || ''}: ${task?.title || ''}`.trim());
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(`Ran deterministically with no LLM call (executor \`${result.executor}\`, ${result.durationMs}ms).`);
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push('## Summary');
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(result.summary || 'Script task completed.');
|
|
189
|
+
if (result.details) {
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('```json');
|
|
192
|
+
lines.push(JSON.stringify(result.details, null, 2));
|
|
193
|
+
lines.push('```');
|
|
194
|
+
}
|
|
195
|
+
if (result.status === 'failed') {
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('The deterministic check is negative. Route to dev for a fix (Fault Vector Protocol: CODE), then re-run the script task.');
|
|
198
|
+
} else if (result.status === 'error') {
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push('The executor could not run. Surface to the human: check the execution_ref and the executor registry.');
|
|
201
|
+
}
|
|
202
|
+
lines.push('');
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
package/src/installer/core.js
CHANGED
|
@@ -147,6 +147,15 @@ export async function installFramework(config) {
|
|
|
147
147
|
join(frameworkDir, '_cli', 'node_modules') // .chati.dev/_cli/node_modules/
|
|
148
148
|
);
|
|
149
149
|
|
|
150
|
+
// v4.3.1+ — Write version stamp so chati-router.js can detect bundle/canonical
|
|
151
|
+
// mismatch on subsequent invocations. Pre-v4.3.1 installs lack this file;
|
|
152
|
+
// mismatch detection silent-skips when it's missing (graceful degradation).
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(frameworkDir, '_cli', 'version.json'),
|
|
155
|
+
JSON.stringify({ version, bundled_at: new Date().toISOString() }, null, 2),
|
|
156
|
+
'utf-8'
|
|
157
|
+
);
|
|
158
|
+
|
|
150
159
|
// Write config.yaml
|
|
151
160
|
writeFileSync(
|
|
152
161
|
join(frameworkDir, 'config.yaml'),
|
|
@@ -235,6 +244,7 @@ const FRAMEWORK_DIRS_TO_COPY = [
|
|
|
235
244
|
'presets',
|
|
236
245
|
'scripts',
|
|
237
246
|
'scaffold',
|
|
247
|
+
'executors',
|
|
238
248
|
];
|
|
239
249
|
|
|
240
250
|
/**
|
|
@@ -701,7 +711,7 @@ function createDir(dir) {
|
|
|
701
711
|
* Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
|
|
702
712
|
* the framework runs self-contained (no `npx chati-dev` dependency).
|
|
703
713
|
*/
|
|
704
|
-
function copyCliSource(srcDir, destDir) {
|
|
714
|
+
export function copyCliSource(srcDir, destDir) {
|
|
705
715
|
if (!existsSync(srcDir)) return;
|
|
706
716
|
createDir(destDir);
|
|
707
717
|
const entries = readdirSync(srcDir);
|
|
@@ -748,7 +758,7 @@ function copyDirVerbatim(srcDir, destDir) {
|
|
|
748
758
|
* which works whether the package is hoisted or nested in the user's
|
|
749
759
|
* node_modules (npx cache, npm install, etc.).
|
|
750
760
|
*/
|
|
751
|
-
function copyCliDependencies(pkgDir, destNodeModules) {
|
|
761
|
+
export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
752
762
|
let pkg;
|
|
753
763
|
try {
|
|
754
764
|
pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
|
|
@@ -760,7 +770,7 @@ function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
760
770
|
if (deps.length === 0) return;
|
|
761
771
|
|
|
762
772
|
// Resolve each dep from the package.json location (works for hoisted + nested layouts).
|
|
763
|
-
|
|
773
|
+
|
|
764
774
|
const require = createRequire(join(pkgDir, 'package.json'));
|
|
765
775
|
createDir(destNodeModules);
|
|
766
776
|
|
|
@@ -130,7 +130,6 @@ export function generateConfigYaml(config) {
|
|
|
130
130
|
rate_limiter_integration: true,
|
|
131
131
|
l5_keywords: true,
|
|
132
132
|
prompt_size_guard: true,
|
|
133
|
-
ids_decision_engine: true,
|
|
134
133
|
surface_criteria: true,
|
|
135
134
|
parallel_fallback: true,
|
|
136
135
|
tool_mesh: true,
|
|
@@ -219,7 +218,7 @@ description: >
|
|
|
219
218
|
|
|
220
219
|
# Chati.dev Orchestrator
|
|
221
220
|
|
|
222
|
-
## CRITICAL
|
|
221
|
+
## CRITICAL: Language Override
|
|
223
222
|
|
|
224
223
|
Read \`.chati/session.yaml\` field \`language\` BEFORE anything else.
|
|
225
224
|
ALL responses MUST be in this language. This overrides any global setting.
|
|
@@ -236,14 +235,14 @@ If session.yaml does not exist or has no language field, default to English.
|
|
|
236
235
|
## Load
|
|
237
236
|
|
|
238
237
|
The orchestrator and all agent files are **pre-configured for Codex CLI**.
|
|
239
|
-
No translation needed
|
|
238
|
+
No translation needed; follow all instructions as written.
|
|
240
239
|
|
|
241
240
|
Read and execute the full orchestrator at \`${orchPath}\`.
|
|
242
241
|
|
|
243
242
|
**NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.**
|
|
244
243
|
|
|
245
244
|
**Context to pass:**
|
|
246
|
-
- \`.chati/session.yaml\` (session state
|
|
245
|
+
- \`.chati/session.yaml\` (session state, includes language)
|
|
247
246
|
- \`AGENTS.md\` (project context)
|
|
248
247
|
- \`artifacts/handoffs/\` (latest handoff)
|
|
249
248
|
- \`.chati.dev/config.yaml\` (version info)
|
|
@@ -265,13 +264,13 @@ ALL responses MUST be in this language (en, pt, es, fr).
|
|
|
265
264
|
If session.yaml does not exist or has no language field, default to English.
|
|
266
265
|
|
|
267
266
|
The orchestrator and all agent files are pre-configured for Gemini CLI.
|
|
268
|
-
No translation needed
|
|
267
|
+
No translation needed; follow all instructions as written.
|
|
269
268
|
|
|
270
269
|
Read and execute the full orchestrator at ${orchPath}.
|
|
271
270
|
NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.
|
|
272
271
|
|
|
273
272
|
Context to load:
|
|
274
|
-
- .chati/session.yaml (session state
|
|
273
|
+
- .chati/session.yaml (session state, includes language)
|
|
275
274
|
- GEMINI.md (project context)
|
|
276
275
|
- artifacts/handoffs/ (latest handoff)
|
|
277
276
|
- .chati.dev/config.yaml (version info)
|
|
@@ -144,7 +144,7 @@ export function runHealthCheck(targetDir) {
|
|
|
144
144
|
const content = readFileSync(constitutionPath, 'utf-8');
|
|
145
145
|
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
146
146
|
checks.constitution.pass = articleCount >= 22;
|
|
147
|
-
checks.constitution.details = `${articleCount}
|
|
147
|
+
checks.constitution.details = `${articleCount} articles`;
|
|
148
148
|
} else {
|
|
149
149
|
checks.constitution.details = 'Not found';
|
|
150
150
|
}
|
package/src/license/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
5
|
import { getMachineId, getMachineName } from './machine-id.js';
|
|
@@ -27,8 +27,14 @@ function readGlobal() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function writeGlobal(data) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const dir = getGlobalDir();
|
|
31
|
+
const path = getGlobalLicensePath();
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
writeFileSync(path, yaml.dump(data, { lineWidth: -1 }));
|
|
34
|
+
// Restrict to owner read/write only (0o600). On multi-user machines this
|
|
35
|
+
// prevents other local users from reading the license key from the
|
|
36
|
+
// world-readable default. POSIX-only; chmodSync is a no-op on Windows.
|
|
37
|
+
try { chmodSync(path, 0o600); } catch { /* non-POSIX FS — skip silently */ }
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
// ---------------------------------------------------------------------------
|
package/src/license/commands.js
CHANGED
|
@@ -10,6 +10,15 @@ import { activateLicense, deactivateLicense, getLicenseKey, getLicenseStatus, va
|
|
|
10
10
|
// activate
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Run the interactive license activation flow.
|
|
15
|
+
*
|
|
16
|
+
* Returns `{ ok: boolean, cancelled?: boolean, plan?, days?, error? }`.
|
|
17
|
+
* On failure this function does NOT call `process.exit` — callers (typically
|
|
18
|
+
* `bin/chati.js`) decide whether to exit non-zero based on the returned `ok`
|
|
19
|
+
* flag. This keeps the function unit-testable and reusable from programmatic
|
|
20
|
+
* call sites.
|
|
21
|
+
*/
|
|
13
22
|
export async function runActivate(projectDir, keyArg) {
|
|
14
23
|
let key = keyArg;
|
|
15
24
|
|
|
@@ -25,7 +34,7 @@ export async function runActivate(projectDir, keyArg) {
|
|
|
25
34
|
});
|
|
26
35
|
if (isCancel(input)) {
|
|
27
36
|
console.log('Activation cancelled.');
|
|
28
|
-
return;
|
|
37
|
+
return { ok: false, cancelled: true };
|
|
29
38
|
}
|
|
30
39
|
key = input.trim();
|
|
31
40
|
}
|
|
@@ -39,10 +48,11 @@ export async function runActivate(projectDir, keyArg) {
|
|
|
39
48
|
console.log(`\n✓ Activated! Plan: ${plan} — ${days} day(s) remaining.`);
|
|
40
49
|
console.log(` Key saved to ~/.chati-dev/license.yaml`);
|
|
41
50
|
console.log(` Run /chati to start.\n`);
|
|
51
|
+
return { ok: true, plan, days };
|
|
42
52
|
} catch (err) {
|
|
43
53
|
console.error(`\n✗ Activation failed: ${err.message}`);
|
|
44
54
|
console.error(` Visit https://chati.dev/pricing to get a key.\n`);
|
|
45
|
-
|
|
55
|
+
return { ok: false, error: err.message };
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const PERSISTENT_ID_FILE = '.chati-dev/machine-id';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read or mint a persistent random per-installation salt at
|
|
10
|
+
* `~/.chati-dev/machine-id`. Used to disambiguate machines that share
|
|
11
|
+
* everything OS-visible (hostname, platform, arch, CPU, username, MAC) —
|
|
12
|
+
* e.g. virtual machines or containers with no external NIC where the prior
|
|
13
|
+
* `'no-mac'` fallback collapsed many machines onto a single fingerprint.
|
|
14
|
+
*
|
|
15
|
+
* The file is created lazily on first call. Format is a single 32-hex-char
|
|
16
|
+
* string with mode 0600 so other local users cannot read it.
|
|
17
|
+
*/
|
|
18
|
+
function getPersistentSalt() {
|
|
19
|
+
const path = join(os.homedir(), PERSISTENT_ID_FILE);
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(path)) {
|
|
22
|
+
const raw = readFileSync(path, 'utf-8').trim();
|
|
23
|
+
if (/^[0-9a-f]{16,}$/i.test(raw)) return raw;
|
|
24
|
+
}
|
|
25
|
+
} catch { /* fall through to mint */ }
|
|
26
|
+
// Mint a new salt.
|
|
27
|
+
try {
|
|
28
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
29
|
+
mkdirSync(join(os.homedir(), '.chati-dev'), { recursive: true });
|
|
30
|
+
writeFileSync(path, salt);
|
|
31
|
+
try { chmodSync(path, 0o600); } catch { /* non-POSIX FS — skip */ }
|
|
32
|
+
return salt;
|
|
33
|
+
} catch {
|
|
34
|
+
// FS write failed (read-only homedir?). Return a deterministic-empty
|
|
35
|
+
// string — fingerprint then falls back to the OS-visible properties
|
|
36
|
+
// only, matching pre-fix behaviour rather than crashing.
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
3
40
|
|
|
4
41
|
/**
|
|
5
42
|
* Generates a stable 16-char machine fingerprint.
|
|
6
|
-
*
|
|
43
|
+
* Inputs: hostname, platform, arch, CPU model, username, MAC address, and a
|
|
44
|
+
* persistent random per-installation salt (see getPersistentSalt). The salt
|
|
45
|
+
* eliminates fingerprint collisions on VMs/containers that share all
|
|
46
|
+
* OS-visible properties and report `'no-mac'`.
|
|
7
47
|
*/
|
|
8
48
|
export function getMachineId() {
|
|
9
49
|
const ifaces = Object.values(os.networkInterfaces()).flat();
|
|
@@ -15,6 +55,7 @@ export function getMachineId() {
|
|
|
15
55
|
os.cpus()[0]?.model ?? '',
|
|
16
56
|
os.userInfo().username,
|
|
17
57
|
mac,
|
|
58
|
+
getPersistentSalt(),
|
|
18
59
|
].join('|');
|
|
19
60
|
return crypto.createHash('sha256').update(raw).digest('hex').substring(0, 16);
|
|
20
61
|
}
|
package/src/memory/gotchas.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { acquireLock } from '../utils/file-lock.js';
|
|
4
5
|
|
|
5
6
|
const GOTCHAS_FILE = '.chati/memories/shared/gotchas.json';
|
|
6
7
|
const ERROR_LOG_FILE = '.chati/memories/shared/error-log.json';
|
|
@@ -203,53 +204,70 @@ export function recordError(projectDir, error) {
|
|
|
203
204
|
});
|
|
204
205
|
|
|
205
206
|
if (recentErrors.length >= ERROR_PATTERN_THRESHOLD) {
|
|
206
|
-
// Promote to gotcha
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
207
|
+
// Promote to gotcha — read-then-write must be atomic across concurrent
|
|
208
|
+
// agents. Without a lock two parallel terminals can both compute the
|
|
209
|
+
// same next ID (G001, G002, …) and overwrite each other's entry, since
|
|
210
|
+
// generateGotchaId() picks max+1 by scanning the file. Hold an advisory
|
|
211
|
+
// lock on the gotchas file for the full read-modify-write window.
|
|
212
|
+
const gotchasPath = join(projectDir, GOTCHAS_FILE);
|
|
213
|
+
const dir = dirname(gotchasPath);
|
|
214
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
215
|
+
const lock = acquireLock(gotchasPath, { timeout: 5000 });
|
|
216
|
+
if (!lock.acquired) {
|
|
217
|
+
// Degraded path: still record but skip promotion. Avoids hanging on a
|
|
218
|
+
// stale lock during pipeline execution.
|
|
219
|
+
return { recorded: true, promoted: false, gotcha: null };
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const gotchas = loadGotchas(projectDir);
|
|
223
|
+
|
|
224
|
+
// Check if already exists
|
|
225
|
+
const existingGotcha = gotchas.find(g => g.pattern === hash);
|
|
226
|
+
|
|
227
|
+
if (existingGotcha) {
|
|
228
|
+
// Update existing gotcha
|
|
229
|
+
existingGotcha.count = recentErrors.length;
|
|
230
|
+
existingGotcha.last_seen = timestamp;
|
|
231
|
+
saveGotchas(projectDir, gotchas);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
recorded: true,
|
|
235
|
+
promoted: false,
|
|
236
|
+
gotcha: existingGotcha,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create new gotcha
|
|
241
|
+
const gotchaId = generateGotchaId(gotchas);
|
|
242
|
+
const classification = classifyError(message);
|
|
243
|
+
const newGotcha = {
|
|
244
|
+
id: gotchaId,
|
|
245
|
+
pattern: hash,
|
|
246
|
+
message: normalizeErrorMessage(message),
|
|
247
|
+
original_message: message,
|
|
248
|
+
agent,
|
|
249
|
+
task,
|
|
250
|
+
count: recentErrors.length,
|
|
251
|
+
first_seen: recentErrors[0].timestamp,
|
|
252
|
+
last_seen: timestamp,
|
|
253
|
+
promoted_at: timestamp,
|
|
254
|
+
resolution: null,
|
|
255
|
+
category: classification.category,
|
|
256
|
+
severity: classification.severity,
|
|
257
|
+
context,
|
|
258
|
+
};
|
|
211
259
|
|
|
212
|
-
|
|
213
|
-
// Update existing gotcha
|
|
214
|
-
existingGotcha.count = recentErrors.length;
|
|
215
|
-
existingGotcha.last_seen = timestamp;
|
|
260
|
+
gotchas.push(newGotcha);
|
|
216
261
|
saveGotchas(projectDir, gotchas);
|
|
217
262
|
|
|
218
263
|
return {
|
|
219
264
|
recorded: true,
|
|
220
|
-
promoted:
|
|
221
|
-
gotcha:
|
|
265
|
+
promoted: true,
|
|
266
|
+
gotcha: newGotcha,
|
|
222
267
|
};
|
|
268
|
+
} finally {
|
|
269
|
+
lock.release();
|
|
223
270
|
}
|
|
224
|
-
|
|
225
|
-
// Create new gotcha
|
|
226
|
-
const gotchaId = generateGotchaId(gotchas);
|
|
227
|
-
const classification = classifyError(message);
|
|
228
|
-
const newGotcha = {
|
|
229
|
-
id: gotchaId,
|
|
230
|
-
pattern: hash,
|
|
231
|
-
message: normalizeErrorMessage(message),
|
|
232
|
-
original_message: message,
|
|
233
|
-
agent,
|
|
234
|
-
task,
|
|
235
|
-
count: recentErrors.length,
|
|
236
|
-
first_seen: recentErrors[0].timestamp,
|
|
237
|
-
last_seen: timestamp,
|
|
238
|
-
promoted_at: timestamp,
|
|
239
|
-
resolution: null,
|
|
240
|
-
category: classification.category,
|
|
241
|
-
severity: classification.severity,
|
|
242
|
-
context,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
gotchas.push(newGotcha);
|
|
246
|
-
saveGotchas(projectDir, gotchas);
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
recorded: true,
|
|
250
|
-
promoted: true,
|
|
251
|
-
gotcha: newGotcha,
|
|
252
|
-
};
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
return {
|
package/src/memory/magic-docs.js
CHANGED
|
@@ -44,7 +44,7 @@ export function updateClaudeMd(projectDir, context = {}) {
|
|
|
44
44
|
lines.push('');
|
|
45
45
|
lines.push('### Recent Decisions');
|
|
46
46
|
for (const d of context.decisions.slice(0, 5)) {
|
|
47
|
-
const text = typeof d === 'object' ? `${d.what}
|
|
47
|
+
const text = typeof d === 'object' ? `${d.what}: ${d.why}` : String(d);
|
|
48
48
|
lines.push(`- ${text}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -177,18 +177,23 @@ export function buildStructuredDigest(projectDir, sessionState, enrichment = {})
|
|
|
177
177
|
},
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
-
// Final budget enforcement — iterative truncation until within budget
|
|
180
|
+
// Final budget enforcement — iterative truncation until within budget.
|
|
181
|
+
// Cache `JSON.stringify(digest).length` and refresh only after a real
|
|
182
|
+
// mutation; the prior implementation serialised the entire object twice
|
|
183
|
+
// per iteration, which became expensive for large sessions.
|
|
181
184
|
const truncOrder = ['next_steps', 'gotchas_found', 'files_changed', 'blockers', 'decisions_made'];
|
|
185
|
+
const measure = () => JSON.stringify(digest).length;
|
|
186
|
+
let size = measure();
|
|
182
187
|
let pass = 0;
|
|
183
|
-
while (
|
|
188
|
+
while (size > MAX_STRUCTURED_CHARS && pass < truncOrder.length) {
|
|
184
189
|
const field = truncOrder[pass];
|
|
185
190
|
if (Array.isArray(digest[field]) && digest[field].length > 1) {
|
|
186
191
|
digest[field] = digest[field].slice(0, Math.max(1, Math.ceil(digest[field].length / 2)));
|
|
192
|
+
size = measure();
|
|
193
|
+
if (size > MAX_STRUCTURED_CHARS) pass++; // still too big — move on
|
|
187
194
|
} else {
|
|
188
195
|
pass++;
|
|
189
196
|
}
|
|
190
|
-
// If we halved and still too big, move to next field
|
|
191
|
-
if (JSON.stringify(digest).length > MAX_STRUCTURED_CHARS) pass++;
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
return digest;
|