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
|
@@ -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;
|
package/src/orchestrator/cli.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
15
|
+
import { isFeatureEnabled } from '../utils/feature-flags.js';
|
|
16
|
+
import { runScriptTask, formatScriptHandoff } from '../executors/runner.js';
|
|
15
17
|
|
|
16
18
|
import {
|
|
17
19
|
loadSession, initSession, updateSession, recordAgentCompletion,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
import { resolveContextLimit } from '../utils/provider-limits.js';
|
|
25
27
|
import { waitForLicense } from '../license/wait.js';
|
|
26
28
|
import { runDoctor } from './doctor.js';
|
|
29
|
+
import { evaluateSurfaceCriteria } from '../autonomy/surface-criteria.js';
|
|
27
30
|
import {
|
|
28
31
|
getNextAgent, getAgentDefinition, AGENT_PIPELINE,
|
|
29
32
|
} from './index.js';
|
|
@@ -38,8 +41,9 @@ import {
|
|
|
38
41
|
import { analyzeDeviationImpact, applyDeviation } from './index.js';
|
|
39
42
|
import { detectQuickFlow, detectStandardFlow } from './index.js';
|
|
40
43
|
import { getAgentFile } from '../terminal/prompt-builder.js';
|
|
41
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync
|
|
42
|
-
import { join, resolve } from 'path';
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
45
|
+
import { join, resolve, dirname } from 'path';
|
|
46
|
+
import { fileURLToPath } from 'url';
|
|
43
47
|
import { execSync } from 'child_process';
|
|
44
48
|
import yaml from 'js-yaml';
|
|
45
49
|
import {
|
|
@@ -50,6 +54,15 @@ import {
|
|
|
50
54
|
} from '../intelligence/timeline.js';
|
|
51
55
|
import { updateClaudeMd } from '../memory/magic-docs.js';
|
|
52
56
|
|
|
57
|
+
// Spawn commands must reference the terminal runners with an absolute,
|
|
58
|
+
// layout-independent path: the old monorepo-relative
|
|
59
|
+
// 'packages/chati-dev/src/terminal/...' only resolves when the command runs
|
|
60
|
+
// from the monorepo root, never in an installed project (.chati.dev/_cli/ or
|
|
61
|
+
// node_modules). Anchoring to this file's own location works in every layout
|
|
62
|
+
// because the src/ tree shape is preserved verbatim by the CLI bundling.
|
|
63
|
+
const _THISDIR = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const RUNNER = (name) => JSON.stringify(join(_THISDIR, '..', 'terminal', name));
|
|
65
|
+
|
|
53
66
|
// ---------------------------------------------------------------------------
|
|
54
67
|
// Constants
|
|
55
68
|
// ---------------------------------------------------------------------------
|
|
@@ -530,13 +543,14 @@ function sessionToPipelineState(session, projectDir) {
|
|
|
530
543
|
|
|
531
544
|
/**
|
|
532
545
|
* Build the spawn command string for an autonomous agent.
|
|
546
|
+
* Exported for testing (path resolution must hold in every install layout).
|
|
533
547
|
*/
|
|
534
|
-
function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout) {
|
|
548
|
+
export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout) {
|
|
535
549
|
const parts = [
|
|
536
|
-
'node', '
|
|
550
|
+
'node', RUNNER('run-agent.js'),
|
|
537
551
|
'--agent', agent,
|
|
538
552
|
'--task-id', `${agent}-task`,
|
|
539
|
-
'--project-dir', resolve(projectDir),
|
|
553
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
540
554
|
'--previous-agent', previousAgent || 'none',
|
|
541
555
|
'--provider', provider || 'claude',
|
|
542
556
|
'--timeout', String(timeout || 600000),
|
|
@@ -546,14 +560,15 @@ function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout)
|
|
|
546
560
|
|
|
547
561
|
/**
|
|
548
562
|
* Build the parallel spawn command string.
|
|
563
|
+
* Exported for testing.
|
|
549
564
|
*/
|
|
550
|
-
function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout) {
|
|
565
|
+
export function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout) {
|
|
551
566
|
const taskIds = agents.map(a => `${a}-task`).join(',');
|
|
552
567
|
const parts = [
|
|
553
|
-
'node', '
|
|
568
|
+
'node', RUNNER('run-parallel.js'),
|
|
554
569
|
'--agents', agents.join(','),
|
|
555
570
|
'--task-ids', taskIds,
|
|
556
|
-
'--project-dir', resolve(projectDir),
|
|
571
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
557
572
|
'--previous-agent', previousAgent || 'none',
|
|
558
573
|
'--provider', provider || 'claude',
|
|
559
574
|
'--timeout', String(timeout || 900000),
|
|
@@ -931,6 +946,25 @@ async function _handleNextInner(projectDir) {
|
|
|
931
946
|
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000);
|
|
932
947
|
}
|
|
933
948
|
|
|
949
|
+
// Surface criteria (Article XVII): even in autonomous mode, a high-stakes
|
|
950
|
+
// transition still needs a human nod. Deploy is external and effectively
|
|
951
|
+
// irreversible, so it trips a critical criterion and is never auto-confirmed
|
|
952
|
+
// by the gates alone. Hold the autonomous spawn and hand control back to the
|
|
953
|
+
// user for that step.
|
|
954
|
+
let surfaceHold = null;
|
|
955
|
+
if (action === 'spawn_autonomous' && (agentDef?.phase === 'deploy' || nextAgent === 'devops')) {
|
|
956
|
+
const verdict = evaluateSurfaceCriteria(
|
|
957
|
+
{ type: 'deploy', description: `${nextAgent} (${agentDef?.phase || 'deploy'} phase)`, tags: ['external', 'destructive'] },
|
|
958
|
+
{},
|
|
959
|
+
);
|
|
960
|
+
const critical = verdict.triggers.filter((t) => t.severity === 'critical');
|
|
961
|
+
if (critical.length > 0) {
|
|
962
|
+
action = 'activate_interactive';
|
|
963
|
+
spawnCommand = null;
|
|
964
|
+
surfaceHold = { criteria: critical.map((t) => t.id), reason: critical.map((t) => t.reason).join('; ') };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
934
968
|
// Record agent activation event
|
|
935
969
|
recordEvent(projectDir, EventType.AGENT_ACTIVATED, nextAgent, { action, phase: agentDef?.phase });
|
|
936
970
|
|
|
@@ -952,6 +986,7 @@ async function _handleNextInner(projectDir) {
|
|
|
952
986
|
pipeline_progress: progress,
|
|
953
987
|
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
954
988
|
status_summary: `${lastAgent} completed. Next: ${nextAgent} (${action.replace('_', ' ')}, ${agentDef?.phase || 'unknown'} phase).`,
|
|
989
|
+
...(surfaceHold ? { surface: surfaceHold } : {}),
|
|
955
990
|
};
|
|
956
991
|
|
|
957
992
|
// Include team data when spawn_team action is selected (Article XXI)
|
|
@@ -989,6 +1024,7 @@ const SCAFFOLD_CONFIDENCE_THRESHOLD = 0.7;
|
|
|
989
1024
|
const PRESET_CONFIDENCE_THRESHOLDS = {
|
|
990
1025
|
'motion-premium': 0.7,
|
|
991
1026
|
'motion-premium-3d': 0.8,
|
|
1027
|
+
'saas-dashboard': 0.7,
|
|
992
1028
|
};
|
|
993
1029
|
|
|
994
1030
|
function thresholdFor(preset) {
|
|
@@ -1537,6 +1573,28 @@ async function handleAdvance(projectDir, args) {
|
|
|
1537
1573
|
},
|
|
1538
1574
|
};
|
|
1539
1575
|
}
|
|
1576
|
+
|
|
1577
|
+
// Spring-motion veto: linear easing on a transition and transition-property:
|
|
1578
|
+
// all are amateur-motion tells measured by visual-qa.js. Block the
|
|
1579
|
+
// autonomous advance on them, the same way layout_errors block, so the code
|
|
1580
|
+
// gate enforces premium motion and not only the QA-Visual agent prose.
|
|
1581
|
+
const motionErrors = (report.summary?.motion_errors || []).filter(e => e.severity === 'error');
|
|
1582
|
+
if (motionErrors.length > 0) {
|
|
1583
|
+
return {
|
|
1584
|
+
advanced: false,
|
|
1585
|
+
blocked: true,
|
|
1586
|
+
reason: 'motion_errors_detected',
|
|
1587
|
+
message: `${motionErrors.length} amateur-motion error(s) measured: linear easing on a transition, or transition-property: all. Premium motion uses spring or cubic-bezier easing on specific compositor properties (transform, opacity).`,
|
|
1588
|
+
motion_error_count: motionErrors.length,
|
|
1589
|
+
motion_errors: motionErrors,
|
|
1590
|
+
fault_origin: 'CODE',
|
|
1591
|
+
action_required: {
|
|
1592
|
+
step1: 'Display the motion errors to the user with the offending selectors',
|
|
1593
|
+
step2: 'Route back to dev for rework (Fault Vector Protocol: CODE -> dev)',
|
|
1594
|
+
step3: 'Dev replaces linear/all easing with a cubic-bezier on transform/opacity, re-runs visual-qa.js, then re-runs advance qa-visual',
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1540
1598
|
} catch {
|
|
1541
1599
|
// report.json unparseable — fall through to other checks, they will catch it
|
|
1542
1600
|
}
|
|
@@ -2654,10 +2712,10 @@ async function handleSpawnTeam(projectDir, args) {
|
|
|
2654
2712
|
|
|
2655
2713
|
// Build spawn command
|
|
2656
2714
|
const spawnCmd = [
|
|
2657
|
-
'node',
|
|
2715
|
+
'node', RUNNER('run-team.js'),
|
|
2658
2716
|
'--team-id', teamId,
|
|
2659
2717
|
'--team-type', teamType,
|
|
2660
|
-
'--project-dir', resolve(projectDir),
|
|
2718
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
2661
2719
|
'--previous-agent', previousAgent,
|
|
2662
2720
|
'--provider', provider,
|
|
2663
2721
|
'--timeout', args.timeout || '1800000',
|
|
@@ -2941,7 +2999,7 @@ async function handleWaitForCapture(projectDir, args) {
|
|
|
2941
2999
|
* qa-visual-score: calculate visual QA score deterministically from report.json.
|
|
2942
3000
|
* Replaces QA-Visual's generative scoring formula.
|
|
2943
3001
|
*/
|
|
2944
|
-
function handleQaVisualScore(args) {
|
|
3002
|
+
export function handleQaVisualScore(args) {
|
|
2945
3003
|
const reportPath = args.report || '/tmp/visual-qa/report.json';
|
|
2946
3004
|
if (!existsSync(reportPath)) {
|
|
2947
3005
|
return errorResult(`report.json not found at ${reportPath}`, 'REPORT_NOT_FOUND');
|
|
@@ -2955,18 +3013,51 @@ function handleQaVisualScore(args) {
|
|
|
2955
3013
|
const s = report.summary || {};
|
|
2956
3014
|
const hasRefs = args['has-refs'] === 'true';
|
|
2957
3015
|
|
|
3016
|
+
// The animation stack the project actually uses. The motion-premium presets
|
|
3017
|
+
// ship GSAP + Lenis; the saas-dashboard app-shell preset ships framer-motion;
|
|
3018
|
+
// some projects use CSS only. The Lenis/GSAP presence checks apply ONLY to the
|
|
3019
|
+
// gsap-lenis stack, so a framer-motion project is not falsely blocked for
|
|
3020
|
+
// missing libraries it deliberately does not use. The agent resolves the stack
|
|
3021
|
+
// from architecture.md and passes --animation-library (default gsap-lenis for
|
|
3022
|
+
// backward compatibility with the motion presets).
|
|
3023
|
+
// Default to gsap-lenis for backward compatibility (the motion presets), but do
|
|
3024
|
+
// NOT assume it when the flag is absent AND the report shows no GSAP or Lenis
|
|
3025
|
+
// usage at all: a clean framer-motion or CSS project would otherwise be
|
|
3026
|
+
// phantom-blocked for libraries it never used. In that case fall back to a
|
|
3027
|
+
// non-GSAP stack (no GSAP/Lenis hard-block) and warn that --animation-library
|
|
3028
|
+
// should be passed explicitly. A project that passes the flag is unaffected.
|
|
3029
|
+
const animLibFlag = args['animation-library'];
|
|
3030
|
+
const animLibInferred = !animLibFlag;
|
|
3031
|
+
let animLib = String(animLibFlag || 'gsap-lenis').toLowerCase();
|
|
3032
|
+
if (animLibInferred && !s.gsap_loaded_all && !(s.total_gsap_animations > 0) && !s.lenis_active_all) {
|
|
3033
|
+
animLib = 'unknown';
|
|
3034
|
+
}
|
|
3035
|
+
const isGsapStack = animLib === 'gsap-lenis' || animLib === 'gsap' || animLib === 'lenis';
|
|
3036
|
+
|
|
3037
|
+
// Measured error signals from the report (objective, not judged).
|
|
3038
|
+
const motionErrorCount = (s.motion_errors || []).filter(e => e.severity === 'error').length;
|
|
3039
|
+
const layoutErrorCount = s.layout_error_count || 0;
|
|
3040
|
+
const reducedMotionViolations = (s.reduced_motion_violations || []).length;
|
|
3041
|
+
|
|
2958
3042
|
// Weights (adjust when references exist)
|
|
2959
3043
|
const w = hasRefs
|
|
2960
3044
|
? { animation: 0.30, scroll: 0.20, responsive: 0.15, hover: 0.10, brand: 0.10, fidelity: 0.15 }
|
|
2961
3045
|
: { animation: 0.35, scroll: 0.25, responsive: 0.20, hover: 0.10, brand: 0.10, fidelity: 0 };
|
|
2962
3046
|
|
|
2963
|
-
//
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3047
|
+
// Animation score is stack-aware. The gsap-lenis stack scores on Lenis + GSAP
|
|
3048
|
+
// presence and animation count; any other stack scores on the motion discipline
|
|
3049
|
+
// the report can measure: reduced-motion support and the absence of amateur
|
|
3050
|
+
// motion (the spring-motion veto), since premium motion can be built without
|
|
3051
|
+
// GSAP.
|
|
3052
|
+
const animScore = isGsapStack
|
|
3053
|
+
? Math.min(100, (s.lenis_active_all ? 50 : 0) + (s.gsap_loaded_all ? 30 : 0) + Math.min(20, (s.total_gsap_animations || 0) * 2))
|
|
3054
|
+
: Math.max(0, Math.min(100, 70 + (s.reduced_motion_support ? 20 : 0) + (motionErrorCount === 0 ? 10 : -30)));
|
|
3055
|
+
|
|
3056
|
+
const scrollScore = (s.total_scroll_triggers || 0) > 0 ? 80 + Math.min(20, s.total_scroll_triggers * 2) : (isGsapStack ? 40 : 80);
|
|
3057
|
+
const responsiveScore = layoutErrorCount > 0 ? 40 : 80; // layout errors are measured; the agent judges the rest
|
|
3058
|
+
const hoverScore = 80; // baseline; the agent judges interaction states from screenshots
|
|
2968
3059
|
const brandScore = (s.reduced_motion_support ? 10 : 0) + Math.min(90, (report.pages?.[0]?.js_checks?.custom_properties_count ?? 0));
|
|
2969
|
-
const fidelityScore = 70; // placeholder
|
|
3060
|
+
const fidelityScore = 70; // placeholder; the Visualizer comparison is the agent's judged signal
|
|
2970
3061
|
|
|
2971
3062
|
let rawScore = Math.round(
|
|
2972
3063
|
animScore * w.animation +
|
|
@@ -2981,18 +3072,96 @@ function handleQaVisualScore(args) {
|
|
|
2981
3072
|
const errors = [];
|
|
2982
3073
|
const warnings = [];
|
|
2983
3074
|
|
|
2984
|
-
if (!s.lenis_active_all) errors.push('Lenis not active on all pages');
|
|
2985
|
-
if (!s.gsap_loaded_all) errors.push('GSAP not loaded on all pages');
|
|
3075
|
+
if (isGsapStack && !s.lenis_active_all) errors.push('Lenis not active on all pages');
|
|
3076
|
+
if (isGsapStack && !s.gsap_loaded_all) errors.push('GSAP not loaded on all pages');
|
|
3077
|
+
if (motionErrorCount > 0) { errors.push(`${motionErrorCount} amateur-motion error(s): linear easing or transition:all`); rawScore -= 15; }
|
|
3078
|
+
if (layoutErrorCount > 0) { errors.push(`${layoutErrorCount} objective layout error(s)`); rawScore -= 15; }
|
|
3079
|
+
if (reducedMotionViolations > 0) { errors.push(`${reducedMotionViolations} reduced-motion violation(s)`); rawScore -= 10; }
|
|
2986
3080
|
if (s.em_dashes_detected) { errors.push('Em-dashes found in visible text'); rawScore -= 10; }
|
|
2987
3081
|
if (s.console_errors_total > 0) { warnings.push(`${s.console_errors_total} console error(s)`); rawScore -= 5; }
|
|
3082
|
+
if (animLibInferred) warnings.push('No --animation-library was passed; resolve the motion stack from architecture.md and pass it (gsap-lenis | framer-motion | css | none) for an accurate verdict.');
|
|
3083
|
+
|
|
3084
|
+
// Hard blocks: measured failures that cannot be APPROVED regardless of the
|
|
3085
|
+
// agent's judged 8-dimension rubric score. The agent applies visual_rubric on
|
|
3086
|
+
// top of this measured floor and SHALL NOT issue APPROVED when hardBlocked.
|
|
3087
|
+
const hardBlocked =
|
|
3088
|
+
(isGsapStack && (!s.lenis_active_all || !s.gsap_loaded_all)) ||
|
|
3089
|
+
motionErrorCount > 0 ||
|
|
3090
|
+
layoutErrorCount > 0 ||
|
|
3091
|
+
reducedMotionViolations > 0 ||
|
|
3092
|
+
!!s.em_dashes_detected;
|
|
2988
3093
|
|
|
2989
|
-
// Hard blocks
|
|
2990
|
-
const hardBlocked = !s.lenis_active_all || !s.gsap_loaded_all || s.em_dashes_detected;
|
|
2991
3094
|
const score = Math.max(0, Math.min(100, rawScore));
|
|
2992
3095
|
const threshold = 90;
|
|
2993
|
-
|
|
3096
|
+
// FLOOR_COMPUTED (not NEEDS_CORRECTION) is the label for a clean-but-below-floor
|
|
3097
|
+
// measured score: it signals that this is the CLI's measured floor, NOT the gate
|
|
3098
|
+
// verdict. The gate verdict is the QA-Visual agent's 8-dimension rubric score. A
|
|
3099
|
+
// clean framer-motion build can sit below the measured floor yet still be APPROVED
|
|
3100
|
+
// by the agent, so the CLI must not emit a correction-sounding label here.
|
|
3101
|
+
const verdict = hardBlocked ? 'BLOCKED' : score >= threshold ? 'APPROVED' : 'FLOOR_COMPUTED';
|
|
3102
|
+
|
|
3103
|
+
return {
|
|
3104
|
+
score, threshold, verdict, hardBlocked, animation_library: animLib, errors, warnings, weights: w,
|
|
3105
|
+
measured_signals: {
|
|
3106
|
+
motion_error_count: motionErrorCount,
|
|
3107
|
+
layout_error_count: layoutErrorCount,
|
|
3108
|
+
reduced_motion_violations: reducedMotionViolations,
|
|
3109
|
+
em_dashes: !!s.em_dashes_detected,
|
|
3110
|
+
},
|
|
3111
|
+
dimensions: { animScore, scrollScore, responsiveScore, hoverScore, brandScore, fidelityScore },
|
|
3112
|
+
note: 'Measured objective floor plus hard-blocks. BLOCKED is binding (a measured failure). APPROVED and FLOOR_COMPUTED are NOT the gate verdict: the QA-Visual agent applies the 8-dimension visual_rubric (chati.dev/data/qa-rubrics.yaml) for the judged dimensions and issues the verdict. FLOOR_COMPUTED means the measured floor is below the threshold but nothing is hard-blocked, so the agent rubric score decides. The agent cannot issue APPROVED when hardBlocked is true.',
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
/**
|
|
3117
|
+
* run-script-task: execute a task whose execution_kind is `script` through its
|
|
3118
|
+
* deterministic executor (no LLM). Gated by the execution_kinds feature flag;
|
|
3119
|
+
* when the flag is off it returns ran:false so the orchestrator uses the agent
|
|
3120
|
+
* path and the pipeline is unchanged. The orchestrator resolves the task fields
|
|
3121
|
+
* from tasks.yaml and passes them as flags (--ref, --id, --title).
|
|
3122
|
+
*/
|
|
3123
|
+
export async function handleRunScriptTask(projectDir, args) {
|
|
3124
|
+
if (!isFeatureEnabled(projectDir, 'execution_kinds')) {
|
|
3125
|
+
return { ran: false, reason: 'The execution_kinds feature is off. Run this task through the agent path.', fallback: 'agent' };
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const kind = String(args.kind || 'script');
|
|
3129
|
+
if (kind !== 'script') {
|
|
3130
|
+
return { ran: false, reason: `execution_kind "${kind}" is not auto-run by the script runner (only "script" is). Route through the normal path.`, kind };
|
|
3131
|
+
}
|
|
2994
3132
|
|
|
2995
|
-
|
|
3133
|
+
const ref = args.ref || '';
|
|
3134
|
+
if (!ref) {
|
|
3135
|
+
return { ran: false, reason: 'Missing --ref (the execution_ref, for example "npm-script:test").' };
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
const id = args.id || args.task || '';
|
|
3139
|
+
const title = args.title || '';
|
|
3140
|
+
const result = await runScriptTask(projectDir, { id, title, execution_ref: ref });
|
|
3141
|
+
|
|
3142
|
+
// Write a synthetic handoff so the pipeline reads it like an agent handoff.
|
|
3143
|
+
const timestamp = new Date().toISOString();
|
|
3144
|
+
const handoff = formatScriptHandoff({ id, title }, result, timestamp);
|
|
3145
|
+
let handoffPath = null;
|
|
3146
|
+
try {
|
|
3147
|
+
const dir = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs');
|
|
3148
|
+
mkdirSync(dir, { recursive: true });
|
|
3149
|
+
handoffPath = join(dir, `script-${(id || 'task').replace(/[^A-Za-z0-9._-]/g, '_')}.md`);
|
|
3150
|
+
writeFileSync(handoffPath, handoff, 'utf-8');
|
|
3151
|
+
} catch {
|
|
3152
|
+
// best-effort; the structured result is still returned for the orchestrator
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
return {
|
|
3156
|
+
ran: true,
|
|
3157
|
+
executor: result.executor,
|
|
3158
|
+
arg: result.arg,
|
|
3159
|
+
status: result.status,
|
|
3160
|
+
summary: result.summary,
|
|
3161
|
+
durationMs: result.durationMs,
|
|
3162
|
+
handoffPath,
|
|
3163
|
+
details: result.details,
|
|
3164
|
+
};
|
|
2996
3165
|
}
|
|
2997
3166
|
|
|
2998
3167
|
/**
|
|
@@ -3213,6 +3382,9 @@ export async function runOrchestrate(subCommand, argv, projectDir) {
|
|
|
3213
3382
|
case 'assess-coverage':
|
|
3214
3383
|
output(handleAssessCoverage(args));
|
|
3215
3384
|
break;
|
|
3385
|
+
case 'run-script-task':
|
|
3386
|
+
output(await handleRunScriptTask(projectDir, args));
|
|
3387
|
+
break;
|
|
3216
3388
|
default:
|
|
3217
3389
|
output(errorResult(`Unknown sub-command: ${subCommand}. Valid: next, advance, init, validate-handoff, status, deviation, exit, providers, detect-flow, backlog, qa-plan-score, qa-impl-score, scan, spawn-team, team-status, team-dissolve`, 'UNKNOWN_COMMAND'));
|
|
3218
3390
|
break;
|
|
@@ -105,7 +105,9 @@ export function detectDeviation(userMessage, _pipelineState) {
|
|
|
105
105
|
* @returns {{ impact: 'low'|'medium'|'high', affectedAgents: string[], recommendation: string, requiresConfirmation: boolean }}
|
|
106
106
|
*/
|
|
107
107
|
export function analyzeDeviationImpact(deviationType, pipelineState, details = {}) {
|
|
108
|
-
|
|
108
|
+
// sessionToPipelineState produces camelCase (currentAgent); accept the legacy
|
|
109
|
+
// snake_case shape too so direct callers keep working.
|
|
110
|
+
const currentAgent = pipelineState.currentAgent ?? pipelineState.current_agent;
|
|
109
111
|
const completedAgents = Object.entries(pipelineState.agents || {})
|
|
110
112
|
.filter(([, data]) => data.status === 'completed')
|
|
111
113
|
.map(([name]) => name);
|
|
@@ -268,7 +270,7 @@ export function applyDeviation(pipelineState, deviationType, details) {
|
|
|
268
270
|
changes.push(`Reset agent: ${agentName}`);
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
newState.
|
|
273
|
+
newState.currentAgent = targetAgent;
|
|
272
274
|
changes.push(`Rolled back to: ${targetAgent}`);
|
|
273
275
|
|
|
274
276
|
break;
|
|
@@ -302,7 +304,7 @@ export function applyDeviation(pipelineState, deviationType, details) {
|
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
// Reset to initial state
|
|
305
|
-
newState.
|
|
307
|
+
newState.currentAgent = '';
|
|
306
308
|
newState.backlog = [];
|
|
307
309
|
changes.push('Reset all agents to initial state');
|
|
308
310
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview /chati doctor — silent
|
|
2
|
+
* @fileoverview /chati doctor — silent self-diagnostic.
|
|
3
3
|
*
|
|
4
|
-
* Runs a battery of fast (< 200ms) filesystem + version checks
|
|
5
|
-
* /chati
|
|
6
|
-
* sees doctor output when everything is fine; they see a
|
|
7
|
-
* actionable message ONLY when something is off.
|
|
4
|
+
* Runs a battery of fast (< 200ms) filesystem + version checks when invoked
|
|
5
|
+
* via the /chati doctor subcommand. Pass-silent, fail-loud: the intent is that
|
|
6
|
+
* the user never sees doctor output when everything is fine; they see a
|
|
7
|
+
* specific, actionable message ONLY when something is off.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Seven checks, two severities:
|
|
10
10
|
*
|
|
11
|
-
* id
|
|
12
|
-
*
|
|
13
|
-
* license
|
|
14
|
-
* scaffold
|
|
15
|
-
* session
|
|
16
|
-
* hooks
|
|
17
|
-
* model
|
|
18
|
-
* node
|
|
11
|
+
* id severity description
|
|
12
|
+
* ------------ -------- -----------
|
|
13
|
+
* license BLOCK ~/.chati-dev/license.yaml status != VALID (fail-open if missing)
|
|
14
|
+
* scaffold BLOCK projectDir/.chati.dev/ directory exists
|
|
15
|
+
* session BLOCK .chati/session.yaml parses cleanly (or is absent)
|
|
16
|
+
* hooks WARN .claude/settings.json has expected hook registrations
|
|
17
|
+
* model WARN session.active_model is populated (hook fills on first fire)
|
|
18
|
+
* node BLOCK process version >= 22
|
|
19
|
+
* constitution WARN articleCount + i18n match the real "## Article" count
|
|
19
20
|
*
|
|
20
21
|
* BLOCK failures short-circuit boot with a fix instruction. WARN failures
|
|
21
22
|
* surface as non-blocking notices.
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
* verdict.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
import { existsSync, readFileSync } from 'fs';
|
|
29
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
29
30
|
import { join } from 'path';
|
|
30
31
|
import { readLicenseStatus } from '../license/wait.js';
|
|
31
32
|
|
|
@@ -242,8 +243,87 @@ export function checkNode({ versionString = process.version } = {}) {
|
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Constitution metadata drift. The articleCount in domains/constitution.yaml
|
|
248
|
+
* and the per-locale i18n strings must match the real number of "## Article"
|
|
249
|
+
* headings in constitution.md. Drift here is metadata-only (runtime is
|
|
250
|
+
* unaffected), so it is a WARN. Catching it keeps the registry, the localized
|
|
251
|
+
* UI and the document in sync.
|
|
252
|
+
* @param {{ projectDir: string }} opts
|
|
253
|
+
* @returns {CheckResult}
|
|
254
|
+
*/
|
|
255
|
+
export function checkConstitution({ projectDir }) {
|
|
256
|
+
const fwDir = ['.chati.dev', 'chati.dev']
|
|
257
|
+
.map(d => join(projectDir, d))
|
|
258
|
+
.find(d => existsSync(join(d, 'constitution.md')));
|
|
259
|
+
if (!fwDir) {
|
|
260
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: 'constitution.md not found — skipping drift check.' };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const md = readFileSync(join(fwDir, 'constitution.md'), 'utf-8');
|
|
264
|
+
const real = (md.match(/^#{2,3} Article /gm) || []).length;
|
|
265
|
+
const mismatches = [];
|
|
266
|
+
|
|
267
|
+
const domainPath = join(fwDir, 'domains', 'constitution.yaml');
|
|
268
|
+
if (existsSync(domainPath)) {
|
|
269
|
+
const m = readFileSync(domainPath, 'utf-8').match(/^articleCount:\s*(\d+)/m);
|
|
270
|
+
if (m && parseInt(m[1], 10) !== real) mismatches.push(`domains/constitution.yaml=${m[1]}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const locale of ['en', 'es', 'fr', 'pt']) {
|
|
274
|
+
const p = join(fwDir, 'i18n', `${locale}.yaml`);
|
|
275
|
+
if (!existsSync(p)) continue;
|
|
276
|
+
const m = readFileSync(p, 'utf-8').match(/constitution:\s*"[^"]*?(\d{1,3})\s*(?:Article|Articulo|Artigo)/i);
|
|
277
|
+
if (m && parseInt(m[1], 10) !== real) mismatches.push(`i18n/${locale}.yaml=${m[1]}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (mismatches.length === 0) {
|
|
281
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: `Constitution metadata in sync (${real} Articles).` };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
id: 'constitution',
|
|
285
|
+
severity: SEVERITY.WARN,
|
|
286
|
+
status: 'fail',
|
|
287
|
+
message: `Constitution article-count drift (real=${real}): ${mismatches.join('; ')}`,
|
|
288
|
+
fix: `Set those values to ${real} to match constitution.md.`,
|
|
289
|
+
};
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: `Cannot verify constitution metadata: ${err.message}` };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Surface durable pipeline-advance failure markers. advance-trigger.js drops a
|
|
297
|
+
* JSON marker in .chati/failures/ when it cannot spawn the router (a silent
|
|
298
|
+
* pipeline stall). Re-surface them here so the stall is visible on the next
|
|
299
|
+
* doctor run instead of only living as an unread file.
|
|
300
|
+
* @param {{ projectDir: string }} opts
|
|
301
|
+
* @returns {CheckResult}
|
|
302
|
+
*/
|
|
303
|
+
export function checkFailures({ projectDir }) {
|
|
304
|
+
const failuresDir = join(projectDir, '.chati', 'failures');
|
|
305
|
+
if (!existsSync(failuresDir)) {
|
|
306
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: 'No pipeline-advance failures recorded.' };
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const markers = readdirSync(failuresDir).filter(f => f.endsWith('.json'));
|
|
310
|
+
if (markers.length === 0) {
|
|
311
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: 'No pipeline-advance failures recorded.' };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
id: 'failures',
|
|
315
|
+
severity: SEVERITY.WARN,
|
|
316
|
+
status: 'fail',
|
|
317
|
+
message: `${markers.length} pipeline-advance failure marker(s) in .chati/failures/ — the pipeline may have stalled silently.`,
|
|
318
|
+
fix: 'Inspect .chati/failures/*.json, resolve the cause (often node not on PATH for the hook process), then delete the markers.',
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: `Cannot read .chati/failures/: ${err.message}` };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
245
325
|
// ---------------------------------------------------------------------------
|
|
246
|
-
// runDoctor — aggregate all
|
|
326
|
+
// runDoctor — aggregate all checks.
|
|
247
327
|
// ---------------------------------------------------------------------------
|
|
248
328
|
|
|
249
329
|
/**
|
|
@@ -263,6 +343,8 @@ export function runDoctor(opts) {
|
|
|
263
343
|
checkHooks({ projectDir: opts.projectDir }),
|
|
264
344
|
checkModel({ projectDir: opts.projectDir }),
|
|
265
345
|
checkNode(),
|
|
346
|
+
checkConstitution({ projectDir: opts.projectDir }),
|
|
347
|
+
checkFailures({ projectDir: opts.projectDir }),
|
|
266
348
|
];
|
|
267
349
|
const blockers = checks.filter(c => c.status === 'fail' && c.severity === SEVERITY.BLOCK);
|
|
268
350
|
const warnings = checks.filter(c => c.status === 'fail' && c.severity === SEVERITY.WARN);
|