@bookedsolid/rea 0.21.0 → 0.21.1
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/dist/cli/init.js +126 -15
- package/package.json +1 -1
package/dist/cli/init.js
CHANGED
|
@@ -85,7 +85,7 @@ function resolveLayered(profileName, reagentTranslated) {
|
|
|
85
85
|
}
|
|
86
86
|
return layered;
|
|
87
87
|
}
|
|
88
|
-
async function runWizard(options, targetDir, reagentPolicyPath, layeredBase) {
|
|
88
|
+
async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, existingPolicy = undefined) {
|
|
89
89
|
const projectName = detectProjectName(targetDir);
|
|
90
90
|
p.intro(`rea init — ${projectName}`);
|
|
91
91
|
let fromReagent = options.fromReagent === true;
|
|
@@ -124,9 +124,15 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase) {
|
|
|
124
124
|
cancel('Init cancelled.');
|
|
125
125
|
profileName = picked;
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
// 0.21.1: prefer the existing on-disk value over the profile default
|
|
128
|
+
// so re-running `rea init` doesn't reset an operator's manual edit.
|
|
129
|
+
const autonomyDefault = existingPolicy?.autonomyLevel
|
|
130
|
+
?? layeredBase.autonomy_level
|
|
131
|
+
?? AutonomyLevel.L1;
|
|
128
132
|
const autonomyPick = await p.select({
|
|
129
|
-
message:
|
|
133
|
+
message: existingPolicy?.autonomyLevel !== undefined
|
|
134
|
+
? `Starting autonomy_level (current: ${existingPolicy.autonomyLevel})`
|
|
135
|
+
: 'Starting autonomy_level',
|
|
130
136
|
initialValue: autonomyDefault,
|
|
131
137
|
options: [
|
|
132
138
|
{ value: AutonomyLevel.L0, label: 'L0', hint: 'read-only; every write needs approval' },
|
|
@@ -139,9 +145,13 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase) {
|
|
|
139
145
|
cancel('Init cancelled.');
|
|
140
146
|
const autonomyLevel = autonomyPick;
|
|
141
147
|
const maxCandidates = AUTONOMY_LEVELS.filter((lvl) => levelRank(lvl) >= levelRank(autonomyLevel));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
// 0.21.1: prefer existing on-disk max_autonomy_level over profile default.
|
|
149
|
+
const defaultMax = (existingPolicy?.maxAutonomyLevel !== undefined &&
|
|
150
|
+
maxCandidates.includes(existingPolicy.maxAutonomyLevel) &&
|
|
151
|
+
existingPolicy.maxAutonomyLevel) ||
|
|
152
|
+
(layeredBase.max_autonomy_level !== undefined &&
|
|
153
|
+
maxCandidates.includes(layeredBase.max_autonomy_level) &&
|
|
154
|
+
layeredBase.max_autonomy_level) ||
|
|
145
155
|
maxCandidates.find((l) => l === AutonomyLevel.L2) ||
|
|
146
156
|
autonomyLevel;
|
|
147
157
|
const maxOptions = maxCandidates.map((lvl) => {
|
|
@@ -233,6 +243,79 @@ async function printCodexInstallAssist() {
|
|
|
233
243
|
console.log(' Install via the Claude Code Codex plugin helper: `/codex:setup`,');
|
|
234
244
|
console.log(' or set `review.codex_required: false` in .rea/policy.yaml to opt out.');
|
|
235
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Read user-mutable values from an existing `.rea/policy.yaml`.
|
|
248
|
+
* Returns undefined when the file doesn't exist or fails to parse.
|
|
249
|
+
*
|
|
250
|
+
* The reader is permissive — any field that fails to extract is
|
|
251
|
+
* dropped from the result; the caller falls back to the profile
|
|
252
|
+
* default for that one field. This is the idempotency contract
|
|
253
|
+
* extension introduced in 0.17.0 (`installed_at` preservation),
|
|
254
|
+
* extended in 0.21.1 to cover every field an operator might
|
|
255
|
+
* manually edit between init runs.
|
|
256
|
+
*
|
|
257
|
+
* Profile-switch is allowed but advisory: when the existing
|
|
258
|
+
* `profile:` value disagrees with the requested one, the existing
|
|
259
|
+
* VALUES are still preserved. Operators who want full reset pass
|
|
260
|
+
* `--force` to bypass the file-existence check entirely.
|
|
261
|
+
*/
|
|
262
|
+
function readExistingPolicyForPreservation(targetDir) {
|
|
263
|
+
const policyPath = path.join(targetDir, REA_DIR, POLICY_FILE);
|
|
264
|
+
if (!fs.existsSync(policyPath))
|
|
265
|
+
return undefined;
|
|
266
|
+
try {
|
|
267
|
+
const raw = fs.readFileSync(policyPath, 'utf8');
|
|
268
|
+
const out = {};
|
|
269
|
+
// Profile (informational; used for stderr advisory).
|
|
270
|
+
const pm = raw.match(/^profile:\s*['"]?([a-z0-9-]+)['"]?\s*$/m);
|
|
271
|
+
if (pm)
|
|
272
|
+
out.profile = pm[1];
|
|
273
|
+
// Autonomy + ceiling (enum).
|
|
274
|
+
const am = raw.match(/^autonomy_level:\s*(L[0-3])\s*$/m);
|
|
275
|
+
const amVal = am?.[1];
|
|
276
|
+
if (amVal !== undefined && Object.values(AutonomyLevel).includes(amVal)) {
|
|
277
|
+
out.autonomyLevel = amVal;
|
|
278
|
+
}
|
|
279
|
+
const mm = raw.match(/^max_autonomy_level:\s*(L[0-3])\s*$/m);
|
|
280
|
+
const mmVal = mm?.[1];
|
|
281
|
+
if (mmVal !== undefined && Object.values(AutonomyLevel).includes(mmVal)) {
|
|
282
|
+
out.maxAutonomyLevel = mmVal;
|
|
283
|
+
}
|
|
284
|
+
// block_ai_attribution.
|
|
285
|
+
const bm = raw.match(/^block_ai_attribution:\s*(true|false)\s*$/m);
|
|
286
|
+
if (bm?.[1] !== undefined)
|
|
287
|
+
out.blockAiAttribution = bm[1] === 'true';
|
|
288
|
+
// blocked_paths block-sequence — line-by-line scan.
|
|
289
|
+
const bpStart = raw.match(/^blocked_paths:\s*$/m);
|
|
290
|
+
if (bpStart) {
|
|
291
|
+
const after = raw.slice((bpStart.index ?? 0) + bpStart[0].length + 1);
|
|
292
|
+
const lines = after.split('\n');
|
|
293
|
+
const collected = [];
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
const m2 = line.match(/^\s*-\s+(?:['"]([^'"]+)['"]|(\S.*?))\s*$/);
|
|
296
|
+
if (!m2)
|
|
297
|
+
break;
|
|
298
|
+
const v = m2[1] ?? m2[2];
|
|
299
|
+
if (v !== undefined)
|
|
300
|
+
collected.push(v);
|
|
301
|
+
}
|
|
302
|
+
if (collected.length > 0)
|
|
303
|
+
out.blockedPaths = collected;
|
|
304
|
+
}
|
|
305
|
+
// notification_channel.
|
|
306
|
+
const nm = raw.match(/^notification_channel:\s*['"]?([^'"\n]*)['"]?\s*$/m);
|
|
307
|
+
if (nm?.[1] !== undefined)
|
|
308
|
+
out.notificationChannel = nm[1];
|
|
309
|
+
// review.codex_required (under nested `review:` block).
|
|
310
|
+
const cm = raw.match(/^\s+codex_required:\s*(true|false)\s*$/m);
|
|
311
|
+
if (cm?.[1] !== undefined)
|
|
312
|
+
out.codexRequired = cm[1] === 'true';
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
236
319
|
function readExistingInstalledAt(policyPath) {
|
|
237
320
|
try {
|
|
238
321
|
if (!fs.existsSync(policyPath))
|
|
@@ -480,30 +563,58 @@ export async function runInit(options) {
|
|
|
480
563
|
}
|
|
481
564
|
}
|
|
482
565
|
const layeredBase = resolveLayered(profileName, reagentTranslated);
|
|
566
|
+
// 0.21.1: preserve user-mutable policy values across re-init (idempotency
|
|
567
|
+
// class — same as the `installed_at` fix from 0.17.0). Pre-fix, every
|
|
568
|
+
// `rea init` re-applied profile defaults, silently resetting an
|
|
569
|
+
// operator's `autonomy_level: L2` back to the profile's L1, etc.
|
|
570
|
+
// Read the existing policy if present and merge: explicit existing
|
|
571
|
+
// value wins over profile default. Operator opts out with --force
|
|
572
|
+
// (existing flag — bypass the file-existence check entirely).
|
|
573
|
+
// Profile-switch case: when the existing profile name disagrees with
|
|
574
|
+
// the requested profile, the existing values are STILL preserved by
|
|
575
|
+
// default but a stderr advisory names what was kept; operator can
|
|
576
|
+
// pass --force to fully reset.
|
|
577
|
+
const existingPolicy = readExistingPolicyForPreservation(targetDir);
|
|
483
578
|
let config;
|
|
484
579
|
if (options.yes === true) {
|
|
485
580
|
// G11.4 non-interactive codex resolution:
|
|
486
581
|
// 1. Explicit --codex / --no-codex flag wins.
|
|
487
|
-
// 2. Otherwise
|
|
582
|
+
// 2. Otherwise existing policy value wins (preserves operator edit).
|
|
583
|
+
// 3. Otherwise derive from the profile name (`*-no-codex` → false).
|
|
488
584
|
const codexRequired = options.codex !== undefined
|
|
489
585
|
? options.codex
|
|
490
|
-
: profileDefaultCodexRequired(profileName);
|
|
586
|
+
: (existingPolicy?.codexRequired ?? profileDefaultCodexRequired(profileName));
|
|
491
587
|
config = {
|
|
492
588
|
profile: profileName,
|
|
493
|
-
autonomyLevel:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
589
|
+
autonomyLevel: existingPolicy?.autonomyLevel
|
|
590
|
+
?? layeredBase.autonomy_level
|
|
591
|
+
?? AutonomyLevel.L1,
|
|
592
|
+
maxAutonomyLevel: existingPolicy?.maxAutonomyLevel
|
|
593
|
+
?? layeredBase.max_autonomy_level
|
|
594
|
+
?? AutonomyLevel.L2,
|
|
595
|
+
blockAiAttribution: existingPolicy?.blockAiAttribution
|
|
596
|
+
?? layeredBase.block_ai_attribution
|
|
597
|
+
?? true,
|
|
598
|
+
blockedPaths: existingPolicy?.blockedPaths
|
|
599
|
+
?? layeredBase.blocked_paths
|
|
600
|
+
?? ['.env', '.env.*'],
|
|
601
|
+
notificationChannel: existingPolicy?.notificationChannel
|
|
602
|
+
?? layeredBase.notification_channel
|
|
603
|
+
?? '',
|
|
498
604
|
codexRequired,
|
|
499
605
|
fromReagent,
|
|
500
606
|
reagentPolicyPath,
|
|
501
607
|
reagentNotices,
|
|
502
608
|
};
|
|
503
|
-
|
|
609
|
+
if (existingPolicy !== undefined) {
|
|
610
|
+
log(`Non-interactive init (re-run): preserving existing autonomy=${config.autonomyLevel}, max=${config.maxAutonomyLevel}, attribution-block=${config.blockAiAttribution}, codex_required=${config.codexRequired}. Pass --force to reset to profile defaults.`);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
log(`Non-interactive init: profile=${profileName}, autonomy=${config.autonomyLevel}, max=${config.maxAutonomyLevel}, attribution-block=${config.blockAiAttribution}, codex_required=${config.codexRequired}`);
|
|
614
|
+
}
|
|
504
615
|
}
|
|
505
616
|
else {
|
|
506
|
-
config = await runWizard(options, targetDir, reagentPolicyPath, layeredBase);
|
|
617
|
+
config = await runWizard(options, targetDir, reagentPolicyPath, layeredBase, existingPolicy);
|
|
507
618
|
config.reagentNotices = reagentNotices;
|
|
508
619
|
}
|
|
509
620
|
if (!fs.existsSync(reaDir))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
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)",
|