chati-dev 4.4.1 → 4.5.2

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 (82) hide show
  1. package/README.md +29 -27
  2. package/bin/chati.js +235 -1
  3. package/framework/agents/plan/tasks.md +46 -1
  4. package/framework/config.yaml +2 -2
  5. package/framework/constitution.md +13 -16
  6. package/framework/context/governance.md +5 -5
  7. package/framework/context/root.md +5 -5
  8. package/framework/i18n/en.yaml +8 -2
  9. package/framework/i18n/es.yaml +8 -2
  10. package/framework/i18n/fr.yaml +8 -2
  11. package/framework/i18n/pt.yaml +8 -2
  12. package/framework/manifest.json +21 -21
  13. package/framework/manifest.sig +1 -1
  14. package/framework/orchestrator/chati.md +43 -12
  15. package/node_modules/@chati/browser-capability/README.md +10 -0
  16. package/node_modules/@chati/browser-capability/package.json +17 -0
  17. package/node_modules/@chati/browser-capability/src/index.js +165 -0
  18. package/node_modules/@chati/core/package.json +13 -0
  19. package/node_modules/@chati/core/src/index.js +111 -0
  20. package/node_modules/@chati/knowledge-context/package.json +17 -0
  21. package/node_modules/@chati/knowledge-context/src/index.js +202 -0
  22. package/node_modules/@chati/planning/package.json +17 -0
  23. package/node_modules/@chati/planning/src/index.js +367 -0
  24. package/node_modules/@chati/provider-registry/package.json +16 -0
  25. package/node_modules/@chati/provider-registry/src/index.js +148 -0
  26. package/node_modules/@chati/rail/README.md +24 -0
  27. package/node_modules/@chati/rail/package.json +19 -0
  28. package/node_modules/@chati/rail/src/index.js +437 -0
  29. package/node_modules/@chati/release-lane/README.md +24 -0
  30. package/node_modules/@chati/release-lane/package.json +17 -0
  31. package/node_modules/@chati/release-lane/src/index.js +172 -0
  32. package/node_modules/@chati/review-council/package.json +17 -0
  33. package/node_modules/@chati/review-council/src/index.js +264 -0
  34. package/node_modules/@chati/tracking-clickup/README.md +55 -0
  35. package/node_modules/@chati/tracking-clickup/package.json +17 -0
  36. package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
  37. package/package.json +25 -3
  38. package/src/config/ide-configs.js +11 -0
  39. package/src/context/domain-loader.js +1 -1
  40. package/src/dashboard/data-reader.js +1 -1
  41. package/src/executors/runner.js +1 -1
  42. package/src/installer/core.js +14 -13
  43. package/src/installer/provider-overlay.js +8 -15
  44. package/src/installer/scaffold-applier.js +1 -1
  45. package/src/installer/templates.js +12 -25
  46. package/src/installer/validator.js +5 -10
  47. package/src/installer-v2/catalog-client.js +165 -0
  48. package/src/installer-v2/index.js +304 -0
  49. package/src/installer-v2/model-catalog-envelope.json +278 -0
  50. package/src/installer-v2/model-catalog.json +130 -0
  51. package/src/installer-v2/model-catalog.sig +1 -0
  52. package/src/installer-v2/wizard-installation.js +116 -0
  53. package/src/intelligence/registry-manager.js +1 -1
  54. package/src/license/client.js +27 -1
  55. package/src/memory/session-digest.js +1 -1
  56. package/src/merger/yaml-merger.js +1 -1
  57. package/src/orchestrator/browser-runtime.js +25 -0
  58. package/src/orchestrator/cli.js +93 -8
  59. package/src/orchestrator/clickup-projection.js +84 -0
  60. package/src/orchestrator/clickup-runtime.js +13 -0
  61. package/src/orchestrator/knowledge-runtime.js +64 -0
  62. package/src/orchestrator/planning-runtime.js +127 -0
  63. package/src/orchestrator/rail-runtime.js +421 -0
  64. package/src/orchestrator/release-runtime.js +14 -0
  65. package/src/orchestrator/review-runtime.js +74 -0
  66. package/src/orchestrator/runtime-installation-v2.js +57 -0
  67. package/src/orchestrator/session-manager.js +1 -1
  68. package/src/telemetry/config.js +1 -1
  69. package/src/terminal/adapters/grok-adapter.js +16 -0
  70. package/src/terminal/adapters/index.js +1 -0
  71. package/src/terminal/cli-registry.js +20 -5
  72. package/src/terminal/prompt-builder.js +4 -2
  73. package/src/terminal/run-agent.js +3 -0
  74. package/src/terminal/run-parallel.js +21 -10
  75. package/src/terminal/spawner.js +7 -1
  76. package/src/terminal/team-task-list.js +1 -1
  77. package/src/upgrade/checker.js +1 -1
  78. package/src/upgrade/migrator.js +1 -1
  79. package/src/utils/config-parser.js +3 -8
  80. package/src/wizard/i18n.js +10 -4
  81. package/src/wizard/index.js +49 -17
  82. package/src/wizard/questions.js +50 -28
