harness-everything 0.15.0 → 0.16.1
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/AGENTS.md +2 -2
- package/CHANGELOG.md +23 -0
- package/README.md +7 -7
- package/VERIFICATION.md +3 -3
- package/bin/cli.js +17 -0
- package/ci/invariant-routing.test.js +58 -158
- package/ci/mechanism-2a-rule-of-3.test.js +73 -138
- package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
- package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
- package/ci/mechanism-2al-codex-hooks-compat.test.js +286 -0
- package/ci/mechanism-2b-boundary-guard.test.js +6 -16
- package/ci/mechanism-2e-scope-guard.test.js +8 -24
- package/ci/mechanism-2f-stop-gate.test.js +21 -75
- package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
- package/ci/openai-plugin-package.test.js +8 -8
- package/ci/openai-plugin-runtime.test.js +2 -2
- package/ci/router-workflow-plan.test.js +6 -6
- package/ci/self-evolve-loop.test.js +4 -1
- package/ci/workflow-execution-contract.test.js +51 -129
- package/ci/workflow-plan-consumers-edge.test.js +1 -1
- package/ci/workflow-plan-consumers.test.js +6 -6
- package/docs/architecture.md +21 -21
- package/docs/enforcement-locks.md +41 -102
- package/docs/mechanism-first-skill-mesh.md +2 -2
- package/docs/philosophy.md +2 -2
- package/docs/platform-capabilities.md +19 -3
- package/docs/platform-compatibility.json +4 -4
- package/docs/routing.md +14 -14
- package/docs/workflow-runtime.md +80 -110
- package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
- package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/harness-everything/SKILL.md +11 -11
- package/harness-everything/references/triage-and-tiers.md +11 -11
- package/harness-everything/scripts/kernel-router-core.js +5 -5
- package/harness-everything/scripts/kernel-router.js +15 -17
- package/harness-everything/scripts/router-contract-core.js +1 -1
- package/hooks/hooks.json +10 -23
- package/hooks/scripts/atomic-commit-check.js +1 -1
- package/hooks/scripts/boundary-guard.js +4 -4
- package/hooks/scripts/contract-test.js +8 -11
- package/hooks/scripts/depth-guard.js +2 -2
- package/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/hooks/scripts/rule-of-3.js +4 -28
- package/hooks/scripts/state-persist.js +36 -136
- package/hooks/scripts/stop-gate.js +15 -48
- package/hooks/scripts/subagent-scope-guard.js +8 -8
- package/hooks/scripts/workflow-disposition.js +2 -10
- package/hooks/scripts/workflow-gate.js +33 -84
- package/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/hooks/scripts/workflow-stop-gate.js +23 -38
- package/opencode-plugin/README.md +9 -10
- package/opencode-plugin/index.mjs +14 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +4 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +3 -3
- package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
- package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
- package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
- package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
- package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
- package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
- package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +15 -17
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
- package/scripts/codex-hooks-compat.js +521 -0
- package/scripts/lib/advisory-text.js +16 -29
- package/using-git-worktrees/SKILL.md +5 -5
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
const SOURCE_PLUGIN_ROOT = path.join(PACKAGE_ROOT, 'plugins', 'harness-everything');
|
|
11
|
+
const PACKAGE_VERSION = require(path.join(PACKAGE_ROOT, 'package.json')).version;
|
|
12
|
+
const MANIFEST_SCHEMA = 1;
|
|
13
|
+
const MANIFEST_MODE = 'codex-user-hooks-compat';
|
|
14
|
+
|
|
15
|
+
function stable(value) {
|
|
16
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
17
|
+
if (!value || typeof value !== 'object') return value;
|
|
18
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stableJson(value) {
|
|
22
|
+
return JSON.stringify(stable(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sha256(value) {
|
|
26
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function treeHash(root) {
|
|
30
|
+
if (fs.lstatSync(root).isSymbolicLink()) {
|
|
31
|
+
throw new Error('refusing linked compatibility runtime: ' + root);
|
|
32
|
+
}
|
|
33
|
+
const rows = [];
|
|
34
|
+
function walk(dir) {
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
36
|
+
const full = path.join(dir, entry.name);
|
|
37
|
+
const rel = path.relative(root, full).replace(/\\/g, '/');
|
|
38
|
+
if (entry.isDirectory()) walk(full);
|
|
39
|
+
else if (entry.isFile()) rows.push(rel + ':' + sha256(fs.readFileSync(full)));
|
|
40
|
+
else if (entry.isSymbolicLink()) throw new Error('refusing linked compatibility runtime entry: ' + full);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(root);
|
|
44
|
+
return sha256(rows.join('\n'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function entryHash(entry) {
|
|
48
|
+
return sha256(stableJson(entry));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultCodexHome(env = process.env) {
|
|
52
|
+
return path.resolve(env.CODEX_HOME || path.join(os.homedir(), '.codex'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pathsFor(codexHome, sourceHash = null) {
|
|
56
|
+
const home = path.resolve(codexHome);
|
|
57
|
+
const ownerRoot = path.join(home, 'harness-everything', 'compat-hooks');
|
|
58
|
+
const runtimeId = sourceHash ? PACKAGE_VERSION + '-' + sourceHash.slice(0, 12) : null;
|
|
59
|
+
return {
|
|
60
|
+
codexHome: home,
|
|
61
|
+
hooksFile: path.join(home, 'hooks.json'),
|
|
62
|
+
configFile: path.join(home, 'config.toml'),
|
|
63
|
+
ownerRoot,
|
|
64
|
+
manifestFile: path.join(ownerRoot, 'manifest.json'),
|
|
65
|
+
runtimeRoot: path.join(ownerRoot, 'runtime'),
|
|
66
|
+
runtimeDir: runtimeId ? path.join(ownerRoot, 'runtime', runtimeId) : null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJsonObject(file, fallback) {
|
|
71
|
+
if (!fs.existsSync(file)) return fallback;
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
74
|
+
throw new Error(file + ' must contain a JSON object');
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readHooksFile(file) {
|
|
80
|
+
const data = readJsonObject(file, { hooks: {} });
|
|
81
|
+
if (data.hooks === undefined) data.hooks = {};
|
|
82
|
+
if (!data.hooks || typeof data.hooks !== 'object' || Array.isArray(data.hooks)) {
|
|
83
|
+
throw new Error(file + ' has invalid hooks object');
|
|
84
|
+
}
|
|
85
|
+
for (const [event, entries] of Object.entries(data.hooks)) {
|
|
86
|
+
if (!Array.isArray(entries)) throw new Error(file + ' hooks.' + event + ' must be an array');
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function samePath(left, right) {
|
|
92
|
+
const normalize = value => {
|
|
93
|
+
const resolved = path.resolve(value);
|
|
94
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
95
|
+
};
|
|
96
|
+
return normalize(left) === normalize(right);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isPathInside(root, candidate) {
|
|
100
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
101
|
+
return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertOwnedPath(codexHome, target, label) {
|
|
105
|
+
const home = path.resolve(codexHome);
|
|
106
|
+
const resolvedTarget = path.resolve(target);
|
|
107
|
+
if (!samePath(home, resolvedTarget) && !isPathInside(home, resolvedTarget)) {
|
|
108
|
+
throw new Error('refusing outside Codex home ' + label + ': ' + target);
|
|
109
|
+
}
|
|
110
|
+
if (!fs.existsSync(home)) return;
|
|
111
|
+
|
|
112
|
+
const realpath = fs.realpathSync.native || fs.realpathSync;
|
|
113
|
+
const physicalHome = realpath(home);
|
|
114
|
+
const relative = path.relative(home, resolvedTarget);
|
|
115
|
+
let current = home;
|
|
116
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
117
|
+
current = path.join(current, segment);
|
|
118
|
+
if (!fs.existsSync(current)) break;
|
|
119
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
120
|
+
throw new Error('refusing linked ' + label + ': ' + current);
|
|
121
|
+
}
|
|
122
|
+
const physical = realpath(current);
|
|
123
|
+
if (!samePath(physicalHome, physical) && !isPathInside(physicalHome, physical)) {
|
|
124
|
+
throw new Error('refusing outside Codex home ' + label + ': ' + current);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function assertCompatibilityPaths(paths) {
|
|
130
|
+
for (const [label, target] of [
|
|
131
|
+
['hooks file', paths.hooksFile],
|
|
132
|
+
['compatibility owner', paths.ownerRoot],
|
|
133
|
+
['compatibility manifest', paths.manifestFile],
|
|
134
|
+
['compatibility runtime root', paths.runtimeRoot],
|
|
135
|
+
['compatibility runtime', paths.runtimeDir],
|
|
136
|
+
]) {
|
|
137
|
+
if (target) assertOwnedPath(paths.codexHome, target, label);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readManifest(file, expectedPaths = null) {
|
|
142
|
+
if (!fs.existsSync(file)) return null;
|
|
143
|
+
const data = readJsonObject(file, null);
|
|
144
|
+
if (data.schemaVersion !== MANIFEST_SCHEMA ||
|
|
145
|
+
data.package !== 'harness-everything' ||
|
|
146
|
+
data.mode !== MANIFEST_MODE ||
|
|
147
|
+
typeof data.packageVersion !== 'string' ||
|
|
148
|
+
!/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(data.packageVersion) ||
|
|
149
|
+
typeof data.sourcePluginSha256 !== 'string' ||
|
|
150
|
+
!/^[0-9a-f]{64}$/.test(data.sourcePluginSha256) ||
|
|
151
|
+
typeof data.runtimeDir !== 'string' ||
|
|
152
|
+
typeof data.hooksFile !== 'string' ||
|
|
153
|
+
typeof data.createdHooksFile !== 'boolean' ||
|
|
154
|
+
!Array.isArray(data.entries)) {
|
|
155
|
+
throw new Error('refusing unknown/corrupt Harness compatibility manifest: ' + file);
|
|
156
|
+
}
|
|
157
|
+
if (!expectedPaths) return data;
|
|
158
|
+
|
|
159
|
+
const expectedRuntimeDir = path.join(
|
|
160
|
+
expectedPaths.runtimeRoot,
|
|
161
|
+
data.packageVersion + '-' + data.sourcePluginSha256.slice(0, 12)
|
|
162
|
+
);
|
|
163
|
+
assertOwnedPath(expectedPaths.codexHome, data.runtimeDir, 'manifest runtime');
|
|
164
|
+
if (!samePath(data.hooksFile, expectedPaths.hooksFile) ||
|
|
165
|
+
!isPathInside(expectedPaths.runtimeRoot, data.runtimeDir) ||
|
|
166
|
+
!samePath(data.runtimeDir, expectedRuntimeDir) ||
|
|
167
|
+
!fs.existsSync(data.runtimeDir) ||
|
|
168
|
+
treeHash(data.runtimeDir) !== data.sourcePluginSha256) {
|
|
169
|
+
throw new Error('refusing unknown/corrupt Harness compatibility manifest: ' + file);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const allowed = new Map();
|
|
173
|
+
for (const record of hookEntriesFrom(path.resolve(data.runtimeDir), path.resolve(data.runtimeDir))) {
|
|
174
|
+
const key = record.event + '\0' + record.hash;
|
|
175
|
+
if (!allowed.has(key)) allowed.set(key, []);
|
|
176
|
+
allowed.get(key).push(stableJson(record.entry));
|
|
177
|
+
}
|
|
178
|
+
for (const record of data.entries) {
|
|
179
|
+
if (!record || typeof record !== 'object' || Array.isArray(record) ||
|
|
180
|
+
typeof record.event !== 'string' || !record.event ||
|
|
181
|
+
typeof record.hash !== 'string' || !/^[0-9a-f]{64}$/.test(record.hash) ||
|
|
182
|
+
!record.entry || typeof record.entry !== 'object' || Array.isArray(record.entry) ||
|
|
183
|
+
entryHash(record.entry) !== record.hash) {
|
|
184
|
+
throw new Error('refusing unknown/corrupt Harness compatibility manifest: ' + file);
|
|
185
|
+
}
|
|
186
|
+
const key = record.event + '\0' + record.hash;
|
|
187
|
+
const candidates = allowed.get(key) || [];
|
|
188
|
+
const serialized = stableJson(record.entry);
|
|
189
|
+
const match = candidates.indexOf(serialized);
|
|
190
|
+
if (match === -1) {
|
|
191
|
+
throw new Error('refusing unknown/corrupt Harness compatibility manifest: ' + file);
|
|
192
|
+
}
|
|
193
|
+
candidates.splice(match, 1);
|
|
194
|
+
}
|
|
195
|
+
return data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function atomicWriteFile(file, value) {
|
|
199
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
200
|
+
const temp = file + '.' + process.pid + '.tmp';
|
|
201
|
+
try {
|
|
202
|
+
fs.writeFileSync(temp, value);
|
|
203
|
+
fs.renameSync(temp, file);
|
|
204
|
+
} finally {
|
|
205
|
+
try { if (fs.existsSync(temp)) fs.rmSync(temp, { force: true }); } catch (_) {}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function atomicWriteJson(file, value) {
|
|
210
|
+
atomicWriteFile(file, JSON.stringify(value, null, 2) + '\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function replacePluginRoot(value, runtimeDir, windowsForm) {
|
|
214
|
+
if (typeof value !== 'string') return value;
|
|
215
|
+
const root = windowsForm
|
|
216
|
+
? runtimeDir.replace(/\//g, '\\')
|
|
217
|
+
: runtimeDir.replace(/\\/g, '/');
|
|
218
|
+
return value
|
|
219
|
+
.replace(/\$\{PLUGIN_ROOT\}/g, root)
|
|
220
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, root);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function transformEntry(entry, runtimeDir) {
|
|
224
|
+
function visit(value, key) {
|
|
225
|
+
if (Array.isArray(value)) return value.map(item => visit(item, key));
|
|
226
|
+
if (value && typeof value === 'object') {
|
|
227
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, visit(child, childKey)]));
|
|
228
|
+
}
|
|
229
|
+
if (typeof value === 'string') return replacePluginRoot(value, runtimeDir, key === 'commandWindows');
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
return visit(JSON.parse(JSON.stringify(entry)), null);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function containsPath(entry, target) {
|
|
236
|
+
const normalized = path.resolve(target).replace(/\\/g, '/').toLowerCase();
|
|
237
|
+
const text = JSON.stringify(entry).replace(/\\\\/g, '/').replace(/\\/g, '/').toLowerCase();
|
|
238
|
+
return text.includes(normalized);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function removePriorEntries(config, prior) {
|
|
242
|
+
const priorByEvent = new Map();
|
|
243
|
+
for (const record of prior.entries || []) {
|
|
244
|
+
if (!priorByEvent.has(record.event)) priorByEvent.set(record.event, new Map());
|
|
245
|
+
const counts = priorByEvent.get(record.event);
|
|
246
|
+
counts.set(record.hash, (counts.get(record.hash) || 0) + 1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const drift = [];
|
|
250
|
+
const removed = [];
|
|
251
|
+
const nextHooks = { ...config.hooks };
|
|
252
|
+
for (const [event, expectedCounts] of priorByEvent) {
|
|
253
|
+
const remaining = new Map(expectedCounts);
|
|
254
|
+
const entries = Array.isArray(nextHooks[event]) ? nextHooks[event] : [];
|
|
255
|
+
const kept = [];
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const hash = entryHash(entry);
|
|
258
|
+
const count = remaining.get(hash) || 0;
|
|
259
|
+
if (count > 0) {
|
|
260
|
+
removed.push({ event, hash });
|
|
261
|
+
remaining.set(hash, count - 1);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (prior.runtimeDir && containsPath(entry, prior.runtimeDir)) {
|
|
265
|
+
drift.push({
|
|
266
|
+
event,
|
|
267
|
+
hash,
|
|
268
|
+
reason: expectedCounts.has(hash)
|
|
269
|
+
? 'unexpected duplicate Harness compatibility hook'
|
|
270
|
+
: 'Harness compatibility hook was modified after install',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
kept.push(entry);
|
|
274
|
+
}
|
|
275
|
+
nextHooks[event] = kept;
|
|
276
|
+
}
|
|
277
|
+
return { config: { ...config, hooks: nextHooks }, drift, removed };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hookEntriesFrom(pluginRoot, runtimeDir) {
|
|
281
|
+
const sourceFile = path.join(pluginRoot, 'hooks', 'hooks.json');
|
|
282
|
+
const source = readHooksFile(sourceFile);
|
|
283
|
+
const records = [];
|
|
284
|
+
for (const [event, entries] of Object.entries(source.hooks)) {
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const transformed = transformEntry(entry, runtimeDir);
|
|
287
|
+
records.push({ event, hash: entryHash(transformed), entry: transformed });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return records;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sourceHookEntries(runtimeDir) {
|
|
294
|
+
return hookEntriesFrom(SOURCE_PLUGIN_ROOT, runtimeDir);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function ensureRuntime(runtimeDir, sourceHash) {
|
|
298
|
+
if (fs.existsSync(runtimeDir)) {
|
|
299
|
+
if (treeHash(runtimeDir) === sourceHash) return;
|
|
300
|
+
fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
fs.mkdirSync(path.dirname(runtimeDir), { recursive: true });
|
|
303
|
+
const temp = runtimeDir + '.tmp-' + process.pid;
|
|
304
|
+
try {
|
|
305
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
306
|
+
fs.cpSync(SOURCE_PLUGIN_ROOT, temp, { recursive: true });
|
|
307
|
+
if (treeHash(temp) !== sourceHash) throw new Error('copied compatibility runtime hash mismatch');
|
|
308
|
+
fs.renameSync(temp, runtimeDir);
|
|
309
|
+
} finally {
|
|
310
|
+
try { if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true, force: true }); } catch (_) {}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function install(options = {}) {
|
|
315
|
+
const codexHome = path.resolve(options.codexHome || defaultCodexHome(options.env));
|
|
316
|
+
const sourceHash = treeHash(SOURCE_PLUGIN_ROOT);
|
|
317
|
+
const p = pathsFor(codexHome, sourceHash);
|
|
318
|
+
assertCompatibilityPaths(p);
|
|
319
|
+
const existedBefore = fs.existsSync(p.hooksFile);
|
|
320
|
+
const hooksBefore = existedBefore ? fs.readFileSync(p.hooksFile) : null;
|
|
321
|
+
const runtimeExistedBefore = fs.existsSync(p.runtimeDir);
|
|
322
|
+
const config = readHooksFile(p.hooksFile);
|
|
323
|
+
const prior = readManifest(p.manifestFile, p);
|
|
324
|
+
|
|
325
|
+
let base = config;
|
|
326
|
+
if (prior) {
|
|
327
|
+
const removal = removePriorEntries(config, prior);
|
|
328
|
+
if (removal.drift.length) {
|
|
329
|
+
throw new Error('refusing to update: installed Harness compatibility hooks were modified; restore or uninstall them manually first');
|
|
330
|
+
}
|
|
331
|
+
base = removal.config;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ensureRuntime(p.runtimeDir, sourceHash);
|
|
335
|
+
assertCompatibilityPaths(p);
|
|
336
|
+
const generated = sourceHookEntries(p.runtimeDir);
|
|
337
|
+
const ownedEntries = [];
|
|
338
|
+
const next = { ...base, hooks: { ...base.hooks } };
|
|
339
|
+
for (const record of generated) {
|
|
340
|
+
const list = Array.isArray(next.hooks[record.event]) ? [...next.hooks[record.event]] : [];
|
|
341
|
+
if (!list.some(entry => entryHash(entry) === record.hash)) {
|
|
342
|
+
list.push(record.entry);
|
|
343
|
+
ownedEntries.push(record);
|
|
344
|
+
}
|
|
345
|
+
next.hooks[record.event] = list;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const manifest = {
|
|
349
|
+
schemaVersion: MANIFEST_SCHEMA,
|
|
350
|
+
package: 'harness-everything',
|
|
351
|
+
mode: MANIFEST_MODE,
|
|
352
|
+
packageVersion: PACKAGE_VERSION,
|
|
353
|
+
sourcePluginSha256: sourceHash,
|
|
354
|
+
runtimeDir: p.runtimeDir,
|
|
355
|
+
hooksFile: p.hooksFile,
|
|
356
|
+
createdHooksFile: prior ? Boolean(prior.createdHooksFile) : !existedBefore,
|
|
357
|
+
installedAt: new Date().toISOString(),
|
|
358
|
+
entries: ownedEntries.map(({ event, hash, entry }) => ({ event, hash, entry })),
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
assertCompatibilityPaths(p);
|
|
362
|
+
atomicWriteJson(p.hooksFile, next);
|
|
363
|
+
atomicWriteJson(p.manifestFile, manifest);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
try {
|
|
366
|
+
if (hooksBefore === null) fs.rmSync(p.hooksFile, { force: true });
|
|
367
|
+
else atomicWriteFile(p.hooksFile, hooksBefore);
|
|
368
|
+
} catch (rollbackError) {
|
|
369
|
+
error.message += '; hooks.json rollback also failed: ' + rollbackError.message;
|
|
370
|
+
}
|
|
371
|
+
if (!runtimeExistedBefore) {
|
|
372
|
+
try { fs.rmSync(p.runtimeDir, { recursive: true, force: true }); } catch (_) {}
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (prior && prior.runtimeDir && path.resolve(prior.runtimeDir) !== path.resolve(p.runtimeDir)) {
|
|
378
|
+
const ownedRoot = path.resolve(p.runtimeRoot) + path.sep;
|
|
379
|
+
const oldRuntime = path.resolve(prior.runtimeDir);
|
|
380
|
+
if (oldRuntime.startsWith(ownedRoot)) fs.rmSync(oldRuntime, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
status: prior ? 'updated' : 'installed',
|
|
385
|
+
codexHome,
|
|
386
|
+
hooksFile: p.hooksFile,
|
|
387
|
+
runtimeDir: p.runtimeDir,
|
|
388
|
+
entries: generated.length,
|
|
389
|
+
trustChanged: false,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function uninstall(options = {}) {
|
|
394
|
+
const codexHome = path.resolve(options.codexHome || defaultCodexHome(options.env));
|
|
395
|
+
const p = pathsFor(codexHome);
|
|
396
|
+
assertCompatibilityPaths(p);
|
|
397
|
+
const prior = readManifest(p.manifestFile, p);
|
|
398
|
+
if (!prior) return { status: 'not-installed', codexHome, removed: 0, trustChanged: false };
|
|
399
|
+
|
|
400
|
+
const hooksFileExists = fs.existsSync(p.hooksFile);
|
|
401
|
+
const config = readHooksFile(p.hooksFile);
|
|
402
|
+
const removal = removePriorEntries(config, prior);
|
|
403
|
+
if (removal.drift.length) {
|
|
404
|
+
throw new Error('refusing to uninstall modified Harness compatibility hooks; no user config was changed');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const remainingHooks = Object.fromEntries(
|
|
408
|
+
Object.entries(removal.config.hooks)
|
|
409
|
+
.map(([event, entries]) => [event, entries])
|
|
410
|
+
.filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
|
|
411
|
+
);
|
|
412
|
+
const next = { ...removal.config, hooks: remainingHooks };
|
|
413
|
+
const otherTopLevelKeys = Object.keys(next).filter(key => key !== 'hooks');
|
|
414
|
+
|
|
415
|
+
if (!hooksFileExists) {
|
|
416
|
+
// The user or host already removed the config. Uninstall must not recreate it.
|
|
417
|
+
} else if (prior.createdHooksFile && otherTopLevelKeys.length === 0 && Object.keys(remainingHooks).length === 0) {
|
|
418
|
+
fs.rmSync(p.hooksFile, { force: true });
|
|
419
|
+
} else {
|
|
420
|
+
atomicWriteJson(p.hooksFile, next);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (prior.runtimeDir) {
|
|
424
|
+
const ownedRoot = path.resolve(p.runtimeRoot) + path.sep;
|
|
425
|
+
const runtime = path.resolve(prior.runtimeDir);
|
|
426
|
+
if (runtime.startsWith(ownedRoot)) fs.rmSync(runtime, { recursive: true, force: true });
|
|
427
|
+
}
|
|
428
|
+
fs.rmSync(p.manifestFile, { force: true });
|
|
429
|
+
try { fs.rmdirSync(p.runtimeRoot); } catch (_) {}
|
|
430
|
+
try { fs.rmdirSync(p.ownerRoot); } catch (_) {}
|
|
431
|
+
try { fs.rmdirSync(path.dirname(p.ownerRoot)); } catch (_) {}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
status: 'uninstalled',
|
|
435
|
+
codexHome,
|
|
436
|
+
removed: removal.removed.length,
|
|
437
|
+
trustChanged: false,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function status(options = {}) {
|
|
442
|
+
const codexHome = path.resolve(options.codexHome || defaultCodexHome(options.env));
|
|
443
|
+
const p = pathsFor(codexHome);
|
|
444
|
+
assertCompatibilityPaths(p);
|
|
445
|
+
const prior = readManifest(p.manifestFile, p);
|
|
446
|
+
if (!prior) return { installed: false, codexHome, hooksFile: p.hooksFile, trustChanged: false };
|
|
447
|
+
const config = readHooksFile(p.hooksFile);
|
|
448
|
+
const removal = removePriorEntries(config, prior);
|
|
449
|
+
const expected = prior.entries.length;
|
|
450
|
+
return {
|
|
451
|
+
installed: true,
|
|
452
|
+
codexHome,
|
|
453
|
+
hooksFile: p.hooksFile,
|
|
454
|
+
runtimeDir: prior.runtimeDir,
|
|
455
|
+
packageVersion: prior.packageVersion,
|
|
456
|
+
sourcePluginSha256: prior.sourcePluginSha256,
|
|
457
|
+
expectedEntries: expected,
|
|
458
|
+
exactEntriesPresent: removal.removed.length,
|
|
459
|
+
drift: removal.drift,
|
|
460
|
+
healthy: removal.removed.length === expected && removal.drift.length === 0 &&
|
|
461
|
+
fs.existsSync(prior.runtimeDir) && treeHash(prior.runtimeDir) === prior.sourcePluginSha256,
|
|
462
|
+
trustChanged: false,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function parseArgs(argv) {
|
|
467
|
+
const action = argv[0] || 'status';
|
|
468
|
+
if (!['install', 'uninstall', 'status'].includes(action)) {
|
|
469
|
+
throw new Error('Usage: harness codex-hooks <install|uninstall|status> [--codex-home <path>]');
|
|
470
|
+
}
|
|
471
|
+
let codexHome;
|
|
472
|
+
for (let i = 1; i < argv.length; i++) {
|
|
473
|
+
if (argv[i] === '--codex-home') codexHome = path.resolve(argv[++i]);
|
|
474
|
+
else throw new Error('unknown argument: ' + argv[i]);
|
|
475
|
+
}
|
|
476
|
+
return { action, codexHome };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function main(argv = process.argv.slice(2)) {
|
|
480
|
+
let args;
|
|
481
|
+
try { args = parseArgs(argv); }
|
|
482
|
+
catch (error) {
|
|
483
|
+
console.error('[Codex Hooks Compatibility] ' + error.message);
|
|
484
|
+
return 2;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const result = args.action === 'install'
|
|
489
|
+
? install({ codexHome: args.codexHome })
|
|
490
|
+
: args.action === 'uninstall'
|
|
491
|
+
? uninstall({ codexHome: args.codexHome })
|
|
492
|
+
: status({ codexHome: args.codexHome });
|
|
493
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
494
|
+
if (args.action === 'status' && result.installed && !result.healthy) return 1;
|
|
495
|
+
return 0;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error('[Codex Hooks Compatibility] ' + error.message);
|
|
498
|
+
return 1;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (require.main === module) process.exit(main());
|
|
503
|
+
|
|
504
|
+
module.exports = {
|
|
505
|
+
MANIFEST_MODE,
|
|
506
|
+
MANIFEST_SCHEMA,
|
|
507
|
+
SOURCE_PLUGIN_ROOT,
|
|
508
|
+
defaultCodexHome,
|
|
509
|
+
entryHash,
|
|
510
|
+
install,
|
|
511
|
+
main,
|
|
512
|
+
pathsFor,
|
|
513
|
+
readHooksFile,
|
|
514
|
+
removePriorEntries,
|
|
515
|
+
sourceHookEntries,
|
|
516
|
+
stableJson,
|
|
517
|
+
status,
|
|
518
|
+
transformEntry,
|
|
519
|
+
treeHash,
|
|
520
|
+
uninstall,
|
|
521
|
+
};
|
|
@@ -7,35 +7,23 @@ const MARKER = "Harness OS Guidance (Advisory)";
|
|
|
7
7
|
|
|
8
8
|
const advisoryInstructions = [
|
|
9
9
|
`\n# ${MARKER}`,
|
|
10
|
-
`
|
|
11
|
-
`Treat the workflow contract below as required agent behavior, while keeping enforcement`,
|
|
12
|
-
`claims bounded by the host's verified capabilities.`,
|
|
10
|
+
`Harness is an observer/router/reminder layer on this surface; do not treat its workflow guidance as a hard scheduler.`,
|
|
13
11
|
``,
|
|
14
|
-
`## 🚦
|
|
15
|
-
`For
|
|
16
|
-
`1. Run \`npx github:dyphn1/Harness-everything next "<Brief summary of user's prompt>"
|
|
17
|
-
`2. For EVERY suggested skill, read its complete \`SKILL.md\` entry and
|
|
18
|
-
`3.
|
|
19
|
-
`4.
|
|
20
|
-
`5. Surface a compact checkpoint
|
|
21
|
-
` \`\`\`markdown`,
|
|
22
|
-
` ## 🚦 Harness OS Routing Checkpoint`,
|
|
23
|
-
` - **Active Tier**: Tier 1 | Tier 2 | Tier 3 | unclassified`,
|
|
24
|
-
` - **Strategy**: <selected/deferred strategy>`,
|
|
25
|
-
` - **Workflow State**: <active | deferred | blocked | escaped>`,
|
|
26
|
-
` - **Required Invariants**: <router invariants>`,
|
|
27
|
-
` - **Suggested Skills**: <deduplicated suggestions or none>`,
|
|
28
|
-
` - **Applicability**: <use | not-applicable + reason | unresolved/unavailable>`,
|
|
29
|
-
` - **Escape**: <none | reason + uncovered scope + evidence>`,
|
|
30
|
-
` \`\`\``,
|
|
12
|
+
`## 🚦 ENTRY TRIAGE & WORKFLOW GUIDANCE`,
|
|
13
|
+
`For software/project work, load the \`harness-everything\` skill and establish routing context before broad mutation:`,
|
|
14
|
+
`1. Run \`npx github:dyphn1/Harness-everything next "<Brief summary of user's prompt>"\` when terminal execution is available.`,
|
|
15
|
+
`2. For EVERY suggested skill, read its complete \`SKILL.md\` entry and evaluate applicability from \`USE FOR\`, \`DO NOT USE FOR\`, and its basic flow.`,
|
|
16
|
+
`3. Treat the selected workflow topology and numeric limits as planning guidance, not a lock. Choose, combine, reorder, or skip steps when evidence supports it.`,
|
|
17
|
+
`4. Prefer objective verification before declaring done and worktree isolation for broad/Tier-3 mutation.`,
|
|
18
|
+
`5. Surface a compact routing checkpoint with tier, strategy, suggested skills, and any relevant warnings.`,
|
|
31
19
|
``,
|
|
32
20
|
`## 🤖 COGNITIVE COMPLIANCE`,
|
|
33
|
-
`- **No universal skill pipeline**: do not force every task through TODO/TDD/Fable
|
|
34
|
-
`- **
|
|
35
|
-
`- **
|
|
36
|
-
`- **
|
|
21
|
+
`- **No universal skill pipeline**: do not force every task through TODO/TDD/Fable.`,
|
|
22
|
+
`- **Rule of 3**: after 3 same-signature failures, stop micro-retrying and use a fresh diagnosis / zoom-out. This reflection boundary may block mutation until completed.`,
|
|
23
|
+
`- **Permission boundaries are separate**: explicit host/user approval for destructive or external actions remains authoritative.`,
|
|
24
|
+
`- **Verification is evidence, not a cage**: missing evidence should trigger a reminder, not a persistent workflow lock.`,
|
|
37
25
|
`- **Environment discovery**: discover OS, shell, package manager/runtime, and host capability before relying on them.`,
|
|
38
|
-
`- **Memory / self-evolve**: persist only verified reusable lessons
|
|
26
|
+
`- **Memory / self-evolve**: persist only verified reusable lessons.`,
|
|
39
27
|
`- **Prefer editing**: prefer targeted edits and logically complete commits over broad rewrites.`,
|
|
40
28
|
``
|
|
41
29
|
].join('\n');
|
|
@@ -149,10 +137,9 @@ function buildCodexGlobalContent() {
|
|
|
149
137
|
`- Discover the environment and host capabilities before relying on them.`,
|
|
150
138
|
`- For software/project work, run \`npx github:dyphn1/Harness-everything next "<summary>"\` before mutation and surface the routing checkpoint.`,
|
|
151
139
|
`- Read every suggested skill's complete \`SKILL.md\` entry before resolving applicability; do not omit it from metadata or confidence alone.`,
|
|
152
|
-
`-
|
|
153
|
-
`- Do not impose a universal TODO/TDD/Fable sequence; use the smallest
|
|
154
|
-
`-
|
|
155
|
-
`- Before declaring done, run \`npx github:dyphn1/Harness-everything verify\` and fix failures.`,
|
|
140
|
+
`- Use the selected workflow topology as planning guidance; the model may adapt or skip steps with an explicit evidence-based reason.`,
|
|
141
|
+
`- Do not impose a universal TODO/TDD/Fable sequence; use the smallest useful topology plus applicable skills.`,
|
|
142
|
+
`- Prefer objective verification before declaring done; missing verification is a reminder, not a persistent lock.`,
|
|
156
143
|
`- After 3 same-signature failures, stop retrying and use a fresh diagnosis / zoom-out.`,
|
|
157
144
|
`- Prefer targeted edits and logically complete commits.`,
|
|
158
145
|
``
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: using-git-worktrees
|
|
3
|
-
description: Use when starting feature work needing isolation or before implementation plans -
|
|
3
|
+
description: Use when starting feature work needing isolation or before implementation plans - recommends an isolated workspace via native tools or git worktree fallback
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.16.1
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Using Git Worktrees
|
|
@@ -13,7 +13,7 @@ Use native worktree support first; raw Git is the fallback.
|
|
|
13
13
|
|
|
14
14
|
## Contract
|
|
15
15
|
|
|
16
|
-
- **Major workflow
|
|
16
|
+
- **Major workflow guidance** — Tier 3 / Fable-class engineering is safer in a linked worktree before broad source/artifact mutation. Reuse one or create one when practical.
|
|
17
17
|
- **Ordinary mode** — isolation-needing feature work may honor an explicit user preference to stay in place.
|
|
18
18
|
- Never nest a worktree inside an already isolated worktree.
|
|
19
19
|
|
|
@@ -28,11 +28,11 @@ git rev-parse --show-superproject-working-tree
|
|
|
28
28
|
```
|
|
29
29
|
Different git/common dirs and no superproject => already isolated; continue to Step 2. A submodule is a normal repo for this check.
|
|
30
30
|
|
|
31
|
-
**Step 1 — Create/enter**: prefer native `EnterWorktree`, `/worktree`, or `--worktree`. Otherwise use `git worktree add "$path" -b "$BRANCH_NAME"`.
|
|
31
|
+
**Step 1 — Create/enter**: prefer native `EnterWorktree`, `/worktree`, or `--worktree`. Otherwise use `git worktree add "$path" -b "$BRANCH_NAME"`. Prefer an already-ignored or external/sibling path. If creation/entry fails, report it and continue only with explicit awareness of the isolation risk.
|
|
32
32
|
|
|
33
33
|
**Step 2 — Setup**: install project dependencies as needed.
|
|
34
34
|
|
|
35
|
-
**Step 3 — Baseline**: run
|
|
35
|
+
**Step 3 — Baseline**: run relevant tests/build before implementation when useful; surface existing failures before relying on the baseline.
|
|
36
36
|
|
|
37
37
|
Deep dive: <this-skill-dir>/references/workflow-details.md
|
|
38
38
|
|