freeharness 0.4.2 → 0.4.3
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/NOTICE +2 -1
- package/README.md +20 -16
- package/canonical/HARVEST.yaml +2 -2
- package/canonical/PROVENANCE.yaml +2 -2
- package/canonical/agents/security-reviewer.md +8 -2
- package/canonical/hooks/freeharness-config-guard.mjs +644 -4567
- package/canonical/hooks/freeharness-cost-tracker.mjs +14 -27
- package/canonical/hooks/freeharness-precompact.mjs +12 -23
- package/canonical/hooks/freeharness-sdd-context.mjs +12 -23
- package/canonical/hooks/hooks.config-guard.claude-code.json +2 -3
- package/canonical/hooks/hooks.memory-guard.claude-code.json +2 -3
- package/canonical/skills/code-review/SKILL.md +2 -2
- package/canonical/skills/code-review/evals/cases.yaml +1 -1
- package/canonical/skills/subagent-driven-development/SKILL.md +4 -4
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +149 -20
- package/canonical/skills/subagent-driven-development/reference/brief.md +15 -13
- package/canonical/skills/subagent-driven-development/reference/handoff.md +29 -15
- package/canonical/skills/subagent-driven-development/reference/loop.md +4 -5
- package/canonical/skills/subagent-driven-development/reference/workspace.md +2 -2
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +301 -1038
- package/dist/adapters/harness/claude-code.d.ts +7 -0
- package/dist/adapters/harness/claude-code.js +27 -21
- package/dist/adapters/harness/opencode.js +1 -1
- package/dist/adapters/harness/plugin.js +9 -9
- package/dist/adapters/memory/doctor.d.ts +11 -17
- package/dist/adapters/memory/doctor.js +55 -68
- package/dist/adapters/memory/paths.d.ts +3 -10
- package/dist/adapters/memory/paths.js +16 -54
- package/dist/adapters/memory/vendored.d.ts +2 -7
- package/dist/adapters/memory/vendored.js +12 -57
- package/dist/adapters/memory/worker-control.d.ts +9 -0
- package/dist/adapters/memory/worker-control.js +30 -0
- package/dist/cli/cleanup.d.ts +1 -1
- package/dist/cli/cleanup.js +29 -14
- package/dist/cli/cost.js +2 -2
- package/dist/cli/doctor.d.ts +9 -9
- package/dist/cli/doctor.js +21 -16
- package/dist/cli/hard-cleanup.js +1 -1
- package/dist/cli/install-facts.d.ts +4 -3
- package/dist/cli/install-facts.js +3 -1
- package/dist/cli/profile.d.ts +1 -1
- package/dist/cli/profile.js +10 -10
- package/dist/index.js +7 -5
- package/dist/infra/fsutil.d.ts +4 -5
- package/dist/infra/fsutil.js +5 -6
- package/node_modules/@freeharness/core/dist/registry.d.ts +1 -1
- package/node_modules/@freeharness/core/dist/registry.js +3 -3
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/package.json +3 -3
- package/provenance/canonical.PROVENANCE.yaml +2 -2
- package/provenance/memory.PROVENANCE.yaml +18 -4
- package/canonical/openspec/config.base.yaml +0 -89
- package/canonical/rules/safety.md +0 -41
|
@@ -611,7 +611,7 @@ function pointerLine(file) {
|
|
|
611
611
|
* A task worktree checks out openspec/changes/ but not the gitignored workspace, so its records belong to the main worktree.
|
|
612
612
|
* Only a directory lexically under a real `<main>/.worktrees/` maps, so a forged `.git` can name only a root that already holds it, and a symlinked `.worktrees` cannot pull an outside tree in.
|
|
613
613
|
* @param {string} dir Real path of a directory holding a project marker.
|
|
614
|
-
* @returns {string | null} Real path of the main worktree; null on any read or parse failure.
|
|
614
|
+
* @returns {string | null} Real path of the main worktree; null on any read or parse failure, when the directory is not under `<main>/.worktrees/`, or when the main worktree holds no project marker.
|
|
615
615
|
*/
|
|
616
616
|
export function mainWorktreeOf(dir) {
|
|
617
617
|
try {
|
|
@@ -1262,7 +1262,7 @@ function sessionTags(root, sessionId, signals) {
|
|
|
1262
1262
|
return { change, changes, task: brief?.task ?? null, tier: brief?.tier ?? null, task_class: brief?.task_class ?? "untagged" };
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
1265
|
-
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"
|
|
1265
|
+
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"];
|
|
1266
1266
|
const BOM = 0xfeff;
|
|
1267
1267
|
|
|
1268
1268
|
function expandMemHome(p, home) {
|
|
@@ -1270,14 +1270,6 @@ function expandMemHome(p, home) {
|
|
|
1270
1270
|
return p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
|
|
1271
1271
|
}
|
|
1272
1272
|
|
|
1273
|
-
function isMemDirectory(p) {
|
|
1274
|
-
try {
|
|
1275
|
-
return fs.statSync(p).isDirectory();
|
|
1276
|
-
} catch {
|
|
1277
|
-
return false;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1273
|
/** Keys as the engine reads them: BOM dropped, `env` first. */
|
|
1282
1274
|
function readMemSettings(file) {
|
|
1283
1275
|
try {
|
|
@@ -1296,34 +1288,29 @@ export function resolveMemoryDataDir(env, home) {
|
|
|
1296
1288
|
if (typeof env[key] === "string" && env[key]) return expandMemHome(env[key], home);
|
|
1297
1289
|
}
|
|
1298
1290
|
const freeDir = path.join(home, ".free-mem");
|
|
1299
|
-
const
|
|
1300
|
-
const
|
|
1301
|
-
for (const
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
// A seeded default is not a redirect; honoring it would pin the directory choice.
|
|
1308
|
-
if (!defaults.includes(path.resolve(target))) return target;
|
|
1309
|
-
}
|
|
1291
|
+
const defaults = [path.resolve(freeDir), path.resolve(home, ".claude-mem")];
|
|
1292
|
+
const s = readMemSettings(path.join(freeDir, "settings.json"));
|
|
1293
|
+
for (const key of MEM_DATA_DIR_KEYS) {
|
|
1294
|
+
const value = s ? s[key] : undefined;
|
|
1295
|
+
if (typeof value !== "string" || !value) continue;
|
|
1296
|
+
const target = expandMemHome(value, home);
|
|
1297
|
+
// A seeded or hand-copied default is no redirect; honoring it would pin the directory choice.
|
|
1298
|
+
if (!defaults.includes(path.resolve(target))) return target;
|
|
1310
1299
|
}
|
|
1311
|
-
return
|
|
1300
|
+
return freeDir;
|
|
1312
1301
|
}
|
|
1313
1302
|
|
|
1314
|
-
/** A resolver error falls back to the
|
|
1303
|
+
/** A resolver error falls back to the default directory. */
|
|
1315
1304
|
function memoryDataDir() {
|
|
1316
1305
|
try {
|
|
1317
1306
|
return resolveMemoryDataDir(process.env, os.homedir());
|
|
1318
1307
|
} catch {
|
|
1319
|
-
return path.join(os.homedir(), ".
|
|
1308
|
+
return path.join(os.homedir(), ".free-mem");
|
|
1320
1309
|
}
|
|
1321
1310
|
}
|
|
1322
1311
|
|
|
1323
|
-
/** `free-mem.db` when present, else the `claude-mem.db` an unmigrated engine still writes. */
|
|
1324
1312
|
function memoryDatabase(dataDir) {
|
|
1325
|
-
|
|
1326
|
-
return fs.existsSync(db) ? db : path.join(dataDir, "claude-mem.db");
|
|
1313
|
+
return path.join(dataDir, "free-mem.db");
|
|
1327
1314
|
}
|
|
1328
1315
|
|
|
1329
1316
|
/** The `sqlite3` binary from an absolute PATH entry only, never a bare name handed to the child process's PATH search. */
|
|
@@ -252,7 +252,7 @@ export function skeletonFromFacts(facts, payload, opts = {}) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// The engine's data-dir and port rule; an emitted hook cannot import the engine.
|
|
255
|
-
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"
|
|
255
|
+
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"];
|
|
256
256
|
const BOM = 0xfeff;
|
|
257
257
|
|
|
258
258
|
function expandMemHome(p, home) {
|
|
@@ -260,14 +260,6 @@ function expandMemHome(p, home) {
|
|
|
260
260
|
return p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
function isMemDirectory(p) {
|
|
264
|
-
try {
|
|
265
|
-
return fs.statSync(p).isDirectory();
|
|
266
|
-
} catch {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
263
|
/** Keys as the engine reads them: BOM dropped, `env` first. */
|
|
272
264
|
function readMemSettings(file) {
|
|
273
265
|
try {
|
|
@@ -291,27 +283,24 @@ export function resolveMemoryDataDir(env, home) {
|
|
|
291
283
|
if (typeof env[key] === "string" && env[key]) return expandMemHome(env[key], home);
|
|
292
284
|
}
|
|
293
285
|
const freeDir = path.join(home, ".free-mem");
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
for (const
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// A seeded default is not a redirect; honoring it would pin the directory choice.
|
|
303
|
-
if (!defaults.includes(path.resolve(target))) return target;
|
|
304
|
-
}
|
|
286
|
+
const defaults = [path.resolve(freeDir), path.resolve(home, ".claude-mem")];
|
|
287
|
+
const s = readMemSettings(path.join(freeDir, "settings.json"));
|
|
288
|
+
for (const key of MEM_DATA_DIR_KEYS) {
|
|
289
|
+
const value = s ? s[key] : undefined;
|
|
290
|
+
if (typeof value !== "string" || !value) continue;
|
|
291
|
+
const target = expandMemHome(value, home);
|
|
292
|
+
// A seeded or hand-copied default is no redirect; honoring it would pin the directory choice.
|
|
293
|
+
if (!defaults.includes(path.resolve(target))) return target;
|
|
305
294
|
}
|
|
306
|
-
return
|
|
295
|
+
return freeDir;
|
|
307
296
|
}
|
|
308
297
|
|
|
309
298
|
/** The worker port: env (empty is unset), then the data dir's settings, else the default; pure over (env, home). */
|
|
310
299
|
export function resolveWorkerPort(env, home) {
|
|
311
|
-
const fromEnv = memPort(env.FREE_MEM_WORKER_PORT
|
|
300
|
+
const fromEnv = memPort(env.FREE_MEM_WORKER_PORT);
|
|
312
301
|
if (fromEnv !== null) return fromEnv;
|
|
313
302
|
const s = readMemSettings(path.join(resolveMemoryDataDir(env, home), "settings.json"));
|
|
314
|
-
const fromSettings = s ? memPort(s.FREE_MEM_WORKER_PORT
|
|
303
|
+
const fromSettings = s ? memPort(s.FREE_MEM_WORKER_PORT) : null;
|
|
315
304
|
return fromSettings ?? defaultWorkerPort();
|
|
316
305
|
}
|
|
317
306
|
|
|
@@ -664,7 +664,7 @@ function workerOff() {
|
|
|
664
664
|
}
|
|
665
665
|
|
|
666
666
|
// The engine's data-dir and port rule, as in freeharness-precompact.mjs, a separate emitted script.
|
|
667
|
-
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"
|
|
667
|
+
const MEM_DATA_DIR_KEYS = ["FREE_MEM_DATA_DIR"];
|
|
668
668
|
const BOM = 0xfeff;
|
|
669
669
|
|
|
670
670
|
function expandMemHome(p, home) {
|
|
@@ -672,14 +672,6 @@ function expandMemHome(p, home) {
|
|
|
672
672
|
return p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
function isMemDirectory(p) {
|
|
676
|
-
try {
|
|
677
|
-
return fs.statSync(p).isDirectory();
|
|
678
|
-
} catch {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
675
|
/** Keys as the engine reads them: BOM dropped, `env` first. */
|
|
684
676
|
function readMemSettings(file) {
|
|
685
677
|
try {
|
|
@@ -703,27 +695,24 @@ export function resolveMemoryDataDir(env, home) {
|
|
|
703
695
|
if (typeof env[key] === "string" && env[key]) return expandMemHome(env[key], home);
|
|
704
696
|
}
|
|
705
697
|
const freeDir = path.join(home, ".free-mem");
|
|
706
|
-
const
|
|
707
|
-
const
|
|
708
|
-
for (const
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// A seeded default is not a redirect; honoring it would pin the directory choice.
|
|
715
|
-
if (!defaults.includes(path.resolve(target))) return target;
|
|
716
|
-
}
|
|
698
|
+
const defaults = [path.resolve(freeDir), path.resolve(home, ".claude-mem")];
|
|
699
|
+
const s = readMemSettings(path.join(freeDir, "settings.json"));
|
|
700
|
+
for (const key of MEM_DATA_DIR_KEYS) {
|
|
701
|
+
const value = s ? s[key] : undefined;
|
|
702
|
+
if (typeof value !== "string" || !value) continue;
|
|
703
|
+
const target = expandMemHome(value, home);
|
|
704
|
+
// A seeded or hand-copied default is no redirect; honoring it would pin the directory choice.
|
|
705
|
+
if (!defaults.includes(path.resolve(target))) return target;
|
|
717
706
|
}
|
|
718
|
-
return
|
|
707
|
+
return freeDir;
|
|
719
708
|
}
|
|
720
709
|
|
|
721
710
|
/** The worker port: env (empty is unset), then the data dir's settings, else the default; pure over (env, home). */
|
|
722
711
|
export function resolveWorkerPort(env, home) {
|
|
723
|
-
const fromEnv = memPort(env.FREE_MEM_WORKER_PORT
|
|
712
|
+
const fromEnv = memPort(env.FREE_MEM_WORKER_PORT);
|
|
724
713
|
if (fromEnv !== null) return fromEnv;
|
|
725
714
|
const s = readMemSettings(path.join(resolveMemoryDataDir(env, home), "settings.json"));
|
|
726
|
-
const fromSettings = s ? memPort(s.FREE_MEM_WORKER_PORT
|
|
715
|
+
const fromSettings = s ? memPort(s.FREE_MEM_WORKER_PORT) : null;
|
|
727
716
|
return fromSettings ?? defaultWorkerPort();
|
|
728
717
|
}
|
|
729
718
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "FreeHarness config guard for Claude Code (rules capability). Merged additively into settings.json. PreToolUse on Edit|Write|MultiEdit|Bash: denies writes to live harness configuration, which runs code or loads instructions in every session, unless <project>/.freeharness/allow-config-edit (or ~/.claude/freeharness/allow-config-edit) exists and is younger than --ttl minutes (240 minutes; a numeric epoch-ms or ISO body sets an explicit expiry). The live set is judged by location at the written and the real path: settings.json and settings.local.json inside a .claude dir, any .mcp.json, ~/.claude.json, ~/.claude/CLAUDE.md, everything under ~/.claude/plugins and under the hooks dir of a .claude, .freeharness, ~/.claude/freeharness or freeharness-plugin dir, a plugin's hooks/hooks.json, the marker, and the dirs whose removal takes one of them (CLAUDE_CONFIG_DIR gets every ~/.claude rule); a project CLAUDE.md, a settings.json, hooks.json or hook source elsewhere and a copy in the temp root outside the home and project pass. Bash is judged per simple command (split on ;, &&, ||, |, & and newlines) from the payload cwd moved by each literal cd, pushd or popd: a
|
|
2
|
+
"$comment": "FreeHarness config guard for Claude Code (rules capability). Merged additively into settings.json. PreToolUse on Edit|Write|MultiEdit|Bash: denies writes to live harness configuration, which runs code or loads instructions in every session, unless <project>/.freeharness/allow-config-edit (or ~/.claude/freeharness/allow-config-edit) exists and is younger than --ttl minutes (240 minutes; a numeric epoch-ms or ISO body sets an explicit expiry). The live set is judged by location at the written and the real path: settings.json and settings.local.json inside a .claude dir, any .mcp.json, ~/.claude.json, ~/.claude/CLAUDE.md, everything under ~/.claude/plugins and under the hooks dir of a .claude, .freeharness, ~/.claude/freeharness or freeharness-plugin dir, a plugin's hooks/hooks.json, the marker, and the dirs whose removal takes one of them (CLAUDE_CONFIG_DIR gets every ~/.claude rule); a project CLAUDE.md, a settings.json, hooks.json or hook source elsewhere and a copy in the temp root outside the home and project pass. Bash is judged per simple command (split on ;, &&, ||, |, & and newlines) from the payload cwd moved by each literal cd, pushd or popd: only a literal writer naming a live path denies: an output redirect target, a tee, sed -i, perl -i, an rm or touch operand, a cp or install destination, any mv operand, a literal inline python, node, perl or ruby write, and the literal string of sh -c, judged as a command of its own; any other command, a word holding an expansion, a heredoc body and code fed on stdin allow, and a command longer than 16,384 characters or one that cannot be split allows unjudged. The deny reason names at most 10 targets followed by \"and N more\", the marker and the `touch` one-liners for the user. `freeharness init`/`update` set and remove their own marker. The guard is a second layer under Claude Code's permission layer and the entry carries no onFailure, so a guard hook that cannot start, crashes or times out allows the tool call, and a missing node leaves every Edit, Write and Bash call to the permission layer alone. The same hook runs a git guard independent of the marker: history-destroying git commands (reset --hard, --keep or --merge; push with --force, -f or a + refspec that no --force-with-lease covers, and push --mirror, --delete/-d, --prune or a :refspec, which a lease never excuses; branch -D, -M, -C, -f or --force; clean -f; checkout -- <path>, checkout ., checkout <tree> <path>, checkout -f or -B; switch --discard-changes, -f or -C; restore without --staged alone; stash drop/clear; reflog expire; filter-branch; worktree remove --force) and git config that can add an alias or run code (set through -c, --config-env, GIT_CONFIG_* variables or git config: alias.*, include.*, help.autocorrect, clean.requireForce, core.pager, core.fsmonitor and every key outside a safe list) are denied from the model whatever the marker says, and the user runs an intended one with the `!` prefix; nesting deeper than 8 levels is not judged as git, and an unreadable payload, a non-string command or an internal error allows; a value bound earlier on the line substitutes into git verb, option and operand slots before the check, interpreter code counts as git only with a process-spawning call and a git word, and sed, grep and awk scripts are data; every other git command, a git command inside a quoted argument, push --force-with-lease, object-writing plumbing (commit-tree, mktree, hash-object) and worktree remove --force under the temp root pass; FREEHARNESS_HOOK_GIT_GUARD=off disables only the git guard. Deny reasons render invisible and control characters as \\u{XXXX} escapes and cap each echoed fragment at 200 characters with an ellipsis, so the whole hook output stays under 16,384 bytes. Disable the whole hook: FREEHARNESS_HOOK_CONFIG_GUARD=off or remove this entry.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "node \"${CLAUDE_PROJECT_DIR}/.freeharness/hooks/freeharness-config-guard.mjs\" --ttl 240"
|
|
11
|
-
"onFailure": "block"
|
|
10
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.freeharness/hooks/freeharness-config-guard.mjs\" --ttl 240"
|
|
12
11
|
}
|
|
13
12
|
]
|
|
14
13
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "FreeHarness PreToolUse memory guard for Claude Code. Merged additively into <project>/.claude/settings.json. Denies hand-written memory .md writes (the free-mem plugin owns memory; saves output tokens). The entry carries onFailure
|
|
2
|
+
"$comment": "FreeHarness PreToolUse memory guard for Claude Code. Merged additively into <project>/.claude/settings.json. Denies hand-written memory .md writes (the free-mem plugin owns memory; saves output tokens). The guard is a second layer under Claude Code's permission layer and the entry carries no onFailure, so a guard hook that cannot start, crashes or times out allows the tool call; a payload that is not a JSON object or a field that is not a string is judged as nothing.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "node \"${CLAUDE_PROJECT_DIR}/.freeharness/hooks/freeharness-memory-guard.mjs\""
|
|
11
|
-
"onFailure": "block"
|
|
10
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.freeharness/hooks/freeharness-memory-guard.mjs\""
|
|
12
11
|
}
|
|
13
12
|
]
|
|
14
13
|
}
|
|
@@ -20,7 +20,7 @@ Audit changes against the active `openspec/specs/` and the assigned task:
|
|
|
20
20
|
Inspect the git diff (`git diff --stat`) and dispatch applicable reviewer subagents in parallel:
|
|
21
21
|
- Python / FastAPI / Async changes -> dispatch `python-reviewer`
|
|
22
22
|
- Migrations / Schemas / SQL / Vector search -> dispatch `database-reviewer`
|
|
23
|
-
-
|
|
23
|
+
- Memory sanitizer or redaction, git guard deny list, config guard path judgment, a settings-merge, uninstall or cleanup path touching a datastore or a foreign hook entry, a new network call from a hook or the engine, harvested third-party content entering `canonical/` -> dispatch `security-reviewer`, only on this trigger list, at most once per change, at the final branch review, never in a task review; a diff touching none of them skips it
|
|
24
24
|
- Change-quality lenses (comments, tests, error handling, types, simplification, silent failures, mle) -> dispatch `pr-reviewer` with the lenses selected below
|
|
25
25
|
|
|
26
26
|
Lens selection from the diff (for `pr-reviewer`): `comments` when a hunk adds or changes comments or docstrings; `tests` when test files change or new behavior lands without a test file in the diff; `errors` + `silent` when the diff contains try/except, catch, fallback, default-on-error, retry, or optional chaining; `types` for new or changed classes, dataclasses, Pydantic models, TypeScript types or interfaces; `mle` when the diff touches prompts, models, evals or RAG pipelines (reads the recorded eval result and the eval-plan thresholds, never runs an evaluation); `simplify` always, but only after the other lenses are clean. The user may restrict the set (`comments tests errors types simplify silent mle`, default all). Scope is explicit, never guessed: `git diff --name-only` (working tree), `git diff <base>...HEAD` (branch), or `gh pr diff <n>` (PR). Diffs above ~300 lines or a `parallel` request split into two `pr-reviewer` lens groups: `comments+simplify+types` and `tests+errors+silent+mle`, never one dispatch per lens. Skip the review when the PR is closed, a draft, trivially mechanical, or already reviewed by this harness; say so.
|
|
@@ -39,7 +39,7 @@ Aggregate reports from all subagents.
|
|
|
39
39
|
- The review passes when `validate-handoff.mjs gate` reports no blocking finding (the Blocking bullet below); every other finding is recorded as a follow-up.
|
|
40
40
|
- Finding types: every finding carries one of `defect`, `hardening`, `refactor`, `test-gap`, `docs`, `design`; `defect` only when the current behavior violates a spec requirement, a documented contract or a behavior the owner has stated; a follow-up's own `Expected:` line alone never makes it a defect.
|
|
41
41
|
- Deferral: a finding left unfixed is recorded with `openspec follow-up add <change-id> '<text>' --type <type> --expected '<...>' --current '<...>' --impact '<...>' --from <task>`, never as prose, the type and the expected, current and impact context taken from the finding; pass every value single-quoted (a `'` written as `'\''`) because finding text may quote shell syntax that double quotes would expand.
|
|
42
|
-
- Blocking: a finding is blocking when it is a P0, or a P1 `defect` introduced by the task's diff (it does not reproduce at the task's `Base:` for a task review, or at the round base for a re-review) or one that fails a brief probe or a spec scenario
|
|
42
|
+
- Blocking: a finding is blocking when it is a P0, or a P1 `defect` introduced by the task's diff (it does not reproduce at the task's `Base:` for a task review, or at the round base for a re-review) or one that fails a brief probe or a spec scenario. The reviewer tags a P1 `defect` that reproduces at the base `[base]`; a `[base]` finding is never blocking. Only blocking findings open a fix round; every other finding is a follow-up recorded at task close. A non-blocking `docs` or `test-gap` finding, or a comment fix, may ride round 1 when a blocking finding opened it and the fix stays inside the write scope, never a later round. A blocking finding whose fix lies outside the write scope gets a controller ruling and a new brief, never a round. The rule is the same whatever the task's class; the lead retypes a finding only through a written ruling line. A security finding of an out-of-scope shape (the `subagent-driven-development` skill's reference/loop.md threat budget) closes as wontfix on sight and is never a follow-up or a round. A security finding blocks only as a P0 of an in-scope threat or a P1 `defect` against a kept requirement with a reproducer; a shape constructed inside the four threats that no probe covers is `hardening` with its reproducer, a follow-up, never a round; a `security` task, or a final-review fix loop opened by a security finding, asks the owner after 2 fix rounds with a blocking finding still open.
|
|
43
43
|
- Gate: after every review run `validate-handoff.mjs gate <findings.md>` over the ledger (the `subagent-driven-development` skill's `scripts/`): it prints each `open` line as `blocking` or `follow-up` with the reason and exits non-zero when any is blocking; a round opens only for a blocking line, and with none the review is accepted.
|
|
44
44
|
- Scoped re-review: before each fix dispatch run `validate-handoff.mjs round-base <brief.md> <n>` (the round base), then give the fresh re-reviewer only the ledger, the output of `validate-handoff.mjs round-diff <brief.md> <n>` and the probes; it judges only the round diff, and a finding on code the round diff does not change is a follow-up unless it is a P0; from round 2 on the re-reviewer adds only a P0 with a reproducer on the round diff, every other new finding being a follow-up.
|
|
45
45
|
Clean review outcome feeds directly into `verification-before-completion`.
|
|
@@ -737,7 +737,7 @@ cases:
|
|
|
737
737
|
expected: "\\[P0[^\\]\\n]*\\][^\\n]*?eval-plan\\.md:\\d+|eval-plan\\.md:\\d+:\\s*P0"
|
|
738
738
|
- type: regex
|
|
739
739
|
flags: i
|
|
740
|
-
expected: "
|
|
740
|
+
expected: "recall_at_k_min|recall(?:_at_k)?[^\\n]*?0\\.81"
|
|
741
741
|
- type: tool_used
|
|
742
742
|
expected: "Agent"
|
|
743
743
|
- type: llm
|
|
@@ -18,8 +18,8 @@ The primary session is the lead: it dispatches, evaluates and rules; it never wr
|
|
|
18
18
|
</workspace>
|
|
19
19
|
|
|
20
20
|
<dispatch>
|
|
21
|
-
1. Brief: start `<task>.brief.md` with `validate-handoff.mjs brief-init <change> <task>` and fill every `<fill: ...>` placeholder it leaves (the template stays the source of the fixed parts; `briefs` refuses one left); one task id per brief and per dispatch, never a batch. Header `Spec:` is the OpenSpec change id, never a copied spec; `Tier:` is explicit on every dispatch. Route by `Class:`: `docs`, `extraction`, `exploration` and `mechanical` (documentation, extraction, exploration, mechanical edits such as matrix rows, renames, one-line values and transcription of complete plan text) run on standard or strong; `summary` on fast, standard or strong, and `extraction` also on fast; `security` (a
|
|
22
|
-
2. Probes and project rules: before the dispatch write `## Acceptance probes` (1 to 12 lines, `P<n>: <command, input or observable> -> <expected outcome>`) from the spec scenarios and the design's edge cases; a `security` or `complex` brief carries one probe expecting an allow or a pass
|
|
21
|
+
1. Brief: start `<task>.brief.md` with `validate-handoff.mjs brief-init <change> <task>` and fill every `<fill: ...>` placeholder it leaves (the template stays the source of the fixed parts; `briefs` refuses one left); one task id per brief and per dispatch, never a batch. Header `Spec:` is the OpenSpec change id, never a copied spec; `Tier:` is explicit on every dispatch. Route by `Class:`: `docs`, `extraction`, `exploration` and `mechanical` (documentation, extraction, exploration, mechanical edits such as matrix rows, renames, one-line values and transcription of complete plan text) run on standard or strong; `summary` on fast, standard or strong, and `extraction` also on fast; `security` (a change to the memory sanitizer or redaction, the git guard deny list, the config guard path judgment, or a datastore delete path; no task owes a hostile-input test) and `complex` (a parser or state machine, concurrency, a cross-file contract, or a task that reached the round cap before) run on strong with `Effort: high` only; `code` (a code change that is neither `security` nor `complex`) runs on strong, or on standard only with `Route: C1-C7 - <reason>` per `reference/brief.md`; any other class, or none, runs on strong only. A fast brief's reading set stays under 100,000 tokens at 4 bytes per token; the validator checks the class, never that bound. A fix round's tier and effort are what `validate-handoff.mjs route <brief> --round <n> [--p0] [--security]` prints (`--p0` or `--security` for a round carrying such a finding), and `round-base` given the same flags refuses a brief below them. `Effort:` is `medium` (default) or `high`, `high` on strong only. Write scope names every file the worker may touch. Read reference/loop.md when a brief is written: the sizing bounds and the split rule, the fixture scope, the grep for statements pinning a changed value, and the class ceilings.
|
|
22
|
+
2. Probes and project rules: before the dispatch write `## Acceptance probes` (1 to 12 lines, `P<n>: <command, input or observable> -> <expected outcome>`) from the spec scenarios and the design's edge cases; a `security` or `complex` brief carries one probe expecting an allow or a pass. Fill `## Project rules` (1 to 20 lines) from the project CLAUDE.md invariants and protected boundaries that bear on the write scope plus the artifact rules; workers run without CLAUDE.md, so the section is their only copy. A probe of a guard or hook verdict runs the hook through node on a payload file, and a guard's probes are one allow and one deny, each run through a payload file (reference/brief.md).
|
|
23
23
|
3. Disjoint write scopes: run `validate-handoff.mjs briefs` over the dispatch; any overlap refuses the whole batch naming the file, so no two implementers in flight share a file.
|
|
24
24
|
4. Dispatch a fresh `implementer` agent with the brief path, the report path and the interfaces earlier tasks produced; nothing from the session history. On every implementer, test-writer and fix dispatch the per-dispatch model is the brief's `Tier:` resolved through `reference/tiers.md` (the alias the tier maps to; never a model name in this skill) and the per-dispatch effort is the brief's `Effort:` (`medium` when absent); the agent frontmatter is never the source of a brief's effort. An omitted per-dispatch model falls back to the implementer agent's tier. A per-dispatch effort overrides the frontmatter effort. A team dispatch inherits the session effort, except a plugin-defined agent spawned by name, which keeps its own; the agent frontmatter effort applies to plain subagent dispatches. The implementer follows `test-driven-development`; dispatch `test-writer` for the RED step when the failing tests are non-trivial. Record `Base:` (`git rev-parse HEAD`) before dispatching. Reviewers and implementers are never resumed: every review, re-review and fix round is a fresh dispatch. Dispatch every implementer, test-writer and reviewer through the Agent tool without `name`: the prompt's first line is `Dispatch: <dispatch-name>` and the Agent tool `description` repeats it. A named dispatch launches as a teammate when agent teams are enabled, at the session effort by the team rule above and with no SubagentStop until shutdown; without `name` it stays a plain subagent.
|
|
25
25
|
5. Band of implementers in flight: small, at most 4 (the default); medium, at most 8; large, at most 12, only on the user's ask recorded in the review package with the latest quota reading under 50%. Medium or large only when the median accepted-task duration in the ledger exceeds four times the lead's service time per task. At most 16 dispatches of any role in flight. Every implementer runs in its own worktree whenever more than one is in flight. Read reference/trees.md when more than one implementer is in flight.
|
|
@@ -47,7 +47,7 @@ On a missing, empty or block-less reply the first step is `validate-handoff.mjs
|
|
|
47
47
|
</acceptance>
|
|
48
48
|
|
|
49
49
|
<fix_loop>
|
|
50
|
-
- Blocking: a finding is blocking when it is a P0, or a P1 `defect` introduced by the task's diff (it does not reproduce at the task's `Base:` for a task review, or at the round base for a re-review) or one that fails a brief probe or a spec scenario
|
|
50
|
+
- Blocking: a finding is blocking when it is a P0, or a P1 `defect` introduced by the task's diff (it does not reproduce at the task's `Base:` for a task review, or at the round base for a re-review) or one that fails a brief probe or a spec scenario. The reviewer tags a P1 `defect` that reproduces at the base `[base]`; a `[base]` finding is never blocking. The rule is the same whatever the task's class. Only blocking findings open a fix round; every other finding is a follow-up recorded at task close. A non-blocking `docs` or `test-gap` finding, or a comment fix, may ride round 1 when a blocking finding opened it and the fix stays inside the write scope, never a later round. A blocking finding whose fix lies outside the write scope gets a controller ruling and a new brief, never a round. The lead retypes a finding only through a written ruling line.
|
|
51
51
|
- Ledger and gate: `<task>.findings.md` holds one line per finding, `F<n> <open|fixed|rejected|follow-up> r<round>: <reviewer contract line>`, written only by the lead. After every review run `validate-handoff.mjs gate <findings.md>`: it prints each `open` line as `blocking` or `follow-up` with the reason and exits non-zero when any is blocking; with no blocking line the task is accepted.
|
|
52
52
|
- A fix round = one fix dispatch + one scoped re-review. Before each fix dispatch run `validate-handoff.mjs round-base <brief.md> <n>`, the round-cap check (it refuses `round-cap <n> over <cap>`), which copies the write-scope files under `rounds/<task>-r<n>/`. Every round dispatches a fresh `implementer` given the brief, the review package and `<task>.findings.md` (the ledger), on the tier and effort `route` prints for the round (dispatch rule 1; strong from round 3 on, never frontier, which is reserved for the agentic-architect role (dispatch rule 10)). Read reference/loop.md when a fix dispatch is written: the fix prompt order and the task state.
|
|
53
53
|
- Re-review: a fresh re-reviewer is given only the ledger, the output of `validate-handoff.mjs round-diff <brief.md> <n>` and the probes; it returns a status for each open id and any new findings (from round 2 on only a P0 with a reproducer on the round diff; any other is a follow-up), and judges only the round diff: a finding on code the round diff does not change is a follow-up unless it is a P0.
|
|
@@ -63,5 +63,5 @@ On a missing, empty or block-less reply the first step is `validate-handoff.mjs
|
|
|
63
63
|
</completion>
|
|
64
64
|
|
|
65
65
|
<stop_conditions>
|
|
66
|
-
Stop and ask the user only for: an irreversible or destructive operation; a security-sensitive action; a merge or push outside the change branch, or a publish; a task flagged "Ask first" in the proposal; the round cap or the class ceiling of the run authorization (the circuit breaker); a plan so broken that every path forward is a guess. Ceilings and the round cap come only from the owner (proposal.md `## Run authorization`, or `--ceiling`); the lead never raises them
|
|
66
|
+
Stop and ask the user only for: an irreversible or destructive operation; a security-sensitive action; a merge or push outside the change branch, or a publish; a task flagged "Ask first" in the proposal; the round cap or the class ceiling of the run authorization (the circuit breaker); a plan so broken that every path forward is a guess. Ceilings and the round cap come only from the owner (proposal.md `## Run authorization`, or `--ceiling`); the lead never raises them. They are the only budget and round stops: below them the lead stops only for a `security` task, or a final-review fix loop opened by a security finding, after 2 fix rounds with a blocking finding still open, whatever the round cap. A cap with every finding ruled "remove the addition" is no stop. Everything else is a ruling, written down, and the run continues.
|
|
67
67
|
</stop_conditions>
|