harness-everything 0.5.0 → 0.5.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 +19 -0
- package/CHANGELOG.md +13 -0
- package/ci/mechanism-2a-rule-of-3.test.js +64 -0
- package/ci/mechanism-2x-release-automation.test.js +16 -5
- package/ci/semantic-release-analyzer.test.mjs +79 -0
- package/docs/repository-contract.md +1 -1
- package/hooks/hooks.json +11 -0
- package/hooks/scripts/rule-of-3-tracker.js +10 -5
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +10 -5
- 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.5.
|
|
13
|
+
"version": "0.5.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.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -45,6 +45,25 @@ jobs:
|
|
|
45
45
|
- name: Verify installer contract on Node 22
|
|
46
46
|
run: node ci/installer-roundtrip.test.js
|
|
47
47
|
|
|
48
|
+
release-semantics:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v7
|
|
52
|
+
with:
|
|
53
|
+
fetch-depth: 0
|
|
54
|
+
- uses: actions/setup-node@v7
|
|
55
|
+
with:
|
|
56
|
+
node-version: 24
|
|
57
|
+
- name: Install project dependencies
|
|
58
|
+
run: npm ci
|
|
59
|
+
- name: Install pinned semantic-release analyzer tooling
|
|
60
|
+
run: >-
|
|
61
|
+
npm install --no-save --package-lock=false --ignore-scripts
|
|
62
|
+
@semantic-release/commit-analyzer@13.0.1
|
|
63
|
+
conventional-changelog-conventionalcommits@10.4.0
|
|
64
|
+
- name: Verify semantic-release against real Git histories
|
|
65
|
+
run: npm run test:release:semantics
|
|
66
|
+
|
|
48
67
|
skill-quality:
|
|
49
68
|
runs-on: ubuntu-latest
|
|
50
69
|
steps:
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.2](https://github.com/dyphn1/Harness-everything/compare/v0.5.1...v0.5.2) (2026-09-16)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **release:** close semantic-release contract gaps ([79d2a76](https://github.com/dyphn1/Harness-everything/commit/79d2a76c931eaaa8928334877ca2b1195e06cce4))
|
|
10
|
+
|
|
11
|
+
## [0.5.1](https://github.com/dyphn1/Harness-everything/compare/v0.5.0...v0.5.1) (2026-09-16)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **rule-of-3:** parse Claude failure hook payload ([83d3106](https://github.com/dyphn1/Harness-everything/commit/83d310653d19fd72f597d119eface1c3baf5a7bd))
|
|
16
|
+
* **rule-of-3:** wire Claude failure hook to tracker ([11b3e9e](https://github.com/dyphn1/Harness-everything/commit/11b3e9e7a08b0394d2dd3dac78bd1464ae5881df))
|
|
17
|
+
|
|
5
18
|
## [0.5.0](https://github.com/dyphn1/Harness-everything/compare/v0.4.4...v0.5.0) (2026-09-16)
|
|
6
19
|
|
|
7
20
|
### Features
|
|
@@ -4,6 +4,70 @@ const helper = require('./test-helper');
|
|
|
4
4
|
|
|
5
5
|
console.log('\n[2a] Rule of 3 circuit breaker...');
|
|
6
6
|
|
|
7
|
+
const hooksConfig = JSON.parse(
|
|
8
|
+
fs.readFileSync(path.join(helper.root, 'hooks', 'hooks.json'), 'utf8')
|
|
9
|
+
);
|
|
10
|
+
const failureTrackerHook = (hooksConfig.hooks.PostToolUseFailure || [])
|
|
11
|
+
.find(entry => entry.id === 'harness:post-failure:rule-of-3-tracker');
|
|
12
|
+
helper.check(
|
|
13
|
+
'2a-claude-wiring. PostToolUseFailure invokes the Rule-of-3 tracker',
|
|
14
|
+
failureTrackerHook &&
|
|
15
|
+
failureTrackerHook.matcher === 'Bash|PowerShell' &&
|
|
16
|
+
failureTrackerHook.hooks?.some(hook => hook.command.includes('rule-of-3-tracker.js')),
|
|
17
|
+
`Hook=${JSON.stringify(failureTrackerHook)}`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const claudeFailurePayload = {
|
|
21
|
+
hook_event_name: 'PostToolUseFailure',
|
|
22
|
+
tool_name: 'Bash',
|
|
23
|
+
tool_input: { command: 'node -e "process.exit(1)"' },
|
|
24
|
+
error: 'Exit code 1\ncommand failed',
|
|
25
|
+
session_id: helper.SESSION_ID,
|
|
26
|
+
};
|
|
27
|
+
const failureRuns = [1, 2, 3].map(() =>
|
|
28
|
+
helper.runHook('rule-of-3-tracker.js', claudeFailurePayload)
|
|
29
|
+
);
|
|
30
|
+
const trackedFailureState = helper.readState('rule-of-3-state.json');
|
|
31
|
+
helper.check(
|
|
32
|
+
'2a-claude-failure. PostToolUseFailure error payload increments the same signature to threshold',
|
|
33
|
+
failureRuns.every(result => result.code === 0) &&
|
|
34
|
+
trackedFailureState.count === 3 &&
|
|
35
|
+
trackedFailureState.threshold === 3 &&
|
|
36
|
+
trackedFailureState.lastHash &&
|
|
37
|
+
trackedFailureState.zoomOutResolved === false,
|
|
38
|
+
`Runs=${JSON.stringify(failureRuns)}, state=${JSON.stringify(trackedFailureState)}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const claudeTripResult = helper.runHook('rule-of-3.js', {
|
|
42
|
+
hook_event_name: 'PreToolUse',
|
|
43
|
+
tool_name: 'Bash',
|
|
44
|
+
tool_input: { command: 'echo retry' },
|
|
45
|
+
session_id: helper.SESSION_ID,
|
|
46
|
+
});
|
|
47
|
+
helper.check(
|
|
48
|
+
'2a-claude-trip. Tracked Claude failures block the next mutation',
|
|
49
|
+
claudeTripResult.code === 2 &&
|
|
50
|
+
claudeTripResult.stderr.includes('RULE OF 3 CIRCUIT BREAKER TRIGGERED'),
|
|
51
|
+
`Got exit=${claudeTripResult.code}, stderr="${claudeTripResult.stderr.slice(0, 200)}"`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const successResult = helper.runHook('rule-of-3-tracker.js', {
|
|
55
|
+
hook_event_name: 'PostToolUse',
|
|
56
|
+
tool_name: 'Bash',
|
|
57
|
+
tool_input: { command: 'echo success' },
|
|
58
|
+
tool_response: { stdout: 'success', stderr: '', exitCode: 0 },
|
|
59
|
+
session_id: helper.SESSION_ID,
|
|
60
|
+
});
|
|
61
|
+
const resetState = helper.readState('rule-of-3-state.json');
|
|
62
|
+
helper.check(
|
|
63
|
+
'2a-claude-success. Confirmed PostToolUse success keeps the existing reset behavior',
|
|
64
|
+
successResult.code === 0 &&
|
|
65
|
+
resetState.count === 0 &&
|
|
66
|
+
resetState.zoomOutResolved === true &&
|
|
67
|
+
resetState.zoomOutCycles === 0,
|
|
68
|
+
`Got exit=${successResult.code}, state=${JSON.stringify(resetState)}`
|
|
69
|
+
);
|
|
70
|
+
|
|
7
71
|
helper.writeState('rule-of-3-state.json', { count: 3, lastHash: 'mech-test', zoomOutResolved: false });
|
|
8
72
|
const tripResult = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
|
|
9
73
|
helper.check(
|
|
@@ -52,12 +52,12 @@ for (const invalid of ['1.2.3-beta', '1.2.3+build', '01.2.3', '1.2', 'v1.2.3', '
|
|
|
52
52
|
assert.ok(!isStableSemVer(invalid), invalid);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
55
|
+
// Once semantic-release has established the stable release line, every
|
|
56
|
+
// release-owned checked-in version target must remain in lockstep. This is a
|
|
57
|
+
// normal CI invariant, not something deferred until the next release run.
|
|
58
58
|
const packageVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
|
|
59
59
|
assert.ok(isStableSemVer(packageVersion), `package.json must use stable SemVer, got ${packageVersion}`);
|
|
60
|
-
for (const target of VERSION_TARGETS
|
|
60
|
+
for (const target of VERSION_TARGETS) {
|
|
61
61
|
const data = JSON.parse(fs.readFileSync(path.join(ROOT, target.file), 'utf8'));
|
|
62
62
|
const current = target.get(data);
|
|
63
63
|
const values = Array.isArray(current) ? current : [current];
|
|
@@ -120,6 +120,17 @@ json(portable, tampered);
|
|
|
120
120
|
assert.throws(() => syncReleaseVersion(root, '1.2.3', { checkOnly: true }), /Release version drift/);
|
|
121
121
|
assert.throws(() => syncReleaseVersion(root, '1.2.3-beta'), /stable SemVer/);
|
|
122
122
|
|
|
123
|
+
const lockDrift = fixture();
|
|
124
|
+
syncReleaseVersion(lockDrift, '1.2.3');
|
|
125
|
+
const driftedLock = JSON.parse(fs.readFileSync(path.join(lockDrift, 'package-lock.json'), 'utf8'));
|
|
126
|
+
driftedLock.packages[''].version = '1.2.2';
|
|
127
|
+
json(path.join(lockDrift, 'package-lock.json'), driftedLock);
|
|
128
|
+
assert.throws(
|
|
129
|
+
() => syncReleaseVersion(lockDrift, '1.2.3', { checkOnly: true }),
|
|
130
|
+
/package-lock\.json/,
|
|
131
|
+
'release verification must reject package-lock drift'
|
|
132
|
+
);
|
|
133
|
+
|
|
123
134
|
const missing = fixture();
|
|
124
135
|
const packageBefore = fs.readFileSync(path.join(missing, 'package.json'), 'utf8');
|
|
125
136
|
fs.rmSync(path.join(missing, 'opencode-plugin/plugin.json'));
|
|
@@ -154,4 +165,4 @@ assert.strictEqual(versionOfSkill(path.join(changed, 'alpha/nested/SKILL.md')),
|
|
|
154
165
|
assert.strictEqual(versionOfSkill(path.join(changed, 'beta/SKILL.md')), '0.1.0');
|
|
155
166
|
syncReleaseVersion(changed, '0.2.0', { base: 'v0.1.0', checkOnly: true });
|
|
156
167
|
|
|
157
|
-
console.log('Release automation contract verified: stable SemVer rules, changed-skill versioning,
|
|
168
|
+
console.log('Release automation contract verified: stable SemVer rules, full manifest lockstep, changed-skill versioning, drift detection, idempotency, and fail-before-write behavior.');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { analyzeCommits } from '@semantic-release/commit-analyzer';
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(import.meta.dirname, '..');
|
|
9
|
+
const config = JSON.parse(fs.readFileSync(path.join(ROOT, '.releaserc.json'), 'utf8'));
|
|
10
|
+
const analyzer = config.plugins.find(plugin => Array.isArray(plugin) && plugin[0] === '@semantic-release/commit-analyzer');
|
|
11
|
+
assert.ok(analyzer, 'commit analyzer configuration missing');
|
|
12
|
+
|
|
13
|
+
function git(root, args) {
|
|
14
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function history(messages) {
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-semantic-history-'));
|
|
19
|
+
try {
|
|
20
|
+
git(root, ['init']);
|
|
21
|
+
git(root, ['config', 'user.email', 'release-test@example.com']);
|
|
22
|
+
git(root, ['config', 'user.name', 'Release Test']);
|
|
23
|
+
git(root, ['commit', '--allow-empty', '-m', 'chore: baseline']);
|
|
24
|
+
git(root, ['tag', 'v1.0.0']);
|
|
25
|
+
|
|
26
|
+
for (const message of messages) {
|
|
27
|
+
git(root, ['commit', '--allow-empty', '-m', message]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const log = execFileSync(
|
|
31
|
+
'git',
|
|
32
|
+
['log', '--reverse', '--format=%B%x00', 'v1.0.0..HEAD'],
|
|
33
|
+
{ cwd: root, encoding: 'utf8' }
|
|
34
|
+
);
|
|
35
|
+
return log.split('\0').map(message => message.trim()).filter(Boolean);
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function releaseType(messages) {
|
|
42
|
+
const commits = history(messages).map((message, index) => ({
|
|
43
|
+
hash: `fixture-${index}`,
|
|
44
|
+
message
|
|
45
|
+
}));
|
|
46
|
+
return analyzeCommits(analyzer[1], {
|
|
47
|
+
commits,
|
|
48
|
+
logger: { log() {} }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assert.equal(await releaseType(['fix(cache): repair invalidation']), 'patch', 'fix-only history must release patch');
|
|
53
|
+
assert.equal(
|
|
54
|
+
await releaseType(['fix(cache): repair invalidation', 'feat(router): add route']),
|
|
55
|
+
'minor',
|
|
56
|
+
'feat must dominate patch commits'
|
|
57
|
+
);
|
|
58
|
+
assert.equal(
|
|
59
|
+
await releaseType(['fix(cache): repair invalidation', 'feat(api)!: replace runtime contract']),
|
|
60
|
+
'major',
|
|
61
|
+
'breaking ! must dominate lower release types'
|
|
62
|
+
);
|
|
63
|
+
assert.equal(
|
|
64
|
+
await releaseType(['feat(api): replace shape\n\nBREAKING CHANGE: remove legacy shape']),
|
|
65
|
+
'major',
|
|
66
|
+
'BREAKING CHANGE footer must produce major'
|
|
67
|
+
);
|
|
68
|
+
assert.equal(
|
|
69
|
+
(await releaseType(['docs: update guide', 'test: cover retry', 'ci: tune matrix'])) ?? null,
|
|
70
|
+
null,
|
|
71
|
+
'docs/test/ci-only history must not release'
|
|
72
|
+
);
|
|
73
|
+
assert.equal(
|
|
74
|
+
(await releaseType(['chore(release): 1.0.1 [skip ci]'])) ?? null,
|
|
75
|
+
null,
|
|
76
|
+
'generated release commit must not recurse into another release'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
console.log('Semantic-release analyzer verified against real temporary Git histories: patch, minor, major, no-release, and recursion guard.');
|
|
@@ -19,7 +19,7 @@ This page is generated from checked-in runtime and workflow sources. Do not edit
|
|
|
19
19
|
| Workflow | Node.js versions | Explicit triggers | npm quality gates |
|
|
20
20
|
| --- | --- | --- | --- |
|
|
21
21
|
| `behavioral-evals.yml` | 24 | schedule, workflow_dispatch | n/a |
|
|
22
|
-
| `ci.yml` | 22, 24 | workflow_call | `test`, `test:mutations`, `test:consistency`, `test:references`, `test:release`, `test:collision`, `test:plugin:openai`, `test:plugin:submission`, `test:platform:compatibility` |
|
|
22
|
+
| `ci.yml` | 22, 24 | workflow_call | `test`, `test:mutations`, `test:release:semantics`, `test:consistency`, `test:references`, `test:release`, `test:collision`, `test:plugin:openai`, `test:plugin:submission`, `test:platform:compatibility` |
|
|
23
23
|
| `release.yml` | 24 | workflow_dispatch | n/a |
|
|
24
24
|
|
|
25
25
|
## Drift prevention
|
package/hooks/hooks.json
CHANGED
|
@@ -194,6 +194,17 @@
|
|
|
194
194
|
],
|
|
195
195
|
"description": "Record that an approved action-gate payload executed but failed",
|
|
196
196
|
"id": "harness:post-failure:action-gate-audit"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"matcher": "Bash|PowerShell",
|
|
200
|
+
"hooks": [
|
|
201
|
+
{
|
|
202
|
+
"type": "command",
|
|
203
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/rule-of-3-tracker.js\""
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"description": "Track failed tool error signatures for Rule of 3",
|
|
207
|
+
"id": "harness:post-failure:rule-of-3-tracker"
|
|
197
208
|
}
|
|
198
209
|
]
|
|
199
210
|
}
|
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
6
6
|
|
|
7
|
-
// PostToolUse hooks in Claude Code
|
|
7
|
+
// PostToolUse/PostToolUseFailure hooks in Claude Code receive their payload via stdin
|
|
8
8
|
let inputData = '';
|
|
9
9
|
|
|
10
10
|
process.stdin.on('data', chunk => {
|
|
@@ -14,18 +14,23 @@ process.stdin.on('data', chunk => {
|
|
|
14
14
|
process.stdin.on('end', () => {
|
|
15
15
|
try {
|
|
16
16
|
const payload = JSON.parse(inputData);
|
|
17
|
+
const hookEventName = payload.hook_event_name || payload.hookEventName || '';
|
|
18
|
+
const isFailureEvent = hookEventName === 'PostToolUseFailure';
|
|
17
19
|
const toolResponse = payload.tool_response || {};
|
|
18
20
|
const stdout = toolResponse.stdout ?? payload.stdout ?? '';
|
|
19
21
|
const stderr = toolResponse.stderr ?? payload.stderr ?? '';
|
|
20
|
-
const
|
|
22
|
+
const failureText = typeof payload.error === 'string' ? payload.error : '';
|
|
23
|
+
const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode ?? payload.exit_code;
|
|
21
24
|
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
22
25
|
|
|
23
26
|
const explicitFailure = exitCode !== undefined && exitCode !== 0;
|
|
24
27
|
const explicitSuccess = exitCode === 0;
|
|
25
28
|
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
26
29
|
const looksLikeError = /\b(error|fail|failed|failure|exception|fatal|panic|traceback|denied|refused|cannot|unable)\b/i.test(stderr);
|
|
27
|
-
const isFailure = explicitFailure || (exitCode === undefined && stderrSignal && looksLikeError);
|
|
28
|
-
const errorText =
|
|
30
|
+
const isFailure = isFailureEvent || explicitFailure || (exitCode === undefined && stderrSignal && looksLikeError);
|
|
31
|
+
const errorText = isFailureEvent
|
|
32
|
+
? (failureText || (stderrSignal ? stderr : stdout) || '')
|
|
33
|
+
: ((stderrSignal ? stderr : stdout) || failureText || '');
|
|
29
34
|
|
|
30
35
|
const root = getWorkspaceRoot(payload);
|
|
31
36
|
const stateFile = path.join(getSessionDir(root, payload.session_id || payload.sessionId), 'rule-of-3-state.json');
|
|
@@ -108,7 +113,7 @@ process.stdin.on('end', () => {
|
|
|
108
113
|
state.lastFailureAt = Date.now();
|
|
109
114
|
|
|
110
115
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
111
|
-
} else if (explicitSuccess) {
|
|
116
|
+
} else if (!isFailureEvent && explicitSuccess) {
|
|
112
117
|
// Only reset on a *confirmed* zero exit code, not merely "not a failure".
|
|
113
118
|
if (state.count > 0 || state.zoomOutCycles > 0) {
|
|
114
119
|
state.count = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
|
|
5
5
|
"main": "harness-everything/scripts/bootstrap.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"test:docs:capabilities": "node ci/doc-capability-consistency.test.js",
|
|
21
21
|
"test:references": "node ci/reference-check.js",
|
|
22
22
|
"test:release": "node ci/release-consistency-check.js && node ci/mechanism-2x-release-automation.test.js && node ci/mechanism-2x-release-commit-message.test.js",
|
|
23
|
+
"test:release:semantics": "node ci/semantic-release-analyzer.test.mjs",
|
|
23
24
|
"test:install": "node scripts/verify-install.js",
|
|
24
25
|
"test:collision": "node ci/description-collision.js",
|
|
25
26
|
"test:routing:skills": "node ci/skill-routing-check.js",
|
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
6
6
|
|
|
7
|
-
// PostToolUse hooks in Claude Code
|
|
7
|
+
// PostToolUse/PostToolUseFailure hooks in Claude Code receive their payload via stdin
|
|
8
8
|
let inputData = '';
|
|
9
9
|
|
|
10
10
|
process.stdin.on('data', chunk => {
|
|
@@ -14,18 +14,23 @@ process.stdin.on('data', chunk => {
|
|
|
14
14
|
process.stdin.on('end', () => {
|
|
15
15
|
try {
|
|
16
16
|
const payload = JSON.parse(inputData);
|
|
17
|
+
const hookEventName = payload.hook_event_name || payload.hookEventName || '';
|
|
18
|
+
const isFailureEvent = hookEventName === 'PostToolUseFailure';
|
|
17
19
|
const toolResponse = payload.tool_response || {};
|
|
18
20
|
const stdout = toolResponse.stdout ?? payload.stdout ?? '';
|
|
19
21
|
const stderr = toolResponse.stderr ?? payload.stderr ?? '';
|
|
20
|
-
const
|
|
22
|
+
const failureText = typeof payload.error === 'string' ? payload.error : '';
|
|
23
|
+
const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode ?? payload.exit_code;
|
|
21
24
|
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
22
25
|
|
|
23
26
|
const explicitFailure = exitCode !== undefined && exitCode !== 0;
|
|
24
27
|
const explicitSuccess = exitCode === 0;
|
|
25
28
|
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
26
29
|
const looksLikeError = /\b(error|fail|failed|failure|exception|fatal|panic|traceback|denied|refused|cannot|unable)\b/i.test(stderr);
|
|
27
|
-
const isFailure = explicitFailure || (exitCode === undefined && stderrSignal && looksLikeError);
|
|
28
|
-
const errorText =
|
|
30
|
+
const isFailure = isFailureEvent || explicitFailure || (exitCode === undefined && stderrSignal && looksLikeError);
|
|
31
|
+
const errorText = isFailureEvent
|
|
32
|
+
? (failureText || (stderrSignal ? stderr : stdout) || '')
|
|
33
|
+
: ((stderrSignal ? stderr : stdout) || failureText || '');
|
|
29
34
|
|
|
30
35
|
const root = getWorkspaceRoot(payload);
|
|
31
36
|
const stateFile = path.join(getSessionDir(root, payload.session_id || payload.sessionId), 'rule-of-3-state.json');
|
|
@@ -108,7 +113,7 @@ process.stdin.on('end', () => {
|
|
|
108
113
|
state.lastFailureAt = Date.now();
|
|
109
114
|
|
|
110
115
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
111
|
-
} else if (explicitSuccess) {
|
|
116
|
+
} else if (!isFailureEvent && explicitSuccess) {
|
|
112
117
|
// Only reset on a *confirmed* zero exit code, not merely "not a failure".
|
|
113
118
|
if (state.count > 0 || state.zoomOutCycles > 0) {
|
|
114
119
|
state.count = 0;
|
|
@@ -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.5.
|
|
4
|
+
"version": "0.5.2",
|
|
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",
|