harness-everything 0.26.0 → 0.28.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 (34) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +35 -0
  4. package/behavioral-evals/README.md +39 -0
  5. package/behavioral-evals/case-validator.js +34 -0
  6. package/behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml +30 -0
  7. package/behavioral-evals/fixtures/setup/fail.js +3 -0
  8. package/behavioral-evals/fixtures/setup/noop.js +4 -0
  9. package/behavioral-evals/fixtures/setup/verify-worktree-submodule.js +46 -0
  10. package/behavioral-evals/fixtures/setup/worktree-submodule.js +86 -0
  11. package/behavioral-evals/paired-benchmark.js +39 -5
  12. package/behavioral-evals/run-with-plugin.js +53 -39
  13. package/behavioral-evals/run.js +88 -9
  14. package/ci/mechanism-2j-behavioral-runner.test.js +121 -1
  15. package/ci/mechanism-2j-fixture-entrypoints.test.js +153 -0
  16. package/ci/mechanism-34-worktree-submodule-reachability.test.js +307 -0
  17. package/ci/mutation-check.js +2 -0
  18. package/ci/mutation-loader.js +4 -0
  19. package/ci/test-behavioral-case-validation.js +26 -0
  20. package/git-commit/SKILL.md +2 -2
  21. package/git-commit/guides/SUBMODULES.md +54 -1
  22. package/opencode-plugin/plugin.json +1 -1
  23. package/package.json +1 -1
  24. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  25. package/plugins/harness-everything/plugin.json +1 -1
  26. package/plugins/harness-everything/skills/git-commit/SKILL.md +2 -2
  27. package/plugins/harness-everything/skills/git-commit/guides/SUBMODULES.md +54 -1
  28. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +1 -1
  29. package/plugins/harness-everything/skills/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
  30. package/plugins/harness-everything/skills/using-git-worktrees/scripts/submodule-reachability.js +265 -0
  31. package/sbom.cdx.json +7 -7
  32. package/using-git-worktrees/SKILL.md +1 -1
  33. package/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
  34. package/using-git-worktrees/scripts/submodule-reachability.js +265 -0
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function runGit(args, cwd, { allowFailure = false } = {}) {
9
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
10
+ if (!allowFailure && result.status !== 0) {
11
+ const message = (result.stderr || result.stdout || '').trim();
12
+ throw new Error(`git ${args.join(' ')} failed${message ? `: ${message}` : ''}`);
13
+ }
14
+ return result;
15
+ }
16
+
17
+ function gitText(args, cwd, options) {
18
+ const result = runGit(args, cwd, options);
19
+ return result.status === 0 ? result.stdout.trim() : '';
20
+ }
21
+
22
+ function absoluteCommonDir(root) {
23
+ const absolute = runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], root, { allowFailure: true });
24
+ if (absolute.status === 0) return path.resolve(absolute.stdout.trim());
25
+ const raw = gitText(['rev-parse', '--git-common-dir'], root);
26
+ return path.resolve(root, raw);
27
+ }
28
+
29
+ function isInside(child, parent) {
30
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
31
+ return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
32
+ }
33
+
34
+ function samePath(a, b) {
35
+ const normalize = value => {
36
+ let resolved = path.resolve(value);
37
+ try { resolved = fs.realpathSync.native(resolved); } catch { /* keep resolved */ }
38
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
39
+ };
40
+ return normalize(a) === normalize(b);
41
+ }
42
+
43
+ function parseSubmoduleStatus(text) {
44
+ if (!text.trim()) return [];
45
+ return text.split(/\r?\n/).filter(Boolean).map(line => {
46
+ // gitText() trims the whole command output, so the first clean-status
47
+ // marker (a leading space) may already be gone. Accept only the four Git
48
+ // status states here; an omitted marker is normalized back to clean.
49
+ const match = line.match(/^([ +U-]?)([0-9a-fA-F]{40,64})\s+(.+?)(?:\s+\(.+\))?$/);
50
+ if (!match) throw new Error(`cannot parse git submodule status line: ${line}`);
51
+ return {
52
+ prefix: match[1] || ' ',
53
+ recordedSha: match[2],
54
+ path: match[3],
55
+ };
56
+ });
57
+ }
58
+
59
+ function primaryHasCommit(primaryRoot, commonDir, submodulePath, sha, moduleGitDir = null) {
60
+ if (!primaryRoot) return false;
61
+ const primarySub = path.join(primaryRoot, ...submodulePath.split('/'));
62
+ // A deinitialized submodule leaves an empty directory, and `git -C` then
63
+ // resolves to the enclosing repository. Only the exact submodule repo counts.
64
+ const toplevel = runGit(['-C', primarySub, 'rev-parse', '--show-toplevel'], primaryRoot, { allowFailure: true });
65
+ let result = null;
66
+ if (toplevel.status === 0 && samePath(toplevel.stdout.trim(), primarySub)) {
67
+ result = runGit(['-C', primarySub, 'cat-file', '-e', `${sha}^{commit}`], primaryRoot, { allowFailure: true });
68
+ if (result.status === 0) return true;
69
+ }
70
+
71
+ // A nested submodule repository is stored below its parent's module repo,
72
+ // e.g. .git/modules/parent/modules/child. Callers pass that exact location
73
+ // when recursion is known; direct submodules retain the historical fallback.
74
+ const candidateGitDir = moduleGitDir || path.join(commonDir, 'modules', ...submodulePath.split('/'));
75
+ result = runGit(['--git-dir', candidateGitDir, 'cat-file', '-e', `${sha}^{commit}`], primaryRoot, { allowFailure: true });
76
+ return result.status === 0;
77
+ }
78
+
79
+ // Read the recorded gitlinks from their owning repository, not from the
80
+ // submodule working HEAD (which may have been checked out to an older commit).
81
+ function recordedGitlinks(owner, relativePath) {
82
+ const commits = new Map();
83
+ function add(sha, source) {
84
+ if (!commits.has(sha)) commits.set(sha, { sha, sources: [] });
85
+ commits.get(sha).sources.push(source);
86
+ }
87
+ const index = runGit(['--literal-pathspecs', 'ls-files', '--stage', '-z', '--', relativePath], owner).stdout;
88
+ for (const record of index.split('\0').filter(Boolean)) {
89
+ const match = record.match(/^160000 ([0-9a-f]{40,64}) ([0-3])\t(.*)$/s);
90
+ if (!match || match[3] !== relativePath) continue;
91
+ if (match[2] !== '0') throw new Error(`unmerged submodule gitlink cannot be verified: ${relativePath}`);
92
+ add(match[1], 'index');
93
+ }
94
+ if (runGit(['rev-parse', '--verify', 'HEAD'], owner, { allowFailure: true }).status === 0) {
95
+ const tree = runGit(['--literal-pathspecs', 'ls-tree', '-z', 'HEAD', '--', relativePath], owner).stdout;
96
+ for (const record of tree.split('\0').filter(Boolean)) {
97
+ const match = record.match(/^160000 commit ([0-9a-f]{40,64})\t(.*)$/s);
98
+ if (match && match[2] === relativePath) add(match[1], 'HEAD');
99
+ }
100
+ }
101
+ return [...commits.values()];
102
+ }
103
+
104
+ function inspect(root = process.cwd()) {
105
+ root = path.resolve(gitText(['rev-parse', '--show-toplevel'], path.resolve(root)));
106
+ const superGitDir = path.resolve(gitText(['rev-parse', '--absolute-git-dir'], root));
107
+ const commonDir = absoluteCommonDir(root);
108
+ const linkedWorktree = superGitDir !== commonDir;
109
+ const primaryRoot = linkedWorktree && path.basename(commonDir).toLowerCase() === '.git'
110
+ ? path.dirname(commonDir)
111
+ : null;
112
+ const status = gitText(['submodule', 'status', '--recursive'], root);
113
+ const submodules = [];
114
+ const initializedPaths = [];
115
+ const primaryModuleDirs = new Map();
116
+
117
+ for (const entry of parseSubmoduleStatus(status)) {
118
+ if (entry.prefix === 'U') {
119
+ throw new Error(`unmerged submodule gitlink cannot be verified: ${entry.path}`);
120
+ }
121
+
122
+ const parentPath = initializedPaths.filter(parent => entry.path.startsWith(parent + '/'))
123
+ .sort((a, b) => b.length - a.length)[0];
124
+ const owner = parentPath ? path.join(root, ...parentPath.split('/')) : root;
125
+ const relativePath = parentPath ? entry.path.slice(parentPath.length + 1) : entry.path;
126
+ const subPath = path.join(root, ...entry.path.split('/'));
127
+ const parentModuleGitDir = parentPath ? primaryModuleDirs.get(parentPath) : null;
128
+ const primaryModuleGitDir = parentPath
129
+ ? path.join(parentModuleGitDir, 'modules', ...relativePath.split('/'))
130
+ : path.join(commonDir, 'modules', ...entry.path.split('/'));
131
+ primaryModuleDirs.set(entry.path, primaryModuleGitDir);
132
+ const referencedCommits = recordedGitlinks(owner, relativePath).map(reference => {
133
+ // A recorded object may be absent locally but already recovered in primary.
134
+ const hasObject = entry.prefix !== '-' && runGit(
135
+ ['cat-file', '-e', `${reference.sha}^{commit}`], subPath, { allowFailure: true }
136
+ ).status === 0;
137
+ const remoteRefs = hasObject ? gitText(
138
+ ['for-each-ref', '--contains', reference.sha, '--format=%(refname:short)', 'refs/remotes/'], subPath
139
+ ).split(/\r?\n/).filter(Boolean) : [];
140
+ const presentInPrimary = linkedWorktree
141
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, reference.sha, primaryModuleGitDir) : true;
142
+ return {
143
+ ...reference,
144
+ remoteRefs,
145
+ reachableFromRemote: remoteRefs.length > 0,
146
+ presentInPrimary,
147
+ externallyReachable: !linkedWorktree || remoteRefs.length > 0 || presentInPrimary,
148
+ };
149
+ });
150
+
151
+ if (entry.prefix === '-') {
152
+ const presentInPrimary = linkedWorktree
153
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, entry.recordedSha, primaryModuleGitDir)
154
+ : true;
155
+ const externallyReachable = !linkedWorktree || presentInPrimary;
156
+ submodules.push({
157
+ path: entry.path,
158
+ sha: entry.recordedSha,
159
+ initialized: false,
160
+ detached: null,
161
+ perWorktreeModuleDir: null,
162
+ reachableFromRemote: false,
163
+ remoteRefs: [],
164
+ presentInPrimary,
165
+ referencedCommits,
166
+ externallyReachable: externallyReachable && referencedCommits.every(reference => reference.externallyReachable),
167
+ });
168
+ continue;
169
+ }
170
+
171
+ initializedPaths.push(entry.path);
172
+ const head = runGit(['-C', subPath, 'rev-parse', 'HEAD'], root, { allowFailure: true });
173
+ if (head.status !== 0) {
174
+ throw new Error(`cannot inspect initialized submodule ${entry.path}: ${(head.stderr || head.stdout || '').trim()}`);
175
+ }
176
+
177
+ const sha = head.stdout.trim();
178
+ const branch = gitText(['-C', subPath, 'branch', '--show-current'], root);
179
+ const subGitDir = path.resolve(gitText(['-C', subPath, 'rev-parse', '--absolute-git-dir'], root));
180
+ const remoteRefs = gitText(
181
+ ['-C', subPath, 'for-each-ref', '--contains', sha, '--format=%(refname:short)', 'refs/remotes/'],
182
+ root
183
+ ).split(/\r?\n/).filter(Boolean);
184
+ const reachableFromRemote = remoteRefs.length > 0;
185
+ const presentInPrimary = linkedWorktree
186
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, sha, primaryModuleGitDir)
187
+ : true;
188
+ const externallyReachable = !linkedWorktree || reachableFromRemote || presentInPrimary;
189
+
190
+ submodules.push({
191
+ path: entry.path,
192
+ sha,
193
+ initialized: true,
194
+ detached: branch === '',
195
+ branch: branch || null,
196
+ gitDir: subGitDir,
197
+ perWorktreeModuleDir: linkedWorktree && isInside(subGitDir, path.join(commonDir, 'worktrees')),
198
+ reachableFromRemote,
199
+ remoteRefs,
200
+ presentInPrimary,
201
+ referencedCommits,
202
+ externallyReachable: externallyReachable && referencedCommits.every(reference => reference.externallyReachable),
203
+ });
204
+ }
205
+
206
+ return {
207
+ root,
208
+ linkedWorktree,
209
+ gitDir: superGitDir,
210
+ commonDir,
211
+ primaryRoot,
212
+ ok: submodules.every(entry => entry.externallyReachable),
213
+ submodules,
214
+ };
215
+ }
216
+
217
+ function printHuman(report) {
218
+ console.log(`Superproject: ${report.root}`);
219
+ console.log(`Linked worktree: ${report.linkedWorktree ? 'yes' : 'no'}`);
220
+ if (!report.submodules.length) {
221
+ console.log('Submodules: none');
222
+ return;
223
+ }
224
+ for (const entry of report.submodules) {
225
+ const state = entry.externallyReachable ? 'OK' : 'UNREACHABLE';
226
+ console.log(`[${state}] ${entry.path} @ ${entry.sha}`);
227
+ for (const reference of entry.referencedCommits) {
228
+ console.log(` ${reference.sources.join('/')} gitlink: ${reference.sha} (${reference.externallyReachable ? 'OK' : 'UNREACHABLE'})`);
229
+ }
230
+ if (!entry.initialized) {
231
+ console.log(' initialized: no');
232
+ console.log(` present in primary checkout: ${entry.presentInPrimary ? 'yes' : 'no'}`);
233
+ if (report.linkedWorktree && !entry.presentInPrimary) {
234
+ console.log(' remote reachability: unverified; initialize the submodule or fetch the commit into the primary checkout');
235
+ }
236
+ continue;
237
+ }
238
+ console.log(` branch: ${entry.branch || '(detached)'}`);
239
+ console.log(` per-worktree git dir: ${entry.perWorktreeModuleDir ? 'yes' : 'no'}`);
240
+ console.log(` remote reachability: ${entry.remoteRefs.length ? entry.remoteRefs.join(', ') : 'none'}`);
241
+ console.log(` present in primary checkout: ${entry.presentInPrimary ? 'yes' : 'no'}`);
242
+ }
243
+ }
244
+
245
+ function main(argv = process.argv.slice(2)) {
246
+ const json = argv.includes('--json');
247
+ const rootIndex = argv.indexOf('--root');
248
+ const root = rootIndex >= 0 ? argv[rootIndex + 1] : process.cwd();
249
+ if (rootIndex >= 0 && !root) throw new Error('--root requires a path');
250
+ const report = inspect(root);
251
+ if (json) process.stdout.write(JSON.stringify(report, null, 2) + '\n');
252
+ else printHuman(report);
253
+ process.exitCode = report.ok ? 0 : 1;
254
+ }
255
+
256
+ if (require.main === module) {
257
+ try {
258
+ main();
259
+ } catch (error) {
260
+ console.error(`submodule-reachability: ${error.message}`);
261
+ process.exit(2);
262
+ }
263
+ }
264
+
265
+ module.exports = { inspect, parseSubmoduleStatus, primaryHasCommit };
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:d5232bfe-e479-5e87-a012-d5dafb7acdc2",
5
+ "serialNumber": "urn:uuid:a9471ff7-cd1e-58b8-b661-a6231462bdd7",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T14:53:53.083Z",
8
+ "timestamp": "2026-09-27T17:31:25.591Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.26.0"
13
+ "version": "0.28.0"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.26.0",
18
+ "bom-ref": "pkg:npm/harness-everything@0.28.0",
19
19
  "name": "harness-everything",
