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,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
+ }
@@ -15,6 +15,7 @@ const checks = [
15
15
  ['transcript command qualifiers', 'behavioral-evals/transcript-parser.js', 'ci/mechanism-2t-transcript-parser.test.js'],
16
16
  ['fixture path boundaries', 'behavioral-evals/run.js', 'ci/mechanism-2j-behavioral-runner.test.js'],
17
17
  ['unbound workspace identity', 'scripts/lib/workspace.js', 'ci/mechanism-2u-issue-42-regressions.test.js'],
18
+ ['worktree submodule reachability', 'using-git-worktrees/scripts/submodule-reachability.js', 'ci/mechanism-34-worktree-submodule-reachability.test.js'],
18
19
  ['verification contract precedence', 'harness-everything/scripts/verify-gate.js', 'ci/mechanism-35-verification-contract.test.js'],
19
20
  ];
20
21
 
@@ -33,6 +34,7 @@ for (const [label, target, testFile] of checks) {
33
34
  'behavioral-evals/transcript-parser.js': 'transcript-command',
34
35
  'behavioral-evals/run.js': 'runner-fixture',
35
36
  'scripts/lib/workspace.js': 'workspace-identity',
37
+ 'using-git-worktrees/scripts/submodule-reachability.js': 'submodule-reachability',
36
38
  'harness-everything/scripts/verify-gate.js': 'verify-contract-precedence',
37
39
  }[target],
38
40
  HARNESS_MUTATION_TARGET: path.join(ROOT, target),
@@ -40,6 +40,10 @@ const mutations = {
40
40
  ' if (!workspaceRoot) return UNBOUND_WORKSPACE_KEY;',
41
41
  ' if (!workspaceRoot) return path.basename(process.cwd()); // mutation: derive unbound identity from cwd'
42
42
  ),
43
+ 'submodule-reachability': (source) => source.replace(
44
+ ' const externallyReachable = !linkedWorktree || reachableFromRemote || presentInPrimary;',
45
+ ' const externallyReachable = true; // mutation: accept isolated worktree-only commits'
46
+ ),
43
47
  'verify-contract-precedence': (source) => source.replace(
44
48
  ' finalize(contract ? contractPlan(contract) : discoveredPlan());',
45
49
  ' finalize(discoveredPlan()); // mutation: ignore the authoritative project contract'
@@ -21,6 +21,32 @@ assert.deepStrictEqual(validateCase({
21
21
  expectations: [{ type: 'tool_call', value: 'Bash' }]
22
22
  }), [], 'tool_call must remain part of the runner/validator schema');
23
23
 
24
+ const setupCase = setup => ({
25
+ id: 'fixture-setup-schema',
26
+ prompt: 'inspect the prepared fixture',
27
+ max_turns: 1,
28
+ fixture: {
29
+ files: [{ path: 'README.md', content: 'fixture' }],
30
+ setup,
31
+ },
32
+ expectations: [{ type: 'trace_contains', value: 'fixture' }],
33
+ });
34
+ assert.deepStrictEqual(
35
+ validateCase(setupCase('behavioral-evals/fixtures/setup/noop.js')),
36
+ [],
37
+ 'checked-in repository-relative .js setup should be valid'
38
+ );
39
+ for (const [setup, expected] of [
40
+ ['../escape.js', /fixture\.setup/],
41
+ ['/tmp/escape.js', /fixture\.setup/],
42
+ ['behavioral-evals/fixtures/setup/noop.txt', /\.js/],
43
+ ['behavioral-evals/fixtures/setup/missing.js', /missing|checked-in/],
44
+ [{ command: 'echo unsafe' }, /fixture\.setup/],
45
+ ]) {
46
+ const setupErrors = validateCase(setupCase(setup));
47
+ assert.ok(setupErrors.some(error => expected.test(error)), `setup ${JSON.stringify(setup)} should be rejected: ${setupErrors.join('; ')}`);
48
+ }
49
+
24
50
  const transcriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'behavioral-case-validation-'));
