chati-dev 4.4.1 → 4.5.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 (75) 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/manifest.json +13 -13
  9. package/framework/manifest.sig +1 -1
  10. package/framework/orchestrator/chati.md +43 -12
  11. package/node_modules/@chati/browser-capability/README.md +10 -0
  12. package/node_modules/@chati/browser-capability/package.json +17 -0
  13. package/node_modules/@chati/browser-capability/src/index.js +165 -0
  14. package/node_modules/@chati/core/package.json +13 -0
  15. package/node_modules/@chati/core/src/index.js +111 -0
  16. package/node_modules/@chati/knowledge-context/package.json +17 -0
  17. package/node_modules/@chati/knowledge-context/src/index.js +202 -0
  18. package/node_modules/@chati/planning/package.json +17 -0
  19. package/node_modules/@chati/planning/src/index.js +367 -0
  20. package/node_modules/@chati/provider-registry/package.json +16 -0
  21. package/node_modules/@chati/provider-registry/src/index.js +132 -0
  22. package/node_modules/@chati/rail/README.md +24 -0
  23. package/node_modules/@chati/rail/package.json +19 -0
  24. package/node_modules/@chati/rail/src/index.js +437 -0
  25. package/node_modules/@chati/release-lane/README.md +24 -0
  26. package/node_modules/@chati/release-lane/package.json +17 -0
  27. package/node_modules/@chati/release-lane/src/index.js +172 -0
  28. package/node_modules/@chati/review-council/package.json +17 -0
  29. package/node_modules/@chati/review-council/src/index.js +264 -0
  30. package/node_modules/@chati/tracking-clickup/README.md +55 -0
  31. package/node_modules/@chati/tracking-clickup/package.json +17 -0
  32. package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
  33. package/package.json +25 -3
  34. package/src/config/ide-configs.js +11 -0
  35. package/src/context/domain-loader.js +1 -1
  36. package/src/dashboard/data-reader.js +1 -1
  37. package/src/executors/runner.js +1 -1
  38. package/src/installer/scaffold-applier.js +1 -1
  39. package/src/installer/templates.js +3 -3
  40. package/src/installer/validator.js +1 -1
  41. package/src/installer-v2/catalog-client.js +141 -0
  42. package/src/installer-v2/index.js +301 -0
  43. package/src/installer-v2/model-catalog-envelope.json +59 -0
  44. package/src/installer-v2/model-catalog.json +33 -0
  45. package/src/installer-v2/model-catalog.sig +1 -0
  46. package/src/installer-v2/wizard-installation.js +115 -0
  47. package/src/intelligence/registry-manager.js +1 -1
  48. package/src/license/client.js +1 -1
  49. package/src/memory/session-digest.js +1 -1
  50. package/src/merger/yaml-merger.js +1 -1
  51. package/src/orchestrator/browser-runtime.js +25 -0
  52. package/src/orchestrator/cli.js +93 -8
  53. package/src/orchestrator/clickup-projection.js +84 -0
  54. package/src/orchestrator/clickup-runtime.js +13 -0
  55. package/src/orchestrator/knowledge-runtime.js +64 -0
  56. package/src/orchestrator/planning-runtime.js +127 -0
  57. package/src/orchestrator/rail-runtime.js +421 -0
  58. package/src/orchestrator/release-runtime.js +14 -0
  59. package/src/orchestrator/review-runtime.js +74 -0
  60. package/src/orchestrator/runtime-installation-v2.js +38 -0
  61. package/src/orchestrator/session-manager.js +1 -1
  62. package/src/telemetry/config.js +1 -1
  63. package/src/terminal/adapters/grok-adapter.js +16 -0
  64. package/src/terminal/adapters/index.js +1 -0
  65. package/src/terminal/cli-registry.js +14 -0
  66. package/src/terminal/prompt-builder.js +2 -0
  67. package/src/terminal/run-agent.js +3 -0
  68. package/src/terminal/run-parallel.js +21 -10
  69. package/src/terminal/spawner.js +7 -1
  70. package/src/terminal/team-task-list.js +1 -1
  71. package/src/upgrade/checker.js +1 -1
  72. package/src/upgrade/migrator.js +1 -1
  73. package/src/wizard/i18n.js +8 -2
  74. package/src/wizard/index.js +39 -4
  75. package/src/wizard/questions.js +59 -2
