chati-dev 4.3.1 → 4.4.1
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 +28 -4
- package/framework/agents/build/dev.md +5 -1
- package/framework/agents/discover/greenfield-wu.md +16 -1
- 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 +24 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +60 -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/manifest.json +1351 -0
- package/framework/manifest.sig +1 -0
- package/framework/orchestrator/chati-router.js +1 -1
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +24 -3
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- 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 +87 -1
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -4
- package/src/config/claude-settings-generator.js +9 -6
- 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 +35 -4
- package/src/installer/manifest.js +30 -4
- package/src/installer/signing-public-key.pem +1 -1
- package/src/installer/templates.js +5 -6
- package/src/installer/validator.js +2 -1
- package/src/intelligence/registry-manager.js +3 -2
- 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 +7 -2
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +195 -23
- package/src/orchestrator/deviation-handler.js +5 -3
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +56 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-parallel.js +14 -14
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/backup.js +22 -44
- package/src/upgrade/migrator.js +51 -12
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/constitution-meta.js +12 -0
- package/src/utils/feature-flags.js +26 -2
- 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
|
@@ -9,6 +9,8 @@ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from
|
|
|
9
9
|
import { sendEvents } from '../telemetry/sender.js';
|
|
10
10
|
import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
|
|
11
11
|
import { getCurrentVersion } from '../upgrade/checker.js';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Pipeline phases in order.
|
|
@@ -107,6 +109,35 @@ const QA_THRESHOLDS = {
|
|
|
107
109
|
'qa-visual': QA_VISUAL_THRESHOLD,
|
|
108
110
|
};
|
|
109
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the gate thresholds from the canonical rubric file
|
|
114
|
+
* (chati.dev/data/qa-rubrics.yaml), falling back to the constants above when
|
|
115
|
+
* the file is unavailable (for example in a unit test with no project on disk).
|
|
116
|
+
* The constants stay as the documented fallback, not as a second source of
|
|
117
|
+
* truth: when the rubric file is present its values win. Cached per directory.
|
|
118
|
+
*/
|
|
119
|
+
const _qaThresholdCache = new Map();
|
|
120
|
+
function loadQaThresholds(projectDir) {
|
|
121
|
+
const dir = projectDir || process.cwd();
|
|
122
|
+
if (_qaThresholdCache.has(dir)) return _qaThresholdCache.get(dir);
|
|
123
|
+
const candidates = ['.chati.dev', 'chati.dev'].map(
|
|
124
|
+
(d) => join(dir, d, 'data', 'qa-rubrics.yaml')
|
|
125
|
+
);
|
|
126
|
+
let raw = null;
|
|
127
|
+
for (const p of candidates) {
|
|
128
|
+
if (existsSync(p)) { try { raw = readFileSync(p, 'utf-8'); break; } catch { /* fall through */ } }
|
|
129
|
+
}
|
|
130
|
+
const out = { ...QA_THRESHOLDS };
|
|
131
|
+
if (raw) {
|
|
132
|
+
for (const key of Object.keys(QA_THRESHOLDS)) {
|
|
133
|
+
const m = raw.match(new RegExp(`^\\s*${key}:\\s*(\\d+)`, 'm'));
|
|
134
|
+
if (m) out[key] = parseInt(m[1], 10);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
_qaThresholdCache.set(dir, out);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
/**
|
|
111
142
|
* Route a failing QA to the correct rework target via Fault Vector Protocol.
|
|
112
143
|
*
|
|
@@ -180,16 +211,6 @@ function correctionKey(qaAgent, target) {
|
|
|
180
211
|
return `${qaAgent}->${target}`;
|
|
181
212
|
}
|
|
182
213
|
|
|
183
|
-
/**
|
|
184
|
-
* Check if a QA agent's score is below its threshold.
|
|
185
|
-
*/
|
|
186
|
-
function qaScoreBelowThreshold(qaAgent, score) {
|
|
187
|
-
const threshold = QA_THRESHOLDS[qaAgent];
|
|
188
|
-
if (threshold === undefined) return false;
|
|
189
|
-
if (score === null || score === undefined) return true;
|
|
190
|
-
return score < threshold;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
214
|
/**
|
|
194
215
|
* Initialize a new pipeline for a project.
|
|
195
216
|
*
|
|
@@ -690,6 +711,7 @@ function getFirstAgentInPhase(state, phase) {
|
|
|
690
711
|
*/
|
|
691
712
|
export function checkPhaseTransition(pipelineState) {
|
|
692
713
|
const { phase, agents } = pipelineState;
|
|
714
|
+
const T = loadQaThresholds(pipelineState.targetDir);
|
|
693
715
|
|
|
694
716
|
// Check based on current phase
|
|
695
717
|
if (phase === 'discover') {
|
|
@@ -731,15 +753,15 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
731
753
|
return {
|
|
732
754
|
canAdvance: false,
|
|
733
755
|
reason: 'QA-Planning not yet completed',
|
|
734
|
-
requiredScore:
|
|
756
|
+
requiredScore: T['qa-planning'],
|
|
735
757
|
};
|
|
736
758
|
}
|
|
737
759
|
|
|
738
|
-
if (qaPlanning.score === null || qaPlanning.score <
|
|
760
|
+
if (qaPlanning.score === null || qaPlanning.score < T['qa-planning']) {
|
|
739
761
|
return {
|
|
740
762
|
canAdvance: false,
|
|
741
|
-
reason: `QA-Planning score ${qaPlanning.score || 0} below threshold ${
|
|
742
|
-
requiredScore:
|
|
763
|
+
reason: `QA-Planning score ${qaPlanning.score || 0} below threshold ${T['qa-planning']}`,
|
|
764
|
+
requiredScore: T['qa-planning'],
|
|
743
765
|
};
|
|
744
766
|
}
|
|
745
767
|
|
|
@@ -795,15 +817,15 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
795
817
|
return {
|
|
796
818
|
canAdvance: false,
|
|
797
819
|
reason: 'QA-Implementation not yet completed',
|
|
798
|
-
requiredScore:
|
|
820
|
+
requiredScore: T['qa-implementation'],
|
|
799
821
|
};
|
|
800
822
|
}
|
|
801
823
|
|
|
802
|
-
if (qaImpl.score === null || qaImpl.score <
|
|
824
|
+
if (qaImpl.score === null || qaImpl.score < T['qa-implementation']) {
|
|
803
825
|
return {
|
|
804
826
|
canAdvance: false,
|
|
805
|
-
reason: `QA-Implementation score ${qaImpl.score || 0} below threshold ${
|
|
806
|
-
requiredScore:
|
|
827
|
+
reason: `QA-Implementation score ${qaImpl.score || 0} below threshold ${T['qa-implementation']}`,
|
|
828
|
+
requiredScore: T['qa-implementation'],
|
|
807
829
|
};
|
|
808
830
|
}
|
|
809
831
|
|
|
@@ -813,14 +835,14 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
813
835
|
return {
|
|
814
836
|
canAdvance: false,
|
|
815
837
|
reason: 'QA-Visual not yet completed',
|
|
816
|
-
requiredScore:
|
|
838
|
+
requiredScore: T['qa-visual'],
|
|
817
839
|
};
|
|
818
840
|
}
|
|
819
|
-
if (qaVisual.score === null || qaVisual.score <
|
|
841
|
+
if (qaVisual.score === null || qaVisual.score < T['qa-visual']) {
|
|
820
842
|
return {
|
|
821
843
|
canAdvance: false,
|
|
822
|
-
reason: `QA-Visual score ${qaVisual.score || 0} below threshold
|
|
823
|
-
requiredScore:
|
|
844
|
+
reason: `QA-Visual score ${qaVisual.score || 0} below threshold ${T['qa-visual']}`,
|
|
845
|
+
requiredScore: T['qa-visual'],
|
|
824
846
|
};
|
|
825
847
|
}
|
|
826
848
|
}
|
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import yaml from 'js-yaml';
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
|
|
12
12
|
const SESSION_FILE = '.chati/session.yaml';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Atomic write helper. Writes `content` to a sibling `.tmp` file and then
|
|
16
|
+
* renames it into place. If the process is killed mid-write the original
|
|
17
|
+
* remains intact. Same-FS rename on POSIX is atomic; on Windows it's
|
|
18
|
+
* best-effort. Mirrors the pattern used by `chati.dev/hooks/prism-engine.js`
|
|
19
|
+
* `writeBackSession` to keep both hook and CLI write paths consistent.
|
|
20
|
+
*/
|
|
21
|
+
function writeFileAtomic(path, content, encoding = 'utf-8') {
|
|
22
|
+
const tmp = `${path}.tmp`;
|
|
23
|
+
writeFileSync(tmp, content, encoding);
|
|
24
|
+
renameSync(tmp, path);
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* Default session template.
|
|
16
29
|
*/
|
|
@@ -26,6 +39,12 @@ const DEFAULT_SESSION = {
|
|
|
26
39
|
started_at: null,
|
|
27
40
|
current_agent: '',
|
|
28
41
|
pipeline_position: 0,
|
|
42
|
+
// execution_mode is a string enum ('interactive' | 'autonomous') per
|
|
43
|
+
// session.schema.json. Article XVIII deprecation (2026-04-18) collapsed
|
|
44
|
+
// the old 4-value execution_profile enum into this binary form. Per-
|
|
45
|
+
// transition metadata (setAt, reason) lives in mode_transitions[] below,
|
|
46
|
+
// not on this field. mode-manager.js was historically reading it as an
|
|
47
|
+
// object — that path was normalized to read the string in 2026-05.
|
|
29
48
|
execution_mode: 'interactive',
|
|
30
49
|
user_level: 'auto',
|
|
31
50
|
user_level_confidence: 0.0,
|
|
@@ -135,7 +154,7 @@ export function initSession(projectDir, options = {}) {
|
|
|
135
154
|
noRefs: true,
|
|
136
155
|
});
|
|
137
156
|
|
|
138
|
-
|
|
157
|
+
writeFileAtomic(sessionPath, yamlContent);
|
|
139
158
|
|
|
140
159
|
return {
|
|
141
160
|
created: true,
|
|
@@ -216,6 +235,14 @@ export function migrateSession(session) {
|
|
|
216
235
|
};
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
// Defensive seed: ensure execution_mode is always present after migration.
|
|
239
|
+
// An unrecognized execution_profile (e.g. a hand-edited 'custom') or no
|
|
240
|
+
// profile at all used to leave the field absent, so validateSession could
|
|
241
|
+
// not catch it and every subsequent load re-migrated the same session. The
|
|
242
|
+
// profile mapping below can still promote this to 'autonomous'. Guarded so
|
|
243
|
+
// an existing 'autonomous' value is preserved.
|
|
244
|
+
if (!session.execution_mode) session.execution_mode = 'interactive';
|
|
245
|
+
|
|
219
246
|
// Article XVIII deprecation (2026-04-18): collapse execution_profile into
|
|
220
247
|
// execution_mode per the migration table. Runs LAST so it overrides any
|
|
221
248
|
// default seeded above. See constitution Article XVIII for rationale.
|
|
@@ -232,6 +259,20 @@ export function migrateSession(session) {
|
|
|
232
259
|
delete session.profile_transitions;
|
|
233
260
|
}
|
|
234
261
|
|
|
262
|
+
// execution_mode normalization (2026-05): the schema declares this field
|
|
263
|
+
// as a string enum ('interactive' | 'autonomous'), and all CLI/dashboard/
|
|
264
|
+
// hook consumers treat it as a string. Earlier `setExecutionMode` writes
|
|
265
|
+
// accidentally produced an object shape `{ mode, setAt, reason }` that
|
|
266
|
+
// only `mode-manager.js#getCurrentMode` understood. Sessions that ran
|
|
267
|
+
// through that path stored the object form; flatten them back to a string
|
|
268
|
+
// here so subsequent reads from cli.js / dashboard / templates work.
|
|
269
|
+
// Per-transition metadata lives in mode_transitions[] (already present).
|
|
270
|
+
if (session.execution_mode && typeof session.execution_mode === 'object' && !Array.isArray(session.execution_mode)) {
|
|
271
|
+
const inner = session.execution_mode.mode;
|
|
272
|
+
const map = { 'human-in-the-loop': 'interactive', interactive: 'interactive', autonomous: 'autonomous' };
|
|
273
|
+
session.execution_mode = map[inner] || 'interactive';
|
|
274
|
+
}
|
|
275
|
+
|
|
235
276
|
return { migrated: true, fromVersion, toVersion: CURRENT_SCHEMA_VERSION };
|
|
236
277
|
}
|
|
237
278
|
|
|
@@ -260,9 +301,16 @@ export function loadSession(projectDir) {
|
|
|
260
301
|
if (migration.migrated) {
|
|
261
302
|
try {
|
|
262
303
|
const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
|
|
263
|
-
|
|
264
|
-
} catch {
|
|
265
|
-
// Migration write failed
|
|
304
|
+
writeFileAtomic(sessionPath, yamlContent);
|
|
305
|
+
} catch (writeErr) {
|
|
306
|
+
// Migration write-back failed (read-only filesystem, EACCES). The
|
|
307
|
+
// session continues with the migrated in-memory copy, but disk stays
|
|
308
|
+
// on the old schema and re-migrates on every load. Surface it so the
|
|
309
|
+
// user can fix the cause (make .chati/ writable) instead of it failing
|
|
310
|
+
// silently. The production path (writable FS) never reaches here.
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`[chati] warning: session migrated in memory but could not be written back to ${sessionPath} (${writeErr.code || writeErr.message}); it will re-migrate on every load until the directory is writable\n`
|
|
313
|
+
);
|
|
266
314
|
}
|
|
267
315
|
}
|
|
268
316
|
|
|
@@ -329,7 +377,7 @@ export function updateSession(projectDir, updates) {
|
|
|
329
377
|
noRefs: true,
|
|
330
378
|
});
|
|
331
379
|
|
|
332
|
-
|
|
380
|
+
writeFileAtomic(sessionPath, yamlContent);
|
|
333
381
|
|
|
334
382
|
return {
|
|
335
383
|
saved: true,
|
|
@@ -392,7 +440,7 @@ function writeSessionToDisk(projectDir, session) {
|
|
|
392
440
|
const sessionPath = join(projectDir, SESSION_FILE);
|
|
393
441
|
try {
|
|
394
442
|
const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
|
|
395
|
-
|
|
443
|
+
writeFileAtomic(sessionPath, yamlContent);
|
|
396
444
|
return { saved: true, session };
|
|
397
445
|
} catch (err) {
|
|
398
446
|
return { saved: false, error: `Failed to write session: ${err.message}` };
|
|
@@ -875,7 +923,7 @@ export function validateSession(projectDir) {
|
|
|
875
923
|
const session = loadResult.session;
|
|
876
924
|
|
|
877
925
|
// Validate required fields
|
|
878
|
-
const requiredFields = ['schema_version', 'version', 'mode', 'language', 'project_type', 'agents'];
|
|
926
|
+
const requiredFields = ['schema_version', 'version', 'mode', 'execution_mode', 'language', 'project_type', 'agents'];
|
|
879
927
|
for (const field of requiredFields) {
|
|
880
928
|
if (!session[field]) {
|
|
881
929
|
return {
|
package/src/telemetry/sender.js
CHANGED
|
@@ -5,30 +5,19 @@
|
|
|
5
5
|
* Fire-and-forget: never blocks the user workflow, fails silently.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
|
-
|
|
12
8
|
// ---------------------------------------------------------------------------
|
|
13
9
|
// Default Endpoint
|
|
14
10
|
// ---------------------------------------------------------------------------
|
|
15
11
|
|
|
16
12
|
export const DEFAULT_ENDPOINT = 'https://chati.dev/api/telemetry';
|
|
17
13
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!existsSync(p)) return null;
|
|
26
|
-
const content = readFileSync(p, 'utf-8');
|
|
27
|
-
const match = content.match(/^key:\s*(.+)$/m);
|
|
28
|
-
const key = match?.[1]?.trim();
|
|
29
|
-
return key && key !== 'null' ? key : null;
|
|
30
|
-
} catch { return null; }
|
|
31
|
-
}
|
|
14
|
+
// Note: the prior implementation read the local license key from
|
|
15
|
+
// `~/.chati-dev/license.yaml` and shipped it inside every telemetry POST as
|
|
16
|
+
// `license_key`. That sent a paying user's credential to the analytics
|
|
17
|
+
// endpoint on every install/pipeline event — outside the per-event PII
|
|
18
|
+
// scrubber. Removed (2026-05). Server-side attribution should be derived
|
|
19
|
+
// from the validated license session correlated to `anonymousId`, never from
|
|
20
|
+
// the raw key over the wire.
|
|
32
21
|
|
|
33
22
|
// ---------------------------------------------------------------------------
|
|
34
23
|
// Sender
|
|
@@ -50,7 +39,6 @@ export async function sendEvents(events, config) {
|
|
|
50
39
|
|
|
51
40
|
const payload = {
|
|
52
41
|
anonymousId: config.anonymousId,
|
|
53
|
-
license_key: readGlobalLicenseKey(),
|
|
54
42
|
project_name: config.projectName ?? null,
|
|
55
43
|
chatiVersion: config.version || 'unknown',
|
|
56
44
|
nodeVersion: process.version,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
import { buildAgentPrompt } from './prompt-builder.js';
|
|
19
|
-
import { spawnParallelGroup, spawnTerminal } from './spawner.js';
|
|
19
|
+
import { spawnParallelGroup, spawnTerminal, killTerminal } from './spawner.js';
|
|
20
20
|
import { TerminalMonitor } from './monitor.js';
|
|
21
21
|
import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
|
|
22
22
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
@@ -254,13 +254,13 @@ async function main() {
|
|
|
254
254
|
* @param {number} timeout - Per-agent timeout in ms
|
|
255
255
|
* @returns {Promise<{ groupId: string, terminals: object[] }>}
|
|
256
256
|
*/
|
|
257
|
-
async function sequentialFallback(configs, timeout) {
|
|
257
|
+
async function sequentialFallback(configs, timeout, _spawnFn = spawnTerminal) {
|
|
258
258
|
const groupId = `seq-fallback-${Date.now()}`;
|
|
259
259
|
const terminals = [];
|
|
260
260
|
|
|
261
261
|
for (let i = 0; i < configs.length; i++) {
|
|
262
262
|
const cfg = configs[i];
|
|
263
|
-
const
|
|
263
|
+
const handle = _spawnFn({
|
|
264
264
|
agent: cfg.agent,
|
|
265
265
|
taskId: cfg.taskId,
|
|
266
266
|
model: cfg.model,
|
|
@@ -270,26 +270,26 @@ async function sequentialFallback(configs, timeout) {
|
|
|
270
270
|
timeout: cfg.timeout || timeout,
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
// Wait for
|
|
273
|
+
// Wait for the REAL process exit before spawning the next agent. The old
|
|
274
|
+
// implementation probed terminal.onExit?.(), a method TerminalHandle never
|
|
275
|
+
// had, so every agent "completed" instantly and the fallback ran the whole
|
|
276
|
+
// group in parallel - defeating its purpose (shedding rate-limit pressure).
|
|
274
277
|
await new Promise((resolve) => {
|
|
275
|
-
|
|
276
|
-
terminal.kill?.();
|
|
278
|
+
if (!handle?.process || handle.status !== 'running') {
|
|
277
279
|
resolve();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
killTerminal(handle).catch(() => {}).then(resolve, resolve);
|
|
278
284
|
}, (cfg.timeout || timeout) + 5_000);
|
|
279
285
|
|
|
280
|
-
|
|
286
|
+
handle.process.once('exit', () => {
|
|
281
287
|
clearTimeout(timer);
|
|
282
288
|
resolve();
|
|
283
289
|
});
|
|
284
|
-
|
|
285
|
-
// If terminal doesn't have onExit, resolve after a short poll
|
|
286
|
-
if (!terminal.onExit) {
|
|
287
|
-
clearTimeout(timer);
|
|
288
|
-
resolve();
|
|
289
|
-
}
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
terminals.push(
|
|
292
|
+
terminals.push(handle);
|
|
293
293
|
|
|
294
294
|
// Small delay between spawns to avoid rate limit pressure
|
|
295
295
|
if (i < configs.length - 1) {
|
package/src/terminal/run-team.js
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'url';
|
|
|
21
21
|
import { existsSync, readFileSync } from 'fs';
|
|
22
22
|
import { join } from 'path';
|
|
23
23
|
import { buildAgentPrompt } from './prompt-builder.js';
|
|
24
|
-
import { spawnParallelGroup
|
|
24
|
+
import { spawnParallelGroup } from './spawner.js';
|
|
25
25
|
import { TerminalMonitor } from './monitor.js';
|
|
26
26
|
import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
|
|
27
27
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
@@ -107,7 +107,7 @@ async function main() {
|
|
|
107
107
|
const taskListPath = join(teamDir, 'tasks.yaml');
|
|
108
108
|
|
|
109
109
|
// Load session state (minimal parse)
|
|
110
|
-
|
|
110
|
+
const sessionState = {};
|
|
111
111
|
try {
|
|
112
112
|
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
113
113
|
if (existsSync(sessionPath)) {
|
|
@@ -166,7 +166,7 @@ async function main() {
|
|
|
166
166
|
|
|
167
167
|
// Spawn team members
|
|
168
168
|
let group;
|
|
169
|
-
|
|
169
|
+
const fallbackUsed = false;
|
|
170
170
|
const echoEvents = [];
|
|
171
171
|
|
|
172
172
|
try {
|
package/src/upgrade/backup.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, cpSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
4
|
+
import { FRAMEWORK_DIRS_TO_COPY } from '../installer/core.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The backup must cover exactly what the upgrade overwrites, or a rollback
|
|
8
|
+
* leaves the install in a chimera state (some dirs at the new version, some
|
|
9
|
+
* restored). The upgrade replaces every dir in FRAMEWORK_DIRS_TO_COPY (via
|
|
10
|
+
* copyFrameworkFiles), so that same list is the backup/restore scope - one
|
|
11
|
+
* source of truth, no asymmetry. Previously the backup saved 12 dirs and the
|
|
12
|
+
* restore returned only 10 (intelligence and data were backed up but never
|
|
13
|
+
* restored; hooks, domains, tasks, scaffold and others were never backed up
|
|
14
|
+
* at all).
|
|
15
|
+
*/
|
|
16
|
+
const BACKUP_DIRS = FRAMEWORK_DIRS_TO_COPY;
|
|
17
|
+
|
|
18
|
+
const BACKUP_FILES = [
|
|
19
|
+
'constitution.md',
|
|
20
|
+
'config.yaml',
|
|
21
|
+
];
|
|
4
22
|
|
|
5
23
|
/**
|
|
6
24
|
* Create backup of chati.dev/ directory before upgrade
|
|
@@ -17,28 +35,7 @@ export function createBackup(targetDir, currentVersion) {
|
|
|
17
35
|
|
|
18
36
|
mkdirSync(backupDir, { recursive: true });
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
const dirsToBackup = [
|
|
22
|
-
'orchestrator',
|
|
23
|
-
'agents',
|
|
24
|
-
'templates',
|
|
25
|
-
'workflows',
|
|
26
|
-
'quality-gates',
|
|
27
|
-
'schemas',
|
|
28
|
-
'frameworks',
|
|
29
|
-
'i18n',
|
|
30
|
-
'patterns',
|
|
31
|
-
'migrations',
|
|
32
|
-
'intelligence',
|
|
33
|
-
'data',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const filesToBackup = [
|
|
37
|
-
'constitution.md',
|
|
38
|
-
'config.yaml',
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
for (const dir of dirsToBackup) {
|
|
38
|
+
for (const dir of BACKUP_DIRS) {
|
|
42
39
|
const src = join(frameworkDir, dir);
|
|
43
40
|
const dest = join(backupDir, dir);
|
|
44
41
|
if (existsSync(src)) {
|
|
@@ -46,7 +43,7 @@ export function createBackup(targetDir, currentVersion) {
|
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
for (const file of
|
|
46
|
+
for (const file of BACKUP_FILES) {
|
|
50
47
|
const src = join(frameworkDir, file);
|
|
51
48
|
const dest = join(backupDir, file);
|
|
52
49
|
if (existsSync(src)) {
|
|
@@ -68,26 +65,7 @@ export function restoreFromBackup(targetDir, version) {
|
|
|
68
65
|
throw new Error(`Backup not found: ${backupDir}`);
|
|
69
66
|
}
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
const dirsToRestore = [
|
|
73
|
-
'orchestrator',
|
|
74
|
-
'agents',
|
|
75
|
-
'templates',
|
|
76
|
-
'workflows',
|
|
77
|
-
'quality-gates',
|
|
78
|
-
'schemas',
|
|
79
|
-
'frameworks',
|
|
80
|
-
'i18n',
|
|
81
|
-
'patterns',
|
|
82
|
-
'migrations',
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
const filesToRestore = [
|
|
86
|
-
'constitution.md',
|
|
87
|
-
'config.yaml',
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
for (const dir of dirsToRestore) {
|
|
68
|
+
for (const dir of BACKUP_DIRS) {
|
|
91
69
|
const src = join(backupDir, dir);
|
|
92
70
|
const dest = join(frameworkDir, dir);
|
|
93
71
|
if (existsSync(src)) {
|
|
@@ -98,7 +76,7 @@ export function restoreFromBackup(targetDir, version) {
|
|
|
98
76
|
}
|
|
99
77
|
}
|
|
100
78
|
|
|
101
|
-
for (const file of
|
|
79
|
+
for (const file of BACKUP_FILES) {
|
|
102
80
|
const src = join(backupDir, file);
|
|
103
81
|
const dest = join(frameworkDir, file);
|
|
104
82
|
if (existsSync(src)) {
|
package/src/upgrade/migrator.js
CHANGED
|
@@ -42,13 +42,25 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
|
|
|
42
42
|
for (const file of files) {
|
|
43
43
|
try {
|
|
44
44
|
const content = yaml.load(readFileSync(join(migrationsDir, file), 'utf-8'));
|
|
45
|
-
|
|
45
|
+
// Two header schemas exist: the nested `migration: { from, to }` form and
|
|
46
|
+
// the older flat `from_version`/`to_version` form (with `operations`
|
|
47
|
+
// instead of `steps`). The flat files were silently skipped here, which
|
|
48
|
+
// hid them from every upgrade.
|
|
49
|
+
const migration = content?.migration
|
|
50
|
+
?? (content?.from_version && content?.to_version
|
|
51
|
+
? { from: content.from_version, to: content.to_version, description: content.description, breaking: content.breaking }
|
|
52
|
+
: null);
|
|
46
53
|
|
|
47
54
|
if (!migration?.from || !migration?.to) continue;
|
|
48
55
|
|
|
49
|
-
//
|
|
56
|
+
// A migration applies when its TARGET version lies inside the upgrade
|
|
57
|
+
// interval (fromVersion, toVersion]. The old condition compared the
|
|
58
|
+
// migration's `from` against the user's version with gte, which skipped
|
|
59
|
+
// every pending migration for users sitting ABOVE a migration's starting
|
|
60
|
+
// point (a 4.2.3 user never received v4.2-to-v4.3) and is wrong for
|
|
61
|
+
// users below it too (a 3.x user needs every step on the way up).
|
|
50
62
|
if (
|
|
51
|
-
semver.
|
|
63
|
+
semver.gt(migration.to, fromVersion) &&
|
|
52
64
|
semver.lte(migration.to, toVersion)
|
|
53
65
|
) {
|
|
54
66
|
applicable.push({
|
|
@@ -57,7 +69,7 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
|
|
|
57
69
|
to: migration.to,
|
|
58
70
|
description: migration.description || '',
|
|
59
71
|
breaking: migration.breaking || false,
|
|
60
|
-
steps: content.steps || [],
|
|
72
|
+
steps: content.steps || content.operations || [],
|
|
61
73
|
rollback: content.rollback || [],
|
|
62
74
|
});
|
|
63
75
|
}
|
|
@@ -73,14 +85,20 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
|
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
/**
|
|
76
|
-
* Execute a single migration step
|
|
88
|
+
* Execute a single migration step. Exported for testing.
|
|
77
89
|
*/
|
|
78
|
-
async function executeMigrationStep(targetDir, step) {
|
|
90
|
+
export async function executeMigrationStep(targetDir, step, sourceFile = '') {
|
|
79
91
|
switch (step.type) {
|
|
80
92
|
case 'create_directory': {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
// Both shapes exist in the migration corpus: `path` (string) in the
|
|
94
|
+
// nested-schema files and `paths` (array) in the older flat-schema ones.
|
|
95
|
+
const dirs = Array.isArray(step.paths) ? step.paths : [step.path];
|
|
96
|
+
for (const rel of dirs) {
|
|
97
|
+
if (!rel) continue;
|
|
98
|
+
const dir = join(targetDir, rel);
|
|
99
|
+
if (!existsSync(dir) || !step.idempotent) {
|
|
100
|
+
mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
84
102
|
}
|
|
85
103
|
break;
|
|
86
104
|
}
|
|
@@ -100,7 +118,19 @@ async function executeMigrationStep(targetDir, step) {
|
|
|
100
118
|
|
|
101
119
|
const content = yaml.load(readFileSync(filePath, 'utf-8')) || {};
|
|
102
120
|
|
|
103
|
-
if (step.
|
|
121
|
+
if (step.changes && typeof step.changes === 'object') {
|
|
122
|
+
// Older flat-schema shape: a `changes:` map of (possibly dotted)
|
|
123
|
+
// field -> value, applied as a plain set.
|
|
124
|
+
for (const [field, value] of Object.entries(step.changes)) {
|
|
125
|
+
const keys = field.split('.');
|
|
126
|
+
let obj = content;
|
|
127
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
128
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
129
|
+
obj = obj[keys[i]];
|
|
130
|
+
}
|
|
131
|
+
obj[keys[keys.length - 1]] = value;
|
|
132
|
+
}
|
|
133
|
+
} else if (step.operation === 'add_field') {
|
|
104
134
|
const keys = step.field.split('.');
|
|
105
135
|
let obj = content;
|
|
106
136
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
@@ -176,7 +206,16 @@ async function executeMigrationStep(targetDir, step) {
|
|
|
176
206
|
}
|
|
177
207
|
|
|
178
208
|
default:
|
|
179
|
-
//
|
|
209
|
+
// Copy-style steps (copy_file, copy_directory) from the older flat-schema
|
|
210
|
+
// migrations are subsumed by the upgrade's blanket copyFrameworkFiles, so
|
|
211
|
+
// skipping them is correct. Anything else unknown is surfaced instead of
|
|
212
|
+
// silently dropped, so an upgrader from a very old version knows what to
|
|
213
|
+
// review manually.
|
|
214
|
+
if (step.type !== 'copy_file' && step.type !== 'copy_directory') {
|
|
215
|
+
process.stderr.write(
|
|
216
|
+
`[chati] warning: migration step type "${step.type}"${sourceFile ? ` in ${sourceFile}` : ''} is not auto-executable; review the migration file manually\n`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
180
219
|
break;
|
|
181
220
|
}
|
|
182
221
|
}
|
|
@@ -196,7 +235,7 @@ export async function runMigrations(targetDir, fromVersion, toVersion) {
|
|
|
196
235
|
for (const migration of migrations) {
|
|
197
236
|
try {
|
|
198
237
|
for (const step of migration.steps) {
|
|
199
|
-
await executeMigrationStep(targetDir, step);
|
|
238
|
+
await executeMigrationStep(targetDir, step, migration.file);
|
|
200
239
|
}
|
|
201
240
|
results.push({ file: migration.file, success: true });
|
|
202
241
|
} catch (err) {
|
|
@@ -41,6 +41,11 @@ function isGitRepo(targetDir) {
|
|
|
41
41
|
* Detect which framework-owned paths are currently tracked by git.
|
|
42
42
|
* Does NOT modify git state — read-only operation.
|
|
43
43
|
*
|
|
44
|
+
* Runs a single `git ls-files -z -- <pattern1> <pattern2> …` invocation and
|
|
45
|
+
* partitions the result locally. The prior implementation spawned one
|
|
46
|
+
* `git ls-files` per pattern (19 processes per call) which added several
|
|
47
|
+
* hundred ms of overhead on a slow disk during every upgrade check.
|
|
48
|
+
*
|
|
44
49
|
* @param {string} targetDir - absolute path to the project root
|
|
45
50
|
* @returns {string[]} array of tracked framework file paths (relative to targetDir)
|
|
46
51
|
*/
|
|
@@ -49,28 +54,41 @@ export function detectTrackedFrameworkFiles(targetDir) {
|
|
|
49
54
|
return [];
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
let stdout;
|
|
58
|
+
try {
|
|
59
|
+
// -z: NUL-separated output (safe for paths with spaces/newlines).
|
|
60
|
+
// Pass all patterns as positional args after `--` so they're treated as
|
|
61
|
+
// pathspecs, not options. Quoting is handled by execFile-style argv if
|
|
62
|
+
// available; we use execSync with a builtin pattern array escape.
|
|
63
|
+
const args = FRAMEWORK_PATTERNS.map(p => JSON.stringify(p)).join(' ');
|
|
64
|
+
stdout = execSync(`git ls-files -z -- ${args}`, {
|
|
65
|
+
cwd: targetDir,
|
|
66
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
67
|
+
timeout: 5000,
|
|
68
|
+
}).toString();
|
|
69
|
+
} catch {
|
|
70
|
+
// git ls-files failed entirely (not a repo, repo corrupt, etc.) → none tracked
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const result = execSync(`git ls-files -- "${pattern}"`, {
|
|
57
|
-
cwd: targetDir,
|
|
58
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
59
|
-
timeout: 5000,
|
|
60
|
-
})
|
|
61
|
-
.toString()
|
|
62
|
-
.trim();
|
|
74
|
+
if (!stdout) return [];
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
// Split on NUL. For each tracked path returned by git, find which of our
|
|
77
|
+
// FRAMEWORK_PATTERNS it belongs under (directory prefixes use trailing
|
|
78
|
+
// slash; exact-file patterns match equality).
|
|
79
|
+
const trackedPaths = stdout.split('\0').filter(Boolean);
|
|
80
|
+
const tracked = new Set();
|
|
81
|
+
for (const path of trackedPaths) {
|
|
82
|
+
for (const pattern of FRAMEWORK_PATTERNS) {
|
|
83
|
+
if (pattern.endsWith('/')) {
|
|
84
|
+
if (path.startsWith(pattern)) { tracked.add(pattern); break; }
|
|
85
|
+
} else if (path === pattern) {
|
|
86
|
+
tracked.add(pattern); break;
|
|
66
87
|
}
|
|
67
|
-
} catch {
|
|
68
|
-
// git ls-files failure means path is not tracked — skip
|
|
69
88
|
}
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
return [...new Set(tracked)];
|
|
91
|
+
return [...tracked];
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
/**
|