chati-dev 4.2.2 → 4.3.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.
Files changed (221) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +170 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +158 -22
  6. package/framework/agents/discover/brownfield-wu.md +26 -14
  7. package/framework/agents/discover/greenfield-wu.md +102 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +38 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +448 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +441 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +295 -94
  61. package/framework/rules/human-writing-style.md +47 -0
  62. package/framework/scaffold/motion-premium/README.md +99 -0
  63. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  64. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  65. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  66. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  67. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  68. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  69. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  70. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  71. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  72. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  73. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  77. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  78. package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
  79. package/framework/scaffold/motion-premium-3d/README.md +90 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  91. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  92. package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
  93. package/framework/schemas/session.schema.json +109 -21
  94. package/framework/scripts/reference-capture.js +430 -0
  95. package/framework/scripts/visual-qa.js +686 -0
  96. package/framework/tasks/orchestrator-handoff.md +1 -1
  97. package/framework/tasks/orchestrator-resume.md +1 -1
  98. package/framework/tasks/orchestrator-route.md +1 -1
  99. package/framework/tasks/orchestrator-status.md +3 -3
  100. package/framework/tasks/qa-planning-gate-define.md +1 -1
  101. package/framework/templates/brandbook-html-tmpl.md +2 -2
  102. package/framework/templates/brandbook-tmpl.yaml +1 -1
  103. package/framework/templates/component-spec-tmpl.yaml +1 -1
  104. package/framework/templates/design-token-tmpl.yaml +1 -1
  105. package/framework/templates/icon-system-tmpl.yaml +1 -1
  106. package/framework/templates/team-planning-tasks.yaml +6 -5
  107. package/framework/workflows/brownfield-discovery.yaml +2 -2
  108. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  109. package/framework/workflows/brownfield-service.yaml +14 -10
  110. package/framework/workflows/brownfield-ui.yaml +15 -11
  111. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  112. package/framework/workflows/quick-flow.yaml +3 -3
  113. package/framework/workflows/standard-flow.yaml +12 -9
  114. package/package.json +12 -6
  115. package/src/autonomy/autonomous-gate.js +1 -0
  116. package/src/autonomy/build-state.js +1 -2
  117. package/src/autonomy/progress-reporter.js +1 -1
  118. package/src/config/agent-customizer.js +11 -3
  119. package/src/config/claude-settings-generator.js +27 -7
  120. package/src/config/context-file-generator.js +41 -21
  121. package/src/config/framework-adapter.js +1 -0
  122. package/src/config/gemini-hooks-generator.js +19 -7
  123. package/src/config/mcp-configs.js +1 -0
  124. package/src/context/layers/l1-global.js +2 -1
  125. package/src/dashboard/data-reader.js +4 -3
  126. package/src/dashboard/layout.js +2 -1
  127. package/src/decision/analyzer.js +6 -30
  128. package/src/decision/engine.js +4 -28
  129. package/src/decision/registry-healer.js +3 -2
  130. package/src/decision/registry-updater.js +23 -14
  131. package/src/extensions/loader.js +2 -8
  132. package/src/gates/g1-planning-complete.js +2 -1
  133. package/src/gates/g2-qa-planning.js +2 -1
  134. package/src/gates/g3-implementation.js +2 -1
  135. package/src/gates/g4-qa-implementation.js +3 -2
  136. package/src/gates/g5-deploy-ready.js +2 -1
  137. package/src/health/engine.js +4 -3
  138. package/src/installer/core.js +431 -81
  139. package/src/installer/preflight.js +131 -0
  140. package/src/installer/provider-overlay.js +3 -3
  141. package/src/installer/scaffold-applier.js +358 -0
  142. package/src/installer/templates.js +46 -29
  143. package/src/installer/validator.js +17 -12
  144. package/src/intelligence/registry-manager.js +22 -29
  145. package/src/intelligence/timeline.js +11 -6
  146. package/src/license/commands.js +1 -1
  147. package/src/license/wait.js +102 -0
  148. package/src/memory/agent-memory.js +81 -0
  149. package/src/memory/dream.js +32 -1
  150. package/src/merger/replace-merger.js +28 -15
  151. package/src/orchestrator/agent-selector.js +2 -1
  152. package/src/orchestrator/cli.js +1880 -71
  153. package/src/orchestrator/doctor.js +270 -0
  154. package/src/orchestrator/handoff-engine.js +4 -3
  155. package/src/orchestrator/index.js +2 -0
  156. package/src/orchestrator/pipeline-manager.js +306 -15
  157. package/src/orchestrator/session-manager.js +339 -6
  158. package/src/tasks/handoff.js +3 -2
  159. package/src/telemetry/config.js +4 -3
  160. package/src/telemetry/schema.js +1 -0
  161. package/src/terminal/collector.js +3 -2
  162. package/src/terminal/index.js +1 -2
  163. package/src/terminal/isolation.js +52 -18
  164. package/src/terminal/prompt-builder.js +42 -25
  165. package/src/terminal/run-parallel.js +1 -1
  166. package/src/terminal/run-team.js +3 -3
  167. package/src/terminal/team-task-list.js +43 -4
  168. package/src/upgrade/backup.js +3 -2
  169. package/src/upgrade/checker.js +3 -2
  170. package/src/upgrade/migrator.js +65 -7
  171. package/src/upgrade/tracked-files-detector.js +86 -0
  172. package/src/upgrade/user-messages.js +94 -0
  173. package/src/utils/config-parser.js +2 -1
  174. package/src/utils/feature-flags.js +2 -1
  175. package/src/utils/flatten-entities.js +69 -0
  176. package/src/utils/framework-dir.js +16 -0
  177. package/src/utils/model-id.js +85 -0
  178. package/src/utils/provider-limits.js +84 -23
  179. package/src/utils/schema-validator.js +1 -1
  180. package/src/wizard/i18n.js +5 -4
  181. package/src/wizard/index.js +14 -0
  182. package/assets/logo - c/303/263pia.png +0 -0
  183. package/assets/logo.svg +0 -42
  184. package/assets/logo2.png +0 -0
  185. package/assets/social-preview.png +0 -0
  186. package/scripts/bundle-framework.js +0 -69
  187. package/scripts/changelog-generator.js +0 -222
  188. package/scripts/codebase-mapper.js +0 -728
  189. package/scripts/commit-message-generator.js +0 -167
  190. package/scripts/coverage-analyzer.js +0 -260
  191. package/scripts/dependency-analyzer.js +0 -280
  192. package/scripts/doctor/checks/agents.js +0 -77
  193. package/scripts/doctor/checks/constitution.js +0 -41
  194. package/scripts/doctor/checks/domain-alignment.js +0 -58
  195. package/scripts/doctor/checks/prism-layers.js +0 -84
  196. package/scripts/doctor/checks/registry.js +0 -55
  197. package/scripts/doctor/checks/schemas.js +0 -61
  198. package/scripts/doctor/fixes/reference-fix.js +0 -100
  199. package/scripts/doctor/fixes/registry-fix.js +0 -56
  200. package/scripts/doctor/index.js +0 -212
  201. package/scripts/framework-analyzer.js +0 -308
  202. package/scripts/generate-constitution-domain.js +0 -253
  203. package/scripts/generate-signing-key.js +0 -33
  204. package/scripts/health-check.js +0 -481
  205. package/scripts/ide-sync.js +0 -326
  206. package/scripts/performance-analyzer.js +0 -325
  207. package/scripts/plan-tracker.js +0 -278
  208. package/scripts/populate-entity-registry.js +0 -481
  209. package/scripts/pr-review.js +0 -317
  210. package/scripts/rollback-manager.js +0 -310
  211. package/scripts/semantic-lint.js +0 -328
  212. package/scripts/sign-manifest.js +0 -53
  213. package/scripts/stuck-detector.js +0 -343
  214. package/scripts/test-quality-assessment.js +0 -257
  215. package/scripts/validate-agents.js +0 -368
  216. package/scripts/validate-package.js +0 -505
  217. package/scripts/validate-tasks.js +0 -465
  218. package/src/autonomy/worktree-manager.js +0 -250
  219. package/src/intelligence/decision-engine.js +0 -256
  220. package/src/intelligence/document-sharder.js +0 -221
  221. package/src/intelligence/elicitation.js +0 -265
