@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
package/dist/cli/init.js
CHANGED
|
@@ -90,6 +90,24 @@ function resolveLayered(profileName, reagentTranslated) {
|
|
|
90
90
|
async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, existingPolicy = undefined) {
|
|
91
91
|
const projectName = detectProjectName(targetDir);
|
|
92
92
|
p.intro(`rea init — ${projectName}`);
|
|
93
|
+
// 0.43.0 UX polish: surface re-run vs fresh-install mode at the top
|
|
94
|
+
// of the wizard so the operator sees up-front that an existing policy
|
|
95
|
+
// is being preserved (not overwritten with defaults). Pre-fix the only
|
|
96
|
+
// signal was an oblique prompt label change ("current: L2") several
|
|
97
|
+
// questions in — easy to miss when running the wizard interactively.
|
|
98
|
+
if (existingPolicy !== undefined) {
|
|
99
|
+
p.note([
|
|
100
|
+
`Existing install detected at ${path.join(REA_DIR, POLICY_FILE)}.`,
|
|
101
|
+
'Re-run mode: your current settings will be preserved as defaults below.',
|
|
102
|
+
'Pass --force to reset everything to profile defaults.',
|
|
103
|
+
].join('\n'), 'Re-running init');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
p.note([
|
|
107
|
+
`Setting up rea governance for ${projectName}.`,
|
|
108
|
+
'You can change every answer later by editing .rea/policy.yaml.',
|
|
109
|
+
].join('\n'), 'Fresh install');
|
|
110
|
+
}
|
|
93
111
|
let fromReagent = options.fromReagent === true;
|
|
94
112
|
if (!fromReagent && reagentPolicyPath !== null) {
|
|
95
113
|
const migrate = await p.confirm({
|
|
@@ -112,18 +130,34 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, exi
|
|
|
112
130
|
}
|
|
113
131
|
else {
|
|
114
132
|
const picked = await p.select({
|
|
115
|
-
message: 'Pick a profile',
|
|
133
|
+
message: 'Pick a profile preset',
|
|
116
134
|
initialValue: 'minimal',
|
|
117
135
|
options: [
|
|
118
|
-
{
|
|
136
|
+
{
|
|
137
|
+
value: 'minimal',
|
|
138
|
+
label: 'minimal',
|
|
139
|
+
hint: 'bare policy, no extras — safe starting point',
|
|
140
|
+
},
|
|
119
141
|
{
|
|
120
142
|
value: 'client-engagement',
|
|
121
143
|
label: 'client-engagement',
|
|
122
|
-
hint: 'zero-trust client project',
|
|
144
|
+
hint: 'zero-trust client project — strict by default',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
value: 'bst-internal',
|
|
148
|
+
label: 'bst-internal',
|
|
149
|
+
hint: 'BookedSolid internal projects (Codex review on)',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
value: 'lit-wc',
|
|
153
|
+
label: 'lit-wc',
|
|
154
|
+
hint: 'Lit / web-component libraries',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
value: 'open-source',
|
|
158
|
+
label: 'open-source',
|
|
159
|
+
hint: 'public OSS repos (Codex review on)',
|
|
123
160
|
},
|
|
124
|
-
{ value: 'bst-internal', label: 'bst-internal', hint: 'internal BST projects' },
|
|
125
|
-
{ value: 'lit-wc', label: 'lit-wc', hint: 'Lit / web component libraries' },
|
|
126
|
-
{ value: 'open-source', label: 'open-source', hint: 'public OSS repos' },
|
|
127
161
|
],
|
|
128
162
|
});
|
|
129
163
|
if (p.isCancel(picked))
|
|
@@ -133,16 +167,34 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, exi
|
|
|
133
167
|
// 0.21.1: prefer the existing on-disk value over the profile default
|
|
134
168
|
// so re-running `rea init` doesn't reset an operator's manual edit.
|
|
135
169
|
const autonomyDefault = existingPolicy?.autonomyLevel ?? layeredBase.autonomy_level ?? AutonomyLevel.L1;
|
|
170
|
+
const autonomyMessage = existingPolicy?.autonomyLevel !== undefined
|
|
171
|
+
? `Starting autonomy level (current: ${existingPolicy.autonomyLevel}). Controls how much the ` +
|
|
172
|
+
`agent can do without asking you first.`
|
|
173
|
+
: 'Starting autonomy level — how much can the agent do without asking you first?';
|
|
136
174
|
const autonomyPick = await p.select({
|
|
137
|
-
message:
|
|
138
|
-
? `Starting autonomy_level (current: ${existingPolicy.autonomyLevel})`
|
|
139
|
-
: 'Starting autonomy_level',
|
|
175
|
+
message: autonomyMessage,
|
|
140
176
|
initialValue: autonomyDefault,
|
|
141
177
|
options: [
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
{
|
|
179
|
+
value: AutonomyLevel.L0,
|
|
180
|
+
label: 'L0 — read-only',
|
|
181
|
+
hint: 'every write needs your approval; safest, slowest',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
value: AutonomyLevel.L1,
|
|
185
|
+
label: 'L1 — supervised writes',
|
|
186
|
+
hint: 'default — writes allowed, destructive ops gated',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
value: AutonomyLevel.L2,
|
|
190
|
+
label: 'L2 — wide latitude',
|
|
191
|
+
hint: 'destructive ops allowed; suitable for experienced operators',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
value: AutonomyLevel.L3,
|
|
195
|
+
label: 'L3 — full autonomy',
|
|
196
|
+
hint: 'rare — supervised long-running agents only',
|
|
197
|
+
},
|
|
146
198
|
],
|
|
147
199
|
});
|
|
148
200
|
if (p.isCancel(autonomyPick))
|
|
@@ -164,7 +216,8 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, exi
|
|
|
164
216
|
return { value: lvl, label: lvl };
|
|
165
217
|
});
|
|
166
218
|
const maxPick = await p.select({
|
|
167
|
-
message: '
|
|
219
|
+
message: 'Ceiling autonomy level — the agent can never exceed this even if asked. ' +
|
|
220
|
+
'Promoting past the ceiling requires editing policy.yaml by hand.',
|
|
168
221
|
initialValue: defaultMax,
|
|
169
222
|
options: maxOptions,
|
|
170
223
|
});
|
|
@@ -172,31 +225,54 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, exi
|
|
|
172
225
|
cancel('Init cancelled.');
|
|
173
226
|
const maxAutonomyLevel = maxPick;
|
|
174
227
|
const attribPick = await p.confirm({
|
|
175
|
-
message: '
|
|
228
|
+
message: 'Block AI-attribution in commits? (rejects "Co-Authored-By: Claude" and similar ' +
|
|
229
|
+
'trailers — keeps your git history human-attributed)',
|
|
176
230
|
initialValue: layeredBase.block_ai_attribution ?? true,
|
|
177
231
|
});
|
|
178
232
|
if (p.isCancel(attribPick))
|
|
179
233
|
cancel('Init cancelled.');
|
|
180
234
|
const blockAiAttribution = attribPick === true;
|
|
181
|
-
// G11.4: "Use Codex adversarial review?" —
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
235
|
+
// G11.4: "Use Codex adversarial review?" — initial-value precedence:
|
|
236
|
+
// 1. explicit --codex / --no-codex flag wins
|
|
237
|
+
// 2. otherwise existing on-disk value (preserves operator edit on re-run)
|
|
238
|
+
// 3. otherwise profile default (`*-no-codex` profiles default to No)
|
|
239
|
+
//
|
|
240
|
+
// 0.43.0 codex round-1 P2: prior to this commit step 2 was skipped on
|
|
241
|
+
// the interactive path — the initial value collapsed to the profile
|
|
242
|
+
// default even on a re-run where the operator had already toggled
|
|
243
|
+
// codex off. The summary screen advertised codex_required as
|
|
244
|
+
// preserved while the prompt default silently reverted it. The
|
|
245
|
+
// `--yes` path already had the correct precedence (see the
|
|
246
|
+
// non-interactive branch in `runInit`); this brings the wizard in
|
|
247
|
+
// line.
|
|
248
|
+
const codexInitial = options.codex !== undefined
|
|
249
|
+
? options.codex
|
|
250
|
+
: (existingPolicy?.codexRequired ?? profileDefaultCodexRequired(profileName));
|
|
185
251
|
const codexPick = await p.confirm({
|
|
186
|
-
message: '
|
|
252
|
+
message: 'Enable Codex adversarial review? (runs a GPT-5.4 second-opinion review on every push; ' +
|
|
253
|
+
'requires the Codex CLI + an OpenAI account — can be installed later via /codex:setup)',
|
|
187
254
|
initialValue: codexInitial,
|
|
188
255
|
});
|
|
189
256
|
if (p.isCancel(codexPick))
|
|
190
257
|
cancel('Init cancelled.');
|
|
191
258
|
const codexRequired = codexPick === true;
|
|
192
|
-
p.outro('Config collected — installing files.');
|
|
193
259
|
return {
|
|
194
260
|
profile: profileName,
|
|
195
261
|
autonomyLevel,
|
|
196
262
|
maxAutonomyLevel,
|
|
197
263
|
blockAiAttribution,
|
|
198
|
-
|
|
199
|
-
|
|
264
|
+
// 0.43.0 codex round-1 P2: preserve the wizard-untouched fields
|
|
265
|
+
// (`blocked_paths` + `notification_channel`) the same way the
|
|
266
|
+
// `--yes` path already does. Pre-fix the wizard return rebuilt
|
|
267
|
+
// these from `layeredBase` on every interactive re-run, silently
|
|
268
|
+
// dropping operator edits even though the new install-summary
|
|
269
|
+
// confirm gate advertised them as preserved. The wizard does NOT
|
|
270
|
+
// prompt for either field (they are policy-file edits, not
|
|
271
|
+
// first-question UX), so falling back to the layered profile
|
|
272
|
+
// default is the correct seed shape on a fresh install; a re-run
|
|
273
|
+
// simply forwards whatever the operator committed to disk.
|
|
274
|
+
blockedPaths: existingPolicy?.blockedPaths ?? layeredBase.blocked_paths ?? ['.env', '.env.*'],
|
|
275
|
+
notificationChannel: existingPolicy?.notificationChannel ?? layeredBase.notification_channel ?? '',
|
|
200
276
|
codexRequired,
|
|
201
277
|
// Round-27 F6: the wizard does NOT prompt for the 0.26.0 knobs (they
|
|
202
278
|
// are advanced config — most teams accept defaults). But when the
|
|
@@ -737,6 +813,174 @@ function readExistingManifestInstalledAt(manifestPath) {
|
|
|
737
813
|
}
|
|
738
814
|
return undefined;
|
|
739
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* 0.43.0 UX polish: build the human-readable install summary shown
|
|
818
|
+
* BEFORE any files are written. Lists, in order: the policy file
|
|
819
|
+
* being written, the chosen profile + autonomy, hook + agent counts
|
|
820
|
+
* planned, the git/husky hooks planned (paths reflect what the
|
|
821
|
+
* installer will ACTUALLY do given the target tree's shape), and
|
|
822
|
+
* whether re-run preservation is active.
|
|
823
|
+
*
|
|
824
|
+
* Rendered via clack's `note` primitive so it sits in a bordered block
|
|
825
|
+
* adjacent to the final `confirm` gate. The string is also returned
|
|
826
|
+
* verbatim so the test suite can assert content without mocking clack.
|
|
827
|
+
*
|
|
828
|
+
* `targetState` is computed by {@link detectTargetState} — kept as a
|
|
829
|
+
* separate argument so tests can drive both shapes (husky-present and
|
|
830
|
+
* husky-absent) without touching the filesystem.
|
|
831
|
+
*/
|
|
832
|
+
export function buildInstallSummary(targetDir, config, reRunMode, targetState) {
|
|
833
|
+
const lines = [];
|
|
834
|
+
const mode = reRunMode ? 'Re-run (preserving your existing edits)' : 'Fresh install';
|
|
835
|
+
lines.push(`Mode: ${mode}`);
|
|
836
|
+
lines.push(`Target: ${targetDir}`);
|
|
837
|
+
lines.push('');
|
|
838
|
+
lines.push('Will write:');
|
|
839
|
+
lines.push(` .rea/policy.yaml — profile=${config.profile}`);
|
|
840
|
+
lines.push(` autonomy=${config.autonomyLevel} (max=${config.maxAutonomyLevel})`);
|
|
841
|
+
lines.push(` attribution-block=${config.blockAiAttribution ? 'on' : 'off'}`);
|
|
842
|
+
lines.push(` codex-review=${config.codexRequired ? 'on' : 'off'}`);
|
|
843
|
+
lines.push(` .rea/registry.yaml — empty MCP-server registry`);
|
|
844
|
+
lines.push(` .rea/install-manifest.json — hash record for drift detection`);
|
|
845
|
+
lines.push(` .claude/agents/ — curated specialist agents`);
|
|
846
|
+
lines.push(` .claude/hooks/ — hook scripts (executable)`);
|
|
847
|
+
lines.push(` .claude/commands/ — slash commands`);
|
|
848
|
+
lines.push(` .claude/settings.json — hook registration entries`);
|
|
849
|
+
// 0.43.0 codex round-1 P3: the installer writes to `.git/hooks/*`
|
|
850
|
+
// ALWAYS when a git repo is present, AND to `.husky/*` ONLY when
|
|
851
|
+
// `.husky/` already exists. Pre-fix the summary hard-coded `.husky/*`
|
|
852
|
+
// and silently omitted the `.git/hooks/*` writes from the "no
|
|
853
|
+
// .husky/" install shape — the most common one. The operator then
|
|
854
|
+
// confirmed without being told their `.git/hooks/` would be
|
|
855
|
+
// modified. We list both surfaces conditionally based on the
|
|
856
|
+
// detected target state so the screen is faithful to what actually
|
|
857
|
+
// happens.
|
|
858
|
+
if (targetState.gitRepoPresent) {
|
|
859
|
+
lines.push(` .git/hooks/commit-msg — commit-message attribution gate`);
|
|
860
|
+
lines.push(` .git/hooks/prepare-commit-msg — attribution augmenter (no-op until enabled)`);
|
|
861
|
+
lines.push(` .git/hooks/pre-push — local-review gate (fallback if no active hook present)`);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
lines.push(` (no .git/ directory detected — git hook copies will be skipped)`);
|
|
865
|
+
}
|
|
866
|
+
if (targetState.huskyDirPresent) {
|
|
867
|
+
lines.push(` .husky/commit-msg — commit-message attribution gate (husky mirror)`);
|
|
868
|
+
lines.push(` .husky/prepare-commit-msg — attribution augmenter (husky mirror)`);
|
|
869
|
+
lines.push(` .husky/pre-push — local-review gate (husky mirror)`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
lines.push(` (no .husky/ directory detected — husky mirrors will be skipped)`);
|
|
873
|
+
}
|
|
874
|
+
lines.push(` CLAUDE.md fragment — managed governance block`);
|
|
875
|
+
lines.push(` .gitignore — managed entries for .rea runtime artifacts`);
|
|
876
|
+
if (reRunMode) {
|
|
877
|
+
lines.push('');
|
|
878
|
+
// 0.43.0 codex round-1 P2: list only the fields the wizard
|
|
879
|
+
// ACTUALLY preserves. `blocked_paths`, `notification_channel`,
|
|
880
|
+
// and `review.codex_required` are now preserved by the wizard
|
|
881
|
+
// path (matching the `--yes` path's documented contract); the
|
|
882
|
+
// wizard does NOT prompt for the 0.26.0 local_review or
|
|
883
|
+
// commit_hygiene knobs, but those values forward verbatim from
|
|
884
|
+
// the existing policy when set.
|
|
885
|
+
lines.push('Re-run preserves your manually-edited:');
|
|
886
|
+
lines.push(' • autonomy_level / max_autonomy_level / block_ai_attribution');
|
|
887
|
+
lines.push(' • blocked_paths / notification_channel');
|
|
888
|
+
lines.push(' • review.codex_required + local_review.* + commit_hygiene.*');
|
|
889
|
+
lines.push(' • attribution.co_author.* + installed_at timestamp');
|
|
890
|
+
}
|
|
891
|
+
return lines.join('\n');
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* 0.43.0 codex round-1 P3: detect which hook surfaces the installer
|
|
895
|
+
* will actually touch. Returns a snapshot so the install-summary
|
|
896
|
+
* confirm screen can show the right paths.
|
|
897
|
+
*
|
|
898
|
+
* Intentionally simple — the installers themselves (commit-msg,
|
|
899
|
+
* prepare-commit-msg, pre-push) each re-check at write time, so this
|
|
900
|
+
* detection is purely presentational. If something races between the
|
|
901
|
+
* snapshot and the writes (a `pnpm install` adding `.husky/` in the
|
|
902
|
+
* window between confirm and spinner), the installer's own checks win
|
|
903
|
+
* and the summary was only slightly stale.
|
|
904
|
+
*/
|
|
905
|
+
export function detectTargetState(targetDir) {
|
|
906
|
+
return {
|
|
907
|
+
gitRepoPresent: fs.existsSync(path.join(targetDir, '.git')),
|
|
908
|
+
huskyDirPresent: fs.existsSync(path.join(targetDir, '.husky')),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* 0.43.0 UX polish: post-install sanity check. Runs synchronously
|
|
913
|
+
* after the file-write phase to catch installs that completed
|
|
914
|
+
* "successfully" but are missing a critical artifact (write
|
|
915
|
+
* permissions issue, partial copy, etc.).
|
|
916
|
+
*
|
|
917
|
+
* Strictly read-only — no probes that touch python3 / jq / codex.
|
|
918
|
+
* Pattern modelled on the synthetic round-trip checks established by
|
|
919
|
+
* `checkDelegationRoundTrip` in 0.29.0/0.31.0: cheap, in-process,
|
|
920
|
+
* sufficient to catch the "looks-installed-but-isn't" failure shape
|
|
921
|
+
* that bites first-time consumers hardest. For deep diagnostics
|
|
922
|
+
* point the operator at `rea doctor`.
|
|
923
|
+
*
|
|
924
|
+
* Returns the list of issues found (empty = healthy). The caller
|
|
925
|
+
* surfaces them via clack's `log.warn` and points the operator at
|
|
926
|
+
* `rea doctor` for follow-up.
|
|
927
|
+
*/
|
|
928
|
+
export function postInstallVerify(targetDir) {
|
|
929
|
+
const issues = [];
|
|
930
|
+
// 1. policy file exists + parses as YAML object.
|
|
931
|
+
const policyPath = path.join(targetDir, REA_DIR, POLICY_FILE);
|
|
932
|
+
if (!fs.existsSync(policyPath)) {
|
|
933
|
+
issues.push(`.rea/policy.yaml missing after install (expected at ${policyPath})`);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
try {
|
|
937
|
+
const raw = fs.readFileSync(policyPath, 'utf8');
|
|
938
|
+
const parsed = parseYaml(raw);
|
|
939
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
940
|
+
issues.push('.rea/policy.yaml parsed to a non-object — run `rea doctor` for details');
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch (e) {
|
|
944
|
+
issues.push(`.rea/policy.yaml failed to parse: ${e instanceof Error ? e.message : String(e)} — ` +
|
|
945
|
+
'run `rea doctor` for details');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// 2. .claude/hooks directory present with executable scripts.
|
|
949
|
+
const hooksDir = path.join(targetDir, '.claude', 'hooks');
|
|
950
|
+
if (!fs.existsSync(hooksDir)) {
|
|
951
|
+
issues.push(`.claude/hooks/ directory missing after install (expected at ${hooksDir})`);
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
let executableCount = 0;
|
|
955
|
+
try {
|
|
956
|
+
for (const entry of fs.readdirSync(hooksDir)) {
|
|
957
|
+
if (!entry.endsWith('.sh'))
|
|
958
|
+
continue;
|
|
959
|
+
const stat = fs.statSync(path.join(hooksDir, entry));
|
|
960
|
+
if ((stat.mode & 0o111) !== 0)
|
|
961
|
+
executableCount += 1;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
catch (e) {
|
|
965
|
+
issues.push(`failed to enumerate .claude/hooks/: ${e instanceof Error ? e.message : String(e)}`);
|
|
966
|
+
}
|
|
967
|
+
if (executableCount === 0) {
|
|
968
|
+
issues.push('.claude/hooks/ contains zero executable .sh files — run `rea doctor`');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// 3. settings.json present.
|
|
972
|
+
const settingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
973
|
+
if (!fs.existsSync(settingsPath)) {
|
|
974
|
+
issues.push(`.claude/settings.json missing after install (expected at ${settingsPath})`);
|
|
975
|
+
}
|
|
976
|
+
// 4. install manifest present.
|
|
977
|
+
const manifestPath = path.join(targetDir, REA_DIR, 'install-manifest.json');
|
|
978
|
+
if (!fs.existsSync(manifestPath)) {
|
|
979
|
+
issues.push(`.rea/install-manifest.json missing after install (expected at ${manifestPath}) — ` +
|
|
980
|
+
'drift detection will not work until `rea init` is re-run');
|
|
981
|
+
}
|
|
982
|
+
return issues;
|
|
983
|
+
}
|
|
740
984
|
export async function runInit(options) {
|
|
741
985
|
const targetDir = process.cwd();
|
|
742
986
|
const reagentPolicyPath = detectReagentPolicy(targetDir);
|
|
@@ -860,46 +1104,105 @@ export async function runInit(options) {
|
|
|
860
1104
|
config = await runWizard(options, targetDir, reagentPolicyPath, layeredBase, existingPolicy);
|
|
861
1105
|
config.reagentNotices = reagentNotices;
|
|
862
1106
|
}
|
|
1107
|
+
// 0.43.0 UX polish: install summary + final confirm gate. The
|
|
1108
|
+
// operator sees exactly what's about to happen BEFORE any
|
|
1109
|
+
// filesystem writes. Skipped on `--yes` / `--force` (non-interactive
|
|
1110
|
+
// paths assume consent). The confirm step is the last chance to
|
|
1111
|
+
// bail without leaving partial state on disk.
|
|
1112
|
+
const reRunMode = existingPolicy !== undefined;
|
|
1113
|
+
const interactive = options.yes !== true;
|
|
1114
|
+
if (interactive) {
|
|
1115
|
+
const targetState = detectTargetState(targetDir);
|
|
1116
|
+
const summary = buildInstallSummary(targetDir, config, reRunMode, targetState);
|
|
1117
|
+
p.note(summary, reRunMode ? 'Ready to refresh' : 'Ready to install');
|
|
1118
|
+
const proceed = await p.confirm({
|
|
1119
|
+
message: reRunMode ? 'Proceed with the refresh?' : 'Proceed with the install?',
|
|
1120
|
+
initialValue: true,
|
|
1121
|
+
});
|
|
1122
|
+
if (p.isCancel(proceed) || proceed !== true) {
|
|
1123
|
+
cancel('Init cancelled — no files written.');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
863
1126
|
if (!fs.existsSync(reaDir))
|
|
864
1127
|
fs.mkdirSync(reaDir, { recursive: true });
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1128
|
+
// 0.43.0 UX polish: wrap the file-write phase in a clack spinner so
|
|
1129
|
+
// operators on slow disks see progress instead of staring at a
|
|
1130
|
+
// motionless prompt. Skipped under `--yes` (non-interactive paths
|
|
1131
|
+
// log line-by-line). All operations remain identical — the spinner
|
|
1132
|
+
// is purely presentational.
|
|
1133
|
+
const spinner = interactive ? p.spinner() : null;
|
|
1134
|
+
if (spinner !== null)
|
|
1135
|
+
spinner.start('Writing rea install');
|
|
1136
|
+
let written;
|
|
1137
|
+
let copyResult;
|
|
1138
|
+
let mergeResult;
|
|
1139
|
+
let commitMsgResult;
|
|
1140
|
+
let prepareCommitMsgResult;
|
|
1141
|
+
let prePushResult;
|
|
1142
|
+
let mdResult;
|
|
1143
|
+
let gitignoreResult;
|
|
1144
|
+
let manifestPath;
|
|
1145
|
+
let fragmentInput;
|
|
1146
|
+
try {
|
|
1147
|
+
written = [];
|
|
1148
|
+
written.push(writePolicyYaml(targetDir, config, layeredBase));
|
|
1149
|
+
written.push(writeRegistryYaml(targetDir));
|
|
1150
|
+
// Artifact copies + settings merge + commit-msg + CLAUDE.md fragment.
|
|
1151
|
+
const copyOptions = {
|
|
1152
|
+
force: options.force === true,
|
|
1153
|
+
yes: options.yes === true || options.force === true,
|
|
1154
|
+
};
|
|
1155
|
+
copyResult = await copyArtifacts(targetDir, copyOptions);
|
|
1156
|
+
const { settings, settingsPath } = readSettings(targetDir);
|
|
1157
|
+
const desired = defaultDesiredHooks();
|
|
1158
|
+
mergeResult = mergeSettings(settings, desired);
|
|
1159
|
+
await writeSettingsAtomic(settingsPath, mergeResult.merged);
|
|
1160
|
+
commitMsgResult = await installCommitMsgHook(targetDir);
|
|
1161
|
+
// 0.30.0 attribution augmenter — install the prepare-commit-msg
|
|
1162
|
+
// hook unconditionally. The hook is a no-op when
|
|
1163
|
+
// policy.attribution.co_author.enabled !== true, so it is safe to
|
|
1164
|
+
// ship under every profile; consumers opt in by editing their
|
|
1165
|
+
// .rea/policy.yaml.
|
|
1166
|
+
prepareCommitMsgResult = await installPrepareCommitMsgHook(targetDir);
|
|
1167
|
+
prePushResult = await installPrePushFallback({ targetDir });
|
|
1168
|
+
fragmentInput = {
|
|
1169
|
+
policyPath: `.${path.sep}rea${path.sep}policy.yaml`.replace(/\\/g, '/'),
|
|
1170
|
+
profile: config.profile,
|
|
1171
|
+
autonomyLevel: config.autonomyLevel,
|
|
1172
|
+
maxAutonomyLevel: config.maxAutonomyLevel,
|
|
1173
|
+
blockedPathsCount: config.blockedPaths.length,
|
|
1174
|
+
blockAiAttribution: config.blockAiAttribution,
|
|
1175
|
+
};
|
|
1176
|
+
mdResult = await writeClaudeMdFragment(targetDir, fragmentInput);
|
|
1177
|
+
// BUG-010 — scaffold `.gitignore` entries for every runtime artifact
|
|
1178
|
+
// `rea serve` / `rea cache` / `/freeze` can write under `.rea/`. Idempotent
|
|
1179
|
+
// append (and `rea upgrade` backfills older installs that never got this).
|
|
1180
|
+
gitignoreResult = await ensureReaGitignore(targetDir);
|
|
1181
|
+
// G12 — record the install manifest. SHAs are of the files actually on disk
|
|
1182
|
+
// after the copy pass, so drift detection compares against real state (not
|
|
1183
|
+
// canonical, which may differ if the consumer's copy was aborted mid-run).
|
|
1184
|
+
manifestPath = await writeInstallManifest(targetDir, config.profile, fragmentInput);
|
|
1185
|
+
}
|
|
1186
|
+
catch (e) {
|
|
1187
|
+
// 0.43.0 UX polish: surface install failures via the spinner's
|
|
1188
|
+
// error state when interactive, then re-throw with a clack-rendered
|
|
1189
|
+
// "what failed → suggested fix" envelope so the operator isn't
|
|
1190
|
+
// left staring at a raw stack trace.
|
|
1191
|
+
if (spinner !== null)
|
|
1192
|
+
spinner.stop('Install failed');
|
|
1193
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1194
|
+
// Pattern: <what failed>: <why> → <suggested fix>. Many of the
|
|
1195
|
+
// underlying installers throw with the why already in `message`;
|
|
1196
|
+
// we always append the actionable next step so the operator
|
|
1197
|
+
// knows where to look.
|
|
1198
|
+
p.log.error(`Install aborted: ${message}\n` +
|
|
1199
|
+
` Suggested fix: re-run with --force to reset, or run \`rea doctor\` to ` +
|
|
1200
|
+
`diagnose the partial state, or escalate via \`rea freeze\` if a hook is ` +
|
|
1201
|
+
`actively blocking the operator's work.`);
|
|
1202
|
+
throw e;
|
|
1203
|
+
}
|
|
1204
|
+
if (spinner !== null)
|
|
1205
|
+
spinner.stop('Install written');
|
|
903
1206
|
console.log('');
|
|
904
1207
|
log('init complete');
|
|
905
1208
|
for (const file of written)
|
|
@@ -971,17 +1274,59 @@ export async function runInit(options) {
|
|
|
971
1274
|
console.log('Codex review disabled. ClaudeSelfReviewer will be used.');
|
|
972
1275
|
console.log(' Set review.codex_required: true in .rea/policy.yaml to re-enable.');
|
|
973
1276
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1277
|
+
// 0.43.0 UX polish: inline post-install verification. NOT a full
|
|
1278
|
+
// `rea doctor` (that takes seconds and spawns subprocesses) — just
|
|
1279
|
+
// a synchronous in-process sanity check that the install is sane.
|
|
1280
|
+
// If anything looks off we surface a loud warning and direct the
|
|
1281
|
+
// operator at `rea doctor` for the deep dive. Modelled on the
|
|
1282
|
+
// 0.29.0/0.31.0 synthetic round-trip pattern.
|
|
1283
|
+
const verifyIssues = postInstallVerify(targetDir);
|
|
1284
|
+
if (verifyIssues.length > 0) {
|
|
1285
|
+
console.log('');
|
|
1286
|
+
warn('post-install verification flagged the following:');
|
|
1287
|
+
for (const issue of verifyIssues)
|
|
1288
|
+
warn(` • ${issue}`);
|
|
1289
|
+
warn('Run `rea doctor` for a full diagnostic.');
|
|
1290
|
+
}
|
|
1291
|
+
else if (interactive) {
|
|
1292
|
+
// Quiet success — confirm we checked, but don't shout about it.
|
|
1293
|
+
p.log.success('Post-install check: install looks healthy.');
|
|
1294
|
+
}
|
|
1295
|
+
if (interactive) {
|
|
1296
|
+
// 0.43.0 UX polish: clack outro with structured next-steps.
|
|
1297
|
+
// Replaces the bare `console.log('Next steps:')` block with a
|
|
1298
|
+
// bordered note so the call-to-action is unmissable on a busy
|
|
1299
|
+
// terminal scrollback. The non-interactive path keeps the plain
|
|
1300
|
+
// console.log block (CI logs don't render clack borders).
|
|
1301
|
+
const nextSteps = [];
|
|
1302
|
+
nextSteps.push('1. Review .rea/policy.yaml and commit it.');
|
|
1303
|
+
nextSteps.push('2. Run `rea doctor` to validate the install end-to-end.');
|
|
1304
|
+
nextSteps.push('3. Run `rea check` to see current status (autonomy, HALT, recent audit).');
|
|
1305
|
+
if (config.fromReagent) {
|
|
1306
|
+
nextSteps.push('');
|
|
1307
|
+
nextSteps.push('Reagent migration:');
|
|
1308
|
+
nextSteps.push(` Source: ${config.reagentPolicyPath ?? '(none)'}`);
|
|
1309
|
+
nextSteps.push(' Copied fields were applied per the translator rules.');
|
|
1310
|
+
nextSteps.push(' Once satisfied, you can remove the .reagent/ directory.');
|
|
1311
|
+
}
|
|
1312
|
+
nextSteps.push('');
|
|
1313
|
+
nextSteps.push('Docs: https://github.com/bookedsolidtech/rea#readme');
|
|
1314
|
+
p.note(nextSteps.join('\n'), 'Next steps');
|
|
1315
|
+
p.outro(reRunMode ? 'rea refresh complete.' : 'rea install complete.');
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
console.log('');
|
|
1319
|
+
console.log('Next steps:');
|
|
1320
|
+
console.log(' 1. Review .rea/policy.yaml and commit it.');
|
|
1321
|
+
console.log(' 2. Run `rea doctor` to validate the install.');
|
|
1322
|
+
console.log(' 3. Run `rea check` to see current status.');
|
|
1323
|
+
if (config.fromReagent) {
|
|
1324
|
+
console.log('');
|
|
1325
|
+
console.log('Reagent migration:');
|
|
1326
|
+
console.log(` Source: ${config.reagentPolicyPath ?? '(none)'}`);
|
|
1327
|
+
console.log(' Copied fields were applied per the translator rules.');
|
|
1328
|
+
console.log(' Once satisfied, you can remove the .reagent/ directory.');
|
|
1329
|
+
}
|
|
980
1330
|
console.log('');
|
|
981
|
-
console.log('Reagent migration:');
|
|
982
|
-
console.log(` Source: ${config.reagentPolicyPath ?? '(none)'}`);
|
|
983
|
-
console.log(' Copied fields were applied per the translator rules.');
|
|
984
|
-
console.log(' Once satisfied, you can remove the .reagent/ directory.');
|
|
985
1331
|
}
|
|
986
|
-
console.log('');
|
|
987
1332
|
}
|
|
@@ -97,6 +97,24 @@ export interface UpgradeCheckFile {
|
|
|
97
97
|
* row (e.g. "removed-upstream — kept by default unless --force"). */
|
|
98
98
|
note?: string;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 0.42.0 charter item 2 — surface the same settings-schema validation
|
|
102
|
+
* the real upgrade flow runs. `runUpgrade` calls `validateSettings`
|
|
103
|
+
* on the merged result and refuses the write (throws) when it fails;
|
|
104
|
+
* pre-0.42.0 `rea upgrade --check` never invoked that check, so a
|
|
105
|
+
* preview could promise a write that the real upgrade would refuse.
|
|
106
|
+
*
|
|
107
|
+
* - `parsed: true` — schema validation succeeded; `errors` is empty.
|
|
108
|
+
* The real upgrade WOULD write the merged settings on demand.
|
|
109
|
+
* - `parsed: false` — schema validation failed. `errors` carries the
|
|
110
|
+
* same zod-issue strings `runUpgrade` would surface in its throw
|
|
111
|
+
* message. The real upgrade would refuse and leave settings.json
|
|
112
|
+
* untouched.
|
|
113
|
+
*/
|
|
114
|
+
export interface UpgradeCheckSettingsValidation {
|
|
115
|
+
parsed: boolean;
|
|
116
|
+
errors: string[];
|
|
117
|
+
}
|
|
100
118
|
export interface UpgradeCheckPlan {
|
|
101
119
|
schema_version: typeof UPGRADE_CHECK_SCHEMA_VERSION;
|
|
102
120
|
rea_version: string;
|
|
@@ -112,6 +130,26 @@ export interface UpgradeCheckPlan {
|
|
|
112
130
|
removed_upstream: number;
|
|
113
131
|
};
|
|
114
132
|
files: UpgradeCheckFile[];
|
|
133
|
+
/** 0.42.0 — schema-validation outcome on the merged settings.json
|
|
134
|
+
* the real `rea upgrade` would write. `null` when the synthetic
|
|
135
|
+
* settings classification did not produce a merged result (should
|
|
136
|
+
* not happen in practice; defensive). */
|
|
137
|
+
settings_validation: UpgradeCheckSettingsValidation | null;
|
|
138
|
+
/**
|
|
139
|
+
* 0.42.0 codex round 3 P2 (2026-05-16) — top-level "preview = real"
|
|
140
|
+
* verdict. `true` when `rea upgrade` would actually start mutating
|
|
141
|
+
* the install; `false` when the new pre-flight (settings-validation)
|
|
142
|
+
* gate would refuse the upgrade before any file is written.
|
|
143
|
+
*
|
|
144
|
+
* Why this matters: `counts` + `files` still describe what WOULD be
|
|
145
|
+
* written if validation passed (operators want the diff so they can
|
|
146
|
+
* fix the underlying invalid setting and see the upgrade preview in
|
|
147
|
+
* one shot). But CI and automation consuming the JSON need a single
|
|
148
|
+
* unambiguous signal that the real upgrade will write nothing in
|
|
149
|
+
* the current state. Use `would_apply` as that signal; treat
|
|
150
|
+
* `files[]` + `counts` as conditional on `would_apply === true`.
|
|
151
|
+
*/
|
|
152
|
+
would_apply: boolean;
|
|
115
153
|
}
|
|
116
154
|
export interface ComputeUpgradeCheckOptions {
|
|
117
155
|
/** Defaults to `process.cwd()`. */
|