@brunobracaioli/aibrain 0.4.0-beta.1 → 0.4.0-beta.10

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # aisac
2
2
 
3
- The AISAC client for Claude Code and Codex on Linux/WSL with Node.js 22 or newer:
3
+ The AISAC client for Claude Code and Codex on Linux/WSL, and for Claude Code on
4
+ macOS, with Node.js 22 or newer:
4
5
  AI Brain memory, context management and, for AISAC Founding Cohort members,
5
6
  AISAC Engineering.
6
7
 
@@ -14,6 +15,41 @@ both provide the `aisac` and `aibrain` commands. State stays in `~/.aibrain`.
14
15
  shows the answer and sends the request only to the configured AI Brain address.
15
16
  `aisac status` stays offline.
16
17
 
18
+ For licensed accounts, `aisac claude` downloads the AISAC Engineering pack for
19
+ the session, verifies its Ed25519 signature against the key pinned in this
20
+ release, and loads it as the `aisac-engineering` plugin from the session's
21
+ private folder, which is deleted on exit. Any failure starts Claude without the
22
+ pack. `aisac setup --lang pt-BR|en` picks the pack language without a network
23
+ call. The pack is never part of this npm package.
24
+
25
+ `aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
26
+ session only: its skills are added to the Codex app-server the launch owns
27
+ (call one with `$`, for example `$spec`) and its agents become session agent
28
+ roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
29
+ session's folder is deleted on exit. An older Codex starts without the pack and
30
+ says `codex_outdated`.
31
+
32
+ Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
33
+ `aisac-guardrails`.
34
+ - **Default on:** `secrets-guard` refuses secret files by name. Four more hooks
35
+ keep continuity: a repository snapshot at start and after a compaction, a
36
+ docs reminder, and the NOTES/spec/CHANGELOG pass.
37
+ - **Default off:** `git-guard`, `destructive-guard` and `format-on-edit`.
38
+ - **Changing them:** `aisac hooks` lists them, and `aisac hooks enable|disable
39
+ <id>` or `aisac hooks reset` changes the next session. The agent cannot change
40
+ them from inside a session.
41
+ - **Code:** it ships in this package and runs locally. Nothing comes from the
42
+ server.
43
+
44
+ Licensed sessions of `aisac claude` and `aisac codex` can also load reviewed MCP
45
+ servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabase`
46
+ (read-only), `vercel`, `playwright` (pinned, isolated) and `context7`.
47
+ - **Default:** every server is off. `aisac mcp` lists them, `aisac mcp enable|disable
48
+ <id>` or `aisac mcp reset` changes the next session, and `aisac mcp info <id>` says
49
+ what a server reaches and how to log in.
50
+ - **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
51
+ and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
52
+
17
53
  Codex capture also requires the `flock` utility from `util-linux`, normally
18
54
  included in Linux/WSL. The launcher and doctor check availability before a
19
55
  monitored Codex session starts. Capture coordination is released by the kernel
@@ -23,7 +59,7 @@ notes or lock files to work around a reported storage error.
23
59
 
24
60
  The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
25
61
  2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
26
- 0.162.0, 0.162.1, 0.163.0 or 0.164.0. A newer stable Codex 0.x release may continue only
62
+ 0.162.0, 0.162.1, 0.163.0, 0.164.0 or 0.166.0 (0.165.0 was never qualified). A newer stable Codex 0.x release may continue only
27
63
  after its locally generated experimental protocol schemas and transport flags
28
64
  match the bundled adapter contract; prereleases, older releases and new majors
29
65
  remain rejected. Starting with beta14, newer stable Claude 2.1 patches can
@@ -39,6 +75,11 @@ record formats. Native launch/resume qualification is separate from a new
39
75
  model/JEV compaction qualification. A later native release that changes the
40
76
  consumed contract can still require an AI Brain compatibility update.
41
77
 
