chati-dev 4.5.28 → 4.5.29
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/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import { lstatAtSync, openAtSync, renameAtSync, renameNoReplaceAtSync, unlinkAtSync } from '@chati/core';
|
|
5
|
+
import { withUpgradeMaintenance, UPGRADE_PENDING_FILE } from './maintenance.js';
|
|
6
|
+
import { managedEntrypoints } from './entrypoints.js';
|
|
7
|
+
import { assertUpgradeParentsUnchanged, createUpgradeParents, openUpgradeDirectory, snapshotUpgradeParents } from './paths.js';
|
|
8
|
+
import {
|
|
9
|
+
UPGRADE_TARGETS, assertUpgradeBindings, assertUpgradeStatePreserved, authorizeUpgradeProfile, prepareUpgradeGeneration,
|
|
10
|
+
readUpgradeInstallation, upgradeFailure, upgradeSnapshot, upgradeTestHook, verifyUpgradePackage,
|
|
11
|
+
} from './generation.js';
|
|
12
|
+
|
|
13
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
14
|
+
const identity = (stat) => `${stat.dev}:${stat.ino}`;
|
|
15
|
+
const openDirectory = (path) => openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
16
|
+
|
|
17
|
+
function readJsonAt(fd, name) {
|
|
18
|
+
const file = openAtSync(fd, name, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
19
|
+
try {
|
|
20
|
+
const stat = fstatSync(file);
|
|
21
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 16 * 1024 * 1024) upgradeFailure('UPGRADE_INTENT_INVALID', 'Upgrade metadata must be a bounded single-link regular file');
|
|
22
|
+
const content = readFileSync(file, 'utf8');
|
|
23
|
+
if (content.length > 16 * 1024 * 1024 || identity(lstatAtSync(fd, name)) !== identity(stat)) upgradeFailure('UPGRADE_INTENT_INVALID', 'Upgrade metadata changed during reading');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
} finally { closeSync(file); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJsonAt(fd, name, value, { replace = false } = {}) {
|
|
29
|
+
const temporary = `.pending-${randomUUID()}`;
|
|
30
|
+
const file = openAtSync(fd, temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
31
|
+
try { writeFileSync(file, JSON.stringify(value)); fsyncSync(file); } finally { closeSync(file); }
|
|
32
|
+
try {
|
|
33
|
+
if (replace) {
|
|
34
|
+
const stat = lstatAtSync(fd, name);
|
|
35
|
+
if (!stat.isFile() || stat.nlink !== 1) upgradeFailure('UPGRADE_INTENT_INVALID', 'Intent destination is not a regular file');
|
|
36
|
+
renameAtSync(fd, temporary, fd, name);
|
|
37
|
+
} else renameNoReplaceAtSync(fd, temporary, fd, name);
|
|
38
|
+
fsyncSync(fd);
|
|
39
|
+
} catch (error) { unlinkAtSync(fd, temporary, { force: true }); throw error; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function syncTree(path) {
|
|
43
|
+
const stat = lstatSync(path);
|
|
44
|
+
if (stat.isDirectory()) for (const name of readdirSync(path)) syncTree(join(path, name));
|
|
45
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | (stat.isDirectory() ? constants.O_DIRECTORY : constants.O_NONBLOCK));
|
|
46
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pathsFor(root, directory, target, index) {
|
|
50
|
+
return { active: join(root, target.path), staged: join(directory, 'stage', target.path), previous: join(directory, `previous-${index}`) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function operationsFor(root, directory, targets) {
|
|
54
|
+
return targets.flatMap((target, index) => {
|
|
55
|
+
const paths = pathsFor(root, directory, target, index);
|
|
56
|
+
return [
|
|
57
|
+
...(target.before ? [{ from: paths.active, to: paths.previous, phase: `after-retire-${index}` }] : []),
|
|
58
|
+
{ from: paths.staged, to: paths.active, phase: `after-publish-${index}` },
|
|
59
|
+
];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function expectedPrefix(root, directory, intent, count) {
|
|
64
|
+
const expected = new Map();
|
|
65
|
+
intent.targets.forEach((target, index) => {
|
|
66
|
+
const paths = pathsFor(root, directory, target, index);
|
|
67
|
+
expected.set(paths.active, target.before); expected.set(paths.staged, target.after); expected.set(paths.previous, null);
|
|
68
|
+
});
|
|
69
|
+
for (const operation of operationsFor(root, directory, intent.targets).slice(0, count)) {
|
|
70
|
+
expected.set(operation.to, expected.get(operation.from)); expected.set(operation.from, null);
|
|
71
|
+
}
|
|
72
|
+
return expected;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parentPaths(root, directory, targets) {
|
|
76
|
+
const paths = new Set([root]);
|
|
77
|
+
for (const operation of operationsFor(root, directory, targets)) {
|
|
78
|
+
for (const location of [operation.from, operation.to]) {
|
|
79
|
+
let path = dirname(location);
|
|
80
|
+
while (path !== root) { paths.add(path); path = dirname(path); }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...paths].sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertParents(root, directory, intent) {
|
|
87
|
+
for (const path of parentPaths(root, directory, intent.targets)) {
|
|
88
|
+
const stat = lstatSync(path);
|
|
89
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || identity(stat) !== intent.parents[relative(root, path)]) {
|
|
90
|
+
upgradeFailure('UPGRADE_STORAGE_CHANGED', 'An upgrade parent directory was replaced');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesPrefix(root, directory, intent, count) {
|
|
96
|
+
return [...expectedPrefix(root, directory, intent, count)].every(([path, snapshot]) => same(upgradeSnapshot(path), snapshot));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function observedProgress(root, directory, intent) {
|
|
100
|
+
assertParents(root, directory, intent);
|
|
101
|
+
const limit = operationsFor(root, directory, intent.targets).length;
|
|
102
|
+
const next = intent.phase === 'rolling_back' ? intent.progress - 1 : intent.progress + 1;
|
|
103
|
+
for (const count of [intent.progress, next]) {
|
|
104
|
+
if (count >= 0 && count <= limit && matchesPrefix(root, directory, intent, count)) return count;
|
|
105
|
+
}
|
|
106
|
+
return upgradeFailure('UPGRADE_STORAGE_CHANGED', 'Installation does not match the recorded upgrade prefix or its immediately following rename. Backups were retained.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function move(operation, context, directory, intent) {
|
|
110
|
+
context.assertCurrent(); assertParents(context.root, directory, intent);
|
|
111
|
+
const from = openUpgradeDirectory(context.root, relative(context.root, dirname(operation.from)), { expected: intent.parents });
|
|
112
|
+
let to;
|
|
113
|
+
try {
|
|
114
|
+
to = openUpgradeDirectory(context.root, relative(context.root, dirname(operation.to)), { expected: intent.parents });
|
|
115
|
+
if (from === undefined || to === undefined) upgradeFailure('UPGRADE_STORAGE_CHANGED', 'Upgrade parent disappeared');
|
|
116
|
+
context.assertCurrent(); assertParents(context.root, directory, intent);
|
|
117
|
+
if (fstatSync(from).dev !== fstatSync(to).dev) upgradeFailure('UPGRADE_FILESYSTEM_MISMATCH', 'Upgrade requires one filesystem for staged and active data');
|
|
118
|
+
renameNoReplaceAtSync(from, relative(dirname(operation.from), operation.from), to, relative(dirname(operation.to), operation.to));
|
|
119
|
+
fsyncSync(from); fsyncSync(to);
|
|
120
|
+
} finally { if (from !== undefined) closeSync(from); if (to !== undefined) closeSync(to); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateIntent(intent, context, directory, packageIdentity, id) {
|
|
124
|
+
if (intent?.schema_version !== 1 || intent.id !== id || intent.root !== context.root
|
|
125
|
+
|| !same(intent.package, packageIdentity) || !['promoting', 'rolling_back', 'complete', 'rolled_back'].includes(intent.phase)
|
|
126
|
+
|| !Array.isArray(intent.targets) || intent.targets.length < 2 || intent.targets.length > UPGRADE_TARGETS.length
|
|
127
|
+
|| !same(intent.targets.map(target => target.path), UPGRADE_TARGETS.filter(path => intent.targets.some(target => target.path === path)))
|
|
128
|
+
|| intent.targets[0].path !== '.chati.dev' || intent.targets.at(-1).path !== '.chati/manifest.json'
|
|
129
|
+
|| !intent.targets[0].before || !Number.isInteger(intent.progress)
|
|
130
|
+
|| intent.progress < 0 || intent.progress > operationsFor(context.root, directory, intent.targets).length
|
|
131
|
+
|| (intent.phase === 'complete' && intent.progress !== operationsFor(context.root, directory, intent.targets).length)
|
|
132
|
+
|| (intent.phase === 'rolled_back' && intent.progress !== 0)
|
|
133
|
+
|| !intent.parents || !intent.protected_state || typeof intent.from_version !== 'string') {
|
|
134
|
+
upgradeFailure('UPGRADE_INTENT_INVALID', 'Interrupted upgrade metadata does not match this project and verified package');
|
|
135
|
+
}
|
|
136
|
+
for (const target of intent.targets) {
|
|
137
|
+
for (const snapshot of [target.before, target.after]) {
|
|
138
|
+
if (snapshot === null && snapshot === target.before) continue;
|
|
139
|
+
if (!snapshot || !/^\d+:\d+$/.test(snapshot.identity) || !/^[a-f0-9]{64}$/.test(snapshot.digest)
|
|
140
|
+
|| snapshot.type !== (target.path === '.chati.dev' ? 'directory' : 'file')) upgradeFailure('UPGRADE_INTENT_INVALID', 'Invalid upgrade inventory');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!same(Object.keys(intent.protected_state).sort(), ['.chati/session.yaml', '.chati/v2/installation.json', 'CLAUDE.md'].sort())) upgradeFailure('UPGRADE_INTENT_INVALID', 'Invalid protected-state write boundary');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function clearPending(context, id) {
|
|
147
|
+
context.assertCurrent();
|
|
148
|
+
if (!same(readJsonAt(context.stateFd, UPGRADE_PENDING_FILE), { schema_version: 1, id })) upgradeFailure('UPGRADE_INTENT_INVALID', 'Pending upgrade marker was replaced');
|
|
149
|
+
unlinkAtSync(context.stateFd, UPGRADE_PENDING_FILE); fsyncSync(context.stateFd);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function finishUpgrade(context, directory, intent, transactionFd) {
|
|
153
|
+
const save = () => writeJsonAt(transactionFd, 'intent.json', intent, { replace: true });
|
|
154
|
+
const operations = operationsFor(context.root, directory, intent.targets);
|
|
155
|
+
intent.progress = observedProgress(context.root, directory, intent);
|
|
156
|
+
const result = () => ({ version: intent.package.version, previousVersion: intent.from_version, backupDir: directory, validation: intent.validation });
|
|
157
|
+
if (intent.phase === 'complete') {
|
|
158
|
+
// Promotion and authorization already committed durably. Requiring the old
|
|
159
|
+
// executable binding here could deadlock the reconfiguration needed after
|
|
160
|
+
// CLI drift. Still require the exact committed generation and user state.
|
|
161
|
+
assertUpgradeStatePreserved(context.root, intent.protected_state);
|
|
162
|
+
clearPending(context, intent.id);
|
|
163
|
+
return result();
|
|
164
|
+
}
|
|
165
|
+
const rollback = () => {
|
|
166
|
+
intent.phase = 'rolling_back'; save();
|
|
167
|
+
while (intent.progress > 0) {
|
|
168
|
+
const operation = operations[intent.progress - 1];
|
|
169
|
+
move({ from: operation.to, to: operation.from }, context, directory, intent);
|
|
170
|
+
upgradeTestHook(`rollback-${operation.phase}`, { directory });
|
|
171
|
+
intent.progress -= 1; save();
|
|
172
|
+
}
|
|
173
|
+
if (!matchesPrefix(context.root, directory, intent, 0)) upgradeFailure('UPGRADE_ROLLBACK_INCOMPLETE', 'Rollback inventory did not match the previous installation');
|
|
174
|
+
intent.phase = 'rolled_back'; save(); clearPending(context, intent.id);
|
|
175
|
+
};
|
|
176
|
+
if (['rolling_back', 'rolled_back'].includes(intent.phase)) {
|
|
177
|
+
rollback();
|
|
178
|
+
upgradeFailure('UPGRADE_ROLLBACK_RECOVERED', `Previous installation restored at v${intent.from_version}. Repeat upgrade to start a fresh transaction. Backup: ${directory}`);
|
|
179
|
+
}
|
|
180
|
+
let committed = false;
|
|
181
|
+
try {
|
|
182
|
+
assertUpgradeStatePreserved(context.root, intent.protected_state);
|
|
183
|
+
await authorizeUpgradeProfile(assertUpgradeBindings(context.root));
|
|
184
|
+
while (intent.progress < operations.length) {
|
|
185
|
+
const operation = operations[intent.progress];
|
|
186
|
+
move(operation, context, directory, intent);
|
|
187
|
+
upgradeTestHook(operation.phase, { directory });
|
|
188
|
+
intent.progress += 1; save();
|
|
189
|
+
}
|
|
190
|
+
if (!matchesPrefix(context.root, directory, intent, operations.length)) upgradeFailure('UPGRADE_STORAGE_CHANGED', 'Published generation did not match the validated inventory');
|
|
191
|
+
assertUpgradeStatePreserved(context.root, intent.protected_state);
|
|
192
|
+
assertUpgradeBindings(context.root);
|
|
193
|
+
intent.phase = 'complete'; save(); committed = true;
|
|
194
|
+
upgradeTestHook('after-commit', { directory });
|
|
195
|
+
clearPending(context, intent.id);
|
|
196
|
+
return result();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (committed) upgradeFailure('UPGRADE_COMMITTED_RECOVERY_REQUIRED', `${error.message}. Upgrade committed; repeat upgrade to finish marker cleanup. Backup: ${directory}`);
|
|
199
|
+
try {
|
|
200
|
+
intent.progress = observedProgress(context.root, directory, intent);
|
|
201
|
+
rollback();
|
|
202
|
+
} catch (rollbackError) {
|
|
203
|
+
upgradeFailure('UPGRADE_RECOVERY_REQUIRED', `${error.message}; rollback incomplete: ${rollbackError.message}. Repeat upgrade after resolving the cause. Backup: ${directory}`);
|
|
204
|
+
}
|
|
205
|
+
return upgradeFailure('UPGRADE_FAILED_ROLLED_BACK', `${error.code || 'UPGRADE_FAILED'}: ${error.message}. Previous installation restored; backup: ${directory}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Managed runtime and exact entrypoint upgrade, not a general project installer. */
|
|
210
|
+
export async function upgradeProject(projectDir, { targetVersion, offline = false } = {}) {
|
|
211
|
+
return withUpgradeMaintenance(projectDir, async (context) => {
|
|
212
|
+
const packageIdentity = verifyUpgradePackage(targetVersion);
|
|
213
|
+
let pending;
|
|
214
|
+
try { pending = readJsonAt(context.stateFd, UPGRADE_PENDING_FILE); }
|
|
215
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
216
|
+
if (pending) {
|
|
217
|
+
if (pending.schema_version !== 1 || !/^[a-f0-9-]{36}$/.test(pending.id) || Object.keys(pending).length !== 2) upgradeFailure('UPGRADE_INTENT_INVALID', 'Invalid pending upgrade pointer');
|
|
218
|
+
const directory = join(context.storageDir, `txn-${pending.id}`);
|
|
219
|
+
const fd = openDirectory(directory);
|
|
220
|
+
try {
|
|
221
|
+
const intent = readJsonAt(fd, 'intent.json');
|
|
222
|
+
validateIntent(intent, context, directory, packageIdentity, pending.id);
|
|
223
|
+
return await finishUpgrade(context, directory, intent, fd);
|
|
224
|
+
} finally { closeSync(fd); }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Capture the exact active generation and optional-file absence before any
|
|
228
|
+
// staging. No CLAUDE/session/installation/authority path is in the write set.
|
|
229
|
+
const runtimeTargets = ['.chati.dev', '.claude/settings.json', '.chati/manifest.json'];
|
|
230
|
+
const runtimeParents = snapshotUpgradeParents(context.root, runtimeTargets);
|
|
231
|
+
const before = Object.fromEntries(runtimeTargets.map(path => [path, upgradeSnapshot(join(context.root, path))]));
|
|
232
|
+
const installation = await readUpgradeInstallation(context.root, { ...packageIdentity, offline });
|
|
233
|
+
assertUpgradeParentsUnchanged(context.root, runtimeParents);
|
|
234
|
+
const entrypointTargets = managedEntrypoints(installation.providers).map(entry => entry.path);
|
|
235
|
+
const initialParents = snapshotUpgradeParents(context.root, [...runtimeTargets, ...entrypointTargets]);
|
|
236
|
+
for (const path of entrypointTargets) before[path] = upgradeSnapshot(join(context.root, path));
|
|
237
|
+
const id = randomUUID();
|
|
238
|
+
const directory = join(context.storageDir, `txn-${id}`);
|
|
239
|
+
mkdirSync(directory, { mode: 0o700 }); fsyncSync(context.stateFd);
|
|
240
|
+
const stagingDir = join(directory, 'stage');
|
|
241
|
+
mkdirSync(stagingDir, { mode: 0o700 });
|
|
242
|
+
const prepared = await prepareUpgradeGeneration(context.root, stagingDir, installation, packageIdentity);
|
|
243
|
+
const targets = prepared.targets.map(path => ({ path, before: before[path], after: upgradeSnapshot(join(stagingDir, path)) }));
|
|
244
|
+
assertUpgradeParentsUnchanged(context.root, initialParents);
|
|
245
|
+
for (const target of targets) {
|
|
246
|
+
if (!same(upgradeSnapshot(join(context.root, target.path)), target.before)) upgradeFailure('UPGRADE_STATE_CHANGED', 'Active installation changed during preparation');
|
|
247
|
+
}
|
|
248
|
+
const activeParents = createUpgradeParents(context.root, prepared.targets, initialParents);
|
|
249
|
+
const parents = Object.fromEntries(parentPaths(context.root, directory, targets).map(path => {
|
|
250
|
+
const stat = lstatSync(path);
|
|
251
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== lstatSync(directory).dev) upgradeFailure('UNSAFE_UPGRADE_PATH', 'Upgrade parents must be real directories on the same filesystem');
|
|
252
|
+
return [relative(context.root, path), identity(stat)];
|
|
253
|
+
}));
|
|
254
|
+
syncTree(stagingDir);
|
|
255
|
+
assertUpgradeParentsUnchanged(context.root, activeParents);
|
|
256
|
+
const intent = { schema_version: 1, id, root: context.root, package: packageIdentity, from_version: installation.config.version,
|
|
257
|
+
phase: 'promoting', progress: 0, targets, parents, protected_state: installation.protectedState, validation: prepared.validation };
|
|
258
|
+
const fd = openDirectory(directory);
|
|
259
|
+
try {
|
|
260
|
+
writeJsonAt(fd, 'intent.json', intent);
|
|
261
|
+
context.assertCurrent();
|
|
262
|
+
writeJsonAt(context.stateFd, UPGRADE_PENDING_FILE, { schema_version: 1, id });
|
|
263
|
+
upgradeTestHook('after-intent', { directory });
|
|
264
|
+
return await finishUpgrade(context, directory, intent, fd);
|
|
265
|
+
} finally { closeSync(fd); }
|
|
266
|
+
}, { exclusive: true });
|
|
267
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Instruction scope only. Never use this text as runtime authorization.
|
|
2
|
+
export const DELEGATED_EXECUTION_BOUNDARY = `## Delegated execution boundary
|
|
3
|
+
|
|
4
|
+
If the parent runtime already launched this process with a concrete agent/task assignment,
|
|
5
|
+
you are its delegated worker, not the user-facing coordinator. Execute only that assignment.
|
|
6
|
+
Do not invoke the Chati entry skill, router, advance, recovery, or dispatch another pipeline agent.
|
|
7
|
+
Use the supplied canonical context, write only the assigned outputs, and return the required
|
|
8
|
+
result envelope to the parent. Return missing information or errors in that envelope;
|
|
9
|
+
do not create interactions, change session state, or authorize transitions yourself.
|
|
10
|
+
An absent or ambiguous delegated assignment is a structured error, not a reason to reactivate.
|
|
11
|
+
Ordinary task commands or specialist delegation remain subject to the assigned scope and binding.
|
|
12
|
+
This role description is not permission to skip any runtime check or expand tool permissions.
|
|
13
|
+
|
|
14
|
+
For a normal user entry or continuation, the user-facing coordinator must still run the
|
|
15
|
+
deterministic router first and preserve its license, integrity, claim and gate checks.`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveFrameworkDir } from './framework-dir.js';
|
|
4
|
+
|
|
5
|
+
const AGENT_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6
|
+
|
|
7
|
+
function validateAgent(agent) {
|
|
8
|
+
if (typeof agent !== 'string' || !AGENT_NAME.test(agent)) {
|
|
9
|
+
throw new Error('Invalid handoff agent name');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Each producer owns a directory, not a mounted file inode. Native CLI editors
|
|
14
|
+
// can therefore create a temporary file and atomically rename it into place.
|
|
15
|
+
export function agentHandoffDirectory(agent) {
|
|
16
|
+
validateAgent(agent);
|
|
17
|
+
return `artifacts/handoffs/${agent}/`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function agentHandoffPath(projectDir, agent) {
|
|
21
|
+
return join(projectDir, agentHandoffDirectory(agent), `${agent}-handoff.md`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handoffRoots(projectDir) {
|
|
25
|
+
return [
|
|
26
|
+
join(projectDir, 'artifacts', 'handoffs'),
|
|
27
|
+
join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs'),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Select by presence, NEVER by successful parsing. An empty, corrupt or dangling
|
|
32
|
+
// new output must not silently resurrect a stale legacy handoff.
|
|
33
|
+
export function findAgentHandoffPath(projectDir, agent) {
|
|
34
|
+
validateAgent(agent);
|
|
35
|
+
const candidates = [agentHandoffPath(projectDir, agent),
|
|
36
|
+
...handoffRoots(projectDir).map(root => join(root, `${agent}-handoff.md`))];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const stat = lstatSync(candidate, { throwIfNoEntry: false });
|
|
39
|
+
if (!stat) continue;
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Handoff must be a regular file');
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function listHandoffAgents(projectDir) {
|
|
47
|
+
const agents = new Set();
|
|
48
|
+
for (const [index, root] of handoffRoots(projectDir).entries()) {
|
|
49
|
+
let entries;
|
|
50
|
+
try { entries = readdirSync(root, { withFileTypes: true }); }
|
|
51
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (index === 0 && entry.isDirectory() && AGENT_NAME.test(entry.name)) agents.add(entry.name);
|
|
54
|
+
const match = entry.name.match(/^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)-handoff\.md$/);
|
|
55
|
+
if (match) agents.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...agents].sort();
|
|
59
|
+
}
|
package/src/wizard/i18n.js
CHANGED
|
@@ -29,6 +29,7 @@ const FALLBACK_EN = {
|
|
|
29
29
|
llm_provider_label: 'AI Provider',
|
|
30
30
|
ide_selection_title: 'Select your IDEs / CLIs (multiple allowed):',
|
|
31
31
|
provider_selection_title: 'Which AI providers will you use?',
|
|
32
|
+
council_providers_required: 'Select at least two distinct providers capable of review for the required quality council.',
|
|
32
33
|
editor_selection_title: 'Which editor IDEs should get rules files? (optional)',
|
|
33
34
|
provider_claude_hint: 'Anthropic official CLI',
|
|
34
35
|
provider_gemini_hint: 'Google AI terminal agent',
|
package/src/wizard/index.js
CHANGED
|
@@ -3,10 +3,11 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { join, dirname, basename } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { logBanner } from '../utils/logger.js';
|
|
6
|
-
import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
|
|
6
|
+
import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, validateCouncilProviderSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
|
|
7
7
|
import { createSpinner, withSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
|
|
8
8
|
import { installFramework } from '../installer/core.js';
|
|
9
|
-
import {
|
|
9
|
+
import { assertInstallerPlatformSupported } from '../installer/preflight.js';
|
|
10
|
+
import { INSTALLATION_ARTIFACT_PATH, assertExecutableV2Installation, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
|
|
10
11
|
import { bindProviderExecutables } from '../installer-v2/provider-executable.js';
|
|
11
12
|
import { buildWizardV2InstallationInput } from '../installer-v2/wizard-installation.js';
|
|
12
13
|
import { resolveCapabilityCatalog, verifySignedCapabilityCatalog } from '../installer-v2/catalog-client.js';
|
|
@@ -27,6 +28,7 @@ const VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.jso
|
|
|
27
28
|
* Run the 4-step installer wizard
|
|
28
29
|
*/
|
|
29
30
|
export async function runWizard(targetDir, options = {}) {
|
|
31
|
+
assertInstallerPlatformSupported();
|
|
30
32
|
// Load ASCII logo
|
|
31
33
|
let logoText;
|
|
32
34
|
try {
|
|
@@ -128,6 +130,8 @@ export async function runWizard(targetDir, options = {}) {
|
|
|
128
130
|
stage = previousInteractiveStage('providers');
|
|
129
131
|
continue;
|
|
130
132
|
}
|
|
133
|
+
const providerError = validateCouncilProviderSelection(value);
|
|
134
|
+
if (providerError) throw Object.assign(new Error(providerError), { code: 'COUNCIL_PROVIDERS_REQUIRED' });
|
|
131
135
|
selectedProviders = value;
|
|
132
136
|
if (!catalogResolution) {
|
|
133
137
|
catalogResolution = await withSpinner(t('installer.catalog_resolving'), async () => (
|
|
@@ -202,7 +206,15 @@ export async function runWizard(targetDir, options = {}) {
|
|
|
202
206
|
capabilityCatalog: catalogResolution.catalog,
|
|
203
207
|
installationMode,
|
|
204
208
|
}));
|
|
205
|
-
dryRunV2(v2InstallationInput, { projectDir: targetDir });
|
|
209
|
+
const preview = dryRunV2(v2InstallationInput, { projectDir: targetDir });
|
|
210
|
+
try {
|
|
211
|
+
assertExecutableV2Installation(preview.artifact);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (options.providers !== undefined || options.modelSelections !== undefined) throw error;
|
|
214
|
+
p.log.error(`${t('installer.council_providers_required')} ${error.message}`);
|
|
215
|
+
stage = 'providers';
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
206
218
|
if (existsSync(join(targetDir, INSTALLATION_ARTIFACT_PATH)) && options.reconfigure !== true) {
|
|
207
219
|
throw Object.assign(new Error('a v2 installation already exists; use --reconfigure to replace it safely'), { code: 'INSTALLATION_ALREADY_EXISTS' });
|
|
208
220
|
}
|
|
@@ -230,8 +242,8 @@ export async function runWizard(targetDir, options = {}) {
|
|
|
230
242
|
|
|
231
243
|
try {
|
|
232
244
|
await installFramework(config);
|
|
233
|
-
if (options.reconfigure === true) reconfigureV2(v2InstallationInput, { projectDir: targetDir });
|
|
234
|
-
else installV2(v2InstallationInput, { projectDir: targetDir });
|
|
245
|
+
if (options.reconfigure === true) await reconfigureV2(v2InstallationInput, { projectDir: targetDir, licenseValidator: options.licenseValidator });
|
|
246
|
+
else await installV2(v2InstallationInput, { projectDir: targetDir, licenseValidator: options.licenseValidator });
|
|
235
247
|
installSpinner.stop();
|
|
236
248
|
|
|
237
249
|
showStep(t('installer.created_chati'));
|
package/src/wizard/questions.js
CHANGED
|
@@ -83,9 +83,15 @@ const PROVIDER_DISPLAY_NAMES = {
|
|
|
83
83
|
/**
|
|
84
84
|
* Step 3a: Provider Selection (multi-select)
|
|
85
85
|
*
|
|
86
|
-
* Asks
|
|
87
|
-
*
|
|
86
|
+
* Asks for at least two providers for the required cross-provider council.
|
|
87
|
+
* Never adds an unselected provider or defines a primary provider.
|
|
88
88
|
*/
|
|
89
|
+
export function validateCouncilProviderSelection(selected) {
|
|
90
|
+
if (!Array.isArray(selected) || selected.some((provider) => !supportedV2WizardProviders.includes(provider))
|
|
91
|
+
|| new Set(selected).size < 2) return t('installer.council_providers_required');
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
export async function stepProviderSelection({ allowBack = false } = {}) {
|
|
90
96
|
const cliIDEs = Object.entries(IDE_CONFIGS).filter(([key, c]) => c.group === 'cli' && supportedV2WizardProviders.includes(IDE_TO_PROVIDER[key]));
|
|
91
97
|
const options = [];
|
|
@@ -108,17 +114,23 @@ export async function stepProviderSelection({ allowBack = false } = {}) {
|
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
let initialValues = [];
|
|
118
|
+
// The bundled Clack multiselect does not forward a custom validate callback.
|
|
119
|
+
// Validate the returned values ourselves, preserving selection on retry.
|
|
120
|
+
while (true) {
|
|
121
|
+
const selected = await p.multiselect({
|
|
122
|
+
message: `${t('installer.provider_selection_title')} ${t('installer.council_providers_required')} ${dim(t('installer.multiselect_hint'))}`,
|
|
123
|
+
options: allowBack ? [...options, backOption()] : options,
|
|
124
|
+
required: false,
|
|
125
|
+
initialValues,
|
|
126
|
+
});
|
|
127
|
+
if (p.isCancel(selected)) cancelInstallation();
|
|
128
|
+
if (allowBack && selected.includes(WIZARD_BACK)) return WIZARD_BACK;
|
|
129
|
+
const error = validateCouncilProviderSelection(selected);
|
|
130
|
+
if (!error) return selected;
|
|
131
|
+
p.log.error(error);
|
|
132
|
+
initialValues = selected.filter((provider) => supportedV2WizardProviders.includes(provider));
|
|
119
133
|
}
|
|
120
|
-
|
|
121
|
-
return selected.includes(WIZARD_BACK) ? WIZARD_BACK : selected;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/**
|