@lannguyensi/harness 0.15.0 → 0.16.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +17 -1
  3. package/dist/cli/doctor/format.js +24 -0
  4. package/dist/cli/doctor/format.js.map +1 -1
  5. package/dist/cli/doctor/index.d.ts +7 -0
  6. package/dist/cli/doctor/index.js +10 -0
  7. package/dist/cli/doctor/index.js.map +1 -1
  8. package/dist/cli/doctor/rogue-ledger.d.ts +25 -0
  9. package/dist/cli/doctor/rogue-ledger.js +106 -0
  10. package/dist/cli/doctor/rogue-ledger.js.map +1 -0
  11. package/dist/cli/doctor/types.d.ts +10 -1
  12. package/dist/cli/doctor/types.js.map +1 -1
  13. package/dist/cli/index.js +177 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/pack/hook-branch-protection.d.ts +30 -0
  16. package/dist/cli/pack/hook-branch-protection.js +279 -0
  17. package/dist/cli/pack/hook-branch-protection.js.map +1 -0
  18. package/dist/cli/pack/hook-codex-pre-tool-use.js +3 -1
  19. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
  20. package/dist/cli/pack/hook-pre-tool-use.js +7 -2
  21. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  22. package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
  23. package/dist/cli/pack/understanding-report-schema-hint.js +54 -0
  24. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -0
  25. package/dist/cli/session-start/branch-check.d.ts +44 -0
  26. package/dist/cli/session-start/branch-check.js +165 -0
  27. package/dist/cli/session-start/branch-check.js.map +1 -0
  28. package/dist/cli/uninstall/index.d.ts +68 -0
  29. package/dist/cli/uninstall/index.js +586 -0
  30. package/dist/cli/uninstall/index.js.map +1 -0
  31. package/dist/cli/uninstall/snapshot.d.ts +40 -0
  32. package/dist/cli/uninstall/snapshot.js +34 -0
  33. package/dist/cli/uninstall/snapshot.js.map +1 -0
  34. package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +47 -0
  35. package/dist/policy-packs/builtin/branch-protection-runtime.js +92 -0
  36. package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -0
  37. package/dist/policy-packs/builtin/branch-protection.d.ts +9 -0
  38. package/dist/policy-packs/builtin/branch-protection.js +146 -0
  39. package/dist/policy-packs/builtin/branch-protection.js.map +1 -0
  40. package/dist/policy-packs/registry.d.ts +1 -1
  41. package/dist/policy-packs/registry.js +10 -3
  42. package/dist/policy-packs/registry.js.map +1 -1
  43. package/package.json +2 -1
