harness-everything 0.16.6 → 0.18.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/AGENTS.md +2 -1
- package/CHANGELOG.md +34 -0
- package/README.md +1 -0
- package/behavioral-evals/cases/pressure-status-visibility.yaml +30 -0
- package/ci/invariant-routing.test.js +13 -0
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +11 -1
- package/ci/mechanism-2an-workflow-obligations.test.js +201 -0
- package/ci/router-workflow-plan.test.js +3 -0
- package/docs/architecture.md +18 -1
- package/docs/routing.md +21 -2
- package/docs/workflow-runtime.md +15 -0
- package/harness-everything/SKILL.md +4 -3
- package/harness-everything/references/triage-and-tiers.md +21 -4
- package/harness-everything/scripts/kernel-router-core.js +12 -0
- package/harness-everything/scripts/kernel-router.js +14 -3
- package/harness-everything/scripts/router-contract-core.js +1 -1
- 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/scripts/lib/advisory-text.js +12 -2
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +4 -3
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +21 -4
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +12 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +14 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
- package/scripts/lib/advisory-text.js +12 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { atomicWriteJson, readJson } = require('./fable-contracts');
|
|
5
|
+
|
|
6
|
+
const OBLIGATION_SCHEMA_VERSION = 1;
|
|
7
|
+
const OBLIGATION_FILE = 'workflow-obligations.json';
|
|
8
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9
|
+
const ALLOWED_ESCAPE_REASONS = new Set(['workflow-uncovered-scope', 'host-capability-unavailable']);
|
|
10
|
+
|
|
11
|
+
function obligationPath(sessionDir) {
|
|
12
|
+
return path.join(sessionDir, OBLIGATION_FILE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function needsPlanningContract(plan) {
|
|
16
|
+
return Boolean(
|
|
17
|
+
plan &&
|
|
18
|
+
plan.strategySelection === 'selected' &&
|
|
19
|
+
['tier2', 'tier3'].includes(plan.tier) &&
|
|
20
|
+
plan.strategy
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function needsGenericExecutionObligations(plan) {
|
|
25
|
+
return needsPlanningContract(plan) && !String(plan.strategy).startsWith('fable-');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function suggestedBindings(plan, kind) {
|
|
29
|
+
const skills = Array.isArray(plan?.suggestedSkills) ? plan.suggestedSkills : [];
|
|
30
|
+
if (kind === 'verification') return skills.filter(skill => skill === 'verification-loop');
|
|
31
|
+
if (kind === 'execution') return skills.filter(skill => skill !== 'verification-loop');
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function planningObligations() {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
id: 'decompose',
|
|
39
|
+
kind: 'planning',
|
|
40
|
+
requirement: 'Decompose the user intent into explicit requirement fragments with acceptance evidence.',
|
|
41
|
+
required: true,
|
|
42
|
+
escapable: false,
|
|
43
|
+
suggestedSkills: [],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'compose',
|
|
47
|
+
kind: 'planning',
|
|
48
|
+
requirement: 'Evaluate the requirement fragments and confirm the smallest sufficient workflow before execution.',
|
|
49
|
+
required: true,
|
|
50
|
+
escapable: false,
|
|
51
|
+
suggestedSkills: [],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function executionObligations(plan) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
id: 'execute',
|
|
60
|
+
kind: 'execution',
|
|
61
|
+
requirement: 'Resolve the planned requirement fragments using adaptive Skill/direct-action bindings.',
|
|
62
|
+
required: true,
|
|
63
|
+
escapable: true,
|
|
64
|
+
suggestedSkills: suggestedBindings(plan, 'execution'),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'verify',
|
|
68
|
+
kind: 'verification',
|
|
69
|
+
requirement: 'Produce objective verification evidence appropriate to the completed requirements before claiming completion.',
|
|
70
|
+
required: true,
|
|
71
|
+
escapable: false,
|
|
72
|
+
suggestedSkills: suggestedBindings(plan, 'verification'),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function newObligation(item) {
|
|
78
|
+
return {
|
|
79
|
+
...item,
|
|
80
|
+
status: 'pending',
|
|
81
|
+
evidence: null,
|
|
82
|
+
reasonCode: null,
|
|
83
|
+
scope: null,
|
|
84
|
+
updatedAt: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function initializePlanningContract(context, plan) {
|
|
89
|
+
if (!needsPlanningContract(plan)) return null;
|
|
90
|
+
const existing = readJson(obligationPath(context.sessionDir));
|
|
91
|
+
if (existing && validateObligationSet(context, existing, plan) === null) return existing;
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const set = {
|
|
94
|
+
schemaVersion: OBLIGATION_SCHEMA_VERSION,
|
|
95
|
+
workflowId: context.workflow.workflowId,
|
|
96
|
+
sessionId: context.sessionId,
|
|
97
|
+
tier: plan.tier,
|
|
98
|
+
candidateStrategy: plan.strategy,
|
|
99
|
+
revision: context.workflow.revision,
|
|
100
|
+
phase: 'planning',
|
|
101
|
+
createdAt: now,
|
|
102
|
+
requirements: [],
|
|
103
|
+
workflowSelection: {
|
|
104
|
+
candidateStrategy: plan.strategy,
|
|
105
|
+
confirmedStrategy: null,
|
|
106
|
+
evidence: null,
|
|
107
|
+
confirmedAt: null,
|
|
108
|
+
},
|
|
109
|
+
obligations: planningObligations().map(newObligation),
|
|
110
|
+
};
|
|
111
|
+
atomicWriteJson(obligationPath(context.sessionDir), set);
|
|
112
|
+
context.workflow.obligationContract = {
|
|
113
|
+
schemaVersion: OBLIGATION_SCHEMA_VERSION,
|
|
114
|
+
file: OBLIGATION_FILE,
|
|
115
|
+
revision: set.revision,
|
|
116
|
+
};
|
|
117
|
+
return set;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseRequirements(requirementsJson) {
|
|
121
|
+
let parsed;
|
|
122
|
+
try { parsed = JSON.parse(String(requirementsJson || '')); }
|
|
123
|
+
catch (_) { throw new Error('--requirements-json must be valid JSON'); }
|
|
124
|
+
if (!Array.isArray(parsed) || parsed.length < 1 || parsed.length > 32) {
|
|
125
|
+
throw new Error('--requirements-json must contain 1..32 requirement fragments');
|
|
126
|
+
}
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
return parsed.map((item, index) => {
|
|
129
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('requirement fragment must be an object');
|
|
130
|
+
const id = String(item.id || '').trim();
|
|
131
|
+
const summary = String(item.summary || '').trim();
|
|
132
|
+
const acceptance = String(item.acceptance || '').trim();
|
|
133
|
+
if (!SAFE_ID.test(id) || ids.has(id)) throw new Error('requirement ids must be unique stable ids');
|
|
134
|
+
if (!summary || !acceptance) throw new Error('each requirement needs summary and acceptance');
|
|
135
|
+
ids.add(id);
|
|
136
|
+
const suggestedSkills = Array.isArray(item.suggestedSkills)
|
|
137
|
+
? [...new Set(item.suggestedSkills.filter(value => typeof value === 'string' && value.trim()).map(value => value.trim()))]
|
|
138
|
+
: [];
|
|
139
|
+
return { id, order: index + 1, summary, acceptance, suggestedSkills };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function recordPlanning(context, requirementsJson, selectedPlan, requestedStrategy, evidence) {
|
|
144
|
+
if (!needsPlanningContract(selectedPlan)) throw new Error('selected workflow does not require a planning contract');
|
|
145
|
+
const loaded = loadObligationSet(context, selectedPlan);
|
|
146
|
+
if (!loaded.set) throw new Error('workflow planning contract unavailable or invalid: ' + loaded.error);
|
|
147
|
+
const requirements = parseRequirements(requirementsJson);
|
|
148
|
+
const requested = String(requestedStrategy || '').trim();
|
|
149
|
+
const planningEvidence = String(evidence || '').trim();
|
|
150
|
+
if (!requested) throw new Error('--strategy is required');
|
|
151
|
+
if (!planningEvidence) throw new Error('--evidence is required');
|
|
152
|
+
|
|
153
|
+
const now = new Date().toISOString();
|
|
154
|
+
loaded.set.requirements = requirements;
|
|
155
|
+
const decompose = loaded.set.obligations.find(item => item.id === 'decompose');
|
|
156
|
+
if (decompose) {
|
|
157
|
+
decompose.status = 'pass';
|
|
158
|
+
decompose.reasonCode = null;
|
|
159
|
+
decompose.evidence = 'requirements:' + requirements.map(item => item.id).join(',');
|
|
160
|
+
decompose.updatedAt = now;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const blocked = selectedPlan.fallback?.disposition === 'blocked';
|
|
164
|
+
loaded.set.workflowSelection = {
|
|
165
|
+
candidateStrategy: loaded.set.candidateStrategy,
|
|
166
|
+
requestedStrategy: requested,
|
|
167
|
+
confirmedStrategy: blocked ? null : selectedPlan.strategy,
|
|
168
|
+
evidence: planningEvidence,
|
|
169
|
+
confirmedAt: blocked ? null : now,
|
|
170
|
+
disposition: blocked ? 'blocked' : 'confirmed',
|
|
171
|
+
fallback: selectedPlan.fallback || null,
|
|
172
|
+
};
|
|
173
|
+
const compose = loaded.set.obligations.find(item => item.id === 'compose');
|
|
174
|
+
if (compose) {
|
|
175
|
+
compose.status = blocked ? 'blocked' : 'pass';
|
|
176
|
+
compose.reasonCode = blocked ? 'workflow-selection-blocked' : null;
|
|
177
|
+
compose.evidence = planningEvidence;
|
|
178
|
+
compose.updatedAt = now;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (blocked) {
|
|
182
|
+
context.workflow.planningWarning = 'workflow-selection-blocked';
|
|
183
|
+
delete context.workflow.pendingPlan;
|
|
184
|
+
} else {
|
|
185
|
+
loaded.set.phase = 'planned';
|
|
186
|
+
delete context.workflow.planningWarning;
|
|
187
|
+
if (selectedPlan.strategy !== context.workflow.workflowPlan.strategy ||
|
|
188
|
+
JSON.stringify(selectedPlan) !== JSON.stringify(context.workflow.workflowPlan)) {
|
|
189
|
+
context.workflow.pendingPlan = selectedPlan;
|
|
190
|
+
} else {
|
|
191
|
+
delete context.workflow.pendingPlan;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
atomicWriteJson(obligationPath(context.sessionDir), loaded.set);
|
|
196
|
+
return { set: loaded.set, blocked, selectedPlan };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function materializeExecutionObligations(context, plan) {
|
|
200
|
+
if (!needsGenericExecutionObligations(plan)) return null;
|
|
201
|
+
const loaded = loadObligationSet(context, plan);
|
|
202
|
+
if (!loaded.set) throw new Error('workflow planning contract unavailable or invalid: ' + loaded.error);
|
|
203
|
+
if (planningUnresolved(context, plan).length) throw new Error('requirements/workflow planning must be resolved before start');
|
|
204
|
+
const existing = new Map(loaded.set.obligations.map(item => [item.id, item]));
|
|
205
|
+
for (const item of executionObligations(plan)) {
|
|
206
|
+
if (!existing.has(item.id)) loaded.set.obligations.push(newObligation(item));
|
|
207
|
+
}
|
|
208
|
+
loaded.set.phase = 'execution';
|
|
209
|
+
loaded.set.activeStrategy = plan.strategy;
|
|
210
|
+
loaded.set.revision = context.workflow.revision;
|
|
211
|
+
atomicWriteJson(obligationPath(context.sessionDir), loaded.set);
|
|
212
|
+
return loaded.set;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateObligationSet(context, set, plan = context.workflow?.pendingPlan || context.workflow?.workflowPlan) {
|
|
216
|
+
if (!set || set.schemaVersion !== OBLIGATION_SCHEMA_VERSION) return 'schema-invalid';
|
|
217
|
+
if (set.workflowId !== context.workflow?.workflowId || set.sessionId !== context.sessionId) return 'correlation-invalid';
|
|
218
|
+
if (!plan || set.tier !== plan.tier) return 'plan-mismatch';
|
|
219
|
+
if (!Array.isArray(set.requirements) || !Array.isArray(set.obligations) || set.obligations.length === 0) return 'obligations-invalid';
|
|
220
|
+
const ids = new Set();
|
|
221
|
+
for (const obligation of set.obligations) {
|
|
222
|
+
if (!obligation || !SAFE_ID.test(obligation.id || '') || ids.has(obligation.id)) return 'obligation-id-invalid';
|
|
223
|
+
ids.add(obligation.id);
|
|
224
|
+
if (!['pending', 'pass', 'escaped', 'blocked'].includes(obligation.status)) return 'obligation-status-invalid';
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function loadObligationSet(context, plan) {
|
|
230
|
+
const set = readJson(obligationPath(context.sessionDir));
|
|
231
|
+
const error = validateObligationSet(context, set, plan);
|
|
232
|
+
return { set: error ? null : set, error };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function planningUnresolved(context, plan = context.workflow?.pendingPlan || context.workflow?.workflowPlan) {
|
|
236
|
+
if (!needsPlanningContract(plan)) return [];
|
|
237
|
+
const loaded = loadObligationSet(context, plan);
|
|
238
|
+
if (!loaded.set) return ['planning:' + (loaded.error || 'missing')];
|
|
239
|
+
const unresolved = [];
|
|
240
|
+
for (const id of ['decompose', 'compose']) {
|
|
241
|
+
const obligation = loaded.set.obligations.find(item => item.id === id);
|
|
242
|
+
if (!obligation) unresolved.push(id + ':missing');
|
|
243
|
+
else if (obligation.status !== 'pass' || !obligation.evidence) unresolved.push(id + ':' + obligation.status);
|
|
244
|
+
}
|
|
245
|
+
if (!loaded.set.requirements.length) unresolved.push('requirements:empty');
|
|
246
|
+
if (loaded.set.workflowSelection?.confirmedStrategy !== plan.strategy ||
|
|
247
|
+
loaded.set.workflowSelection?.disposition !== 'confirmed') {
|
|
248
|
+
unresolved.push('workflow-selection:unconfirmed');
|
|
249
|
+
}
|
|
250
|
+
return [...new Set(unresolved)];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function updateObligation(context, obligationId, disposition, options = {}) {
|
|
254
|
+
if (!SAFE_ID.test(obligationId || '')) throw new Error('--obligation-id must be a stable obligation id');
|
|
255
|
+
if (!['pass', 'escaped', 'blocked'].includes(disposition)) throw new Error('--disposition must be pass, escaped, or blocked');
|
|
256
|
+
if (['decompose', 'compose'].includes(obligationId)) throw new Error('planning obligations are resolved through the plan command');
|
|
257
|
+
const plan = context.workflow?.pendingPlan || context.workflow?.workflowPlan;
|
|
258
|
+
const loaded = loadObligationSet(context, plan);
|
|
259
|
+
if (!loaded.set) throw new Error('workflow obligation contract unavailable or invalid: ' + loaded.error);
|
|
260
|
+
const obligation = loaded.set.obligations.find(item => item.id === obligationId);
|
|
261
|
+
if (!obligation) throw new Error('unknown workflow obligation: ' + obligationId);
|
|
262
|
+
const evidence = String(options.evidence || '').trim();
|
|
263
|
+
if (!evidence) throw new Error('--evidence is required for every obligation disposition');
|
|
264
|
+
const reasonCode = String(options.reasonCode || '').trim() || null;
|
|
265
|
+
const scope = String(options.scope || '').trim() || null;
|
|
266
|
+
if (disposition === 'escaped') {
|
|
267
|
+
if (!obligation.escapable) throw new Error('this obligation cannot be escaped');
|
|
268
|
+
if (!ALLOWED_ESCAPE_REASONS.has(reasonCode)) throw new Error('escaped obligations require workflow-uncovered-scope or host-capability-unavailable');
|
|
269
|
+
if (!scope) throw new Error('--scope is required when escaping an obligation');
|
|
270
|
+
}
|
|
271
|
+
if (disposition === 'blocked' && !reasonCode) throw new Error('--reason-code is required when blocking an obligation');
|
|
272
|
+
obligation.status = disposition;
|
|
273
|
+
obligation.evidence = evidence;
|
|
274
|
+
obligation.reasonCode = reasonCode;
|
|
275
|
+
obligation.scope = scope;
|
|
276
|
+
obligation.updatedAt = new Date().toISOString();
|
|
277
|
+
atomicWriteJson(obligationPath(context.sessionDir), loaded.set);
|
|
278
|
+
return obligation;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function unresolvedObligations(context) {
|
|
282
|
+
const plan = context.workflow?.pendingPlan || context.workflow?.workflowPlan;
|
|
283
|
+
if (!needsPlanningContract(plan)) return [];
|
|
284
|
+
const unresolved = planningUnresolved(context, plan);
|
|
285
|
+
const loaded = loadObligationSet(context, plan);
|
|
286
|
+
if (!loaded.set) return unresolved;
|
|
287
|
+
if (!['running', 'satisfied', 'blocked', 'failed'].includes(context.workflow.state)) {
|
|
288
|
+
unresolved.push('execution:not-started');
|
|
289
|
+
}
|
|
290
|
+
if (needsGenericExecutionObligations(plan)) {
|
|
291
|
+
for (const obligation of loaded.set.obligations.filter(item => ['execute', 'verify'].includes(item.id))) {
|
|
292
|
+
if (obligation.status === 'pending') unresolved.push(obligation.id + ':pending');
|
|
293
|
+
else if (obligation.status === 'blocked') unresolved.push(obligation.id + ':blocked');
|
|
294
|
+
else if (!obligation.evidence) unresolved.push(obligation.id + ':evidence-missing');
|
|
295
|
+
else if (obligation.status === 'escaped' && (!obligation.escapable || !ALLOWED_ESCAPE_REASONS.has(obligation.reasonCode) || !obligation.scope)) {
|
|
296
|
+
unresolved.push(obligation.id + ':escape-invalid');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return [...new Set(unresolved)];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
OBLIGATION_FILE,
|
|
305
|
+
ALLOWED_ESCAPE_REASONS,
|
|
306
|
+
obligationPath,
|
|
307
|
+
needsPlanningContract,
|
|
308
|
+
needsGenericExecutionObligations,
|
|
309
|
+
initializePlanningContract,
|
|
310
|
+
recordPlanning,
|
|
311
|
+
materializeExecutionObligations,
|
|
312
|
+
loadObligationSet,
|
|
313
|
+
planningUnresolved,
|
|
314
|
+
updateObligation,
|
|
315
|
+
unresolvedObligations,
|
|
316
|
+
};
|
|
@@ -7,7 +7,7 @@ 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', 'escape', 'block']);
|
|
10
|
+
const WORKFLOW_CONTROLLER_COMMANDS = new Set(['plan', 'start', 'revision', 'obligation', 'escape', 'block']);
|
|
11
11
|
|
|
12
12
|
function activeWorkflowPlan(workflow) {
|
|
13
13
|
if (!workflow) return null;
|
|
@@ -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.18.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",
|
|
@@ -15,7 +15,16 @@ const advisoryInstructions = [
|
|
|
15
15
|
`2. For EVERY suggested skill, read its complete \`SKILL.md\` entry and evaluate applicability from \`USE FOR\`, \`DO NOT USE FOR\`, and its basic flow.`,
|
|
16
16
|
`3. Treat the selected workflow topology and numeric limits as planning guidance, not a lock. Choose, combine, reorder, or skip steps when evidence supports it.`,
|
|
17
17
|
`4. Prefer objective verification before declaring done and worktree isolation for broad/Tier-3 mutation.`,
|
|
18
|
-
`5.
|
|
18
|
+
`5. Use the routing checkpoint as source state for the first Harness Status; do not expose a competing progress format.`,
|
|
19
|
+
``,
|
|
20
|
+
`## 📣 USER-VISIBLE STATUS (MUST)`,
|
|
21
|
+
`For non-trivial software/project work, this semantic communication contract is mandatory even when the host integration itself is instruction-only:`,
|
|
22
|
+
`- \`Harness Status\``,
|
|
23
|
+
`- \`Current:\` what is being done now`,
|
|
24
|
+
`- \`Read/Evidence:\` important files, sources, or evidence read/confirmed`,
|
|
25
|
+
`- \`Next:\` the next intended action`,
|
|
26
|
+
`- \`Blocked/Risk:\` only when materially applicable`,
|
|
27
|
+
`Emit it before substantive execution, after a major phase, when direction materially changes, at meaningful long-running phase boundaries, and before final completion. This MUST does not imply a hard runtime lock.`,
|
|
19
28
|
``,
|
|
20
29
|
`## 🤖 COGNITIVE COMPLIANCE`,
|
|
21
30
|
`- **No universal skill pipeline**: do not force every task through TODO/TDD/Fable.`,
|
|
@@ -135,7 +144,8 @@ function buildCodexGlobalContent() {
|
|
|
135
144
|
`This instruction surface cannot be assumed to provide hard lifecycle blocking.`,
|
|
136
145
|
``,
|
|
137
146
|
`- Discover the environment and host capabilities before relying on them.`,
|
|
138
|
-
`- For software/project work, run \`npx github:dyphn1/Harness-everything next "<summary>"\` before mutation and
|
|
147
|
+
`- For software/project work, run \`npx github:dyphn1/Harness-everything next "<summary>"\` before mutation and use the routing checkpoint as source state for the first Harness Status.`,
|
|
148
|
+
`- For non-trivial work, MUST keep the user informed with one Harness Status shape: Current, Read/Evidence, Next, plus Blocked/Risk only when material; repeat it at major phase/direction boundaries and before final completion.`,
|
|
139
149
|
`- Read every suggested skill's complete \`SKILL.md\` entry before resolving applicability; do not omit it from metadata or confidence alone.`,
|
|
140
150
|
`- Use the selected workflow topology as planning guidance; the model may adapt or skip steps with an explicit evidence-based reason.`,
|
|
141
151
|
`- Do not impose a universal TODO/TDD/Fable sequence; use the smallest useful topology plus applicable skills.`,
|
|
@@ -4,7 +4,7 @@ description: "Route software/project work through the Harness kernel: classify T
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.18.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Harness Everything
|
|
@@ -23,11 +23,12 @@ Kernel routing entry.
|
|
|
23
23
|
2. **Verify before claim** — seek objective evidence before saying work is done.
|
|
24
24
|
3. **Zoom out on repetition** — after 3 same-signature failures, use `zoom-out`.
|
|
25
25
|
4. **Evaluate before omission** — read each suggested skill's `SKILL.md` before deciding applicability.
|
|
26
|
-
5. **
|
|
26
|
+
5. **Surface status** — non-trivial work MUST use one `Harness Status`: `Current`, `Read/Evidence`, `Next`, optional `Blocked/Risk`, at major phase/direction boundaries.
|
|
27
|
+
6. **Keep agency** — workflow state and numeric limits guide; they do not hard-stop execution.
|
|
27
28
|
|
|
28
29
|
## Workflow
|
|
29
30
|
1. Reuse kernel output, or run `node "<this-skill-dir>/scripts/kernel-router.js" "<prompt summary>"`.
|
|
30
|
-
2.
|
|
31
|
+
2. Use the **Harness Routing Checkpoint** to populate the first Harness Status; it is not a second progress format.
|
|
31
32
|
3. Read each suggested `SKILL.md`; evaluate `USE FOR`, `DO NOT USE FOR`, workflow/basic flow, and hard rules.
|
|
32
33
|
4. Use the selected topology as planning guidance. Choose, combine, reorder, or skip steps when evidence supports doing so.
|
|
33
34
|
5. Prefer worktree isolation for broad/Tier-3 mutation and objective verification before completion.
|
|
@@ -44,6 +44,7 @@ The kernel protects lifecycle obligations without micromanaging model reasoning.
|
|
|
44
44
|
3. **Re-plan on repetition** — after three same-signature failures, stop micro-retrying and use a fresh diagnosis / `zoom-out`.
|
|
45
45
|
4. **Evaluate before omission** — when the router suggests a skill, read its complete `SKILL.md` entry/basic flow before deciding applicability.
|
|
46
46
|
5. **Review selected workflow** — use the topology as planning guidance and keep any skipped step evidence-based.
|
|
47
|
+
6. **Surface status** — non-trivial software/project work MUST use the single Harness Status format at required phase boundaries.
|
|
47
48
|
|
|
48
49
|
The model remains free to choose tools, implementation technique, decomposition details, and reasoning inside the workflow.
|
|
49
50
|
|
|
@@ -148,9 +149,25 @@ On Claude and the local OpenAI package, supported hooks observe workflow/worktre
|
|
|
148
149
|
|
|
149
150
|
Never turn mechanism/package evidence into a cross-host live-enforcement claim. #82 owns host-specific retained evidence.
|
|
150
151
|
|
|
151
|
-
## 6.
|
|
152
|
+
## 6. User-visible status contract
|
|
152
153
|
|
|
153
|
-
For software/project work,
|
|
154
|
+
For non-trivial software/project work, user-facing progress **MUST** use one stable shape across direct, iterative, Fable, and multi-agent execution:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
Harness Status
|
|
158
|
+
- Current: <what is being done now>
|
|
159
|
+
- Read/Evidence: <important files/sources/evidence read or confirmed>
|
|
160
|
+
- Next: <next intended action>
|
|
161
|
+
- Blocked/Risk: <only when materially applicable>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Emit it before substantive execution, after a major phase completes, when the plan/assumption/direction materially changes, at meaningful phase boundaries during long-running work, and before final completion. The final response may merge the last status naturally.
|
|
165
|
+
|
|
166
|
+
This is a semantic communication MUST. It does **not** create a runtime lock, counter, reset requirement, or permission boundary. The routing checkpoint below is source state for this format, not a second competing progress template.
|
|
167
|
+
|
|
168
|
+
## 7. Routing Checkpoint
|
|
169
|
+
|
|
170
|
+
For software/project work, consume this runtime state when producing the first Harness Status:
|
|
154
171
|
|
|
155
172
|
```markdown
|
|
156
173
|
## 🚦 Harness Routing Checkpoint
|
|
@@ -163,9 +180,9 @@ For software/project work, surface this state before or with the first progress
|
|
|
163
180
|
- Escape: <none | reason + uncovered scope + evidence>
|
|
164
181
|
```
|
|
165
182
|
|
|
166
|
-
The checkpoint is state, not a
|
|
183
|
+
The checkpoint is runtime/source state, not a competing user-facing template. Render user progress through the single Harness Status contract above. The key distinction is: **skill suggestions are evaluated for applicability; a selected topology guides execution but does not trap it.**
|
|
167
184
|
|
|
168
|
-
##
|
|
185
|
+
## 8. Self-Healing and Dynamic Skills
|
|
169
186
|
|
|
170
187
|
Self-heal, generated-skill discovery, knowledge-guide matching, and fact-audit behavior remain part of the runtime. Dynamic recommendations still require applicability evaluation.
|
|
171
188
|
|