harness-everything 0.48.0 → 0.50.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.
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.48.0",
13
+ "version": "0.50.0",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.48.0",
4
+ "version": "0.50.0",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
@@ -21,88 +21,6 @@ jobs:
21
21
  - name: Verify pilot instrumentation, sanitizer and offline replay
22
22
  run: python3 benchmarks/issue290-pilot/check.py
23
23
 
24
- doc-governance:
25
- runs-on: ubuntu-latest
26
- steps:
27
- - uses: actions/checkout@v7
28
- with:
29
- fetch-depth: 0
30
- - uses: actions/setup-node@v7
31
- with:
32
- node-version: 24
33
- - name: Run base-owned document governance guard
34
- env:
35
- HARNESS_GUARD_EVENT: ${{ github.event_name }}
36
- HARNESS_GUARD_BASE_SHA: ${{ github.event.pull_request.base.sha }}
37
- HARNESS_GUARD_CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
38
- run: |
39
- node -e 'const crypto = require("node:crypto");
40
- const fs = require("node:fs");
41
- const os = require("node:os");
42
- const path = require("node:path");
43
- const { spawnSync } = require("node:child_process");
44
- (function main() {
45
- const guardPath = "scripts/doc-governance-guard.js";
46
- const event = process.env.HARNESS_GUARD_EVENT || "unknown";
47
- const baseInput = process.env.HARNESS_GUARD_BASE_SHA || "";
48
- const candidateInput = process.env.HARNESS_GUARD_CANDIDATE_SHA || "";
49
- function warn(state, message) {
50
- console.log("::warning title=" + state + "::" + message);
51
- if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, "## Document governance guard\n\n**" + state + "**: " + message + "\n\n", "utf8");
52
- }
53
- function git(args) {
54
- const result = spawnSync("git", args, { encoding: "buffer", maxBuffer: 16 * 1024 * 1024, env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_NO_REPLACE_OBJECTS: "1", GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0" } });
55
- if (result.error) throw result.error;
56
- if (result.status !== 0) throw new Error("git " + args[0] + " failed: " + Buffer.from(result.stderr || result.stdout || "").toString("utf8").trim());
57
- return Buffer.from(result.stdout || "");
58
- }
59
- function resolveCommit(input, label) {
60
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(input)) throw new Error(label + " is not a full commit SHA");
61
- const resolved = git(["rev-parse", "--verify", "--quiet", "--end-of-options", input + "^{commit}"]).toString("ascii").trim().toLowerCase();
62
- if (resolved !== input.toLowerCase()) throw new Error(label + " did not resolve to the supplied commit SHA");
63
- return resolved;
64
- }
65
- if (event !== "pull_request") {
66
- warn("UNCHECKED", "No externally selected PR base/head is available for event " + event + "; the base-owned guard was not executed.");
67
- return;
68
- }
69
- try {
70
- const base = resolveCommit(baseInput, "PR base SHA");
71
- const candidate = resolveCommit(candidateInput, "PR head SHA");
72
- const checkout = process.env.GITHUB_SHA || "unavailable";
73
- console.log("PR head SHA used by the guard: " + candidate);
74
- console.log("GITHUB_SHA used by checkout and the ordinary suite: " + checkout + " (the pull_request checkout may be a synthetic merge commit).");
75
- const entries = git(["ls-tree", "-z", "--full-tree", base, "--", guardPath]);
76
- if (entries.length === 0) {
77
- warn("BOOTSTRAP_UNCHECKED", "Trusted base " + base + " has no " + guardPath + "; candidate guard was not executed. Independent review of the frozen candidate guard is required.");
78
- return;
79
- }
80
- if (entries[entries.length - 1] !== 0) throw new Error("base guard tree entry is not NUL terminated");
81
- const entry = entries.subarray(0, entries.length - 1);
82
- const tab = entry.indexOf(9);
83
- if (tab < 0 || entry.subarray(tab + 1).toString("utf8") !== guardPath) throw new Error("base guard tree entry is malformed");
84
- const [mode, type, oid] = entry.subarray(0, tab).toString("ascii").split(" ");
85
- if (!(["100644", "100755"].includes(mode)) || type !== "blob" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid || "")) throw new Error("base guard is not a regular Git blob");
86
- const guardBytes = git(["cat-file", "blob", oid]);
87
- const digest = crypto.createHash("sha256").update(guardBytes).digest("hex");
88
- console.log("Base guard blob: " + oid + "; SHA-256: " + digest);
89
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "harness-doc-governance-"));
90
- const tempGuard = path.join(tempDir, "doc-governance-guard.js");
91
- try {
92
- fs.writeFileSync(tempGuard, guardBytes, { mode: 0o600 });
93
- const result = spawnSync(process.execPath, [tempGuard, "--repo", process.cwd(), "--base", base, "--candidate", candidate], { encoding: "buffer", maxBuffer: 64 * 1024 * 1024, env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_NO_REPLACE_OBJECTS: "1", GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0" } });
94
- if (result.error) throw result.error;
95
- if (result.stdout) process.stdout.write(result.stdout);
96
- if (result.stderr) process.stderr.write(result.stderr);
97
- process.exitCode = result.status === null ? 1 : result.status;
98
- } finally {
99
- fs.rmSync(tempDir, { recursive: true, force: true });
100
- }
101
- } catch (error) {
102
- console.error("Document governance guard setup failed: " + (error.message || String(error)));
103
- process.exitCode = 1;
104
- }
105
- })();'
106
24
  self-regression:
