harness-everything 0.31.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +30 -0
  4. package/bin/cli.js +1 -0
  5. package/ci/lib/isolated-test-process.js +30 -0
  6. package/ci/mechanism-2al-codex-hooks-compat.test.js +14 -14
  7. package/ci/mechanism-2ao-plugin-sync.test.js +19 -2
  8. package/ci/mechanism-2at-fable-execution-phases.test.js +46 -0
  9. package/ci/mechanism-2au-codex-runtime-safety.test.js +340 -0
  10. package/ci/mechanism-2av-test-home-isolation.test.js +48 -0
  11. package/ci/mechanism-2az-step-type-registry.test.js +837 -0
  12. package/ci/mechanism-2ba-step-type-docs.test.js +166 -0
  13. package/ci/mechanism-test.js +2 -2
  14. package/docs/platform-capabilities.md +4 -0
  15. package/docs/platform-compatibility.json +1 -1
  16. package/fable-mode/SKILL.md +1 -1
  17. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  18. package/fable-mode/fable-haiku/SKILL.md +1 -1
  19. package/fable-mode/fable-opus/SKILL.md +1 -1
  20. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  21. package/fable-mode/references/execution-phases.md +93 -25
  22. package/harness-everything/SKILL.md +1 -1
  23. package/harness-everything/references/router-workflow-plan.md +18 -0
  24. package/harness-everything/scripts/kernel-router.js +37 -5
  25. package/hooks/scripts/codex-user-prompt.js +5 -2
  26. package/hooks/scripts/lib/step-types.json +37 -0
  27. package/hooks/scripts/lib/workflow-obligations.js +117 -14
  28. package/hooks/scripts/workflow-disposition.js +16 -9
  29. package/hooks/scripts/workflow-stop-gate.js +31 -8
  30. package/opencode-plugin/plugin.json +1 -1
  31. package/package.json +2 -2
  32. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  33. package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +5 -2
  34. package/plugins/harness-everything/hooks/scripts/lib/step-types.json +37 -0
  35. package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +117 -14
  36. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +16 -9
  37. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +31 -8
  38. package/plugins/harness-everything/plugin.json +1 -1
  39. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  40. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  41. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  42. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  43. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  44. package/plugins/harness-everything/skills/fable-mode/references/execution-phases.md +93 -25
  45. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  46. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +18 -0
  47. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +37 -5
  48. package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
  49. package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +8 -6
  50. package/sbom.cdx.json +7 -7
  51. package/scripts/codex-hooks-compat.js +265 -25
  52. package/self-evolve/SKILL.md +1 -1
  53. package/self-evolve/scripts/self-regression.js +8 -6
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.31.2",
13
+ "version": "0.32.0",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.31.2",
4
+ "version": "0.32.0",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.32.0](https://github.com/dyphn1/Harness-everything/compare/v0.31.3...v0.32.0) (2026-10-17)
6
+
7
+ ### Features
8
+
9
+ * **workflow:** add declarative step-type registry with doc-change ([#326](https://github.com/dyphn1/Harness-everything/issues/326)) ([aca941a](https://github.com/dyphn1/Harness-everything/commit/aca941ae704da48653d9f4d2654496e399e62fbc))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **workflow:** keep registry-independent commands working when the registry fails ([46900fd](https://github.com/dyphn1/Harness-everything/commit/46900fd4c8342163b1c5013a0d7e14dca920c560))
14
+ * **workflow:** restore record-plan spacing and state registry revision policy ([a5283cd](https://github.com/dyphn1/Harness-everything/commit/a5283cd203ea664b59659bdf7fd101ad27972ed9))
15
+
16
+ ## [0.31.3](https://github.com/dyphn1/Harness-everything/compare/v0.31.2...v0.31.3) (2026-10-16)
17
+
18
+ ### Bug Fixes
19
+
20
+ * **codex:** build runtime from tracked files and check previous runtime ([6bbb2c4](https://github.com/dyphn1/Harness-everything/commit/6bbb2c4c21c8bdecc69148e19126d0fe2b168328)), closes [#323](https://github.com/dyphn1/Harness-everything/issues/323)
21
+ * **codex:** guard stale-lock reclaim with token checks ([abf4026](https://github.com/dyphn1/Harness-everything/commit/abf40263766c33619f8a2c62dd3c16752774b654)), closes [#323](https://github.com/dyphn1/Harness-everything/issues/323)
22
+ * **codex:** retain compat runtimes and isolate test installs ([c5ee4e8](https://github.com/dyphn1/Harness-everything/commit/c5ee4e8c54dec758d77625913f1096aa281626a6))
23
+ * **codex:** scope source guards and isolate test runner homes ([c9cf30b](https://github.com/dyphn1/Harness-everything/commit/c9cf30b6bb1a336f4eff03037de0e08ab7bf49e7))
24
+ * **codex:** serialize compat installs on an owner lock ([b67e1c0](https://github.com/dyphn1/Harness-everything/commit/b67e1c0b95f16d0eae37485ab23ae4dfb1dc954a)), closes [#323](https://github.com/dyphn1/Harness-everything/issues/323) [#323](https://github.com/dyphn1/Harness-everything/issues/323)
25
+
5
26
  ## [0.31.2](https://github.com/dyphn1/Harness-everything/compare/v0.31.1...v0.31.2) (2026-10-15)
6
27
 
7
28
  ### Bug Fixes
@@ -29,6 +50,13 @@ All notable changes to this project are documented in this file.
29
50
 
30
51
  ## [Unreleased]
31
52
 
53
+ ### Fixed
54
+
55
+ - Codex source guards ignore unrelated repository dirtiness and untracked packaged copies; test runners isolate child HOME and CODEX_HOME directories (#323).
56
+ - Codex compatibility runtimes copy only Git-tracked plugin files, so ignored files cannot enter the runtime and do not block installs; `codex-hooks status` reports a missing or modified previous runtime as unhealthy, and the mechanism test fixture uses a portable Git config on Windows (#323).
57
+ - Codex compatibility install/update/uninstall serialize on an owner lock so concurrent installs cannot garbage-collect each other's current runtime; stale-lock recovery is guarded and token-checked so it cannot remove a live holder's lock (#323).
58
+ - Codex compatibility updates retain the current and previous runtime, reject unreleased checkout sources without explicit opt-in, and isolate plugin-sync tests from the real user hook installation (#323).
59
+
32
60
  ### Changes
33
61
 
34
62
  * **workflow:** replace topology-wide skill/reference pushes with ordered active-step bindings; carry the same contract through dependency-ready Fable stages while keeping hooks and Stop fail-open (#297).
@@ -36,6 +64,8 @@ All notable changes to this project are documented in this file.
36
64
  * **workflow:** resolve step/stage binding availability against real files (workspace references stay workspace-scoped; only recognized Harness bindings search installed plugin layouts); preserve knowledge signals across strategy recomposition; keep self-evolved manifest skills discoverable as metadata-only ids; positive skill evals assert skill-specific candidate bindings (#303 review).
37
65
  * **workflow:** reject present non-array required/optional binding declarations before accepting a requirement plan, preserving workflow state on invalid input (#303 review).
38
66
  * **test:** isolate verification fixtures from an inherited project-check recursion guard while preserving explicit guard coverage.
67
+ * **workflow:** document blocked Fable recovery by revising the stage map while retaining approved requirements (#316).
68
+ * **fable:** align execution phases with correlated workflow stages and scope the legacy/manual path (#317).
39
69
 
40
70
  ### Security
41
71
 
package/bin/cli.js CHANGED
@@ -111,6 +111,7 @@ Commands:
111
111
  codex-hooks Explicit Codex user-hook compatibility fallback for hosts
112
112
  that fail to mount enabled plugin-bundled hooks.
113
113
  Usage: harness codex-hooks <install|status|uninstall>
114
+ [--codex-home <path>] [--allow-unreleased-source]
114
115
  Does not trust hooks or modify Codex config.toml.
115
116
  plugin-sync Install or update the native Harness plugin on Claude Code
116
117
  and Codex. Existing installations always take the update
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ // Do not rely on the caller remembering HOME/CODEX_HOME: even a test that
9
+ // accidentally invokes a real installer must inherit a disposable user home.
10
+ function spawnTestNode(args, options = {}) {
11
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-test-home-'));
12
+ try {
13
+ return spawnSync(process.execPath, args, {
14
+ ...options,
15
+ env: {
16
+ ...process.env,
17
+ ...options.env,
18
+ HOME: home,
19
+ USERPROFILE: home,
20
+ CODEX_HOME: path.join(home, '.codex'),
21
+ APPDATA: path.join(home, 'AppData', 'Roaming'),
22
+ LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
23
+ },
24
+ });
25
+ } finally {
26
+ fs.rmSync(home, { recursive: true, force: true });
27
+ }
28
+ }
29
+
30
+ module.exports = { spawnTestNode };
@@ -64,7 +64,7 @@ fs.writeFileSync(configFile, 'model = "test"\n# user trust/config state\n', 'utf
64
64
  const configBefore = fs.readFileSync(configFile, 'utf8');
65
65
 
66
66
  try {
67
- const installed = compat.install({ codexHome });
67
+ const installed = compat.install({ allowUnreleasedSource: true, codexHome });
68
68
  check(installed.status === 'installed' && installed.entries > 0,
69
69
  'explicit install materializes compatibility entries');
70
70
  check(installed.trustChanged === false && fs.readFileSync(configFile, 'utf8') === configBefore,
@@ -95,7 +95,7 @@ try {
95
95
  'compatibility runtime is self-contained for hook and router dependencies');
96
96
 
97
97
  const countsBefore = Object.fromEntries(Object.entries(config.hooks).map(([event, entries]) => [event, entries.length]));
98
- const updated = compat.install({ codexHome });
98
+ const updated = compat.install({ allowUnreleasedSource: true, codexHome });
99
99
  const configAfterUpdate = read(hooksFile);
100
100
  const countsAfter = Object.fromEntries(Object.entries(configAfterUpdate.hooks).map(([event, entries]) => [event, entries.length]));
101
101
  check(updated.status === 'updated' && JSON.stringify(countsAfter) === JSON.stringify(countsBefore),
@@ -105,7 +105,7 @@ try {
105
105
  const addedUserHook = { hooks: [{ type: 'command', command: 'node "/user/later.js"', timeout: 3 }] };
106
106
  configAfterUpdate.hooks.PostToolUse.unshift(addedUserHook);
107
107
  fs.writeFileSync(hooksFile, JSON.stringify(configAfterUpdate, null, 2) + '\n', 'utf8');
108
- compat.install({ codexHome });
108
+ compat.install({ allowUnreleasedSource: true, codexHome });
109
109
  check(read(hooksFile).hooks.PostToolUse.some(entry => entry.hooks?.[0]?.command === 'node "/user/later.js"'),
110
110
  'update preserves user hooks added after the initial compatibility install');
111
111
 
@@ -125,7 +125,7 @@ try {
125
125
  fs.writeFileSync(hooksFile, JSON.stringify(tampered, null, 2) + '\n', 'utf8');
126
126
  const tamperedBytes = fs.readFileSync(hooksFile, 'utf8');
127
127
  let updateRefused = false;
128
- try { compat.install({ codexHome }); } catch (error) { updateRefused = /modified/.test(error.message); }
128
+ try { compat.install({ allowUnreleasedSource: true, codexHome }); } catch (error) { updateRefused = /modified/.test(error.message); }
129
129
  check(updateRefused && fs.readFileSync(hooksFile, 'utf8') === tamperedBytes,
130
130
  'modified Harness compatibility entry makes update fail closed without rewriting user config');
131
131
 
@@ -136,7 +136,7 @@ try {
136
136
 
137
137
  // Restore the exact generated config by starting from the pre-tamper bytes.
138
138
  fs.writeFileSync(hooksFile, beforeTamper, 'utf8');
139
- compat.install({ codexHome });
139
+ compat.install({ allowUnreleasedSource: true, codexHome });
140
140
  const removed = compat.uninstall({ codexHome });
141
141
  const finalConfig = read(hooksFile);
142
142
  check(removed.status === 'uninstalled' && removed.removed > 0 && removed.trustChanged === false,
@@ -153,7 +153,7 @@ try {
153
153
 
154
154
  const emptyHome = path.join(root, 'empty-codex');
155
155
  fs.mkdirSync(emptyHome, { recursive: true });
156
- compat.install({ codexHome: emptyHome });
156
+ compat.install({ allowUnreleasedSource: true, codexHome: emptyHome });
157
157
  check(fs.existsSync(path.join(emptyHome, 'hooks.json')),
158
158
  'compatibility install can create a new hooks.json when none exists');
159
159
  compat.uninstall({ codexHome: emptyHome });
@@ -164,7 +164,7 @@ try {
164
164
  const marketplaceHome = path.join(root, 'marketplace-codex');
165
165
  fs.cpSync(compat.SOURCE_PLUGIN_ROOT, marketplaceSource, { recursive: true });
166
166
  fs.writeFileSync(path.join(marketplaceSource, 'hooks', 'marketplace-source-marker.txt'), 'newer marketplace source\n');
167
- const marketplaceInstall = compat.install({ codexHome: marketplaceHome, sourcePluginRoot: marketplaceSource });
167
+ const marketplaceInstall = compat.install({ allowUnreleasedSource: true, codexHome: marketplaceHome, sourcePluginRoot: marketplaceSource });
168
168
  const marketplaceManifest = read(path.join(marketplaceHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
169
169
  check(marketplaceInstall.sourcePluginSha256 === compat.treeHash(marketplaceSource) &&
170
170
  marketplaceManifest.sourcePluginSha256 === compat.treeHash(marketplaceSource) &&
@@ -174,7 +174,7 @@ try {
174
174
 
175
175
  const duplicateHome = path.join(root, 'duplicate-codex');
176
176
  fs.mkdirSync(duplicateHome, { recursive: true });
177
- compat.install({ codexHome: duplicateHome });
177
+ compat.install({ allowUnreleasedSource: true, codexHome: duplicateHome });
178
178
  const duplicateConfig = read(path.join(duplicateHome, 'hooks.json'));
179
179
  duplicateConfig.hooks.PreToolUse.push(JSON.parse(JSON.stringify(duplicateConfig.hooks.PreToolUse[0])));
180
180
  fs.writeFileSync(path.join(duplicateHome, 'hooks.json'), JSON.stringify(duplicateConfig, null, 2) + '\n', 'utf8');
@@ -187,7 +187,7 @@ try {
187
187
  const removedConfigHome = path.join(root, 'removed-config-codex');
188
188
  fs.mkdirSync(removedConfigHome, { recursive: true });
189
189
  fs.writeFileSync(path.join(removedConfigHome, 'hooks.json'), JSON.stringify(original, null, 2) + '\n', 'utf8');
190
- compat.install({ codexHome: removedConfigHome });
190
+ compat.install({ allowUnreleasedSource: true, codexHome: removedConfigHome });
191
191
  fs.rmSync(path.join(removedConfigHome, 'hooks.json'), { force: true });
192
192
  const removedConfigResult = compat.uninstall({ codexHome: removedConfigHome });
193
193
  check(removedConfigResult.status === 'uninstalled' && !fs.existsSync(path.join(removedConfigHome, 'hooks.json')),
@@ -206,7 +206,7 @@ try {
206
206
  fs.writeFileSync(path.join(preexistingHome, 'hooks.json'),
207
207
  JSON.stringify({ hooks: preexistingHooks }, null, 2) + '\n', 'utf8');
208
208
  const preexistingBefore = fs.readFileSync(path.join(preexistingHome, 'hooks.json'), 'utf8');
209
- const preexistingInstall = compat.install({ codexHome: preexistingHome });
209
+ const preexistingInstall = compat.install({ allowUnreleasedSource: true, codexHome: preexistingHome });
210
210
  const preexistingManifest = read(path.join(preexistingHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
211
211
  check(preexistingInstall.status === 'installed' && preexistingManifest.entries.length === 0,
212
212
  'byte-identical pre-existing user hooks are reused but never recorded as Harness-owned');
@@ -219,7 +219,7 @@ try {
219
219
  fs.mkdirSync(forgedManifestHome, { recursive: true });
220
220
  fs.writeFileSync(path.join(forgedManifestHome, 'hooks.json'),
221
221
  JSON.stringify({ hooks: { PreToolUse: [userHook] } }, null, 2) + '\n', 'utf8');
222
- compat.install({ codexHome: forgedManifestHome });
222
+ compat.install({ allowUnreleasedSource: true, codexHome: forgedManifestHome });
223
223
  const forgedManifestPath = path.join(
224
224
  forgedManifestHome, 'harness-everything', 'compat-hooks', 'manifest.json');
225
225
  const forgedManifest = read(forgedManifestPath);
@@ -254,7 +254,7 @@ try {
254
254
  }
255
255
  return realRenameSync(source, destination);
256
256
  };
257
- try { compat.install({ codexHome: failedInstallHome }); }
257
+ try { compat.install({ allowUnreleasedSource: true, codexHome: failedInstallHome }); }
258
258
  catch (error) { failedInstallRefused = /injected manifest commit failure/.test(error.message); }
259
259
  finally { fs.renameSync = realRenameSync; }
260
260
  check(failedInstallRefused &&
@@ -272,7 +272,7 @@ try {
272
272
  process.platform === 'win32' ? 'junction' : 'dir'
273
273
  );
274
274
  let linkedOwnerRefused = false;
275
- try { compat.install({ codexHome: linkedOwnerHome }); }
275
+ try { compat.install({ allowUnreleasedSource: true, codexHome: linkedOwnerHome }); }
276
276
  catch (error) { linkedOwnerRefused = /linked|outside/.test(error.message); }
277
277
  check(linkedOwnerRefused && fs.readdirSync(linkedOwnerOutside).length === 0,
278
278
  'linked compatibility owner path cannot redirect writes outside Codex home');
@@ -283,7 +283,7 @@ try {
283
283
  fs.writeFileSync(malformed, '{ this is not json', 'utf8');
284
284
  const malformedBefore = fs.readFileSync(malformed, 'utf8');
285
285
  let malformedRefused = false;
286
- try { compat.install({ codexHome: malformedHome }); } catch (_) { malformedRefused = true; }
286
+ try { compat.install({ allowUnreleasedSource: true, codexHome: malformedHome }); } catch (_) { malformedRefused = true; }
287
287
  check(malformedRefused && fs.readFileSync(malformed, 'utf8') === malformedBefore,
288
288
  'malformed user hooks.json fails closed without replacement');
289
289
 
@@ -15,6 +15,17 @@ const {
15
15
 
16
16
  const ROOT = path.resolve(__dirname, '..');
17
17
  const codexHooksCompat = require('../scripts/codex-hooks-compat');
18
+ // A fake host runner does not mock the compatibility installer's filesystem
19
+ // writes. Fail closed if any case forgets to pass its explicit temporary home.
20
+ const realInstall = codexHooksCompat.install;
21
+ codexHooksCompat.install = options => {
22
+ assert(options && options.codexHome, 'tests must supply an explicit temporary Codex home');
23
+ const relative = path.relative(os.tmpdir(), path.resolve(options.codexHome));
24
+ assert(relative && !relative.startsWith('..') && !path.isAbsolute(relative),
25
+ 'test Codex home must be inside the temporary directory');
26
+ return realInstall(options);
27
+ };
28
+
18
29
 
19
30
  function fakeHost({ claudeInstalled = false, codexInstalled = false, codexPluginSourcePath = path.join(ROOT, 'plugins', 'harness-everything'), codexPluginSourceType = 'local', codexAvailableOnly = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false, unknownClaudeYesFlag = false, unknownClaudeJsonFlag = false, clapCodexJsonFlag = false, missingCodexPluginCommand = false, codexHooksError = null } = {}) {
20
31
  const calls = [];
@@ -283,7 +294,9 @@ for (const sourceType of ['git', 'npm', 'remote', 'unknown']) {
283
294
  {
284
295
  const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-codex-home-'));
285
296
  try {
286
- const fake = fakeHost({ codexInstalled: true });
297
+ const source = path.join(codexHome, 'plugin-cache');
298
+ fs.cpSync(codexHooksCompat.SOURCE_PLUGIN_ROOT, source, { recursive: true });
299
+ const fake = fakeHost({ codexInstalled: true, codexPluginSourcePath: source });
287
300
  const result = sync({ host: 'codex', codexHome }, {
288
301
  runner: fake.runner,
289
302
  commandAvailable: fake.commandAvailable,
@@ -368,18 +381,22 @@ for (const sourceType of ['git', 'npm', 'remote', 'unknown']) {
368
381
  // progress notice emitted before that work starts, the terminal stays
369
382
  // silent for the whole run, which is indistinguishable from a hang.
370
383
  const events = [];
384
+ const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-progress-codex-'));
371
385
  const fake = fakeHost({ claudeInstalled: true, codexInstalled: true });
372
386
  const runner = (command, args, opts) => {
373
387
  events.push(`call:${command}`);
374
388
  return fake.runner(command, args, opts);
375
389
  };
376
- const result = sync({ host: 'all', repository: 'dyphn1/Harness-everything', ref: 'main' }, {
390
+ const result = sync({ host: 'all', codexHome, repository: 'dyphn1/Harness-everything', ref: 'main' }, {
377
391
  runner,
392
+ codexHooksInstall: fake.codexHooksInstall,
378
393
  commandAvailable: () => true,
379
394
  log: () => {},
380
395
  warn: message => events.push(`warn:${message}`)
381
396
  });
382
397
  assert.strictEqual(result.failed, 0);
398
+ fs.rmSync(codexHome, { recursive: true, force: true });
399
+ assert.strictEqual(fake.codexHookInstalls.length, 1, 'progress test must mock hook installation as well as host commands');
383
400
  assert.ok(events.includes('warn:[...] claude: syncing'), 'a claude progress notice must be emitted');
384
401
  assert.ok(events.includes('warn:[...] codex: syncing'), 'a codex progress notice must be emitted');
385
402
  assert.ok(events.indexOf('warn:[...] claude: syncing') < events.indexOf('call:claude'), 'the claude notice must be emitted before its first host command, not buffered until the run finishes');
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const ROOT = path.resolve(__dirname, '..');
8
+ const guide = fs.readFileSync(path.join(ROOT, 'fable-mode/references/execution-phases.md'), 'utf8');
9
+ const packagedGuide = fs.readFileSync(path.join(ROOT, 'plugins/harness-everything/skills/fable-mode/references/execution-phases.md'), 'utf8');
10
+ const contract = fs.readFileSync(path.join(ROOT, 'fable-mode/CONTRACT-FORMAT.md'), 'utf8');
11
+ const activeStart = guide.indexOf('## Active workflow');
12
+ const recoveryStart = guide.indexOf('## Blocked recovery');
13
+ const legacyStart = guide.indexOf('## Legacy/manual fallback');
14
+ const active = guide.slice(activeStart, recoveryStart);
15
+ const legacyPath = '.claude/harness-everything/state/contracts/';
16
+
17
+ assert.strictEqual(packagedGuide, guide, 'OpenAI package includes the canonical execution phases reference');
18
+ assert.ok(activeStart >= 0 && recoveryStart > activeStart, 'active workflow and blocked recovery are explicit sections');
19
+ assert.ok(legacyStart > recoveryStart, 'legacy instructions are isolated after current recovery guidance');
20
+ assert.match(active, /workflow-stages\.json/);
21
+ assert.match(active, /workflow-disposition\.js.*start/s);
22
+ assert.match(active, /stage-binding.*--session-id.*--stage-id.*--binding-id.*--disposition.*--evidence/s);
23
+ assert.match(active, /contract-test\.js/);
24
+ assert.match(active, /worker identity/i);
25
+ assert.match(active, /exit code/i);
26
+ assert.match(active, /semantic MUSTs/i);
27
+ assert.match(active, /reminder-only/i);
28
+ assert.match(active, /without locking\s+ordinary tools or Stop/i);
29
+ assert.ok(!active.includes(legacyPath), 'active workflows never use the legacy contracts path');
30
+ assert.match(contract, /workflow-stages\.json/);
31
+ assert.match(contract, /stage results need observed worker identities and numeric exit codes/i);
32
+
33
+ const recovery = guide.slice(recoveryStart, legacyStart);
34
+ assert.match(recovery, /workflow already running; record a blocker before replanning/);
35
+ assert.match(recovery, /workflow-disposition\.js.*block/s);
36
+ assert.match(recovery, /workflow-stages\.json/);
37
+ assert.match(recovery, /workflow-disposition\.js.*start/s);
38
+ assert.match(recovery, /retaining|preserving/i);
39
+ assert.match(recovery, /plan.*blocked|blocked.*plan/s);
40
+
41
+ assert.match(guide, /behavior profile|role\/profile alias/i);
42
+ assert.match(guide, /explicit user or\s+host runtime choice/i);
43
+ assert.doesNotMatch(guide, /Resolve requested model|Opus macro work|do not silently change models/i);
44
+ assert.match(guide.slice(legacyStart), /`\.claude\/harness-everything\/state\/contracts\//);
45
+
46
+ console.log('PASS Fable execution phases match the active workflow contract and preserve a scoped legacy fallback');