@@ -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.1",
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
  /**
@@ -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,4 +1,4 @@
1
- import yaml from 'js-yaml';
1
+ import * as yaml from 'js-yaml';
2
2
 
3
3
  /**
4
4
  * Generate session.yaml content
@@ -60,7 +60,7 @@ export function generateSessionYaml(config) {
60
60
  };
61
61
  }
62
62
 
63
- return yaml.dump(session, { lineWidth: -1, quotingType: '"', forceQuotes: false });
63
+ return yaml.dump(session, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
64
64
  }
65
65
 
66
66
  /**
@@ -176,7 +176,7 @@ export function generateConfigYaml(config) {
176
176
  }
177
177
  }
178
178
 
179
- return yaml.dump(configData, { lineWidth: -1, quotingType: '"', forceQuotes: false });
179
+ return yaml.dump(configData, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
180
180
  }
181
181
 
182
182
  /**
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
- import yaml from 'js-yaml';
3
+ import * as yaml from 'js-yaml';
4
4
  import { hashFile } from './file-hasher.js';
5
5
  import { loadManifest } from './manifest.js';
6
6
  import { ADAPTABLE_FILES } from '../config/framework-adapter.js';
@@ -0,0 +1,141 @@
1
+ import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { isIP } from 'node:net';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalSerialize, ContractError } from '@chati/core';
7
+ import { validateCapabilitySnapshot } from '@chati/provider-registry';
8
+
9
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
10
+
11
+ export const DEFAULT_CATALOG_URL = 'https://raw.githubusercontent.com/Chati-dev/Chati.dev/main/packages/chati-dev/src/installer-v2/model-catalog-envelope.json';
12
+ export const CATALOG_CACHE_PATH = '.chati/v2/cache/model-catalog.json';
13
+ export const BUNDLED_CATALOG_PATH = join(moduleDir, 'model-catalog.json');
14
+ export const BUNDLED_CATALOG_SIGNATURE_PATH = join(moduleDir, 'model-catalog.sig');
15
+ const PUBLIC_KEY_PATH = join(moduleDir, '..', 'installer', 'signing-public-key.pem');
16
+
17
+ function catalogError(code, message, details = {}) {
18
+ return new ContractError(code, message, details);
19
+ }
20
+
21
+ function parseSignedPayload(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ throw catalogError('INVALID_SIGNED_CATALOG', 'signed catalog payload must be an object');
24
+ }
25
+ if (!value.catalog || typeof value.signature !== 'string' || !value.signature.trim()) {
26
+ throw catalogError('INVALID_SIGNED_CATALOG', 'signed catalog payload requires catalog and signature');
27
+ }
28
+ return { catalog: value.catalog, signature: value.signature.trim() };
29
+ }
30
+
31
+ function readPublicKey(publicKeyPem) {
32
+ const source = publicKeyPem ?? readFileSync(PUBLIC_KEY_PATH, 'utf8');
33
+ return createPublicKey(source);
34
+ }
35
+
36
+ export function assertCatalogUrl(value) {
37
+ let url;
38
+ try { url = new URL(value); } catch { throw catalogError('CATALOG_URL_INVALID', 'catalog URL must be an absolute HTTPS URL'); }
39
+ if (url.protocol !== 'https:' || url.username || url.password) throw catalogError('CATALOG_URL_INVALID', 'catalog URL must use HTTPS without embedded credentials');
40
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
41
+ const ipKind = isIP(hostname);
42
+ const blockedIpv4 = ipKind === 4 && (/^10\./.test(hostname)
43
+ || /^127\./.test(hostname)
44
+ || /^169\.254\./.test(hostname)
45
+ || /^192\.168\./.test(hostname)
46
+ || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname)
47
+ || /^0\./.test(hostname));
48
+ const blockedIpv6 = ipKind === 6 && (hostname === '::1' || hostname === '::' || /^(fc|fd|fe8|fe9|fea|feb)/.test(hostname));
49
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || blockedIpv4 || blockedIpv6) {
50
+ throw catalogError('CATALOG_URL_FORBIDDEN', 'catalog URL may not target a loopback, link-local, or private host');
51
+ }
52
+ return url.toString();
53
+ }
54
+
55
+ export function verifySignedCapabilityCatalog(payload, { clock = () => new Date(), publicKeyPem } = {}) {
56
+ const { catalog, signature } = parseSignedPayload(payload);
57
+ const verified = cryptoVerify(
58
+ null,
59
+ Buffer.from(canonicalSerialize(catalog)),
60
+ readPublicKey(publicKeyPem),
61
+ Buffer.from(signature, 'base64'),
62
+ );
63
+ if (!verified) throw catalogError('CATALOG_SIGNATURE_INVALID', 'model catalog signature is invalid');
64
+ return validateCapabilitySnapshot(catalog, { clock });
65
+ }
66
+
67
+ function readSignedCatalogFile(path) {
68
+ return parseSignedPayload(JSON.parse(readFileSync(path, 'utf8')));
69
+ }
70
+
71
+ function writeCache(projectDir, payload) {
72
+ const path = join(projectDir, CATALOG_CACHE_PATH);
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ const temporary = `${path}.tmp-${process.pid}`;
75
+ writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
76
+ renameSync(temporary, path);
77
+ return CATALOG_CACHE_PATH;
78
+ }
79
+
80
+ function readBundledPayload() {
81
+ return {
82
+ catalog: JSON.parse(readFileSync(BUNDLED_CATALOG_PATH, 'utf8')),
83
+ signature: readFileSync(BUNDLED_CATALOG_SIGNATURE_PATH, 'utf8').trim(),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Resolves a signed CHATI model catalog. Order is remote, verified local cache,
89
+ * then the signed catalog bundled with the installed release. Vendor APIs and
90
+ * local binary inference are deliberately outside this trust boundary.
91
+ */
92
+ export async function resolveCapabilityCatalog({
93
+ projectDir,
94
+ catalogUrl = process.env.CHATI_MODEL_CATALOG_URL || DEFAULT_CATALOG_URL,
95
+ fetchImpl = globalThis.fetch,
96
+ clock = () => new Date(),
97
+ publicKeyPem,
98
+ timeoutMs = 5000,
99
+ } = {}) {
100
+ const failures = [];
101
+
102
+ if (catalogUrl && typeof fetchImpl === 'function') {
103
+ try {
104
+ const safeCatalogUrl = assertCatalogUrl(catalogUrl);
105
+ const response = await fetchImpl(safeCatalogUrl, {
106
+ headers: { accept: 'application/json' },
107
+ signal: AbortSignal.timeout(timeoutMs),
108
+ });
109
+ if (!response.ok) throw catalogError('CATALOG_HTTP_ERROR', `catalog endpoint returned HTTP ${response.status}`);
110
+ const payload = parseSignedPayload(await response.json());
111
+ const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
112
+ const cache_path = projectDir ? writeCache(projectDir, payload) : null;
113
+ return Object.freeze({ source: 'remote', source_url: safeCatalogUrl, cache_path, catalog });
114
+ } catch (error) {
115
+ failures.push({ source: 'remote', code: error.code || error.name || 'CATALOG_FETCH_FAILED' });
116
+ }
117
+ }
118
+
119
+ if (projectDir) {
120
+ const cachePath = join(projectDir, CATALOG_CACHE_PATH);
121
+ if (existsSync(cachePath)) {
122
+ try {
123
+ const payload = readSignedCatalogFile(cachePath);
124
+ const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
125
+ return Object.freeze({ source: 'cache', cache_path: CATALOG_CACHE_PATH, catalog });
126
+ } catch (error) {
127
+ failures.push({ source: 'cache', code: error.code || error.name || 'CATALOG_CACHE_INVALID' });
128
+ }
129
+ }
130
+ }
131
+
132
+ try {
133
+ const payload = readBundledPayload();
134
+ const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
135
+ return Object.freeze({ source: 'bundled', cache_path: null, catalog });
136
+ } catch (error) {
137
+ failures.push({ source: 'bundled', code: error.code || error.name || 'CATALOG_BUNDLE_INVALID' });
138
+ }
139
+
140
+ throw catalogError('CATALOG_UNAVAILABLE', 'no fresh, signed CHATI model catalog is available', { failures });
141
+ }