@bookedsolid/rea 0.41.0 → 0.43.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.
@@ -1264,18 +1264,74 @@ function defaultPython3PyYamlReachable(baseDir) {
1264
1264
  return false;
1265
1265
  }
1266
1266
  }
1267
+ /**
1268
+ * 0.42.0 codex round 5 P2 (2026-05-16) — execution probe for the
1269
+ * python3 list-walker. Mirrors `defaultPython3PyYamlReachable` exactly
1270
+ * but swaps the `import yaml` for `import json` (the actual stdlib
1271
+ * module the shim's list-walker branch imports). Spawning the
1272
+ * interpreter end-to-end catches the broken-symlink / unreachable-
1273
+ * shim case that a bare PATH check misses.
1274
+ */
1275
+ function defaultPython3ListWalkerReachable(baseDir) {
1276
+ try {
1277
+ const probeEnv = { ...process.env, PYTHONSAFEPATH: '1' };
1278
+ delete probeEnv['PYTHONPATH'];
1279
+ delete probeEnv['PYTHONHOME'];
1280
+ delete probeEnv['PYTHONSTARTUP'];
1281
+ // Same sys.path scrub as the production loader, applied before
1282
+ // `import json`. `json` is stdlib so a malicious `./json.py`
1283
+ // attack would matter the same way `./yaml.py` does.
1284
+ const probeBody = [
1285
+ 'import sys',
1286
+ 'import os',
1287
+ '_cwd = os.getcwd()',
1288
+ '_cwd_real = os.path.realpath(_cwd)',
1289
+ 'sys.path[:] = [p for p in sys.path if p not in ("", ".", _cwd, _cwd_real)]',
1290
+ 'import json',
1291
+ 'sys.stdout.write("ok")',
1292
+ ].join('\n');
1293
+ const res = spawnSync('python3', ['-c', probeBody], {
1294
+ cwd: baseDir,
1295
+ env: probeEnv,
1296
+ timeout: 5_000,
1297
+ stdio: ['ignore', 'pipe', 'ignore'],
1298
+ });
1299
+ if (res.status !== 0)
1300
+ return false;
1301
+ return (res.stdout?.toString().trim() ?? '') === 'ok';
1302
+ }
1303
+ catch {
1304
+ return false;
1305
+ }
1306
+ }
1267
1307
  const DEFAULT_PROBES = {
1268
1308
  cliDistExists: defaultCliDistExists,
1269
1309
  cliInvokable: defaultCliInvokable,
1270
1310
  python3OnPath: () => resolveBinaryOnPath('python3'),
1271
1311
  python3PyYamlReachable: defaultPython3PyYamlReachable,
1312
+ python3ListWalkerReachable: defaultPython3ListWalkerReachable,
1272
1313
  awkOnPath: () => resolveBinaryOnPath('awk'),
1273
1314
  jqOnPath: () => resolveBinaryOnPath('jq'),
1274
1315
  };
1275
1316
  function resolveProbes(probes) {
1276
1317
  if (probes === undefined)
1277
1318
  return DEFAULT_PROBES;
1278
- return { ...DEFAULT_PROBES, ...probes };
1319
+ // 0.42.0 codex round 5 P2 (2026-05-16): when a caller (typically
1320
+ // a unit test) stubs `python3OnPath` but does NOT stub the new
1321
+ // `python3ListWalkerReachable` execution probe, derive a faithful
1322
+ // fallback from `python3OnPath` instead of falling through to the
1323
+ // real `defaultPython3ListWalkerReachable` (which would spawn a
1324
+ // python3 subprocess and break test determinism). The convention
1325
+ // matches `python3PyYamlReachable` in the existing test suite:
1326
+ // stubs that say "python3 is present" want both downstream probes
1327
+ // to report reachable, and stubs that say "python3 is absent" want
1328
+ // both downstream probes to report unreachable.
1329
+ const overrides = { ...probes };
1330
+ if (overrides.python3ListWalkerReachable === undefined && overrides.python3OnPath !== undefined) {
1331
+ const stubbedPython3OnPath = overrides.python3OnPath;
1332
+ overrides.python3ListWalkerReachable = () => stubbedPython3OnPath() !== null;
1333
+ }
1334
+ return { ...DEFAULT_PROBES, ...overrides };
1279
1335
  }
