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
@@ -0,0 +1,166 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ const ROOT = path.resolve(__dirname, '..');
7
+ const registryFile = path.join(ROOT, 'hooks', 'scripts', 'lib', 'step-types.json');
8
+ const routerFile = path.join(ROOT, 'harness-everything', 'scripts', 'kernel-router.js');
9
+ const AUTHORITATIVE_DOC = 'harness-everything/references/router-workflow-plan.md';
10
+ const ALLOWED_LIST_DOCS = [
11
+ 'docs/routing.md',
12
+ 'harness-everything/references/router-workflow-plan.md',
13
+ 'harness-everything/references/triage-and-tiers.md',
14
+ 'docs/workflows/harness-everything.md',
15
+ ];
16
+ const START = '<!-- step-types:start -->';
17
+ const END = '<!-- step-types:end -->';
18
+ const FIX_PROMPT = 'Fix this checkout bug and add a regression test.';
19
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-step-type-docs-'));
20
+ let failed = 0;
21
+
22
+ process.on('exit', () => fs.rmSync(tempRoot, { recursive: true, force: true }));
23
+
24
+ console.log('\n[2ba] step-type registry documentation and router drift...');
25
+
26
+ function check(name, ok, detail) {
27
+ if (ok) console.log(`✅ ${name}`);
28
+ else {
29
+ failed++;
30
+ console.log(`❌ ${name}`);
31
+ if (detail) console.log(` ${detail}`);
32
+ }
33
+ }
34
+
35
+ function checkCase(name, run) {
36
+ try {
37
+ run();
38
+ check(name, true);
39
+ } catch (error) {
40
+ check(name, false, error && error.message ? error.message : String(error));
41
+ }
42
+ }
43
+
44
+ // Independent oracle: renders the registry directly from the JSON file, not through the runtime loader.
45
+ function renderRegistry(entries) {
46
+ const byId = (left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
47
+ const list = bindings => bindings.map(binding => `\`${binding}\``).join(', ') || 'none';
48
+ return entries.slice().sort(byId).map(entry =>
49
+ `- \`${entry.id}\` — required bindings: ${list(entry.requiredBindings)} — forbidden bindings: ${list(entry.forbiddenBindings)}`,
50
+ );
51
+ }
52
+
53
+ function markerBlocks(text, label) {
54
+ const lines = text.split('\n');
55
+ const blocks = [];
56
+ let openAt = null;
57
+ lines.forEach((line, index) => {
58
+ const marker = line.trim();
59
+ if (marker === START) {
60
+ if (openAt !== null) throw new Error(`${label}: nested step-types:start at line ${index + 1}`);
61
+ openAt = index;
62
+ } else if (marker === END) {
63
+ if (openAt === null) throw new Error(`${label}: step-types:end without start at line ${index + 1}`);
64
+ blocks.push(lines.slice(openAt + 1, index).map(item => item.trim()).filter(Boolean));
65
+ openAt = null;
66
+ }
67
+ });
68
+ if (openAt !== null) throw new Error(`${label}: step-types:start is never closed`);
69
+ return blocks;
70
+ }
71
+
72
+ function routeFixPrompt(runtimeRoot, workspace) {
73
+ const home = path.join(tempRoot, `home-${Math.random().toString(36).slice(2)}`);
74
+ fs.mkdirSync(home, { recursive: true });
75
+ const payload = { session_id: `2ba-${Math.random().toString(36).slice(2)}`, cwd: workspace, prompt: FIX_PROMPT };
76
+ return spawnSync(process.execPath, [path.join(runtimeRoot, 'harness-everything', 'scripts', 'kernel-router.js')], {
77
+ cwd: workspace,
78
+ encoding: 'utf8',
79
+ input: JSON.stringify(payload),
80
+ env: {
81
+ ...process.env,
82
+ HARNESS_STATE_HOME: path.join(tempRoot, 'state'),
83
+ HARNESS_WORKSPACE_ROOT: workspace,
84
+ HOME: home,
85
+ USERPROFILE: home,
86
+ },
87
+ });
88
+ }
89
+
90
+ const registry = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
91
+ const registryIds = registry.map(entry => entry.id).sort();
92
+ const expectedLines = renderRegistry(registry);
93
+
94
+ checkCase('2ba. every step-types marker block in the allowed-list docs equals the registry rendering', () => {
95
+ let total = 0;
96
+ for (const doc of ALLOWED_LIST_DOCS) {
97
+ const blocks = markerBlocks(fs.readFileSync(path.join(ROOT, doc), 'utf8'), doc);
98
+ for (const block of blocks) {
99
+ total++;
100
+ if (JSON.stringify(block) !== JSON.stringify(expectedLines)) {
101
+ throw new Error(`${doc} marker block drifted from the registry:\n${block.join('\n')}\nexpected:\n${expectedLines.join('\n')}`);
102
+ }
103
+ }
104
+ }
105
+ if (total === 0) throw new Error('no step-types marker block found in any allowed-list doc');
106
+ const authoritative = markerBlocks(fs.readFileSync(path.join(ROOT, AUTHORITATIVE_DOC), 'utf8'), AUTHORITATIVE_DOC);
107
+ if (authoritative.length !== 1) throw new Error(`${AUTHORITATIVE_DOC} must carry exactly one step-types marker block; found ${authoritative.length}`);
108
+ });
109
+
110
+ checkCase('2ba. router lists exactly the registry step-type ids', () => {
111
+ const routed = routeFixPrompt(ROOT, ROOT);
112
+ if (routed.status !== 0) throw new Error(`router exited ${routed.status}: ${routed.stderr}`);
113
+ const match = routed.stdout.match(/Record ordered step types \(([^)]*)\) and their bindings/);
114
+ if (!match) throw new Error('router did not print the ordered step-type list');
115
+ const listed = match[1].split(',').map(item => item.trim());
116
+ if (JSON.stringify(listed) !== JSON.stringify(registryIds)) {
117
+ throw new Error(`router listed [${listed.join(', ')}]; registry ids are [${registryIds.join(', ')}]`);
118
+ }
119
+ });
120
+
121
+ checkCase('2ba. router example plan does not steer step authoring toward behavior-change', () => {
122
+ const routed = routeFixPrompt(ROOT, ROOT);
123
+ const recordPlan = routed.stdout.split('\n').find(line => line.includes('Record plan:')) || '';
124
+ if (!recordPlan) throw new Error('router did not print a Record plan example');
125
+ if (recordPlan.includes('behavior-change')) throw new Error(`example still hard-codes behavior-change: ${recordPlan}`);
126
+ });
127
+
128
+ checkCase('2ba. router prints the explicit-choice guidance line exactly once in fresh tier2 output', () => {
129
+ const guidance = '- Replace <step-type-id> with a registered type you choose explicitly; mixed docs+code work uses separate doc-change and behavior-change steps; keep the verification step.';
130
+ const routed = routeFixPrompt(ROOT, ROOT);
131
+ if (routed.status !== 0) throw new Error(`router exited ${routed.status}: ${routed.stderr}`);
132
+ const matches = routed.stdout.split('\n').filter(line => line.trim() === guidance);
133
+ if (matches.length !== 1) throw new Error(`expected the guidance line exactly once; found ${matches.length}`);
134
+ });
135
+
136
+ checkCase('2ba. router record-plan guidance keeps the space after "node" in fresh tier2 output', () => {
137
+ const routed = routeFixPrompt(ROOT, ROOT);
138
+ if (routed.status !== 0) throw new Error(`router exited ${routed.status}: ${routed.stderr}`);
139
+ if (!routed.stdout.includes(' - Record plan: node "')) {
140
+ throw new Error(`router record-plan line lacks "Record plan: node"; got: ${routed.stdout.split('\n').find(line => line.includes('Record plan')) || '(none)'}`);
141
+ }
142
+ });
143
+
144
+ checkCase('2ba. router never crashes or prints a stale step-type list when the registry is missing', () => {
145
+ const copy = path.join(tempRoot, 'runtime-without-registry');
146
+ fs.cpSync(ROOT, copy, {
147
+ recursive: true,
148
+ filter: source => !/[\\/](node_modules|\.git|benchmarks|plugins)([\\/]|$)/.test(source),
149
+ });
150
+ fs.rmSync(path.join(copy, 'hooks', 'scripts', 'lib', 'step-types.json'));
151
+ const routed = routeFixPrompt(copy, copy);
152
+ if (routed.status !== 0) throw new Error(`router exited ${routed.status} without the registry: ${routed.stderr}`);
153
+ if (/Record ordered step types \(/.test(routed.stdout)) throw new Error('router printed a step-type list without a registry');
154
+ if (!routed.stderr.includes('cannot load step-type registry')) {
155
+ throw new Error(`router did not surface the missing registry visibly; stderr: ${routed.stderr}`);
156
+ }
157
+ });
158
+
159
+ checkCase('2ba. router-workflow-plan documents old-runtime rejection of persisted doc-change plans', () => {
160
+ const text = fs.readFileSync(path.join(ROOT, AUTHORITATIVE_DOC), 'utf8');
161
+ for (const required of ['doc-change', 'declared-only', 'planning:steps-invalid']) {
162
+ if (!text.includes(required)) throw new Error(`${AUTHORITATIVE_DOC} must mention ${required}`);
163
+ }
164
+ });
165
+
166
+ process.exitCode = failed ? 1 : 0;
@@ -7,7 +7,7 @@
7
7
  * keeps the test code highly decoupled, and simplifies debugging and adding new test suites.
8
8
  */
9
9
 
10
- const { spawnSync } = require('child_process');
10
+ const { spawnTestNode } = require('./lib/isolated-test-process');
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
 
@@ -35,7 +35,7 @@ files.forEach(file => {
35
35
  const filePath = path.join(evalDir, file);
36
36
  console.log(`\n▶️ Running test suite: ${file}`);
37
37
 
38
- const testRun = spawnSync('node', [filePath], {
38
+ const testRun = spawnTestNode([filePath], {
39
39
  stdio: 'inherit',
40
40
  cwd: projectRoot,
41
41
  env: { ...process.env, CLAUDE: '1' }
@@ -116,6 +116,10 @@ harness codex-hooks uninstall
116
116
 
117
117
  The installer copies the current packaged runtime into `$CODEX_HOME/harness-everything/compat-hooks/` and merges absolute-path entries into `$CODEX_HOME/hooks.json`. harness codex-hooks status reports `inactive` when the compatibility hooks are absent, or `configured-unverified` when their files are healthy; it cannot read Codex trust state. A successful Codex run of plugin-sync.js installs or refreshes this copy automatically. The installer does **not** modify `config.toml`, trust hashes, approval policy, or hook enablement. Codex must still show/review/trust the resulting non-managed hooks through its normal `/hooks` flow.
118
118
 
119
+ Updates retain at most two fingerprinted runtimes: the current runtime and the immediately previous distinct runtime. A third distinct install collects the oldest; sessions older than one update must restart. Reinstalling the same fingerprint preserves an intact previous runtime and drops a missing or modified one. `status` reports current and previous runtime file integrity separately and is unhealthy while a recorded previous runtime is missing or modified; this is filesystem health, not Codex hook trust or dispatch. Install, update, and uninstall hold an exclusive `compat-hooks.lock` directory for the whole manifest-read → runtime → hooks/manifest commit → collection transaction, so concurrent installers serialize instead of collecting each other's runtime. Each lock directory is created atomically together with its owner token. Acquisition and stale reclaim run under a short-lived `compat-hooks.lock.guard`, stale ownership is revalidated inside it, and a lock is removed only when it still carries the exact token judged stale; release likewise removes only the releasing holder's token. A lock or guard left by a dead local process is reclaimed, and a live lock times out (60 s) before any write. Uninstall removes all retained Harness-owned runtimes. Command paths remain fingerprinted: updates may require normal Codex hook review again; Harness does not bypass that security decision.
120
+
121
+ Installs of a plugin source tracked by a linked Git worktree refuse before writing. For an ordinary checkout, only modified or untracked files inside the plugin source subtree cause refusal. The runtime is built from the subtree's Git-tracked files, so `.gitignore`-excluded files (for example `.DS_Store` or `__pycache__`) are neither copied nor fingerprinted and do not block installation. The operator can explicitly pass `--allow-unreleased-source`. Packaged copies whose plugin manifest is not tracked by an enclosing repository remain supported, including copies inside an unrelated dirty repository. The mechanism and self-regression runners give child processes temporary HOME/USERPROFILE and CODEX_HOME directories. Tests must pass temporary Codex homes or mock the hook installer; mocking host commands alone does not isolate filesystem writes.
122
+
119
123
  Ownership is deliberately strict: update/uninstall removes only exact entries recorded in the Harness compatibility manifest. If an installed Harness entry was edited after installation, the operation fails closed rather than overwriting or deleting that user-modified entry. Unrelated pre-existing and later-added user hooks are preserved.
120
124
 
121
125
  The compatibility path registers hook commands in Codex user configuration and covers the missing plugin-mount route where user hooks dispatch. It does not repair a Codex carrier that fails to dispatch `PreToolUse` at all; those host-level execution-path limitations remain a live compatibility boundary.
@@ -114,7 +114,7 @@
114
114
  "globalScope": {"status": "Mechanism verified", "paths": {"skills": ["~/.agents/skills"]}},
115
115
  "installUninstallSymmetry": {"status": "Mechanism verified", "notes": "General Harness installer ownership is mechanism-tested. The optional codex-hooks compatibility fallback separately has exact-entry update/uninstall ownership tests and preserves unrelated user hooks. Live evidence confirms trusted dispatch for the two observation handlers on one Windows Codex CLI host; native plugin mounting and other scopes remain unverified."},
116
116
  "assetsReferences": {"status": "Mechanism verified", "notes": "Packaged skill references are checked and remain inside the package."},
117
- "updateSync": {"status": "Mechanism verified", "notes": "scripts/plugin-sync.js detects installed state and uses marketplace upgrade for an installed Codex plugin; after successful install/update verification it refreshes the local codex-hooks compatibility runtime and hooks.json entries. Unsupported CLI options are dropped and retried, and a Codex build without codex plugin list/add is reported as an actionable capability-boundary skip. Live marketplace source reporting and plugin-sync refresh remain unverified; the separate compatibility fallback dispatch is live-verified as recorded in the evidence artifact."},
117
+ "updateSync": {"status": "Mechanism verified", "notes": "scripts/plugin-sync.js detects installed state and uses marketplace upgrade for an installed Codex plugin; after successful install/update verification it refreshes the local codex-hooks compatibility runtime and hooks.json entries, retaining current plus previous distinct runtime. Tracked linked-worktree sources or dirty plugin subtrees require explicit unreleased-source opt-in; untracked packaged sources remain supported even inside unrelated dirty repositories. Unsupported CLI options are dropped and retried, and a Codex build without codex plugin list/add is reported as an actionable capability-boundary skip. Live marketplace source reporting and plugin-sync refresh remain unverified; the separate compatibility fallback dispatch is live-verified as recorded in the evidence artifact."},
118
118
  "liveHostVerification": {"status": "Partial", "evidence": "benchmarks/results/live-host/codex-2026-10-13/README.md", "notes": "The preserved artifact verifies two consecutive observation captures through the user-hook compatibility fallback on Codex CLI 0.162.1 for Windows, including current-turn labels. Native plugin mounting, marketplace source reporting, other lifecycle events, and other host/version combinations remain unverified."}
119
119
  }
120
120
  },
@@ -4,7 +4,7 @@ description: "Stage large tasks through plans, named fable agents, failable chec
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.31.3
8
8
  ---
9
9
 
10
10
  # Fable Mode
@@ -4,7 +4,7 @@ description: "Reference guardrails for verified warnings and safe replacements;
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.31.3
8
8
  ---
9
9
 
10
10
  # Execution Guardrails
@@ -4,7 +4,7 @@ description: "Reference mechanical behavior profile (legacy alias haiku) for bul
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.31.3
8
8
  ---
9
9
 
10
10
  # Fable Mode — Haiku
@@ -4,7 +4,7 @@ description: "Reference orchestrator behavior profile (legacy alias opus) for st
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.31.3
8
8
  ---
9
9
 
10
10
  # Fable Mode — Opus
@@ -4,7 +4,7 @@ description: "Reference reasoning behavior profile (legacy alias sonnet/sonnect)
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.31.3
8
8
  ---
9
9
 
10
10
  # Fable Mode — Sonnet
@@ -2,37 +2,105 @@
2
2
 
3
3
  ```mermaid
4
4
  flowchart TD
5
- Start[Qualifying Tier 3 task] --> Discover[Discover sources, runtime, and authorized scope]
6
- Discover --> Plan[Write stage map and pass conditions]
7
- Plan --> Select[Resolve requested model and write audit record]
8
- Select --> Delegate[Delegate to named agent or visible inline fallback]
9
- Delegate --> Contract[Write stage contract]
10
- Contract --> Verify[Run the named failable check]
11
- Verify --> Review[Cold-review high-stakes deliverables]
12
- Review --> Done[Record result or escalate blocker]
5
+ Start[Qualifying Tier 3 task] --> Discover[Discover runtime and authorized scope]
6
+ Discover --> Profile[Resolve behavior profile and runtime choice]
7
+ Profile --> Plan[Write dependency-aware stage map and checks]
8
+ Plan --> Register[Write workflow-stages.json in session state]
9
+ Register --> Execute[Start workflow-disposition and execute ready stages]
10
+ Execute --> Check[Run exact check and record correlated evidence]
11
+ Check --> Review[Cold-review when the selected plan requires it]
12
+ Review --> Done[Record result or blocker]
13
13
  ```
14
14
 
15
15
  ## Discovery and planning
16
16
 
17
- Read the relevant files and sources before producing an artifact. Lock the
18
- authorized write scope in the handoff. Number the stages, assign one artifact
19
- and one pass condition to each. Replan counts are advisory and never a hard
20
- stop; three same-signature failures still require Rule-of-3 `zoom-out`.
17
+ Read relevant files and sources before producing an artifact. Record the
18
+ authorized write scope. Give each stage an objective, dependencies, write set,
19
+ expected output, exact check command, and pass condition. Required and optional
20
+ bindings belong on the stage that needs them. Replan counts are advisory; after
21
+ three same-signature failures, use Rule-of-3 `zoom-out`.
21
22
 
22
- ## Delegation
23
+ ## Profiles and delegation
23
24
 
24
- Use `fable-orchestrator` for Opus macro work, `fable-worker-sonnet` for
25
- reasoning and synthesis, and `fable-worker-haiku` for mechanical work. Brief
26
- each worker with its exact output path, context, audit record, and check.
27
- Workers do not spawn workers. If the named agent is unavailable, choose the
28
- explicit inline fallback or stop and escalate; do not silently change models.
25
+ Resolve a requested behavior profile with Fable's profile selector and record
26
+ it separately from the runtime choice. `fable-orchestrator`,
27
+ `fable-worker-sonnet`, and `fable-worker-haiku` name coordination,
28
+ reasoning/synthesis, and mechanical-work roles. These are behavior-profile
29
+ aliases; they do not switch the runtime model. Preserve an explicit user or
30
+ host runtime choice. Workers do not spawn workers. If a named worker is
31
+ unavailable, use a declared inline fallback or report the gap.
29
32
 
30
- ## Verification and handoff
33
+ ## Active workflow
34
+
35
+ For an active mandatory workflow, use the router-displayed session path for
36
+ `workflow-stages.json` and the router-displayed `workflow-disposition.js`
37
+ controller. Do not register active stages in a repository-local contracts
38
+ directory. Every required stage needs `dependsOn`, `writeSet`, `checkCommand`,
39
+ and `passCondition`; an independent plan also needs a read-only `fable-verifier`
40
+ stage that depends on all implementation stages.
41
+
42
+ Start the selected run with the controller path and session id from the router:
43
+
44
+ ```bash
45
+ node "<workflow-disposition.js path from the router>" start --session-id "<session-id>"
46
+ ```
47
+
48
+ The controller creates correlated run-scoped contracts. Load only bindings on
49
+ dependency-ready stages. Record each disposition with evidence:
50
+
51
+ ```bash
52
+ node "<workflow-disposition.js path from the router>" stage-binding \
53
+ --session-id "<session-id>" --stage-id "<stage-id>" \
54
+ --binding-id "<binding-id>" --disposition loaded \
55
+ --evidence "Read the resolved required reference for this stage."
56
+ ```
57
+
58
+ Run the exact `checkCommand`; the post-tool `contract-test.js` records the
59
+ session, run, stage, worker identity, numeric exit code, and check evidence. If
60
+ bindings are unresolved, resolve them and rerun the exact check before passing
61
+ the stage. Keep results under the run's `contracts/<stageId>.json` and
62
+ `evidence/<stageId>.json` records. Stage checks, required bindings, and selected
63
+ independent verification remain semantic MUSTs. The controller and lifecycle
64
+ hooks are reminder-only: unresolved evidence stays visible without locking
65
+ ordinary tools or Stop. See [`../CONTRACT-FORMAT.md`](../CONTRACT-FORMAT.md) for
66
+ the schema and correlation rules.
67
+
68
+ ## Blocked recovery
31
69
 
32
- Before each spot-check, create `.claude/harness-everything/state/contracts/<stage>.json`
33
- using `CONTRACT-FORMAT.md`, then run the exact command in that contract. A
34
- stage is not complete until the command passes and the audit record contains
35
- the verifier result. Use `fable-verifier` with only the spec and artifact for
36
- high-stakes or unsupervised delivery. Review scope-guard output before merging.
70
+ If `start` is called while the workflow is running, it exits 2 with
71
+ `workflow already running; record a blocker before replanning`. From that
72
+ running state, record evidence with `block`:
73
+
74
+ ```bash
75
+ node "<workflow-disposition.js path from the router>" block \
76
+ --session-id "<session-id>" \
77
+ --evidence "The current stage map needs a bounded recovery update."
78
+ ```
79
+
80
+ Update the same session's `workflow-stages.json`, retaining the approved
81
+ requirements, workflow selection, dependencies, and required bindings. Then
82
+ start the revised stage map:
83
+
84
+ ```bash
85
+ node "<workflow-disposition.js path from the router>" start --session-id "<session-id>"
86
+ ```
87
+
88
+ This starts a new run and clears the recorded block reason. If the session is
89
+ already `blocked`, update its stage map and run `start` directly. Do not call
90
+ `plan` from `blocked`: planning is accepted only while the workflow is pending
91
+ or active. When an explicitly authorized scope change requires new requirement
92
+ composition, route a new session instead of replacing approved requirements.
93
+
94
+ ## Legacy/manual fallback
95
+
96
+ The path `.claude/harness-everything/state/contracts/<stage>.json` belongs only
97
+ to an explicitly selected legacy/manual workflow without an active mandatory
98
+ run. It does not register stages or correlated evidence for the current
99
+ workflow controller. Do not use it for an active Fable workflow.
100
+
101
+ ## Verification and handoff
37
102
 
38
- When macro scaffolding ends, transition to `tdd` for focused implementation.
103
+ Run each named check and review scope-guard evidence before delivery. When the
104
+ selected plan requires independent verification, the verifier is read-only and
105
+ uses a distinct worker identity after all other stages pass. Record a blocker
106
+ or an evidence-backed result; do not discard unresolved requirements.
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.0
7
+ version: 0.32.0
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -118,6 +118,24 @@ Examples:
118
118
 
119
119
  There is no universal `TODO → TDD → verification` pipeline.
120
120
 
121
+ ## Step types
122
+
123
+ The allowed step types are the entries of `hooks/scripts/lib/step-types.json`. The list below is rendered from that registry and checked against it by `ci/mechanism-2ba-step-type-docs.test.js`; examples elsewhere may name a subset.
124
+
125
+ <!-- step-types:start -->
126
+ - `behavior-change` — required bindings: `tdd` — forbidden bindings: none
127
+ - `commit` — required bindings: `git-commit` — forbidden bindings: none
128
+ - `doc-change` — required bindings: none — forbidden bindings: none
129
+ - `review` — required bindings: none — forbidden bindings: `tdd`
130
+ - `verification` — required bindings: `verification-loop` — forbidden bindings: none
131
+ <!-- step-types:end -->
132
+
133
+ `doc-change` is declared-only: a plan author chooses it explicitly, and router classification, keyword routing and System One never infer or insert it. It has no default bindings, and an explicitly declared `tdd` binding stays legal. Mixed documentation and code work is written as ordered `doc-change` and `behavior-change` steps, and one shared verification step may verify both.
134
+
135
+ Old runtimes reject a persisted non-Fable `doc-change` plan visibly at the controller and Stop boundaries (`planning:steps-invalid`). The old retained router does not show that rejection: it shows no active step. Persisted bytes are not renamed or reset. Inert Fable planning metadata is not rejected by old runtimes, because step bindings are disabled there.
136
+
137
+ Registry revisions: binding defaults and required/forbidden binding policy are materialized when a plan is recorded and are not reapplied on ordinary load. Existing typed plans must still reference currently registered step types; removing a type can invalidate a plan rather than migrate or waive its stored bindings. A policy fingerprint or migration path is future work and is not implemented today.
138
+
121
139
  ## Binding and disclosure rule
122
140
 
123
141
  Each ordered requirement or Fable stage may declare `requiredBindings` and `optionalBindings` by stable id and concrete repository-relative path. Only the active requirement or dependency-ready stage exposes its paths. Required bindings must resolve before that step/stage passes; unknown required bindings remain visible and unresolved. Future-step paths and keyword-matched documents are never preloaded.
@@ -33,6 +33,14 @@ function issueMemoryCapability(workflow) {
33
33
  return token;
34
34
  }
35
35
 
36
+ function needsStepTypeRegistry(plan) {
37
+ return Boolean(
38
+ plan && plan.strategySelection === 'selected' &&
39
+ ['tier2', 'tier3'].includes(plan.tier) && plan.strategy &&
40
+ !String(plan.strategy).startsWith('fable-')
41
+ );
42
+ }
43
+
36
44
  // Host-generated background-task notifications arrive as UserPromptSubmit
37
45
  // hook payloads but are not user input (#168). Routing them like prompts
38
46
  // replaces the workflow contract mid-turn (e.g. deferred -> a new Tier-2).
@@ -44,11 +52,28 @@ function isHostNotificationPrompt(prompt) {
44
52
  return /^<task-notification[\s>][\s\S]*<\/task-notification>\s*$/i.test(text);
45
53
  }
46
54
 
55
+ function stepTypeIdsFor(hooksRoot) {
56
+ try {
57
+ return require(path.join(hooksRoot, 'lib/workflow-obligations')).stepTypeIds();
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
47
63
  function persistWorkflow(contract, payload, prompt) {
48
64
  const plan = contract && contract.workflowPlan;
49
65
  if (!payload || !(payload.session_id || payload.sessionId)) return null;
50
66
  const { runtime, hooksRoot } = loadRuntime();
51
67
  const context = runtime.loadWorkflow(payload);
68
+ const hasRetainedWorkflow = Boolean(context.workflow && runtime.OPEN_STATES.has(context.workflow.state));
69
+ const activePlan = hasRetainedWorkflow ? context.workflow.workflowPlan : plan;
70
+ if (needsStepTypeRegistry(activePlan)) {
71
+ try { require(path.join(hooksRoot, 'lib/workflow-obligations')).loadStepTypeRegistry(); }
72
+ catch (error) {
73
+ if (hasRetainedWorkflow) return { ...context, hooksRoot, retained: true, registryError: error };
74
+ throw error;
75
+ }
76
+ }
52
77
  const { writeCurrentSession } = require(path.join(hooksRoot, 'lib/harness-state'));
53
78
  writeCurrentSession(context.root, context.sessionId);
54
79
  if (isHostNotificationPrompt(prompt)) {
@@ -121,8 +146,8 @@ function activeExecutionFor(persisted, plan) {
121
146
  const obligations = require(path.join(persisted.hooksRoot, 'lib/workflow-obligations'));
122
147
  const loaded = obligations.loadObligationSet(persisted, plan);
123
148
  return { activeStep: loaded.set ? obligations.activeStepSnapshot(loaded.set) : null };
124
- } catch (_) {
125
- return null;
149
+ } catch (error) {
150
+ return { error: error && error.message ? error.message : String(error) };
126
151
  }
127
152
  }
128
153
 
@@ -163,10 +188,15 @@ function run(raw) {
163
188
  if (persisted?.retained && persisted.workflow) {
164
189
  if (persisted.hostNotification) console.log('\n=> Host notification: routing unchanged; keeping the active execution contract.');
165
190
  else console.log('\n=> Retaining unresolved workflow; showing its compact active contract.');
166
- const activeExecution = activeExecutionFor(persisted, plan) || {};
191
+ const activeExecution = persisted.registryError
192
+ ? { error: persisted.registryError.message || String(persisted.registryError) }
193
+ : activeExecutionFor(persisted, plan) || {};
167
194
  const activeStep = activeExecution.activeStep || null;
168
195
  const activeStages = activeExecution.activeStages || [];
169
196
  core.printRetainedWorkflow(plan, persisted.workflow, activeStep, activeStages);
197
+ if (activeExecution.error) {
198
+ console.log(' - [Workflow Reminder] Active-step context unavailable: ' + activeExecution.error);
199
+ }
170
200
  if (activeStep) {
171
201
  const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
172
202
  console.log(' - Resolve only bindings for this active step, then pass this step with evidence.');
@@ -205,8 +235,10 @@ function run(raw) {
205
235
  if (plan.strategySelection === 'selected' && ['tier2', 'tier3'].includes(plan.tier)) {
206
236
  console.log(' - Planning contract: ' + path.join(persisted.sessionDir, 'workflow-obligations.json'));
207
237
  console.log(' - Requirements first: decompose intent into requirement fragments, then confirm the smallest sufficient workflow.');
208
- console.log(' - Record ordered step types (review, behavior-change, verification, commit) and their bindings before execution.');
209
- console.log(' - Record plan: node "' + controller + '" plan --session-id "' + persisted.sessionId + '" --requirements-json "[{\\"id\\":\\"req-1\\",\\"stepType\\":\\"behavior-change\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"},{\\"id\\":\\"req-verify\\",\\"stepType\\":\\"verification\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"}]" --strategy "' + plan.strategy + '" --evidence "<why this workflow fits>"');
238
+ const stepTypeIds = stepTypeIdsFor(persisted.hooksRoot);
239
+ console.log(' - Record ordered step types' + (stepTypeIds ? ' (' + stepTypeIds.join(', ') + ')' : '') + ' and their bindings before execution.');
240
+ console.log(' - Replace <step-type-id> with a registered type you choose explicitly; mixed docs+code work uses separate doc-change and behavior-change steps; keep the verification step.');
241
+ console.log(' - Record plan: node "' + controller + '" plan --session-id "' + persisted.sessionId + '" --requirements-json "[{\\"id\\":\\"req-1\\",\\"stepType\\":\\"<step-type-id>\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"},{\\"id\\":\\"req-verify\\",\\"stepType\\":\\"verification\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"}]" --strategy "' + plan.strategy + '" --evidence "<why this workflow fits>"');
210
242
  }
211
243
  console.log(' - Enter selected workflow after planning: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
212
244
  console.log(' - Escape one declared stage: node "' + controller + '" escape --session-id "' + persisted.sessionId + '" --stage-id "<id>" --reason-code workflow-uncovered-scope --scope "<uncovered scope>" --evidence "<evidence>"');
@@ -24,8 +24,11 @@ function finish() {
24
24
  clearTimeout(timer);
25
25
  if (!ROUTER) process.exit(0);
26
26
  const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
27
- if (result.stderr) process.stderr.write(result.stderr);
28
- const context = String(result.stdout || '').trim();
27
+ const stderr = String(result.stderr || '');
28
+ if (stderr) process.stderr.write(stderr);
29
+ const registryDiagnostics = stderr.split(/\r?\n/)
30
+ .filter(line => line.includes('cannot load step-type registry') && /(repair|restore|plugin:sync)/i.test(line));
31
+ const context = [String(result.stdout || '').trim(), ...registryDiagnostics].filter(Boolean).join('\n').trim();
29
32
  if (context) {
30
33
  process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } }));
31
34
  }
@@ -0,0 +1,37 @@
1
+ [
2
+ {
3
+ "id": "behavior-change",
4
+ "requiredBindings": ["tdd"],
5
+ "optionalBindings": [],
6
+ "forbiddenBindings": [],
7
+ "evidenceHint": "Evidence should show the requested behavior changed."
8
+ },
9
+ {
10
+ "id": "commit",
11
+ "requiredBindings": ["git-commit"],
12
+ "optionalBindings": [],
13
+ "forbiddenBindings": [],
14
+ "evidenceHint": "Evidence should identify the authorized commit."
15
+ },
16
+ {
17
+ "id": "doc-change",
18
+ "requiredBindings": [],
19
+ "optionalBindings": [],
20
+ "forbiddenBindings": [],
21
+ "evidenceHint": "Evidence should show the requested document update."
22
+ },
23
+ {
24
+ "id": "review",
25
+ "requiredBindings": [],
26
+ "optionalBindings": [],
27
+ "forbiddenBindings": ["tdd"],
28
+ "evidenceHint": "Evidence should identify the reviewed scope and findings."
29
+ },
30
+ {
31
+ "id": "verification",
32
+ "requiredBindings": ["verification-loop"],
33
+ "optionalBindings": [],
34
+ "forbiddenBindings": [],
35
+ "evidenceHint": "Evidence should record objective checks appropriate to the change."
36
+ }
37
+ ]