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.
- package/README.md +29 -27
- package/bin/chati.js +235 -1
- package/framework/agents/plan/tasks.md +46 -1
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +13 -16
- package/framework/context/governance.md +5 -5
- package/framework/context/root.md +5 -5
- package/framework/manifest.json +13 -13
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati.md +43 -12
- 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/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/scaffold-applier.js +1 -1
- package/src/installer/templates.js +3 -3
- package/src/installer/validator.js +1 -1
- 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 +1 -1
- package/src/license/client.js +1 -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 +93 -8
- package/src/orchestrator/clickup-projection.js +84 -0
- package/src/orchestrator/clickup-runtime.js +13 -0
- 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 +21 -10
- package/src/terminal/spawner.js +7 -1
- package/src/terminal/team-task-list.js +1 -1
- package/src/upgrade/checker.js +1 -1
- package/src/upgrade/migrator.js +1 -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,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer v2 is deliberately a planner, not an installer.
|
|
3
|
+
*
|
|
4
|
+
* It creates one provider-neutral installation artifact, inspects a target for
|
|
5
|
+
* legacy v1 markers, and returns a migration/rollback plan. Artifact writes
|
|
6
|
+
* are explicit through installV2 or authorized migrateV2. It never invokes a
|
|
7
|
+
* provider CLI or reads credentials.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, copyFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
import { canonicalize, canonicalSerialize, sha256, ContractError } from '@chati/core';
|
|
12
|
+
import { validateInstallation } from '@chati/provider-registry';
|
|
13
|
+
|
|
14
|
+
export const INSTALLATION_SCHEMA_VERSION = 2;
|
|
15
|
+
export const INSTALLATION_ARTIFACT_PATH = '.chati/v2/installation.json';
|
|
16
|
+
|
|
17
|
+
const V1_MARKERS = Object.freeze([
|
|
18
|
+
{ provider_id: 'anthropic', harness_id: 'claude', path: '.claude/settings.json' },
|
|
19
|
+
{ provider_id: 'anthropic', harness_id: 'claude', path: 'CLAUDE.local.md' },
|
|
20
|
+
{ provider_id: 'openai', harness_id: 'codex', path: '.codex/config.toml' },
|
|
21
|
+
{ provider_id: 'xai', harness_id: 'grok', path: '.grok/config.json' },
|
|
22
|
+
{ provider_id: null, harness_id: null, path: '.chati/session.yaml' },
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const PROVIDER_SPECIFIC_ROOT_FILES = new Set([
|
|
26
|
+
'claude.md', 'claude.local.md', 'codex.md', 'codex.local.md', 'grok.md', 'grok.local.md',
|
|
27
|
+
'.claude.lock', '.codex.lock', '.grok.lock',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function assertObject(value, code, label) {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new ContractError(code, `${label} must be an object`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertString(value, code, label) {
|
|
35
|
+
if (typeof value !== 'string' || value.trim() === '') throw new ContractError(code, `${label} must be a non-empty string`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readClock(clock) {
|
|
39
|
+
if (typeof clock !== 'function') throw new ContractError('MISSING_CLOCK', 'a deterministic clock function is required');
|
|
40
|
+
const result = clock();
|
|
41
|
+
const date = result instanceof Date ? result : new Date(result);
|
|
42
|
+
if (Number.isNaN(date.getTime())) throw new ContractError('INVALID_CLOCK', 'clock must return a valid timestamp');
|
|
43
|
+
return date.toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureRelativePath(path) {
|
|
47
|
+
assertString(path, 'INVALID_MANAGED_PATH', 'managed path');
|
|
48
|
+
const normalized = path.replaceAll('\\', '/');
|
|
49
|
+
const root = normalized.split('/')[0];
|
|
50
|
+
if (normalized.startsWith('/') || normalized.split('/').includes('..')) {
|
|
51
|
+
throw new ContractError('INVALID_MANAGED_PATH', 'managed path must remain inside the project');
|
|
52
|
+
}
|
|
53
|
+
if (['.claude', '.codex', '.grok'].includes(root)) {
|
|
54
|
+
throw new ContractError('PROVIDER_SPECIFIC_MANAGED_PATH', 'installer v2 may not manage provider-specific configuration paths');
|
|
55
|
+
}
|
|
56
|
+
if (PROVIDER_SPECIFIC_ROOT_FILES.has(root.toLowerCase())) {
|
|
57
|
+
throw new ContractError('PROVIDER_SPECIFIC_MANAGED_PATH', 'installer v2 may not manage provider-specific root markers or locks');
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeBrainReadCapability(capability) {
|
|
63
|
+
if (capability === undefined || capability === null || capability === false) return { status: 'not-configured' };
|
|
64
|
+
assertObject(capability, 'INVALID_BRAIN_CAPABILITY', 'brain_read_capability');
|
|
65
|
+
if (capability.status === 'not-configured') return { status: 'not-configured' };
|
|
66
|
+
if (capability.mode !== 'read-only') throw new ContractError('BRAIN_WRITE_FORBIDDEN', 'installer v2 permits only a read-only Brain capability');
|
|
67
|
+
assertString(capability.policy_ref, 'INVALID_BRAIN_CAPABILITY', 'brain_read_capability.policy_ref');
|
|
68
|
+
return { status: 'configured', mode: 'read-only', policy_ref: capability.policy_ref };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function artifactCore(input, createdAt) {
|
|
72
|
+
assertObject(input, 'INVALID_INSTALLATION', 'installation input');
|
|
73
|
+
assertObject(input.installation, 'INVALID_INSTALLATION', 'installation');
|
|
74
|
+
assertObject(input.capability_snapshot, 'INVALID_CATALOG', 'capability_snapshot');
|
|
75
|
+
assertString(input.project_id, 'INVALID_INSTALLATION', 'project_id');
|
|
76
|
+
const installation = validateInstallation(input.installation, input.capability_snapshot, { clock: () => createdAt });
|
|
77
|
+
const brain_read_capability = normalizeBrainReadCapability(input.brain_read_capability);
|
|
78
|
+
const managed_paths = (input.managed_paths ?? [INSTALLATION_ARTIFACT_PATH]).map(ensureRelativePath);
|
|
79
|
+
if (!managed_paths.includes(INSTALLATION_ARTIFACT_PATH)) managed_paths.unshift(INSTALLATION_ARTIFACT_PATH);
|
|
80
|
+
if (new Set(managed_paths).size !== managed_paths.length) throw new ContractError('DUPLICATE_MANAGED_PATH', 'managed_paths must be unique');
|
|
81
|
+
const selectedModelKeys = new Set(installation.enabled_providers.flatMap((binding) =>
|
|
82
|
+
binding.allowed_models.map((model_id) => `${binding.provider_id}:${model_id}`),
|
|
83
|
+
));
|
|
84
|
+
const capability_snapshot = {
|
|
85
|
+
// This is a projection, not a catalog copy. Keep only attributes needed by
|
|
86
|
+
// the installation contract and only models explicitly selected by a binding.
|
|
87
|
+
snapshot_id: input.capability_snapshot.snapshot_id,
|
|
88
|
+
expires_at: input.capability_snapshot.expires_at,
|
|
89
|
+
models: input.capability_snapshot.models
|
|
90
|
+
.filter((model) => selectedModelKeys.has(`${model.provider_id}:${model.model_id}`))
|
|
91
|
+
.map(({ provider_id, model_id, actions, adjudication_priority, highest_reasoning_configuration }) => ({
|
|
92
|
+
provider_id,
|
|
93
|
+
model_id,
|
|
94
|
+
actions,
|
|
95
|
+
...(adjudication_priority !== undefined ? { adjudication_priority } : {}),
|
|
96
|
+
...(highest_reasoning_configuration !== undefined ? { highest_reasoning_configuration } : {}),
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
schema_version: INSTALLATION_SCHEMA_VERSION,
|
|
101
|
+
artifact_type: 'chati-installation',
|
|
102
|
+
project_id: input.project_id,
|
|
103
|
+
created_at: createdAt,
|
|
104
|
+
installation,
|
|
105
|
+
// An artifact exposes no capability information for providers not selected
|
|
106
|
+
// during installation. The full catalog remains an input, never an output.
|
|
107
|
+
capability_snapshot,
|
|
108
|
+
brain_read_capability,
|
|
109
|
+
managed_paths,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Creates a deterministic provider-neutral artifact. It never installs it. */
|
|
114
|
+
export function createInstallationArtifact(input, { clock = () => new Date(), artifactId } = {}) {
|
|
115
|
+
const core = artifactCore(input, readClock(clock));
|
|
116
|
+
const digest = sha256(core);
|
|
117
|
+
const installation_artifact_id = artifactId ?? `install-v2-${digest.slice(0, 16)}`;
|
|
118
|
+
assertString(installation_artifact_id, 'INVALID_INSTALLATION', 'installation_artifact_id');
|
|
119
|
+
return Object.freeze(canonicalize({ ...core, installation_artifact_id, digest }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validates an artifact without inspecting provider-specific configuration files. */
|
|
123
|
+
export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
124
|
+
const checks = [];
|
|
125
|
+
try {
|
|
126
|
+
assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
|
|
127
|
+
if (artifact.schema_version !== INSTALLATION_SCHEMA_VERSION || artifact.artifact_type !== 'chati-installation') {
|
|
128
|
+
throw new ContractError('UNSUPPORTED_INSTALLATION_ARTIFACT', 'expected a chati installation artifact v2');
|
|
129
|
+
}
|
|
130
|
+
const { digest, installation_artifact_id, ...core } = artifact;
|
|
131
|
+
assertString(installation_artifact_id, 'INVALID_ARTIFACT', 'installation_artifact_id');
|
|
132
|
+
if (sha256(core) !== digest) throw new ContractError('INSTALLATION_ARTIFACT_DRIFT', 'installation artifact digest does not match its content');
|
|
133
|
+
validateInstallation(artifact.installation, artifact.capability_snapshot, { clock: () => readClock(clock) });
|
|
134
|
+
normalizeBrainReadCapability(artifact.brain_read_capability);
|
|
135
|
+
if (!Array.isArray(artifact.managed_paths) || !artifact.managed_paths.every((path) => ensureRelativePath(path))) {
|
|
136
|
+
throw new ContractError('INVALID_MANAGED_PATH', 'managed_paths must be valid project-relative paths');
|
|
137
|
+
}
|
|
138
|
+
checks.push({ name: 'artifact', status: 'pass', message: 'artifact schema and digest are valid' });
|
|
139
|
+
checks.push({ name: 'selected-bindings', status: 'pass', message: 'only explicitly selected provider/harness bindings are present' });
|
|
140
|
+
checks.push({ name: 'brain-capability', status: 'pass', message: 'Brain capability is absent or read-only' });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
checks.push({ name: 'artifact', status: 'fail', code: error.code ?? 'INVALID_ARTIFACT', message: error.message });
|
|
143
|
+
}
|
|
144
|
+
return Object.freeze({ schema_version: 1, doctor: 'chati-installation-v2', passed: checks.every((check) => check.status === 'pass'), checks });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Read-only detector for known v1 footprints. Marker presence never enables a provider. */
|
|
148
|
+
export function detectV1Installation(projectDir, { exists = existsSync } = {}) {
|
|
149
|
+
assertString(projectDir, 'INVALID_PROJECT_DIR', 'projectDir');
|
|
150
|
+
if (typeof exists !== 'function') throw new ContractError('INVALID_DETECTOR', 'exists must be a function');
|
|
151
|
+
const markers = V1_MARKERS.filter((marker) => exists(join(projectDir, marker.path))).map((marker) => ({ ...marker }));
|
|
152
|
+
const bindings = markers
|
|
153
|
+
.filter((marker) => marker.provider_id)
|
|
154
|
+
.map(({ provider_id, harness_id }) => ({ provider_id, harness_id }))
|
|
155
|
+
.filter((binding, index, all) => all.findIndex((entry) => entry.provider_id === binding.provider_id && entry.harness_id === binding.harness_id) === index);
|
|
156
|
+
return Object.freeze({ detector_version: 1, detected: markers.length > 0, markers, detected_bindings: bindings });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Builds a backup plan only. The caller must separately authorize and execute any write. */
|
|
160
|
+
export function createBackupPlan(projectDir, detector, artifact) {
|
|
161
|
+
assertString(projectDir, 'INVALID_PROJECT_DIR', 'projectDir');
|
|
162
|
+
assertObject(detector, 'INVALID_DETECTOR', 'detector');
|
|
163
|
+
assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
|
|
164
|
+
const backup_id = `backup-v2-${artifact.digest.slice(0, 16)}`;
|
|
165
|
+
const entries = detector.markers.map((marker) => ({ source_path: marker.path, backup_path: `.chati/v2/backups/${backup_id}/${marker.path}` }));
|
|
166
|
+
return Object.freeze(canonicalize({ schema_version: 1, backup_id, project_dir: resolve(projectDir), entries, write_required: false }));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Produces the inverse of a future authorized migration, without performing either operation. */
|
|
170
|
+
export function createRollbackPlan(artifact, backupPlan) {
|
|
171
|
+
assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
|
|
172
|
+
assertObject(backupPlan, 'INVALID_BACKUP_PLAN', 'backupPlan');
|
|
173
|
+
return Object.freeze(canonicalize({
|
|
174
|
+
schema_version: 1,
|
|
175
|
+
rollback_id: `rollback-v2-${artifact.digest.slice(0, 16)}`,
|
|
176
|
+
installation_artifact_id: artifact.installation_artifact_id,
|
|
177
|
+
backup_id: backupPlan.backup_id,
|
|
178
|
+
restore_entries: backupPlan.entries,
|
|
179
|
+
remove_created_paths: artifact.managed_paths,
|
|
180
|
+
requires_explicit_authorization: true,
|
|
181
|
+
write_required: false,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read-only migration preview. It does not write a v2 artifact or back up v1.
|
|
187
|
+
* Re-running with the same inputs returns the same plans and does not duplicate
|
|
188
|
+
* provider bindings or managed paths.
|
|
189
|
+
*/
|
|
190
|
+
export function dryRunV2(input, { projectDir, clock = () => new Date(), exists = existsSync } = {}) {
|
|
191
|
+
assertString(projectDir, 'INVALID_PROJECT_DIR', 'projectDir');
|
|
192
|
+
const artifact = createInstallationArtifact(input, { clock });
|
|
193
|
+
const doctor = doctorV2(artifact, { clock });
|
|
194
|
+
const detector = detectV1Installation(projectDir, { exists });
|
|
195
|
+
const backup_plan = createBackupPlan(projectDir, detector, artifact);
|
|
196
|
+
const rollback_plan = createRollbackPlan(artifact, backup_plan);
|
|
197
|
+
const configured = new Set(artifact.installation.enabled_providers.map(({ provider_id, harness_id }) => `${provider_id}:${harness_id}`));
|
|
198
|
+
const incompatibilities = detector.detected_bindings
|
|
199
|
+
.filter(({ provider_id, harness_id }) => !configured.has(`${provider_id}:${harness_id}`))
|
|
200
|
+
.map(({ provider_id, harness_id }) => ({ code: 'V1_BINDING_NOT_SELECTED', provider_id, harness_id, message: 'legacy marker is not a selected v2 binding' }));
|
|
201
|
+
const pathOutsideProject = artifact.managed_paths.some((managedPath) => relative(resolve(projectDir), resolve(projectDir, managedPath)).split(sep).includes('..'));
|
|
202
|
+
if (pathOutsideProject) throw new ContractError('INVALID_MANAGED_PATH', 'managed path resolves outside project');
|
|
203
|
+
return Object.freeze(canonicalize({ schema_version: 1, mode: 'dry-run', writes_performed: false, artifact, doctor, detector, backup_plan, rollback_plan, incompatibilities }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Explicit v2 installation. It writes only the provider-neutral artifact path. */
|
|
207
|
+
export function installV2(input, { projectDir, clock = () => new Date(), exists = existsSync } = {}) {
|
|
208
|
+
const report = dryRunV2(input, { projectDir, clock, exists });
|
|
209
|
+
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
210
|
+
const directory = join(projectDir, '.chati', 'v2');
|
|
211
|
+
mkdirSync(directory, { recursive: true });
|
|
212
|
+
const serialized = `${canonicalSerialize(report.artifact)}\n`;
|
|
213
|
+
if (existsSync(artifactPath)) {
|
|
214
|
+
const existing = readFileSync(artifactPath, 'utf8');
|
|
215
|
+
if (existing === serialized) return Object.freeze(canonicalize({ ...report, mode: 'installed', writes_performed: false, artifact_path: INSTALLATION_ARTIFACT_PATH }));
|
|
216
|
+
throw new ContractError('INSTALLATION_ARTIFACT_EXISTS', 'existing v2 artifact differs; use an explicit migration or rollback flow');
|
|
217
|
+
}
|
|
218
|
+
const temporary = `${artifactPath}.tmp-${process.pid}`;
|
|
219
|
+
writeFileSync(temporary, serialized, { encoding: 'utf8', flag: 'wx' });
|
|
220
|
+
renameSync(temporary, artifactPath);
|
|
221
|
+
return Object.freeze(canonicalize({ ...report, mode: 'installed', writes_performed: true, artifact_path: INSTALLATION_ARTIFACT_PATH }));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Explicitly replaces an existing v2 artifact after preserving its exact
|
|
226
|
+
* bytes in the project-local backup store. Provider-specific files are never
|
|
227
|
+
* read or rewritten by this operation.
|
|
228
|
+
*/
|
|
229
|
+
export function reconfigureV2(input, { projectDir, clock = () => new Date(), exists = existsSync } = {}) {
|
|
230
|
+
const report = dryRunV2(input, { projectDir, clock, exists });
|
|
231
|
+
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
232
|
+
if (!existsSync(artifactPath)) return installV2(input, { projectDir, clock, exists });
|
|
233
|
+
const existing = readFileSync(artifactPath, 'utf8');
|
|
234
|
+
let existingArtifact;
|
|
235
|
+
try { existingArtifact = JSON.parse(existing); } catch (error) {
|
|
236
|
+
throw new ContractError('EXISTING_INSTALLATION_CORRUPT', `existing v2 artifact is not valid JSON: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
if (existingArtifact?.schema_version !== INSTALLATION_SCHEMA_VERSION || existingArtifact?.artifact_type !== 'chati-installation') {
|
|
239
|
+
throw new ContractError('EXISTING_INSTALLATION_CORRUPT', 'existing v2 artifact has an unsupported schema');
|
|
240
|
+
}
|
|
241
|
+
const backupId = `reconfigure-${sha256({ previous: existingArtifact.digest, next: report.artifact.digest }).slice(0, 20)}`;
|
|
242
|
+
const backupPath = join(projectDir, '.chati', 'v2', 'backups', backupId, 'installation.json');
|
|
243
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
244
|
+
if (existsSync(backupPath) && readFileSync(backupPath, 'utf8') !== existing) {
|
|
245
|
+
throw new ContractError('RECONFIGURE_BACKUP_CONFLICT', 'reconfiguration backup path contains different material');
|
|
246
|
+
}
|
|
247
|
+
if (!existsSync(backupPath)) writeFileSync(backupPath, existing, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
248
|
+
const serialized = `${canonicalSerialize(report.artifact)}\n`;
|
|
249
|
+
const temporary = `${artifactPath}.tmp-${process.pid}`;
|
|
250
|
+
writeFileSync(temporary, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
251
|
+
renameSync(temporary, artifactPath);
|
|
252
|
+
return Object.freeze(canonicalize({
|
|
253
|
+
...report,
|
|
254
|
+
mode: 'reconfigured',
|
|
255
|
+
writes_performed: true,
|
|
256
|
+
artifact_path: INSTALLATION_ARTIFACT_PATH,
|
|
257
|
+
backup_path: relative(projectDir, backupPath).replaceAll('\\', '/'),
|
|
258
|
+
provider_specific_files_rewritten: false,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Performs the narrow, explicit v4 to v2 activation. It backs up detected
|
|
264
|
+
* legacy markers, writes the provider-neutral artifact, and never rewrites a
|
|
265
|
+
* provider-specific configuration file.
|
|
266
|
+
*/
|
|
267
|
+
export function migrateV2(input, { projectDir, authorize = false, clock = () => new Date(), exists = existsSync } = {}) {
|
|
268
|
+
const preview = dryRunV2(input, { projectDir, clock, exists });
|
|
269
|
+
if (authorize !== true) {
|
|
270
|
+
return Object.freeze(canonicalize({ ...preview, mode: 'migration-preview', writes_performed: false, requires_explicit_authorization: true }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
274
|
+
if (existsSync(artifactPath)) {
|
|
275
|
+
const expected = `${canonicalSerialize(preview.artifact)}\n`;
|
|
276
|
+
if (readFileSync(artifactPath, 'utf8') !== expected) {
|
|
277
|
+
throw new ContractError('INSTALLATION_ARTIFACT_EXISTS', 'existing v2 artifact differs; use an explicit rollback flow');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const backed_up = [];
|
|
282
|
+
for (const entry of preview.backup_plan.entries) {
|
|
283
|
+
const source = join(projectDir, entry.source_path);
|
|
284
|
+
const destination = join(projectDir, entry.backup_path);
|
|
285
|
+
if (!existsSync(source) || existsSync(destination)) continue;
|
|
286
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
287
|
+
copyFileSync(source, destination, 0);
|
|
288
|
+
backed_up.push(entry);
|
|
289
|
+
}
|
|
290
|
+
const installed = installV2(input, { projectDir, clock, exists });
|
|
291
|
+
return Object.freeze(canonicalize({
|
|
292
|
+
...preview,
|
|
293
|
+
mode: 'migrated',
|
|
294
|
+
writes_performed: installed.writes_performed || backed_up.length > 0,
|
|
295
|
+
backed_up,
|
|
296
|
+
artifact_path: installed.artifact_path,
|
|
297
|
+
provider_specific_files_rewritten: false,
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export { canonicalSerialize };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"catalog": {
|
|
3
|
+
"snapshot_id": "chati-model-catalog-2026-08-26",
|
|
4
|
+
"expires_at": "2026-11-24T00:00:00.000Z",
|
|
5
|
+
"models": [
|
|
6
|
+
{
|
|
7
|
+
"provider_id": "anthropic",
|
|
8
|
+
"model_id": "fable",
|
|
9
|
+
"actions": [
|
|
10
|
+
"discovery",
|
|
11
|
+
"planning",
|
|
12
|
+
"build",
|
|
13
|
+
"review",
|
|
14
|
+
"adjudication"
|
|
15
|
+
],
|
|
16
|
+
"adjudication_priority": 100,
|
|
17
|
+
"highest_reasoning_configuration": "xhigh"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"provider_id": "openai",
|
|
21
|
+
"model_id": "gpt-5.6-sol",
|
|
22
|
+
"actions": [
|
|
23
|
+
"discovery",
|
|
24
|
+
"planning",
|
|
25
|
+
"build",
|
|
26
|
+
"review",
|
|
27
|
+
"adjudication"
|
|
28
|
+
],
|
|
29
|
+
"adjudication_priority": 100,
|
|
30
|
+
"highest_reasoning_configuration": "xhigh"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"provider_id": "xai",
|
|
34
|
+
"model_id": "grok-4.6",
|
|
35
|
+
"actions": [
|
|
36
|
+
"discovery",
|
|
37
|
+
"planning",
|
|
38
|
+
"build",
|
|
39
|
+
"review",
|
|
40
|
+
"adjudication"
|
|
41
|
+
],
|
|
42
|
+
"adjudication_priority": 100,
|
|
43
|
+
"highest_reasoning_configuration": "xhigh"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"provider_id": "xai",
|
|
47
|
+
"model_id": "grok-4.5",
|
|
48
|
+
"actions": [
|
|
49
|
+
"discovery",
|
|
50
|
+
"planning",
|
|
51
|
+
"build",
|
|
52
|
+
"review"
|
|
53
|
+
],
|
|
54
|
+
"adjudication_priority": 50
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"signature": "p7nTZ2/WlWVWeuqt2eKFTXnSEKZX0X6EM7C0T30+syGV09qjM8soj11U7G4Wjl9u5W6cwEErJZCXdP8WgXHeBA=="
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"snapshot_id": "chati-model-catalog-2026-08-26",
|
|
3
|
+
"expires_at": "2026-11-24T00:00:00.000Z",
|
|
4
|
+
"models": [
|
|
5
|
+
{
|
|
6
|
+
"provider_id": "anthropic",
|
|
7
|
+
"model_id": "fable",
|
|
8
|
+
"actions": ["discovery", "planning", "build", "review", "adjudication"],
|
|
9
|
+
"adjudication_priority": 100,
|
|
10
|
+
"highest_reasoning_configuration": "xhigh"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"provider_id": "openai",
|
|
14
|
+
"model_id": "gpt-5.6-sol",
|
|
15
|
+
"actions": ["discovery", "planning", "build", "review", "adjudication"],
|
|
16
|
+
"adjudication_priority": 100,
|
|
17
|
+
"highest_reasoning_configuration": "xhigh"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"provider_id": "xai",
|
|
21
|
+
"model_id": "grok-4.6",
|
|
22
|
+
"actions": ["discovery", "planning", "build", "review", "adjudication"],
|
|
23
|
+
"adjudication_priority": 100,
|
|
24
|
+
"highest_reasoning_configuration": "xhigh"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"provider_id": "xai",
|
|
28
|
+
"model_id": "grok-4.5",
|
|
29
|
+
"actions": ["discovery", "planning", "build", "review"],
|
|
30
|
+
"adjudication_priority": 50
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
p7nTZ2/WlWVWeuqt2eKFTXnSEKZX0X6EM7C0T30+syGV09qjM8soj11U7G4Wjl9u5W6cwEErJZCXdP8WgXHeBA==
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { sha256 } from '@chati/core';
|
|
2
|
+
|
|
3
|
+
const PROVIDER_BINDINGS = Object.freeze({
|
|
4
|
+
claude: Object.freeze({ provider_id: 'anthropic', harness_id: 'claude', default_model: 'fable' }),
|
|
5
|
+
codex: Object.freeze({ provider_id: 'openai', harness_id: 'codex', default_model: 'gpt-5.6-sol' }),
|
|
6
|
+
grok: Object.freeze({ provider_id: 'xai', harness_id: 'grok', default_model: 'grok-4.6' }),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bootstrap suggestions used only when the installer has no fresh capability
|
|
11
|
+
* catalog. The selected values are always written explicitly to C-01.
|
|
12
|
+
*/
|
|
13
|
+
export const wizardDefaultModels = Object.freeze(Object.fromEntries(
|
|
14
|
+
Object.entries(PROVIDER_BINDINGS).map(([provider, binding]) => [provider, binding.default_model])
|
|
15
|
+
));
|
|
16
|
+
|
|
17
|
+
const V2_ACTIONS = Object.freeze(['discovery', 'planning', 'build', 'review', 'adjudication']);
|
|
18
|
+
|
|
19
|
+
function fail(code, message) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.code = code;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeModels(provider, selections) {
|
|
26
|
+
const value = selections?.[provider];
|
|
27
|
+
const values = value === undefined ? [PROVIDER_BINDINGS[provider].default_model] : (Array.isArray(value) ? value : [value]);
|
|
28
|
+
const models = [...new Set(values.map((model) => typeof model === 'string' ? model.trim() : '').filter(Boolean))];
|
|
29
|
+
if (models.length === 0) fail('WIZARD_MODEL_REQUIRED', `${provider} requires at least one selected model`);
|
|
30
|
+
return models;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function wizardModelSuggestions(provider, capabilityCatalog) {
|
|
34
|
+
const binding = PROVIDER_BINDINGS[provider];
|
|
35
|
+
if (!binding) fail('V2_PROVIDER_UNSUPPORTED', `${provider} is not supported by v2`);
|
|
36
|
+
const models = capabilityCatalog?.models
|
|
37
|
+
?.filter((model) => model.provider_id === binding.provider_id)
|
|
38
|
+
.sort((left, right) => (right.adjudication_priority ?? 0) - (left.adjudication_priority ?? 0))
|
|
39
|
+
.map((model) => model.model_id);
|
|
40
|
+
return Object.freeze(models?.length ? [...new Set(models)] : [binding.default_model]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds the explicit C-01 input consumed by installV2. This deliberately does
|
|
45
|
+
* not inspect installed binaries or silently add a provider. A future catalog
|
|
46
|
+
* refresh may replace defaults, but its result must be supplied as selections.
|
|
47
|
+
*/
|
|
48
|
+
export function buildWizardV2InstallationInput({
|
|
49
|
+
projectName,
|
|
50
|
+
selectedProviders,
|
|
51
|
+
primaryProvider,
|
|
52
|
+
modelSelections = {},
|
|
53
|
+
capabilityCatalog,
|
|
54
|
+
profile = 'other-supported-profile',
|
|
55
|
+
clock = () => new Date(),
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (typeof projectName !== 'string' || !projectName.trim()) fail('WIZARD_PROJECT_REQUIRED', 'projectName is required');
|
|
58
|
+
if (!Array.isArray(selectedProviders) || selectedProviders.length === 0) fail('WIZARD_PROVIDER_REQUIRED', 'at least one v2 provider is required');
|
|
59
|
+
const providers = [...new Set(selectedProviders)];
|
|
60
|
+
for (const provider of providers) if (!PROVIDER_BINDINGS[provider]) fail('V2_PROVIDER_UNSUPPORTED', `${provider} is not supported by v2`);
|
|
61
|
+
if (!providers.includes(primaryProvider)) fail('WIZARD_PRIMARY_PROVIDER_REQUIRED', 'primaryProvider must be selected');
|
|
62
|
+
const now = clock();
|
|
63
|
+
const issuedAt = (now instanceof Date ? now : new Date(now));
|
|
64
|
+
if (Number.isNaN(issuedAt.getTime())) fail('WIZARD_CLOCK_INVALID', 'clock must return a valid time');
|
|
65
|
+
const localExpiry = issuedAt.getTime() + (7 * 24 * 60 * 60 * 1000);
|
|
66
|
+
const sourceExpiry = capabilityCatalog ? Date.parse(capabilityCatalog.expires_at) : localExpiry;
|
|
67
|
+
if (Number.isNaN(sourceExpiry) || sourceExpiry <= issuedAt.getTime()) fail('WIZARD_CATALOG_EXPIRED', 'capability catalog is expired');
|
|
68
|
+
// A verified signed catalog keeps its authoritative expiry. The short local
|
|
69
|
+
// TTL applies only to the unsigned bootstrap seam used by direct API tests.
|
|
70
|
+
const expiresAt = new Date(capabilityCatalog ? sourceExpiry : localExpiry).toISOString();
|
|
71
|
+
const sourceCatalogId = capabilityCatalog?.snapshot_id || 'bootstrap';
|
|
72
|
+
const enabled_providers = providers.map((provider) => {
|
|
73
|
+
const binding = PROVIDER_BINDINGS[provider];
|
|
74
|
+
return {
|
|
75
|
+
provider_id: binding.provider_id,
|
|
76
|
+
harness_id: binding.harness_id,
|
|
77
|
+
allowed_models: normalizeModels(provider, modelSelections),
|
|
78
|
+
allowed_reasoning_configurations: ['low', 'medium', 'high', 'xhigh'],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
const primary = PROVIDER_BINDINGS[primaryProvider];
|
|
82
|
+
const models = enabled_providers.flatMap((binding) => binding.allowed_models.map((model_id) => {
|
|
83
|
+
const catalogModel = capabilityCatalog?.models?.find((candidate) => candidate.provider_id === binding.provider_id && candidate.model_id === model_id);
|
|
84
|
+
if (capabilityCatalog && !catalogModel) fail('WIZARD_MODEL_NOT_IN_CATALOG', `${model_id} is not in the signed catalog for ${binding.provider_id}`);
|
|
85
|
+
return {
|
|
86
|
+
provider_id: binding.provider_id,
|
|
87
|
+
model_id,
|
|
88
|
+
actions: catalogModel?.actions || V2_ACTIONS,
|
|
89
|
+
...(catalogModel?.adjudication_priority !== undefined ? { adjudication_priority: catalogModel.adjudication_priority } : {}),
|
|
90
|
+
...(catalogModel?.highest_reasoning_configuration !== undefined
|
|
91
|
+
? { highest_reasoning_configuration: catalogModel.highest_reasoning_configuration }
|
|
92
|
+
: (!capabilityCatalog ? { highest_reasoning_configuration: 'xhigh' } : {})),
|
|
93
|
+
};
|
|
94
|
+
}));
|
|
95
|
+
const selectionDigest = sha256({ source_catalog_ref: sourceCatalogId, models, enabled_providers }).slice(0, 20);
|
|
96
|
+
const snapshotId = `wizard-catalog-${selectionDigest}`;
|
|
97
|
+
const projectSlug = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || 'project';
|
|
98
|
+
const installation = {
|
|
99
|
+
installation_id: `wizard-${projectSlug}`,
|
|
100
|
+
profile,
|
|
101
|
+
primary_harness: primary.harness_id,
|
|
102
|
+
primary_binding: { provider_id: primary.provider_id, harness_id: primary.harness_id },
|
|
103
|
+
enabled_providers,
|
|
104
|
+
capability_snapshot_ref: snapshotId,
|
|
105
|
+
policy_ref: `policy://installation/${snapshotId}`,
|
|
106
|
+
...(profile === 'focus-ai-internal' ? { external_integrations: { clickup: 'required' } } : {}),
|
|
107
|
+
};
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
project_id: `project-${projectSlug}`,
|
|
110
|
+
installation,
|
|
111
|
+
capability_snapshot: { snapshot_id: snapshotId, expires_at: expiresAt, models },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const supportedV2WizardProviders = Object.freeze(Object.keys(PROVIDER_BINDINGS));
|
|
@@ -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 { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
5
5
|
import { EXPECTED_ARTICLE_COUNT } from '../utils/constitution-meta.js';
|
|
6
6
|
import { flattenEntitiesMap } from '../utils/flatten-entities.js';
|
package/src/license/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
5
|
import { getMachineId, getMachineName } from './machine-id.js';
|
|
6
6
|
|
|
7
7
|
const API_BASE = 'https://chati.dev/api';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { assertBrowserCapabilityForTask, BrowserCapability } from '@chati/browser-capability';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* G3 browser boundary. This runtime has no reference to Playwright, Chrome,
|
|
5
|
+
* MCP, credentials or a shared profile. It can only validate a task policy and
|
|
6
|
+
* use the internal fake adapter supplied by BrowserCapability.
|
|
7
|
+
*/
|
|
8
|
+
export function createProjectBrowserSession({ task, request } = {}) {
|
|
9
|
+
const policy = assertBrowserCapabilityForTask(task);
|
|
10
|
+
if (!policy) return Object.freeze({ state: 'not-required', manifest: null, capability: null });
|
|
11
|
+
const capability = new BrowserCapability({});
|
|
12
|
+
const manifest = capability.preflight({ ...request, policy });
|
|
13
|
+
return Object.freeze({ state: 'preflighted', policy, manifest, capability });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Only the same local session may advance its fake adapter past preflight. */
|
|
17
|
+
export function launchProjectBrowserSession({ session, request } = {}) {
|
|
18
|
+
if (!session || session.state !== 'preflighted' || !session.capability || !session.policy) {
|
|
19
|
+
const error = new Error('browser launch requires a preflighted project-local browser session');
|
|
20
|
+
error.code = 'BROWSER_PREFLIGHT_REQUIRED';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const manifest = session.capability.launch({ ...request, policy: session.policy });
|
|
24
|
+
return Object.freeze({ state: 'launched', manifest, fake_launches: session.capability.fakeLaunches, capability: session.capability, policy: session.policy });
|
|
25
|
+
}
|