@bookedsolid/rea 0.1.0 → 0.2.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/.husky/commit-msg +130 -0
- package/.husky/pre-push +128 -0
- package/README.md +5 -5
- package/agents/codex-adversarial.md +23 -8
- package/commands/codex-review.md +2 -2
- package/dist/audit/append.d.ts +62 -0
- package/dist/audit/append.js +189 -0
- package/dist/audit/codex-event.d.ts +28 -0
- package/dist/audit/codex-event.js +15 -0
- package/dist/cli/doctor.d.ts +60 -1
- package/dist/cli/doctor.js +459 -20
- package/dist/cli/index.js +35 -5
- package/dist/cli/init.d.ts +13 -0
- package/dist/cli/init.js +278 -67
- package/dist/cli/install/canonical.d.ts +43 -0
- package/dist/cli/install/canonical.js +101 -0
- package/dist/cli/install/claude-md.d.ts +48 -0
- package/dist/cli/install/claude-md.js +93 -0
- package/dist/cli/install/commit-msg.d.ts +30 -0
- package/dist/cli/install/commit-msg.js +102 -0
- package/dist/cli/install/copy.d.ts +169 -0
- package/dist/cli/install/copy.js +455 -0
- package/dist/cli/install/fs-safe.d.ts +91 -0
- package/dist/cli/install/fs-safe.js +347 -0
- package/dist/cli/install/manifest-io.d.ts +12 -0
- package/dist/cli/install/manifest-io.js +44 -0
- package/dist/cli/install/manifest-schema.d.ts +83 -0
- package/dist/cli/install/manifest-schema.js +80 -0
- package/dist/cli/install/reagent.d.ts +59 -0
- package/dist/cli/install/reagent.js +160 -0
- package/dist/cli/install/settings-merge.d.ts +91 -0
- package/dist/cli/install/settings-merge.js +239 -0
- package/dist/cli/install/sha.d.ts +9 -0
- package/dist/cli/install/sha.js +21 -0
- package/dist/cli/serve.d.ts +11 -0
- package/dist/cli/serve.js +72 -6
- package/dist/cli/upgrade.d.ts +67 -0
- package/dist/cli/upgrade.js +509 -0
- package/dist/gateway/downstream-pool.d.ts +39 -0
- package/dist/gateway/downstream-pool.js +93 -0
- package/dist/gateway/downstream.d.ts +80 -0
- package/dist/gateway/downstream.js +196 -0
- package/dist/gateway/middleware/audit-types.d.ts +10 -0
- package/dist/gateway/middleware/audit.js +14 -0
- package/dist/gateway/middleware/injection.d.ts +59 -2
- package/dist/gateway/middleware/injection.js +91 -14
- package/dist/gateway/middleware/kill-switch.d.ts +20 -5
- package/dist/gateway/middleware/kill-switch.js +57 -35
- package/dist/gateway/middleware/redact.d.ts +83 -6
- package/dist/gateway/middleware/redact.js +133 -46
- package/dist/gateway/observability/codex-probe.d.ts +110 -0
- package/dist/gateway/observability/codex-probe.js +234 -0
- package/dist/gateway/observability/codex-telemetry.d.ts +93 -0
- package/dist/gateway/observability/codex-telemetry.js +221 -0
- package/dist/gateway/redact-safe/match-timeout.d.ts +83 -0
- package/dist/gateway/redact-safe/match-timeout.js +179 -0
- package/dist/gateway/reviewers/claude-self.d.ts +99 -0
- package/dist/gateway/reviewers/claude-self.js +316 -0
- package/dist/gateway/reviewers/codex.d.ts +64 -0
- package/dist/gateway/reviewers/codex.js +80 -0
- package/dist/gateway/reviewers/select.d.ts +64 -0
- package/dist/gateway/reviewers/select.js +102 -0
- package/dist/gateway/reviewers/types.d.ts +85 -0
- package/dist/gateway/reviewers/types.js +14 -0
- package/dist/gateway/server.d.ts +51 -0
- package/dist/gateway/server.js +258 -0
- package/dist/gateway/session.d.ts +9 -0
- package/dist/gateway/session.js +17 -0
- package/dist/policy/loader.d.ts +59 -0
- package/dist/policy/loader.js +65 -0
- package/dist/policy/profiles.d.ts +80 -0
- package/dist/policy/profiles.js +94 -0
- package/dist/policy/types.d.ts +38 -0
- package/dist/registry/loader.d.ts +98 -0
- package/dist/registry/loader.js +153 -0
- package/dist/registry/types.d.ts +44 -0
- package/dist/registry/types.js +6 -0
- package/dist/scripts/read-policy-field.d.ts +36 -0
- package/dist/scripts/read-policy-field.js +96 -0
- package/hooks/push-review-gate.sh +627 -17
- package/package.json +13 -2
- package/profiles/bst-internal-no-codex.yaml +40 -0
- package/profiles/bst-internal.yaml +23 -0
- package/profiles/client-engagement.yaml +23 -0
- package/profiles/lit-wc.yaml +17 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +33 -0
- package/profiles/open-source.yaml +18 -0
- package/scripts/lint-safe-regex.mjs +78 -0
- package/scripts/postinstall.mjs +131 -0
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -1 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
import { type CodexProbeState } from '../gateway/observability/codex-probe.js';
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
label: string;
|
|
4
|
+
/**
|
|
5
|
+
* `info` is purely informational — not a pass, fail, or warning. Used to
|
|
6
|
+
* print a one-line note about why a check was skipped (e.g. "codex:
|
|
7
|
+
* disabled via policy.review.codex_required").
|
|
8
|
+
*/
|
|
9
|
+
status: 'pass' | 'fail' | 'warn' | 'info';
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
14
|
+
* responsiveness (pass/warn) and one informational line about the last
|
|
15
|
+
* probe time. Extracted so tests can feed a stub state without running
|
|
16
|
+
* the real probe.
|
|
17
|
+
*/
|
|
18
|
+
export declare function checksFromProbeState(state: CodexProbeState): CheckResult[];
|
|
19
|
+
/**
|
|
20
|
+
* Assemble the full checklist for a given baseDir. Exported so tests can
|
|
21
|
+
* exercise the conditional branching without capturing stdout from
|
|
22
|
+
* `runDoctor`.
|
|
23
|
+
*
|
|
24
|
+
* `codexProbeState` is consulted ONLY when Codex is required by policy.
|
|
25
|
+
* Callers that already have a fresh probe state (e.g. `runDoctor`) should
|
|
26
|
+
* pass it; callers that don't (e.g. unit tests of the existing doctor
|
|
27
|
+
* surface) can omit it and the probe-derived fields are skipped.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState): CheckResult[];
|
|
30
|
+
export interface RunDoctorOptions {
|
|
31
|
+
/** When true, print a 7-day telemetry summary after the checks (G11.5). */
|
|
32
|
+
metrics?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* G12 — when true, append a read-only drift report comparing on-disk SHAs
|
|
35
|
+
* to the install manifest + canonical sources. Never mutates; never fails
|
|
36
|
+
* the exit code on its own (drift is information, not a hard error — use
|
|
37
|
+
* `rea upgrade` to reconcile).
|
|
38
|
+
*/
|
|
39
|
+
drift?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface DriftRow {
|
|
42
|
+
path: string;
|
|
43
|
+
status: 'unmodified' | 'drifted-from-canonical' | 'drifted-from-manifest' | 'missing' | 'untracked' | 'removed-upstream';
|
|
44
|
+
detail?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface DriftReport {
|
|
47
|
+
hasManifest: boolean;
|
|
48
|
+
bootstrap: boolean;
|
|
49
|
+
manifestVersion: string | null;
|
|
50
|
+
reaVersion: string;
|
|
51
|
+
rows: DriftRow[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compute drift between the install manifest and the current state of the
|
|
55
|
+
* consumer's tree. Strictly read-only — no file writes. Synthetic entries
|
|
56
|
+
* (settings subset, managed CLAUDE.md fragment) are checked by hashing the
|
|
57
|
+
* computed values against the manifest SHA.
|
|
58
|
+
*/
|
|
59
|
+
export declare function collectDriftReport(baseDir: string): Promise<DriftReport>;
|
|
60
|
+
export declare function runDoctor(opts?: RunDoctorOptions): Promise<void>;
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadPolicy } from '../policy/loader.js';
|
|
4
|
-
import {
|
|
4
|
+
import { loadRegistry } from '../registry/loader.js';
|
|
5
|
+
import { CodexProbe, } from '../gateway/observability/codex-probe.js';
|
|
6
|
+
import { summarizeTelemetry } from '../gateway/observability/codex-telemetry.js';
|
|
7
|
+
import { CLAUDE_MD_MANIFEST_PATH, SETTINGS_MANIFEST_PATH, enumerateCanonicalFiles, } from './install/canonical.js';
|
|
8
|
+
import { buildFragment } from './install/claude-md.js';
|
|
9
|
+
import { canonicalSettingsSubsetHash, defaultDesiredHooks, } from './install/settings-merge.js';
|
|
10
|
+
import { manifestExists, readManifest } from './install/manifest-io.js';
|
|
11
|
+
import { sha256OfBuffer, sha256OfFile } from './install/sha.js';
|
|
12
|
+
import { POLICY_FILE, REA_DIR, REGISTRY_FILE, getPkgVersion, log, reaPath } from './utils.js';
|
|
5
13
|
function checkFileExists(label, filePath, fatal) {
|
|
6
14
|
const exists = fs.existsSync(filePath);
|
|
7
15
|
if (exists)
|
|
@@ -32,46 +40,453 @@ function checkPolicyParses(baseDir, policyPath) {
|
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
if (!fs.existsSync(commandsDir)) {
|
|
43
|
+
function checkRegistryParses(baseDir, registryPath) {
|
|
44
|
+
if (!fs.existsSync(registryPath)) {
|
|
38
45
|
return {
|
|
39
|
-
label: '
|
|
46
|
+
label: 'registry.yaml parses',
|
|
40
47
|
status: 'warn',
|
|
41
|
-
detail:
|
|
48
|
+
detail: `missing: ${registryPath}`,
|
|
42
49
|
};
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
try {
|
|
52
|
+
const registry = loadRegistry(baseDir);
|
|
53
|
+
return {
|
|
54
|
+
label: 'registry.yaml parses',
|
|
55
|
+
status: 'pass',
|
|
56
|
+
detail: `${registry.servers.length} server(s) declared`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return {
|
|
61
|
+
label: 'registry.yaml parses',
|
|
62
|
+
status: 'fail',
|
|
63
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const EXPECTED_AGENTS = [
|
|
68
|
+
'accessibility-engineer.md',
|
|
69
|
+
'backend-engineer.md',
|
|
70
|
+
'code-reviewer.md',
|
|
71
|
+
'codex-adversarial.md',
|
|
72
|
+
'frontend-specialist.md',
|
|
73
|
+
'qa-engineer.md',
|
|
74
|
+
'rea-orchestrator.md',
|
|
75
|
+
'security-engineer.md',
|
|
76
|
+
'technical-writer.md',
|
|
77
|
+
'typescript-specialist.md',
|
|
78
|
+
];
|
|
79
|
+
const EXPECTED_HOOKS = [
|
|
80
|
+
'architecture-review-gate.sh',
|
|
81
|
+
'attribution-advisory.sh',
|
|
82
|
+
'blocked-paths-enforcer.sh',
|
|
83
|
+
'changeset-security-gate.sh',
|
|
84
|
+
'commit-review-gate.sh',
|
|
85
|
+
'dangerous-bash-interceptor.sh',
|
|
86
|
+
'dependency-audit-gate.sh',
|
|
87
|
+
'env-file-protection.sh',
|
|
88
|
+
'pr-issue-link-gate.sh',
|
|
89
|
+
'push-review-gate.sh',
|
|
90
|
+
'secret-scanner.sh',
|
|
91
|
+
'security-disclosure-gate.sh',
|
|
92
|
+
'settings-protection.sh',
|
|
93
|
+
];
|
|
94
|
+
function checkAgentsPresent(baseDir) {
|
|
95
|
+
const agentsDir = path.join(baseDir, '.claude', 'agents');
|
|
96
|
+
if (!fs.existsSync(agentsDir)) {
|
|
97
|
+
return { label: 'curated agents installed', status: 'fail', detail: `missing: ${agentsDir}` };
|
|
98
|
+
}
|
|
99
|
+
const missing = EXPECTED_AGENTS.filter((name) => !fs.existsSync(path.join(agentsDir, name)));
|
|
100
|
+
if (missing.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
label: 'curated agents installed',
|
|
103
|
+
status: 'pass',
|
|
104
|
+
detail: `${EXPECTED_AGENTS.length} agents present`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
label: 'curated agents installed',
|
|
109
|
+
status: 'fail',
|
|
110
|
+
detail: `missing: ${missing.join(', ')}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function checkHooksInstalled(baseDir) {
|
|
114
|
+
const hooksDir = path.join(baseDir, '.claude', 'hooks');
|
|
115
|
+
if (!fs.existsSync(hooksDir)) {
|
|
116
|
+
return { label: 'hooks installed + executable', status: 'fail', detail: `missing: ${hooksDir}` };
|
|
117
|
+
}
|
|
118
|
+
const issues = [];
|
|
119
|
+
for (const name of EXPECTED_HOOKS) {
|
|
120
|
+
const p = path.join(hooksDir, name);
|
|
121
|
+
if (!fs.existsSync(p)) {
|
|
122
|
+
issues.push(`missing ${name}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const stat = fs.statSync(p);
|
|
126
|
+
if ((stat.mode & 0o111) === 0)
|
|
127
|
+
issues.push(`${name} not executable (mode=${(stat.mode & 0o777).toString(8)})`);
|
|
128
|
+
}
|
|
129
|
+
if (issues.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
label: 'hooks installed + executable',
|
|
132
|
+
status: 'pass',
|
|
133
|
+
detail: `${EXPECTED_HOOKS.length} hooks present`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { label: 'hooks installed + executable', status: 'fail', detail: issues.join('; ') };
|
|
137
|
+
}
|
|
138
|
+
function checkSettingsJson(baseDir) {
|
|
139
|
+
const settingsPath = path.join(baseDir, '.claude', 'settings.json');
|
|
140
|
+
if (!fs.existsSync(settingsPath)) {
|
|
141
|
+
return {
|
|
142
|
+
label: 'settings.json matchers cover Bash + Write|Edit',
|
|
143
|
+
status: 'fail',
|
|
144
|
+
detail: `missing: ${settingsPath}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
149
|
+
const parsed = JSON.parse(raw);
|
|
150
|
+
const pre = parsed.hooks?.PreToolUse ?? [];
|
|
151
|
+
const matchers = new Set(pre.map((g) => g.matcher).filter((m) => typeof m === 'string'));
|
|
152
|
+
const needs = [];
|
|
153
|
+
if (!matchers.has('Bash'))
|
|
154
|
+
needs.push('Bash');
|
|
155
|
+
if (!matchers.has('Write|Edit'))
|
|
156
|
+
needs.push('Write|Edit');
|
|
157
|
+
if (needs.length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
label: 'settings.json matchers cover Bash + Write|Edit',
|
|
160
|
+
status: 'pass',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
label: 'settings.json matchers cover Bash + Write|Edit',
|
|
165
|
+
status: 'fail',
|
|
166
|
+
detail: `missing PreToolUse matchers: ${needs.join(', ')}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
return {
|
|
171
|
+
label: 'settings.json matchers cover Bash + Write|Edit',
|
|
172
|
+
status: 'fail',
|
|
173
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function checkCommitMsgHook(baseDir) {
|
|
178
|
+
const hookPath = path.join(baseDir, '.git', 'hooks', 'commit-msg');
|
|
179
|
+
if (!fs.existsSync(hookPath)) {
|
|
180
|
+
return {
|
|
181
|
+
label: 'commit-msg hook installed',
|
|
182
|
+
status: 'warn',
|
|
183
|
+
detail: `missing: ${hookPath} (block_ai_attribution will not be enforced at commit time)`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const stat = fs.statSync(hookPath);
|
|
188
|
+
if (stat.size === 0) {
|
|
189
|
+
return { label: 'commit-msg hook installed', status: 'fail', detail: 'file is empty' };
|
|
190
|
+
}
|
|
191
|
+
return { label: 'commit-msg hook installed', status: 'pass' };
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
return {
|
|
195
|
+
label: 'commit-msg hook installed',
|
|
196
|
+
status: 'fail',
|
|
197
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
198
|
+
};
|
|
48
199
|
}
|
|
200
|
+
}
|
|
201
|
+
function checkCodexAgent(baseDir) {
|
|
202
|
+
const agentPath = path.join(baseDir, '.claude', 'agents', 'codex-adversarial.md');
|
|
203
|
+
if (fs.existsSync(agentPath))
|
|
204
|
+
return { label: 'codex-adversarial agent installed', status: 'pass' };
|
|
205
|
+
return {
|
|
206
|
+
label: 'codex-adversarial agent installed',
|
|
207
|
+
status: 'warn',
|
|
208
|
+
detail: `missing: ${agentPath}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function checkCodexCommand(baseDir) {
|
|
212
|
+
const cmdPath = path.join(baseDir, '.claude', 'commands', 'codex-review.md');
|
|
213
|
+
if (fs.existsSync(cmdPath))
|
|
214
|
+
return { label: '/codex-review command installed', status: 'pass' };
|
|
49
215
|
return {
|
|
50
|
-
label: '
|
|
216
|
+
label: '/codex-review command installed',
|
|
51
217
|
status: 'warn',
|
|
52
|
-
detail:
|
|
218
|
+
detail: `missing: ${cmdPath}`,
|
|
53
219
|
};
|
|
54
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
223
|
+
* responsiveness (pass/warn) and one informational line about the last
|
|
224
|
+
* probe time. Extracted so tests can feed a stub state without running
|
|
225
|
+
* the real probe.
|
|
226
|
+
*/
|
|
227
|
+
export function checksFromProbeState(state) {
|
|
228
|
+
const responsive = state.cli_responsive
|
|
229
|
+
? state.version !== undefined
|
|
230
|
+
? {
|
|
231
|
+
label: 'codex.cli_responsive',
|
|
232
|
+
status: 'pass',
|
|
233
|
+
detail: `version: ${state.version}`,
|
|
234
|
+
}
|
|
235
|
+
: { label: 'codex.cli_responsive', status: 'pass' }
|
|
236
|
+
: {
|
|
237
|
+
label: 'codex.cli_responsive',
|
|
238
|
+
status: 'warn',
|
|
239
|
+
detail: state.last_error ?? 'Codex CLI did not respond',
|
|
240
|
+
};
|
|
241
|
+
const lastProbe = {
|
|
242
|
+
label: 'codex.last_probe_at',
|
|
243
|
+
status: 'info',
|
|
244
|
+
detail: state.last_probe_at,
|
|
245
|
+
};
|
|
246
|
+
return [responsive, lastProbe];
|
|
247
|
+
}
|
|
55
248
|
function formatSymbol(status) {
|
|
56
249
|
if (status === 'pass')
|
|
57
250
|
return '[ok] ';
|
|
58
251
|
if (status === 'warn')
|
|
59
252
|
return '[warn]';
|
|
253
|
+
if (status === 'info')
|
|
254
|
+
return '[info]';
|
|
60
255
|
return '[fail]';
|
|
61
256
|
}
|
|
62
|
-
|
|
63
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Return whether Codex adversarial review is required. Read from the parsed
|
|
259
|
+
* policy; default is `true` when the field is absent. Isolated so tests can
|
|
260
|
+
* stub a policy without having to touch disk.
|
|
261
|
+
*/
|
|
262
|
+
function codexRequiredFromPolicy(baseDir) {
|
|
263
|
+
try {
|
|
264
|
+
const policy = loadPolicy(baseDir);
|
|
265
|
+
return policy.review?.codex_required !== false;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// If the policy itself is unreadable, checkPolicyParses will already
|
|
269
|
+
// report a fail. Default to "Codex required" so we still run those
|
|
270
|
+
// checks and surface the full picture.
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Assemble the full checklist for a given baseDir. Exported so tests can
|
|
276
|
+
* exercise the conditional branching without capturing stdout from
|
|
277
|
+
* `runDoctor`.
|
|
278
|
+
*
|
|
279
|
+
* `codexProbeState` is consulted ONLY when Codex is required by policy.
|
|
280
|
+
* Callers that already have a fresh probe state (e.g. `runDoctor`) should
|
|
281
|
+
* pass it; callers that don't (e.g. unit tests of the existing doctor
|
|
282
|
+
* surface) can omit it and the probe-derived fields are skipped.
|
|
283
|
+
*/
|
|
284
|
+
export function collectChecks(baseDir, codexProbeState) {
|
|
64
285
|
const policyPath = reaPath(baseDir, POLICY_FILE);
|
|
65
286
|
const registryPath = reaPath(baseDir, REGISTRY_FILE);
|
|
66
|
-
const
|
|
67
|
-
const commitMsgHook = path.join(baseDir, '.git', 'hooks', 'commit-msg');
|
|
287
|
+
const reaDirPath = path.join(baseDir, REA_DIR);
|
|
68
288
|
const checks = [
|
|
69
|
-
checkFileExists('.rea/ directory exists',
|
|
289
|
+
checkFileExists('.rea/ directory exists', reaDirPath, true),
|
|
70
290
|
checkPolicyParses(baseDir, policyPath),
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
291
|
+
checkRegistryParses(baseDir, registryPath),
|
|
292
|
+
checkAgentsPresent(baseDir),
|
|
293
|
+
checkHooksInstalled(baseDir),
|
|
294
|
+
checkSettingsJson(baseDir),
|
|
295
|
+
checkCommitMsgHook(baseDir),
|
|
74
296
|
];
|
|
297
|
+
if (codexRequiredFromPolicy(baseDir)) {
|
|
298
|
+
checks.push(checkCodexAgent(baseDir), checkCodexCommand(baseDir));
|
|
299
|
+
if (codexProbeState !== undefined) {
|
|
300
|
+
checks.push(...checksFromProbeState(codexProbeState));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// Single informational line replaces the two Codex-specific checks.
|
|
305
|
+
// The `codex-adversarial.md` agent is still expected to be present by
|
|
306
|
+
// checkAgentsPresent — that's deliberate; the agent is cheap to ship
|
|
307
|
+
// and flipping the flag should not require a re-install.
|
|
308
|
+
checks.push({
|
|
309
|
+
label: 'codex',
|
|
310
|
+
status: 'info',
|
|
311
|
+
detail: 'disabled via policy.review.codex_required — skipping Codex-related checks',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return checks;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Compute drift between the install manifest and the current state of the
|
|
318
|
+
* consumer's tree. Strictly read-only — no file writes. Synthetic entries
|
|
319
|
+
* (settings subset, managed CLAUDE.md fragment) are checked by hashing the
|
|
320
|
+
* computed values against the manifest SHA.
|
|
321
|
+
*/
|
|
322
|
+
export async function collectDriftReport(baseDir) {
|
|
323
|
+
const reaVersion = getPkgVersion();
|
|
324
|
+
const rows = [];
|
|
325
|
+
if (!manifestExists(baseDir)) {
|
|
326
|
+
return {
|
|
327
|
+
hasManifest: false,
|
|
328
|
+
bootstrap: false,
|
|
329
|
+
manifestVersion: null,
|
|
330
|
+
reaVersion,
|
|
331
|
+
rows,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const manifest = await readManifest(baseDir);
|
|
335
|
+
if (manifest === null) {
|
|
336
|
+
return {
|
|
337
|
+
hasManifest: false,
|
|
338
|
+
bootstrap: false,
|
|
339
|
+
manifestVersion: null,
|
|
340
|
+
reaVersion,
|
|
341
|
+
rows,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const manifestByPath = new Map(manifest.files.map((e) => [e.path, e]));
|
|
345
|
+
const canonical = await enumerateCanonicalFiles();
|
|
346
|
+
const canonicalByPath = new Map(canonical.map((c) => [c.destRelPath, c]));
|
|
347
|
+
// Canonical files: compare on-disk against canonical + manifest.
|
|
348
|
+
for (const c of canonical) {
|
|
349
|
+
const abs = path.join(baseDir, c.destRelPath);
|
|
350
|
+
if (!fs.existsSync(abs)) {
|
|
351
|
+
rows.push({
|
|
352
|
+
path: c.destRelPath,
|
|
353
|
+
status: 'missing',
|
|
354
|
+
detail: 'file not installed',
|
|
355
|
+
});
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const localSha = await sha256OfFile(abs);
|
|
359
|
+
const canonicalSha = await sha256OfFile(c.sourceAbsPath);
|
|
360
|
+
const entry = manifestByPath.get(c.destRelPath);
|
|
361
|
+
if (localSha === canonicalSha) {
|
|
362
|
+
rows.push({ path: c.destRelPath, status: 'unmodified' });
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (entry !== undefined && localSha === entry.sha256) {
|
|
366
|
+
rows.push({
|
|
367
|
+
path: c.destRelPath,
|
|
368
|
+
status: 'drifted-from-canonical',
|
|
369
|
+
detail: `local matches manifest but differs from rea v${reaVersion} canonical — run \`rea upgrade\``,
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
rows.push({
|
|
374
|
+
path: c.destRelPath,
|
|
375
|
+
status: 'drifted-from-manifest',
|
|
376
|
+
detail: 'file modified locally since install',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Manifest entries no longer in canonical (removed upstream), excluding
|
|
380
|
+
// synthetic entries handled below.
|
|
381
|
+
for (const entry of manifest.files) {
|
|
382
|
+
if (entry.path === CLAUDE_MD_MANIFEST_PATH ||
|
|
383
|
+
entry.path === SETTINGS_MANIFEST_PATH)
|
|
384
|
+
continue;
|
|
385
|
+
if (!canonicalByPath.has(entry.path)) {
|
|
386
|
+
rows.push({
|
|
387
|
+
path: entry.path,
|
|
388
|
+
status: 'removed-upstream',
|
|
389
|
+
detail: 'no longer shipped — run `rea upgrade` to remove',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Synthetic: rea-owned settings subset.
|
|
394
|
+
const settingsEntry = manifestByPath.get(SETTINGS_MANIFEST_PATH);
|
|
395
|
+
const settingsSha = canonicalSettingsSubsetHash(defaultDesiredHooks());
|
|
396
|
+
if (settingsEntry === undefined) {
|
|
397
|
+
rows.push({ path: SETTINGS_MANIFEST_PATH, status: 'untracked' });
|
|
398
|
+
}
|
|
399
|
+
else if (settingsEntry.sha256 !== settingsSha) {
|
|
400
|
+
rows.push({
|
|
401
|
+
path: SETTINGS_MANIFEST_PATH,
|
|
402
|
+
status: 'drifted-from-canonical',
|
|
403
|
+
detail: 'desired-hooks set has changed since install',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
rows.push({ path: SETTINGS_MANIFEST_PATH, status: 'unmodified' });
|
|
408
|
+
}
|
|
409
|
+
// Synthetic: managed CLAUDE.md fragment. Render the fragment from the
|
|
410
|
+
// current policy and compare. If policy is unreadable, skip gracefully.
|
|
411
|
+
const mdEntry = manifestByPath.get(CLAUDE_MD_MANIFEST_PATH);
|
|
412
|
+
try {
|
|
413
|
+
const policy = loadPolicy(baseDir);
|
|
414
|
+
const fragment = buildFragment({
|
|
415
|
+
policyPath: '.rea/policy.yaml',
|
|
416
|
+
profile: policy.profile,
|
|
417
|
+
autonomyLevel: policy.autonomy_level,
|
|
418
|
+
maxAutonomyLevel: policy.max_autonomy_level,
|
|
419
|
+
blockedPathsCount: policy.blocked_paths.length,
|
|
420
|
+
blockAiAttribution: policy.block_ai_attribution,
|
|
421
|
+
});
|
|
422
|
+
const currentSha = sha256OfBuffer(fragment);
|
|
423
|
+
if (mdEntry === undefined) {
|
|
424
|
+
rows.push({ path: CLAUDE_MD_MANIFEST_PATH, status: 'untracked' });
|
|
425
|
+
}
|
|
426
|
+
else if (mdEntry.sha256 !== currentSha) {
|
|
427
|
+
rows.push({
|
|
428
|
+
path: CLAUDE_MD_MANIFEST_PATH,
|
|
429
|
+
status: 'drifted-from-canonical',
|
|
430
|
+
detail: 'policy values or fragment template changed since install',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
rows.push({ path: CLAUDE_MD_MANIFEST_PATH, status: 'unmodified' });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Policy unreadable — drift of the fragment is meaningless to compute.
|
|
439
|
+
// The main doctor checks will already surface the policy failure.
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
hasManifest: true,
|
|
443
|
+
bootstrap: manifest.bootstrap === true,
|
|
444
|
+
manifestVersion: manifest.version,
|
|
445
|
+
reaVersion,
|
|
446
|
+
rows,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function printDriftReport(report) {
|
|
450
|
+
console.log('');
|
|
451
|
+
log('Drift report');
|
|
452
|
+
if (!report.hasManifest) {
|
|
453
|
+
console.log(' no .rea/install-manifest.json — run `rea upgrade` once to bootstrap a manifest.');
|
|
454
|
+
console.log('');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
console.log(` manifest v${report.manifestVersion ?? '?'} — running rea v${report.reaVersion}${report.bootstrap ? ' (bootstrap)' : ''}`);
|
|
458
|
+
console.log('');
|
|
459
|
+
let clean = 0;
|
|
460
|
+
for (const row of report.rows) {
|
|
461
|
+
if (row.status === 'unmodified') {
|
|
462
|
+
clean += 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const detail = row.detail !== undefined ? ` — ${row.detail}` : '';
|
|
466
|
+
console.log(` [${row.status}] ${row.path}${detail}`);
|
|
467
|
+
}
|
|
468
|
+
console.log('');
|
|
469
|
+
console.log(` ${clean} clean, ${report.rows.length - clean} with drift/issues.`);
|
|
470
|
+
console.log('');
|
|
471
|
+
}
|
|
472
|
+
export async function runDoctor(opts = {}) {
|
|
473
|
+
const baseDir = process.cwd();
|
|
474
|
+
// G11.3 — one-shot probe when Codex is required by policy. Doctor may be
|
|
475
|
+
// invoked without a running gateway, so we don't share state with
|
|
476
|
+
// `rea serve`; we just run a fresh probe here. Failure is observational —
|
|
477
|
+
// a warn row, never a hard failure of `rea doctor`.
|
|
478
|
+
let probeState;
|
|
479
|
+
if (codexRequiredFromPolicy(baseDir)) {
|
|
480
|
+
try {
|
|
481
|
+
probeState = await new CodexProbe().probe();
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// `probe()` is documented as never-throws, but belt-and-suspenders:
|
|
485
|
+
// missing probe data should never crash doctor.
|
|
486
|
+
probeState = undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const checks = collectChecks(baseDir, probeState);
|
|
75
490
|
console.log('');
|
|
76
491
|
log(`Doctor — ${baseDir}`);
|
|
77
492
|
console.log('');
|
|
@@ -82,6 +497,17 @@ export function runDoctor() {
|
|
|
82
497
|
if (c.status === 'fail')
|
|
83
498
|
hardFail = true;
|
|
84
499
|
}
|
|
500
|
+
// G11.5 — optional telemetry summary. Prints AFTER the main checks and
|
|
501
|
+
// NEVER contributes to the exit code. Purely observational.
|
|
502
|
+
if (opts.metrics === true) {
|
|
503
|
+
await printTelemetrySummary(baseDir);
|
|
504
|
+
}
|
|
505
|
+
// G12 — optional drift report. Also purely observational; `rea upgrade`
|
|
506
|
+
// is the action path.
|
|
507
|
+
if (opts.drift === true) {
|
|
508
|
+
const report = await collectDriftReport(baseDir);
|
|
509
|
+
printDriftReport(report);
|
|
510
|
+
}
|
|
85
511
|
console.log('');
|
|
86
512
|
if (hardFail) {
|
|
87
513
|
log('Doctor: one or more hard checks failed.');
|
|
@@ -91,3 +517,16 @@ export function runDoctor() {
|
|
|
91
517
|
log('Doctor: OK (warnings do not fail the check).');
|
|
92
518
|
console.log('');
|
|
93
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Render the telemetry summary after the doctor checks. Compact by
|
|
522
|
+
* design — the exact shape may evolve as G5 formalizes metrics export.
|
|
523
|
+
*/
|
|
524
|
+
async function printTelemetrySummary(baseDir) {
|
|
525
|
+
const summary = await summarizeTelemetry(baseDir);
|
|
526
|
+
console.log('');
|
|
527
|
+
log(`Telemetry — last ${summary.window_days} days`);
|
|
528
|
+
console.log(` invocations/day: ${summary.invocations_per_day.join(', ')}`);
|
|
529
|
+
console.log(` total estimated tokens: ${summary.total_estimated_tokens}`);
|
|
530
|
+
console.log(` rate-limited responses: ${summary.rate_limited_count}`);
|
|
531
|
+
console.log(` avg latency: ${Math.round(summary.avg_latency_ms)} ms`);
|
|
532
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runDoctor } from './doctor.js';
|
|
|
5
5
|
import { runFreeze, runUnfreeze } from './freeze.js';
|
|
6
6
|
import { runInit } from './init.js';
|
|
7
7
|
import { runServe } from './serve.js';
|
|
8
|
+
import { runUpgrade } from './upgrade.js';
|
|
8
9
|
import { err, getPkgVersion } from './utils.js';
|
|
9
10
|
async function main() {
|
|
10
11
|
const program = new Command();
|
|
@@ -14,15 +15,39 @@ async function main() {
|
|
|
14
15
|
.version(getPkgVersion(), '-v, --version', 'print rea version');
|
|
15
16
|
program
|
|
16
17
|
.command('init')
|
|
17
|
-
.description('Interactive wizard — write .rea/policy.yaml and .
|
|
18
|
-
.option('-y, --yes', 'non-interactive mode — accept defaults
|
|
18
|
+
.description('Interactive wizard — write .rea/policy.yaml, install .claude/, commit-msg hook, and CLAUDE.md fragment')
|
|
19
|
+
.option('-y, --yes', 'non-interactive mode — accept defaults, skip existing files')
|
|
19
20
|
.option('--from-reagent', 'migrate from a .reagent/ directory if present')
|
|
20
|
-
.option('--profile <name>', 'profile: minimal | client-engagement | bst-internal | lit-wc | open-source')
|
|
21
|
+
.option('--profile <name>', 'profile: minimal | client-engagement | bst-internal | bst-internal-no-codex | lit-wc | open-source | open-source-no-codex')
|
|
22
|
+
.option('--force', 'overwrite existing .claude/ artifacts and .rea/policy.yaml')
|
|
23
|
+
.option('--accept-dropped-fields', 'allow reagent translation when drop-list fields are present (security-adjacent)')
|
|
24
|
+
// Commander's boolean-with-negation pair: `--codex` sets codex=true,
|
|
25
|
+
// `--no-codex` sets codex=false. Leaving both unset produces
|
|
26
|
+
// `opts.codex === undefined`, and runInit derives the value from the
|
|
27
|
+
// profile name.
|
|
28
|
+
.option('--codex', 'require Codex adversarial review (writes review.codex_required: true)')
|
|
29
|
+
.option('--no-codex', 'disable Codex adversarial review (writes review.codex_required: false)')
|
|
21
30
|
.action(async (opts) => {
|
|
22
31
|
await runInit({
|
|
23
32
|
yes: opts.yes,
|
|
24
33
|
fromReagent: opts.fromReagent,
|
|
25
34
|
profile: opts.profile,
|
|
35
|
+
force: opts.force,
|
|
36
|
+
acceptDroppedFields: opts.acceptDroppedFields,
|
|
37
|
+
codex: opts.codex,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
program
|
|
41
|
+
.command('upgrade')
|
|
42
|
+
.description('Sync .claude/, .husky/, and managed fragments with this rea version. Prompts on drift; auto-updates unmodified files.')
|
|
43
|
+
.option('--dry-run', 'show what would change; write nothing')
|
|
44
|
+
.option('-y, --yes', 'non-interactive — keep drifted files, skip removed-upstream')
|
|
45
|
+
.option('--force', 'non-interactive — overwrite drift, delete removed-upstream')
|
|
46
|
+
.action(async (opts) => {
|
|
47
|
+
await runUpgrade({
|
|
48
|
+
dryRun: opts.dryRun,
|
|
49
|
+
yes: opts.yes,
|
|
50
|
+
force: opts.force,
|
|
26
51
|
});
|
|
27
52
|
});
|
|
28
53
|
program
|
|
@@ -54,8 +79,13 @@ async function main() {
|
|
|
54
79
|
program
|
|
55
80
|
.command('doctor')
|
|
56
81
|
.description('Validate the install: policy parses, .rea/ layout, hooks, Codex plugin.')
|
|
57
|
-
.
|
|
58
|
-
|
|
82
|
+
.option('--metrics', 'also print a 7-day summary of Codex telemetry (G11.5)')
|
|
83
|
+
.option('--drift', 'report drift vs. the install manifest (read-only; does not mutate)')
|
|
84
|
+
.action(async (opts) => {
|
|
85
|
+
await runDoctor({
|
|
86
|
+
...(opts.metrics === true ? { metrics: true } : {}),
|
|
87
|
+
...(opts.drift === true ? { drift: true } : {}),
|
|
88
|
+
});
|
|
59
89
|
});
|
|
60
90
|
await program.parseAsync(process.argv);
|
|
61
91
|
}
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -2,5 +2,18 @@ export interface InitOptions {
|
|
|
2
2
|
yes?: boolean | undefined;
|
|
3
3
|
fromReagent?: boolean | undefined;
|
|
4
4
|
profile?: string | undefined;
|
|
5
|
+
force?: boolean | undefined;
|
|
6
|
+
acceptDroppedFields?: boolean | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* G11.4: explicit override for Codex adversarial review. `true` forces
|
|
9
|
+
* `review.codex_required: true` in the written policy; `false` forces
|
|
10
|
+
* `false`. Undefined → derive default from the chosen profile name
|
|
11
|
+
* (profiles whose name ends with `-no-codex` default to no-codex).
|
|
12
|
+
*
|
|
13
|
+
* Non-interactive semantics: in `--yes` mode the flag is honored
|
|
14
|
+
* directly. Interactive mode confirms the flag value as the prompt's
|
|
15
|
+
* initial value (but still prompts for a final answer).
|
|
16
|
+
*/
|
|
17
|
+
codex?: boolean | undefined;
|
|
5
18
|
}
|
|
6
19
|
export declare function runInit(options: InitOptions): Promise<void>;
|