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.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +23 -0
  5. package/README.md +7 -7
  6. package/VERIFICATION.md +3 -3
  7. package/bin/cli.js +17 -0
  8. package/ci/invariant-routing.test.js +58 -158
  9. package/ci/mechanism-2a-rule-of-3.test.js +73 -138
  10. package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
  11. package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
  12. package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
  13. package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
  14. package/ci/mechanism-2al-codex-hooks-compat.test.js +286 -0
  15. package/ci/mechanism-2b-boundary-guard.test.js +6 -16
  16. package/ci/mechanism-2e-scope-guard.test.js +8 -24
  17. package/ci/mechanism-2f-stop-gate.test.js +21 -75
  18. package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
  19. package/ci/openai-plugin-package.test.js +8 -8
  20. package/ci/openai-plugin-runtime.test.js +2 -2
  21. package/ci/router-workflow-plan.test.js +6 -6
  22. package/ci/self-evolve-loop.test.js +4 -1
  23. package/ci/workflow-execution-contract.test.js +51 -129
  24. package/ci/workflow-plan-consumers-edge.test.js +1 -1
  25. package/ci/workflow-plan-consumers.test.js +6 -6
  26. package/docs/architecture.md +21 -21
  27. package/docs/enforcement-locks.md +41 -102
  28. package/docs/mechanism-first-skill-mesh.md +2 -2
  29. package/docs/philosophy.md +2 -2
  30. package/docs/platform-capabilities.md +19 -3
  31. package/docs/platform-compatibility.json +4 -4
  32. package/docs/routing.md +14 -14
  33. package/docs/workflow-runtime.md +80 -110
  34. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  35. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  36. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  37. package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
  38. package/fable-mode/SKILL.md +1 -1
  39. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  40. package/fable-mode/fable-haiku/SKILL.md +1 -1
  41. package/fable-mode/fable-opus/SKILL.md +1 -1
  42. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  43. package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  44. package/harness-everything/SKILL.md +11 -11
  45. package/harness-everything/references/triage-and-tiers.md +11 -11
  46. package/harness-everything/scripts/kernel-router-core.js +5 -5
  47. package/harness-everything/scripts/kernel-router.js +15 -17
  48. package/harness-everything/scripts/router-contract-core.js +1 -1
  49. package/hooks/hooks.json +10 -23
  50. package/hooks/scripts/atomic-commit-check.js +1 -1
  51. package/hooks/scripts/boundary-guard.js +4 -4
  52. package/hooks/scripts/contract-test.js +8 -11
  53. package/hooks/scripts/depth-guard.js +2 -2
  54. package/hooks/scripts/lib/workflow-runtime.js +24 -402
  55. package/hooks/scripts/rule-of-3-tracker.js +28 -31
  56. package/hooks/scripts/rule-of-3.js +4 -28
  57. package/hooks/scripts/state-persist.js +36 -136
  58. package/hooks/scripts/stop-gate.js +15 -48
  59. package/hooks/scripts/subagent-scope-guard.js +8 -8
  60. package/hooks/scripts/workflow-disposition.js +2 -10
  61. package/hooks/scripts/workflow-gate.js +33 -84
  62. package/hooks/scripts/workflow-mutation-denied.js +3 -15
  63. package/hooks/scripts/workflow-stop-gate.js +23 -38
  64. package/opencode-plugin/README.md +9 -10
  65. package/opencode-plugin/index.mjs +14 -30
  66. package/opencode-plugin/plugin.json +1 -1
  67. package/package.json +4 -2
  68. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  69. package/plugins/harness-everything/hooks/hooks.json +3 -3
  70. package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
  71. package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
  72. package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
  73. package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
  74. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
  75. package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
  76. package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
  77. package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
  78. package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
  79. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
  80. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
  81. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
  82. package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
  83. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
  84. package/plugins/harness-everything/plugin.json +1 -1
  85. package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
  86. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  87. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  88. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  89. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  90. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  91. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  92. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
  93. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
  94. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
  95. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +15 -17
  96. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
  97. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
  98. package/scripts/codex-hooks-compat.js +521 -0
  99. package/scripts/lib/advisory-text.js +16 -29
  100. 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
- `This surface cannot be assumed to mechanically enforce every Harness lifecycle transition.`,
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
- `## 🚦 MANDATORY ENTRY TRIAGE & WORKFLOW ROUTING`,
15
- `For every software/project prompt, load the \`harness-everything\` skill and establish the routing contract before mutation:`,
16
- `1. Run \`npx github:dyphn1/Harness-everything next "<Brief summary of user's prompt>"\`. If terminal execution is unavailable, apply the same contract from the skill instead of skipping routing.`,
17
- `2. For EVERY suggested skill, read its complete \`SKILL.md\` entry and resolve applicability from \`USE FOR\`, \`DO NOT USE FOR\`, workflow/basic flow, and hard rules. A name, description, router summary, or "routine/simple/already clear" judgement is not enough to omit it.`,
18
- `3. The selected workflow topology is mandatory once applicable. Execute it to resolution; choose tools, reasoning, implementation technique, and decomposition freely inside it. Do not replace it with a direct path because you believe you can solve the task without the workflow.`,
19
- `4. If the selected workflow genuinely cannot cover part of the task, record an explicit escape for the uncovered scope with evidence. Covered obligations remain mandatory. Host-capability loss must be surfaced, not silently converted into optional execution.`,
20
- `5. Surface a compact checkpoint near the first progress update:`,
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. The router selects the smallest sufficient topology; suggested skills are resolved for applicability.`,
34
- `- **Applicable skill flow**: reading a skill is not permission to discard it when its workflow actually applies.`,
35
- `- **Pre-completion verification**: before declaring done, run \`npx github:dyphn1/Harness-everything verify\` in the project root and fix failures. An instruction-only host may not block you mechanically, but the requirement remains part of the contract.`,
36
- `- **Rule of 3**: after 3 same-signature failures, stop micro-retrying and use a fresh diagnosis / zoom-out.`,
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; use escape/replan/recovery evidence to improve workflow coverage rather than bypassing the active workflow.`,
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
- `- Execute the selected workflow topology to resolution. The model controls HOW inside the topology, not WHETHER the selected topology exists.`,
153
- `- Do not impose a universal TODO/TDD/Fable sequence; use the smallest selected topology plus applicable skills.`,
154
- `- Escape only for genuinely uncovered workflow scope and record reason + scope + evidence.`,
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 - ensures an isolated workspace via native tools or git worktree fallback
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.8.1
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 mode** — Tier 3 / Fable-class engineering must be isolated before source/artifact mutation. Reuse an existing linked worktree or create one. If isolation cannot be established, stop `BLOCKED`; never fall back to the primary working tree.
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"`. For major mode, choose an already-ignored or external/sibling path so setup does not require modifying the primary tree first. Creation/entry failure => `BLOCKED`. Only ordinary mode may continue in place after an explicit decline/unsupported environment.
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 the relevant tests/build in the isolated tree before implementation; existing failures must be surfaced before mutation proceeds.
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