78
+ `0.4.0-beta.9` refreshes the bundled contract to Codex 0.166.0. Its delta is
79
+ additive: optional item timing, an image generation id, an attach-to-source fork
80
+ flag and two error codes. See the
81
+ [Codex 0.166 qualification record](../../docs/reference/codex-166-compatibility-2026-11-04.md).
82
+
42
83
  The Codex terminal bridge permits the native session picker's separate read-only
43
84
  connection while keeping the primary conversation and monitor intact. A picker
44
85
  cannot compact, start a turn, inject history or take ownership of the primary
@@ -61,7 +102,9 @@ Installation does not contact AI Brain, change native client configuration, or
61
102
  enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
62
103
  versioned disclosure, and successful provider-free pilot access. Claude and
63
104
  Codex continue to own their own authentication. Windows native is unsupported;
64
- run the beta inside WSL. Pricing and general customer activation are outside
105
+ run the beta inside WSL. On macOS, `aisac setup` configures Claude Code, and
106
+ `aisac codex` is refused with `CODEX_MACOS_UNSUPPORTED`. Codex capture needs
107
+ Linux procfs and `flock`, so use Codex from Linux/WSL. Pricing and general customer activation are outside
65
108
  this beta.
66
109
 
67
110
  For reviewed noninteractive automation only, use a private token file and
@@ -151,7 +151,7 @@ type Register = (
151
151
  ) => void;
152
152
 
153
153
  const MAX_RESULT_BYTES = 256 * 1024;
154
- const CLAUDE_HOOK_VERSION = '0.4.0-beta.1';
154
+ const CLAUDE_HOOK_VERSION = '0.4.0-beta.7';
155
155
  const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
156
156
  const COOLDOWN_MS = 60_000;
157
157
  const MAX_TRUNCATED_CODE_POINTS = 300;
@@ -463,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
463
463
 
