harness-everything 0.8.0 → 0.8.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 (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +12 -2
  4. package/AGENTS.md +36 -35
  5. package/CHANGELOG.md +17 -0
  6. package/README.md +2 -2
  7. package/benchmarks/results/reviews/pr142-verification.json +95 -0
  8. package/ci/action-gate.test.js +1 -1
  9. package/ci/doc-capability-consistency.test.js +3 -0
  10. package/ci/invariant-routing.test.js +87 -74
  11. package/ci/mechanism-2af-worktree-isolation.test.js +152 -0
  12. package/ci/mechanism-2ag-router-worktree-contract.test.js +44 -0
  13. package/ci/mechanism-2ah-workflow-lifecycle.test.js +140 -0
  14. package/ci/openai-plugin-package.test.js +9 -6
  15. package/ci/workflow-execution-contract.test.js +60 -0
  16. package/docs/architecture.md +111 -114
  17. package/docs/mechanism-first-skill-mesh.md +92 -103
  18. package/docs/openai-plugin.md +1 -1
  19. package/docs/philosophy.md +40 -33
  20. package/docs/platform-capabilities.md +2 -2
  21. package/docs/reviews/pr-142-issue-131.md +89 -0
  22. package/docs/routing.md +61 -124
  23. package/docs/workflow-runtime.md +109 -0
  24. package/fable-mode/CONTRACT-FORMAT.md +3 -1
  25. package/fable-mode/SKILL.md +1 -1
  26. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  27. package/fable-mode/fable-haiku/SKILL.md +1 -1
  28. package/fable-mode/fable-opus/SKILL.md +1 -1
  29. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  30. package/fable-mode/scripts/workflow-plan-consumer.js +3 -1
  31. package/harness-everything/SKILL.md +11 -10
  32. package/harness-everything/references/triage-and-tiers.md +76 -107
  33. package/harness-everything/scripts/kernel-router-core.js +263 -0
  34. package/harness-everything/scripts/kernel-router.js +97 -225
  35. package/harness-everything/scripts/router-contract-core.js +612 -0
  36. package/harness-everything/scripts/router-contract.js +21 -581
  37. package/hooks/hooks.json +22 -1
  38. package/hooks/scripts/contract-test.js +24 -2
  39. package/hooks/scripts/lib/workflow-isolation.js +102 -0
  40. package/hooks/scripts/lib/workflow-runtime.js +102 -0
  41. package/hooks/scripts/state-persist.js +1 -0
  42. package/hooks/scripts/workflow-disposition.js +97 -0
  43. package/hooks/scripts/workflow-gate.js +53 -0
  44. package/hooks/scripts/workflow-stop-gate.js +44 -0
  45. package/opencode-plugin/plugin.json +1 -1
  46. package/package.json +2 -2
  47. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  48. package/plugins/harness-everything/hooks/hooks.json +20 -1
  49. package/plugins/harness-everything/hooks/scripts/contract-test.js +24 -2
  50. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +102 -0
  51. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +102 -0
  52. package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -0
  53. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +97 -0
  54. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +53 -0
  55. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +44 -0
  56. package/plugins/harness-everything/plugin.json +1 -1
  57. package/plugins/harness-everything/scripts/lib/advisory-text.js +51 -54
  58. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +3 -1
  59. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  60. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  61. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  62. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  63. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  64. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +3 -1
  65. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -10
  66. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +76 -107
  67. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +263 -0
  68. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +97 -225
  69. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +612 -0
  70. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +21 -581
  71. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +14 -16
  72. package/plugins/harness-everything/skills/using-git-worktrees/references/workflow-details.md +44 -85
  73. package/scripts/lib/advisory-text.js +51 -54
  74. package/using-git-worktrees/SKILL.md +14 -16
  75. package/using-git-worktrees/references/workflow-details.md +44 -85
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ function git(cwd, args) {
8
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, timeout: 5000 });
9
+ if (result.status !== 0) throw new Error('cannot verify Git worktree isolation');
10
+ return result.stdout.trim();
11
+ }
12
+
13
+ function canonical(value) {
14
+ const resolved = path.resolve(value);
15
+ // The JS resolver preserves Windows 8.3 spellings (RUNNER~1), while Git
16
+ // reports the long directory name. Native resolution gives one identity.
17
+ try { return fs.realpathSync.native(resolved); }
18
+ catch (error) {
19
+ if (error.code !== 'ENOENT') throw error;
20
+ const parent = path.dirname(resolved);
21
+ if (parent === resolved) return resolved;
22
+ return path.join(canonical(parent), path.basename(resolved));
23
+ }
24
+ }
25
+
26
+ function key(value) {
27
+ const result = canonical(value);
28
+ return process.platform === 'win32' ? result.toLowerCase() : result;
29
+ }
30
+
31
+ function within(root, target) {
32
+ const relative = path.relative(key(root), key(target));
33
+ return !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`);
34
+ }
35
+
36
+ function linkedWorktree(cwd, boundRoot) {
37
+ const common = canonical(git(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
38
+ const gitDir = canonical(git(cwd, ['rev-parse', '--absolute-git-dir']));
39
+ const boundCommon = canonical(git(boundRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
40
+ const top = canonical(git(cwd, ['rev-parse', '--show-toplevel']));
41
+ if (key(common) === key(gitDir) || key(common) !== key(boundCommon)) return null;
42
+ const worktrees = git(boundRoot, ['worktree', 'list', '--porcelain', '-z']).split('\0');
43
+ return worktrees.some(field => field.startsWith('worktree ') && key(field.slice(9)) === key(top)) ? top : null;
44
+ }
45
+
46
+ // A conservative read subset, not a general shell sandbox.
47
+ function classifyShell(command) {
48
+ const text = String(command || '').trim();
49
+ if (!text || /[;&|`\r\n<>$(){}\x00]/.test(text)) return 'mutation-or-unknown';
50
+ if (/^git\s+worktree\s+add\s+/i.test(text) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(text)) return 'worktree-setup';
51
+ if (/\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i.test(text)) return 'mutation-or-unknown';
52
+ if (/^git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)/i.test(text) ||
53
+ /^git\s+branch\s+--show-current$/i.test(text) || /^git\s+worktree\s+list(?:\s|$)/i.test(text) ||
54
+ /^(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$)/i.test(text)) return 'read-only';
55
+ return 'mutation-or-unknown';
56
+ }
57
+
58
+ function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
59
+ function cwdOf(payload, root) {
60
+ const input = inputOf(payload);
61
+ return input.workdir || input.cwd || input.working_directory || input.workingDirectory ||
62
+ payload.cwd || payload.working_directory || payload.workingDirectory || root;
63
+ }
64
+ function commandOf(payload) {
65
+ const input = inputOf(payload);
66
+ return String(input.command || input.script || input.cmd || '');
67
+ }
68
+ function mutationPaths(payload) {
69
+ const input = inputOf(payload);
70
+ if ((payload.tool_name || payload.tool) === 'apply_patch') {
71
+ const patch = typeof input === 'string' ? input : input.patch || input.input || '';
72
+ return [...String(patch).matchAll(/^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)\r?$/gm)].map(match => match[1].trim());
73
+ }
74
+ return [input.file_path || input.filePath || input.path].filter(value => typeof value === 'string' && value.trim());
75
+ }
76
+ function assertTargets(payload, cwd, isolatedRoot) {
77
+ const targets = mutationPaths(payload);
78
+ if (!targets.length) throw new Error('mutation target is unknown; cannot verify worktree isolation');
79
+ for (const target of targets) {
80
+ const resolved = path.resolve(cwd, target);
81
+ if (!within(isolatedRoot, resolved) || within(path.join(isolatedRoot, '.git'), resolved)) {
82
+ throw new Error('mutation target escapes Git worktree isolation or modifies Git metadata');
83
+ }
84
+ let parent = path.dirname(resolved);
85
+ while (!fs.existsSync(parent)) parent = path.dirname(parent);
86
+ if (key(git(parent, ['rev-parse', '--show-toplevel'])) !== key(isolatedRoot)) throw new Error('mutation target belongs to a different repository');
87
+ }
88
+ }
89
+ function assertShellScope(command, cwd, isolatedRoot) {
90
+ if (/(?:^|[;&|\r\n])\s*(?:cd|chdir|pushd|Set-Location|Push-Location)\b/i.test(command)) {
91
+ throw new Error('use the shell tool working-directory field so isolation can be verified');
92
+ }
93
+ const tokens = command.match(/"[^"]*"|'[^']*'|[^\s;&|<>]+/g) || [];
94
+ for (const token of tokens.slice(1)) {
95
+ const value = token.replace(/^["']|["']$/g, '').replace(/^[A-Za-z-]+=/, '');
96
+ if ((path.isAbsolute(value) || /^\.\.[\\/]/.test(value)) && !within(isolatedRoot, path.resolve(cwd, value))) {
97
+ throw new Error('explicit shell path escapes Git worktree isolation');
98
+ }
99
+ }
100
+ }
101
+
102
+ module.exports = { canonical, key, within, linkedWorktree, classifyShell, inputOf, cwdOf, commandOf, mutationPaths, assertTargets, assertShellScope };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getWorkspaceRoot, getSessionDir, getSessionId, getStateRoot } = require('./harness-state');
6
+ const { atomicWriteJson, readJson } = require('./fable-contracts');
7
+
8
+ const OPEN_STATES = new Set(['pending', 'active', 'running', 'failed', 'blocked', 'escaped']);
9
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10
+
11
+ function isMajorWorkflow(workflow) {
12
+ return Boolean(workflow && (workflow.mutationIsolation?.required || workflow.tier === 'tier3' || String(workflow.strategy || '').startsWith('fable-')));
13
+ }
14
+
15
+ function loadWorkflow(payload) {
16
+ const root = getWorkspaceRoot(payload);
17
+ const sessionId = getSessionId(payload);
18
+ const sessionDir = getSessionDir(root, sessionId, payload);
19
+ const file = path.join(sessionDir, 'workflow-run.json');
20
+ if (!fs.existsSync(file)) return { root, sessionId, sessionDir, file, workflow: null };
21
+ const workflow = readJson(file);
22
+ if (!workflow || !['deferred', 'satisfied', ...OPEN_STATES].includes(workflow.state)) {
23
+ throw new Error('recorded workflow is unreadable or invalid; repair its state before mutation/completion');
24
+ }
25
+ const strategies = ['direct-single', 'iterative-single', 'fable-staged', 'fable-parallel', 'fable-multi-agent-workspace'];
26
+ if (![1, 2].includes(workflow.schemaVersion) || workflow.sessionId !== sessionId ||
27
+ (workflow.state !== 'deferred' && !strategies.includes(workflow.strategy)) ||
28
+ (workflow.schemaVersion === 2 && (!workflow.workflowId || !workflow.workflowPlan ||
29
+ workflow.workflowPlan.strategy !== workflow.strategy || !Number.isInteger(workflow.revision) || workflow.revision < 0))) {
30
+ throw new Error('recorded workflow schema or session correlation is invalid');
31
+ }
32
+ return { root, sessionId, sessionDir, file, workflow };
33
+ }
34
+
35
+ function saveWorkflow(context) { atomicWriteJson(context.file, context.workflow); }
36
+
37
+ function matchingRun(context) {
38
+ const { workflow, root, sessionId } = context;
39
+ if (!workflow || !SAFE_ID.test(workflow.runId || '') || !workflow.workflowId || !sessionId) return null;
40
+ const runRoot = path.join(getStateRoot(root), 'fable-runs', workflow.runId);
41
+ const run = readJson(path.join(runRoot, 'run.json'));
42
+ if (!run || run.runId !== workflow.runId || run.workflowId !== workflow.workflowId ||
43
+ run.sessionId !== sessionId || run.strategy !== workflow.strategy ||
44
+ !Number.isFinite(Date.parse(run.createdAt)) || !Array.isArray(run.stageIds) || !run.stageIds.length ||
45
+ new Set(run.stageIds).size !== run.stageIds.length || run.stageIds.some(id => !SAFE_ID.test(id))) return null;
46
+ return { runRoot, run };
47
+ }
48
+
49
+ function unresolvedStages(match, workflow) {
50
+ const unresolved = [];
51
+ const stages = [];
52
+ for (const stageId of match.run.stageIds) {
53
+ const contract = readJson(path.join(match.runRoot, 'contracts', `${stageId}.json`));
54
+ if (!contract || contract.stageId !== stageId || contract.runId !== match.run.runId ||
55
+ contract.planId !== match.run.planId || contract.sessionId !== match.run.sessionId) {
56
+ unresolved.push(`${stageId}:missing-or-uncorrelated`);
57
+ continue;
58
+ }
59
+ stages.push(contract);
60
+ const escape = (workflow.escapes || []).find(item => item.stageId === stageId && item.runId === match.run.runId);
61
+ if (escape && escape.evidence && escape.uncoveredScope &&
62
+ ['workflow-uncovered-scope', 'host-capability-unavailable'].includes(escape.reasonCode)) continue;
63
+ const evidence = readJson(path.join(match.runRoot, 'evidence', `${stageId}.json`));
64
+ if (contract.status !== 'pass' || !contract.checkCommand || !evidence || evidence.status !== 'pass' ||
65
+ evidence.exitCode !== 0 || evidence.planId !== contract.planId || evidence.runId !== contract.runId ||
66
+ evidence.stageId !== stageId || evidence.sessionId !== contract.sessionId ||
67
+ evidence.checkCommand !== contract.checkCommand || !Number.isFinite(Date.parse(evidence.observedAt)) ||
68
+ evidence.workerId !== contract.workerId || !evidence.workerId || evidence.observedAt !== contract.verifiedAt) {
69
+ unresolved.push(`${stageId}:verification-unresolved`);
70
+ }
71
+ }
72
+ if (workflow.verification?.independent) {
73
+ const verifiers = stages.filter(stage => stage.agent === 'fable-verifier' && stage.writeSet?.length === 0);
74
+ const workers = stages.filter(stage => stage.agent !== 'fable-verifier').map(stage => stage.workerId).filter(Boolean);
75
+ const latestCheck = Math.max(0, ...stages.filter(stage => stage.agent !== 'fable-verifier').map(stage => Date.parse(stage.verifiedAt) || 0));
76
+ if (!verifiers.some(stage => stage.workerId && !workers.includes(stage.workerId) && stage.status === 'pass' &&
77
+ Date.parse(stage.verifiedAt) >= Math.max(workflow.lastMutationAt || 0, latestCheck) &&
78
+ match.run.stageIds.filter(id => id !== stage.stageId).every(id => stage.dependsOn?.includes(id)))) {
79
+ unresolved.push('independent-verifier-missing');
80
+ }
81
+ }
82
+ return unresolved;
83
+ }
84
+
85
+ function readHookInput(decide) {
86
+ let input = '';
87
+ let finished = false;
88
+ const finish = () => {
89
+ if (finished) return;
90
+ finished = true;
91
+ clearTimeout(timeout);
92
+ try { decide(input.trim() ? JSON.parse(input) : null); }
93
+ catch (error) { console.error(`[Workflow Gate] ${error.message}`); process.exitCode = 2; }
94
+ };
95
+ const timeout = setTimeout(finish, 500);
96
+ process.stdin.setEncoding('utf8');
97
+ process.stdin.on('data', chunk => { input += chunk; });
98
+ process.stdin.on('end', finish);
99
+ process.stdin.on('error', finish);
100
+ }
101
+
102
+ module.exports = { OPEN_STATES, SAFE_ID, isMajorWorkflow, loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput };
@@ -83,6 +83,7 @@ function processState(payload) {
83
83
  const command = (payload.tool_input && payload.tool_input.command) || '';
84
84
  if (VERIFY_COMMAND_RE.test(command)) {
85
85
  state.lastVerifyAt = Date.now();
86
+ state.lastVerifyExitCode = exitCode ?? null;
86
87
  }
87
88
  }
88
89
 
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES } = require('./lib/workflow-runtime');
7
+ const { getWorkspaceRoot, readCurrentSession } = require('./lib/harness-state');
8
+ const { atomicWriteJson, readJson } = require('./lib/fable-contracts');
9
+
10
+ const ALLOWED_ESCAPE_REASONS = new Set(['workflow-uncovered-scope', 'host-capability-unavailable']);
11
+
12
+ function parseArgs(argv) {
13
+ const args = { command: argv[0] };
14
+ const flags = new Map([['--reason-code', 'reasonCode'], ['--scope', 'scope'], ['--evidence', 'evidence'], ['--session-id', 'sessionId'], ['--stage-id', 'stageId']]);
15
+ for (let i = 1; i < argv.length; i++) {
16
+ const key = flags.get(argv[i]);
17
+ if (!key || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error('unknown or incomplete argument: ' + argv[i]);
18
+ args[key] = argv[++i];
19
+ }
20
+ return args;
21
+ }
22
+
23
+ function consumer() {
24
+ for (const relative of ['../../fable-mode/scripts/workflow-plan-consumer.js', '../../skills/fable-mode/scripts/workflow-plan-consumer.js']) {
25
+ const file = path.resolve(__dirname, relative);
26
+ if (fs.existsSync(file)) return require(file);
27
+ }
28
+ throw new Error('Fable consumer unavailable');
29
+ }
30
+
31
+ function main() {
32
+ const args = parseArgs(process.argv.slice(2));
33
+ const sessionId = args.sessionId || readCurrentSession(getWorkspaceRoot());
34
+ if (!sessionId) throw new Error('no active Harness session; pass --session-id explicitly');
35
+ const context = loadWorkflow({ session_id: sessionId });
36
+ const { workflow } = context;
37
+ if (!workflow || !OPEN_STATES.has(workflow.state)) throw new Error('no unresolved workflow contract exists for this session');
38
+ if (args.command === 'start') {
39
+ if (!workflow.workflowId || !workflow.workflowPlan) throw new Error('legacy workflow lacks a correlated plan; cannot start it implicitly');
40
+ if (workflow.state === 'running') throw new Error('workflow already running; record a blocker before replanning');
41
+ const max = workflow.workflowPlan.limits?.maxRevisionRounds ?? 2;
42
+ if (workflow.revision > max) throw new Error('replan budget exhausted; workflow remains blocked');
43
+ const plan = workflow.pendingPlan || workflow.workflowPlan;
44
+ if (!String(plan.strategy || '').startsWith('fable-')) {
45
+ workflow.revision++;
46
+ workflow.state = 'running';
47
+ delete workflow.blockReason;
48
+ saveWorkflow(context);
49
+ process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, revision: workflow.revision }) + '\n');
50
+ return;
51
+ }
52
+ const stages = readJson(path.join(context.sessionDir, 'workflow-stages.json'));
53
+ if (!Array.isArray(stages) || !stages.length) throw new Error('write a non-empty workflow-stages.json stage array at the session path first');
54
+ if (stages.some(stage => !stage.checkCommand || !stage.passCondition)) throw new Error('every required stage needs an objective checkCommand and passCondition');
55
+ if (plan.verification?.independent && !stages.some(stage => stage.agent === 'fable-verifier' && stage.writeSet?.length === 0 &&
56
+ stages.filter(other => other !== stage).every(other => stage.dependsOn?.includes(other.stageId)))) {
57
+ throw new Error('independent verification requires a read-only fable-verifier stage depending on all other stages');
58
+ }
59
+ const run = consumer().prepareRun({
60
+ routerContract: { workflowPlan: plan }, stages, workspaceRoot: context.root,
61
+ sessionId, workflowId: workflow.workflowId,
62
+ });
63
+ workflow.workflowPlan = plan;
64
+ workflow.strategy = plan.strategy;
65
+ workflow.tier = plan.tier;
66
+ workflow.verification = plan.verification;
67
+ workflow.mutationIsolation = plan.mutationIsolation || workflow.mutationIsolation;
68
+ workflow.runId = run.runId;
69
+ workflow.revision++;
70
+ workflow.state = 'running';
71
+ workflow.escapes = [];
72
+ delete workflow.pendingPlan;
73
+ delete workflow.blockReason;
74
+ } else if (args.command === 'escape') {
75
+ if (!ALLOWED_ESCAPE_REASONS.has(args.reasonCode)) throw new Error('generic simple/routine/already-clear reasons are intentionally rejected');
76
+ if (!args.scope?.trim()) throw new Error('--scope is required');
77
+ if (!args.evidence?.trim()) throw new Error('--evidence is required');
78
+ const match = matchingRun(context);
79
+ if (!match || !match.run.stageIds.includes(args.stageId)) throw new Error('--stage-id must name a stage in the correlated active run');
80
+ const contract = readJson(path.join(match.runRoot, 'contracts', args.stageId + '.json'));
81
+ if (contract?.agent === 'fable-verifier') throw new Error('independent verification cannot be escaped; record BLOCKED when unavailable');
82
+ const disposition = {
83
+ status: 'escaped', stageId: args.stageId, runId: match.run.runId,
84
+ reasonCode: args.reasonCode, uncoveredScope: args.scope.trim(), evidence: args.evidence.trim(), recordedAt: new Date().toISOString(),
85
+ };
86
+ workflow.escapes = [...(workflow.escapes || []).filter(item => item.stageId !== args.stageId), disposition];
87
+ atomicWriteJson(path.join(match.runRoot, 'escapes', args.stageId + '.json'), disposition);
88
+ // Scope-limited exception; never change the entire workflow to escaped.
89
+ } else if (args.command === 'block') {
90
+ if (!args.evidence?.trim()) throw new Error('--evidence is required');
91
+ workflow.state = 'blocked';
92
+ workflow.blockReason = args.evidence.trim();
93
+ } else throw new Error('Usage: workflow-disposition.js <start|escape|block> --session-id <id> [--stage-id <id> --reason-code <reason> --scope <scope> --evidence <evidence>]');
94
+ saveWorkflow(context);
95
+ process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
96
+ }
97
+ try { main(); } catch (error) { console.error('[Workflow Disposition] ' + error.message); process.exitCode = 2; }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, readHookInput } = require('./lib/workflow-runtime');
7
+ const { key, classifyShell, cwdOf, commandOf, mutationPaths, linkedWorktree, assertTargets, assertShellScope } = require('./lib/workflow-isolation');
8
+
9
+ const DIRECT = new Set(['Edit', 'Write', 'apply_patch']);
10
+ const SHELL = new Set(['Bash', 'PowerShell', 'exec_command']);
11
+
12
+ function isController(command, cwd) {
13
+ // One trusted local executable, literal arguments, no shell evaluation.
14
+ if (/[;&|`\r\n<>$(){}]/.test(command)) return false;
15
+ const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+(start|escape|block)\b/);
16
+ return Boolean(match && key(path.resolve(cwd, match[1] || match[2] || match[3])) === key(path.join(__dirname, 'workflow-disposition.js')));
17
+ }
18
+
19
+ function decide(payload) {
20
+ if (!payload) return;
21
+ const tool = payload.tool_name || payload.tool;
22
+ if (!DIRECT.has(tool) && !SHELL.has(tool)) return;
23
+ const context = loadWorkflow(payload);
24
+ const { workflow, root, sessionDir } = context;
25
+ if (!workflow || workflow.state === 'deferred') return;
26
+ const cwd = cwdOf(payload, root);
27
+ const command = commandOf(payload);
28
+ const commandClass = SHELL.has(tool) ? classifyShell(command) : 'direct-mutation';
29
+ if (commandClass === 'read-only' || commandClass === 'worktree-setup') return;
30
+ if (SHELL.has(tool) && isController(command, cwd)) return;
31
+ // Only the stage specification is allowed before run entry. Other runtime
32
+ // state, including workflow-run.json, cannot use this bootstrap exception.
33
+ const targets = DIRECT.has(tool) ? mutationPaths(payload) : [];
34
+ const spec = path.join(sessionDir, 'workflow-stages.json');
35
+ const specStat = fs.existsSync(spec) ? fs.lstatSync(spec) : null;
36
+ if ((!specStat || (specStat.isFile() && !specStat.isSymbolicLink() && specStat.nlink === 1)) &&
37
+ targets.length && targets.every(target => key(path.resolve(cwd, target)) === key(spec))) return;
38
+ if (['blocked', 'failed'].includes(workflow.state)) throw new Error('workflow is BLOCKED; replan through the controller before mutation');
39
+ if (isMajorWorkflow(workflow)) {
40
+ const isolatedRoot = linkedWorktree(cwd, root);
41
+ if (!isolatedRoot) throw new Error('Git worktree isolation is required before mutation. Enter a linked worktree; unavailable isolation means BLOCKED, never in-place fallback.');
42
+ if (DIRECT.has(tool)) assertTargets(payload, cwd, isolatedRoot);
43
+ else assertShellScope(command, cwd, isolatedRoot);
44
+ }
45
+ if (workflow.state === 'satisfied') throw new Error('workflow is already satisfied; submit a new task before further mutation');
46
+ if (String(workflow.strategy || '').startsWith('fable-') && !matchingRun(context)) {
47
+ throw new Error('selected Fable workflow has no correlated run. Write workflow-stages.json at the displayed session path, then use workflow-disposition.js start.');
48
+ }
49
+ workflow.lastMutationAt = Date.now();
50
+ saveWorkflow(context);
51
+ }
52
+
53
+ readHookInput(decide);
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { readJson } = require('./lib/fable-contracts');
6
+ const { loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput } = require('./lib/workflow-runtime');
7
+
8
+ function decide(payload) {
9
+ if (!payload) return;
10
+ const context = loadWorkflow(payload);
11
+ const { workflow, sessionDir } = context;
12
+ if (!workflow || ['deferred', 'satisfied'].includes(workflow.state)) return;
13
+ if (workflow.state === 'blocked') {
14
+ console.error('[Workflow Completion Gate] BLOCKED: report the unresolved work; completion has not been verified.');
15
+ return;
16
+ }
17
+ let unresolved = [];
18
+ if (String(workflow.strategy || '').startsWith('fable-')) {
19
+ const match = matchingRun(context);
20
+ unresolved = match ? unresolvedStages(match, workflow) : ['correlated-run-missing'];
21
+ } else {
22
+ const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
23
+ const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
24
+ if (lastEdit && ((handoff?.lastVerifyAt || 0) < lastEdit || handoff?.lastVerifyExitCode !== 0)) unresolved = ['verification-after-edit-missing'];
25
+ }
26
+ if (unresolved.length) {
27
+ workflow.unresolved = unresolved;
28
+ if (payload.stop_hook_active) {
29
+ // Do not loop forever or pretend the retry satisfied the contract.
30
+ workflow.state = 'blocked';
31
+ workflow.blockReason = 'unresolved-stop-retry';
32
+ saveWorkflow(context);
33
+ console.error('[Workflow Completion Gate] BLOCKED: ' + unresolved.join(', ') + '. Report the blocker; this is not completion.');
34
+ return;
35
+ }
36
+ saveWorkflow(context);
37
+ throw new Error('Cannot complete: unresolved workflow obligations: ' + unresolved.join(', '));
38
+ }
39
+ workflow.state = 'satisfied';
40
+ workflow.resolvedAt = new Date().toISOString();
41
+ workflow.unresolved = [];
42
+ saveWorkflow(context);
43
+ }
44
+ readHookInput(decide);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
@@ -1,6 +1,5 @@
1
- // Prompt-injection-only guidance for platforms with no hook/execution
2
- // mechanism (Cursor, Copilot, Codex, Continue, Hermes). Single source of
3
- // truth for the advisory block.
1
+ // Prompt-injection guidance for integration paths where Harness cannot prove
2
+ // lifecycle-hook enforcement. Single source of truth for advisory blocks.
4
3
  const fs = require('fs');
5
4
  const path = require('path');
6
5
 
@@ -8,34 +7,36 @@ const MARKER = "Harness OS Guidance (Advisory)";
8
7
 
9
8
  const advisoryInstructions = [
10
9
  `\n# ${MARKER}`,
11
- `This file is advisory only - this platform has no hook/execution mechanism to`,
12
- `enforce it mechanically (unlike Claude Code's hook-based circuit breaker). Treat`,
13
- `these as strong defaults, not guarantees.`,
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.`,
14
13
  ``,
15
- `## 🚦 MANDATORY ENTRY TRIAGE & ROUTING (ALWAYS RUN FIRST)`,
16
- `Every time you receive a new software/project prompt, you MUST load the \`harness-everything\` skill and immediately do the following:`,
17
- `1. Run \`npx github:dyphn1/Harness-everything next "<Brief summary of user's prompt>"\` (this prints the same routing recommendation kernel-router.js produces - it works from any directory and does not depend on where a local copy of the skill was installed). If the terminal is not available yet, apply the same routing contract from the loaded \`harness-everything\` skill instead of skipping this step.`,
18
- `2. For EVERY suggested skill, resolve and read its complete \`SKILL.md\` entry before deciding to skip it. Evaluate \`USE FOR\`, \`DO NOT USE FOR\`, workflow/basic flow, and hard rules against the task. If the entry explicitly requires another document to decide applicability, read that required material too. Do not reject a suggestion from only its name, description, router summary, or because the task seems routine/common.`,
19
- `3. After that evaluation, execution is advisory: use, combine, reorder, or skip skills as justified. Using one suggestion does not waive read-before-skip for the others. If a suggested skill cannot be resolved/read, mark it unresolved/unavailable instead of silently skipping it.`,
20
- `4. Output a clear routing checkpoint near the beginning of your response/update:`,
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
21
  ` \`\`\`markdown`,
22
22
  ` ## 🚦 Harness OS Routing Checkpoint`,
23
- ` - **Active Tier**: Tier 1 (Trivial) | Tier 2 (Standard) | Tier 3 (Macro)`,
24
- ` - **Rationale**: <1-sentence rationale from the router output>`,
23
+ ` - **Active Tier**: Tier 1 | Tier 2 | Tier 3 | unclassified`,
24
+ ` - **Strategy**: <selected/deferred strategy>`,
25
+ ` - **Workflow State**: <active | deferred | blocked | escaped>`,
25
26
  ` - **Required Invariants**: <router invariants>`,
26
- ` - **Suggested Skills**: <deduplicated router suggestions or none>`,
27
- ` - **Suggestion Evaluation**: <evaluated before skip | pending | unresolved/unavailable>`,
28
- ` - **Disposition**: <skills used, or skipped all after evaluation with a brief flow-grounded reason>`,
27
+ ` - **Suggested Skills**: <deduplicated suggestions or none>`,
28
+ ` - **Applicability**: <use | not-applicable + reason | unresolved/unavailable>`,
29
+ ` - **Escape**: <none | reason + uncovered scope + evidence>`,
29
30
  ` \`\`\``,
30
31
  ``,
31
- `## 🤖 COGNITIVE COMPLIANCE (NO SILENT DEGRADES FOR NEW FEATURES)`,
32
- `- **Newly Added Features / Extensions**: Do not silently collapse a task that introduces new logic, a new API endpoint, or a new file/module into a trivial direct-edit path. Route it first and justify the tier from task evidence.`,
33
- `- **Tier 2 / Tier 3 Suggestions**: There is NO universal TODO/TDD/Fable sequence. Router suggestions are mandatory to evaluate by reading their skill entry, but optional to execute after that evaluation. Choose the smallest applicable set.`,
34
- `- **Pre-Completion Verification Gate**: Before telling the human the task is done, run \`npx github:dyphn1/Harness-everything verify\` in the project root. If it exits non-zero, fix the failures and run it again before declaring completion. On Claude Code this same check can be mechanically enforced by a stop hook; here nothing blocks you from skipping it, so treat it as a real requirement, not a suggestion.`,
35
- `- **Memory Summarization (Self-Evolve)**: When the task produces a reusable verified lesson, use the \`self-evolve\` flow to persist it; do not manufacture memory for routine work.`,
36
- `- **Environment Discovery**: Discover the environment (OS, shell, package manager) before running commands - don't assume.`,
37
- `- **Rule of 3**: If the same error repeats 3 times in a row, STOP retrying. Explain what's failing and use a fresh diagnosis / zoom-out instead of continuing to guess.`,
38
- `- **Prefer Editing**: Prefer editing over rewriting; commit logically complete chunks rather than one giant diff.`,
32
+ `## 🤖 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.`,
37
+ `- **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.`,
39
+ `- **Prefer editing**: prefer targeted edits and logically complete commits over broad rewrites.`,
39
40
  ``
40
41
  ].join('\n');
41
42
 
@@ -70,30 +71,27 @@ function injectAdvisoryText(targetFile, header, label) {
70
71
  function removeAdvisoryText(targetFile) {
71
72
  if (!fs.existsSync(targetFile)) return;
72
73
  try {
73
- let content = fs.readFileSync(targetFile, 'utf8');
74
+ const content = fs.readFileSync(targetFile, 'utf8');
74
75
  const markerIndex = content.indexOf(MARKER);
75
- if (markerIndex !== -1) {
76
- let cleanContent = content;
77
- const hashMarkerIndex = content.lastIndexOf('#', markerIndex);
78
- if (hashMarkerIndex !== -1) cleanContent = content.substring(0, hashMarkerIndex).trim() + '\n';
79
- else cleanContent = content.substring(0, markerIndex).trim() + '\n';
76
+ if (markerIndex === -1) return;
77
+ let cleanContent = content;
78
+ const hashMarkerIndex = content.lastIndexOf('#', markerIndex);
79
+ if (hashMarkerIndex !== -1) cleanContent = content.substring(0, hashMarkerIndex).trim() + '\n';
80
+ else cleanContent = content.substring(0, markerIndex).trim() + '\n';
80
81
 
81
- const lines = cleanContent.trim().split('\n').map(l => l.trim()).filter(l => l !== '');
82
- if (lines.length === 0 || (lines.length === 1 && (lines[0] === '# Cursor Project Rules' || lines[0] === '# AGENTS.md' || lines[0] === '# Copilot Instructions' || lines[0] === '# .hermes.md'))) {
83
- fs.unlinkSync(targetFile);
84
- console.log(` ✅ Removed empty advisory file: ${targetFile}`);
85
- } else {
86
- fs.writeFileSync(targetFile, cleanContent.trim() + '\n', 'utf8');
87
- console.log(` ✅ Removed Harness guidance from: ${targetFile}`);
88
- }
82
+ const lines = cleanContent.trim().split('\n').map(l => l.trim()).filter(Boolean);
83
+ if (lines.length === 0 || (lines.length === 1 && ['# Cursor Project Rules', '# AGENTS.md', '# Copilot Instructions', '# .hermes.md'].includes(lines[0]))) {
84
+ fs.unlinkSync(targetFile);
85
+ console.log(` ✅ Removed empty advisory file: ${targetFile}`);
86
+ } else {
87
+ fs.writeFileSync(targetFile, cleanContent.trim() + '\n', 'utf8');
88
+ console.log(` ✅ Removed Harness guidance from: ${targetFile}`);
89
89
  }
90
90
  } catch (e) {
91
91
  console.warn(` ⚠️ Error removing advisory text from ${targetFile}: ${e.message}`);
92
92
  }
93
93
  }
94
94
 
95
- // Continue gets a dedicated rule file. If a user already owns that exact
96
- // path, do not replace it just because Harness chose the same filename.
97
95
  function installContinueRule(targetFile, label) {
98
96
  const dir = path.dirname(targetFile);
99
97
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -103,7 +101,7 @@ function installContinueRule(targetFile, label) {
103
101
  `---`,
104
102
  `name: Harness OS Guidance`,
105
103
  `alwaysApply: true`,
106
- `description: "Harness OS routing, circuit-breaker awareness, and environment-discovery guidance (advisory only on Continue - no hook mechanism)"`,
104
+ `description: "Harness OS routing and workflow guidance (instruction-governed on Continue unless a lifecycle mechanism is independently verified)"`,
107
105
  `---`,
108
106
  advisoryInstructions.replace(/^\n/, '')
109
107
  ].join('\n');
@@ -139,25 +137,24 @@ function buildCopilotGlobalContent() {
139
137
  function buildCodexGlobalContent() {
140
138
  return [
141
139
  `---`,
142
- `description: "Harness OS Guidance - Global custom agent for orchestrating multi-agent workflows"`,
140
+ `description: "Harness OS Guidance - Global custom agent for orchestrating software workflows"`,
143
141
  `name: "Harness"`,
144
142
  `user-invocable: true`,
145
143
  `---`,
146
144
  `# AGENTS.md`,
147
145
  ``,
148
146
  `# ${MARKER}`,
149
- `This file is advisory only - this platform has no hook/execution mechanism to`,
150
- `enforce it mechanically. Treat these as strong defaults, not guarantees.`,
147
+ `This instruction surface cannot be assumed to provide hard lifecycle blocking.`,
151
148
  ``,
152
- `- Discover the environment (OS, shell, package manager) before running commands - don't assume.`,
149
+ `- Discover the environment and host capabilities before relying on them.`,
153
150
  `- For software/project work, run \`npx github:dyphn1/Harness-everything next "<summary>"\` before mutation and surface the routing checkpoint.`,
154
- `- For EVERY suggested skill, read its complete \`SKILL.md\` entry/basic flow before skipping it. Evaluate \`USE FOR\`, \`DO NOT USE FOR\`, workflow, and hard rules; do not skip from the name/description/router summary alone.`,
155
- `- Suggested-skill execution remains optional after evaluation. Using one suggestion does not waive evaluation of the other omitted suggestions; unreadable suggestions are unresolved/unavailable, not silently skipped.`,
156
- `- Do not impose a universal TODO/TDD/Fable sequence. Choose the smallest applicable skill set after evaluation.`,
157
- `- Before declaring a task done, run \`npx github:dyphn1/Harness-everything verify\` and fix any failures`,
158
- ` it reports - nothing here blocks you from skipping this, so treat it as a real requirement.`,
159
- `- If the same error repeats 3 times in a row, STOP retrying and use a fresh diagnosis / zoom-out.`,
160
- `- Prefer editing over rewriting; commit logically complete chunks rather than one giant diff.`,
151
+ `- 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.`,
156
+ `- After 3 same-signature failures, stop retrying and use a fresh diagnosis / zoom-out.`,
157
+ `- Prefer targeted edits and logically complete commits.`,
161
158
  ``
162
159
  ].join('\n');
163
160
  }
@@ -173,4 +170,4 @@ module.exports = {
173
170
  removeContinueRule,
174
171
  buildCopilotGlobalContent,
175
172
  buildCodexGlobalContent,
176
- };
173
+ };
@@ -59,7 +59,9 @@ Rules:
59
59
 
60
60
  ## Workflow-plan consumer
61
61
 
62
- Before dispatch, write the router contract and stage array to files and run:
62
+ For a session with an active mandatory workflow, write the stage array to the router-displayed `workflow-stages.json` path and invoke its `workflow-disposition.js start --session-id <id>` command. This binds the run to the active `workflowId` and bound workspace even after entering a linked worktree. Every required stage needs an objective check, and an independent plan needs a read-only `fable-verifier` stage depending on all other stages. Stage results need observed worker identities and numeric exit codes; missing metadata is blocked/degraded, not success.
63
+
64
+ For standalone use without an active session contract, write the router contract and stage array to files and run:
63
65
 
64
66
  ```bash
65
67
  node fable-mode/scripts/workflow-plan-consumer.js \
@@ -4,7 +4,7 @@ description: "Stage large, multi-source or multi-session tasks through a written
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.4.0
7
+ version: 0.8.1
8
8
  ---
9
9
 
10
10
  # Fable Mode (v3)
@@ -7,7 +7,7 @@ description: >-
7
7
  license: Apache-2.0
8
8
  metadata:
9
9
  author: Miya Daniel
10
- version: 0.4.0
10
+ version: 0.8.1
11
11
  ---
12
12
 
13
13
  # Execution Guardrails