harness-everything 0.11.2 → 0.11.4
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 +12 -0
- package/ci/mechanism-2af-worktree-isolation.test.js +34 -0
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +4 -1
- package/ci/mechanism-2f-stop-gate.test.js +53 -0
- package/hooks/scripts/lib/workflow-isolation.js +26 -8
- package/hooks/scripts/workflow-stop-gate.js +9 -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/lib/workflow-isolation.js +26 -8
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +9 -1
- package/plugins/harness-everything/plugin.json +1 -1
|
@@ -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.11.
|
|
13
|
+
"version": "0.11.4",
|
|
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.11.
|
|
4
|
+
"version": "0.11.4",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
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.11.4](https://github.com/dyphn1/Harness-everything/compare/v0.11.3...v0.11.4) (2026-09-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **workflow:** Stop classifyShell misclassifying safe read-only chains as mutations ([96d687a](https://github.com/dyphn1/Harness-everything/commit/96d687ab4437ab19e3a743d1b9bf5113cfe52525)), closes [82/#149](https://github.com/82/Harness-everything/issues/149) [#155](https://github.com/dyphn1/Harness-everything/issues/155) [#82](https://github.com/dyphn1/Harness-everything/issues/82) [#133](https://github.com/dyphn1/Harness-everything/issues/133) [#149](https://github.com/dyphn1/Harness-everything/issues/149) [#153](https://github.com/dyphn1/Harness-everything/issues/153)
|
|
10
|
+
|
|
11
|
+
## [0.11.3](https://github.com/dyphn1/Harness-everything/compare/v0.11.2...v0.11.3) (2026-09-18)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **workflow:** Resolve verification-after-edit-missing without a host exit code ([e5dae03](https://github.com/dyphn1/Harness-everything/commit/e5dae03799cca594ac3e4f9d0facca6209dec3c8)), closes [#153](https://github.com/dyphn1/Harness-everything/issues/153)
|
|
16
|
+
|
|
5
17
|
## [0.11.2](https://github.com/dyphn1/Harness-everything/compare/v0.11.1...v0.11.2) (2026-09-18)
|
|
6
18
|
|
|
7
19
|
### Bug Fixes
|
|
@@ -106,6 +106,40 @@ try {
|
|
|
106
106
|
const readOnly = runGate('Bash', repo, { command: 'git status --short' });
|
|
107
107
|
check(readOnly.status === 0, 'read-only discovery remains allowed before isolation');
|
|
108
108
|
|
|
109
|
+
// #82-family follow-up: chaining/redirects that are hidden behind the old
|
|
110
|
+
// blanket metacharacter reject must stay blocked, but a compound command
|
|
111
|
+
// whose every segment is independently trusted read-only must not burn
|
|
112
|
+
// iteration budget or the mutation-timestamp just because it uses `;`,
|
|
113
|
+
// `&&`, `|`, or a non-writing `2>&1` redirect.
|
|
114
|
+
for (const command of [
|
|
115
|
+
'git status --short; git log --oneline -1',
|
|
116
|
+
'git status --short && git log --oneline -1',
|
|
117
|
+
'git log --oneline -1 | head -1',
|
|
118
|
+
'echo hello',
|
|
119
|
+
'echo hello; git status --short',
|
|
120
|
+
'git status --short 2>&1',
|
|
121
|
+
'gh issue view 1 --repo octocat/hello-world --json number,title 2>&1',
|
|
122
|
+
'gh pr list --repo octocat/hello-world --json number | head -1',
|
|
123
|
+
'gh repo view octocat/hello-world --json name',
|
|
124
|
+
'gh auth status',
|
|
125
|
+
'cat README.md | grep fixture',
|
|
126
|
+
]) {
|
|
127
|
+
check(runGate('Bash', repo, { command }).status === 0, `trusted read-only chain remains allowed: ${command}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const command of [
|
|
131
|
+
'git status --short; rm -rf src',
|
|
132
|
+
'git status --short | rm -rf src',
|
|
133
|
+
'echo hi & rm -rf src',
|
|
134
|
+
'git status --short > tracked.txt',
|
|
135
|
+
'git status --short 2>tracked.txt',
|
|
136
|
+
'git worktree add "harness-chain-probe" -b harness-chain-probe; rm -rf src',
|
|
137
|
+
'gh issue comment 1 --body hi',
|
|
138
|
+
'gh issue close 1',
|
|
139
|
+
]) {
|
|
140
|
+
check(runGate('Bash', repo, { command }).status === 2, `chained/redirected command is not read-only: ${command}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
const setup = runGate('Bash', repo, { command: `git worktree add "${linked}" -b harness-isolation-test` });
|
|
110
144
|
check(setup.status === 0, 'git worktree creation is allowed as the isolation transition');
|
|
111
145
|
|
|
@@ -175,8 +175,11 @@ try {
|
|
|
175
175
|
check(stop().status === 2, 'direct/iterative route cannot complete unverified mutation');
|
|
176
176
|
check(stop({ stop_hook_active: true }).status === 0 && read(file).state === 'blocked', 'iterative retry reports incomplete state');
|
|
177
177
|
check(control('start').status === 0, 'blocked iterative route can resume without a Fable stage map');
|
|
178
|
+
// Issue #153: a host is not guaranteed to report a numeric exit code at
|
|
179
|
+
// all (state-persist.js's own isFailed logic already treats "no exit code
|
|
180
|
+
// and no stderr" as success), so this must resolve rather than deadlock.
|
|
178
181
|
node('hooks/scripts/state-persist.js', { ...payload, tool_name: 'Bash', tool_input: { command: 'npm test' }, tool_response: { stdout: 'no code' } });
|
|
179
|
-
check(stop().status ===
|
|
182
|
+
check(stop().status === 0 && read(file).state === 'satisfied', 'verification without a host-reported numeric exit status still resolves completion');
|
|
180
183
|
node('hooks/scripts/state-persist.js', { ...payload, tool_name: 'Bash', tool_input: { command: 'npm test' }, tool_response: { exitCode: 0, stdout: 'passed' } });
|
|
181
184
|
check(stop().status === 0 && read(file).state === 'satisfied', 'observed iterative verification resolves completion');
|
|
182
185
|
console.log(`PASS: workflow lifecycle (${passed} assertions)`);
|
|
@@ -20,4 +20,57 @@ helper.check(
|
|
|
20
20
|
);
|
|
21
21
|
fs.unlinkSync(dirtyFile);
|
|
22
22
|
|
|
23
|
+
console.log('\n[2f] workflow-stop-gate (iterative-single) verification signal...');
|
|
24
|
+
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
|
|
27
|
+
// Bug reproduction (issue #153): a verify-classified command that genuinely
|
|
28
|
+
// succeeded but whose host never reported a numeric exit code (observed on
|
|
29
|
+
// at least one live host - state-persist.js writes lastVerifyExitCode: null
|
|
30
|
+
// for any non-failing command when the PostToolUse payload has no numeric
|
|
31
|
+
// exitCode) must not be treated as unverified.
|
|
32
|
+
helper.writeState('workflow-run.json', {
|
|
33
|
+
schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
|
|
34
|
+
state: 'running', lastMutationAt: now - 5000,
|
|
35
|
+
});
|
|
36
|
+
helper.writeState('handoff-state.json', {
|
|
37
|
+
status: 'idle', lastEditAt: now - 5000, lastVerifyAt: now, lastVerifyExitCode: null,
|
|
38
|
+
});
|
|
39
|
+
const hostExitCodeUnavailable = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
|
|
40
|
+
helper.check(
|
|
41
|
+
'2f. workflow-stop-gate resolves when verify ran after edit even without a numeric exit code',
|
|
42
|
+
hostExitCodeUnavailable.code === 0 && helper.readState('workflow-run.json').state === 'satisfied',
|
|
43
|
+
`Got exit=${hostExitCodeUnavailable.code}, stderr="${hostExitCodeUnavailable.stderr.slice(0, 300)}"`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Regression guard: explicit exit 0 (host does report it) must still resolve.
|
|
47
|
+
helper.writeState('workflow-run.json', {
|
|
48
|
+
schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
|
|
49
|
+
state: 'running', lastMutationAt: now - 5000,
|
|
50
|
+
});
|
|
51
|
+
helper.writeState('handoff-state.json', {
|
|
52
|
+
status: 'idle', lastEditAt: now - 5000, lastVerifyAt: now, lastVerifyExitCode: 0,
|
|
53
|
+
});
|
|
54
|
+
const hostExitCodeReported = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
|
|
55
|
+
helper.check(
|
|
56
|
+
'2f. workflow-stop-gate still resolves when the host does report exit code 0',
|
|
57
|
+
hostExitCodeReported.code === 0 && helper.readState('workflow-run.json').state === 'satisfied',
|
|
58
|
+
`Got exit=${hostExitCodeReported.code}`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Negative control: no verification ran after the edit at all -> stays blocked.
|
|
62
|
+
helper.writeState('workflow-run.json', {
|
|
63
|
+
schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
|
|
64
|
+
state: 'running', lastMutationAt: now,
|
|
65
|
+
});
|
|
66
|
+
helper.writeState('handoff-state.json', {
|
|
67
|
+
status: 'idle', lastEditAt: now, lastVerifyAt: now - 5000, lastVerifyExitCode: null,
|
|
68
|
+
});
|
|
69
|
+
const staleVerify = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
|
|
70
|
+
helper.check(
|
|
71
|
+
'2f. workflow-stop-gate still blocks when no verification ran after the edit',
|
|
72
|
+
staleVerify.code === 2 && staleVerify.stderr.includes('verification-after-edit-missing'),
|
|
73
|
+
`Got exit=${staleVerify.code}, stderr="${staleVerify.stderr.slice(0, 300)}"`
|
|
74
|
+
);
|
|
75
|
+
|
|
23
76
|
helper.finish();
|
|
@@ -43,16 +43,34 @@ function linkedWorktree(cwd, boundRoot) {
|
|
|
43
43
|
return worktrees.some(field => field.startsWith('worktree ') && key(field.slice(9)) === key(top)) ? top : null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// A conservative read subset, not a general shell sandbox.
|
|
46
|
+
// A conservative read subset, not a general shell sandbox. Chaining (`;`, `&&`,
|
|
47
|
+
// `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect are permitted only
|
|
48
|
+
// when every resulting segment independently matches the trusted read-only set
|
|
49
|
+
// below; any other metacharacter (substitution, grouping, a lone `&`, a file
|
|
50
|
+
// redirect, `<`) still forces mutation-or-unknown for the whole command.
|
|
51
|
+
const SHELL_SEPARATOR_RE = /\s*(?:&&|\|\||;|\|)\s*/;
|
|
52
|
+
const SAFE_FD_REDIRECT_RE = /[12]>&[12]/g;
|
|
53
|
+
const DANGEROUS_FLAG_RE = /\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i;
|
|
54
|
+
const READ_ONLY_PREFIX_RE = /^(?:git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)|git\s+branch\s+--show-current$|git\s+worktree\s+list(?:\s|$)|gh\s+(?:issue|pr)\s+(?:view|list)(?:\s|$)|gh\s+repo\s+view(?:\s|$)|gh\s+auth\s+status(?:\s|$)|(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|grep|echo|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$))/i;
|
|
55
|
+
|
|
56
|
+
function isReadOnlySegment(segment) {
|
|
57
|
+
return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
function classifyShell(command) {
|
|
48
61
|
const text = String(command || '').trim();
|
|
49
|
-
if (!text || /[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
if (!text || /[`$(){}\r\n\x00]/.test(text)) return 'mutation-or-unknown';
|
|
63
|
+
// Strip the safe `2>&1`/`1>&2` fd-duplication redirect first: it contains a
|
|
64
|
+
// literal `&` that must not be mistaken for a lone backgrounding `&`.
|
|
65
|
+
const withoutSafeRedirects = text.replace(SAFE_FD_REDIRECT_RE, ' ');
|
|
66
|
+
if (/&/.test(withoutSafeRedirects.replace(/&&/g, ''))) return 'mutation-or-unknown';
|
|
67
|
+
if (/[<>]/.test(withoutSafeRedirects)) return 'mutation-or-unknown';
|
|
68
|
+
const segments = text.split(SHELL_SEPARATOR_RE).map(segment => segment.replace(SAFE_FD_REDIRECT_RE, '').trim());
|
|
69
|
+
if (segments.length === 1) {
|
|
70
|
+
if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
|
|
71
|
+
return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
|
|
72
|
+
}
|
|
73
|
+
return segments.every(segment => segment && isReadOnlySegment(segment)) ? 'read-only' : 'mutation-or-unknown';
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
|
|
@@ -21,7 +21,15 @@ function decide(payload) {
|
|
|
21
21
|
} else {
|
|
22
22
|
const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
|
|
23
23
|
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
24
|
-
|
|
24
|
+
// state-persist.js only ever records lastVerifyAt/lastVerifyExitCode from
|
|
25
|
+
// its `!isFailed` branch, so reaching this timestamp already proves the
|
|
26
|
+
// command succeeded. Do not additionally require lastVerifyExitCode===0:
|
|
27
|
+
// hosts are not guaranteed to report a numeric exit code at all (some
|
|
28
|
+
// PostToolUse payloads omit it entirely), in which case state-persist.js
|
|
29
|
+
// correctly records success with lastVerifyExitCode: null, and requiring
|
|
30
|
+
// a literal 0 here would make this gate permanently unresolvable on that
|
|
31
|
+
// host after any edit.
|
|
32
|
+
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved = ['verification-after-edit-missing'];
|
|
25
33
|
}
|
|
26
34
|
if (unresolved.length) {
|
|
27
35
|
workflow.unresolved = unresolved;
|
package/package.json
CHANGED
|
@@ -43,16 +43,34 @@ function linkedWorktree(cwd, boundRoot) {
|
|
|
43
43
|
return worktrees.some(field => field.startsWith('worktree ') && key(field.slice(9)) === key(top)) ? top : null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// A conservative read subset, not a general shell sandbox.
|
|
46
|
+
// A conservative read subset, not a general shell sandbox. Chaining (`;`, `&&`,
|
|
47
|
+
// `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect are permitted only
|
|
48
|
+
// when every resulting segment independently matches the trusted read-only set
|
|
49
|
+
// below; any other metacharacter (substitution, grouping, a lone `&`, a file
|
|
50
|
+
// redirect, `<`) still forces mutation-or-unknown for the whole command.
|
|
51
|
+
const SHELL_SEPARATOR_RE = /\s*(?:&&|\|\||;|\|)\s*/;
|
|
52
|
+
const SAFE_FD_REDIRECT_RE = /[12]>&[12]/g;
|
|
53
|
+
const DANGEROUS_FLAG_RE = /\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i;
|
|
54
|
+
const READ_ONLY_PREFIX_RE = /^(?:git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)|git\s+branch\s+--show-current$|git\s+worktree\s+list(?:\s|$)|gh\s+(?:issue|pr)\s+(?:view|list)(?:\s|$)|gh\s+repo\s+view(?:\s|$)|gh\s+auth\s+status(?:\s|$)|(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|grep|echo|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$))/i;
|
|
55
|
+
|
|
56
|
+
function isReadOnlySegment(segment) {
|
|
57
|
+
return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
function classifyShell(command) {
|
|
48
61
|
const text = String(command || '').trim();
|
|
49
|
-
if (!text || /[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
if (!text || /[`$(){}\r\n\x00]/.test(text)) return 'mutation-or-unknown';
|
|
63
|
+
// Strip the safe `2>&1`/`1>&2` fd-duplication redirect first: it contains a
|
|
64
|
+
// literal `&` that must not be mistaken for a lone backgrounding `&`.
|
|
65
|
+
const withoutSafeRedirects = text.replace(SAFE_FD_REDIRECT_RE, ' ');
|
|
66
|
+
if (/&/.test(withoutSafeRedirects.replace(/&&/g, ''))) return 'mutation-or-unknown';
|
|
67
|
+
if (/[<>]/.test(withoutSafeRedirects)) return 'mutation-or-unknown';
|
|
68
|
+
const segments = text.split(SHELL_SEPARATOR_RE).map(segment => segment.replace(SAFE_FD_REDIRECT_RE, '').trim());
|
|
69
|
+
if (segments.length === 1) {
|
|
70
|
+
if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
|
|
71
|
+
return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
|
|
72
|
+
}
|
|
73
|
+
return segments.every(segment => segment && isReadOnlySegment(segment)) ? 'read-only' : 'mutation-or-unknown';
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
|
|
@@ -21,7 +21,15 @@ function decide(payload) {
|
|
|
21
21
|
} else {
|
|
22
22
|
const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
|
|
23
23
|
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
24
|
-
|
|
24
|
+
// state-persist.js only ever records lastVerifyAt/lastVerifyExitCode from
|
|
25
|
+
// its `!isFailed` branch, so reaching this timestamp already proves the
|
|
26
|
+
// command succeeded. Do not additionally require lastVerifyExitCode===0:
|
|
27
|
+
// hosts are not guaranteed to report a numeric exit code at all (some
|
|
28
|
+
// PostToolUse payloads omit it entirely), in which case state-persist.js
|
|
29
|
+
// correctly records success with lastVerifyExitCode: null, and requiring
|
|
30
|
+
// a literal 0 here would make this gate permanently unresolvable on that
|
|
31
|
+
// host after any edit.
|
|
32
|
+
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved = ['verification-after-edit-missing'];
|
|
25
33
|
}
|
|
26
34
|
if (unresolved.length) {
|
|
27
35
|
workflow.unresolved = unresolved;
|
|
@@ -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.11.
|
|
4
|
+
"version": "0.11.4",
|
|
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",
|