@m8t-stack/cli 0.2.35 → 0.2.36

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
@@ -121,12 +121,14 @@ Key flags: `--client-id <appId>` (reuse an existing app reg + skip **all** Micro
121
121
 
122
122
  ## Binding management — `m8t bind`
123
123
 
124
- Wire channel bots (Telegram, Slack, Teams) to Foundry workers. F4 ships
125
- the management API + CLI scaffolding; the actual adapters (which deliver
126
- inbound messages from each channel) ship in later features.
124
+ Wire channel bots (Telegram, Slack, Teams) to Foundry workers. The binding
125
+ management API + CLI cover all three channels; a channel can only complete
126
+ a bind once its adapter (which delivers inbound messages from that
127
+ channel) is registered at gateway startup — Telegram ships built in, Slack
128
+ and Teams adapters are not registered yet.
127
129
 
128
130
  ```bash
129
- # Create a binding (requires the channel adapter Telegram ships in F6)
131
+ # Create a binding (Telegram's adapter ships built in)
130
132
  m8t bind add telegram \
131
133
  --worker cmo \
132
134
  --bot-token <from-botfather> \
@@ -152,17 +154,18 @@ m8t bind cleanup --all-orphaned
152
154
 
153
155
  > `--worker` is the **case-sensitive, lowercase** Foundry agent name as deployed (e.g. `cmo`). A wrong name isn't rejected at bind time — it surfaces later as an orphaned binding on the first message.
154
156
 
155
- ### Pre-F6 behavior
157
+ ### Binding a channel with no registered adapter
156
158
 
157
- `m8t bind add telegram ...` currently fails with:
159
+ `m8t bind add slack ...` (or `teams`) currently fails with:
158
160
 
159
161
  ```text
160
- error: no adapter for channel 'telegram'. This is expected for F4 alone;
161
- the channel-specific adapter ships in a later feature.
162
+ error: no adapter for channel 'slack'. The channel-specific adapter is not
163
+ registered in this build.
162
164
  ```
163
165
 
164
- This is correct. F6 (Telegram adapter) will register the adapter at
165
- startup and unlock the create flow.
166
+ This is correct Slack and Teams adapters aren't registered yet. Once a
167
+ channel's adapter registers at gateway startup, binding creation for that
168
+ channel unlocks.
166
169
 
167
170
  ### Cascade-delete idempotency
168
171
 
package/dist/cli.js CHANGED
@@ -293,19 +293,9 @@ var init_secrets = __esm({
293
293
  });
294
294
 
295
295
  // ../../packages/github-app-auth/dist/esm/cache.js
296
- var cache_exports = {};
297
- __export(cache_exports, {
298
- EXPIRY_SAFETY_MS: () => EXPIRY_SAFETY_MS,
299
- _resetTokenCache: () => _resetTokenCache,
300
- getCachedToken: () => getCachedToken,
301
- putCachedToken: () => putCachedToken
302
- });
303
296
  function key(installationId, repository) {
304
297
  return `${installationId}:${repository}`;
305
298
  }
306
- function _resetTokenCache() {
307
- cache2.clear();
308
- }
309
299
  function getCachedToken(installationId, repository) {
310
300
  const e = cache2.get(key(installationId, repository));
311
301
  if (!e)
@@ -384,17 +374,18 @@ var init_mint = __esm({
384
374
  });
385
375
 
386
376
  // ../../packages/github-app-auth/dist/esm/rotate.js
377
+ function connectionStateKey(projectArmId, connectionName) {
378
+ return `${projectArmId}/connections/${connectionName}`;
379
+ }
387
380
  async function rotateConnectionAuth(args) {
388
- const { getCachedToken: getCachedToken2, putCachedToken: putCachedToken2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
389
- const before = getCachedToken2(args.installationId, args.repository);
390
381
  const minted = await mintInstallationToken({
391
382
  credential: args.credential,
392
383
  kvUri: args.kvUri,
393
384
  installationId: args.installationId,
394
385
  repository: args.repository
395
386
  });
396
- const cacheHit = before !== null && before.token === minted.token;
397
- if (!cacheHit) {
387
+ const stateKey = connectionStateKey(args.projectArmId, args.connectionName);
388
+ if (connectionPatchState.get(stateKey) !== minted.token) {
398
389
  const armTokenResp = await args.credential.getToken(ARM_SCOPE);
399
390
  if (!armTokenResp?.token) {
400
391
  throw new Error("rotateConnectionAuth: failed to acquire ARM management token");
@@ -423,17 +414,18 @@ async function rotateConnectionAuth(args) {
423
414
  throw new Error(`rotateConnectionAuth: HTTP ${String(res.status)} on PATCH ${url}
424
415
  ${text.slice(0, 500)}`);
425
416
  }
426
- putCachedToken2(args.installationId, args.repository, minted.token, minted.expiresAt);
417
+ connectionPatchState.set(stateKey, minted.token);
427
418
  }
428
419
  return { rotatedAt: /* @__PURE__ */ new Date(), expiresAt: minted.expiresAt };
429
420
  }
430
- var ARM_SCOPE, FOUNDRY_API;
421
+ var ARM_SCOPE, FOUNDRY_API, connectionPatchState;
431
422
  var init_rotate = __esm({
432
423
  "../../packages/github-app-auth/dist/esm/rotate.js"() {
433
424
  "use strict";
434
425
  init_mint();
435
426
  ARM_SCOPE = "https://management.azure.com/.default";
436
427
  FOUNDRY_API = "2025-04-01-preview";
428
+ connectionPatchState = /* @__PURE__ */ new Map();
437
429
  }
438
430
  });
439
431
 
@@ -673,6 +665,53 @@ var init_foundry_agent_get = __esm({
673
665
  }
674
666
  });
675
667
 
668
+ // src/lib/foundry-agent-version.ts
669
+ async function createAgentVersion(args, fetchImpl = fetch) {
670
+ const token = await args.credential.getToken(FOUNDRY_DATA_SCOPE);
671
+ if (!token?.token) throw new LocalCliError({ code: "FOUNDRY_AUTH", message: "Could not acquire Foundry data-plane token" });
672
+ const url = `${args.projectEndpoint}/agents/${args.agentName}/versions?api-version=v1`;
673
+ const res = await fetchImpl(url, {
674
+ method: "POST",
675
+ headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", ...args.extraHeaders ?? {} },
676
+ body: JSON.stringify({ definition: args.definition, metadata: args.metadata })
677
+ });
678
+ if (!res.ok) {
679
+ const text = await res.text();
680
+ throw new LocalCliError({ code: "AGENT_CREATE_VERSION_FAILED", message: `POST ${url}: HTTP ${res.status.toString()}
681
+ ${text.slice(0, 500)}` });
682
+ }
683
+ await ensureAgentEndpointEntraIsolation(
684
+ { credential: args.credential, projectEndpoint: args.projectEndpoint, agentName: args.agentName, extraHeaders: args.extraHeaders },
685
+ fetchImpl
686
+ );
687
+ const data = await res.json();
688
+ if (!data.version) throw new LocalCliError({ code: "AGENT_CREATE_VERSION_NO_VERSION", message: "createVersion returned no version" });
689
+ return data.version;
690
+ }
691
+ async function ensureAgentEndpointEntraIsolation(args, fetchImpl = fetch) {
692
+ const token = await args.credential.getToken(FOUNDRY_DATA_SCOPE);
693
+ if (!token?.token) throw new LocalCliError({ code: "FOUNDRY_AUTH", message: "Could not acquire Foundry data-plane token" });
694
+ const url = `${args.projectEndpoint}/agents/${args.agentName}?api-version=v1`;
695
+ const res = await fetchImpl(url, {
696
+ method: "PATCH",
697
+ headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", ...args.extraHeaders ?? {} },
698
+ body: JSON.stringify({ agent_endpoint: { authorization_schemes: [{ type: "Entra", isolation_key_source: { kind: "Entra" } }] } })
699
+ });
700
+ if (!res.ok) {
701
+ const text = await res.text();
702
+ throw new LocalCliError({ code: "AGENT_ENDPOINT_AUTH_PATCH_FAILED", message: `PATCH ${url}: HTTP ${res.status.toString()}
703
+ ${text.slice(0, 500)}` });
704
+ }
705
+ }
706
+ var FOUNDRY_DATA_SCOPE;
707
+ var init_foundry_agent_version = __esm({
708
+ "src/lib/foundry-agent-version.ts"() {
709
+ "use strict";
710
+ init_errors();
711
+ FOUNDRY_DATA_SCOPE = "https://ai.azure.com/.default";
712
+ }
713
+ });
714
+
676
715
  // src/lib/brain-yaml-mirror.ts
677
716
  import { parse as parseYaml4 } from "yaml";
678
717
  function linkSection(args) {
@@ -841,6 +880,12 @@ async function createCoderVersion(args) {
841
880
  foundryFeatures: "HostedAgents=V1Preview",
842
881
  metadata
843
882
  });
883
+ await ensureAgentEndpointEntraIsolation({
884
+ credential: args.credential,
885
+ projectEndpoint: args.endpoint,
886
+ agentName: args.name,
887
+ extraHeaders: { "Foundry-Features": "HostedAgents=V1Preview" }
888
+ });
844
889
  const v = version.version;
845
890
  if (v === void 0) {
846
891
  throw new LocalCliError({
@@ -905,6 +950,11 @@ async function createPromptVersion(args) {
905
950
  if (args.metadata.personaVersion !== null) metadata.personaVersion = args.metadata.personaVersion;
906
951
  if (args.metadata.fillableFieldValues !== void 0) metadata.fillableFieldValues = args.metadata.fillableFieldValues;
907
952
  const version = await project.agents.createVersion(args.name, definition, { metadata });
953
+ await ensureAgentEndpointEntraIsolation({
954
+ credential: args.credential,
955
+ projectEndpoint: args.endpoint,
956
+ agentName: args.name
957
+ });
908
958
  const v = version.version;
909
959
  if (v === void 0) {
910
960
  throw new LocalCliError({
@@ -931,6 +981,7 @@ var init_foundry_agents = __esm({
931
981
  "use strict";
932
982
  init_http();
933
983
  init_errors();
984
+ init_foundry_agent_version();
934
985
  METADATA_SOURCE = "m8t";
935
986
  FOUNDRY_SCOPE2 = "https://ai.azure.com/.default";
936
987
  API = "v1";
@@ -1247,7 +1298,7 @@ var init_enable_hosted_brain = __esm({
1247
1298
  import { Builtins, Cli } from "clipanion";
1248
1299
 
1249
1300
  // src/lib/package-version.ts
1250
- var CLI_VERSION = "0.2.35";
1301
+ var CLI_VERSION = "0.2.36";
1251
1302
 
1252
1303
  // src/lib/render-error.ts
1253
1304
  init_errors();
@@ -1341,7 +1392,7 @@ var BACKEND_RULES = [
1341
1392
  reason: "no_adapter_registered",
1342
1393
  hint: (e) => {
1343
1394
  const channel = getDetailField(e, "channel") ?? "<channel>";
1344
- return `no adapter for channel '${channel}'. This is expected for F4 alone; the channel-specific adapter ships in a later feature.`;
1395
+ return `no adapter for channel '${channel}'. The channel-specific adapter is not registered in this build.`;
1345
1396
  }
1346
1397
  },
1347
1398
  {
@@ -1453,6 +1504,14 @@ var LOCAL_RULES = {
1453
1504
  APP_HEALTH_FAILED: "Run `m8t brain check-app` for details, then re-run this command.",
1454
1505
  APP_UNINSTALL_FAILED: "GitHub App-uninstall DELETE failed. Re-run with `--keep-app-install` if you want to skip this step.",
1455
1506
  ARM_AUTH: "Run `az login` and retry.",
1507
+ // BOOTSTRAP_OCCUPANCY_UNVERIFIED and BOOTSTRAP_REINSTALL_TARGET_MISMATCH are
1508
+ // deliberately absent here: both are always thrown with an inline `hint`
1509
+ // (see bootstrap-occupancy.ts / commands/bootstrap/launch.ts), and
1510
+ // render-error.ts prefers that inline hint over this table — so a table
1511
+ // entry for either code would be dead text that could drift from what
1512
+ // actually prints. BOOTSTRAP_TARGET_OCCUPIED has no inline hint, so its
1513
+ // entry below is the live one.
1514
+ BOOTSTRAP_TARGET_OCCUPIED: "Pick an empty resource group with `--resource-group <new-rg>`, or run with both `--resource-group <rg>` and `--reinstall-into <rg>` (same group name) to install into the existing one anyway.",
1456
1515
  // m8t brain create: stageAsGitRepo step
1457
1516
  BRAIN_GIT_INIT_FAILED: "If git is not installed, install it. Otherwise re-run with --verbose.",
1458
1517
  BRAIN_NOT_LINKED: "The worker has no brain link. Use `m8t brain link` or `m8t brain create` first.",
@@ -11303,6 +11362,14 @@ function isUndiciDispatcherVersionMismatchError(error) {
11303
11362
  return false;
11304
11363
  }
11305
11364
 
11365
+ // ../../packages/foundry-invoke/dist/esm/openai-error.js
11366
+ function openAIErrorStatus(err) {
11367
+ if (typeof err !== "object" || err === null)
11368
+ return void 0;
11369
+ const status = err.status;
11370
+ return typeof status === "number" ? status : void 0;
11371
+ }
11372
+
11306
11373
  // ../../packages/foundry-invoke/dist/esm/invoke-cli-core.js
11307
11374
  function isBrainConnPropagation(err) {
11308
11375
  const e = err;
@@ -11329,9 +11396,10 @@ function errText(err) {
11329
11396
  return `${e.code ?? ""} ${e.error?.code ?? ""} ${e.error?.message ?? ""} ${e.message ?? ""}`.trim();
11330
11397
  }
11331
11398
  function errStatus(err) {
11399
+ const direct = openAIErrorStatus(err);
11400
+ if (direct !== void 0)
11401
+ return direct;
11332
11402
  const e = err;
11333
- if (typeof e.status === "number")
11334
- return e.status;
11335
11403
  if (typeof e.upstreamStatus === "number")
11336
11404
  return e.upstreamStatus;
11337
11405
  const m = /\b(4\d\d|5\d\d)\b/.exec(e.message ?? "");
@@ -11929,7 +11997,7 @@ var BindAddCommand = class extends M8tCommand {
11929
11997
  static paths = [["bind", "add"]];
11930
11998
  static usage = Command2.Usage({
11931
11999
  description: "Create a channel\u2192worker binding with bot token storage.",
11932
- details: "Stores the bot token in Key Vault and writes a Bindings table row. Returns the webhook URL the channel platform should call. F4-alone smoke fails with 'no_adapter_registered' until F6 (Telegram adapter) ships.",
12000
+ details: "Stores the bot token in Key Vault and writes a Bindings table row. Returns the webhook URL the channel platform should call. Channels without a registered adapter (Slack, Teams) fail with 'no_adapter_registered'.",
11933
12001
  examples: [
11934
12002
  [
11935
12003
  "Add a Telegram binding for the cmo worker",
@@ -13898,49 +13966,7 @@ function hasA2aSnippet(instructions) {
13898
13966
 
13899
13967
  // src/lib/brain-link.ts
13900
13968
  init_foundry_agent_get();
13901
-
13902
- // src/lib/foundry-agent-version.ts
13903
- init_errors();
13904
- var FOUNDRY_DATA_SCOPE = "https://ai.azure.com/.default";
13905
- async function createAgentVersion(args, fetchImpl = fetch) {
13906
- const token = await args.credential.getToken(FOUNDRY_DATA_SCOPE);
13907
- if (!token?.token) throw new LocalCliError({ code: "FOUNDRY_AUTH", message: "Could not acquire Foundry data-plane token" });
13908
- const url = `${args.projectEndpoint}/agents/${args.agentName}/versions?api-version=v1`;
13909
- const res = await fetchImpl(url, {
13910
- method: "POST",
13911
- headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", ...args.extraHeaders ?? {} },
13912
- body: JSON.stringify({ definition: args.definition, metadata: args.metadata })
13913
- });
13914
- if (!res.ok) {
13915
- const text = await res.text();
13916
- throw new LocalCliError({ code: "AGENT_CREATE_VERSION_FAILED", message: `POST ${url}: HTTP ${res.status.toString()}
13917
- ${text.slice(0, 500)}` });
13918
- }
13919
- const data = await res.json();
13920
- if (!data.version) throw new LocalCliError({ code: "AGENT_CREATE_VERSION_NO_VERSION", message: "createVersion returned no version" });
13921
- await ensureAgentEndpointEntraIsolation(
13922
- { credential: args.credential, projectEndpoint: args.projectEndpoint, agentName: args.agentName, extraHeaders: args.extraHeaders },
13923
- fetchImpl
13924
- );
13925
- return data.version;
13926
- }
13927
- async function ensureAgentEndpointEntraIsolation(args, fetchImpl = fetch) {
13928
- const token = await args.credential.getToken(FOUNDRY_DATA_SCOPE);
13929
- if (!token?.token) throw new LocalCliError({ code: "FOUNDRY_AUTH", message: "Could not acquire Foundry data-plane token" });
13930
- const url = `${args.projectEndpoint}/agents/${args.agentName}?api-version=v1`;
13931
- const res = await fetchImpl(url, {
13932
- method: "PATCH",
13933
- headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", ...args.extraHeaders ?? {} },
13934
- body: JSON.stringify({ agent_endpoint: { authorization_schemes: [{ type: "Entra", isolation_key_source: { kind: "Entra" } }] } })
13935
- });
13936
- if (!res.ok) {
13937
- const text = await res.text();
13938
- throw new LocalCliError({ code: "AGENT_ENDPOINT_AUTH_PATCH_FAILED", message: `PATCH ${url}: HTTP ${res.status.toString()}
13939
- ${text.slice(0, 500)}` });
13940
- }
13941
- }
13942
-
13943
- // src/lib/brain-link.ts
13969
+ init_foundry_agent_version();
13944
13970
  init_brain_yaml_mirror();
13945
13971
  var FOUNDRY_ARM_API = "2025-04-01-preview";
13946
13972
  var ARM_SCOPE4 = "https://management.azure.com/.default";
@@ -14131,7 +14157,7 @@ ${text.slice(0, 300)}`
14131
14157
  target: MCP_SERVER_URL,
14132
14158
  isSharedToAll: false,
14133
14159
  credentials: { keys: { Authorization: "Bearer placeholder-will-be-rotated" } },
14134
- metadata: { managedBy: "m8t-brain-f02" }
14160
+ metadata: { managedBy: "m8t-brain-link" }
14135
14161
  }
