@tiangong-ai/cli 0.0.61 → 0.0.62
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/AGENTS.md +17 -6
- package/README.md +414 -33
- package/dist/data/builtins.js +33 -5
- package/dist/data/builtins.js.map +1 -1
- package/dist/data/catalog.js +46 -0
- package/dist/data/catalog.js.map +1 -1
- package/dist/data/commands.js +15 -1
- package/dist/data/commands.js.map +1 -1
- package/dist/data/connectors/airnow-hourly-observations.js +60 -10
- package/dist/data/connectors/airnow-hourly-observations.js.map +1 -1
- package/dist/data/connectors/epa-eis-records.js +16 -2
- package/dist/data/connectors/epa-eis-records.js.map +1 -1
- package/dist/data/connectors/gdelt-doc-search.js +54 -18
- package/dist/data/connectors/gdelt-doc-search.js.map +1 -1
- package/dist/data/connectors/gdelt-file-feeds.js +20 -11
- package/dist/data/connectors/gdelt-file-feeds.js.map +1 -1
- package/dist/data/connectors/gdelt-web-ngrams.d.ts +2 -0
- package/dist/data/connectors/gdelt-web-ngrams.js +350 -0
- package/dist/data/connectors/gdelt-web-ngrams.js.map +1 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.d.ts +188 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.js +138 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.js.map +1 -0
- package/dist/data/connectors/nasa-firms-fire.js +4 -0
- package/dist/data/connectors/nasa-firms-fire.js.map +1 -1
- package/dist/data/connectors/open-meteo-air-quality.js +27 -22
- package/dist/data/connectors/open-meteo-air-quality.js.map +1 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.js.map +1 -1
- package/dist/data/connectors/open-meteo-flood.js +28 -23
- package/dist/data/connectors/open-meteo-flood.js.map +1 -1
- package/dist/data/connectors/open-meteo-flood.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-flood.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-flood.schemas.js.map +1 -1
- package/dist/data/connectors/open-meteo-historical-weather.js +26 -21
- package/dist/data/connectors/open-meteo-historical-weather.js.map +1 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.js.map +1 -1
- package/dist/data/connectors/openaq-air-quality.js +50 -12
- package/dist/data/connectors/openaq-air-quality.js.map +1 -1
- package/dist/data/connectors/openaq-air-quality.schemas.d.ts +0 -4
- package/dist/data/connectors/openaq-air-quality.schemas.js +2 -2
- package/dist/data/connectors/openaq-air-quality.schemas.js.map +1 -1
- package/dist/data/connectors/youtube-public-content.js +76 -6
- package/dist/data/connectors/youtube-public-content.js.map +1 -1
- package/dist/data/connectors/youtube-public-content.schemas.d.ts +63 -3
- package/dist/data/connectors/youtube-public-content.schemas.js +56 -2
- package/dist/data/connectors/youtube-public-content.schemas.js.map +1 -1
- package/dist/data/contracts.d.ts +22 -1
- package/dist/data/runtime/bounded-http.d.ts +2 -0
- package/dist/data/runtime/bounded-http.js +221 -59
- package/dist/data/runtime/bounded-http.js.map +1 -1
- package/dist/data/runtime/execute.d.ts +2 -0
- package/dist/data/runtime/execute.js +82 -60
- package/dist/data/runtime/execute.js.map +1 -1
- package/dist/data/runtime/http-timing.d.ts +15 -0
- package/dist/data/runtime/http-timing.js +59 -0
- package/dist/data/runtime/http-timing.js.map +1 -0
- package/dist/data/runtime/http-transport.d.ts +1 -0
- package/dist/data/runtime/http-transport.js +27 -0
- package/dist/data/runtime/http-transport.js.map +1 -0
- package/dist/data/schemas/catalog.v1.json +46 -0
- package/dist/data/schemas/discovery.v1.json +12 -0
- package/dist/data/schemas/error.v1.json +1 -0
- package/dist/data/schemas/manifest.v1.json +19 -1
- package/dist/research/orchestration.js +233 -10
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/setup-command.js +37 -6
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/audit-bundle.js +28 -8
- package/dist/research/workspace/audit-bundle.js.map +1 -1
- package/dist/research/workspace/broker.js +25 -4
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -0
- package/dist/research/workspace/constants.js +1 -0
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/content-evidence.d.ts +1 -0
- package/dist/research/workspace/content-evidence.js +1 -1
- package/dist/research/workspace/content-evidence.js.map +1 -1
- package/dist/research/workspace/credentials.js +2 -1
- package/dist/research/workspace/credentials.js.map +1 -1
- package/dist/research/workspace/data-evidence-adapter.d.ts +71 -0
- package/dist/research/workspace/data-evidence-adapter.js +269 -59
- package/dist/research/workspace/data-evidence-adapter.js.map +1 -1
- package/dist/research/workspace/data-evidence-view.d.ts +58 -0
- package/dist/research/workspace/data-evidence-view.js +411 -0
- package/dist/research/workspace/data-evidence-view.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.js +32 -29
- package/dist/research/workspace/evidence-ledger.js.map +1 -1
- package/dist/research/workspace/evidence.d.ts +28 -0
- package/dist/research/workspace/evidence.js +48 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/executor.d.ts +27 -1
- package/dist/research/workspace/executor.js +180 -20
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/investigation-attempt.d.ts +159 -0
- package/dist/research/workspace/investigation-attempt.js +619 -0
- package/dist/research/workspace/investigation-attempt.js.map +1 -0
- package/dist/research/workspace/investigation-audit.d.ts +20 -0
- package/dist/research/workspace/investigation-audit.js +306 -0
- package/dist/research/workspace/investigation-audit.js.map +1 -0
- package/dist/research/workspace/investigation-candidate.d.ts +84 -0
- package/dist/research/workspace/investigation-candidate.js +156 -0
- package/dist/research/workspace/investigation-candidate.js.map +1 -0
- package/dist/research/workspace/investigation-certification.d.ts +39 -0
- package/dist/research/workspace/investigation-certification.js +152 -0
- package/dist/research/workspace/investigation-certification.js.map +1 -0
- package/dist/research/workspace/investigation-close.d.ts +47 -0
- package/dist/research/workspace/investigation-close.js +153 -0
- package/dist/research/workspace/investigation-close.js.map +1 -0
- package/dist/research/workspace/investigation-lineage.d.ts +8 -0
- package/dist/research/workspace/investigation-lineage.js +10 -0
- package/dist/research/workspace/investigation-lineage.js.map +1 -0
- package/dist/research/workspace/investigation-observer.d.ts +22 -0
- package/dist/research/workspace/investigation-observer.js +122 -0
- package/dist/research/workspace/investigation-observer.js.map +1 -0
- package/dist/research/workspace/investigation-promotion.d.ts +129 -0
- package/dist/research/workspace/investigation-promotion.js +316 -0
- package/dist/research/workspace/investigation-promotion.js.map +1 -0
- package/dist/research/workspace/investigation-requirements.d.ts +5 -0
- package/dist/research/workspace/investigation-requirements.js +32 -0
- package/dist/research/workspace/investigation-requirements.js.map +1 -0
- package/dist/research/workspace/investigation-resources.d.ts +12 -0
- package/dist/research/workspace/investigation-resources.js +78 -0
- package/dist/research/workspace/investigation-resources.js.map +1 -0
- package/dist/research/workspace/investigation-source-export.d.ts +6 -0
- package/dist/research/workspace/investigation-source-export.js +68 -0
- package/dist/research/workspace/investigation-source-export.js.map +1 -0
- package/dist/research/workspace/investigation-store.d.ts +9 -0
- package/dist/research/workspace/investigation-store.js +55 -0
- package/dist/research/workspace/investigation-store.js.map +1 -0
- package/dist/research/workspace/investigation.d.ts +424 -0
- package/dist/research/workspace/investigation.js +716 -0
- package/dist/research/workspace/investigation.js.map +1 -0
- package/dist/research/workspace/native-process-supervisor.d.ts +1 -0
- package/dist/research/workspace/native-process-supervisor.js +97 -0
- package/dist/research/workspace/native-process-supervisor.js.map +1 -0
- package/dist/research/workspace/native-run.d.ts +79 -6
- package/dist/research/workspace/native-run.js +293 -51
- package/dist/research/workspace/native-run.js.map +1 -1
- package/dist/research/workspace/preflight.d.ts +3 -0
- package/dist/research/workspace/preflight.js +11 -6
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/project-authority.js +2 -0
- package/dist/research/workspace/project-authority.js.map +1 -1
- package/dist/research/workspace/project-budget.d.ts +41 -0
- package/dist/research/workspace/project-budget.js +265 -0
- package/dist/research/workspace/project-budget.js.map +1 -0
- package/dist/research/workspace/project-mutations.d.ts +1 -1
- package/dist/research/workspace/project-mutations.js +27 -3
- package/dist/research/workspace/project-mutations.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +66 -0
- package/dist/research/workspace/projects.js +181 -3
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/provider-budget.d.ts +3 -0
- package/dist/research/workspace/provider-budget.js +45 -0
- package/dist/research/workspace/provider-budget.js.map +1 -0
- package/dist/research/workspace/provider-state.d.ts +1 -0
- package/dist/research/workspace/provider-state.js +20 -0
- package/dist/research/workspace/provider-state.js.map +1 -0
- package/dist/research/workspace/publication-lineage.d.ts +34 -0
- package/dist/research/workspace/publication-lineage.js +165 -0
- package/dist/research/workspace/publication-lineage.js.map +1 -0
- package/dist/research/workspace/publication-workflow.d.ts +24 -0
- package/dist/research/workspace/publication-workflow.js +118 -14
- package/dist/research/workspace/publication-workflow.js.map +1 -1
- package/dist/research/workspace/review-executor.d.ts +6 -1
- package/dist/research/workspace/review-executor.js +17 -3
- package/dist/research/workspace/review-executor.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +12 -1
- package/dist/research/workspace/runtime.js +143 -19
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/scientific-amendment.d.ts +103 -0
- package/dist/research/workspace/scientific-amendment.js +537 -0
- package/dist/research/workspace/scientific-amendment.js.map +1 -0
- package/dist/research/workspace/scientific-fulfillment-audit.d.ts +20 -2
- package/dist/research/workspace/scientific-fulfillment-audit.js +87 -7
- package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -1
- package/dist/research/workspace/scientific-fulfillment.d.ts +10 -1
- package/dist/research/workspace/scientific-fulfillment.js +20 -6
- package/dist/research/workspace/scientific-fulfillment.js.map +1 -1
- package/dist/research/workspace/scientific-review-execution.js +162 -22
- package/dist/research/workspace/scientific-review-execution.js.map +1 -1
- package/dist/research/workspace/scientific-review.d.ts +2 -1
- package/dist/research/workspace/scientific-review.js +33 -2
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +3 -3
- package/dist/research/workspace/setup-release.d.ts +22 -0
- package/dist/research/workspace/setup-release.js +426 -0
- package/dist/research/workspace/setup-release.js.map +1 -0
- package/dist/research/workspace/setup-runtime-integrity.d.ts +2 -0
- package/dist/research/workspace/setup-runtime-integrity.js +31 -0
- package/dist/research/workspace/setup-runtime-integrity.js.map +1 -0
- package/dist/research/workspace/setup-upgrade.d.ts +17 -0
- package/dist/research/workspace/setup-upgrade.js +784 -0
- package/dist/research/workspace/setup-upgrade.js.map +1 -0
- package/dist/research/workspace/setup.d.ts +31 -39
- package/dist/research/workspace/setup.js +220 -17
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/task-acceptance.d.ts +8 -1
- package/dist/research/workspace/task-acceptance.js +37 -5
- package/dist/research/workspace/task-acceptance.js.map +1 -1
- package/dist/research/workspace/task-audit.d.ts +5 -1
- package/dist/research/workspace/task-audit.js +33 -18
- package/dist/research/workspace/task-audit.js.map +1 -1
- package/dist/research/workspace/types.d.ts +44 -0
- package/dist/research/workspace/workspace.js +34 -4
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { cp, lstat, mkdir, readFile, readdir, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CliError } from "../../errors.js";
|
|
5
|
+
import { appendJournalEvent, readVerifiedJournal } from "./journal.js";
|
|
6
|
+
import { exactResearchCliCommand, researchSetupApplyCommand } from "./setup-invocation.js";
|
|
7
|
+
import { SETUP_UPGRADING_MARKER } from "./constants.js";
|
|
8
|
+
import { assertResearchSetupRuntimeIntegrity } from "./setup-runtime-integrity.js";
|
|
9
|
+
import { applyResearchSetupPlan, assertUpgradeParent, createResearchSetupPlan, doctorResearchSetup, loadAndVerifyResearchSetupPlan, loadHashVerifiedResearchSetupPlan, researchSetupUpgradeCandidatePath, } from "./setup.js";
|
|
10
|
+
import { acquireFileLock, canonicalJson, hashRegularTree, isObject, pathExists, sha256Bytes, workspacePaths, writeBytesAtomic, writeJsonAtomic, } from "./storage.js";
|
|
11
|
+
const MAX_CONTROL_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
13
|
+
const DIAGNOSTICS = new Set(["state", "report", "attestation"]);
|
|
14
|
+
function fail(message, code = "RESEARCH_SETUP_UPGRADE_CONFLICT") {
|
|
15
|
+
return new CliError(message, { code, exitCode: 3 });
|
|
16
|
+
}
|
|
17
|
+
function directory(root, hash) {
|
|
18
|
+
if (!HASH.test(hash))
|
|
19
|
+
throw fail("Invalid upgrade identity.");
|
|
20
|
+
return join(workspacePaths(root).control, "setup-upgrades", hash);
|
|
21
|
+
}
|
|
22
|
+
function files(root) {
|
|
23
|
+
const p = workspacePaths(root);
|
|
24
|
+
return {
|
|
25
|
+
marker: p.marker,
|
|
26
|
+
runtime: p.runtimeLock,
|
|
27
|
+
config: p.config,
|
|
28
|
+
plan: p.setupPlan,
|
|
29
|
+
state: p.setupState,
|
|
30
|
+
capabilities: p.capabilityDeclarations,
|
|
31
|
+
capabilityLock: p.capabilityLock,
|
|
32
|
+
setupConfig: p.setupConfig,
|
|
33
|
+
env: p.env,
|
|
34
|
+
adapter: p.setupAdapterEnv,
|
|
35
|
+
declaration: p.setupDeclarationBinding,
|
|
36
|
+
routing: join(p.control, "setup-instruction-routing.json"),
|
|
37
|
+
agents: join(root, "AGENTS.md"),
|
|
38
|
+
claudeRules: join(root, ".claude/rules/tiangong-auto-research.md"),
|
|
39
|
+
report: p.setupReport,
|
|
40
|
+
attestation: p.doctorAttestation,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function regularParents(path) {
|
|
44
|
+
let current = resolve(path);
|
|
45
|
+
while (true) {
|
|
46
|
+
const info = await lstat(current).catch((error) => {
|
|
47
|
+
if (error.code === "ENOENT")
|
|
48
|
+
return null;
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
if (info && (!info.isDirectory() || info.isSymbolicLink()))
|
|
52
|
+
throw fail("Upgrade paths must use regular directories.");
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current)
|
|
55
|
+
return;
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function bytes(path) {
|
|
60
|
+
await regularParents(dirname(path));
|
|
61
|
+
const info = await lstat(path).catch((error) => {
|
|
62
|
+
if (error.code === "ENOENT")
|
|
63
|
+
return null;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
if (!info)
|
|
67
|
+
return null;
|
|
68
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_CONTROL_BYTES)
|
|
69
|
+
throw fail("Upgrade control data is not a bounded regular file.");
|
|
70
|
+
const value = await readFile(path);
|
|
71
|
+
if (value.length !== info.size)
|
|
72
|
+
throw fail("Upgrade control data changed while reading.");
|
|
73
|
+
return { bytes: value, mode: info.mode & 0o777 };
|
|
74
|
+
}
|
|
75
|
+
async function image(dir, path) {
|
|
76
|
+
const value = await bytes(path);
|
|
77
|
+
return value ? store(dir, value.bytes, value.mode) : null;
|
|
78
|
+
}
|
|
79
|
+
async function store(dir, value, mode = 0o600) {
|
|
80
|
+
if (value.byteLength > MAX_CONTROL_BYTES)
|
|
81
|
+
throw fail("Upgrade control object exceeds its size bound.");
|
|
82
|
+
const hash = sha256Bytes(value);
|
|
83
|
+
const path = join(dir, "objects", hash);
|
|
84
|
+
await regularParents(dirname(path));
|
|
85
|
+
if (await pathExists(path)) {
|
|
86
|
+
const existing = await bytes(path);
|
|
87
|
+
if (!existing || sha256Bytes(existing.bytes) !== hash)
|
|
88
|
+
throw fail("Upgrade object hash mismatch.");
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
await writeBytesAtomic(path, value, 0o600);
|
|
92
|
+
return { sha256: hash, mode };
|
|
93
|
+
}
|
|
94
|
+
async function objectBytes(dir, value) {
|
|
95
|
+
if (!HASH.test(value.sha256))
|
|
96
|
+
throw fail("Invalid upgrade object identity.");
|
|
97
|
+
const object = await bytes(join(dir, "objects", value.sha256));
|
|
98
|
+
if (!object || sha256Bytes(object.bytes) !== value.sha256)
|
|
99
|
+
throw fail("Upgrade object is missing or changed.");
|
|
100
|
+
return object.bytes;
|
|
101
|
+
}
|
|
102
|
+
async function currentHash(path) {
|
|
103
|
+
const value = await bytes(path);
|
|
104
|
+
return value ? sha256Bytes(value.bytes) : null;
|
|
105
|
+
}
|
|
106
|
+
async function installFile(dir, path, value) {
|
|
107
|
+
await regularParents(dirname(path));
|
|
108
|
+
if (value)
|
|
109
|
+
await writeBytesAtomic(path, await objectBytes(dir, value), value.mode);
|
|
110
|
+
else
|
|
111
|
+
await rm(path, { force: true });
|
|
112
|
+
}
|
|
113
|
+
async function save(dir, state) {
|
|
114
|
+
const { stateSha256: _ignored, ...core } = state;
|
|
115
|
+
state.stateSha256 = sha256Bytes(Buffer.from(canonicalJson(core)));
|
|
116
|
+
await regularParents(dir);
|
|
117
|
+
await writeJsonAtomic(join(dir, "state.json"), state);
|
|
118
|
+
}
|
|
119
|
+
async function load(dir, plan) {
|
|
120
|
+
const raw = await bytes(join(dir, "state.json"));
|
|
121
|
+
if (!raw)
|
|
122
|
+
return null;
|
|
123
|
+
const value = JSON.parse(raw.bytes.toString("utf8"));
|
|
124
|
+
if (!isObject(value) ||
|
|
125
|
+
value.schemaVersion !== 1 ||
|
|
126
|
+
value.planSha256 !== plan.planSha256 ||
|
|
127
|
+
!isObject(value.before) ||
|
|
128
|
+
!Array.isArray(value.files) ||
|
|
129
|
+
!Array.isArray(value.trees) ||
|
|
130
|
+
typeof value.initialized !== "boolean" ||
|
|
131
|
+
typeof value.doctorAttempted !== "boolean" ||
|
|
132
|
+
!Number.isSafeInteger(value.journalBoundary) ||
|
|
133
|
+
![
|
|
134
|
+
"preparing",
|
|
135
|
+
"prepared",
|
|
136
|
+
"committing",
|
|
137
|
+
"activating",
|
|
138
|
+
"committed",
|
|
139
|
+
"rolling-back",
|
|
140
|
+
"rolled-back",
|
|
141
|
+
].includes(String(value.phase)))
|
|
142
|
+
throw fail("Upgrade state is invalid.");
|
|
143
|
+
const { stateSha256, ...core } = value;
|
|
144
|
+
if (stateSha256 !== sha256Bytes(Buffer.from(canonicalJson(core))))
|
|
145
|
+
throw fail("Upgrade state hash mismatch.");
|
|
146
|
+
const validImage = (v) => v === null ||
|
|
147
|
+
(isObject(v) &&
|
|
148
|
+
typeof v.sha256 === "string" &&
|
|
149
|
+
HASH.test(v.sha256) &&
|
|
150
|
+
Number.isInteger(v.mode) &&
|
|
151
|
+
Number(v.mode) >= 0 &&
|
|
152
|
+
Number(v.mode) <= 0o777);
|
|
153
|
+
const known = files(plan.workspace.path);
|
|
154
|
+
if (Object.entries(value.before).some(([k, v]) => !Object.hasOwn(known, k) || !validImage(v)) ||
|
|
155
|
+
Object.keys(value.before).length !== Object.keys(known).length ||
|
|
156
|
+
value.files.some((f) => !isObject(f) ||
|
|
157
|
+
typeof f.key !== "string" ||
|
|
158
|
+
!Object.hasOwn(known, f.key) ||
|
|
159
|
+
!validImage(f.before) ||
|
|
160
|
+
!validImage(f.after)) ||
|
|
161
|
+
value.trees.some((t) => !isObject(t) ||
|
|
162
|
+
!["codex", "claude-code"].includes(String(t.agent)) ||
|
|
163
|
+
typeof t.skillId !== "string" ||
|
|
164
|
+
!plan.skills.some((s) => s.id === t.skillId) ||
|
|
165
|
+
(t.before !== null && (typeof t.before !== "string" || !HASH.test(t.before))) ||
|
|
166
|
+
typeof t.after !== "string" ||
|
|
167
|
+
!HASH.test(t.after)))
|
|
168
|
+
throw fail("Upgrade state contains an unknown target.");
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
async function treeHash(path) {
|
|
172
|
+
await regularParents(dirname(path));
|
|
173
|
+
const info = await lstat(path).catch((error) => {
|
|
174
|
+
if (error.code === "ENOENT")
|
|
175
|
+
return null;
|
|
176
|
+
throw error;
|
|
177
|
+
});
|
|
178
|
+
if (!info)
|
|
179
|
+
return null;
|
|
180
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
181
|
+
throw fail("Install destination is unsafe.", "RESEARCH_SETUP_INSTALL_DESTINATION_UNSAFE");
|
|
182
|
+
return hashRegularTree(path);
|
|
183
|
+
}
|
|
184
|
+
function treePaths(plan, tree) {
|
|
185
|
+
const target = plan.install.targets.find((t) => t.agent === tree.agent);
|
|
186
|
+
const skill = plan.skills.find((s) => s.id === tree.skillId);
|
|
187
|
+
const scratch = join(dirname(target.root), ".tiangong-setup-upgrades", plan.planSha256, tree.agent);
|
|
188
|
+
return {
|
|
189
|
+
active: join(target.root, skill.skillName),
|
|
190
|
+
old: join(scratch, "old", skill.skillName),
|
|
191
|
+
next: join(scratch, "new", skill.skillName),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function copyTree(source, target, expected) {
|
|
195
|
+
const existing = await treeHash(target);
|
|
196
|
+
if (existing === expected)
|
|
197
|
+
return;
|
|
198
|
+
if (existing !== null)
|
|
199
|
+
throw fail("Prepared Skill tree changed; it will not be overwritten.");
|
|
200
|
+
await regularParents(dirname(target));
|
|
201
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
202
|
+
await cp(source, target, {
|
|
203
|
+
recursive: true,
|
|
204
|
+
dereference: false,
|
|
205
|
+
errorOnExist: true,
|
|
206
|
+
force: false,
|
|
207
|
+
mode: constants.COPYFILE_FICLONE,
|
|
208
|
+
});
|
|
209
|
+
if ((await treeHash(target)) !== expected)
|
|
210
|
+
throw fail("Prepared Skill tree failed its content binding.");
|
|
211
|
+
}
|
|
212
|
+
async function assertIdle(root) {
|
|
213
|
+
const p = workspacePaths(root);
|
|
214
|
+
for (const entry of await readdir(p.projects, { withFileTypes: true })) {
|
|
215
|
+
if (entry.isSymbolicLink())
|
|
216
|
+
throw fail("Project metadata is unsafe.");
|
|
217
|
+
if (entry.isDirectory() &&
|
|
218
|
+
(await pathExists(join(p.projects, entry.name, "native/active.json"))))
|
|
219
|
+
throw fail("Complete or abort active native stages before upgrading.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function projectPlanInput(plan, stage, environment) {
|
|
223
|
+
return {
|
|
224
|
+
workspace: stage,
|
|
225
|
+
name: plan.workspace.name,
|
|
226
|
+
mode: plan.workspace.mode,
|
|
227
|
+
evidenceProfile: plan.selection.evidenceProfile,
|
|
228
|
+
skillIds: plan.selection.skillIds,
|
|
229
|
+
scope: "project",
|
|
230
|
+
agents: plan.install.agents,
|
|
231
|
+
acceptedLicenseIds: plan.acceptedLicenses.map((x) => x.licenseId),
|
|
232
|
+
settings: plan.settings,
|
|
233
|
+
credentialEnvironment: Object.fromEntries(plan.credentialSources.map((x) => [x.id, x.fromEnvironment])),
|
|
234
|
+
agentRoutes: plan.agentRoutes,
|
|
235
|
+
reviewerExecution: plan.reviewerExecution,
|
|
236
|
+
confirmNetworkDownloads: true,
|
|
237
|
+
environment,
|
|
238
|
+
liveChecks: false,
|
|
239
|
+
agentSmoke: false,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function prepare(plan, state, dir, options) {
|
|
243
|
+
const root = plan.workspace.path, active = files(root), stage = join(dir, "stage"), target = files(stage);
|
|
244
|
+
await assertUpgradeParent(plan);
|
|
245
|
+
const prior = JSON.parse((await objectBytes(dir, state.before.plan)).toString("utf8"));
|
|
246
|
+
const sourceEnvironment = options.environment ?? process.env;
|
|
247
|
+
const environment = {
|
|
248
|
+
...sourceEnvironment,
|
|
249
|
+
HOME: join(stage, "installer-home"),
|
|
250
|
+
};
|
|
251
|
+
delete environment.CODEX_HOME;
|
|
252
|
+
delete environment.CLAUDE_CONFIG_DIR;
|
|
253
|
+
if (!state.initialized) {
|
|
254
|
+
const { initializeResearchWorkspace } = await import("./workspace.js");
|
|
255
|
+
if (!(await pathExists(target.marker)))
|
|
256
|
+
await initializeResearchWorkspace(stage, plan.workspace.name, plan.workspace.mode);
|
|
257
|
+
for (const key of [
|
|
258
|
+
"config",
|
|
259
|
+
"capabilities",
|
|
260
|
+
"capabilityLock",
|
|
261
|
+
"env",
|
|
262
|
+
"adapter",
|
|
263
|
+
"agents",
|
|
264
|
+
"claudeRules",
|
|
265
|
+
]) {
|
|
266
|
+
if (state.before[key])
|
|
267
|
+
await installFile(dir, target[key], state.before[key]);
|
|
268
|
+
}
|
|
269
|
+
if (state.before.routing) {
|
|
270
|
+
const old = JSON.parse((await objectBytes(dir, state.before.routing)).toString("utf8"));
|
|
271
|
+
for (const item of old.targets)
|
|
272
|
+
item.path = join(stage, relative(root, item.path));
|
|
273
|
+
await writeJsonAtomic(target.routing, old);
|
|
274
|
+
}
|
|
275
|
+
for (const actual of plan.install.targets)
|
|
276
|
+
for (const skill of plan.skills) {
|
|
277
|
+
const before = prior.skills.find((x) => x.id === skill.id);
|
|
278
|
+
const source = join(actual.root, skill.skillName);
|
|
279
|
+
if (before?.expectedTreeSha256 === skill.expectedTreeSha256 &&
|
|
280
|
+
(await treeHash(source)) === skill.expectedTreeSha256)
|
|
281
|
+
await copyTree(source, join(stage, actual.agent === "codex" ? ".agents" : ".claude", "skills", skill.skillName), skill.expectedTreeSha256);
|
|
282
|
+
}
|
|
283
|
+
state.initialized = true;
|
|
284
|
+
await save(dir, state);
|
|
285
|
+
}
|
|
286
|
+
let stagePlan;
|
|
287
|
+
if (await pathExists(target.plan))
|
|
288
|
+
stagePlan = await loadAndVerifyResearchSetupPlan(target.plan);
|
|
289
|
+
else
|
|
290
|
+
stagePlan = await createResearchSetupPlan(projectPlanInput(plan, stage, environment));
|
|
291
|
+
if (stagePlan.workspace.path !== stage ||
|
|
292
|
+
canonicalJson(stagePlan.selection) !== canonicalJson(plan.selection) ||
|
|
293
|
+
canonicalJson(stagePlan.agentRoutes) !== canonicalJson(plan.agentRoutes) ||
|
|
294
|
+
canonicalJson(stagePlan.reviewerExecution) !== canonicalJson(plan.reviewerExecution))
|
|
295
|
+
throw fail("Prepared plan does not match its candidate.");
|
|
296
|
+
state.stagePlanSha256 = stagePlan.planSha256;
|
|
297
|
+
await save(dir, state);
|
|
298
|
+
const applied = await applyResearchSetupPlan(target.plan, {
|
|
299
|
+
...options,
|
|
300
|
+
environment,
|
|
301
|
+
skipDoctor: true,
|
|
302
|
+
sourceCacheWorkspace: root,
|
|
303
|
+
installerCacheDirectory: join(dir, "npm-cache"),
|
|
304
|
+
});
|
|
305
|
+
const replacements = [
|
|
306
|
+
...stagePlan.install.targets.map((t) => [t.root, plan.install.targets.find((x) => x.agent === t.agent).root]),
|
|
307
|
+
[stage, root],
|
|
308
|
+
].sort((a, b) => b[0].length - a[0].length);
|
|
309
|
+
const remap = (path) => {
|
|
310
|
+
for (const [from, to] of replacements)
|
|
311
|
+
if (path === from || path.startsWith(from + sep))
|
|
312
|
+
return to + path.slice(from.length);
|
|
313
|
+
return path;
|
|
314
|
+
};
|
|
315
|
+
const desired = { ...state.before };
|
|
316
|
+
for (const key of ["config", "env", "adapter"])
|
|
317
|
+
desired[key] = await image(dir, target[key]);
|
|
318
|
+
for (const key of ["capabilities", "capabilityLock", "setupConfig", "routing"]) {
|
|
319
|
+
const contents = await bytes(target[key]);
|
|
320
|
+
if (!contents) {
|
|
321
|
+
desired[key] = null;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const value = JSON.parse(contents.bytes.toString("utf8"));
|
|
325
|
+
if (key === "capabilities" || key === "capabilityLock")
|
|
326
|
+
for (const capability of value.capabilities)
|
|
327
|
+
capability.skillPath = remap(capability.skillPath);
|
|
328
|
+
if (key === "setupConfig" || key === "routing")
|
|
329
|
+
value.planSha256 = plan.planSha256;
|
|
330
|
+
if (key === "routing")
|
|
331
|
+
for (const item of value.targets)
|
|
332
|
+
item.path = remap(item.path);
|
|
333
|
+
desired[key] = await store(dir, Buffer.from(JSON.stringify(value, null, 2) + "\n"), contents.mode);
|
|
334
|
+
}
|
|
335
|
+
if (plan.instructionRouting.targets.length) {
|
|
336
|
+
for (const key of ["agents", "claudeRules"]) {
|
|
337
|
+
if (plan.instructionRouting.targets.some((t) => t.path === active[key]))
|
|
338
|
+
desired[key] = await image(dir, target[key]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else
|
|
342
|
+
desired.routing = state.before.routing ?? null;
|
|
343
|
+
if (state.before.declaration) {
|
|
344
|
+
const binding = JSON.parse((await objectBytes(dir, state.before.declaration)).toString("utf8"));
|
|
345
|
+
if (binding.planSha256 !== prior.planSha256)
|
|
346
|
+
throw fail("Declarative setup binding is stale.");
|
|
347
|
+
desired.declaration = await store(dir, Buffer.from(JSON.stringify({ ...binding, planSha256: plan.planSha256 }, null, 2) + "\n"), state.before.declaration.mode);
|
|
348
|
+
}
|
|
349
|
+
desired.plan = await store(dir, Buffer.from(JSON.stringify(plan, null, 2) + "\n"), 0o444);
|
|
350
|
+
const lock = JSON.parse((await objectBytes(dir, state.before.runtime)).toString("utf8"));
|
|
351
|
+
desired.runtime = await store(dir, Buffer.from(JSON.stringify({ ...lock, packageVersion: plan.cli.version }, null, 2) + "\n"), state.before.runtime.mode);
|
|
352
|
+
desired.state = await store(dir, Buffer.from(JSON.stringify({ ...applied.state, planSha256: plan.planSha256, status: "partially-ready" }, null, 2) + "\n"));
|
|
353
|
+
desired.report = null;
|
|
354
|
+
desired.attestation = null;
|
|
355
|
+
state.files = Object.keys(active)
|
|
356
|
+
.filter((key) => key !== "marker")
|
|
357
|
+
.map((key) => ({ key, before: state.before[key] ?? null, after: desired[key] ?? null }));
|
|
358
|
+
// A verified staged tree is copied to its destination filesystem before any
|
|
359
|
+
// active directory is moved; the final rename is always same-filesystem.
|
|
360
|
+
for (const tree of state.trees) {
|
|
361
|
+
const skill = plan.skills.find((s) => s.id === tree.skillId);
|
|
362
|
+
await copyTree(join(stage, tree.agent === "codex" ? ".agents" : ".claude", "skills", skill.skillName), treePaths(plan, tree).next, tree.after);
|
|
363
|
+
}
|
|
364
|
+
state.phase = "prepared";
|
|
365
|
+
await save(dir, state);
|
|
366
|
+
await appendJournalEvent(workspacePaths(root).journal, "research.setup.upgrade.prepared", "workspace", { planSha256: plan.planSha256, definitionSha256: definitionHash(state) });
|
|
367
|
+
await options.upgradeCheckpoint?.("prepared");
|
|
368
|
+
}
|
|
369
|
+
async function assertInputs(plan, state) {
|
|
370
|
+
await assertUpgradeParent(plan);
|
|
371
|
+
const active = files(plan.workspace.path);
|
|
372
|
+
for (const [key, before] of Object.entries(state.before))
|
|
373
|
+
if ((await currentHash(active[key])) !== (before?.sha256 ?? null))
|
|
374
|
+
throw fail("Active setup input changed during preparation.");
|
|
375
|
+
for (const tree of state.trees)
|
|
376
|
+
if ((await treeHash(treePaths(plan, tree).active)) !== tree.before)
|
|
377
|
+
throw fail("Active Skill changed during preparation.", "RESEARCH_SETUP_INSTALL_DESTINATION_UNSAFE");
|
|
378
|
+
}
|
|
379
|
+
async function activate(plan, state, dir, options) {
|
|
380
|
+
const root = plan.workspace.path, active = files(root), p = workspacePaths(root);
|
|
381
|
+
await assertIdle(root);
|
|
382
|
+
if (state.phase === "prepared") {
|
|
383
|
+
await assertInputs(plan, state);
|
|
384
|
+
state.phase = "committing";
|
|
385
|
+
state.journalBoundary = (await readVerifiedJournal(p.journal)).length;
|
|
386
|
+
await save(dir, state);
|
|
387
|
+
}
|
|
388
|
+
const markerBytes = await objectBytes(dir, state.before.marker);
|
|
389
|
+
const marker = JSON.parse(markerBytes.toString("utf8"));
|
|
390
|
+
const blocked = Buffer.from(JSON.stringify({ ...marker, kind: SETUP_UPGRADING_MARKER, setupUpgradePlanSha256: plan.planSha256 }, null, 2) + "\n");
|
|
391
|
+
const markerHash = await currentHash(active.marker);
|
|
392
|
+
if (markerHash !== state.before.marker.sha256 && markerHash !== sha256Bytes(blocked))
|
|
393
|
+
throw fail("Workspace marker changed during upgrade.");
|
|
394
|
+
if (state.phase !== "activating")
|
|
395
|
+
await writeBytesAtomic(active.marker, blocked, state.before.marker.mode);
|
|
396
|
+
for (const tree of state.trees) {
|
|
397
|
+
const locations = treePaths(plan, tree), current = await treeHash(locations.active), backup = await treeHash(locations.old);
|
|
398
|
+
if (current === tree.after)
|
|
399
|
+
continue;
|
|
400
|
+
if (current !== tree.before && !(current === null && backup === tree.before))
|
|
401
|
+
throw fail("Skill transition conflicts with owner changes.");
|
|
402
|
+
if (current !== null) {
|
|
403
|
+
if (backup !== null)
|
|
404
|
+
throw fail("Upgrade backup destination is occupied.");
|
|
405
|
+
await regularParents(dirname(locations.old));
|
|
406
|
+
await mkdir(dirname(locations.old), { recursive: true, mode: 0o700 });
|
|
407
|
+
await rename(locations.active, locations.old);
|
|
408
|
+
}
|
|
409
|
+
if ((await treeHash(locations.next)) !== tree.after)
|
|
410
|
+
throw fail("Prepared replacement tree is missing or changed.");
|
|
411
|
+
await mkdir(dirname(locations.active), { recursive: true, mode: 0o700 });
|
|
412
|
+
await rename(locations.next, locations.active);
|
|
413
|
+
await options.upgradeCheckpoint?.(`after-tree:${tree.agent}`);
|
|
414
|
+
}
|
|
415
|
+
for (const change of [...state.files].sort((a, b) => Number(a.key === "runtime") - Number(b.key === "runtime"))) {
|
|
416
|
+
const current = await currentHash(active[change.key]);
|
|
417
|
+
if (current === (change.after?.sha256 ?? null))
|
|
418
|
+
continue;
|
|
419
|
+
if (current !== (change.before?.sha256 ?? null))
|
|
420
|
+
throw fail("A setup control file changed during upgrade.");
|
|
421
|
+
await installFile(dir, active[change.key], change.after);
|
|
422
|
+
}
|
|
423
|
+
await options.upgradeCheckpoint?.("after-files");
|
|
424
|
+
state.phase = "activating";
|
|
425
|
+
await save(dir, state);
|
|
426
|
+
await options.upgradeCheckpoint?.("before-activation");
|
|
427
|
+
await writeBytesAtomic(active.marker, markerBytes, state.before.marker.mode);
|
|
428
|
+
state.phase = "committed";
|
|
429
|
+
await save(dir, state);
|
|
430
|
+
await appendJournalEvent(p.journal, "research.setup.upgrade.committed", "workspace", {
|
|
431
|
+
planSha256: plan.planSha256,
|
|
432
|
+
parentPlanSha256: plan.upgrade.parentPlanSha256,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
export async function applyManagedSetupUpgrade(plan, options) {
|
|
436
|
+
const root = plan.workspace.path, p = workspacePaths(root), dir = directory(root, plan.planSha256);
|
|
437
|
+
const release = await acquireFileLock(p.setupLock, {
|
|
438
|
+
pid: process.pid,
|
|
439
|
+
operation: "research.setup.upgrade",
|
|
440
|
+
acquiredAt: new Date().toISOString(),
|
|
441
|
+
planSha256: plan.planSha256,
|
|
442
|
+
});
|
|
443
|
+
let releaseWorkspace = null;
|
|
444
|
+
let state = null;
|
|
445
|
+
try {
|
|
446
|
+
releaseWorkspace = await acquireFileLock(join(p.locks, "workspace.lock"), {
|
|
447
|
+
pid: process.pid,
|
|
448
|
+
operation: "research.setup.upgrade",
|
|
449
|
+
acquiredAt: new Date().toISOString(),
|
|
450
|
+
});
|
|
451
|
+
await assertIdle(root);
|
|
452
|
+
state = await load(dir, plan);
|
|
453
|
+
if (!state) {
|
|
454
|
+
const prior = await assertUpgradeParent(plan);
|
|
455
|
+
if (new Set(plan.install.targets.map((t) => t.root)).size !== plan.install.targets.length)
|
|
456
|
+
throw fail("Upgrade targets must be distinct.");
|
|
457
|
+
const before = {};
|
|
458
|
+
for (const [key, path] of Object.entries(files(root)))
|
|
459
|
+
before[key] = await image(dir, path);
|
|
460
|
+
const lock = JSON.parse((await objectBytes(dir, before.runtime)).toString("utf8"));
|
|
461
|
+
const marker = JSON.parse((await objectBytes(dir, before.marker)).toString("utf8"));
|
|
462
|
+
if (lock.packageName !== "@tiangong-ai/cli" ||
|
|
463
|
+
lock.protocolVersion !== 1 ||
|
|
464
|
+
lock.workspaceId !== marker.workspaceId ||
|
|
465
|
+
lock.packageVersion !== prior.cli.version)
|
|
466
|
+
throw fail("The active plan and runtime lock need verified legacy recovery before this upgrade.", "RESEARCH_SETUP_LEGACY_UPGRADE_RECOVERY_REQUIRED");
|
|
467
|
+
const trees = [];
|
|
468
|
+
for (const target of plan.install.targets)
|
|
469
|
+
for (const skill of plan.skills) {
|
|
470
|
+
const old = prior.skills.find((s) => s.id === skill.id);
|
|
471
|
+
const observed = await treeHash(join(target.root, skill.skillName));
|
|
472
|
+
if (observed !== null && observed !== old?.expectedTreeSha256)
|
|
473
|
+
throw fail("Existing install destination differs from its verified parent.", "RESEARCH_SETUP_INSTALL_DESTINATION_UNSAFE");
|
|
474
|
+
if (observed !== skill.expectedTreeSha256)
|
|
475
|
+
trees.push({
|
|
476
|
+
agent: target.agent,
|
|
477
|
+
skillId: skill.id,
|
|
478
|
+
before: observed,
|
|
479
|
+
after: skill.expectedTreeSha256,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
state = {
|
|
483
|
+
schemaVersion: 1,
|
|
484
|
+
planSha256: plan.planSha256,
|
|
485
|
+
phase: "preparing",
|
|
486
|
+
initialized: false,
|
|
487
|
+
stagePlanSha256: null,
|
|
488
|
+
before,
|
|
489
|
+
files: [],
|
|
490
|
+
trees,
|
|
491
|
+
doctorAttempted: false,
|
|
492
|
+
journalBoundary: 0,
|
|
493
|
+
stateSha256: "",
|
|
494
|
+
};
|
|
495
|
+
await save(dir, state);
|
|
496
|
+
}
|
|
497
|
+
await verifyAnchors(plan, state, dir);
|
|
498
|
+
if (state.phase === "rolling-back")
|
|
499
|
+
throw fail("Resume the recorded rollback before applying another upgrade.", "RESEARCH_SETUP_UPGRADE_PENDING");
|
|
500
|
+
if (state.phase === "rolled-back")
|
|
501
|
+
throw fail("This candidate was rolled back; create a fresh candidate from the restored generation.");
|
|
502
|
+
if (state.phase === "preparing")
|
|
503
|
+
await prepare(plan, state, dir, options);
|
|
504
|
+
if (state.phase !== "committed")
|
|
505
|
+
await activate(plan, state, dir, options);
|
|
506
|
+
if ((await loadAndVerifyResearchSetupPlan(p.setupPlan)).planSha256 !== plan.planSha256)
|
|
507
|
+
throw fail("A later generation superseded this upgrade.");
|
|
508
|
+
for (const target of plan.install.targets)
|
|
509
|
+
for (const skill of plan.skills)
|
|
510
|
+
if ((await treeHash(join(target.root, skill.skillName))) !== skill.expectedTreeSha256)
|
|
511
|
+
throw fail("Committed installation drifted.");
|
|
512
|
+
const setupState = JSON.parse((await bytes(p.setupState)).bytes.toString("utf8"));
|
|
513
|
+
if (!options.skipDoctor && !state.doctorAttempted) {
|
|
514
|
+
await appendJournalEvent(p.journal, "research.setup.upgrade.doctor-started", "workspace", {
|
|
515
|
+
planSha256: plan.planSha256,
|
|
516
|
+
});
|
|
517
|
+
state.doctorAttempted = true;
|
|
518
|
+
await save(dir, state);
|
|
519
|
+
await releaseWorkspace();
|
|
520
|
+
releaseWorkspace = null;
|
|
521
|
+
const report = await doctorResearchSetup(root, {
|
|
522
|
+
live: plan.checks.live,
|
|
523
|
+
allowSyntheticUnstructureUpload: plan.checks.allowSyntheticUnstructureUpload,
|
|
524
|
+
agentSmoke: plan.checks.agentSmoke,
|
|
525
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
526
|
+
...(options.runner ? { runner: options.runner } : {}),
|
|
527
|
+
...(options.fetcher ? { fetcher: options.fetcher } : {}),
|
|
528
|
+
...(options.sleeper ? { sleeper: options.sleeper } : {}),
|
|
529
|
+
...(options.executor ? { executor: options.executor } : {}),
|
|
530
|
+
});
|
|
531
|
+
setupState.status =
|
|
532
|
+
report.researchReadiness === "BLOCKED"
|
|
533
|
+
? "blocked"
|
|
534
|
+
: report.overallReadiness === "PARTIALLY_READY"
|
|
535
|
+
? "partially-ready"
|
|
536
|
+
: "ready";
|
|
537
|
+
setupState.completedSteps = [...new Set([...setupState.completedSteps, "doctor"])];
|
|
538
|
+
setupState.updatedAt = new Date().toISOString();
|
|
539
|
+
await writeJsonAtomic(p.setupState, setupState);
|
|
540
|
+
return { schemaVersion: 1, plan, state: setupState, report };
|
|
541
|
+
}
|
|
542
|
+
return { schemaVersion: 1, plan, state: setupState, report: null };
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
throw upgradeFailure(error, plan, state?.phase ?? "preflight");
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
try {
|
|
549
|
+
await releaseWorkspace?.();
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
await release();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
export async function rollbackResearchSetupUpgrade(candidatePath, workspace, options = {}) {
|
|
557
|
+
const plan = await loadAndVerifyResearchSetupPlan(resolve(candidatePath));
|
|
558
|
+
if (!plan.upgrade || plan.workspace.path !== resolve(workspace))
|
|
559
|
+
throw fail("Rollback must select the exact candidate and workspace.");
|
|
560
|
+
await assertResearchSetupRuntimeIntegrity(plan.upgrade.cliRuntimeSha256);
|
|
561
|
+
const root = plan.workspace.path, p = workspacePaths(root), dir = directory(root, plan.planSha256);
|
|
562
|
+
const releaseSetup = await acquireFileLock(p.setupLock, {
|
|
563
|
+
pid: process.pid,
|
|
564
|
+
operation: "research.setup.upgrade.rollback",
|
|
565
|
+
acquiredAt: new Date().toISOString(),
|
|
566
|
+
});
|
|
567
|
+
let state = null;
|
|
568
|
+
try {
|
|
569
|
+
const release = await acquireFileLock(join(p.locks, "workspace.lock"), {
|
|
570
|
+
pid: process.pid,
|
|
571
|
+
operation: "research.setup.upgrade.rollback",
|
|
572
|
+
acquiredAt: new Date().toISOString(),
|
|
573
|
+
});
|
|
574
|
+
try {
|
|
575
|
+
state = await load(dir, plan);
|
|
576
|
+
if (!state)
|
|
577
|
+
throw fail("No upgrade transition exists for this candidate.");
|
|
578
|
+
await verifyAnchors(plan, state, dir);
|
|
579
|
+
await assertIdle(root);
|
|
580
|
+
const active = files(root), markerBytes = await objectBytes(dir, state.before.marker);
|
|
581
|
+
const marker = JSON.parse(markerBytes.toString("utf8"));
|
|
582
|
+
const events = await readVerifiedJournal(p.journal);
|
|
583
|
+
if (["committed", "rolling-back"].includes(state.phase) &&
|
|
584
|
+
events
|
|
585
|
+
.slice(state.journalBoundary)
|
|
586
|
+
.some((e) => e.scope !== "workspace" && e.scope !== marker.workspaceId))
|
|
587
|
+
throw fail("New research activity prevents rollback; preserve it and use a reviewed forward upgrade.");
|
|
588
|
+
const blocked = Buffer.from(JSON.stringify({ ...marker, kind: SETUP_UPGRADING_MARKER, setupUpgradePlanSha256: plan.planSha256 }, null, 2) + "\n");
|
|
589
|
+
const actualMarker = await currentHash(active.marker);
|
|
590
|
+
if (actualMarker !== state.before.marker.sha256 && actualMarker !== sha256Bytes(blocked))
|
|
591
|
+
throw fail("Workspace marker changed after this upgrade.");
|
|
592
|
+
// Check every target before reversing even one file. Unknown owner bytes
|
|
593
|
+
// are never overwritten to make another part of the transaction succeed.
|
|
594
|
+
for (const change of state.files) {
|
|
595
|
+
const current = await currentHash(active[change.key]);
|
|
596
|
+
if (!DIAGNOSTICS.has(change.key) &&
|
|
597
|
+
current !== (change.before?.sha256 ?? null) &&
|
|
598
|
+
current !== (change.after?.sha256 ?? null))
|
|
599
|
+
throw fail("Owner changes conflict with rollback.");
|
|
600
|
+
if (change.before)
|
|
601
|
+
await objectBytes(dir, change.before);
|
|
602
|
+
}
|
|
603
|
+
for (const tree of state.trees) {
|
|
604
|
+
const loc = treePaths(plan, tree), current = await treeHash(loc.active), backup = await treeHash(loc.old), prepared = await treeHash(loc.next);
|
|
605
|
+
if (current !== tree.before && current !== tree.after && current !== null)
|
|
606
|
+
throw fail("Owner Skill changes conflict with rollback.");
|
|
607
|
+
if (current !== tree.before && tree.before !== null && backup !== tree.before)
|
|
608
|
+
throw fail("The prior Skill backup is missing or changed.");
|
|
609
|
+
if (current === tree.after && prepared !== null)
|
|
610
|
+
throw fail("Rollback staging path is occupied.");
|
|
611
|
+
}
|
|
612
|
+
if (state.phase === "rolled-back")
|
|
613
|
+
return {
|
|
614
|
+
schemaVersion: 1,
|
|
615
|
+
status: "rolled-back",
|
|
616
|
+
replayed: true,
|
|
617
|
+
planSha256: plan.planSha256,
|
|
618
|
+
parentPlanSha256: plan.upgrade.parentPlanSha256,
|
|
619
|
+
};
|
|
620
|
+
if (state.phase !== "rolling-back") {
|
|
621
|
+
const diagnostics = {};
|
|
622
|
+
for (const key of DIAGNOSTICS)
|
|
623
|
+
diagnostics[key] = await image(dir, active[key]);
|
|
624
|
+
await writeJsonAtomic(join(dir, "rollback-diagnostics.json"), {
|
|
625
|
+
planSha256: plan.planSha256,
|
|
626
|
+
files: diagnostics,
|
|
627
|
+
});
|
|
628
|
+
state.phase = "rolling-back";
|
|
629
|
+
await save(dir, state);
|
|
630
|
+
await appendJournalEvent(p.journal, "research.setup.upgrade.rollback-started", "workspace", { planSha256: plan.planSha256 });
|
|
631
|
+
}
|
|
632
|
+
await writeBytesAtomic(active.marker, blocked, state.before.marker.mode);
|
|
633
|
+
for (const change of [...state.files].reverse()) {
|
|
634
|
+
if ((await currentHash(active[change.key])) === (change.before?.sha256 ?? null))
|
|
635
|
+
continue;
|
|
636
|
+
await installFile(dir, active[change.key], change.before);
|
|
637
|
+
await options.checkpoint?.(`after-rollback-file:${change.key}`);
|
|
638
|
+
}
|
|
639
|
+
for (const tree of [...state.trees].reverse()) {
|
|
640
|
+
const loc = treePaths(plan, tree), current = await treeHash(loc.active);
|
|
641
|
+
if (current === tree.before)
|
|
642
|
+
continue;
|
|
643
|
+
if (current === tree.after) {
|
|
644
|
+
await mkdir(dirname(loc.next), { recursive: true, mode: 0o700 });
|
|
645
|
+
await rename(loc.active, loc.next);
|
|
646
|
+
}
|
|
647
|
+
if (tree.before !== null)
|
|
648
|
+
await rename(loc.old, loc.active);
|
|
649
|
+
}
|
|
650
|
+
await installFile(dir, active.marker, state.before.marker);
|
|
651
|
+
state.phase = "rolled-back";
|
|
652
|
+
await save(dir, state);
|
|
653
|
+
await appendJournalEvent(p.journal, "research.setup.upgrade.rolled-back", "workspace", {
|
|
654
|
+
planSha256: plan.planSha256,
|
|
655
|
+
parentPlanSha256: plan.upgrade.parentPlanSha256,
|
|
656
|
+
});
|
|
657
|
+
return {
|
|
658
|
+
schemaVersion: 1,
|
|
659
|
+
status: "rolled-back",
|
|
660
|
+
planSha256: plan.planSha256,
|
|
661
|
+
parentPlanSha256: plan.upgrade.parentPlanSha256,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
await release();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
throw upgradeFailure(error, plan, state?.phase ?? "rollback");
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
await releaseSetup();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function definitionHash(state) {
|
|
676
|
+
return sha256Bytes(Buffer.from(canonicalJson({
|
|
677
|
+
before: state.before,
|
|
678
|
+
files: state.files,
|
|
679
|
+
trees: state.trees,
|
|
680
|
+
stagePlanSha256: state.stagePlanSha256,
|
|
681
|
+
})));
|
|
682
|
+
}
|
|
683
|
+
async function verifyAnchors(plan, state, dir) {
|
|
684
|
+
const binding = plan.upgrade;
|
|
685
|
+
if (state.before.runtime?.sha256 !== binding.parentRuntimeLockSha256 ||
|
|
686
|
+
state.before.config?.sha256 !== binding.parentConfigSha256 ||
|
|
687
|
+
state.before.marker?.sha256 !== binding.parentMarkerSha256 ||
|
|
688
|
+
!state.before.plan)
|
|
689
|
+
throw fail("Upgrade preimages do not bind their approved parent.");
|
|
690
|
+
const parent = await loadHashVerifiedResearchSetupPlan(join(dir, "objects", state.before.plan.sha256));
|
|
691
|
+
if (parent.planSha256 !== binding.parentPlanSha256 ||
|
|
692
|
+
parent.workspace.path !== plan.workspace.path)
|
|
693
|
+
throw fail("Upgrade parent identity changed.");
|
|
694
|
+
const treeKeys = new Set();
|
|
695
|
+
for (const tree of state.trees) {
|
|
696
|
+
const key = tree.agent + ":" + tree.skillId;
|
|
697
|
+
if (treeKeys.has(key) ||
|
|
698
|
+
!plan.install.targets.some((t) => t.agent === tree.agent) ||
|
|
699
|
+
tree.after !== plan.skills.find((s) => s.id === tree.skillId)?.expectedTreeSha256 ||
|
|
700
|
+
(tree.before !== null &&
|
|
701
|
+
tree.before !== parent.skills.find((s) => s.id === tree.skillId)?.expectedTreeSha256))
|
|
702
|
+
throw fail("Upgrade tree intents differ from the reviewed generations.");
|
|
703
|
+
treeKeys.add(key);
|
|
704
|
+
}
|
|
705
|
+
const path = workspacePaths(plan.workspace.path).journal;
|
|
706
|
+
let events = await readVerifiedJournal(path);
|
|
707
|
+
const matching = (type) => events.filter((e) => e.scope === "workspace" && e.type === type && e.payload.planSha256 === plan.planSha256);
|
|
708
|
+
const beforeHash = sha256Bytes(Buffer.from(canonicalJson({ before: state.before, trees: state.trees })));
|
|
709
|
+
let starts = matching("research.setup.upgrade.started");
|
|
710
|
+
if (!starts.length) {
|
|
711
|
+
if (state.phase !== "preparing" || state.initialized || state.files.length)
|
|
712
|
+
throw fail("Upgrade has no authoritative start record.");
|
|
713
|
+
await assertInputs(plan, state);
|
|
714
|
+
await appendJournalEvent(path, "research.setup.upgrade.started", "workspace", {
|
|
715
|
+
planSha256: plan.planSha256,
|
|
716
|
+
beforeSha256: beforeHash,
|
|
717
|
+
});
|
|
718
|
+
events = await readVerifiedJournal(path);
|
|
719
|
+
starts = matching("research.setup.upgrade.started");
|
|
720
|
+
}
|
|
721
|
+
if (starts.length !== 1 || starts[0].payload.beforeSha256 !== beforeHash)
|
|
722
|
+
throw fail("Upgrade state differs from its journal-bound preimages.");
|
|
723
|
+
state.journalBoundary = events.indexOf(starts[0]) + 1;
|
|
724
|
+
const prepared = matching("research.setup.upgrade.prepared");
|
|
725
|
+
if (prepared.length) {
|
|
726
|
+
const keys = Object.keys(files(plan.workspace.path)).filter((k) => k !== "marker");
|
|
727
|
+
if (prepared.length !== 1 ||
|
|
728
|
+
prepared[0].payload.definitionSha256 !== definitionHash(state) ||
|
|
729
|
+
state.files.length !== keys.length ||
|
|
730
|
+
new Set(state.files.map((f) => f.key)).size !== keys.length ||
|
|
731
|
+
state.files.some((f) => canonicalJson(f.before) !== canonicalJson(state.before[f.key])))
|
|
732
|
+
throw fail("Upgrade definitions differ from their journal-bound preparation.");
|
|
733
|
+
if (state.phase === "preparing") {
|
|
734
|
+
state.phase = "prepared";
|
|
735
|
+
await save(dir, state);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else if (state.phase === "prepared") {
|
|
739
|
+
// A kill between private state and journal append preceded all active writes.
|
|
740
|
+
state.phase = "preparing";
|
|
741
|
+
await save(dir, state);
|
|
742
|
+
}
|
|
743
|
+
else if (state.phase !== "preparing")
|
|
744
|
+
throw fail("Upgrade has no authoritative preparation record.");
|
|
745
|
+
state.doctorAttempted = matching("research.setup.upgrade.doctor-started").length > 0;
|
|
746
|
+
}
|
|
747
|
+
function upgradeFailure(error, plan, phase) {
|
|
748
|
+
const code = error instanceof CliError && /^[A-Z0-9_]{1,128}$/.test(error.code)
|
|
749
|
+
? error.code
|
|
750
|
+
: "RESEARCH_SETUP_UPGRADE_INTERRUPTED";
|
|
751
|
+
const planPath = researchSetupUpgradeCandidatePath(plan);
|
|
752
|
+
const legacyMismatch = code === "RESEARCH_SETUP_LEGACY_UPGRADE_RECOVERY_REQUIRED";
|
|
753
|
+
return new CliError("Managed setup upgrade did not complete. Preserve its candidate and use the recorded recovery action.", {
|
|
754
|
+
code,
|
|
755
|
+
exitCode: error instanceof CliError ? error.exitCode : 3,
|
|
756
|
+
details: {
|
|
757
|
+
step: "upgrade-" + phase,
|
|
758
|
+
planSha256: plan.planSha256,
|
|
759
|
+
planPath,
|
|
760
|
+
retryCommand: legacyMismatch
|
|
761
|
+
? null
|
|
762
|
+
: researchSetupApplyCommand({ version: plan.cli.version, planPath }),
|
|
763
|
+
rollbackCommand: legacyMismatch
|
|
764
|
+
? null
|
|
765
|
+
: exactResearchCliCommand([
|
|
766
|
+
"research",
|
|
767
|
+
"setup",
|
|
768
|
+
"upgrade",
|
|
769
|
+
"--rollback",
|
|
770
|
+
"--candidate",
|
|
771
|
+
planPath,
|
|
772
|
+
"--workspace",
|
|
773
|
+
plan.workspace.path,
|
|
774
|
+
"--json",
|
|
775
|
+
], plan.cli.version),
|
|
776
|
+
minimumAction: legacyMismatch
|
|
777
|
+
? "The candidate's hash-bound active plan and runtime lock describe different generations. No active controls were changed. Preserve these exact files and the journal-linked predecessor for verified legacy recovery; repeating apply or rolling back an unstarted transition cannot repair this mismatch. Do not rewrite locks or delete installed trees."
|
|
778
|
+
: phase === "rolling-back"
|
|
779
|
+
? "Repeat the exact rollback command; do not apply a partly reversed candidate."
|
|
780
|
+
: "Inspect the candidate's staged setup diagnostics, then retry its apply or explicitly roll it back. Do not edit control files.",
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
//# sourceMappingURL=setup-upgrade.js.map
|