harness-everything 0.6.0 → 0.7.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 +12 -0
- package/ci/invariant-routing.test.js +15 -0
- package/ci/mechanism-2ac-atomic-commit-check.test.js +111 -0
- package/ci/mechanism-2ad-action-gate-classifier-hardening.test.js +90 -0
- package/harness-everything/SKILL.md +5 -4
- package/harness-everything/references/triage-and-tiers.md +13 -10
- package/harness-everything/scripts/kernel-router.js +27 -0
- package/hooks/scripts/action-gate-rules.json +10 -10
- package/hooks/scripts/action-gate.js +21 -11
- package/hooks/scripts/atomic-commit-check.js +117 -9
- 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-rules.json +10 -10
- package/plugins/harness-everything/hooks/scripts/action-gate.js +21 -11
- package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +117 -9
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +5 -4
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +13 -10
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +27 -0
|
@@ -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.7.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.7.0",
|
|
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.7.0](https://github.com/dyphn1/Harness-everything/compare/v0.6.1...v0.7.0) (2026-09-17)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **router:** make routing checkpoint observable ([#139](https://github.com/dyphn1/Harness-everything/issues/139)) ([97dc06d](https://github.com/dyphn1/Harness-everything/commit/97dc06d6b07747ba5e9c328838b8d535c9585b00)), closes [#282](https://github.com/dyphn1/Harness-everything/issues/282)
|
|
10
|
+
|
|
11
|
+
## [0.6.1](https://github.com/dyphn1/Harness-everything/compare/v0.6.0...v0.6.1) (2026-09-17)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **hooks:** harden atomic commit and destructive action gates ([#132](https://github.com/dyphn1/Harness-everything/issues/132)) ([e7dee3a](https://github.com/dyphn1/Harness-everything/commit/e7dee3aed9bc2c2922151743e573f62ecdd60039)), closes [#90](https://github.com/dyphn1/Harness-everything/issues/90) [#100](https://github.com/dyphn1/Harness-everything/issues/100)
|
|
16
|
+
|
|
5
17
|
## [0.6.0](https://github.com/dyphn1/Harness-everything/compare/v0.5.3...v0.6.0) (2026-09-17)
|
|
6
18
|
|
|
7
19
|
### Features
|
|
@@ -40,6 +40,14 @@ check(tier2.stdout.includes('tdd:'), 'Tier 2 can still recommend TDD');
|
|
|
40
40
|
check(!tier2.stdout.includes('BASE EXECUTION LOOP'), 'legacy fixed base-execution-loop output is suppressed');
|
|
41
41
|
check(tier2.stdout.includes('Do not enforce workflow order'), 'self-orchestration policy is explicit');
|
|
42
42
|
|
|
43
|
+
const tier2Checkpoints = tier2.stdout.match(/HARNESS ROUTING CHECKPOINT \(REQUIRED VISIBLE STATE\)/g) || [];
|
|
44
|
+
check(tier2Checkpoints.length === 1, 'Tier 2 emits exactly one required routing checkpoint');
|
|
45
|
+
check(tier2.stdout.includes('- Tier: Tier 2'), 'checkpoint exposes the human-readable Tier 2');
|
|
46
|
+
check(tier2.stdout.includes('- Strategy: iterative-single'), 'checkpoint exposes the selected strategy');
|
|
47
|
+
check(/- Required invariants: .*verify-before-claim/.test(tier2.stdout), 'checkpoint exposes required invariants');
|
|
48
|
+
check(/- Suggested skills: .*tdd/.test(tier2.stdout), 'checkpoint exposes deduplicated suggestions');
|
|
49
|
+
check(tier2.stdout.includes('If all are skipped, state one brief reason'), 'non-empty suggestions carry an all-skipped rationale contract');
|
|
50
|
+
|
|
43
51
|
const tier3 = spawnSync(process.execPath, [kernel, 'audit the entire repository architecture and coordinate multiple modules'], {
|
|
44
52
|
cwd: ROOT,
|
|
45
53
|
encoding: 'utf8',
|
|
@@ -49,11 +57,14 @@ check(/RECOMMENDED TIER:\s*Tier 3/i.test(tier3.stdout), 'macro task remains Tier
|
|
|
49
57
|
check(tier3.stdout.includes('"strategy":"fable-staged"'), 'Tier 3 dependent/unproven work selects fable-staged');
|
|
50
58
|
check(tier3.stdout.includes('fable-mode / fable-discipline'), 'Tier 3 selected plan suggests Fable capabilities');
|
|
51
59
|
check(tier3.stdout.includes('suggested skills remain advisory'), 'Tier 3 suggestions remain non-mandatory');
|
|
60
|
+
check(tier3.stdout.includes('HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE)'), 'Tier 3 also emits the required routing checkpoint');
|
|
52
61
|
|
|
53
62
|
const harnessSkill = read('harness-everything/SKILL.md');
|
|
54
63
|
check(!/Requests that already name a skill/i.test(harnessSkill), 'harness entrypoint no longer opts out when another skill is named');
|
|
55
64
|
check(/including work that already names or strongly matches another skill/i.test(harnessSkill), 'named domain skills still pass through Harness routing');
|
|
56
65
|
check(/not a fixed pipeline/i.test(harnessSkill), 'skill contract rejects fixed pipeline semantics');
|
|
66
|
+
check(/Ensure the emitted \*\*Harness Routing Checkpoint\*\* is user-visible/.test(harnessSkill), 'skill contract requires checkpoint visibility');
|
|
67
|
+
check(/If suggestions are non-empty and you use none, state one brief skip reason/.test(harnessSkill), 'skill contract requires rationale only when all suggestions are skipped');
|
|
57
68
|
|
|
58
69
|
const cognitiveSkill = read('install-cognitive-os/SKILL.md');
|
|
59
70
|
check(/policy, not a required peer-skill selection/i.test(cognitiveSkill), 'Cognitive OS is defined as policy rather than peer-skill dependency');
|
|
@@ -69,6 +80,10 @@ check(triageDoc.includes('Do not enforce workflow order. Enforce workflow invari
|
|
|
69
80
|
check(triageDoc.includes('A tier is **not** a fixed pipeline'), 'tier documentation rejects rigid orchestration');
|
|
70
81
|
check(!triageDoc.includes('MUST load `todo-driven-workflow`'), 'documentation no longer mandates TODO as universal Tier 2/3 first step');
|
|
71
82
|
check(triageDoc.includes('```mermaid'), 'updated architecture retains executable flowchart documentation');
|
|
83
|
+
check(triageDoc.includes('the kernel always emits a compact checkpoint'), 'reference makes checkpoint emission mandatory');
|
|
84
|
+
check(!triageDoc.includes('When a visible checkpoint is useful'), 'reference no longer makes checkpoint visibility optional');
|
|
85
|
+
check(triageDoc.includes('if every suggestion is skipped, state one brief reason'), 'reference records the all-skipped rationale rule');
|
|
86
|
+
check(triageDoc.includes('Kernel emission alone is not proof that a host UI displayed the checkpoint'), 'reference preserves the #82 host-evidence boundary');
|
|
72
87
|
|
|
73
88
|
console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: invariant routing contract (${failed} failure${failed === 1 ? '' : 's'})`);
|
|
74
89
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const canonical = path.join(ROOT, 'hooks', 'scripts', 'atomic-commit-check.js');
|
|
12
|
+
const plugin = path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'atomic-commit-check.js');
|
|
13
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-atomic-commit-'));
|
|
14
|
+
const repo = path.join(temp, 'repo');
|
|
15
|
+
const outside = path.join(temp, 'outside');
|
|
16
|
+
const stateHome = path.join(temp, 'state');
|
|
17
|
+
|
|
18
|
+
function git(args) {
|
|
19
|
+
return execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function invoke({ session, tool = 'Edit', target, patch }) {
|
|
23
|
+
const toolInput = tool === 'apply_patch'
|
|
24
|
+
? { patch }
|
|
25
|
+
: { file_path: target };
|
|
26
|
+
const payload = {
|
|
27
|
+
session_id: session,
|
|
28
|
+
hook_event_name: 'PostToolUse',
|
|
29
|
+
tool_name: tool,
|
|
30
|
+
tool_input: toolInput,
|
|
31
|
+
cwd: repo,
|
|
32
|
+
};
|
|
33
|
+
return spawnSync(process.execPath, [canonical], {
|
|
34
|
+
cwd: repo,
|
|
35
|
+
input: JSON.stringify(payload),
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
env: { ...process.env, HARNESS_STATE_HOME: stateHome },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sixInvocations(args) {
|
|
42
|
+
const results = [];
|
|
43
|
+
for (let i = 0; i < 6; i++) results.push(invoke(args));
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
49
|
+
fs.mkdirSync(outside, { recursive: true });
|
|
50
|
+
git(['init']);
|
|
51
|
+
git(['config', 'user.email', 'harness@example.invalid']);
|
|
52
|
+
git(['config', 'user.name', 'Harness Test']);
|
|
53
|
+
fs.writeFileSync(path.join(repo, 'tracked.txt'), 'base\n', 'utf8');
|
|
54
|
+
git(['add', 'tracked.txt']);
|
|
55
|
+
git(['commit', '-m', 'initial']);
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(
|
|
58
|
+
fs.readFileSync(canonical, 'utf8'),
|
|
59
|
+
fs.readFileSync(plugin, 'utf8'),
|
|
60
|
+
'canonical and packaged atomic-commit hooks must remain byte-identical',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Negative control for #90: keep the repository dirty with a pre-existing,
|
|
64
|
+
// unrelated file. Six host edits outside the workspace must still never
|
|
65
|
+
// increment the repository edit counter or emit a commit nudge.
|
|
66
|
+
fs.writeFileSync(path.join(repo, 'pre-existing-unrelated.txt'), 'unrelated\n', 'utf8');
|
|
67
|
+
const outsideTarget = path.join(outside, 'scratch.txt');
|
|
68
|
+
const outsideResults = sixInvocations({ session: 'outside-edit', target: outsideTarget });
|
|
69
|
+
assert.ok(outsideResults.every(result => result.status === 0), 'outside-workspace Edit calls must never nudge even when the repo is already dirty');
|
|
70
|
+
assert.ok(outsideResults.every(result => !String(result.stderr || '').includes('Atomic Commit Check')), 'outside-workspace Edit calls must stay silent');
|
|
71
|
+
|
|
72
|
+
const outsidePatchResults = sixInvocations({
|
|
73
|
+
session: 'outside-patch',
|
|
74
|
+
tool: 'apply_patch',
|
|
75
|
+
patch: `*** Begin Patch\n*** Update File: ${outsideTarget}\n@@\n-old\n+new\n*** End Patch`,
|
|
76
|
+
});
|
|
77
|
+
assert.ok(outsidePatchResults.every(result => result.status === 0), 'outside-workspace apply_patch headers must not increment the count');
|
|
78
|
+
fs.rmSync(path.join(repo, 'pre-existing-unrelated.txt'));
|
|
79
|
+
|
|
80
|
+
// A clean worktree is never a valid reason to request a commit. The sixth
|
|
81
|
+
// in-workspace event therefore stays non-blocking and resets the stale count.
|
|
82
|
+
const cleanResults = sixInvocations({ session: 'clean-tree', target: path.join(repo, 'tracked.txt') });
|
|
83
|
+
assert.ok(cleanResults.every(result => result.status === 0), 'clean worktree must not nudge at the threshold');
|
|
84
|
+
assert.ok(cleanResults.every(result => !String(result.stderr || '').includes('Atomic Commit Check')), 'clean worktree must not print a commit reminder');
|
|
85
|
+
|
|
86
|
+
// Real in-workspace edits with Git-visible changes still preserve the nudge.
|
|
87
|
+
fs.writeFileSync(path.join(repo, 'tracked.txt'), 'changed\n', 'utf8');
|
|
88
|
+
const dirtyResults = sixInvocations({ session: 'dirty-tree', target: path.join(repo, 'tracked.txt') });
|
|
89
|
+
assert.ok(dirtyResults.slice(0, 5).every(result => result.status === 0), 'first five in-workspace edits remain non-blocking');
|
|
90
|
+
assert.strictEqual(dirtyResults[5].status, 2, 'sixth in-workspace edit with uncommitted changes nudges');
|
|
91
|
+
assert.match(String(dirtyResults[5].stderr || ''), /6 edits since the last commit/, 'nudge wording is tool-neutral');
|
|
92
|
+
assert.doesNotMatch(String(dirtyResults[5].stderr || ''), /apply_patch calls/, 'nudge no longer claims all edits are apply_patch calls');
|
|
93
|
+
|
|
94
|
+
// apply_patch paths inside the repository count too.
|
|
95
|
+
const patchResults = sixInvocations({
|
|
96
|
+
session: 'inside-patch',
|
|
97
|
+
tool: 'apply_patch',
|
|
98
|
+
patch: '*** Begin Patch\n*** Update File: tracked.txt\n@@\n-old\n+new\n*** End Patch',
|
|
99
|
+
});
|
|
100
|
+
assert.strictEqual(patchResults[5].status, 2, 'in-workspace apply_patch header participates in the threshold');
|
|
101
|
+
|
|
102
|
+
// Moving HEAD resets the counter for the same session.
|
|
103
|
+
git(['add', 'tracked.txt']);
|
|
104
|
+
git(['commit', '-m', 'save change']);
|
|
105
|
+
const afterCommit = invoke({ session: 'dirty-tree', target: path.join(repo, 'tracked.txt') });
|
|
106
|
+
assert.strictEqual(afterCommit.status, 0, 'counter resets after a commit moves HEAD');
|
|
107
|
+
|
|
108
|
+
console.log('PASS: atomic commit check scopes edits to the workspace and suppresses clean-tree false nudges');
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
111
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
const canonicalScript = path.join(ROOT, 'hooks', 'scripts', 'action-gate.js');
|
|
11
|
+
const pluginScript = path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'action-gate.js');
|
|
12
|
+
const canonicalRules = path.join(ROOT, 'hooks', 'scripts', 'action-gate-rules.json');
|
|
13
|
+
const pluginRules = path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'action-gate-rules.json');
|
|
14
|
+
const actionGate = require(canonicalScript);
|
|
15
|
+
|
|
16
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-action-gate-hardening-'));
|
|
17
|
+
const sessionDir = path.join(temp, 'session');
|
|
18
|
+
fs.mkdirSync(path.join(sessionDir, 'scratch'), { recursive: true });
|
|
19
|
+
|
|
20
|
+
function payload(command, tool = 'Bash', cwd = ROOT) {
|
|
21
|
+
return {
|
|
22
|
+
session_id: 'classifier-hardening',
|
|
23
|
+
hook_event_name: 'PreToolUse',
|
|
24
|
+
tool_name: tool,
|
|
25
|
+
tool_input: { command },
|
|
26
|
+
cwd,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function matched(table, command, expected, tool = 'Bash', cwd = ROOT) {
|
|
31
|
+
const rule = actionGate.classify(payload(command, tool, cwd), table.rules, sessionDir);
|
|
32
|
+
assert.ok(rule, `${command} should match ${expected}`);
|
|
33
|
+
assert.strictEqual(rule.id, expected, `${command} should match ${expected}, got ${rule && rule.id}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function allowed(table, command, tool = 'Bash', cwd = ROOT) {
|
|
37
|
+
const rule = actionGate.classify(payload(command, tool, cwd), table.rules, sessionDir);
|
|
38
|
+
assert.strictEqual(rule, null, `${command} should not be classified as destructive`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
assert.strictEqual(fs.readFileSync(canonicalScript, 'utf8'), fs.readFileSync(pluginScript, 'utf8'), 'canonical and packaged action-gate scripts must remain byte-identical');
|
|
43
|
+
assert.strictEqual(fs.readFileSync(canonicalRules, 'utf8'), fs.readFileSync(pluginRules, 'utf8'), 'canonical and packaged action-gate rule tables must remain byte-identical');
|
|
44
|
+
|
|
45
|
+
const table = actionGate.loadRuleTable(canonicalRules);
|
|
46
|
+
assert.strictEqual(table.degraded, false, 'production rule table must load normally');
|
|
47
|
+
|
|
48
|
+
const builtinById = new Map(actionGate.BUILTIN_RULES.map(rule => [rule.id, rule]));
|
|
49
|
+
for (const rule of table.rules) {
|
|
50
|
+
const fallback = builtinById.get(rule.id);
|
|
51
|
+
assert.ok(fallback, `built-in fallback must contain ${rule.id}`);
|
|
52
|
+
assert.deepStrictEqual(
|
|
53
|
+
{ tools: fallback.tools, pattern: fallback.pattern, flags: fallback.flags, reason: fallback.reason, scope: fallback.scope },
|
|
54
|
+
{ tools: rule.tools, pattern: rule.pattern, flags: rule.flags, reason: rule.reason, scope: rule.scope },
|
|
55
|
+
`built-in fallback must stay aligned with configured rule ${rule.id}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Confirmed fail-open variants from #100.
|
|
60
|
+
matched(table, 'git -C . push --force origin main', 'git-force-push');
|
|
61
|
+
matched(table, 'git push origin +main', 'git-force-push');
|
|
62
|
+
matched(table, 'git push -uf origin main', 'git-force-push');
|
|
63
|
+
matched(table, 'git reset -q --hard HEAD~1', 'git-reset-hard');
|
|
64
|
+
matched(table, 'git -c key=value reset --hard', 'git-reset-hard');
|
|
65
|
+
matched(table, 'sudo rm -rf /opt/app', 'recursive-delete');
|
|
66
|
+
matched(table, 'bash -c "rm -rf C:/important"', 'recursive-delete');
|
|
67
|
+
matched(table, 'ri -Recurse -Force C:\\repo\\x', 'powershell-recursive-force-delete', 'PowerShell');
|
|
68
|
+
matched(table, 'rm -r -fo C:\\repo\\x', 'powershell-recursive-force-delete', 'PowerShell');
|
|
69
|
+
|
|
70
|
+
// Quoted/descriptive text is not an executable destructive command.
|
|
71
|
+
allowed(table, 'node -e "console.log(\'Remove-Item -Recurse -Force C:\\\\repo\\\\x\')"', 'Bash');
|
|
72
|
+
allowed(table, 'echo "never DROP TABLE users"');
|
|
73
|
+
allowed(table, 'grep -rn "npm publish" docs/');
|
|
74
|
+
allowed(table, 'git push origin main');
|
|
75
|
+
|
|
76
|
+
// Scratch cleanup keeps working for direct commands and PowerShell aliases.
|
|
77
|
+
const bashScratch = process.platform === 'win32' ? '/tmp/harness-action-gate-safe' : path.join(os.tmpdir(), 'harness-action-gate-safe');
|
|
78
|
+
allowed(table, `rm -rf "${bashScratch}"`, 'Bash');
|
|
79
|
+
const psScratch = path.join(sessionDir, 'scratch', 'build');
|
|
80
|
+
allowed(table, `rm -r -fo "${psScratch}"`, 'PowerShell');
|
|
81
|
+
allowed(table, `ri -Recurse -Force "${psScratch}"`, 'PowerShell');
|
|
82
|
+
|
|
83
|
+
// An unresolved variable remains conservative rather than creating a new
|
|
84
|
+
// fail-open path: the gate cannot prove that the target is scratch space.
|
|
85
|
+
matched(table, 'rm -rf "$S"', 'recursive-delete');
|
|
86
|
+
|
|
87
|
+
console.log('PASS: destructive-command classifier covers confirmed bypasses while suppressing quoted-text false positives');
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
90
|
+
}
|
|
@@ -4,7 +4,7 @@ description: "Route software/project work through the Harness kernel: classify T
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.7.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Harness Everything
|
|
@@ -29,8 +29,9 @@ These rails are mandatory; Tier guidance is not a fixed pipeline.
|
|
|
29
29
|
## Workflow
|
|
30
30
|
|
|
31
31
|
1. Reuse current-turn kernel output, or run `node "<this-skill-dir>/scripts/kernel-router.js" "<prompt summary>"`.
|
|
32
|
-
2.
|
|
33
|
-
3.
|
|
32
|
+
2. Ensure the emitted **Harness Routing Checkpoint** is user-visible: reuse a host-visible rendering or include it in the first progress/update message.
|
|
33
|
+
3. Preserve the invariants.
|
|
34
|
+
4. Treat suggested skills as advisory: choose, combine, reorder, or omit them as useful. If suggestions are non-empty and you use none, state one brief skip reason with the visible checkpoint/progress update.
|
|
34
35
|
|
|
35
36
|
## Tier Guidance
|
|
36
37
|
|
|
@@ -38,6 +39,6 @@ These rails are mandatory; Tier guidance is not a fixed pipeline.
|
|
|
38
39
|
- **Tier 2:** TODO tracking, TDD, and verification are common suggestions.
|
|
39
40
|
- **Tier 3:** Fable/multi-agent skills are optional when macro planning or delegation helps.
|
|
40
41
|
|
|
41
|
-
**Rule:**
|
|
42
|
+
**Rule:** enforce checkpoint visibility and workflow invariants, not workflow order; let the agent orchestrate itself inside those rails.
|
|
42
43
|
|
|
43
44
|
Deep dive: <this-skill-dir>/references/triage-and-tiers.md
|
|
@@ -73,11 +73,12 @@ npx github:dyphn1/Harness-everything next "<brief prompt summary>"
|
|
|
73
73
|
`kernel-router.js` delegates classification and guide discovery to `tier-router.js`, removes legacy fixed-pipeline instructions, and emits:
|
|
74
74
|
|
|
75
75
|
- the recommended tier and rationale,
|
|
76
|
+
- a compact **Harness Routing Checkpoint**,
|
|
76
77
|
- the **required Harness invariants**,
|
|
77
78
|
- **suggested skills** marked advisory,
|
|
78
79
|
- the policy that the agent may choose the smallest useful skill/tool set.
|
|
79
80
|
|
|
80
|
-
If a host's `UserPromptSubmit` integration already ran the kernel this turn, reuse that output rather than running it twice.
|
|
81
|
+
If a host's `UserPromptSubmit` integration already ran the kernel this turn, reuse that output rather than running it twice. The checkpoint must still become user-visible for software/project work: use the host's visible hook rendering when it has one, otherwise include the checkpoint in the first progress/update message.
|
|
81
82
|
|
|
82
83
|
The tier classifier is heuristic. Treat its result as the default route, not an oracle. A clear task reading or explicit user instruction may override it; record the reason when doing so.
|
|
83
84
|
|
|
@@ -116,7 +117,7 @@ Common **suggestions**, not a pipeline:
|
|
|
116
117
|
| Isolated workspace would reduce risk | `using-git-worktrees` |
|
|
117
118
|
| Quantitative scoring/benchmarking is requested | `eval-harness` |
|
|
118
119
|
|
|
119
|
-
The model decides which of these are useful and in what order. It may use none, one, or several while preserving the kernel invariants.
|
|
120
|
+
The model decides which of these are useful and in what order. It may use none, one, or several while preserving the kernel invariants. If the router emitted one or more suggestions and the agent uses none of them, the visible checkpoint/progress update must include one brief skip reason; using at least one suggestion does not require explaining why the others were omitted.
|
|
120
121
|
|
|
121
122
|
### Tier 3 — Macro
|
|
122
123
|
|
|
@@ -137,7 +138,7 @@ Common **suggestions**, not a pipeline:
|
|
|
137
138
|
| Settled intent needs a spec | `to-spec` |
|
|
138
139
|
| Settled work needs tracer-bullet issues | `to-tickets` |
|
|
139
140
|
|
|
140
|
-
Tier 3 does **not** automatically require multi-agent execution. A strong model may complete macro work directly when that is the safer/smaller choice.
|
|
141
|
+
Tier 3 does **not** automatically require multi-agent execution. A strong model may complete macro work directly when that is the safer/smaller choice. The same all-skipped rationale rule applies when the router emitted suggestions.
|
|
141
142
|
|
|
142
143
|
## 4. Cognitive OS Relationship
|
|
143
144
|
|
|
@@ -160,24 +161,26 @@ But this diagram describes a reasoning policy, not a peer-skill dependency graph
|
|
|
160
161
|
|
|
161
162
|
| Host mode | Expected behavior |
|
|
162
163
|
|---|---|
|
|
163
|
-
| Lifecycle hook available | Run the Harness Kernel on prompt submission so routing context exists before domain-skill execution. Hard gates may enforce supported invariants at tool/stop boundaries. |
|
|
164
|
-
| Advisory instructions only | Tell the agent to run/reuse `harness next` before software mutation and `harness verify` before completion. The behavior is advisory, not a hard gate. |
|
|
165
|
-
| Manual use | Invoke `harness-everything` or `install-cognitive-os` explicitly to inspect/re-establish the contract. |
|
|
164
|
+
| Lifecycle hook available | Run the Harness Kernel on prompt submission so routing context exists before domain-skill execution. Surface the emitted checkpoint through a host-visible hook surface when supported; otherwise the agent carries it into the first visible progress/update. Hard gates may enforce supported invariants at tool/stop boundaries. |
|
|
165
|
+
| Advisory instructions only | Tell the agent to run/reuse `harness next` before software mutation, surface the checkpoint, and run `harness verify` before completion. The behavior is advisory, not a hard host gate. |
|
|
166
|
+
| Manual use | Invoke `harness-everything` or `install-cognitive-os` explicitly to inspect/re-establish and surface the contract. |
|
|
166
167
|
|
|
167
|
-
Never describe an advisory integration as hard enforcement. Platform-specific adapters may differ while preserving functional intent.
|
|
168
|
+
Never describe an advisory integration as hard enforcement. Platform-specific adapters may differ while preserving functional intent. Kernel emission alone is not proof that a host UI displayed the checkpoint; #82 live evidence owns that claim.
|
|
168
169
|
|
|
169
170
|
## 6. Routing Checkpoint
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
For software/project work, the kernel always emits a compact checkpoint and the execution surface must make it user-visible before or with the first progress update:
|
|
172
173
|
|
|
173
174
|
```markdown
|
|
174
175
|
## 🚦 Harness Routing Checkpoint
|
|
175
176
|
- Tier: Tier X — <reason>
|
|
177
|
+
- Strategy: <selected/deferred strategy>
|
|
176
178
|
- Required invariants: route-before-execution; verify-before-claim; re-plan-after-3-same-failures
|
|
177
|
-
- Suggested skills: <
|
|
179
|
+
- Suggested skills: <deduplicated suggestions, or none>
|
|
180
|
+
- Suggestion disposition: <using one or more | skipped all — brief reason>
|
|
178
181
|
```
|
|
179
182
|
|
|
180
|
-
The checkpoint reports state; it does not prescribe a universal workflow.
|
|
183
|
+
The checkpoint reports state; it does not prescribe a universal workflow. Suggested skills remain advisory. The only extra accountability rule is that a non-empty suggestion set cannot disappear silently: if every suggestion is skipped, state one brief reason. Do not add a skip explanation when at least one suggestion is used.
|
|
181
184
|
|
|
182
185
|
## 7. Self-Healing and Dynamic Skills
|
|
183
186
|
|
|
@@ -117,6 +117,32 @@ function printWorkflowPlan(plan) {
|
|
|
117
117
|
console.log(`\n=> ROUTER WORKFLOW PLAN (JSON): ${JSON.stringify(visible)}`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function uniqueSuggestedSkills(plan) {
|
|
121
|
+
if (!Array.isArray(plan.suggestedSkills)) return [];
|
|
122
|
+
return [...new Set(plan.suggestedSkills.filter(skill => typeof skill === 'string' && skill.trim()))];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function displayTier(tier) {
|
|
126
|
+
if (tier === 'tier1') return 'Tier 1';
|
|
127
|
+
if (tier === 'tier2') return 'Tier 2';
|
|
128
|
+
if (tier === 'tier3') return 'Tier 3';
|
|
129
|
+
return tier || 'unclassified';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printRoutingCheckpoint(plan) {
|
|
133
|
+
const invariants = Array.isArray(plan.requiredInvariants) ? plan.requiredInvariants : [];
|
|
134
|
+
const suggestions = uniqueSuggestedSkills(plan);
|
|
135
|
+
|
|
136
|
+
console.log('\n=> HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE):');
|
|
137
|
+
console.log(` - Tier: ${displayTier(plan.tier)}`);
|
|
138
|
+
console.log(` - Strategy: ${plan.strategy || 'deferred'}`);
|
|
139
|
+
console.log(` - Required invariants: ${invariants.length ? invariants.join(', ') : 'none'}`);
|
|
140
|
+
console.log(` - Suggested skills: ${suggestions.length ? suggestions.join(', ') : 'none'}`);
|
|
141
|
+
if (suggestions.length > 0) {
|
|
142
|
+
console.log(' - Suggestion disposition: suggestions are advisory; use any that add value. If all are skipped, state one brief reason in the first visible progress/update message.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
function printKernelContract(plan) {
|
|
121
147
|
console.log('\n=> REQUIRED HARNESS INVARIANTS:');
|
|
122
148
|
for (const invariant of plan.requiredInvariants || []) {
|
|
@@ -194,6 +220,7 @@ function run(prompt, stdinPayload) {
|
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
printWorkflowPlan(contract.workflowPlan);
|
|
223
|
+
printRoutingCheckpoint(contract.workflowPlan);
|
|
197
224
|
printKernelContract(contract.workflowPlan);
|
|
198
225
|
|
|
199
226
|
if (result.status !== 0) process.exit(result.status === null ? 1 : result.status);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": "git-force-push",
|
|
6
6
|
"tools": ["Bash", "PowerShell"],
|
|
7
|
-
"pattern": "\\
|
|
7
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*push\\b(?=[^\\r\\n;&|]*(?:--force(?:-with-lease)?\\b|-[A-Za-z]*f[A-Za-z]*\\b|\\s\\+[^\\s;&|]+))",
|
|
8
8
|
"flags": "i",
|
|
9
9
|
"reason": "Force-pushing can rewrite remote history.",
|
|
10
10
|
"scope": "always"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"id": "git-reset-hard",
|
|
14
14
|
"tools": ["Bash", "PowerShell"],
|
|
15
|
-
"pattern": "\\
|
|
15
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*reset\\b[^\\r\\n;&|]*--hard\\b",
|
|
16
16
|
"flags": "i",
|
|
17
17
|
"reason": "git reset --hard can discard local work.",
|
|
18
18
|
"scope": "always"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"id": "git-clean-force-delete",
|
|
22
22
|
"tools": ["Bash", "PowerShell"],
|
|
23
|
-
"pattern": "\\
|
|
23
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*clean\\b(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*f|f\\b))(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*d|d\\b))",
|
|
24
24
|
"flags": "i",
|
|
25
25
|
"reason": "git clean with force and directory deletion can remove untracked work.",
|
|
26
26
|
"scope": "always"
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
{
|
|
29
29
|
"id": "recursive-delete",
|
|
30
30
|
"tools": ["Bash"],
|
|
31
|
-
"pattern": "(?:^|[;&|\\n]\\s*)rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))",
|
|
31
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:(?:bash|sh)\\s+(?:-\\S+\\s+)*-c\\s+[\\\"']\\s*)?(?:sudo(?:\\s+-\\S+)*\\s+)?rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))",
|
|
32
32
|
"flags": "i",
|
|
33
33
|
"reason": "Recursive forced deletion outside approved scratch space is destructive.",
|
|
34
34
|
"scope": "outside-scratch"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"id": "powershell-recursive-force-delete",
|
|
38
|
-
"tools": ["
|
|
39
|
-
"pattern": "\\
|
|
38
|
+
"tools": ["PowerShell"],
|
|
39
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:Remove-Item|ri|rm)\\b(?=[^\\r\\n;|]*-(?:Recurse|r)\\b)(?=[^\\r\\n;|]*-(?:Force|fo)\\b)",
|
|
40
40
|
"flags": "i",
|
|
41
41
|
"reason": "PowerShell recursive forced deletion outside approved scratch space is destructive.",
|
|
42
42
|
"scope": "outside-scratch"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
{
|
|
45
45
|
"id": "sql-destructive-ddl",
|
|
46
46
|
"tools": ["Bash", "PowerShell"],
|
|
47
|
-
"pattern": "\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b",
|
|
47
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:(?:psql|mysql|sqlcmd|sqlite3)\\b[^\\r\\n;&|]*)?\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b",
|
|
48
48
|
"flags": "i",
|
|
49
49
|
"reason": "Destructive SQL DDL can irreversibly remove production data or schema state.",
|
|
50
50
|
"scope": "always"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
{
|
|
53
53
|
"id": "package-publish",
|
|
54
54
|
"tools": ["Bash", "PowerShell"],
|
|
55
|
-
"pattern": "\\
|
|
55
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:sudo\\s+)?(?:npm|pnpm|yarn|cargo|twine)\\s+publish\\b",
|
|
56
56
|
"flags": "i",
|
|
57
57
|
"reason": "Publishing creates an external package/release side effect.",
|
|
58
58
|
"scope": "always"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
{
|
|
61
61
|
"id": "github-release-create",
|
|
62
62
|
"tools": ["Bash", "PowerShell"],
|
|
63
|
-
"pattern": "\\
|
|
63
|
+
"pattern": "(?:^|[;&|\\n]\\s*)gh\\s+release\\s+create\\b",
|
|
64
64
|
"flags": "i",
|
|
65
65
|
"reason": "Creating a GitHub release is an external irreversible publication action.",
|
|
66
66
|
"scope": "always"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
{
|
|
69
69
|
"id": "production-deploy",
|
|
70
70
|
"tools": ["Bash", "PowerShell"],
|
|
71
|
-
"pattern": "\\
|
|
71
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:kubectl\\s+apply|helm\\s+(?:upgrade|install)|terraform\\s+apply|vercel\\s+deploy|fly\\s+deploy|gcloud\\s+run\\s+deploy|(?:npm|pnpm|yarn)\\s+(?:run\\s+)?deploy|deploy\\s+to\\s+(?:prod|production|live))\\b",
|
|
72
72
|
"flags": "i",
|
|
73
73
|
"reason": "Deployment changes an external environment and requires explicit approval.",
|
|
74
74
|
"scope": "always"
|
|
@@ -20,15 +20,15 @@ const ALWAYS_ASK_POLICY = 'always-ask';
|
|
|
20
20
|
// Fail-safe defaults are used only when the configured table is missing or
|
|
21
21
|
// invalid. A valid-but-empty table stays empty so CI can catch policy erasure.
|
|
22
22
|
const BUILTIN_RULES = [
|
|
23
|
-
['git-force-push', ['Bash', 'PowerShell'], '\\
|
|
24
|
-
['git-reset-hard', ['Bash', 'PowerShell'], '\\
|
|
25
|
-
['git-clean-force-delete', ['Bash', 'PowerShell'], '\\
|
|
26
|
-
['recursive-delete', ['Bash'], '(?:^|[;&|\\n]\\s*)rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))', 'i', 'Recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
27
|
-
['powershell-recursive-force-delete', ['
|
|
28
|
-
['sql-destructive-ddl', ['Bash', 'PowerShell'], '\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b', 'i', 'Destructive SQL DDL can irreversibly remove production data or schema state.', 'always'],
|
|
29
|
-
['package-publish', ['Bash', 'PowerShell'], '\\
|
|
30
|
-
['github-release-create', ['Bash', 'PowerShell'], '\\
|
|
31
|
-
['production-deploy', ['Bash', 'PowerShell'], '\\
|
|
23
|
+
['git-force-push', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*push\\b(?=[^\\r\\n;&|]*(?:--force(?:-with-lease)?\\b|-[A-Za-z]*f[A-Za-z]*\\b|\\s\\+[^\\s;&|]+))', 'i', 'Force-pushing can rewrite remote history.', 'always'],
|
|
24
|
+
['git-reset-hard', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*reset\\b[^\\r\\n;&|]*--hard\\b', 'i', 'git reset --hard can discard local work.', 'always'],
|
|
25
|
+
['git-clean-force-delete', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*clean\\b(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*f|f\\b))(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*d|d\\b))', 'i', 'git clean with force and directory deletion can remove untracked work.', 'always'],
|
|
26
|
+
['recursive-delete', ['Bash'], '(?:^|[;&|\\n]\\s*)(?:(?:bash|sh)\\s+(?:-\\S+\\s+)*-c\\s+[\\"\']\\s*)?(?:sudo(?:\\s+-\\S+)*\\s+)?rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))', 'i', 'Recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
27
|
+
['powershell-recursive-force-delete', ['PowerShell'], '(?:^|[;&|\\n]\\s*)(?:Remove-Item|ri|rm)\\b(?=[^\\r\\n;|]*-(?:Recurse|r)\\b)(?=[^\\r\\n;|]*-(?:Force|fo)\\b)', 'i', 'PowerShell recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
28
|
+
['sql-destructive-ddl', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:(?:psql|mysql|sqlcmd|sqlite3)\\b[^\\r\\n;&|]*)?\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b', 'i', 'Destructive SQL DDL can irreversibly remove production data or schema state.', 'always'],
|
|
29
|
+
['package-publish', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:sudo\\s+)?(?:npm|pnpm|yarn|cargo|twine)\\s+publish\\b', 'i', 'Publishing creates an external package/release side effect.', 'always'],
|
|
30
|
+
['github-release-create', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)gh\\s+release\\s+create\\b', 'i', 'Creating a GitHub release is an external irreversible publication action.', 'always'],
|
|
31
|
+
['production-deploy', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:kubectl\\s+apply|helm\\s+(?:upgrade|install)|terraform\\s+apply|vercel\\s+deploy|fly\\s+deploy|gcloud\\s+run\\s+deploy|(?:npm|pnpm|yarn)\\s+(?:run\\s+)?deploy|deploy\\s+to\\s+(?:prod|production|live))\\b', 'i', 'Deployment changes an external environment and requires explicit approval.', 'always'],
|
|
32
32
|
].map(([id, tools, pattern, flags, reason, scope]) => ({ id, tools, pattern, flags, reason, scope }));
|
|
33
33
|
|
|
34
34
|
function hashExact(value) {
|
|
@@ -153,9 +153,15 @@ function deleteTargets(command, ruleId) {
|
|
|
153
153
|
const tails = [];
|
|
154
154
|
let regex = null;
|
|
155
155
|
if (ruleId === 'recursive-delete') {
|
|
156
|
-
regex = /(?:^|[;&|\n]\s*)rm\s+([^;&|\n]+)/ig;
|
|
156
|
+
regex = /(?:^|[;&|\n]\s*)(?:sudo(?:\s+-\S+)*\s+)?rm\s+([^;&|\n]+)/ig;
|
|
157
|
+
const wrapper = /(?:^|[;&|\n]\s*)(?:bash|sh)\s+(?:-\S+\s+)*-c\s+(["'])([\s\S]*?)\1/ig;
|
|
158
|
+
let wrapped;
|
|
159
|
+
while ((wrapped = wrapper.exec(text)) !== null) {
|
|
160
|
+
tails.push(...deleteTargets(wrapped[2], ruleId));
|
|
161
|
+
if (wrapped[0].length === 0) wrapper.lastIndex++;
|
|
162
|
+
}
|
|
157
163
|
} else if (ruleId === 'powershell-recursive-force-delete') {
|
|
158
|
-
regex =
|
|
164
|
+
regex = /(?:^|[;|\n]\s*)(?:Remove-Item|ri|rm)\b([^;|\n]+)/ig;
|
|
159
165
|
}
|
|
160
166
|
if (!regex) return [];
|
|
161
167
|
|
|
@@ -190,6 +196,10 @@ function isWithin(target, root) {
|
|
|
190
196
|
|
|
191
197
|
function scratchRoots(payload, sessionDir) {
|
|
192
198
|
const roots = [os.tmpdir(), path.join(sessionDir, 'scratch')];
|
|
199
|
+
// Git Bash/MSYS exposes the Windows temp area as /tmp (and sometimes
|
|
200
|
+
// /var/tmp). Treat those aliases as scratch on Windows instead of gating a
|
|
201
|
+
// harmless temp cleanup merely because the host path syntax differs.
|
|
202
|
+
if (process.platform === 'win32') roots.push('/tmp', '/var/tmp');
|
|
193
203
|
const declared = payload && (payload.scratchpad_path || payload.scratchpadPath || (payload.harness && payload.harness.scratchpadPath));
|
|
194
204
|
if (declared) roots.push(declared);
|
|
195
205
|
if (process.env.HARNESS_SCRATCH_DIR) roots.push(process.env.HARNESS_SCRATCH_DIR);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Atomic Commit Check (PostToolUse: apply_patch)
|
|
4
|
-
* PostToolUse can't block - the edit/write already happened.
|
|
5
|
-
*
|
|
6
|
-
* threshold,
|
|
7
|
-
*
|
|
3
|
+
* Atomic Commit Check (PostToolUse: Edit/Write/apply_patch)
|
|
4
|
+
* PostToolUse can't block - the edit/write already happened. Count only edits
|
|
5
|
+
* whose targets resolve inside the current workspace. Once the count crosses
|
|
6
|
+
* the threshold, emit a strong reminder only when the Git worktree actually
|
|
7
|
+
* has uncommitted changes. Resets whenever HEAD moves.
|
|
8
8
|
* Fails open on any error - this is a nudge, never a hard stop.
|
|
9
9
|
*/
|
|
10
10
|
const { execSync } = require('child_process');
|
|
@@ -15,6 +15,90 @@ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
|
15
15
|
const THRESHOLD = 6;
|
|
16
16
|
const RENUDGE_EVERY = 3;
|
|
17
17
|
|
|
18
|
+
function toolNameOf(payload) {
|
|
19
|
+
return String((payload && (payload.tool_name || payload.toolName)) || '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toolInputOf(payload) {
|
|
23
|
+
return (payload && (payload.tool_input || payload.toolInput || payload.input)) || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathApiFor(value) {
|
|
27
|
+
return /^[A-Za-z]:[\\/]/.test(String(value || '')) ? path.win32 : path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalPath(value) {
|
|
31
|
+
const text = String(value || '');
|
|
32
|
+
if (!text) return null;
|
|
33
|
+
const api = pathApiFor(text);
|
|
34
|
+
const resolved = api.resolve(text);
|
|
35
|
+
|
|
36
|
+
// realpath() is necessary on macOS where /var is a symlink to /private/var.
|
|
37
|
+
// New Write targets may not exist yet, so canonicalize the nearest existing
|
|
38
|
+
// ancestor and append the missing path segments without requiring creation.
|
|
39
|
+
const missing = [];
|
|
40
|
+
let cursor = resolved;
|
|
41
|
+
while (cursor && !fs.existsSync(cursor)) {
|
|
42
|
+
const parent = api.dirname(cursor);
|
|
43
|
+
if (!parent || parent === cursor) break;
|
|
44
|
+
missing.unshift(api.basename(cursor));
|
|
45
|
+
cursor = parent;
|
|
46
|
+
}
|
|
47
|
+
if (!cursor || !fs.existsSync(cursor)) return resolved;
|
|
48
|
+
|
|
49
|
+
let real = fs.realpathSync.native ? fs.realpathSync.native(cursor) : fs.realpathSync(cursor);
|
|
50
|
+
for (const segment of missing) real = api.join(real, segment);
|
|
51
|
+
return api.normalize(real);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isWithin(target, root) {
|
|
55
|
+
if (!target || !root) return false;
|
|
56
|
+
const api = pathApiFor(target) === path.win32 || pathApiFor(root) === path.win32 ? path.win32 : path;
|
|
57
|
+
const resolvedTarget = canonicalPath(target);
|
|
58
|
+
const resolvedRoot = canonicalPath(root);
|
|
59
|
+
const relative = api.relative(resolvedRoot, resolvedTarget);
|
|
60
|
+
if (!relative) return true;
|
|
61
|
+
return relative !== '..' && !relative.startsWith(`..${api.sep}`) && !api.isAbsolute(relative);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function absoluteTarget(target, root) {
|
|
65
|
+
const text = String(target || '').trim();
|
|
66
|
+
if (!text) return null;
|
|
67
|
+
const api = pathApiFor(text) === path.win32 || pathApiFor(root) === path.win32 ? path.win32 : path;
|
|
68
|
+
return api.isAbsolute(text) ? api.normalize(text) : api.resolve(root, text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function patchTargets(patchText) {
|
|
72
|
+
const targets = [];
|
|
73
|
+
const regex = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+?)\s*$/gm;
|
|
74
|
+
let match;
|
|
75
|
+
while ((match = regex.exec(String(patchText || ''))) !== null) targets.push(match[1]);
|
|
76
|
+
return targets;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function editTargets(payload) {
|
|
80
|
+
const tool = toolNameOf(payload);
|
|
81
|
+
const input = toolInputOf(payload);
|
|
82
|
+
if (tool === 'Edit' || tool === 'Write') {
|
|
83
|
+
const target = input.file_path || input.filePath || input.path;
|
|
84
|
+
return target ? [String(target)] : [];
|
|
85
|
+
}
|
|
86
|
+
if (tool === 'apply_patch') {
|
|
87
|
+
return patchTargets(input.patch || input.command || input.content || '');
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function touchesWorkspace(payload, root) {
|
|
93
|
+
const targets = editTargets(payload);
|
|
94
|
+
if (targets.length === 0) return false;
|
|
95
|
+
return targets.some(target => isWithin(absoluteTarget(target, root), root));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeState(stateFile, state) {
|
|
99
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
18
102
|
function main(payload) {
|
|
19
103
|
try {
|
|
20
104
|
const root = getWorkspaceRoot(payload);
|
|
@@ -33,17 +117,32 @@ function main(payload) {
|
|
|
33
117
|
state.editCount = 0;
|
|
34
118
|
}
|
|
35
119
|
|
|
120
|
+
// Host scratchpads, memory stores, temp files, and any other target outside
|
|
121
|
+
// the repository must not contribute to a repository commit reminder.
|
|
122
|
+
if (!touchesWorkspace(payload, root)) {
|
|
123
|
+
writeState(stateFile, state);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
36
127
|
state.editCount += 1;
|
|
37
|
-
|
|
128
|
+
writeState(stateFile, state);
|
|
38
129
|
|
|
39
130
|
const overThreshold = state.editCount >= THRESHOLD;
|
|
40
131
|
const onNudgeBeat = overThreshold && (state.editCount - THRESHOLD) % RENUDGE_EVERY === 0;
|
|
41
132
|
|
|
42
133
|
if (onNudgeBeat) {
|
|
43
134
|
const statusRaw = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
if (!statusRaw) {
|
|
136
|
+
// Calls that produced no Git-visible change (or work already reverted)
|
|
137
|
+
// must not leave a stale counter that immediately re-nudges later.
|
|
138
|
+
state.editCount = 0;
|
|
139
|
+
writeState(stateFile, state);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const changedFiles = statusRaw.split('\n').length;
|
|
144
|
+
console.error(`[Atomic Commit Check] ${state.editCount} edits since the last commit (${changedFiles} files currently changed).`);
|
|
145
|
+
console.error('If a logically complete chunk of work is done, commit it now before continuing - large uncommitted diffs are harder to review and harder to recover from.');
|
|
47
146
|
process.exit(2);
|
|
48
147
|
}
|
|
49
148
|
|
|
@@ -66,3 +165,12 @@ process.stdin.on('error', () => {
|
|
|
66
165
|
clearTimeout(timeout);
|
|
67
166
|
main(null);
|
|
68
167
|
});
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
absoluteTarget,
|
|
171
|
+
canonicalPath,
|
|
172
|
+
editTargets,
|
|
173
|
+
isWithin,
|
|
174
|
+
patchTargets,
|
|
175
|
+
touchesWorkspace,
|
|
176
|
+
};
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": "git-force-push",
|
|
6
6
|
"tools": ["Bash", "PowerShell"],
|
|
7
|
-
"pattern": "\\
|
|
7
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*push\\b(?=[^\\r\\n;&|]*(?:--force(?:-with-lease)?\\b|-[A-Za-z]*f[A-Za-z]*\\b|\\s\\+[^\\s;&|]+))",
|
|
8
8
|
"flags": "i",
|
|
9
9
|
"reason": "Force-pushing can rewrite remote history.",
|
|
10
10
|
"scope": "always"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"id": "git-reset-hard",
|
|
14
14
|
"tools": ["Bash", "PowerShell"],
|
|
15
|
-
"pattern": "\\
|
|
15
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*reset\\b[^\\r\\n;&|]*--hard\\b",
|
|
16
16
|
"flags": "i",
|
|
17
17
|
"reason": "git reset --hard can discard local work.",
|
|
18
18
|
"scope": "always"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"id": "git-clean-force-delete",
|
|
22
22
|
"tools": ["Bash", "PowerShell"],
|
|
23
|
-
"pattern": "\\
|
|
23
|
+
"pattern": "(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*clean\\b(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*f|f\\b))(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*d|d\\b))",
|
|
24
24
|
"flags": "i",
|
|
25
25
|
"reason": "git clean with force and directory deletion can remove untracked work.",
|
|
26
26
|
"scope": "always"
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
{
|
|
29
29
|
"id": "recursive-delete",
|
|
30
30
|
"tools": ["Bash"],
|
|
31
|
-
"pattern": "(?:^|[;&|\\n]\\s*)rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))",
|
|
31
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:(?:bash|sh)\\s+(?:-\\S+\\s+)*-c\\s+[\\\"']\\s*)?(?:sudo(?:\\s+-\\S+)*\\s+)?rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))",
|
|
32
32
|
"flags": "i",
|
|
33
33
|
"reason": "Recursive forced deletion outside approved scratch space is destructive.",
|
|
34
34
|
"scope": "outside-scratch"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"id": "powershell-recursive-force-delete",
|
|
38
|
-
"tools": ["
|
|
39
|
-
"pattern": "\\
|
|
38
|
+
"tools": ["PowerShell"],
|
|
39
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:Remove-Item|ri|rm)\\b(?=[^\\r\\n;|]*-(?:Recurse|r)\\b)(?=[^\\r\\n;|]*-(?:Force|fo)\\b)",
|
|
40
40
|
"flags": "i",
|
|
41
41
|
"reason": "PowerShell recursive forced deletion outside approved scratch space is destructive.",
|
|
42
42
|
"scope": "outside-scratch"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
{
|
|
45
45
|
"id": "sql-destructive-ddl",
|
|
46
46
|
"tools": ["Bash", "PowerShell"],
|
|
47
|
-
"pattern": "\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b",
|
|
47
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:(?:psql|mysql|sqlcmd|sqlite3)\\b[^\\r\\n;&|]*)?\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b",
|
|
48
48
|
"flags": "i",
|
|
49
49
|
"reason": "Destructive SQL DDL can irreversibly remove production data or schema state.",
|
|
50
50
|
"scope": "always"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
{
|
|
53
53
|
"id": "package-publish",
|
|
54
54
|
"tools": ["Bash", "PowerShell"],
|
|
55
|
-
"pattern": "\\
|
|
55
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:sudo\\s+)?(?:npm|pnpm|yarn|cargo|twine)\\s+publish\\b",
|
|
56
56
|
"flags": "i",
|
|
57
57
|
"reason": "Publishing creates an external package/release side effect.",
|
|
58
58
|
"scope": "always"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
{
|
|
61
61
|
"id": "github-release-create",
|
|
62
62
|
"tools": ["Bash", "PowerShell"],
|
|
63
|
-
"pattern": "\\
|
|
63
|
+
"pattern": "(?:^|[;&|\\n]\\s*)gh\\s+release\\s+create\\b",
|
|
64
64
|
"flags": "i",
|
|
65
65
|
"reason": "Creating a GitHub release is an external irreversible publication action.",
|
|
66
66
|
"scope": "always"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
{
|
|
69
69
|
"id": "production-deploy",
|
|
70
70
|
"tools": ["Bash", "PowerShell"],
|
|
71
|
-
"pattern": "\\
|
|
71
|
+
"pattern": "(?:^|[;&|\\n]\\s*)(?:kubectl\\s+apply|helm\\s+(?:upgrade|install)|terraform\\s+apply|vercel\\s+deploy|fly\\s+deploy|gcloud\\s+run\\s+deploy|(?:npm|pnpm|yarn)\\s+(?:run\\s+)?deploy|deploy\\s+to\\s+(?:prod|production|live))\\b",
|
|
72
72
|
"flags": "i",
|
|
73
73
|
"reason": "Deployment changes an external environment and requires explicit approval.",
|
|
74
74
|
"scope": "always"
|
|
@@ -20,15 +20,15 @@ const ALWAYS_ASK_POLICY = 'always-ask';
|
|
|
20
20
|
// Fail-safe defaults are used only when the configured table is missing or
|
|
21
21
|
// invalid. A valid-but-empty table stays empty so CI can catch policy erasure.
|
|
22
22
|
const BUILTIN_RULES = [
|
|
23
|
-
['git-force-push', ['Bash', 'PowerShell'], '\\
|
|
24
|
-
['git-reset-hard', ['Bash', 'PowerShell'], '\\
|
|
25
|
-
['git-clean-force-delete', ['Bash', 'PowerShell'], '\\
|
|
26
|
-
['recursive-delete', ['Bash'], '(?:^|[;&|\\n]\\s*)rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))', 'i', 'Recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
27
|
-
['powershell-recursive-force-delete', ['
|
|
28
|
-
['sql-destructive-ddl', ['Bash', 'PowerShell'], '\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b', 'i', 'Destructive SQL DDL can irreversibly remove production data or schema state.', 'always'],
|
|
29
|
-
['package-publish', ['Bash', 'PowerShell'], '\\
|
|
30
|
-
['github-release-create', ['Bash', 'PowerShell'], '\\
|
|
31
|
-
['production-deploy', ['Bash', 'PowerShell'], '\\
|
|
23
|
+
['git-force-push', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*push\\b(?=[^\\r\\n;&|]*(?:--force(?:-with-lease)?\\b|-[A-Za-z]*f[A-Za-z]*\\b|\\s\\+[^\\s;&|]+))', 'i', 'Force-pushing can rewrite remote history.', 'always'],
|
|
24
|
+
['git-reset-hard', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*reset\\b[^\\r\\n;&|]*--hard\\b', 'i', 'git reset --hard can discard local work.', 'always'],
|
|
25
|
+
['git-clean-force-delete', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)git\\s+(?:(?:-C|-c)\\s+\\S+\\s+|-\\S+\\s+)*clean\\b(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*f|f\\b))(?=[^\\r\\n;&|]*\\s-(?:[^\\s]*d|d\\b))', 'i', 'git clean with force and directory deletion can remove untracked work.', 'always'],
|
|
26
|
+
['recursive-delete', ['Bash'], '(?:^|[;&|\\n]\\s*)(?:(?:bash|sh)\\s+(?:-\\S+\\s+)*-c\\s+[\\"\']\\s*)?(?:sudo(?:\\s+-\\S+)*\\s+)?rm\\s+(?=[^\\r\\n;&|]*-(?:[^\\s]*r|r\\b))(?=[^\\r\\n;&|]*-(?:[^\\s]*f|f\\b))', 'i', 'Recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
27
|
+
['powershell-recursive-force-delete', ['PowerShell'], '(?:^|[;&|\\n]\\s*)(?:Remove-Item|ri|rm)\\b(?=[^\\r\\n;|]*-(?:Recurse|r)\\b)(?=[^\\r\\n;|]*-(?:Force|fo)\\b)', 'i', 'PowerShell recursive forced deletion outside approved scratch space is destructive.', 'outside-scratch'],
|
|
28
|
+
['sql-destructive-ddl', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:(?:psql|mysql|sqlcmd|sqlite3)\\b[^\\r\\n;&|]*)?\\b(?:DROP\\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE(?:\\s+TABLE)?)\\b', 'i', 'Destructive SQL DDL can irreversibly remove production data or schema state.', 'always'],
|
|
29
|
+
['package-publish', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:sudo\\s+)?(?:npm|pnpm|yarn|cargo|twine)\\s+publish\\b', 'i', 'Publishing creates an external package/release side effect.', 'always'],
|
|
30
|
+
['github-release-create', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)gh\\s+release\\s+create\\b', 'i', 'Creating a GitHub release is an external irreversible publication action.', 'always'],
|
|
31
|
+
['production-deploy', ['Bash', 'PowerShell'], '(?:^|[;&|\\n]\\s*)(?:kubectl\\s+apply|helm\\s+(?:upgrade|install)|terraform\\s+apply|vercel\\s+deploy|fly\\s+deploy|gcloud\\s+run\\s+deploy|(?:npm|pnpm|yarn)\\s+(?:run\\s+)?deploy|deploy\\s+to\\s+(?:prod|production|live))\\b', 'i', 'Deployment changes an external environment and requires explicit approval.', 'always'],
|
|
32
32
|
].map(([id, tools, pattern, flags, reason, scope]) => ({ id, tools, pattern, flags, reason, scope }));
|
|
33
33
|
|
|
34
34
|
function hashExact(value) {
|
|
@@ -153,9 +153,15 @@ function deleteTargets(command, ruleId) {
|
|
|
153
153
|
const tails = [];
|
|
154
154
|
let regex = null;
|
|
155
155
|
if (ruleId === 'recursive-delete') {
|
|
156
|
-
regex = /(?:^|[;&|\n]\s*)rm\s+([^;&|\n]+)/ig;
|
|
156
|
+
regex = /(?:^|[;&|\n]\s*)(?:sudo(?:\s+-\S+)*\s+)?rm\s+([^;&|\n]+)/ig;
|
|
157
|
+
const wrapper = /(?:^|[;&|\n]\s*)(?:bash|sh)\s+(?:-\S+\s+)*-c\s+(["'])([\s\S]*?)\1/ig;
|
|
158
|
+
let wrapped;
|
|
159
|
+
while ((wrapped = wrapper.exec(text)) !== null) {
|
|
160
|
+
tails.push(...deleteTargets(wrapped[2], ruleId));
|
|
161
|
+
if (wrapped[0].length === 0) wrapper.lastIndex++;
|
|
162
|
+
}
|
|
157
163
|
} else if (ruleId === 'powershell-recursive-force-delete') {
|
|
158
|
-
regex =
|
|
164
|
+
regex = /(?:^|[;|\n]\s*)(?:Remove-Item|ri|rm)\b([^;|\n]+)/ig;
|
|
159
165
|
}
|
|
160
166
|
if (!regex) return [];
|
|
161
167
|
|
|
@@ -190,6 +196,10 @@ function isWithin(target, root) {
|
|
|
190
196
|
|
|
191
197
|
function scratchRoots(payload, sessionDir) {
|
|
192
198
|
const roots = [os.tmpdir(), path.join(sessionDir, 'scratch')];
|
|
199
|
+
// Git Bash/MSYS exposes the Windows temp area as /tmp (and sometimes
|
|
200
|
+
// /var/tmp). Treat those aliases as scratch on Windows instead of gating a
|
|
201
|
+
// harmless temp cleanup merely because the host path syntax differs.
|
|
202
|
+
if (process.platform === 'win32') roots.push('/tmp', '/var/tmp');
|
|
193
203
|
const declared = payload && (payload.scratchpad_path || payload.scratchpadPath || (payload.harness && payload.harness.scratchpadPath));
|
|
194
204
|
if (declared) roots.push(declared);
|
|
195
205
|
if (process.env.HARNESS_SCRATCH_DIR) roots.push(process.env.HARNESS_SCRATCH_DIR);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Atomic Commit Check (PostToolUse: apply_patch)
|
|
4
|
-
* PostToolUse can't block - the edit/write already happened.
|
|
5
|
-
*
|
|
6
|
-
* threshold,
|
|
7
|
-
*
|
|
3
|
+
* Atomic Commit Check (PostToolUse: Edit/Write/apply_patch)
|
|
4
|
+
* PostToolUse can't block - the edit/write already happened. Count only edits
|
|
5
|
+
* whose targets resolve inside the current workspace. Once the count crosses
|
|
6
|
+
* the threshold, emit a strong reminder only when the Git worktree actually
|
|
7
|
+
* has uncommitted changes. Resets whenever HEAD moves.
|
|
8
8
|
* Fails open on any error - this is a nudge, never a hard stop.
|
|
9
9
|
*/
|
|
10
10
|
const { execSync } = require('child_process');
|
|
@@ -15,6 +15,90 @@ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
|
15
15
|
const THRESHOLD = 6;
|
|
16
16
|
const RENUDGE_EVERY = 3;
|
|
17
17
|
|
|
18
|
+
function toolNameOf(payload) {
|
|
19
|
+
return String((payload && (payload.tool_name || payload.toolName)) || '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toolInputOf(payload) {
|
|
23
|
+
return (payload && (payload.tool_input || payload.toolInput || payload.input)) || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathApiFor(value) {
|
|
27
|
+
return /^[A-Za-z]:[\\/]/.test(String(value || '')) ? path.win32 : path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalPath(value) {
|
|
31
|
+
const text = String(value || '');
|
|
32
|
+
if (!text) return null;
|
|
33
|
+
const api = pathApiFor(text);
|
|
34
|
+
const resolved = api.resolve(text);
|
|
35
|
+
|
|
36
|
+
// realpath() is necessary on macOS where /var is a symlink to /private/var.
|
|
37
|
+
// New Write targets may not exist yet, so canonicalize the nearest existing
|
|
38
|
+
// ancestor and append the missing path segments without requiring creation.
|
|
39
|
+
const missing = [];
|
|
40
|
+
let cursor = resolved;
|
|
41
|
+
while (cursor && !fs.existsSync(cursor)) {
|
|
42
|
+
const parent = api.dirname(cursor);
|
|
43
|
+
if (!parent || parent === cursor) break;
|
|
44
|
+
missing.unshift(api.basename(cursor));
|
|
45
|
+
cursor = parent;
|
|
46
|
+
}
|
|
47
|
+
if (!cursor || !fs.existsSync(cursor)) return resolved;
|
|
48
|
+
|
|
49
|
+
let real = fs.realpathSync.native ? fs.realpathSync.native(cursor) : fs.realpathSync(cursor);
|
|
50
|
+
for (const segment of missing) real = api.join(real, segment);
|
|
51
|
+
return api.normalize(real);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isWithin(target, root) {
|
|
55
|
+
if (!target || !root) return false;
|
|
56
|
+
const api = pathApiFor(target) === path.win32 || pathApiFor(root) === path.win32 ? path.win32 : path;
|
|
57
|
+
const resolvedTarget = canonicalPath(target);
|
|
58
|
+
const resolvedRoot = canonicalPath(root);
|
|
59
|
+
const relative = api.relative(resolvedRoot, resolvedTarget);
|
|
60
|
+
if (!relative) return true;
|
|
61
|
+
return relative !== '..' && !relative.startsWith(`..${api.sep}`) && !api.isAbsolute(relative);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function absoluteTarget(target, root) {
|
|
65
|
+
const text = String(target || '').trim();
|
|
66
|
+
if (!text) return null;
|
|
67
|
+
const api = pathApiFor(text) === path.win32 || pathApiFor(root) === path.win32 ? path.win32 : path;
|
|
68
|
+
return api.isAbsolute(text) ? api.normalize(text) : api.resolve(root, text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function patchTargets(patchText) {
|
|
72
|
+
const targets = [];
|
|
73
|
+
const regex = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+?)\s*$/gm;
|
|
74
|
+
let match;
|
|
75
|
+
while ((match = regex.exec(String(patchText || ''))) !== null) targets.push(match[1]);
|
|
76
|
+
return targets;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function editTargets(payload) {
|
|
80
|
+
const tool = toolNameOf(payload);
|
|
81
|
+
const input = toolInputOf(payload);
|
|
82
|
+
if (tool === 'Edit' || tool === 'Write') {
|
|
83
|
+
const target = input.file_path || input.filePath || input.path;
|
|
84
|
+
return target ? [String(target)] : [];
|
|
85
|
+
}
|
|
86
|
+
if (tool === 'apply_patch') {
|
|
87
|
+
return patchTargets(input.patch || input.command || input.content || '');
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function touchesWorkspace(payload, root) {
|
|
93
|
+
const targets = editTargets(payload);
|
|
94
|
+
if (targets.length === 0) return false;
|
|
95
|
+
return targets.some(target => isWithin(absoluteTarget(target, root), root));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeState(stateFile, state) {
|
|
99
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
18
102
|
function main(payload) {
|
|
19
103
|
try {
|
|
20
104
|
const root = getWorkspaceRoot(payload);
|
|
@@ -33,17 +117,32 @@ function main(payload) {
|
|
|
33
117
|
state.editCount = 0;
|
|
34
118
|
}
|
|
35
119
|
|
|
120
|
+
// Host scratchpads, memory stores, temp files, and any other target outside
|
|
121
|
+
// the repository must not contribute to a repository commit reminder.
|
|
122
|
+
if (!touchesWorkspace(payload, root)) {
|
|
123
|
+
writeState(stateFile, state);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
36
127
|
state.editCount += 1;
|
|
37
|
-
|
|
128
|
+
writeState(stateFile, state);
|
|
38
129
|
|
|
39
130
|
const overThreshold = state.editCount >= THRESHOLD;
|
|
40
131
|
const onNudgeBeat = overThreshold && (state.editCount - THRESHOLD) % RENUDGE_EVERY === 0;
|
|
41
132
|
|
|
42
133
|
if (onNudgeBeat) {
|
|
43
134
|
const statusRaw = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
if (!statusRaw) {
|
|
136
|
+
// Calls that produced no Git-visible change (or work already reverted)
|
|
137
|
+
// must not leave a stale counter that immediately re-nudges later.
|
|
138
|
+
state.editCount = 0;
|
|
139
|
+
writeState(stateFile, state);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const changedFiles = statusRaw.split('\n').length;
|
|
144
|
+
console.error(`[Atomic Commit Check] ${state.editCount} edits since the last commit (${changedFiles} files currently changed).`);
|
|
145
|
+
console.error('If a logically complete chunk of work is done, commit it now before continuing - large uncommitted diffs are harder to review and harder to recover from.');
|
|
47
146
|
process.exit(2);
|
|
48
147
|
}
|
|
49
148
|
|
|
@@ -66,3 +165,12 @@ process.stdin.on('error', () => {
|
|
|
66
165
|
clearTimeout(timeout);
|
|
67
166
|
main(null);
|
|
68
167
|
});
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
absoluteTarget,
|
|
171
|
+
canonicalPath,
|
|
172
|
+
editTargets,
|
|
173
|
+
isWithin,
|
|
174
|
+
patchTargets,
|
|
175
|
+
touchesWorkspace,
|
|
176
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -4,7 +4,7 @@ description: "Route software/project work through the Harness kernel: classify T
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.7.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Harness Everything
|
|
@@ -29,8 +29,9 @@ These rails are mandatory; Tier guidance is not a fixed pipeline.
|
|
|
29
29
|
## Workflow
|
|
30
30
|
|
|
31
31
|
1. Reuse current-turn kernel output, or run `node "<this-skill-dir>/scripts/kernel-router.js" "<prompt summary>"`.
|
|
32
|
-
2.
|
|
33
|
-
3.
|
|
32
|
+
2. Ensure the emitted **Harness Routing Checkpoint** is user-visible: reuse a host-visible rendering or include it in the first progress/update message.
|
|
33
|
+
3. Preserve the invariants.
|
|
34
|
+
4. Treat suggested skills as advisory: choose, combine, reorder, or omit them as useful. If suggestions are non-empty and you use none, state one brief skip reason with the visible checkpoint/progress update.
|
|
34
35
|
|
|
35
36
|
## Tier Guidance
|
|
36
37
|
|
|
@@ -38,6 +39,6 @@ These rails are mandatory; Tier guidance is not a fixed pipeline.
|
|
|
38
39
|
- **Tier 2:** TODO tracking, TDD, and verification are common suggestions.
|
|
39
40
|
- **Tier 3:** Fable/multi-agent skills are optional when macro planning or delegation helps.
|
|
40
41
|
|
|
41
|
-
**Rule:**
|
|
42
|
+
**Rule:** enforce checkpoint visibility and workflow invariants, not workflow order; let the agent orchestrate itself inside those rails.
|
|
42
43
|
|
|
43
44
|
Deep dive: <this-skill-dir>/references/triage-and-tiers.md
|
|
@@ -73,11 +73,12 @@ npx github:dyphn1/Harness-everything next "<brief prompt summary>"
|
|
|
73
73
|
`kernel-router.js` delegates classification and guide discovery to `tier-router.js`, removes legacy fixed-pipeline instructions, and emits:
|
|
74
74
|
|
|
75
75
|
- the recommended tier and rationale,
|
|
76
|
+
- a compact **Harness Routing Checkpoint**,
|
|
76
77
|
- the **required Harness invariants**,
|
|
77
78
|
- **suggested skills** marked advisory,
|
|
78
79
|
- the policy that the agent may choose the smallest useful skill/tool set.
|
|
79
80
|
|
|
80
|
-
If a host's `UserPromptSubmit` integration already ran the kernel this turn, reuse that output rather than running it twice.
|
|
81
|
+
If a host's `UserPromptSubmit` integration already ran the kernel this turn, reuse that output rather than running it twice. The checkpoint must still become user-visible for software/project work: use the host's visible hook rendering when it has one, otherwise include the checkpoint in the first progress/update message.
|
|
81
82
|
|
|
82
83
|
The tier classifier is heuristic. Treat its result as the default route, not an oracle. A clear task reading or explicit user instruction may override it; record the reason when doing so.
|
|
83
84
|
|
|
@@ -116,7 +117,7 @@ Common **suggestions**, not a pipeline:
|
|
|
116
117
|
| Isolated workspace would reduce risk | `using-git-worktrees` |
|
|
117
118
|
| Quantitative scoring/benchmarking is requested | `eval-harness` |
|
|
118
119
|
|
|
119
|
-
The model decides which of these are useful and in what order. It may use none, one, or several while preserving the kernel invariants.
|
|
120
|
+
The model decides which of these are useful and in what order. It may use none, one, or several while preserving the kernel invariants. If the router emitted one or more suggestions and the agent uses none of them, the visible checkpoint/progress update must include one brief skip reason; using at least one suggestion does not require explaining why the others were omitted.
|
|
120
121
|
|
|
121
122
|
### Tier 3 — Macro
|
|
122
123
|
|
|
@@ -137,7 +138,7 @@ Common **suggestions**, not a pipeline:
|
|
|
137
138
|
| Settled intent needs a spec | `to-spec` |
|
|
138
139
|
| Settled work needs tracer-bullet issues | `to-tickets` |
|
|
139
140
|
|
|
140
|
-
Tier 3 does **not** automatically require multi-agent execution. A strong model may complete macro work directly when that is the safer/smaller choice.
|
|
141
|
+
Tier 3 does **not** automatically require multi-agent execution. A strong model may complete macro work directly when that is the safer/smaller choice. The same all-skipped rationale rule applies when the router emitted suggestions.
|
|
141
142
|
|
|
142
143
|
## 4. Cognitive OS Relationship
|
|
143
144
|
|
|
@@ -160,24 +161,26 @@ But this diagram describes a reasoning policy, not a peer-skill dependency graph
|
|
|
160
161
|
|
|
161
162
|
| Host mode | Expected behavior |
|
|
162
163
|
|---|---|
|
|
163
|
-
| Lifecycle hook available | Run the Harness Kernel on prompt submission so routing context exists before domain-skill execution. Hard gates may enforce supported invariants at tool/stop boundaries. |
|
|
164
|
-
| Advisory instructions only | Tell the agent to run/reuse `harness next` before software mutation and `harness verify` before completion. The behavior is advisory, not a hard gate. |
|
|
165
|
-
| Manual use | Invoke `harness-everything` or `install-cognitive-os` explicitly to inspect/re-establish the contract. |
|
|
164
|
+
| Lifecycle hook available | Run the Harness Kernel on prompt submission so routing context exists before domain-skill execution. Surface the emitted checkpoint through a host-visible hook surface when supported; otherwise the agent carries it into the first visible progress/update. Hard gates may enforce supported invariants at tool/stop boundaries. |
|
|
165
|
+
| Advisory instructions only | Tell the agent to run/reuse `harness next` before software mutation, surface the checkpoint, and run `harness verify` before completion. The behavior is advisory, not a hard host gate. |
|
|
166
|
+
| Manual use | Invoke `harness-everything` or `install-cognitive-os` explicitly to inspect/re-establish and surface the contract. |
|
|
166
167
|
|
|
167
|
-
Never describe an advisory integration as hard enforcement. Platform-specific adapters may differ while preserving functional intent.
|
|
168
|
+
Never describe an advisory integration as hard enforcement. Platform-specific adapters may differ while preserving functional intent. Kernel emission alone is not proof that a host UI displayed the checkpoint; #82 live evidence owns that claim.
|
|
168
169
|
|
|
169
170
|
## 6. Routing Checkpoint
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
For software/project work, the kernel always emits a compact checkpoint and the execution surface must make it user-visible before or with the first progress update:
|
|
172
173
|
|
|
173
174
|
```markdown
|
|
174
175
|
## 🚦 Harness Routing Checkpoint
|
|
175
176
|
- Tier: Tier X — <reason>
|
|
177
|
+
- Strategy: <selected/deferred strategy>
|
|
176
178
|
- Required invariants: route-before-execution; verify-before-claim; re-plan-after-3-same-failures
|
|
177
|
-
- Suggested skills: <
|
|
179
|
+
- Suggested skills: <deduplicated suggestions, or none>
|
|
180
|
+
- Suggestion disposition: <using one or more | skipped all — brief reason>
|
|
178
181
|
```
|
|
179
182
|
|
|
180
|
-
The checkpoint reports state; it does not prescribe a universal workflow.
|
|
183
|
+
The checkpoint reports state; it does not prescribe a universal workflow. Suggested skills remain advisory. The only extra accountability rule is that a non-empty suggestion set cannot disappear silently: if every suggestion is skipped, state one brief reason. Do not add a skip explanation when at least one suggestion is used.
|
|
181
184
|
|
|
182
185
|
## 7. Self-Healing and Dynamic Skills
|
|
183
186
|
|
|
@@ -117,6 +117,32 @@ function printWorkflowPlan(plan) {
|
|
|
117
117
|
console.log(`\n=> ROUTER WORKFLOW PLAN (JSON): ${JSON.stringify(visible)}`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function uniqueSuggestedSkills(plan) {
|
|
121
|
+
if (!Array.isArray(plan.suggestedSkills)) return [];
|
|
122
|
+
return [...new Set(plan.suggestedSkills.filter(skill => typeof skill === 'string' && skill.trim()))];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function displayTier(tier) {
|
|
126
|
+
if (tier === 'tier1') return 'Tier 1';
|
|
127
|
+
if (tier === 'tier2') return 'Tier 2';
|
|
128
|
+
if (tier === 'tier3') return 'Tier 3';
|
|
129
|
+
return tier || 'unclassified';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printRoutingCheckpoint(plan) {
|
|
133
|
+
const invariants = Array.isArray(plan.requiredInvariants) ? plan.requiredInvariants : [];
|
|
134
|
+
const suggestions = uniqueSuggestedSkills(plan);
|
|
135
|
+
|
|
136
|
+
console.log('\n=> HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE):');
|
|
137
|
+
console.log(` - Tier: ${displayTier(plan.tier)}`);
|
|
138
|
+
console.log(` - Strategy: ${plan.strategy || 'deferred'}`);
|
|
139
|
+
console.log(` - Required invariants: ${invariants.length ? invariants.join(', ') : 'none'}`);
|
|
140
|
+
console.log(` - Suggested skills: ${suggestions.length ? suggestions.join(', ') : 'none'}`);
|
|
141
|
+
if (suggestions.length > 0) {
|
|
142
|
+
console.log(' - Suggestion disposition: suggestions are advisory; use any that add value. If all are skipped, state one brief reason in the first visible progress/update message.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
function printKernelContract(plan) {
|
|
121
147
|
console.log('\n=> REQUIRED HARNESS INVARIANTS:');
|
|
122
148
|
for (const invariant of plan.requiredInvariants || []) {
|
|
@@ -194,6 +220,7 @@ function run(prompt, stdinPayload) {
|
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
printWorkflowPlan(contract.workflowPlan);
|
|
223
|
+
printRoutingCheckpoint(contract.workflowPlan);
|
|
197
224
|
printKernelContract(contract.workflowPlan);
|
|
198
225
|
|
|
199
226
|
if (result.status !== 0) process.exit(result.status === null ? 1 : result.status);
|