@@ -14,7 +14,7 @@ const SESSION_FILE = '.chati/session.yaml';
14
14
  /**
15
15
  * Default session template.
16
16
  */
17
- const CURRENT_SCHEMA_VERSION = '1.0';
17
+ const CURRENT_SCHEMA_VERSION = '1.2';
18
18
 
19
19
  const DEFAULT_SESSION = {
20
20
  schema_version: CURRENT_SCHEMA_VERSION,
@@ -35,6 +35,31 @@ const DEFAULT_SESSION = {
35
35
  agent_results: {},
36
36
  mode_transitions: [],
37
37
  deviations: [],
38
+ // Scaffold Premium (Article XXI / Fase 10) — auto-gate wiring.
39
+ // scaffold_candidates: presets eligible for this project (e.g. ['motion-premium'])
40
+ // written by greenfield-wu when stack answer signals a match.
41
+ // scaffold_signals: per-preset confidence/evidence map, written by
42
+ // ux-brand-architect when reference analysis detects premium patterns.
43
+ // scaffold_applied: presets already applied by the decision dispatcher
44
+ // (prevents double-apply when advance is re-run).
45
+ scaffold_candidates: [],
46
+ scaffold_signals: {},
47
+ scaffold_applied: [],
48
+ // Context Window Detection (Article XII v2) — model-aware brackets.
49
+ // active_model: normalized key from parseClaudeModelId() (e.g. 'opus[1m]'),
50
+ // written by /chati boot; drives context limit resolution.
51
+ // active_provider: redundant with providers_enabled[0] but flat for hooks.
52
+ // context_tokens_used: rolling cumulative estimate (char-based); written
53
+ // by prism-engine on every UserPromptSubmit. Replaces turn-count as the
54
+ // primary metric for bracket determination.
55
+ // context_window_tokens: cached limit resolved from active_model+provider
56
+ // so hooks avoid re-loading model-limits.json every fire.
57
+ // context_last_bracket: last computed bracket — telemetry + /chati status.
58
+ active_model: null,
59
+ active_provider: null,
60
+ context_tokens_used: 0,
61
+ context_window_tokens: null,
62
+ context_last_bracket: null,
38
63
  agents: {
39
64
  'greenfield-wu': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
40
65
  'brownfield-wu': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
@@ -47,10 +72,32 @@ const DEFAULT_SESSION = {
47
72
  'qa-planning': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
48
73
  'dev': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
49
74
  'qa-implementation': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
75
+ 'qa-visual': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
50
76
  'devops': { status: 'pending', score: 0, criteria_count: 0, completed_at: null },
51
77
  },
52
78
  backlog: [],
53
79
  last_handoff: '',
80
+ // Fault Vector Protocol (Article XXII) — append-only known-bad state log.
81
+ // Consumed by agents before iterating so the same defect is not re-introduced.
82
+ decision_trail: [],
83
+ // Agent Teams (Article XXI) — empty until spawn-team is called.
84
+ teams: [],
85
+ team_events: [],
86
+ // Execution Mode (Article XVII) — canonical. Article XVIII was deprecated
87
+ // 2026-04-18: execution_profile + profile_transitions were folded into
88
+ // execution_mode (set in DEFAULT_SESSION above as 'interactive'/'autonomous').
89
+ providers_enabled: ['claude'],
90
+ // BUILD → DEPLOY preview state (user approves before devops ships).
91
+ preview: {
92
+ status: 'inactive',
93
+ url: null,
94
+ port: null,
95
+ framework: null,
96
+ logs_captured: 0,
97
+ adjustment_count: 0,
98
+ },
99
+ // Model Governance (Article XVI) — per-agent model selection log.
100
+ model_selections: [],
54
101
  };
55
102
 
56
103
  /**
@@ -120,16 +167,71 @@ export function migrateSession(session) {
120
167
  return { migrated: false, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
121
168
  }
122
169
 
123
- // v0 (no schema_version) → v1.0
170
+ // v0 (no schema_version) → v1.0 → v1.1 → v1.2
124
171
  const fromVersion = session.schema_version || null;
125
172
  session.schema_version = CURRENT_SCHEMA_VERSION;
126
173
 
174
+ // Seed required scalar fields absent from pre-v4.3.0 sessions.
175
+ // validateSession() requires version/mode/project_type; older sessions may lack them.
176
+ // Without these guards, upgrade-path catastrophe (data-centralisation E2E 2026-04-28):
177
+ // v3.x install → v4.3.0 migrate adds schema_version: '1.2' but validator fails on missing fields.
178
+ if (!session.version) session.version = '1.0';
179
+ if (!session.mode) session.mode = 'discover';
180
+ if (!session.project_type) session.project_type = session.project?.type || 'greenfield';
181
+
127
182
  // Ensure fields added in v1.0 exist
128
183
  if (!session.completed_agents) session.completed_agents = [];
129
184
  if (!session.agent_results) session.agent_results = {};
130
185
  if (!session.deviations) session.deviations = [];
131
186
  if (!session.mode_transitions) session.mode_transitions = [];
132
187
 
188
+ // v1.1 — scaffold auto-gate fields (Fase 10).
189
+ if (!Array.isArray(session.scaffold_candidates)) session.scaffold_candidates = [];
190
+ if (!session.scaffold_signals || typeof session.scaffold_signals !== 'object') {
191
+ session.scaffold_signals = {};
192
+ }
193
+ if (!Array.isArray(session.scaffold_applied)) session.scaffold_applied = [];
194
+
195
+ // v1.2 — context window detection fields (Article XII v2).
196
+ // Defaults null/0 — /chati boot or prism-engine will populate on first hook.
197
+ if (!('active_model' in session)) session.active_model = null;
198
+ if (!('active_provider' in session)) session.active_provider = null;
199
+ if (typeof session.context_tokens_used !== 'number') session.context_tokens_used = 0;
200
+ if (!('context_window_tokens' in session)) session.context_window_tokens = null;
201
+ if (!('context_last_bracket' in session)) session.context_last_bracket = null;
202
+
203
+ // Schema parity catch-up (audit-fixes-v1 P4, additive). These fields live in
204
+ // session.schema.json but were not seeded into DEFAULT_SESSION historically.
205
+ // Runtime guards (`|| []`, `|| {}`) elsewhere kept things safe, but older
206
+ // sessions re-loaded here now get proper defaults without those guards.
207
+ if (!Array.isArray(session.decision_trail)) session.decision_trail = [];
208
+ if (!Array.isArray(session.teams)) session.teams = [];
209
+ if (!Array.isArray(session.team_events)) session.team_events = [];
210
+ if (!Array.isArray(session.providers_enabled)) session.providers_enabled = ['claude'];
211
+ if (!Array.isArray(session.model_selections)) session.model_selections = [];
212
+ if (!session.preview || typeof session.preview !== 'object') {
213
+ session.preview = {
214
+ status: 'inactive', url: null, port: null, framework: null,
215
+ logs_captured: 0, adjustment_count: 0,
216
+ };
217
+ }
218
+
219
+ // Article XVIII deprecation (2026-04-18): collapse execution_profile into
220
+ // execution_mode per the migration table. Runs LAST so it overrides any
221
+ // default seeded above. See constitution Article XVIII for rationale.
222
+ if (typeof session.execution_profile === 'string') {
223
+ const map = { explore: 'interactive', guided: 'interactive', autonomous: 'autonomous' };
224
+ const mapped = map[session.execution_profile.toLowerCase()];
225
+ if (mapped && session.execution_mode !== 'autonomous') {
226
+ // Only downgrade if current execution_mode is not already autonomous.
227
+ session.execution_mode = mapped;
228
+ }
229
+ delete session.execution_profile;
230
+ }
231
+ if (Array.isArray(session.profile_transitions)) {
232
+ delete session.profile_transitions;
233
+ }
234
+
133
235
  return { migrated: true, fromVersion, toVersion: CURRENT_SCHEMA_VERSION };
134
236
  }
135
237
 
@@ -275,13 +377,35 @@ export function recordModeTransition(projectDir, transition) {
275
377
  session.project.state = transition.to;
276
378
  }
277
379
 
278
- return updateSession(projectDir, session);
380
+ return writeSessionToDisk(projectDir, session);
381
+ }
382
+
383
+ /**
384
+ * Persist the full session object to disk. Avoids routing through updateSession's
385
+ * spread+deep-merge logic (which can corrupt list fields like `completed_agents`
386
+ * when the full session is passed as the updates arg).
387
+ * @param {string} projectDir
388
+ * @param {object} session
389
+ * @returns {{ saved: boolean, session?: object, error?: string }}
390
+ */
391
+ function writeSessionToDisk(projectDir, session) {
392
+ const sessionPath = join(projectDir, SESSION_FILE);
393
+ try {
394
+ const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
395
+ writeFileSync(sessionPath, yamlContent, 'utf-8');
396
+ return { saved: true, session };
397
+ } catch (err) {
398
+ return { saved: false, error: `Failed to write session: ${err.message}` };
399
+ }
279
400
  }
280
401
 
281
402
  /**
282
403
  * Record an agent completion in session.
283
404
  * @param {string} projectDir
284
- * @param {object} completion - { agent, status, score, outputs }
405
+ * @param {object} completion - { agent, status, score, outputs, handoffData }
406
+ * handoffData may include:
407
+ * - decision_trail_entries: array of new Decision Trail objects to append
408
+ * - resolved_decision_trail: array of DT entry IDs to mark resolved: true
285
409
  * @returns {{ saved: boolean }}
286
410
  */
287
411
  export function recordAgentCompletion(projectDir, completion) {
@@ -295,7 +419,7 @@ export function recordAgentCompletion(projectDir, completion) {
295
419
  }
296
420
 
297
421
  const session = loadResult.session;
298
- const { agent, status, score, outputs = [] } = completion;
422
+ const { agent, status, score, outputs = [], handoffData = {} } = completion;
299
423
 
300
424
  // Update agent data
301
425
  if (!session.agents[agent]) {
@@ -329,7 +453,216 @@ export function recordAgentCompletion(projectDir, completion) {
329
453
  // Update last handoff
330
454
  session.last_handoff = agent;
331
455
 
332
- return updateSession(projectDir, session);
456
+ // Merge Decision Trail entries from agent handoff document.
457
+ // Agents (e.g. qa-implementation) write Decision Trail entries to their handoff
458
+ // document instead of directly editing session.yaml (which is blocked by the
459
+ // mode-governance hook). The CLI reads them here and merges into session.yaml.
460
+ if (Array.isArray(handoffData.decision_trail_entries) && handoffData.decision_trail_entries.length > 0) {
461
+ session.decision_trail = [
462
+ ...(session.decision_trail || []),
463
+ ...handoffData.decision_trail_entries,
464
+ ];
465
+ }
466
+
467
+ // Mark resolved Decision Trail entries from agent handoff document.
468
+ if (Array.isArray(handoffData.resolved_decision_trail) && handoffData.resolved_decision_trail.length > 0) {
469
+ const trail = session.decision_trail || [];
470
+ for (const resolvedId of handoffData.resolved_decision_trail) {
471
+ const entry = trail.find(e => e.id === resolvedId);
472
+ if (entry) {
473
+ entry.resolved = true;
474
+ }
475
+ }
476
+ session.decision_trail = trail;
477
+ }
478
+
479
+ // Merge scaffold candidates/signals from handoff (Fase 10 auto-gate).
480
+ // - candidates: union into session.scaffold_candidates (dedup).
481
+ // - signals: overwrite session.scaffold_signals[key] (later signal wins)
482
+ // and stamp source_agent + updated_at.
483
+ if (Array.isArray(handoffData.scaffold_candidates) && handoffData.scaffold_candidates.length > 0) {
484
+ session.scaffold_candidates = session.scaffold_candidates || [];
485
+ for (const preset of handoffData.scaffold_candidates) {
486
+ if (!session.scaffold_candidates.includes(preset)) {
487
+ session.scaffold_candidates.push(preset);
488
+ }
489
+ }
490
+ }
491
+ if (handoffData.scaffold_signals && typeof handoffData.scaffold_signals === 'object') {
492
+ session.scaffold_signals = session.scaffold_signals || {};
493
+ for (const [key, signal] of Object.entries(handoffData.scaffold_signals)) {
494
+ session.scaffold_signals[key] = {
495
+ ...signal,
496
+ source_agent: agent,
497
+ updated_at: new Date().toISOString(),
498
+ };
499
+ }
500
+ }
501
+
502
+ return writeSessionToDisk(projectDir, session);
503
+ }
504
+
505
+ /**
506
+ * Parse Decision Trail entries from an agent's handoff markdown document.
507
+ *
508
+ * The agent writes Decision Trail entries under a "## Decision Trail" heading
509
+ * and resolved IDs under "## Resolved Decision Trail". This function extracts
510
+ * both so they can be passed to recordAgentCompletion() as handoffData.
511
+ *
512
+ * @param {string} handoffMarkdown - Full text of the handoff .md file
513
+ * @returns {{ decision_trail_entries: object[], resolved_decision_trail: string[] }}
514
+ */
515
+ export function parseHandoffDecisionTrail(handoffMarkdown) {
516
+ const decision_trail_entries = [];
517
+ const resolved_decision_trail = [];
518
+
519
+ if (!handoffMarkdown || typeof handoffMarkdown !== 'string') {
520
+ return { decision_trail_entries, resolved_decision_trail };
521
+ }
522
+
523
+ // Extract ## Decision Trail section (stop at next ## heading or end of input).
524
+ // Note: JS regex does not support \z (end-of-input anchor). Using $(?![\s\S])
525
+ // — an $ followed by negative lookahead "no more chars" — gives the same
526
+ // semantics. The previous \z was treated as literal 'z', silently truncating
527
+ // content at the first 'z' character in the Decision Trail block.
528
+ const dtMatch = handoffMarkdown.match(
529
+ /^## Decision Trail\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m,
530
+ );
531
+ if (dtMatch) {
532
+ const block = dtMatch[1];
533
+ // Parse YAML-like bullet blocks: lines starting with " id: DT-"
534
+ // Split on entries that start a new id: field
535
+ const entryBlocks = block.split(/(?=^\s*-?\s*id:\s*DT-)/m).filter(Boolean);
536
+ for (const entryBlock of entryBlocks) {
537
+ try {
538
+ // Build a minimal object from key: value lines
539
+ const entry = {};
540
+ for (const line of entryBlock.split('\n')) {
541
+ const kv = line.match(/^\s*-?\s*(\w+(?:_\w+)*):\s*(.+)$/);
542
+ if (kv) {
543
+ const [, key, value] = kv;
544
+ // Coerce resolved field to boolean
545
+ if (key === 'resolved') {
546
+ entry[key] = value.trim() === 'true';
547
+ } else {
548
+ entry[key] = value.trim().replace(/^["']|["']$/g, '');
549
+ }
550
+ }
551
+ }
552
+ if (entry.id && entry.id.startsWith('DT-')) {
553
+ decision_trail_entries.push(entry);
554
+ }
555
+ } catch {
556
+ // Malformed entry — skip silently
557
+ }
558
+ }
559
+ }
560
+
561
+ // Extract ## Resolved Decision Trail section (same \z → $(?![\s\S]) fix)
562
+ const resolvedMatch = handoffMarkdown.match(
563
+ /^## Resolved Decision Trail\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m,
564
+ );
565
+ if (resolvedMatch) {
566
+ const block = resolvedMatch[1];
567
+ // Each line may be "- DT-001" or just "DT-001"
568
+ for (const line of block.split('\n')) {
569
+ const idMatch = line.match(/\bDT-\d+\b/);
570
+ if (idMatch) {
571
+ resolved_decision_trail.push(idMatch[0]);
572
+ }
573
+ }
574
+ }
575
+
576
+ return { decision_trail_entries, resolved_decision_trail };
577
+ }
578
+
579
+ /**
580
+ * Parse scaffold-auto-gate metadata from an agent's handoff markdown.
581
+ *
582
+ * Two sections are recognized (both optional):
583
+ *
584
+ * ## Scaffold Candidates
585
+ * - <preset-id>
586
+ * - <preset-id>
587
+ *
588
+ * ## Scaffold Signals
589
+ * - preset: <preset-id>
590
+ * candidate: true
591
+ * confidence: 0.0–1.0
592
+ * evidence:
593
+ * - "<short match string>"
594
+ *
595
+ * Returns arrays/objects the CLI merges into session via recordAgentCompletion.
596
+ *
597
+ * @param {string} handoffMarkdown
598
+ * @returns {{ scaffold_candidates: string[], scaffold_signals: object }}
599
+ */
600
+ export function parseHandoffScaffoldSignals(handoffMarkdown) {
601
+ const scaffold_candidates = [];
602
+ const scaffold_signals = {};
603
+
604
+ if (!handoffMarkdown || typeof handoffMarkdown !== 'string') {
605
+ return { scaffold_candidates, scaffold_signals };
606
+ }
607
+
608
+ // ## Scaffold Candidates — bullet list of preset ids.
609
+ const candidatesMatch = handoffMarkdown.match(
610
+ /^## Scaffold Candidates\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m,
611
+ );
612
+ if (candidatesMatch) {
613
+ const block = candidatesMatch[1];
614
+ for (const line of block.split('\n')) {
615
+ const m = line.match(/^\s*[-*]\s+([a-z][a-z0-9-]*)\s*$/);
616
+ if (m && !scaffold_candidates.includes(m[1])) {
617
+ scaffold_candidates.push(m[1]);
618
+ }
619
+ }
620
+ }
621
+
622
+ // ## Scaffold Signals — one YAML-ish entry per preset.
623
+ const signalsMatch = handoffMarkdown.match(
624
+ /^## Scaffold Signals\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m,
625
+ );
626
+ if (signalsMatch) {
627
+ const block = signalsMatch[1];
628
+ // Split on lines that start a new "- preset:" entry.
629
+ const entries = block.split(/(?=^\s*-\s*preset:\s*)/m).filter(Boolean);
630
+ for (const entry of entries) {
631
+ const presetMatch = entry.match(/preset:\s*([a-z][a-z0-9-]*)/);
632
+ if (!presetMatch) continue;
633
+ const preset = presetMatch[1];
634
+ // Normalize for YAML keys (underscore, safer than dash for downstream).
635
+ const key = preset.replace(/-/g, '_');
636
+
637
+ const confMatch = entry.match(/confidence:\s*([\d.]+)/);
638
+ const confidence = confMatch ? Math.max(0, Math.min(1, parseFloat(confMatch[1]))) : 0;
639
+
640
+ const candidateMatch = entry.match(/candidate:\s*(true|false)/);
641
+ const isCandidate = candidateMatch ? candidateMatch[1] === 'true' : confidence > 0;
642
+ if (!isCandidate) continue;
643
+
644
+ const evidence = [];
645
+ const evidenceSection = entry.match(/evidence:\s*\n([\s\S]*?)(?=\n\s*[-*]\s*preset:|$)/);
646
+ if (evidenceSection) {
647
+ for (const line of evidenceSection[1].split('\n')) {
648
+ const em = line.match(/^\s*[-*]\s+"?(.+?)"?\s*$/);
649
+ if (em && em[1].trim()) evidence.push(em[1].trim());
650
+ }
651
+ }
652
+
653
+ scaffold_signals[key] = {
654
+ confidence,
655
+ evidence,
656
+ source_agent: null, // filled in by recordAgentCompletion
657
+ updated_at: null,
658
+ };
659
+
660
+ // If the signal is emitted, the preset is an implicit candidate too.
661
+ if (!scaffold_candidates.includes(preset)) scaffold_candidates.push(preset);
662
+ }
663
+ }
664
+
665
+ return { scaffold_candidates, scaffold_signals };
333
666
  }
334
667
 
335
668
  /**
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
10
  import { join } from 'path';
11
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
11
12
 
12
13
  /**
13
14
  * Build a handoff document from task execution results.
@@ -123,7 +124,7 @@ export function formatHandoff(handoff) {
123
124
  */
124
125
  export function saveHandoff(projectDir, handoff) {
125
126
  try {
126
- const handoffsDir = join(projectDir, 'chati.dev', 'artifacts', 'handoffs');
127
+ const handoffsDir = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs');
127
128
  if (!existsSync(handoffsDir)) {
128
129
  mkdirSync(handoffsDir, { recursive: true });
129
130
  }
@@ -147,7 +148,7 @@ export function saveHandoff(projectDir, handoff) {
147
148
  * @returns {{ loaded: boolean, handoff: object|null, error: string|null }}
148
149
  */
149
150
  export function loadHandoff(projectDir, agentName) {
150
- const filePath = join(projectDir, 'chati.dev', 'artifacts', 'handoffs', `${agentName}-handoff.md`);
151
+ const filePath = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs', `${agentName}-handoff.md`);
151
152
 
152
153
  if (!existsSync(filePath)) {
153
154
  return { loaded: false, handoff: null, error: `No handoff found from agent '${agentName}'.` };
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { readFileSync, writeFileSync, existsSync } from 'fs';
9
9
  import { join } from 'path';
10
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
10
11
  import { randomUUID } from 'crypto';
11
12
  import yaml from 'js-yaml';
12
13
 
@@ -21,7 +22,7 @@ import yaml from 'js-yaml';
21
22
  * @returns {{ enabled: boolean, anonymousId: string | null, endpoint: string }}
22
23
  */
23
24
  export function getTelemetryConfig(targetDir) {
24
- const configPath = join(targetDir, 'chati.dev', 'config.yaml');
25
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
25
26
 
26
27
  const defaults = {
27
28
  enabled: true,
@@ -64,7 +65,7 @@ export function isEnabled(targetDir) {
64
65
  * @param {boolean} enabled
65
66
  */
66
67
  export function setEnabled(targetDir, enabled) {
67
- const configPath = join(targetDir, 'chati.dev', 'config.yaml');
68
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
68
69
 
69
70
  if (!existsSync(configPath)) return;
70
71
 
@@ -99,7 +100,7 @@ export function getAnonymousId(targetDir) {
99
100
 
100
101
  // Generate and persist
101
102
  const id = randomUUID();
102
- const configPath = join(targetDir, 'chati.dev', 'config.yaml');
103
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
103
104
 
104
105
  if (existsSync(configPath)) {
105
106
  try {
@@ -19,6 +19,7 @@ export const TELEMETRY_EVENTS = [
19
19
  'session_started',
20
20
  'session_completed',
21
21
  'token_usage',
22
+ 'rate_limit_wait',
22
23
  ];
23
24
 
24
25
  // ---------------------------------------------------------------------------
@@ -59,9 +59,10 @@ export function collectResults(groupId, terminals) {
59
59
  * { agent, outputs[], decisions{}, blockers[], summary }
60
60
  *
61
61
  * @param {object[]} results - Per-terminal result objects (from collectResults)
62
+ * @param {string} [projectDir] - Project root directory (used for write scope resolution)
62
63
  * @returns {{ merged: boolean, outputs: string[], decisions: Record<string,string>, blockers: string[], summary: string }}
63
64
  */
64
- export function mergeHandoffs(results) {
65
+ export function mergeHandoffs(results, projectDir) {
65
66
  if (!Array.isArray(results) || results.length === 0) {
66
67
  return {
67
68
  merged: false,
@@ -82,7 +83,7 @@ export function mergeHandoffs(results) {
82
83
  if (Array.isArray(result.outputs)) {
83
84
  outputs.push(...result.outputs);
84
85
  } else {
85
- const scope = getWriteScope(result.agent);
86
+ const scope = getWriteScope(result.agent, projectDir);
86
87
  if (scope.length > 0) {
87
88
  outputs.push(...scope.map(s => `${result.agent}: ${s}`));
88
89
  }
@@ -22,7 +22,6 @@ export {
22
22
  } from './collector.js';
23
23
 
24
24
  export {
25
- WRITE_SCOPES,
26
25
  getWriteScope,
27
26
  validateWriteScopes,
28
27
  isPathAllowed,
@@ -32,7 +31,7 @@ export {
32
31
 
33
32
  export {
34
33
  buildAgentPrompt,
35
- AGENT_FILE_MAP,
34
+ getAgentFile,
36
35
  } from './prompt-builder.js';
37
36
 
38
37
  export {
@@ -1,4 +1,5 @@
1
1
  import { posix } from 'path';
2
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
2
3
 
3
4
  /**
4
5
  * @fileoverview Write scope isolation for multi-terminal execution.
@@ -12,38 +13,71 @@ import { posix } from 'path';
12
13
 
13
14
  /**
14
15
  * Default write scope mapping per agent.
15
- * Each key is an agent name; each value is an array of path prefixes
16
- * (relative to the project root) the agent is allowed to write.
16
+ * Paths are relative to the framework directory (e.g. 'artifacts/0-WU/').
17
+ * Non-framework paths (src/, test/, etc.) are stored as-is.
18
+ *
19
+ * Use getWriteScope(agent, projectDir) to get resolved paths.
17
20
  *
18
21
  * @type {Record<string, string[]>}
19
22
  */
20
- export const WRITE_SCOPES = {
21
- 'greenfield-wu': ['chati.dev/artifacts/0-WU/'],
22
- 'brownfield-wu': ['chati.dev/artifacts/0-WU/'],
23
- 'brief': ['chati.dev/artifacts/1-Brief/'],
24
- 'detail': ['chati.dev/artifacts/2-PRD/'],
25
- 'architect': ['chati.dev/artifacts/3-Architecture/'],
26
- 'ux': ['chati.dev/artifacts/4-UX/'],
27
- 'phases': ['chati.dev/artifacts/5-Phases/'],
28
- 'tasks': ['chati.dev/artifacts/6-Tasks/'],
29
- 'qa-planning': ['chati.dev/artifacts/7-QA-Planning/'],
30
- 'dev': ['src/', 'test/', 'package.json'],
31
- 'qa-implementation': ['test/', 'chati.dev/artifacts/8-QA-Impl/'],
32
- 'devops': ['.github/', 'Dockerfile', 'docker-compose.yml', 'chati.dev/artifacts/9-Deploy/'],
23
+ // IMPORTANT: artifacts/ lives at PROJECT ROOT (visible user output), NOT inside
24
+ // the framework dir. All artifact paths use the :non-framework: prefix so they
25
+ // resolve to <projectDir>/artifacts/, not <projectDir>/.chati.dev/artifacts/.
26
+ const WRITE_SCOPE_TEMPLATES = {
27
+ 'greenfield-wu': [':non-framework:artifacts/0-WU/', ':non-framework:artifacts/handoffs/greenfield-wu-handoff.md'],
28
+ 'brownfield-wu': [':non-framework:artifacts/0-WU/', ':non-framework:artifacts/handoffs/brownfield-wu-handoff.md'],
29
+ 'brief': [':non-framework:artifacts/1-Brief/', ':non-framework:artifacts/handoffs/brief-handoff.md'],
30
+ 'detail': [':non-framework:artifacts/2-PRD/', ':non-framework:artifacts/handoffs/detail-handoff.md'],
31
+ 'architect': [':non-framework:artifacts/3-Architecture/', ':non-framework:artifacts/handoffs/architect-handoff.md'],
32
+ 'ux': [':non-framework:artifacts/4-UX/', ':non-framework:artifacts/handoffs/ux-handoff.md'],
33
+ 'phases': [':non-framework:artifacts/5-Phases/', ':non-framework:artifacts/handoffs/phases-handoff.md'],
34
+ 'tasks': [':non-framework:artifacts/6-Tasks/', ':non-framework:artifacts/handoffs/tasks-handoff.md'],
35
+ 'qa-planning': [':non-framework:artifacts/7-QA-Planning/', ':non-framework:artifacts/handoffs/qa-planning-handoff.md'],
36
+ 'dev': [':non-framework:src/', ':non-framework:test/', ':non-framework:package.json', ':non-framework:artifacts/8-Implementation/', ':non-framework:artifacts/handoffs/dev-handoff.md'],
37
+ 'qa-implementation': [':non-framework:test/', ':non-framework:artifacts/9-QA-Implementation/', ':non-framework:artifacts/handoffs/qa-implementation-handoff.md'],
38
+ 'qa-visual': [':non-framework:artifacts/handoffs/qa-visual-handoff.md', ':non-framework:artifacts/9-QA-Implementation/qa-visual-report.md'],
39
+ 'devops': [':non-framework:.github/', ':non-framework:Dockerfile', ':non-framework:docker-compose.yml', ':non-framework:artifacts/10-Deploy/', ':non-framework:artifacts/handoffs/devops-handoff.md'],
33
40
  };
34
41
 
35
42
  /**
36
- * Return the write scope for a given agent.
43
+ * Resolved write scopes keyed as 'agent:fwDir' for memoization.
44
+ * @type {Map<string, string[]>}
45
+ */
46
+ const _writeScopeCache = new Map();
47
+
48
+
49
+ /**
50
+ * Return the write scope for a given agent, resolved against the project structure.
37
51
  *
38
52
  * @param {string} agent - Agent name (e.g. "architect")
53
+ * @param {string} [projectDir] - Project root directory. When provided, paths
54
+ * are resolved using resolveFrameworkDir() so .chati.dev/ vs chati.dev/ is correct.
39
55
  * @returns {string[]} Array of path prefixes the agent may write to.
40
56
  * Returns an empty array for unknown agents.
41
57
  */
42
- export function getWriteScope(agent) {
58
+ export function getWriteScope(agent, projectDir) {
43
59
  if (!agent || typeof agent !== 'string') {
44
60
  return [];
45
61
  }
46
- return WRITE_SCOPES[agent] || [];
62
+ const templates = WRITE_SCOPE_TEMPLATES[agent];
63
+ if (!templates) return [];
64
+
65
+ // Without projectDir, return legacy chati.dev/ paths (backward compat)
66
+ if (!projectDir) {
67
+ return templates.map(p => p.startsWith(':non-framework:') ? p.slice(':non-framework:'.length) : `chati.dev/${p}`);
68
+ }
69
+
70
+ const fwDir = resolveFrameworkDir(projectDir);
71
+ const cacheKey = `${agent}:${fwDir}`;
72
+ if (_writeScopeCache.has(cacheKey)) {
73
+ return _writeScopeCache.get(cacheKey);
74
+ }
75
+
76
+ const resolved = templates.map(p =>
77
+ p.startsWith(':non-framework:') ? p.slice(':non-framework:'.length) : `${fwDir}/${p}`
78
+ );
79
+ _writeScopeCache.set(cacheKey, resolved);
80
+ return resolved;
47
81
  }
48
82
 
49
83
  /**