464
464
  function safeErrorCategory(error: unknown): string {
465
465
  const record = isRecord(error) ? error : {};
466
+ // A coded error names its own reason; the keyword guess below would read
467
+ // "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
468
+ if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
469
+ return record.compactErrorCode as CompactErrorCode;
466
470
  const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
467
471
  const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
468
472
  const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
@@ -54,6 +54,12 @@ const MAX_OWNED_REQUESTS = 1_000;
54
54
  const MAX_RETIRED_REQUESTS = 2_000;
55
55
  const MAX_CLIENT_REQUEST_BINDINGS = 128;
56
56
  const MAX_OWNED_THREAD_IDS = 256;
57
+ // AISAC Engineering (ADR-0047 E4c): the launcher's skills root is added to the
58
+ // owned app-server before the client sees its initialize answer.
59
+ const ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
60
+ const ENGINEERING_SKILLS_ACK_TIMEOUT_MS = 2_000;
61
+ const MAX_ENGINEERING_PATH_LENGTH = 4_096;
62
+ const MAX_TRACKED_INITIALIZE_REQUESTS = 4;
57
63
  const MAX_RECOVERY_ITEMS = 128;
58
64
  const MAX_RECOVERY_SOURCE_INDEX = 65_536;
59
65
  const MAX_NOTE_BYTES = 128 * 1024;
@@ -2707,6 +2713,8 @@ export function createCodexContextProxy({
2707
2713
  createDiagnosticSink = null,
2708
2714
  brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
2709
2715
  clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
2716
+ engineeringSkillsRoot = null,
2717
+ engineeringAckTimeoutMs = ENGINEERING_SKILLS_ACK_TIMEOUT_MS,
2710
2718
  }) {
2711
2719
  const contextPolicy = normalizeContextPolicy(policy);
2712
2720
  const effectiveThreshold = contextPolicy?.compactAtPercent ?? threshold;
@@ -2719,6 +2727,15 @@ export function createCodexContextProxy({
2719
2727
  const telemetryByThread = new Map();
2720
2728
  const diagnosticSinks = new Map();
2721
2729
  let stopped = false;
2730
+ // AISAC Engineering skills root: 'off' without a root; 'waiting' for the
2731
+ // client's first successful initialize; 'holding' app-server output until
2732
+ // skills/extraRoots/set is answered (or times out); then 'done'.
2733
+ let engineeringState =
2734
+ typeof engineeringSkillsRoot === 'string' && engineeringSkillsRoot.length > 0
2735
+ ? 'waiting'
2736
+ : 'off';
2737
+ const initializeRequestKeys = new Set();
2738
+ let engineeringHeld = null;
2722
2739
  function emitDiagnostic(event) {
2723
2740
  try {
2724
2741
  const phase = typeof event?.phase === 'string' ? event.phase : '';
@@ -4131,6 +4148,11 @@ export function createCodexContextProxy({
4131
4148
  const owned = ownedRequests.get(message.id);
4132
4149
  ownedRequests.delete(message.id);
4133
4150
  retire(message.id);
4151
+ if (owned.kind === 'engineering') {
4152
+ // An error leaves the session without the pack skills; it is never shown.
4153
+ releaseEngineering();
4154
+ return false;
4155
+ }
4134
4156
  if (
4135
4157
  owned.kind === 'read' ||
4136
4158
  owned.kind === 'page' ||
@@ -4364,6 +4386,14 @@ export function createCodexContextProxy({
4364
4386
  if (!validRequestId) return;
4365
4387
  const requestKey = clientRequestKey(requestId);
4366
4388
  if (requestKey === null) return;
4389
+ if (
4390
+ message.method === 'initialize' &&
4391
+ engineeringState === 'waiting' &&
4392
+ initializeRequestKeys.size < MAX_TRACKED_INITIALIZE_REQUESTS
4393
+ ) {
4394
+ initializeRequestKeys.add(requestKey);
4395
+ return;
4396
+ }
4367
4397
  if (['thread/start', 'thread/resume', 'thread/fork'].includes(message.method)) {
4368
4398
  const threadId =
4369
4399
  isRecord(message.params) && typeof message.params.threadId === 'string'
@@ -4428,7 +4458,7 @@ export function createCodexContextProxy({
4428
4458
  let passthrough = false;
4429
4459
  let discardOversized = false;
4430
4460
  let pausedForBackpressure = false;
4431
- const forward = (text) => {
4461
+ const writeOut = (text) => {
4432
4462
  if (!stdout.write(text) && !pausedForBackpressure) {
4433
4463
  pausedForBackpressure = true;
4434
4464
  child.stdout.pause();
@@ -4438,6 +4468,54 @@ export function createCodexContextProxy({
4438
4468
  });
4439
4469
  }
4440
4470
  };
4471
+ const forward = (text) => {
4472
+ if (engineeringHeld === null) {
4473
+ writeOut(text);
4474
+ return;
4475
+ }
4476
+ engineeringHeld.lines.push(text);
4477
+ engineeringHeld.bytes += Buffer.byteLength(text, 'utf8');
4478
+ if (engineeringHeld.bytes > MAX_LINE_BYTES) releaseEngineering();
4479
+ };
4480
+ /** The client's first successful initialize answer, which starts the hold. */
4481
+ function isEngineeringTrigger(message) {
4482
+ if (engineeringState !== 'waiting' || !isRecord(message)) return false;
4483
+ if (message.method !== undefined || !('result' in message)) return false;
4484
+ const key = clientRequestKey(message.id);
4485
+ return key !== null && initializeRequestKeys.has(key);
4486
+ }
4487
+ function holdForEngineering(text) {
4488
+ engineeringState = 'holding';
4489
+ initializeRequestKeys.clear();
4490
+ engineeringHeld = {
4491
+ lines: [text],
4492
+ bytes: Buffer.byteLength(text, 'utf8'),
4493
+ id: null,
4494
+ cancel: null,
4495
+ };
4496
+ try {
4497
+ engineeringHeld.id = sendOwned(
4498
+ 'skills/extraRoots/set',
4499
+ { extraRoots: [engineeringSkillsRoot] },
4500
+ 'engineering',
4501
+ null,
4502
+ );
4503
+ } catch {
4504
+ releaseEngineering();
4505
+ return;
4506
+ }
4507
+ engineeringHeld.cancel = scheduleTimeout(() => releaseEngineering(), engineeringAckTimeoutMs);
4508
+ }
4509
+ /** Forward everything held, in order. Idempotent; the session never waits on the pack. */
4510
+ function releaseEngineering() {
4511
+ if (engineeringHeld === null) return;
4512
+ const held = engineeringHeld;
4513
+ engineeringHeld = null;
4514
+ engineeringState = 'done';
4515
+ held.cancel?.();
4516
+ if (held.id !== null && ownedRequests.has(held.id)) cleanupRequest(held.id);
4517
+ for (const text of held.lines) writeOut(text);
4518
+ }
4441
4519
  const processLine = (line) => {
4442
4520
  if (Buffer.byteLength(line, 'utf8') + 1 > MAX_LINE_BYTES) {
4443
4521
  const ownedId = internalRequestId(line);
@@ -4451,6 +4529,10 @@ export function createCodexContextProxy({
4451
4529
  try {
4452
4530
  const message = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
4453
4531
  observeClientResponse(message);
4532
+ if (isEngineeringTrigger(message)) {
4533
+ holdForEngineering(`${line}\n`);
4534
+ return;
4535
+ }
4454
4536
  forwardLine = handle(message);
4455
4537
  } catch {
4456
4538
  /* preserve malformed external protocol lines */
@@ -4536,6 +4618,7 @@ export function createCodexContextProxy({
4536
4618
  }
4537
4619
  });
4538
4620
  child.stdout.on('end', () => {
4621
+ releaseEngineering();
4539
4622
  const tail = inputBuffer + decoder.end();
4540
4623
  if (tail && !discardOversized) forward(tail);
4541
4624
  inputBuffer = '';
@@ -4565,6 +4648,7 @@ export function createCodexContextProxy({
4565
4648
  observeClientRequest,
4566
4649
  observeClientResponse,
4567
4650
  async stop() {
4651
+ releaseEngineering();
4568
4652
  if (stopped) {
4569
4653
  await Promise.all(
4570
4654
  [...diagnosticSinks.values()].map((sink) =>
@@ -4594,6 +4678,18 @@ export function createCodexContextProxy({
4594
4678
  };
4595
4679
  }
4596
4680
 
4681
+ /**
4682
+ * The launcher's AISAC Engineering skills root, or null. Only an absolute,
4683
+ * normalized path without control characters is used.
4684
+ */
4685
+ export function engineeringSkillsRootFrom(value) {
4686
+ if (typeof value !== 'string' || value.length === 0) return null;
4687
+ if (value.length > MAX_ENGINEERING_PATH_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
4688
+ return null;
4689
+ if (!value.startsWith('/') || resolve(value) !== value) return null;
4690
+ return value;
4691
+ }
4692
+
4597
4693
  async function main() {
4598
4694
  if (process.argv.includes('--help')) {
4599
4695
  process.stderr.write(
@@ -4648,6 +4744,7 @@ async function main() {
4648
4744
  timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
4649
4745
  policy,
4650
4746
  createDiagnosticSink: diagnosticFactory,
4747
+ engineeringSkillsRoot: engineeringSkillsRootFrom(process.env[ENGINEERING_SKILLS_ROOT_ENV]),
4651
4748
  });
4652
4749
  const clientDecoder = new StringDecoder('utf8');
4653
4750
  let clientInputBuffer = '';
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
16
16
  const LOCK_WAIT_MS = 250;
17
17
  const LOCK_STALE_MS = 30_000;
18
18
  const ADMISSION_MS = 60 * 60 * 1000;
19
- export const ADAPTER_VERSION = '0.4.0-beta.1';
19
+ export const ADAPTER_VERSION = '0.4.0-beta.10';
20
20
  const MAX_DIAGNOSTIC_COUNT = 10_000_000;
21
21
  const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
22
22
  const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
package/dist/cli.mjs CHANGED
@@ -17,6 +17,8 @@ import {
17
17
  } from './state.mjs';
18
18
  import {
19
19
  captureLockPlatformCheck,
20
+ codexPlatformCheck,
21
+ defaultClientSelection,
20
22
  nativeVersion,
21
23
  platformCheck,
22
24
  resolveNative,
@@ -29,8 +31,18 @@ import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.m
29
31
  import { readDiagnostics } from './claude-observation.mjs';
30
32
  import { readCodexDiagnostics } from './codex-diagnostics.mjs';
31
33
  import { checkEngineeringLicense, licenseLine, unavailableLicense } from './engineering.mjs';
34
+ import {
35
+ isPackLocale,
36
+ readLastLaunch,
37
+ readStoredLocale,
38
+ resolvePackLocale,
39
+ writeStoredLocale,
40
+ } from './engineering-pack.mjs';
41
+ import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mjs';
42
+ import { sessionServers } from './mcp-servers.mjs';
43
+ import { anyFormatter } from './guardrails/format.mjs';
32
44
 
33
- const VERSION = '0.4.0-beta.1';
45
+ const VERSION = '0.4.0-beta.10';
34
46
  // The command is `aisac`; `aibrain` is an alias installed from the same entry.
35
47
  const COMMAND = 'aisac';
36
48
  const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
@@ -48,7 +60,13 @@ function usage() {
48
60
  'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
49
61
  );
50
62
  console.log(
51
- 'AISAC Engineering: setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
63
+ 'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language; setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
64
+ );
65
+ console.log(
66
+ 'Guardrails: `aisac hooks` lists them (a checklist on a terminal); `aisac hooks enable|disable <id>...` and `aisac hooks reset` change the next `aisac claude` session.',
67
+ );
68
+ console.log(
69
+ 'MCP catalog: `aisac mcp` lists the reviewed servers (GitHub, Supabase, Vercel, Playwright, Context7); `aisac mcp enable|disable <id>...`, `aisac mcp reset`, `aisac mcp info <id>` and `aisac mcp login <id>` (Codex) change or explain the next session.',
52
70
  );
53
71
  }
54
72
 
@@ -198,7 +216,7 @@ function selectedPolicy(args, previous) {
198
216
  if (option(args, '--goal') === undefined && inherited.goal !== undefined)
199
217
  overrides.goal = inherited.goal;
200
218
  }
201
- const clients = option(args, '--client') ?? 'both';
219
+ const clients = option(args, '--client') ?? defaultClientSelection();
202
220
  if (
203
221
  clients === 'codex' &&
204
222
  args.some(
@@ -310,6 +328,26 @@ async function updateLocalOptOut(previous, args) {
310
328
  return true;
311
329
  }
312
330
 
331
+ /** Store the AISAC Engineering pack language; local only, no network call. @param {'pt-BR' | 'en'} locale */
332
+ async function setPackLocale(locale) {
333
+ const root = await ensureStateRoot();
334
+ await writeStoredLocale(root, locale);
335
+ console.log(
336
+ `AISAC Engineering pack language set to ${locale} for future launches; no network call was made.`,
337
+ );
338
+ }
339
+
340
+ /** Offline view of the pack language and the last launch (status, doctor). */
341
+ async function engineeringPackState() {
342
+ const root = stateRoot();
343
+ const stored = await readStoredLocale(root);
344
+ return {
345
+ locale: resolvePackLocale(stored),
346
+ localeSource: stored === null ? 'system' : 'setup',
347
+ lastLaunch: await readLastLaunch(root),
348
+ };
349
+ }
350
+
313
351
  /** @param {string} endpoint */
314
352
  function validateEndpoint(endpoint) {
315
353
  if (endpoint.length > 2048) throw new Error('endpoint_too_long');
@@ -385,6 +423,7 @@ async function setup(args) {
385
423
  '--telemetry',
386
424
  '--claude-shadow',
387
425
  '--claude-diagnostics',
426
+ '--lang',
388
427
  ]),
389
428
  new Set([
390
429
  '--client',
@@ -396,10 +435,14 @@ async function setup(args) {
396
435
  '--telemetry',
397
436
  '--claude-shadow',
398
437
  '--claude-diagnostics',
438
+ '--lang',
399
439
  ]),
400
440
  );
401
441
  if (args.includes('--undo') && args.length !== 1)
402
442
  throw new Error('setup_undo_cannot_have_options');
443
+ const lang = option(args, '--lang');
444
+ if (lang !== undefined && !isPackLocale(lang)) throw new Error('invalid_lang');
445
+ if (lang !== undefined && args.length === 2) return setPackLocale(lang);
403
446
  const previous = await readConfig();
404
447
  if (await updateLocalOptOut(previous, args)) return;
405
448
  const localOptOut =
@@ -408,8 +451,12 @@ async function setup(args) {
408
451
  if (localOptOut) return disableTelemetryLocally(previous);
409
452
  const platform = platformCheck();
410
453
  if (!platform.ok) throw new Error(`${platform.code}:${platform.message}`);
411
- const clients = option(args, '--client') ?? 'both';
454
+ const clients = option(args, '--client') ?? defaultClientSelection();
412
455
  if (!['claude', 'codex', 'both'].includes(clients)) throw new Error('invalid_client');
456
+ if (clients !== 'claude') {
457
+ const codexPlatform = codexPlatformCheck();
458
+ if (!codexPlatform.ok) throw new Error(`${codexPlatform.code}:${codexPlatform.message}`);
459
+ }
413
460
  if (
414
461
  clients === 'codex' &&
415
462
  (option(args, '--claude-shadow') !== undefined ||
@@ -501,11 +548,27 @@ async function setup(args) {
501
548
  console.log(
502
549
  `Configured ${clients} in ${root}. Provider-free access verified; no provider batch was used.`,
503
550
  );
551
+ if (lang !== undefined) await setPackLocale(lang);
504
552
  // Display only: the server decides again on every pack request (ADR-0047).
505
553
  const line = licenseLine(await checkEngineeringLicense(endpoint, token));
506
554
  if (line !== null) console.log(line);
507
555
  }
508
556
 
557
+ /** Extension state plus whether `format-on-edit` has a formatter in this folder (FR8). */
558
+ async function doctorExtensions() {
559
+ const summary = await extensionsSummary(stateRoot());
560
+ // A token server is skipped at launch when its variable is unset (names only).
561
+ const mcpMissingEnv = sessionServers(summary.mcp, 'claude', process.env).missingEnv.map(
562
+ (skipped) => skipped.id,
563
+ );
564
+ const formatOnEdit = !summary.hooks.includes('format-on-edit')
565
+ ? 'off'
566
+ : (await anyFormatter(process.cwd()))
567
+ ? 'ready'
568
+ : 'no_formatter_found';
569
+ return { ...summary, formatOnEdit, mcpMissingEnv };
570
+ }
571
+
509
572
  /** @param {boolean} json */
510
573
  async function doctor(json) {
511
574
  const platform = platformCheck();
@@ -599,6 +662,8 @@ async function doctor(json) {
599
662
  config?.claudeObservation?.disclosedAt !== undefined,
600
663
  },
601
664
  engineering,
665
+ engineeringPack: await engineeringPackState(),
666
+ extensions: await doctorExtensions(),
602
667
  paidOrPilot: probe?.ok
603
668
  ? 'provider_free_probe_ok; paid entitlement or pilot invitation remains server-enforced'
604
669
  : config
@@ -634,6 +699,8 @@ async function status(json) {
634
699
  config?.claudeObservation?.disclosedAt !== null &&
635
700
  config?.claudeObservation?.disclosedAt !== undefined,
636
701
  },
702
+ engineeringPack: await engineeringPackState(),
703
+ extensions: await extensionsSummary(stateRoot()),
637
704
  monitoredSession: null,
638
705
  monitoringStatus: 'not_observed',
639
706
  };
@@ -682,6 +749,8 @@ async function main(argv) {
682
749
  else console.log(records);
683
750
  return;
684
751
  }
752
+ if (command === 'hooks') return hooksCommand(rest);
753
+ if (command === 'mcp') return mcpCommand(rest);
685
754
  if (command === 'disable') {
686
755
  if (rest.length > 0) throw new Error('disable_does_not_accept_options');
687
756
  return removeOwnedSetup();
@@ -26,7 +26,14 @@ const PAGINATION_CONTRACT_VALID =
26
26
  fingerprint.roots[PAGINATION_CONTRACT.response_schema]?.sha256 &&
27
27
  typeof PAGINATION_CONTRACT.graph_sha256 === 'string' &&
28
28
  PAGINATION_CONTRACT.graph_sha256.length === 64);
29
- const QUALIFIED_VERSIONS = new Set(['0.161.0', '0.162.0', '0.162.1', '0.163.0', '0.164.0']);
29
+ const QUALIFIED_VERSIONS = new Set([
30
+ '0.161.0',
31
+ '0.162.0',
32
+ '0.162.1',
33
+ '0.163.0',
34
+ '0.164.0',
35
+ '0.166.0',
36
+ ]);
30
37
  const ANNOTATIONS = new Set([
31
38
  '$comment',
32
39
  'title',
@@ -475,7 +482,50 @@ export async function resolveCodexCompatibility(executable, baseEnvironment = pr
475
482
  }
476
483
  }
477
484
 
485
+ /** @param {string} left @param {string} right */
486
+ function compareStableVersions(left, right) {
487
+ const a = left.split('.').map(Number);
488
+ const b = right.split('.').map(Number);
489
+ for (let index = 0; index < 3; index += 1) {
490
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
491
+ if (difference !== 0) return difference;
492
+ }
493
+ return 0;
494
+ }
495
+
496
+ /** The newest Codex release this client was qualified against. */
497
+ export const LATEST_QUALIFIED_CODEX = /** @type {string} */ (
498
+ [...QUALIFIED_VERSIONS].sort(compareStableVersions).at(-1)
499
+ );
500
+
501
+ // Rejections a user fixes by installing a qualified Codex (or a newer aisac):
502
+ // the installed release's contract or transport differs from the bundled one.
503
+ const CONTRACT_REJECTIONS = new Set([
504
+ 'protocol_schema_mismatch',
505
+ 'protocol_method_missing',
506
+ 'schema_root_missing_or_invalid',
507
+ 'schema_set_too_large',
508
+ 'schema_generation_failed',
509
+ 'transport_flags_missing',
510
+ 'protocol_probe_failed',
511
+ ]);
512
+
513
+ /** @param {{reason:string, version:string|null}} decision @returns {string|null} */
514
+ export function codexCompatibilityRemedy(decision) {
515
+ const detected = decision.version === null ? 'The installed Codex' : `Codex ${decision.version}`;
516
+ const reinstall = `npm install -g @openai/codex@${LATEST_QUALIFIED_CODEX}`;
517
+ if (CONTRACT_REJECTIONS.has(decision.reason))
518
+ return `${detected} is not qualified by this aisac (newest qualified: ${LATEST_QUALIFIED_CODEX}). Install the qualified Codex with \`${reinstall}\`, or check for an aisac release that qualifies it with \`npm install -g @aisac/cli@beta\`.`;
519
+ if (decision.reason === 'version_unqualified')
520
+ return `${detected} is older than this aisac supports. Update it with \`${reinstall}\`.`;
521
+ if (decision.reason === 'version_not_stable_0x')
522
+ return `${detected} is not a stable 0.x release. Install a qualified one with \`${reinstall}\`.`;
523
+ return null;
524
+ }
525
+
478
526
  /** @param {{reason:string, version:string|null}} decision @returns {string} */
479
527
  export function codexCompatibilityError(decision) {
480
- return `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aisac doctor --json`;
528
+ const coded = `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aisac doctor --json`;
529
+ const remedy = codexCompatibilityRemedy(decision);
530
+ return remedy === null ? coded : `${coded}\n${remedy}`;
481
531
  }