@nurix/nustack 0.23.11 → 0.23.13

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.
Files changed (63) hide show
  1. package/dist/commands/check.js +73 -0
  2. package/dist/commands/deploy.js +2 -1
  3. package/dist/commands/doctor.js +2 -1
  4. package/dist/commands/harness.js +127 -48
  5. package/dist/commands/init.js +103 -3
  6. package/dist/commands/login.js +9 -2
  7. package/dist/commands/mcp.js +7 -0
  8. package/dist/commands/project-sync.js +2 -1
  9. package/dist/commands/records.js +10 -0
  10. package/dist/commands/review.js +3 -2
  11. package/dist/commands/sessions.js +4 -1
  12. package/dist/commands/sync.js +2 -1
  13. package/dist/index.js +4 -0
  14. package/dist/lib/agent/claudeHooks.js +4 -1
  15. package/dist/lib/auth_grant.js +15 -3
  16. package/dist/lib/auth_recovery.js +10 -3
  17. package/dist/lib/etna/manager.js +24 -6
  18. package/dist/lib/etna/profile.js +6 -0
  19. package/dist/lib/etna/reconcile.js +6 -0
  20. package/dist/lib/harness/codes.js +24 -0
  21. package/dist/lib/harness/hookCheck.js +325 -0
  22. package/dist/lib/harness/hookEntry.js +27 -0
  23. package/dist/lib/harness/legacyMount.js +231 -0
  24. package/dist/lib/harness/mount.js +199 -131
  25. package/dist/lib/harness/storeWaivers.js +43 -0
  26. package/dist/lib/harness/vendor.js +29 -0
  27. package/dist/lib/harness/versionsClient.js +54 -0
  28. package/dist/lib/local_store/database.js +18 -0
  29. package/dist/lib/nustack_client.js +35 -2
  30. package/dist/lib/onboarding/attach_existing_git.js +14 -2
  31. package/dist/lib/onboarding/binding.js +46 -2
  32. package/dist/lib/onboarding/commit_trailer_hook.js +66 -0
  33. package/dist/lib/onboarding/manifest_writer.js +19 -3
  34. package/dist/lib/onboarding/session_hook.js +76 -0
  35. package/dist/lib/origin_guard.js +43 -0
  36. package/dist/lib/project_contract.js +104 -0
  37. package/dist/lib/records/door.js +26 -19
  38. package/dist/lib/records/drain.js +10 -4
  39. package/dist/lib/records/refusalLog.js +30 -1
  40. package/dist/lib/session-engine/supervisor.js +6 -18
  41. package/dist/lib/sessions/client.js +0 -12
  42. package/dist/program.js +14 -3
  43. package/dist/vendor/harness/assemble/runtimes.js +32 -0
  44. package/dist/vendor/harness/assemble/settingsMerge.js +136 -0
  45. package/dist/vendor/harness/digest/index.js +38 -0
  46. package/dist/vendor/harness/lib/findings.js +32 -0
  47. package/dist/vendor/harness/lib/frontmatter.js +79 -0
  48. package/dist/vendor/harness/lib/paths.js +31 -0
  49. package/dist/vendor/harness/mount/cache.js +65 -0
  50. package/dist/vendor/harness/mount/exclude.js +77 -0
  51. package/dist/vendor/harness/mount/files.js +38 -0
  52. package/dist/vendor/harness/mount/index.js +352 -0
  53. package/dist/vendor/harness/rules/changeRules.js +162 -0
  54. package/dist/vendor/harness/rules/context.js +38 -0
  55. package/dist/vendor/harness/rules/docsRoot.js +34 -0
  56. package/dist/vendor/harness/rules/index.js +109 -0
  57. package/dist/vendor/harness/rules/node.js +144 -0
  58. package/dist/vendor/harness/rules/outputGate.js +28 -0
  59. package/dist/vendor/harness/rules/ownership.js +178 -0
  60. package/dist/vendor/harness/rules/packageTable.js +113 -0
  61. package/dist/vendor/harness/rules/plans.js +96 -0
  62. package/dist/vendor/harness/rules/sessionWalls.js +31 -0
  63. package/package.json +5 -5
