@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.
- package/MIGRATING.md +139 -0
- package/dist/cli/audit-summary.d.ts +15 -0
- package/dist/cli/audit-summary.js +94 -38
- package/dist/cli/doctor.d.ts +44 -4
- package/dist/cli/doctor.js +170 -43
- package/dist/cli/init.d.ts +102 -0
- package/dist/cli/init.js +417 -72
- package/dist/cli/upgrade-check.d.ts +38 -0
- package/dist/cli/upgrade-check.js +93 -7
- package/dist/cli/upgrade.js +42 -0
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@ import { manifestExists, readManifest } from './install/manifest-io.js';
|
|
|
65
65
|
import { sha256OfBuffer, sha256OfFile } from './install/sha.js';
|
|
66
66
|
import { diffUnified, DIFF_TOO_LARGE_NOTICE } from './install/unified-diff.js';
|
|
67
67
|
import { loadPolicy } from '../policy/loader.js';
|
|
68
|
+
import { validateSettings } from '../config/settings-schema.js';
|
|
68
69
|
import { err, getPkgVersion, log } from './utils.js';
|
|
69
70
|
/** Hard cap on the diff input size. Mirrors `DIFF_SIZE_CAP_BYTES` in
|
|
70
71
|
* upgrade.ts so the two preview surfaces agree on the "too big to
|
|
@@ -288,6 +289,11 @@ async function classifyClaudeMd(resolvedRoot, includeDiffs) {
|
|
|
288
289
|
* upgrade flow, we prune known-stale hook tokens BEFORE merging the
|
|
289
290
|
* default-desired hook set — the order matters so the merge sees a
|
|
290
291
|
* clean baseline.
|
|
292
|
+
*
|
|
293
|
+
* 0.42.0 — also returns the merged object so the caller can run the
|
|
294
|
+
* same `validateSettings` check the real `runUpgrade` runs. We hand
|
|
295
|
+
* back the merged shape directly (not the file rendering) so the
|
|
296
|
+
* caller can decide whether to thread it into the schema check.
|
|
291
297
|
*/
|
|
292
298
|
async function classifySettings(resolvedRoot, includeDiffs) {
|
|
293
299
|
const desired = defaultDesiredHooks();
|
|
@@ -344,7 +350,7 @@ async function classifySettings(resolvedRoot, includeDiffs) {
|
|
|
344
350
|
Object.assign(file, safeDiff(oldText, newText, file.path));
|
|
345
351
|
}
|
|
346
352
|
}
|
|
347
|
-
return file;
|
|
353
|
+
return { file, merged: mergeResult.merged };
|
|
348
354
|
}
|
|
349
355
|
/**
|
|
350
356
|
* Build a `removed_upstream` entry from a manifest record that has no
|
|
@@ -480,14 +486,37 @@ export async function computeUpgradeCheck(options = {}) {
|
|
|
480
486
|
}
|
|
481
487
|
}
|
|
482
488
|
// Synthetic entries.
|
|
483
|
-
const
|
|
484
|
-
files.push(
|
|
489
|
+
const settingsClassification = await classifySettings(resolvedRoot, includeDiffs);
|
|
490
|
+
files.push(settingsClassification.file);
|
|
485
491
|
const claudeMd = await classifyClaudeMd(resolvedRoot, includeDiffs);
|
|
486
492
|
if (claudeMd !== null)
|
|
487
493
|
files.push(claudeMd);
|
|
488
494
|
const gitignoreFile = await classifyGitignore(resolvedRoot, includeDiffs);
|
|
489
495
|
if (gitignoreFile !== null)
|
|
490
496
|
files.push(gitignoreFile);
|
|
497
|
+
// 0.42.0 charter item 2 — schema-validate the merged settings the
|
|
498
|
+
// real `runUpgrade` would write. `runUpgrade` calls `validateSettings`
|
|
499
|
+
// (non-strict, matching the upgrade flow's posture) and throws when
|
|
500
|
+
// the merged result fails — refusing the write. Pre-0.42.0 the
|
|
501
|
+
// preview never ran this check, so the planner could promise a write
|
|
502
|
+
// that the real upgrade would refuse. Reproduce the exact validation
|
|
503
|
+
// shape here so the JSON `settings_validation` field is byte-for-byte
|
|
504
|
+
// what `runUpgrade` would see.
|
|
505
|
+
const validation = validateSettings(settingsClassification.merged, { strict: false });
|
|
506
|
+
const settingsValidation = {
|
|
507
|
+
parsed: validation.parsed,
|
|
508
|
+
errors: validation.errors,
|
|
509
|
+
};
|
|
510
|
+
// If validation failed, annotate the settings file row so the
|
|
511
|
+
// human-readable rendering surfaces the refusal alongside the count
|
|
512
|
+
// table (operators reading the table without scrolling to the
|
|
513
|
+
// footer still see the warning). Note appends rather than overwrites
|
|
514
|
+
// so the existing merge / prune annotations remain visible.
|
|
515
|
+
if (!validation.parsed) {
|
|
516
|
+
const refusalNote = `WOULD REFUSE: schema validation failed — ${validation.errors.join('; ')}`;
|
|
517
|
+
const f = settingsClassification.file;
|
|
518
|
+
f.note = f.note !== undefined ? `${f.note}; ${refusalNote}` : refusalNote;
|
|
519
|
+
}
|
|
491
520
|
// Stable sort: action priority (modified → created → removed_upstream
|
|
492
521
|
// → unchanged) then path. Operators reviewing the table want to see
|
|
493
522
|
// the changed entries first.
|
|
@@ -513,6 +542,13 @@ export async function computeUpgradeCheck(options = {}) {
|
|
|
513
542
|
bootstrap: isBootstrap,
|
|
514
543
|
counts,
|
|
515
544
|
files,
|
|
545
|
+
settings_validation: settingsValidation,
|
|
546
|
+
// Codex round 3 P2 (2026-05-16): mirror runUpgrade's pre-flight
|
|
547
|
+
// gate. `would_apply` is true when the real upgrade would actually
|
|
548
|
+
// start mutating disk — currently the only gate is settings-schema
|
|
549
|
+
// validation, but more pre-flight checks may land in future
|
|
550
|
+
// releases (in which case they get ANDed in here).
|
|
551
|
+
would_apply: settingsValidation.parsed,
|
|
516
552
|
};
|
|
517
553
|
}
|
|
518
554
|
/**
|
|
@@ -528,8 +564,20 @@ export function renderUpgradeCheck(plan) {
|
|
|
528
564
|
lines.push(` bootstrap mode: no install-manifest found yet`);
|
|
529
565
|
}
|
|
530
566
|
lines.push('');
|
|
567
|
+
// Codex round 3 P2 (2026-05-16): when a pre-flight gate would
|
|
568
|
+
// refuse the upgrade, the counts/files below describe what WOULD
|
|
569
|
+
// happen if the gate passed — but `rea upgrade` will actually
|
|
570
|
+
// write nothing in the current state. Lead with that banner so
|
|
571
|
+
// operators don't read the summary table as a promise of action.
|
|
572
|
+
if (!plan.would_apply) {
|
|
573
|
+
lines.push('BLOCKED — `rea upgrade` would refuse to apply this plan in its current state. ' +
|
|
574
|
+
'The summary below describes the would-be plan IF the refusal were fixed first; ' +
|
|
575
|
+
'the real upgrade writes nothing until the refusal clears.');
|
|
576
|
+
lines.push('');
|
|
577
|
+
}
|
|
531
578
|
const totalChanges = plan.counts.created + plan.counts.modified + plan.counts.removed_upstream;
|
|
532
|
-
|
|
579
|
+
const summaryLabel = plan.would_apply ? 'planned change(s)' : 'change(s) blocked by refusal';
|
|
580
|
+
lines.push(`Summary — ${String(totalChanges)} ${summaryLabel}:`);
|
|
533
581
|
lines.push(` created: ${String(plan.counts.created)}`);
|
|
534
582
|
lines.push(` modified: ${String(plan.counts.modified)}`);
|
|
535
583
|
lines.push(` removed-upstream: ${String(plan.counts.removed_upstream)}`);
|
|
@@ -538,6 +586,17 @@ export function renderUpgradeCheck(plan) {
|
|
|
538
586
|
if (totalChanges === 0) {
|
|
539
587
|
lines.push('No changes — your install is already in sync with this rea version.');
|
|
540
588
|
lines.push('');
|
|
589
|
+
// 0.42.0 — even with zero planned changes, surface a validation
|
|
590
|
+
// failure here so an operator doesn't see "in sync" and miss the
|
|
591
|
+
// settings refusal.
|
|
592
|
+
if (plan.settings_validation !== null && !plan.settings_validation.parsed) {
|
|
593
|
+
lines.push('WARNING: `rea upgrade` would REFUSE to run — the merged ' +
|
|
594
|
+
'.claude/settings.json would fail schema validation:');
|
|
595
|
+
for (const e of plan.settings_validation.errors) {
|
|
596
|
+
lines.push(` - ${e}`);
|
|
597
|
+
}
|
|
598
|
+
lines.push('');
|
|
599
|
+
}
|
|
541
600
|
return lines.join('\n');
|
|
542
601
|
}
|
|
543
602
|
// Per-file detail — skip `unchanged` entries.
|
|
@@ -545,7 +604,12 @@ export function renderUpgradeCheck(plan) {
|
|
|
545
604
|
if (file.action === 'unchanged')
|
|
546
605
|
continue;
|
|
547
606
|
const marker = file.action === 'created' ? '+' : file.action === 'removed_upstream' ? '-' : '~';
|
|
548
|
-
const
|
|
607
|
+
const baseLabel = file.action === 'removed_upstream' ? 'removed-upstream' : file.action;
|
|
608
|
+
// Codex round 3 P2 (2026-05-16): when a refusal is active, every
|
|
609
|
+
// would-be-mutated file row gets a BLOCKED suffix so a quick scroll
|
|
610
|
+
// through the per-file detail cannot miss the fact that no write
|
|
611
|
+
// will happen until the refusal clears.
|
|
612
|
+
const label = plan.would_apply ? baseLabel : `${baseLabel} (BLOCKED — refusal active)`;
|
|
549
613
|
const syntheticTag = file.synthetic !== undefined ? ` [${file.synthetic}]` : '';
|
|
550
614
|
lines.push(`${marker} ${file.path}${syntheticTag} — ${label}`);
|
|
551
615
|
if (file.note !== undefined)
|
|
@@ -572,8 +636,30 @@ export function renderUpgradeCheck(plan) {
|
|
|
572
636
|
}
|
|
573
637
|
lines.push('');
|
|
574
638
|
}
|
|
575
|
-
|
|
576
|
-
|
|
639
|
+
// 0.42.0 — surface settings-schema validation outcome alongside the
|
|
640
|
+
// footer. When the merged settings would fail validation, `rea
|
|
641
|
+
// upgrade` would refuse to start at all (codex round 2 P2 moved the
|
|
642
|
+
// validation to a pre-flight check BEFORE any file writes); we
|
|
643
|
+
// report that here so consumers can fix policy + settings before
|
|
644
|
+
// invoking the real upgrade.
|
|
645
|
+
if (plan.settings_validation !== null && !plan.settings_validation.parsed) {
|
|
646
|
+
lines.push('');
|
|
647
|
+
lines.push('WARNING: `rea upgrade` would REFUSE to run — the merged ' +
|
|
648
|
+
'.claude/settings.json would fail schema validation:');
|
|
649
|
+
for (const e of plan.settings_validation.errors) {
|
|
650
|
+
lines.push(` - ${e}`);
|
|
651
|
+
}
|
|
652
|
+
lines.push('No files will be written by `rea upgrade` while this is true — the ' +
|
|
653
|
+
'pre-flight check runs before any canonical hook or agent file is ' +
|
|
654
|
+
'installed AND before the 0.11.0 .rea/policy.yaml migration, so your ' +
|
|
655
|
+
'existing install stays untouched. Fix the settings entries flagged ' +
|
|
656
|
+
'above and re-run `rea upgrade --check`.');
|
|
657
|
+
lines.push('');
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
lines.push('No changes were written. Run `rea upgrade` (without --check) to apply.');
|
|
661
|
+
lines.push('');
|
|
662
|
+
}
|
|
577
663
|
return lines.join('\n');
|
|
578
664
|
}
|
|
579
665
|
/**
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -460,10 +460,52 @@ export async function runUpgrade(options = {}) {
|
|
|
460
460
|
if (options.force === true && !dryRun) {
|
|
461
461
|
warn('--force: overwriting locally-modified files and deleting removed-upstream entries without prompt.');
|
|
462
462
|
}
|
|
463
|
+
// 0.42.0 codex round 2 P2 (2026-05-16): pre-flight the merged
|
|
464
|
+
// settings validation BEFORE any file writes happen. Pre-correction,
|
|
465
|
+
// `upgradeSettings` ran AFTER the canonical-file write loop and only
|
|
466
|
+
// then validated the merged result; throwing here meant canonical
|
|
467
|
+
// hook + agent files had already been written to disk, breaking
|
|
468
|
+
// `rea upgrade --check`'s implicit "preview = real" contract (the
|
|
469
|
+
// check footer correctly claimed validation would refuse, but did
|
|
470
|
+
// not warn that hook files would still be written first).
|
|
471
|
+
//
|
|
472
|
+
// Codex round 2 P2 (2026-05-16, follow-up): this MUST run before
|
|
473
|
+
// `migrateReviewPolicyFor0110` as well — the 0.11.0 policy migration
|
|
474
|
+
// rewrites `.rea/policy.yaml` and creates a `policy.yaml.bak-*`
|
|
475
|
+
// sibling. If the pre-flight runs after that migration, the
|
|
476
|
+
// "no files have been written" claim in the refusal message
|
|
477
|
+
// becomes false for pre-0.11 consumers with malformed settings.
|
|
478
|
+
// Ordering: pre-flight FIRST, then migration, then canonical
|
|
479
|
+
// enumeration + write loop.
|
|
480
|
+
//
|
|
481
|
+
// The pre-flight reuses the same helpers as `upgradeSettings` —
|
|
482
|
+
// `readSettings` / `pruneHookCommands` / `mergeSettings` /
|
|
483
|
+
// `validateSettings` are all pure functions over existing on-disk
|
|
484
|
+
// state and `defaultDesiredHooks()`, so running them twice is
|
|
485
|
+
// cheap (microseconds — single JSON parse + small object merge)
|
|
486
|
+
// and the second run inside `upgradeSettings` re-derives the same
|
|
487
|
+
// result before writing. Atomicity guarantee: if validation fails,
|
|
488
|
+
// `runUpgrade` aborts here with ZERO mutations to disk.
|
|
489
|
+
{
|
|
490
|
+
const desired = defaultDesiredHooks();
|
|
491
|
+
const { settings: existingSettings } = readSettings(resolvedRoot);
|
|
492
|
+
const pruned = pruneHookCommands(existingSettings, STALE_HOOK_COMMAND_TOKENS);
|
|
493
|
+
const mergeResult = mergeSettings(pruned.merged, desired);
|
|
494
|
+
const validation = validateSettings(mergeResult.merged);
|
|
495
|
+
if (!validation.parsed) {
|
|
496
|
+
throw new Error(`rea upgrade: refusing to start because the merged .claude/settings.json would ` +
|
|
497
|
+
`fail schema validation. This is a safety guardrail — no files have been written ` +
|
|
498
|
+
`(including no .rea/policy.yaml 0.11.0 migration). Your existing install is ` +
|
|
499
|
+
`unchanged. zod errors: ${validation.errors.join('; ')}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
463
502
|
// 0.11.0 migration — strip removed review.* fields and backfill the new
|
|
464
503
|
// concerns_blocks default. Runs before canonical file reconciliation so a
|
|
465
504
|
// policy that fails strict schema load (which happens on upgrade from
|
|
466
505
|
// 0.10.x the moment we re-read `.rea/policy.yaml`) is cleaned up first.
|
|
506
|
+
// 0.42.0 round 2 P2 follow-up: ordered AFTER the pre-flight validation
|
|
507
|
+
// above so a settings-validation refusal does not leave a half-migrated
|
|
508
|
+
// .rea/policy.yaml + backup behind.
|
|
467
509
|
await migrateReviewPolicyFor0110(resolvedRoot, { dryRun });
|
|
468
510
|
const canonicalFiles = await enumerateCanonicalFiles();
|
|
469
511
|
if (canonicalFiles.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|