14136
14162
  });
14137
14163
  const putRes = await fetch(url, {
@@ -14967,7 +14993,7 @@ var BrainLinkCommand = class extends M8tCommand {
14967
14993
  static paths = [["brain", "link"]];
14968
14994
  static usage = Command19.Usage({
14969
14995
  description: "Link a worker to an existing brain repo. Idempotent.",
14970
- details: "Installs the GitHub App on <repo> (browser click + poll), mints an installation token + PATCHes the Foundry connection, deploys a new agent version with the brain loader + tools[type:mcp] + metadata.brain JSON string, and pushes .m8t/brain.yaml to the repo. F1's PAT mode keeps working untouched. --force re-runs the full link cascade (loader re-render + createVersion + brain.yaml mirror) even when metadata.brain already matches \u2014 the F1 rescue path.",
14996
+ details: "Installs the GitHub App on <repo> (browser click + poll), mints an installation token + PATCHes the Foundry connection, deploys a new agent version with the brain loader + tools[type:mcp] + metadata.brain JSON string, and pushes .m8t/brain.yaml to the repo. PAT mode keeps working untouched. --force re-runs the full link cascade (loader re-render + createVersion + brain.yaml mirror) even when metadata.brain already matches \u2014 useful to rescue a stuck or stale link.",
14971
14997
  examples: [
14972
14998
  ["Link cmo to orkeren21/cmo-brain", "$0 brain link cmo --repo orkeren21/cmo-brain"],
14973
14999
  ["Force a re-link (re-render brain loader + new agent version, even if already linked)", "$0 brain link cmo --repo orkeren21/cmo-brain --force"]
@@ -15242,6 +15268,7 @@ import * as fs12 from "fs";
15242
15268
  import * as os5 from "os";
15243
15269
  import * as path11 from "path";
15244
15270
  init_foundry_agent_get();
15271
+ init_foundry_agent_version();
15245
15272
  var FOUNDRY_ARM_API2 = "2025-04-01-preview";
15246
15273
  var ARM_SCOPE5 = "https://management.azure.com/.default";
15247
15274
  var GITHUB_APP_API = "https://api.github.com";
@@ -15687,6 +15714,7 @@ import { spawnSync as spawnSync2 } from "child_process";
15687
15714
  // src/lib/a2a-enable.ts
15688
15715
  init_errors();
15689
15716
  init_foundry_agent_get();
15717
+ init_foundry_agent_version();
15690
15718
  import { randomBytes as randomBytes2, createHash } from "crypto";
15691
15719
 
15692
15720
  // src/lib/data-plane-ready.ts
@@ -18399,6 +18427,7 @@ async function runBicepDeployment(opts) {
18399
18427
 
18400
18428
  // src/lib/platform-converge.ts
18401
18429
  init_foundry_agent_get();
18430
+ init_foundry_agent_version();
18402
18431
  init_errors();
18403
18432
 
18404
18433
  // src/lib/persona-compose.ts
@@ -20049,7 +20078,7 @@ var PlatformConvergeCommand = class extends M8tCommand {
20049
20078
  static usage = Command33.Usage({
20050
20079
  category: "Platform",
20051
20080
  description: "Headless updater-job driver: claim a pending apply-request and converge the platform to it.",
20052
- details: "The Managed-Identity-authenticated entry point run by the updater Container Apps job. One invocation is one tick: it claims the pending apply-request row (if any), refuses a downgrade against the installed stamp, self-fetches and validates the target manifest, self-updates its own engine image when the manifest requires a newer CLI, then drives the F02 converge engine behind the T15 health-gate + auto-rollback. Reads its subscription/RG/endpoint/channel from env (MI_CLIENT_ID, SUBSCRIPTION_ID, RESOURCE_GROUP, FOUNDRY_ENDPOINT, M8T_UPDATE_CHANNEL_URL). Not intended for interactive use \u2014 the founder-facing path is 'm8t platform update'."
20081
+ details: "The Managed-Identity-authenticated entry point run by the updater Container Apps job. One invocation is one tick: it claims the pending apply-request row (if any), refuses a downgrade against the installed stamp, self-fetches and validates the target manifest, self-updates its own engine image when the manifest requires a newer CLI, then drives the converge engine behind the health-gate + auto-rollback. Reads its subscription/RG/endpoint/channel from env (MI_CLIENT_ID, SUBSCRIPTION_ID, RESOURCE_GROUP, FOUNDRY_ENDPOINT, M8T_UPDATE_CHANNEL_URL). Not intended for interactive use \u2014 the founder-facing path is 'm8t platform update'."
20053
20082
  });
20054
20083
  async executeCommand() {
20055
20084
  const ctx = resolveHeadlessContextFromEnv(process.env);
@@ -21335,7 +21364,7 @@ function parseVerdict(stdout) {
21335
21364
  var EvalSkillCommand = class extends M8tCommand {
21336
21365
  static paths = [["eval", "skill"]];
21337
21366
  static usage = Command38.Usage({
21338
- description: "Vet one inbox skill candidate (Brain Faculties Eval F1): promote / reject / needs_review. Shells out to the Python `brain-eval` core (override its path with $BRAIN_EVAL_BIN)."
21367
+ description: "Vet one inbox skill candidate: promote / reject / needs_review. Shells out to the Python `brain-eval` core (override its path with $BRAIN_EVAL_BIN)."
21339
21368
  });
21340
21369
  candidate = Option35.String();
21341
21370
  skillsDir = Option35.String("--skills-dir");
@@ -21423,7 +21452,7 @@ function parseArmToken(tok, opts) {
21423
21452
  if (!opts.allowStub) {
21424
21453
  throw new LocalCliError({
21425
21454
  code: "USAGE",
21426
- message: `imported: arms are stub-only in F2 (a one-file placeholder) \u2014 pass --allow-stub to run one anyway, or use live/off/pinned for a real scored run (got '${tok}')`
21455
+ message: `imported arms are stub-only (a one-file placeholder) \u2014 pass --allow-stub to run one anyway, or use live/off/pinned for a real scored run (got '${tok}')`
21427
21456
  });
21428
21457
  }
21429
21458
  return { state: { imported: ref }, profile };
@@ -21531,7 +21560,7 @@ function resolveJudgeDeployment(flag, env) {
21531
21560
  if (typeof fromEnv === "string" && fromEnv.length > 0) return { deployment: fromEnv };
21532
21561
  return {
21533
21562
  deployment: "gpt-5-mini",
21534
- warning: "no --deployment and no $EXAM_JUDGE_DEPLOYMENT \u2014 falling back to gpt-5-mini as the judge. Name the DISTINCT judge deployment before any blessing/calibration run (DESIGN \xA75.5)."
21563
+ warning: "no --deployment and no $EXAM_JUDGE_DEPLOYMENT \u2014 falling back to gpt-5-mini as the judge. Name the DISTINCT judge deployment before any blessing/calibration run."
21535
21564
  };
21536
21565
  }
21537
21566
  var BRAIN_REPO_MARKERS = ["m8t-labs/", "/azure-advisor-brain", "/stacey-brain", "exam-arm-"];
@@ -21539,7 +21568,7 @@ function assertOutNotInBrainRepo(out) {
21539
21568
  if (BRAIN_REPO_MARKERS.some((m) => out.includes(m))) {
21540
21569
  throw new LocalCliError({
21541
21570
  code: "USAGE",
21542
- message: `--out '${out}' is under a brain repo \u2014 reports MUST live in the referee home (Law-1 guard, DESIGN \xA77.6)`
21571
+ message: `--out '${out}' is under a brain repo \u2014 reports MUST live in the referee home (Law-1 guard)`
21543
21572
  });
21544
21573
  }
21545
21574
  }
@@ -21631,7 +21660,7 @@ function buildPlan(args) {
21631
21660
  var EvalExamCommand = class extends M8tCommand {
21632
21661
  static paths = [["eval", "exam"]];
21633
21662
  static usage = Command39.Usage({
21634
- description: "Run a brain exam (Brain Referee E2-F2): impact A/B + dream-delta. Resolves the plan, then shells out to the Python `brain-exam` orchestrator (override its path with $BRAIN_EXAM_BIN). Renders an ExamVerdict: three-valued verdict + power note + per-task flips."
21663
+ description: "Run a brain exam: impact A/B + dream-delta. Resolves the plan, then shells out to the Python `brain-exam` orchestrator (override its path with $BRAIN_EXAM_BIN). Renders an ExamVerdict: three-valued verdict + power note + per-task flips."
21635
21664
  });
21636
21665
  worker = Option36.String();
21637
21666
  arms = Option36.String("--arms");
@@ -21704,7 +21733,7 @@ var EvalExamCommand = class extends M8tCommand {
21704
21733
  this.context.stdout.write(renderJson(plan) + "\n");
21705
21734
  this.context.stdout.write(
21706
21735
  `
21707
- (dry-run) ~${String(arms.length)} arms x n=${String(reps)}; judge grading fan-out is the cost driver \u2014 confirm the run sits inside the ~150-550 judge-call band before driving workers (DESIGN \xA710).
21736
+ (dry-run) ~${String(arms.length)} arms x n=${String(reps)}; judge grading fan-out is the cost driver \u2014 confirm the run sits inside the ~150-550 judge-call band before driving workers.
21708
21737
  `
21709
21738
  );
21710
21739
  return 0;
@@ -24307,7 +24336,7 @@ function forcePolicyQuarantine(delta) {
24307
24336
  return {
24308
24337
  verb: "quarantine",
24309
24338
  slug: slugify(delta.title),
24310
- reason: "code-side standing-policy/auto-approval/privilege assertion (F16)",
24339
+ reason: "code-side standing-policy/auto-approval/privilege assertion",
24311
24340
  body: delta.body,
24312
24341
  evidence: delta.evidence
24313
24342
  };
@@ -25680,6 +25709,17 @@ function ingestUrl(path38) {
25680
25709
  // src/commands/bootstrap/preflight.ts
25681
25710
  init_errors();
25682
25711
 
25712
+ // src/lib/spend-disclosure.ts
25713
+ var SPEND_DISCLOSURE = [
25714
+ "You are about to install the m8t platform.",
25715
+ "",
25716
+ " - It installs into YOUR Azure subscription and YOUR GitHub account or organization.",
25717
+ " - The Azure resources it creates bill to your Azure account - this uses your budget.",
25718
+ " - With your workers installed, you can ask Azzy what you're spending at any time,",
25719
+ " and you can turn on a cost report by email every two weeks.",
25720
+ " - You can remove it later - see guides/uninstall.md."
25721
+ ].join("\n");
25722
+
25683
25723
  // src/lib/bootstrap-preflight.ts
25684
25724
  var ADMIN_DIRECTORY_ROLES = ["Global Administrator", "Application Administrator", "Cloud Application Administrator"];
25685
25725
  async function checkSubScopeAdmin(callerObjectId, subscriptionId) {
@@ -25766,6 +25806,7 @@ var BootstrapPreflightCommand = class extends M8tCommand {
25766
25806
  const account = await getAzAccount();
25767
25807
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
25768
25808
  const oid = await getCallerObjectId();
25809
+ this.context.stdout.write(SPEND_DISCLOSURE + "\n\n");
25769
25810
  this.context.stdout.write(buildPreflightBanner({ upn: account.upn, tenantId: account.tenantId, subscriptionId }) + "\n");
25770
25811
  this.context.stdout.write(`
25771
25812
  ${colors.dim(DISCLOSURE_TIER1)}
@@ -26076,10 +26117,95 @@ async function writeBootstrapState(state, home = os13.homedir()) {
26076
26117
  await fs29.rename(tmp, statePath(home));
26077
26118
  }
26078
26119
 
26120
+ // src/lib/bootstrap-occupancy.ts
26121
+ init_errors();
26122
+ var SAMPLE_LIMIT = 3;
26123
+ var M8T_NAME_RE = /^m8t/i;
26124
+ async function checkTargetRgOccupancy(opts) {
26125
+ let existsRaw;
26126
+ try {
26127
+ existsRaw = await runAz([
26128
+ "group",
26129
+ "exists",
26130
+ "--name",
26131
+ opts.resourceGroup,
26132
+ "--subscription",
26133
+ opts.subscriptionId,
26134
+ "-o",
26135
+ "tsv"
26136
+ ]);
26137
+ } catch (cause) {
26138
+ throw unverified(opts.resourceGroup, cause);
26139
+ }
26140
+ const exists2 = existsRaw.trim().toLowerCase();
26141
+ if (exists2 === "false") return { state: "absent", found: [], total: 0, m8tShaped: false };
26142
+ if (exists2 !== "true") {
26143
+ throw unverified(opts.resourceGroup, new Error(`unexpected 'az group exists' output: ${exists2}`));
26144
+ }
26145
+ let names;
26146
+ try {
26147
+ const listRaw = await runAz([
26148
+ "resource",
26149
+ "list",
26150
+ "--resource-group",
26151
+ opts.resourceGroup,
26152
+ "--subscription",
26153
+ opts.subscriptionId,
26154
+ "--query",
26155
+ "[].name",
26156
+ "-o",
26157
+ "json"
26158
+ ]);
26159
+ names = JSON.parse(listRaw);
26160
+ } catch (cause) {
26161
+ throw unverified(opts.resourceGroup, cause);
26162
+ }
26163
+ if (!Array.isArray(names)) {
26164
+ throw unverified(opts.resourceGroup, new Error("resource listing was not an array"));
26165
+ }
26166
+ if (names.length === 0) return { state: "empty", found: [], total: 0, m8tShaped: false };
26167
+ return {
26168
+ state: "occupied",
26169
+ found: names.slice(0, SAMPLE_LIMIT),
26170
+ total: names.length,
26171
+ // Computed BEFORE the cap: an m8t-named resource sorting past the sample
26172
+ // must still escalate the wording.
26173
+ m8tShaped: names.some((n) => M8T_NAME_RE.test(n))
26174
+ };
26175
+ }
26176
+ function unverified(resourceGroup, cause) {
26177
+ return new LocalCliError({
26178
+ code: "BOOTSTRAP_OCCUPANCY_UNVERIFIED",
26179
+ message: `Could not verify whether resource group '${resourceGroup}' is empty.`,
26180
+ hint: "Check your connection and 'az login', then retry.",
26181
+ cause
26182
+ });
26183
+ }
26184
+ function buildOccupiedRefusal(args) {
26185
+ const { resourceGroup: rg, subscriptionId, location, occupancy: o } = args;
26186
+ const hidden = o.total - o.found.length;
26187
+ const more = hidden > 0 ? ` (+${String(hidden)} more)` : "";
26188
+ const risk = o.m8tShaped ? "Installing into a non-empty resource group risks overwriting what's already there.\n This looks like an existing m8t deployment; installing here would overwrite its identity." : "Installing into a non-empty resource group risks overwriting what's already there.";
26189
+ return [
26190
+ "",
26191
+ "\u26D4 CANNOT PROCEED",
26192
+ ` Resource group '${rg}' in subscription ${subscriptionId} is not empty \u2014 it holds`,
26193
+ ` ${String(o.total)} resources, including: ${o.found.join(", ")}${more}.`,
26194
+ ` ${risk}`,
26195
+ " remedy: pick an empty resource group \u2014",
26196
+ ` m8t bootstrap launch --location ${location} --resource-group <new-rg>`,
26197
+ " or, if you really mean to install into this one:",
26198
+ ` m8t bootstrap launch --location ${location} --resource-group ${rg} --reinstall-into ${rg}`,
26199
+ " Nothing has been created.",
26200
+ "",
26201
+ ""
26202
+ ].join("\n");
26203
+ }
26204
+
26079
26205
  // src/commands/bootstrap/launch.ts
26080
26206
  var DEFAULT_RG = "rg-m8t-stack";
26081
26207
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
26082
- var DEFAULT_INSTALLER_TAG = "v0.1.41";
26208
+ var DEFAULT_INSTALLER_TAG = "v0.1.42";
26083
26209
  var ACI_NAME = "m8t-installer";
26084
26210
  var MI_NAME = "m8t-installer-mi";
26085
26211
  var BootstrapLaunchCommand = class extends M8tCommand {
@@ -26108,12 +26234,18 @@ var BootstrapLaunchCommand = class extends M8tCommand {
26108
26234
  githubAppCreds = Option47.String("--github-app-creds");
26109
26235
  contactEmail = Option47.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
26110
26236
  company = Option47.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
26237
+ // Value-carrying on purpose: a bare --force would be cargo-culted into
26238
+ // runbooks and harness prompts and erode the protection, whereas a faithful
26239
+ // paste can never accidentally carry the victim group's name. It AUTHORIZES
26240
+ // the target; --resource-group is what CHOOSES it.
26241
+ reinstallInto = Option47.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
26111
26242
  async executeCommand() {
26112
26243
  const location = typeof this.location === "string" ? this.location : void 0;
26113
26244
  if (!location) {
26114
26245
  throw new LocalCliError({ code: "USAGE", message: "--location is required.", hint: "Example: m8t bootstrap launch --location eastus2" });
26115
26246
  }
26116
- const resourceGroup = (typeof this.resourceGroup === "string" ? this.resourceGroup : void 0) ?? DEFAULT_RG;
26247
+ const resourceGroupOpt = typeof this.resourceGroup === "string" ? this.resourceGroup : void 0;
26248
+ const resourceGroup = resourceGroupOpt ?? DEFAULT_RG;
26117
26249
  const clientIdOpt = typeof this.clientId === "string" ? this.clientId : void 0;
26118
26250
  const installerTag = (typeof this.installerTag === "string" ? this.installerTag : void 0) ?? DEFAULT_INSTALLER_TAG;
26119
26251
  const installerImageOverride = typeof this.installerImage === "string" ? this.installerImage : void 0;
@@ -26123,6 +26255,28 @@ var BootstrapLaunchCommand = class extends M8tCommand {
26123
26255
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
26124
26256
  const out = (m) => this.context.stderr.write(` ${colors.dim(m)}
26125
26257
  `);
26258
+ const reinstallInto = typeof this.reinstallInto === "string" ? this.reinstallInto : void 0;
26259
+ if (reinstallInto !== void 0 && reinstallInto.toLowerCase() !== resourceGroup.toLowerCase()) {
26260
+ const targetExplanation = resourceGroupOpt === void 0 ? `you did not pass --resource-group, so the target is the default '${DEFAULT_RG}'` : `the target resource group is '${resourceGroup}'`;
26261
+ throw new LocalCliError({
26262
+ code: "BOOTSTRAP_REINSTALL_TARGET_MISMATCH",
26263
+ message: `--reinstall-into '${reinstallInto}' does not match \u2014 ${targetExplanation}.`,
26264
+ hint: `--reinstall-into authorizes the target; it does not choose it. Re-run with --resource-group ${reinstallInto} on its own first \u2014 the guard will show you what that group holds.`
26265
+ });
26266
+ }
26267
+ const occupancy = await checkTargetRgOccupancy({ resourceGroup, subscriptionId });
26268
+ if (occupancy.state === "occupied" && reinstallInto === void 0) {
26269
+ this.context.stderr.write(buildOccupiedRefusal({ resourceGroup, subscriptionId, location, occupancy }));
26270
+ throw new LocalCliError({
26271
+ code: "BOOTSTRAP_TARGET_OCCUPIED",
26272
+ message: `Resource group '${resourceGroup}' already holds ${String(occupancy.total)} resources.`
26273
+ });
26274
+ }
26275
+ if (this.context.env.M8T_HALT_AFTER_OCCUPANCY === "1") {
26276
+ this.context.stdout.write(`halt: occupancy check passed (${occupancy.state}) for ${resourceGroup}
26277
+ `);
26278
+ return 0;
26279
+ }
26126
26280
  const credsPath = typeof this.githubAppCreds === "string" ? this.githubAppCreds : path32.join(os14.homedir(), ".m8t", "github-app.json");
26127
26281
  let githubApp;
26128
26282
  if (fs30.existsSync(credsPath)) {