@@ -0,0 +1,65 @@
1
+ // packages/harness/src/mount/cache.ts - The machine's digest-keyed harness cache: `<cacheDir>/<digest>/files/<prefixed
2
+ // path>` plus `<cacheDir>/<digest>/manifest.json`, written last as the completeness mark. Every read is verified;
3
+ // a folder that fails is evicted. docs/architecture/systems/harness-records.md speaks for it (Offline mount).
4
+ import { homedir } from 'node:os';
5
+ import { readdirSync, rmSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { readBytes, writeAtomic } from './files.js';
8
+ export const HARNESS_CACHE_DIR_ENV = 'NUSTACK_HARNESS_CACHE_DIR';
9
+ const DIGEST_NAME = /^[0-9a-f]{64}$/;
10
+ /** The machine cache every repository shares: NUSTACK_HARNESS_CACHE_DIR, else ~/.nustack/cache/harness. The only reader of the variable. */
11
+ export function defaultHarnessCacheDir(env = process.env) {
12
+ return env[HARNESS_CACHE_DIR_ENV]?.trim() || join(homedir(), '.nustack', 'cache', 'harness');
13
+ }
14
+ export const cacheFolder = (cacheDir, digest) => join(cacheDir, digest);
15
+ const manifestFile = (cacheDir, digest) => join(cacheFolder(cacheDir, digest), 'manifest.json');
16
+ const objectFile = (cacheDir, digest, entry) => join(cacheFolder(cacheDir, digest), 'files', ...entry.path.split('/'));
17
+ /** The folder's manifest, or null when the folder is half-written (no manifest) or the file does not parse. */
18
+ export function readCachedManifest(cacheDir, digest) {
19
+ const bytes = readBytes(manifestFile(cacheDir, digest));
20
+ if (!bytes)
21
+ return null;
22
+ try {
23
+ const parsed = JSON.parse(bytes.toString('utf8'));
24
+ return parsed && typeof parsed === 'object' && Array.isArray(parsed.files) ? parsed : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /** A cached object's bytes, unverified; null when the folder lacks it. */
31
+ export const readCachedObject = (cacheDir, digest, entry) => readBytes(objectFile(cacheDir, digest, entry));
32
+ export function writeCachedObject(cacheDir, digest, entry, bytes) {
33
+ writeAtomic(objectFile(cacheDir, digest, entry), bytes);
34
+ }
35
+ /** Writes the completeness mark last, keeping the first `cachedAt` and the union of cached runtimes. */
36
+ export function writeCachedManifest(cacheDir, manifest, runtime) {
37
+ const prior = readCachedManifest(cacheDir, manifest.digest);
38
+ const cachedRuntimes = [...new Set([...(prior?.cachedRuntimes ?? []), runtime])].sort();
39
+ const body = { ...manifest, cachedAt: prior?.cachedAt ?? new Date().toISOString(), cachedRuntimes };
40
+ writeAtomic(manifestFile(cacheDir, manifest.digest), `${JSON.stringify(body, null, 2)}\n`);
41
+ }
42
+ export function evictCacheFolder(cacheDir, digest) {
43
+ rmSync(cacheFolder(cacheDir, digest), { recursive: true, force: true });
44
+ }
45
+ /** The newest cached version for a project: 64-hex folders holding a complete manifest of that project, latest cachedAt. */
46
+ export function newestCachedDigest(cacheDir, projectId) {
47
+ let names;
48
+ try {
49
+ names = readdirSync(cacheDir);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ let best = null;
55
+ for (const name of names) {
56
+ if (!DIGEST_NAME.test(name))
57
+ continue;
58
+ const manifest = readCachedManifest(cacheDir, name);
59
+ if (!manifest || manifest.projectId !== projectId || typeof manifest.cachedAt !== 'string')
60
+ continue;
61
+ if (!best || manifest.cachedAt > best.cachedAt)
62
+ best = { digest: name, semver: String(manifest.semver ?? ''), cachedAt: manifest.cachedAt };
63
+ }
64
+ return best ? { digest: best.digest, semver: best.semver } : null;
65
+ }
@@ -0,0 +1,77 @@
1
+ // packages/harness/src/mount/exclude.ts - Keeps the mounted harness out of git without editing a tracked file: the
2
+ // managed block in the repository's info/exclude (the git directory followed through a worktree's gitdir and
3
+ // commondir), and the one git call that tells a tracked merge target from an untracked one.
4
+ // docs/architecture/systems/harness-records.md speaks for it (Harness mount).
5
+ import { execFileSync } from 'node:child_process';
6
+ import { existsSync, readFileSync, statSync } from 'node:fs';
7
+ import { isAbsolute, join, resolve } from 'node:path';
8
+ import { writeAtomic } from './files.js';
9
+ export const MOUNT_EXCLUDE_START = '# nustack:mount:start';
10
+ export const MOUNT_EXCLUDE_END = '# nustack:mount:end';
11
+ /** The repository's git directory and its common directory (the same folder outside a worktree); null outside git. */
12
+ export function resolveGitDirs(root) {
13
+ const dotGit = join(root, '.git');
14
+ let gitDir;
15
+ try {
16
+ if (statSync(dotGit).isDirectory())
17
+ gitDir = dotGit;
18
+ else {
19
+ const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, 'utf8'));
20
+ if (!pointer)
21
+ return null;
22
+ gitDir = resolve(root, pointer[1].trim());
23
+ }
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ let commonDir = gitDir;
29
+ try {
30
+ const common = readFileSync(join(gitDir, 'commondir'), 'utf8').trim();
31
+ if (common)
32
+ commonDir = isAbsolute(common) ? common : resolve(gitDir, common);
33
+ }
34
+ catch {
35
+ /* no commondir: not a linked worktree */
36
+ }
37
+ return { gitDir, commonDir };
38
+ }
39
+ /** The block's lines: a folder per skill or hook library, a file per agent, rule or merge target. */
40
+ export function excludeBlockLines(paths) {
41
+ const names = new Set();
42
+ for (const path of paths) {
43
+ const parts = path.split('/');
44
+ names.add(parts.length > 3 ? `${parts.slice(0, 3).join('/')}/` : path);
45
+ }
46
+ return [MOUNT_EXCLUDE_START, '# Managed by nustack: the mounted harness, rewritten on every mount. Edit outside this block.', ...[...names].sort(), MOUNT_EXCLUDE_END];
47
+ }
48
+ /** Rewrites the managed block in place (appended when absent), leaving every line outside it untouched. False outside git. */
49
+ export function writeExcludeBlock(root, paths) {
50
+ const dirs = resolveGitDirs(root);
51
+ if (!dirs)
52
+ return false;
53
+ const file = join(dirs.commonDir, 'info', 'exclude');
54
+ const before = existsSync(file) ? readFileSync(file, 'utf8') : '';
55
+ const lines = before === '' ? [] : before.replace(/\n$/, '').split('\n');
56
+ const start = lines.findIndex((line) => line.trim() === MOUNT_EXCLUDE_START);
57
+ const end = start >= 0 ? lines.findIndex((line, index) => index > start && line.trim() === MOUNT_EXCLUDE_END) : -1;
58
+ const block = excludeBlockLines(paths);
59
+ const next = start >= 0 && end > start ? [...lines.slice(0, start), ...block, ...lines.slice(end + 1)] : [...lines, ...block];
60
+ const after = `${next.join('\n')}\n`;
61
+ if (after === before)
62
+ return true;
63
+ writeAtomic(file, after);
64
+ return true;
65
+ }
66
+ /** The merge targets git tracks, by one `git ls-files` call; a failed call counts every target as tracked. */
67
+ export function trackedMergeTargets(root, targets) {
68
+ if (targets.length === 0)
69
+ return new Set();
70
+ try {
71
+ const out = execFileSync('git', ['ls-files', '-z', '--', ...targets], { cwd: root, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] });
72
+ return new Set(out.split('\0').filter(Boolean));
73
+ }
74
+ catch {
75
+ return new Set(targets);
76
+ }
77
+ }
@@ -0,0 +1,38 @@
1
+ // packages/harness/src/mount/files.ts - The mount's filesystem floor: atomic writes, byte reads and hashes, and the
2
+ // removal of folders a prune empties. docs/architecture/systems/harness-records.md speaks for it (Harness mount).
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, writeFileSync } from 'node:fs';
5
+ import { basename, dirname, join } from 'node:path';
6
+ export const sha256Of = (bytes) => createHash('sha256').update(bytes).digest('hex');
7
+ export function readBytes(file) {
8
+ try {
9
+ return readFileSync(file);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ let sequence = 0;
16
+ /** Temp file then rename, so a concurrent reader sees the old file or the new one, never a torn one. */
17
+ export function writeAtomic(file, bytes) {
18
+ mkdirSync(dirname(file), { recursive: true });
19
+ sequence += 1;
20
+ const temp = join(dirname(file), `.${basename(file)}.mount-${process.pid}-${sequence}`);
21
+ writeFileSync(temp, bytes);
22
+ renameSync(temp, file);
23
+ }
24
+ /** Removes the now-empty folders above a deleted repository path, never a top-level folder or its first child. */
25
+ export function removeEmptyParents(root, relPath) {
26
+ let dir = dirname(relPath);
27
+ while (dir !== '.' && dir.split('/').length > 2) {
28
+ try {
29
+ if (readdirSync(join(root, dir)).length > 0)
30
+ return;
31
+ rmdirSync(join(root, dir));
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ dir = dirname(dir);
37
+ }
38
+ }
@@ -0,0 +1,352 @@
1
+ // packages/harness/src/mount/index.ts - Mounts one runtime's half of a harness version into a repository: the digest
2
+ // verified over the whole manifest, every object verified (fetched or cached) before the first write, writes deepest
3
+ // path first, contract and seed files kept when a person owns them, merge targets folded, the unmodified files a
4
+ // version drops pruned, the mounted paths kept out of git, and the mount record written by its one writer.
5
+ // Ported from etna packages/cli/lib/install/mount.mjs and packages/cli/src/lib/harness/mount.ts.
6
+ // docs/architecture/systems/harness-records.md speaks for it (Harness mount).
7
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { DOCS_ROOT_TOKEN, INSTRUCTION_FILE_OF, isRuntime, MERGE_TARGET_OF, splitRuntimePath } from '../assemble/runtimes.js';
10
+ import { mergeClaudeSettings, mergeCodexHooks, mergeMcpServers } from '../assemble/settingsMerge.js';
11
+ import { contentDigest } from '../digest/index.js';
12
+ import { depthOf, isSafeRelativePath } from '../lib/paths.js';
13
+ import { resolveDocsRoot } from '../rules/docsRoot.js';
14
+ import { defaultHarnessCacheDir, evictCacheFolder, newestCachedDigest, readCachedManifest, readCachedObject, writeCachedManifest, writeCachedObject, } from './cache.js';
15
+ import { trackedMergeTargets, writeExcludeBlock } from './exclude.js';
16
+ import { readBytes, removeEmptyParents, sha256Of, writeAtomic } from './files.js';
17
+ export { FINDING_CODES } from '../lib/findings.js';
18
+ export { defaultHarnessCacheDir, newestCachedDigest, HARNESS_CACHE_DIR_ENV } from './cache.js';
19
+ export { MOUNT_EXCLUDE_START, MOUNT_EXCLUDE_END, resolveGitDirs, excludeBlockLines } from './exclude.js';
20
+ export const MOUNT_RECORD_PATH = '.claude/state/mount.json';
21
+ export const NO_ROUTE_LINE = 'harness not mounted: no route to NuStack and no cached version';
22
+ const SHA256 = /^[0-9a-f]{64}$/;
23
+ const FETCH_CONCURRENCY = 8;
24
+ const MERGE_OF = { 'claude-settings': mergeClaudeSettings, 'codex-hooks': mergeCodexHooks, 'mcp-servers': mergeMcpServers };
25
+ /** The mount record, or null when none parses; an older record parses as one with no `runtimes`. */
26
+ export function readMountRecord(root) {
27
+ const bytes = readBytes(join(root, MOUNT_RECORD_PATH));
28
+ if (!bytes)
29
+ return null;
30
+ try {
31
+ const parsed = JSON.parse(bytes.toString('utf8'));
32
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /** A repository path the mount may write: safe, outside `.git` and outside the mount's own state folder. */
39
+ function isWritablePath(path) {
40
+ return isSafeRelativePath(path) && !path.split('/').includes('.git') && !path.startsWith('.claude/state/');
41
+ }
42
+ /** The first reason the manifest must be refused, before anything is fetched or written. */
43
+ function manifestProblem(manifest, expectedDigest) {
44
+ if (!manifest || typeof manifest !== 'object' || manifest.schemaVersion !== 1)
45
+ return { code: 'mount.schema_unknown', message: `schemaVersion ${String(manifest?.schemaVersion)} is not 1` };
46
+ if (!Array.isArray(manifest.files))
47
+ return { code: 'mount.schema_unknown', message: 'the manifest lists no files' };
48
+ for (const entry of manifest.files) {
49
+ if (typeof entry?.sha256 !== 'string' || !SHA256.test(entry.sha256))
50
+ return { code: 'mount.bad_sha256', message: `${String(entry?.path)}: sha256 is not 64 lowercase hex`, path: String(entry?.path) };
51
+ const split = typeof entry.path === 'string' && isSafeRelativePath(entry.path) ? splitRuntimePath(entry.path) : null;
52
+ if (!split)
53
+ return { code: 'mount.unsafe_path', message: `${String(entry.path)}: not a safe <runtime>/<path>`, path: String(entry.path) };
54
+ const placed = entry.policy === 'seed' ? (split.path.startsWith(`${DOCS_ROOT_TOKEN}/`) ? split.path.slice(DOCS_ROOT_TOKEN.length + 1) : '') : split.path;
55
+ if (!isWritablePath(placed) || (entry.policy !== 'seed' && placed.startsWith('@')))
56
+ return { code: 'mount.unsafe_path', message: `${entry.path}: unsafe repository path`, path: entry.path };
57
+ if (entry.policy === 'contract' && split.path !== INSTRUCTION_FILE_OF[split.runtime])
58
+ return { code: 'mount.contract_outside', message: `${entry.path}: a contract entry outside ${split.runtime}'s instruction file`, path: entry.path };
59
+ if (entry.policy !== 'mount' && entry.policy !== 'contract' && entry.policy !== 'seed')
60
+ return { code: 'mount.schema_unknown', message: `${entry.path}: unknown policy ${String(entry.policy)}`, path: entry.path };
61
+ if (entry.merge !== undefined && (entry.policy !== 'mount' || MERGE_TARGET_OF[entry.merge] !== split.path))
62
+ return { code: 'mount.unsafe_path', message: `${entry.path}: merge ${String(entry.merge)} at the wrong path`, path: entry.path };
63
+ }
64
+ if (contentDigest(manifest.files) !== manifest.digest)
65
+ return { code: 'mount.digest_mismatch', message: 'the manifest digest does not match its files' };
66
+ if (expectedDigest && manifest.digest !== expectedDigest)
67
+ return { code: 'mount.expected_digest_mismatch', message: `the manifest digest ${manifest.digest} is not the expected ${expectedDigest}` };
68
+ return null;
69
+ }
70
+ /** Runs task over items with at most `limit` in flight. */
71
+ async function eachLimited(items, limit, task) {
72
+ let next = 0;
73
+ const worker = async () => {
74
+ while (next < items.length) {
75
+ const item = items[next];
76
+ next += 1;
77
+ await task(item);
78
+ }
79
+ };
80
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
81
+ }
82
+ export async function mountVersion(options) {
83
+ const runtime = (options.runtime ?? 'claude-code');
84
+ const root = options.root;
85
+ const cacheDir = options.cacheDir ?? defaultHarnessCacheDir();
86
+ const writeContract = options.writeContract ?? 'when-unmodified';
87
+ const result = { status: 'unavailable', digest: null, semver: null, runtime, source: null, written: [], kept: [], removed: [], contractDrift: [], findings: [], line: '' };
88
+ const finish = (status, line, finding) => {
89
+ if (finding)
90
+ result.findings.push({ code: finding.code, message: finding.message, ...(finding.path ? { path: finding.path } : {}) });
91
+ return { ...result, status, line };
92
+ };
93
+ const refuse = (finding) => finish('refused', `harness not mounted: ${finding.message}`, finding);
94
+ if (!isRuntime(runtime))
95
+ return refuse({ code: 'mount.runtime_unknown', message: `unknown runtime ${String(runtime)}` });
96
+ // The manifest: the caller's, verified; else the cache entry the expected digest or the project's newest names.
97
+ let manifest;
98
+ let source;
99
+ if (options.manifest) {
100
+ const problem = manifestProblem(options.manifest, options.expectedDigest);
101
+ if (problem)
102
+ return refuse(problem);
103
+ manifest = options.manifest;
104
+ source = 'network';
105
+ }
106
+ else {
107
+ const projectId = options.projectId;
108
+ let digest = options.expectedDigest;
109
+ if (!digest) {
110
+ if (!projectId)
111
+ return refuse({ code: 'mount.project_unknown', message: 'no manifest, no expected digest and no projectId to find a cached version by' });
112
+ digest = newestCachedDigest(cacheDir, projectId)?.digest ?? null;
113
+ }
114
+ const cached = digest ? readCachedManifest(cacheDir, digest) : null;
115
+ if (!digest || !cached)
116
+ return finish('unavailable', NO_ROUTE_LINE, { code: 'mount.no_route', message: NO_ROUTE_LINE });
117
+ const problem = manifestProblem(cached, digest);
118
+ if (problem || cached.digest !== digest) {
119
+ evictCacheFolder(cacheDir, digest);
120
+ return refuse({ code: 'mount.cache_corrupt', message: `the cached version ${digest} failed verification (${problem?.message ?? 'its digest is not its folder'}) and was evicted` });
121
+ }
122
+ manifest = cached;
123
+ source = 'cache';
124
+ }
125
+ result.digest = manifest.digest;
126
+ result.semver = manifest.semver;
127
+ result.source = source;
128
+ const planned = manifest.files.flatMap((entry) => {
129
+ const split = splitRuntimePath(entry.path);
130
+ return split && split.runtime === runtime ? [{ entry, path: split.path }] : [];
131
+ });
132
+ const record = readMountRecord(root) ?? {};
133
+ const prior = record.runtimes?.[runtime];
134
+ // Current: the record names this digest for this runtime and every recorded file still matches.
135
+ if (prior && prior.digest === manifest.digest && Array.isArray(prior.files) && prior.files.every((file) => sha256Of(readBytes(join(root, file.path)) ?? '\0missing') === file.sha256)) {
136
+ result.kept = prior.files.map((file) => file.path);
137
+ return finish('current', `harness current: ${manifest.semver} (${manifest.digest.slice(0, 12)}) for ${runtime}`);
138
+ }
139
+ const docsRoot = resolveDocsRoot({ read: (path) => readBytes(join(root, path))?.toString('utf8') ?? null });
140
+ const placedPath = (item) => (item.entry.policy === 'seed' ? `${docsRoot}/${item.path.slice(DOCS_ROOT_TOKEN.length + 1)}` : item.path);
141
+ const onDisk = new Map();
142
+ const disk = (path) => {
143
+ if (!onDisk.has(path))
144
+ onDisk.set(path, readBytes(join(root, path)));
145
+ return onDisk.get(path);
146
+ };
147
+ const priorSha = new Map((prior?.files ?? []).map((file) => [file.path, file.sha256]));
148
+ // Decide what each entry needs before fetching anything: only the bytes a write uses are fetched.
149
+ const needed = [];
150
+ const unchanged = [];
151
+ for (const item of planned) {
152
+ const target = placedPath(item);
153
+ const held = disk(target);
154
+ const heldSha = held ? sha256Of(held) : null;
155
+ if (item.entry.merge)
156
+ needed.push(item);
157
+ else if (item.entry.policy === 'mount') {
158
+ if (heldSha === item.entry.sha256)
159
+ unchanged.push(target);
160
+ else
161
+ needed.push(item);
162
+ }
163
+ else if (item.entry.policy === 'seed') {
164
+ if (held)
165
+ result.kept.push(target);
166
+ else if (writeContract !== 'never')
167
+ needed.push(item);
168
+ }
169
+ else if (heldSha === item.entry.sha256)
170
+ unchanged.push(target);
171
+ else if (writeContract === 'never') {
172
+ if (held)
173
+ result.kept.push(target);
174
+ }
175
+ else if (!held || writeContract === 'always' || priorSha.get(target) === heldSha)
176
+ needed.push(item);
177
+ else {
178
+ result.kept.push(target);
179
+ result.contractDrift.push(target);
180
+ result.findings.push({ code: 'mount.contract_edited', message: `${target} differs from what the last mount wrote; left as edited`, path: target });
181
+ }
182
+ }
183
+ // Every needed object, from the cache (verified) or fetched (verified), before the first write.
184
+ const bytesOf = new Map();
185
+ const fetched = [];
186
+ for (const item of needed) {
187
+ const cached = readCachedObject(cacheDir, manifest.digest, item.entry);
188
+ if (cached && sha256Of(cached) === item.entry.sha256)
189
+ bytesOf.set(item.entry.path, cached);
190
+ else if (source === 'cache') {
191
+ if (cached) {
192
+ evictCacheFolder(cacheDir, manifest.digest);
193
+ return refuse({ code: 'mount.cache_corrupt', message: `the cached ${item.entry.path} does not match its entry; the cached version was evicted`, path: item.entry.path });
194
+ }
195
+ return finish('unavailable', `harness not mounted: no route to NuStack and the cached version lacks ${item.entry.path}`, { code: 'mount.no_route', message: `the cached version lacks ${item.entry.path}`, path: item.entry.path });
196
+ }
197
+ else
198
+ fetched.push(item);
199
+ }
200
+ let failure = null;
201
+ await eachLimited(fetched, FETCH_CONCURRENCY, async (item) => {
202
+ if (failure)
203
+ return;
204
+ try {
205
+ const bytes = await options.fetchObject(item.entry);
206
+ if (sha256Of(bytes) !== item.entry.sha256)
207
+ failure ??= { code: 'mount.object_mismatch', message: `${item.entry.path}: the object does not match its sha256`, path: item.entry.path };
208
+ else
209
+ bytesOf.set(item.entry.path, bytes);
210
+ }
211
+ catch (error) {
212
+ failure ??= { code: 'mount.fetch_failed', message: `fetching ${item.entry.path} failed: ${error instanceof Error ? error.message : String(error)}`, path: item.entry.path };
213
+ }
214
+ });
215
+ if (failure) {
216
+ const found = failure;
217
+ return found.code === 'mount.fetch_failed' ? finish('unavailable', `harness not mounted: ${found.message}`, found) : refuse(found);
218
+ }
219
+ // The cache: fetched objects, then the manifest last as the completeness mark.
220
+ for (const item of fetched)
221
+ writeCachedObject(cacheDir, manifest.digest, item.entry, bytesOf.get(item.entry.path));
222
+ if (source === 'network')
223
+ writeCachedManifest(cacheDir, manifest, runtime);
224
+ // Merge targets: tracked ones are left alone; an unparsable one is left alone; the rest fold the fragment in.
225
+ const merges = needed.filter((item) => item.entry.merge);
226
+ const mergeTargets = [...new Set([...merges.map((item) => item.path), ...Object.keys(prior?.managed ?? {})])];
227
+ const tracked = trackedMergeTargets(root, mergeTargets);
228
+ const managed = {};
229
+ const createdMergeTargets = new Set((prior?.createdMergeTargets ?? []).filter((path) => existsSync(join(root, path))));
230
+ const mergeWrites = [];
231
+ for (const target of mergeTargets) {
232
+ const item = merges.find((candidate) => candidate.path === target);
233
+ const kind = (item?.entry.merge ?? Object.entries(MERGE_TARGET_OF).find(([, path]) => path === target)?.[0]);
234
+ if (!kind)
235
+ continue;
236
+ if (tracked.has(target)) {
237
+ result.findings.push({ code: 'mount.merge_target_tracked', message: `${target} is tracked by git; left untouched`, path: target });
238
+ continue;
239
+ }
240
+ const held = disk(target);
241
+ let existing = null;
242
+ if (held) {
243
+ try {
244
+ const parsed = JSON.parse(held.toString('utf8'));
245
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
246
+ throw new Error('not an object');
247
+ existing = parsed;
248
+ }
249
+ catch {
250
+ result.findings.push({ code: 'mount.merge_target_unparsable', message: `${target} does not parse as a JSON object; left untouched`, path: target });
251
+ continue;
252
+ }
253
+ }
254
+ let fragment = {};
255
+ if (item) {
256
+ try {
257
+ fragment = JSON.parse(Buffer.from(bytesOf.get(item.entry.path)).toString('utf8'));
258
+ }
259
+ catch {
260
+ return refuse({ code: 'mount.object_mismatch', message: `${item.entry.path}: the merge fragment is not JSON`, path: item.entry.path });
261
+ }
262
+ }
263
+ const merged = MERGE_OF[kind](existing, fragment, prior?.managed?.[target]);
264
+ if (Object.keys(merged.managed).length > 0)
265
+ managed[target] = merged.managed;
266
+ const content = `${JSON.stringify(merged.merged, null, 2)}\n`;
267
+ if (held && held.toString('utf8') === content)
268
+ continue;
269
+ if (!held) {
270
+ if (!item)
271
+ continue;
272
+ createdMergeTargets.add(target);
273
+ }
274
+ mergeWrites.push({ target, content });
275
+ }
276
+ // All writes, deepest path first, so a module lands before the file importing it.
277
+ const writes = needed.filter((item) => !item.entry.merge).map((item) => ({ target: placedPath(item), bytes: bytesOf.get(item.entry.path) }));
278
+ writes.sort((a, b) => depthOf(b.target) - depthOf(a.target) || (a.target < b.target ? -1 : a.target > b.target ? 1 : 0));
279
+ for (const write of writes) {
280
+ writeAtomic(join(root, write.target), write.bytes);
281
+ result.written.push(write.target);
282
+ }
283
+ for (const write of mergeWrites) {
284
+ writeAtomic(join(root, write.target), write.content);
285
+ result.written.push(write.target);
286
+ }
287
+ result.kept.push(...unchanged);
288
+ // What this runtime now holds: mount and contract files whose bytes equal their entry.
289
+ const files = [];
290
+ for (const item of planned) {
291
+ if (item.entry.merge || item.entry.policy === 'seed')
292
+ continue;
293
+ const target = placedPath(item);
294
+ const held = result.written.includes(target) ? item.entry.sha256 : sha256Of(disk(target) ?? '\0missing');
295
+ if (held === item.entry.sha256)
296
+ files.push({ path: target, sha256: item.entry.sha256, policy: item.entry.policy });
297
+ }
298
+ // Prune: a file the previous mount of this runtime recorded and this version omits, still unmodified, and no
299
+ // other runtime's record listing it.
300
+ const now = new Set(files.map((file) => file.path));
301
+ const otherRuntimes = Object.entries(record.runtimes ?? {}).filter(([name]) => name !== runtime);
302
+ const listedElsewhere = (path) => otherRuntimes.some(([, other]) => (other?.files ?? []).some((file) => file.path === path));
303
+ for (const old of prior?.files ?? []) {
304
+ if (old.policy !== 'mount' || now.has(old.path) || planned.some((item) => placedPath(item) === old.path))
305
+ continue;
306
+ const held = readBytes(join(root, old.path));
307
+ if (!held)
308
+ continue;
309
+ if (sha256Of(held) !== old.sha256) {
310
+ result.kept.push(old.path);
311
+ result.findings.push({ code: 'mount.kept_edited', message: `${old.path} was dropped by this version but edited since the last mount; left in place`, path: old.path });
312
+ continue;
313
+ }
314
+ if (listedElsewhere(old.path))
315
+ continue;
316
+ rmSync(join(root, old.path), { force: true });
317
+ removeEmptyParents(root, old.path);
318
+ result.removed.push(old.path);
319
+ }
320
+ // Out of git: every runtime's mounted files and the merge targets a mount created.
321
+ const runtimes = { ...(record.runtimes ?? {}) };
322
+ const mountedAt = new Date().toISOString();
323
+ runtimes[runtime] = {
324
+ digest: manifest.digest,
325
+ treeDigest: contentDigest(planned.filter((item) => !item.entry.merge && item.entry.policy !== 'seed').map((item) => ({ path: placedPath(item), sha256: item.entry.sha256 }))),
326
+ semver: manifest.semver,
327
+ source,
328
+ mountedAt,
329
+ files,
330
+ managed,
331
+ createdMergeTargets: [...createdMergeTargets].sort(),
332
+ };
333
+ const excluded = Object.values(runtimes).flatMap((entry) => [...(entry?.files ?? []).filter((file) => file.policy === 'mount').map((file) => file.path), ...(entry?.createdMergeTargets ?? [])]);
334
+ writeExcludeBlock(root, excluded);
335
+ // The record, by its one writer: unknown top-level keys kept, the caller's extras beside the owned keys.
336
+ const stateDir = join(root, '.claude', 'state');
337
+ mkdirSync(stateDir, { recursive: true });
338
+ if (!existsSync(join(stateDir, '.gitignore')))
339
+ writeFileSync(join(stateDir, '.gitignore'), '*\n');
340
+ const body = {
341
+ ...record,
342
+ ...(options.recordExtras ?? {}),
343
+ version: manifest.semver,
344
+ digest: manifest.digest,
345
+ mountedAt,
346
+ projectId: options.projectId ?? manifest.projectId,
347
+ source,
348
+ runtimes,
349
+ };
350
+ writeAtomic(join(root, MOUNT_RECORD_PATH), `${JSON.stringify(body, null, 2)}\n`);
351
+ return finish('mounted', `harness mounted: ${manifest.semver} (${manifest.digest.slice(0, 12)}) for ${runtime} from ${source}`);
352
+ }