20
- "version": "0.26.0",
21
- "purl": "pkg:npm/harness-everything@0.26.0"
20
+ "version": "0.28.0",
21
+ "purl": "pkg:npm/harness-everything@0.28.0"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -1999,7 +1999,7 @@
1999
1999
  ]
2000
2000
  },
2001
2001
  {
2002
- "ref": "pkg:npm/harness-everything@0.26.0",
2002
+ "ref": "pkg:npm/harness-everything@0.28.0",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -4,7 +4,7 @@ description: Use when work needs isolation or before major implementation plans
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.25.1
7
+ version: 0.27.0
8
8
  ---
9
9
 
10
10
  # Using Git Worktrees
@@ -111,9 +111,19 @@ git -C <primary> submodule update
111
111
 
112
112
  The PowerShell form is identical except paths/refs should be quoted as shown above.
113
113
 
114
+ ## Preferred automated check
115
+
116
+ From the superproject worktree, run the cross-platform helper before staging a gitlink and again before removing the worktree:
117
+
118
+ ```bash
119
+ node <this-skill-dir>/scripts/submodule-reachability.js --json
120
+ ```
121
+
122
+ Exit 0 means every linked-worktree submodule commit was proven reachable outside the current worktree (or the superproject is not a linked worktree). An uninitialized gitlink is checked against the primary checkout and fails closed if that object is absent; initialize it to inspect remote refs, or fetch the commit into the primary checkout. Exit 1 means at least one commit is not proven externally reachable. Exit 2 is a usage/Git inspection error, including an unmerged submodule gitlink. The check MUST cover the current submodule HEAD and the gitlink SHAs in both the superproject index and HEAD. Checking out an older published submodule commit MUST NOT hide an unpublished staged or committed gitlink. The JSON report retains the working HEAD fields and adds `referencedCommits`, with each SHA, its `sources` (`index` and/or `HEAD`), and its external reachability evidence. Every referenced commit must pass before removal; a clean submodule status is valid input.
123
+
114
124
  ## Pre-removal check
115
125
 
116
- For every changed gitlink SHA, prove at least one external home exists:
126
+ Prefer the helper above. Raw fallback: for every changed gitlink SHA, prove at least one external home exists:
117
127
 
118
128
  ```bash
119
129
  git -C <submodule> branch -r --contains <sha>
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function runGit(args, cwd, { allowFailure = false } = {}) {
9
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
10
+ if (!allowFailure && result.status !== 0) {
11
+ const message = (result.stderr || result.stdout || '').trim();
12
+ throw new Error(`git ${args.join(' ')} failed${message ? `: ${message}` : ''}`);
13
+ }
14
+ return result;
15
+ }
16
+
17
+ function gitText(args, cwd, options) {
18
+ const result = runGit(args, cwd, options);
19
+ return result.status === 0 ? result.stdout.trim() : '';
20
+ }
21
+
22
+ function absoluteCommonDir(root) {
23
+ const absolute = runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], root, { allowFailure: true });
24
+ if (absolute.status === 0) return path.resolve(absolute.stdout.trim());
25
+ const raw = gitText(['rev-parse', '--git-common-dir'], root);
26
+ return path.resolve(root, raw);
27
+ }
28
+
29
+ function isInside(child, parent) {
30
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
31
+ return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
32
+ }
33
+
34
+ function samePath(a, b) {
35
+ const normalize = value => {
36
+ let resolved = path.resolve(value);
37
+ try { resolved = fs.realpathSync.native(resolved); } catch { /* keep resolved */ }
38
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
39
+ };
40
+ return normalize(a) === normalize(b);
41
+ }
42
+
43
+ function parseSubmoduleStatus(text) {
44
+ if (!text.trim()) return [];
45
+ return text.split(/\r?\n/).filter(Boolean).map(line => {
46
+ // gitText() trims the whole command output, so the first clean-status
47
+ // marker (a leading space) may already be gone. Accept only the four Git
48
+ // status states here; an omitted marker is normalized back to clean.
49
+ const match = line.match(/^([ +U-]?)([0-9a-fA-F]{40,64})\s+(.+?)(?:\s+\(.+\))?$/);
50
+ if (!match) throw new Error(`cannot parse git submodule status line: ${line}`);
51
+ return {
52
+ prefix: match[1] || ' ',
53
+ recordedSha: match[2],
54
+ path: match[3],
55
+ };
56
+ });
57
+ }
58
+
59
+ function primaryHasCommit(primaryRoot, commonDir, submodulePath, sha, moduleGitDir = null) {
60
+ if (!primaryRoot) return false;
61
+ const primarySub = path.join(primaryRoot, ...submodulePath.split('/'));
62
+ // A deinitialized submodule leaves an empty directory, and `git -C` then
63
+ // resolves to the enclosing repository. Only the exact submodule repo counts.
64
+ const toplevel = runGit(['-C', primarySub, 'rev-parse', '--show-toplevel'], primaryRoot, { allowFailure: true });
65
+ let result = null;
66
+ if (toplevel.status === 0 && samePath(toplevel.stdout.trim(), primarySub)) {
67
+ result = runGit(['-C', primarySub, 'cat-file', '-e', `${sha}^{commit}`], primaryRoot, { allowFailure: true });
68
+ if (result.status === 0) return true;
69
+ }
70
+
71
+ // A nested submodule repository is stored below its parent's module repo,
72
+ // e.g. .git/modules/parent/modules/child. Callers pass that exact location
73
+ // when recursion is known; direct submodules retain the historical fallback.
74
+ const candidateGitDir = moduleGitDir || path.join(commonDir, 'modules', ...submodulePath.split('/'));
75
+ result = runGit(['--git-dir', candidateGitDir, 'cat-file', '-e', `${sha}^{commit}`], primaryRoot, { allowFailure: true });
76
+ return result.status === 0;
77
+ }
78
+
79
+ // Read the recorded gitlinks from their owning repository, not from the
80
+ // submodule working HEAD (which may have been checked out to an older commit).
81
+ function recordedGitlinks(owner, relativePath) {
82
+ const commits = new Map();
83
+ function add(sha, source) {
84
+ if (!commits.has(sha)) commits.set(sha, { sha, sources: [] });
85
+ commits.get(sha).sources.push(source);
86
+ }
87
+ const index = runGit(['--literal-pathspecs', 'ls-files', '--stage', '-z', '--', relativePath], owner).stdout;
88
+ for (const record of index.split('\0').filter(Boolean)) {
89
+ const match = record.match(/^160000 ([0-9a-f]{40,64}) ([0-3])\t(.*)$/s);
90
+ if (!match || match[3] !== relativePath) continue;
91
+ if (match[2] !== '0') throw new Error(`unmerged submodule gitlink cannot be verified: ${relativePath}`);
92
+ add(match[1], 'index');
93
+ }
94
+ if (runGit(['rev-parse', '--verify', 'HEAD'], owner, { allowFailure: true }).status === 0) {
95
+ const tree = runGit(['--literal-pathspecs', 'ls-tree', '-z', 'HEAD', '--', relativePath], owner).stdout;
96
+ for (const record of tree.split('\0').filter(Boolean)) {
97
+ const match = record.match(/^160000 commit ([0-9a-f]{40,64})\t(.*)$/s);
98
+ if (match && match[2] === relativePath) add(match[1], 'HEAD');
99
+ }
100
+ }
101
+ return [...commits.values()];
102
+ }
103
+
104
+ function inspect(root = process.cwd()) {
105
+ root = path.resolve(gitText(['rev-parse', '--show-toplevel'], path.resolve(root)));
106
+ const superGitDir = path.resolve(gitText(['rev-parse', '--absolute-git-dir'], root));
107
+ const commonDir = absoluteCommonDir(root);
108
+ const linkedWorktree = superGitDir !== commonDir;
109
+ const primaryRoot = linkedWorktree && path.basename(commonDir).toLowerCase() === '.git'
110
+ ? path.dirname(commonDir)
111
+ : null;
112
+ const status = gitText(['submodule', 'status', '--recursive'], root);
113
+ const submodules = [];
114
+ const initializedPaths = [];
115
+ const primaryModuleDirs = new Map();
116
+
117
+ for (const entry of parseSubmoduleStatus(status)) {
118
+ if (entry.prefix === 'U') {
119
+ throw new Error(`unmerged submodule gitlink cannot be verified: ${entry.path}`);
120
+ }
121
+
122
+ const parentPath = initializedPaths.filter(parent => entry.path.startsWith(parent + '/'))
123
+ .sort((a, b) => b.length - a.length)[0];
124
+ const owner = parentPath ? path.join(root, ...parentPath.split('/')) : root;
125
+ const relativePath = parentPath ? entry.path.slice(parentPath.length + 1) : entry.path;
126
+ const subPath = path.join(root, ...entry.path.split('/'));
127
+ const parentModuleGitDir = parentPath ? primaryModuleDirs.get(parentPath) : null;
128
+ const primaryModuleGitDir = parentPath
129
+ ? path.join(parentModuleGitDir, 'modules', ...relativePath.split('/'))
130
+ : path.join(commonDir, 'modules', ...entry.path.split('/'));
131
+ primaryModuleDirs.set(entry.path, primaryModuleGitDir);
132
+ const referencedCommits = recordedGitlinks(owner, relativePath).map(reference => {
133
+ // A recorded object may be absent locally but already recovered in primary.
134
+ const hasObject = entry.prefix !== '-' && runGit(
135
+ ['cat-file', '-e', `${reference.sha}^{commit}`], subPath, { allowFailure: true }
136
+ ).status === 0;
137
+ const remoteRefs = hasObject ? gitText(
138
+ ['for-each-ref', '--contains', reference.sha, '--format=%(refname:short)', 'refs/remotes/'], subPath
139
+ ).split(/\r?\n/).filter(Boolean) : [];
140
+ const presentInPrimary = linkedWorktree
141
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, reference.sha, primaryModuleGitDir) : true;
142
+ return {
143
+ ...reference,
144
+ remoteRefs,
145
+ reachableFromRemote: remoteRefs.length > 0,
146
+ presentInPrimary,
147
+ externallyReachable: !linkedWorktree || remoteRefs.length > 0 || presentInPrimary,
148
+ };
149
+ });
150
+
151
+ if (entry.prefix === '-') {
152
+ const presentInPrimary = linkedWorktree
153
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, entry.recordedSha, primaryModuleGitDir)
154
+ : true;
155
+ const externallyReachable = !linkedWorktree || presentInPrimary;
156
+ submodules.push({
157
+ path: entry.path,
158
+ sha: entry.recordedSha,
159
+ initialized: false,
160
+ detached: null,
161
+ perWorktreeModuleDir: null,
162
+ reachableFromRemote: false,
163
+ remoteRefs: [],
164
+ presentInPrimary,
165
+ referencedCommits,
166
+ externallyReachable: externallyReachable && referencedCommits.every(reference => reference.externallyReachable),
167
+ });
168
+ continue;
169
+ }
170
+
171
+ initializedPaths.push(entry.path);
172
+ const head = runGit(['-C', subPath, 'rev-parse', 'HEAD'], root, { allowFailure: true });
173
+ if (head.status !== 0) {
174
+ throw new Error(`cannot inspect initialized submodule ${entry.path}: ${(head.stderr || head.stdout || '').trim()}`);
175
+ }
176
+
177
+ const sha = head.stdout.trim();
178
+ const branch = gitText(['-C', subPath, 'branch', '--show-current'], root);
179
+ const subGitDir = path.resolve(gitText(['-C', subPath, 'rev-parse', '--absolute-git-dir'], root));
180
+ const remoteRefs = gitText(
181
+ ['-C', subPath, 'for-each-ref', '--contains', sha, '--format=%(refname:short)', 'refs/remotes/'],
182
+ root
183
+ ).split(/\r?\n/).filter(Boolean);
184
+ const reachableFromRemote = remoteRefs.length > 0;
185
+ const presentInPrimary = linkedWorktree
186
+ ? primaryHasCommit(primaryRoot, commonDir, entry.path, sha, primaryModuleGitDir)
187
+ : true;
188
+ const externallyReachable = !linkedWorktree || reachableFromRemote || presentInPrimary;
189
+
190
+ submodules.push({
191
+ path: entry.path,
192
+ sha,
193
+ initialized: true,
194
+ detached: branch === '',
195
+ branch: branch || null,
196
+ gitDir: subGitDir,
197
+ perWorktreeModuleDir: linkedWorktree && isInside(subGitDir, path.join(commonDir, 'worktrees')),
198
+ reachableFromRemote,
199
+ remoteRefs,
200
+ presentInPrimary,
201
+ referencedCommits,
202
+ externallyReachable: externallyReachable && referencedCommits.every(reference => reference.externallyReachable),
203
+ });
204
+ }
205
+
206
+ return {
207
+ root,
208
+ linkedWorktree,
209
+ gitDir: superGitDir,
210
+ commonDir,
211
+ primaryRoot,
212
+ ok: submodules.every(entry => entry.externallyReachable),
213
+ submodules,
214
+ };
215
+ }
216
+
217
+ function printHuman(report) {
218
+ console.log(`Superproject: ${report.root}`);
219
+ console.log(`Linked worktree: ${report.linkedWorktree ? 'yes' : 'no'}`);
220
+ if (!report.submodules.length) {
221
+ console.log('Submodules: none');
222
+ return;
223
+ }
224
+ for (const entry of report.submodules) {
225
+ const state = entry.externallyReachable ? 'OK' : 'UNREACHABLE';
226
+ console.log(`[${state}] ${entry.path} @ ${entry.sha}`);
227
+ for (const reference of entry.referencedCommits) {
228
+ console.log(` ${reference.sources.join('/')} gitlink: ${reference.sha} (${reference.externallyReachable ? 'OK' : 'UNREACHABLE'})`);
229
+ }
230
+ if (!entry.initialized) {
231
+ console.log(' initialized: no');
232
+ console.log(` present in primary checkout: ${entry.presentInPrimary ? 'yes' : 'no'}`);
233
+ if (report.linkedWorktree && !entry.presentInPrimary) {
234
+ console.log(' remote reachability: unverified; initialize the submodule or fetch the commit into the primary checkout');
235
+ }
236
+ continue;
237
+ }
238
+ console.log(` branch: ${entry.branch || '(detached)'}`);
239
+ console.log(` per-worktree git dir: ${entry.perWorktreeModuleDir ? 'yes' : 'no'}`);
240
+ console.log(` remote reachability: ${entry.remoteRefs.length ? entry.remoteRefs.join(', ') : 'none'}`);
241
+ console.log(` present in primary checkout: ${entry.presentInPrimary ? 'yes' : 'no'}`);
242
+ }
243
+ }
244
+
245
+ function main(argv = process.argv.slice(2)) {
246
+ const json = argv.includes('--json');
247
+ const rootIndex = argv.indexOf('--root');
248
+ const root = rootIndex >= 0 ? argv[rootIndex + 1] : process.cwd();
249
+ if (rootIndex >= 0 && !root) throw new Error('--root requires a path');
250
+ const report = inspect(root);
251
+ if (json) process.stdout.write(JSON.stringify(report, null, 2) + '\n');
252
+ else printHuman(report);
253
+ process.exitCode = report.ok ? 0 : 1;
254
+ }
255
+
256
+ if (require.main === module) {
257
+ try {
258
+ main();
259
+ } catch (error) {
260
+ console.error(`submodule-reachability: ${error.message}`);
261
+ process.exit(2);
262
+ }
263
+ }
264
+
265
+ module.exports = { inspect, parseSubmoduleStatus, primaryHasCommit };