harness-everything 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +14 -0
- package/README.md +7 -7
- package/VERIFICATION.md +3 -3
- package/behavioral-evals/PAIRED-BENCHMARK.md +3 -3
- package/behavioral-evals/README.md +38 -32
- package/behavioral-evals/opencode-hardlock-live.js +6 -290
- package/behavioral-evals/opencode-reflection-gate-live.js +342 -0
- package/behavioral-evals/paired-benchmark.js +3 -3
- package/ci/consistency-check.js +20 -0
- package/ci/invariant-routing.test.js +58 -158
- package/ci/mechanism-2a-rule-of-3.test.js +73 -138
- package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
- package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
- package/ci/mechanism-2b-boundary-guard.test.js +6 -16
- package/ci/mechanism-2e-scope-guard.test.js +8 -24
- package/ci/mechanism-2f-stop-gate.test.js +21 -75
- package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
- package/ci/mechanism-31-opencode-hardlock-evidence.test.js +43 -99
- package/ci/openai-plugin-package.test.js +8 -8
- package/ci/openai-plugin-runtime.test.js +2 -2
- package/ci/router-workflow-plan.test.js +25 -6
- package/ci/self-evolve-loop.test.js +4 -1
- package/ci/workflow-execution-contract.test.js +51 -129
- package/ci/workflow-plan-consumers-edge.test.js +1 -1
- package/ci/workflow-plan-consumers.test.js +6 -6
- package/docs/architecture.md +21 -21
- package/docs/enforcement-locks.md +41 -102
- package/docs/mechanism-first-skill-mesh.md +2 -2
- package/docs/philosophy.md +2 -2
- package/docs/platform-capabilities.md +3 -3
- package/docs/routing.md +14 -14
- package/docs/workflow-runtime.md +80 -110
- package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
- package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +7 -6
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/harness-everything/SKILL.md +11 -11
- package/harness-everything/references/router-workflow-plan.md +2 -2
- package/harness-everything/references/triage-and-tiers.md +11 -11
- package/harness-everything/scripts/kernel-router-core.js +5 -5
- package/harness-everything/scripts/kernel-router.js +16 -21
- package/harness-everything/scripts/router-contract-core.js +1 -1
- package/hooks/hooks.json +10 -23
- package/hooks/scripts/atomic-commit-check.js +1 -1
- package/hooks/scripts/boundary-guard.js +4 -4
- package/hooks/scripts/contract-test.js +8 -11
- package/hooks/scripts/depth-guard.js +2 -2
- package/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/hooks/scripts/rule-of-3.js +4 -28
- package/hooks/scripts/state-persist.js +36 -136
- package/hooks/scripts/stop-gate.js +15 -48
- package/hooks/scripts/subagent-scope-guard.js +8 -8
- package/hooks/scripts/workflow-disposition.js +2 -10
- package/hooks/scripts/workflow-gate.js +33 -84
- package/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/hooks/scripts/workflow-stop-gate.js +23 -38
- package/opencode-plugin/README.md +10 -13
- package/opencode-plugin/index.mjs +14 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +3 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +3 -3
- package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
- package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
- package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
- package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
- package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
- package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
- package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +7 -6
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +2 -2
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +16 -21
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +3 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +4 -4
- package/scripts/lib/advisory-text.js +16 -29
- package/using-git-worktrees/SKILL.md +5 -5
- package/zoom-out/SKILL.md +3 -3
- package/zoom-out/references/circuit-breaker.md +4 -4
|
@@ -2,151 +2,51 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
5
|
-
const {
|
|
6
|
-
const { cwdOf, shellProbeKey, isVerificationShell, workspaceFingerprint, directMutationInWorkspace } = require('./lib/workflow-isolation');
|
|
5
|
+
const { cwdOf, classifyShell, isVerificationShell, directMutationInWorkspace } = require('./lib/workflow-isolation');
|
|
7
6
|
const { observeTool } = require('./lib/telemetry');
|
|
8
|
-
|
|
9
7
|
let inputData = '';
|
|
10
|
-
const timeout = setTimeout(() =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
process.stdin.on('data', chunk => {
|
|
15
|
-
inputData += chunk;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
process.stdin.on('end', () => {
|
|
19
|
-
clearTimeout(timeout);
|
|
20
|
-
try {
|
|
21
|
-
const payload = JSON.parse(inputData.trim());
|
|
22
|
-
processState(payload);
|
|
23
|
-
} catch (err) {
|
|
24
|
-
processState(null);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function observeWorkspaceMutation(payload, root) {
|
|
29
|
-
const toolName = payload && (payload.tool_name || payload.tool);
|
|
30
|
-
if (toolName !== 'Bash' && toolName !== 'PowerShell' && toolName !== 'exec_command') return false;
|
|
31
|
-
const context = loadWorkflow(payload);
|
|
32
|
-
if (!context.workflow || context.workflow.state === 'deferred') return false;
|
|
33
|
-
const cwd = cwdOf(payload, root);
|
|
34
|
-
const probeKey = shellProbeKey(payload, cwd);
|
|
35
|
-
const probe = peekMutationProbe(context, probeKey);
|
|
36
|
-
if (!probe) return false;
|
|
37
|
-
const observationCwd = probe.observationCwd || cwd;
|
|
38
|
-
let after;
|
|
39
|
-
try {
|
|
40
|
-
after = workspaceFingerprint(observationCwd);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
const fingerprintLimited = error && error.code === 'HARNESS_FINGERPRINT_LIMIT';
|
|
43
|
-
if (!isMajorWorkflow(context.workflow) || fingerprintLimited) {
|
|
44
|
-
const evidence = fingerprintLimited ? 'fingerprint-limit' : 'unobservable-workspace';
|
|
45
|
-
const conservative = settleMutationProbeConservative(context, probeKey, toolName, evidence);
|
|
46
|
-
return Boolean(conservative && conservative.changed);
|
|
47
|
-
}
|
|
48
|
-
context.workflow.state = 'blocked';
|
|
49
|
-
context.workflow.blockReason = 'mutation-observation-failed';
|
|
50
|
-
saveWorkflow(context);
|
|
51
|
-
const critical = new Error('cannot verify whether shell execution changed workspace content');
|
|
52
|
-
critical.code = 'HARNESS_MUTATION_OBSERVATION';
|
|
53
|
-
throw critical;
|
|
54
|
-
}
|
|
55
|
-
const result = settleMutationProbe(context, probeKey, after, toolName);
|
|
56
|
-
return Boolean(result && result.changed);
|
|
57
|
-
}
|
|
58
|
-
|
|
8
|
+
const timeout = setTimeout(() => processState(null), 200);
|
|
9
|
+
process.stdin.on('data', chunk => { inputData += chunk; });
|
|
10
|
+
process.stdin.on('end', () => { clearTimeout(timeout); try { processState(JSON.parse(inputData.trim())); } catch (_) { processState(null); } });
|
|
59
11
|
function processState(payload) {
|
|
60
|
-
let exitCodeForHook = 0;
|
|
61
12
|
try {
|
|
62
13
|
const sessionId = payload && (payload.session_id || payload.sessionId);
|
|
63
14
|
const root = getWorkspaceRoot(payload);
|
|
64
15
|
const stateFile = path.join(getSessionDir(root, sessionId), 'handoff-state.json');
|
|
65
|
-
|
|
66
|
-
// Merge into the existing state rather than replacing it, so the
|
|
67
|
-
// failure/idle status and the Stop-gate milestones can coexist.
|
|
68
16
|
let state = { status: 'idle', timestamp: new Date().toISOString() };
|
|
69
|
-
if (fs.existsSync(stateFile)) {
|
|
70
|
-
|
|
17
|
+
if (fs.existsSync(stateFile)) { try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (_) {} }
|
|
18
|
+
if (!payload) {
|
|
19
|
+
if (!fs.existsSync(stateFile)) fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
20
|
+
process.exit(0);
|
|
71
21
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(!isSuccessEvent && exitCode === undefined && stderrSignal);
|
|
89
|
-
const toolName = payload.tool_name || payload.tool || 'command';
|
|
90
|
-
const observedMutation = observeWorkspaceMutation(payload, root);
|
|
91
|
-
|
|
92
|
-
if (isFailed) {
|
|
93
|
-
// Truncate output to avoid state bloat
|
|
94
|
-
let output = (stderrSignal ? stderr : stdout) || payload.output || '';
|
|
95
|
-
if (output.length > 500) {
|
|
96
|
-
output = '...' + output.slice(-500);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
state.status = 'failed';
|
|
100
|
-
state.timestamp = new Date().toISOString();
|
|
101
|
-
state.tool = toolName;
|
|
102
|
-
state.exitCode = exitCode;
|
|
103
|
-
state.errorSummary = output.trim();
|
|
104
|
-
} else if (!isFailed && state.status === 'failed') {
|
|
105
|
-
// Clear failed state or mark as idle/resolved.
|
|
106
|
-
// NOTE (#166, follows #153): Claude Code Bash PostToolUse payloads
|
|
107
|
-
// carry no numeric exit code, so `exitCode === 0` never holds there.
|
|
108
|
-
// A PostToolUse event with no numeric non-zero exit status counts as
|
|
109
|
-
// success even when stderr contains diagnostics. Hosts that omit the
|
|
110
|
-
// lifecycle event name retain the conservative stderr fallback.
|
|
111
|
-
state.lastResolved = { tool: state.tool, timestamp: state.timestamp };
|
|
112
|
-
state.status = 'idle';
|
|
113
|
-
state.timestamp = new Date().toISOString();
|
|
114
|
-
delete state.exitCode;
|
|
115
|
-
delete state.errorSummary;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Milestones for the Stop gate: when did the last mutation happen, and
|
|
119
|
-
// has any verification-ish command succeeded since. Direct-tool edits
|
|
120
|
-
// outside the workspace advance neither milestone (#165).
|
|
121
|
-
const isDirectTool = toolName === 'Edit' || toolName === 'Write' || toolName === 'apply_patch';
|
|
122
|
-
if ((isDirectTool && directMutationInWorkspace(payload, cwdOf(payload, root), root)) || observedMutation) {
|
|
123
|
-
state.lastEditAt = Date.now();
|
|
124
|
-
}
|
|
125
|
-
if ((toolName === 'Bash' || toolName === 'PowerShell') && !isFailed) {
|
|
126
|
-
const command = (payload.tool_input && payload.tool_input.command) || '';
|
|
127
|
-
if (isVerificationShell(command)) {
|
|
128
|
-
state.lastVerifyAt = Date.now();
|
|
129
|
-
state.lastVerifyExitCode = exitCode ?? null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
134
|
-
// Reuse this already-running post-tool hook instead of spawning another
|
|
135
|
-
// process for every tool solely for telemetry. The observer is fail-open
|
|
136
|
-
// and stores no command/args/output content.
|
|
137
|
-
observeTool(payload);
|
|
138
|
-
} else {
|
|
139
|
-
// No standard input payload, or invalid JSON. Just maintain a heartbeat timestamp
|
|
140
|
-
if (!fs.existsSync(stateFile)) {
|
|
141
|
-
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
142
|
-
}
|
|
22
|
+
const toolResponse = payload.tool_response || {};
|
|
23
|
+
const stdout = toolResponse.stdout ?? payload.stdout ?? '';
|
|
24
|
+
const stderr = toolResponse.stderr ?? payload.stderr ?? payload.error ?? '';
|
|
25
|
+
const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode;
|
|
26
|
+
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
27
|
+
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
28
|
+
const hookEvent = payload.hook_event_name || payload.hookEventName || '';
|
|
29
|
+
const isSuccessEvent = hookEvent === 'PostToolUse';
|
|
30
|
+
const isFailed = hookEvent === 'PostToolUseFailure' || (exitCode !== undefined && exitCode !== 0) || (!isSuccessEvent && exitCode === undefined && stderrSignal);
|
|
31
|
+
const toolName = payload.tool_name || payload.tool || 'command';
|
|
32
|
+
if (isFailed) {
|
|
33
|
+
let output = (stderrSignal ? stderr : stdout) || payload.output || '';
|
|
34
|
+
if (output.length > 500) output = '...' + output.slice(-500);
|
|
35
|
+
state.status = 'failed'; state.timestamp = new Date().toISOString(); state.tool = toolName; state.exitCode = exitCode; state.errorSummary = output.trim();
|
|
36
|
+
} else if (state.status === 'failed') {
|
|
37
|
+
state.lastResolved = { tool: state.tool, timestamp: state.timestamp }; state.status = 'idle'; state.timestamp = new Date().toISOString(); delete state.exitCode; delete state.errorSummary;
|
|
143
38
|
}
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
39
|
+
const isDirectTool = toolName === 'Edit' || toolName === 'Write' || toolName === 'apply_patch';
|
|
40
|
+
if (isDirectTool && directMutationInWorkspace(payload, cwdOf(payload, root), root)) state.lastEditAt = Date.now();
|
|
41
|
+
const isShell = toolName === 'Bash' || toolName === 'PowerShell' || toolName === 'exec_command';
|
|
42
|
+
const command = (payload.tool_input && payload.tool_input.command) || '';
|
|
43
|
+
if (isShell) {
|
|
44
|
+
const commandClass = classifyShell(command);
|
|
45
|
+
if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
|
|
46
|
+
if (!isFailed && isVerificationShell(command)) { state.lastVerifyAt = Date.now(); state.lastVerifyExitCode = exitCode ?? null; }
|
|
148
47
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
48
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
49
|
+
observeTool(payload);
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
process.exit(0);
|
|
152
52
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* while uncommitted changes sit in the tree gets bounced back ONCE per edit
|
|
9
|
-
* batch. Objective facts only; fails open on any error.
|
|
3
|
+
* Verification reminder for Stop hooks.
|
|
4
|
+
*
|
|
5
|
+
* If uncommitted edits exist and no later successful verification was observed,
|
|
6
|
+
* emit one concise reminder for this Stop event. No gate state is persisted and
|
|
7
|
+
* Stop is never rejected.
|
|
10
8
|
*/
|
|
11
9
|
const fs = require('fs');
|
|
12
10
|
const path = require('path');
|
|
@@ -15,66 +13,35 @@ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
|
15
13
|
|
|
16
14
|
function decide(payload) {
|
|
17
15
|
try {
|
|
18
|
-
// Loop guard: if this stop already resulted from a Stop-hook block,
|
|
19
|
-
// always let it through - one bounce is the contract, never a cage.
|
|
20
|
-
if (payload && payload.stop_hook_active) {
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
const root = getWorkspaceRoot(payload);
|
|
25
17
|
const sessionDir = getSessionDir(root, payload && (payload.session_id || payload.sessionId));
|
|
26
18
|
const handoffFile = path.join(sessionDir, 'handoff-state.json');
|
|
27
|
-
|
|
19
|
+
if (!fs.existsSync(handoffFile)) return;
|
|
28
20
|
|
|
29
|
-
if (!fs.existsSync(handoffFile)) process.exit(0);
|
|
30
21
|
const handoff = JSON.parse(fs.readFileSync(handoffFile, 'utf8'));
|
|
31
22
|
const lastEditAt = handoff.lastEditAt || 0;
|
|
32
23
|
const lastVerifyAt = handoff.lastVerifyAt || 0;
|
|
24
|
+
if (!lastEditAt || lastVerifyAt >= lastEditAt) return;
|
|
33
25
|
|
|
34
|
-
// No edits this session, or verification already ran after the last edit.
|
|
35
|
-
if (!lastEditAt || lastVerifyAt >= lastEditAt) process.exit(0);
|
|
36
|
-
|
|
37
|
-
// One bounce per edit batch: if we already blocked for this exact edit
|
|
38
|
-
// timestamp, the model either verified (which moves lastVerifyAt) or
|
|
39
|
-
// explicitly chose not to - don't nag twice for the same batch.
|
|
40
|
-
let gate = {};
|
|
41
|
-
if (fs.existsSync(gateFile)) {
|
|
42
|
-
try { gate = JSON.parse(fs.readFileSync(gateFile, 'utf8')); } catch (e) { /* ignore */ }
|
|
43
|
-
}
|
|
44
|
-
if (gate.blockedForEditAt === lastEditAt) process.exit(0);
|
|
45
|
-
|
|
46
|
-
// Only relevant when unverified work is actually sitting in the tree.
|
|
47
26
|
const dirty = execSync('git status --porcelain', {
|
|
48
27
|
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
|
|
49
28
|
}).trim();
|
|
50
|
-
if (!dirty)
|
|
51
|
-
|
|
52
|
-
fs.writeFileSync(gateFile, JSON.stringify({ blockedForEditAt: lastEditAt }, null, 2), 'utf8');
|
|
29
|
+
if (!dirty) return;
|
|
53
30
|
|
|
54
|
-
console.error(
|
|
55
|
-
console.error(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
process.exit(2);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
// Fail open - a broken gate must never trap the session.
|
|
61
|
-
process.exit(0);
|
|
31
|
+
console.error('[Verification Reminder] Uncommitted edits have no observed successful verification after the latest edit.');
|
|
32
|
+
console.error("Run a relevant test/build/lint check before claiming completion, or state why verification is not applicable.");
|
|
33
|
+
} catch (_) {
|
|
34
|
+
// Reminder infrastructure is always fail-open.
|
|
62
35
|
}
|
|
63
36
|
}
|
|
64
37
|
|
|
65
38
|
let inputData = '';
|
|
66
|
-
const timeout = setTimeout(() =>
|
|
67
|
-
decide(null);
|
|
68
|
-
}, 200);
|
|
69
|
-
|
|
39
|
+
const timeout = setTimeout(() => decide(null), 200);
|
|
70
40
|
process.stdin.on('data', chunk => { inputData += chunk; });
|
|
71
41
|
process.stdin.on('end', () => {
|
|
72
42
|
clearTimeout(timeout);
|
|
73
43
|
let payload = null;
|
|
74
|
-
try { payload = JSON.parse(inputData.trim()); } catch (
|
|
44
|
+
try { payload = JSON.parse(inputData.trim()); } catch (_) {}
|
|
75
45
|
decide(payload);
|
|
76
46
|
});
|
|
77
|
-
process.stdin.on('error', () => {
|
|
78
|
-
clearTimeout(timeout);
|
|
79
|
-
decide(null);
|
|
80
|
-
});
|
|
47
|
+
process.stdin.on('error', () => { clearTimeout(timeout); decide(null); });
|
|
@@ -161,20 +161,20 @@ process.stdin.on('end', () => {
|
|
|
161
161
|
atomicWriteJson(path.join(sessionDir, 'subagent-scope-last.json'), audit);
|
|
162
162
|
|
|
163
163
|
if (newlyChanged.length === 0) {
|
|
164
|
-
console.log('[Subagent Scope
|
|
164
|
+
console.log('[Subagent Scope Reminder] No new changed files detected during the subagent burst.');
|
|
165
165
|
} else if (declaredContracts.length === 0) {
|
|
166
|
-
console.error(`[Subagent Scope
|
|
166
|
+
console.error(`[Subagent Scope Reminder] ${newlyChanged.length} file(s) changed with no machine-readable writeSet contract:`);
|
|
167
167
|
newlyChanged.forEach(line => console.error(` ${line}`));
|
|
168
168
|
console.error('Confirm every path was actually in scope before trusting or committing this output.');
|
|
169
169
|
} else {
|
|
170
170
|
for (const entry of classified.expected) {
|
|
171
|
-
console.log(`[Subagent Scope
|
|
171
|
+
console.log(`[Subagent Scope Reminder] in-scope ${entry.statusLine} -> ${entry.planId}/${entry.runId}/${entry.stageId}${entry.workerId ? ` worker=${entry.workerId}` : ''}`);
|
|
172
172
|
}
|
|
173
173
|
for (const entry of classified.ambiguous) {
|
|
174
|
-
console.error(`[Subagent Scope
|
|
174
|
+
console.error(`[Subagent Scope Reminder] ambiguous scope ${entry.statusLine}; matches ${entry.stages.map(stage => `${stage.runId}/${stage.stageId}`).join(', ')}`);
|
|
175
175
|
}
|
|
176
176
|
for (const entry of classified.outOfScope) {
|
|
177
|
-
console.error(`[Subagent Scope
|
|
177
|
+
console.error(`[Subagent Scope Reminder] OUT-OF-SCOPE ${entry.statusLine}${entry.workerId ? ` worker=${entry.workerId}` : ''}`);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -189,7 +189,7 @@ process.stdin.on('end', () => {
|
|
|
189
189
|
: classified.ambiguous.length > 0 || classified.outOfScope.length > 0;
|
|
190
190
|
if (violated) {
|
|
191
191
|
console.error('Do not "git add -A"; stage files explicitly so unexpected paths stay visible.');
|
|
192
|
-
process.exit(
|
|
192
|
+
process.exit(0);
|
|
193
193
|
}
|
|
194
194
|
process.exit(0);
|
|
195
195
|
}
|
|
@@ -197,8 +197,8 @@ process.stdin.on('end', () => {
|
|
|
197
197
|
process.exit(0);
|
|
198
198
|
} catch (error) {
|
|
199
199
|
if (error && error.code === 'HARNESS_WORKFLOW_BUDGET') {
|
|
200
|
-
console.error(`[Subagent Scope
|
|
201
|
-
process.exit(
|
|
200
|
+
console.error(`[Subagent Scope Reminder] ${error.message}`);
|
|
201
|
+
process.exit(0);
|
|
202
202
|
}
|
|
203
203
|
// This is otherwise a post-execution visibility gate. Runtime errors fail open;
|
|
204
204
|
// actionGate has a stricter fail-ask policy for side effects.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES,
|
|
6
|
+
const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES, WORKFLOW_CONTROLLER_COMMANDS } = require('./lib/workflow-runtime');
|
|
7
7
|
const { getWorkspaceRoot, readCurrentSession } = require('./lib/harness-state');
|
|
8
8
|
const { atomicWriteJson, readJson } = require('./lib/fable-contracts');
|
|
9
9
|
|
|
@@ -41,9 +41,6 @@ function activatePlan(workflow, plan) {
|
|
|
41
41
|
workflow.suggestedSkills = plan.suggestedSkills || [];
|
|
42
42
|
workflow.verification = plan.verification;
|
|
43
43
|
workflow.mutationIsolation = plan.mutationIsolation || { required: false };
|
|
44
|
-
const budget = ensureWorkflowBudget(workflow);
|
|
45
|
-
budget.limits = budgetLimits(workflow);
|
|
46
|
-
budget.updatedAt = new Date().toISOString();
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
function main() {
|
|
@@ -57,8 +54,6 @@ function main() {
|
|
|
57
54
|
if (args.command === 'start') {
|
|
58
55
|
if (!workflow.workflowId || !workflow.workflowPlan) throw new Error('legacy workflow lacks a correlated plan; cannot start it implicitly');
|
|
59
56
|
if (workflow.state === 'running') throw new Error('workflow already running; record a blocker before replanning');
|
|
60
|
-
const isReplan = workflow.revision > 0 || Boolean(workflow.runId);
|
|
61
|
-
if (isReplan) recordBudgetEvent(context, 'replan', { evidence: 'workflow-disposition:start' });
|
|
62
57
|
const plan = workflow.pendingPlan || workflow.workflowPlan;
|
|
63
58
|
if (!String(plan.strategy || '').startsWith('fable-')) {
|
|
64
59
|
activatePlan(workflow, plan);
|
|
@@ -90,9 +85,7 @@ function main() {
|
|
|
90
85
|
delete workflow.pendingPlan;
|
|
91
86
|
delete workflow.blockReason;
|
|
92
87
|
} else if (args.command === 'revision') {
|
|
93
|
-
|
|
94
|
-
} else if (args.command === 'reset-budget') {
|
|
95
|
-
resetWorkflowBudget(context, args.evidence);
|
|
88
|
+
console.error('[Workflow Reminder] Revision requested; no hard revision budget is enforced.');
|
|
96
89
|
} else if (args.command === 'escape') {
|
|
97
90
|
if (!ALLOWED_ESCAPE_REASONS.has(args.reasonCode)) throw new Error('generic simple/routine/already-clear reasons are intentionally rejected');
|
|
98
91
|
if (!args.scope?.trim()) throw new Error('--scope is required');
|
|
@@ -114,7 +107,6 @@ function main() {
|
|
|
114
107
|
workflow.blockReason = args.evidence.trim();
|
|
115
108
|
} else throw new Error(usage());
|
|
116
109
|
saveWorkflow(context);
|
|
117
|
-
syncBudgetToRun(context);
|
|
118
110
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
|
|
119
111
|
}
|
|
120
112
|
try { main(); } catch (error) { console.error('[Workflow Disposition] ' + error.message); process.exitCode = 2; }
|
|
@@ -1,101 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const fs = require('fs');
|
|
6
|
-
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun,
|
|
7
|
-
const { key, classifyShell,
|
|
8
|
-
|
|
5
|
+
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, readHookInput, WORKFLOW_CONTROLLER_COMMANDS } = require('./lib/workflow-runtime');
|
|
6
|
+
const { key, classifyShell, cwdOf, commandOf, mutationPaths, directMutationInWorkspace, linkedWorktree, assertTargets, assertShellScope } = require('./lib/workflow-isolation');
|
|
9
7
|
const DIRECT = new Set(['Edit', 'Write', 'apply_patch']);
|
|
10
8
|
const SHELL = new Set(['Bash', 'PowerShell', 'exec_command']);
|
|
11
|
-
|
|
12
|
-
function shellTimeoutMs(payload) {
|
|
13
|
-
const input = payload?.tool_input || payload?.input || {};
|
|
14
|
-
for (const value of [input.timeout_ms, input.timeoutMs, input.timeout, payload.timeout_ms, payload.timeoutMs]) {
|
|
15
|
-
const parsed = Number(value);
|
|
16
|
-
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
9
|
+
function warn(message) { console.error('[Workflow Reminder] ' + message); }
|
|
21
10
|
function isController(command, cwd) {
|
|
22
|
-
|
|
23
|
-
if (/[;&|`\r\n<>$(){}]/.test(command)) return false;
|
|
11
|
+
if (/[;&|\x60\r\n<>$(){}]/.test(command)) return false;
|
|
24
12
|
const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+([A-Za-z][A-Za-z0-9-]*)\b/);
|
|
25
13
|
return Boolean(match && WORKFLOW_CONTROLLER_COMMANDS.has(match[4]) &&
|
|
26
14
|
key(path.resolve(cwd, match[1] || match[2] || match[3])) === key(path.join(__dirname, 'workflow-disposition.js')));
|
|
27
15
|
}
|
|
28
|
-
|
|
29
16
|
function decide(payload) {
|
|
30
17
|
if (!payload) return;
|
|
31
|
-
const tool = payload.tool_name || payload.tool;
|
|
32
|
-
if (!DIRECT.has(tool) && !SHELL.has(tool)) return;
|
|
33
|
-
const context = loadWorkflow(payload);
|
|
34
|
-
const { workflow, root, sessionDir } = context;
|
|
35
|
-
if (!workflow || workflow.state === 'deferred') return;
|
|
36
|
-
const cwd = cwdOf(payload, root);
|
|
37
|
-
const command = commandOf(payload);
|
|
38
|
-
const commandClass = SHELL.has(tool) ? classifyShell(command) : 'direct-mutation';
|
|
39
|
-
if (SHELL.has(tool) && isController(command, cwd)) return;
|
|
40
|
-
if (commandClass === 'read-only' || commandClass === 'worktree-setup') return;
|
|
41
|
-
// Only the stage specification is allowed before run entry. Other runtime
|
|
42
|
-
// state, including workflow-run.json, cannot use this bootstrap exception.
|
|
43
|
-
const targets = DIRECT.has(tool) ? mutationPaths(payload) : [];
|
|
44
|
-
const spec = path.join(sessionDir, 'workflow-stages.json');
|
|
45
|
-
const specStat = fs.existsSync(spec) ? fs.lstatSync(spec) : null;
|
|
46
|
-
if ((!specStat || (specStat.isFile() && !specStat.isSymbolicLink() && specStat.nlink === 1)) &&
|
|
47
|
-
targets.length && targets.every(target => key(path.resolve(cwd, target)) === key(spec))) return;
|
|
48
|
-
if (['blocked', 'failed'].includes(workflow.state)) throw new Error('workflow is BLOCKED; replan through the controller before mutation');
|
|
49
|
-
if (isMajorWorkflow(workflow)) {
|
|
50
|
-
const isolatedRoot = linkedWorktree(cwd, root);
|
|
51
|
-
if (!isolatedRoot) throw new Error('Git worktree isolation is required before mutation. Enter a linked worktree; unavailable isolation means BLOCKED, never in-place fallback.');
|
|
52
|
-
if (DIRECT.has(tool)) assertTargets(payload, cwd, isolatedRoot);
|
|
53
|
-
else assertShellScope(command, cwd, isolatedRoot);
|
|
54
|
-
}
|
|
55
|
-
if (workflow.state === 'satisfied') throw new Error('workflow is already satisfied; submit a new task before further mutation');
|
|
56
|
-
if (String(workflow.strategy || '').startsWith('fable-') && !matchingRun(context)) {
|
|
57
|
-
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.');
|
|
58
|
-
}
|
|
59
|
-
if (DIRECT.has(tool)) {
|
|
60
|
-
// Bookkeeping outside the workspace (scratchpad, memory files, temp
|
|
61
|
-
// notes) is not a workspace mutation: it consumes no Tier-2 iteration
|
|
62
|
-
// and advances no mutation milestone (#165).
|
|
63
|
-
if (!directMutationInWorkspace(payload, cwd, root)) return;
|
|
64
|
-
if (workflow.strategy === 'iterative-single') {
|
|
65
|
-
recordBudgetEvent(context, 'iteration', { evidence: `${tool}:direct-mutation` });
|
|
66
|
-
}
|
|
67
|
-
workflow.lastMutationAt = Date.now();
|
|
68
|
-
saveWorkflow(context);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Shell safety and shell mutation accounting are intentionally separate.
|
|
73
|
-
// `untrusted` means "not proven read-only", not "a mutation occurred".
|
|
74
|
-
// Capture an opaque pre-execution workspace fingerprint when possible. A
|
|
75
|
-
// non-major workflow may run outside Git, where mutation observation is an
|
|
76
|
-
// accounting aid rather than an isolation boundary; degrade conservatively
|
|
77
|
-
// instead of blocking the user's command (#162).
|
|
78
|
-
let fingerprint;
|
|
79
18
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
19
|
+
const tool = payload.tool_name || payload.tool;
|
|
20
|
+
if (!DIRECT.has(tool) && !SHELL.has(tool)) return;
|
|
21
|
+
const context = loadWorkflow(payload);
|
|
22
|
+
const { workflow, root, sessionDir } = context;
|
|
23
|
+
if (!workflow || workflow.state === 'deferred') return;
|
|
24
|
+
const cwd = cwdOf(payload, root), command = commandOf(payload);
|
|
25
|
+
const commandClass = SHELL.has(tool) ? classifyShell(command) : 'direct-mutation';
|
|
26
|
+
if (SHELL.has(tool) && isController(command, cwd)) return;
|
|
27
|
+
if (commandClass === 'read-only' || commandClass === 'worktree-setup') return;
|
|
28
|
+
const targets = DIRECT.has(tool) ? mutationPaths(payload) : [];
|
|
29
|
+
const spec = path.join(sessionDir, 'workflow-stages.json');
|
|
30
|
+
const specStat = fs.existsSync(spec) ? fs.lstatSync(spec) : null;
|
|
31
|
+
if ((!specStat || (specStat.isFile() && !specStat.isSymbolicLink() && specStat.nlink === 1)) &&
|
|
32
|
+
targets.length && targets.every(target => key(path.resolve(cwd, target)) === key(spec))) return;
|
|
33
|
+
if (['blocked', 'failed'].includes(workflow.state)) warn('Workflow is marked ' + workflow.state.toUpperCase() + '; review the recorded reason before continuing.');
|
|
34
|
+
if (workflow.state === 'satisfied') warn('Workflow was already marked satisfied; confirm this mutation belongs to the current task.');
|
|
35
|
+
if (String(workflow.strategy || '').startsWith('fable-') && !matchingRun(context)) warn('Selected Fable workflow has no correlated run; consider creating/updating the stage map before broad changes.');
|
|
36
|
+
if (isMajorWorkflow(workflow)) {
|
|
37
|
+
const isolatedRoot = linkedWorktree(cwd, root);
|
|
38
|
+
if (!isolatedRoot) warn('This is a major workflow; a linked Git worktree is strongly recommended before mutation.');
|
|
39
|
+
else {
|
|
40
|
+
try { if (DIRECT.has(tool)) assertTargets(payload, cwd, isolatedRoot); else assertShellScope(command, cwd, isolatedRoot); }
|
|
41
|
+
catch (error) { warn(error.message); }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (DIRECT.has(tool) && directMutationInWorkspace(payload, cwd, root)) {
|
|
45
|
+
workflow.lastMutationAt = Date.now();
|
|
46
|
+
saveWorkflow(context);
|
|
87
47
|
}
|
|
88
|
-
|
|
89
|
-
saveWorkflow(context);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const probeKey = shellProbeKey(payload, cwd);
|
|
93
|
-
const iterative = workflow.strategy === 'iterative-single';
|
|
94
|
-
registerMutationProbe(context, probeKey, fingerprint, iterative, cwd, {
|
|
95
|
-
countIteration: iterative,
|
|
96
|
-
allowAtLimit: iterative && isVerificationShell(command),
|
|
97
|
-
timeoutMs: shellTimeoutMs(payload),
|
|
98
|
-
});
|
|
48
|
+
} catch (error) { warn(error && error.message ? error.message : String(error)); }
|
|
99
49
|
}
|
|
100
|
-
|
|
101
50
|
readHookInput(decide);
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function discardDenied(payload) {
|
|
8
|
-
if (!payload) return;
|
|
9
|
-
const tool = payload.tool_name || payload.tool;
|
|
10
|
-
if (tool !== 'Bash' && tool !== 'PowerShell' && tool !== 'exec_command') return;
|
|
11
|
-
const context = loadWorkflow(payload);
|
|
12
|
-
if (!context.workflow || context.workflow.state === 'deferred') return;
|
|
13
|
-
const cwd = cwdOf(payload, context.root);
|
|
14
|
-
discardMutationProbe(context, shellProbeKey(payload, cwd));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
readHookInput(discardDenied);
|
|
3
|
+
// Compatibility no-op: mutation reservations were removed by #190.
|
|
4
|
+
const { readHookInput } = require('./lib/workflow-runtime');
|
|
5
|
+
readHookInput(() => {});
|
|
@@ -1,52 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const { readJson } = require('./lib/fable-contracts');
|
|
6
5
|
const { loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput } = require('./lib/workflow-runtime');
|
|
7
|
-
|
|
6
|
+
function warn(message) { console.error('[Workflow Reminder] ' + message); }
|
|
8
7
|
function decide(payload) {
|
|
9
8
|
if (!payload) return;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
9
|
+
try {
|
|
10
|
+
const context = loadWorkflow(payload);
|
|
11
|
+
const { workflow, sessionDir } = context;
|
|
12
|
+
if (!workflow || workflow.state === 'deferred') return;
|
|
22
13
|
const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
workflow.unresolved = unresolved;
|
|
36
|
-
if (payload.stop_hook_active) {
|
|
37
|
-
// Do not loop forever or pretend the retry satisfied the contract.
|
|
38
|
-
workflow.state = 'blocked';
|
|
39
|
-
workflow.blockReason = 'unresolved-stop-retry';
|
|
14
|
+
let unresolved = [];
|
|
15
|
+
if (String(workflow.strategy || '').startsWith('fable-')) {
|
|
16
|
+
const match = matchingRun(context);
|
|
17
|
+
unresolved = match ? unresolvedStages(match, workflow, handoff?.lastEditAt || 0) : ['correlated-run-missing'];
|
|
18
|
+
} else {
|
|
19
|
+
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
20
|
+
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved = ['verification-after-edit-missing'];
|
|
21
|
+
}
|
|
22
|
+
const wasBlocked = workflow.state === 'blocked';
|
|
23
|
+
if (wasBlocked) warn('Workflow is marked BLOCKED; report or revisit the recorded blocker, but Harness will not trap the session.');
|
|
24
|
+
if (unresolved.length) {
|
|
25
|
+
workflow.unresolved = unresolved;
|
|
40
26
|
saveWorkflow(context);
|
|
41
|
-
|
|
27
|
+
warn('Unresolved workflow evidence: ' + unresolved.join(', ') + '. Verify/review before claiming completion.');
|
|
42
28
|
return;
|
|
43
29
|
}
|
|
30
|
+
if (wasBlocked) return;
|
|
31
|
+
workflow.state = 'satisfied';
|
|
32
|
+
workflow.resolvedAt = new Date().toISOString();
|
|
33
|
+
workflow.unresolved = [];
|
|
44
34
|
saveWorkflow(context);
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
workflow.state = 'satisfied';
|
|
48
|
-
workflow.resolvedAt = new Date().toISOString();
|
|
49
|
-
workflow.unresolved = [];
|
|
50
|
-
saveWorkflow(context);
|
|
35
|
+
} catch (error) { warn(error && error.message ? error.message : String(error)); }
|
|
51
36
|
}
|
|
52
37
|
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.16.
|
|
4
|
+
"version": "0.16.2",
|
|
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",
|