@pieai/pro-gov 0.3.8 → 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 +16 -0
- package/assets/docs/reference/adoption/adoption-playbook.md +30 -1
- package/assets/profiles/engineering-runtime/profile.md +6 -0
- package/assets/starter/.agents/hooks.json +26 -0
- package/assets/starter/.claude/settings.json +26 -0
- package/assets/starter/.codex/hooks.json +28 -0
- package/assets/starter/AGENTS.template.md +1 -0
- package/dist/cli.js +304 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -139,6 +139,22 @@ engineering work. The default PGS pattern is a Compound Gate: run
|
|
|
139
139
|
skipped. Full CE workflows such as `ce-plan`, `ce-work`, and `lfg` remain
|
|
140
140
|
explicit user choices.
|
|
141
141
|
|
|
142
|
+
Engineering-runtime starters include cross-host Stop hooks for Codex,
|
|
143
|
+
Claude Code, and Antigravity. Those hooks call `pro-gov host-hook` and require
|
|
144
|
+
the final report to include either:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
Compound Gate: ran ce-compound -> <path>
|
|
148
|
+
Compound Gate: skipped -> <reason>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use `pro-gov doctor --strict-hooks` after syncing an engineering project to
|
|
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.
|
|
157
|
+
|
|
142
158
|
Ponytail can be installed as an optional complexity adviser. Keep its global mode
|
|
143
159
|
`off`; test `lite` in one isolated task before considering a stronger mode.
|
|
144
160
|
Ponytail must not remove requested scope, tests, safety, accessibility, or proof.
|
|
@@ -6,7 +6,7 @@ status: stable
|
|
|
6
6
|
canonical: true
|
|
7
7
|
owner: human
|
|
8
8
|
created: 2026-05-06
|
|
9
|
-
last_reviewed: 2026-
|
|
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
|
|
@@ -39,3 +39,9 @@ project shape. `pro-gov init --apply` can install it into a fresh target, but
|
|
|
39
39
|
refuses all writes when any destination already exists. `sync --check` compares
|
|
40
40
|
shared core files while leaving project-local router, policy, and current-work
|
|
41
41
|
content under the target project's ownership.
|
|
42
|
+
|
|
43
|
+
Engineering-runtime starters also include Codex, Claude Code, and Antigravity
|
|
44
|
+
Stop hooks that call `pro-gov host-hook`. Use `pro-gov doctor --strict-hooks`
|
|
45
|
+
after sync to confirm the exit gate is wired. The hook enforces the Compound
|
|
46
|
+
Gate final-report marker; it does not replace verification, tests, or
|
|
47
|
+
`ce-compound` itself.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pgs-compound-gate": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "cd \"$(git rev-parse --show-toplevel)\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host antigravity --event Stop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host antigravity --event Stop; else npx --no-install @pieai/pro-gov host-hook --host antigravity --event Stop; fi",
|
|
9
|
+
"timeout": 30
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SubagentStop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "cd \"$(git rev-parse --show-toplevel)\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host antigravity --event SubagentStop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host antigravity --event SubagentStop; else npx --no-install @pieai/pro-gov host-hook --host antigravity --event SubagentStop; fi",
|
|
20
|
+
"timeout": 30
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "cd \"${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host claude-code --event Stop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host claude-code --event Stop; else npx --no-install @pieai/pro-gov host-hook --host claude-code --event Stop; fi",
|
|
9
|
+
"timeout": 30
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SubagentStop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "cd \"${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host claude-code --event SubagentStop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host claude-code --event SubagentStop; else npx --no-install @pieai/pro-gov host-hook --host claude-code --event SubagentStop; fi",
|
|
20
|
+
"timeout": 30
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "cd \"$(git rev-parse --show-toplevel)\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host codex --event Stop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host codex --event Stop; else npx --no-install @pieai/pro-gov host-hook --host codex --event Stop; fi",
|
|
9
|
+
"timeout": 30,
|
|
10
|
+
"statusMessage": "Checking PGS Compound Gate"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SubagentStop": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "cd \"$(git rev-parse --show-toplevel)\" && if [ -x ./node_modules/.bin/pro-gov ]; then ./node_modules/.bin/pro-gov host-hook --host codex --event SubagentStop; elif [ -f packages/pro-gov/src/cli.ts ] && command -v pnpm >/dev/null 2>&1; then pnpm --silent --filter @pieai/pro-gov dev host-hook --host codex --event SubagentStop; else npx --no-install @pieai/pro-gov host-hook --host codex --event SubagentStop; fi",
|
|
21
|
+
"timeout": 30,
|
|
22
|
+
"statusMessage": "Checking PGS subagent Compound Gate"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
- Point to the chosen agents-routing file from `docs/governance/agents-routing/`.
|
|
40
40
|
- Engineering projects may use Superpowers, Directed Development, GStack, or other external workflows only inside the selected lane; do not copy upstream integration guides into the target project by default.
|
|
41
41
|
- For engineering projects, Superpowers is the default engineering workflow. Compound Engineering is used by default only as the post-work Compound Gate through `ce-compound`; full CE workflows require an explicit user request.
|
|
42
|
+
- Engineering projects should have PGS Stop hooks wired for Codex, Claude Code, and Antigravity. Before final reporting after completed engineering work, report `Compound Gate: ran ce-compound -> <path>` or `Compound Gate: skipped -> <reason>`.
|
|
42
43
|
- Doc-only projects should say that Superpowers TDD and Directed Development are not enabled by default.
|
|
43
44
|
- External workflow systems such as Superpowers or GStack run inside the lane selected by this router. They must not replace this project router.
|
|
44
45
|
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var assetRoots = [
|
|
|
14
14
|
"docs/reference/adoption"
|
|
15
15
|
];
|
|
16
16
|
function listAssets() {
|
|
17
|
-
const root = existsSync(
|
|
17
|
+
const root = existsSync(join(sourceRoot, "starter")) ? sourceRoot : packagedAssetsRoot;
|
|
18
18
|
return assetRoots.flatMap((assetRoot) => {
|
|
19
19
|
const absoluteRoot = join(root, assetRoot);
|
|
20
20
|
if (!existsSync(absoluteRoot)) return [];
|
|
@@ -1542,7 +1542,7 @@ function printNpxUsage() {
|
|
|
1542
1542
|
|
|
1543
1543
|
// src/commands/doctor.ts
|
|
1544
1544
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1545
|
-
import { existsSync as existsSync12 } from "node:fs";
|
|
1545
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
1546
1546
|
import { createRequire } from "node:module";
|
|
1547
1547
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
1548
1548
|
var REQUIRED_ASSETS = [
|
|
@@ -1554,9 +1554,11 @@ var REQUIRED_ASSETS = [
|
|
|
1554
1554
|
"profiles/doc-only/profile.md"
|
|
1555
1555
|
];
|
|
1556
1556
|
function runDoctor(_args) {
|
|
1557
|
+
const strictHooks = _args.includes("--strict-hooks");
|
|
1557
1558
|
const assets = listAssets();
|
|
1558
1559
|
const assetPaths = new Set(assets.map((asset) => asset.path));
|
|
1559
1560
|
const missing = REQUIRED_ASSETS.filter((assetPath) => !assetPaths.has(assetPath));
|
|
1561
|
+
const hookIssues = strictHooks ? checkStrictHostHooks(process.cwd()) : [];
|
|
1560
1562
|
console.log("pro-gov doctor");
|
|
1561
1563
|
console.log(`assets: ${assets.length}`);
|
|
1562
1564
|
if (missing.length > 0) {
|
|
@@ -1567,7 +1569,13 @@ function runDoctor(_args) {
|
|
|
1567
1569
|
console.log("assets: required project-governance assets found");
|
|
1568
1570
|
}
|
|
1569
1571
|
console.log(checkDocGov());
|
|
1570
|
-
|
|
1572
|
+
for (const issue of hookIssues) {
|
|
1573
|
+
console.error(issue);
|
|
1574
|
+
}
|
|
1575
|
+
if (strictHooks && hookIssues.length === 0) {
|
|
1576
|
+
console.log("host-hooks: PGS Compound Gate hooks wired");
|
|
1577
|
+
}
|
|
1578
|
+
return missing.length > 0 || hookIssues.length > 0 ? 1 : 0;
|
|
1571
1579
|
}
|
|
1572
1580
|
function checkDocGov() {
|
|
1573
1581
|
const fromPath = spawnSync2("doc-gov", ["--help"], {
|
|
@@ -1600,9 +1608,276 @@ function resolveDocGovDependencyCli() {
|
|
|
1600
1608
|
return null;
|
|
1601
1609
|
}
|
|
1602
1610
|
}
|
|
1611
|
+
function checkStrictHostHooks(root) {
|
|
1612
|
+
if (!isEngineeringRuntimeProject(root)) {
|
|
1613
|
+
return [];
|
|
1614
|
+
}
|
|
1615
|
+
const expected = [
|
|
1616
|
+
{ path: ".codex/hooks.json", host: "codex" },
|
|
1617
|
+
{ path: ".claude/settings.json", host: "claude-code" },
|
|
1618
|
+
{ path: ".agents/hooks.json", host: "antigravity" }
|
|
1619
|
+
];
|
|
1620
|
+
const issues = [];
|
|
1621
|
+
for (const entry of expected) {
|
|
1622
|
+
const absolutePath = join12(root, entry.path);
|
|
1623
|
+
if (!existsSync12(absolutePath)) {
|
|
1624
|
+
issues.push(`host-hooks: missing ${entry.path}`);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const content = readFileSync8(absolutePath, "utf8");
|
|
1628
|
+
if (!content.includes("pro-gov host-hook") || !content.includes(`--host ${entry.host}`)) {
|
|
1629
|
+
issues.push(`host-hooks: ${entry.path} does not call pro-gov host-hook for ${entry.host}`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return issues;
|
|
1633
|
+
}
|
|
1634
|
+
function isEngineeringRuntimeProject(root) {
|
|
1635
|
+
return existsSync12(join12(root, "docs/governance/agents-routing/engineering-runtime-v0.9.md"));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/commands/host-hook.ts
|
|
1639
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
1640
|
+
|
|
1641
|
+
// src/host-hooks/host-hook-runner.ts
|
|
1642
|
+
import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
|
|
1643
|
+
var gateMarkerPattern = /Compound Gate:\s*(ran ce-compound|skipped)\s*->/i;
|
|
1644
|
+
var completionSignalPatterns = [
|
|
1645
|
+
/\b(done|completed|implemented|fixed|verified|validated|shipped|pushed|committed)\b/i,
|
|
1646
|
+
/\b(tests?|typecheck|build|lint|doctor|pack)\b.*\b(pass|passed|green|succeed|succeeded|ok)\b/i,
|
|
1647
|
+
/\b(changed|updated|modified|created|deleted|refactored)\b.*\b(files?|docs?|tests?|hooks?|configs?)\b/i,
|
|
1648
|
+
/完成|已完成|修好了|实现了|验证通过|测试通过|已经提交|已经推送|提交并推送/
|
|
1649
|
+
];
|
|
1650
|
+
var compoundGateInstruction = [
|
|
1651
|
+
"Before final reporting, pass the PGS Compound Gate.",
|
|
1652
|
+
"If this completed work produced reusable learning, run compound-engineering:ce-compound and report:",
|
|
1653
|
+
"Compound Gate: ran ce-compound -> <path>",
|
|
1654
|
+
"If there is no reusable learning, report:",
|
|
1655
|
+
"Compound Gate: skipped -> <reason>"
|
|
1656
|
+
].join("\n");
|
|
1657
|
+
var maxTranscriptBytes = 2 * 1024 * 1024;
|
|
1658
|
+
function evaluateHostHook(request) {
|
|
1659
|
+
if (request.event !== "Stop" && request.event !== "SubagentStop") {
|
|
1660
|
+
return { action: "allow" };
|
|
1661
|
+
}
|
|
1662
|
+
const input = normalizeStopInput(request.input);
|
|
1663
|
+
if (input.stopHookActive) {
|
|
1664
|
+
return { action: "allow" };
|
|
1665
|
+
}
|
|
1666
|
+
if (!input.lastAssistantMessage) {
|
|
1667
|
+
return { action: "allow" };
|
|
1668
|
+
}
|
|
1669
|
+
if (gateMarkerPattern.test(input.lastAssistantMessage)) {
|
|
1670
|
+
return { action: "allow" };
|
|
1671
|
+
}
|
|
1672
|
+
if (!looksLikeCompletedEngineeringWork(input.lastAssistantMessage)) {
|
|
1673
|
+
return { action: "allow" };
|
|
1674
|
+
}
|
|
1675
|
+
return { action: "continue", reason: compoundGateInstruction };
|
|
1676
|
+
}
|
|
1677
|
+
function formatHostHookOutput(host, event, decision) {
|
|
1678
|
+
if (decision.action === "allow") {
|
|
1679
|
+
return {};
|
|
1680
|
+
}
|
|
1681
|
+
if (host === "codex") {
|
|
1682
|
+
if (event === "Stop" || event === "SubagentStop") {
|
|
1683
|
+
return { decision: "block", reason: decision.reason };
|
|
1684
|
+
}
|
|
1685
|
+
if (decision.action === "block") {
|
|
1686
|
+
return {
|
|
1687
|
+
hookSpecificOutput: {
|
|
1688
|
+
hookEventName: event,
|
|
1689
|
+
permissionDecision: "deny",
|
|
1690
|
+
permissionDecisionReason: decision.reason
|
|
1691
|
+
},
|
|
1692
|
+
systemMessage: decision.reason
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
return { systemMessage: decision.reason };
|
|
1696
|
+
}
|
|
1697
|
+
if (host === "claude" || host === "claude-code") {
|
|
1698
|
+
if (event === "Stop" || event === "SubagentStop") {
|
|
1699
|
+
return { decision: "block", reason: decision.reason };
|
|
1700
|
+
}
|
|
1701
|
+
if (decision.action === "block") {
|
|
1702
|
+
return {
|
|
1703
|
+
hookSpecificOutput: {
|
|
1704
|
+
hookEventName: event,
|
|
1705
|
+
permissionDecision: "deny",
|
|
1706
|
+
permissionDecisionReason: decision.reason
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
return { systemMessage: decision.reason };
|
|
1711
|
+
}
|
|
1712
|
+
if (host === "antigravity") {
|
|
1713
|
+
if (event === "Stop" || event === "SubagentStop") {
|
|
1714
|
+
return { decision: "continue", reason: decision.reason };
|
|
1715
|
+
}
|
|
1716
|
+
if (decision.action === "block") {
|
|
1717
|
+
return { decision: "deny", reason: decision.reason };
|
|
1718
|
+
}
|
|
1719
|
+
return { decision: "allow", reason: decision.reason };
|
|
1720
|
+
}
|
|
1721
|
+
return {};
|
|
1722
|
+
}
|
|
1723
|
+
function normalizeStopInput(input) {
|
|
1724
|
+
if (!isRecord(input)) {
|
|
1725
|
+
return { lastAssistantMessage: void 0, stopHookActive: false };
|
|
1726
|
+
}
|
|
1727
|
+
return {
|
|
1728
|
+
lastAssistantMessage: findString(input, ["last_assistant_message", "lastAssistantMessage", "message", "text"]) ?? findLastAssistantMessageFromTranscript(findString(input, ["agent_transcript_path", "agentTranscriptPath"])) ?? findLastAssistantMessageFromTranscript(findString(input, ["transcript_path", "transcriptPath"])),
|
|
1729
|
+
stopHookActive: findBoolean(input, ["stop_hook_active", "stopHookActive"]) ?? false
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
function looksLikeCompletedEngineeringWork(message) {
|
|
1733
|
+
return completionSignalPatterns.some((pattern) => pattern.test(message));
|
|
1734
|
+
}
|
|
1735
|
+
function findString(input, keys) {
|
|
1736
|
+
for (const key of keys) {
|
|
1737
|
+
const value = input[key];
|
|
1738
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1739
|
+
return value;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return void 0;
|
|
1743
|
+
}
|
|
1744
|
+
function findBoolean(input, keys) {
|
|
1745
|
+
for (const key of keys) {
|
|
1746
|
+
const value = input[key];
|
|
1747
|
+
if (typeof value === "boolean") {
|
|
1748
|
+
return value;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return void 0;
|
|
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
|
+
}
|
|
1833
|
+
function isRecord(value) {
|
|
1834
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/host-hooks/types.ts
|
|
1838
|
+
function isHostHookHost(value) {
|
|
1839
|
+
return value === "antigravity" || value === "claude" || value === "claude-code" || value === "codex";
|
|
1840
|
+
}
|
|
1841
|
+
function isHostHookEvent(value) {
|
|
1842
|
+
return value === "PostToolUse" || value === "PreToolUse" || value === "Stop" || value === "SubagentStop" || value === "UserPromptSubmit";
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/commands/host-hook.ts
|
|
1846
|
+
function runHostHook(args) {
|
|
1847
|
+
const host = readOption(args, "--host");
|
|
1848
|
+
const event = readOption(args, "--event");
|
|
1849
|
+
if (!isHostHookHost(host)) {
|
|
1850
|
+
console.error("Expected --host <codex|claude-code|antigravity>");
|
|
1851
|
+
return 1;
|
|
1852
|
+
}
|
|
1853
|
+
if (!isHostHookEvent(event)) {
|
|
1854
|
+
console.error("Expected --event <Stop|SubagentStop|PreToolUse|PostToolUse|UserPromptSubmit>");
|
|
1855
|
+
return 1;
|
|
1856
|
+
}
|
|
1857
|
+
const input = readStdinJson();
|
|
1858
|
+
const decision = evaluateHostHook({ host, event, input });
|
|
1859
|
+
const output = formatHostHookOutput(host, event, decision);
|
|
1860
|
+
console.log(`${JSON.stringify(output)}
|
|
1861
|
+
`);
|
|
1862
|
+
return 0;
|
|
1863
|
+
}
|
|
1864
|
+
function readOption(args, name) {
|
|
1865
|
+
const index = args.indexOf(name);
|
|
1866
|
+
if (index < 0) return void 0;
|
|
1867
|
+
return args[index + 1];
|
|
1868
|
+
}
|
|
1869
|
+
function readStdinJson() {
|
|
1870
|
+
const raw = readFileSync10(0, "utf8").trim();
|
|
1871
|
+
if (!raw) return {};
|
|
1872
|
+
try {
|
|
1873
|
+
return JSON.parse(raw);
|
|
1874
|
+
} catch {
|
|
1875
|
+
return {};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1603
1878
|
|
|
1604
1879
|
// src/commands/init.ts
|
|
1605
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as
|
|
1880
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1606
1881
|
import { basename as basename3, dirname as dirname7, join as join13 } from "node:path";
|
|
1607
1882
|
|
|
1608
1883
|
// src/commands/shared.ts
|
|
@@ -1611,6 +1886,7 @@ function planStarterFiles(profile) {
|
|
|
1611
1886
|
const targetPath = starterTargetPath(asset.path);
|
|
1612
1887
|
if (!targetPath) return [];
|
|
1613
1888
|
if (profile && isOtherProfileRouting(targetPath, profile)) return [];
|
|
1889
|
+
if (profile && isEngineeringOnlyGuardrail(targetPath) && profile !== "engineering-runtime") return [];
|
|
1614
1890
|
return [
|
|
1615
1891
|
{
|
|
1616
1892
|
sourcePath: asset.path,
|
|
@@ -1633,6 +1909,9 @@ function classifyOwnership(targetPath) {
|
|
|
1633
1909
|
function isOtherProfileRouting(targetPath, profile) {
|
|
1634
1910
|
return targetPath.startsWith("docs/governance/agents-routing/") && targetPath !== `docs/governance/agents-routing/${profile}-v0.9.md`;
|
|
1635
1911
|
}
|
|
1912
|
+
function isEngineeringOnlyGuardrail(targetPath) {
|
|
1913
|
+
return targetPath === ".codex/hooks.json" || targetPath === ".claude/settings.json" || targetPath === ".agents/hooks.json";
|
|
1914
|
+
}
|
|
1636
1915
|
function starterTargetPath(sourcePath) {
|
|
1637
1916
|
if (sourcePath === "starter/AGENTS.template.md") return "AGENTS.md";
|
|
1638
1917
|
if (sourcePath === "starter/CLAUDE.template.md") return "CLAUDE.md";
|
|
@@ -1681,7 +1960,7 @@ function applyStarterFiles(files, profile) {
|
|
|
1681
1960
|
for (const file of files) {
|
|
1682
1961
|
const targetPath = join13(root, file.targetPath);
|
|
1683
1962
|
mkdirSync4(dirname7(targetPath), { recursive: true });
|
|
1684
|
-
const source =
|
|
1963
|
+
const source = readFileSync11(file.absoluteSourcePath);
|
|
1685
1964
|
const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename3(root), profile) : source;
|
|
1686
1965
|
writeFileSync3(targetPath, content);
|
|
1687
1966
|
}
|
|
@@ -1715,7 +1994,7 @@ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:f
|
|
|
1715
1994
|
import { dirname as dirname9 } from "node:path";
|
|
1716
1995
|
|
|
1717
1996
|
// src/lens/audit.ts
|
|
1718
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as
|
|
1997
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1719
1998
|
import { basename as basename4, dirname as dirname8, join as join14 } from "node:path";
|
|
1720
1999
|
var REQUIRED_ARTIFACTS = [
|
|
1721
2000
|
"manifest.md",
|
|
@@ -1763,7 +2042,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
|
|
|
1763
2042
|
}
|
|
1764
2043
|
let contract;
|
|
1765
2044
|
try {
|
|
1766
|
-
contract = JSON.parse(
|
|
2045
|
+
contract = JSON.parse(readFileSync12(contractPath, "utf8"));
|
|
1767
2046
|
} catch (error) {
|
|
1768
2047
|
return {
|
|
1769
2048
|
ok: false,
|
|
@@ -1801,7 +2080,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
|
|
|
1801
2080
|
issues.push({ type: "missing-required-artifact", path: artifactPath });
|
|
1802
2081
|
continue;
|
|
1803
2082
|
}
|
|
1804
|
-
const content =
|
|
2083
|
+
const content = readFileSync12(absolutePath, "utf8");
|
|
1805
2084
|
if (isPendingArtifact(content)) {
|
|
1806
2085
|
issues.push({ type: "artifact-not-complete", path: artifactPath });
|
|
1807
2086
|
} else if (hasTemplateBody(content)) {
|
|
@@ -2061,7 +2340,7 @@ function bulletList(values) {
|
|
|
2061
2340
|
|
|
2062
2341
|
// src/lens/scan.ts
|
|
2063
2342
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
2064
|
-
import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as
|
|
2343
|
+
import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
|
|
2065
2344
|
import { join as join15, relative as relative5 } from "node:path";
|
|
2066
2345
|
var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
2067
2346
|
".git",
|
|
@@ -2089,14 +2368,14 @@ function scanProjectLensTarget(targetDir, options = {}) {
|
|
|
2089
2368
|
governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
|
|
2090
2369
|
},
|
|
2091
2370
|
git: readGitState(targetDir),
|
|
2092
|
-
largeFiles: files.map((file) => ({ path: file, bytes:
|
|
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)
|
|
2093
2372
|
};
|
|
2094
2373
|
}
|
|
2095
2374
|
function readPackageJson(targetDir) {
|
|
2096
2375
|
const packageJsonPath = join15(targetDir, "package.json");
|
|
2097
2376
|
if (!existsSync15(packageJsonPath)) return void 0;
|
|
2098
2377
|
try {
|
|
2099
|
-
const packageJson = JSON.parse(
|
|
2378
|
+
const packageJson = JSON.parse(readFileSync13(packageJsonPath, "utf8"));
|
|
2100
2379
|
return {
|
|
2101
2380
|
scripts: Object.keys(packageJson.scripts ?? {}).sort(),
|
|
2102
2381
|
dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
|
|
@@ -2297,12 +2576,12 @@ import { existsSync as existsSync17 } from "node:fs";
|
|
|
2297
2576
|
import { join as join16 } from "node:path";
|
|
2298
2577
|
|
|
2299
2578
|
// src/portfolio/manifest.ts
|
|
2300
|
-
import { existsSync as existsSync16, readFileSync as
|
|
2579
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
|
|
2301
2580
|
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
|
|
2302
2581
|
function loadPortfolioManifest(configPath) {
|
|
2303
2582
|
let parsed;
|
|
2304
2583
|
try {
|
|
2305
|
-
parsed = JSON.parse(
|
|
2584
|
+
parsed = JSON.parse(readFileSync14(configPath, "utf8"));
|
|
2306
2585
|
} catch (error) {
|
|
2307
2586
|
return {
|
|
2308
2587
|
configPath,
|
|
@@ -2323,9 +2602,9 @@ function loadPortfolioManifest(configPath) {
|
|
|
2323
2602
|
};
|
|
2324
2603
|
}
|
|
2325
2604
|
function resolveManifestPaths(value, configDir) {
|
|
2326
|
-
if (!
|
|
2605
|
+
if (!isRecord2(value)) return value;
|
|
2327
2606
|
const resolveEndpoint = (endpoint) => {
|
|
2328
|
-
if (!
|
|
2607
|
+
if (!isRecord2(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
|
|
2329
2608
|
return endpoint;
|
|
2330
2609
|
}
|
|
2331
2610
|
return { ...endpoint, path: resolve2(configDir, endpoint.path) };
|
|
@@ -2339,7 +2618,7 @@ function resolveManifestPaths(value, configDir) {
|
|
|
2339
2618
|
}
|
|
2340
2619
|
function validatePortfolioManifest(value) {
|
|
2341
2620
|
const issues = [];
|
|
2342
|
-
if (!
|
|
2621
|
+
if (!isRecord2(value)) {
|
|
2343
2622
|
return [
|
|
2344
2623
|
{
|
|
2345
2624
|
type: "invalid-field",
|
|
@@ -2375,7 +2654,7 @@ function validatePortfolioManifest(value) {
|
|
|
2375
2654
|
}
|
|
2376
2655
|
const seenTargetIds = /* @__PURE__ */ new Set();
|
|
2377
2656
|
for (const target of value.targets) {
|
|
2378
|
-
if (!
|
|
2657
|
+
if (!isRecord2(target)) {
|
|
2379
2658
|
issues.push({
|
|
2380
2659
|
type: "invalid-field",
|
|
2381
2660
|
field: "targets",
|
|
@@ -2432,7 +2711,7 @@ function validateAllowedFields(value, location, allowedFields, issues) {
|
|
|
2432
2711
|
}
|
|
2433
2712
|
function validateEndpoint(value, field, issues) {
|
|
2434
2713
|
if (value === void 0) return;
|
|
2435
|
-
if (!
|
|
2714
|
+
if (!isRecord2(value)) {
|
|
2436
2715
|
issues.push({
|
|
2437
2716
|
type: "invalid-field",
|
|
2438
2717
|
field,
|
|
@@ -2476,7 +2755,7 @@ function validateOptionalStringArray(value, id, field, issues) {
|
|
|
2476
2755
|
});
|
|
2477
2756
|
}
|
|
2478
2757
|
}
|
|
2479
|
-
function
|
|
2758
|
+
function isRecord2(value) {
|
|
2480
2759
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2481
2760
|
}
|
|
2482
2761
|
|
|
@@ -2742,7 +3021,7 @@ function printUsage3() {
|
|
|
2742
3021
|
}
|
|
2743
3022
|
|
|
2744
3023
|
// src/commands/sync.ts
|
|
2745
|
-
import { existsSync as existsSync18, readFileSync as
|
|
3024
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
|
|
2746
3025
|
import { join as join17 } from "node:path";
|
|
2747
3026
|
function runSync(args) {
|
|
2748
3027
|
if (!args.includes("--check")) {
|
|
@@ -2778,8 +3057,8 @@ function runSync(args) {
|
|
|
2778
3057
|
continue;
|
|
2779
3058
|
}
|
|
2780
3059
|
if (file.ownership === "project-local-seed") continue;
|
|
2781
|
-
const source =
|
|
2782
|
-
const target =
|
|
3060
|
+
const source = readFileSync15(file.absoluteSourcePath, "utf8");
|
|
3061
|
+
const target = readFileSync15(targetPath, "utf8");
|
|
2783
3062
|
if (source !== target) {
|
|
2784
3063
|
console.log(`different: ${file.targetPath}`);
|
|
2785
3064
|
differences += 1;
|
|
@@ -2822,6 +3101,7 @@ var COMMANDS = [
|
|
|
2822
3101
|
"lens report --target <path> --out <path>",
|
|
2823
3102
|
"lens audit init --target <path> --out <path>",
|
|
2824
3103
|
"lens audit check --dir <path> [--json]",
|
|
3104
|
+
"host-hook --host <codex|claude-code|antigravity> --event <Stop|SubagentStop|...>",
|
|
2825
3105
|
"init --profile <engineering-runtime|doc-only> <--dry-run|--apply>",
|
|
2826
3106
|
"sync --check [--profile <engineering-runtime|doc-only>]",
|
|
2827
3107
|
"doctor"
|
|
@@ -2836,6 +3116,8 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
2836
3116
|
process.exitCode = runLens(process.argv.slice(3));
|
|
2837
3117
|
} else if (command === "portfolio") {
|
|
2838
3118
|
process.exitCode = runPortfolio(process.argv.slice(3));
|
|
3119
|
+
} else if (command === "host-hook") {
|
|
3120
|
+
process.exitCode = runHostHook(process.argv.slice(3));
|
|
2839
3121
|
} else if (command === "init") {
|
|
2840
3122
|
process.exitCode = runInit(process.argv.slice(3));
|
|
2841
3123
|
} else if (command === "sync") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pieai/pro-gov",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
38
|
+
"@pieai/doc-gov": "^0.3.10"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "24.13.2",
|