harness-everything 0.11.0 → 0.11.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/CHANGELOG.md +15 -0
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +52 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +0 -1
- package/hooks/scripts/lib/workflow-runtime.js +11 -2
- package/hooks/scripts/workflow-disposition.js +7 -2
- package/hooks/scripts/workflow-gate.js +4 -3
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +11 -2
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +7 -2
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +4 -3
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +0 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.11.
|
|
13
|
+
"version": "0.11.2",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.11.2](https://github.com/dyphn1/Harness-everything/compare/v0.11.1...v0.11.2) (2026-09-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **workflow:** classify major workflow from active plan ([b1eae14](https://github.com/dyphn1/Harness-everything/commit/b1eae14ca5157c73ba9cf7c0f2e4b0ed122e9f77))
|
|
10
|
+
* **workflow:** keep pending plan isolation inert ([8b522c2](https://github.com/dyphn1/Harness-everything/commit/8b522c26589e2140bd0971365ad0569c18da6d3f))
|
|
11
|
+
|
|
12
|
+
## [0.11.1](https://github.com/dyphn1/Harness-everything/compare/v0.11.0...v0.11.1) (2026-09-18)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **workflow:** admit all trusted controller commands ([65ec5f3](https://github.com/dyphn1/Harness-everything/commit/65ec5f31e9d218d7c32e1d9ed82992814be79b9b))
|
|
17
|
+
* **workflow:** share controller command contract ([b9ba2dc](https://github.com/dyphn1/Harness-everything/commit/b9ba2dc59a5c2dce10533aad79b9a5303b921506))
|
|
18
|
+
* **workflow:** validate shared controller command set ([8d3247d](https://github.com/dyphn1/Harness-everything/commit/8d3247d51d8aa0f1cfa61328c1061cb239dfba6a))
|
|
19
|
+
|
|
5
20
|
## [0.11.0](https://github.com/dyphn1/Harness-everything/compare/v0.10.0...v0.11.0) (2026-09-18)
|
|
6
21
|
|
|
7
22
|
### Features
|
|
@@ -16,6 +16,7 @@ fs.mkdirSync(repo);
|
|
|
16
16
|
const env = { ...process.env, HARNESS_STATE_HOME: path.join(temp, 'state'), HARNESS_WORKSPACE_ROOT: repo };
|
|
17
17
|
process.env.HARNESS_STATE_HOME = env.HARNESS_STATE_HOME;
|
|
18
18
|
const state = require(path.join(runtimeRoot, 'hooks/scripts/lib/harness-state'));
|
|
19
|
+
const workflowRuntime = require(path.join(runtimeRoot, 'hooks/scripts/lib/workflow-runtime'));
|
|
19
20
|
const sessionId = 'workflow-lifecycle';
|
|
20
21
|
const payload = { session_id: sessionId, cwd: repo };
|
|
21
22
|
const sessionDir = state.getSessionDir(repo, sessionId);
|
|
@@ -113,6 +114,24 @@ try {
|
|
|
113
114
|
check(gate('Write', { file_path: path.join(linked, 'src.js') }).status === 2, 'blocked state still prohibits mutation');
|
|
114
115
|
for (let i = 0; i < 3; i++) { check(control('start').status === 0, 'bounded replan attempt ' + i); control('block', '--evidence', 'fixture blocker'); }
|
|
115
116
|
check(control('start').status === 2 && read(file).state === 'blocked', 'exhausted replan budget remains blocked');
|
|
117
|
+
check(read(file).budget?.state === 'budget-exhausted' && read(file).budget?.reasonCode === 'replan-budget-exhausted', 'Fable replan exhaustion is auditable before recovery');
|
|
118
|
+
const dispositionPath = path.join(runtimeRoot, 'hooks/scripts/workflow-disposition.js');
|
|
119
|
+
const resetCommand = `node "${dispositionPath}" reset-budget --session-id ${sessionId} --evidence fixture-fable-budget-reset`;
|
|
120
|
+
check(gate('Bash', { command: resetCommand }, repo).status === 0, 'blocked Fable workflow admits trusted reset-budget controller through gate');
|
|
121
|
+
check(gate('Bash', { command: resetCommand + ' && echo bypass' }, repo).status === 2, 'controller shell chaining is not exempted');
|
|
122
|
+
check(gate('Bash', { command: resetCommand + ' | echo bypass' }, repo).status === 2, 'controller pipe composition is not exempted');
|
|
123
|
+
check(gate('Bash', { command: `node "${dispositionPath}" reset-budget --session-id ${sessionId} --evidence $(echo bypass)` }, repo).status === 2, 'controller command substitution is not exempted');
|
|
124
|
+
check(gate('Bash', { command: `node "${path.join(runtimeRoot, 'hooks/scripts/not-workflow-disposition.js')}" reset-budget --session-id ${sessionId} --evidence wrong-path` }, repo).status === 2, 'wrong controller script path is not exempted');
|
|
125
|
+
check(control('reset-budget', '--evidence', 'fixture-fable-budget-reset').status === 0, 'reset-budget executes after gate admission');
|
|
126
|
+
const fableReset = read(file);
|
|
127
|
+
check(fableReset.state === 'pending' && fableReset.budget?.state === 'active' && fableReset.budget?.epoch === 1, 'budget reset returns Fable workflow to pending with audited epoch');
|
|
128
|
+
check(gate('Write', { file_path: path.join(repo, 'still-primary.js') }, repo).status === 2, 'budget reset does not weaken primary-tree worktree isolation');
|
|
129
|
+
const restartCommand = `node "${dispositionPath}" start --session-id ${sessionId}`;
|
|
130
|
+
check(gate('Bash', { command: restartCommand }, repo).status === 0, 'trusted start controller remains admitted after reset');
|
|
131
|
+
check(control('start').status === 0 && read(file).state === 'running', 'Fable workflow resumes after audited budget reset');
|
|
132
|
+
const revisionCommand = `node "${dispositionPath}" revision --session-id ${sessionId} --evidence fixture-revision`;
|
|
133
|
+
check(gate('Bash', { command: revisionCommand }, repo).status === 0, 'revision controller is admitted through workflow gate');
|
|
134
|
+
check(control('revision', '--evidence', 'fixture-revision').status === 0 && read(file).budget?.counters?.revisionRounds === 1, 'revision controller reaches budget accounting');
|
|
116
135
|
fs.writeFileSync(file, '{broken');
|
|
117
136
|
check(route('continue').status === 2 && fs.readFileSync(file, 'utf8') === '{broken', 'router cannot overwrite unreadable unresolved state');
|
|
118
137
|
check(stop().status === 2, 'malformed state cannot silently pass completion');
|
|
@@ -120,7 +139,39 @@ try {
|
|
|
120
139
|
check(gate('Write', { file_path: path.join(linked, 'src.js') }).status === 2 && stop().status === 2, 'partial JSON state cannot bypass workflow gates');
|
|
121
140
|
fs.unlinkSync(file);
|
|
122
141
|
check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single', 'bounded fix selects iterative lifecycle');
|
|
123
|
-
|
|
142
|
+
const activeTier2 = read(file);
|
|
143
|
+
check(activeTier2.tier === 'tier2' && activeTier2.mutationIsolation?.required === false && !workflowRuntime.isMajorWorkflow(activeTier2), 'Tier 2 iterative workflow starts non-major');
|
|
144
|
+
check(route('Refactor the entire repository architecture in dependent stages.').status === 0, 'stronger Tier 3 route is retained for explicit replan');
|
|
145
|
+
const queuedTier3 = read(file);
|
|
146
|
+
check(queuedTier3.state === 'blocked' && queuedTier3.pendingPlan?.tier === 'tier3' && queuedTier3.pendingPlan?.mutationIsolation?.required === true, 'stronger Tier 3 plan is queued as pending only');
|
|
147
|
+
check(queuedTier3.workflowPlan?.tier === 'tier2' && queuedTier3.strategy === 'iterative-single' &&
|
|
148
|
+
queuedTier3.mutationIsolation?.required === false, 'pending Tier 3 plan does not overwrite active Tier 2 fields');
|
|
149
|
+
check(!workflowRuntime.isMajorWorkflow(queuedTier3), 'major-workflow classification follows the authoritative active plan, not pending metadata');
|
|
150
|
+
write(spec, stages);
|
|
151
|
+
check(control('start').status === 0, 'explicit replan activates queued Tier 3 plan');
|
|
152
|
+
const promotedTier3 = read(file);
|
|
153
|
+
check(promotedTier3.workflowPlan?.tier === 'tier3' && promotedTier3.strategy.startsWith('fable-') &&
|
|
154
|
+
promotedTier3.mutationIsolation?.required === true && !promotedTier3.pendingPlan, 'pending Tier 3 isolation becomes active atomically on start');
|
|
155
|
+
check(workflowRuntime.isMajorWorkflow(promotedTier3), 'major-workflow classification flips only after explicit activation');
|
|
156
|
+
check(gate('Write', { file_path: path.join(repo, 'premature-primary.js') }, repo).status === 2, 'activated Tier 3 plan still blocks primary-tree mutation');
|
|
157
|
+
fs.unlinkSync(file);
|
|
158
|
+
check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single', 'fresh bounded fix returns to Tier 2 iterative lifecycle');
|
|
159
|
+
const iterationLimit = read(file).workflowPlan.limits.maxIterations;
|
|
160
|
+
check(Number.isInteger(iterationLimit) && iterationLimit > 0, 'iterative route exposes a numeric iteration budget');
|
|
161
|
+
for (let i = 0; i < iterationLimit; i++) {
|
|
162
|
+
check(gate('Write', { file_path: path.join(repo, `small-${i}.js`) }, repo).status === 0, `iterative mutation ${i + 1}/${iterationLimit} stays within budget`);
|
|
163
|
+
}
|
|
164
|
+
check(gate('Write', { file_path: path.join(repo, 'small-overflow.js') }, repo).status === 2, 'iteration beyond maxIterations is blocked');
|
|
165
|
+
const iterativeExhausted = read(file);
|
|
166
|
+
check(iterativeExhausted.state === 'blocked' && iterativeExhausted.budget?.state === 'budget-exhausted' &&
|
|
167
|
+
iterativeExhausted.budget?.reasonCode === 'iteration-budget-exhausted', 'iterative exhaustion records blocked budget state');
|
|
168
|
+
const iterativeResetCommand = `node "${dispositionPath}" reset-budget --session-id ${sessionId} --evidence fixture-iteration-budget-reset`;
|
|
169
|
+
check(gate('Bash', { command: iterativeResetCommand }, repo).status === 0, 'iteration-exhausted workflow admits reset-budget through gate');
|
|
170
|
+
check(control('reset-budget', '--evidence', 'fixture-iteration-budget-reset').status === 0, 'iteration budget reset executes');
|
|
171
|
+
check(read(file).state === 'pending' && read(file).budget?.epoch === 1 && read(file).budget?.counters?.iterations === 0, 'iteration budget reset is audited and clears counters');
|
|
172
|
+
const iterativeStartCommand = `node "${dispositionPath}" start --session-id ${sessionId}`;
|
|
173
|
+
check(gate('Bash', { command: iterativeStartCommand }, repo).status === 0, 'iterative workflow restart is admitted through gate');
|
|
174
|
+
check(control('start').status === 0 && read(file).state === 'running', 'iteration-exhausted workflow resumes after reset');
|
|
124
175
|
check(stop().status === 2, 'direct/iterative route cannot complete unverified mutation');
|
|
125
176
|
check(stop({ stop_hook_active: true }).status === 0 && read(file).state === 'blocked', 'iterative retry reports incomplete state');
|
|
126
177
|
check(control('start').status === 0, 'blocked iterative route can resume without a Fable stage map');
|
|
@@ -47,7 +47,6 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
47
47
|
context.workflow.state = 'blocked';
|
|
48
48
|
context.workflow.pendingPlan = plan;
|
|
49
49
|
context.workflow.blockReason = 'stronger-route-requires-replan';
|
|
50
|
-
context.workflow.mutationIsolation = plan.mutationIsolation;
|
|
51
50
|
}
|
|
52
51
|
context.workflow.lastPromptHash = crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24);
|
|
53
52
|
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
@@ -7,9 +7,18 @@ const { atomicWriteJson, readJson } = require('./fable-contracts');
|
|
|
7
7
|
|
|
8
8
|
const OPEN_STATES = new Set(['pending', 'active', 'running', 'failed', 'blocked', 'escaped']);
|
|
9
9
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
+
const WORKFLOW_CONTROLLER_COMMANDS = new Set(['start', 'revision', 'reset-budget', 'escape', 'block']);
|
|
11
|
+
|
|
12
|
+
function activeWorkflowPlan(workflow) {
|
|
13
|
+
if (!workflow) return null;
|
|
14
|
+
const plan = workflow.workflowPlan;
|
|
15
|
+
if (plan && typeof plan === 'object' && plan.strategy === workflow.strategy) return plan;
|
|
16
|
+
return workflow;
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
function isMajorWorkflow(workflow) {
|
|
12
|
-
|
|
20
|
+
const active = activeWorkflowPlan(workflow);
|
|
21
|
+
return Boolean(active && (active.mutationIsolation?.required || active.tier === 'tier3' || String(active.strategy || '').startsWith('fable-')));
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
function loadWorkflow(payload) {
|
|
@@ -251,4 +260,4 @@ function readHookInput(decide) {
|
|
|
251
260
|
process.stdin.on('error', finish);
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
module.exports = { OPEN_STATES, SAFE_ID, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
|
|
263
|
+
module.exports = { OPEN_STATES, SAFE_ID, WORKFLOW_CONTROLLER_COMMANDS, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
|
|
@@ -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, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, 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
|
|
|
@@ -20,6 +20,10 @@ function parseArgs(argv) {
|
|
|
20
20
|
return args;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function usage() {
|
|
24
|
+
return `Usage: workflow-disposition.js <${[...WORKFLOW_CONTROLLER_COMMANDS].join('|')}> --session-id <id> [--stage-id <id> --reason-code <reason> --scope <scope> --evidence <evidence>]`;
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function consumer() {
|
|
24
28
|
for (const relative of ['../../fable-mode/scripts/workflow-plan-consumer.js', '../../skills/fable-mode/scripts/workflow-plan-consumer.js']) {
|
|
25
29
|
const file = path.resolve(__dirname, relative);
|
|
@@ -30,6 +34,7 @@ function consumer() {
|
|
|
30
34
|
|
|
31
35
|
function main() {
|
|
32
36
|
const args = parseArgs(process.argv.slice(2));
|
|
37
|
+
if (!WORKFLOW_CONTROLLER_COMMANDS.has(args.command)) throw new Error(usage());
|
|
33
38
|
const sessionId = args.sessionId || readCurrentSession(getWorkspaceRoot());
|
|
34
39
|
if (!sessionId) throw new Error('no active Harness session; pass --session-id explicitly');
|
|
35
40
|
const context = loadWorkflow({ session_id: sessionId });
|
|
@@ -96,7 +101,7 @@ function main() {
|
|
|
96
101
|
if (!args.evidence?.trim()) throw new Error('--evidence is required');
|
|
97
102
|
workflow.state = 'blocked';
|
|
98
103
|
workflow.blockReason = args.evidence.trim();
|
|
99
|
-
} else throw new Error(
|
|
104
|
+
} else throw new Error(usage());
|
|
100
105
|
saveWorkflow(context);
|
|
101
106
|
syncBudgetToRun(context);
|
|
102
107
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, recordBudgetEvent, readHookInput } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, recordBudgetEvent, readHookInput, WORKFLOW_CONTROLLER_COMMANDS } = require('./lib/workflow-runtime');
|
|
7
7
|
const { key, classifyShell, cwdOf, commandOf, mutationPaths, linkedWorktree, assertTargets, assertShellScope } = require('./lib/workflow-isolation');
|
|
8
8
|
|
|
9
9
|
const DIRECT = new Set(['Edit', 'Write', 'apply_patch']);
|
|
@@ -12,8 +12,9 @@ const SHELL = new Set(['Bash', 'PowerShell', 'exec_command']);
|
|
|
12
12
|
function isController(command, cwd) {
|
|
13
13
|
// One trusted local executable, literal arguments, no shell evaluation.
|
|
14
14
|
if (/[;&|`\r\n<>$(){}]/.test(command)) return false;
|
|
15
|
-
const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+(
|
|
16
|
-
return Boolean(match &&
|
|
15
|
+
const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+([A-Za-z][A-Za-z0-9-]*)\b/);
|
|
16
|
+
return Boolean(match && WORKFLOW_CONTROLLER_COMMANDS.has(match[4]) &&
|
|
17
|
+
key(path.resolve(cwd, match[1] || match[2] || match[3])) === key(path.join(__dirname, 'workflow-disposition.js')));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function decide(payload) {
|
package/package.json
CHANGED
|
@@ -7,9 +7,18 @@ const { atomicWriteJson, readJson } = require('./fable-contracts');
|
|
|
7
7
|
|
|
8
8
|
const OPEN_STATES = new Set(['pending', 'active', 'running', 'failed', 'blocked', 'escaped']);
|
|
9
9
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
+
const WORKFLOW_CONTROLLER_COMMANDS = new Set(['start', 'revision', 'reset-budget', 'escape', 'block']);
|
|
11
|
+
|
|
12
|
+
function activeWorkflowPlan(workflow) {
|
|
13
|
+
if (!workflow) return null;
|
|
14
|
+
const plan = workflow.workflowPlan;
|
|
15
|
+
if (plan && typeof plan === 'object' && plan.strategy === workflow.strategy) return plan;
|
|
16
|
+
return workflow;
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
function isMajorWorkflow(workflow) {
|
|
12
|
-
|
|
20
|
+
const active = activeWorkflowPlan(workflow);
|
|
21
|
+
return Boolean(active && (active.mutationIsolation?.required || active.tier === 'tier3' || String(active.strategy || '').startsWith('fable-')));
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
function loadWorkflow(payload) {
|
|
@@ -251,4 +260,4 @@ function readHookInput(decide) {
|
|
|
251
260
|
process.stdin.on('error', finish);
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
module.exports = { OPEN_STATES, SAFE_ID, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
|
|
263
|
+
module.exports = { OPEN_STATES, SAFE_ID, WORKFLOW_CONTROLLER_COMMANDS, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
|
|
@@ -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, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, 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
|
|
|
@@ -20,6 +20,10 @@ function parseArgs(argv) {
|
|
|
20
20
|
return args;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function usage() {
|
|
24
|
+
return `Usage: workflow-disposition.js <${[...WORKFLOW_CONTROLLER_COMMANDS].join('|')}> --session-id <id> [--stage-id <id> --reason-code <reason> --scope <scope> --evidence <evidence>]`;
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function consumer() {
|
|
24
28
|
for (const relative of ['../../fable-mode/scripts/workflow-plan-consumer.js', '../../skills/fable-mode/scripts/workflow-plan-consumer.js']) {
|
|
25
29
|
const file = path.resolve(__dirname, relative);
|
|
@@ -30,6 +34,7 @@ function consumer() {
|
|
|
30
34
|
|
|
31
35
|
function main() {
|
|
32
36
|
const args = parseArgs(process.argv.slice(2));
|
|
37
|
+
if (!WORKFLOW_CONTROLLER_COMMANDS.has(args.command)) throw new Error(usage());
|
|
33
38
|
const sessionId = args.sessionId || readCurrentSession(getWorkspaceRoot());
|
|
34
39
|
if (!sessionId) throw new Error('no active Harness session; pass --session-id explicitly');
|
|
35
40
|
const context = loadWorkflow({ session_id: sessionId });
|
|
@@ -96,7 +101,7 @@ function main() {
|
|
|
96
101
|
if (!args.evidence?.trim()) throw new Error('--evidence is required');
|
|
97
102
|
workflow.state = 'blocked';
|
|
98
103
|
workflow.blockReason = args.evidence.trim();
|
|
99
|
-
} else throw new Error(
|
|
104
|
+
} else throw new Error(usage());
|
|
100
105
|
saveWorkflow(context);
|
|
101
106
|
syncBudgetToRun(context);
|
|
102
107
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, recordBudgetEvent, readHookInput } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, recordBudgetEvent, readHookInput, WORKFLOW_CONTROLLER_COMMANDS } = require('./lib/workflow-runtime');
|
|
7
7
|
const { key, classifyShell, cwdOf, commandOf, mutationPaths, linkedWorktree, assertTargets, assertShellScope } = require('./lib/workflow-isolation');
|
|
8
8
|
|
|
9
9
|
const DIRECT = new Set(['Edit', 'Write', 'apply_patch']);
|
|
@@ -12,8 +12,9 @@ const SHELL = new Set(['Bash', 'PowerShell', 'exec_command']);
|
|
|
12
12
|
function isController(command, cwd) {
|
|
13
13
|
// One trusted local executable, literal arguments, no shell evaluation.
|
|
14
14
|
if (/[;&|`\r\n<>$(){}]/.test(command)) return false;
|
|
15
|
-
const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+(
|
|
16
|
-
return Boolean(match &&
|
|
15
|
+
const match = command.match(/^node\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+([A-Za-z][A-Za-z0-9-]*)\b/);
|
|
16
|
+
return Boolean(match && WORKFLOW_CONTROLLER_COMMANDS.has(match[4]) &&
|
|
17
|
+
key(path.resolve(cwd, match[1] || match[2] || match[3])) === key(path.join(__dirname, 'workflow-disposition.js')));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function decide(payload) {
|
|
@@ -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.11.
|
|
4
|
+
"version": "0.11.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",
|
|
@@ -47,7 +47,6 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
47
47
|
context.workflow.state = 'blocked';
|
|
48
48
|
context.workflow.pendingPlan = plan;
|
|
49
49
|
context.workflow.blockReason = 'stronger-route-requires-replan';
|
|
50
|
-
context.workflow.mutationIsolation = plan.mutationIsolation;
|
|
51
50
|
}
|
|
52
51
|
context.workflow.lastPromptHash = crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24);
|
|
53
52
|
const memoryCapability = issueMemoryCapability(context.workflow);
|