@lamentis/naome 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -2
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +19 -5
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +2 -1
- package/crates/naome-cli/src/main.rs +3 -0
- package/crates/naome-cli/src/quality_commands.rs +90 -2
- 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/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +5 -3
- 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/cleanup.rs +13 -1
- 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 +18 -0
- package/crates/naome-core/src/quality/scanner.rs +20 -8
- 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 +144 -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 +94 -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 +124 -0
- package/crates/naome-core/src/quality/types.rs +3 -0
- package/crates/naome-core/src/route/builtin_checks.rs +155 -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/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +2 -0
- package/crates/naome-core/tests/quality.rs +12 -118
- 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 +125 -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 +198 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -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_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 +170 -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 +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
- package/templates/naome-root/.naome/bin/naome.js +25 -21
- package/templates/naome-root/.naome/manifest.json +4 -2
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/verification.json +1 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +3 -0
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- package/templates/naome-root/docs/naome/testing.md +2 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { printError } from "./output.js";
|
|
12
|
+
|
|
13
|
+
export function walk(dir) {
|
|
14
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
15
|
+
const files = [];
|
|
16
|
+
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
files.push(...walk(fullPath));
|
|
21
|
+
} else if (entry.isFile()) {
|
|
22
|
+
files.push(fullPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function copyTemplateFile(ctx, sourcePath) {
|
|
30
|
+
const relativePath = relative(ctx.templateRoot, sourcePath);
|
|
31
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
32
|
+
|
|
33
|
+
if (hasSymlinkInTargetPath(ctx, relativePath)) {
|
|
34
|
+
ctx.skipped.push(relativePath);
|
|
35
|
+
ctx.unsafeSkipped.push(relativePath);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (existsSync(targetPath)) {
|
|
40
|
+
ctx.skipped.push(relativePath);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
45
|
+
copyFileSync(sourcePath, targetPath);
|
|
46
|
+
ctx.installed.push(relativePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertTemplateRoot(ctx) {
|
|
50
|
+
if (!existsSync(ctx.templateRoot) || !statSync(ctx.templateRoot).isDirectory()) {
|
|
51
|
+
printError(ctx, "NAOME installer templates are missing from this package.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function removeSkipped(ctx, relativePath) {
|
|
57
|
+
const index = ctx.skipped.indexOf(relativePath);
|
|
58
|
+
if (index !== -1) {
|
|
59
|
+
ctx.skipped.splice(index, 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function archiveUpgradePath(ctx, archiveDirName, relativePath) {
|
|
64
|
+
return join(ctx.targetRoot, ".naome", "archive", archiveDirName, relativePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ensureArchiveDirectory(ctx) {
|
|
68
|
+
const archivePath = ".naome/archive";
|
|
69
|
+
const targetPath = join(ctx.targetRoot, archivePath);
|
|
70
|
+
if (hasSymlinkInTargetPath(ctx, archivePath)) {
|
|
71
|
+
printError(ctx, "NAOME cannot create .naome/archive safely.");
|
|
72
|
+
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (existsSync(targetPath)) {
|
|
77
|
+
if (!lstatSync(targetPath).isDirectory()) {
|
|
78
|
+
printError(ctx, "NAOME cannot create .naome/archive because that path is not a directory.");
|
|
79
|
+
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ctx.skipped.push(`${archivePath}/`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mkdirSync(targetPath, { recursive: true });
|
|
88
|
+
ctx.installed.push(`${archivePath}/`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function nextArchivePath(ctx, baseName) {
|
|
92
|
+
const archiveDir = ".naome/archive";
|
|
93
|
+
|
|
94
|
+
if (!isArchiveDirectoryAvailable(ctx)) {
|
|
95
|
+
ctx.skipped.push(`${archiveDir}/${baseName}`);
|
|
96
|
+
ctx.unsafeSkipped.push(`${archiveDir}/${baseName}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mkdirSync(join(ctx.targetRoot, archiveDir), { recursive: true });
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < 1000; index += 1) {
|
|
103
|
+
const fileName = index === 0 ? baseName : baseName.replace(/\.md$/, `.${index}.md`);
|
|
104
|
+
const archivePath = join(ctx.targetRoot, archiveDir, fileName);
|
|
105
|
+
|
|
106
|
+
if (!existsSync(archivePath)) {
|
|
107
|
+
return archivePath;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function assertArchiveDirectoryAvailableForTakeover(ctx) {
|
|
115
|
+
if (isArchiveDirectoryAvailable(ctx)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
printError(ctx, "NAOME cannot archive AGENTS.md safely.");
|
|
120
|
+
console.error(".naome/archive must be a regular directory or must not exist.");
|
|
121
|
+
console.error("Fix that path before installing NAOME.");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isArchiveDirectoryAvailable(ctx) {
|
|
126
|
+
const parts = [".naome", "archive"];
|
|
127
|
+
let current = ctx.targetRoot;
|
|
128
|
+
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
current = join(current, part);
|
|
131
|
+
try {
|
|
132
|
+
const stats = lstatSync(current);
|
|
133
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error.code === "ENOENT") {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function hasSymlinkInTargetPath(ctx, relativePath) {
|
|
148
|
+
const parts = relativePath.split(/[\\/]+/);
|
|
149
|
+
let current = ctx.targetRoot;
|
|
150
|
+
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
current = join(current, part);
|
|
153
|
+
try {
|
|
154
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === "ENOENT") {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ensureArchiveDirectory, copyTemplateFile, walk } from "./filesystem.js";
|
|
2
|
+
import {
|
|
3
|
+
ensureBuiltInVerificationChecks,
|
|
4
|
+
ensureCoreHarnessFiles,
|
|
5
|
+
ensureHarnessHealthFiles,
|
|
6
|
+
ensureRepositoryStructurePolicyFiles,
|
|
7
|
+
ensureTaskControlHarnessFiles,
|
|
8
|
+
ensureTestingProofHarnessSections,
|
|
9
|
+
} from "./harness-files.js";
|
|
10
|
+
import { ensureLocalOnlySourceControlBoundary } from "./git-local.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureCompleteUpgradeState,
|
|
13
|
+
patchManifestDate,
|
|
14
|
+
refreshManifestHealthMetadata,
|
|
15
|
+
} from "./manifest-state.js";
|
|
16
|
+
import { installNativeDecisionBinary, patchInstalledMachineOwnedIntegrity } from "./native.js";
|
|
17
|
+
import { printError } from "./output.js";
|
|
18
|
+
import { compareVersions } from "./version.js";
|
|
19
|
+
import { confirmAgentsTakeover, takeoverExistingAgents } from "./agents.js";
|
|
20
|
+
|
|
21
|
+
export async function runFreshInstall(ctx) {
|
|
22
|
+
await confirmAgentsTakeover(ctx);
|
|
23
|
+
|
|
24
|
+
for (const sourcePath of walk(ctx.templateRoot)) {
|
|
25
|
+
copyTemplateFile(ctx, sourcePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
installNativeDecisionBinary(ctx);
|
|
29
|
+
patchInstalledMachineOwnedIntegrity(ctx);
|
|
30
|
+
ensureBuiltInVerificationChecks(ctx);
|
|
31
|
+
patchManifestDate(ctx);
|
|
32
|
+
ensureCompleteUpgradeState(ctx, null);
|
|
33
|
+
ensureArchiveDirectory(ctx);
|
|
34
|
+
takeoverExistingAgents(ctx);
|
|
35
|
+
ensureLocalOnlySourceControlBoundary(ctx);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runExistingInstall(ctx, existingInstall) {
|
|
39
|
+
const comparison = compareVersions(ctx, existingInstall.version, ctx.packageVersion);
|
|
40
|
+
|
|
41
|
+
if (comparison > 0) {
|
|
42
|
+
printError(ctx, "This repository already uses a newer NAOME harness.");
|
|
43
|
+
console.error(`Installed: v${existingInstall.version}`);
|
|
44
|
+
console.error(`Package: v${ctx.packageVersion}`);
|
|
45
|
+
console.error("Run `naome update`, then rerun `naome sync` from the repository root.");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (comparison < 0) {
|
|
50
|
+
if (compareVersions(ctx, existingInstall.version, ctx.minimumSupportedUpgradeVersion) < 0) {
|
|
51
|
+
rejectUnsupportedHistoricalInstall(ctx, existingInstall.version);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ensureArchiveDirectory(ctx);
|
|
55
|
+
runRepair(ctx, existingInstall.version, { fromVersion: existingInstall.version });
|
|
56
|
+
} else {
|
|
57
|
+
ensureArchiveDirectory(ctx);
|
|
58
|
+
runRepair(ctx, existingInstall.version);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rejectUnsupportedHistoricalInstall(ctx, version) {
|
|
63
|
+
printError(ctx, "Installed NAOME harness version is outside the supported upgrade range.");
|
|
64
|
+
console.error(`Installed: v${version}`);
|
|
65
|
+
console.error(`Package: v${ctx.packageVersion}`);
|
|
66
|
+
console.error(`Minimum supported upgrade version: v${ctx.minimumSupportedUpgradeVersion}`);
|
|
67
|
+
console.error("Create a clean NAOME baseline instead of upgrading a pre-Rust harness in place.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runRepair(ctx, version, options = {}) {
|
|
72
|
+
ctx.summaryTitle = "NAOME harness checked";
|
|
73
|
+
ensureCoreHarnessFiles(ctx, `repair-${version}`);
|
|
74
|
+
ensureTaskControlHarnessFiles(ctx, `repair-${version}`);
|
|
75
|
+
ensureHarnessHealthFiles(ctx, `repair-${version}`);
|
|
76
|
+
installNativeDecisionBinary(ctx);
|
|
77
|
+
patchInstalledMachineOwnedIntegrity(ctx);
|
|
78
|
+
ensureBuiltInVerificationChecks(ctx);
|
|
79
|
+
ensureTestingProofHarnessSections(ctx);
|
|
80
|
+
ensureRepositoryStructurePolicyFiles(ctx);
|
|
81
|
+
refreshManifestHealthMetadata(ctx);
|
|
82
|
+
ensureCompleteUpgradeState(ctx, options.fromVersion ?? null);
|
|
83
|
+
ensureLocalOnlySourceControlBoundary(ctx);
|
|
84
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { ensureWritableRegularFile, absoluteGitDir } from "./git-shared.js";
|
|
6
|
+
import { printCommandFailure, printError } from "./output.js";
|
|
7
|
+
|
|
8
|
+
export function ensureLocalOnlySourceControlBoundary(ctx) {
|
|
9
|
+
if (!isInsideGitWorkTree(ctx)) {
|
|
10
|
+
ensureLocalOnlyGitIgnoreFallback(ctx);
|
|
11
|
+
ctx.skipped.push("local git exclude");
|
|
12
|
+
ctx.skipped.push("local-only git index cleanup");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
cleanupLegacyLocalOnlyGitIgnore(ctx);
|
|
17
|
+
ensureLocalOnlyGitExclude(ctx);
|
|
18
|
+
untrackLocalOnlyGitPaths(ctx);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isInsideGitWorkTree(ctx) {
|
|
22
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
23
|
+
cwd: ctx.targetRoot,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return result.status === 0 && result.stdout.trim() === "true";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensureLocalOnlyGitIgnoreFallback(ctx) {
|
|
31
|
+
const relativePath = ".gitignore";
|
|
32
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
33
|
+
ensureWritableRegularFile(ctx, relativePath, ".gitignore");
|
|
34
|
+
|
|
35
|
+
const content = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
|
|
36
|
+
const existingLines = new Set(content.split(/\r?\n/).map((line) => line.trim()));
|
|
37
|
+
const missingEntries = legacyLocalOnlyGitIgnoreEntries(ctx).filter((entry) => !existingLines.has(entry));
|
|
38
|
+
|
|
39
|
+
if (missingEntries.length === 0) {
|
|
40
|
+
ctx.skipped.push(relativePath);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextContent = `${content.trimEnd()}${content.trimEnd() ? "\n\n" : ""}${missingEntries.join("\n")}\n`;
|
|
45
|
+
writeFileSync(targetPath, nextContent);
|
|
46
|
+
if (content.length === 0) {
|
|
47
|
+
ctx.installed.push(relativePath);
|
|
48
|
+
} else {
|
|
49
|
+
ctx.updated.push(relativePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cleanupLegacyLocalOnlyGitIgnore(ctx) {
|
|
54
|
+
const relativePath = ".gitignore";
|
|
55
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
56
|
+
if (!existsSync(targetPath)) {
|
|
57
|
+
ctx.skipped.push(relativePath);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
ensureWritableRegularFile(ctx, relativePath, ".gitignore");
|
|
61
|
+
|
|
62
|
+
const content = readFileSync(targetPath, "utf8");
|
|
63
|
+
const legacyEntries = new Set(legacyLocalOnlyGitIgnoreEntries(ctx));
|
|
64
|
+
const nextContent = cleanBlankLines(
|
|
65
|
+
content
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.filter((line) => !legacyEntries.has(line.trim()))
|
|
68
|
+
.join("\n"),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (nextContent === content) {
|
|
72
|
+
ctx.skipped.push(relativePath);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (nextContent.trim().length === 0) {
|
|
77
|
+
unlinkSync(targetPath);
|
|
78
|
+
} else {
|
|
79
|
+
writeFileSync(targetPath, nextContent);
|
|
80
|
+
}
|
|
81
|
+
ctx.updated.push(relativePath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ensureLocalOnlyGitExclude(ctx) {
|
|
85
|
+
const gitDir = absoluteGitDir(ctx, "local git exclude");
|
|
86
|
+
if (!gitDir) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const excludePath = join(gitDir, "info", "exclude");
|
|
91
|
+
mkdirSync(dirname(excludePath), { recursive: true });
|
|
92
|
+
|
|
93
|
+
if (existsSync(excludePath) && !lstatSync(excludePath).isFile()) {
|
|
94
|
+
printError(ctx, "NAOME cannot update local git exclude because that path is not a file.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const content = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
99
|
+
const existingLines = new Set(content.split(/\r?\n/).map((line) => line.trim()));
|
|
100
|
+
const missingEntries = ctx.localOnlyGitExcludeEntries.filter((entry) => !existingLines.has(entry));
|
|
101
|
+
|
|
102
|
+
if (missingEntries.length === 0) {
|
|
103
|
+
ctx.skipped.push(".git/info/exclude");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const nextContent = `${content.trimEnd()}${content.trimEnd() ? "\n\n" : ""}${missingEntries.join("\n")}\n`;
|
|
108
|
+
writeFileSync(excludePath, nextContent);
|
|
109
|
+
ctx.updated.push(".git/info/exclude");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function untrackLocalOnlyGitPaths(ctx) {
|
|
113
|
+
const tracked = trackedLocalOnlyGitPaths(ctx);
|
|
114
|
+
if (tracked.length === 0) {
|
|
115
|
+
ctx.skipped.push("local-only git index cleanup");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = spawnSync("git", ["rm", "--cached", "-q", "--", ...tracked], {
|
|
120
|
+
cwd: ctx.targetRoot,
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
});
|
|
123
|
+
if (result.status !== 0) {
|
|
124
|
+
printCommandFailure(ctx, "NAOME could not remove local-only harness files from the git index.", result);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ctx.updated.push(...tracked.map((path) => `git index: ${path}`));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function trackedLocalOnlyGitPaths(ctx) {
|
|
131
|
+
const result = spawnSync("git", ["ls-files", "-z", "--", ...ctx.localOnlyGitUntrackPaths], {
|
|
132
|
+
cwd: ctx.targetRoot,
|
|
133
|
+
encoding: "utf8",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (result.status !== 0) {
|
|
137
|
+
printError(ctx, "NAOME could not inspect tracked local-only harness files.");
|
|
138
|
+
if (result.stderr.trim()) {
|
|
139
|
+
console.error(result.stderr.trim());
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function legacyLocalOnlyGitIgnoreEntries(ctx) {
|
|
148
|
+
return [
|
|
149
|
+
"# NAOME local machine-owned harness files.",
|
|
150
|
+
".naome/archive/",
|
|
151
|
+
".naome/bin/naome-rust*",
|
|
152
|
+
...ctx.localOnlyMachineOwnedPaths,
|
|
153
|
+
...ctx.localOnlyGitIgnoreEntries,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cleanBlankLines(content) {
|
|
158
|
+
const lines = content.split(/\r?\n/);
|
|
159
|
+
const cleaned = [];
|
|
160
|
+
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (line.trim() === "" && cleaned.at(-1)?.trim() === "") {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
cleaned.push(line);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const trimmed = cleaned.join("\n").trimEnd();
|
|
169
|
+
return trimmed ? `${trimmed}\n` : "";
|
|
170
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { templateIntegrity } from "./native.js";
|
|
2
|
+
|
|
3
|
+
export function localGitHookContent(ctx, hookName, mode, localNative) {
|
|
4
|
+
const expectedIntegrityJson = JSON.stringify(templateIntegrity(ctx));
|
|
5
|
+
return `#!/bin/sh
|
|
6
|
+
# NAOME managed local git hook: ${hookName}
|
|
7
|
+
# Run naome sync after reviewing harness changes to refresh this hook.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
11
|
+
cd "$REPO_ROOT"
|
|
12
|
+
|
|
13
|
+
NODE_BIN=${shellSingleQuote(process.execPath)}
|
|
14
|
+
if [ ! -x "$NODE_BIN" ]; then
|
|
15
|
+
NODE_BIN="node"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
NATIVE_BIN=${shellSingleQuote(localNative.path)}
|
|
19
|
+
EXPECTED_SHA=${shellSingleQuote(localNative.hash)}
|
|
20
|
+
NAOME_EXPECTED_INTEGRITY_JSON=${shellSingleQuote(expectedIntegrityJson)}
|
|
21
|
+
export NAOME_EXPECTED_INTEGRITY_JSON
|
|
22
|
+
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)"
|
|
23
|
+
|
|
24
|
+
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
|
|
25
|
+
echo "NAOME hook refused to run because its local native binary integrity does not match the installed hook." >&2
|
|
26
|
+
echo "Run naome sync after reviewing harness changes." >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
exec "$NATIVE_BIN" check-task-state --root "$REPO_ROOT" ${mode}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shellSingleQuote(value) {
|
|
35
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { hasSymlinkInTargetPath } from "./filesystem.js";
|
|
6
|
+
import { localGitHookContent } from "./git-hook-content.js";
|
|
7
|
+
import { absoluteGitDir } from "./git-shared.js";
|
|
8
|
+
import { findNativeDecisionBinary, sha256 } from "./native.js";
|
|
9
|
+
|
|
10
|
+
export function ensureExecutableMachineOwnedFiles(ctx) {
|
|
11
|
+
for (const relativePath of ctx.executableMachineOwnedPaths) {
|
|
12
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
13
|
+
if (!existsSync(targetPath) || hasSymlinkInTargetPath(ctx, relativePath)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
chmodSync(targetPath, 0o755);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function configureGitHooks(ctx) {
|
|
22
|
+
const gitCheck = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
23
|
+
cwd: ctx.targetRoot,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (gitCheck.status !== 0 || gitCheck.stdout.trim() !== "true") {
|
|
28
|
+
ctx.skipped.push("local git hooks");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!clearLegacyHooksPath(ctx)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const gitDir = absoluteGitDir(ctx, "local git hooks");
|
|
36
|
+
if (!gitDir) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hooksDir = join(gitDir, "hooks");
|
|
41
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
42
|
+
const localNative = installLocalGitNativeBinary(ctx, gitDir);
|
|
43
|
+
if (!localNative) {
|
|
44
|
+
ctx.skipped.push("local git hooks native binary");
|
|
45
|
+
ctx.unsafeSkipped.push("local git hooks native binary");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
installLocalGitHook(ctx, hooksDir, "pre-commit", "--commit-gate", localNative);
|
|
50
|
+
installLocalGitHook(ctx, hooksDir, "pre-push", "--push-gate", localNative);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearLegacyHooksPath(ctx) {
|
|
54
|
+
const currentHooksPath = spawnSync("git", ["config", "--local", "--get", "core.hooksPath"], {
|
|
55
|
+
cwd: ctx.targetRoot,
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
});
|
|
58
|
+
const currentValue = currentHooksPath.status === 0 ? currentHooksPath.stdout.trim() : "";
|
|
59
|
+
|
|
60
|
+
if (currentValue && currentValue !== ".naome/git-hooks") {
|
|
61
|
+
ctx.skipped.push(`git core.hooksPath (${currentValue})`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (currentValue !== ".naome/git-hooks") {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const unsetResult = spawnSync("git", ["config", "--local", "--unset", "core.hooksPath"], {
|
|
69
|
+
cwd: ctx.targetRoot,
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
});
|
|
72
|
+
if (unsetResult.status !== 0) {
|
|
73
|
+
ctx.skipped.push("git core.hooksPath unset");
|
|
74
|
+
ctx.unsafeSkipped.push("git core.hooksPath unset");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ctx.updated.push("git core.hooksPath unset");
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function installLocalGitNativeBinary(ctx, gitDir) {
|
|
83
|
+
const sourcePath = findNativeDecisionBinary(ctx);
|
|
84
|
+
if (!sourcePath) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const localDir = join(gitDir, "naome");
|
|
89
|
+
const localPath = join(localDir, process.platform === "win32" ? "naome-rust.exe" : "naome-rust");
|
|
90
|
+
const sourceContent = readFileSync(sourcePath);
|
|
91
|
+
const sourceHash = sha256(sourceContent);
|
|
92
|
+
mkdirSync(localDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
const currentHash =
|
|
95
|
+
existsSync(localPath) && lstatSync(localPath).isFile() ? sha256(readFileSync(localPath)) : null;
|
|
96
|
+
if (currentHash !== sourceHash) {
|
|
97
|
+
copyFileSync(sourcePath, localPath);
|
|
98
|
+
ctx.updated.push(".git/naome/naome-rust");
|
|
99
|
+
} else {
|
|
100
|
+
ctx.skipped.push(".git/naome/naome-rust");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
chmodSync(localPath, 0o755);
|
|
104
|
+
return { path: localPath, hash: sourceHash };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function installLocalGitHook(ctx, hooksDir, hookName, mode, localNative) {
|
|
108
|
+
const hookPath = join(hooksDir, hookName);
|
|
109
|
+
const nextContent = localGitHookContent(ctx, hookName, mode, localNative);
|
|
110
|
+
|
|
111
|
+
if (existsSync(hookPath) && !canReplaceLocalHook(ctx, hookPath, hookName, nextContent)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeFileSync(hookPath, nextContent);
|
|
116
|
+
chmodSync(hookPath, 0o755);
|
|
117
|
+
ctx.updated.push(`.git/hooks/${hookName}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function canReplaceLocalHook(ctx, hookPath, hookName, nextContent) {
|
|
121
|
+
const stats = lstatSync(hookPath);
|
|
122
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
123
|
+
ctx.skipped.push(`.git/hooks/${hookName}`);
|
|
124
|
+
ctx.unsafeSkipped.push(`.git/hooks/${hookName}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const currentContent = readFileSync(hookPath, "utf8");
|
|
129
|
+
if (!currentContent.includes("NAOME managed local git hook") || currentContent === nextContent) {
|
|
130
|
+
ctx.skipped.push(`.git/hooks/${hookName}`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { hasSymlinkInTargetPath } from "./filesystem.js";
|
|
6
|
+
import { printError } from "./output.js";
|
|
7
|
+
|
|
8
|
+
export function absoluteGitDir(ctx, label) {
|
|
9
|
+
const result = spawnSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
10
|
+
cwd: ctx.targetRoot,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
15
|
+
ctx.skipped.push(label);
|
|
16
|
+
ctx.unsafeSkipped.push(label);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return result.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ensureWritableRegularFile(ctx, relativePath, label) {
|
|
24
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
25
|
+
if (hasSymlinkInTargetPath(ctx, relativePath)) {
|
|
26
|
+
printError(ctx, `NAOME cannot write ${label} safely.`);
|
|
27
|
+
console.error(`${label} must be a regular file or must not exist.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (existsSync(targetPath) && !lstatSync(targetPath).isFile()) {
|
|
32
|
+
printError(ctx, `NAOME cannot update ${label} because that path is not a file.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|