1280
1336
  /**
1281
1337
  * Tier 1 — `rea hook policy-get`. Reachable when the rea CLI is
@@ -1374,12 +1430,12 @@ export function checkPolicyReaderTier2(baseDir, probes) {
1374
1430
  * Practically always present (POSIX requirement).
1375
1431
  *
1376
1432
  * 0.40.0 charter item 2 — conditional verdict, refined by codex
1377
- * round 1 P2:
1433
+ * round 1 P2 (0.40.0) and round 2 P2 (0.42.0):
1378
1434
  * - awk present → `pass`
1379
1435
  * - awk absent AND Tier 2 reachable → `warn`
1380
1436
  * (Tier 2 implies python3, which is a list-walker)
1381
1437
  * - awk absent AND Tier 1 reachable AND a list walker
1382
- * (jq OR python3) is on PATH → `warn`
1438
+ * (jq OR full Tier-2 reachable) is usable → `warn`
1383
1439
  * - awk absent AND Tier 1 reachable BUT no list walker → `fail`
1384
1440
  * (codex round 1 P2 — list-valued policy reads silently
1385
1441
  * fail-closed even though scalar reads work, so the
@@ -1398,9 +1454,29 @@ export function checkPolicyReaderTier2(baseDir, probes) {
1398
1454
  * functional box that has python3 + jq + the rea CLI all wired but
1399
1455
  * happens to lack awk.
1400
1456
  *
1457
+ * List-iteration semantic (clarifying note for codex round 2 P2,
1458
+ * 2026-05-16): `policy_reader_get_list` in
1459
+ * `hooks/_lib/policy-reader.sh` walks the cached subtree JSON via
1460
+ * `jq` OR `python3` (stdlib-only — `json` module, no PyYAML import).
1461
+ * PyYAML is only needed for Tier 2 itself (YAML PARSING into JSON),
1462
+ * NOT for iterating the already-parsed JSON arrays at list-read time.
1463
+ *
1464
+ * Codex round 5 P2 (2026-05-16): the "list walker" predicate uses
1465
+ * `python3ListWalkerReachable` — an EXECUTION probe that actually
1466
+ * spawns `python3 -c "import json"` — instead of `python3OnPath`. A
1467
+ * PATH-only check passes for broken pyenv/asdf shims, dangling
1468
+ * symlinks, and sandboxed environments where the interpreter cannot
1469
+ * start; in those cases the shim's list-walker branch would actually
1470
+ * fail and `blocked_paths`/`protected_writes` enforcement would
1471
+ * silently break while doctor reported `warn`. The execution probe
1472
+ * mirrors `defaultPython3PyYamlReachable` exactly but swaps the
1473
+ * `import yaml` for `import json` so it's not gated on PyYAML
1474
+ * availability (which is irrelevant to list iteration).
1475
+ *
1401
1476
  * Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
1402
- * present + CLI invokable) and Tier 2's reachability. Probes are
1403
- * threaded through identically.
1477
+ * present + CLI invokable), Tier 2's reachability, and the
1478
+ * list-walker execution probe. All probes are threaded through
1479
+ * identically.
1404
1480
  */