107
25
  strategy:
108
26
  fail-fast: false
@@ -0,0 +1,91 @@
1
+ name: Document Governance
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call:
8
+
9
+ jobs:
10
+ doc-governance:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ with:
15
+ fetch-depth: 0
16
+ - uses: actions/setup-node@v7
17
+ with:
18
+ node-version: 24
19
+ - name: Run base-owned document governance guard
20
+ env:
21
+ HARNESS_GUARD_EVENT: ${{ github.event_name }}
22
+ HARNESS_GUARD_BASE_SHA: ${{ github.event.pull_request.base.sha }}
23
+ HARNESS_GUARD_CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
24
+ run: |
25
+ node -e 'const crypto = require("node:crypto");
26
+ const fs = require("node:fs");
27
+ const os = require("node:os");
28
+ const path = require("node:path");
29
+ const { spawnSync } = require("node:child_process");
30
+ (function main() {
31
+ const guardPath = "scripts/doc-governance-guard.js";
32
+ const event = process.env.HARNESS_GUARD_EVENT || "unknown";
33
+ const baseInput = process.env.HARNESS_GUARD_BASE_SHA || "";
34
+ const candidateInput = process.env.HARNESS_GUARD_CANDIDATE_SHA || "";
35
+ function warn(state, message) {
36
+ console.log("::warning title=" + state + "::" + message);
37
+ if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, "## Document governance guard\n\n**" + state + "**: " + message + "\n\n", "utf8");
38
+ }
39
+ function git(args) {
40
+ const result = spawnSync("git", args, { encoding: "buffer", maxBuffer: 16 * 1024 * 1024, env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_NO_REPLACE_OBJECTS: "1", GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0" } });
41
+ if (result.error) throw result.error;
42
+ if (result.status !== 0) throw new Error("git " + args[0] + " failed: " + Buffer.from(result.stderr || result.stdout || "").toString("utf8").trim());
43
+ return Buffer.from(result.stdout || "");
44
+ }
45
+ function resolveCommit(input, label) {
46
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(input)) throw new Error(label + " is not a full commit SHA");
47
+ const resolved = git(["rev-parse", "--verify", "--quiet", "--end-of-options", input + "^{commit}"]).toString("ascii").trim().toLowerCase();
48
+ if (resolved !== input.toLowerCase()) throw new Error(label + " did not resolve to the supplied commit SHA");
49
+ return resolved;
50
+ }
51
+ if (event !== "pull_request") {
52
+ warn("UNCHECKED", "No externally selected PR base/head is available for event " + event + "; the base-owned guard was not executed.");
53
+ return;
54
+ }
55
+ try {
56
+ const base = resolveCommit(baseInput, "PR base SHA");
57
+ const candidate = resolveCommit(candidateInput, "PR head SHA");
58
+ const checkout = process.env.GITHUB_SHA || "unavailable";
59
+ console.log("PR head SHA used by the guard: " + candidate);
60
+ console.log("GITHUB_SHA used by checkout and the ordinary suite: " + checkout + " (the pull_request checkout may be a synthetic merge commit).");
61
+ const entries = git(["ls-tree", "-z", "--full-tree", base, "--", guardPath]);
62
+ if (entries.length === 0) {
63
+ warn("BOOTSTRAP_UNCHECKED", "Trusted base " + base + " has no " + guardPath + "; candidate guard was not executed. Independent review of the frozen candidate guard is required.");
64
+ return;
65
+ }
66
+ if (entries[entries.length - 1] !== 0) throw new Error("base guard tree entry is not NUL terminated");
67
+ const entry = entries.subarray(0, entries.length - 1);
68
+ const tab = entry.indexOf(9);
69
+ if (tab < 0 || entry.subarray(tab + 1).toString("utf8") !== guardPath) throw new Error("base guard tree entry is malformed");
70
+ const [mode, type, oid] = entry.subarray(0, tab).toString("ascii").split(" ");
71
+ if (!(["100644", "100755"].includes(mode)) || type !== "blob" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid || "")) throw new Error("base guard is not a regular Git blob");
72
+ const guardBytes = git(["cat-file", "blob", oid]);
73
+ const digest = crypto.createHash("sha256").update(guardBytes).digest("hex");
74
+ console.log("Base guard blob: " + oid + "; SHA-256: " + digest);
75
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "harness-doc-governance-"));
76
+ const tempGuard = path.join(tempDir, "doc-governance-guard.js");
77
+ try {
78
+ fs.writeFileSync(tempGuard, guardBytes, { mode: 0o600 });
79
+ const result = spawnSync(process.execPath, [tempGuard, "--repo", process.cwd(), "--base", base, "--candidate", candidate], { encoding: "buffer", maxBuffer: 64 * 1024 * 1024, env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_NO_REPLACE_OBJECTS: "1", GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0" } });
80
+ if (result.error) throw result.error;
81
+ if (result.stdout) process.stdout.write(result.stdout);
82
+ if (result.stderr) process.stderr.write(result.stderr);
83
+ process.exitCode = result.status === null ? 1 : result.status;
84
+ } finally {
85
+ fs.rmSync(tempDir, { recursive: true, force: true });
86
+ }
87
+ } catch (error) {
88
+ console.error("Document governance guard setup failed: " + (error.message || String(error)));
89
+ process.exitCode = 1;
90
+ }
91
+ })();'
@@ -19,12 +19,15 @@ permissions:
19
19
  id-token: write
