chati-dev 4.4.0 → 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.
- package/README.md +29 -27
- package/bin/chati.js +251 -1
- package/framework/agents/plan/tasks.md +46 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +16 -17
- package/framework/context/governance.md +5 -5
- package/framework/context/root.md +5 -5
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/manifest.json +1351 -0
- package/framework/manifest.sig +1 -0
- package/framework/orchestrator/chati.md +48 -14
- package/node_modules/@chati/browser-capability/README.md +10 -0
- package/node_modules/@chati/browser-capability/package.json +17 -0
- package/node_modules/@chati/browser-capability/src/index.js +165 -0
- package/node_modules/@chati/core/package.json +13 -0
- package/node_modules/@chati/core/src/index.js +111 -0
- package/node_modules/@chati/knowledge-context/package.json +17 -0
- package/node_modules/@chati/knowledge-context/src/index.js +202 -0
- package/node_modules/@chati/planning/package.json +17 -0
- package/node_modules/@chati/planning/src/index.js +367 -0
- package/node_modules/@chati/provider-registry/package.json +16 -0
- package/node_modules/@chati/provider-registry/src/index.js +132 -0
- package/node_modules/@chati/rail/README.md +24 -0
- package/node_modules/@chati/rail/package.json +19 -0
- package/node_modules/@chati/rail/src/index.js +437 -0
- package/node_modules/@chati/release-lane/README.md +24 -0
- package/node_modules/@chati/release-lane/package.json +17 -0
- package/node_modules/@chati/release-lane/src/index.js +172 -0
- package/node_modules/@chati/review-council/package.json +17 -0
- package/node_modules/@chati/review-council/src/index.js +264 -0
- package/node_modules/@chati/tracking-clickup/README.md +55 -0
- package/node_modules/@chati/tracking-clickup/package.json +17 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
- package/package.json +25 -3
- package/src/config/claude-settings-generator.js +9 -6
- package/src/config/ide-configs.js +11 -0
- package/src/context/domain-loader.js +1 -1
- package/src/dashboard/data-reader.js +1 -1
- package/src/executors/runner.js +1 -1
- package/src/installer/core.js +33 -3
- package/src/installer/manifest.js +30 -4
- package/src/installer/scaffold-applier.js +1 -1
- package/src/installer/signing-public-key.pem +1 -1
- package/src/installer/templates.js +3 -3
- package/src/installer/validator.js +3 -2
- package/src/installer-v2/catalog-client.js +141 -0
- package/src/installer-v2/index.js +301 -0
- package/src/installer-v2/model-catalog-envelope.json +59 -0
- package/src/installer-v2/model-catalog.json +33 -0
- package/src/installer-v2/model-catalog.sig +1 -0
- package/src/installer-v2/wizard-installation.js +115 -0
- package/src/intelligence/registry-manager.js +3 -2
- package/src/license/client.js +1 -1
- package/src/memory/magic-docs.js +6 -1
- package/src/memory/session-digest.js +1 -1
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/browser-runtime.js +25 -0
- package/src/orchestrator/cli.js +112 -15
- package/src/orchestrator/clickup-projection.js +84 -0
- package/src/orchestrator/clickup-runtime.js +13 -0
- package/src/orchestrator/deviation-handler.js +5 -3
- package/src/orchestrator/knowledge-runtime.js +64 -0
- package/src/orchestrator/planning-runtime.js +127 -0
- package/src/orchestrator/rail-runtime.js +421 -0
- package/src/orchestrator/release-runtime.js +14 -0
- package/src/orchestrator/review-runtime.js +74 -0
- package/src/orchestrator/runtime-installation-v2.js +38 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/telemetry/config.js +1 -1
- package/src/terminal/adapters/grok-adapter.js +16 -0
- package/src/terminal/adapters/index.js +1 -0
- package/src/terminal/cli-registry.js +14 -0
- package/src/terminal/prompt-builder.js +2 -0
- package/src/terminal/run-agent.js +3 -0
- package/src/terminal/run-parallel.js +35 -24
- package/src/terminal/spawner.js +7 -1
- package/src/terminal/team-task-list.js +1 -1
- package/src/upgrade/backup.js +22 -44
- package/src/upgrade/checker.js +1 -1
- package/src/upgrade/migrator.js +52 -13
- package/src/utils/constitution-meta.js +12 -0
- package/src/utils/feature-flags.js +25 -1
- package/src/wizard/i18n.js +8 -2
- package/src/wizard/index.js +39 -4
- 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.
|
|
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/
|
|
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.
|
|
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
|
},
|
|
@@ -30,6 +30,7 @@ const HOOK_REGISTRY = [
|
|
|
30
30
|
// 2. PreToolUse catch-all below (backup — Claude sees deny reason, reports it)
|
|
31
31
|
{ name: 'prism-engine', event: 'UserPromptSubmit', matcher: '.*' },
|
|
32
32
|
{ name: 'model-governance', event: 'UserPromptSubmit', matcher: '.*' },
|
|
33
|
+
{ name: 'reasoning-escalator', event: 'UserPromptSubmit', matcher: '.*' },
|
|
33
34
|
|
|
34
35
|
// PreToolUse — fires before each tool call. Matcher targets specific tools.
|
|
35
36
|
// license-guard is first on catch-all ".*" — blocks ALL tool calls when
|
|
@@ -38,11 +39,13 @@ const HOOK_REGISTRY = [
|
|
|
38
39
|
// no tool call succeeds with an expired license.
|
|
39
40
|
{ name: 'license-guard', event: 'PreToolUse', matcher: '.*' },
|
|
40
41
|
{ name: 'read-protection', event: 'PreToolUse', matcher: 'Read' },
|
|
41
|
-
{ name: 'constitution-guard',event: 'PreToolUse', matcher: 'Bash|Write|Edit' },
|
|
42
|
-
{ name: '
|
|
43
|
-
{ name: '
|
|
44
|
-
{ name: '
|
|
45
|
-
{ name: '
|
|
42
|
+
{ name: 'constitution-guard',event: 'PreToolUse', matcher: 'Bash|Write|Edit|NotebookEdit' },
|
|
43
|
+
{ name: 'git-push-authority', event: 'PreToolUse', matcher: 'Bash' },
|
|
44
|
+
{ name: 'mode-governance', event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit' },
|
|
45
|
+
{ name: 'style-guard', event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit|Bash' },
|
|
46
|
+
{ name: 'undercover-guard', event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit|Bash' },
|
|
47
|
+
{ name: 'team-quality-gate', event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit' },
|
|
48
|
+
{ name: 'reasoning-escalator', event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit' },
|
|
46
49
|
|
|
47
50
|
// PostToolUse — fires after each tool call. Used to auto-advance the pipeline
|
|
48
51
|
// when an agent finishes writing its handoff. Hook inspects the written path
|
|
@@ -55,7 +58,7 @@ const HOOK_REGISTRY = [
|
|
|
55
58
|
{ name: 'post-dev', event: 'PostToolUse', matcher: 'Write' },
|
|
56
59
|
|
|
57
60
|
// PreCompact — fires before context compaction (observational).
|
|
58
|
-
{ name: 'session-digest', event: 'PreCompact', matcher: '' },
|
|
61
|
+
{ name: 'session-digest', event: 'PreCompact', matcher: '.*' },
|
|
59
62
|
];
|
|
60
63
|
|
|
61
64
|
/**
|
|
@@ -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,
|
package/src/executors/runner.js
CHANGED
|
@@ -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
|
/**
|
package/src/installer/core.js
CHANGED
|
@@ -224,7 +224,7 @@ export async function installFramework(config) {
|
|
|
224
224
|
*
|
|
225
225
|
* New files added to the source framework are picked up automatically.
|
|
226
226
|
*/
|
|
227
|
-
const FRAMEWORK_DIRS_TO_COPY = [
|
|
227
|
+
export const FRAMEWORK_DIRS_TO_COPY = [
|
|
228
228
|
'orchestrator',
|
|
229
229
|
'agents',
|
|
230
230
|
'templates',
|
|
@@ -824,7 +824,7 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
824
824
|
* If the file exists but is malformed: leave it alone, log a warning. We will
|
|
825
825
|
* not silently destroy user data.
|
|
826
826
|
*/
|
|
827
|
-
function writeClaudeSettingsWithMerge(settingsPath) {
|
|
827
|
+
export function writeClaudeSettingsWithMerge(settingsPath) {
|
|
828
828
|
createDir(dirname(settingsPath));
|
|
829
829
|
const fresh = JSON.parse(generateClaudeSettings());
|
|
830
830
|
|
|
@@ -878,7 +878,37 @@ export function mergeClaudeSettings(existing, fresh) {
|
|
|
878
878
|
merged.hooks = { ...existingHooks };
|
|
879
879
|
|
|
880
880
|
for (const [eventName, freshGroups] of Object.entries(freshHooks)) {
|
|
881
|
-
|
|
881
|
+
let existingGroups = existingHooks[eventName] || [];
|
|
882
|
+
|
|
883
|
+
// Widened-matcher upgrade: when a chati hook's matcher grows (e.g.
|
|
884
|
+
// Write|Edit -> Write|Edit|NotebookEdit), the old registration must be
|
|
885
|
+
// REPLACED, not kept alongside the new one - otherwise the hook fires
|
|
886
|
+
// twice for the overlapping tools on every upgraded install and
|
|
887
|
+
// settings.json grows without bound. Drop an existing registration of a
|
|
888
|
+
// command when the fresh settings register the SAME command on a matcher
|
|
889
|
+
// that is a strict superset of the existing one. A user-widened matcher
|
|
890
|
+
// (existing wider than fresh) is left untouched.
|
|
891
|
+
const toolsOf = (matcher) =>
|
|
892
|
+
matcher === '.*' || matcher === '' || matcher == null ? null : String(matcher).split('|');
|
|
893
|
+
const covers = (superTools, subTools) =>
|
|
894
|
+
superTools === null || (subTools !== null && subTools.every((t) => superTools.includes(t)));
|
|
895
|
+
const freshByCommand = new Map();
|
|
896
|
+
for (const g of freshGroups) {
|
|
897
|
+
for (const h of g.hooks || []) {
|
|
898
|
+
freshByCommand.set(h.command, { matcher: g.matcher, tools: toolsOf(g.matcher) });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
existingGroups = existingGroups
|
|
902
|
+
.map((group) => ({
|
|
903
|
+
...group,
|
|
904
|
+
hooks: (group.hooks || []).filter((h) => {
|
|
905
|
+
const freshReg = freshByCommand.get(h.command);
|
|
906
|
+
if (!freshReg || freshReg.matcher === group.matcher) return true;
|
|
907
|
+
return !covers(freshReg.tools, toolsOf(group.matcher));
|
|
908
|
+
}),
|
|
909
|
+
}))
|
|
910
|
+
.filter((group) => (group.hooks || []).length > 0);
|
|
911
|
+
|
|
882
912
|
// Append fresh groups; we don't dedup at the group level (different
|
|
883
913
|
// matchers are different groups). Within a single fresh group, the
|
|
884
914
|
// hook commands are unique to chati so duplication risk is minimal.
|
|
@@ -138,12 +138,38 @@ export function compareManifests(oldManifest, newManifest) {
|
|
|
138
138
|
* @param {string} signatureBase64 - Base64-encoded Ed25519 signature
|
|
139
139
|
* @returns {{ valid: boolean, reason: string }}
|
|
140
140
|
*/
|
|
141
|
-
export function verifyManifest(manifest, signatureBase64) {
|
|
142
|
-
|
|
141
|
+
export function verifyManifest(manifest, signatureBase64, publicKeyPem = null) {
|
|
142
|
+
// publicKeyPem is a test seam: production callers use the embedded key.
|
|
143
|
+
const key = publicKeyPem ? createPublicKey(publicKeyPem) : SIGNING_PUBLIC_KEY;
|
|
144
|
+
if (!key) return { valid: false, reason: 'no-public-key' };
|
|
143
145
|
|
|
144
|
-
const manifestJson =
|
|
146
|
+
const manifestJson = serializeManifest(manifest);
|
|
145
147
|
const signature = Buffer.from(signatureBase64, 'base64');
|
|
146
148
|
|
|
147
|
-
const valid = cryptoVerify(null, Buffer.from(manifestJson),
|
|
149
|
+
const valid = cryptoVerify(null, Buffer.from(manifestJson), key, signature);
|
|
148
150
|
return { valid, reason: valid ? 'ok' : 'signature-mismatch' };
|
|
149
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Canonical manifest serialization used by BOTH signing and verification.
|
|
155
|
+
*
|
|
156
|
+
* MUST NOT use a JSON.stringify replacer ARRAY: a replacer array filters keys
|
|
157
|
+
* at EVERY depth, so the per-file path keys inside `files` were dropped and
|
|
158
|
+
* the signature attested to an empty file map (and the written manifest.json
|
|
159
|
+
* itself contained "files": {}). Determinism comes from sorting the top-level
|
|
160
|
+
* keys and the files map explicitly.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} manifest
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function serializeManifest(manifest) {
|
|
166
|
+
const sorted = Object.fromEntries(
|
|
167
|
+
Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b))
|
|
168
|
+
);
|
|
169
|
+
if (sorted.files && typeof sorted.files === 'object') {
|
|
170
|
+
sorted.files = Object.fromEntries(
|
|
171
|
+
Object.entries(sorted.files).sort(([a], [b]) => a.localeCompare(b))
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return JSON.stringify(sorted, null, 2);
|
|
175
|
+
}
|
|
@@ -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,
|
|
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,
|
|
179
|
+
return yaml.dump(configData, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
@@ -1,10 +1,11 @@
|
|
|
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';
|
|
7
7
|
import { validateSchema, CONFIG_SCHEMA } from '../utils/schema-validator.js';
|
|
8
|
+
import { EXPECTED_ARTICLE_COUNT } from '../utils/constitution-meta.js';
|
|
8
9
|
|
|
9
10
|
function resolveFrameworkDir(targetDir) {
|
|
10
11
|
if (existsSync(join(targetDir, '.chati.dev'))) return '.chati.dev';
|
|
@@ -72,7 +73,7 @@ export async function validateInstallation(targetDir) {
|
|
|
72
73
|
if (existsSync(constitutionPath)) {
|
|
73
74
|
const content = readFileSync(constitutionPath, 'utf-8');
|
|
74
75
|
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
75
|
-
results.constitution.pass = articleCount
|
|
76
|
+
results.constitution.pass = articleCount === EXPECTED_ARTICLE_COUNT;
|
|
76
77
|
results.constitution.details.push({ articleCount });
|
|
77
78
|
}
|
|
78
79
|
results.total += 1;
|