@lamentis/naome 1.2.0 → 1.3.0
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/Cargo.lock +2 -2
- package/README.md +108 -47
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +34 -5
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +7 -2
- package/crates/naome-cli/src/main.rs +37 -22
- package/crates/naome-cli/src/quality_commands.rs +317 -10
- package/crates/naome-cli/src/workflow_commands.rs +21 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision/checks.rs +64 -0
- package/crates/naome-core/src/decision/idle.rs +67 -0
- package/crates/naome-core/src/decision/json.rs +36 -0
- package/crates/naome-core/src/decision/states.rs +165 -0
- package/crates/naome-core/src/decision.rs +131 -353
- package/crates/naome-core/src/git.rs +4 -2
- package/crates/naome-core/src/install_plan.rs +4 -0
- package/crates/naome-core/src/lib.rs +12 -6
- package/crates/naome-core/src/paths.rs +3 -1
- package/crates/naome-core/src/quality/adapter_support.rs +89 -0
- package/crates/naome-core/src/quality/adapters.rs +20 -67
- package/crates/naome-core/src/quality/baseline.rs +8 -0
- package/crates/naome-core/src/quality/cache.rs +153 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
- package/crates/naome-core/src/quality/checks.rs +7 -8
- package/crates/naome-core/src/quality/cleanup.rs +48 -3
- package/crates/naome-core/src/quality/config.rs +8 -15
- package/crates/naome-core/src/quality/config_support.rs +24 -0
- package/crates/naome-core/src/quality/mod.rs +72 -6
- package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
- package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
- package/crates/naome-core/src/quality/scanner.rs +200 -215
- package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
- package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
- package/crates/naome-core/src/quality/semantic/model.rs +85 -0
- package/crates/naome-core/src/quality/semantic/route.rs +52 -0
- package/crates/naome-core/src/quality/semantic.rs +68 -0
- package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
- package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
- package/crates/naome-core/src/quality/structure/checks/directory.rs +134 -0
- package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
- package/crates/naome-core/src/quality/structure/checks.rs +124 -0
- package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
- package/crates/naome-core/src/quality/structure/classify.rs +146 -0
- package/crates/naome-core/src/quality/structure/config.rs +89 -0
- package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
- package/crates/naome-core/src/quality/structure/mod.rs +77 -0
- package/crates/naome-core/src/quality/structure/model.rs +131 -0
- package/crates/naome-core/src/quality/types.rs +43 -2
- package/crates/naome-core/src/route/builtin_checks.rs +141 -0
- package/crates/naome-core/src/route/builtin_context.rs +73 -0
- package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
- package/crates/naome-core/src/route/builtin_require.rs +40 -0
- package/crates/naome-core/src/route/context.rs +180 -0
- package/crates/naome-core/src/route/execution.rs +96 -0
- package/crates/naome-core/src/route/execution_baselines.rs +146 -0
- package/crates/naome-core/src/route/execution_support.rs +57 -0
- package/crates/naome-core/src/route/execution_tasks.rs +71 -0
- package/crates/naome-core/src/route/git_ops.rs +72 -0
- package/crates/naome-core/src/route/quality_gate.rs +73 -0
- package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
- package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
- package/crates/naome-core/src/route/worktree.rs +75 -0
- package/crates/naome-core/src/route/worktree_files.rs +32 -0
- package/crates/naome-core/src/route/worktree_plan.rs +131 -0
- package/crates/naome-core/src/route.rs +44 -1217
- package/crates/naome-core/src/verification.rs +1 -0
- package/crates/naome-core/src/workflow/doctor.rs +144 -0
- package/crates/naome-core/src/workflow/mod.rs +2 -0
- package/crates/naome-core/src/workflow/mutation.rs +1 -2
- package/crates/naome-core/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +2 -0
- package/crates/naome-core/tests/install_plan.rs +2 -0
- package/crates/naome-core/tests/quality.rs +26 -123
- package/crates/naome-core/tests/quality_performance.rs +231 -0
- package/crates/naome-core/tests/quality_structure.rs +116 -0
- package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +144 -0
- package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
- package/crates/naome-core/tests/repo_support/mod.rs +16 -0
- package/crates/naome-core/tests/repo_support/repo.rs +113 -0
- package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
- package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
- package/crates/naome-core/tests/repo_support/routes.rs +81 -0
- package/crates/naome-core/tests/repo_support/verification.rs +168 -0
- package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
- package/crates/naome-core/tests/route.rs +1 -1376
- package/crates/naome-core/tests/route_baseline.rs +86 -0
- package/crates/naome-core/tests/route_completion.rs +141 -0
- package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
- package/crates/naome-core/tests/route_user_diff.rs +202 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -0
- package/crates/naome-core/tests/semantic_legacy.rs +140 -0
- package/crates/naome-core/tests/task_state.rs +60 -432
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
- package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
- package/crates/naome-core/tests/task_state_support/states.rs +84 -0
- package/crates/naome-core/tests/verification.rs +4 -45
- package/crates/naome-core/tests/verification_contract.rs +22 -78
- package/crates/naome-core/tests/workflow_doctor.rs +24 -0
- package/crates/naome-core/tests/workflow_policy.rs +6 -1
- package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
- package/installer/agents.js +90 -0
- package/installer/context.js +67 -0
- package/installer/filesystem.js +166 -0
- package/installer/flows.js +84 -0
- package/installer/git-boundary.js +171 -0
- package/installer/git-hook-content.js +36 -0
- package/installer/git-hooks.js +134 -0
- package/installer/git-local.js +2 -0
- package/installer/git-shared.js +35 -0
- package/installer/harness-file-ops.js +140 -0
- package/installer/harness-files.js +56 -0
- package/installer/harness-verification.js +123 -0
- package/installer/install-plan.js +66 -0
- package/installer/main.js +25 -0
- package/installer/manifest-state.js +167 -0
- package/installer/native-build.js +24 -0
- package/installer/native-format.js +6 -0
- package/installer/native.js +162 -0
- package/installer/output.js +131 -0
- package/installer/version.js +32 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
- package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
- package/templates/naome-root/.naome/bin/naome.js +32 -21
- package/templates/naome-root/.naome/manifest.json +5 -3
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/verification.json +1 -0
- package/templates/naome-root/.naomeignore +1 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +66 -4
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- package/templates/naome-root/docs/naome/testing.md +2 -1
package/bin/naome-node.js
CHANGED
|
@@ -1,1582 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { runNaomeNodeCli } from "../installer/main.js";
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
const templateRoot = join(packageRoot, "templates", "naome-root");
|
|
13
|
-
const targetRoot = process.cwd();
|
|
14
|
-
const packageVersion = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version;
|
|
15
|
-
const installed = [];
|
|
16
|
-
const archived = [];
|
|
17
|
-
const updated = [];
|
|
18
|
-
const skipped = [];
|
|
19
|
-
const unsafeSkipped = [];
|
|
20
|
-
const useColor = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
21
|
-
const verboseOutput = process.argv.includes("--verbose");
|
|
22
|
-
const healthCheckerRelativePath = ".naome/bin/check-harness-health.js";
|
|
23
|
-
const taskStateCheckerRelativePath = ".naome/bin/check-task-state.js";
|
|
24
|
-
const naomeCommandRelativePath = ".naome/bin/naome.js";
|
|
25
|
-
const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
|
|
26
|
-
const nativeBinaryRelativePath = process.platform === "win32" ? ".naome/bin/naome-rust.exe" : ".naome/bin/naome-rust";
|
|
27
|
-
const minimumSupportedUpgradeVersion = "0.6.1";
|
|
28
|
-
const integrityBlockPattern = /^const expectedMachineOwnedIntegrity = Object\.freeze\(\{[\s\S]*?\n\}\);\n/m;
|
|
29
|
-
const normalizedIntegrityBlock = "const expectedMachineOwnedIntegrity = Object.freeze({\n __generated__: \"sha256:generated\"\n});\n";
|
|
30
|
-
const nativeIntegrityPattern = /^const expectedNativeBinaryIntegrity = "(?:sha256:[a-f0-9]{64}|sha256:generated)";\n/m;
|
|
31
|
-
const normalizedNativeIntegrity = 'const expectedNativeBinaryIntegrity = "sha256:generated";\n';
|
|
32
|
-
|
|
33
|
-
let installPlan = null;
|
|
34
|
-
let machineOwnedPaths = [];
|
|
35
|
-
let projectOwnedPaths = [];
|
|
36
|
-
let localOnlyMachineOwnedPaths = [];
|
|
37
|
-
let localOnlyGitIgnoreEntries = [];
|
|
38
|
-
let localOnlyGitExcludeEntries = [];
|
|
39
|
-
let localOnlyGitUntrackPaths = [];
|
|
40
|
-
|
|
41
|
-
const executableMachineOwnedPaths = new Set([
|
|
42
|
-
".naome/bin/naome.js",
|
|
43
|
-
".naome/bin/check-task-state.js",
|
|
44
|
-
".naome/bin/check-harness-health.js",
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
const color = {
|
|
48
|
-
bold: (value) => format(value, "1"),
|
|
49
|
-
dim: (value) => format(value, "2"),
|
|
50
|
-
green: (value) => format(value, "32"),
|
|
51
|
-
yellow: (value) => format(value, "33"),
|
|
52
|
-
red: (value) => format(value, "31"),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const firstRunPrompt = `Run the NAOME first-run protocol for this repository.
|
|
56
|
-
|
|
57
|
-
Start by reading .naomeignore, then docs/naome/index.md, then .naome/init-state.json.
|
|
58
|
-
Then read .naome/upgrade-state.json, .naome/task-state.json, and docs/naome/execution.md.
|
|
59
|
-
Run node .naome/bin/check-harness-health.js before feature work.
|
|
60
|
-
If initialized is false, follow docs/naome/first-run.md.
|
|
61
|
-
Do not begin feature work until NAOME intake is complete.
|
|
62
|
-
After intake, save the user's next natural-language request to a temporary prompt file, then run node .naome/bin/naome.js route --prompt-file <path> --execute --json.
|
|
63
|
-
Follow userMessage, humanOptions, requiredContext, and canCreateTask from the route output before creating the requested task.`;
|
|
64
|
-
|
|
65
|
-
let summaryTitle = "NAOME intake harness installed";
|
|
66
|
-
let nextStepPrompt = firstRunPrompt;
|
|
67
|
-
|
|
68
|
-
function format(value, code) {
|
|
69
|
-
return useColor ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function walk(dir) {
|
|
73
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
74
|
-
const files = [];
|
|
75
|
-
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const fullPath = join(dir, entry.name);
|
|
78
|
-
if (entry.isDirectory()) {
|
|
79
|
-
files.push(...walk(fullPath));
|
|
80
|
-
} else if (entry.isFile()) {
|
|
81
|
-
files.push(fullPath);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return files;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function copyTemplateFile(sourcePath) {
|
|
89
|
-
const relativePath = relative(templateRoot, sourcePath);
|
|
90
|
-
const targetPath = join(targetRoot, relativePath);
|
|
91
|
-
|
|
92
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
93
|
-
skipped.push(relativePath);
|
|
94
|
-
unsafeSkipped.push(relativePath);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (existsSync(targetPath)) {
|
|
99
|
-
skipped.push(relativePath);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
104
|
-
copyFileSync(sourcePath, targetPath);
|
|
105
|
-
installed.push(relativePath);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function ensureExecutableMachineOwnedFiles() {
|
|
109
|
-
for (const relativePath of executableMachineOwnedPaths) {
|
|
110
|
-
const targetPath = join(targetRoot, relativePath);
|
|
111
|
-
if (!existsSync(targetPath) || hasSymlinkInTargetPath(relativePath)) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
chmodSync(targetPath, 0o755);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function configureGitHooks() {
|
|
120
|
-
const gitCheck = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
121
|
-
cwd: targetRoot,
|
|
122
|
-
encoding: "utf8",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (gitCheck.status !== 0 || gitCheck.stdout.trim() !== "true") {
|
|
126
|
-
skipped.push("local git hooks");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const currentHooksPath = spawnSync("git", ["config", "--local", "--get", "core.hooksPath"], {
|
|
131
|
-
cwd: targetRoot,
|
|
132
|
-
encoding: "utf8",
|
|
133
|
-
});
|
|
134
|
-
const currentValue = currentHooksPath.status === 0 ? currentHooksPath.stdout.trim() : "";
|
|
135
|
-
|
|
136
|
-
if (currentValue && currentValue !== ".naome/git-hooks") {
|
|
137
|
-
skipped.push(`git core.hooksPath (${currentValue})`);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (currentValue === ".naome/git-hooks") {
|
|
142
|
-
const unsetResult = spawnSync("git", ["config", "--local", "--unset", "core.hooksPath"], {
|
|
143
|
-
cwd: targetRoot,
|
|
144
|
-
encoding: "utf8",
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (unsetResult.status !== 0) {
|
|
148
|
-
skipped.push("git core.hooksPath unset");
|
|
149
|
-
unsafeSkipped.push("git core.hooksPath unset");
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
updated.push("git core.hooksPath unset");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const gitDirResult = spawnSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
157
|
-
cwd: targetRoot,
|
|
158
|
-
encoding: "utf8",
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (gitDirResult.status !== 0 || !gitDirResult.stdout.trim()) {
|
|
162
|
-
skipped.push("local git hooks");
|
|
163
|
-
unsafeSkipped.push("local git hooks");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const gitDir = gitDirResult.stdout.trim();
|
|
168
|
-
const hooksDir = join(gitDir, "hooks");
|
|
169
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
170
|
-
|
|
171
|
-
const localNative = installLocalGitNativeBinary(gitDir);
|
|
172
|
-
if (!localNative) {
|
|
173
|
-
skipped.push("local git hooks native binary");
|
|
174
|
-
unsafeSkipped.push("local git hooks native binary");
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
installLocalGitHook(hooksDir, "pre-commit", "--commit-gate", localNative);
|
|
179
|
-
installLocalGitHook(hooksDir, "pre-push", "--push-gate", localNative);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function installLocalGitNativeBinary(gitDir) {
|
|
183
|
-
const sourcePath = findNativeDecisionBinary();
|
|
184
|
-
if (!sourcePath) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const localDir = join(gitDir, "naome");
|
|
189
|
-
const localPath = join(localDir, process.platform === "win32" ? "naome-rust.exe" : "naome-rust");
|
|
190
|
-
const sourceContent = readFileSync(sourcePath);
|
|
191
|
-
const sourceHash = sha256(sourceContent);
|
|
192
|
-
mkdirSync(localDir, { recursive: true });
|
|
193
|
-
|
|
194
|
-
const currentHash = existsSync(localPath) && lstatSync(localPath).isFile() ? sha256(readFileSync(localPath)) : null;
|
|
195
|
-
if (currentHash !== sourceHash) {
|
|
196
|
-
copyFileSync(sourcePath, localPath);
|
|
197
|
-
updated.push(".git/naome/naome-rust");
|
|
198
|
-
} else {
|
|
199
|
-
skipped.push(".git/naome/naome-rust");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
chmodSync(localPath, 0o755);
|
|
203
|
-
return {
|
|
204
|
-
path: localPath,
|
|
205
|
-
hash: sourceHash,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function installLocalGitHook(hooksDir, hookName, mode, localNative) {
|
|
210
|
-
const hookPath = join(hooksDir, hookName);
|
|
211
|
-
const nextContent = localGitHookContent(hookName, mode, localNative);
|
|
212
|
-
|
|
213
|
-
if (existsSync(hookPath)) {
|
|
214
|
-
const stats = lstatSync(hookPath);
|
|
215
|
-
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
216
|
-
skipped.push(`.git/hooks/${hookName}`);
|
|
217
|
-
unsafeSkipped.push(`.git/hooks/${hookName}`);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const currentContent = readFileSync(hookPath, "utf8");
|
|
222
|
-
if (!currentContent.includes("NAOME managed local git hook")) {
|
|
223
|
-
skipped.push(`.git/hooks/${hookName}`);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (currentContent === nextContent) {
|
|
228
|
-
skipped.push(`.git/hooks/${hookName}`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
writeFileSync(hookPath, nextContent);
|
|
234
|
-
chmodSync(hookPath, 0o755);
|
|
235
|
-
updated.push(`.git/hooks/${hookName}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function localGitHookContent(hookName, mode, localNative) {
|
|
239
|
-
const expectedIntegrityJson = JSON.stringify(templateIntegrity());
|
|
240
|
-
return `#!/bin/sh
|
|
241
|
-
# NAOME managed local git hook: ${hookName}
|
|
242
|
-
# Run naome sync after reviewing harness changes to refresh this hook.
|
|
243
|
-
set -eu
|
|
244
|
-
|
|
245
|
-
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
246
|
-
cd "$REPO_ROOT"
|
|
247
|
-
|
|
248
|
-
NODE_BIN=${shellSingleQuote(process.execPath)}
|
|
249
|
-
if [ ! -x "$NODE_BIN" ]; then
|
|
250
|
-
NODE_BIN="node"
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
|
-
NATIVE_BIN=${shellSingleQuote(localNative.path)}
|
|
254
|
-
EXPECTED_SHA=${shellSingleQuote(localNative.hash)}
|
|
255
|
-
NAOME_EXPECTED_INTEGRITY_JSON=${shellSingleQuote(expectedIntegrityJson)}
|
|
256
|
-
export NAOME_EXPECTED_INTEGRITY_JSON
|
|
257
|
-
ACTUAL_SHA="$("$NODE_BIN" -e 'const fs = require("node:fs"); const crypto = require("node:crypto"); const file = process.argv[1]; process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"));' "$NATIVE_BIN" 2>/dev/null || true)"
|
|
258
|
-
|
|
259
|
-
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
|
|
260
|
-
echo "NAOME hook refused to run because its local native binary integrity does not match the installed hook." >&2
|
|
261
|
-
echo "Run naome sync after reviewing harness changes." >&2
|
|
262
|
-
exit 1
|
|
263
|
-
fi
|
|
264
|
-
|
|
265
|
-
exec "$NATIVE_BIN" check-task-state --root "$REPO_ROOT" ${mode}
|
|
266
|
-
`;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function shellSingleQuote(value) {
|
|
270
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function installNativeDecisionBinary() {
|
|
274
|
-
const targetPath = join(targetRoot, nativeBinaryRelativePath);
|
|
275
|
-
|
|
276
|
-
if (hasSymlinkInTargetPath(nativeBinaryRelativePath)) {
|
|
277
|
-
skipped.push(nativeBinaryRelativePath);
|
|
278
|
-
unsafeSkipped.push(nativeBinaryRelativePath);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const sourcePath = findNativeDecisionBinary();
|
|
283
|
-
if (!sourcePath) {
|
|
284
|
-
printError("NAOME native decision binary is unavailable.");
|
|
285
|
-
console.error("Install Cargo or provide NAOME_NATIVE_BIN with a built naome binary, then rerun naome sync.");
|
|
286
|
-
process.exit(1);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (usesSourceNativeFallback()) {
|
|
290
|
-
patchNaomeCommandNativeIntegrity("sha256:generated");
|
|
291
|
-
skipped.push(nativeBinaryRelativePath);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
296
|
-
const sourceHash = sha256(readFileSync(sourcePath));
|
|
297
|
-
const targetHash = existsSync(targetPath) && lstatSync(targetPath).isFile() ? sha256(readFileSync(targetPath)) : null;
|
|
298
|
-
|
|
299
|
-
if (sourceHash !== targetHash) {
|
|
300
|
-
copyFileSync(sourcePath, targetPath);
|
|
301
|
-
updated.push(nativeBinaryRelativePath);
|
|
302
|
-
} else {
|
|
303
|
-
skipped.push(nativeBinaryRelativePath);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
chmodSync(targetPath, 0o755);
|
|
307
|
-
patchNaomeCommandNativeIntegrity(`sha256:${sourceHash}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function findNativeDecisionBinary() {
|
|
311
|
-
const candidates = [];
|
|
312
|
-
|
|
313
|
-
if (process.env.NAOME_NATIVE_BIN) {
|
|
314
|
-
candidates.push(resolve(targetRoot, process.env.NAOME_NATIVE_BIN));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
candidates.push(join(packageRoot, "native", `${process.platform}-${process.arch}`, nativeBinaryName));
|
|
318
|
-
candidates.push(join(packageRoot, "target", "release", nativeBinaryName));
|
|
319
|
-
|
|
320
|
-
for (const candidate of candidates) {
|
|
321
|
-
if (existsSync(candidate) && lstatSync(candidate).isFile()) {
|
|
322
|
-
return candidate;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return buildNativeDecisionBinary();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function loadInstallPlan() {
|
|
330
|
-
if (installPlan) {
|
|
331
|
-
return installPlan;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const nativePath = findNativeDecisionBinary();
|
|
335
|
-
if (!nativePath) {
|
|
336
|
-
printError("NAOME native installer policy is unavailable.");
|
|
337
|
-
console.error("Install Cargo or provide NAOME_NATIVE_BIN with a built naome binary, then rerun naome sync.");
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const result = spawnSync(nativePath, ["install-plan", "--harness-version", packageVersion], {
|
|
342
|
-
cwd: targetRoot,
|
|
343
|
-
encoding: "utf8",
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
if (result.status !== 0) {
|
|
347
|
-
printError("NAOME could not load its Rust install plan.");
|
|
348
|
-
if (result.stdout.trim()) {
|
|
349
|
-
console.error(result.stdout.trim());
|
|
350
|
-
}
|
|
351
|
-
if (result.stderr.trim()) {
|
|
352
|
-
console.error(result.stderr.trim());
|
|
353
|
-
}
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
installPlan = JSON.parse(result.stdout);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
printError("NAOME Rust install plan is not valid JSON.");
|
|
361
|
-
console.error(error.message);
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
assertInstallPlanArray("machineOwned");
|
|
366
|
-
assertInstallPlanArray("projectOwned");
|
|
367
|
-
assertInstallPlanArray("localOnlyMachineOwned");
|
|
368
|
-
assertInstallPlanArray("gitignoreEntries");
|
|
369
|
-
assertInstallPlanArray("gitExcludeEntries");
|
|
370
|
-
assertInstallPlanArray("gitUntrackPaths");
|
|
371
|
-
|
|
372
|
-
machineOwnedPaths = installPlan.machineOwned;
|
|
373
|
-
projectOwnedPaths = installPlan.projectOwned;
|
|
374
|
-
localOnlyMachineOwnedPaths = installPlan.localOnlyMachineOwned;
|
|
375
|
-
localOnlyGitIgnoreEntries = installPlan.gitignoreEntries;
|
|
376
|
-
localOnlyGitExcludeEntries = installPlan.gitExcludeEntries;
|
|
377
|
-
localOnlyGitUntrackPaths = installPlan.gitUntrackPaths;
|
|
378
|
-
|
|
379
|
-
return installPlan;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function assertInstallPlanArray(key) {
|
|
383
|
-
if (!Array.isArray(installPlan?.[key]) || installPlan[key].some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
384
|
-
printError(`NAOME Rust install plan has invalid ${key}.`);
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function buildNativeDecisionBinary() {
|
|
390
|
-
const manifestPath = join(packageRoot, "Cargo.toml");
|
|
391
|
-
if (!existsSync(manifestPath)) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const result = spawnSync("cargo", ["build", "--release", "--manifest-path", manifestPath, "-p", "naome-cli"], {
|
|
396
|
-
cwd: packageRoot,
|
|
397
|
-
encoding: "utf8",
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
if (result.status !== 0) {
|
|
401
|
-
skipped.push("native decision binary build");
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const builtPath = join(packageRoot, "target", "release", nativeBinaryName);
|
|
406
|
-
return existsSync(builtPath) && lstatSync(builtPath).isFile() ? builtPath : null;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function patchNaomeCommandNativeIntegrity(expectedIntegrity) {
|
|
410
|
-
const commandPath = join(targetRoot, naomeCommandRelativePath);
|
|
411
|
-
if (!existsSync(commandPath) || hasSymlinkInTargetPath(naomeCommandRelativePath)) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const content = readFileSync(commandPath, "utf8");
|
|
416
|
-
const nextContent = content.replace(
|
|
417
|
-
nativeIntegrityPattern,
|
|
418
|
-
`const expectedNativeBinaryIntegrity = "${expectedIntegrity}";\n`
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
if (nextContent !== content) {
|
|
422
|
-
writeFileSync(commandPath, nextContent);
|
|
423
|
-
updated.push(naomeCommandRelativePath);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function patchInstalledMachineOwnedIntegrity() {
|
|
428
|
-
const integrityBlock = formatExpectedIntegrityBlock(templateIntegrity());
|
|
429
|
-
|
|
430
|
-
for (const relativePath of [healthCheckerRelativePath, taskStateCheckerRelativePath]) {
|
|
431
|
-
const targetPath = join(targetRoot, relativePath);
|
|
432
|
-
if (!existsSync(targetPath) || hasSymlinkInTargetPath(relativePath)) {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const content = readFileSync(targetPath, "utf8");
|
|
437
|
-
const nextContent = content.replace(integrityBlockPattern, integrityBlock);
|
|
438
|
-
if (nextContent !== content) {
|
|
439
|
-
writeFileSync(targetPath, nextContent);
|
|
440
|
-
updated.push(relativePath);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function formatExpectedIntegrityBlock(integrity) {
|
|
446
|
-
const entries = Object.entries(integrity)
|
|
447
|
-
.map(([relativePath, expectedHash]) => ` ${JSON.stringify(relativePath)}: ${JSON.stringify(expectedHash)}`)
|
|
448
|
-
.join(",\n");
|
|
449
|
-
return `const expectedMachineOwnedIntegrity = Object.freeze({\n${entries}\n});\n`;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function usesSourceNativeFallback() {
|
|
453
|
-
return existsSync(join(targetRoot, "packages", "naome", "Cargo.toml"))
|
|
454
|
-
&& existsSync(join(targetRoot, "packages", "naome", "crates", "naome-cli", "src", "main.rs"));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function patchManifestDate() {
|
|
458
|
-
const manifestPath = join(targetRoot, ".naome", "manifest.json");
|
|
459
|
-
if (
|
|
460
|
-
!existsSync(manifestPath) ||
|
|
461
|
-
skipped.includes(".naome/manifest.json") ||
|
|
462
|
-
hasSymlinkInTargetPath(".naome/manifest.json")
|
|
463
|
-
) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
468
|
-
applyManifestHealthMetadata(manifest);
|
|
469
|
-
manifest.installedAt = new Date().toISOString();
|
|
470
|
-
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function readExistingInstall() {
|
|
474
|
-
const manifestPath = join(targetRoot, ".naome", "manifest.json");
|
|
475
|
-
if (!existsSync(manifestPath)) {
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (hasSymlinkInTargetPath(".naome/manifest.json")) {
|
|
480
|
-
printError("NAOME cannot read .naome/manifest.json safely.");
|
|
481
|
-
console.error(".naome/manifest.json must be a regular file.");
|
|
482
|
-
process.exit(1);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
let manifest;
|
|
486
|
-
try {
|
|
487
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
488
|
-
} catch (error) {
|
|
489
|
-
printError("NAOME manifest is not valid JSON.");
|
|
490
|
-
console.error(error.message);
|
|
491
|
-
process.exit(1);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (!isVersion(manifest.harnessVersion)) {
|
|
495
|
-
printError("NAOME manifest has no valid harnessVersion.");
|
|
496
|
-
console.error(".naome/manifest.json must contain a semver harnessVersion.");
|
|
497
|
-
process.exit(1);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
manifest,
|
|
502
|
-
version: manifest.harnessVersion,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async function runFreshInstall() {
|
|
507
|
-
await confirmAgentsTakeover();
|
|
508
|
-
|
|
509
|
-
for (const sourcePath of walk(templateRoot)) {
|
|
510
|
-
copyTemplateFile(sourcePath);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
installNativeDecisionBinary();
|
|
514
|
-
patchInstalledMachineOwnedIntegrity();
|
|
515
|
-
ensureBuiltInVerificationChecks();
|
|
516
|
-
patchManifestDate();
|
|
517
|
-
ensureCompleteUpgradeState(null);
|
|
518
|
-
ensureArchiveDirectory();
|
|
519
|
-
takeoverExistingAgents();
|
|
520
|
-
ensureLocalOnlySourceControlBoundary();
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function runExistingInstall(existingInstall) {
|
|
524
|
-
const comparison = compareVersions(existingInstall.version, packageVersion);
|
|
525
|
-
|
|
526
|
-
if (comparison > 0) {
|
|
527
|
-
printError("This repository already uses a newer NAOME harness.");
|
|
528
|
-
console.error(`Installed: v${existingInstall.version}`);
|
|
529
|
-
console.error(`Package: v${packageVersion}`);
|
|
530
|
-
console.error("Run `naome update`, then rerun `naome sync` from the repository root.");
|
|
531
|
-
process.exit(1);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (comparison < 0) {
|
|
535
|
-
if (compareVersions(existingInstall.version, minimumSupportedUpgradeVersion) < 0) {
|
|
536
|
-
rejectUnsupportedHistoricalInstall(existingInstall.version);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
ensureArchiveDirectory();
|
|
540
|
-
runRepair(existingInstall.version, { fromVersion: existingInstall.version });
|
|
541
|
-
} else {
|
|
542
|
-
ensureArchiveDirectory();
|
|
543
|
-
runRepair(existingInstall.version);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function rejectUnsupportedHistoricalInstall(version) {
|
|
548
|
-
printError("Installed NAOME harness version is outside the supported upgrade range.");
|
|
549
|
-
console.error(`Installed: v${version}`);
|
|
550
|
-
console.error(`Package: v${packageVersion}`);
|
|
551
|
-
console.error(`Minimum supported upgrade version: v${minimumSupportedUpgradeVersion}`);
|
|
552
|
-
console.error("Create a clean NAOME baseline instead of upgrading a pre-Rust harness in place.");
|
|
553
|
-
process.exit(1);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function runRepair(version, options = {}) {
|
|
557
|
-
summaryTitle = "NAOME harness checked";
|
|
558
|
-
ensureCoreHarnessFiles(`repair-${version}`);
|
|
559
|
-
ensureTaskControlHarnessFiles(`repair-${version}`);
|
|
560
|
-
ensureHarnessHealthFiles(`repair-${version}`);
|
|
561
|
-
installNativeDecisionBinary();
|
|
562
|
-
patchInstalledMachineOwnedIntegrity();
|
|
563
|
-
ensureBuiltInVerificationChecks();
|
|
564
|
-
ensureTestingProofHarnessSections();
|
|
565
|
-
refreshManifestHealthMetadata();
|
|
566
|
-
ensureCompleteUpgradeState(options.fromVersion ?? null);
|
|
567
|
-
ensureLocalOnlySourceControlBoundary();
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function readUpgradeState() {
|
|
571
|
-
const relativePath = ".naome/upgrade-state.json";
|
|
572
|
-
const targetPath = join(targetRoot, relativePath);
|
|
573
|
-
|
|
574
|
-
if (!existsSync(targetPath)) {
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
579
|
-
printError("NAOME cannot read .naome/upgrade-state.json safely.");
|
|
580
|
-
process.exit(1);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
return JSON.parse(readFileSync(targetPath, "utf8"));
|
|
585
|
-
} catch (error) {
|
|
586
|
-
printError("NAOME upgrade state is not valid JSON.");
|
|
587
|
-
console.error(error.message);
|
|
588
|
-
process.exit(1);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function ensureCoreHarnessFiles(archiveDirName) {
|
|
593
|
-
ensureNaomeIgnore();
|
|
594
|
-
ensureTemplateFile(".naome/verification.json");
|
|
595
|
-
replaceHarnessFile("AGENTS.md", archiveDirName);
|
|
596
|
-
replaceHarnessFile("docs/naome/index.md", archiveDirName);
|
|
597
|
-
replaceHarnessFile("docs/naome/first-run.md", archiveDirName);
|
|
598
|
-
replaceHarnessFile("docs/naome/agent-workflow.md", archiveDirName);
|
|
599
|
-
ensureTemplateFile("docs/naome/security.md");
|
|
600
|
-
replaceHarnessFile("docs/naome/upgrade.md", archiveDirName);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function ensureTaskControlHarnessFiles(archiveDirName) {
|
|
604
|
-
ensureTemplateFile(".naome/task-state.json");
|
|
605
|
-
replaceHarnessFile(".naome/task-contract.schema.json", archiveDirName);
|
|
606
|
-
replaceHarnessFile(".naome/bin/check-task-state.js", archiveDirName);
|
|
607
|
-
replaceHarnessFile("AGENTS.md", archiveDirName);
|
|
608
|
-
replaceHarnessFile("docs/naome/index.md", archiveDirName);
|
|
609
|
-
replaceHarnessFile("docs/naome/agent-workflow.md", archiveDirName);
|
|
610
|
-
replaceHarnessFile("docs/naome/execution.md", archiveDirName);
|
|
611
|
-
replaceHarnessFile("docs/naome/upgrade.md", archiveDirName);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function ensureHarnessHealthFiles(archiveDirName) {
|
|
615
|
-
replaceHarnessFile("AGENTS.md", archiveDirName);
|
|
616
|
-
replaceHarnessFile(".naome/package.json", archiveDirName);
|
|
617
|
-
replaceHarnessFile(".naome/bin/naome.js", archiveDirName);
|
|
618
|
-
replaceHarnessFile(".naome/bin/check-task-state.js", archiveDirName);
|
|
619
|
-
replaceHarnessFile(".naome/bin/check-harness-health.js", archiveDirName);
|
|
620
|
-
removeBranchControlledHookShim(".naome/git-hooks/pre-commit", archiveDirName);
|
|
621
|
-
removeBranchControlledHookShim(".naome/git-hooks/pre-push", archiveDirName);
|
|
622
|
-
replaceHarnessFile(".naome/task-contract.schema.json", archiveDirName);
|
|
623
|
-
replaceHarnessFile("docs/naome/index.md", archiveDirName);
|
|
624
|
-
replaceHarnessFile("docs/naome/first-run.md", archiveDirName);
|
|
625
|
-
replaceHarnessFile("docs/naome/agent-workflow.md", archiveDirName);
|
|
626
|
-
replaceHarnessFile("docs/naome/execution.md", archiveDirName);
|
|
627
|
-
ensureTemplateFile("docs/naome/security.md");
|
|
628
|
-
replaceHarnessFile("docs/naome/upgrade.md", archiveDirName);
|
|
629
|
-
ensureNaomeIgnore();
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function removeBranchControlledHookShim(relativePath, archiveDirName) {
|
|
633
|
-
const targetPath = join(targetRoot, relativePath);
|
|
634
|
-
if (!existsSync(targetPath)) {
|
|
635
|
-
skipped.push(relativePath);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (hasSymlinkInTargetPath(relativePath) || !lstatSync(targetPath).isFile()) {
|
|
640
|
-
skipped.push(relativePath);
|
|
641
|
-
unsafeSkipped.push(relativePath);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const archivePath = archiveUpgradePath(archiveDirName, relativePath);
|
|
646
|
-
mkdirSync(dirname(archivePath), { recursive: true });
|
|
647
|
-
copyFileSync(targetPath, archivePath);
|
|
648
|
-
unlinkSync(targetPath);
|
|
649
|
-
updated.push(relativePath);
|
|
650
|
-
archived.push({
|
|
651
|
-
from: relativePath,
|
|
652
|
-
to: relative(targetRoot, archivePath),
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function ensureTemplateFile(relativePath) {
|
|
657
|
-
const sourcePath = join(templateRoot, relativePath);
|
|
658
|
-
const targetPath = join(targetRoot, relativePath);
|
|
659
|
-
|
|
660
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
661
|
-
printError(`NAOME cannot write ${relativePath} safely.`);
|
|
662
|
-
console.error(`${relativePath} must be a regular file or must not exist.`);
|
|
663
|
-
process.exit(1);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (existsSync(targetPath)) {
|
|
667
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
668
|
-
printError(`NAOME cannot write ${relativePath} because that path is not a file.`);
|
|
669
|
-
process.exit(1);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
skipped.push(relativePath);
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
677
|
-
copyFileSync(sourcePath, targetPath);
|
|
678
|
-
installed.push(relativePath);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function ensureBuiltInVerificationChecks() {
|
|
682
|
-
const relativePath = ".naome/verification.json";
|
|
683
|
-
const targetPath = join(targetRoot, relativePath);
|
|
684
|
-
|
|
685
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
686
|
-
printError("NAOME cannot update .naome/verification.json safely.");
|
|
687
|
-
console.error(".naome/verification.json must be a regular file or must not exist.");
|
|
688
|
-
process.exit(1);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (!existsSync(targetPath)) {
|
|
692
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
693
|
-
copyFileSync(join(templateRoot, relativePath), targetPath);
|
|
694
|
-
installed.push(relativePath);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
698
|
-
printError("NAOME cannot update .naome/verification.json because that path is not a file.");
|
|
699
|
-
process.exit(1);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const nativePath = usesSourceNativeFallback()
|
|
703
|
-
? findNativeDecisionBinary()
|
|
704
|
-
: join(targetRoot, nativeBinaryRelativePath);
|
|
705
|
-
if (
|
|
706
|
-
!nativePath ||
|
|
707
|
-
!existsSync(nativePath) ||
|
|
708
|
-
(!usesSourceNativeFallback() && hasSymlinkInTargetPath(nativeBinaryRelativePath)) ||
|
|
709
|
-
!lstatSync(nativePath).isFile()
|
|
710
|
-
) {
|
|
711
|
-
printError("NAOME native decision binary is unavailable for verification seeding.");
|
|
712
|
-
console.error(`Expected ${nativeBinaryRelativePath} to be installed before seeding verification checks.`);
|
|
713
|
-
process.exit(1);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const result = spawnSync(nativePath, ["seed-verification"], {
|
|
717
|
-
cwd: targetRoot,
|
|
718
|
-
encoding: "utf8",
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
if (result.status !== 0) {
|
|
722
|
-
printError("NAOME could not seed built-in verification checks.");
|
|
723
|
-
if (result.stdout.trim()) {
|
|
724
|
-
console.error(result.stdout.trim());
|
|
725
|
-
}
|
|
726
|
-
if (result.stderr.trim()) {
|
|
727
|
-
console.error(result.stderr.trim());
|
|
728
|
-
}
|
|
729
|
-
process.exit(1);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (!result.stdout.includes("updated")) {
|
|
733
|
-
skipped.push(relativePath);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
removeSkipped(relativePath);
|
|
738
|
-
updated.push(relativePath);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function replaceHarnessFile(relativePath, archiveDirName) {
|
|
742
|
-
const sourcePath = join(templateRoot, relativePath);
|
|
743
|
-
const targetPath = join(targetRoot, relativePath);
|
|
744
|
-
|
|
745
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
746
|
-
printError(`NAOME cannot update ${relativePath} safely.`);
|
|
747
|
-
console.error(`${relativePath} must be a regular file or must not exist.`);
|
|
748
|
-
process.exit(1);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (!existsSync(targetPath)) {
|
|
752
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
753
|
-
copyFileSync(sourcePath, targetPath);
|
|
754
|
-
installed.push(relativePath);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
759
|
-
printError(`NAOME cannot update ${relativePath} because that path is not a file.`);
|
|
760
|
-
process.exit(1);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const nextContent = readFileSync(sourcePath);
|
|
764
|
-
const currentContent = readFileSync(targetPath);
|
|
765
|
-
if (currentContent.equals(nextContent)) {
|
|
766
|
-
skipped.push(relativePath);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (
|
|
771
|
-
!hasGeneratedIntegrity(relativePath) &&
|
|
772
|
-
machineFileHash(relativePath, currentContent) === machineFileHash(relativePath, nextContent)
|
|
773
|
-
) {
|
|
774
|
-
skipped.push(relativePath);
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const archivePath = archiveUpgradePath(archiveDirName, relativePath);
|
|
779
|
-
mkdirSync(dirname(archivePath), { recursive: true });
|
|
780
|
-
copyFileSync(targetPath, archivePath);
|
|
781
|
-
writeFileSync(targetPath, nextContent);
|
|
782
|
-
updated.push(relativePath);
|
|
783
|
-
archived.push({
|
|
784
|
-
from: relativePath,
|
|
785
|
-
to: relative(targetRoot, archivePath),
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function hasGeneratedIntegrity(relativePath) {
|
|
790
|
-
return relativePath === healthCheckerRelativePath || relativePath === taskStateCheckerRelativePath;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function archiveUpgradePath(archiveDirName, relativePath) {
|
|
794
|
-
return join(targetRoot, ".naome", "archive", archiveDirName, relativePath);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function ensureNaomeIgnore() {
|
|
798
|
-
const relativePath = ".naomeignore";
|
|
799
|
-
const sourcePath = join(templateRoot, relativePath);
|
|
800
|
-
const targetPath = join(targetRoot, relativePath);
|
|
801
|
-
|
|
802
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
803
|
-
printError("NAOME cannot write .naomeignore safely.");
|
|
804
|
-
console.error(".naomeignore must be a regular file or must not exist.");
|
|
805
|
-
process.exit(1);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (!existsSync(targetPath)) {
|
|
809
|
-
copyFileSync(sourcePath, targetPath);
|
|
810
|
-
installed.push(relativePath);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
815
|
-
printError("NAOME cannot update .naomeignore because that path is not a file.");
|
|
816
|
-
process.exit(1);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const content = readFileSync(targetPath, "utf8");
|
|
820
|
-
if (content.split(/\r?\n/).some((line) => line.trim() === ".naome/archive/")) {
|
|
821
|
-
skipped.push(relativePath);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
writeFileSync(targetPath, `${content.trimEnd()}\n\n.naome/archive/\n`);
|
|
826
|
-
updated.push(relativePath);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function ensureLocalOnlySourceControlBoundary() {
|
|
830
|
-
if (!isInsideGitWorkTree()) {
|
|
831
|
-
ensureLocalOnlyGitIgnoreFallback();
|
|
832
|
-
skipped.push("local git exclude");
|
|
833
|
-
skipped.push("local-only git index cleanup");
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
cleanupLegacyLocalOnlyGitIgnore();
|
|
838
|
-
ensureLocalOnlyGitExclude();
|
|
839
|
-
untrackLocalOnlyGitPaths();
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function ensureLocalOnlyGitIgnoreFallback() {
|
|
843
|
-
const relativePath = ".gitignore";
|
|
844
|
-
const targetPath = join(targetRoot, relativePath);
|
|
845
|
-
|
|
846
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
847
|
-
printError("NAOME cannot write .gitignore safely.");
|
|
848
|
-
console.error(".gitignore must be a regular file or must not exist.");
|
|
849
|
-
process.exit(1);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (existsSync(targetPath) && !lstatSync(targetPath).isFile()) {
|
|
853
|
-
printError("NAOME cannot update .gitignore because that path is not a file.");
|
|
854
|
-
process.exit(1);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const content = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
|
|
858
|
-
const existingLines = new Set(content.split(/\r?\n/).map((line) => line.trim()));
|
|
859
|
-
const missingEntries = legacyLocalOnlyGitIgnoreEntries().filter((entry) => !existingLines.has(entry));
|
|
860
|
-
|
|
861
|
-
if (missingEntries.length === 0) {
|
|
862
|
-
skipped.push(relativePath);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const nextContent = `${content.trimEnd()}${content.trimEnd() ? "\n\n" : ""}${missingEntries.join("\n")}\n`;
|
|
867
|
-
writeFileSync(targetPath, nextContent);
|
|
868
|
-
if (content.length === 0) {
|
|
869
|
-
installed.push(relativePath);
|
|
870
|
-
} else {
|
|
871
|
-
updated.push(relativePath);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function cleanupLegacyLocalOnlyGitIgnore() {
|
|
876
|
-
const relativePath = ".gitignore";
|
|
877
|
-
const targetPath = join(targetRoot, relativePath);
|
|
878
|
-
|
|
879
|
-
if (!existsSync(targetPath)) {
|
|
880
|
-
skipped.push(relativePath);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
885
|
-
printError("NAOME cannot clean up .gitignore safely.");
|
|
886
|
-
console.error(".gitignore must be a regular file or must not exist.");
|
|
887
|
-
process.exit(1);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
891
|
-
printError("NAOME cannot update .gitignore because that path is not a file.");
|
|
892
|
-
process.exit(1);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
const content = readFileSync(targetPath, "utf8");
|
|
896
|
-
const legacyEntries = new Set(legacyLocalOnlyGitIgnoreEntries());
|
|
897
|
-
const nextContent = cleanBlankLines(content
|
|
898
|
-
.split(/\r?\n/)
|
|
899
|
-
.filter((line) => !legacyEntries.has(line.trim()))
|
|
900
|
-
.join("\n"));
|
|
901
|
-
|
|
902
|
-
if (nextContent === content) {
|
|
903
|
-
skipped.push(relativePath);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (nextContent.trim().length === 0) {
|
|
908
|
-
unlinkSync(targetPath);
|
|
909
|
-
} else {
|
|
910
|
-
writeFileSync(targetPath, nextContent);
|
|
911
|
-
}
|
|
912
|
-
updated.push(relativePath);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function ensureLocalOnlyGitExclude() {
|
|
916
|
-
if (!isInsideGitWorkTree()) {
|
|
917
|
-
skipped.push("local git exclude");
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const gitDirResult = spawnSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
922
|
-
cwd: targetRoot,
|
|
923
|
-
encoding: "utf8",
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
if (gitDirResult.status !== 0 || !gitDirResult.stdout.trim()) {
|
|
927
|
-
skipped.push("local git exclude");
|
|
928
|
-
unsafeSkipped.push("local git exclude");
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const excludePath = join(gitDirResult.stdout.trim(), "info", "exclude");
|
|
933
|
-
mkdirSync(dirname(excludePath), { recursive: true });
|
|
934
|
-
|
|
935
|
-
if (existsSync(excludePath) && !lstatSync(excludePath).isFile()) {
|
|
936
|
-
printError("NAOME cannot update local git exclude because that path is not a file.");
|
|
937
|
-
process.exit(1);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const content = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
941
|
-
const existingLines = new Set(content.split(/\r?\n/).map((line) => line.trim()));
|
|
942
|
-
const missingEntries = localOnlyGitExcludeEntries.filter((entry) => !existingLines.has(entry));
|
|
943
|
-
|
|
944
|
-
if (missingEntries.length === 0) {
|
|
945
|
-
skipped.push(".git/info/exclude");
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const nextContent = `${content.trimEnd()}${content.trimEnd() ? "\n\n" : ""}${missingEntries.join("\n")}\n`;
|
|
950
|
-
writeFileSync(excludePath, nextContent);
|
|
951
|
-
updated.push(".git/info/exclude");
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function legacyLocalOnlyGitIgnoreEntries() {
|
|
955
|
-
return [
|
|
956
|
-
"# NAOME local machine-owned harness files.",
|
|
957
|
-
".naome/archive/",
|
|
958
|
-
".naome/bin/naome-rust*",
|
|
959
|
-
...localOnlyMachineOwnedPaths,
|
|
960
|
-
...localOnlyGitIgnoreEntries,
|
|
961
|
-
];
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function cleanBlankLines(content) {
|
|
965
|
-
const lines = content.split(/\r?\n/);
|
|
966
|
-
const cleaned = [];
|
|
967
|
-
|
|
968
|
-
for (const line of lines) {
|
|
969
|
-
if (line.trim() === "" && cleaned.at(-1)?.trim() === "") {
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
cleaned.push(line);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const trimmed = cleaned.join("\n").trimEnd();
|
|
976
|
-
return trimmed ? `${trimmed}\n` : "";
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function untrackLocalOnlyGitPaths() {
|
|
980
|
-
if (!isInsideGitWorkTree()) {
|
|
981
|
-
skipped.push("local-only git index cleanup");
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const tracked = trackedLocalOnlyGitPaths();
|
|
986
|
-
if (tracked.length === 0) {
|
|
987
|
-
skipped.push("local-only git index cleanup");
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const result = spawnSync("git", ["rm", "--cached", "-q", "--", ...tracked], {
|
|
992
|
-
cwd: targetRoot,
|
|
993
|
-
encoding: "utf8",
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
if (result.status !== 0) {
|
|
997
|
-
printError("NAOME could not remove local-only harness files from the git index.");
|
|
998
|
-
if (result.stdout.trim()) {
|
|
999
|
-
console.error(result.stdout.trim());
|
|
1000
|
-
}
|
|
1001
|
-
if (result.stderr.trim()) {
|
|
1002
|
-
console.error(result.stderr.trim());
|
|
1003
|
-
}
|
|
1004
|
-
process.exit(1);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
updated.push(...tracked.map((path) => `git index: ${path}`));
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function isInsideGitWorkTree() {
|
|
1011
|
-
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1012
|
-
cwd: targetRoot,
|
|
1013
|
-
encoding: "utf8",
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
return result.status === 0 && result.stdout.trim() === "true";
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function trackedLocalOnlyGitPaths() {
|
|
1020
|
-
const result = spawnSync("git", ["ls-files", "-z", "--", ...localOnlyGitUntrackPaths], {
|
|
1021
|
-
cwd: targetRoot,
|
|
1022
|
-
encoding: "utf8",
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
if (result.status !== 0) {
|
|
1026
|
-
printError("NAOME could not inspect tracked local-only harness files.");
|
|
1027
|
-
if (result.stderr.trim()) {
|
|
1028
|
-
console.error(result.stderr.trim());
|
|
1029
|
-
}
|
|
1030
|
-
process.exit(1);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return result.stdout.split("\0").filter(Boolean);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function ensureTestingProofHarnessSections() {
|
|
1037
|
-
const relativePath = "docs/naome/testing.md";
|
|
1038
|
-
const targetPath = join(targetRoot, relativePath);
|
|
1039
|
-
|
|
1040
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
1041
|
-
printError("NAOME cannot update docs/naome/testing.md safely.");
|
|
1042
|
-
console.error("docs/naome/testing.md must be a regular file or must not exist.");
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (!existsSync(targetPath)) {
|
|
1047
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1048
|
-
copyFileSync(join(templateRoot, relativePath), targetPath);
|
|
1049
|
-
installed.push(relativePath);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (!lstatSync(targetPath).isFile()) {
|
|
1054
|
-
printError("NAOME cannot update docs/naome/testing.md because that path is not a file.");
|
|
1055
|
-
process.exit(1);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
let content = readFileSync(targetPath, "utf8");
|
|
1059
|
-
const originalContent = content;
|
|
1060
|
-
const sections = [
|
|
1061
|
-
["## Verification Map", "\n| Change type | Required proof | Command | Notes |\n|---|---|---|---|\n| Unknown | Unknown | Unknown | Fill during NAOME upgrade. |\n"],
|
|
1062
|
-
["## Known Checks", "\n| Check id | Command | Cwd | Cost | Last verified |\n|---|---|---|---|---|\n| Unknown | Unknown | Unknown | Unknown | Unknown |\n"],
|
|
1063
|
-
["## Change Type Rules", "\n| Change type | Paths | Required checks |\n|---|---|---|\n| Unknown | Unknown | Unknown |\n"],
|
|
1064
|
-
["## Release Gates", "\n| Check id | Required when |\n|---|---|\n| Unknown | Before release, when applicable. |\n"],
|
|
1065
|
-
["## Rules", "\n- Mirror durable entries in `.naome/verification.json`.\n- Use only commands proven by repository files, CI, or user confirmation.\n- Report exact commands and results. Do not claim proof that did not run.\n"],
|
|
1066
|
-
["## Evidence", "\n- Unknown.\n"],
|
|
1067
|
-
];
|
|
1068
|
-
|
|
1069
|
-
for (const [heading, body] of sections) {
|
|
1070
|
-
if (!hasMarkdownHeading(content, heading)) {
|
|
1071
|
-
content = `${content.trimEnd()}\n\n${heading}\n${body}`;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (content !== originalContent) {
|
|
1076
|
-
writeFileSync(targetPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
1077
|
-
updated.push(relativePath);
|
|
1078
|
-
} else {
|
|
1079
|
-
skipped.push(relativePath);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function hasMarkdownHeading(content, heading) {
|
|
1084
|
-
return content.split(/\r?\n/).some((line) => line.trim() === heading);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function writeUpgradeState(state) {
|
|
1088
|
-
const relativePath = ".naome/upgrade-state.json";
|
|
1089
|
-
const targetPath = join(targetRoot, relativePath);
|
|
1090
|
-
|
|
1091
|
-
assertWritableUpgradeStatePath();
|
|
1092
|
-
|
|
1093
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1094
|
-
writeFileSync(targetPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1095
|
-
updated.push(relativePath);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function ensureCompleteUpgradeState(fromVersion = null) {
|
|
1099
|
-
const existingUpgradeState = readUpgradeState();
|
|
1100
|
-
if (
|
|
1101
|
-
existingUpgradeState?.status === "complete" &&
|
|
1102
|
-
existingUpgradeState?.toVersion === packageVersion &&
|
|
1103
|
-
(fromVersion === null || existingUpgradeState?.fromVersion === fromVersion) &&
|
|
1104
|
-
Array.isArray(existingUpgradeState.pending) &&
|
|
1105
|
-
existingUpgradeState.pending.length === 0 &&
|
|
1106
|
-
Array.isArray(existingUpgradeState.completed)
|
|
1107
|
-
) {
|
|
1108
|
-
skipped.push(".naome/upgrade-state.json");
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
writeUpgradeState({
|
|
1113
|
-
status: "complete",
|
|
1114
|
-
fromVersion,
|
|
1115
|
-
toVersion: packageVersion,
|
|
1116
|
-
pending: [],
|
|
1117
|
-
completed: [],
|
|
1118
|
-
updatedAt: new Date().toISOString(),
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function assertWritableUpgradeStatePath() {
|
|
1123
|
-
const relativePath = ".naome/upgrade-state.json";
|
|
1124
|
-
const targetPath = join(targetRoot, relativePath);
|
|
1125
|
-
|
|
1126
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
1127
|
-
printError("NAOME cannot write .naome/upgrade-state.json safely.");
|
|
1128
|
-
console.error(".naome/upgrade-state.json must be a regular file or must not exist.");
|
|
1129
|
-
process.exit(1);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
if (existsSync(targetPath) && !lstatSync(targetPath).isFile()) {
|
|
1133
|
-
printError("NAOME cannot write .naome/upgrade-state.json because that path is not a file.");
|
|
1134
|
-
console.error(".naome/upgrade-state.json must be a regular file or must not exist.");
|
|
1135
|
-
process.exit(1);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function refreshManifestHealthMetadata() {
|
|
1140
|
-
const relativePath = ".naome/manifest.json";
|
|
1141
|
-
const targetPath = join(targetRoot, relativePath);
|
|
1142
|
-
|
|
1143
|
-
if (hasSymlinkInTargetPath(relativePath)) {
|
|
1144
|
-
printError("NAOME cannot write .naome/manifest.json safely.");
|
|
1145
|
-
console.error(".naome/manifest.json must be a regular file.");
|
|
1146
|
-
process.exit(1);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
if (!existsSync(targetPath) || !lstatSync(targetPath).isFile()) {
|
|
1150
|
-
printError("NAOME cannot write .naome/manifest.json because that path is not a file.");
|
|
1151
|
-
process.exit(1);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const manifest = JSON.parse(readFileSync(targetPath, "utf8"));
|
|
1155
|
-
const originalContent = JSON.stringify(manifest, null, 2);
|
|
1156
|
-
applyManifestHealthMetadata(manifest);
|
|
1157
|
-
const nextContent = JSON.stringify(manifest, null, 2);
|
|
1158
|
-
|
|
1159
|
-
if (nextContent === originalContent) {
|
|
1160
|
-
skipped.push(relativePath);
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
writeFileSync(targetPath, `${nextContent}\n`);
|
|
1165
|
-
updated.push(relativePath);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function applyManifestHealthMetadata(manifest) {
|
|
1169
|
-
manifest.harnessVersion = packageVersion;
|
|
1170
|
-
manifest.machineOwned = [...machineOwnedPaths];
|
|
1171
|
-
manifest.projectOwned = projectOwnedPaths;
|
|
1172
|
-
manifest.integrity = templateIntegrity();
|
|
1173
|
-
|
|
1174
|
-
const nativeHash = installedNativeBinaryHash();
|
|
1175
|
-
if (!usesSourceNativeFallback() && nativeHash) {
|
|
1176
|
-
if (!manifest.machineOwned.includes(nativeBinaryRelativePath)) {
|
|
1177
|
-
manifest.machineOwned.push(nativeBinaryRelativePath);
|
|
1178
|
-
}
|
|
1179
|
-
manifest.integrity[nativeBinaryRelativePath] = `sha256:${nativeHash}`;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function installedNativeBinaryHash() {
|
|
1184
|
-
const targetPath = join(targetRoot, nativeBinaryRelativePath);
|
|
1185
|
-
if (!existsSync(targetPath) || hasSymlinkInTargetPath(nativeBinaryRelativePath) || !lstatSync(targetPath).isFile()) {
|
|
1186
|
-
return null;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
return sha256(readFileSync(targetPath));
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function templateIntegrity() {
|
|
1193
|
-
const integrity = {};
|
|
1194
|
-
|
|
1195
|
-
for (const relativePath of machineOwnedPaths) {
|
|
1196
|
-
const sourcePath = join(templateRoot, relativePath);
|
|
1197
|
-
integrity[relativePath] = `sha256:${machineFileHash(relativePath, readFileSync(sourcePath))}`;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return integrity;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function sha256(content) {
|
|
1204
|
-
return createHash("sha256").update(content).digest("hex");
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function machineFileHash(relativePath, content) {
|
|
1208
|
-
let normalized = content;
|
|
1209
|
-
|
|
1210
|
-
if (hasGeneratedIntegrity(relativePath)) {
|
|
1211
|
-
normalized = normalized.toString("utf8").replace(integrityBlockPattern, normalizedIntegrityBlock);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (relativePath === naomeCommandRelativePath) {
|
|
1215
|
-
normalized = normalized.toString("utf8").replace(nativeIntegrityPattern, normalizedNativeIntegrity);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
return sha256(normalized);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
function ensureArchiveDirectory() {
|
|
1222
|
-
const archivePath = ".naome/archive";
|
|
1223
|
-
const targetPath = join(targetRoot, archivePath);
|
|
1224
|
-
if (hasSymlinkInTargetPath(archivePath)) {
|
|
1225
|
-
printError("NAOME cannot create .naome/archive safely.");
|
|
1226
|
-
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
1227
|
-
process.exit(1);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
if (existsSync(targetPath)) {
|
|
1231
|
-
if (!lstatSync(targetPath).isDirectory()) {
|
|
1232
|
-
printError("NAOME cannot create .naome/archive because that path is not a directory.");
|
|
1233
|
-
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
1234
|
-
process.exit(1);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
skipped.push(`${archivePath}/`);
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
mkdirSync(targetPath, { recursive: true });
|
|
1242
|
-
installed.push(`${archivePath}/`);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
function hasAgentsTakeoverCandidate() {
|
|
1246
|
-
const agentsPath = join(targetRoot, "AGENTS.md");
|
|
1247
|
-
if (!existsSync(agentsPath)) {
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
if (hasSymlinkInTargetPath("AGENTS.md")) {
|
|
1252
|
-
return true;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
return readFileSync(agentsPath, "utf8") !== readFileSync(join(templateRoot, "AGENTS.md"), "utf8");
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
async function confirmAgentsTakeover() {
|
|
1259
|
-
if (!hasAgentsTakeoverCandidate()) {
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
if (hasSymlinkInTargetPath("AGENTS.md")) {
|
|
1264
|
-
printError("NAOME cannot take over AGENTS.md because it is a symlink.");
|
|
1265
|
-
console.error("Replace the symlink with a regular file before installing NAOME.");
|
|
1266
|
-
process.exit(1);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (!process.stdin.isTTY || process.env.CI === "true") {
|
|
1270
|
-
printError("AGENTS.md already exists.");
|
|
1271
|
-
console.error("NAOME must replace AGENTS.md to become the active harness entrypoint.");
|
|
1272
|
-
console.error("Run this installer in an interactive terminal and confirm the takeover.");
|
|
1273
|
-
process.exit(1);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
printSection("AGENTS.md takeover required");
|
|
1277
|
-
console.log(`${color.dim("existing")} AGENTS.md`);
|
|
1278
|
-
console.log(`${color.dim("archive ")} .naome/archive/AGENTS.pre-naome.md`);
|
|
1279
|
-
console.log(`${color.dim("replace ")} AGENTS.md with the NAOME entrypoint`);
|
|
1280
|
-
console.log(`${color.dim("ignore ")} .naome/archive/ via .naomeignore`);
|
|
1281
|
-
console.log("");
|
|
1282
|
-
|
|
1283
|
-
const rl = createInterface({ input, output });
|
|
1284
|
-
const answer = await rl.question(`${color.yellow("?")} Replace AGENTS.md and continue installation? (y/N) `);
|
|
1285
|
-
rl.close();
|
|
1286
|
-
|
|
1287
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
1288
|
-
printCancelled("NAOME was not installed. AGENTS.md was left unchanged.");
|
|
1289
|
-
process.exit(1);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
assertArchiveDirectoryAvailableForTakeover();
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
function takeoverExistingAgents() {
|
|
1296
|
-
const agentsPath = join(targetRoot, "AGENTS.md");
|
|
1297
|
-
if (!existsSync(agentsPath)) {
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
if (hasSymlinkInTargetPath("AGENTS.md")) {
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const templateAgentsPath = join(templateRoot, "AGENTS.md");
|
|
1306
|
-
const templateAgents = readFileSync(templateAgentsPath, "utf8");
|
|
1307
|
-
const existingAgents = readFileSync(agentsPath, "utf8");
|
|
1308
|
-
|
|
1309
|
-
if (existingAgents === templateAgents) {
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
const archivePath = nextArchivePath("AGENTS.pre-naome.md");
|
|
1314
|
-
|
|
1315
|
-
if (!archivePath) {
|
|
1316
|
-
printError("AGENTS.md already exists, but NAOME could not archive it safely.");
|
|
1317
|
-
console.error("AGENTS.md was left unchanged.");
|
|
1318
|
-
process.exit(1);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
renameSync(agentsPath, archivePath);
|
|
1322
|
-
copyFileSync(templateAgentsPath, agentsPath);
|
|
1323
|
-
removeSkipped("AGENTS.md");
|
|
1324
|
-
installed.push("AGENTS.md");
|
|
1325
|
-
archived.push({
|
|
1326
|
-
from: "AGENTS.md",
|
|
1327
|
-
to: relative(targetRoot, archivePath),
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
function removeSkipped(relativePath) {
|
|
1332
|
-
const index = skipped.indexOf(relativePath);
|
|
1333
|
-
if (index !== -1) {
|
|
1334
|
-
skipped.splice(index, 1);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
function nextArchivePath(baseName) {
|
|
1339
|
-
const archiveDir = ".naome/archive";
|
|
1340
|
-
|
|
1341
|
-
if (!isArchiveDirectoryAvailable()) {
|
|
1342
|
-
skipped.push(`${archiveDir}/${baseName}`);
|
|
1343
|
-
unsafeSkipped.push(`${archiveDir}/${baseName}`);
|
|
1344
|
-
return null;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
mkdirSync(join(targetRoot, archiveDir), { recursive: true });
|
|
1348
|
-
|
|
1349
|
-
for (let index = 0; index < 1000; index += 1) {
|
|
1350
|
-
const fileName = index === 0 ? baseName : baseName.replace(/\.md$/, `.${index}.md`);
|
|
1351
|
-
const archivePath = join(targetRoot, archiveDir, fileName);
|
|
1352
|
-
|
|
1353
|
-
if (!existsSync(archivePath)) {
|
|
1354
|
-
return archivePath;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function assertArchiveDirectoryAvailableForTakeover() {
|
|
1362
|
-
if (isArchiveDirectoryAvailable()) {
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
printError("NAOME cannot archive AGENTS.md safely.");
|
|
1367
|
-
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
1368
|
-
console.error("Fix that path before installing NAOME.");
|
|
1369
|
-
process.exit(1);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
function isArchiveDirectoryAvailable() {
|
|
1373
|
-
const parts = [".naome", "archive"];
|
|
1374
|
-
let current = targetRoot;
|
|
1375
|
-
|
|
1376
|
-
for (const part of parts) {
|
|
1377
|
-
current = join(current, part);
|
|
1378
|
-
try {
|
|
1379
|
-
const stats = lstatSync(current);
|
|
1380
|
-
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
1381
|
-
return false;
|
|
1382
|
-
}
|
|
1383
|
-
} catch (error) {
|
|
1384
|
-
if (error.code === "ENOENT") {
|
|
1385
|
-
return true;
|
|
1386
|
-
}
|
|
1387
|
-
throw error;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
return true;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
function hasSymlinkInTargetPath(relativePath) {
|
|
1395
|
-
const parts = relativePath.split(/[\\/]+/);
|
|
1396
|
-
let current = targetRoot;
|
|
1397
|
-
|
|
1398
|
-
for (const part of parts) {
|
|
1399
|
-
current = join(current, part);
|
|
1400
|
-
try {
|
|
1401
|
-
if (lstatSync(current).isSymbolicLink()) {
|
|
1402
|
-
return true;
|
|
1403
|
-
}
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
if (error.code === "ENOENT") {
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
throw error;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
return false;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function assertTemplateRoot() {
|
|
1416
|
-
if (!existsSync(templateRoot) || !statSync(templateRoot).isDirectory()) {
|
|
1417
|
-
printError("NAOME installer templates are missing from this package.");
|
|
1418
|
-
process.exit(1);
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function printHeader() {
|
|
1423
|
-
console.log("");
|
|
1424
|
-
console.log(`${color.bold("NAOME")} ${color.dim(`v${packageVersion}`)}`);
|
|
1425
|
-
console.log(color.dim("AI coding-agent harness installer"));
|
|
1426
|
-
console.log("");
|
|
1427
|
-
console.log(`${color.dim("target ")} ${targetRoot}`);
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function printSection(title) {
|
|
1431
|
-
console.log("");
|
|
1432
|
-
console.log(color.bold(title));
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function printList(items, marker) {
|
|
1436
|
-
for (const item of uniqueStrings(items)) {
|
|
1437
|
-
console.log(`${color.dim(marker)} ${item}`);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function uniqueStrings(items) {
|
|
1442
|
-
return Array.from(new Set(items));
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
function printError(message) {
|
|
1446
|
-
console.error("");
|
|
1447
|
-
console.error(`${color.red("x")} ${message}`);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
function printCancelled(message) {
|
|
1451
|
-
console.log("");
|
|
1452
|
-
console.log(`${color.yellow("-")} ${message}`);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function printSummary() {
|
|
1456
|
-
console.log("");
|
|
1457
|
-
console.log(`${color.green("+")} ${summaryTitle}`);
|
|
1458
|
-
|
|
1459
|
-
const detailedOutput = shouldPrintDetailedSummary();
|
|
1460
|
-
if (!detailedOutput) {
|
|
1461
|
-
printCompactSummary();
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
if (detailedOutput && installed.length > 0) {
|
|
1465
|
-
const installedItems = uniqueStrings(installed);
|
|
1466
|
-
printSection(`Installed ${installedItems.length} ${installedItems.length === 1 ? "item" : "items"}`);
|
|
1467
|
-
printList(installed, "+");
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
if (detailedOutput && updated.length > 0) {
|
|
1471
|
-
const updatedItems = uniqueStrings(updated);
|
|
1472
|
-
printSection(`Updated ${updatedItems.length} ${updatedItems.length === 1 ? "item" : "items"}`);
|
|
1473
|
-
printList(updated, "~");
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
if (detailedOutput && archived.length > 0) {
|
|
1477
|
-
printSection("Archived");
|
|
1478
|
-
for (const entry of archived) {
|
|
1479
|
-
console.log(`${color.dim(">")} ${entry.from} -> ${entry.to}`);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
if (detailedOutput && skipped.length > 0) {
|
|
1484
|
-
const skippedItems = uniqueStrings(skipped);
|
|
1485
|
-
printSection(`Skipped ${skippedItems.length} existing ${skippedItems.length === 1 ? "path" : "paths"}`);
|
|
1486
|
-
printList(skipped, "-");
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
if (unsafeSkipped.length > 0) {
|
|
1490
|
-
printSection("Skipped unsafe symlinked paths");
|
|
1491
|
-
printList(unsafeSkipped, "!");
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
if (isRepositoryInitialized()) {
|
|
1495
|
-
console.log("");
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
printSection("Next step");
|
|
1500
|
-
console.log("Copy this into your coding agent:");
|
|
1501
|
-
console.log("");
|
|
1502
|
-
console.log(color.dim("```"));
|
|
1503
|
-
console.log(nextStepPrompt);
|
|
1504
|
-
console.log(color.dim("```"));
|
|
1505
|
-
console.log("");
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function shouldPrintDetailedSummary() {
|
|
1509
|
-
return verboseOutput || !existingInstall;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function printCompactSummary() {
|
|
1513
|
-
const installedCount = uniqueStrings(installed).length;
|
|
1514
|
-
const updatedCount = uniqueStrings(updated).length;
|
|
1515
|
-
const archivedCount = archived.length;
|
|
1516
|
-
|
|
1517
|
-
if (installedCount > 0) {
|
|
1518
|
-
console.log(`${color.dim("installed")} ${installedCount}`);
|
|
1519
|
-
}
|
|
1520
|
-
if (updatedCount > 0) {
|
|
1521
|
-
console.log(`${color.dim("updated")} ${updatedCount}`);
|
|
1522
|
-
}
|
|
1523
|
-
if (archivedCount > 0) {
|
|
1524
|
-
console.log(`${color.dim("archived")} ${archivedCount}`);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function isRepositoryInitialized() {
|
|
1529
|
-
try {
|
|
1530
|
-
const initState = JSON.parse(readFileSync(join(targetRoot, ".naome", "init-state.json"), "utf8"));
|
|
1531
|
-
return initState.initialized === true;
|
|
1532
|
-
} catch {
|
|
1533
|
-
return false;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
assertTemplateRoot();
|
|
1538
|
-
loadInstallPlan();
|
|
1539
|
-
|
|
1540
|
-
printHeader();
|
|
1541
|
-
const existingInstall = readExistingInstall();
|
|
1542
|
-
|
|
1543
|
-
if (existingInstall) {
|
|
1544
|
-
runExistingInstall(existingInstall);
|
|
1545
|
-
} else {
|
|
1546
|
-
await runFreshInstall();
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
ensureExecutableMachineOwnedFiles();
|
|
1550
|
-
configureGitHooks();
|
|
1551
|
-
printSummary();
|
|
1552
|
-
|
|
1553
|
-
function compareVersions(left, right) {
|
|
1554
|
-
const leftParts = parseVersion(left);
|
|
1555
|
-
const rightParts = parseVersion(right);
|
|
1556
|
-
|
|
1557
|
-
for (let index = 0; index < 3; index += 1) {
|
|
1558
|
-
if (leftParts[index] > rightParts[index]) {
|
|
1559
|
-
return 1;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
if (leftParts[index] < rightParts[index]) {
|
|
1563
|
-
return -1;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
return 0;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
function parseVersion(version) {
|
|
1571
|
-
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
1572
|
-
if (!match) {
|
|
1573
|
-
printError(`Invalid NAOME version: ${version}`);
|
|
1574
|
-
process.exit(1);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
return match.slice(1).map((part) => Number.parseInt(part, 10));
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function isVersion(version) {
|
|
1581
|
-
return typeof version === "string" && /^\d+\.\d+\.\d+$/.test(version);
|
|
1582
|
-
}
|
|
5
|
+
await runNaomeNodeCli();
|