25
51
  try {
26
52
  const transcriptPath = path.join(transcriptDir, 'trace.jsonl');
@@ -4,7 +4,7 @@ description: "Generate Angular-style commit messages after verifying the environ
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.19.1
7
+ version: 0.27.0
8
8
  ---
9
9
 
10
10
  # Git Commit (Angular Style)
@@ -20,7 +20,7 @@ metadata:
20
20
 
21
21
  ## Quick Workflow
22
22
 
23
- 1. **MUST** run `git status`; no repo → offer `git init` or skip; submodules changed → commit first per `<this-skill-dir>/guides/SUBMODULES.md`.
23
+ 1. **MUST** run `git status`; no repo → offer `git init` or skip; submodules changed → commit first per `<this-skill-dir>/guides/SUBMODULES.md`, including its worktree reachability gate.
24
24
  2. **MUST** inspect `git diff --cached` before committing. Nothing staged → prompt user / stage only user-authorized targeted files. Unrelated concerns **SHOULD** be split unless coupling or explicit user intent justifies one commit.
25
25
  3. Format `<type>(<scope>): <subject>` per `<this-skill-dir>/guides/ANGULAR_STYLE.md`. Multiline/Windows → `.git-commit-msg.txt` + `git commit -F`, clean up.
26
26
  4. `git commit -m "..."`, then **MUST** verify the result with `git log -1`.
@@ -14,6 +14,59 @@
14
14
  3. Generation: For Sub Repos WITH staged changes, MUST execute `COMMIT_GENERATION.md`.
15
15
  4. Validation: MUST verify generation success. IF ANY Sub Repo has remaining staged changes, MUST diagnose failure and halt.
16
16
 
17
+ ## [Reachability Phase]
18
+ Before staging any changed submodule gitlink in the Main Repo, MUST run the preferred deterministic check:
19
+
20
+ ```bash
21
+ node <skills-repo-root>/using-git-worktrees/scripts/submodule-reachability.js --json
22
+ ```
23
+
24
+ - Exit 0: continue to Indexing.
25
+ - Exit 1: MUST stop before staging and inspect the reported unreachable submodule(s); use the options below.
26
+ - Exit 2: MUST diagnose the inspection failure before staging.
27
+
28
+ If the helper is unavailable, use this raw fallback. First determine whether the Main Repo is a linked worktree:
29
+
30
+ ```bash
31
+ git rev-parse --git-dir
32
+ git rev-parse --git-common-dir
33
+ ```
34
+
35
+ If the git dir and common dir differ, then for EACH changed Sub Repo:
36
+
37
+ 1. Identify the repository that **owns** the gitlink and the submodule path relative to that owner. For a direct submodule the owner is the Main Repo; for a nested submodule it is the immediate parent submodule. If this cannot be identified reliably, MUST stop and use the helper rather than guess.
38
+ 2. Collect every commit that can affect the recorded gitlink, not only the current working HEAD:
39
+ ```bash
40
+ git -C <submodule> rev-parse HEAD
41
+ git -C <owner> ls-files --stage -- <relative-submodule-path>
42
+ git -C <owner> ls-tree HEAD -- <relative-submodule-path>
43
+ ```
44
+ From the last two commands, only mode `160000` stage `0` entries are valid gitlinks. Any stage `1`–`3` entry is unmerged and MUST stop. Check every unique SHA from working HEAD, index, and owner HEAD.
45
+ 3. Check whether the Sub Repo is detached:
46
+ ```bash
47
+ git -C <submodule> branch --show-current
48
+ ```
49
+ 4. For EACH collected SHA, first check remote-tracking reachability:
50
+ ```bash
51
+ git -C <submodule> branch -r --contains <sha>
52
+ ```
53
+ If no remote-tracking ref contains it, prove that the **exact** primary-checkout submodule repository owns the object:
54
+ ```bash
55
+ git -C <primary>/<submodule> rev-parse --show-toplevel
56
+ git -C <primary>/<submodule> cat-file -e <sha>^{commit}
57
+ ```
58
+ The reported top-level path MUST equal the expected primary submodule path. A missing/deinitialized nested path can make `git -C` fall back to a parent repository; that is not proof. If the exact primary submodule cannot be proven, initialize/fetch it or use the helper, otherwise MUST fail closed.
59
+
60
+ A branch name alone does not publish a commit. If ANY working/index/HEAD gitlink SHA has neither a containing remote-tracking ref nor a proven exact primary-submodule object, it is an **unpublished gitlink risk**, whether the submodule is detached or on a named branch. MUST stop before `git add <submodule>` and present:
61
+
62
+ - **[1] Create/publish a branch** — hand off to `<skills-repo-root>/using-git-worktrees/references/submodules-in-worktrees.md`.
63
+ - **[2] Stage anyway** — only on explicit user choice; MUST record the unpublished gitlink risk in the handoff.
64
+ - **[3] Abort** — do not stage the gitlink.
65
+
66
+ `git-commit` MUST NOT create branches or push as part of this gate. It only detects the risk and hands off to the worktree reference when publication is needed.
67
+
68
+ If the Main Repo is not a linked worktree, or the commit is already externally reachable, continue normally.
69
+
17
70
  ## [Indexing Phase]
18
71
  5. Preference Check: MUST read the resolved Harness memory file `git-commit-prefs.md` for the `auto_stage_submodules` setting. Prefer an existing `MEMORY.md`/`RULES.md`; otherwise use `.github/harness-everything/memories/git-commit-prefs.md`.
19
72
  6. Execution:
@@ -24,4 +77,4 @@
24
77
  - IF user selects [2]: MUST NOT run `git add`.
25
78
 
26
79
  ## [State Handoff]
27
- 7. Handoff: MUST summarize completed Sub Repo updates (including paths and logs) and explicitly carry them over when returning control to `SKILL.md`.
80
+ 7. Handoff: MUST summarize completed Sub Repo updates (including paths, commit SHA, publication/reachability evidence, and logs) and explicitly carry them over when returning control to `SKILL.md`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.26.0",
4
+ "version": "0.28.0",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.26.0",
4
+ "version": "0.28.0",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
@@ -4,7 +4,7 @@ description: "Generate Angular-style commit messages after verifying the environ
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.19.1
7
+ version: 0.27.0
8
8
  ---
9
9
 
10
10
  # Git Commit (Angular Style)
@@ -20,7 +20,7 @@ metadata:
20
20
 
21
21
  ## Quick Workflow
22
22
 
23
- 1. **MUST** run `git status`; no repo → offer `git init` or skip; submodules changed → commit first per `<this-skill-dir>/guides/SUBMODULES.md`.
23
+ 1. **MUST** run `git status`; no repo → offer `git init` or skip; submodules changed → commit first per `<this-skill-dir>/guides/SUBMODULES.md`, including its worktree reachability gate.
24
24
  2. **MUST** inspect `git diff --cached` before committing. Nothing staged → prompt user / stage only user-authorized targeted files. Unrelated concerns **SHOULD** be split unless coupling or explicit user intent justifies one commit.
25
25
  3. Format `<type>(<scope>): <subject>` per `<this-skill-dir>/guides/ANGULAR_STYLE.md`. Multiline/Windows → `.git-commit-msg.txt` + `git commit -F`, clean up.
26
26
  4. `git commit -m "..."`, then **MUST** verify the result with `git log -1`.
@@ -14,6 +14,59 @@
14
14
  3. Generation: For Sub Repos WITH staged changes, MUST execute `COMMIT_GENERATION.md`.
15
15
  4. Validation: MUST verify generation success. IF ANY Sub Repo has remaining staged changes, MUST diagnose failure and halt.
16
16
 
17
+ ## [Reachability Phase]
18
+ Before staging any changed submodule gitlink in the Main Repo, MUST run the preferred deterministic check:
19
+
20
+ ```bash
21
+ node <skills-repo-root>/using-git-worktrees/scripts/submodule-reachability.js --json
22
+ ```
23
+
24
+ - Exit 0: continue to Indexing.
25
+ - Exit 1: MUST stop before staging and inspect the reported unreachable submodule(s); use the options below.
26
+ - Exit 2: MUST diagnose the inspection failure before staging.
27
+
28
+ If the helper is unavailable, use this raw fallback. First determine whether the Main Repo is a linked worktree:
29
+
30
+ ```bash
31
+ git rev-parse --git-dir
32
+ git rev-parse --git-common-dir
33
+ ```
34
+
35
+ If the git dir and common dir differ, then for EACH changed Sub Repo:
36
+
37
+ 1. Identify the repository that **owns** the gitlink and the submodule path relative to that owner. For a direct submodule the owner is the Main Repo; for a nested submodule it is the immediate parent submodule. If this cannot be identified reliably, MUST stop and use the helper rather than guess.
38
+ 2. Collect every commit that can affect the recorded gitlink, not only the current working HEAD:
39
+ ```bash
40
+ git -C <submodule> rev-parse HEAD
41
+ git -C <owner> ls-files --stage -- <relative-submodule-path>
42
+ git -C <owner> ls-tree HEAD -- <relative-submodule-path>
43
+ ```
44
+ From the last two commands, only mode `160000` stage `0` entries are valid gitlinks. Any stage `1`–`3` entry is unmerged and MUST stop. Check every unique SHA from working HEAD, index, and owner HEAD.
45
+ 3. Check whether the Sub Repo is detached:
46
+ ```bash
47
+ git -C <submodule> branch --show-current
48
+ ```
49
+ 4. For EACH collected SHA, first check remote-tracking reachability:
50
+ ```bash
51
+ git -C <submodule> branch -r --contains <sha>
52
+ ```
53
+ If no remote-tracking ref contains it, prove that the **exact** primary-checkout submodule repository owns the object:
54
+ ```bash
55
+ git -C <primary>/<submodule> rev-parse --show-toplevel
56
+ git -C <primary>/<submodule> cat-file -e <sha>^{commit}
57
+ ```
58
+ The reported top-level path MUST equal the expected primary submodule path. A missing/deinitialized nested path can make `git -C` fall back to a parent repository; that is not proof. If the exact primary submodule cannot be proven, initialize/fetch it or use the helper, otherwise MUST fail closed.
59
+
60
+ A branch name alone does not publish a commit. If ANY working/index/HEAD gitlink SHA has neither a containing remote-tracking ref nor a proven exact primary-submodule object, it is an **unpublished gitlink risk**, whether the submodule is detached or on a named branch. MUST stop before `git add <submodule>` and present:
61
+
62
+ - **[1] Create/publish a branch** — hand off to `<skills-repo-root>/using-git-worktrees/references/submodules-in-worktrees.md`.
63
+ - **[2] Stage anyway** — only on explicit user choice; MUST record the unpublished gitlink risk in the handoff.
64
+ - **[3] Abort** — do not stage the gitlink.
65
+
66
+ `git-commit` MUST NOT create branches or push as part of this gate. It only detects the risk and hands off to the worktree reference when publication is needed.
67
+
68
+ If the Main Repo is not a linked worktree, or the commit is already externally reachable, continue normally.
69
+
17
70
  ## [Indexing Phase]
18
71
  5. Preference Check: MUST read the resolved Harness memory file `git-commit-prefs.md` for the `auto_stage_submodules` setting. Prefer an existing `MEMORY.md`/`RULES.md`; otherwise use `.github/harness-everything/memories/git-commit-prefs.md`.
19
72
  6. Execution:
@@ -24,4 +77,4 @@
24
77
  - IF user selects [2]: MUST NOT run `git add`.
25
78
 
26
79
  ## [State Handoff]
27
- 7. Handoff: MUST summarize completed Sub Repo updates (including paths and logs) and explicitly carry them over when returning control to `SKILL.md`.
80
+ 7. Handoff: MUST summarize completed Sub Repo updates (including paths, commit SHA, publication/reachability evidence, and logs) and explicitly carry them over when returning control to `SKILL.md`.
@@ -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>