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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -0
- package/ci/mechanism-34-worktree-submodule-reachability.test.js +307 -0
- package/ci/mechanism-35-verification-contract.test.js +416 -0
- package/ci/mutation-check.js +4 -0
- package/ci/mutation-loader.js +8 -0
- package/docs/audit.md +2 -0
- package/git-commit/SKILL.md +2 -2
- package/git-commit/guides/SUBMODULES.md +54 -1
- package/harness-everything/SKILL.md +2 -2
- package/harness-everything/scripts/verify-gate.js +223 -75
- package/hooks/scripts/lib/workflow-isolation.js +3 -4
- package/hooks/scripts/state-persist.js +1 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +3 -4
- package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/verification-contract.js +357 -0
- package/plugins/harness-everything/skills/git-commit/SKILL.md +2 -2
- package/plugins/harness-everything/skills/git-commit/guides/SUBMODULES.md +54 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +2 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/verify-gate.js +223 -75
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +1 -1
- package/plugins/harness-everything/skills/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
- package/plugins/harness-everything/skills/using-git-worktrees/scripts/submodule-reachability.js +265 -0
- package/plugins/harness-everything/skills/verification-loop/SKILL.md +15 -23
- package/plugins/harness-everything/skills/verification-loop/references/project-verification-contract.md +112 -0
- package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +4 -3
- package/plugins/harness-everything/skills/verification-loop/references/verify-contract.schema.json +32 -0
- package/plugins/harness-everything/skills/verification-loop/references/verify-result.schema.json +65 -0
- package/sbom.cdx.json +7 -7
- package/scripts/lib/verification-contract.js +357 -0
- package/using-git-worktrees/SKILL.md +1 -1
- package/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
- package/using-git-worktrees/scripts/submodule-reachability.js +265 -0
- package/verification-loop/SKILL.md +15 -23
- package/verification-loop/references/project-verification-contract.md +112 -0
- package/verification-loop/references/verification-phases.md +4 -3
- package/verification-loop/references/verify-contract.schema.json +32 -0
- package/verification-loop/references/verify-result.schema.json +65 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const CONTRACT_RELATIVE = path.join('.harness', 'verify.json');
|
|
8
|
+
const CONTRACT_VERSION = 1;
|
|
9
|
+
const MAX_TIMEOUT_SEC = 3600;
|
|
10
|
+
|
|
11
|
+
function runGit(cwd, args) {
|
|
12
|
+
return spawnSync('git', args, {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
timeout: 5000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findGitRoot(cwd = process.cwd()) {
|
|
21
|
+
const result = runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
22
|
+
return result.status === 0 ? path.resolve(result.stdout.trim()) : path.resolve(cwd);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function within(root, target) {
|
|
26
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
27
|
+
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveContained(root, value, label, { mustExist = true } = {}) {
|
|
31
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} must be a non-empty relative path`);
|
|
32
|
+
if (path.isAbsolute(value)) throw new Error(`${label} must stay inside the repository`);
|
|
33
|
+
const resolved = path.resolve(root, value);
|
|
34
|
+
if (!within(root, resolved)) throw new Error(`${label} escapes the repository`);
|
|
35
|
+
if (mustExist && !fs.existsSync(resolved)) throw new Error(`${label} does not exist: ${value}`);
|
|
36
|
+
if (mustExist) {
|
|
37
|
+
const physicalRoot = fs.realpathSync.native(path.resolve(root));
|
|
38
|
+
const physicalTarget = fs.realpathSync.native(resolved);
|
|
39
|
+
if (!within(physicalRoot, physicalTarget)) throw new Error(`${label} escapes the repository through a symbolic link`);
|
|
40
|
+
return physicalTarget;
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertKnownFields(value, allowed, label) {
|
|
46
|
+
for (const key of Object.keys(value)) {
|
|
47
|
+
if (!allowed.has(key)) throw new Error(`${label} contains unknown field: ${key}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readContract(repoRoot) {
|
|
52
|
+
const contractPath = path.join(repoRoot, CONTRACT_RELATIVE);
|
|
53
|
+
if (!fs.existsSync(contractPath)) return null;
|
|
54
|
+
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`verify.json is invalid JSON: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('verify.json must contain a JSON object');
|
|
62
|
+
assertKnownFields(raw, new Set(['version', 'roots', 'checks']), 'verify.json');
|
|
63
|
+
if (raw.version !== CONTRACT_VERSION) throw new Error(`verify.json version must be ${CONTRACT_VERSION}`);
|
|
64
|
+
if (!Array.isArray(raw.checks) || raw.checks.length === 0) throw new Error('verify.json checks must be a non-empty array');
|
|
65
|
+
|
|
66
|
+
const rootValues = raw.roots === undefined ? ['.'] : raw.roots;
|
|
67
|
+
if (!Array.isArray(rootValues) || rootValues.length === 0) throw new Error('verify.json roots must be a non-empty array');
|
|
68
|
+
const seenRoots = new Set();
|
|
69
|
+
const roots = rootValues.map((value, index) => {
|
|
70
|
+
const resolved = resolveContained(repoRoot, value, `verify.json roots[${index}]`);
|
|
71
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
72
|
+
if (seenRoots.has(key)) throw new Error(`verify.json has duplicate root: ${value}`);
|
|
73
|
+
seenRoots.add(key);
|
|
74
|
+
return { declared: value, path: resolved };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const ids = new Set();
|
|
78
|
+
const checks = raw.checks.map((check, index) => {
|
|
79
|
+
if (!check || typeof check !== 'object' || Array.isArray(check)) throw new Error(`verify.json checks[${index}] must be an object`);
|
|
80
|
+
assertKnownFields(check, new Set(['id', 'run', 'cwd', 'timeoutSec']), `verify.json checks[${index}]`);
|
|
81
|
+
if (typeof check.id !== 'string' || !check.id.trim()) throw new Error(`verify.json checks[${index}].id must be non-empty`);
|
|
82
|
+
if (ids.has(check.id)) throw new Error(`verify.json check id is duplicated: ${check.id}`);
|
|
83
|
+
ids.add(check.id);
|
|
84
|
+
if (typeof check.run !== 'string' || !check.run.trim()) throw new Error(`verify.json checks[${index}].run must be non-empty`);
|
|
85
|
+
const cwdValue = check.cwd === undefined ? '.' : check.cwd;
|
|
86
|
+
const cwd = resolveContained(repoRoot, cwdValue, `verify.json checks[${index}].cwd`);
|
|
87
|
+
const timeoutSec = check.timeoutSec === undefined ? 600 : check.timeoutSec;
|
|
88
|
+
if (!Number.isInteger(timeoutSec) || timeoutSec < 1 || timeoutSec > MAX_TIMEOUT_SEC) {
|
|
89
|
+
throw new Error(`verify.json checks[${index}].timeoutSec must be an integer from 1 to ${MAX_TIMEOUT_SEC}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: check.id,
|
|
93
|
+
run: check.run.trim(),
|
|
94
|
+
cwd,
|
|
95
|
+
cwdDeclared: cwdValue,
|
|
96
|
+
timeoutSec,
|
|
97
|
+
source: 'contract',
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
const owners = roots.filter(root => within(root.path, check.cwd))
|
|
103
|
+
.sort((a, b) => b.path.length - a.path.length);
|
|
104
|
+
if (!owners.length) throw new Error(`verify.json check ${check.id} cwd is outside declared roots`);
|
|
105
|
+
check.root = owners[0].path;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { path: contractPath, roots, checks };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detectRunner(dir) {
|
|
112
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
113
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
114
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun';
|
|
115
|
+
return 'npm';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function packageCommand(runner, name) {
|
|
119
|
+
return `${runner} run ${name}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function addCandidate(list, seen, candidate) {
|
|
123
|
+
const key = [candidate.source, candidate.run, candidate.cwd].join('\0');
|
|
124
|
+
if (seen.has(key)) return;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
list.push(candidate);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readTextIfExists(file) {
|
|
130
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function detectRoot(root) {
|
|
134
|
+
const checks = [];
|
|
135
|
+
const candidates = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const pkgPath = path.join(root, 'package.json');
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(pkgPath)) {
|
|
140
|
+
let pkg;
|
|
141
|
+
try {
|
|
142
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(`package.json at ${root} is unreadable/invalid JSON (${error.message})`);
|
|
145
|
+
}
|
|
146
|
+
const scripts = pkg && typeof pkg === 'object' && pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
147
|
+
const runner = detectRunner(root);
|
|
148
|
+
for (const name of ['lint', 'test']) {
|
|
149
|
+
const script = scripts[name];
|
|
150
|
+
if (typeof script === 'string' && script.trim() && !/no test specified/i.test(script)) {
|
|
151
|
+
checks.push({
|
|
152
|
+
id: `package-${name}`,
|
|
153
|
+
run: packageCommand(runner, name),
|
|
154
|
+
cwd: root,
|
|
155
|
+
timeoutSec: 600,
|
|
156
|
+
source: 'package-auto',
|
|
157
|
+
root,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const name of ['check', 'typecheck', 'verify', 'build']) {
|
|
162
|
+
const script = scripts[name];
|
|
163
|
+
if (typeof script === 'string' && script.trim()) {
|
|
164
|
+
addCandidate(candidates, seen, { source: `package:${name}`, run: packageCommand(runner, name), cwd: root });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (fs.existsSync(path.join(root, '.pre-commit-config.yaml')) || fs.existsSync(path.join(root, '.pre-commit-config.yml'))) {
|
|
170
|
+
addCandidate(candidates, seen, { source: 'pre-commit', run: 'pre-commit run --all-files', cwd: root });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const makeText = readTextIfExists(path.join(root, 'Makefile'));
|
|
174
|
+
if (makeText) {
|
|
175
|
+
const targets = new Set([...makeText.matchAll(/^([A-Za-z0-9_.-]+)\s*:(?![=])/gm)].map(match => match[1]));
|
|
176
|
+
for (const target of ['test', 'check', 'lint', 'ci']) {
|
|
177
|
+
if (targets.has(target)) addCandidate(candidates, seen, { source: 'make', run: `make ${target}`, cwd: root });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const justText = readTextIfExists(path.join(root, 'justfile')) || readTextIfExists(path.join(root, 'Justfile'));
|
|
182
|
+
if (justText) {
|
|
183
|
+
const recipes = new Set([...justText.matchAll(/^([A-Za-z0-9_-]+)\s*(?:[^:=\r\n]*)?:\s*$/gm)].map(match => match[1]));
|
|
184
|
+
for (const recipe of ['test', 'check', 'ci']) {
|
|
185
|
+
if (recipes.has(recipe)) addCandidate(candidates, seen, { source: 'just', run: `just ${recipe}`, cwd: root });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const pyproject = readTextIfExists(path.join(root, 'pyproject.toml'));
|
|
190
|
+
if (/\[tool\.pytest(?:\.|\])/m.test(pyproject)) {
|
|
191
|
+
addCandidate(candidates, seen, { source: 'pyproject:pytest', run: 'pytest', cwd: root });
|
|
192
|
+
addCandidate(candidates, seen, { source: 'pyproject:pytest', run: 'uv run pytest', cwd: root });
|
|
193
|
+
}
|
|
194
|
+
if (/\[tool\.ruff(?:\.|\])/m.test(pyproject)) {
|
|
195
|
+
addCandidate(candidates, seen, { source: 'pyproject:ruff', run: 'ruff check .', cwd: root });
|
|
196
|
+
addCandidate(candidates, seen, { source: 'pyproject:ruff', run: 'uv run ruff check .', cwd: root });
|
|
197
|
+
}
|
|
198
|
+
if (fs.existsSync(path.join(root, 'tsconfig.json'))) addCandidate(candidates, seen, { source: 'typescript', run: 'npx tsc --noEmit', cwd: root });
|
|
199
|
+
if (['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc', '.eslintrc.json', '.eslintrc.js', '.eslintrc.cjs'].some(name => fs.existsSync(path.join(root, name)))) {
|
|
200
|
+
addCandidate(candidates, seen, { source: 'eslint', run: 'npx eslint .', cwd: root });
|
|
201
|
+
}
|
|
202
|
+
if (fs.existsSync(path.join(root, 'tox.ini'))) addCandidate(candidates, seen, { source: 'tox', run: 'tox', cwd: root });
|
|
203
|
+
if (fs.existsSync(path.join(root, 'noxfile.py'))) addCandidate(candidates, seen, { source: 'nox', run: 'nox', cwd: root });
|
|
204
|
+
if (fs.existsSync(path.join(root, 'Cargo.toml'))) addCandidate(candidates, seen, { source: 'cargo', run: 'cargo test', cwd: root });
|
|
205
|
+
if (fs.existsSync(path.join(root, 'go.mod'))) addCandidate(candidates, seen, { source: 'go', run: 'go test ./...', cwd: root });
|
|
206
|
+
|
|
207
|
+
if (hasCommitHook(root)) {
|
|
208
|
+
addCandidate(candidates, seen, { source: 'git-hook', run: 'git commit (pre-commit hook configured)', cwd: root });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { checks, candidates };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function enumerateSubmoduleRoots(repoRoot) {
|
|
215
|
+
const roots = [path.resolve(repoRoot)];
|
|
216
|
+
const result = runGit(repoRoot, ['submodule', 'status', '--recursive']);
|
|
217
|
+
if (result.status !== 0 || !result.stdout.trim()) return roots;
|
|
218
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
219
|
+
if (!line.trim()) continue;
|
|
220
|
+
const match = line.match(/^[ +-U]?([0-9a-fA-F]{40,64})\s+(.+?)(?:\s+\(.+\))?$/);
|
|
221
|
+
if (!match) continue;
|
|
222
|
+
const candidate = path.resolve(repoRoot, match[2]);
|
|
223
|
+
if (within(repoRoot, candidate) && fs.existsSync(candidate)) roots.push(candidate);
|
|
224
|
+
}
|
|
225
|
+
return [...new Set(roots)];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hookIsRunnable(stat, platform = process.platform) {
|
|
229
|
+
return !!stat && stat.isFile() && (platform === 'win32' || (stat.mode & 0o111) !== 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function hasCommitHook(repoRoot, platform = process.platform) {
|
|
233
|
+
const result = runGit(repoRoot, ['rev-parse', '--git-path', 'hooks/pre-commit']);
|
|
234
|
+
if (result.status !== 0) return false;
|
|
235
|
+
const value = result.stdout.trim();
|
|
236
|
+
if (!value) return false;
|
|
237
|
+
const hook = path.isAbsolute(value) ? value : path.resolve(repoRoot, value);
|
|
238
|
+
let stat;
|
|
239
|
+
try { stat = fs.statSync(hook); }
|
|
240
|
+
catch (_) { return false; }
|
|
241
|
+
return hookIsRunnable(stat, platform);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeCommand(command) {
|
|
245
|
+
return String(command || '').trim().replace(/\s+/g, ' ');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function hasControlOperators(command) {
|
|
249
|
+
return /(?:&&|\|\||[;|\r\n])/.test(command);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function commandMatches(command, expected) {
|
|
253
|
+
const actual = normalizeCommand(command);
|
|
254
|
+
const target = normalizeCommand(expected);
|
|
255
|
+
if (!actual || !target || hasControlOperators(actual)) return false;
|
|
256
|
+
return actual === target || actual.startsWith(target + ' ');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isGitCommit(command) {
|
|
260
|
+
const actual = normalizeCommand(command);
|
|
261
|
+
return /^git\s+commit(?:\s|$)/i.test(actual);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function bypassesHooks(command) {
|
|
265
|
+
const actual = normalizeCommand(command);
|
|
266
|
+
if (/(?:^|\s)--no-verify(?:\s|$)/i.test(actual) || /(?:^|\s)-n(?:\s|$)/.test(actual)) return true;
|
|
267
|
+
|
|
268
|
+
// Git accepts boolean short options in clusters, e.g. `git commit -an`.
|
|
269
|
+
// Treat a cluster containing -n as hook bypass only when every clustered
|
|
270
|
+
// option is a no-argument commit flag; this avoids mistaking attached
|
|
271
|
+
// arguments such as `-S<key>` or `-m<message>` for --no-verify.
|
|
272
|
+
return actual.split(/\s+/).some(token =>
|
|
273
|
+
/^-[avqseipon]+$/i.test(token) && token.length > 2 && token.slice(1).toLowerCase().includes('n')
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function legacyVerificationCommand(command) {
|
|
278
|
+
const value = normalizeCommand(command);
|
|
279
|
+
return /^(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|build|typecheck|check|verify)\b|npx\s+(?:jest|vitest|mocha|tsc|eslint)\b|(?:jest|vitest|mocha|pytest|rspec|phpunit)\b|uv\s+run\s+(?:pytest|ruff)\b|(?:make|just)\s+(?:test|check|lint|ci)\b|cargo\s+(?:test|check)\b|go\s+test\b|dotnet\s+(?:test|build)\b)/i.test(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function verificationCommandSet(cwd, options = {}) {
|
|
283
|
+
const repoRoot = path.resolve(options.root || findGitRoot(cwd));
|
|
284
|
+
let contract = null;
|
|
285
|
+
try { contract = readContract(repoRoot); }
|
|
286
|
+
catch (error) {
|
|
287
|
+
// The verify gate treats an invalid authoritative contract as FAILED.
|
|
288
|
+
// Preserve that fail-closed state here instead of falling through to
|
|
289
|
+
// legacy recognition and manufacturing contradictory verification evidence.
|
|
290
|
+
return {
|
|
291
|
+
repoRoot,
|
|
292
|
+
contract: null,
|
|
293
|
+
contractInvalid: true,
|
|
294
|
+
contractError: error && error.message ? error.message : String(error),
|
|
295
|
+
commands: [],
|
|
296
|
+
commitHookDeclared: false,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (contract) {
|
|
301
|
+
return {
|
|
302
|
+
repoRoot,
|
|
303
|
+
contract,
|
|
304
|
+
contractInvalid: false,
|
|
305
|
+
commands: contract.checks.map(check => check.run),
|
|
306
|
+
commitHookDeclared: contract.checks.some(check => /^pre-commit\s+/i.test(normalizeCommand(check.run))),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const root = findGitRoot(cwd);
|
|
311
|
+
let detected;
|
|
312
|
+
try { detected = detectRoot(root); }
|
|
313
|
+
catch (_) { detected = { checks: [], candidates: [] }; }
|
|
314
|
+
return {
|
|
315
|
+
repoRoot,
|
|
316
|
+
contract: null,
|
|
317
|
+
contractInvalid: false,
|
|
318
|
+
commands: [
|
|
319
|
+
...detected.checks.map(check => check.run),
|
|
320
|
+
...detected.candidates.filter(candidate => candidate.source !== 'git-hook').map(candidate => candidate.run),
|
|
321
|
+
],
|
|
322
|
+
commitHookDeclared: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isVerificationCommand(command, options = {}) {
|
|
327
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
328
|
+
const actual = normalizeCommand(command);
|
|
329
|
+
if (!actual || hasControlOperators(actual)) return false;
|
|
330
|
+
const set = verificationCommandSet(cwd, options);
|
|
331
|
+
if (set.contractInvalid) return false;
|
|
332
|
+
|
|
333
|
+
if (isGitCommit(actual)) {
|
|
334
|
+
return !!set.commitHookDeclared && !bypassesHooks(actual) && hasCommitHook(set.repoRoot);
|
|
335
|
+
}
|
|
336
|
+
if (set.commands.some(expected => commandMatches(actual, expected))) return true;
|
|
337
|
+
return !set.contract && legacyVerificationCommand(actual);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
CONTRACT_RELATIVE,
|
|
342
|
+
MAX_TIMEOUT_SEC,
|
|
343
|
+
addCandidate,
|
|
344
|
+
commandMatches,
|
|
345
|
+
detectRoot,
|
|
346
|
+
enumerateSubmoduleRoots,
|
|
347
|
+
findGitRoot,
|
|
348
|
+
hasCommitHook,
|
|
349
|
+
hookIsRunnable,
|
|
350
|
+
isVerificationCommand,
|
|
351
|
+
legacyVerificationCommand,
|
|
352
|
+
normalizeCommand,
|
|
353
|
+
readContract,
|
|
354
|
+
resolveContained,
|
|
355
|
+
verificationCommandSet,
|
|
356
|
+
within,
|
|
357
|
+
};
|
|
@@ -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.
|
|
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: "Route software/project work through Harness: classify tier/topolog
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.26.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Harness Everything
|
|
@@ -20,7 +20,7 @@ Kernel routing entry.
|
|
|
20
20
|
|
|
21
21
|
## Invariants
|
|
22
22
|
1. **Route (MUST)** — establish scope/tier and selected/deferred topology.
|
|
23
|
-
2. **Verify (MUST)** — completion claims need objective evidence.
|
|
23
|
+
2. **Verify (MUST)** — completion claims need objective evidence; `verify-gate.js --json` distinguishes passed, failed, discovered-but-unrun, and absent project checks.
|
|
24
24
|
3. **Zoom out (MUST)** — after 3 same-signature failures, use `zoom-out`.
|
|
25
25
|
4. **Resolve suggestions (MUST)** — read/evaluate each suggested `SKILL.md`; applicable core contracts MUST run, otherwise retain a flow-grounded reason.
|
|
26
26
|
5. **Surface status (MUST)** — non-trivial work renders `### 🚦 Harness Status` with bold `Current`, `Read / Evidence`, `Next`; optional `Risk / Blocked`.
|