@pieai/pro-gov 0.3.9 → 0.3.10

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/README.md CHANGED
@@ -149,8 +149,11 @@ Compound Gate: skipped -> <reason>
149
149
  ```
150
150
 
151
151
  Use `pro-gov doctor --strict-hooks` after syncing an engineering project to
152
- verify that the host configs are wired. Open a fresh AI session after installing
153
- or changing hooks; old sessions may not reload host configuration.
152
+ verify that the host configs are wired. This is a wiring check: it proves the
153
+ project calls `pro-gov host-hook` for Codex, Claude Code, and Antigravity. The
154
+ host behavior contract is covered by PGS package tests against recorded
155
+ per-host Stop/SubagentStop fixtures. Open a fresh AI session after installing or
156
+ changing hooks; old sessions may not reload host configuration.
154
157
 
155
158
  Ponytail can be installed as an optional complexity adviser. Keep its global mode
156
159
  `off`; test `lite` in one isolated task before considering a stronger mode.
@@ -6,7 +6,7 @@ status: stable
6
6
  canonical: true
7
7
  owner: human
8
8
  created: 2026-05-06
9
- last_reviewed: 2026-06-15
9
+ last_reviewed: 2026-07-02
10
10
  domain: adoption
11
11
  tags:
12
12
  - adoption
@@ -92,8 +92,17 @@ Current package-based method:
92
92
  - copy `starter/lefthook.template.yml` to `lefthook.yml` and
93
93
  `starter/.github/workflows/docs-check.yml` to `.github/workflows/docs-check.yml`
94
94
  when the target project is ready for standard guardrails
95
+ - for an `engineering-runtime` target, install the three host-hook starter files
96
+ or merge their Stop/SubagentStop entries into the target's existing host
97
+ config:
98
+ - `.codex/hooks.json`
99
+ - `.claude/settings.json`
100
+ - `.agents/hooks.json`
95
101
  - run `doc-gov doctor` after wiring guardrails to verify they are actually
96
102
  connected
103
+ - run `pro-gov doctor --strict-hooks` after wiring host hooks to prove the
104
+ target has the expected host-hook configuration for Codex, Claude Code, and
105
+ Antigravity
97
106
  - treat the npm package as the CLI source and the local `docs/governance/`
98
107
  files as the project's checked-in governance contract
99
108
 
@@ -198,6 +207,16 @@ Use Compound Engineering by default only as the post-work Compound Gate:
198
207
  `ce-compound` captures reusable lessons when they exist; otherwise the agent
199
208
  reports a skip reason. Full CE workflows require an explicit user request.
200
209
 
210
+ The host-hook gate is deliberately small. It does not make Compound Engineering
211
+ the main workflow. It only blocks a final completion report that looks like
212
+ finished engineering work but does not say whether the Compound Gate ran or was
213
+ skipped:
214
+
215
+ ```text
216
+ Compound Gate: ran ce-compound -> <path>
217
+ Compound Gate: skipped -> <reason>
218
+ ```
219
+
201
220
  ### Doc-Only
202
221
 
203
222
  Add:
@@ -257,6 +276,7 @@ For the v0.9 structural migration, use
257
276
  Minimum:
258
277
 
259
278
  ```bash
279
+ pnpm pro-gov doctor --strict-hooks
260
280
  pnpm doc-gov check
261
281
  pnpm doc-gov router-check
262
282
  pnpm doc-gov scan --check
@@ -266,6 +286,15 @@ pnpm doc-gov doctor
266
286
  git diff --check