@@ -0,0 +1,293 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { ContractError, canonicalize, sha256 } from '@chati/core';
4
+
5
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
6
+ const DELIVERY_STATES = new Set(['pending', 'sent_unconfirmed', 'confirmed', 'retryable_failure', 'permanent_failure']);
7
+ const RECONCILIATION_STATES = new Set(['not_due', 'pending', 'reconciled', 'drift_detected', 'blocked']);
8
+ const OPERATIONS = new Set(['create', 'update', 'close']);
9
+ export const HOURLY_WATCHER_INTERVAL_MS = 60 * 60 * 1000;
10
+
11
+ function fail(code, message, details = {}) { throw new ContractError(code, message, details); }
12
+ function string(value, code, label) { if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`); }
13
+ function object(value, code, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`); }
14
+ function timestamp(clock) {
15
+ if (typeof clock !== 'function') fail('MISSING_CLOCK', 'a deterministic clock function is required');
16
+ const result = clock();
17
+ const value = result instanceof Date ? result.toISOString() : result;
18
+ if (typeof value !== 'string' || !RFC3339.test(value) || Number.isNaN(Date.parse(value))) fail('INVALID_CLOCK', 'clock must return a valid RFC3339 timestamp or Date');
19
+ return value;
20
+ }
21
+ function writeAtomic(path, data) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ const temporary = `${path}.${process.pid}.tmp`;
24
+ writeFileSync(temporary, `${JSON.stringify(canonicalize(data), null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
25
+ renameSync(temporary, path);
26
+ }
27
+ function readOutbox(path) {
28
+ if (!existsSync(path)) return { schema_version: 1, projections: [] };
29
+ let data;
30
+ try { data = JSON.parse(readFileSync(path, 'utf8')); } catch { fail('OUTBOX_CORRUPT', 'outbox JSON is malformed'); }
31
+ if (data?.schema_version !== 1 || !Array.isArray(data.projections)) fail('OUTBOX_CORRUPT', 'outbox schema is invalid');
32
+ return data;
33
+ }
34
+ function closureMutation(payload) {
35
+ const status = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
36
+ return ['closed', 'close', 'done', 'complete', 'completed'].includes(status)
37
+ || Object.hasOwn(payload, 'end_date') || Object.hasOwn(payload, 'endDate');
38
+ }
39
+
40
+ /**
41
+ * C-07 local projection authority. It intentionally has no ClickUp SDK,
42
+ * credentials, network client, timer or knowledge-store integration. A caller
43
+ * injects a transport only after an independently authorized boundary.
44
+ */
45
+ export class TrackingClickUp {
46
+ #outboxPath;
47
+ #evolutionDir;
48
+ #clock;
49
+ #transport;
50
+ #verifyCompletion;
51
+ #verifyReference;
52
+ #resolveReference;
53
+ #preflight;
54
+ #watcherIntervalMs;
55
+ #state;
56
+
57
+ constructor({ outbox_path, project_evolution_dir, clock, transport = null, verify_completion, verify_reference, resolve_reference, preflight = null, watcher_interval_ms = HOURLY_WATCHER_INTERVAL_MS } = {}) {
58
+ string(outbox_path, 'INVALID_OUTBOX_PATH', 'outbox_path');
59
+ string(project_evolution_dir, 'INVALID_EVOLUTION_DIR', 'project_evolution_dir');
60
+ if (transport !== null && (!transport || typeof transport.send !== 'function')) fail('INVALID_TRANSPORT', 'transport.send must be a function');
61
+ if (typeof verify_completion !== 'function') fail('MISSING_COMPLETION_VERIFIER', 'verify_completion must be an injected C-06 verifier');
62
+ if (typeof verify_reference !== 'function') fail('MISSING_REFERENCE_VERIFIER', 'verify_reference must verify immutable handoff and journal references');
63
+ if (typeof resolve_reference !== 'function') fail('MISSING_REFERENCE_RESOLVER', 'resolve_reference must produce verified handoff and journal views');
64
+ if (preflight !== null && (typeof preflight.validate_schema !== 'function' || typeof preflight.authorize_transport !== 'function')) fail('INVALID_PREFLIGHT', 'preflight requires validate_schema and authorize_transport functions');
65
+ if (watcher_interval_ms !== HOURLY_WATCHER_INTERVAL_MS) fail('INVALID_WATCHER_INTERVAL', 'watcher interval must be exactly hourly');
66
+ this.#outboxPath = outbox_path;
67
+ this.#evolutionDir = project_evolution_dir;
68
+ this.#clock = clock;
69
+ this.#transport = transport;
70
+ this.#verifyCompletion = verify_completion;
71
+ this.#verifyReference = verify_reference;
72
+ this.#resolveReference = resolve_reference;
73
+ this.#preflight = preflight;
74
+ this.#watcherIntervalMs = watcher_interval_ms;
75
+ this.#state = readOutbox(outbox_path);
76
+ // Project Evolution is a derivative, never the only copy of evidence.
77
+ // Rebuild it after an interrupted outbox/evolution write sequence.
78
+ for (const projectId of new Set(this.#state.projections.map((item) => item.project_id))) this.#evolve(projectId);
79
+ }
80
+
81
+ get projections() { return Object.freeze(this.#state.projections.map((item) => Object.freeze(canonicalize(item)))); }
82
+ get outboxPath() { return this.#outboxPath; }
83
+ get watcherConfig() { return Object.freeze({ interval_ms: this.#watcherIntervalMs, mode: 'external-one-shot-scheduler' }); }
84
+
85
+ #persist() { writeAtomic(this.#outboxPath, this.#state); }
86
+ #find(projectionId) {
87
+ string(projectionId, 'INVALID_PROJECTION_ID', 'projection_id');
88
+ const projection = this.#state.projections.find((item) => item.projection_id === projectionId);
89
+ if (!projection) fail('PROJECTION_NOT_FOUND', 'projection does not exist', { projection_id: projectionId });
90
+ return projection;
91
+ }
92
+ #evolve(projectId) {
93
+ const relevant = this.#state.projections.filter((item) => item.project_id === projectId);
94
+ const latestByTask = new Map();
95
+ for (const item of relevant) latestByTask.set(item.task_id, item);
96
+ const sourceProjections = relevant.map(({ project_evolution_ref, ...source }) => source);
97
+ const snapshot = canonicalize({
98
+ schema_version: 1,
99
+ project_id: projectId,
100
+ current_phase: [...latestByTask.values()].at(-1)?.handoff_view.phase ?? 'unknown',
101
+ tasks: [...latestByTask.values()].map((item) => ({ task_id: item.task_id, attempt_id: item.attempt_id, phase: item.handoff_view.phase, canonical_state: item.journal_view.task_state, blockers: item.journal_view.blockers, delivery_state: item.delivery_state, reconciliation_state: item.reconciliation_state })),
102
+ decisions: [...new Map(relevant.flatMap((item) => item.decisions.map((decision) => [decision.decision_ref, decision]))).values()],
103
+ verified_handoff_refs: [...new Set(relevant.map((item) => item.handoff_ref))],
104
+ verified_journal_refs: [...new Set(relevant.map((item) => item.journal_ref))],
105
+ receipt_refs: [...new Set(relevant.map((item) => item.receipt_ref).filter(Boolean))],
106
+ blockers: relevant.filter((item) => item.delivery_state === 'permanent_failure' || item.reconciliation_state === 'blocked').map((item) => item.projection_id),
107
+ projection_backlog: relevant.filter((item) => !['confirmed', 'permanent_failure'].includes(item.delivery_state)).map((item) => item.projection_id),
108
+ source_outbox_hash: sha256({ schema_version: 1, projections: sourceProjections }),
109
+ });
110
+ const digest = sha256(snapshot);
111
+ const path = join(this.#evolutionDir, projectId, `${digest}.json`);
112
+ writeAtomic(path, snapshot);
113
+ return Object.freeze({ project_evolution_ref: `project-evolution://${projectId}/${digest}`, path, digest });
114
+ }
115
+ #setEvolution(projection) {
116
+ const evolution = this.#evolve(projection.project_id);
117
+ projection.project_evolution_ref = evolution.project_evolution_ref;
118
+ return evolution;
119
+ }
120
+ #preflightTransport(projection) {
121
+ validateProjection(projection);
122
+ if (!this.#preflight) fail('TRANSPORT_PRECONDITION_UNMET', 'schema and authorization preflight are required before any transport call');
123
+ if (this.#preflight.validate_schema(Object.freeze(canonicalize(projection))) !== true) fail('SCHEMA_NOT_AUTHORIZED', 'remote schema preflight rejected projection');
124
+ if (this.#preflight.authorize_transport(Object.freeze(canonicalize(projection))) !== true) fail('TRANSPORT_NOT_AUTHORIZED', 'transport authorization or token gate rejected projection');
125
+ }
126
+
127
+ enqueue(input = {}) {
128
+ object(input, 'INVALID_PROJECTION', 'projection');
129
+ const { projection_id, idempotency_key, canonical_event_id, task_id, clickup_ref, operation, payload, project_id, handoff_ref, journal_ref } = input;
130
+ const attempt_id = input.attempt_id ?? `project:${project_id}:no-attempt`;
131
+ for (const [name, value] of Object.entries({ projection_id, idempotency_key, canonical_event_id, task_id, clickup_ref, project_id })) string(value, 'INVALID_PROJECTION', name);
132
+ string(attempt_id, 'INVALID_PROJECTION', 'attempt_id');
133
+ if (!OPERATIONS.has(operation)) fail('INVALID_PROJECTION_OPERATION', 'operation must be create, update, or close');
134
+ object(payload, 'INVALID_PROJECTION', 'payload');
135
+ string(handoff_ref, 'MISSING_SOURCE_REFERENCE', 'handoff_ref');
136
+ string(journal_ref, 'MISSING_SOURCE_REFERENCE', 'journal_ref');
137
+ if (this.#verifyReference(handoff_ref, 'handoff') !== true || this.#verifyReference(journal_ref, 'journal') !== true) fail('UNVERIFIED_SOURCE_REFERENCE', 'handoff and journal references must be verified before projection');
138
+ const handoff_view = this.#resolveReference(handoff_ref, 'handoff');
139
+ const journal_view = this.#resolveReference(journal_ref, 'journal');
140
+ object(handoff_view, 'INVALID_HANDOFF_VIEW', 'resolved handoff view');
141
+ object(journal_view, 'INVALID_JOURNAL_VIEW', 'resolved journal view');
142
+ string(handoff_view.phase, 'INVALID_HANDOFF_VIEW', 'resolved handoff view.phase');
143
+ string(journal_view.task_state, 'INVALID_JOURNAL_VIEW', 'resolved journal view.task_state');
144
+ if (!Array.isArray(journal_view.blockers) || !journal_view.blockers.every((item) => typeof item === 'string' && item)) fail('INVALID_JOURNAL_VIEW', 'resolved journal view.blockers must be non-empty strings');
145
+ if (input.decisions !== undefined && (!Array.isArray(input.decisions) || !input.decisions.every((item) => item && typeof item === 'object' && typeof item.decision_ref === 'string' && item.decision_ref && this.#verifyReference(item.decision_ref, 'decision') === true))) fail('INVALID_DECISIONS', 'decisions must be verified immutable project decision references');
146
+ const duplicateByKey = this.#state.projections.find((item) => item.idempotency_key === idempotency_key);
147
+ const duplicateById = this.#state.projections.find((item) => item.projection_id === projection_id);
148
+ const duplicate = duplicateByKey || duplicateById;
149
+ if (duplicate) {
150
+ const sameIdentity = duplicateByKey === duplicateById
151
+ && duplicate.task_id === task_id
152
+ && duplicate.attempt_id === attempt_id
153
+ && duplicate.operation === operation
154
+ && duplicate.payload_hash === sha256(payload);
155
+ if (sameIdentity) return Object.freeze(canonicalize(duplicate));
156
+ if (duplicateByKey) fail('DUPLICATE_IDEMPOTENCY_KEY', 'idempotency_key already exists with different material');
157
+ fail('DUPLICATE_PROJECTION_ID', 'projection_id already exists with different material');
158
+ }
159
+ if (operation === 'close' || closureMutation(payload)) {
160
+ object(input.completion, 'MISSING_COMPLETION_EVIDENCE', 'completion');
161
+ const verdict = this.#verifyCompletion(input.completion);
162
+ if (verdict !== true) fail('INVALID_COMPLETION_EVIDENCE', 'close requires a valid C-06 completion');
163
+ }
164
+ const projection = canonicalize({
165
+ schema_version: 1, projection_id, idempotency_key, canonical_event_id, task_id, attempt_id, clickup_ref, project_id, handoff_ref, journal_ref, handoff_view: canonicalize(handoff_view), journal_view: canonicalize(journal_view), decisions: canonicalize(input.decisions ?? []), operation,
166
+ payload_hash: sha256(payload), payload, delivery_state: 'pending', reconciliation_state: 'not_due', created_at: timestamp(this.#clock), attempts: 0,
167
+ });
168
+ this.#state.projections.push(projection);
169
+ this.#persist(); // Durable before any injected transport is called.
170
+ this.#setEvolution(projection);
171
+ this.#persist();
172
+ return Object.freeze(canonicalize(projection));
173
+ }
174
+
175
+ deliver({ projection_id } = {}) {
176
+ const projection = this.#find(projection_id);
177
+ if (!this.#transport) fail('TRANSPORT_NOT_CONFIGURED', 'no authorized projection transport is configured');
178
+ if (projection.delivery_state === 'confirmed') return Object.freeze(canonicalize(projection));
179
+ if (projection.delivery_state === 'permanent_failure') fail('PERMANENT_FAILURE', 'permanently failed projection cannot be retried');
180
+ if (projection.delivery_state === 'sent_unconfirmed') fail('UNCONFIRMED_DELIVERY_REQUIRES_LOOKUP', 'unconfirmed delivery must be resolved by idempotency lookup before resend');
181
+ this.#preflightTransport(projection);
182
+ projection.attempts += 1;
183
+ projection.last_attempt_at = timestamp(this.#clock);
184
+ try {
185
+ const response = this.#transport.send(Object.freeze(canonicalize(projection)));
186
+ object(response, 'INVALID_TRANSPORT_RECEIPT', 'transport response');
187
+ if (response.receipt_ref) {
188
+ string(response.receipt_ref, 'INVALID_TRANSPORT_RECEIPT', 'receipt_ref');
189
+ if (this.#verifyReference(response.receipt_ref, 'receipt') !== true) fail('UNVERIFIED_RECEIPT', 'delivery receipt must be an immutable verified reference');
190
+ projection.delivery_state = 'confirmed';
191
+ projection.receipt_ref = response.receipt_ref;
192
+ projection.reconciliation_state = 'pending';
193
+ } else projection.delivery_state = 'sent_unconfirmed';
194
+ } catch (error) {
195
+ projection.delivery_state = error?.permanent === true ? 'permanent_failure' : 'retryable_failure';
196
+ projection.failure_code = typeof error?.code === 'string' ? error.code : 'TRANSPORT_FAILURE';
197
+ }
198
+ this.#setEvolution(projection);
199
+ this.#persist();
200
+ return Object.freeze(canonicalize(projection));
201
+ }
202
+
203
+ reconcile({ projection_id } = {}) {
204
+ const projection = this.#find(projection_id);
205
+ if (!this.#transport || typeof this.#transport.reconcile !== 'function') fail('RECONCILIATION_NOT_CONFIGURED', 'no authorized reconciliation transport is configured');
206
+ if (projection.delivery_state !== 'confirmed') fail('RECEIPT_REQUIRED', 'reconciliation requires confirmed delivery receipt');
207
+ this.#preflightTransport(projection);
208
+ const result = this.#transport.reconcile(Object.freeze(canonicalize(projection)));
209
+ object(result, 'INVALID_RECONCILIATION_RESULT', 'reconciliation result');
210
+ if (!['reconciled', 'drift_detected', 'blocked'].includes(result.state)) fail('INVALID_RECONCILIATION_RESULT', 'reconciliation state is invalid');
211
+ projection.reconciliation_state = result.state;
212
+ if (result.evidence_ref !== undefined) { string(result.evidence_ref, 'INVALID_RECONCILIATION_RESULT', 'evidence_ref'); projection.reconciliation_evidence_ref = result.evidence_ref; }
213
+ this.#setEvolution(projection);
214
+ this.#persist();
215
+ return Object.freeze(canonicalize(projection));
216
+ }
217
+
218
+ resolveSentUnconfirmed({ projection_id } = {}) {
219
+ const projection = this.#find(projection_id);
220
+ if (projection.delivery_state !== 'sent_unconfirmed') fail('NOT_SENT_UNCONFIRMED', 'idempotency lookup only applies to sent_unconfirmed delivery');
221
+ if (!this.#transport || typeof this.#transport.lookup !== 'function') fail('IDEMPOTENCY_LOOKUP_NOT_CONFIGURED', 'sent_unconfirmed requires an idempotency lookup transport');
222
+ this.#preflightTransport(projection);
223
+ const result = this.#transport.lookup(Object.freeze(canonicalize(projection)));
224
+ object(result, 'INVALID_LOOKUP_RESULT', 'idempotency lookup result');
225
+ if (result.receipt_ref !== undefined) {
226
+ string(result.receipt_ref, 'INVALID_LOOKUP_RESULT', 'receipt_ref');
227
+ if (this.#verifyReference(result.receipt_ref, 'receipt') !== true) fail('UNVERIFIED_RECEIPT', 'lookup receipt must be an immutable verified reference');
228
+ projection.delivery_state = 'confirmed';
229
+ projection.receipt_ref = result.receipt_ref;
230
+ projection.reconciliation_state = 'pending';
231
+ } else if (result.state === 'not_found') projection.delivery_state = 'pending';
232
+ else if (result.state !== 'unknown') fail('INVALID_LOOKUP_RESULT', 'lookup requires receipt_ref, not_found, or unknown');
233
+ this.#setEvolution(projection);
234
+ this.#persist();
235
+ return Object.freeze(canonicalize(projection));
236
+ }
237
+
238
+ watchOnce() {
239
+ // Intentionally one-shot: a host scheduler may invoke this hourly. No timer is created.
240
+ const replayed = [];
241
+ for (const projection of this.#state.projections) {
242
+ if (projection.delivery_state === 'sent_unconfirmed') {
243
+ const resolved = this.resolveSentUnconfirmed({ projection_id: projection.projection_id });
244
+ if (resolved.delivery_state === 'pending') replayed.push(this.deliver({ projection_id: projection.projection_id }));
245
+ } else if (['pending', 'retryable_failure'].includes(projection.delivery_state)) replayed.push(this.deliver({ projection_id: projection.projection_id }));
246
+ }
247
+ return Object.freeze(replayed);
248
+ }
249
+
250
+ rederiveProjectEvolution({ project_id } = {}) {
251
+ string(project_id, 'INVALID_PROJECT_ID', 'project_id');
252
+ if (!this.#state.projections.some((item) => item.project_id === project_id)) fail('PROJECT_NOT_FOUND', 'project has no durable projections', { project_id });
253
+ return this.#evolve(project_id);
254
+ }
255
+
256
+ recordMetrics({ projection_id, actual_human_effort, actual_ai_processing, rework_cycles, rework_review_refs, completion } = {}) {
257
+ const projection = this.#find(projection_id);
258
+ object(actual_human_effort, 'INVALID_HUMAN_EFFORT', 'actual_human_effort');
259
+ object(actual_ai_processing, 'INVALID_AI_PROCESSING', 'actual_ai_processing');
260
+ if (!Number.isInteger(rework_cycles) || rework_cycles < 0) fail('INVALID_REWORK_CYCLES', 'rework_cycles must be a non-negative integer');
261
+ object(completion, 'MISSING_COMPLETION_EVIDENCE', 'completion');
262
+ if (this.#verifyCompletion(completion) !== true) fail('INVALID_COMPLETION_EVIDENCE', 'metrics require a valid C-06 completion');
263
+ if (!Array.isArray(rework_review_refs) || rework_review_refs.length !== rework_cycles || !rework_review_refs.every((ref) => typeof ref === 'string' && ref && this.#verifyReference(ref, 'review') === true)) {
264
+ fail('INVALID_REWORK_PROVENANCE', 'each rework cycle requires one verified C-05 review reference');
265
+ }
266
+ string(actual_human_effort.evidence_ref, 'INVALID_HUMAN_EFFORT', 'actual_human_effort.evidence_ref');
267
+ string(actual_ai_processing.evidence_ref, 'INVALID_AI_PROCESSING', 'actual_ai_processing.evidence_ref');
268
+ if (this.#verifyReference(actual_human_effort.evidence_ref, 'human-effort') !== true || this.#verifyReference(actual_ai_processing.evidence_ref, 'attempt-instrumentation') !== true) {
269
+ fail('UNVERIFIED_METRIC_PROVENANCE', 'metrics require verified explicit human and attempt instrumentation evidence');
270
+ }
271
+ // No wall-clock value exists here. Provenance is durable with the metric.
272
+ const metricMaterial = canonicalize({ actual_human_effort, actual_ai_processing, rework_cycles, rework_review_refs, completion_id: completion.completion_id });
273
+ if (projection.metrics) {
274
+ const { recorded_at: _recordedAt, ...existingMaterial } = projection.metrics;
275
+ if (sha256(existingMaterial) !== sha256(metricMaterial)) fail('METRICS_ALREADY_RECORDED', 'projection metrics are immutable');
276
+ return Object.freeze(canonicalize(projection));
277
+ }
278
+ projection.metrics = canonicalize({ ...metricMaterial, recorded_at: timestamp(this.#clock) });
279
+ this.#setEvolution(projection);
280
+ this.#persist();
281
+ return Object.freeze(canonicalize(projection));
282
+ }
283
+ }
284
+
285
+ export function validateProjection(projection) {
286
+ object(projection, 'INVALID_PROJECTION', 'projection');
287
+ for (const field of ['projection_id', 'idempotency_key', 'canonical_event_id', 'task_id', 'clickup_ref', 'project_id', 'payload_hash']) string(projection[field], 'INVALID_PROJECTION', field);
288
+ if (!OPERATIONS.has(projection.operation)) fail('INVALID_PROJECTION_OPERATION', 'operation is invalid');
289
+ if (!DELIVERY_STATES.has(projection.delivery_state)) fail('INVALID_DELIVERY_STATE', 'delivery_state is invalid');
290
+ if (!RECONCILIATION_STATES.has(projection.reconciliation_state)) fail('INVALID_RECONCILIATION_STATE', 'reconciliation_state is invalid');
291
+ if (projection.delivery_state === 'confirmed' && typeof projection.receipt_ref !== 'string') fail('MISSING_RECEIPT', 'confirmed projection requires receipt_ref');
292
+ return Object.freeze(canonicalize(projection));
293
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.4.1",
3
+ "version": "4.5.2",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,9 @@
28
28
  "sync": "node scripts/sync-framework.js",
29
29
  "scan": "node scripts/scan-stale.js",
30
30
  "install-to": "node scripts/install-to.js",
31
- "prepublishOnly": "node scripts/sync-framework.js && node scripts/scan-stale.js && node scripts/sign-manifest.js && REQUIRE_SIGNED_MANIFEST=1 node scripts/validate-package.js",
31
+ "prepublishOnly": "node scripts/sync-framework.js && node scripts/scan-stale.js && node scripts/sign-model-catalog.js && node scripts/sign-manifest.js && REQUIRE_SIGNED_MANIFEST=1 node scripts/validate-package.js",
32
+ "prepack": "node scripts/prepare-bundled-dependencies.js",
33
+ "postpack": "node scripts/prepare-bundled-dependencies.js --clean",
32
34
  "test": "node --test test/**/*.test.js",
33
35
  "lint": "eslint src/ bin/",
34
36
  "lint:fix": "eslint src/ bin/ --fix",
@@ -62,12 +64,32 @@
62
64
  "url": "https://github.com/Chati-dev/Chati.dev/issues"
63
65
  },
64
66
  "dependencies": {
67
+ "@chati/browser-capability": "0.1.0",
68
+ "@chati/core": "0.1.0",
69
+ "@chati/knowledge-context": "0.1.0",
70
+ "@chati/planning": "0.1.0",
71
+ "@chati/provider-registry": "0.1.0",
72
+ "@chati/rail": "0.1.0",
73
+ "@chati/release-lane": "0.1.0",
74
+ "@chati/review-council": "0.1.0",
75
+ "@chati/tracking-clickup": "0.1.0",
65
76
  "@clack/prompts": "^0.7.0",
66
77
  "chalk": "^5.3.0",
67
- "js-yaml": "^4.1.0",
78
+ "js-yaml": "^5.4.0",
68
79
  "ora": "^8.0.1",
69
80
  "semver": "^7.6.0"
70
81
  },
82
+ "bundleDependencies": [
83
+ "@chati/browser-capability",
84
+ "@chati/core",
85
+ "@chati/knowledge-context",
86
+ "@chati/planning",
87
+ "@chati/provider-registry",
88
+ "@chati/rail",
89
+ "@chati/release-lane",
90
+ "@chati/review-council",
91
+ "@chati/tracking-clickup"
92
+ ],
71
93
  "engines": {
72
94
  "node": ">=20.0.0"
73
95
  },
@@ -37,6 +37,16 @@ export const IDE_CONFIGS = {
37
37
  rulesPath: '.codex/rules/',
38
38
  overrideFile: 'AGENTS.override.md',
39
39
  },
40
+ 'grok-cli': {
41
+ name: 'Grok CLI',
42
+ description: 'xAI terminal agent',
43
+ group: 'cli',
44
+ recommended: false,
45
+ configPath: '.grok/commands/',
46
+ rulesFile: 'AGENTS.md',
47
+ mcpConfigFile: '.grok/mcp.json',
48
+ formatNotes: 'Native markdown command format',
49
+ },
40
50
  'vscode': {
41
51
  name: 'VS Code',
42
52
  description: 'Extensions: Continue, AI Chat, etc',
@@ -87,6 +97,7 @@ export const IDE_TO_PROVIDER = {
87
97
  'claude-code': 'claude',
88
98
  'gemini-cli': 'gemini',
89
99
  'codex-cli': 'codex',
100
+ 'grok-cli': 'grok',
90
101
  'vscode': null,
91
102
  'cursor': null,
92
103
  'windsurf': null,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { existsSync, readFileSync, readdirSync } from 'fs';
9
9
  import { join, basename } from 'path';
10
- import yaml from 'js-yaml';
10
+ import * as yaml from 'js-yaml';
11
11
 
12
12
  /**
13
13
  * Load a single domain YAML file.
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, existsSync, readdirSync } from 'fs';
2
2
  import { join } from 'path';
3
- import yaml from 'js-yaml';
3
+ import * as yaml from 'js-yaml';
4
4
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
5
5
 
6
6
  /**
@@ -17,7 +17,7 @@
17
17
  import { spawn } from 'child_process';
18
18
  import { existsSync, readFileSync } from 'fs';
19
19
  import { join, resolve, sep } from 'path';
20
- import yaml from 'js-yaml';
20
+ import * as yaml from 'js-yaml';
21
21
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
22
22
 
23
23
  /**
@@ -23,7 +23,7 @@ const FRAMEWORK_SOURCE = existsSync(BUNDLED_SOURCE) ? BUNDLED_SOURCE : MONOREPO_
23
23
  * Install Chati.dev framework into target directory
24
24
  */
25
25
  export async function installFramework(config) {
26
- const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, llmProvider, allProviders } = config;
26
+ const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, allProviders } = config;
27
27
  // Always derive version from package.json (single source of truth)
28
28
  let { version } = config;
29
29
  if (!version) {
@@ -55,7 +55,7 @@ export async function installFramework(config) {
55
55
  if (!existsSync(sessionPath)) {
56
56
  writeFileSync(
57
57
  sessionPath,
58
- generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
58
+ generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders }),
59
59
  'utf-8'
60
60
  );
61
61
  }
@@ -114,12 +114,13 @@ export async function installFramework(config) {
114
114
  createDir(join(memoriesBase, dir));
115
115
  }
116
116
 
117
- // Copy framework files from source (adapted for non-Claude providers)
118
- copyFrameworkFiles(frameworkDir, llmProvider || 'claude');
117
+ // Copy the canonical framework as a provider-neutral fallback. Every enabled
118
+ // CLI receives its own explicit adapted overlay below.
119
+ copyFrameworkFiles(frameworkDir, 'claude');
119
120
 
120
- // Generate provider overlays for secondary CLI providers
121
- if (allProviders && allProviders.length > 1) {
122
- generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, llmProvider || 'claude', allProviders);
121
+ // Generate symmetric provider overlays. No CLI is permanently primary.
122
+ if (allProviders && allProviders.length > 0) {
123
+ generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, allProviders);
123
124
  }
124
125
 
125
126
  // Bundle the CLI source into the framework dir so chati-router.js can run
@@ -159,19 +160,19 @@ export async function installFramework(config) {
159
160
  // Write config.yaml
160
161
  writeFileSync(
161
162
  join(frameworkDir, 'config.yaml'),
162
- generateConfigYaml({ version, projectType, language, selectedIDEs, llmProvider, allProviders }),
163
+ generateConfigYaml({ version, projectType, language, selectedIDEs, allProviders }),
163
164
  'utf-8'
164
165
  );
165
166
 
166
- // 3. Configure IDEs (secondary providers get overlay orchestrator path)
167
- const primary = llmProvider || 'claude';
167
+ // 3. Configure every CLI against its own overlay. The CLI that opens a
168
+ // session becomes that session's orchestrator.
168
169
  for (const ideKey of selectedIDEs) {
169
170
  const ideProvider = IDE_TO_PROVIDER[ideKey];
170
- const isSecondary = ideProvider && ideProvider !== primary && allProviders && allProviders.length > 1;
171
- const orchestratorPath = isSecondary
171
+ const hasProviderOverlay = ideProvider && allProviders?.includes(ideProvider);
172
+ const orchestratorPath = hasProviderOverlay
172
173
  ? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
173
174
  : '.chati.dev/orchestrator/chati.md';
174
- await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary, providerName: ideProvider });
175
+ await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary: false, providerName: ideProvider });
175
176
  }
176
177
 
177
178
  const hasClaude = selectedIDEs.includes('claude-code');
@@ -2,8 +2,8 @@
2
2
  * @fileoverview Provider Overlay Generator for multi-CLI continuity.
3
3
  *
4
4
  * When multiple CLI providers are selected (e.g. Claude Code + Gemini CLI),
5
- * the primary provider's adapted files go in chati.dev/ (main). For each
6
- * secondary provider, this module generates an overlay directory at
5
+ * the provider-neutral framework lives in chati.dev/ (main). For each
6
+ * enabled provider, this module generates an overlay directory at
7
7
  * chati.dev/.adapted/<provider>/ with the ADAPTABLE_FILES adapted for
8
8
  * that provider. Each CLI reads only its own files — zero runtime translation.
9
9
  *
@@ -15,31 +15,24 @@ import { join, dirname } from 'path';
15
15
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
16
16
 
17
17
  /**
18
- * Generate adapted framework overlays for secondary providers.
18
+ * Generate adapted framework overlays for all enabled providers.
19
19
  *
20
- * For each non-primary CLI provider, creates chati.dev/.adapted/<provider>/
20
+ * For each CLI provider, creates chati.dev/.adapted/<provider>/
21
21
  * with provider-adapted copies of all ADAPTABLE_FILES.
22
22
  *
23
- * Reads CANONICAL (Claude) content from frameworkSource, then adapts for
24
- * each secondary provider. This avoids double-adaptation issues when the
25
- * primary provider is not Claude.
23
+ * Reads canonical source content from frameworkSource, then adapts it for
24
+ * each enabled provider. Every CLI resolves its own overlay symmetrically.
26
25
  *
27
26
  * @param {string} targetDir - Project root directory
28
27
  * @param {string} frameworkSource - Source directory with canonical framework files
29
- * @param {string} primaryProvider - Primary provider name ('claude', 'gemini', 'codex')
30
28
  * @param {string[]} allProviders - All CLI provider names
31
29
  * @returns {{ generated: string[], skipped: string[] }}
32
30
  */
33
- export function generateProviderOverlays(targetDir, frameworkSource, primaryProvider, allProviders) {
31
+ export function generateProviderOverlays(targetDir, frameworkSource, allProviders) {
34
32
  const result = { generated: [], skipped: [] };
35
33
  const frameworkDir = join(targetDir, '.chati.dev');
36
34
 
37
35
  for (const provider of allProviders) {
38
- if (provider === primaryProvider) {
39
- result.skipped.push(provider);
40
- continue;
41
- }
42
-
43
36
  const overlayDir = join(frameworkDir, '.adapted', provider);
44
37
 
45
38
  for (const file of ADAPTABLE_FILES) {
@@ -64,7 +57,7 @@ export function generateProviderOverlays(targetDir, frameworkSource, primaryProv
64
57
  /**
65
58
  * Resolve the correct framework file path for a given provider.
66
59
  *
67
- * Returns the overlay path for secondary providers (if it exists),
60
+ * Returns the provider overlay path when it exists,
68
61
  * otherwise falls back to the main chati.dev/ path.
69
62
  *
70
63
  * @param {string} projectDir - Project root directory
@@ -24,7 +24,7 @@ import {
24
24
  } from 'fs';
25
25
  import { join, dirname } from 'path';
26
26
  import { fileURLToPath } from 'url';
27
- import yaml from 'js-yaml';
27
+ import * as yaml from 'js-yaml';
28
28
  import { hashContent } from './file-hasher.js';
29
29
 
30
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -1,10 +1,10 @@
1
- import yaml from 'js-yaml';
1
+ import * as yaml from 'js-yaml';
2
2
 
3
3
  /**
4
4
  * Generate session.yaml content
5
5
  */
6
6
  export function generateSessionYaml(config) {
7
- const { projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders } = config;
7
+ const { projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders } = config;
8
8
 
9
9
  // Schema MUST match session-manager.js DEFAULT_SESSION fields,
10
10
  // otherwise validateSession() rejects installer-generated sessions.
@@ -27,9 +27,7 @@ export function generateSessionYaml(config) {
27
27
  user_level_confidence: 0.0,
28
28
  ides: selectedIDEs,
29
29
  mcps: selectedMCPs,
30
- providers_enabled: allProviders && allProviders.length > 0 ? allProviders : [llmProvider || 'claude'],
31
- primary_provider: llmProvider || 'claude',
32
- active_provider: llmProvider || 'claude',
30
+ providers_enabled: allProviders && allProviders.length > 0 ? allProviders : ['claude'],
33
31
  completed_agents: [],
34
32
  agent_results: {},
35
33
  mode_transitions: [],
@@ -60,7 +58,7 @@ export function generateSessionYaml(config) {
60
58
  };
61
59
  }
62
60
 
63
- return yaml.dump(session, { lineWidth: -1, quotingType: '"', forceQuotes: false });
61
+ return yaml.dump(session, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
64
62
  }
65
63
 
66
64
  /**
@@ -102,10 +100,9 @@ export const PROVIDER_MODEL_MAPS = {
102
100
  * Generate config.yaml content
103
101
  */
104
102
  export function generateConfigYaml(config) {
105
- const { version, projectType, language, llmProvider, allProviders } = config;
103
+ const { version, projectType, language, allProviders } = config;
106
104
  const selectedIDEs = config.selectedIDEs || [];
107
- const provider = llmProvider || 'claude';
108
- const providers = allProviders && allProviders.length > 0 ? allProviders : [provider];
105
+ const providers = allProviders && allProviders.length > 0 ? allProviders : ['claude'];
109
106
 
110
107
  const configData = {
111
108
  version: version,
@@ -116,9 +113,10 @@ export function generateConfigYaml(config) {
116
113
  language: language,
117
114
  ides: selectedIDEs,
118
115
  providers: {
119
- claude: { enabled: providers.includes('claude') || provider === 'claude', primary: provider === 'claude' },
120
- gemini: { enabled: providers.includes('gemini') || selectedIDEs.includes('gemini-cli'), model_default: 'pro', primary: provider === 'gemini' },
121
- codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli'), model_default: 'codex', primary: provider === 'codex' },
116
+ claude: { enabled: providers.includes('claude') },
117
+ gemini: { enabled: providers.includes('gemini') || selectedIDEs.includes('gemini-cli') },
118
+ codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli') },
119
+ grok: { enabled: providers.includes('grok') || selectedIDEs.includes('grok-cli') },
122
120
  },
123
121
  };
124
122
 
@@ -148,7 +146,7 @@ export function generateConfigYaml(config) {
148
146
  frustration_detection: true,
149
147
  bash_security_checks: true,
150
148
  // Agent Teams (Article XXI) — default ON in v4.2.2.
151
- // Only effective when provider === 'claude' (gated in cli.js
149
+ // Only effective when the active session runs in Claude (gated in cli.js
152
150
  // isAgentTeamsEnabled). Gemini and Codex always fall back to sequential
153
151
  // pipeline silently. See plan: was previously false for "safe rollout"
154
152
  // but the v4.2.0 launch never actually shipped, so this is a fresh start.
@@ -165,18 +163,7 @@ export function generateConfigYaml(config) {
165
163
  anonymous_id: null,
166
164
  };
167
165
 
168
- // Add agent_overrides with optimal model map for the selected provider
169
- if (provider !== 'claude') {
170
- const modelMap = PROVIDER_MODEL_MAPS[provider];
171
- if (modelMap) {
172
- configData.agent_overrides = {};
173
- for (const [agent, model] of Object.entries(modelMap.agents)) {
174
- configData.agent_overrides[agent] = { provider, model };
175
- }
176
- }
177
- }
178
-
179
- return yaml.dump(configData, { lineWidth: -1, quotingType: '"', forceQuotes: false });
166
+ return yaml.dump(configData, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
180
167
  }
181
168
 
182
169
  /**