chati-dev 4.3.1 → 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 +12 -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 +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 +1 -1
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +19 -1
- 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 +3 -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 +2 -1
- 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 +174 -14
- 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-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
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,7 +41,7 @@ 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
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
42
45
|
import { join, resolve } from 'path';
|
|
43
46
|
import { execSync } from 'child_process';
|
|
44
47
|
import yaml from 'js-yaml';
|
|
@@ -931,6 +934,25 @@ async function _handleNextInner(projectDir) {
|
|
|
931
934
|
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000);
|
|
932
935
|
}
|
|
933
936
|
|
|
937
|
+
// Surface criteria (Article XVII): even in autonomous mode, a high-stakes
|
|
938
|
+
// transition still needs a human nod. Deploy is external and effectively
|
|
939
|
+
// irreversible, so it trips a critical criterion and is never auto-confirmed
|
|
940
|
+
// by the gates alone. Hold the autonomous spawn and hand control back to the
|
|
941
|
+
// user for that step.
|
|
942
|
+
let surfaceHold = null;
|
|
943
|
+
if (action === 'spawn_autonomous' && (agentDef?.phase === 'deploy' || nextAgent === 'devops')) {
|
|
944
|
+
const verdict = evaluateSurfaceCriteria(
|
|
945
|
+
{ type: 'deploy', description: `${nextAgent} (${agentDef?.phase || 'deploy'} phase)`, tags: ['external', 'destructive'] },
|
|
946
|
+
{},
|
|
947
|
+
);
|
|
948
|
+
const critical = verdict.triggers.filter((t) => t.severity === 'critical');
|
|
949
|
+
if (critical.length > 0) {
|
|
950
|
+
action = 'activate_interactive';
|
|
951
|
+
spawnCommand = null;
|
|
952
|
+
surfaceHold = { criteria: critical.map((t) => t.id), reason: critical.map((t) => t.reason).join('; ') };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
934
956
|
// Record agent activation event
|
|
935
957
|
recordEvent(projectDir, EventType.AGENT_ACTIVATED, nextAgent, { action, phase: agentDef?.phase });
|
|
936
958
|
|
|
@@ -952,6 +974,7 @@ async function _handleNextInner(projectDir) {
|
|
|
952
974
|
pipeline_progress: progress,
|
|
953
975
|
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
954
976
|
status_summary: `${lastAgent} completed. Next: ${nextAgent} (${action.replace('_', ' ')}, ${agentDef?.phase || 'unknown'} phase).`,
|
|
977
|
+
...(surfaceHold ? { surface: surfaceHold } : {}),
|
|
955
978
|
};
|
|
956
979
|
|
|
957
980
|
// Include team data when spawn_team action is selected (Article XXI)
|
|
@@ -989,6 +1012,7 @@ const SCAFFOLD_CONFIDENCE_THRESHOLD = 0.7;
|
|
|
989
1012
|
const PRESET_CONFIDENCE_THRESHOLDS = {
|
|
990
1013
|
'motion-premium': 0.7,
|
|
991
1014
|
'motion-premium-3d': 0.8,
|
|
1015
|
+
'saas-dashboard': 0.7,
|
|
992
1016
|
};
|
|
993
1017
|
|
|
994
1018
|
function thresholdFor(preset) {
|
|
@@ -1537,6 +1561,28 @@ async function handleAdvance(projectDir, args) {
|
|
|
1537
1561
|
},
|
|
1538
1562
|
};
|
|
1539
1563
|
}
|
|
1564
|
+
|
|
1565
|
+
// Spring-motion veto: linear easing on a transition and transition-property:
|
|
1566
|
+
// all are amateur-motion tells measured by visual-qa.js. Block the
|
|
1567
|
+
// autonomous advance on them, the same way layout_errors block, so the code
|
|
1568
|
+
// gate enforces premium motion and not only the QA-Visual agent prose.
|
|
1569
|
+
const motionErrors = (report.summary?.motion_errors || []).filter(e => e.severity === 'error');
|
|
1570
|
+
if (motionErrors.length > 0) {
|
|
1571
|
+
return {
|
|
1572
|
+
advanced: false,
|
|
1573
|
+
blocked: true,
|
|
1574
|
+
reason: 'motion_errors_detected',
|
|
1575
|
+
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).`,
|
|
1576
|
+
motion_error_count: motionErrors.length,
|
|
1577
|
+
motion_errors: motionErrors,
|
|
1578
|
+
fault_origin: 'CODE',
|
|
1579
|
+
action_required: {
|
|
1580
|
+
step1: 'Display the motion errors to the user with the offending selectors',
|
|
1581
|
+
step2: 'Route back to dev for rework (Fault Vector Protocol: CODE -> dev)',
|
|
1582
|
+
step3: 'Dev replaces linear/all easing with a cubic-bezier on transform/opacity, re-runs visual-qa.js, then re-runs advance qa-visual',
|
|
1583
|
+
},
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1540
1586
|
} catch {
|
|
1541
1587
|
// report.json unparseable — fall through to other checks, they will catch it
|
|
1542
1588
|
}
|
|
@@ -2941,7 +2987,7 @@ async function handleWaitForCapture(projectDir, args) {
|
|
|
2941
2987
|
* qa-visual-score: calculate visual QA score deterministically from report.json.
|
|
2942
2988
|
* Replaces QA-Visual's generative scoring formula.
|
|
2943
2989
|
*/
|
|
2944
|
-
function handleQaVisualScore(args) {
|
|
2990
|
+
export function handleQaVisualScore(args) {
|
|
2945
2991
|
const reportPath = args.report || '/tmp/visual-qa/report.json';
|
|
2946
2992
|
if (!existsSync(reportPath)) {
|
|
2947
2993
|
return errorResult(`report.json not found at ${reportPath}`, 'REPORT_NOT_FOUND');
|
|
@@ -2955,18 +3001,51 @@ function handleQaVisualScore(args) {
|
|
|
2955
3001
|
const s = report.summary || {};
|
|
2956
3002
|
const hasRefs = args['has-refs'] === 'true';
|
|
2957
3003
|
|
|
3004
|
+
// The animation stack the project actually uses. The motion-premium presets
|
|
3005
|
+
// ship GSAP + Lenis; the saas-dashboard app-shell preset ships framer-motion;
|
|
3006
|
+
// some projects use CSS only. The Lenis/GSAP presence checks apply ONLY to the
|
|
3007
|
+
// gsap-lenis stack, so a framer-motion project is not falsely blocked for
|
|
3008
|
+
// missing libraries it deliberately does not use. The agent resolves the stack
|
|
3009
|
+
// from architecture.md and passes --animation-library (default gsap-lenis for
|
|
3010
|
+
// backward compatibility with the motion presets).
|
|
3011
|
+
// Default to gsap-lenis for backward compatibility (the motion presets), but do
|
|
3012
|
+
// NOT assume it when the flag is absent AND the report shows no GSAP or Lenis
|
|
3013
|
+
// usage at all: a clean framer-motion or CSS project would otherwise be
|
|
3014
|
+
// phantom-blocked for libraries it never used. In that case fall back to a
|
|
3015
|
+
// non-GSAP stack (no GSAP/Lenis hard-block) and warn that --animation-library
|
|
3016
|
+
// should be passed explicitly. A project that passes the flag is unaffected.
|
|
3017
|
+
const animLibFlag = args['animation-library'];
|
|
3018
|
+
const animLibInferred = !animLibFlag;
|
|
3019
|
+
let animLib = String(animLibFlag || 'gsap-lenis').toLowerCase();
|
|
3020
|
+
if (animLibInferred && !s.gsap_loaded_all && !(s.total_gsap_animations > 0) && !s.lenis_active_all) {
|
|
3021
|
+
animLib = 'unknown';
|
|
3022
|
+
}
|
|
3023
|
+
const isGsapStack = animLib === 'gsap-lenis' || animLib === 'gsap' || animLib === 'lenis';
|
|
3024
|
+
|
|
3025
|
+
// Measured error signals from the report (objective, not judged).
|
|
3026
|
+
const motionErrorCount = (s.motion_errors || []).filter(e => e.severity === 'error').length;
|
|
3027
|
+
const layoutErrorCount = s.layout_error_count || 0;
|
|
3028
|
+
const reducedMotionViolations = (s.reduced_motion_violations || []).length;
|
|
3029
|
+
|
|
2958
3030
|
// Weights (adjust when references exist)
|
|
2959
3031
|
const w = hasRefs
|
|
2960
3032
|
? { animation: 0.30, scroll: 0.20, responsive: 0.15, hover: 0.10, brand: 0.10, fidelity: 0.15 }
|
|
2961
3033
|
: { animation: 0.35, scroll: 0.25, responsive: 0.20, hover: 0.10, brand: 0.10, fidelity: 0 };
|
|
2962
3034
|
|
|
2963
|
-
//
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3035
|
+
// Animation score is stack-aware. The gsap-lenis stack scores on Lenis + GSAP
|
|
3036
|
+
// presence and animation count; any other stack scores on the motion discipline
|
|
3037
|
+
// the report can measure: reduced-motion support and the absence of amateur
|
|
3038
|
+
// motion (the spring-motion veto), since premium motion can be built without
|
|
3039
|
+
// GSAP.
|
|
3040
|
+
const animScore = isGsapStack
|
|
3041
|
+
? Math.min(100, (s.lenis_active_all ? 50 : 0) + (s.gsap_loaded_all ? 30 : 0) + Math.min(20, (s.total_gsap_animations || 0) * 2))
|
|
3042
|
+
: Math.max(0, Math.min(100, 70 + (s.reduced_motion_support ? 20 : 0) + (motionErrorCount === 0 ? 10 : -30)));
|
|
3043
|
+
|
|
3044
|
+
const scrollScore = (s.total_scroll_triggers || 0) > 0 ? 80 + Math.min(20, s.total_scroll_triggers * 2) : (isGsapStack ? 40 : 80);
|
|
3045
|
+
const responsiveScore = layoutErrorCount > 0 ? 40 : 80; // layout errors are measured; the agent judges the rest
|
|
3046
|
+
const hoverScore = 80; // baseline; the agent judges interaction states from screenshots
|
|
2968
3047
|
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
|
|
3048
|
+
const fidelityScore = 70; // placeholder; the Visualizer comparison is the agent's judged signal
|
|
2970
3049
|
|
|
2971
3050
|
let rawScore = Math.round(
|
|
2972
3051
|
animScore * w.animation +
|
|
@@ -2981,18 +3060,96 @@ function handleQaVisualScore(args) {
|
|
|
2981
3060
|
const errors = [];
|
|
2982
3061
|
const warnings = [];
|
|
2983
3062
|
|
|
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');
|
|
3063
|
+
if (isGsapStack && !s.lenis_active_all) errors.push('Lenis not active on all pages');
|
|
3064
|
+
if (isGsapStack && !s.gsap_loaded_all) errors.push('GSAP not loaded on all pages');
|
|
3065
|
+
if (motionErrorCount > 0) { errors.push(`${motionErrorCount} amateur-motion error(s): linear easing or transition:all`); rawScore -= 15; }
|
|
3066
|
+
if (layoutErrorCount > 0) { errors.push(`${layoutErrorCount} objective layout error(s)`); rawScore -= 15; }
|
|
3067
|
+
if (reducedMotionViolations > 0) { errors.push(`${reducedMotionViolations} reduced-motion violation(s)`); rawScore -= 10; }
|
|
2986
3068
|
if (s.em_dashes_detected) { errors.push('Em-dashes found in visible text'); rawScore -= 10; }
|
|
2987
3069
|
if (s.console_errors_total > 0) { warnings.push(`${s.console_errors_total} console error(s)`); rawScore -= 5; }
|
|
3070
|
+
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.');
|
|
3071
|
+
|
|
3072
|
+
// Hard blocks: measured failures that cannot be APPROVED regardless of the
|
|
3073
|
+
// agent's judged 8-dimension rubric score. The agent applies visual_rubric on
|
|
3074
|
+
// top of this measured floor and SHALL NOT issue APPROVED when hardBlocked.
|
|
3075
|
+
const hardBlocked =
|
|
3076
|
+
(isGsapStack && (!s.lenis_active_all || !s.gsap_loaded_all)) ||
|
|
3077
|
+
motionErrorCount > 0 ||
|
|
3078
|
+
layoutErrorCount > 0 ||
|
|
3079
|
+
reducedMotionViolations > 0 ||
|
|
3080
|
+
!!s.em_dashes_detected;
|
|
2988
3081
|
|
|
2989
|
-
// Hard blocks
|
|
2990
|
-
const hardBlocked = !s.lenis_active_all || !s.gsap_loaded_all || s.em_dashes_detected;
|
|
2991
3082
|
const score = Math.max(0, Math.min(100, rawScore));
|
|
2992
3083
|
const threshold = 90;
|
|
2993
|
-
|
|
3084
|
+
// FLOOR_COMPUTED (not NEEDS_CORRECTION) is the label for a clean-but-below-floor
|
|
3085
|
+
// measured score: it signals that this is the CLI's measured floor, NOT the gate
|
|
3086
|
+
// verdict. The gate verdict is the QA-Visual agent's 8-dimension rubric score. A
|
|
3087
|
+
// clean framer-motion build can sit below the measured floor yet still be APPROVED
|
|
3088
|
+
// by the agent, so the CLI must not emit a correction-sounding label here.
|
|
3089
|
+
const verdict = hardBlocked ? 'BLOCKED' : score >= threshold ? 'APPROVED' : 'FLOOR_COMPUTED';
|
|
3090
|
+
|
|
3091
|
+
return {
|
|
3092
|
+
score, threshold, verdict, hardBlocked, animation_library: animLib, errors, warnings, weights: w,
|
|
3093
|
+
measured_signals: {
|
|
3094
|
+
motion_error_count: motionErrorCount,
|
|
3095
|
+
layout_error_count: layoutErrorCount,
|
|
3096
|
+
reduced_motion_violations: reducedMotionViolations,
|
|
3097
|
+
em_dashes: !!s.em_dashes_detected,
|
|
3098
|
+
},
|
|
3099
|
+
dimensions: { animScore, scrollScore, responsiveScore, hoverScore, brandScore, fidelityScore },
|
|
3100
|
+
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.',
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
2994
3103
|
|
|
2995
|
-
|
|
3104
|
+
/**
|
|
3105
|
+
* run-script-task: execute a task whose execution_kind is `script` through its
|
|
3106
|
+
* deterministic executor (no LLM). Gated by the execution_kinds feature flag;
|
|
3107
|
+
* when the flag is off it returns ran:false so the orchestrator uses the agent
|
|
3108
|
+
* path and the pipeline is unchanged. The orchestrator resolves the task fields
|
|
3109
|
+
* from tasks.yaml and passes them as flags (--ref, --id, --title).
|
|
3110
|
+
*/
|
|
3111
|
+
export async function handleRunScriptTask(projectDir, args) {
|
|
3112
|
+
if (!isFeatureEnabled(projectDir, 'execution_kinds')) {
|
|
3113
|
+
return { ran: false, reason: 'The execution_kinds feature is off. Run this task through the agent path.', fallback: 'agent' };
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const kind = String(args.kind || 'script');
|
|
3117
|
+
if (kind !== 'script') {
|
|
3118
|
+
return { ran: false, reason: `execution_kind "${kind}" is not auto-run by the script runner (only "script" is). Route through the normal path.`, kind };
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
const ref = args.ref || '';
|
|
3122
|
+
if (!ref) {
|
|
3123
|
+
return { ran: false, reason: 'Missing --ref (the execution_ref, for example "npm-script:test").' };
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const id = args.id || args.task || '';
|
|
3127
|
+
const title = args.title || '';
|
|
3128
|
+
const result = await runScriptTask(projectDir, { id, title, execution_ref: ref });
|
|
3129
|
+
|
|
3130
|
+
// Write a synthetic handoff so the pipeline reads it like an agent handoff.
|
|
3131
|
+
const timestamp = new Date().toISOString();
|
|
3132
|
+
const handoff = formatScriptHandoff({ id, title }, result, timestamp);
|
|
3133
|
+
let handoffPath = null;
|
|
3134
|
+
try {
|
|
3135
|
+
const dir = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs');
|
|
3136
|
+
mkdirSync(dir, { recursive: true });
|
|
3137
|
+
handoffPath = join(dir, `script-${(id || 'task').replace(/[^A-Za-z0-9._-]/g, '_')}.md`);
|
|
3138
|
+
writeFileSync(handoffPath, handoff, 'utf-8');
|
|
3139
|
+
} catch {
|
|
3140
|
+
// best-effort; the structured result is still returned for the orchestrator
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
return {
|
|
3144
|
+
ran: true,
|
|
3145
|
+
executor: result.executor,
|
|
3146
|
+
arg: result.arg,
|
|
3147
|
+
status: result.status,
|
|
3148
|
+
summary: result.summary,
|
|
3149
|
+
durationMs: result.durationMs,
|
|
3150
|
+
handoffPath,
|
|
3151
|
+
details: result.details,
|
|
3152
|
+
};
|
|
2996
3153
|
}
|
|
2997
3154
|
|
|
2998
3155
|
/**
|
|
@@ -3213,6 +3370,9 @@ export async function runOrchestrate(subCommand, argv, projectDir) {
|
|
|
3213
3370
|
case 'assess-coverage':
|
|
3214
3371
|
output(handleAssessCoverage(args));
|
|
3215
3372
|
break;
|
|
3373
|
+
case 'run-script-task':
|
|
3374
|
+
output(await handleRunScriptTask(projectDir, args));
|
|
3375
|
+
break;
|
|
3216
3376
|
default:
|
|
3217
3377
|
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
3378
|
break;
|
|
@@ -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);
|
|
@@ -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
|
}
|