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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.17.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.17.0](https://github.com/dyphn1/Harness-everything/compare/v0.16.6...v0.17.0) (2026-09-21)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **router:** expose obligation resolution contract ([767f8ef](https://github.com/dyphn1/Harness-everything/commit/767f8ef20b97fe8da2dcbe11a49f00fd03d0976b))
|
|
10
|
+
* **router:** retain task shape for post-decomposition composition ([cff0e48](https://github.com/dyphn1/Harness-everything/commit/cff0e489cdb5cd8a80b48370ab9905e508eac4e7))
|
|
11
|
+
* **router:** surface requirements-first planning ([3ad82ae](https://github.com/dyphn1/Harness-everything/commit/3ad82aedbae7943b66cf686b05a685d247f8c0b0))
|
|
12
|
+
* **workflow:** add non-Fable obligation contract ([d31bb0e](https://github.com/dyphn1/Harness-everything/commit/d31bb0e3b997a597692ae0af8741d5963bddc3aa))
|
|
13
|
+
* **workflow:** add planning transition ([5f5855b](https://github.com/dyphn1/Harness-everything/commit/5f5855bb2219efcc6a0964319324e7d8215a775d))
|
|
14
|
+
* **workflow:** confirm recomposed post-decomposition plan ([a5b2698](https://github.com/dyphn1/Harness-everything/commit/a5b26989da664c8f9e4c276210ba23fc67e2aeda))
|
|
15
|
+
* **workflow:** decompose intent before start ([d1aba6d](https://github.com/dyphn1/Harness-everything/commit/d1aba6d01a2e3e260fc79b996b7e754fa3cd53e8))
|
|
16
|
+
* **workflow:** expose obligation controller command ([4235144](https://github.com/dyphn1/Harness-everything/commit/42351443f315c130b302e120f382929f84cf5896))
|
|
17
|
+
* **workflow:** keep unresolved obligations visible ([926dc2b](https://github.com/dyphn1/Harness-everything/commit/926dc2bb900b5891a3f217989216eeb5fc1917fd))
|
|
18
|
+
* **workflow:** materialize and resolve obligations ([3cc25ad](https://github.com/dyphn1/Harness-everything/commit/3cc25ad18fa6325a6fd93b69749a7dadb605e099))
|
|
19
|
+
* **workflow:** recompose topology after requirement decomposition ([bbb3ed7](https://github.com/dyphn1/Harness-everything/commit/bbb3ed7ca6bf9cfd24b8089428db0eb6b6cd52c0))
|
|
20
|
+
* **workflow:** report unresolved planning obligations ([0c088d7](https://github.com/dyphn1/Harness-everything/commit/0c088d7cc7bf2c75631bf9b9b56dcd8b5851600a))
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **workflow:** keep composition pre-execution ([a22e1de](https://github.com/dyphn1/Harness-everything/commit/a22e1de876bcc6a1bc36a6c679c2dcbcac1ce672))
|
|
25
|
+
* **workflow:** keep recomposed pending execution unresolved ([f29672a](https://github.com/dyphn1/Harness-everything/commit/f29672a55d2b764a8e0dd909cd700af851bda415))
|
|
26
|
+
* **workflow:** preserve decomposition before replan ([8400383](https://github.com/dyphn1/Harness-everything/commit/840038349cb11636152f68be560961f75a9859f2))
|
|
27
|
+
|
|
5
28
|
## [0.16.6](https://github.com/dyphn1/Harness-everything/compare/v0.16.5...v0.16.6) (2026-09-21)
|
|
6
29
|
|
|
7
30
|
### Bug Fixes
|
|
@@ -188,8 +188,18 @@ try {
|
|
|
188
188
|
prompt: 'Fix this checkout bug with a regression test',
|
|
189
189
|
});
|
|
190
190
|
check(routed.status === 0, '#169 large-workspace Tier-2 fixture routes normally');
|
|
191
|
+
const requirements = JSON.stringify([
|
|
192
|
+
{ id: 'req-fingerprint', summary: 'Exercise large-workspace shell admission', acceptance: 'the shell call remains available above the fingerprint observation cap' },
|
|
193
|
+
]);
|
|
194
|
+
const planned = node('hooks/scripts/workflow-disposition.js', undefined, [
|
|
195
|
+
'plan', '--session-id', sessionId,
|
|
196
|
+
'--requirements-json', requirements,
|
|
197
|
+
'--strategy', 'iterative-single',
|
|
198
|
+
'--evidence', 'single bounded runtime regression uses the iterative workflow',
|
|
199
|
+
]);
|
|
200
|
+
check(planned.status === 0, '#169 large-workspace Tier-2 fixture resolves requirements/workflow planning');
|
|
191
201
|
const started = node('hooks/scripts/workflow-disposition.js', undefined, ['start', '--session-id', sessionId]);
|
|
192
|
-
check(started.status === 0, '#169 large-workspace Tier-2 fixture starts
|
|
202
|
+
check(started.status === 0, '#169 large-workspace Tier-2 fixture starts after planning');
|
|
193
203
|
|
|
194
204
|
const admitted = node('hooks/scripts/workflow-gate.js', {
|
|
195
205
|
...payload,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-issue85-obligations-'));
|
|
11
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
12
|
+
process.env.HARNESS_WORKSPACE_ROOT = ROOT;
|
|
13
|
+
|
|
14
|
+
const { getSessionDir } = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js'));
|
|
15
|
+
const kernel = path.join(ROOT, 'harness-everything/scripts/kernel-router.js');
|
|
16
|
+
const controller = path.join(ROOT, 'hooks/scripts/workflow-disposition.js');
|
|
17
|
+
const stopGate = path.join(ROOT, 'hooks/scripts/workflow-stop-gate.js');
|
|
18
|
+
const workflowGate = path.join(ROOT, 'hooks/scripts/workflow-gate.js');
|
|
19
|
+
const statePersist = path.join(ROOT, 'hooks/scripts/state-persist.js');
|
|
20
|
+
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function check(condition, message) {
|
|
23
|
+
if (condition) console.log(' PASS ' + message);
|
|
24
|
+
else { console.error(' FAIL ' + message); failed++; }
|
|
25
|
+
}
|
|
26
|
+
function run(script, args = [], input = null) {
|
|
27
|
+
return spawnSync(process.execPath, [script, ...args], {
|
|
28
|
+
cwd: ROOT,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
input: input === null ? undefined : JSON.stringify(input),
|
|
31
|
+
env: { ...process.env, HARNESS_STATE_HOME: stateHome, HARNESS_WORKSPACE_ROOT: ROOT },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
35
|
+
|
|
36
|
+
console.log('=== Issue #85 requirements-first workflow contract ===');
|
|
37
|
+
|
|
38
|
+
const sessionId = 'issue85-tier2';
|
|
39
|
+
const payload = { session_id: sessionId, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
|
|
40
|
+
const routed = run(kernel, [], payload);
|
|
41
|
+
check(routed.status === 0, 'Tier 2 route persists successfully');
|
|
42
|
+
|
|
43
|
+
const sessionDir = getSessionDir(ROOT, sessionId, payload);
|
|
44
|
+
const workflowFile = path.join(sessionDir, 'workflow-run.json');
|
|
45
|
+
const obligationFile = path.join(sessionDir, 'workflow-obligations.json');
|
|
46
|
+
let workflow = readJson(workflowFile);
|
|
47
|
+
let obligations = readJson(obligationFile);
|
|
48
|
+
check(workflow.tier === 'tier2' && workflow.strategy === 'iterative-single', 'Tier 2 receives a preliminary iterative-single route');
|
|
49
|
+
check(workflow.state === 'pending', 'selected Tier 2 workflow begins pending');
|
|
50
|
+
check(obligations.phase === 'planning', 'Tier 2 immediately materializes a planning contract');
|
|
51
|
+
check(obligations.obligations.map(item => item.id).join(',') === 'decompose,compose', 'planning starts with decompose then compose');
|
|
52
|
+
check(obligations.requirements.length === 0, 'requirements begin empty instead of being invented by the router');
|
|
53
|
+
check(obligations.workflowSelection.candidateStrategy === 'iterative-single', 'router topology is recorded only as candidate strategy');
|
|
54
|
+
|
|
55
|
+
const prematureStart = run(controller, ['start', '--session-id', sessionId]);
|
|
56
|
+
check(prematureStart.status !== 0, 'workflow start refuses to pretend planning is complete');
|
|
57
|
+
workflow = readJson(workflowFile);
|
|
58
|
+
check(workflow.state === 'pending', 'failed start leaves workflow pending');
|
|
59
|
+
|
|
60
|
+
const mutationReminder = run(workflowGate, [], {
|
|
61
|
+
session_id: sessionId, cwd: ROOT, hook_event_name: 'PreToolUse',
|
|
62
|
+
tool_name: 'Write', tool_input: { file_path: path.join(ROOT, 'tmp-issue85.txt'), content: 'x' },
|
|
63
|
+
});
|
|
64
|
+
check(mutationReminder.status === 0, 'unresolved planning does not hard-block ordinary tools');
|
|
65
|
+
|
|
66
|
+
const beforePlanStop = run(stopGate, [], { session_id: sessionId, cwd: ROOT, hook_event_name: 'Stop' });
|
|
67
|
+
workflow = readJson(workflowFile);
|
|
68
|
+
check(beforePlanStop.status === 0, 'Stop remains fail-open while planning is unresolved');
|
|
69
|
+
check(workflow.state === 'pending', 'unresolved planning cannot silently become satisfied');
|
|
70
|
+
check((workflow.unresolved || []).includes('decompose:pending'), 'decomposition gap is visible');
|
|
71
|
+
check((workflow.unresolved || []).includes('compose:pending'), 'workflow-composition gap is visible');
|
|
72
|
+
|
|
73
|
+
const requirements = JSON.stringify([
|
|
74
|
+
{ id: 'req-fix', summary: 'Correct the checkout failure', acceptance: 'the failing scenario succeeds', suggestedSkills: ['tdd'] },
|
|
75
|
+
{ id: 'req-regression', summary: 'Protect the behavior with a regression test', acceptance: 'the test fails before the fix and passes after it', suggestedSkills: ['tdd', 'verification-loop'] },
|
|
76
|
+
]);
|
|
77
|
+
const planned = run(controller, [
|
|
78
|
+
'plan', '--session-id', sessionId,
|
|
79
|
+
'--requirements-json', requirements,
|
|
80
|
+
'--strategy', 'iterative-single',
|
|
81
|
+
'--evidence', 'two dependent requirements fit a bounded single-agent test/fix/verify loop',
|
|
82
|
+
]);
|
|
83
|
+
check(planned.status === 0, 'requirements and workflow selection are recorded before execution');
|
|
84
|
+
obligations = readJson(obligationFile);
|
|
85
|
+
check(obligations.requirements.map(item => item.id).join(',') === 'req-fix,req-regression', 'requirement fragments are machine-readable');
|
|
86
|
+
check(obligations.workflowSelection.disposition === 'confirmed', 'workflow choice is explicitly confirmed after decomposition');
|
|
87
|
+
check(obligations.obligations.find(item => item.id === 'decompose').status === 'pass', 'decompose obligation resolves from requirement fragments');
|
|
88
|
+
check(obligations.obligations.find(item => item.id === 'compose').status === 'pass', 'compose obligation resolves from workflow-selection evidence');
|
|
89
|
+
|
|
90
|
+
const started = run(controller, ['start', '--session-id', sessionId]);
|
|
91
|
+
check(started.status === 0, 'start activates the confirmed non-Fable workflow');
|
|
92
|
+
workflow = readJson(workflowFile);
|
|
93
|
+
obligations = readJson(obligationFile);
|
|
94
|
+
check(workflow.state === 'running', 'confirmed workflow becomes running');
|
|
95
|
+
check(obligations.phase === 'execution', 'planning contract advances to execution phase');
|
|
96
|
+
check(obligations.obligations.map(item => item.id).join(',') === 'decompose,compose,execute,verify', 'execution adds execute/verify without losing planning evidence');
|
|
97
|
+
|
|
98
|
+
check(run(controller, ['obligation', '--session-id', sessionId, '--obligation-id', 'execute', '--disposition', 'escaped', '--reason-code', 'workflow-uncovered-scope', '--scope', 'optional unrelated cleanup', '--evidence', 'outside requested bug-fix requirements']).status === 0,
|
|
99
|
+
'execution obligation accepts bounded evidence-backed escape');
|
|
100
|
+
check(run(controller, ['obligation', '--session-id', sessionId, '--obligation-id', 'verify', '--disposition', 'escaped', '--reason-code', 'workflow-uncovered-scope', '--scope', 'tests', '--evidence', 'attempt']).status !== 0,
|
|
101
|
+
'verification obligation cannot be escaped');
|
|
102
|
+
check(run(controller, ['obligation', '--session-id', sessionId, '--obligation-id', 'verify', '--disposition', 'blocked', '--reason-code', 'verification-environment-unavailable', '--evidence', 'fixture intentionally blocks verification']).status === 0,
|
|
103
|
+
'verification obligation can record explicit blocked evidence');
|
|
104
|
+
|
|
105
|
+
const blockedStop = run(stopGate, [], { session_id: sessionId, cwd: ROOT, hook_event_name: 'Stop' });
|
|
106
|
+
workflow = readJson(workflowFile);
|
|
107
|
+
check(blockedStop.status === 0, 'blocked obligation still reminds without trapping Stop');
|
|
108
|
+
check(workflow.state === 'running', 'blocked obligation keeps workflow unresolved rather than satisfied');
|
|
109
|
+
check((workflow.unresolved || []).includes('verify:blocked'), 'blocked verification remains visible');
|
|
110
|
+
|
|
111
|
+
check(run(controller, ['obligation', '--session-id', sessionId, '--obligation-id', 'verify', '--disposition', 'pass', '--evidence', 'npm test and focused regression passed']).status === 0,
|
|
112
|
+
'verification can later resolve to pass with evidence');
|
|
113
|
+
check(run(statePersist, [], {
|
|
114
|
+
session_id: sessionId, cwd: ROOT, hook_event_name: 'PostToolUse',
|
|
115
|
+
tool_name: 'Bash', tool_input: { command: 'npm test' },
|
|
116
|
+
tool_response: { stdout: 'pass', stderr: '', exitCode: 0 },
|
|
117
|
+
}).status === 0, 'objective verification milestone is observed after the earlier mutation');
|
|
118
|
+
const resolvedStop = run(stopGate, [], { session_id: sessionId, cwd: ROOT, hook_event_name: 'Stop' });
|
|
119
|
+
workflow = readJson(workflowFile);
|
|
120
|
+
check(resolvedStop.status === 0, 'resolved workflow Stop remains fail-open');
|
|
121
|
+
check(workflow.state === 'satisfied', 'planning + execution + verification allow satisfied');
|
|
122
|
+
check((workflow.unresolved || []).length === 0, 'satisfied workflow has no unresolved obligations');
|
|
123
|
+
|
|
124
|
+
const mismatchSession = 'issue85-replan';
|
|
125
|
+
const mismatchPayload = { session_id: mismatchSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
|
|
126
|
+
check(run(kernel, [], mismatchPayload).status === 0, 'second Tier 2 route persists for replan discriminator');
|
|
127
|
+
const mismatchDir = getSessionDir(ROOT, mismatchSession, mismatchPayload);
|
|
128
|
+
const mismatchPlan = run(controller, [
|
|
129
|
+
'plan', '--session-id', mismatchSession,
|
|
130
|
+
'--requirements-json', requirements,
|
|
131
|
+
'--strategy', 'fable-staged',
|
|
132
|
+
'--evidence', 'decomposition indicates dependent multi-stage work instead',
|
|
133
|
+
]);
|
|
134
|
+
check(mismatchPlan.status === 0, 'post-decomposition workflow can be recomposed through the router contract');
|
|
135
|
+
const mismatchObligations = readJson(path.join(mismatchDir, 'workflow-obligations.json'));
|
|
136
|
+
const mismatchWorkflow = readJson(path.join(mismatchDir, 'workflow-run.json'));
|
|
137
|
+
check(mismatchObligations.workflowSelection.disposition === 'confirmed', 'recomposed workflow receives an explicit confirmed disposition');
|
|
138
|
+
check(mismatchObligations.workflowSelection.requestedStrategy === 'fable-staged' &&
|
|
139
|
+
mismatchObligations.workflowSelection.confirmedStrategy === 'fable-staged',
|
|
140
|
+
'requested and router-confirmed post-decomposition strategy are preserved');
|
|
141
|
+
check(mismatchWorkflow.pendingPlan?.strategy === 'fable-staged', 'recomposed plan becomes the pending execution contract');
|
|
142
|
+
const mismatchStop = run(stopGate, [], { session_id: mismatchSession, cwd: ROOT, hook_event_name: 'Stop' });
|
|
143
|
+
const mismatchAfterStop = readJson(path.join(mismatchDir, 'workflow-run.json'));
|
|
144
|
+
check(mismatchStop.status === 0 && mismatchAfterStop.state === 'pending' &&
|
|
145
|
+
(mismatchAfterStop.unresolved || []).includes('execution:not-started'),
|
|
146
|
+
'recomposed workflow cannot become satisfied before its selected topology starts');
|
|
147
|
+
const mismatchStart = run(controller, ['start', '--session-id', mismatchSession]);
|
|
148
|
+
check(mismatchStart.status !== 0 && /workflow-stages\.json/.test(mismatchStart.stderr || ''),
|
|
149
|
+
'recomposed Fable topology proceeds to its native stage-contract requirement');
|
|
150
|
+
check(run(workflowGate, [], {
|
|
151
|
+
session_id: mismatchSession, cwd: ROOT, hook_event_name: 'PreToolUse',
|
|
152
|
+
tool_name: 'Write', tool_input: { file_path: path.join(ROOT, 'tmp-replan.txt'), content: 'x' },
|
|
153
|
+
}).status === 0, 'recomposed planning state still does not recreate a tool lock');
|
|
154
|
+
|
|
155
|
+
const prohibitedSession = 'issue85-prohibited-recompose';
|
|
156
|
+
const prohibitedPayload = { session_id: prohibitedSession, cwd: ROOT, prompt: 'Fix this checkout bug with a regression test. Do not use fable.' };
|
|
157
|
+
check(run(kernel, [], prohibitedPayload).status === 0, 'prohibition fixture routes successfully');
|
|
158
|
+
const prohibitedRequirements = JSON.stringify([
|
|
159
|
+
{ id: 'req-safe', summary: 'Resolve the bug under the user constraints', acceptance: 'the fix is verified without prohibited topology' },
|
|
160
|
+
]);
|
|
161
|
+
const prohibitedPlan = run(controller, [
|
|
162
|
+
'plan', '--session-id', prohibitedSession,
|
|
163
|
+
'--requirements-json', prohibitedRequirements,
|
|
164
|
+
'--strategy', 'fable-staged',
|
|
165
|
+
'--evidence', 'candidate considered after decomposition',
|
|
166
|
+
]);
|
|
167
|
+
check(prohibitedPlan.status === 0, 'prohibited post-decomposition choice is evaluated by router policy');
|
|
168
|
+
const prohibitedDir = getSessionDir(ROOT, prohibitedSession, prohibitedPayload);
|
|
169
|
+
const prohibitedObligations = readJson(path.join(prohibitedDir, 'workflow-obligations.json'));
|
|
170
|
+
check(prohibitedObligations.workflowSelection.disposition === 'blocked', 'original user prohibition survives recomposition');
|
|
171
|
+
check(prohibitedObligations.obligations.find(item => item.id === 'decompose').status === 'pass' &&
|
|
172
|
+
prohibitedObligations.obligations.find(item => item.id === 'compose').status === 'blocked',
|
|
173
|
+
'requirements remain valid while only the prohibited workflow choice stays unresolved');
|
|
174
|
+
|
|
175
|
+
const fableSession = 'issue85-fable';
|
|
176
|
+
const fablePayload = { session_id: fableSession, cwd: ROOT, prompt: 'Refactor the entire authentication architecture in dependent stages.' };
|
|
177
|
+
check(run(kernel, [], fablePayload).status === 0, 'Tier 3 Fable candidate persists successfully');
|
|
178
|
+
const fableDir = getSessionDir(ROOT, fableSession, fablePayload);
|
|
179
|
+
const fableWorkflow = readJson(path.join(fableDir, 'workflow-run.json'));
|
|
180
|
+
const fablePlanning = readJson(path.join(fableDir, 'workflow-obligations.json'));
|
|
181
|
+
check(fableWorkflow.strategy === 'fable-staged', 'Tier 3 keeps Fable as preliminary candidate');
|
|
182
|
+
check(fablePlanning.obligations.map(item => item.id).join(',') === 'decompose,compose', 'Fable also requires requirements/workflow planning before stage execution');
|
|
183
|
+
check(run(controller, ['start', '--session-id', fableSession]).status !== 0, 'Fable cannot enter stage execution before planning contract resolves');
|
|
184
|
+
|
|
185
|
+
const tier1Session = 'issue85-tier1';
|
|
186
|
+
const tier1Payload = { session_id: tier1Session, cwd: ROOT, prompt: 'Update one README typo' };
|
|
187
|
+
check(run(kernel, [], tier1Payload).status === 0, 'Tier 1 route persists successfully');
|
|
188
|
+
const tier1Dir = getSessionDir(ROOT, tier1Session, tier1Payload);
|
|
189
|
+
const tier1WorkflowFile = path.join(tier1Dir, 'workflow-run.json');
|
|
190
|
+
check(!fs.existsSync(path.join(tier1Dir, 'workflow-obligations.json')), 'clear bounded Tier 1 intent has no decomposition overhead');
|
|
191
|
+
check(run(stopGate, [], { session_id: tier1Session, cwd: ROOT, hook_event_name: 'Stop' }).status === 0, 'Tier 1 direct Stop remains lightweight');
|
|
192
|
+
const tier1Workflow = readJson(tier1WorkflowFile);
|
|
193
|
+
check(tier1Workflow.strategy === 'direct-single' && tier1Workflow.state === 'satisfied', 'Tier 1 remains a minimal direct path');
|
|
194
|
+
|
|
195
|
+
const runtimeSource = fs.readFileSync(path.join(ROOT, 'hooks/scripts/lib/workflow-runtime.js'), 'utf8');
|
|
196
|
+
check(!runtimeSource.includes('.mutation-probes.lock'), '#190 mutation-probe lock remains retired');
|
|
197
|
+
check(!runtimeSource.includes('budget-exhausted'), '#190 hard budget state remains retired');
|
|
198
|
+
|
|
199
|
+
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
200
|
+
console.log('\n' + (failed === 0 ? 'PASS' : 'FAIL') + ': issue #85 requirements-first workflow contract (' + failed + ' failures)');
|
|
201
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -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.');
|
|
@@ -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
|
+
};
|