1405
1481
  export function checkPolicyReaderTier3(baseDir, probes) {
1406
1482
  const label = 'policy-reader Tier 3 (awk)';
@@ -1420,22 +1496,25 @@ export function checkPolicyReaderTier3(baseDir, probes) {
1420
1496
  // ladder would actually do at runtime.
1421
1497
  const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
1422
1498
  const tier2 = p.python3OnPath() !== null && p.python3PyYamlReachable(baseDir);
1423
- // Codex round 1 P2 (2026-05-16): the downgrade-to-warn branch needs
1424
- // a list walker too. `policy_reader_get_list` (the helper that reads
1425
- // list-valued keys like `blocked_paths`) iterates the parsed JSON
1426
- // array via jq OR python3, falling back to Tier 3 awk for inline
1427
- // arrays. With awk gone AND no jq AND no python3, list-valued
1428
- // policy reads silently fail-closed even when Tier 1 is reachable
1429
- // `blocked-paths-bash-gate.sh` etc. would see an EMPTY blocked-paths
1430
- // set and stop enforcing entries the operator declared. Pre-fix
1431
- // this concrete shape (cliInvokable + no python3 + no jq + no awk)
1432
- // returned `warn` and the doctor exited 0 on a broken install.
1433
- // Post-fix the downgrade requires a list walker; otherwise we stay
1434
- // on `fail`. Tier 2 implies python3 on PATH (the interpreter that
1435
- // ran PyYAML), so Tier 2 always brings list-walker support — no
1436
- // additional check needed for the Tier-2 branch.
1437
- const py = p.python3OnPath();
1438
- const listWalker = p.jqOnPath() !== null || py !== null;
1499
+ // Codex round 1 P2 (0.40.0) + round 2 P2 corrected (0.42.0,
1500
+ // 2026-05-16) + round 5 P2 (0.42.0, 2026-05-16): the
1501
+ // downgrade-to-warn branch needs a list walker too.
1502
+ // `policy_reader_get_list` iterates the parsed JSON array via jq
1503
+ // OR python3. The python3 branch uses `json` from stdlib only
1504
+ // PyYAML is NOT required (it's only needed for Tier 2's YAML
1505
+ // parsing step, which has already run by the time list iteration
1506
+ // executes).
1507
+ //
1508
+ // Round 5 P2 hardening: the python3 leg of this predicate uses an
1509
+ // EXECUTION probe (`python3ListWalkerReachable`), not just a PATH
1510
+ // check. A `python3` symlink can resolve on PATH while the
1511
+ // interpreter itself fails to start (dangling pyenv/asdf shim,
1512
+ // sandboxed runner without dynamic libs, permission denied on the
1513
+ // resolved binary). PATH-only would let doctor declare `warn` on
1514
+ // a box where the shim's list walker would actually fail —
1515
+ // silently breaking `blocked_paths` / `protected_writes`
1516
+ // enforcement while doctor exits 0.
1517
+ const listWalker = p.jqOnPath() !== null || p.python3ListWalkerReachable(baseDir);
1439
1518
  if (tier2 || (tier1 && listWalker)) {
1440
1519
  const reachable = [];
1441
1520
  if (tier1)
@@ -1451,21 +1530,43 @@ export function checkPolicyReaderTier3(baseDir, probes) {
1451
1530
  'to restore the last-resort fallback.',
1452
1531
  };
1453
1532
  }
1454
- // Codex round 1 P2: separate "no list walker" diagnosis from the
1455
- // catastrophic "no tier at all" case. Tier 1 reachable but no jq
1456
- // AND no python3 AND no awk means list-valued policy reads
1457
- // fail-closed silently — distinct from the truly-empty
1458
- // no-CLI-no-python-no-awk shape, and worth a precise remediation.
1533
+ // Codex round 1 P2 (0.40.0) + round 2 P2 (0.42.0): separate "no list
1534
+ // walker" diagnosis from the catastrophic "no tier at all" case.
1535
+ // Tier 1 reachable but no jq AND no python3 AND no awk means
1536
+ // list-valued policy reads fail-closed silently — distinct from the
1537
+ // truly-empty no-CLI-no-python-no-awk shape, and worth a precise
1538
+ // remediation. The python3-as-list-walker signal is plain
1539
+ // `python3OnPath` (the `json` module is stdlib — PyYAML is NOT
1540
+ // required for list iteration).
1459
1541
  if (tier1) {
1542
+ // 0.42.0 codex round 6 P3 (2026-05-16): distinguish "python3 not
1543
+ // on PATH" from "python3 on PATH but execution fails". Pre-fix
1544
+ // this branch always reported "python3 is not on PATH" even when
1545
+ // a python3 binary was resolvable but a broken pyenv/asdf shim
1546
+ // or sandboxed interpreter failed the execution probe — that
1547
+ // sent operators toward the wrong remediation. Round 5 added
1548
+ // the execution probe specifically to surface this case; the
1549
+ // diagnostic needs to follow.
1550
+ const pythonOnPath = p.python3OnPath();
1551
+ const pythonState = pythonOnPath === null
1552
+ ? 'python3 is not on PATH'
1553
+ : `python3 at ${pythonOnPath} cannot execute \`import json\` (broken pyenv/asdf shim, ` +
1554
+ 'sandboxed interpreter, or permission-denied binary — fix the interpreter or ' +
1555
+ 'remove the shim)';
1556
+ const remediation = pythonOnPath === null
1557
+ ? 'Install awk OR jq OR python3 to restore list-iteration.'
1558
+ : `Install awk OR jq, or repair the python3 interpreter at ${pythonOnPath}, ` +
1559
+ 'to restore list-iteration.';
1460
1560
  return {
1461
1561
  label,
1462
1562
  status: 'fail',
1463
- detail: 'awk not on PATH AND neither jq nor python3 is on PATH Tier 1 (rea CLI) parses ' +
1464
- 'flow-form scalars, but `policy_reader_get_list` cannot iterate list-valued keys ' +
1465
- '(e.g. `blocked_paths: [.env, ...]`) without jq, python3, OR awk to walk the ' +
1466
- 'resulting JSON arrays. Affected hooks (`blocked-paths-bash-gate.sh`, ' +
1467
- '`blocked-paths-enforcer.sh`, …) see an EMPTY list and silently stop enforcing. ' +
1468
- 'Install awk OR jq OR python3 to restore list-iteration.',
1563
+ detail: `awk not on PATH AND jq is not on PATH AND ${pythonState} ` +
1564
+ 'Tier 1 (rea CLI) parses flow-form scalars, but `policy_reader_get_list` ' +
1565
+ 'cannot iterate list-valued keys (e.g. `blocked_paths: [.env, ...]`) ' +
1566
+ 'without jq OR python3 OR awk to walk the resulting JSON arrays. ' +
1567
+ 'Affected hooks (`blocked-paths-bash-gate.sh`, ' +
1568
+ `\`blocked-paths-enforcer.sh\`, …) see an EMPTY list and silently stop ` +
1569
+ `enforcing. ${remediation}`,
1469
1570
  };
1470
1571
  }
1471
1572
  return {
@@ -1542,11 +1643,14 @@ export function checkPolicyReaderTierSummary(baseDir, probes) {
1542
1643
  const tier2 = py !== null && p.python3PyYamlReachable(baseDir);
1543
1644
  const tier3 = p.awkOnPath() !== null;
1544
1645
  const jq = p.jqOnPath();
1545
- // List iteration after Tier 1/2 needs jq OR python3 to walk the
1546
- // JSON. Tier 2 implies python3 on PATH (the interpreter that ran
1547
- // the loader); so the only "lists broken" shape is Tier 1 reachable
1548
- // but neither jq nor python3 on PATH.
1549
- const listWalker = jq !== null || py !== null;
1646
+ // 0.42.0 codex round 5 P2 (2026-05-16): list iteration after Tier
1647
+ // 1/2 needs jq OR a python3 that can ACTUALLY execute (not just
1648
+ // resolve on PATH). The execution probe catches the broken-shim
1649
+ // case where `python3` resolves but the interpreter cannot start
1650
+ // PATH-only would falsely declare the list walker "usable" on a
1651
+ // box where the shim's python3 branch will fall through to Tier 3
1652
+ // and silently miss flow-form arrays.
1653
+ const listWalker = jq !== null || p.python3ListWalkerReachable(baseDir);
1550
1654
  const reachable = [];
1551
1655
  if (tier1)
1552
1656
  reachable.push('Tier 1 (CLI)');
@@ -1556,17 +1660,40 @@ export function checkPolicyReaderTierSummary(baseDir, probes) {
1556
1660
  reachable.push('Tier 3 (awk)');
1557
1661
  if (tier1 || tier2) {
1558
1662
  if (!listWalker) {
1559
- // Tier 1 + no python3/jq. flow-form scalars work; flow-form
1560
- // arrays silently no-op via Tier 3 fallthrough. (Tier 2 path
1561
- // is unreachable here because Tier 2 requires python3.)
1663
+ // Tier 1 + no working list walker. flow-form scalars work;
1664
+ // flow-form arrays silently no-op via Tier 3 fallthrough.
1665
+ // (Tier 2 path is unreachable here because Tier 2 requires
1666
+ // python3 itself reachable.)
1667
+ //
1668
+ // 0.43.0 round-7 P3 (2026-05-17): mirror the round-6 P3 fix
1669
+ // from `checkPolicyReaderTier3`. Pre-fix this branch always
1670
+ // said "neither jq nor python3 is on PATH" — but the
1671
+ // `listWalker` predicate is `jq OR python3ListWalkerReachable`,
1672
+ // so it also fires when python3 IS on PATH but the EXECUTION
1673
+ // probe fails (broken pyenv/asdf shim, dangling symlink,
1674
+ // sandboxed interpreter that fails to start). That
1675
+ // misdiagnosis sent operators chasing the wrong remediation
1676
+ // ("install python3" when python3 was already installed but
1677
+ // broken). Distinguish the two shapes so the operator sees
1678
+ // the actual problem, and surface the resolved path so they
1679
+ // can `ls -l` it on the filesystem.
1680
+ const pythonOnPath = py;
1681
+ const pythonState = pythonOnPath === null
1682
+ ? 'neither jq nor python3 is on PATH'
1683
+ : `jq is not on PATH AND python3 at ${pythonOnPath} cannot execute \`import json\` ` +
1684
+ '(broken pyenv/asdf shim, sandboxed interpreter, or permission-denied binary — ' +
1685
+ 'fix the interpreter or remove the shim)';
1686
+ const remediation = pythonOnPath === null
1687
+ ? 'Install jq (`brew install jq` / `apt-get install jq`) or python3 to close the gap.'
1688
+ : `Install jq (\`brew install jq\` / \`apt-get install jq\`) or repair the python3 ` +
1689
+ `interpreter at ${pythonOnPath} to close the gap.`;
1562
1690
  return {
1563
1691
  label,
1564
1692
  status: 'warn',
1565
1693
  detail: `${reachable.join(', ')} reachable — flow-form scalars parse via Tier 1 CLI, ` +
1566
- 'BUT neither jq nor python3 is on PATH so `policy_reader_get_list` cannot iterate ' +
1694
+ `BUT ${pythonState} so \`policy_reader_get_list\` cannot iterate ` +
1567
1695
  'the resulting JSON arrays. Flow-form list policy (e.g. `blocked_paths: [.env, ...]`) ' +
1568
- 'silently falls through to Tier 3 awk and misses inline arrays. Install jq ' +
1569
- '(`brew install jq` / `apt-get install jq`) or python3 to close the gap.',
1696
+ `silently falls through to Tier 3 awk and misses inline arrays. ${remediation}`,
1570
1697
  };
1571
1698
  }
1572
1699
  return {
@@ -1,3 +1,4 @@
1
+ import { AutonomyLevel } from '../policy/types.js';
1
2
  export interface InitOptions {
2
3
  yes?: boolean | undefined;
3
4
  fromReagent?: boolean | undefined;
@@ -16,4 +17,105 @@ export interface InitOptions {
16
17
  */
17
18
  codex?: boolean | undefined;
18
19
  }
20
+ type ProfileName = 'client-engagement' | 'bst-internal' | 'bst-internal-no-codex' | 'lit-wc' | 'open-source' | 'open-source-no-codex' | 'minimal';
21
+ export interface ResolvedConfig {
22
+ profile: ProfileName;
23
+ autonomyLevel: AutonomyLevel;
24
+ maxAutonomyLevel: AutonomyLevel;
25
+ blockAiAttribution: boolean;
26
+ blockedPaths: string[];
27
+ notificationChannel: string;
28
+ /**
29
+ * G11.4: written to `.rea/policy.yaml` as `review.codex_required`. We
30
+ * always emit the field explicitly — no implicit defaults — so an
31
+ * operator reading the file sees the choice that was made at init time.
32
+ */
33
+ codexRequired: boolean;
34
+ /**
35
+ * Round-27 F6: preserved 0.26.0 local-review + commit-hygiene knobs.
36
+ * Each is `undefined` when the operator never set it, in which case
37
+ * the policy writer omits the corresponding line from the YAML output
38
+ * (consumers fall through to the documented 0.26.0 defaults).
39
+ */
40
+ localReviewMode?: 'enforced' | 'off';
41
+ localReviewRefuseAt?: 'push' | 'commit' | 'both';
42
+ localReviewBypassEnvVar?: string;
43
+ localReviewMaxAgeSeconds?: number;
44
+ commitHygieneWarnAtCommits?: number;
45
+ commitHygieneRefuseAtCommits?: number;
46
+ /**
47
+ * 0.30.0 attribution augmenter. Preserved across re-init from a prior
48
+ * on-disk policy and seeded from the chosen profile on first install.
49
+ * Every shipped profile pins `enabled: false`, so the default for new
50
+ * installs is "block ready, opt in by editing the policy".
51
+ */
52
+ attributionCoAuthor?: {
53
+ enabled?: boolean;
54
+ name?: string;
55
+ email?: string;
56
+ skipMerge?: boolean;
57
+ };
58
+ fromReagent: boolean;
59
+ reagentPolicyPath: string | null;
60
+ reagentNotices: string[];
61
+ }
62
+ /**
63
+ * 0.43.0 UX polish: build the human-readable install summary shown
64
+ * BEFORE any files are written. Lists, in order: the policy file
65
+ * being written, the chosen profile + autonomy, hook + agent counts
66
+ * planned, the git/husky hooks planned (paths reflect what the
67
+ * installer will ACTUALLY do given the target tree's shape), and
68
+ * whether re-run preservation is active.
69
+ *
70
+ * Rendered via clack's `note` primitive so it sits in a bordered block
71
+ * adjacent to the final `confirm` gate. The string is also returned
72
+ * verbatim so the test suite can assert content without mocking clack.
73
+ *
74
+ * `targetState` is computed by {@link detectTargetState} — kept as a
75
+ * separate argument so tests can drive both shapes (husky-present and
76
+ * husky-absent) without touching the filesystem.
77
+ */
78
+ export declare function buildInstallSummary(targetDir: string, config: ResolvedConfig, reRunMode: boolean, targetState: TargetState): string;
79
+ /**
80
+ * 0.43.0 codex round-1 P3: shape of the target tree the installer
81
+ * will see. `buildInstallSummary` and the post-install verifier both
82
+ * need to know whether `.git/` and `.husky/` are present so the
83
+ * summary doesn't lie about which hook files will be written.
84
+ */
85
+ export interface TargetState {
86
+ gitRepoPresent: boolean;
87
+ huskyDirPresent: boolean;
88
+ }
89
+ /**
90
+ * 0.43.0 codex round-1 P3: detect which hook surfaces the installer
91
+ * will actually touch. Returns a snapshot so the install-summary
92
+ * confirm screen can show the right paths.
93
+ *
94
+ * Intentionally simple — the installers themselves (commit-msg,
95
+ * prepare-commit-msg, pre-push) each re-check at write time, so this
96
+ * detection is purely presentational. If something races between the
97
+ * snapshot and the writes (a `pnpm install` adding `.husky/` in the
98
+ * window between confirm and spinner), the installer's own checks win
99
+ * and the summary was only slightly stale.
100
+ */
101
+ export declare function detectTargetState(targetDir: string): TargetState;
102
+ /**
103
+ * 0.43.0 UX polish: post-install sanity check. Runs synchronously
104
+ * after the file-write phase to catch installs that completed
105
+ * "successfully" but are missing a critical artifact (write
106
+ * permissions issue, partial copy, etc.).
107
+ *
108
+ * Strictly read-only — no probes that touch python3 / jq / codex.
109
+ * Pattern modelled on the synthetic round-trip checks established by
110
+ * `checkDelegationRoundTrip` in 0.29.0/0.31.0: cheap, in-process,
111
+ * sufficient to catch the "looks-installed-but-isn't" failure shape
112
+ * that bites first-time consumers hardest. For deep diagnostics
113
+ * point the operator at `rea doctor`.
114
+ *
115
+ * Returns the list of issues found (empty = healthy). The caller
116
+ * surfaces them via clack's `log.warn` and points the operator at
117
+ * `rea doctor` for follow-up.
118
+ */
119
+ export declare function postInstallVerify(targetDir: string): string[];
19
120
  export declare function runInit(options: InitOptions): Promise<void>;
121
+ export {};