harness-everything 0.16.6 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +11 -1
- package/ci/mechanism-2an-workflow-obligations.test.js +201 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +14 -3
- package/hooks/scripts/lib/workflow-obligations.js +316 -0
- package/hooks/scripts/lib/workflow-runtime.js +1 -1
- package/hooks/scripts/workflow-disposition.js +97 -5
- package/hooks/scripts/workflow-stop-gate.js +12 -4
- 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-obligations.js +316 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +1 -1
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +97 -5
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +12 -4
- 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 +14 -3
|
@@ -6,22 +6,66 @@ const path = require('path');
|
|
|
6
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
|
+
const {
|
|
10
|
+
planningUnresolved,
|
|
11
|
+
recordPlanning,
|
|
12
|
+
materializeExecutionObligations,
|
|
13
|
+
updateObligation,
|
|
14
|
+
} = require('./lib/workflow-obligations');
|
|
9
15
|
|
|
10
16
|
const ALLOWED_ESCAPE_REASONS = new Set(['workflow-uncovered-scope', 'host-capability-unavailable']);
|
|
11
17
|
|
|
12
18
|
function parseArgs(argv) {
|
|
13
19
|
const args = { command: argv[0] };
|
|
14
|
-
const flags = new Map([
|
|
20
|
+
const flags = new Map([
|
|
21
|
+
['--reason-code', 'reasonCode'], ['--scope', 'scope'], ['--evidence', 'evidence'],
|
|
22
|
+
['--session-id', 'sessionId'], ['--stage-id', 'stageId'],
|
|
23
|
+
['--obligation-id', 'obligationId'], ['--disposition', 'disposition'],
|
|
24
|
+
['--requirements-json', 'requirementsJson'], ['--strategy', 'strategy'],
|
|
25
|
+
]);
|
|
15
26
|
for (let i = 1; i < argv.length; i++) {
|
|
16
27
|
const key = flags.get(argv[i]);
|
|
17
|
-
if (!key ||
|
|
28
|
+
if (!key || argv[i + 1] === undefined || argv[i + 1].startsWith('--')) throw new Error('unknown or incomplete argument: ' + argv[i]);
|
|
18
29
|
args[key] = argv[++i];
|
|
19
30
|
}
|
|
20
31
|
return args;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
function usage() {
|
|
24
|
-
return `Usage: workflow-disposition.js <${[...WORKFLOW_CONTROLLER_COMMANDS].join('|')}> --session-id <id> [--
|
|
35
|
+
return `Usage: workflow-disposition.js <${[...WORKFLOW_CONTROLLER_COMMANDS].join('|')}> --session-id <id> [--requirements-json <json> --strategy <strategy> --obligation-id <id> --disposition <pass|escaped|blocked> --reason-code <reason> --scope <scope> --evidence <evidence>]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SELECTABLE_STRATEGIES = new Set(['direct-single', 'iterative-single', 'fable-staged', 'fable-parallel', 'fable-multi-agent-workspace']);
|
|
39
|
+
|
|
40
|
+
function routerContract() {
|
|
41
|
+
for (const relative of ['../../harness-everything/scripts/router-contract.js', '../../skills/harness-everything/scripts/router-contract.js']) {
|
|
42
|
+
const file = path.resolve(__dirname, relative);
|
|
43
|
+
if (fs.existsSync(file)) return require(file);
|
|
44
|
+
}
|
|
45
|
+
throw new Error('router contract unavailable');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function recomposePlan(workflow, requestedStrategy) {
|
|
49
|
+
const strategy = String(requestedStrategy || '').trim();
|
|
50
|
+
if (!SELECTABLE_STRATEGIES.has(strategy)) throw new Error('--strategy must name a supported execution topology');
|
|
51
|
+
if (strategy === workflow.workflowPlan.strategy) return workflow.workflowPlan;
|
|
52
|
+
if (!workflow.taskShape || typeof workflow.taskShape !== 'object') {
|
|
53
|
+
throw new Error('workflow task-shape context is unavailable; cannot safely recompose topology');
|
|
54
|
+
}
|
|
55
|
+
const taskShape = {
|
|
56
|
+
...workflow.taskShape,
|
|
57
|
+
explicitRequest: {
|
|
58
|
+
...(workflow.taskShape.explicitRequest || {}),
|
|
59
|
+
strategy,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return routerContract().buildWorkflowPlan({
|
|
63
|
+
routingStatus: workflow.workflowPlan.routingStatus,
|
|
64
|
+
tier: workflow.tier,
|
|
65
|
+
taskShape,
|
|
66
|
+
actionGateReasonCodes: workflow.workflowPlan.actionGate?.reasonCodes || [],
|
|
67
|
+
reasonCodes: ['post-decomposition-workflow-selection'],
|
|
68
|
+
});
|
|
25
69
|
}
|
|
26
70
|
|
|
27
71
|
function consumer() {
|
|
@@ -51,10 +95,31 @@ function main() {
|
|
|
51
95
|
const context = loadWorkflow({ session_id: sessionId });
|
|
52
96
|
const { workflow } = context;
|
|
53
97
|
if (!workflow || !OPEN_STATES.has(workflow.state)) throw new Error('no unresolved workflow contract exists for this session');
|
|
98
|
+
|
|
99
|
+
if (args.command === 'plan') {
|
|
100
|
+
if (!workflow.workflowId || !workflow.workflowPlan) throw new Error('legacy workflow lacks a correlated plan; cannot plan it implicitly');
|
|
101
|
+
if (!['pending', 'active'].includes(workflow.state)) throw new Error('requirements/workflow composition belongs before execution starts');
|
|
102
|
+
const selectedPlan = recomposePlan(workflow, args.strategy);
|
|
103
|
+
const result = recordPlanning(context, args.requirementsJson, selectedPlan, args.strategy, args.evidence);
|
|
104
|
+
saveWorkflow(context);
|
|
105
|
+
process.stdout.write(JSON.stringify({
|
|
106
|
+
state: workflow.state,
|
|
107
|
+
workflowId: workflow.workflowId,
|
|
108
|
+
requirements: result.set.requirements.map(item => ({ id: item.id, summary: item.summary })),
|
|
109
|
+
workflowSelection: result.set.workflowSelection,
|
|
110
|
+
selectedStrategy: result.selectedPlan.strategy,
|
|
111
|
+
blocked: result.blocked,
|
|
112
|
+
}) + '\n');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
if (args.command === 'start') {
|
|
55
117
|
if (!workflow.workflowId || !workflow.workflowPlan) throw new Error('legacy workflow lacks a correlated plan; cannot start it implicitly');
|
|
56
118
|
if (workflow.state === 'running') throw new Error('workflow already running; record a blocker before replanning');
|
|
57
119
|
const plan = workflow.pendingPlan || workflow.workflowPlan;
|
|
120
|
+
const unresolvedPlanning = planningUnresolved(context, plan);
|
|
121
|
+
if (unresolvedPlanning.length) throw new Error('resolve requirement decomposition/workflow selection first: ' + unresolvedPlanning.join(', '));
|
|
122
|
+
|
|
58
123
|
if (!String(plan.strategy || '').startsWith('fable-')) {
|
|
59
124
|
activatePlan(workflow, plan);
|
|
60
125
|
workflow.revision++;
|
|
@@ -62,10 +127,17 @@ function main() {
|
|
|
62
127
|
workflow.escapes = [];
|
|
63
128
|
delete workflow.pendingPlan;
|
|
64
129
|
delete workflow.blockReason;
|
|
130
|
+
const obligations = materializeExecutionObligations(context, plan);
|
|
65
131
|
saveWorkflow(context);
|
|
66
|
-
process.stdout.write(JSON.stringify({
|
|
132
|
+
process.stdout.write(JSON.stringify({
|
|
133
|
+
state: workflow.state,
|
|
134
|
+
workflowId: workflow.workflowId,
|
|
135
|
+
revision: workflow.revision,
|
|
136
|
+
obligations: obligations ? obligations.obligations.map(item => ({ id: item.id, status: item.status })) : [],
|
|
137
|
+
}) + '\n');
|
|
67
138
|
return;
|
|
68
139
|
}
|
|
140
|
+
|
|
69
141
|
const stages = readJson(path.join(context.sessionDir, 'workflow-stages.json'));
|
|
70
142
|
if (!Array.isArray(stages) || !stages.length) throw new Error('write a non-empty workflow-stages.json stage array at the session path first');
|
|
71
143
|
if (stages.some(stage => !stage.checkCommand || !stage.passCondition)) throw new Error('every required stage needs an objective checkCommand and passCondition');
|
|
@@ -84,6 +156,25 @@ function main() {
|
|
|
84
156
|
workflow.escapes = [];
|
|
85
157
|
delete workflow.pendingPlan;
|
|
86
158
|
delete workflow.blockReason;
|
|
159
|
+
} else if (args.command === 'obligation') {
|
|
160
|
+
if (String(workflow.strategy || '').startsWith('fable-')) throw new Error('Fable workflows use correlated stage contracts, not generic execution obligations');
|
|
161
|
+
if (workflow.state !== 'running') throw new Error('start the selected workflow before recording execution obligation dispositions');
|
|
162
|
+
const obligation = updateObligation(context, args.obligationId, args.disposition, {
|
|
163
|
+
evidence: args.evidence,
|
|
164
|
+
reasonCode: args.reasonCode,
|
|
165
|
+
scope: args.scope,
|
|
166
|
+
});
|
|
167
|
+
process.stdout.write(JSON.stringify({
|
|
168
|
+
state: workflow.state,
|
|
169
|
+
workflowId: workflow.workflowId,
|
|
170
|
+
obligation: {
|
|
171
|
+
id: obligation.id,
|
|
172
|
+
status: obligation.status,
|
|
173
|
+
reasonCode: obligation.reasonCode,
|
|
174
|
+
evidence: obligation.evidence,
|
|
175
|
+
},
|
|
176
|
+
}) + '\n');
|
|
177
|
+
return;
|
|
87
178
|
} else if (args.command === 'revision') {
|
|
88
179
|
console.error('[Workflow Reminder] Revision requested; no hard revision budget is enforced.');
|
|
89
180
|
} else if (args.command === 'escape') {
|
|
@@ -100,13 +191,14 @@ function main() {
|
|
|
100
191
|
};
|
|
101
192
|
workflow.escapes = [...(workflow.escapes || []).filter(item => item.stageId !== args.stageId), disposition];
|
|
102
193
|
atomicWriteJson(path.join(match.runRoot, 'escapes', args.stageId + '.json'), disposition);
|
|
103
|
-
// Scope-limited exception; never change the entire workflow to escaped.
|
|
104
194
|
} else if (args.command === 'block') {
|
|
105
195
|
if (!args.evidence?.trim()) throw new Error('--evidence is required');
|
|
106
196
|
workflow.state = 'blocked';
|
|
107
197
|
workflow.blockReason = args.evidence.trim();
|
|
108
198
|
} else throw new Error(usage());
|
|
199
|
+
|
|
109
200
|
saveWorkflow(context);
|
|
110
201
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
|
|
111
202
|
}
|
|
203
|
+
|
|
112
204
|
try { main(); } catch (error) { console.error('[Workflow Disposition] ' + error.message); process.exitCode = 2; }
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { readJson } = require('./lib/fable-contracts');
|
|
5
5
|
const { loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput } = require('./lib/workflow-runtime');
|
|
6
|
+
const { planningUnresolved, unresolvedObligations } = require('./lib/workflow-obligations');
|
|
7
|
+
|
|
6
8
|
function warn(message) { console.error('[Workflow Reminder] ' + message); }
|
|
9
|
+
|
|
7
10
|
function decide(payload) {
|
|
8
11
|
if (!payload) return;
|
|
9
12
|
try {
|
|
@@ -11,20 +14,24 @@ function decide(payload) {
|
|
|
11
14
|
const { workflow, sessionDir } = context;
|
|
12
15
|
if (!workflow || workflow.state === 'deferred') return;
|
|
13
16
|
const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
|
|
14
|
-
let unresolved =
|
|
17
|
+
let unresolved = planningUnresolved(context);
|
|
18
|
+
|
|
15
19
|
if (String(workflow.strategy || '').startsWith('fable-')) {
|
|
16
20
|
const match = matchingRun(context);
|
|
17
|
-
unresolved
|
|
21
|
+
unresolved.push(...(match ? unresolvedStages(match, workflow, handoff?.lastEditAt || 0) : ['correlated-run-missing']));
|
|
18
22
|
} else {
|
|
23
|
+
unresolved.push(...unresolvedObligations(context));
|
|
19
24
|
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
20
|
-
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved
|
|
25
|
+
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved.push('verification-after-edit-missing');
|
|
21
26
|
}
|
|
27
|
+
|
|
28
|
+
unresolved = [...new Set(unresolved)];
|
|
22
29
|
const wasBlocked = workflow.state === 'blocked';
|
|
23
30
|
if (wasBlocked) warn('Workflow is marked BLOCKED; report or revisit the recorded blocker, but Harness will not trap the session.');
|
|
24
31
|
if (unresolved.length) {
|
|
25
32
|
workflow.unresolved = unresolved;
|
|
26
33
|
saveWorkflow(context);
|
|
27
|
-
warn('Unresolved workflow evidence: ' + unresolved.join(', ') + '.
|
|
34
|
+
warn('Unresolved workflow evidence: ' + unresolved.join(', ') + '. Resolve/verify before claiming completion.');
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
if (wasBlocked) return;
|
|
@@ -34,4 +41,5 @@ function decide(payload) {
|
|
|
34
41
|
saveWorkflow(context);
|
|
35
42
|
} catch (error) { warn(error && error.message ? error.message : String(error)); }
|
|
36
43
|
}
|
|
44
|
+
|
|
37
45
|
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.
|
|
4
|
+
"version": "0.17.0",
|
|
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",
|
|
@@ -44,7 +44,8 @@ function isHostNotificationPrompt(prompt) {
|
|
|
44
44
|
return /^<task-notification[\s>][\s\S]*<\/task-notification>\s*$/i.test(text);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function persistWorkflow(
|
|
47
|
+
function persistWorkflow(contract, payload, prompt) {
|
|
48
|
+
const plan = contract && contract.workflowPlan;
|
|
48
49
|
if (!payload || !(payload.session_id || payload.sessionId)) return null;
|
|
49
50
|
const { runtime, hooksRoot } = loadRuntime();
|
|
50
51
|
const context = runtime.loadWorkflow(payload);
|
|
@@ -83,6 +84,7 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
83
84
|
verification: plan.verification,
|
|
84
85
|
mutationIsolation: plan.mutationIsolation || { required: false },
|
|
85
86
|
workflowPlan: plan,
|
|
87
|
+
taskShape: contract.taskShape || null,
|
|
86
88
|
state: selected ? 'pending' : 'deferred',
|
|
87
89
|
planningWarnings: plan.fallback?.disposition === 'none' ? [] : [...(plan.fallback?.reasonCodes || [])],
|
|
88
90
|
runId: null,
|
|
@@ -94,6 +96,10 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
94
96
|
hardBoundaries: ['rule-of-3-reflection', 'user-host-permission'],
|
|
95
97
|
},
|
|
96
98
|
};
|
|
99
|
+
if (selected && ['tier2', 'tier3'].includes(plan.tier)) {
|
|
100
|
+
const { initializePlanningContract } = require(path.join(hooksRoot, 'lib/workflow-obligations'));
|
|
101
|
+
initializePlanningContract(context, plan);
|
|
102
|
+
}
|
|
97
103
|
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
98
104
|
runtime.saveWorkflow(context);
|
|
99
105
|
return { ...context, hooksRoot, retained: false, memoryCapability };
|
|
@@ -115,7 +121,7 @@ function run(raw) {
|
|
|
115
121
|
let persisted = null;
|
|
116
122
|
let failure = null;
|
|
117
123
|
try {
|
|
118
|
-
persisted = persistWorkflow(
|
|
124
|
+
persisted = persistWorkflow(result?.contract || null, payload, prompt);
|
|
119
125
|
if (persisted?.retained && persisted.workflow) plan = persisted.workflow.workflowPlan;
|
|
120
126
|
} catch (error) { failure = error; }
|
|
121
127
|
if (hostNotification && !persisted?.workflow) {
|
|
@@ -146,7 +152,12 @@ function run(raw) {
|
|
|
146
152
|
console.log(' - Memory write disposition: ' + (persisted.workflow.workflowPlan.memory?.write || 'none'));
|
|
147
153
|
if (persisted.memoryCapability) console.log(' - Memory capability (single-use, workflow/session-bound): ' + persisted.memoryCapability);
|
|
148
154
|
console.log(' - Stage specification: ' + path.join(persisted.sessionDir, 'workflow-stages.json'));
|
|
149
|
-
|
|
155
|
+
if (plan.strategySelection === 'selected' && ['tier2', 'tier3'].includes(plan.tier)) {
|
|
156
|
+
console.log(' - Planning contract: ' + path.join(persisted.sessionDir, 'workflow-obligations.json'));
|
|
157
|
+
console.log(' - Requirements first: decompose intent into requirement fragments, then confirm the smallest sufficient workflow.');
|
|
158
|
+
console.log(' - Record plan: node "' + controller + '" plan --session-id "' + persisted.sessionId + '" --requirements-json "[{\\\"id\\\":\\\"req-1\\\",\\\"summary\\\":\\\"...\\\",\\\"acceptance\\\":\\\"...\\\"}]" --strategy "' + plan.strategy + '" --evidence "<why this workflow fits>"');
|
|
159
|
+
}
|
|
160
|
+
console.log(' - Enter selected workflow after planning: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
|
|
150
161
|
console.log(' - Escape one declared stage: node "' + controller + '" escape --session-id "' + persisted.sessionId + '" --stage-id "<id>" --reason-code workflow-uncovered-scope --scope "<uncovered scope>" --evidence "<evidence>"');
|
|
151
162
|
} else {
|
|
152
163
|
console.log(' - Runtime state was not persisted; reminders may be less precise, but execution remains available.');
|