harness-everything 0.8.1 → 0.9.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 +34 -0
- package/ci/action-gate.test.js +82 -2
- package/ci/phase4-ensemble-memory.test.js +128 -10
- package/ci/router-workflow-plan.test.js +23 -0
- package/ci/workflow-execution-contract.test.js +73 -0
- package/ci/workflow-plan-consumers.test.js +18 -0
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +9 -7
- 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 +10 -7
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/harness-everything/scripts/kernel-router-core.js +2 -0
- package/harness-everything/scripts/kernel-router.js +25 -2
- package/harness-everything/scripts/router-contract-core.js +25 -4
- package/harness-everything/scripts/tier-router.js +19 -1
- package/hooks/hooks.json +3 -3
- package/hooks/scripts/action-gate.js +111 -2
- package/hooks/scripts/contract-test.js +4 -1
- package/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/hooks/scripts/subagent-scope-guard.js +20 -3
- package/hooks/scripts/workflow-disposition.js +11 -4
- package/hooks/scripts/workflow-gate.js +4 -1
- package/multi-agent-workspace/SKILL.md +2 -2
- package/multi-agent-workspace/scripts/index_memory.js +107 -1
- 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/hooks.json +24 -0
- package/plugins/harness-everything/hooks/scripts/action-gate.js +111 -2
- package/plugins/harness-everything/hooks/scripts/contract-test.js +4 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +20 -3
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +11 -4
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +4 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +9 -7
- 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 +10 -7
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +2 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +25 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +25 -4
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +19 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +2 -2
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +107 -1
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +6 -6
- package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +22 -33
- package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +336 -57
- package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +1 -0
- package/self-evolve/SKILL.md +6 -6
- package/self-evolve/references/memory-resolution.md +22 -33
- package/self-evolve/scripts/persist-memory.js +336 -57
- package/self-evolve/scripts/self-regression.js +1 -0
|
@@ -34,6 +34,158 @@ function loadWorkflow(payload) {
|
|
|
34
34
|
|
|
35
35
|
function saveWorkflow(context) { atomicWriteJson(context.file, context.workflow); }
|
|
36
36
|
|
|
37
|
+
function budgetLimits(workflow) {
|
|
38
|
+
const limits = workflow?.workflowPlan?.limits || {};
|
|
39
|
+
return {
|
|
40
|
+
maxIterations: Number.isInteger(limits.maxIterations) && limits.maxIterations > 0 ? limits.maxIterations : null,
|
|
41
|
+
maxRevisionRounds: Number.isInteger(limits.maxRevisionRounds) && limits.maxRevisionRounds >= 0 ? limits.maxRevisionRounds : 0,
|
|
42
|
+
maxReplans: Number.isInteger(limits.maxReplans) && limits.maxReplans >= 0 ? limits.maxReplans : 0,
|
|
43
|
+
maxWorkers: Number.isInteger(limits.maxWorkers) && limits.maxWorkers > 0 ? limits.maxWorkers : 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureWorkflowBudget(workflow) {
|
|
48
|
+
if (workflow.budget && workflow.budget.schemaVersion === 1) return workflow.budget;
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
workflow.budget = {
|
|
51
|
+
schemaVersion: 1,
|
|
52
|
+
epoch: 0,
|
|
53
|
+
state: 'active',
|
|
54
|
+
limits: budgetLimits(workflow),
|
|
55
|
+
counters: { iterations: 0, revisionRounds: 0, replans: 0 },
|
|
56
|
+
activeWorkers: {},
|
|
57
|
+
reasonCode: null,
|
|
58
|
+
exhaustedAt: null,
|
|
59
|
+
resets: [],
|
|
60
|
+
events: [],
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
};
|
|
64
|
+
return workflow.budget;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function syncBudgetToRun(context) {
|
|
68
|
+
const match = matchingRun(context);
|
|
69
|
+
if (!match || !context.workflow?.budget) return;
|
|
70
|
+
const updated = {
|
|
71
|
+
...match.run,
|
|
72
|
+
budget: JSON.parse(JSON.stringify(context.workflow.budget)),
|
|
73
|
+
};
|
|
74
|
+
atomicWriteJson(path.join(match.runRoot, 'run.json'), updated);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function persistBudget(context) {
|
|
78
|
+
saveWorkflow(context);
|
|
79
|
+
syncBudgetToRun(context);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function budgetError(message) {
|
|
83
|
+
const error = new Error(message);
|
|
84
|
+
error.code = 'HARNESS_WORKFLOW_BUDGET';
|
|
85
|
+
return error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendBudgetEvent(budget, event) {
|
|
89
|
+
budget.events = [...(budget.events || []), event].slice(-64);
|
|
90
|
+
budget.updatedAt = event.observedAt;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function exhaustBudget(context, reasonCode, evidence) {
|
|
94
|
+
const budget = ensureWorkflowBudget(context.workflow);
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
budget.state = 'budget-exhausted';
|
|
97
|
+
budget.reasonCode = reasonCode;
|
|
98
|
+
budget.exhaustedAt = now;
|
|
99
|
+
appendBudgetEvent(budget, { type: 'budget-exhausted', reasonCode, evidence: evidence || null, observedAt: now });
|
|
100
|
+
context.workflow.state = 'blocked';
|
|
101
|
+
context.workflow.blockReason = reasonCode;
|
|
102
|
+
persistBudget(context);
|
|
103
|
+
throw budgetError(`budget-exhausted: ${reasonCode}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function recordBudgetEvent(context, type, options = {}) {
|
|
107
|
+
const { workflow } = context;
|
|
108
|
+
if (!workflow) throw budgetError('no workflow available for budget accounting');
|
|
109
|
+
const budget = ensureWorkflowBudget(workflow);
|
|
110
|
+
if (budget.state !== 'active') throw budgetError(`workflow budget is ${budget.state}: ${budget.reasonCode || 'unavailable'}`);
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
const evidence = options.evidence || null;
|
|
113
|
+
|
|
114
|
+
const consume = (counter, limitName, reasonCode) => {
|
|
115
|
+
const limit = budget.limits[limitName];
|
|
116
|
+
if (limit === null) return { applicable: false, value: budget.counters[counter] };
|
|
117
|
+
if (budget.counters[counter] >= limit) return exhaustBudget(context, reasonCode, evidence);
|
|
118
|
+
budget.counters[counter]++;
|
|
119
|
+
appendBudgetEvent(budget, { type, counter, value: budget.counters[counter], limit, evidence, observedAt: now });
|
|
120
|
+
persistBudget(context);
|
|
121
|
+
return { applicable: true, value: budget.counters[counter], limit };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (type === 'iteration') return consume('iterations', 'maxIterations', 'iteration-budget-exhausted');
|
|
125
|
+
if (type === 'revision') return consume('revisionRounds', 'maxRevisionRounds', 'revision-budget-exhausted');
|
|
126
|
+
if (type === 'replan') return consume('replans', 'maxReplans', 'replan-budget-exhausted');
|
|
127
|
+
|
|
128
|
+
if (type === 'worker-acquire') {
|
|
129
|
+
const workerId = String(options.workerId || '').trim();
|
|
130
|
+
if (!SAFE_ID.test(workerId)) throw budgetError('worker-acquire requires a stable safe worker id');
|
|
131
|
+
if (budget.activeWorkers[workerId]) return { active: Object.keys(budget.activeWorkers).length, idempotent: true };
|
|
132
|
+
const active = Object.keys(budget.activeWorkers).length;
|
|
133
|
+
if (active >= budget.limits.maxWorkers) return exhaustBudget(context, 'worker-budget-exhausted', evidence || workerId);
|
|
134
|
+
budget.activeWorkers[workerId] = { acquiredAt: now, evidence };
|
|
135
|
+
appendBudgetEvent(budget, { type, workerId, active: active + 1, limit: budget.limits.maxWorkers, evidence, observedAt: now });
|
|
136
|
+
persistBudget(context);
|
|
137
|
+
return { active: active + 1, limit: budget.limits.maxWorkers };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (type === 'worker-release') {
|
|
141
|
+
const workerId = String(options.workerId || '').trim();
|
|
142
|
+
if (!SAFE_ID.test(workerId)) throw budgetError('worker-release requires a stable safe worker id');
|
|
143
|
+
const existed = Boolean(budget.activeWorkers[workerId]);
|
|
144
|
+
delete budget.activeWorkers[workerId];
|
|
145
|
+
appendBudgetEvent(budget, { type, workerId, existed, active: Object.keys(budget.activeWorkers).length, evidence, observedAt: now });
|
|
146
|
+
persistBudget(context);
|
|
147
|
+
return { active: Object.keys(budget.activeWorkers).length, existed };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw budgetError(`unknown workflow budget event: ${type}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resetWorkflowBudget(context, evidence) {
|
|
154
|
+
const { workflow } = context;
|
|
155
|
+
if (!workflow) throw budgetError('no workflow available for budget reset');
|
|
156
|
+
const previous = ensureWorkflowBudget(workflow);
|
|
157
|
+
if (previous.state !== 'budget-exhausted' || workflow.state !== 'blocked') {
|
|
158
|
+
throw budgetError('budget reset is allowed only after explicit budget exhaustion');
|
|
159
|
+
}
|
|
160
|
+
if (!String(evidence || '').trim()) throw budgetError('budget reset requires audit evidence');
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
const resets = [...(previous.resets || []), {
|
|
163
|
+
epoch: previous.epoch,
|
|
164
|
+
reasonCode: previous.reasonCode,
|
|
165
|
+
counters: previous.counters,
|
|
166
|
+
evidence: String(evidence).trim(),
|
|
167
|
+
resetAt: now,
|
|
168
|
+
}].slice(-16);
|
|
169
|
+
workflow.budget = {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
epoch: previous.epoch + 1,
|
|
172
|
+
state: 'active',
|
|
173
|
+
limits: budgetLimits(workflow),
|
|
174
|
+
counters: { iterations: 0, revisionRounds: 0, replans: 0 },
|
|
175
|
+
activeWorkers: {},
|
|
176
|
+
reasonCode: null,
|
|
177
|
+
exhaustedAt: null,
|
|
178
|
+
resets,
|
|
179
|
+
events: [{ type: 'budget-reset', evidence: String(evidence).trim(), observedAt: now }],
|
|
180
|
+
createdAt: previous.createdAt || now,
|
|
181
|
+
updatedAt: now,
|
|
182
|
+
};
|
|
183
|
+
workflow.state = 'pending';
|
|
184
|
+
delete workflow.blockReason;
|
|
185
|
+
persistBudget(context);
|
|
186
|
+
return workflow.budget;
|
|
187
|
+
}
|
|
188
|
+
|
|
37
189
|
function matchingRun(context) {
|
|
38
190
|
const { workflow, root, sessionId } = context;
|
|
39
191
|
if (!workflow || !SAFE_ID.test(workflow.runId || '') || !workflow.workflowId || !sessionId) return null;
|
|
@@ -99,4 +251,4 @@ function readHookInput(decide) {
|
|
|
99
251
|
process.stdin.on('error', finish);
|
|
100
252
|
}
|
|
101
253
|
|
|
102
|
-
module.exports = { OPEN_STATES, SAFE_ID, isMajorWorkflow, loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput };
|
|
254
|
+
module.exports = { OPEN_STATES, SAFE_ID, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
|
|
@@ -30,6 +30,17 @@ const {
|
|
|
30
30
|
getWorkerId,
|
|
31
31
|
stagesForChangedPath,
|
|
32
32
|
} = require('./lib/fable-contracts');
|
|
33
|
+
const { loadWorkflow, recordBudgetEvent } = require('./lib/workflow-runtime');
|
|
34
|
+
|
|
35
|
+
function accountWorker(payload, event) {
|
|
36
|
+
const context = loadWorkflow(payload);
|
|
37
|
+
const workflow = context.workflow;
|
|
38
|
+
if (!workflow || workflow.state !== 'running' || !String(workflow.strategy || '').startsWith('fable-')) return;
|
|
39
|
+
const workerId = getWorkerId(payload);
|
|
40
|
+
if (!workerId) return;
|
|
41
|
+
const type = event === 'PreToolUse' || event === 'SubagentStart' ? 'worker-acquire' : 'worker-release';
|
|
42
|
+
recordBudgetEvent(context, type, { workerId, evidence: event });
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
function gitStatus(root) {
|
|
35
46
|
try {
|
|
@@ -112,6 +123,7 @@ process.stdin.on('end', () => {
|
|
|
112
123
|
const stateFile = path.join(sessionDir, 'subagent-scope-state.json');
|
|
113
124
|
|
|
114
125
|
if (event === 'PreToolUse' || event === 'SubagentStart') {
|
|
126
|
+
accountWorker(payload, event);
|
|
115
127
|
if (!fs.existsSync(stateFile)) {
|
|
116
128
|
const status = gitStatus(root);
|
|
117
129
|
if (status !== null) {
|
|
@@ -126,6 +138,7 @@ process.stdin.on('end', () => {
|
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
if (event === 'PostToolUse' || event === 'SubagentStop') {
|
|
141
|
+
accountWorker(payload, event);
|
|
129
142
|
if (!fs.existsSync(stateFile)) process.exit(0);
|
|
130
143
|
|
|
131
144
|
let state;
|
|
@@ -182,9 +195,13 @@ process.stdin.on('end', () => {
|
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
process.exit(0);
|
|
185
|
-
} catch (
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error && error.code === 'HARNESS_WORKFLOW_BUDGET') {
|
|
200
|
+
console.error(`[Subagent Scope Guard] ${error.message}`);
|
|
201
|
+
process.exit(2);
|
|
202
|
+
}
|
|
203
|
+
// This is otherwise a post-execution visibility gate. Runtime errors fail open;
|
|
204
|
+
// actionGate has a stricter fail-ask policy for side effects.
|
|
188
205
|
process.exit(0);
|
|
189
206
|
}
|
|
190
207
|
});
|
|
@@ -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 } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES, ensureWorkflowBudget, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun } = require('./lib/workflow-runtime');
|
|
7
7
|
const { getWorkspaceRoot, readCurrentSession } = require('./lib/harness-state');
|
|
8
8
|
const { atomicWriteJson, readJson } = require('./lib/fable-contracts');
|
|
9
9
|
|
|
@@ -38,13 +38,14 @@ function main() {
|
|
|
38
38
|
if (args.command === 'start') {
|
|
39
39
|
if (!workflow.workflowId || !workflow.workflowPlan) throw new Error('legacy workflow lacks a correlated plan; cannot start it implicitly');
|
|
40
40
|
if (workflow.state === 'running') throw new Error('workflow already running; record a blocker before replanning');
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
41
|
+
const isReplan = workflow.revision > 0 || Boolean(workflow.runId);
|
|
42
|
+
if (isReplan) recordBudgetEvent(context, 'replan', { evidence: 'workflow-disposition:start' });
|
|
43
43
|
const plan = workflow.pendingPlan || workflow.workflowPlan;
|
|
44
44
|
if (!String(plan.strategy || '').startsWith('fable-')) {
|
|
45
45
|
workflow.revision++;
|
|
46
46
|
workflow.state = 'running';
|
|
47
47
|
delete workflow.blockReason;
|
|
48
|
+
ensureWorkflowBudget(workflow);
|
|
48
49
|
saveWorkflow(context);
|
|
49
50
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, revision: workflow.revision }) + '\n');
|
|
50
51
|
return;
|
|
@@ -71,6 +72,11 @@ function main() {
|
|
|
71
72
|
workflow.escapes = [];
|
|
72
73
|
delete workflow.pendingPlan;
|
|
73
74
|
delete workflow.blockReason;
|
|
75
|
+
ensureWorkflowBudget(workflow);
|
|
76
|
+
} else if (args.command === 'revision') {
|
|
77
|
+
recordBudgetEvent(context, 'revision', { evidence: args.evidence || 'workflow-disposition:revision' });
|
|
78
|
+
} else if (args.command === 'reset-budget') {
|
|
79
|
+
resetWorkflowBudget(context, args.evidence);
|
|
74
80
|
} else if (args.command === 'escape') {
|
|
75
81
|
if (!ALLOWED_ESCAPE_REASONS.has(args.reasonCode)) throw new Error('generic simple/routine/already-clear reasons are intentionally rejected');
|
|
76
82
|
if (!args.scope?.trim()) throw new Error('--scope is required');
|
|
@@ -90,8 +96,9 @@ function main() {
|
|
|
90
96
|
if (!args.evidence?.trim()) throw new Error('--evidence is required');
|
|
91
97
|
workflow.state = 'blocked';
|
|
92
98
|
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>]');
|
|
99
|
+
} else throw new Error('Usage: workflow-disposition.js <start|revision|reset-budget|escape|block> --session-id <id> [--stage-id <id> --reason-code <reason> --scope <scope> --evidence <evidence>]');
|
|
94
100
|
saveWorkflow(context);
|
|
101
|
+
syncBudgetToRun(context);
|
|
95
102
|
process.stdout.write(JSON.stringify({ state: workflow.state, workflowId: workflow.workflowId, runId: workflow.runId, revision: workflow.revision, escapes: workflow.escapes }) + '\n');
|
|
96
103
|
}
|
|
97
104
|
try { main(); } catch (error) { console.error('[Workflow Disposition] ' + error.message); process.exitCode = 2; }
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, readHookInput } = require('./lib/workflow-runtime');
|
|
6
|
+
const { loadWorkflow, saveWorkflow, isMajorWorkflow, matchingRun, recordBudgetEvent, readHookInput } = 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']);
|
|
@@ -46,6 +46,9 @@ function decide(payload) {
|
|
|
46
46
|
if (String(workflow.strategy || '').startsWith('fable-') && !matchingRun(context)) {
|
|
47
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
48
|
}
|
|
49
|
+
if (workflow.strategy === 'iterative-single') {
|
|
50
|
+
recordBudgetEvent(context, 'iteration', { evidence: `${tool}:${commandClass}` });
|
|
51
|
+
}
|
|
49
52
|
workflow.lastMutationAt = Date.now();
|
|
50
53
|
saveWorkflow(context);
|
|
51
54
|
}
|
|
@@ -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.9.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",
|
|
@@ -53,9 +53,10 @@ overlap reject dispatch instead of becoming a verbal warning.
|
|
|
53
53
|
|
|
54
54
|
The stage map remains a living orchestration document only inside those
|
|
55
55
|
constraints. When new information invalidates it, re-plan and create revised
|
|
56
|
-
stage contracts before dispatching new work. Replan budget
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
stage contracts before dispatching new work. Replan budget comes from `workflowPlan.limits.maxReplans` and is accounted by
|
|
57
|
+
the runtime controller. Re-enter through `workflow-disposition.js start`; a
|
|
58
|
+
third full replan under the default budget is machine-blocked as
|
|
59
|
+
`replan-budget-exhausted` instead of being only an orchestration reminder. Scope rule:
|
|
59
60
|
deliver the task as specified; new scope discovered mid-run is surfaced as a
|
|
60
61
|
recommendation at delivery, not silently built.
|
|
61
62
|
|
|
@@ -75,10 +76,11 @@ spawn workers.
|
|
|
75
76
|
|
|
76
77
|
For `fable-parallel`, spawn only stages in the same validated execution batch;
|
|
77
78
|
never invent an additional parallel edge. A stage depending on another stage is
|
|
78
|
-
not in the same ready batch.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
not in the same ready batch. Never exceed `workflowPlan.limits.maxWorkers`. The router plan is the numeric
|
|
80
|
+
source of truth; `workflow-plan-consumer.js` chunks ready sets to that cap and
|
|
81
|
+
runtime worker leases enforce it when the host exposes stable worker IDs. For
|
|
82
|
+
other Fable strategies, serialize the returned batches unless a later validated
|
|
83
|
+
plan says otherwise.
|
|
82
84
|
|
|
83
85
|
**3. Verify with a check that can fail — external artifacts only.** Each stage
|
|
84
86
|
defines a pass condition an external artifact satisfies: a test that runs, a file
|
|
@@ -155,20 +155,23 @@ function validateParallelBatch(stages) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function dependencyBatches(stages, validateWrites) {
|
|
158
|
+
function dependencyBatches(stages, validateWrites, maxBatchSize = Infinity) {
|
|
159
159
|
const byId = new Map(stages.map(stage => [stage.stageId, stage]));
|
|
160
160
|
const remaining = new Set(byId.keys());
|
|
161
161
|
const completed = new Set();
|
|
162
162
|
const batches = [];
|
|
163
|
+
const boundedSize = Number.isInteger(maxBatchSize) && maxBatchSize > 0 ? maxBatchSize : Infinity;
|
|
163
164
|
|
|
164
165
|
while (remaining.size > 0) {
|
|
165
166
|
const ready = [...remaining]
|
|
166
167
|
.filter(stageId => byId.get(stageId).dependsOn.every(dep => completed.has(dep)))
|
|
167
168
|
.sort();
|
|
168
169
|
if (ready.length === 0) throw new Error('unresolved or cyclic dependency graph');
|
|
169
|
-
const
|
|
170
|
-
if (validateWrites) validateParallelBatch(
|
|
171
|
-
|
|
170
|
+
const readyStages = ready.map(stageId => byId.get(stageId));
|
|
171
|
+
if (validateWrites) validateParallelBatch(readyStages);
|
|
172
|
+
for (let offset = 0; offset < readyStages.length; offset += boundedSize) {
|
|
173
|
+
batches.push(readyStages.slice(offset, offset + boundedSize).map(stage => stage.stageId));
|
|
174
|
+
}
|
|
172
175
|
for (const stageId of ready) {
|
|
173
176
|
remaining.delete(stageId);
|
|
174
177
|
completed.add(stageId);
|
|
@@ -177,8 +180,8 @@ function dependencyBatches(stages, validateWrites) {
|
|
|
177
180
|
return batches;
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
function topologicalBatches(stages) {
|
|
181
|
-
return dependencyBatches(stages, true);
|
|
183
|
+
function topologicalBatches(stages, maxWorkers) {
|
|
184
|
+
return dependencyBatches(stages, true, maxWorkers);
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function sequentialBatches(stages) {
|
|
@@ -222,7 +225,7 @@ function prepareRun({ routerContract, stages, workspaceRoot, runId, sessionId =
|
|
|
222
225
|
if (fs.existsSync(runRoot)) throw new Error(`runId already exists in this workspace: ${resolvedRunId}`);
|
|
223
226
|
|
|
224
227
|
const batches = plan.strategy === 'fable-parallel'
|
|
225
|
-
? topologicalBatches(normalizedStages)
|
|
228
|
+
? topologicalBatches(normalizedStages, plan.limits && plan.limits.maxWorkers)
|
|
226
229
|
: sequentialBatches(normalizedStages);
|
|
227
230
|
|
|
228
231
|
const createdAt = new Date().toISOString();
|
package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json
CHANGED
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"crossDomain",
|
|
92
92
|
"highUncertainty",
|
|
93
93
|
"irreversibleAction",
|
|
94
|
-
"externalSideEffect"
|
|
94
|
+
"externalSideEffect",
|
|
95
|
+
"memoryPersistenceRequested"
|
|
95
96
|
],
|
|
96
97
|
"additionalProperties": false,
|
|
97
98
|
"properties": {
|
|
@@ -113,7 +114,8 @@
|
|
|
113
114
|
"crossDomain": { "type": "boolean" },
|
|
114
115
|
"highUncertainty": { "type": "boolean" },
|
|
115
116
|
"irreversibleAction": { "type": "boolean" },
|
|
116
|
-
"externalSideEffect": { "type": "boolean" }
|
|
117
|
+
"externalSideEffect": { "type": "boolean" },
|
|
118
|
+
"memoryPersistenceRequested": { "type": "boolean" }
|
|
117
119
|
}
|
|
118
120
|
},
|
|
119
121
|
"reasonCodes": {
|
|
@@ -47,12 +47,13 @@
|
|
|
47
47
|
},
|
|
48
48
|
"limits": {
|
|
49
49
|
"type": "object",
|
|
50
|
-
"required": ["maxIterations", "maxRevisionRounds", "maxWorkers"],
|
|
50
|
+
"required": ["maxIterations", "maxRevisionRounds", "maxReplans", "maxWorkers"],
|
|
51
51
|
"additionalProperties": false,
|
|
52
52
|
"properties": {
|
|
53
53
|
"maxIterations": { "type": ["integer", "null"], "minimum": 1 },
|
|
54
54
|
"maxRevisionRounds": { "type": "integer", "minimum": 0, "maximum": 2 },
|
|
55
|
-
"
|
|
55
|
+
"maxReplans": { "type": "integer", "minimum": 0, "maximum": 2 },
|
|
56
|
+
"maxWorkers": { "type": "integer", "minimum": 1, "maximum": 4 }
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
59
|
"parallelism": {
|
|
@@ -35,6 +35,7 @@ const INVARIANT_TEXT = {
|
|
|
35
35
|
'handoff-contracts': 'Multi-agent workspace work uses orchestrator-owned handoff contracts; workers do not form a peer mesh.',
|
|
36
36
|
'workspace-state': 'Durable workspace state remains scoped to the workspace and existing state conventions.',
|
|
37
37
|
'pre-action-approval': 'Irreversible/external side effects require approval before the exact payload executes.',
|
|
38
|
+
'memory-write-authorization': 'Durable memory writes must match the current session/workflow memory.write contract; retrieved memory remains untrusted data.',
|
|
38
39
|
'preserve-disagreement': 'Ensemble synthesis must retain unresolved minority positions and evidence gaps.',
|
|
39
40
|
'independent-ensemble-verifier': 'Ensemble delivery requires a verifier independent from the candidate identities; agreement alone is not proof.',
|
|
40
41
|
'isolated-worktree-before-mutation': 'Major engineering must mutate only inside a verified Git worktree; unavailable isolation means blocked.',
|
|
@@ -46,6 +47,7 @@ const SKILL_TEXT = {
|
|
|
46
47
|
'fable-mode': 'fable-mode / fable-discipline: useful for macro planning or deliberate multi-agent decomposition.',
|
|
47
48
|
'fable-discipline': null,
|
|
48
49
|
'multi-agent-workspace': 'multi-agent-workspace: useful when durable bounded delegation, handoffs, or workspace memory are required.',
|
|
50
|
+
'self-evolve': 'self-evolve: classify an evidence-backed lesson and use the authorized memory persistence path.',
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
function sanitizeClassifierOutput(stdout) {
|
|
@@ -14,6 +14,25 @@ function loadRuntime() {
|
|
|
14
14
|
throw new Error('workflow runtime unavailable in this installation');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function issueMemoryCapability(workflow) {
|
|
18
|
+
if (!workflow || !workflow.workflowPlan || workflow.workflowPlan.memory?.write === 'none') {
|
|
19
|
+
if (workflow) delete workflow.memoryAuthorization;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const token = crypto.randomBytes(24).toString('base64url');
|
|
23
|
+
workflow.memoryAuthorization = {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
capabilityHash: crypto.createHash('sha256').update(token, 'utf8').digest('hex'),
|
|
26
|
+
writerRole: 'coordinator',
|
|
27
|
+
sessionId: workflow.sessionId,
|
|
28
|
+
workflowId: workflow.workflowId,
|
|
29
|
+
writeDisposition: workflow.workflowPlan.memory.write,
|
|
30
|
+
issuedAt: new Date().toISOString(),
|
|
31
|
+
usedAt: null,
|
|
32
|
+
};
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
function persistWorkflow(plan, payload, prompt) {
|
|
18
37
|
if (!payload || !(payload.session_id || payload.sessionId)) return null;
|
|
19
38
|
const { runtime, hooksRoot } = loadRuntime();
|
|
@@ -31,8 +50,9 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
31
50
|
context.workflow.mutationIsolation = plan.mutationIsolation;
|
|
32
51
|
}
|
|
33
52
|
context.workflow.lastPromptHash = crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24);
|
|
53
|
+
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
34
54
|
runtime.saveWorkflow(context);
|
|
35
|
-
return { ...context, hooksRoot, retained: true };
|
|
55
|
+
return { ...context, hooksRoot, retained: true, memoryCapability };
|
|
36
56
|
}
|
|
37
57
|
const selected = plan.strategySelection === 'selected' && Boolean(plan.strategy);
|
|
38
58
|
const now = Date.now();
|
|
@@ -61,8 +81,9 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
61
81
|
reasoningPolicy: 'model-controls-how',
|
|
62
82
|
},
|
|
63
83
|
};
|
|
84
|
+
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
64
85
|
runtime.saveWorkflow(context);
|
|
65
|
-
return { ...context, hooksRoot, retained: false };
|
|
86
|
+
return { ...context, hooksRoot, retained: false, memoryCapability };
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function run(raw) {
|
|
@@ -99,6 +120,8 @@ function run(raw) {
|
|
|
99
120
|
if (persisted) {
|
|
100
121
|
const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
|
|
101
122
|
console.log(' - Workflow id: ' + persisted.workflow.workflowId);
|
|
123
|
+
console.log(' - Memory write disposition: ' + (persisted.workflow.workflowPlan.memory?.write || 'none'));
|
|
124
|
+
if (persisted.memoryCapability) console.log(' - Memory capability (single-use, workflow/session-bound): ' + persisted.memoryCapability);
|
|
102
125
|
console.log(' - Stage specification: ' + path.join(persisted.sessionDir, 'workflow-stages.json'));
|
|
103
126
|
console.log(' - Enter/replan: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
|
|
104
127
|
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>"');
|
package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js
CHANGED
|
@@ -5,6 +5,9 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const SCHEMA_VERSION = 1;
|
|
7
7
|
const ITERATIVE_MAX_ITERATIONS = 8;
|
|
8
|
+
const WORKFLOW_MAX_REVISION_ROUNDS = 2;
|
|
9
|
+
const FABLE_MAX_REPLANS = 2;
|
|
10
|
+
const FABLE_MAX_WORKERS = 4;
|
|
8
11
|
const TIERS = new Set(['tier1', 'tier2', 'tier3', 'unclassified']);
|
|
9
12
|
const ROUTING_STATUSES = new Set(['ok', 'degraded']);
|
|
10
13
|
const STRATEGIES = new Set([
|
|
@@ -154,6 +157,7 @@ function buildTaskShape(input = {}) {
|
|
|
154
157
|
highUncertainty: Boolean(signals.highUncertainty),
|
|
155
158
|
irreversibleAction: Boolean(signals.irreversibleAction),
|
|
156
159
|
externalSideEffect: Boolean(signals.externalSideEffect),
|
|
160
|
+
memoryPersistenceRequested: Boolean(signals.memoryPersistenceRequested),
|
|
157
161
|
},
|
|
158
162
|
reasonCodes: uniqueReasonCodes(input.reasonCodes),
|
|
159
163
|
};
|
|
@@ -418,6 +422,14 @@ function buildWorkflowPlan(input = {}) {
|
|
|
418
422
|
|
|
419
423
|
const requiredInvariants = [...meta.requiredInvariants];
|
|
420
424
|
const patterns = [...meta.patterns];
|
|
425
|
+
const suggestedSkills = [...meta.suggestedSkills];
|
|
426
|
+
const memoryPersistenceRequested = Boolean(taskShape.observedSignals && taskShape.observedSignals.memoryPersistenceRequested);
|
|
427
|
+
const memoryProhibited = Boolean(taskShape.explicitRequest && taskShape.explicitRequest.prohibitions && taskShape.explicitRequest.prohibitions.includes('memory'));
|
|
428
|
+
const memoryWrite = memoryProhibited ? 'none' : (memoryPersistenceRequested ? 'persist-via-self-evolve' : meta.memoryWrite);
|
|
429
|
+
if (memoryPersistenceRequested && !memoryProhibited) {
|
|
430
|
+
requiredInvariants.push('memory-write-authorization');
|
|
431
|
+
suggestedSkills.push('self-evolve');
|
|
432
|
+
}
|
|
421
433
|
if (actionGateRequired) {
|
|
422
434
|
requiredInvariants.push('pre-action-approval');
|
|
423
435
|
patterns.push('verifier-gated');
|
|
@@ -438,6 +450,8 @@ function buildWorkflowPlan(input = {}) {
|
|
|
438
450
|
...selection.reasonCodes,
|
|
439
451
|
...fallback.reasonCodes,
|
|
440
452
|
...actionGateReasonCodes,
|
|
453
|
+
...(memoryPersistenceRequested ? ['memory-persistence-requested'] : []),
|
|
454
|
+
...(memoryPersistenceRequested && memoryProhibited ? ['memory-persistence-prohibited'] : []),
|
|
441
455
|
]);
|
|
442
456
|
|
|
443
457
|
return {
|
|
@@ -448,7 +462,7 @@ function buildWorkflowPlan(input = {}) {
|
|
|
448
462
|
strategySelection: selection.strategySelection,
|
|
449
463
|
patterns: Array.from(new Set(patterns)),
|
|
450
464
|
requiredInvariants: Array.from(new Set(requiredInvariants)),
|
|
451
|
-
suggestedSkills: Array.from(new Set(
|
|
465
|
+
suggestedSkills: Array.from(new Set(suggestedSkills)),
|
|
452
466
|
actionGate: {
|
|
453
467
|
required: actionGateRequired,
|
|
454
468
|
reasonCodes: actionGateReasonCodes,
|
|
@@ -457,8 +471,9 @@ function buildWorkflowPlan(input = {}) {
|
|
|
457
471
|
},
|
|
458
472
|
limits: {
|
|
459
473
|
maxIterations: meta.maxIterations,
|
|
460
|
-
maxRevisionRounds:
|
|
461
|
-
|
|
474
|
+
maxRevisionRounds: WORKFLOW_MAX_REVISION_ROUNDS,
|
|
475
|
+
maxReplans: FABLE_MAX_REPLANS,
|
|
476
|
+
maxWorkers: FABLE_MAX_WORKERS,
|
|
462
477
|
},
|
|
463
478
|
parallelism: {
|
|
464
479
|
allowed: meta.parallelAllowed,
|
|
@@ -472,7 +487,7 @@ function buildWorkflowPlan(input = {}) {
|
|
|
472
487
|
memory: {
|
|
473
488
|
read: meta.memoryRead,
|
|
474
489
|
scope: 'task-relevant-only',
|
|
475
|
-
write:
|
|
490
|
+
write: memoryWrite,
|
|
476
491
|
},
|
|
477
492
|
verification: {
|
|
478
493
|
mode: meta.verificationMode,
|
|
@@ -553,6 +568,9 @@ function validateWorkflowPlan(plan) {
|
|
|
553
568
|
if (plan.actionGate && plan.actionGate.required && plan.actionGate.disposition !== 'pending-approval') errors.push('required actionGate must be pending-approval before execution');
|
|
554
569
|
if (!plan.limits || !Object.prototype.hasOwnProperty.call(plan.limits, 'maxIterations')) errors.push('limits.maxIterations is required');
|
|
555
570
|
if (plan.strategy === 'iterative-single' && (!Number.isInteger(plan.limits.maxIterations) || plan.limits.maxIterations < 1)) errors.push('iterative-single requires maxIterations');
|
|
571
|
+
if (!Number.isInteger(plan.limits?.maxRevisionRounds) || plan.limits.maxRevisionRounds < 0) errors.push('limits.maxRevisionRounds must be a non-negative integer');
|
|
572
|
+
if (!Number.isInteger(plan.limits?.maxReplans) || plan.limits.maxReplans < 0) errors.push('limits.maxReplans must be a non-negative integer');
|
|
573
|
+
if (!Number.isInteger(plan.limits?.maxWorkers) || plan.limits.maxWorkers < 1) errors.push('limits.maxWorkers must be a positive integer');
|
|
556
574
|
if (plan.strategy === 'fable-parallel' && (!plan.parallelism || plan.parallelism.allowed !== true)) errors.push('fable-parallel requires parallelism.allowed=true');
|
|
557
575
|
if (!plan.fallback || !['none', 'reduced', 'blocked'].includes(plan.fallback.disposition)) errors.push('fallback.disposition is invalid');
|
|
558
576
|
if (!Array.isArray(plan.reasonCodes)) errors.push('reasonCodes must be an array');
|
|
@@ -597,6 +615,9 @@ function writeRouterContract(contractPath, contract) {
|
|
|
597
615
|
module.exports = {
|
|
598
616
|
SCHEMA_VERSION,
|
|
599
617
|
ITERATIVE_MAX_ITERATIONS,
|
|
618
|
+
WORKFLOW_MAX_REVISION_ROUNDS,
|
|
619
|
+
FABLE_MAX_REPLANS,
|
|
620
|
+
FABLE_MAX_WORKERS,
|
|
600
621
|
buildRouterContract,
|
|
601
622
|
buildTaskShape,
|
|
602
623
|
buildWorkflowPlan,
|