harness-everything 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +2 -2
- package/CHANGELOG.md +14 -0
- package/ci/mechanism-365-step-edit-accounting.test.js +395 -0
- package/ci/mechanism-codex-action-authorization.test.js +292 -0
- package/docs/codex-action-gate.md +22 -0
- package/docs/doc-workflow-reconciliation.md +13 -13
- package/docs/document-lifecycle.md +7 -5
- package/docs/platform-capabilities.md +17 -0
- package/hooks/scripts/action-gate.js +53 -3
- package/hooks/scripts/codex-user-prompt.js +20 -0
- package/hooks/scripts/lib/codex-action-authorization.js +193 -0
- package/hooks/scripts/state-persist.js +27 -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/scripts/action-gate.js +53 -3
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +20 -0
- package/plugins/harness-everything/hooks/scripts/lib/codex-action-authorization.js +193 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +27 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -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.41.
|
|
13
|
+
"version": "0.41.2",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.41.
|
|
4
|
+
"version": "0.41.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -36,9 +36,9 @@ jobs:
|
|
|
36
36
|
- uses: actions/checkout@v7
|
|
37
37
|
- uses: actions/setup-node@v7
|
|
38
38
|
with:
|
|
39
|
-
node-version: '24.
|
|
39
|
+
node-version: '24.22.1'
|
|
40
40
|
- name: Verify exact patched runtime
|
|
41
|
-
run: node -e "if (process.versions.node !== '24.
|
|
41
|
+
run: node -e "if (process.versions.node !== '24.22.1') process.exit(1)"
|
|
42
42
|
- name: Exercise real evaluator subprocess and repeated resident teardown
|
|
43
43
|
run: node --test ci/mechanism-system-one-resident.test.js
|
|
44
44
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.41.2](https://github.com/dyphn1/Harness-everything/compare/v0.41.1...v0.41.2) (2026-10-25)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **codex:** bind explicit action approvals ([eeec25c](https://github.com/dyphn1/Harness-everything/commit/eeec25c204bae89fa293e77a1d7de9e8f7135e13))
|
|
10
|
+
|
|
11
|
+
## [0.41.1](https://github.com/dyphn1/Harness-everything/compare/v0.41.0...v0.41.1) (2026-10-23)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **hooks:** stop counting a recorded workflow step as an edit ([#365](https://github.com/dyphn1/Harness-everything/issues/365)) ([b1beefe](https://github.com/dyphn1/Harness-everything/commit/b1beefe55f3f2b794901a4bad4fe3d0310de6745))
|
|
16
|
+
|
|
5
17
|
## [0.41.0](https://github.com/dyphn1/Harness-everything/compare/v0.40.0...v0.41.0) (2026-10-23)
|
|
6
18
|
|
|
7
19
|
### Features
|
|
@@ -76,6 +88,8 @@ All notable changes to this project are documented in this file.
|
|
|
76
88
|
|
|
77
89
|
## [Unreleased]
|
|
78
90
|
|
|
91
|
+
- Keep successful installed workflow step dispositions from advancing verification freshness while unknown workflow commands and mutations remain tracked (#365).
|
|
92
|
+
|
|
79
93
|
- Accept an interior `~` in unquoted installed doc-check path tokens, and reject `%VAR%` expansion before identity and digest checks.
|
|
80
94
|
|
|
81
95
|
- Preserve Windows path separators while classifying installed doc-check commands, and make the OpenCode workspace-key fixture match the plugin's Windows case normalization.
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-365-step-edit-'));
|
|
12
|
+
|
|
13
|
+
function run(command, args, cwd, env, input) {
|
|
14
|
+
return spawnSync(command, args, {
|
|
15
|
+
cwd,
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
env: { ...process.env, ...env },
|
|
18
|
+
input: input === undefined ? undefined : JSON.stringify(input),
|
|
19
|
+
timeout: 30000,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mustRun(command, args, cwd, env, input) {
|
|
24
|
+
const result = run(command, args, cwd, env, input);
|
|
25
|
+
assert.strictEqual(result.status, 0,
|
|
26
|
+
`${command} ${args.join(' ')} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function startContext(runtimeRoot, packaged, pilot) {
|
|
31
|
+
const label = `${packaged ? 'packaged' : 'source'}-${pilot ? 'pilot-on' : 'pilot-off'}`;
|
|
32
|
+
const repo = path.join(tempRoot, label, 'repo');
|
|
33
|
+
const stateHome = path.join(tempRoot, label, 'state');
|
|
34
|
+
const fakeHome = path.join(tempRoot, label, 'home');
|
|
35
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
36
|
+
fs.mkdirSync(fakeHome, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const env = {
|
|
39
|
+
HARNESS_STATE_HOME: stateHome,
|
|
40
|
+
HARNESS_WORKSPACE_ROOT: repo,
|
|
41
|
+
HARNESS_DOC_RECONCILIATION_PILOT: pilot ? '1' : '0',
|
|
42
|
+
HARNESS_365_EVIDENCE: 'expanded',
|
|
43
|
+
HOME: fakeHome,
|
|
44
|
+
USERPROFILE: fakeHome,
|
|
45
|
+
CLAUDE: '1',
|
|
46
|
+
};
|
|
47
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
48
|
+
delete env.PLUGIN_ROOT;
|
|
49
|
+
|
|
50
|
+
mustRun('git', ['init', '-b', 'main'], repo, env);
|
|
51
|
+
mustRun('git', ['config', 'user.name', 'Harness #365 Test'], repo, env);
|
|
52
|
+
mustRun('git', ['config', 'user.email', 'harness-365@example.invalid'], repo, env);
|
|
53
|
+
fs.writeFileSync(path.join(repo, 'README.md'), 'baseline\n');
|
|
54
|
+
fs.writeFileSync(path.join(repo, 'package.json'), JSON.stringify({
|
|
55
|
+
name: 'harness-365-verification-fixture',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
private: true,
|
|
58
|
+
scripts: { test: 'node -e "process.exit(0)"' },
|
|
59
|
+
}, null, 2) + '\n');
|
|
60
|
+
mustRun('git', ['add', 'README.md', 'package.json'], repo, env);
|
|
61
|
+
mustRun('git', ['commit', '-m', 'seed verification fixture'], repo, env);
|
|
62
|
+
|
|
63
|
+
const kernel = packaged
|
|
64
|
+
? path.join(runtimeRoot, 'skills/harness-everything/scripts/kernel-router.js')
|
|
65
|
+
: path.join(runtimeRoot, 'harness-everything/scripts/kernel-router.js');
|
|
66
|
+
const controller = path.join(runtimeRoot, 'hooks/scripts/workflow-disposition.js');
|
|
67
|
+
const sessionId = `pm2-365-${label}`;
|
|
68
|
+
const routePayload = {
|
|
69
|
+
hook_event_name: 'UserPromptSubmit',
|
|
70
|
+
session_id: sessionId,
|
|
71
|
+
cwd: repo,
|
|
72
|
+
prompt: 'Fix this checkout bug and add a regression test.',
|
|
73
|
+
};
|
|
74
|
+
mustRun(process.execPath, [kernel], repo, env, routePayload);
|
|
75
|
+
|
|
76
|
+
const requirements = [
|
|
77
|
+
{ id: 'req-behavior', stepType: 'behavior-change', summary: 'Record the behavior change', acceptance: 'the behavior change is recorded' },
|
|
78
|
+
{ id: 'req-doc', stepType: 'doc-change', summary: 'Record the documentation change', acceptance: 'the documentation change is recorded' },
|
|
79
|
+
{ id: 'req-expansion', stepType: 'behavior-change', summary: 'Record a shell expansion control', acceptance: 'the shell expansion control is recorded' },
|
|
80
|
+
{ id: 'req-percent', stepType: 'behavior-change', summary: 'Record a Windows expansion control', acceptance: 'the percent expansion control is recorded' },
|
|
81
|
+
{ id: 'req-blocked', stepType: 'doc-change', summary: 'Record a blocked step control', acceptance: 'the blocked step is recorded' },
|
|
82
|
+
{ id: 'req-verify', stepType: 'verification', summary: 'Verify the change', acceptance: 'verification evidence is recorded' },
|
|
83
|
+
];
|
|
84
|
+
mustRun(process.execPath, [controller, 'plan', '--session-id', sessionId,
|
|
85
|
+
'--requirements-json', JSON.stringify(requirements), '--strategy', 'iterative-single',
|
|
86
|
+
'--evidence', 'mechanism test plan'], repo, env);
|
|
87
|
+
mustRun(process.execPath, [controller, 'start', '--session-id', sessionId], repo, env);
|
|
88
|
+
mustRun(process.execPath, [controller, 'binding', '--session-id', sessionId,
|
|
89
|
+
'--step-id', 'req-behavior', '--binding-id', 'tdd', '--disposition', 'loaded',
|
|
90
|
+
'--evidence', 'the packaged TDD skill is available'], repo, env);
|
|
91
|
+
|
|
92
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
93
|
+
const { getSessionDir } = require(path.join(runtimeRoot, 'hooks/scripts/lib/harness-state.js'));
|
|
94
|
+
const payload = { session_id: sessionId, cwd: repo };
|
|
95
|
+
const sessionDir = getSessionDir(repo, sessionId, payload);
|
|
96
|
+
return { label, repo, env, controller, sessionId, payload, sessionDir,
|
|
97
|
+
handoffFile: path.join(sessionDir, 'handoff-state.json'),
|
|
98
|
+
obligationsFile: path.join(sessionDir, 'workflow-obligations.json') };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function state(context) {
|
|
102
|
+
return JSON.parse(fs.readFileSync(context.handoffFile, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hook(context, runtimeRoot, script, payload) {
|
|
106
|
+
return mustRun(process.execPath, [path.join(runtimeRoot, `hooks/scripts/${script}`)],
|
|
107
|
+
context.repo, context.env, { ...context.payload, ...payload });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function shell(context, command) {
|
|
111
|
+
return spawnSync(command, {
|
|
112
|
+
cwd: context.repo,
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
env: { ...process.env, ...context.env },
|
|
115
|
+
shell: true,
|
|
116
|
+
timeout: 30000,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shellEvent(context, runtimeRoot, command, result) {
|
|
121
|
+
return hook(context, runtimeRoot, 'state-persist.js', {
|
|
122
|
+
hook_event_name: result.status === 0 ? 'PostToolUse' : 'PostToolUseFailure',
|
|
123
|
+
tool_name: 'Bash',
|
|
124
|
+
tool_input: { command },
|
|
125
|
+
tool_response: { exitCode: result.status, stdout: result.stdout, stderr: result.stderr },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function verifyFresh(context, runtimeRoot, label) {
|
|
130
|
+
const changed = path.join(context.repo, `${label}.js`);
|
|
131
|
+
fs.writeFileSync(changed, `// ${label}\n`);
|
|
132
|
+
hook(context, runtimeRoot, 'state-persist.js', {
|
|
133
|
+
hook_event_name: 'PostToolUse',
|
|
134
|
+
tool_name: 'Edit',
|
|
135
|
+
tool_input: { file_path: changed },
|
|
136
|
+
});
|
|
137
|
+
const edited = state(context);
|
|
138
|
+
assert.ok(edited.lastEditAt, `${context.label}: direct edit is recorded`);
|
|
139
|
+
|
|
140
|
+
const verification = shell(context, 'npm test');
|
|
141
|
+
assert.strictEqual(verification.status, 0, `${context.label}: fixture npm test succeeds`);
|
|
142
|
+
shellEvent(context, runtimeRoot, 'npm test', verification);
|
|
143
|
+
const fresh = state(context);
|
|
144
|
+
assert.ok(fresh.lastVerifyAt >= fresh.lastEditAt,
|
|
145
|
+
`${context.label}: successful verification follows the edit`);
|
|
146
|
+
return fresh;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stepCommand(context, stepId, disposition = 'pass') {
|
|
150
|
+
return `node "${context.controller}" step --session-id "${context.sessionId}" ` +
|
|
151
|
+
`--step-id "${stepId}" --disposition ${disposition} --evidence "recorded by the mechanism test"`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function blockedStepCommand(context, stepId) {
|
|
155
|
+
return 'node "' + context.controller + '" step --session-id "' + context.sessionId + '" ' +
|
|
156
|
+
'--step-id "' + stepId + '" --disposition blocked --reason-code test-blocked ' +
|
|
157
|
+
'--evidence "recorded by the mechanism test"';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function percentStepCommand(context, stepId) {
|
|
161
|
+
return 'node "' + context.controller + '" step --session-id "' + context.sessionId + '" ' +
|
|
162
|
+
'--step-id "' + stepId + '" --disposition pass --evidence "%HARNESS_365_EVIDENCE%"';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stopMessages(context, runtimeRoot) {
|
|
166
|
+
const basic = hook(context, runtimeRoot, 'stop-gate.js', { hook_event_name: 'Stop' });
|
|
167
|
+
const workflow = hook(context, runtimeRoot, 'workflow-stop-gate.js', { hook_event_name: 'Stop' });
|
|
168
|
+
return `${basic.stdout}\n${basic.stderr}\n${workflow.stdout}\n${workflow.stderr}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertFreshStops(context, runtimeRoot, label) {
|
|
172
|
+
const messages = stopMessages(context, runtimeRoot);
|
|
173
|
+
assert.doesNotMatch(messages, /verification after edit|verification-after-edit-missing|no observed successful verification/i,
|
|
174
|
+
`${context.label}: ${label} leaves both Stop hooks fresh\n${messages}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function assertStaleStops(context, runtimeRoot, label) {
|
|
178
|
+
const messages = stopMessages(context, runtimeRoot);
|
|
179
|
+
assert.match(messages, /no observed successful verification after the latest edit/i,
|
|
180
|
+
`${context.label}: ${label} makes the ordinary Stop remind\n${messages}`);
|
|
181
|
+
assert.match(messages, /verification-after-edit-missing/i,
|
|
182
|
+
`${context.label}: ${label} makes the workflow Stop remind\n${messages}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function waitPast(timestamp) {
|
|
186
|
+
while (Date.now() <= timestamp) {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function nonStepEscapeControl(context, runtimeRoot, packaged) {
|
|
190
|
+
const sessionId = context.sessionId + '-fable';
|
|
191
|
+
const kernel = packaged
|
|
192
|
+
? path.join(runtimeRoot, 'skills/harness-everything/scripts/kernel-router.js')
|
|
193
|
+
: path.join(runtimeRoot, 'harness-everything/scripts/kernel-router.js');
|
|
194
|
+
const payload = {
|
|
195
|
+
hook_event_name: 'UserPromptSubmit',
|
|
196
|
+
session_id: sessionId,
|
|
197
|
+
cwd: context.repo,
|
|
198
|
+
prompt: 'Refactor the entire authentication architecture in dependent stages.',
|
|
199
|
+
};
|
|
200
|
+
mustRun(process.execPath, [kernel], context.repo, context.env, payload);
|
|
201
|
+
mustRun(process.execPath, [context.controller, 'plan', '--session-id', sessionId,
|
|
202
|
+
'--requirements-json', JSON.stringify([
|
|
203
|
+
{ id: 'req-implement', stepType: 'behavior-change', summary: 'Implement the change', acceptance: 'the change is implemented' },
|
|
204
|
+
{ id: 'req-verify', stepType: 'verification', summary: 'Verify the change', acceptance: 'verification evidence is recorded' },
|
|
205
|
+
]), '--strategy', 'fable-staged', '--evidence', 'dependent fixture stages'], context.repo, context.env);
|
|
206
|
+
|
|
207
|
+
const { getSessionDir } = require(path.join(runtimeRoot, 'hooks/scripts/lib/harness-state.js'));
|
|
208
|
+
const sessionDir = getSessionDir(context.repo, sessionId, payload);
|
|
209
|
+
fs.writeFileSync(path.join(sessionDir, 'workflow-stages.json'), JSON.stringify([
|
|
210
|
+
{
|
|
211
|
+
stageId: 'implement', goal: 'implement a bounded fixture change', agent: 'fable-worker-sonnet',
|
|
212
|
+
task: 'prepare the run artifact control', dependsOn: [], writeSet: ['generated.js'], inputs: [],
|
|
213
|
+
expectedOutputs: ['generated.js'], requiredBindings: [], optionalBindings: [],
|
|
214
|
+
checkCommand: 'node fixture-check.js', passCondition: 'exit 0',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
stageId: 'verify', goal: 'verify the fixture change', agent: 'fable-verifier',
|
|
218
|
+
task: 'independently verify the fixture', dependsOn: ['implement'], writeSet: [],
|
|
219
|
+
inputs: ['generated.js'], expectedOutputs: ['verification evidence'], requiredBindings: [],
|
|
220
|
+
optionalBindings: [], checkCommand: 'node verifier-check.js', passCondition: 'exit 0',
|
|
221
|
+
},
|
|
222
|
+
], null, 2));
|
|
223
|
+
mustRun(process.execPath, [context.controller, 'start', '--session-id', sessionId], context.repo, context.env);
|
|
224
|
+
|
|
225
|
+
const consumerPath = path.join(runtimeRoot, packaged
|
|
226
|
+
? 'skills/fable-mode/scripts/workflow-plan-consumer.js'
|
|
227
|
+
: 'fable-mode/scripts/workflow-plan-consumer.js');
|
|
228
|
+
const consumer = require(consumerPath);
|
|
229
|
+
const workflow = JSON.parse(fs.readFileSync(path.join(sessionDir, 'workflow-run.json'), 'utf8'));
|
|
230
|
+
const runRoot = path.join(consumer.getWorkspaceStateRoot(context.repo), 'fable-runs', workflow.runId);
|
|
231
|
+
assert.ok(fs.existsSync(path.join(runRoot, 'contracts', 'implement.json')),
|
|
232
|
+
context.label + ': non-step start writes stage run artifacts');
|
|
233
|
+
|
|
234
|
+
const fableContext = {
|
|
235
|
+
...context,
|
|
236
|
+
sessionId,
|
|
237
|
+
payload: { session_id: sessionId, cwd: context.repo },
|
|
238
|
+
sessionDir,
|
|
239
|
+
handoffFile: path.join(sessionDir, 'handoff-state.json'),
|
|
240
|
+
};
|
|
241
|
+
const fresh = verifyFresh(fableContext, runtimeRoot, 'fable-escape-control');
|
|
242
|
+
waitPast(fresh.lastVerifyAt);
|
|
243
|
+
const escapeCommand = 'node "' + context.controller + '" escape --session-id "' + sessionId + '" ' +
|
|
244
|
+
'--stage-id implement --reason-code workflow-uncovered-scope --scope "fixture scope" ' +
|
|
245
|
+
'--evidence "write a run escape artifact"';
|
|
246
|
+
const escape = shell(fableContext, escapeCommand);
|
|
247
|
+
assert.strictEqual(escape.status, 0, context.label + ': non-step escape operation succeeds\n' + escape.stderr);
|
|
248
|
+
shellEvent(fableContext, runtimeRoot, escapeCommand, escape);
|
|
249
|
+
assert.ok(state(fableContext).lastEditAt > fresh.lastVerifyAt,
|
|
250
|
+
context.label + ': non-step workflow operation advances lastEditAt');
|
|
251
|
+
assert.ok(fs.existsSync(path.join(runRoot, 'escapes', 'implement.json')),
|
|
252
|
+
context.label + ': non-step workflow operation writes its run artifact');
|
|
253
|
+
const basic = hook(fableContext, runtimeRoot, 'stop-gate.js', { hook_event_name: 'Stop' });
|
|
254
|
+
const workflowStop = hook(fableContext, runtimeRoot, 'workflow-stop-gate.js', { hook_event_name: 'Stop' });
|
|
255
|
+
assert.match(basic.stderr, /no observed successful verification after the latest edit/i,
|
|
256
|
+
context.label + ': non-step operation makes the ordinary Stop remind');
|
|
257
|
+
assert.match(workflowStop.stderr, /Workflow Reminder/,
|
|
258
|
+
context.label + ': non-step operation leaves unresolved Fable evidence visible');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkTrace(runtimeRoot, packaged, pilot) {
|
|
262
|
+
const context = startContext(runtimeRoot, packaged, pilot);
|
|
263
|
+
const first = verifyFresh(context, runtimeRoot, 'initial-edit');
|
|
264
|
+
|
|
265
|
+
const readOnly = shell(context, 'git status --short');
|
|
266
|
+
assert.strictEqual(readOnly.status, 0, `${context.label}: read-only git status succeeds`);
|
|
267
|
+
shellEvent(context, runtimeRoot, 'git status --short', readOnly);
|
|
268
|
+
assert.strictEqual(state(context).lastEditAt, first.lastEditAt,
|
|
269
|
+
`${context.label}: read-only command does not advance lastEditAt`);
|
|
270
|
+
assertFreshStops(context, runtimeRoot, 'read-only command');
|
|
271
|
+
|
|
272
|
+
const step = shell(context, stepCommand(context, 'req-behavior'));
|
|
273
|
+
assert.strictEqual(step.status, 0, `${context.label}: installed workflow step succeeds\n${step.stderr}`);
|
|
274
|
+
shellEvent(context, runtimeRoot, stepCommand(context, 'req-behavior'), step);
|
|
275
|
+
assert.strictEqual(state(context).lastEditAt, first.lastEditAt,
|
|
276
|
+
`${context.label}: installed metadata-only step does not advance lastEditAt`);
|
|
277
|
+
const obligations = JSON.parse(fs.readFileSync(context.obligationsFile, 'utf8'));
|
|
278
|
+
assert.strictEqual(obligations.requirements.find(item => item.id === 'req-behavior').status, 'pass',
|
|
279
|
+
`${context.label}: installed step still records its workflow-state disposition`);
|
|
280
|
+
assertFreshStops(context, runtimeRoot, 'installed step recording');
|
|
281
|
+
|
|
282
|
+
let fresh = verifyFresh(context, runtimeRoot, 'edit-control');
|
|
283
|
+
waitPast(fresh.lastVerifyAt);
|
|
284
|
+
const realEdit = path.join(context.repo, 'real-edit.js');
|
|
285
|
+
fs.writeFileSync(realEdit, 'real edit\n');
|
|
286
|
+
hook(context, runtimeRoot, 'state-persist.js', {
|
|
287
|
+
hook_event_name: 'PostToolUse', tool_name: 'Edit', tool_input: { file_path: realEdit },
|
|
288
|
+
});
|
|
289
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
290
|
+
`${context.label}: real Edit advances lastEditAt`);
|
|
291
|
+
assertStaleStops(context, runtimeRoot, 'real Edit after verification');
|
|
292
|
+
|
|
293
|
+
fresh = verifyFresh(context, runtimeRoot, 'chain-control');
|
|
294
|
+
waitPast(fresh.lastVerifyAt);
|
|
295
|
+
const chained = `${stepCommand(context, 'req-doc')} && echo x > "${path.join(context.repo, 'chained.txt')}"`;
|
|
296
|
+
const chainedResult = shell(context, chained);
|
|
297
|
+
assert.strictEqual(chainedResult.status, 0, `${context.label}: step plus chained mutation succeeds\n${chainedResult.stderr}`);
|
|
298
|
+
shellEvent(context, runtimeRoot, chained, chainedResult);
|
|
299
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
300
|
+
`${context.label}: chained mutation advances lastEditAt`);
|
|
301
|
+
assertStaleStops(context, runtimeRoot, 'chained mutation');
|
|
302
|
+
|
|
303
|
+
fresh = verifyFresh(context, runtimeRoot, 'newline-chain-control');
|
|
304
|
+
waitPast(fresh.lastVerifyAt);
|
|
305
|
+
const newlineMutator = path.join(context.repo, 'newline-mutate.sh');
|
|
306
|
+
fs.writeFileSync(newlineMutator, `#!/bin/sh\necho x > "${path.join(context.repo, 'newline-chained.txt')}"\n`);
|
|
307
|
+
fs.chmodSync(newlineMutator, 0o755);
|
|
308
|
+
const newlineChained = `node "${context.controller}" step --session-id "${context.sessionId}" ` +
|
|
309
|
+
'--step-id "req-doc" --disposition pass --evidence\n./newline-mutate.sh';
|
|
310
|
+
// cmd.exe does not split commands on a newline, so on Windows replay the host outcome a POSIX shell produces.
|
|
311
|
+
const newlineResult = process.platform === 'win32'
|
|
312
|
+
? (fs.writeFileSync(path.join(context.repo, 'newline-chained.txt'), 'x\n'), { status: 0, stdout: '', stderr: '' })
|
|
313
|
+
: shell(context, newlineChained);
|
|
314
|
+
assert.strictEqual(newlineResult.status, 0, `${context.label}: newline-separated mutation succeeds\n${newlineResult.stderr}`);
|
|
315
|
+
shellEvent(context, runtimeRoot, newlineChained, newlineResult);
|
|
316
|
+
assert.ok(fs.existsSync(path.join(context.repo, 'newline-chained.txt')), `${context.label}: newline-separated mutation ran`);
|
|
317
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
318
|
+
`${context.label}: newline-separated mutation advances lastEditAt`);
|
|
319
|
+
assertStaleStops(context, runtimeRoot, 'newline-separated mutation');
|
|
320
|
+
|
|
321
|
+
mustRun(process.execPath, [context.controller, 'binding', '--session-id', context.sessionId,
|
|
322
|
+
'--step-id', 'req-expansion', '--binding-id', 'tdd', '--disposition', 'loaded',
|
|
323
|
+
'--evidence', 'the packaged TDD skill is available'], context.repo, context.env);
|
|
324
|
+
fresh = verifyFresh(context, runtimeRoot, 'expansion-control');
|
|
325
|
+
waitPast(fresh.lastVerifyAt);
|
|
326
|
+
const expansion = `${stepCommand(context, 'req-expansion').replace('recorded by the mechanism test', '$(echo expanded)')}`;
|
|
327
|
+
const expansionResult = shell(context, expansion);
|
|
328
|
+
assert.strictEqual(expansionResult.status, 0, `${context.label}: quoted shell expansion step succeeds\n${expansionResult.stderr}`);
|
|
329
|
+
shellEvent(context, runtimeRoot, expansion, expansionResult);
|
|
330
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
331
|
+
`${context.label}: shell expansion is not mistaken for a metadata-only command`);
|
|
332
|
+
assertStaleStops(context, runtimeRoot, 'quoted shell expansion');
|
|
333
|
+
|
|
334
|
+
mustRun(process.execPath, [context.controller, 'binding', '--session-id', context.sessionId,
|
|
335
|
+
'--step-id', 'req-percent', '--binding-id', 'tdd', '--disposition', 'loaded',
|
|
336
|
+
'--evidence', 'the packaged TDD skill is available'], context.repo, context.env);
|
|
337
|
+
fresh = verifyFresh(context, runtimeRoot, 'percent-expansion-control');
|
|
338
|
+
waitPast(fresh.lastVerifyAt);
|
|
339
|
+
const percentCommand = percentStepCommand(context, 'req-percent');
|
|
340
|
+
const percent = shell(context, percentCommand);
|
|
341
|
+
assert.strictEqual(percent.status, 0, `${context.label}: percent expansion step succeeds\n${percent.stderr}`);
|
|
342
|
+
shellEvent(context, runtimeRoot, percentCommand, percent);
|
|
343
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
344
|
+
`${context.label}: percent expansion cannot bypass edit accounting`);
|
|
345
|
+
assertStaleStops(context, runtimeRoot, 'percent expansion');
|
|
346
|
+
|
|
347
|
+
fresh = verifyFresh(context, runtimeRoot, 'blocked-step-control');
|
|
348
|
+
waitPast(fresh.lastVerifyAt);
|
|
349
|
+
const blockedCommand = blockedStepCommand(context, 'req-blocked');
|
|
350
|
+
const blocked = shell(context, blockedCommand);
|
|
351
|
+
assert.strictEqual(blocked.status, 0, `${context.label}: installed blocked step succeeds\n${blocked.stderr}`);
|
|
352
|
+
shellEvent(context, runtimeRoot, blockedCommand, blocked);
|
|
353
|
+
assert.strictEqual(state(context).lastEditAt, fresh.lastEditAt,
|
|
354
|
+
`${context.label}: installed blocked step does not advance lastEditAt`);
|
|
355
|
+
const blockedObligations = JSON.parse(fs.readFileSync(context.obligationsFile, 'utf8'));
|
|
356
|
+
assert.strictEqual(blockedObligations.requirements.find(item => item.id === 'req-blocked').status, 'blocked',
|
|
357
|
+
`${context.label}: installed blocked step still records its workflow state`);
|
|
358
|
+
assertFreshStops(context, runtimeRoot, 'installed blocked step recording');
|
|
359
|
+
|
|
360
|
+
fresh = verifyFresh(context, runtimeRoot, 'lookalike-control');
|
|
361
|
+
waitPast(fresh.lastVerifyAt);
|
|
362
|
+
const lookalike = path.join(tempRoot, 'outside', 'workflow-disposition.js');
|
|
363
|
+
fs.mkdirSync(path.dirname(lookalike), { recursive: true });
|
|
364
|
+
fs.writeFileSync(lookalike, 'process.exit(0);\n');
|
|
365
|
+
const lookalikeCommand = `node "${lookalike}" step --session-id "${context.sessionId}" ` +
|
|
366
|
+
'--step-id "req-doc" --disposition pass --evidence "lookalike"';
|
|
367
|
+
const lookalikeResult = shell(context, lookalikeCommand);
|
|
368
|
+
assert.strictEqual(lookalikeResult.status, 0, `${context.label}: lookalike script succeeds`);
|
|
369
|
+
shellEvent(context, runtimeRoot, lookalikeCommand, lookalikeResult);
|
|
370
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
371
|
+
`${context.label}: lookalike script advances lastEditAt`);
|
|
372
|
+
assertStaleStops(context, runtimeRoot, 'lookalike workflow script');
|
|
373
|
+
|
|
374
|
+
fresh = verifyFresh(context, runtimeRoot, 'non-step-control');
|
|
375
|
+
waitPast(fresh.lastVerifyAt);
|
|
376
|
+
const nonStepCommand = `node "${context.controller}" block --session-id "${context.sessionId}" --evidence "blocked by mechanism test"`;
|
|
377
|
+
const nonStep = shell(context, nonStepCommand);
|
|
378
|
+
assert.strictEqual(nonStep.status, 0, `${context.label}: non-step workflow operation succeeds`);
|
|
379
|
+
shellEvent(context, runtimeRoot, nonStepCommand, nonStep);
|
|
380
|
+
assert.ok(state(context).lastEditAt > fresh.lastVerifyAt,
|
|
381
|
+
`${context.label}: non-step workflow operation advances lastEditAt`);
|
|
382
|
+
assertStaleStops(context, runtimeRoot, 'non-step workflow operation');
|
|
383
|
+
nonStepEscapeControl(context, runtimeRoot, packaged);
|
|
384
|
+
console.log(`PASS ${context.label}: exact step, read-only, Edit, chain, lookalike, and non-step controls`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
for (const packaged of [false, true]) {
|
|
389
|
+
const runtimeRoot = packaged ? path.join(ROOT, 'plugins/harness-everything') : ROOT;
|
|
390
|
+
for (const pilot of [false, true]) checkTrace(runtimeRoot, packaged, pilot);
|
|
391
|
+
}
|
|
392
|
+
console.log('PASS #365 step edit-accounting mechanism test');
|
|
393
|
+
} finally {
|
|
394
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
395
|
+
}
|