@lannguyensi/harness 0.59.0 → 0.59.1
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/CHANGELOG.md +13 -3
- package/README.md +70 -266
- package/dist/cli/apply/apply.js +2 -2
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/apply/install-codex-config.js +37 -4
- package/dist/cli/apply/install-codex-config.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +6 -0
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.d.ts +70 -2
- package/dist/cli/pack/hook-solution-acceptance.js +81 -28
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +4 -2
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +87 -3
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.59.1] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The solution-acceptance deny note no longer names an unobserved cause for the never-evaluated reading, and its state table now pins what the docs claimed** (task `8fe0f633`, follow-up to the late review findings of task `799de976`; agent-facing text and tests only, no gate decision changes). The never-evaluated line said `solution_evaluate` "has not (yet) been called for this id, or a prior call never got far enough to record one", but what the gate established is only that its one marker read found nothing (the shared reader folds every `lstat` failure into `missing`) and that the lock read not-live at a later moment; a marker recorded then deleted, an attempt finishing between the two reads, and a non-`ENOENT` `lstat` failure each falsify it. It now reads `No verdict marker was found for "<id>"; no attempt reads as currently live: run solution_evaluate for this id.` (with the unknown-liveness clause, `No verdict marker was found for "<id>" and liveness could not be determined: ...`). Only that note line changed: the `evaluateGate` reason above it (`no solution-acceptance verdict recorded for "<id>" (run ... solution_evaluate first)`) is a different module surface and is unchanged, so its `recorded` and `first` wording is still open to the same three falsifiers. The state table gained the three missing liveness rows (symlink, not-a-regular-file and unreadable marker, each with an unknown-liveness fixture): it now holds fourteen rows covering the twelve reachable (note state, liveness) combinations (two combinations have a second fixture), so its "one exact line per reachable (note state, liveness) combination" claim is true; a mutant hardcoding the not-live clause into the symlink line no longer survives. Each row declares the coordinate it means to reach, and `renderNullVerdictNote` (exported, with the renderer record and the reachable set `NULL_VERDICT_NOTE_COORDINATES`) is total over reachable coordinates only: `live` renders only with the live-attempt state, the unusable-id state carries no liveness at all, and any other pair throws, so a row can no longer claim a state or a liveness its fixture never reaches, and the set of lines that must not appear is derived from the renderer instead of listed by hand. The classifier's parameter excludes the `ok` outcome by type and the hook classifies a denial only when its read yielded no verdict, which removes an arm that would have called a valid record invalid if the gate ever gained a second null return. The single marker observation now has two guards: a `dependency-cruiser` rule (`npm run check:boundaries`) forbidding the hook from importing the shared marker reader directly, and a call-count test asserting one `readVerdictDetailed` call per invocation. `readVerdict`'s doc comment no longer claims callers it does not have. The OKF log entry for task `799de976` moved to its chronological place and its commit reference now names the merge commit.
|
|
15
|
+
|
|
16
|
+
- **The Codex install splice keeps a trailing operator comment at EOF when there is no END marker, and keeps a leading UTF-8 BOM exactly once** (task `b34ed105`). Two pre-existing comment-only or BOM-only effects of the managed-range detection, documented as limitations in the task `6a037359` entry, with no semantic loss (the TOML-semantic safety net saw no difference): (1) in a config with a BEGIN marker, a legacy source prefix or a legacy generated header but no END marker, the ownership scan ran to EOF and folded a trailing operator comment after the last harness hook table into the replaced block; it now backs off to the start of that trailing comment run, so the comment is kept once, after the fresh END marker (a run of only blank lines is still consumed, and so is a trailing run that holds a harness-authored line such as the generated header, a `# harness hook:` line or the source prefix, since that is stale harness output; the back-off also needs at least one owned content line before the run); (2) a BOM in front of the BEGIN marker, a legacy source-prefix comment or a legacy generated header on line 1 was dropped by the splice, and a BOM before a source-prefix line hid it from the previous-line check, so the prefix comment was duplicated; the managed range now starts just past the BOM, and the BOM is kept exactly once with no duplicate prefix line. The BOM change leaves the ranges of files without a BOM unchanged. Pinned in `tests/cli/apply/apply-codex-runtime.test.ts`, with one fixture per harness-authored clause (generated header, `# harness hook:` line, source prefix): the no-END, legacy source-prefix and legacy generated-header trailing-comment fixtures and the BOM + BEGIN, BOM + source-prefix and BOM + generated-header fixtures fail against the previous implementation and pass now. Mutation probes (`agent-primitives probe`) reverting the EOF back-off and reverting the BOM handling are killed by those fixtures.
|
|
17
|
+
|
|
18
|
+
- **The solution-acceptance write-guard no longer refuses a command that only names a pack source file** (task `e8c9edaf`). `bashReferencesVerdictDir`'s glob/brace leaf-word fallback fires when the command text carries any of `*?[{` and also contains a distinctive word of the verdict directory's leaf (`solution`, `verdicts`). `solution` is the first word of the pack's own name, so any non-read-only Bash whose text named `docs/policy-packs/solution-acceptance.md`, `src/cli/pack/hook-solution-acceptance-writeguard.ts`, `src/policy-packs/builtin/solution-acceptance-runtime.ts` or one of the pack's test files was refused as a verdict-dir write as soon as a `[x]`, a `**bold**`, a `.*` in a sed expression or a `{...}` appeared anywhere in the text (a heredoc body, a `git commit -m`); for Codex `apply_patch` the `***` of every patch header made any patch that edits a pack file refuse. The fallback now scans the command with the pack name scrubbed out of each plain, whitespace-delimited word (a word with no `*?[]{}` character cannot expand at runtime, so its literal text reaches the verdict directory only through the leaf, tail or env token that the unchanged direct checks already see), but only when the command carries no runtime-construction syntax: a `$` anywhere (parameter expansion, command substitution, `$'...'`), a backtick, `<(` or `>(`, `printf -v`, a `.` in command position, or one of the words `eval`, `source`, `read`, `declare`, `typeset`, `local`, `export`, `set`, `let`, `mapfile`, `readarray`, `exec`, `xargs`, `sh`, `bash`, `zsh`, `dash`, `ksh`. A command that can build text at runtime could derive the directory leaf from the very pack-name word the scrub would erase, so it is scanned unscrubbed, exactly as before this change. Everything else is unchanged: the literal directory, the `$SOLUTION_VERDICT_DIR` token, the `agent-grounding/solution-verdicts` tail and the literal leaf are still matched on the untouched command; a word that carries a metacharacter is never scrubbed, so `solution-acceptance/../solution-ver*` and `[solution-acceptance]*` keep their leaf word; a custom directory whose own leaf contains the pack name is scanned unscrubbed; the brace/glob leaf-word fallback, `cd` target handling, the cwd-inside check and the read-only classifier are not touched. Both call sites (the Bash arm and the `apply_patch` arm) share the matcher, so both narrow. Still refused (fails closed, cannot be decided without evaluating the shell): a command with a glob/brace character that also contains the word `solution` or `verdicts` outside a plain pack-name word (prose in a heredoc body, a glob over the pack sources such as `solution-acceptance*`), and a command that carries runtime-construction syntax next to a glob/brace character and the word `solution` or `verdicts` (so a heredoc body with a `$` in it, for example, is refused again when it also carries a glob character and the pack name). A name that is built at runtime and never shows the word `solution` or `verdicts` in the command text is not caught, before or after this change; that stays an open residual, as the guard header already says. The named residuals stay open and unchanged: a backslash-escaped leaf, a case-variant leaf on a case-insensitive filesystem, a trailing backslash on a `cd` target, a path in which globs or braces obscure every segment so that neither `solution` nor `verdicts` survives in the text (for example `sol*verd*`), and a path constructed at runtime inside an interpreter. Pinned by the refuse-side fixtures (committed first, passing against the previous matcher) and the allow-side fixtures in `tests/cli/pack-hook-solution-acceptance-writeguard.test.ts` and `tests/policy-packs/solution-acceptance-runtime.test.ts`.
|
|
19
|
+
|
|
10
20
|
## [0.59.0] - 2026-09-25
|
|
11
21
|
|
|
12
22
|
### Upgrade notes
|
|
@@ -52,9 +62,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
52
62
|
|
|
53
63
|
**Upgrade note:** Codex tracks per-hook trust in `[hooks.state]` (`<config path>:<event>:<group index>:<handler index>` keys, `trusted_hash` computed over a normalized hook identity, not raw source text; see `codex-rs/hooks/src/engine/discovery.rs`'s `hook_hash`/`hook_trust_status` and `codex-rs/hooks/src/lib.rs`'s `hook_key`, openai/codex commit `30fc6864cc1318121eca1843c217fe00ce1212f1`). Harness never writes or forges `trusted_hash`; it only stops deleting the table that holds it. A hook whose emitted command, matcher, or `[[hooks.<Event>]]` table position changes (the key embeds the table's ordinal index for that event) gets a new hash/key and reverts to `Untrusted` until the operator re-approves it in Codex, same as any hand-edited hook; this was already true before this fix, it is just no longer masked by the whole trust table being deleted. The UG Codex `post-tool-use` hook disappearing from an install is separate and expected: it is suppressed under `approval_lifecycle: { mode: session }` (`resolveExpireOnToolMatch`, `src/policy-packs/builtin/understanding-before-execution.ts`), pinned by `tests/policy-packs/expand.test.ts` ("Codex adapter contributes..." for the default-present case, "...suppressed when approval_lifecycle.mode = session..." for the opt-out); re-running the install under `mode: session` correctly removes that stale block, it is not a regression. A command, matcher, or timeout change (also an `async`/`status_message` change) yields a new hash under the SAME key; a position change instead moves which key the entry lives under; either way the affected hook's status reads `Modified` or `Untrusted` and Codex will not run it until re-approved. Separately, a `[hooks.state]` entry can carry a positional `enabled` override for that same key; adding or removing a hook earlier in an event's list shifts every later hook's ordinal index, which can land an operator's own `enabled = false` override onto a DIFFERENT hook than the one they disabled. Review Codex's hook list after any install that changes the hook roster (adds, removes, or reorders a hook for an event), not only after one that changes a single hook's command. Also: since harness 0.57.0 (stay-in-scope became an explicit opt-in), a re-install removes the codex "stay in scope" hook block unless `stay_in_scope` is enabled in the manifest -- expected, not a regression, same class as the `post-tool-use` disappearance above.
|
|
54
64
|
|
|
55
|
-
Also hardened in the same change (task `6a037359`): a dedicated fixture in `tests/cli/apply/apply-codex-runtime.test.ts` for each guard that previously had no fixture exercising it -- an operator comment sitting directly above the first foreign table (stays attached to that table, not folded into the replaced block, and appears exactly once); two harness-managed BEGIN markers (refused, file untouched); a BEGIN with no END where a foreign table is followed further down by a real `# harness hook:` comment (refused, since the block boundaries can no longer be read as a plain line scan); a managed block whose END marker is followed by a trailing operator comment at EOF (kept, not silently replaced away;
|
|
65
|
+
Also hardened in the same change (task `6a037359`): a dedicated fixture in `tests/cli/apply/apply-codex-runtime.test.ts` for each guard that previously had no fixture exercising it -- an operator comment sitting directly above the first foreign table (stays attached to that table, not folded into the replaced block, and appears exactly once); two harness-managed BEGIN markers (refused, file untouched); a BEGIN with no END where a foreign table is followed further down by a real `# harness hook:` comment (refused, since the block boundaries can no longer be read as a plain line scan); a managed block whose END marker is followed by a trailing operator comment at EOF (kept, not silently replaced away; a config with no END marker keeps a trailing operator comment too, unless the trailing run holds a harness-authored line, see the task `b34ed105` entry above); exact (not `arrayContaining`) equality on `foreignSectionsPreserved` and `removedHookIds`, plus one exact byte-for-byte equality on the whole installed output for the drifted fixture. A CLI-level test (through the built program, not only the library function) asserts `--dry-run`'s and the applied install's own stdout for the removed-hook-block and preserved-foreign-section lines. The refusal itself is more actionable: it names the RESOLVED config path (never a hard-coded `~/.codex`), the first foreign table (its bracketed header only) and the line number of the first line beyond it that still looks like harness content (never that line's own text), and either tells the operator to move that table below the END marker or delete the lines between BEGIN and END (when one exists), or, when there is no END marker at all, to add one after the last harness hook table or move the foreign table below it instead (telling the operator to move something below a marker that does not exist would be nonsensical); a duplicate-BEGIN refusal tells the operator to delete the entire unwanted BEGIN/END pair, including its own marker lines and source-prefix comment, not just the lines between them. Either refusal offers the `.harness-backup-*` restore only as a last resort with an explicit warning that it discards any Codex state (hook trust, marketplaces, plugins) written since that backup; it throws a dedicated `CodexInstallRefusalError` (a `HarnessExitError` at `EX_FAIL`) instead of a plain `Error`, and `--json` emits a structured `{ outcome: "codex-install-refuse", configPath, error }` object on stdout the same way `target-exists-refuse`/`drift-refuse` already do, instead of only the plain-text message reaching stderr. The install's own permissions no longer widen a secret-bearing config: it preserves the EXISTING file's mode for both the rewritten config and its `.harness-backup-*` copy (0640 as well as 0600, pinned), defaulting to `0600` (not `atomicWriteFile`'s own `0644`) only when creating a config that did not exist before. `foreignSectionsPreserved` prints a namespace's ROOT with a wildcard and its table count when more than one table collapses into it (e.g. `[marketplaces.*] (2 tables)`), instead of silently representing several preserved tables as one indistinguishable header; a single table under a root still prints its own full header. The namespace is the header's first key, or its first two keys under `hooks` (e.g. `[hooks.state.*] (27 tables)`, so Codex's trust table is not reported as `hooks`, which also holds the harness-managed hook arrays), with keys split quote-aware and a trailing comment never part of the namespace or of a printed header. Table-header recognition is a full single-line TOML header shape, quote-aware so a key containing `[`, `]`, `.` or `#` (a real path used as a TOML key, e.g. `[projects."/Users/x/Photos [2024]"]`) is still recognized as one header on every managed-range detection path (a drifted END, no END, and the legacy no-BEGIN path), instead of being misread as ending the header early or as still-owned content; a multi-line array's own continuation line inside a still-owned hook table (e.g. ` [1, 2],`) is never misread as a foreign table header ending the region early. The stray-END search matches a whole trimmed line, not a raw substring, so a foreign single-line value that merely contains the END marker text as a substring is never mistaken for the real stray END. A TOML multi-line string is now tracked by its own opening delimiter (`"""` or `'''`), closed only by that same delimiter, with an escaped quote and a delimiter inside a comment or a single-line string ignored, so a foreign table's own multi-line string value (or a still-owned hook table's own multi-line matcher) can contain a line that reads exactly like a comment, a table header, or even the END marker without corrupting the scan.
|
|
56
66
|
|
|
57
|
-
A TOML-parsing safety net now runs after the byte-preserving splice above computes its output, before anything is written: both the current and the about-to-be-written config are parsed with `smol-toml` (a new runtime dependency, pinned `~1.8.0`) and compared. The invariant: both must parse, and once every harness-managed hook event array (`[[hooks.<Event>]]`) is removed from both documents, everything else must come out identical: `[hooks.state]` and every other single-table `hooks.*` key, plus all non-hooks content (`[projects.*]`, `[plugins.*]`, `[marketplaces.*]`, `[mcp_servers.*]`, top-level keys). Any difference refuses via `CodexInstallRefusalError`, naming the first differing key path, with the file left untouched (a `--dry-run` plan hits the same check and reports the same refusal). For that content the net is independent of the header-recognition and multi-line-string fixes above, so a future scanner regression there refuses instead of silently dropping content. It does not cover the hook event arrays themselves: an operator-authored `[[hooks.<Event>]]` entry that the line scan counts as part of the harness-managed block (for example one sitting between the block and a drifted END marker, or after a BEGIN with no END marker) is still replaced without a refusal; tracked as follow-up task `01053b27`. An operator hook entry outside the managed block is kept whatever its header spelling (a trailing comment, a quoted event key). A leading UTF-8 byte order mark is ignored for parsing (the
|
|
67
|
+
A TOML-parsing safety net now runs after the byte-preserving splice above computes its output, before anything is written: both the current and the about-to-be-written config are parsed with `smol-toml` (a new runtime dependency, pinned `~1.8.0`) and compared. The invariant: both must parse, and once every harness-managed hook event array (`[[hooks.<Event>]]`) is removed from both documents, everything else must come out identical: `[hooks.state]` and every other single-table `hooks.*` key, plus all non-hooks content (`[projects.*]`, `[plugins.*]`, `[marketplaces.*]`, `[mcp_servers.*]`, top-level keys). Any difference refuses via `CodexInstallRefusalError`, naming the first differing key path, with the file left untouched (a `--dry-run` plan hits the same check and reports the same refusal). For that content the net is independent of the header-recognition and multi-line-string fixes above, so a future scanner regression there refuses instead of silently dropping content. It does not cover the hook event arrays themselves: an operator-authored `[[hooks.<Event>]]` entry that the line scan counts as part of the harness-managed block (for example one sitting between the block and a drifted END marker, or after a BEGIN with no END marker) is still replaced without a refusal; tracked as follow-up task `01053b27`. An operator hook entry outside the managed block is kept whatever its header spelling (a trailing comment, a quoted event key). A leading UTF-8 byte order mark is ignored for parsing (the splice keeps it exactly once, also when the harness block starts on the file's first line, see the task `b34ed105` entry above), and an integer beyond 2^53 (TOML integers are 64-bit) is compared exactly instead of failing the parse. A config the harness parser cannot read refuses with "the TOML parser used by harness could not read this file", the parser's line, column and one-line reason, not a claim that the operator's syntax is wrong; a would-be output that does not parse names an operator-defined inline `hooks` table or inline hook-event array as the likely cause before a harness defect. No refusal quotes a value line of the config or the parser's code frame; only table headers and key paths are named: parse refusals carry only that position and reason (never the parser's code frame, which would echo neighbouring lines such as an API token), and the split-block and duplicate-BEGIN refusals name a line number instead of that line's text.
|
|
58
68
|
|
|
59
69
|
## [0.58.0] - 2026-09-23
|
|
60
70
|
|
|
@@ -81,7 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
81
91
|
|
|
82
92
|
- **`harness pack hook solution-acceptance`'s completion-gate deny text now distinguishes the three `gate.verdict === null` readings instead of appending the same reconnect-vs-retry paragraph to all three** (task `799de976`). The hook reads grounding-mcp's documented attempt-lock anchor (`<verdict dir>/<id>.attempt-lock`, README "Attempt lock anchor" row at `grounding-mcp-v0.12.0`) via `proper-lockfile`'s own read-only `checkSync` (already a harness dependency; no new one added) to tell "an attempt is live" apart from "never evaluated" and "a marker path exists but what is there was not accepted as a verdict"; liveness is three-valued (`live`, `not-live`, `unknown`, through a read-only `checkFileLock` in `src/io/lock.ts`), and a note never asserts an absent attempt or a missing marker when the check could not read; the full reconnect-vs-retry paragraph (`solution-acceptance-reconnect.ts`) is now appended only for the live-attempt reading, and the other two readings get their own short, reading-named line instead; the paragraph's own wording is corrected to say the hook itself read the anchor and detected the live-attempt reading, not that it cannot tell the three readings apart (its shared facts, `RECONNECT_FACT_*` and `renderReconnectInstructionsSection`, stay byte-identical). A lock directory left behind by a dead process is read as stale using the producer's OWN staleness window (`DEFAULT_ATTEMPT_LOCK_STALE_MS = 30_000`, `grounding-mcp-v0.12.0` `solution-attempt-log.ts:102`, the default `acquireAttemptLock` passes to `proper-lockfile` when no caller overrides `staleMs`), so this hook's "live" reading and the producer's reclamation rule agree as long as the producer keeps that default and no caller overrides it: an operational pin re-checked on every grounding-mcp bump, not a guarantee. `instructions.md` and grounding-mcp's lock layout are unchanged. See `docs/policy-packs/solution-acceptance.md`, "Reading the attempt-lock anchor to distinguish the three readings", for the full decision record and source citations. Pinned in `tests/cli/pack-hook-solution-acceptance.test.ts` (one case per reading, the overlap of a live lock with an unreadable marker, a stale-lock regression, two check-error-path regressions, a symlinked anchor) and in `tests/io/lock.test.ts` and a literal-wording pin in `tests/policy-packs/solution-acceptance-reconnect.test.ts`; mutation probes removing the live-attempt branch, neutralising the stale rule, substituting a hand-copied paragraph, flipping the check-error catch arm, and reverting the corrected wording (`agent-primitives probe --pre 'npm run build'`) all killed.
|
|
83
93
|
|
|
84
|
-
- **Review-round fixes: the deny note now states only what the hook established** (task `799de976`, commit `3d3bddf`; the same defect class recurred in four consecutive rounds, so this round closes the CLASS). Each round's note asserted something the hook had not established, and each fix replaced one guessed cause with a narrower guess that the next reachable state falsified: first "no attempt reads as currently live" where the liveness check had thrown, then "the verdict directory could not be read" where only the lock path was unreadable, then "the verdict directory or the attempt-lock path could not be read" for an id from which NEITHER path can be derived. An id `sanitizeVerdictId` rejects (`.`, `..`, anything sanitizing to an empty basename, reachable from an active claim; the `SOLUTION_VERDICT_ID` path validates through the same function and answers null instead) is now its own `NullVerdictInfo` union member, settled before anything is read and carrying no `liveness` field, so "the id was unusable" has no field left to render as "a path could not be read"; its note names that one fact, points at the active claim, and asks for a usable id instead of repeating `solution_evaluate` for an id that can never be accepted. The remaining unknown-liveness line names no cause at all. Reading (3) no longer says "could not be read or parsed" for a marker `readVerdict` refuses by policy (a symlink, a non-regular file); it names the complete set of ways that read answers null for a present path, at the hook and at the shared reading-label site (`RECONNECT_THREE_READINGS_LABELS`). A state table in `tests/cli/pack-hook-solution-acceptance.test.ts` ("null-verdict deny note: one exact line per reachable state") pins one exact expected line per reachable (id-usability, reading, liveness) combination, nine rows, and asserts no other state's line appears, so this class fails there rather than reaching an agent; it doubles as the checklist for extending the classifier. The stale-lock fixture's back-date is a literal (90 s) rather than derived from `ATTEMPT_LOCK_STALE_MS`, so it discriminates a widened window instead of staying stale under every mutation of the constant it was meant to pin. Mutation probes restoring the guessed cause, narrowing reading (3)'s wording back, and widening the stale window to 300 s (`agent-primitives probe`, the last one scoped to the stale test alone) all killed.
|
|
94
|
+
- **Review-round fixes: the deny note now states only what the hook established** (task `799de976`, commit `3d3bddf`; the same defect class recurred in four consecutive rounds, so this round closes the CLASS). Each round's note asserted something the hook had not established, and each fix replaced one guessed cause with a narrower guess that the next reachable state falsified: first "no attempt reads as currently live" where the liveness check had thrown, then "the verdict directory could not be read" where only the lock path was unreadable, then "the verdict directory or the attempt-lock path could not be read" for an id from which NEITHER path can be derived. An id `sanitizeVerdictId` rejects (`.`, `..`, anything sanitizing to an empty basename, reachable from an active claim; the `SOLUTION_VERDICT_ID` path validates through the same function and answers null instead) is now its own `NullVerdictInfo` union member, settled before anything is read and carrying no `liveness` field, so "the id was unusable" has no field left to render as "a path could not be read"; its note names that one fact, points at the active claim, and asks for a usable id instead of repeating `solution_evaluate` for an id that can never be accepted. The remaining unknown-liveness line names no cause at all. Reading (3) no longer says "could not be read or parsed" for a marker `readVerdict` refuses by policy (a symlink, a non-regular file); it names the complete set of ways that read answers null for a present path, at the hook and at the shared reading-label site (`RECONNECT_THREE_READINGS_LABELS`). A state table in `tests/cli/pack-hook-solution-acceptance.test.ts` ("null-verdict deny note: one exact line per reachable state") pins one exact expected line per reachable (id-usability, reading, liveness) combination, nine rows at that commit (eleven after the round-6 fixture additions, fourteen once the liveness axis was completed, see the `[Unreleased]` entry for task `8fe0f633`), and asserts no other state's line appears, so this class fails there rather than reaching an agent; it doubles as the checklist for extending the classifier. The stale-lock fixture's back-date is a literal (90 s) rather than derived from `ATTEMPT_LOCK_STALE_MS`, so it discriminates a widened window instead of staying stale under every mutation of the constant it was meant to pin. Mutation probes restoring the guessed cause, narrowing reading (3)'s wording back, and widening the stale window to 300 s (`agent-primitives probe`, the last one scoped to the stale test alone) all killed.
|
|
85
95
|
|
|
86
96
|
- **Round-6 review fix: the deny text is classified from ONE read of the marker** (task `799de976`, commit `244f9c3`). The gate read the marker at the decision site and the deny text classified it again a few statements later, so the two could disagree: a background attempt finishing in that window, the case this pack exists for, made the note tell the agent its fresh marker had been rejected when the gate had simply not seen it yet, and the reasons the note enumerated did not include the one that actually applied. A new `readVerdictDetailed` reports WHY a read yielded no verdict (`invalid-id`, `missing`, the shared reader's `symlink` / `not-regular` / `unreadable`, plus `invalid-record`) in one observation; `readVerdict` is the same function with the reason dropped, so every caller that needs only the gate decision is unchanged. Reading (3) no longer restates a disjunction over the rejection kinds: each kind renders its own line naming what that read established. The lines are a `Record` keyed by the exported `NULL_VERDICT_NOTE_STATES`, so a state with no line fails the build, and the state table's new coverage test fails when a listed state has no fixture, which makes the table a checklist rather than a convention. Two fixtures are added (a schema-invalid record, and a regular file the process may not read), the symlink and directory fixtures now pin their own distinct lines, and the unusable-id remedy names the verb that clears the claim instead of asking for an impossible re-run. The liveness helper loses a `catch` that became unreachable once id usability was settled first: an arm no mutant can kill is worse than no arm. Mutation probes collapsing one rejection line into another, and adding a note state with no fixture, both killed; the removed race itself is not fixture-testable, which is why the fix removes the second observation rather than adding a test for it.
|
|
87
97
|
|
package/README.md
CHANGED
|
@@ -2,85 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
**Declarative control plane for agent harnesses.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
policies, and workflows, plus a CLI that describes, validates, diffs,
|
|
7
|
-
applies, audits, and *enforces*.
|
|
8
|
-
|
|
9
|
-
> Most config tools tell you what an agent is configured to use.
|
|
10
|
-
> `harness` tells you what an agent is *allowed to do*, under this
|
|
11
|
-
> exact context, and why.
|
|
5
|
+
## Overview
|
|
12
6
|
|
|
13
7
|
A coding agent like Claude Code is configured across half a dozen
|
|
14
|
-
files
|
|
15
|
-
hook scripts, per-project overrides
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
generates the config the agent loads from it; and at
|
|
19
|
-
tool calls that violate the declared rules while
|
|
20
|
-
decision.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
until it has logged a review.*
|
|
26
|
-
|
|
27
|
-
Claude Code goes to merge PR 42. Before the tool call runs, the
|
|
28
|
-
runtime hands the event to `harness`, which checks it against the
|
|
29
|
-
manifest. The hook protocol wire shape is the legacy engine-vocabulary
|
|
30
|
-
envelope (operators see this on stderr; agents read it via
|
|
31
|
-
`permissionDecisionReason` when the policy declares no `ux:` block):
|
|
32
|
-
|
|
33
|
-
```console
|
|
34
|
-
$ harness policy intercept # Claude Code runs this before each tool call
|
|
35
|
-
{"decision":"block","reason":"review-before-merge: no matching ledger entry for tag `review:42`","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"review-before-merge: no matching ledger entry for tag `review:42`"}}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Built-in block-enforcement policies ship a `ux:` block since v0.17.0,
|
|
39
|
-
so the agent sees a plain-language three-section form
|
|
40
|
-
([`docs/for-agents.md`](docs/for-agents.md#agent-facing-block-messages-ux-block));
|
|
41
|
-
the engine-vocabulary text above stays in the audit ledger.
|
|
42
|
-
|
|
43
|
-
Blocked. `harness explain` says exactly why:
|
|
44
|
-
|
|
45
|
-
```console
|
|
46
|
-
$ harness explain review-before-merge --trace
|
|
47
|
-
name: review-before-merge
|
|
48
|
-
decision: deny
|
|
49
|
-
enforcement: block
|
|
50
|
-
reason: no matching ledger entry for tag `review:42`
|
|
51
|
-
ledgerTag: review:42
|
|
52
|
-
extract:
|
|
53
|
-
PR_NUMBER: "42"
|
|
54
|
-
requiresEval:
|
|
55
|
-
matchedCount: 0
|
|
56
|
-
reason: no matching ledger entry for tag `review:42`
|
|
57
|
-
# ... (trimmed; the full trace also shows the matched trigger, every extracted variable, and the ledger query)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The rule pulled `PR_NUMBER=42` out of the tool call and looked for a
|
|
61
|
-
`review:42` entry in the evidence ledger. There wasn't one. So the
|
|
62
|
-
reviewer (or a review subagent) logs that entry, and the *same* merge
|
|
63
|
-
call, retried, goes straight through, no restart, no config edit:
|
|
64
|
-
|
|
65
|
-
```console
|
|
66
|
-
$ harness policy intercept # same call, after the review was logged
|
|
67
|
-
$ # (no output, exit 0: allowed)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Every one of those decisions is recorded:
|
|
71
|
-
|
|
72
|
-
```console
|
|
73
|
-
$ harness audit --since 1h --policy review-before-merge
|
|
74
|
-
timestamp policy outcome reason
|
|
75
|
-
------------------- ------------------- ------- --------------------------------------------
|
|
76
|
-
2026-05-14 19:09:03 review-before-merge deny no matching ledger entry for tag `review:42`
|
|
77
|
-
2026-05-14 19:09:13 review-before-merge allow 1 matching ledger entry for tag `review:42`
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Declare the rule once; every session is held to it, with a paper
|
|
81
|
-
trail of every decision.
|
|
82
|
-
|
|
83
|
-
## Concepts in six lines
|
|
8
|
+
files: `settings.json`, `CLAUDE.md`, memory notes, MCP registrations,
|
|
9
|
+
hook scripts, per-project overrides. No single file answers "what can
|
|
10
|
+
this agent do right now, and why is it set up that way?" `harness`
|
|
11
|
+
puts all of it in one zod-validated YAML manifest you read, validate,
|
|
12
|
+
and diff; generates the config the agent runtime loads from it; and at
|
|
13
|
+
runtime blocks tool calls that violate the declared rules while
|
|
14
|
+
recording every decision to an evidence ledger. Most config tools
|
|
15
|
+
describe what an agent is configured to use; `harness` decides what it
|
|
16
|
+
is allowed to do, under the exact context, and records why.
|
|
17
|
+
|
|
18
|
+
## Key concepts
|
|
84
19
|
|
|
85
20
|
| Term | What it is |
|
|
86
21
|
|------|-----------|
|
|
@@ -91,213 +26,82 @@ trail of every decision.
|
|
|
91
26
|
| **hook** | A script the agent runtime runs at a lifecycle event (session start, before every tool call, ...). How policies get enforced. |
|
|
92
27
|
| **policy pack** | A reusable bundle of policies, hooks, and templates shipped under one name and enabled with a single manifest key. |
|
|
93
28
|
|
|
94
|
-
##
|
|
95
|
-
|
|
96
|
-
```mermaid
|
|
97
|
-
flowchart LR
|
|
98
|
-
declare["1. Declare<br/><code>harness.yaml</code>"]
|
|
99
|
-
apply["2. Apply<br/><code>harness apply</code>"]
|
|
100
|
-
enforce["3. Enforce<br/>hooks + policies<br/>at runtime"]
|
|
101
|
-
record[("4. Record<br/>evidence ledger")]
|
|
102
|
-
observe["5. Observe<br/><code>audit</code> / <code>explain</code> /<br/><code>session-export</code>"]
|
|
103
|
-
|
|
104
|
-
declare --> apply
|
|
105
|
-
apply --> enforce
|
|
106
|
-
enforce --> record
|
|
107
|
-
record --> observe
|
|
108
|
-
observe -. refine .-> declare
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Observe → refine → declare is the whole loop. The read-side surfaces
|
|
112
|
-
(`audit`, `explain --trace`, `session-export`) replay rows the runtime
|
|
113
|
-
already recorded, so what flows back into the manifest is grounded in
|
|
114
|
-
what actually happened.
|
|
115
|
-
|
|
116
|
-
## Pick your audience
|
|
117
|
-
|
|
118
|
-
- **Operator?** [`docs/for-humans.md`](docs/for-humans.md): install through first `apply`, first real policy, diagnostics cheat sheet.
|
|
119
|
-
- **Agent (or onboarding one)?** [`docs/for-agents.md`](docs/for-agents.md): workflow lifecycle, policy / ledger sequence, CLI cheat sheet by side-effect class, the audit triumvirate.
|
|
120
|
-
- **Writing your own policy?** [`docs/writing-custom-policies.md`](docs/writing-custom-policies.md): three tripwires, four worked recipes (each validated in CI), author loop, field reference.
|
|
121
|
-
- **Guarding destructive runtime commands?** [`docs/runtime-reality-hook.md`](docs/runtime-reality-hook.md): block compose/systemctl/kill/deploy calls when the live process state has drifted from what the docs expect.
|
|
122
|
-
- **Looking up a CLI verb?** [`docs/CLI.md`](docs/CLI.md): every command the `harness` binary exposes, grouped by purpose (manifest, runtime, hooks, approvals, gates, preflight).
|
|
123
|
-
|
|
124
|
-
## Install
|
|
29
|
+
## Quick start
|
|
125
30
|
|
|
126
31
|
```bash
|
|
127
|
-
npm i -g @lannguyensi/harness
|
|
32
|
+
npm i -g @lannguyensi/harness # Node 20 or newer
|
|
33
|
+
harness init --interactive # guided wizard, or: --template solo|team|full
|
|
128
34
|
```
|
|
129
35
|
|
|
130
|
-
The
|
|
36
|
+
The wizard detects your `~/.claude/` and `~/.codex/` setup, MCP
|
|
37
|
+
servers already wired in `settings.json`, and the harness binary
|
|
38
|
+
version, then writes a starting `harness.yaml`. Full operator
|
|
39
|
+
walkthrough: [`docs/for-humans.md`](docs/for-humans.md); a five-minute
|
|
40
|
+
non-interactive path plus a profile comparison table:
|
|
41
|
+
[`docs/quickstart.md`](docs/quickstart.md).
|
|
131
42
|
|
|
132
|
-
##
|
|
43
|
+
## Usage
|
|
133
44
|
|
|
134
|
-
|
|
135
|
-
|
|
45
|
+
Preview which policies would fire for a tool call, before any ledger
|
|
46
|
+
I/O or file write:
|
|
136
47
|
|
|
137
48
|
```bash
|
|
138
|
-
harness
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Guided wizard. Detects `~/.claude/` and `~/.codex/`, MCP servers
|
|
142
|
-
already wired in `settings.json`, harness binary version. Picks a
|
|
143
|
-
profile (`solo` / `team` / `full` / `custom`) and writes a starting
|
|
144
|
-
`harness.yaml`. Ctrl-C aborts cleanly. Walkthrough +
|
|
145
|
-
limitations: [`docs/init-interactive.md`](docs/init-interactive.md).
|
|
146
|
-
|
|
147
|
-
Every enabled `tools.mcp[]` server is then registered with Claude
|
|
148
|
-
Code via `claude mcp add-json --scope user` — the surface Claude Code
|
|
149
|
-
actually reads for user-scope MCP servers; harness itself never
|
|
150
|
-
writes `~/.claude.json`. This step requires the `claude` CLI on
|
|
151
|
-
`$PATH`; if it's missing, the wizard warns and prints the equivalent
|
|
152
|
-
`claude mcp add-json` commands to run by hand instead of failing.
|
|
153
|
-
`harness doctor` includes a "Claude Code MCP Registration" section
|
|
154
|
-
that re-verifies the live registration via `claude mcp list`.
|
|
155
|
-
|
|
156
|
-
### Profiles at a glance
|
|
157
|
-
|
|
158
|
-
| Profile | External accounts / tools required | Best for |
|
|
159
|
-
|---------|------------------------------------|----------|
|
|
160
|
-
| `solo` | None. `npm` + Claude Code is enough. | Single operators who want the Understanding Gate without committing to a tasking system. |
|
|
161
|
-
| `team` | An **agent-tasks** account ([hosted](https://agent-tasks.opentriologue.ai) or [self-hosted](https://github.com/LanNguyenSi/agent-tasks)). | Teams that already use `agent-tasks` for PR review tracking. The merge gate (`review:<pr-number>` ledger tag) wires against the agent-tasks MCP. |
|
|
162
|
-
| `full` | Same as `team` plus `@lannguyensi/agent-preflight` and `gh` on PATH. | Operators who want every reference policy enforced (dogfood gate, preflight gates, review-subagent gate, merge gate). |
|
|
163
|
-
|
|
164
|
-
**Not using agent-tasks?** Pick `solo`. The `team` and `full` review gates currently match only the agent-tasks MCP tool names, so a `gh pr create` workflow stays unprotected by them today. Tool-agnostic gates that also match `gh pr` are tracked in the backlog.
|
|
165
|
-
|
|
166
|
-
If you prefer non-interactive (CI, fresh-VM provisioning), pick a
|
|
167
|
-
template directly:
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
harness init --template solo # memory-router + understanding-before-execution pack
|
|
171
|
-
harness init --template team # solo + agent-tasks MCP + review-before-merge policy
|
|
172
|
-
harness init --template full # everything from the Appendix A reference manifest
|
|
49
|
+
harness dry-run "merge PR 42" \
|
|
50
|
+
--tool mcp__agent-tasks__pull_requests_merge \
|
|
51
|
+
--tool-args '{"prNumber":42}'
|
|
173
52
|
```
|
|
174
53
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
54
|
+
Once a manifest is applied, the same shape runs live as
|
|
55
|
+
`harness policy intercept` (invoked by the runtime's PreToolUse hook),
|
|
56
|
+
and `harness explain <policy> --trace` / `harness audit --since 1h`
|
|
57
|
+
replay what actually fired and why. The full install-to-audit
|
|
58
|
+
walkthrough is in [`docs/quickstart.md`](docs/quickstart.md).
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
- [`docs/for-humans.md`](docs/for-humans.md): operator path, install through first real policy, diagnostics cheat sheet.
|
|
63
|
+
- [`docs/for-agents.md`](docs/for-agents.md): agent integration contract, workflow lifecycle, CLI cheat sheet by side-effect class.
|
|
64
|
+
- [`docs/quickstart.md`](docs/quickstart.md): five-minute bare-command path and profile comparison.
|
|
65
|
+
- [`docs/init-interactive.md`](docs/init-interactive.md): the `harness init --interactive` wizard, walkthrough and limitations.
|
|
66
|
+
- [`docs/CLI.md`](docs/CLI.md): every CLI verb, grouped by purpose.
|
|
67
|
+
- [`docs/risk-gate.md`](docs/risk-gate.md): the four-way `allow / warn / require_approval / deny` Risk Gate.
|
|
68
|
+
- [`docs/writing-custom-policies.md`](docs/writing-custom-policies.md): tripwires, worked recipes, and the policy field reference.
|
|
69
|
+
- [`docs/runtime-reality-hook.md`](docs/runtime-reality-hook.md): blocking destructive runtime commands when live process state has drifted from what the docs expect.
|
|
70
|
+
- [`docs/policy-packs/README.md`](docs/policy-packs/README.md): the built-in policy packs (`understanding-before-execution`, `branch-protection`, `solution-acceptance`, `post-merge-gate`).
|
|
71
|
+
- [`docs/uninstall.md`](docs/uninstall.md): the single-command teardown, dry-run by default.
|
|
72
|
+
- [`docs/examples/full-manifest.yaml`](docs/examples/full-manifest.yaml): a schema-coverage reference (not a runnable config; its header explains why).
|
|
73
|
+
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md): manifest schema, file layout, CLI surface (historical design intent; see its own note for the current shape).
|
|
74
|
+
- [`docs/VISION.md`](docs/VISION.md): why a declarative control plane, not more individual tools.
|
|
75
|
+
- [`docs/ROADMAP.md`](docs/ROADMAP.md): the phase-by-phase plan and the acceptance criteria each phase shipped against.
|
|
76
|
+
- [`CHANGELOG.md`](CHANGELOG.md): what shipped in each version.
|
|
77
|
+
|
|
78
|
+
### Related projects
|
|
79
|
+
|
|
80
|
+
- [`agent-grounding`](https://github.com/LanNguyenSi/agent-grounding): evidence-ledger, claim-gate, review-claim-gate; `grounding-mcp` is the canonical client surface harness queries.
|
|
81
|
+
- [`agent-memory`](https://github.com/LanNguyenSi/agent-memory): the memory surfaces the control plane inventories.
|
|
82
|
+
- [`agent-tasks`](https://github.com/LanNguyenSi/agent-tasks): MCP-registered task platform whose registration and health appear in `harness describe`.
|
|
83
|
+
- [`agent-preflight`](https://github.com/LanNguyenSi/agent-preflight): local preflight validator; the canonical implementation of preflight-hook content harness wires.
|
|
84
|
+
- [`codebase-oracle`](https://github.com/LanNguyenSi/codebase-oracle): opt-in MCP for multi-repo RAG search; wire via `harness add mcp codebase-oracle --command codebase-oracle,mcp`.
|
|
85
|
+
- [`agent-dx`](https://github.com/LanNguyenSi/agent-dx): ships `git-batch-cli`, a day-to-day tool whose inventory appears in `harness describe`.
|
|
179
86
|
|
|
180
|
-
|
|
181
|
-
for a given tool call, against the reference manifest, before any
|
|
182
|
-
ledger I/O:
|
|
87
|
+
## Development and contributing
|
|
183
88
|
|
|
184
89
|
```bash
|
|
185
|
-
|
|
186
|
-
npm
|
|
187
|
-
|
|
188
|
-
--tool mcp__agent-tasks__pull_requests_merge \
|
|
189
|
-
--tool-args '{"prNumber":42}' \
|
|
190
|
-
--config docs/examples/full-manifest.yaml
|
|
90
|
+
npm install
|
|
91
|
+
npm run build
|
|
92
|
+
npm test
|
|
191
93
|
```
|
|
192
94
|
|
|
193
|
-
`
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`harness init --interactive`.
|
|
197
|
-
|
|
198
|
-
## Uninstall
|
|
199
|
-
|
|
200
|
-
`harness uninstall` is the single-command teardown: dry-run by default,
|
|
201
|
-
`--apply` to mutate, `--restore-from <backup>` to roll back. Full
|
|
202
|
-
inventory + recommended order in [`docs/uninstall.md`](docs/uninstall.md).
|
|
95
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full PR checklist
|
|
96
|
+
(import-boundary and duplication gates, changelog coverage, release
|
|
97
|
+
steps).
|
|
203
98
|
|
|
204
99
|
## Status
|
|
205
100
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
the way: `harness pause/resume` in `v0.22.0`, `migrate-home` in
|
|
211
|
-
`v0.24.0`, Codex-runtime adapter polish in `v0.28.x` and `v0.29.0`,
|
|
212
|
-
`approve risk --force` in `v0.30.0`, the opt-in `runtime-reality`
|
|
213
|
-
drift gate in `v0.31.0`, the opt-in `solution-acceptance` pack in
|
|
214
|
-
`v0.32.0`, the operator-only `approve branch-protection` marker in
|
|
215
|
-
`v0.33.0`, the `harness gc` retention cleanup plus non-TTY-safe
|
|
216
|
-
confirmations in `v0.34.0`, `apply` failing loud when policies are
|
|
217
|
-
declared without `grounding-mcp` wired in `v0.35.0`, and the
|
|
218
|
-
`harness doctor --rm-rogue-ledgers` cleanup mode plus the read-only
|
|
219
|
-
Bash classifier re-admitting `sort` / `tree` / `file` in `v0.36.0`.
|
|
220
|
-
Selected milestones since then (not exhaustive; see `CHANGELOG.md`
|
|
221
|
-
for the full list): HMAC-signed understanding-gate approval markers
|
|
222
|
-
in `v0.41.0`, the opt-in `post-merge-gate` policy pack in `v0.42.0`,
|
|
223
|
-
the `&`-as-command-boundary trigger fix in `v0.43.0`, per-policy
|
|
224
|
-
cross-repository attribution for `${REPO}`/`${BRANCH}` preflight
|
|
225
|
-
facts in `v0.44.0`, and `apply --runtime codex|opencode` next-steps
|
|
226
|
-
hints in `v0.46.0`.
|
|
227
|
-
The current release is `v0.59.0`.
|
|
228
|
-
|
|
229
|
-
The phase-by-phase plan with acceptance criteria lives in
|
|
230
|
-
[`docs/ROADMAP.md`](docs/ROADMAP.md); what shipped in each version is
|
|
231
|
-
in [`CHANGELOG.md`](CHANGELOG.md).
|
|
232
|
-
|
|
233
|
-
## Policy Packs
|
|
234
|
-
|
|
235
|
-
A *Policy Pack* is a reusable bundle of hooks, policies, instruction
|
|
236
|
-
template, and permission profiles shipped under one name and enabled
|
|
237
|
-
from `harness.yaml` with a single key:
|
|
238
|
-
|
|
239
|
-
```yaml
|
|
240
|
-
policy_packs:
|
|
241
|
-
- name: understanding-before-execution
|
|
242
|
-
config:
|
|
243
|
-
mode: grill_me # fast_confirm | grill_me | strict
|
|
244
|
-
permission_profile: safe-start # safe-start | implementation-after-approval | high-risk-grill-me
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Manage packs with `harness pack add / remove / list`. Four packs ship
|
|
248
|
-
today: [`understanding-before-execution`](docs/policy-packs/understanding-before-execution.md)
|
|
249
|
-
(forces an Understanding Report before any write-capable tool fires),
|
|
250
|
-
[`branch-protection`](docs/policy-packs/branch-protection.md)
|
|
251
|
-
(blocks source mutations on protected branches without an explicit
|
|
252
|
-
override), the opt-in
|
|
253
|
-
[`solution-acceptance`](docs/policy-packs/solution-acceptance.md)
|
|
254
|
-
completion gate (added in `v0.32.0`; holds a task done until an
|
|
255
|
-
accepted solution verdict is logged), and the opt-in
|
|
256
|
-
[`post-merge-gate`](docs/policy-packs/post-merge-gate.md) pack (added
|
|
257
|
-
in `v0.42.0`; denies mutating git/gh work on a branch whose tip has
|
|
258
|
-
already been merged). Custom packs from `path:`, `npm:`, or `git:` sources are
|
|
259
|
-
out of scope for v1 (see the pack docs for the future-vocabulary
|
|
260
|
-
contract).
|
|
261
|
-
|
|
262
|
-
## What's next
|
|
263
|
-
|
|
264
|
-
The seven-phase roadmap is complete. The Risk Gate (Phase 7) shipped in
|
|
265
|
-
`v0.27.0`: `harness policy intercept` reasons about the action itself
|
|
266
|
-
(Action Envelope → Context Resolver → Risk Classifier), evaluates each
|
|
267
|
-
policy's `when:` clauses, and enforces a four-way `allow / warn /
|
|
268
|
-
require_approval / deny` decision, so `DROP TABLE users`, `kubectl
|
|
269
|
-
delete namespace prod`, and `terraform destroy` against an unverified
|
|
270
|
-
production target are blocked before the runtime fires them. See
|
|
271
|
-
[`docs/risk-gate.md`](docs/risk-gate.md).
|
|
272
|
-
|
|
273
|
-
Capability beyond the seven phases is not a quiet roadmap expansion: it
|
|
274
|
-
lands as an explicit follow-up design doc or a separate sibling
|
|
275
|
-
project, per [`docs/ROADMAP.md`](docs/ROADMAP.md) ("Out of scope across
|
|
276
|
-
all phases").
|
|
277
|
-
|
|
278
|
-
> Bring your favorite agent harness. Add governance.
|
|
279
|
-
|
|
280
|
-
## Why this exists
|
|
281
|
-
|
|
282
|
-
On 2026-04-23, an `agent-grounding` checkout that was 16 commits
|
|
283
|
-
behind origin led two tasks to be incorrectly called "stale". The
|
|
284
|
-
check that would have caught it already existed:
|
|
285
|
-
[`agent-preflight`](https://github.com/LanNguyenSi/agent-preflight)
|
|
286
|
-
runs `git fetch` + `git status` and emits a structured `ready` +
|
|
287
|
-
confidence-score result. The missing piece was not the check, it was
|
|
288
|
-
the deterministic *trigger*: a `SessionStart` hook that invokes
|
|
289
|
-
`preflight run` and a policy that gates further work on the result.
|
|
290
|
-
Building that wiring needs an agreed-upon place for harness config to
|
|
291
|
-
live first. That conversation is the origin of this repo.
|
|
292
|
-
|
|
293
|
-
## Related
|
|
294
|
-
|
|
295
|
-
- [`agent-grounding`](https://github.com/LanNguyenSi/agent-grounding): evidence-ledger, claim-gate, review-claim-gate; `grounding-mcp` is the canonical client surface harness queries through `queryLedgerByTag`.
|
|
296
|
-
- [`agent-memory`](https://github.com/LanNguyenSi/agent-memory): the memory surfaces the control plane inventories.
|
|
297
|
-
- [`agent-tasks`](https://github.com/LanNguyenSi/agent-tasks): MCP-registered task platform whose registration + health appear in `harness describe`.
|
|
298
|
-
- [`agent-preflight`](https://github.com/LanNguyenSi/agent-preflight): local preflight validator; the canonical implementation of preflight-hook content harness wires.
|
|
299
|
-
- [`codebase-oracle`](https://github.com/LanNguyenSi/codebase-oracle): opt-in MCP for multi-repo RAG search; not in Full, wire via `harness add mcp codebase-oracle --command codebase-oracle,mcp`.
|
|
300
|
-
- [`agent-dx`](https://github.com/LanNguyenSi/agent-dx): ships `git-batch-cli`, a day-to-day tool whose inventory appears in `harness describe`.
|
|
101
|
+
The current release is `v0.59.1`. All seven planned phases have
|
|
102
|
+
shipped; phase acceptance criteria are in
|
|
103
|
+
[`docs/ROADMAP.md`](docs/ROADMAP.md), and what shipped in each version
|
|
104
|
+
is in [`CHANGELOG.md`](CHANGELOG.md).
|
|
301
105
|
|
|
302
106
|
## License
|
|
303
107
|
|
package/dist/cli/apply/apply.js
CHANGED
|
@@ -397,7 +397,7 @@ function buildExpectedFiles(manifest, opts, manifestPath, generatedDir) {
|
|
|
397
397
|
// settings.json, same reasoning as the codex branch above --
|
|
398
398
|
// settings.json is Claude Code's contract and meaningless to
|
|
399
399
|
// opencode. MEMORY.md ships unchanged. Pack instructions.md files
|
|
400
|
-
// are NOT runtime-agnostic (
|
|
400
|
+
// are NOT runtime-agnostic (batch18 fix-round, task
|
|
401
401
|
// f34eb233): each builtin pack's buildInstructions() branches on
|
|
402
402
|
// `runtime` and, under opencode, documents that its hooks are
|
|
403
403
|
// UNSUPPORTED/not-wired (opencode has no declarative hook/event
|
|
@@ -482,7 +482,7 @@ function buildPrevManifestForHints(record) {
|
|
|
482
482
|
// `emitRestartHints` are the same view. A snapshot written by a
|
|
483
483
|
// release that predates the derivation therefore compares equal to
|
|
484
484
|
// today's manifest instead of emitting a one-time phantom "hooks
|
|
485
|
-
// changed" hint (
|
|
485
|
+
// changed" hint (task 99f47307, Slice 1).
|
|
486
486
|
return withDerivedPolicies(parseManifest(parsed));
|
|
487
487
|
}
|
|
488
488
|
catch {
|