267
287
  ```
268
288
 
289
+ Use `pro-gov doctor --strict-hooks` for engineering-runtime projects. Doc-only
290
+ projects may omit strict host hooks unless they intentionally adopt the
291
+ engineering runtime profile.
292
+
293
+ `pro-gov doctor --strict-hooks` is a wiring check, not a live host simulation.
294
+ It proves the project has hook files that call `pro-gov host-hook`; PGS package
295
+ tests prove the expected Stop/SubagentStop behavior for the supported host
296
+ schemas. After installing or changing hooks, validate from a fresh AI session.
297
+
269
298
  Engineering projects should also run their local verification ladder.
270
299
 
271
300
  ## Example: Migrating A Runtime Product
package/dist/cli.js CHANGED
@@ -1636,9 +1636,10 @@ function isEngineeringRuntimeProject(root) {
1636
1636
  }
1637
1637
 
1638
1638
  // src/commands/host-hook.ts
1639
- import { readFileSync as readFileSync9 } from "node:fs";
1639
+ import { readFileSync as readFileSync10 } from "node:fs";
1640
1640
 
1641
1641
  // src/host-hooks/host-hook-runner.ts
1642
+ import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
1642
1643
  var gateMarkerPattern = /Compound Gate:\s*(ran ce-compound|skipped)\s*->/i;
1643
1644
  var completionSignalPatterns = [
1644
1645
  /\b(done|completed|implemented|fixed|verified|validated|shipped|pushed|committed)\b/i,
@@ -1653,6 +1654,7 @@ var compoundGateInstruction = [
1653
1654
  "If there is no reusable learning, report:",
1654
1655
  "Compound Gate: skipped -> <reason>"
1655
1656
  ].join("\n");
1657
+ var maxTranscriptBytes = 2 * 1024 * 1024;
1656
1658
  function evaluateHostHook(request) {
1657
1659
  if (request.event !== "Stop" && request.event !== "SubagentStop") {
1658
1660
  return { action: "allow" };
@@ -1694,7 +1696,7 @@ function formatHostHookOutput(host, event, decision) {
1694
1696
  }
1695
1697
  if (host === "claude" || host === "claude-code") {
1696
1698
  if (event === "Stop" || event === "SubagentStop") {
1697
- return { ok: false, reason: decision.reason };
1699
+ return { decision: "block", reason: decision.reason };
1698
1700
  }
1699
1701
  if (decision.action === "block") {
1700
1702
  return {
@@ -1723,7 +1725,7 @@ function normalizeStopInput(input) {
1723
1725
  return { lastAssistantMessage: void 0, stopHookActive: false };
1724
1726
  }
1725
1727
  return {
1726
- lastAssistantMessage: findString(input, ["last_assistant_message", "lastAssistantMessage", "message", "text"]),
1728
+ lastAssistantMessage: findString(input, ["last_assistant_message", "lastAssistantMessage", "message", "text"]) ?? findLastAssistantMessageFromTranscript(findString(input, ["agent_transcript_path", "agentTranscriptPath"])) ?? findLastAssistantMessageFromTranscript(findString(input, ["transcript_path", "transcriptPath"])),
1727
1729
  stopHookActive: findBoolean(input, ["stop_hook_active", "stopHookActive"]) ?? false
1728
1730
  };
1729
1731
  }
@@ -1748,6 +1750,86 @@ function findBoolean(input, keys) {
1748
1750
  }
1749
1751
  return void 0;
1750
1752
  }
1753
+ function findLastAssistantMessageFromTranscript(path) {
1754
+ if (!path || path.startsWith("~/")) {
1755
+ return void 0;
1756
+ }
1757
+ let content;
1758
+ try {
1759
+ content = readTranscriptTail(path);
1760
+ } catch {
1761
+ return void 0;
1762
+ }
1763
+ const lines = content.split(/\r?\n/);
1764
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1765
+ const line = lines[index]?.trim();
1766
+ if (!line) {
1767
+ continue;
1768
+ }
1769
+ let record;
1770
+ try {
1771
+ record = JSON.parse(line);
1772
+ } catch {
1773
+ continue;
1774
+ }
1775
+ const message = extractAssistantText(record);
1776
+ if (message) {
1777
+ return message;
1778
+ }
1779
+ }
1780
+ return void 0;
1781
+ }
1782
+ function readTranscriptTail(path) {
1783
+ const size = statSync3(path).size;
1784
+ if (size <= maxTranscriptBytes) {
1785
+ return readFileSync9(path, "utf8");
1786
+ }
1787
+ const fd = openSync(path, "r");
1788
+ try {
1789
+ const buffer = Buffer.allocUnsafe(maxTranscriptBytes);
1790
+ readSync(fd, buffer, 0, maxTranscriptBytes, size - maxTranscriptBytes);
1791
+ return buffer.toString("utf8");
1792
+ } finally {
1793
+ closeSync(fd);
1794
+ }
1795
+ }
1796
+ function extractAssistantText(value) {
1797
+ if (!isRecord(value)) {
1798
+ return void 0;
1799
+ }
1800
+ const nested = value.message;
1801
+ const role = findString(value, ["role", "type"]);
1802
+ if (isRecord(nested)) {
1803
+ const nestedRole = findString(nested, ["role", "type"]);
1804
+ if (nestedRole === "assistant") {
1805
+ return extractTextContent(nested.content);
1806
+ }
1807
+ }
1808
+ if (role === "assistant") {
1809
+ return extractTextContent(value.content) ?? extractTextContent(value.text);
1810
+ }
1811
+ return void 0;
1812
+ }
1813
+ function extractTextContent(value) {
1814
+ if (typeof value === "string" && value.trim().length > 0) {
1815
+ return value;
1816
+ }
1817
+ if (!Array.isArray(value)) {
1818
+ return void 0;
1819
+ }
1820
+ const parts = [];
1821
+ for (const item of value) {
1822
+ if (typeof item === "string") {
1823
+ parts.push(item);
1824
+ continue;
1825
+ }
1826
+ if (isRecord(item) && typeof item.text === "string") {
1827
+ parts.push(item.text);
1828
+ }
1829
+ }
1830
+ const text = parts.join("\n").trim();
1831
+ return text.length > 0 ? text : void 0;
1832
+ }
1751
1833
  function isRecord(value) {
1752
1834
  return typeof value === "object" && value !== null && !Array.isArray(value);
1753
1835
  }
@@ -1785,7 +1867,7 @@ function readOption(args, name) {
1785
1867
  return args[index + 1];
1786
1868
  }
1787
1869
  function readStdinJson() {
1788
- const raw = readFileSync9(0, "utf8").trim();
1870
+ const raw = readFileSync10(0, "utf8").trim();
1789
1871
  if (!raw) return {};
1790
1872
  try {
1791
1873
  return JSON.parse(raw);
@@ -1795,7 +1877,7 @@ function readStdinJson() {
1795
1877
  }
1796
1878
 
1797
1879
  // src/commands/init.ts
1798
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
1880
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
1799
1881
  import { basename as basename3, dirname as dirname7, join as join13 } from "node:path";
1800
1882
 
1801
1883
  // src/commands/shared.ts
@@ -1878,7 +1960,7 @@ function applyStarterFiles(files, profile) {
1878
1960
  for (const file of files) {
1879
1961
  const targetPath = join13(root, file.targetPath);
1880
1962
  mkdirSync4(dirname7(targetPath), { recursive: true });
1881
- const source = readFileSync10(file.absoluteSourcePath);
1963
+ const source = readFileSync11(file.absoluteSourcePath);
1882
1964
  const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename3(root), profile) : source;
1883
1965
  writeFileSync3(targetPath, content);
1884
1966
  }
@@ -1912,7 +1994,7 @@ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:f
1912
1994
  import { dirname as dirname9 } from "node:path";
1913
1995
 
1914
1996
  // src/lens/audit.ts
1915
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
1997
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
1916
1998
  import { basename as basename4, dirname as dirname8, join as join14 } from "node:path";
1917
1999
  var REQUIRED_ARTIFACTS = [
1918
2000
  "manifest.md",
@@ -1960,7 +2042,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
1960
2042
  }
1961
2043
  let contract;
1962
2044
  try {
1963
- contract = JSON.parse(readFileSync11(contractPath, "utf8"));
2045
+ contract = JSON.parse(readFileSync12(contractPath, "utf8"));
1964
2046
  } catch (error) {
1965
2047
  return {
1966
2048
  ok: false,
@@ -1998,7 +2080,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
1998
2080
  issues.push({ type: "missing-required-artifact", path: artifactPath });
1999
2081
  continue;
2000
2082
  }
2001
- const content = readFileSync11(absolutePath, "utf8");
2083
+ const content = readFileSync12(absolutePath, "utf8");
2002
2084
  if (isPendingArtifact(content)) {
2003
2085
  issues.push({ type: "artifact-not-complete", path: artifactPath });
2004
2086
  } else if (hasTemplateBody(content)) {
@@ -2258,7 +2340,7 @@ function bulletList(values) {
2258
2340
 
2259
2341
  // src/lens/scan.ts
2260
2342
  import { spawnSync as spawnSync3 } from "node:child_process";
2261
- import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
2343
+ import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
2262
2344
  import { join as join15, relative as relative5 } from "node:path";
2263
2345
  var ignoredDirectories = /* @__PURE__ */ new Set([
2264
2346
  ".git",
@@ -2286,14 +2368,14 @@ function scanProjectLensTarget(targetDir, options = {}) {
2286
2368
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
2287
2369
  },
2288
2370
  git: readGitState(targetDir),
2289
- largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join15(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2371
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join15(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2290
2372
  };
2291
2373
  }
2292
2374
  function readPackageJson(targetDir) {
2293
2375
  const packageJsonPath = join15(targetDir, "package.json");
2294
2376
  if (!existsSync15(packageJsonPath)) return void 0;
2295
2377
  try {
2296
- const packageJson = JSON.parse(readFileSync12(packageJsonPath, "utf8"));
2378
+ const packageJson = JSON.parse(readFileSync13(packageJsonPath, "utf8"));
2297
2379
  return {
2298
2380
  scripts: Object.keys(packageJson.scripts ?? {}).sort(),
2299
2381
  dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
@@ -2494,12 +2576,12 @@ import { existsSync as existsSync17 } from "node:fs";
2494
2576
  import { join as join16 } from "node:path";
2495
2577
 
2496
2578
  // src/portfolio/manifest.ts
2497
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
2579
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
2498
2580
  import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
2499
2581
  function loadPortfolioManifest(configPath) {
2500
2582
  let parsed;
2501
2583
  try {
2502
- parsed = JSON.parse(readFileSync13(configPath, "utf8"));
2584
+ parsed = JSON.parse(readFileSync14(configPath, "utf8"));
2503
2585
  } catch (error) {
2504
2586
  return {
2505
2587
  configPath,
@@ -2939,7 +3021,7 @@ function printUsage3() {
2939
3021
  }
2940
3022
 
2941
3023
  // src/commands/sync.ts
2942
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
3024
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
2943
3025
  import { join as join17 } from "node:path";
2944
3026
  function runSync(args) {
2945
3027
  if (!args.includes("--check")) {
@@ -2975,8 +3057,8 @@ function runSync(args) {
2975
3057
  continue;
2976
3058
  }
2977
3059
  if (file.ownership === "project-local-seed") continue;
2978
- const source = readFileSync14(file.absoluteSourcePath, "utf8");
2979
- const target = readFileSync14(targetPath, "utf8");
3060
+ const source = readFileSync15(file.absoluteSourcePath, "utf8");
3061
+ const target = readFileSync15(targetPath, "utf8");
2980
3062
  if (source !== target) {
2981
3063
  console.log(`different: ${file.targetPath}`);
2982
3064
  differences += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/pro-gov",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Project-level distribution kit for Project Governance System.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@pieai/doc-gov": "^0.3.9"
38
+ "@pieai/doc-gov": "^0.3.10"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "24.13.2",