harness-everything 0.25.1 → 0.27.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.
Files changed (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +37 -0
  4. package/ci/mechanism-34-worktree-submodule-reachability.test.js +307 -0
  5. package/ci/mechanism-35-verification-contract.test.js +416 -0
  6. package/ci/mutation-check.js +4 -0
  7. package/ci/mutation-loader.js +8 -0
  8. package/docs/audit.md +2 -0
  9. package/git-commit/SKILL.md +2 -2
  10. package/git-commit/guides/SUBMODULES.md +54 -1
  11. package/harness-everything/SKILL.md +2 -2
  12. package/harness-everything/scripts/verify-gate.js +223 -75
  13. package/hooks/scripts/lib/workflow-isolation.js +3 -4
  14. package/hooks/scripts/state-persist.js +1 -1
  15. package/opencode-plugin/plugin.json +1 -1
  16. package/package.json +1 -1
  17. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  18. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +3 -4
  19. package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -1
  20. package/plugins/harness-everything/plugin.json +1 -1
  21. package/plugins/harness-everything/scripts/lib/verification-contract.js +357 -0
  22. package/plugins/harness-everything/skills/git-commit/SKILL.md +2 -2
  23. package/plugins/harness-everything/skills/git-commit/guides/SUBMODULES.md +54 -1
  24. package/plugins/harness-everything/skills/harness-everything/SKILL.md +2 -2
  25. package/plugins/harness-everything/skills/harness-everything/scripts/verify-gate.js +223 -75
  26. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +1 -1
  27. package/plugins/harness-everything/skills/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
  28. package/plugins/harness-everything/skills/using-git-worktrees/scripts/submodule-reachability.js +265 -0
  29. package/plugins/harness-everything/skills/verification-loop/SKILL.md +15 -23
  30. package/plugins/harness-everything/skills/verification-loop/references/project-verification-contract.md +112 -0
  31. package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +4 -3
  32. package/plugins/harness-everything/skills/verification-loop/references/verify-contract.schema.json +32 -0
  33. package/plugins/harness-everything/skills/verification-loop/references/verify-result.schema.json +65 -0
  34. package/sbom.cdx.json +7 -7
  35. package/scripts/lib/verification-contract.js +357 -0
  36. package/using-git-worktrees/SKILL.md +1 -1
  37. package/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
  38. package/using-git-worktrees/scripts/submodule-reachability.js +265 -0
  39. package/verification-loop/SKILL.md +15 -23
  40. package/verification-loop/references/project-verification-contract.md +112 -0
  41. package/verification-loop/references/verification-phases.md +4 -3
  42. package/verification-loop/references/verify-contract.schema.json +32 -0
  43. package/verification-loop/references/verify-result.schema.json +65 -0
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.25.1",
13
+ "version": "0.27.0",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.25.1",
4
+ "version": "0.27.0",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.27.0](https://github.com/dyphn1/Harness-everything/compare/v0.26.0...v0.27.0) (2026-09-27)
6
+
7
+ ### Features
8
+
9
+ * **skills:** restore deterministic submodule reachability check ([0dd06e0](https://github.com/dyphn1/Harness-everything/commit/0dd06e0bd6aa38e04b3e8c11892d8d006c440821))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **plugin:** sync nested submodule reachability ([94dbe4c](https://github.com/dyphn1/Harness-everything/commit/94dbe4c94ed2278fcb5cb6faf0f252ef5b282323))
14
+ * **skills:** land submodule reachability gate on main ([1b6de2b](https://github.com/dyphn1/Harness-everything/commit/1b6de2b5fd0cc6ae1b99530ef00c24f12efefb55))
15
+ * **skills:** require exact primary submodule repo for reachability ([c3f0f76](https://github.com/dyphn1/Harness-everything/commit/c3f0f76e1a127837b1faa2f5bebe06c01da1e8e4))
16
+ * **skills:** resolve nested submodule object stores ([aa6440b](https://github.com/dyphn1/Harness-everything/commit/aa6440b625b223948d6e69cbf9775fe45b861c50))
17
+ * **skills:** verify staged and committed submodule references ([6678496](https://github.com/dyphn1/Harness-everything/commit/6678496acadea25465ff3283b97bc31e4d17d455))
18
+
19
+ ## [0.26.0](https://github.com/dyphn1/Harness-everything/compare/v0.25.1...v0.26.0) (2026-09-24)
20
+
21
+ ### Features
22
+
23
+ * **verify:** add shared verification contract model ([8e9548c](https://github.com/dyphn1/Harness-everything/commit/8e9548c278c4c13d4d29a07093a0d1dec67b7a76))
24
+ * **verify:** add structured multi-root verification gate ([e616978](https://github.com/dyphn1/Harness-everything/commit/e616978a2ae81dda9123f189b4639c6c6c9f97ed))
25
+
26
+ ### Bug Fixes
27
+
28
+ * **verify:** align hook verification command recognition ([943659a](https://github.com/dyphn1/Harness-everything/commit/943659aa0639316beea7f90327c32b393d441931))
29
+ * **verify:** keep hook evidence fail closed ([ff19098](https://github.com/dyphn1/Harness-everything/commit/ff190980e2452b297624ae06be60a29bcfc74fb5))
30
+ * **verify:** preserve legacy evidence without broad word matching ([aa4ac71](https://github.com/dyphn1/Harness-everything/commit/aa4ac715b85321f29cd744c02221130e659de318))
31
+ * **verify:** record project-aware verification evidence ([cab04fc](https://github.com/dyphn1/Harness-everything/commit/cab04fce9691938dcf17da88591e4063a94b5339))
32
+ * **verify:** Require runnable commit hooks ([735790e](https://github.com/dyphn1/Harness-everything/commit/735790ea0fc39603f0e68f160f9bd20f49636095))
33
+ * **verify:** route shell evidence through shared classifier ([e205319](https://github.com/dyphn1/Harness-everything/commit/e205319011fb8cfef4e43310e550b2725d41bacb))
34
+ * **verify:** sync fail-closed verification classifier ([cfe1eb2](https://github.com/dyphn1/Harness-everything/commit/cfe1eb221686edac554f545637925e7c37002057))
35
+
36
+ ## [Unreleased]
37
+
38
+ ### Bug Fixes
39
+
40
+ * **verify:** discover and execute project verification contracts across npm and non-npm repositories, with multi-root structured evidence and aligned hook recognition (#242)
41
+
5
42
  ## [0.25.1](https://github.com/dyphn1/Harness-everything/compare/v0.25.0...v0.25.1) (2026-09-24)
6
43
 
7
44
  ### Bug Fixes
@@ -0,0 +1,307 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ const ROOT = path.resolve(__dirname, '..');
10
+ const HELPER = path.join(ROOT, 'using-git-worktrees', 'scripts', 'submodule-reachability.js');
11
+
12
+ function run(command, args, cwd, { allowFailure = false, env = {}, input } = {}) {
13
+ const result = spawnSync(command, args, {
14
+ cwd,
15
+ encoding: 'utf8',
16
+ input,
17
+ env: { ...process.env, ...env },
18
+ });
19
+ if (!allowFailure && result.status !== 0) {
20
+ throw new Error([
21
+ `${command} ${args.join(' ')} failed with ${result.status}`,
22
+ result.stdout,
23
+ result.stderr,
24
+ ].filter(Boolean).join('\n'));
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function git(cwd, args, options) {
30
+ return run('git', args, cwd, options);
31
+ }
32
+
33
+ function initRepo(dir) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ git(dir, ['init', '-b', 'main']);
36
+ git(dir, ['config', 'user.name', 'Harness Test']);
37
+ git(dir, ['config', 'user.email', 'harness@example.invalid']);
38
+ }
39
+
40
+ function readJsonRun(cwd) {
41
+ const result = run(process.execPath, [HELPER, '--json'], cwd, { allowFailure: true });
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(result.stdout);
45
+ } catch (error) {
46
+ throw new Error(`helper did not emit JSON (status=${result.status}): ${result.stdout}\n${result.stderr}`);
47
+ }
48
+ return { ...result, parsed };
49
+ }
50
+
51
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-submodule-reachability-'));
52
+
53
+ try {
54
+ const child = path.join(base, 'child');
55
+ const superRepo = path.join(base, 'super');
56
+ const worktree = path.join(base, 'super-wt');
57
+
58
+ initRepo(child);
59
+ fs.writeFileSync(path.join(child, 'seed.txt'), 'seed\n');
60
+ git(child, ['add', 'seed.txt']);
61
+ git(child, ['commit', '-m', 'seed']);
62
+ const seedSha = git(child, ['rev-parse', 'HEAD']).stdout.trim();
63
+
64
+ initRepo(superRepo);
65
+ fs.writeFileSync(path.join(superRepo, 'README.md'), 'super\n');
66
+ git(superRepo, ['add', 'README.md']);
67
+ git(superRepo, ['commit', '-m', 'super seed']);
68
+ git(superRepo, ['-c', 'protocol.file.allow=always', 'submodule', 'add', child, 'libs/sub']);
69
+ git(superRepo, ['commit', '-am', 'add submodule']);
70
+
71
+ git(superRepo, ['worktree', 'add', worktree, '-b', 'feat']);
72
+ git(worktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init']);
73
+ const linkedSub = path.join(worktree, 'libs', 'sub');
74
+ git(linkedSub, ['config', 'user.name', 'Harness Test']);
75
+ git(linkedSub, ['config', 'user.email', 'harness@example.invalid']);
76
+
77
+ // Clean initialized submodules use a leading-space status marker. The helper
78
+ // must preserve/normalize that state instead of consuming the first SHA byte
79
+ // as a prefix when command output is trimmed.
80
+ const cleanRawStatus = git(worktree, ['submodule', 'status', '--recursive']).stdout;
81
+ assert.match(cleanRawStatus, /^ [0-9a-f]{40,64}\s+libs\/sub\b/, 'fixture must begin with a clean initialized submodule');
82
+ const clean = readJsonRun(worktree);
83
+ assert.strictEqual(clean.status, 0, 'a clean initialized linked-worktree submodule must be inspectable');
84
+ assert.strictEqual(clean.parsed.ok, true);
85
+ assert.strictEqual(clean.parsed.linkedWorktree, true);
86
+ const cleanSub = clean.parsed.submodules.find(entry => entry.path === 'libs/sub');
87
+ assert.ok(cleanSub, 'clean initialized submodule must be reported');
88
+ assert.strictEqual(cleanSub.sha, seedSha);
89
+ assert.strictEqual(cleanSub.initialized, true);
90
+ assert.strictEqual(cleanSub.detached, true);
91
+ assert.strictEqual(cleanSub.externallyReachable, true);
92
+
93
+ // RED case: a new commit exists only in the linked worktree's submodule git dir.
94
+ git(linkedSub, ['commit', '--allow-empty', '-m', 'isolated child commit']);
95
+ const isolatedSha = git(linkedSub, ['rev-parse', 'HEAD']).stdout.trim();
96
+ const isolated = readJsonRun(worktree);
97
+ assert.strictEqual(isolated.status, 1, 'an unpublished linked-worktree submodule commit must fail');
98
+ assert.strictEqual(isolated.parsed.ok, false);
99
+ assert.strictEqual(isolated.parsed.linkedWorktree, true);
100
+ const isolatedSub = isolated.parsed.submodules.find(entry => entry.path === 'libs/sub');
101
+ assert.ok(isolatedSub, 'submodule must be reported');
102
+ assert.strictEqual(isolatedSub.sha, isolatedSha);
103
+ assert.strictEqual(isolatedSub.detached, true, 'submodule update should leave HEAD detached');
104
+ assert.strictEqual(isolatedSub.perWorktreeModuleDir, true);
105
+ assert.strictEqual(isolatedSub.reachableFromRemote, false);
106
+ assert.strictEqual(isolatedSub.presentInPrimary, false);
107
+ assert.strictEqual(isolatedSub.externallyReachable, false);
108
+
109
+ // A local branch name alone does not make the commit externally reachable.
110
+ git(linkedSub, ['switch', '-c', 'task']);
111
+ const namedLocal = readJsonRun(worktree);
112
+ assert.strictEqual(namedLocal.status, 1, 'a named but unpublished branch must still fail');
113
+ const namedLocalSub = namedLocal.parsed.submodules.find(entry => entry.path === 'libs/sub');
114
+ assert.strictEqual(namedLocalSub.detached, false);
115
+ assert.strictEqual(namedLocalSub.reachableFromRemote, false);
116
+ assert.strictEqual(namedLocalSub.presentInPrimary, false);
117
+ assert.strictEqual(namedLocalSub.externallyReachable, false);
118
+
119
+ // Recovery path: name the commit, fetch it into the primary checkout's submodule object store.
120
+ const primarySub = path.join(superRepo, 'libs', 'sub');
121
+ git(primarySub, ['fetch', linkedSub, 'task:task']);
122
+ const recovered = readJsonRun(worktree);
123
+ assert.strictEqual(recovered.status, 0, 'a commit present in the primary checkout must pass');
124
+ const recoveredSub = recovered.parsed.submodules.find(entry => entry.path === 'libs/sub');
125
+ assert.strictEqual(recoveredSub.presentInPrimary, true);
126
+ assert.strictEqual(recoveredSub.externallyReachable, true);
127
+
128
+ // Remote path: make a new commit that is absent from primary, then publish it.
129
+ git(linkedSub, ['switch', 'task']);
130
+ git(linkedSub, ['commit', '--allow-empty', '-m', 'remote child commit']);
131
+ const remoteSha = git(linkedSub, ['rev-parse', 'HEAD']).stdout.trim();
132
+ git(linkedSub, ['push', '-u', 'origin', 'task']);
133
+ const remote = readJsonRun(worktree);
134
+ assert.strictEqual(remote.status, 0, 'a commit reachable from a remote-tracking ref must pass');
135
+ const remoteSub = remote.parsed.submodules.find(entry => entry.path === 'libs/sub');
136
+ assert.strictEqual(remoteSub.sha, remoteSha);
137
+ assert.strictEqual(remoteSub.reachableFromRemote, true);
138
+ assert.strictEqual(remoteSub.externallyReachable, true);
139
+
140
+ // A published working HEAD must not hide an unpublished staged/committed gitlink.
141
+ git(linkedSub, ['commit', '--allow-empty', '-m', 'recorded but unpublished']);
142
+ const recordedSha = git(linkedSub, ['rev-parse', 'HEAD']).stdout.trim();
143
+ git(worktree, ['add', 'libs/sub']);
144
+ git(linkedSub, ['checkout', '--detach', seedSha]);
145
+ const stagedOnly = readJsonRun(worktree);
146
+ assert.deepStrictEqual(readJsonRun(worktree).parsed, stagedOnly.parsed, 'identical topology must produce identical evidence');
147
+ assert.strictEqual(stagedOnly.status, 1, 'the unpublished index SHA must fail even with a published working HEAD');
148
+ let reference = stagedOnly.parsed.submodules[0].referencedCommits.find(item => item.sha === recordedSha);
149
+ assert.deepStrictEqual(reference.sources, ['index']);
150
+ assert.strictEqual(reference.externallyReachable, false);
151
+
152
+ git(worktree, ['commit', '-m', 'record unpublished gitlink']);
153
+ // Stage the safe SHA over it: the unsafe committed SHA must still be checked.
154
+ git(worktree, ['add', 'libs/sub']);
155
+ const committedOnly = readJsonRun(worktree);
156
+ assert.strictEqual(committedOnly.status, 1, 'the unpublished superproject HEAD SHA must fail independently of index and working HEAD');
157
+ reference = committedOnly.parsed.submodules[0].referencedCommits.find(item => item.sha === recordedSha);
158
+ assert.deepStrictEqual(reference.sources, ['HEAD']);
159
+ assert.strictEqual(reference.externallyReachable, false);
160
+
161
+ git(primarySub, ['fetch', linkedSub, 'task:recorded-recovery']);
162
+ const recordedRecovered = readJsonRun(worktree);
163
+ assert.deepStrictEqual(readJsonRun(worktree).parsed, recordedRecovered.parsed, 'recovered evidence must be deterministic');
164
+ assert.strictEqual(recordedRecovered.status, 0, 'recovering the recorded SHA in primary must allow the unchanged topology');
165
+ reference = recordedRecovered.parsed.submodules[0].referencedCommits.find(item => item.sha === recordedSha);
166
+ assert.strictEqual(reference.presentInPrimary, true);
167
+ assert.strictEqual(reference.externallyReachable, true);
168
+
169
+ // Nested recovery: Git stores a nested submodule under the parent's module
170
+ // repository (.../modules/<parent>/modules/<child>), not by flattening the
171
+ // full worktree path below the top-level common dir.
172
+ const nestedLeafOrigin = path.join(base, 'nested-leaf-origin');
173
+ const nestedParentOrigin = path.join(base, 'nested-parent-origin');
174
+ const nestedSuper = path.join(base, 'nested-super');
175
+ const nestedWorktree = path.join(base, 'nested-super-wt');
176
+
177
+ initRepo(nestedLeafOrigin);
178
+ fs.writeFileSync(path.join(nestedLeafOrigin, 'leaf.txt'), 'leaf\n');
179
+ git(nestedLeafOrigin, ['add', 'leaf.txt']);
180
+ git(nestedLeafOrigin, ['commit', '-m', 'leaf seed']);
181
+
182
+ initRepo(nestedParentOrigin);
183
+ fs.writeFileSync(path.join(nestedParentOrigin, 'parent.txt'), 'parent\n');
184
+ git(nestedParentOrigin, ['add', 'parent.txt']);
185
+ git(nestedParentOrigin, ['commit', '-m', 'parent seed']);
186
+ git(nestedParentOrigin, ['-c', 'protocol.file.allow=always', 'submodule', 'add', nestedLeafOrigin, 'nested/leaf']);
187
+ git(nestedParentOrigin, ['commit', '-am', 'add nested leaf']);
188
+
189
+ initRepo(nestedSuper);
190
+ fs.writeFileSync(path.join(nestedSuper, 'README.md'), 'nested super\n');
191
+ git(nestedSuper, ['add', 'README.md']);
192
+ git(nestedSuper, ['commit', '-m', 'nested super seed']);
193
+ git(nestedSuper, ['-c', 'protocol.file.allow=always', 'submodule', 'add', nestedParentOrigin, 'libs/sub']);
194
+ git(nestedSuper, ['commit', '-am', 'add nested parent']);
195
+ git(nestedSuper, ['worktree', 'add', nestedWorktree, '-b', 'nested-feat']);
196
+ git(nestedWorktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init', '--recursive']);
197
+
198
+ const linkedNestedLeaf = path.join(nestedWorktree, 'libs', 'sub', 'nested', 'leaf');
199
+ git(linkedNestedLeaf, ['config', 'user.name', 'Harness Test']);
200
+ git(linkedNestedLeaf, ['config', 'user.email', 'harness@example.invalid']);
201
+ git(linkedNestedLeaf, ['switch', '-c', 'nested-task']);
202
+ git(linkedNestedLeaf, ['commit', '--allow-empty', '-m', 'nested worktree-only commit']);
203
+ const nestedSha = git(linkedNestedLeaf, ['rev-parse', 'HEAD']).stdout.trim();
204
+
205
+ const primaryNestedParent = path.join(nestedSuper, 'libs', 'sub');
206
+ git(primaryNestedParent, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init', '--recursive']);
207
+ const primaryNestedLeaf = path.join(primaryNestedParent, 'nested', 'leaf');
208
+ git(primaryNestedLeaf, ['fetch', linkedNestedLeaf, 'nested-task:nested-recovery']);
209
+ git(primaryNestedParent, ['submodule', 'deinit', '--force', '--', 'nested/leaf']);
210
+
211
+ const nestedRecovered = readJsonRun(nestedWorktree);
212
+ assert.strictEqual(nestedRecovered.status, 0, 'a nested commit recovered only in the primary module object store must pass');
213
+ const nestedEntry = nestedRecovered.parsed.submodules.find(entry => entry.path === 'libs/sub/nested/leaf');
214
+ assert.ok(nestedEntry, 'nested submodule must be reported');
215
+ assert.strictEqual(nestedEntry.sha, nestedSha);
216
+ assert.strictEqual(nestedEntry.reachableFromRemote, false);
217
+ assert.strictEqual(nestedEntry.presentInPrimary, true);
218
+ assert.strictEqual(nestedEntry.externallyReachable, true);
219
+
220
+ // No-submodule control.
221
+ const plain = path.join(base, 'plain');
222
+ initRepo(plain);
223
+ git(plain, ['commit', '--allow-empty', '-m', 'plain']);
224
+ const noSubmodule = readJsonRun(plain);
225
+ assert.strictEqual(noSubmodule.status, 0);
226
+ assert.strictEqual(noSubmodule.parsed.ok, true);
227
+ assert.deepStrictEqual(noSubmodule.parsed.submodules, []);
228
+
229
+ // A deinitialized submodule can make `git -C <submodule>` resolve to the
230
+ // superproject. Never use that unrelated HEAD or its remote refs as proof
231
+ // that the staged gitlink is reachable.
232
+ const uninitWorktree = path.join(base, 'super-wt-uninitialized');
233
+ git(superRepo, ['worktree', 'add', uninitWorktree, '-b', 'feat-uninitialized']);
234
+ git(uninitWorktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init']);
235
+ const uninitializedSub = path.join(uninitWorktree, 'libs', 'sub');
236
+ git(uninitializedSub, ['config', 'user.name', 'Harness Test']);
237
+ git(uninitializedSub, ['config', 'user.email', 'harness@example.invalid']);
238
+ git(uninitializedSub, ['commit', '--allow-empty', '-m', 'uninitialized submodule commit']);
239
+ const uninitializedSha = git(uninitializedSub, ['rev-parse', 'HEAD']).stdout.trim();
240
+ git(uninitWorktree, ['add', 'libs/sub']);
241
+ const superSha = git(uninitWorktree, ['rev-parse', 'HEAD']).stdout.trim();
242
+ git(uninitWorktree, ['update-ref', 'refs/remotes/origin/main', superSha]);
243
+ git(uninitWorktree, ['submodule', 'deinit', '--force', '--', 'libs/sub']);
244
+
245
+ const uninitialized = readJsonRun(uninitWorktree);
246
+ assert.strictEqual(uninitialized.status, 1, 'an uninitialized staged gitlink without external reachability must fail');
247
+ const uninitializedEntry = uninitialized.parsed.submodules.find(entry => entry.path === 'libs/sub');
248
+ assert.ok(uninitializedEntry, 'uninitialized submodule must be reported');
249
+ assert.strictEqual(uninitializedEntry.initialized, false);
250
+ assert.strictEqual(uninitializedEntry.sha, uninitializedSha);
251
+ assert.strictEqual(uninitializedEntry.reachableFromRemote, false);
252
+ assert.strictEqual(uninitializedEntry.presentInPrimary, false);
253
+ assert.strictEqual(uninitializedEntry.externallyReachable, false);
254
+
255
+ // An unmerged gitlink has no single target SHA and must fail as an
256
+ // inspection error instead of checking the submodule's current HEAD.
257
+ const conflictWorktree = path.join(base, 'super-wt-conflict');
258
+ git(superRepo, ['worktree', 'add', conflictWorktree, '-b', 'feat-conflict']);
259
+ git(conflictWorktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init']);
260
+ const zeroSha = '0'.repeat(seedSha.length);
261
+ const conflictIndex = [
262
+ `0 ${zeroSha} 0\tlibs/sub`,
263
+ `160000 ${seedSha} 1\tlibs/sub`,
264
+ `160000 ${isolatedSha} 2\tlibs/sub`,
265
+ `160000 ${remoteSha} 3\tlibs/sub`,
266
+ '',
267
+ ].join('\n');
268
+ git(conflictWorktree, ['update-index', '--index-info'], { input: conflictIndex });
269
+ const conflictStatus = git(conflictWorktree, ['submodule', 'status', '--recursive']).stdout.trim();
270
+ assert.match(conflictStatus, /^U/, `fixture must produce an unmerged gitlink: ${conflictStatus}`);
271
+ const conflict = run(process.execPath, [HELPER, '--json'], conflictWorktree, { allowFailure: true });
272
+ assert.strictEqual(conflict.status, 2, `unmerged gitlink must fail inspection: ${conflict.stdout}\n${conflict.stderr}`);
273
+ assert.match(conflict.stderr, /unmerged submodule gitlink/i);
274
+
275
+ // A deinitialized primary submodule leaves an empty directory, so
276
+ // `git -C <primary>/<submodule>` resolves to the primary superproject. An
277
+ // object in that unrelated store is not proof of submodule reachability.
278
+ const fallbackWorktree = path.join(base, 'super-wt-primary-fallback');
279
+ git(superRepo, ['worktree', 'add', fallbackWorktree, '-b', 'feat-primary-fallback']);
280
+ git(fallbackWorktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init']);
281
+ const fallbackSub = path.join(fallbackWorktree, 'libs', 'sub');
282
+ git(fallbackSub, ['config', 'user.name', 'Harness Test']);
283
+ git(fallbackSub, ['config', 'user.email', 'harness@example.invalid']);
284
+ git(fallbackSub, ['switch', '-c', 'fallback-task']);
285
+ git(fallbackSub, ['commit', '--allow-empty', '-m', 'superproject-store only commit']);
286
+ const fallbackSha = git(fallbackSub, ['rev-parse', 'HEAD']).stdout.trim();
287
+ git(fallbackWorktree, ['add', 'libs/sub']);
288
+ // Empty the primary checkout directory without touching shared submodule
289
+ // config, which would also uninitialize the linked worktree.
290
+ fs.rmSync(primarySub, { recursive: true, force: true });
291
+ fs.mkdirSync(primarySub);
292
+ fs.rmSync(path.join(superRepo, '.git', 'modules', 'libs', 'sub'), { recursive: true, force: true });
293
+ git(superRepo, ['fetch', fallbackSub, 'fallback-task:refs/foreign/fallback-task']);
294
+ assert.strictEqual(git(superRepo, ['cat-file', '-e', `${fallbackSha}^{commit}`], { allowFailure: true }).status, 0,
295
+ 'fixture must place the commit only in the primary superproject object store');
296
+ const fallback = readJsonRun(fallbackWorktree);
297
+ assert.strictEqual(fallback.status, 1, 'a commit found only through git -C fallback to the superproject must fail');
298
+ const fallbackEntry = fallback.parsed.submodules.find(entry => entry.path === 'libs/sub');
299
+ assert.strictEqual(fallbackEntry.initialized, true);
300
+ assert.strictEqual(fallbackEntry.sha, fallbackSha);
301
+ assert.strictEqual(fallbackEntry.presentInPrimary, false);
302
+ assert.strictEqual(fallbackEntry.externallyReachable, false);
303
+
304
+ console.log('Worktree submodule reachability: detached, named-unpublished, uninitialized, and unmerged gitlinks fail safely; primary/remote reachability and no-submodule controls pass.');
305
+ } finally {
306
+ fs.rmSync(base, { recursive: true, force: true });
307
+ }