@@ -0,0 +1,34 @@
1
+ // Snapshot file format + path helpers for `harness uninstall --apply`.
2
+ //
3
+ // Sibling of `src/cli/gate/snapshot.ts`. A snapshot captures one
4
+ // uninstall invocation: which hook groups and which `mcpServers` entries
5
+ // were removed from `~/.claude/settings.json`, the sha256 of settings.json
6
+ // before/after, and the path the original was backed up to. Reading the
7
+ // snapshot is enough to manually reverse the uninstall (or to drive a
8
+ // future `harness reinstall` verb).
9
+ //
10
+ // The format mirrors `GateDisableSnapshot` shape but lives in its own
11
+ // file: the two verbs have different scopes (gate-disable is matcher-
12
+ // driven; uninstall is harness-ownership-driven) and storing them in the
13
+ // same snapshot stream would mean every reader has to disambiguate via
14
+ // `filter` or a discriminator. Cheaper to keep separate.
15
+ import * as crypto from "node:crypto";
16
+ import * as path from "node:path";
17
+ export const SNAPSHOT_BASENAME_PREFIX = "harness.uninstall.";
18
+ export const SNAPSHOT_BASENAME_SUFFIX = ".json";
19
+ export const BACKUP_INFIX = ".bak.uninstall.";
20
+ export const SNAPSHOT_VERSION = 1;
21
+ export function sha256Hex(input) {
22
+ return crypto.createHash("sha256").update(input).digest("hex");
23
+ }
24
+ /** Build the snapshot file path for `settingsPath` at `now`. */
25
+ export function snapshotPath(settingsPath, now) {
26
+ const stamp = now.toISOString().replace(/:/g, "-");
27
+ return path.join(path.dirname(settingsPath), `${SNAPSHOT_BASENAME_PREFIX}${stamp}${SNAPSHOT_BASENAME_SUFFIX}`);
28
+ }
29
+ /** Build the settings-backup file path for `settingsPath` at `now`. */
30
+ export function backupPath(settingsPath, now) {
31
+ const stamp = now.toISOString().replace(/:/g, "-");
32
+ return `${settingsPath}${BACKUP_INFIX}${stamp}`;
33
+ }
34
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/cli/uninstall/snapshot.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,iEAAiE;AACjE,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,sEAAsE;AACtE,oCAAoC;AACpC,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,yEAAyE;AACzE,uEAAuE;AACvE,yDAAyD;AAEzD,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAAoB,CAAC;AAC7D,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAChD,MAAM,CAAC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC;AAoC3C,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,YAAoB,EAAE,GAAS;IAC1D,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAC1B,GAAG,wBAAwB,GAAG,KAAK,GAAG,wBAAwB,EAAE,CACjE,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAC,YAAoB,EAAE,GAAS;IACxD,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,KAAK,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { PolicyPack } from "../../schema/index.js";
2
+ export declare const PACK_NAME = "branch-protection";
3
+ /**
4
+ * Ledger tag written by the producer when the current branch is NOT in
5
+ * the operator's protected list. The blocker substring-matches this
6
+ * prefix; the trailing `:<branch>` is informational and keeps the
7
+ * ledger row self-describing for auditors.
8
+ */
9
+ export declare const NON_PROTECTED_TAG_PREFIX = "branch:non-protected";
10
+ /**
11
+ * Operator escape-hatch tag. Set via `mcp__agent-grounding__ledger_add`
12
+ * (Bash is gated by this very pack, so a shell-based override would be
13
+ * unreachable). The blocker substring-matches this prefix; the trailing
14
+ * `:<reason>` is a free-form note the operator types so a later audit
15
+ * can read WHY the override fired (e.g. `branch-protection-ack:hotfix
16
+ * for prod`).
17
+ */
18
+ export declare const ACK_TAG_PREFIX = "branch-protection-ack";
19
+ /**
20
+ * Freshness window for the producer tag. Five minutes lets a single
21
+ * branch-check satisfy a whole edit batch without re-running for every
22
+ * Write; longer than that and a branch switch in the middle of a
23
+ * session would silently keep the gate open against the new HEAD.
24
+ */
25
+ export declare const PRODUCER_FRESHNESS_MS: number;
26
+ /** Branches gated by default when no `config.protected_branches` is set. */
27
+ export declare const DEFAULT_PROTECTED_BRANCHES: readonly string[];
28
+ /**
29
+ * Parse the pack's `config.protected_branches` list. Falls back to the
30
+ * default allowlist when the operator hasn't customized it, OR when the
31
+ * provided value isn't a non-empty string array (the warning surfaces
32
+ * the type mismatch so the operator can fix it).
33
+ *
34
+ * Returns the resolved list plus a non-null warning message when the
35
+ * raw config was ill-formed. Caller appends the warning to the pack's
36
+ * `warnings` collection so it lands in apply output.
37
+ */
38
+ export declare function resolveProtectedBranches(pack: PolicyPack): {
39
+ branches: string[];
40
+ warning: string | null;
41
+ };
42
+ /**
43
+ * True when `branch` is in the protected list. Empty branch (detached
44
+ * HEAD) is treated as protected, because we can't audit-by-name what
45
+ * the agent is about to commit to.
46
+ */
47
+ export declare function isProtectedBranch(branch: string, protectedList: readonly string[]): boolean;
@@ -0,0 +1,92 @@
1
+ // Shared runtime constants + helpers for the `branch-protection` policy pack.
2
+ //
3
+ // The pack itself (`branch-protection.ts`) only emits hooks + the
4
+ // audit-copy instructions. The actual enforcement lives in two CLI verbs
5
+ // — `harness session-start branch-check` (producer) and `harness pack
6
+ // hook branch-protection` (blocker) — both under `src/cli/`. This module
7
+ // is the small shared surface they pull from: tag formats, default
8
+ // protected list, config parsing.
9
+ export const PACK_NAME = "branch-protection";
10
+ /**
11
+ * Ledger tag written by the producer when the current branch is NOT in
12
+ * the operator's protected list. The blocker substring-matches this
13
+ * prefix; the trailing `:<branch>` is informational and keeps the
14
+ * ledger row self-describing for auditors.
15
+ */
16
+ export const NON_PROTECTED_TAG_PREFIX = "branch:non-protected";
17
+ /**
18
+ * Operator escape-hatch tag. Set via `mcp__agent-grounding__ledger_add`
19
+ * (Bash is gated by this very pack, so a shell-based override would be
20
+ * unreachable). The blocker substring-matches this prefix; the trailing
21
+ * `:<reason>` is a free-form note the operator types so a later audit
22
+ * can read WHY the override fired (e.g. `branch-protection-ack:hotfix
23
+ * for prod`).
24
+ */
25
+ export const ACK_TAG_PREFIX = "branch-protection-ack";
26
+ /**
27
+ * Freshness window for the producer tag. Five minutes lets a single
28
+ * branch-check satisfy a whole edit batch without re-running for every
29
+ * Write; longer than that and a branch switch in the middle of a
30
+ * session would silently keep the gate open against the new HEAD.
31
+ */
32
+ export const PRODUCER_FRESHNESS_MS = 5 * 60 * 1000;
33
+ /** Branches gated by default when no `config.protected_branches` is set. */
34
+ export const DEFAULT_PROTECTED_BRANCHES = [
35
+ "master",
36
+ "main",
37
+ "develop",
38
+ ];
39
+ /**
40
+ * Parse the pack's `config.protected_branches` list. Falls back to the
41
+ * default allowlist when the operator hasn't customized it, OR when the
42
+ * provided value isn't a non-empty string array (the warning surfaces
43
+ * the type mismatch so the operator can fix it).
44
+ *
45
+ * Returns the resolved list plus a non-null warning message when the
46
+ * raw config was ill-formed. Caller appends the warning to the pack's
47
+ * `warnings` collection so it lands in apply output.
48
+ */
49
+ export function resolveProtectedBranches(pack) {
50
+ const raw = pack.config["protected_branches"];
51
+ if (raw === undefined) {
52
+ return { branches: [...DEFAULT_PROTECTED_BRANCHES], warning: null };
53
+ }
54
+ if (!Array.isArray(raw)) {
55
+ return {
56
+ branches: [...DEFAULT_PROTECTED_BRANCHES],
57
+ warning: `policy_packs[${pack.name}].config.protected_branches: expected an array of strings, got ${typeof raw}; falling back to defaults (${DEFAULT_PROTECTED_BRANCHES.join(", ")}).`,
58
+ };
59
+ }
60
+ const ok = [];
61
+ const bad = [];
62
+ for (const entry of raw) {
63
+ if (typeof entry === "string" && entry.length > 0)
64
+ ok.push(entry);
65
+ else
66
+ bad.push(entry);
67
+ }
68
+ if (ok.length === 0) {
69
+ return {
70
+ branches: [...DEFAULT_PROTECTED_BRANCHES],
71
+ warning: `policy_packs[${pack.name}].config.protected_branches: every entry was rejected (need non-empty strings); falling back to defaults (${DEFAULT_PROTECTED_BRANCHES.join(", ")}).`,
72
+ };
73
+ }
74
+ if (bad.length > 0) {
75
+ return {
76
+ branches: ok,
77
+ warning: `policy_packs[${pack.name}].config.protected_branches: skipped ${bad.length} non-string entr${bad.length === 1 ? "y" : "ies"}; using ${ok.length} valid one${ok.length === 1 ? "" : "s"} (${ok.join(", ")}).`,
78
+ };
79
+ }
80
+ return { branches: ok, warning: null };
81
+ }
82
+ /**
83
+ * True when `branch` is in the protected list. Empty branch (detached
84
+ * HEAD) is treated as protected, because we can't audit-by-name what
85
+ * the agent is about to commit to.
86
+ */
87
+ export function isProtectedBranch(branch, protectedList) {
88
+ if (branch.length === 0)
89
+ return true;
90
+ return protectedList.includes(branch);
91
+ }
92
+ //# sourceMappingURL=branch-protection-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-protection-runtime.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/branch-protection-runtime.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,kEAAkE;AAClE,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,kCAAkC;AAIlC,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D,QAAQ;IACR,MAAM;IACN,SAAS;CACV,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IAIvD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACtE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC;YACzC,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,kEAAkE,OAAO,GAAG,+BAA+B,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACvL,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAa,EAAE,CAAC;IACxB,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;;YAC7D,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO;YACL,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC;YACzC,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,6GAA6G,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACzL,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO;YACL,QAAQ,EAAE,EAAE;YACZ,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,wCAAwC,GAAG,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC,MAAM,aAAa,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACvN,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,aAAgC;IAChF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { PolicyPack } from "../../schema/index.js";
2
+ import { type Runtime } from "../runtime.js";
3
+ import type { PackContribution } from "../types.js";
4
+ import { PACK_NAME } from "./branch-protection-runtime.js";
5
+ export { PACK_NAME };
6
+ export declare function resolve(pack: PolicyPack, runtime?: Runtime): {
7
+ contribution: PackContribution;
8
+ warnings: string[];
9
+ };
@@ -0,0 +1,146 @@
1
+ // Builtin Policy Pack: `branch-protection`.
2
+ //
3
+ // Blocks Write/Edit (and the codex `apply_patch` equivalent) when the
4
+ // agent is on a protected branch (default: master, main, develop). The
5
+ // gate fires at the FIRST source mutation, complementing the existing
6
+ // `preflight-before-push` gate which fires at the LAST reversible step.
7
+ //
8
+ // Mechanics, mirroring `understanding-before-execution`:
9
+ //
10
+ // 1. SessionStart producer (`harness session-start branch-check`) reads
11
+ // `.git/HEAD` for the cwd and, if the branch is NOT protected,
12
+ // writes a `branch:non-protected:<branch>` fact to the evidence
13
+ // ledger for the current session.
14
+ //
15
+ // 2. PreToolUse blocker (`harness pack hook branch-protection`)
16
+ // consults the ledger on every Write/Edit (or `apply_patch`) and
17
+ // emits a Claude Code deny envelope unless either:
18
+ // - a fresh (<5m) `branch:non-protected` tag exists, OR
19
+ // - a `branch-protection-ack:` override tag exists (any age,
20
+ // written by the operator via `mcp__agent-grounding__ledger_add`
21
+ // since Bash is gated by this same pack).
22
+ //
23
+ // The producer is also runnable on-demand from the operator's `!` shell
24
+ // — same CLI verb, no SessionStart event piped on stdin — so an agent
25
+ // that just branched can refresh the gate without restarting the
26
+ // session.
27
+ //
28
+ // Pack is OFF by default: it must be enabled per-installation via
29
+ // `harness pack add branch-protection`. The `full` init template does
30
+ // NOT wire it (revisit after one cycle of operator feedback).
31
+ import { DEFAULT_RUNTIME } from "../runtime.js";
32
+ import { ACK_TAG_PREFIX, DEFAULT_PROTECTED_BRANCHES, NON_PROTECTED_TAG_PREFIX, PACK_NAME, PRODUCER_FRESHNESS_MS, resolveProtectedBranches, } from "./branch-protection-runtime.js";
33
+ export { PACK_NAME };
34
+ const HOOK_NAME_PREFIX = `policy-pack:${PACK_NAME}`;
35
+ const PRE_TOOL_USE_MATCH_CLAUDE = "Write|Edit";
36
+ const PRE_TOOL_USE_MATCH_CODEX = "apply_patch";
37
+ const PRODUCER_COMMAND = "harness session-start branch-check";
38
+ const BLOCKER_COMMAND = "harness pack hook branch-protection";
39
+ function buildHooks(runtime) {
40
+ const isCodex = runtime === "codex";
41
+ const blockerMatch = isCodex ? PRE_TOOL_USE_MATCH_CODEX : PRE_TOOL_USE_MATCH_CLAUDE;
42
+ return [
43
+ {
44
+ name: `${HOOK_NAME_PREFIX}:session-start`,
45
+ event: "SessionStart",
46
+ command: PRODUCER_COMMAND,
47
+ blocking: false,
48
+ budget_ms: 5000,
49
+ description: "Producer: write `branch:non-protected:<branch>` to the evidence ledger when the session opens on a non-protected branch. Non-blocking; failures leave the gate closed.",
50
+ },
51
+ {
52
+ name: `${HOOK_NAME_PREFIX}:pre-tool-use`,
53
+ event: "PreToolUse",
54
+ match: blockerMatch,
55
+ command: BLOCKER_COMMAND,
56
+ blocking: "hard",
57
+ budget_ms: 5000,
58
+ description: `Blocker: deny ${blockerMatch} on protected branches unless a fresh branch:non-protected tag or a branch-protection-ack override exists in the ledger.`,
59
+ },
60
+ ];
61
+ }
62
+ function buildInstructions(pack, branches, runtime) {
63
+ const description = pack.description?.trim() ?? "";
64
+ const isCodex = runtime === "codex";
65
+ const blockerMatch = isCodex ? PRE_TOOL_USE_MATCH_CODEX : PRE_TOOL_USE_MATCH_CLAUDE;
66
+ const settingsArtefact = isCodex
67
+ ? "`harness.generated/codex/config.toml`"
68
+ : "harness-managed `settings.json`";
69
+ const minutes = Math.round(PRODUCER_FRESHNESS_MS / 60000);
70
+ return `# Policy Pack: ${PACK_NAME}
71
+
72
+ > Operator audit copy. This pack blocks source-mutating tool calls when
73
+ > the agent is on a protected branch, closing the loop on the
74
+ > "edit-on-master" incident pattern.
75
+
76
+ ## Runtime
77
+
78
+ ${runtime}
79
+
80
+ ## Protected branches
81
+
82
+ ${branches.map((b) => `- \`${b}\``).join("\n")}
83
+
84
+ Set \`config.protected_branches\` in your manifest to override.
85
+
86
+ ## Effect
87
+
88
+ While this pack is enabled, hooks are wired into the ${settingsArtefact}:
89
+
90
+ 1. \`SessionStart\` producer (\`${PRODUCER_COMMAND}\`, blocking: false):
91
+ reads the cwd's \`.git/HEAD\`. If the branch is NOT in the protected
92
+ list, writes \`${NON_PROTECTED_TAG_PREFIX}:<branch>\` to the evidence
93
+ ledger for the current session.
94
+
95
+ 2. \`PreToolUse\` blocker (\`${BLOCKER_COMMAND}\`, blocking: hard) on
96
+ \`${blockerMatch}\`: refuses the tool call unless EITHER
97
+ - a \`${NON_PROTECTED_TAG_PREFIX}\` tag exists in the ledger from
98
+ within the last ${minutes} minutes, OR
99
+ - a \`${ACK_TAG_PREFIX}:<reason>\` override tag exists (any age).
100
+
101
+ ## Escape hatches
102
+
103
+ - **Refresh after branching**: the producer is runnable on demand from
104
+ the operator's \`!\` shell as \`${PRODUCER_COMMAND}\`. The agent's Bash
105
+ is gated by the Understanding Gate but the producer command is itself
106
+ a \`harness ...\` invocation that the gate's allowlist accepts.
107
+
108
+ - **Explicit override** (any age, lasts the session): write the ack tag
109
+ via \`mcp__agent-grounding__ledger_add\` with
110
+ \`content: "${ACK_TAG_PREFIX}:<reason>"\`. Use this when you have a
111
+ deliberate reason to edit a protected branch — version bumps, CI
112
+ workflow patches, etc. The override survives session restarts only as
113
+ long as the ledger row does.
114
+
115
+ ## Out of scope (v1)
116
+
117
+ - Locking down \`git\` itself (would create false-positive churn on
118
+ read-only commands like \`git status\`).
119
+ - Auto-branching on Write attempt (silent autocorrect is wrong; the
120
+ agent should be the one who notices and branches).
121
+ - Path-allowlist for safe-on-master files (CHANGELOG.md, version
122
+ bumps). Open for v2 if operators report friction.
123
+
124
+ ## Pack metadata
125
+ ${description ? `\n> ${description.replace(/\n/g, "\n> ")}\n` : ""}
126
+ - Source: \`builtin\`
127
+ - Pack: \`${PACK_NAME}\`
128
+ - Runtime: \`${runtime}\`
129
+ - Defaults: ${DEFAULT_PROTECTED_BRANCHES.join(", ")}
130
+ `;
131
+ }
132
+ export function resolve(pack, runtime = DEFAULT_RUNTIME) {
133
+ const { branches, warning } = resolveProtectedBranches(pack);
134
+ const hooks = buildHooks(runtime);
135
+ const files = [
136
+ {
137
+ relativePath: `policy-packs/${PACK_NAME}/instructions.md`,
138
+ content: buildInstructions(pack, branches, runtime),
139
+ },
140
+ ];
141
+ const warnings = [];
142
+ if (warning)
143
+ warnings.push(warning);
144
+ return { contribution: { hooks, files }, warnings };
145
+ }
146
+ //# sourceMappingURL=branch-protection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-protection.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/branch-protection.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ import type { PolicyPack } from "../schema/index.js";
2
2
  import { type ResolvePackOptions } from "./builtin/understanding-before-execution.js";