20
20
 
21
21
  jobs:
22
- # Release publication is gated by exactly the same test matrix as a PR.
22
+ # Release publication waits for the full CI matrix and document governance.
23
23
  gate:
24
24
  uses: ./.github/workflows/ci.yml
25
25
 
26
+ doc-governance:
27
+ uses: ./.github/workflows/doc-governance.yml
28
+
26
29
  release:
27
- needs: gate
30
+ needs: [gate, doc-governance]
28
31
  runs-on: ubuntu-latest
29
32
  steps:
30
33
  - name: Checkout full release history
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.50.0](https://github.com/dyphn1/Harness-everything/compare/v0.49.0...v0.50.0) (2026-11-09)
6
+
7
+ ### Features
8
+
9
+ * **governance:** split the guard workflow and protect only authoritative doc membership ([5a46e67](https://github.com/dyphn1/Harness-everything/commit/5a46e673525a28f9df770b8eefe2dce4bf7ae9e8))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **governance:** keep the extracted guard job in the release gate ([24c3b10](https://github.com/dyphn1/Harness-everything/commit/24c3b10a6530dd33fff22788a2219bd36ceb1447))
14
+
15
+ ## [0.49.0](https://github.com/dyphn1/Harness-everything/compare/v0.48.0...v0.49.0) (2026-11-08)
16
+
17
+ ### Features
18
+
19
+ * **doc-reconciliation:** attribute outline writers by phase under the approved e1 contract ([fe1a3e0](https://github.com/dyphn1/Harness-everything/commit/fe1a3e0bc2f73ab7789615d116ea27696d55860f)), closes [#341](https://github.com/dyphn1/Harness-everything/issues/341) [#341](https://github.com/dyphn1/Harness-everything/issues/341)
20
+ * **doc-reconciliation:** honor an owner-approved dependency-artifact inventory scope ([9c08c8a](https://github.com/dyphn1/Harness-everything/commit/9c08c8aa16cf876522afd554320e9b39e7db3bcb))
21
+
22
+ ### Bug Fixes
23
+
24
+ * **doc-reconciliation:** bind b-to-o git add to pinned targets and allow only simple commits ([20128c3](https://github.com/dyphn1/Harness-everything/commit/20128c353e545ca3070b70048678e6a4b02df36f))
25
+ * **doc-reconciliation:** bind the outline review to the git identity recorded at reviewer start ([85ecc8a](https://github.com/dyphn1/Harness-everything/commit/85ecc8abf272f921c3513d666c34cc75b602a43c)), closes [#341](https://github.com/dyphn1/Harness-everything/issues/341) [#341](https://github.com/dyphn1/Harness-everything/issues/341)
26
+ * **doc-reconciliation:** credit outline writers only for exact pinned-target spellings ([22dc944](https://github.com/dyphn1/Harness-everything/commit/22dc944ace6efc098ecf8b865ad0b82168b2f3fa))
27
+ * **doc-reconciliation:** fail closed on unsupported selector globs and self-contain husky fixture ([3732b13](https://github.com/dyphn1/Harness-everything/commit/3732b1346c97b7229426a587338c800f5a5e5968)), closes [#325](https://github.com/dyphn1/Harness-everything/issues/325) [#341](https://github.com/dyphn1/Harness-everything/issues/341) [#396](https://github.com/dyphn1/Harness-everything/issues/396)
28
+ * **doc-reconciliation:** keep task-pin identity out of the dependency-root overlap guard ([4962c5e](https://github.com/dyphn1/Harness-everything/commit/4962c5ed13942af00d9792e760ea88bbbb06370e))
29
+ * **doc-reconciliation:** require a unique outline reviewer lifecycle and per-commit outline scope ([06ee12d](https://github.com/dyphn1/Harness-everything/commit/06ee12dd0e22e077cdaed39de45681706339b09a)), closes [#341](https://github.com/dyphn1/Harness-everything/issues/341)
30
+ * **doc-reconciliation:** require regular in-root outline targets and regular-file o tree entries ([782e19f](https://github.com/dyphn1/Harness-everything/commit/782e19f7ee448090cfde6d328210a01be02ddb40))
31
+ * **doc-reconciliation:** resolve absolute selectors against the workspace root ([0a1c8b1](https://github.com/dyphn1/Harness-everything/commit/0a1c8b130c6c9c6d66383c69218c20133dd30f10))
32
+ * **doc-reconciliation:** treat brace, bracket and dot-dot selectors as overlapping ([59037db](https://github.com/dyphn1/Harness-everything/commit/59037db846ca3b3a93f8bbf8c373b03b275015dd)), closes [#341](https://github.com/dyphn1/Harness-everything/issues/341) [#396](https://github.com/dyphn1/Harness-everything/issues/396)
33
+
5
34
  ## [0.48.0](https://github.com/dyphn1/Harness-everything/compare/v0.47.5...v0.48.0) (2026-11-07)
6
35
 
7
36
  ### Features
@@ -13,6 +42,7 @@ All notable changes to this project are documented in this file.
13
42
  ### Bug Fixes
14
43
 
15
44
  * **doc-reconciliation:** keep outline approvals durable and detect dependency-root selector overlaps conservatively, including unsupported glob syntax and parent traversal
45
+ * **doc-reconciliation:** replace the blanket outline writer rule with a phase-aware policy: a coordinator write or git add/commit of the pinned target before review earns credit, and every other captured writer stays ambiguous (#341)
16
46
 
17
47
  ## [0.47.5](https://github.com/dyphn1/Harness-everything/compare/v0.47.4...v0.47.5) (2026-11-03)
18
48
 
@@ -1625,15 +1625,25 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1625
1625
  'plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js',
1626
1626
  '.agents/harness-everything/project-docs.json',
1627
1627
  '.agents/harness-everything/document-inventory.json',
1628
- '.github/workflows/ci.yml',
1628
+ '.agents/harness-everything/errors/README.md',
1629
+ '.github/workflows/doc-governance.yml',
1629
1630
  'package.json',
1630
1631
  'self-evolve/scripts/self-regression.js',
1631
1632
  'ci/mechanism-test.js',
1632
1633
  'ci/mutation-check.js',
1633
1634
  'ci/lib/isolated-test-process.js',
1634
1635
  'ci/test-helper.js',
1636
+ 'scripts/errors-sync.js',
1635
1637
  'scripts/verify-all.js'
1636
1638
  ];
1639
+ const membershipPaths = ['AGENTS.md', 'SECURITY.md', 'CODE_OF_CONDUCT.md', 'PRIVACY.md', 'TERMS.md', 'docs/document-lifecycle.md'];
1640
+ const membershipPrefixPaths = ['docs/policy/existing.md', 'docs/prd/future.md'];
1641
+ const fixtureSupportPaths = [
1642
+ '.github/workflows/ci.yml',
1643
+ 'benchmarks/issue290-pilot/README.md',
1644
+ '.agents/harness-everything/errors/E-0001-fixture.md',
1645
+ ...membershipPrefixPaths
1646
+ ];
1637
1647
  const frozenGuard = fs.readFileSync(governanceGuardPath);
1638
1648
  const frozenTest = fs.readFileSync(__filename);
1639
1649
  const frozenGuardPath = path.join(tempRoot, 'frozen-doc-governance-guard.js');
@@ -1680,6 +1690,13 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1680
1690
  : fs.readFileSync(path.join(REPOSITORY_ROOT, ...relativePath.split('/')));
1681
1691
  writeRelative(root, relativePath, bytes);
1682
1692
  }
1693
+ for (const relativePath of membershipPaths) writeRelative(root, relativePath, fs.readFileSync(path.join(REPOSITORY_ROOT, relativePath)));
1694
+ for (const relativePath of fixtureSupportPaths) {
1695
+ const bytes = relativePath === '.github/workflows/ci.yml'
1696
+ ? fs.readFileSync(path.join(REPOSITORY_ROOT, relativePath))
1697
+ : Buffer.from(`baseline ${relativePath}\n`);
1698
+ writeRelative(root, relativePath, bytes);
1699
+ }
1683
1700
  const copyScenarioTree = (from, into) => {
1684
1701
  fs.mkdirSync(into, { recursive: true });
1685
1702
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
@@ -1715,6 +1732,9 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1715
1732
  }
1716
1733
 
1717
1734
  const fixture = createFixture();
1735
+ resetFixture(fixture);
1736
+ writeRelative(fixture.root, 'docs/adr/GRPH-001.md', 'baseline ADR\n');
1737
+ fixture.base = commitFixture(fixture.root, 'add ADR fixture member');
1718
1738
  const ordinaryPath = path.join(fixture.root, 'docs', 'ordinary.md');
1719
1739
  fs.writeFileSync(ordinaryPath, 'ordinary document edit\n');
1720
1740
  // Git for Windows rejects LF paths even when they are added directly to the index.
@@ -1737,6 +1757,9 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1737
1757
  assert.strictEqual(positive.report.verifier.sha256, crypto.createHash('sha256').update(frozenGuard).digest('hex'));
1738
1758
  assert.strictEqual(positive.report.verifier.matchesBaseObject, true, 'running verifier bytes match the baseline guard blob');
1739
1759
  assert.deepStrictEqual(positive.report.trustSurface.paths.slice(0, trustedPaths.length).map(item => item.path), trustedPaths, 'fixed trust surface is explicit');
1760
+ for (const relativePath of [...membershipPaths, ...membershipPrefixPaths]) {
1761
+ assert.ok(positive.report.trustSurface.paths.some(item => item.path === relativePath), `${relativePath} is an explicit membership-only path`);
1762
+ }
1740
1763
  const scenarioExpectedPath = 'ci/fixtures/doc-lifecycle/scenarios/active-to-active/expected.json';
1741
1764
  const scenarioOracle = positive.report.trustSurface.paths.find(item => item.path === scenarioExpectedPath);
1742
1765
  assert.ok(scenarioOracle, 'tracked scenario oracle is part of the baseline trust surface');
@@ -1784,6 +1807,90 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1784
1807
  assert.strictEqual(unchanged.status, 0, 'an unchanged tree passes');
1785
1808
  assert.strictEqual(unchanged.report.verdict, 'PASS');
1786
1809
 
1810
+ resetFixture(fixture);
1811
+ const ciWorkflowPath = path.join(fixture.root, '.github/workflows/ci.yml');
1812
+ const ciWorkflow = fs.readFileSync(ciWorkflowPath, 'utf8');
1813
+ assert.ok(ciWorkflow.includes(' pilot-offline-evidence:\n runs-on: ubuntu-latest'), 'unrelated CI job fixture exists');
1814
+ writeRelative(fixture.root, '.github/workflows/ci.yml', ciWorkflow.replace(' pilot-offline-evidence:\n runs-on: ubuntu-latest', ' pilot-offline-evidence:\n name: Updated unrelated CI job\n runs-on: ubuntu-latest'));
1815
+ const unrelatedCiCandidate = commitFixture(fixture.root, 'change unrelated CI job');
1816
+ const unrelatedCi = runFrozenGuard(fixture, fixture.base, unrelatedCiCandidate);
1817
+ assert.strictEqual(unrelatedCi.status, 0, 'unrelated ci.yml job changes pass');
1818
+ assert.strictEqual(unrelatedCi.report.verdict, 'PASS');
1819
+
1820
+ resetFixture(fixture);
1821
+ writeRelative(fixture.root, '.github/workflows/doc-governance.yml', `${fs.readFileSync(path.join(fixture.root, '.github/workflows/doc-governance.yml'), 'utf8')}\n# guard workflow change\n`);
1822
+ const governanceWorkflowCandidate = commitFixture(fixture.root, 'change guard workflow');
1823
+ const governanceWorkflowChange = runFrozenGuard(fixture, fixture.base, governanceWorkflowCandidate);
1824
+ assert.strictEqual(governanceWorkflowChange.status, 2, 'the standalone guard workflow remains review-required');
1825
+ assert.strictEqual(governanceWorkflowChange.report.verdict, 'REVIEW_REQUIRED');
1826
+
1827
+ resetFixture(fixture);
1828
+ for (const relativePath of ['benchmarks/issue290-pilot/README.md', '.agents/harness-everything/errors/E-0001-fixture.md']) {
1829
+ fs.appendFileSync(path.join(fixture.root, ...relativePath.split('/')), '\nrecord update\n');
1830
+ }
1831
+ const recordsCandidate = commitFixture(fixture.root, 'change records only');
1832
+ const recordsOnly = runFrozenGuard(fixture, fixture.base, recordsCandidate);
1833
+ assert.strictEqual(recordsOnly.status, 0, 'benchmark and error-ledger records are outside the governance verdict');
1834
+ assert.strictEqual(recordsOnly.report.verdict, 'PASS');
1835
+
1836
+ resetFixture(fixture);
1837
+ writeRelative(fixture.root, '.github/workflows/doc-governance.yml', `${fs.readFileSync(path.join(fixture.root, '.github/workflows/doc-governance.yml'), 'utf8')}\n# guard workflow change\n`);
1838
+ fs.appendFileSync(path.join(fixture.root, '.agents/harness-everything/errors/E-0001-fixture.md'), '\nrecord update\n');
1839
+ const mixedCandidate = commitFixture(fixture.root, 'change T1 and T3');
1840
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, mixedCandidate).report.verdict, 'REVIEW_REQUIRED', 'a T1 change controls a mixed T1 and T3 change');
1841
+
1842
+ for (const relativePath of membershipPaths) {
1843
+ resetFixture(fixture);
1844
+ fs.appendFileSync(path.join(fixture.root, relativePath), '\nordinary content revision\n');
1845
+ const candidate = commitFixture(fixture.root, `revise ${relativePath}`);
1846
+ const result = runFrozenGuard(fixture, fixture.base, candidate);
1847
+ assert.strictEqual(result.report.verdict, 'PASS', `${relativePath} content revision does not require U5`);
1848
+ }
1849
+
1850
+ for (const relativePath of membershipPrefixPaths) {
1851
+ resetFixture(fixture);
1852
+ fs.appendFileSync(path.join(fixture.root, ...relativePath.split('/')), '\nordinary content revision\n');
1853
+ const candidate = commitFixture(fixture.root, `revise ${relativePath}`);
1854
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, candidate).report.verdict, 'PASS', `${relativePath} content revision does not require U5`);
1855
+ }
1856
+
1857
+ for (const relativePath of ['docs/policy/new.md', 'docs/prd/new.md']) {
1858
+ resetFixture(fixture);
1859
+ writeRelative(fixture.root, relativePath, 'new T1 member\n');
1860
+ const candidate = commitFixture(fixture.root, `add ${relativePath}`);
1861
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, candidate).report.verdict, 'REVIEW_REQUIRED', `adding ${relativePath} changes protected membership`);
1862
+ }
1863
+
1864
+ resetFixture(fixture);
1865
+ fixtureGit(fixture.root, ['rm', '--quiet', '--', 'AGENTS.md']);
1866
+ const deletedRootAgentsCandidate = commitFixture(fixture.root, 'delete root AGENTS');
1867
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, deletedRootAgentsCandidate).report.verdict, 'REVIEW_REQUIRED', 'deleting root AGENTS changes protected membership');
1868
+
1869
+ resetFixture(fixture);
1870
+ const changedRootAgentsModeCandidate = commitFixture(fixture.root, 'change root AGENTS mode', 'AGENTS.md');
1871
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, changedRootAgentsModeCandidate).report.verdict, 'REVIEW_REQUIRED', 'changing root AGENTS mode is not a content-only revision');
1872
+
1873
+ resetFixture(fixture);
1874
+ fixtureGit(fixture.root, ['mv', '--', 'AGENTS.md', 'docs/AGENTS.md']);
1875
+ const renamedRootAgentsCandidate = commitFixture(fixture.root, 'move root AGENTS');
1876
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, renamedRootAgentsCandidate).report.verdict, 'REVIEW_REQUIRED', 'moving root AGENTS out changes protected membership');
1877
+
1878
+ resetFixture(fixture);
1879
+ writeRelative(fixture.root, 'docs/adr/GRPH-001.md', 'revised ADR contents\n');
1880
+ const adrContentCandidate = commitFixture(fixture.root, 'revise ADR contents');
1881
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, adrContentCandidate).report.verdict, 'PASS', 'ADR content revisions do not require U5');
1882
+
1883
+ resetFixture(fixture);
1884
+ fixtureGit(fixture.root, ['rm', '--quiet', '--', 'docs/adr/GRPH-001.md']);
1885
+ const deletedAdrCandidate = commitFixture(fixture.root, 'delete ADR');
1886
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, deletedAdrCandidate).report.verdict, 'REVIEW_REQUIRED', 'deleting an ADR changes protected membership');
1887
+
1888
+ resetFixture(fixture);
1889
+ fs.mkdirSync(path.join(fixture.root, 'docs/records'), { recursive: true });
1890
+ fixtureGit(fixture.root, ['mv', '--', 'docs/adr/GRPH-001.md', 'docs/records/GRPH-001.md']);
1891
+ const movedAdrCandidate = commitFixture(fixture.root, 'move ADR out of governed folder');
1892
+ assert.strictEqual(runFrozenGuard(fixture, fixture.base, movedAdrCandidate).report.verdict, 'REVIEW_REQUIRED', 'moving an ADR out of the governed folder requires review');
1893
+
1787
1894
  const rawIdentity = entry => ({ mode: entry.mode, type: entry.type, oid: entry.oid });
1788
1895
  const configuredIdentity = (commit, relativePath) => {
1789
1896
  const tree = parseTreeOracle(fixtureGit(fixture.root, ['ls-tree', '-r', '-z', '--full-tree', commit]));
@@ -1813,11 +1920,12 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1813
1920
  writeRelative(fixture.root, 'docs/governed-index.md', 'changed derived output\n');
1814
1921
  const derivedCandidate = commitFixture(fixture.root, 'change derived document');
1815
1922
  const derivedChange = runFrozenGuard(fixture, fixture.base, derivedCandidate);
1816
- assert.strictEqual(derivedChange.status, 2, 'derived content change still requires review');
1817
- assert.strictEqual(derivedChange.report.verdict, 'REVIEW_REQUIRED');
1818
- const derivedEntry = derivedChange.report.policyPreservation.configuredPaths.missingOrChanged.find(item => item.path === 'docs/governed-index.md');
1819
- assert.ok(derivedEntry, 'derived content change remains in review-required paths');
1820
- assert.strictEqual(derivedEntry.state, 'review_required');
1923
+ assert.strictEqual(derivedChange.status, 0, 'generated-view content changes are left to ordinary CI');
1924
+ assert.strictEqual(derivedChange.report.verdict, 'PASS');
1925
+ const derivedEntry = derivedChange.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/governed-index.md');
1926
+ assert.ok(derivedEntry, 'derived content change remains visible in the report');
1927
+ assert.strictEqual(derivedEntry.state, 'pass');
1928
+ assert.match(derivedEntry.reason, /regenerate-equals CI/);
1821
1929
  assert.deepStrictEqual(derivedEntry.base, configuredIdentity(fixture.base, 'docs/governed-index.md'));
1822
1930
  assert.deepStrictEqual(derivedEntry.candidate, configuredIdentity(derivedCandidate, 'docs/governed-index.md'));
1823
1931
 
@@ -1930,8 +2038,10 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1930
2038
  writeRelative(globFixture.root, 'generated/view.md', 'changed derived glob output\n');
1931
2039
  const derivedGlobCandidate = commitFixture(globFixture.root, 'change derived glob member');
1932
2040
  const derivedGlob = runFrozenGuard(globFixture, globFixture.base, derivedGlobCandidate);
1933
- assert.strictEqual(derivedGlob.status, 2, 'a derived glob member content change requires review');
1934
- assert.strictEqual(derivedGlob.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'generated/view.md').state, 'review_required');
2041
+ assert.strictEqual(derivedGlob.status, 0, 'a generated-view revision is left to ordinary CI');
2042
+ const derivedGlobEntry = derivedGlob.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'generated/view.md');
2043
+ assert.strictEqual(derivedGlobEntry.state, 'pass');
2044
+ assert.match(derivedGlobEntry.reason, /regenerate-equals CI/);
1935
2045
 
1936
2046
  if (process.platform !== 'win32') {
1937
2047
  resetFixture(globFixture);
@@ -2119,11 +2229,20 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
2119
2229
  assert.notStrictEqual(missingCommit.status, 0);
2120
2230
  assert.notStrictEqual(missingCommit.report.verdict, 'PASS');
2121
2231
 
2122
- checkCase('2ax. CI workflow keeps the base-owned guard in a failing standalone job', () => {
2123
- const workflowPath = path.join(REPOSITORY_ROOT, '.github/workflows/ci.yml');
2232
+ checkCase('2ax. guard has a standalone workflow outside the general CI workflow', () => {
2233
+ const workflowPath = path.join(REPOSITORY_ROOT, '.github/workflows/doc-governance.yml');
2234
+ assert.ok(fs.existsSync(workflowPath), 'dedicated document-governance workflow exists');
2235
+ });
2236
+
2237
+ checkCase('2ax. standalone workflow keeps the base-owned guard in a failing job', () => {
2238
+ const workflowPath = path.join(REPOSITORY_ROOT, '.github/workflows/doc-governance.yml');
2124
2239
  const workflow = require('js-yaml').load(fs.readFileSync(workflowPath, 'utf8'));
2240
+ const ciWorkflow = require('js-yaml').load(fs.readFileSync(path.join(REPOSITORY_ROOT, '.github/workflows/ci.yml'), 'utf8'));
2125
2241
  const job = workflow.jobs['doc-governance'];
2126
2242
  assert.ok(job, 'standalone doc-governance job exists');
2243
+ assert.ok(!ciWorkflow.jobs['doc-governance'], 'general CI workflow no longer carries the governance runner');
2244
+ assert.deepStrictEqual(workflow.on, ciWorkflow.on, 'standalone guard keeps the original CI workflow triggers');
2245
+ assert.ok(!/"paths(?:-ignore)?"/.test(JSON.stringify(workflow.on)), 'the guard workflow is not path-filtered');
2127
2246
  assert.strictEqual(job['runs-on'], 'ubuntu-latest');
2128
2247
  assert.notStrictEqual(job['continue-on-error'], true, 'the job does not mask guard failures');
2129
2248
  assert.ok(job.steps.some(step => step.uses === 'actions/checkout@v7' && step.with['fetch-depth'] === 0), 'job fetches base Git objects');
@@ -2131,7 +2250,6 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
2131
2250
  const guardStep = job.steps.find(step => step.name === 'Run base-owned document governance guard');
2132
2251
  assert.ok(guardStep, 'standalone job owns the guard step');
2133
2252
  assert.notStrictEqual(guardStep['continue-on-error'], true, 'the guard step propagates nonzero exits');
2134
- assert.ok(!workflow.jobs['self-regression'].steps.some(step => step.name === guardStep.name), 'self-regression no longer runs the guard');
2135
2253
  const command = String(guardStep.run || '').trimEnd();
2136
2254
  const inline = command.match(/^node -e '([\s\S]*)'$/);
2137
2255
  assert.ok(inline, 'guard step remains a directly executable Node script');
@@ -2169,7 +2287,7 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
2169
2287
  assert.match(passEvent.output, /"verdict": "PASS"/);
2170
2288
 
2171
2289
  resetFixture(fixture);
2172
- writeRelative(fixture.root, 'docs/governed-index.md', 'changed derived output for CI simulation\n');
2290
+ writeRelative(fixture.root, '.github/workflows/doc-governance.yml', `${fs.readFileSync(path.join(fixture.root, '.github/workflows/doc-governance.yml'), 'utf8')}\n# guard workflow change\n`);
2173
2291
  const reviewCandidate = commitFixture(fixture.root, 'simulate guard review failure');
2174
2292
  const reviewEvent = simulate('pull_request', fixture.base, reviewCandidate);
2175
2293
  assert.strictEqual(reviewEvent.status, 2, 'REVIEW_REQUIRED stays a failing workflow job');
@@ -2183,6 +2301,18 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
2183
2301
  assert.strictEqual(result.status, exitCode, `the standalone job propagates guard exit ${exitCode}`);
2184
2302
  }
2185
2303
  });
2304
+
2305
+ checkCase('2ax. release gate calls and waits for the standalone document-governance workflow', () => {
2306
+ const releaseWorkflow = require('js-yaml').load(fs.readFileSync(path.join(REPOSITORY_ROOT, '.github/workflows/release.yml'), 'utf8'));
2307
+ const ciWorkflow = require('js-yaml').load(fs.readFileSync(path.join(REPOSITORY_ROOT, '.github/workflows/ci.yml'), 'utf8'));
2308
+ const governanceWorkflow = require('js-yaml').load(fs.readFileSync(path.join(REPOSITORY_ROOT, '.github/workflows/doc-governance.yml'), 'utf8'));
2309
+
2310
+ assert.strictEqual(releaseWorkflow.jobs.gate.uses, './.github/workflows/ci.yml', 'release keeps the CI reusable-workflow gate');
2311
+ assert.strictEqual(releaseWorkflow.jobs['doc-governance']?.uses, './.github/workflows/doc-governance.yml', 'release calls the extracted governance workflow');
2312
+ assert.deepStrictEqual(releaseWorkflow.jobs.release.needs, ['gate', 'doc-governance'], 'release waits for CI and document governance');
2313
+ assert.ok(!ciWorkflow.jobs['doc-governance'], 'CI does not duplicate the extracted governance runner');
2314
+ assert.ok(governanceWorkflow.jobs['doc-governance'], 'the called standalone workflow owns the governance job');
2315
+ });
2186
2316
  });
2187
2317
 
2188
2318
  helper.finish();