3
3
  import { type Runtime } from "./runtime.js";
4
4
  import type { PackContribution } from "./types.js";
5
- export declare const KNOWN_BUILTIN_PACKS: readonly ["understanding-before-execution"];
5
+ export declare const KNOWN_BUILTIN_PACKS: readonly ["understanding-before-execution", "branch-protection"];
6
6
  export type BuiltinPackName = (typeof KNOWN_BUILTIN_PACKS)[number];
7
7
  export declare function isBuiltinPackName(name: string): name is BuiltinPackName;
8
8
  export interface ResolveBuiltinResult {
@@ -1,11 +1,16 @@
1
1
  // Registry of builtin policy-pack names.
2
2
  //
3
- // Phase 6 #2 ships exactly one builtin: `understanding-before-execution`.
4
- // Future builtins are added here. Non-builtin sources (path/npm/git) are
3
+ // Phase 6 #2 shipped `understanding-before-execution`; subsequent
4
+ // builtins are added by appending to `KNOWN_BUILTIN_PACKS` and a case
5
+ // arm in `resolveBuiltin()`. Non-builtin sources (path/npm/git) are
5
6
  // out of scope for v1; their resolution lands in a later sub-task.
7
+ import { PACK_NAME as BRANCH_PROTECTION, resolve as resolveBranchProtection, } from "./builtin/branch-protection.js";
6
8
  import { PACK_NAME as UNDERSTANDING_BEFORE_EXECUTION, resolve as resolveUnderstandingBeforeExecution, } from "./builtin/understanding-before-execution.js";
7
9
  import { DEFAULT_RUNTIME } from "./runtime.js";
8
- export const KNOWN_BUILTIN_PACKS = [UNDERSTANDING_BEFORE_EXECUTION];
10
+ export const KNOWN_BUILTIN_PACKS = [
11
+ UNDERSTANDING_BEFORE_EXECUTION,
12
+ BRANCH_PROTECTION,
13
+ ];
9
14
  export function isBuiltinPackName(name) {
10
15
  return KNOWN_BUILTIN_PACKS.includes(name);
11
16
  }
@@ -15,6 +20,8 @@ export function resolveBuiltin(pack, runtime = DEFAULT_RUNTIME, opts = {}) {
15
20
  switch (pack.name) {
16
21
  case UNDERSTANDING_BEFORE_EXECUTION:
17
22
  return resolveUnderstandingBeforeExecution(pack, runtime, opts);
23
+ case BRANCH_PROTECTION:
24
+ return resolveBranchProtection(pack, runtime);
18
25
  }
19
26
  }
20
27
  //# sourceMappingURL=registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/policy-packs/registry.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,mEAAmE;AAGnE,OAAO,EACL,SAAS,IAAI,8BAA8B,EAC3C,OAAO,IAAI,mCAAmC,GAE/C,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,eAAe,EAAgB,MAAM,cAAc,CAAC;AAG7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,8BAA8B,CAAU,CAAC;AAG7E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAQ,mBAAyC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAOD,MAAM,UAAU,cAAc,CAC5B,IAAgB,EAChB,UAAmB,eAAe,EAClC,OAA2B,EAAE;IAE7B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,QAAQ,IAAI,CAAC,IAAuB,EAAE,CAAC;QACrC,KAAK,8BAA8B;YACjC,OAAO,mCAAmC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/policy-packs/registry.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kEAAkE;AAClE,sEAAsE;AACtE,oEAAoE;AACpE,mEAAmE;AAGnE,OAAO,EACL,SAAS,IAAI,iBAAiB,EAC9B,OAAO,IAAI,uBAAuB,GACnC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,SAAS,IAAI,8BAA8B,EAC3C,OAAO,IAAI,mCAAmC,GAE/C,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,eAAe,EAAgB,MAAM,cAAc,CAAC;AAG7D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,8BAA8B;IAC9B,iBAAiB;CACT,CAAC;AAGX,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAQ,mBAAyC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAOD,MAAM,UAAU,cAAc,CAC5B,IAAgB,EAChB,UAAmB,eAAe,EAClC,OAA2B,EAAE;IAE7B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,QAAQ,IAAI,CAAC,IAAuB,EAAE,CAAC;QACrC,KAAK,8BAA8B;YACjC,OAAO,mCAAmC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAClE,KAAK,iBAAiB;YACpB,OAAO,uBAAuB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lannguyensi/harness",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Declarative control plane for agent harnesses — one YAML for grounding, tools, memory, and hooks.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/LanNguyenSi/harness",
@@ -37,6 +37,7 @@
37
37
  "test:watch": "vitest",
38
38
  "test:cov": "vitest run --coverage",
39
39
  "typecheck": "tsc --noEmit",
40
+ "check:ug-schema-drift": "node scripts/check-ug-schema-drift.mjs",
40
41
  "prepublishOnly": "npm run build"
41
42
  },
42
43
  "dependencies": {