@fabricorg/cli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fabric.js CHANGED
@@ -6,7 +6,7 @@ import { Command as Command22 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@fabricorg/cli",
9
- version: "0.6.0",
9
+ version: "0.6.1",
10
10
  description: "CLI for the Fabric AI platform",
11
11
  keywords: [
12
12
  "fabric",
@@ -235,10 +235,10 @@ var FabricHttpClient = class _FabricHttpClient {
235
235
  * Inject default `org` / `personal` query params if the path doesn't already
236
236
  * carry one. Resources that explicitly pass `?org=other` are untouched.
237
237
  */
238
- buildUrl(path29) {
239
- const url = `${this.baseUrl}/api/v1${path29}`;
240
- const hasOrg = /[?&]org=/.test(path29);
241
- const hasPersonal = /[?&]personal=/.test(path29);
238
+ buildUrl(path30) {
239
+ const url = `${this.baseUrl}/api/v1${path30}`;
240
+ const hasOrg = /[?&]org=/.test(path30);
241
+ const hasPersonal = /[?&]personal=/.test(path30);
242
242
  if (hasOrg || hasPersonal) {
243
243
  return url;
244
244
  }
@@ -251,11 +251,11 @@ var FabricHttpClient = class _FabricHttpClient {
251
251
  if (additions.length === 0) {
252
252
  return url;
253
253
  }
254
- const sep = path29.includes("?") ? "&" : "?";
254
+ const sep = path30.includes("?") ? "&" : "?";
255
255
  return `${url}${sep}${additions.join("&")}`;
256
256
  }
257
- async request(method, path29, options = {}) {
258
- const url = this.buildUrl(path29);
257
+ async request(method, path30, options = {}) {
258
+ const url = this.buildUrl(path30);
259
259
  const isMutating = MUTATING_METHODS.has(method);
260
260
  const idempotencyKey = options.idempotencyKey ?? (isMutating ? randomId() : void 0);
261
261
  const maxAttempts = (options.retry ?? this.retry).maxRetries + 1;
@@ -265,7 +265,7 @@ var FabricHttpClient = class _FabricHttpClient {
265
265
  throw options.signal.reason;
266
266
  }
267
267
  const start = Date.now();
268
- this.emit({ kind: "request", method, path: path29, attempt: attempt2 });
268
+ this.emit({ kind: "request", method, path: path30, attempt: attempt2 });
269
269
  try {
270
270
  const result = await this.executeOnce(
271
271
  method,
@@ -276,7 +276,7 @@ var FabricHttpClient = class _FabricHttpClient {
276
276
  this.emit({
277
277
  kind: "response",
278
278
  method,
279
- path: path29,
279
+ path: path30,
280
280
  attempt: attempt2,
281
281
  status: 200,
282
282
  durationMs: Date.now() - start
@@ -289,7 +289,7 @@ var FabricHttpClient = class _FabricHttpClient {
289
289
  this.emit({
290
290
  kind: retriable ? "retry" : "error",
291
291
  method,
292
- path: path29,
292
+ path: path30,
293
293
  attempt: attempt2,
294
294
  status,
295
295
  durationMs: Date.now() - start,
@@ -433,28 +433,28 @@ var FabricHttpClient = class _FabricHttpClient {
433
433
  } catch {
434
434
  }
435
435
  }
436
- get(path29, options = {}) {
436
+ get(path30, options = {}) {
437
437
  return this.request(
438
438
  "GET",
439
- path29,
439
+ path30,
440
440
  options.signal ? { signal: options.signal } : {}
441
441
  );
442
442
  }
443
- post(path29, body, options = {}) {
444
- return this.request("POST", path29, {
443
+ post(path30, body, options = {}) {
444
+ return this.request("POST", path30, {
445
445
  body,
446
446
  idempotencyKey: options.idempotencyKey,
447
447
  retry: options.retry
448
448
  });
449
449
  }
450
- patch(path29, body, options = {}) {
451
- return this.request("PATCH", path29, {
450
+ patch(path30, body, options = {}) {
451
+ return this.request("PATCH", path30, {
452
452
  body,
453
453
  idempotencyKey: options.idempotencyKey
454
454
  });
455
455
  }
456
- put(path29, body, options = {}) {
457
- return this.request("PUT", path29, {
456
+ put(path30, body, options = {}) {
457
+ return this.request("PUT", path30, {
458
458
  body,
459
459
  idempotencyKey: options.idempotencyKey,
460
460
  retry: options.retry
@@ -465,8 +465,8 @@ var FabricHttpClient = class _FabricHttpClient {
465
465
  * delete names the path and the version in its body). Without `body`,
466
466
  * nothing is sent, as before.
467
467
  */
468
- delete(path29, body, options = {}) {
469
- return this.request("DELETE", path29, {
468
+ delete(path30, body, options = {}) {
469
+ return this.request("DELETE", path30, {
470
470
  body,
471
471
  idempotencyKey: options.idempotencyKey
472
472
  });
@@ -1091,14 +1091,14 @@ function buildQuery3(opts) {
1091
1091
 
1092
1092
  // ../sdk/src/resources/integrations.ts
1093
1093
  var RESOURCE_BUILTINS = /* @__PURE__ */ new Set(["list", "constructor", "then"]);
1094
- function makeDotPath(http, slug, path29) {
1095
- const fn = (args) => http.post(`/integrations/${slug}/${path29.join(".")}`, args ?? {});
1094
+ function makeDotPath(http, slug, path30) {
1095
+ const fn = (args) => http.post(`/integrations/${slug}/${path30.join(".")}`, args ?? {});
1096
1096
  return new Proxy(fn, {
1097
1097
  get(target, prop) {
1098
1098
  if (prop in target || typeof prop !== "string") {
1099
1099
  return Reflect.get(target, prop);
1100
1100
  }
1101
- return makeDotPath(http, slug, [...path29, prop]);
1101
+ return makeDotPath(http, slug, [...path30, prop]);
1102
1102
  }
1103
1103
  });
1104
1104
  }
@@ -2238,19 +2238,23 @@ function pathAsCommandWord(file, platform = process.platform) {
2238
2238
  }
2239
2239
  return `"${normalised}"`;
2240
2240
  }
2241
- function launcherWords(origin = launcherOrigin ?? implicitOrigin(), script) {
2241
+ function launcherWords(script) {
2242
2242
  if (!isServedBundle()) {
2243
2243
  return ["fabric"];
2244
2244
  }
2245
2245
  const tarball = bundleTarballPath();
2246
- if (tarball !== void 0) {
2247
- return ["npx", "-y", `${origin}${tarball}`];
2246
+ const packedFor = bakedOrigin();
2247
+ if (tarball !== void 0 && packedFor !== void 0) {
2248
+ return ["npx", "-y", `${packedFor}${tarball}`];
2248
2249
  }
2249
2250
  const word = pathAsCommandWord(script ?? bundleScriptPath());
2250
2251
  return word === null ? ["fabric"] : ["node", word];
2251
2252
  }
2252
- function printableLauncherWords(origin = launcherOrigin ?? implicitOrigin()) {
2253
- const words = launcherWords(origin);
2253
+ function printableLauncherWords() {
2254
+ if (launcherOrigin !== void 0 && launcherOrigin !== implicitOrigin() && !isSafeArgument(launcherOrigin)) {
2255
+ return null;
2256
+ }
2257
+ const words = launcherWords();
2254
2258
  return words[0] === "npx" && !isSafeArgument(words[2] ?? "") ? null : words;
2255
2259
  }
2256
2260
  function launcherPathText() {
@@ -2262,7 +2266,7 @@ function implicitOrigin() {
2262
2266
  }
2263
2267
  function fabricCommand(args, origin = launcherOrigin) {
2264
2268
  const deployment = origin ?? implicitOrigin();
2265
- const words = printableLauncherWords(deployment);
2269
+ const words = printableLauncherWords();
2266
2270
  const bound = deployment !== implicitOrigin() && !args.includes("--base-url") ? ` --base-url ${deployment}` : "";
2267
2271
  if (words === null || bound !== "" && !isSafeArgument(deployment)) {
2268
2272
  return NO_COMMAND;
@@ -2341,8 +2345,8 @@ function sameOriginEndpoint(value, field2, issuer) {
2341
2345
  }
2342
2346
  function authorizationServerMetadataUrl(issuer) {
2343
2347
  const url = new URL(issuer);
2344
- const path29 = url.pathname.replace(/\/+$/, "");
2345
- return `${url.origin}/.well-known/oauth-authorization-server${path29}`;
2348
+ const path30 = url.pathname.replace(/\/+$/, "");
2349
+ return `${url.origin}/.well-known/oauth-authorization-server${path30}`;
2346
2350
  }
2347
2351
  async function discoverAuthorizationServer(baseUrl, options = {}) {
2348
2352
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
@@ -2399,6 +2403,20 @@ async function discoverAuthorizationServer(baseUrl, options = {}) {
2399
2403
  import { createServer } from "http";
2400
2404
  var CALLBACK_PATH = "/callback";
2401
2405
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2406
+ var CallbackError = class extends Error {
2407
+ constructor(error, description) {
2408
+ super(
2409
+ error === "access_denied" ? "Sign-in was denied in the browser." : `Sign-in failed (${error}).`
2410
+ );
2411
+ this.error = error;
2412
+ this.description = description;
2413
+ this.name = "CallbackError";
2414
+ }
2415
+ };
2416
+ var FINAL_PAGE_HEADERS = {
2417
+ "Content-Type": "text/html; charset=utf-8",
2418
+ Connection: "close"
2419
+ };
2402
2420
  var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-weight:400">${title}</h1><p>${body}</p></body>`;
2403
2421
  async function startLoopbackListener(options) {
2404
2422
  const callbackPath = options.callbackPath ?? CALLBACK_PATH;
@@ -2415,9 +2433,9 @@ async function startLoopbackListener(options) {
2415
2433
  });
2416
2434
  result.catch(() => {
2417
2435
  });
2418
- const server = createServer((request, response) => {
2419
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
2420
- if (request.method !== "GET" || url.pathname !== callbackPath) {
2436
+ const server = createServer((request2, response) => {
2437
+ const url = new URL(request2.url ?? "/", "http://127.0.0.1");
2438
+ if (request2.method !== "GET" || url.pathname !== callbackPath) {
2421
2439
  response.writeHead(404).end();
2422
2440
  return;
2423
2441
  }
@@ -2434,17 +2452,17 @@ async function startLoopbackListener(options) {
2434
2452
  return;
2435
2453
  }
2436
2454
  if (error) {
2437
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(
2455
+ const failure = new CallbackError(
2456
+ error,
2457
+ url.searchParams.get("error_description")
2458
+ );
2459
+ response.writeHead(200, FINAL_PAGE_HEADERS).end(
2438
2460
  PAGE(
2439
2461
  "Sign-in cancelled",
2440
2462
  "You can close this tab and return to your terminal."
2441
- )
2463
+ ),
2464
+ () => settle({ error: failure })
2442
2465
  );
2443
- settle({
2444
- error: new Error(
2445
- error === "access_denied" ? "Sign-in was denied in the browser." : `Sign-in failed (${error}).`
2446
- )
2447
- });
2448
2466
  return;
2449
2467
  }
2450
2468
  if (!code) {
@@ -2456,13 +2474,13 @@ async function startLoopbackListener(options) {
2456
2474
  );
2457
2475
  return;
2458
2476
  }
2459
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(
2477
+ response.writeHead(200, FINAL_PAGE_HEADERS).end(
2460
2478
  PAGE(
2461
2479
  "Signed in",
2462
2480
  "You can close this tab and return to your terminal."
2463
- )
2481
+ ),
2482
+ () => settle({ code, params: url.searchParams })
2464
2483
  );
2465
- settle({ code, params: url.searchParams });
2466
2484
  });
2467
2485
  const listen = (port2) => new Promise((resolve, reject) => {
2468
2486
  server.once("error", reject);
@@ -2526,15 +2544,73 @@ function isProjectId(value) {
2526
2544
  function projectResource(origin, audience, projectId) {
2527
2545
  return `${origin.replace(/\/+$/, "")}${RESOURCE_BASE_PATH[audience]}/projects/${projectId}`;
2528
2546
  }
2547
+ function normalizeServerUrl(value) {
2548
+ let parsed;
2549
+ try {
2550
+ parsed = new URL(value);
2551
+ } catch {
2552
+ return null;
2553
+ }
2554
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.username !== "" || parsed.password !== "") {
2555
+ return null;
2556
+ }
2557
+ return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}${parsed.search}${parsed.hash}`;
2558
+ }
2529
2559
  function isGatewayUrlOf(origin, url) {
2530
- const gateway = `${origin.replace(/\/+$/, "")}${RESOURCE_BASE_PATH.mcp}`;
2531
- return url === gateway || new RegExp(`^${escaped(gateway)}/projects/[A-Za-z0-9_-]{1,64}$`).test(
2532
- url
2560
+ const gateway = normalizeServerUrl(
2561
+ `${origin.replace(/\/+$/, "")}${RESOURCE_BASE_PATH.mcp}`
2562
+ );
2563
+ const candidate = normalizeServerUrl(url);
2564
+ if (gateway === null || candidate === null) {
2565
+ return false;
2566
+ }
2567
+ return candidate === gateway || new RegExp(`^${escaped(gateway)}/projects/[A-Za-z0-9_-]{1,64}$`).test(
2568
+ candidate
2569
+ );
2570
+ }
2571
+ function isOrgGatewayUrlOf(origin, url) {
2572
+ const gateway = normalizeServerUrl(
2573
+ `${origin.replace(/\/+$/, "")}${RESOURCE_BASE_PATH.mcp}`
2533
2574
  );
2575
+ return gateway !== null && normalizeServerUrl(url) === gateway;
2534
2576
  }
2535
2577
  function escaped(text3) {
2536
2578
  return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2537
2579
  }
2580
+ function originOf2(value) {
2581
+ const normal = normalizeServerUrl(value);
2582
+ return normal === null ? null : new URL(normal).origin;
2583
+ }
2584
+ function classifyGatewayUrl(origin, projectId, url) {
2585
+ const candidate = normalizeServerUrl(url);
2586
+ if (candidate === null || originOf2(url) !== originOf2(origin)) {
2587
+ return "foreign";
2588
+ }
2589
+ if (candidate === normalizeServerUrl(projectResource(origin, "mcp", projectId))) {
2590
+ return "this-project";
2591
+ }
2592
+ if (isOrgGatewayUrlOf(origin, url)) {
2593
+ return "org-wide";
2594
+ }
2595
+ return isGatewayUrlOf(origin, url) ? "other-project" : "unfamiliar";
2596
+ }
2597
+ function classifyGatewayResource(origin, projectId, resource) {
2598
+ const relation = classifyGatewayUrl(origin, projectId, resource);
2599
+ if (relation !== "unfamiliar") {
2600
+ return relation;
2601
+ }
2602
+ const api = normalizeServerUrl(
2603
+ `${origin.replace(/\/+$/, "")}${RESOURCE_BASE_PATH.api}`
2604
+ );
2605
+ const candidate = normalizeServerUrl(resource);
2606
+ const named = api === null || candidate === null ? null : new RegExp(
2607
+ `^${escaped(api)}/projects/([A-Za-z0-9_-]{1,64})$`
2608
+ ).exec(candidate);
2609
+ if (named === null) {
2610
+ return "unfamiliar";
2611
+ }
2612
+ return named[1] === projectId ? "this-project" : "other-project";
2613
+ }
2538
2614
 
2539
2615
  // src/lib/oauth/flow.ts
2540
2616
  var REQUESTED_SCOPES = [
@@ -4111,7 +4187,7 @@ async function loginInBrowser(opts) {
4111
4187
  if (origin === null) {
4112
4188
  printError(BAD_BASE_URL_LINE, 2);
4113
4189
  }
4114
- const forProject = opts.project === void 0 ? "" : ` for project ${opts.project}`;
4190
+ const forProject2 = opts.project === void 0 ? "" : ` for project ${opts.project}`;
4115
4191
  let identity;
4116
4192
  try {
4117
4193
  identity = await signInWithBrowser({
@@ -4121,7 +4197,7 @@ async function loginInBrowser(opts) {
4121
4197
  project: opts.project,
4122
4198
  announce: (url) => {
4123
4199
  process.stdout.write(
4124
- `Opening your browser to sign in to ${effectiveBaseUrl}${forProject}.
4200
+ `Opening your browser to sign in to ${effectiveBaseUrl}${forProject2}.
4125
4201
  If it does not open, visit:
4126
4202
 
4127
4203
  ${url}
@@ -4137,7 +4213,7 @@ If it does not open, visit:
4137
4213
  throw error;
4138
4214
  }
4139
4215
  printSuccess(
4140
- `Authenticated as ${identity.name} (${identity.email})${forProject}`
4216
+ `Authenticated as ${identity.name} (${identity.email})${forProject2}`
4141
4217
  );
4142
4218
  }
4143
4219
  function buildLoginCommand() {
@@ -4649,16 +4725,47 @@ var AGENT_NAME = "Fabric";
4649
4725
  function registrationPath() {
4650
4726
  return join(dirname(getConfigPath()), "chatgpt-plan-host.json");
4651
4727
  }
4652
- async function loadRegistration(path29 = registrationPath()) {
4728
+ async function loadRegistrationStore(path30 = registrationPath()) {
4729
+ let stored;
4653
4730
  try {
4654
- return JSON.parse(await readFile(path29, "utf8"));
4731
+ stored = JSON.parse(await readFile(path30, "utf8"));
4655
4732
  } catch {
4656
- return { hostId: `urn:uuid:${randomUUID2()}` };
4733
+ stored = {};
4734
+ }
4735
+ const hostId = stored.hostId ?? `urn:uuid:${randomUUID2()}`;
4736
+ if (stored.accounts && typeof stored.accounts === "object") {
4737
+ return { hostId, accounts: stored.accounts };
4657
4738
  }
4739
+ return {
4740
+ hostId,
4741
+ accounts: stored.subject && stored.clientId ? { [stored.subject]: { clientId: stored.clientId } } : {}
4742
+ };
4658
4743
  }
4659
- async function saveRegistration(registration, path29 = registrationPath()) {
4660
- await mkdir(dirname(path29), { recursive: true, mode: 448 });
4661
- await writeFile(path29, JSON.stringify(registration), { mode: 384 });
4744
+ async function saveAccountRegistration(account, path30 = registrationPath()) {
4745
+ const store = await loadRegistrationStore(path30);
4746
+ const { hostId, subject, ...registration } = account;
4747
+ const next = {
4748
+ hostId,
4749
+ accounts: { ...store.accounts, [subject]: registration }
4750
+ };
4751
+ await mkdir(dirname(path30), { recursive: true, mode: 448 });
4752
+ await writeFile(path30, JSON.stringify(next), { mode: 384 });
4753
+ }
4754
+ function selectRegistration(store, account) {
4755
+ if (account === void 0) {
4756
+ return { hostId: store.hostId };
4757
+ }
4758
+ const email = account.toLowerCase();
4759
+ const match = Object.entries(store.accounts).find(
4760
+ ([subject2, saved2]) => subject2 === account || saved2.email?.toLowerCase() === email
4761
+ );
4762
+ if (!match) {
4763
+ throw new Error(
4764
+ `No ChatGPT account ${account} is saved on this machine; run without --account to sign in with a new registration`
4765
+ );
4766
+ }
4767
+ const [subject, saved] = match;
4768
+ return { hostId: store.hostId, clientId: saved.clientId, subject };
4662
4769
  }
4663
4770
  function randomToken() {
4664
4771
  return randomBytes2(24).toString("base64url");
@@ -4747,6 +4854,84 @@ function checkIdToken(idToken, expected) {
4747
4854
  }
4748
4855
  return { sub: claims.sub, email: claims.email };
4749
4856
  }
4857
+ var WORKSPACE_DENIED_ERROR = "3p_login_workspace_scope_denied";
4858
+ function isWorkspaceDenied(err) {
4859
+ if (err instanceof CallbackError) {
4860
+ return err.error === WORKSPACE_DENIED_ERROR || /workspace/i.test(err.description ?? "");
4861
+ }
4862
+ return err instanceof Error && err.message.includes(WORKSPACE_DENIED_ERROR);
4863
+ }
4864
+ function explainDenial(err) {
4865
+ if (!isWorkspaceDenied(err)) {
4866
+ return err;
4867
+ }
4868
+ const description = err instanceof CallbackError && err.description ? ` ${err.description}` : "";
4869
+ return new Error(
4870
+ `${err.message}${description} ChatGPT refused it for that workspace: run \`fabric connect chatgpt\` again without --account (the same as --new-registration) and choose your personal Plus or Pro workspace when ChatGPT asks which one to sign in to.`
4871
+ );
4872
+ }
4873
+ async function attemptSignIn(registration, options) {
4874
+ const verifier = createCodeVerifier();
4875
+ const state = createState();
4876
+ const nonce = randomToken();
4877
+ const listener = await startLoopbackListener({
4878
+ state,
4879
+ callbackPath: CHATGPT_CALLBACK_PATH,
4880
+ preferredPort: options.preferredPort ?? CHATGPT_CALLBACK_PORT
4881
+ });
4882
+ try {
4883
+ options.open(
4884
+ buildAuthorizeUrl({
4885
+ clientId: registration.clientId ?? DYNAMIC_CLIENT_ID,
4886
+ hostId: registration.hostId,
4887
+ redirectUri: listener.redirectUri,
4888
+ state,
4889
+ nonce,
4890
+ codeChallenge: codeChallengeS256(verifier)
4891
+ })
4892
+ );
4893
+ const { code, params } = await listener.result;
4894
+ const clientId = issuedClientId(params, registration);
4895
+ const tokens = await exchangeCode(
4896
+ {
4897
+ clientId,
4898
+ code,
4899
+ codeVerifier: verifier,
4900
+ redirectUri: listener.redirectUri
4901
+ },
4902
+ options.fetchImpl
4903
+ );
4904
+ const { sub, email } = checkIdToken(tokens.id_token ?? "", {
4905
+ nonce,
4906
+ subject: registration.subject
4907
+ });
4908
+ await options.save?.({
4909
+ hostId: registration.hostId,
4910
+ subject: sub,
4911
+ clientId,
4912
+ ...email !== void 0 && { email }
4913
+ });
4914
+ return { tokens, clientId, subject: sub, email };
4915
+ } finally {
4916
+ listener.close();
4917
+ }
4918
+ }
4919
+ async function signInWithChatGpt(options) {
4920
+ const { registration } = options;
4921
+ try {
4922
+ return await attemptSignIn(registration, options);
4923
+ } catch (err) {
4924
+ if (!registration.clientId || !isWorkspaceDenied(err)) {
4925
+ throw explainDenial(err);
4926
+ }
4927
+ }
4928
+ options.onRetry?.();
4929
+ try {
4930
+ return await attemptSignIn({ hostId: registration.hostId }, options);
4931
+ } catch (err) {
4932
+ throw explainDenial(err);
4933
+ }
4934
+ }
4750
4935
  function buildUploadPayload(tokens, registration) {
4751
4936
  if (!tokens.refresh_token || !tokens.id_token) {
4752
4937
  throw new Error("OpenAI's reply is missing the refresh or ID token");
@@ -4815,6 +5000,12 @@ function connectOptionsError(opts) {
4815
5000
  if (opts.org && !opts.shared) {
4816
5001
  return "--org is used only with --shared, to connect an account the organization shares.";
4817
5002
  }
5003
+ if (opts.account !== void 0 && opts.newRegistration) {
5004
+ return "--account reuses a saved registration; --new-registration makes a new one. Use one of them.";
5005
+ }
5006
+ if (opts.account !== void 0 && opts.shared) {
5007
+ return "--account is for your own plan; a shared account always signs in with a new registration.";
5008
+ }
4818
5009
  return null;
4819
5010
  }
4820
5011
  function open(url, what) {
@@ -4878,75 +5069,50 @@ async function connect(opts) {
4878
5069
  printError(BAD_BASE_URL_LINE, 2);
4879
5070
  }
4880
5071
  const sharedOrganizationSlug = opts.shared ? opts.org : void 0;
5072
+ let registration;
5073
+ if (sharedOrganizationSlug) {
5074
+ registration = { hostId: `urn:uuid:${randomUUID3()}` };
5075
+ } else {
5076
+ try {
5077
+ registration = selectRegistration(
5078
+ await loadRegistrationStore(),
5079
+ opts.account
5080
+ );
5081
+ } catch (err) {
5082
+ printError(err.message, 2);
5083
+ }
5084
+ }
4881
5085
  let ticket;
4882
5086
  try {
4883
5087
  ticket = await approveInFabric(origin, sharedOrganizationSlug);
4884
5088
  } catch (err) {
4885
5089
  printError(err.message, 1);
4886
5090
  }
4887
- const stored = await loadRegistration();
4888
- let registration = stored;
4889
- if (sharedOrganizationSlug) {
4890
- registration = { hostId: `urn:uuid:${randomUUID3()}` };
4891
- } else if (opts.newRegistration) {
4892
- registration = { hostId: stored.hostId };
4893
- }
4894
- const verifier = createCodeVerifier();
4895
- const state = createState();
4896
- const nonce = randomToken();
4897
- const listener = await startLoopbackListener({
4898
- state,
4899
- callbackPath: CHATGPT_CALLBACK_PATH,
4900
- preferredPort: CHATGPT_CALLBACK_PORT
4901
- });
4902
- open(
4903
- buildAuthorizeUrl({
4904
- clientId: registration.clientId ?? DYNAMIC_CLIENT_ID,
4905
- hostId: registration.hostId,
4906
- redirectUri: listener.redirectUri,
4907
- state,
4908
- nonce,
4909
- codeChallenge: codeChallengeS256(verifier)
4910
- }),
4911
- "sign in with ChatGPT"
4912
- );
4913
5091
  let result;
4914
- let clientId;
4915
- let subject;
4916
5092
  try {
4917
- const { code, params } = await listener.result;
4918
- clientId = issuedClientId(params, registration);
4919
- if (!sharedOrganizationSlug) {
4920
- await saveRegistration({ ...registration, clientId });
4921
- }
4922
- const tokens = await exchangeCode({
4923
- clientId,
4924
- code,
4925
- codeVerifier: verifier,
4926
- redirectUri: listener.redirectUri
5093
+ const signIn = await signInWithChatGpt({
5094
+ registration,
5095
+ open: (url) => open(url, "sign in with ChatGPT"),
5096
+ ...!sharedOrganizationSlug && { save: saveAccountRegistration },
5097
+ onRetry: () => process.stdout.write(
5098
+ "ChatGPT refused this machine's saved registration; signing in again with a new one.\n"
5099
+ )
4927
5100
  });
4928
- subject = checkIdToken(tokens.id_token ?? "", {
4929
- nonce,
4930
- subject: registration.subject
4931
- }).sub;
4932
5101
  result = await uploadChatGptPlan({
4933
5102
  origin,
4934
5103
  ticket,
4935
- payload: buildUploadPayload(tokens, {
4936
- clientId,
5104
+ payload: buildUploadPayload(signIn.tokens, {
5105
+ clientId: signIn.clientId,
4937
5106
  hostId: registration.hostId
4938
5107
  })
4939
5108
  });
4940
5109
  } catch (err) {
4941
5110
  printError(err.message, 1);
4942
- } finally {
4943
- listener.close();
4944
5111
  }
4945
5112
  if ("shared" in result) {
4946
5113
  printSuccess(describeSharedAccount(result));
4947
5114
  return;
4948
5115
  }
4949
- await saveRegistration({ hostId: registration.hostId, clientId, subject });
4950
5116
  printSuccess(
4951
5117
  `Connected your ChatGPT plan${result.email ? ` (${result.email})` : ""} to Fabric.`
4952
5118
  );
@@ -4960,9 +5126,12 @@ function buildConnectCommand() {
4960
5126
  connectCommand.addCommand(
4961
5127
  new Command8("chatgpt").description(
4962
5128
  "Approve in Fabric, sign in with ChatGPT and run your own Fabric AI work on your plan"
5129
+ ).option(
5130
+ "--account <subject-or-email>",
5131
+ "Reuse the registration saved for this ChatGPT account instead of making a new one"
4963
5132
  ).option(
4964
5133
  "--new-registration",
4965
- "Register this machine with OpenAI again, for example to switch accounts"
5134
+ "Register this machine with OpenAI again (the default without --account)"
4966
5135
  ).option(
4967
5136
  "--base-url <url>",
4968
5137
  "Deployment to connect, such as https://example.com"
@@ -5582,23 +5751,23 @@ function normalizeContextSourcePath(input) {
5582
5751
  if (slashed.startsWith("/") || DRIVE_LETTER_PREFIX.test(slashed)) {
5583
5752
  return { ok: false, reason: "absolute" };
5584
5753
  }
5585
- let path29 = slashed.replace(/\/{2,}/g, "/");
5586
- while (path29.startsWith("./")) {
5587
- path29 = path29.slice(2);
5754
+ let path30 = slashed.replace(/\/{2,}/g, "/");
5755
+ while (path30.startsWith("./")) {
5756
+ path30 = path30.slice(2);
5588
5757
  }
5589
- if (path29.length === 0) {
5758
+ if (path30.length === 0) {
5590
5759
  return { ok: false, reason: "empty" };
5591
5760
  }
5592
- if (path29.endsWith("/")) {
5761
+ if (path30.endsWith("/")) {
5593
5762
  return { ok: false, reason: "trailing-slash" };
5594
5763
  }
5595
- if (path29.split("/").some((segment) => segment === "." || segment === "..")) {
5764
+ if (path30.split("/").some((segment) => segment === "." || segment === "..")) {
5596
5765
  return { ok: false, reason: "dot-segment" };
5597
5766
  }
5598
- if (path29.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
5767
+ if (path30.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
5599
5768
  return { ok: false, reason: "too-long" };
5600
5769
  }
5601
- return { ok: true, sourcePath: path29 };
5770
+ return { ok: true, sourcePath: path30 };
5602
5771
  }
5603
5772
 
5604
5773
  // src/lib/context-sync/lock.ts
@@ -5810,8 +5979,8 @@ async function walkContextDirectory(root, rules) {
5810
5979
  const pending = [""];
5811
5980
  while (pending.length > 0) {
5812
5981
  const relativeDirectory = pending.pop();
5813
- const names = await readdir(path5.join(root, relativeDirectory));
5814
- for (const name of names) {
5982
+ const names2 = await readdir(path5.join(root, relativeDirectory));
5983
+ for (const name of names2) {
5815
5984
  const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
5816
5985
  let stats;
5817
5986
  try {
@@ -5897,8 +6066,8 @@ function sharedPathProblem(entry, group2) {
5897
6066
  if (others.length === 0) {
5898
6067
  return null;
5899
6068
  }
5900
- const names = others.map((other) => other.diskPath).join(", ");
5901
- return others.every((other) => other.sourcePath === entry.sourcePath) ? `names the same source as ${names}` : `case-only collision with ${names}`;
6069
+ const names2 = others.map((other) => other.diskPath).join(", ");
6070
+ return others.every((other) => other.sourcePath === entry.sourcePath) ? `names the same source as ${names2}` : `case-only collision with ${names2}`;
5902
6071
  }
5903
6072
  async function computeContextPlan(input) {
5904
6073
  const { root, lock } = input;
@@ -6486,9 +6655,9 @@ async function pushContextPlan(input) {
6486
6655
  }
6487
6656
  };
6488
6657
  const pruneTargetsSoFar = () => {
6489
- const keptByMoves = results.map(moveKeptOldPath).filter((path29) => path29 !== null);
6490
- return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path29) => {
6491
- const locked = input.lock?.files[path29];
6658
+ const keptByMoves = results.map(moveKeptOldPath).filter((path30) => path30 !== null);
6659
+ return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path30) => {
6660
+ const locked = input.lock?.files[path30];
6492
6661
  return locked !== void 0 && locked.state !== "duplicate";
6493
6662
  }).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6494
6663
  };
@@ -7126,7 +7295,7 @@ function pruneSummary(results, targets) {
7126
7295
  (r) => r.status === "deleted" || r.status === "already-gone"
7127
7296
  ).map((r) => r.sourcePath)
7128
7297
  );
7129
- const notDeleted = targets.filter((path29) => !settled.has(path29)).length;
7298
+ const notDeleted = targets.filter((path30) => !settled.has(path30)).length;
7130
7299
  let text3 = `--prune: ${count("deleted")} deleted, ${count("already-gone")} already gone, ${notDeleted} not deleted`;
7131
7300
  const first = results.find((r) => r.status === "delete-failed");
7132
7301
  if (first && first.status === "delete-failed") {
@@ -7510,7 +7679,7 @@ function buildFramesCommand() {
7510
7679
 
7511
7680
  // src/commands/instructions/index.ts
7512
7681
  import { readdir as readdir3 } from "fs/promises";
7513
- import path28 from "path";
7682
+ import path29 from "path";
7514
7683
  import { Command as Command13 } from "commander";
7515
7684
 
7516
7685
  // src/lib/environment.ts
@@ -7523,15 +7692,15 @@ function runningInCi(env2 = process.env) {
7523
7692
  }
7524
7693
 
7525
7694
  // src/lib/instructions/adoption.ts
7526
- import path9 from "path";
7695
+ import path11 from "path";
7527
7696
 
7528
7697
  // src/lib/instructions/checkout.ts
7529
7698
  import { realpath as realpath2 } from "fs/promises";
7530
- import path8 from "path";
7699
+ import path10 from "path";
7531
7700
 
7532
7701
  // src/lib/instructions/git.ts
7533
- import { lstat as lstat4, stat } from "fs/promises";
7534
- import path7 from "path";
7702
+ import { lstat as lstat5, stat } from "fs/promises";
7703
+ import path9 from "path";
7535
7704
 
7536
7705
  // src/lib/instructions/git-literals.ts
7537
7706
  var COMMIT_SHA = /^[0-9a-f]{40}$/;
@@ -7560,10 +7729,21 @@ function cloneableUrl(url) {
7560
7729
  }
7561
7730
 
7562
7731
  // src/lib/instructions/git-run.ts
7563
- import { spawn as spawn2 } from "child_process";
7732
+ import { spawn as spawn2, spawnSync } from "child_process";
7733
+
7734
+ // src/lib/instructions/taskkill-path.ts
7735
+ import path7 from "path";
7736
+ function taskkillPath(env2 = process.env) {
7737
+ const root = env2.SystemRoot ?? env2.SYSTEMROOT ?? env2.windir ?? "C:\\Windows";
7738
+ return path7.win32.join(root, "System32", "taskkill.exe");
7739
+ }
7740
+
7741
+ // src/lib/instructions/git-run.ts
7564
7742
  var STDOUT_CAP_BYTES = 64 * 1024;
7565
7743
  var STDERR_CAP_BYTES = 8 * 1024;
7566
7744
  var KILL_GRACE_MS = 1e3;
7745
+ var GROUP_KILL_GRACE_MS = 500;
7746
+ var BINARY_STDOUT_CAP_BYTES = 1024 * 1024;
7567
7747
  var STRIPPED_VARIABLES = /* @__PURE__ */ new Set([
7568
7748
  "GIT_DIR",
7569
7749
  "GIT_WORK_TREE",
@@ -7657,6 +7837,8 @@ function runGit(cwd, args, deadline, options = {}) {
7657
7837
  });
7658
7838
  return;
7659
7839
  }
7840
+ const grouped = Boolean(options.write && options.unattended);
7841
+ let escalate;
7660
7842
  let settled = false;
7661
7843
  const finish = (result) => {
7662
7844
  if (!settled) {
@@ -7687,9 +7869,16 @@ function runGit(cwd, args, deadline, options = {}) {
7687
7869
  ...options.write && options.unattended ? hookWriteEnvironment(process.env) : gitEnvironment(process.env, options),
7688
7870
  ...gateway.env
7689
7871
  },
7690
- stdio: ["ignore", "pipe", "pipe"],
7872
+ stdio: [
7873
+ options.input === void 0 ? "ignore" : "pipe",
7874
+ "pipe",
7875
+ "pipe"
7876
+ ],
7691
7877
  shell: false,
7692
- windowsHide: true
7878
+ windowsHide: true,
7879
+ // Its own process group, so the deadline can take the
7880
+ // transport children with it (`killTree`).
7881
+ detached: grouped && process.platform !== "win32"
7693
7882
  }
7694
7883
  );
7695
7884
  } catch (error) {
@@ -7701,12 +7890,17 @@ function runGit(cwd, args, deadline, options = {}) {
7701
7890
  let outOverflow = false;
7702
7891
  const err = [];
7703
7892
  let errBytes = 0;
7893
+ const stdoutCap = options.binary ? options.binaryCapBytes ?? BINARY_STDOUT_CAP_BYTES : STDOUT_CAP_BYTES;
7894
+ if (options.input !== void 0) {
7895
+ child.stdin?.on("error", () => void 0);
7896
+ child.stdin?.end(options.input);
7897
+ }
7704
7898
  child.stdout?.on("data", (chunk) => {
7705
- if (chunk.length > STDOUT_CAP_BYTES - outBytes) {
7899
+ if (chunk.length > stdoutCap - outBytes) {
7706
7900
  outOverflow = true;
7707
7901
  }
7708
- if (outBytes < STDOUT_CAP_BYTES) {
7709
- const room = STDOUT_CAP_BYTES - outBytes;
7902
+ if (outBytes < stdoutCap) {
7903
+ const room = stdoutCap - outBytes;
7710
7904
  out.push(chunk.subarray(0, room));
7711
7905
  outBytes += Math.min(room, chunk.length);
7712
7906
  }
@@ -7718,12 +7912,34 @@ function runGit(cwd, args, deadline, options = {}) {
7718
7912
  errBytes += Math.min(room, chunk.length);
7719
7913
  }
7720
7914
  });
7721
- let escalate;
7915
+ let stoppingGroup = false;
7916
+ const releaseGroup = () => {
7917
+ child.stdout?.destroy();
7918
+ child.stderr?.destroy();
7919
+ child.unref();
7920
+ };
7722
7921
  const timer = setTimeout(() => {
7723
- child.kill("SIGTERM");
7724
- escalate = setTimeout(() => {
7725
- child.kill("SIGKILL");
7726
- }, KILL_GRACE_MS);
7922
+ if (grouped) {
7923
+ stopTree(child, "SIGTERM");
7924
+ if (process.platform !== "win32") {
7925
+ stoppingGroup = true;
7926
+ escalate = setTimeout(() => {
7927
+ stopTree(child, "SIGKILL");
7928
+ releaseGroup();
7929
+ finish({
7930
+ kind: "unavailable",
7931
+ reason: "git timed out"
7932
+ });
7933
+ }, GROUP_KILL_GRACE_MS);
7934
+ return;
7935
+ }
7936
+ releaseGroup();
7937
+ } else {
7938
+ child.kill("SIGTERM");
7939
+ escalate = setTimeout(() => {
7940
+ child.kill("SIGKILL");
7941
+ }, KILL_GRACE_MS);
7942
+ }
7727
7943
  finish({ kind: "unavailable", reason: "git timed out" });
7728
7944
  }, remaining);
7729
7945
  timer.unref();
@@ -7731,6 +7947,9 @@ function runGit(cwd, args, deadline, options = {}) {
7731
7947
  finish(spawnFailure(error));
7732
7948
  });
7733
7949
  child.on("close", (code) => {
7950
+ if (stoppingGroup) {
7951
+ return;
7952
+ }
7734
7953
  clearTimeout(escalate);
7735
7954
  if (outOverflow) {
7736
7955
  finish({
@@ -7742,12 +7961,36 @@ function runGit(cwd, args, deadline, options = {}) {
7742
7961
  finish({
7743
7962
  kind: "exited",
7744
7963
  code: code ?? -1,
7745
- stdout: Buffer.concat(out).toString("utf8"),
7964
+ stdout: Buffer.concat(out).toString(
7965
+ options.binary ? "latin1" : "utf8"
7966
+ ),
7746
7967
  stderr: Buffer.concat(err).toString("utf8")
7747
7968
  });
7748
7969
  });
7749
7970
  });
7750
7971
  }
7972
+ function stopTree(child, signal) {
7973
+ const pid = child.pid;
7974
+ if (pid === void 0) {
7975
+ child.kill(signal);
7976
+ return;
7977
+ }
7978
+ try {
7979
+ if (process.platform === "win32") {
7980
+ if (child.exitCode === null && child.signalCode === null) {
7981
+ spawnSync(taskkillPath(), ["/PID", String(pid), "/T", "/F"], {
7982
+ stdio: "ignore",
7983
+ windowsHide: true,
7984
+ timeout: 2e3
7985
+ });
7986
+ }
7987
+ } else {
7988
+ process.kill(-pid, signal);
7989
+ }
7990
+ } catch {
7991
+ }
7992
+ child.kill(signal);
7993
+ }
7751
7994
  function spawnFailure(error) {
7752
7995
  const code = error?.code;
7753
7996
  if (code === "ENOENT") {
@@ -7831,7 +8074,12 @@ async function changedTrackedPaths(root, deadline) {
7831
8074
  }
7832
8075
 
7833
8076
  // src/lib/instructions/git-write.ts
8077
+ import { lstat as lstat4, readFile as readFile3 } from "fs/promises";
8078
+ import path8 from "path";
7834
8079
  function fetchFailureOf(stderr) {
8080
+ if (/config-env/i.test(stderr) && /unknown option|usage:/i.test(stderr)) {
8081
+ return "old-git";
8082
+ }
7835
8083
  if (/\(non-fast-forward\)|\[rejected\]|cannot fast-forward/i.test(stderr)) {
7836
8084
  return "diverged";
7837
8085
  }
@@ -7885,12 +8133,16 @@ function blockedFilesOf(stderr) {
7885
8133
  }
7886
8134
  return files;
7887
8135
  }
7888
- async function fetchRef(root, remote, ref, deadline) {
8136
+ var fetchHeadFlagWorks = true;
8137
+ function timedOut(reason) {
8138
+ return reason === "git timed out";
8139
+ }
8140
+ async function fetchInto(root, source, remote, ref, deadline, httpAuthorization) {
7889
8141
  if (!isRemoteName(remote) || !isBranchLiteral(ref)) {
7890
8142
  return { kind: "unavailable", reason: "not a branch this will fetch" };
7891
8143
  }
7892
8144
  const tracking = `refs/remotes/${remote}/${ref}`;
7893
- const fetched = await runGit(
8145
+ const fetchWith = (skipFetchHead) => runGit(
7894
8146
  root,
7895
8147
  [
7896
8148
  "-c",
@@ -7899,15 +8151,26 @@ async function fetchRef(root, remote, ref, deadline) {
7899
8151
  "maintenance.auto=false",
7900
8152
  "fetch",
7901
8153
  "--no-recurse-submodules",
8154
+ "--no-tags",
8155
+ ...skipFetchHead ? ["--no-write-fetch-head"] : [],
7902
8156
  "--",
7903
- remote,
8157
+ source,
7904
8158
  `refs/heads/${ref}:${tracking}`
7905
8159
  ],
7906
8160
  deadline,
7907
- { write: true, unattended: true }
8161
+ {
8162
+ write: true,
8163
+ unattended: true,
8164
+ ...httpAuthorization === void 0 ? {} : { httpAuthorization }
8165
+ }
7908
8166
  );
8167
+ let fetched = await fetchWith(fetchHeadFlagWorks);
8168
+ if (fetched.kind === "exited" && fetched.code !== 0 && fetchHeadFlagWorks && /no-write-fetch-head/i.test(fetched.stderr)) {
8169
+ fetchHeadFlagWorks = false;
8170
+ fetched = await fetchWith(false);
8171
+ }
7909
8172
  if (fetched.kind === "unavailable") {
7910
- return fetched.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: fetched.reason };
8173
+ return timedOut(fetched.reason) ? { kind: "timed-out" } : { kind: "unavailable", reason: fetched.reason };
7911
8174
  }
7912
8175
  if (fetched.code !== 0) {
7913
8176
  return { kind: "failed", reason: fetchFailureOf(fetched.stderr) };
@@ -7918,7 +8181,7 @@ async function fetchRef(root, remote, ref, deadline) {
7918
8181
  deadline
7919
8182
  );
7920
8183
  if (tip.kind === "unavailable") {
7921
- return tip.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: tip.reason };
8184
+ return timedOut(tip.reason) ? { kind: "timed-out" } : { kind: "unavailable", reason: tip.reason };
7922
8185
  }
7923
8186
  const [sha] = lines(tip.stdout);
7924
8187
  if (tip.code !== 0 || !sha || !COMMIT_SHA.test(sha)) {
@@ -7926,59 +8189,29 @@ async function fetchRef(root, remote, ref, deadline) {
7926
8189
  }
7927
8190
  return { kind: "fetched", tip: sha };
7928
8191
  }
7929
- async function fetchRefFromUrl(root, url, remote, ref, deadline, httpAuthorization) {
7930
- if (!isRemoteName(remote) || !isBranchLiteral(ref)) {
7931
- return { kind: "unavailable", reason: "not a branch this will fetch" };
7932
- }
7933
- const tracking = `refs/remotes/${remote}/${ref}`;
7934
- const fetched = await runGit(
8192
+ function fetchRef(root, remote, ref, deadline) {
8193
+ return fetchInto(root, remote, remote, ref, deadline);
8194
+ }
8195
+ function fetchRefFromUrl(root, url, remote, ref, deadline, httpAuthorization) {
8196
+ return fetchInto(root, url, remote, ref, deadline, httpAuthorization);
8197
+ }
8198
+ async function mergeOnce(root, sha, deadline) {
8199
+ const merged = await runGit(
7935
8200
  root,
7936
8201
  [
7937
8202
  "-c",
7938
- "gc.auto=0",
7939
- "-c",
7940
- "maintenance.auto=false",
7941
- "fetch",
7942
- "--no-recurse-submodules",
7943
- "--",
7944
- url,
7945
- `refs/heads/${ref}:${tracking}`
8203
+ "core.quotepath=false",
8204
+ "merge",
8205
+ "--ff-only",
8206
+ "--no-edit",
8207
+ "--quiet",
8208
+ sha
7946
8209
  ],
7947
8210
  deadline,
7948
- { write: true, unattended: true, httpAuthorization }
7949
- );
7950
- if (fetched.kind === "unavailable") {
7951
- return fetched.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: fetched.reason };
7952
- }
7953
- if (fetched.code !== 0) {
7954
- return { kind: "failed", reason: fetchFailureOf(fetched.stderr) };
7955
- }
7956
- const tip = await runGit(
7957
- root,
7958
- ["rev-parse", "--verify", "-q", `${tracking}^{commit}`],
7959
- deadline
7960
- );
7961
- if (tip.kind === "unavailable") {
7962
- return tip.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: tip.reason };
7963
- }
7964
- const [sha] = lines(tip.stdout);
7965
- if (tip.code !== 0 || !sha || !COMMIT_SHA.test(sha)) {
7966
- return { kind: "failed", reason: "missing-ref" };
7967
- }
7968
- return { kind: "fetched", tip: sha };
7969
- }
7970
- async function fastForwardTo(root, sha, deadline) {
7971
- if (!COMMIT_SHA.test(sha)) {
7972
- return { kind: "unavailable", reason: "not a commit name" };
7973
- }
7974
- const merged = await runGit(
7975
- root,
7976
- ["merge", "--ff-only", "--no-edit", "--quiet", sha],
7977
- deadline,
7978
8211
  { write: true, unattended: true }
7979
8212
  );
7980
8213
  if (merged.kind === "unavailable") {
7981
- return merged.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: merged.reason };
8214
+ return timedOut(merged.reason) ? { kind: "timed-out" } : { kind: "unavailable", reason: merged.reason };
7982
8215
  }
7983
8216
  if (merged.code !== 0) {
7984
8217
  const reason = mergeFailureOf(merged.stderr);
@@ -7990,55 +8223,294 @@ async function fastForwardTo(root, sha, deadline) {
7990
8223
  deadline
7991
8224
  );
7992
8225
  if (head.kind === "unavailable") {
7993
- return head.reason === "git timed out" ? { kind: "timed-out" } : { kind: "unavailable", reason: head.reason };
8226
+ return timedOut(head.reason) ? { kind: "timed-out" } : { kind: "unavailable", reason: head.reason };
7994
8227
  }
7995
8228
  const [now] = lines(head.stdout);
7996
8229
  return head.code === 0 && now === sha ? { kind: "merged", head: now } : { kind: "failed", reason: "other" };
7997
8230
  }
7998
-
7999
- // src/lib/instructions/git.ts
8000
- var OPERATION_MARKERS = [
8001
- { marker: "MERGE_HEAD", operation: "merge" },
8002
- { marker: "REBASE_HEAD", operation: "rebase" },
8003
- { marker: "rebase-merge", operation: "rebase" },
8004
- { marker: "rebase-apply", operation: "rebase" },
8005
- { marker: "CHERRY_PICK_HEAD", operation: "cherry-pick" },
8006
- { marker: "REVERT_HEAD", operation: "revert" },
8007
- { marker: "BISECT_LOG", operation: "bisect" },
8008
- // Left by a multi-commit cherry-pick or revert between steps; the two
8009
- // HEAD markers above name which one while a step is stopped.
8010
- { marker: "sequencer", operation: "cherry-pick" }
8011
- ];
8012
- async function nearestExistingDirectory(dir) {
8013
- let current = path7.resolve(dir);
8014
- for (; ; ) {
8015
- const found = await stat(current).catch(() => null);
8016
- if (found?.isDirectory()) {
8017
- return current;
8018
- }
8019
- const parent = path7.dirname(current);
8020
- if (parent === current) {
8021
- return null;
8022
- }
8023
- current = parent;
8231
+ function nulPaths(stdout) {
8232
+ return stdout.split("\0").filter((entry) => entry !== "");
8233
+ }
8234
+ var CONVERTING_ATTRIBUTES = ["filter", "ident", "working-tree-encoding"];
8235
+ var MAX_COMPARED_BYTES = 1024 * 1024;
8236
+ var CRLF = /\r\n/g;
8237
+ function nulFields(stdout) {
8238
+ const fields = stdout.split("\0");
8239
+ if (fields[fields.length - 1] === "") {
8240
+ fields.pop();
8241
+ }
8242
+ return fields;
8243
+ }
8244
+ async function readBlobs(root, ids, deadline) {
8245
+ const read = /* @__PURE__ */ new Map();
8246
+ const unique = [...new Set(ids)];
8247
+ if (unique.length === 0) {
8248
+ return read;
8249
+ }
8250
+ const answer = await runGit(root, ["cat-file", "--batch"], deadline, {
8251
+ binary: true,
8252
+ binaryCapBytes: 16 * MAX_COMPARED_BYTES,
8253
+ input: `${unique.join("\n")}
8254
+ `
8255
+ });
8256
+ if (answer.kind === "unavailable") {
8257
+ return timedOut(answer.reason) ? "ran-out-of-time" : void 0;
8024
8258
  }
8025
- }
8026
- async function hasGitEntryAbove(dir) {
8027
- let current = dir;
8028
- for (; ; ) {
8029
- if (await lstat4(path7.join(current, ".git")).catch(() => null) !== null) {
8030
- return true;
8259
+ if (answer.code !== 0) {
8260
+ return void 0;
8261
+ }
8262
+ let at = 0;
8263
+ for (const id of unique) {
8264
+ const end = answer.stdout.indexOf("\n", at);
8265
+ if (end === -1) {
8266
+ return void 0;
8031
8267
  }
8032
- const parent = path7.dirname(current);
8033
- if (parent === current) {
8034
- return false;
8268
+ const header = answer.stdout.slice(at, end).split(" ");
8269
+ if (header[1] === "missing") {
8270
+ read.set(id, null);
8271
+ at = end + 1;
8272
+ continue;
8035
8273
  }
8036
- current = parent;
8274
+ const size = Number(header[2]);
8275
+ if (header[1] !== "blob" || !Number.isInteger(size)) {
8276
+ return void 0;
8277
+ }
8278
+ read.set(
8279
+ id,
8280
+ size > MAX_COMPARED_BYTES ? null : answer.stdout.slice(end + 1, end + 1 + size)
8281
+ );
8282
+ at = end + 1 + size + 1;
8037
8283
  }
8284
+ return read;
8038
8285
  }
8039
- async function findWorkTree(dir, deadline) {
8040
- const cwd = await nearestExistingDirectory(dir);
8041
- if (cwd === null) {
8286
+ async function restorableBytes(root, file, entry, indexed, fileMode) {
8287
+ if (indexed === null || indexed === void 0 || entry.mode === "120000") {
8288
+ return null;
8289
+ }
8290
+ const absolute = path8.join(root, file);
8291
+ const info = await lstat4(absolute).catch(() => null);
8292
+ if (info === null || !info.isFile() || info.size > MAX_COMPARED_BYTES) {
8293
+ return null;
8294
+ }
8295
+ const executable = (info.mode & 73) !== 0;
8296
+ if (fileMode && executable !== (entry.mode === "100755")) {
8297
+ return null;
8298
+ }
8299
+ const local = await readFile3(absolute).catch(() => null);
8300
+ if (local === null) {
8301
+ return null;
8302
+ }
8303
+ const mine = local.toString("latin1");
8304
+ if (mine.includes("\0") || indexed.includes("\0")) {
8305
+ return null;
8306
+ }
8307
+ return mine.replace(CRLF, "\n") === indexed.replace(CRLF, "\n") ? local : null;
8308
+ }
8309
+ async function realBlockers(root, files, deadline) {
8310
+ const scope = ["--literal-pathspecs", "-c", "core.quotepath=false"];
8311
+ const [staged, unstaged, flags, stages, attributes, fileModeSetting] = await Promise.all([
8312
+ runGit(
8313
+ root,
8314
+ [
8315
+ ...scope,
8316
+ "diff",
8317
+ "--cached",
8318
+ "--name-only",
8319
+ "-z",
8320
+ "--",
8321
+ ...files
8322
+ ],
8323
+ deadline
8324
+ ),
8325
+ runGit(
8326
+ root,
8327
+ [...scope, "diff", "--name-only", "-z", "--", ...files],
8328
+ deadline
8329
+ ),
8330
+ runGit(
8331
+ root,
8332
+ [...scope, "ls-files", "-v", "-z", "--", ...files],
8333
+ deadline
8334
+ ),
8335
+ runGit(
8336
+ root,
8337
+ [...scope, "ls-files", "-s", "-z", "--", ...files],
8338
+ deadline
8339
+ ),
8340
+ runGit(
8341
+ root,
8342
+ [
8343
+ ...scope,
8344
+ "check-attr",
8345
+ "-z",
8346
+ ...CONVERTING_ATTRIBUTES,
8347
+ "--",
8348
+ ...files
8349
+ ],
8350
+ deadline
8351
+ ),
8352
+ runGit(
8353
+ root,
8354
+ ["config", "--type=bool", "--get", "core.fileMode"],
8355
+ deadline
8356
+ )
8357
+ ]);
8358
+ const answers = [
8359
+ staged,
8360
+ unstaged,
8361
+ flags,
8362
+ stages,
8363
+ attributes,
8364
+ fileModeSetting
8365
+ ];
8366
+ if (answers.some((answer) => answer.kind === "unavailable")) {
8367
+ return answers.some(
8368
+ (answer) => answer.kind === "unavailable" && timedOut(answer.reason)
8369
+ ) ? "ran-out-of-time" : null;
8370
+ }
8371
+ if (staged.kind !== "exited" || unstaged.kind !== "exited" || flags.kind !== "exited" || stages.kind !== "exited" || attributes.kind !== "exited" || fileModeSetting.kind !== "exited" || [staged, unstaged, flags, stages, attributes].some(
8372
+ (answer) => answer.kind === "exited" && answer.code !== 0
8373
+ )) {
8374
+ return null;
8375
+ }
8376
+ const fileMode = fileModeSetting.stdout.trim() !== "false";
8377
+ const changed = /* @__PURE__ */ new Set([
8378
+ ...nulPaths(staged.stdout),
8379
+ ...nulPaths(unstaged.stdout)
8380
+ ]);
8381
+ const ordinary = new Set(
8382
+ nulPaths(flags.stdout).filter((entry) => entry.startsWith("H ")).map((entry) => entry.slice(2))
8383
+ );
8384
+ const entries = /* @__PURE__ */ new Map();
8385
+ for (const entry of nulPaths(stages.stdout)) {
8386
+ const match = /^(\d+) ([0-9a-f]{40,64}) (\d)\t([\s\S]*)$/.exec(entry);
8387
+ if (match?.[3] === "0" && match[1] !== void 0 && match[2] !== void 0 && match[4] !== void 0) {
8388
+ entries.set(match[4], { mode: match[1], blob: match[2] });
8389
+ }
8390
+ }
8391
+ const converted = /* @__PURE__ */ new Set();
8392
+ const fields = nulFields(attributes.stdout);
8393
+ for (let at = 0; at + 2 < fields.length; at += 3) {
8394
+ if (fields[at + 2] !== "unspecified") {
8395
+ converted.add(fields[at]);
8396
+ }
8397
+ }
8398
+ const candidates = files.filter(
8399
+ (file) => entries.has(file) && ordinary.has(file) && !converted.has(file) && !changed.has(file)
8400
+ );
8401
+ const blobs = await readBlobs(
8402
+ root,
8403
+ candidates.map((file) => entries.get(file)?.blob ?? ""),
8404
+ deadline
8405
+ );
8406
+ if (blobs === "ran-out-of-time" || blobs === void 0) {
8407
+ return blobs === void 0 ? null : blobs;
8408
+ }
8409
+ const real = [];
8410
+ const restorable = /* @__PURE__ */ new Map();
8411
+ for (const file of files) {
8412
+ const entry = entries.get(file);
8413
+ const bytes = entry === void 0 || !candidates.includes(file) ? null : await restorableBytes(
8414
+ root,
8415
+ file,
8416
+ entry,
8417
+ blobs.get(entry.blob),
8418
+ fileMode
8419
+ );
8420
+ if (bytes === null) {
8421
+ real.push(file);
8422
+ } else {
8423
+ restorable.set(file, bytes);
8424
+ }
8425
+ }
8426
+ return { real, restorable };
8427
+ }
8428
+ var RESTORE_AND_MERGE_FLOOR_MS = 1500;
8429
+ async function stillUntouched(root, restorable) {
8430
+ for (const [file, bytes] of restorable) {
8431
+ const now = await readFile3(path8.join(root, file)).catch(() => null);
8432
+ if (now === null || !now.equals(bytes)) {
8433
+ return false;
8434
+ }
8435
+ }
8436
+ return true;
8437
+ }
8438
+ async function fastForwardTo(root, sha, deadline) {
8439
+ if (!COMMIT_SHA.test(sha)) {
8440
+ return { kind: "unavailable", reason: "not a commit name" };
8441
+ }
8442
+ const first = await mergeOnce(root, sha, deadline);
8443
+ if (first.kind !== "failed" || first.reason !== "local-changes" || first.files.length === 0) {
8444
+ return first;
8445
+ }
8446
+ const judged = await realBlockers(root, first.files, deadline);
8447
+ if (judged === "ran-out-of-time") {
8448
+ return { kind: "timed-out" };
8449
+ }
8450
+ if (judged === null) {
8451
+ return first;
8452
+ }
8453
+ if (judged.real.length > 0) {
8454
+ return { kind: "failed", reason: "local-changes", files: judged.real };
8455
+ }
8456
+ if (deadline - Date.now() < RESTORE_AND_MERGE_FLOOR_MS || !await stillUntouched(root, judged.restorable)) {
8457
+ return first;
8458
+ }
8459
+ const restored = await runGit(
8460
+ root,
8461
+ ["--literal-pathspecs", "checkout", "--", ...first.files],
8462
+ deadline,
8463
+ { write: true, unattended: true }
8464
+ );
8465
+ if (restored.kind !== "exited" || restored.code !== 0) {
8466
+ return first;
8467
+ }
8468
+ return mergeOnce(root, sha, deadline);
8469
+ }
8470
+
8471
+ // src/lib/instructions/git.ts
8472
+ var OPERATION_MARKERS = [
8473
+ { marker: "MERGE_HEAD", operation: "merge" },
8474
+ { marker: "REBASE_HEAD", operation: "rebase" },
8475
+ { marker: "rebase-merge", operation: "rebase" },
8476
+ { marker: "rebase-apply", operation: "rebase" },
8477
+ { marker: "CHERRY_PICK_HEAD", operation: "cherry-pick" },
8478
+ { marker: "REVERT_HEAD", operation: "revert" },
8479
+ { marker: "BISECT_LOG", operation: "bisect" },
8480
+ // Left by a multi-commit cherry-pick or revert between steps; the two
8481
+ // HEAD markers above name which one while a step is stopped.
8482
+ { marker: "sequencer", operation: "cherry-pick" }
8483
+ ];
8484
+ async function nearestExistingDirectory(dir) {
8485
+ let current = path9.resolve(dir);
8486
+ for (; ; ) {
8487
+ const found = await stat(current).catch(() => null);
8488
+ if (found?.isDirectory()) {
8489
+ return current;
8490
+ }
8491
+ const parent = path9.dirname(current);
8492
+ if (parent === current) {
8493
+ return null;
8494
+ }
8495
+ current = parent;
8496
+ }
8497
+ }
8498
+ async function hasGitEntryAbove(dir) {
8499
+ let current = dir;
8500
+ for (; ; ) {
8501
+ if (await lstat5(path9.join(current, ".git")).catch(() => null) !== null) {
8502
+ return true;
8503
+ }
8504
+ const parent = path9.dirname(current);
8505
+ if (parent === current) {
8506
+ return false;
8507
+ }
8508
+ current = parent;
8509
+ }
8510
+ }
8511
+ async function findWorkTree(dir, deadline) {
8512
+ const cwd = await nearestExistingDirectory(dir);
8513
+ if (cwd === null) {
8042
8514
  return { kind: "absent" };
8043
8515
  }
8044
8516
  const result = await runGit(
@@ -8075,7 +8547,7 @@ async function findWorkTree(dir, deadline) {
8075
8547
  kind: "ok",
8076
8548
  value: {
8077
8549
  toplevel,
8078
- gitDir: path7.resolve(cwd, gitDir),
8550
+ gitDir: path9.resolve(cwd, gitDir),
8079
8551
  shallow: shallow === "true",
8080
8552
  superproject: superproject ? superproject : null
8081
8553
  }
@@ -8174,7 +8646,7 @@ async function isAncestor(root, ancestorSha, descendantSha, deadline) {
8174
8646
  }
8175
8647
  return { kind: "unavailable", reason: "the commit is not in this clone" };
8176
8648
  }
8177
- async function isClean(root, deadline) {
8649
+ async function hasNoTrackedContentChanges(root, deadline) {
8178
8650
  for (const scope of [[], ["--cached"]]) {
8179
8651
  const result = await simple(
8180
8652
  root,
@@ -8201,7 +8673,7 @@ async function trackedPaths(root, relativePaths, deadline) {
8201
8673
  return { kind: "ok", value: [] };
8202
8674
  }
8203
8675
  if (relativePaths.some(
8204
- (relativePath) => relativePath === "" || relativePath.startsWith("-") || path7.isAbsolute(relativePath) || path7.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
8676
+ (relativePath) => relativePath === "" || relativePath.startsWith("-") || path9.isAbsolute(relativePath) || path9.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
8205
8677
  )) {
8206
8678
  return {
8207
8679
  kind: "unavailable",
@@ -8287,8 +8759,8 @@ async function operationInProgress(root, deadline) {
8287
8759
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8288
8760
  }
8289
8761
  for (const [index, { operation }] of OPERATION_MARKERS.entries()) {
8290
- const marker = path7.resolve(root, paths[index]);
8291
- if (await lstat4(marker).catch(() => null) !== null) {
8762
+ const marker = path9.resolve(root, paths[index]);
8763
+ if (await lstat5(marker).catch(() => null) !== null) {
8292
8764
  return { kind: "ok", value: operation };
8293
8765
  }
8294
8766
  }
@@ -8378,7 +8850,7 @@ async function lockFilesPresent(root, deadline) {
8378
8850
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8379
8851
  }
8380
8852
  for (const lock of paths) {
8381
- if (await lstat4(path7.resolve(root, lock)).catch(() => null) !== null) {
8853
+ if (await lstat5(path9.resolve(root, lock)).catch(() => null) !== null) {
8382
8854
  return { kind: "ok", value: true };
8383
8855
  }
8384
8856
  }
@@ -8426,7 +8898,7 @@ async function commonDir(root, deadline) {
8426
8898
  if (result.value.code !== 0 || !dir) {
8427
8899
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8428
8900
  }
8429
- return { kind: "ok", value: path7.resolve(root, dir) };
8901
+ return { kind: "ok", value: path9.resolve(root, dir) };
8430
8902
  }
8431
8903
  async function excludeFilePath(root, deadline) {
8432
8904
  const result = await simple(
@@ -8441,7 +8913,7 @@ async function excludeFilePath(root, deadline) {
8441
8913
  if (result.value.code !== 0 || !answer) {
8442
8914
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8443
8915
  }
8444
- return { kind: "ok", value: path7.resolve(root, answer) };
8916
+ return { kind: "ok", value: path9.resolve(root, answer) };
8445
8917
  }
8446
8918
  async function isIgnored(root, relativePath, deadline) {
8447
8919
  if (relativePath.startsWith("-") || relativePath === "") {
@@ -8470,6 +8942,9 @@ async function isIgnored(root, relativePath, deadline) {
8470
8942
  };
8471
8943
  }
8472
8944
  function cloneFailureOf(stderr) {
8945
+ if (/config-env/i.test(stderr) && /unknown option|usage:/i.test(stderr)) {
8946
+ return "old-git";
8947
+ }
8473
8948
  if (/already exists and is not an empty directory/i.test(stderr)) {
8474
8949
  return "not-empty";
8475
8950
  }
@@ -8550,6 +9025,12 @@ var TRAIT_TEXT = {
8550
9025
  sparse: " (sparse checkout)",
8551
9026
  superproject: " (inside a superproject)"
8552
9027
  };
9028
+ function forProject(project, otherProjects) {
9029
+ return project !== void 0 && otherProjects === true ? ` for project ${project} (it is signed in for other projects)` : "";
9030
+ }
9031
+ function shown(value) {
9032
+ return sanitizeDisplayText(value, 255);
9033
+ }
8553
9034
  var MAX_BLOCKED_FILES = 3;
8554
9035
  function blockedFilesText(files) {
8555
9036
  if (files.length === 0) {
@@ -8559,8 +9040,8 @@ function blockedFilesText(files) {
8559
9040
  const more = files.length - MAX_BLOCKED_FILES;
8560
9041
  return ` (${named}${more > 0 ? ` and ${more} more` : ""})`;
8561
9042
  }
8562
- function shown(value) {
8563
- return sanitizeDisplayText(value, 255);
9043
+ function isUserActionableMergeFailure(reason) {
9044
+ return reason === "diverged" || reason === "local-changes";
8564
9045
  }
8565
9046
  var LAG_TEXT = {
8566
9047
  refused: "a commit was refused by the secret scan \u2014 see the project's Coding Instructions tab",
@@ -8575,15 +9056,15 @@ var TOOL_LABEL = {
8575
9056
  codex: "Codex"
8576
9057
  };
8577
9058
  function sessionStartClause(tools, applies) {
8578
- const names = tools.map((tool) => TOOL_LABEL[tool]);
8579
- const single = names.length === 1;
9059
+ const names2 = tools.map((tool) => TOOL_LABEL[tool]);
9060
+ const single = names2.length === 1;
8580
9061
  const verb = applies ? `${single ? "applies" : "apply"} updates` : `${single ? "checks" : "check"} for updates`;
8581
- return `${names.join(" and ")} ${verb} at every session start.`;
9062
+ return `${names2.join(" and ")} ${verb} at every session start.`;
8582
9063
  }
8583
9064
  function fastForwardClause(tools, ref) {
8584
- const names = tools.map((tool) => TOOL_LABEL[tool]);
8585
- const verb = names.length === 1 ? "fast-forwards" : "fast-forward";
8586
- return `${names.join(" and ")} ${verb} ${ref} at session start when safe.`;
9065
+ const names2 = tools.map((tool) => TOOL_LABEL[tool]);
9066
+ const verb = names2.length === 1 ? "fast-forwards" : "fast-forward";
9067
+ return `${names2.join(" and ")} ${verb} ${ref} at session start when safe.`;
8587
9068
  }
8588
9069
  function loginCommand(origin, project) {
8589
9070
  if (!isSafeArgument(origin)) {
@@ -8594,6 +9075,22 @@ function loginCommand(origin, project) {
8594
9075
  origin
8595
9076
  );
8596
9077
  }
9078
+ function behindAdvice(condition, ref) {
9079
+ switch (condition.kind) {
9080
+ case "clean":
9081
+ return ` \u2014 run: ${condition.pull}`;
9082
+ case "dirty":
9083
+ return " and has uncommitted changes \u2014 commit or stash, then pull.";
9084
+ case "operation":
9085
+ return `; a ${condition.operation} is in progress; nothing was changed.`;
9086
+ case "detached":
9087
+ return `; HEAD is detached \u2014 check out ${ref} and pull.`;
9088
+ case "other-branch":
9089
+ return `; you are on ${condition.branch} \u2014 pull ${ref} when you switch to it.`;
9090
+ default:
9091
+ return condition;
9092
+ }
9093
+ }
8597
9094
  var LINES = {
8598
9095
  "needs-project": ({ verb }) => `This folder is not a git checkout, so its project cannot be found. Run: ${fabricCommand(`instructions ${verb} --project <id>`)}`,
8599
9096
  "no-remote": ({ verb }) => `This checkout has no remote, so its project cannot be found. Run: ${fabricCommand(`instructions ${verb} --project <id>`)}`,
@@ -8612,8 +9109,8 @@ var LINES = {
8612
9109
  "bad-project-id": () => "--project must be a project id: letters, digits, '.', '_' or '-', starting with a letter or digit, at most 64 characters.",
8613
9110
  "bad-org-slug": () => "--org must be an organization slug: letters, digits, '.', '_' or '-', starting with a letter or digit, at most 64 characters.",
8614
9111
  "bad-remote-name": () => "--remote must be the name of a git remote: letters, digits, '.', '_', '-' and '/', starting with a letter, digit, '.' or '_'.",
8615
- "not-signed-in": ({ origin, project }) => `Not signed in to ${origin}. Run: ${loginCommand(origin, project)}`,
8616
- "hook-signed-out": ({ origin, project }) => `not signed in to ${origin} \u2014 run: ${loginCommand(origin, project)}`,
9112
+ "not-signed-in": ({ origin, project, otherProjects }) => `Not signed in to ${origin}${forProject(project, otherProjects)}. Run: ${loginCommand(origin, project)}`,
9113
+ "hook-signed-out": ({ origin, project, otherProjects }) => `not signed in to ${origin}${forProject(project, otherProjects)} \u2014 run: ${loginCommand(origin, project)}`,
8617
9114
  "sign-in-failed": ({ origin, project }) => `Could not sign in to ${origin}. Run: ${loginCommand(origin, project)} to see why.`,
8618
9115
  "unknown-tool": ({ tool }) => `Unsupported tool "${tool}". Use --tool claude-code or --tool codex.`,
8619
9116
  "lessons-need-claude": () => "--lessons is not yet supported for codex; Codex hooks for lesson capture are not wired.",
@@ -8644,6 +9141,8 @@ var LINES = {
8644
9141
  return `Could not clone ${repo}: it has no branch ${shown(ref)}. The Fabric MCP server was not registered.`;
8645
9142
  case "checkout":
8646
9143
  return `Could not finish checking out ${repo}. Fabric did not change the partial checkout or register its MCP server. Inspect it with git status, then repair or remove it yourself before trying again.`;
9144
+ case "old-git":
9145
+ return `Could not clone ${repo}: this git is older than 2.31, which Fabric's repository transport needs. The Fabric MCP server was not registered. Update git and try again.`;
8647
9146
  case "other":
8648
9147
  return `Could not clone ${repo}. The Fabric MCP server was not registered. Run: git clone -- ${url} to see why.`;
8649
9148
  default:
@@ -8653,23 +9152,8 @@ var LINES = {
8653
9152
  "no-clone-url": ({ repo }) => `This deployment did not say where to clone ${repo} from. Clone it yourself, then run: ${fabricCommand("instructions init")}`,
8654
9153
  "lock-removed": ({ repo }) => `Removed .fabric/instructions.lock: this checkout follows ${repo} through git now.`,
8655
9154
  "exclude-failed": ({ entries }) => `Could not update .git/info/exclude. Add these to your own ignore rules: ${entries.map(shown).join(" ")}`,
8656
- behind: ({ version, sha7, ref, notFetched, traits, condition }) => {
8657
- const state = `${HOOK_PREFIX} v${version} (${sha7}) is on ${ref}; this checkout ${notFetched ? "has not fetched it yet" : "is behind"}${traits.map((trait) => TRAIT_TEXT[trait]).join("")}`;
8658
- switch (condition.kind) {
8659
- case "clean":
8660
- return `${state} \u2014 run: ${condition.pull}`;
8661
- case "dirty":
8662
- return `${state} and has uncommitted changes \u2014 commit or stash, then pull.`;
8663
- case "operation":
8664
- return `${state}; a ${condition.operation} is in progress; nothing was changed.`;
8665
- case "detached":
8666
- return `${state}; HEAD is detached \u2014 check out ${ref} and pull.`;
8667
- case "other-branch":
8668
- return `${state}; you are on ${condition.branch} \u2014 pull ${ref} when you switch to it.`;
8669
- default:
8670
- return condition;
8671
- }
8672
- },
9155
+ behind: ({ version, sha7, ref, notFetched, traits, condition }) => `${HOOK_PREFIX} v${version} (${sha7}) is on ${ref}; this checkout ${notFetched ? "has not fetched it yet" : "is behind"}${traits.map((trait) => TRAIT_TEXT[trait]).join("")}${behindAdvice(condition, ref)}`,
9156
+ "behind-direct": ({ repo, sha7, ref, traits, condition }) => `${HOOK_PREFIX}: read directly from ${repo} at ${sha7}; this checkout is behind${traits.map((trait) => TRAIT_TEXT[trait]).join("")}${behindAdvice(condition, ref)}`,
8673
9157
  "earlier-source": ({ version, sourceRef, ref, repo }) => `${HOOK_PREFIX} v${version} was published from ${sourceRef}; the project now syncs ${ref} of ${repo} \u2014 pull ${ref} to pick up the next publication`,
8674
9158
  "nothing-published": ({ repo }) => `${HOOK_PREFIX}: the project is repository-sourced but nothing has been published from ${repo} yet`,
8675
9159
  "already-current": ({ version, sha7, ref, repo }) => `${HOOK_PREFIX} v${version}${sha7 === null ? "" : ` (${sha7})`} from ${ref} of ${repo} is already in this checkout's history; nothing to sync`,
@@ -8702,16 +9186,18 @@ var LINES = {
8702
9186
  return `could not fetch ${repo}: it has no branch ${ref}; nothing was updated.`;
8703
9187
  case "timeout":
8704
9188
  return `could not fetch ${repo}: ${host} answered too slowly; nothing was updated.`;
9189
+ case "old-git":
9190
+ return `${HOOK_PREFIX}: could not fetch ${repo}: this git is older than 2.31, which Fabric's repository transport needs. Update git.`;
8705
9191
  case "other":
8706
9192
  return `could not fetch ${repo}. Run: ${commands.fetch} to see why.`;
8707
9193
  default:
8708
9194
  return reason;
8709
9195
  }
8710
9196
  },
8711
- "ff-merge-failed": ({ reason, files, ref, remote, commands }) => {
8712
- switch (reason) {
9197
+ "ff-merge-failed": ({ failure, ref, remote, commands }) => {
9198
+ switch (failure.reason) {
8713
9199
  case "local-changes":
8714
- return `${HOOK_PREFIX}: ${ref} is behind ${remote}/${ref}, but local changes would be overwritten${blockedFilesText(files ?? [])}, so nothing was updated. Commit or stash them, then run: ${commands.pull}`;
9200
+ return `${HOOK_PREFIX}: ${ref} is behind ${remote}/${ref}, but local changes would be overwritten${blockedFilesText(failure.files)}, so nothing was updated. Stash them or move them to another branch; the next session start updates this checkout on its own.`;
8715
9201
  case "diverged":
8716
9202
  return `${HOOK_PREFIX}: ${ref} and ${remote}/${ref} have diverged, so nothing was updated. Run: ${commands.rebase}, or merge ${remote}/${ref} yourself.`;
8717
9203
  case "timeout":
@@ -8719,7 +9205,7 @@ var LINES = {
8719
9205
  case "other":
8720
9206
  return `git could not fast-forward ${ref}. Run: ${commands.pull} to see why.`;
8721
9207
  default:
8722
- return reason;
9208
+ return failure;
8723
9209
  }
8724
9210
  },
8725
9211
  "ff-locked": () => `${HOOK_PREFIX}: another fabric process is updating this checkout; nothing was changed.`,
@@ -8783,6 +9269,7 @@ var EXIT_CODES = {
8783
9269
  "lock-removed": 0,
8784
9270
  "exclude-failed": 0,
8785
9271
  behind: 0,
9272
+ "behind-direct": 0,
8786
9273
  "earlier-source": 0,
8787
9274
  "nothing-published": 0,
8788
9275
  "already-current": 0,
@@ -9056,7 +9543,7 @@ function downloadsIn(classification, manual) {
9056
9543
  return manual && classification.class === "foreign";
9057
9544
  }
9058
9545
  function isSafeRootPath(rootPath) {
9059
- if (path8.posix.isAbsolute(rootPath) || path8.win32.isAbsolute(rootPath) || rootPath.startsWith("~")) {
9546
+ if (path10.posix.isAbsolute(rootPath) || path10.win32.isAbsolute(rootPath) || rootPath.startsWith("~")) {
9060
9547
  return false;
9061
9548
  }
9062
9549
  return !rootPath.split(/[\\/]/).some((segment) => segment === "..");
@@ -9120,15 +9607,15 @@ async function classify({
9120
9607
  reason: "the project's branch name is not one this hook will use"
9121
9608
  };
9122
9609
  }
9123
- const names = await remotes(root, deadline);
9124
- if (names.kind !== "ok") {
9610
+ const names2 = await remotes(root, deadline);
9611
+ if (names2.kind !== "ok") {
9125
9612
  return {
9126
9613
  class: "unknown",
9127
- reason: names.kind === "absent" ? "not a git checkout" : names.reason
9614
+ reason: names2.kind === "absent" ? "not a git checkout" : names2.reason
9128
9615
  };
9129
9616
  }
9130
9617
  const matching = [];
9131
- for (const name of remote === void 0 ? names.value : names.value.filter((candidate) => candidate === remote)) {
9618
+ for (const name of remote === void 0 ? names2.value : names2.value.filter((candidate) => candidate === remote)) {
9132
9619
  const url = await effectiveFetchUrl(root, name, deadline);
9133
9620
  if (url.kind !== "ok") {
9134
9621
  return {
@@ -9150,7 +9637,7 @@ async function classify({
9150
9637
  if (matching.length > 1) {
9151
9638
  return { class: "ambiguous", remotes: matching };
9152
9639
  }
9153
- const mapped = path8.join(root, repository.rootPath);
9640
+ const mapped = path10.join(root, repository.rootPath);
9154
9641
  if (!await sameDirectory(destination, mapped)) {
9155
9642
  return {
9156
9643
  class: "unmapped",
@@ -9176,34 +9663,29 @@ async function inspectMatchingCheckout(input) {
9176
9663
  const { classification, repository, snapshot, deadline } = input;
9177
9664
  const root = classification.toplevel;
9178
9665
  const unknown = (reason) => reportFor(repository, { class: "unknown", reason });
9179
- const branch = await currentBranch(root, deadline);
9180
- if (branch.kind !== "ok") {
9181
- return unknown(
9182
- branch.kind === "absent" ? "not a git checkout" : branch.reason
9183
- );
9184
- }
9185
- const head = await headSha(root, deadline);
9186
- if (head.kind !== "ok") {
9187
- return unknown(
9188
- head.kind === "absent" ? "not a git checkout" : head.reason
9189
- );
9190
- }
9191
- const clean = await isClean(root, deadline);
9192
- if (clean.kind !== "ok") {
9193
- return unknown(
9194
- clean.kind === "absent" ? "not a git checkout" : clean.reason
9195
- );
9666
+ const [branch, head, operation, readClean] = await Promise.all([
9667
+ currentBranch(root, deadline),
9668
+ headSha(root, deadline),
9669
+ operationInProgress(root, deadline),
9670
+ input.contentChanges === "always" ? hasNoTrackedContentChanges(root, deadline) : Promise.resolve(null)
9671
+ ]);
9672
+ for (const answer of [branch, head, operation, readClean]) {
9673
+ if (answer === null) {
9674
+ continue;
9675
+ }
9676
+ if (answer.kind !== "ok") {
9677
+ return unknown(
9678
+ answer.kind === "absent" ? "not a git checkout" : answer.reason
9679
+ );
9680
+ }
9196
9681
  }
9197
- const operation = await operationInProgress(root, deadline);
9198
- if (operation.kind !== "ok") {
9199
- return unknown(
9200
- operation.kind === "absent" ? "not a git checkout" : operation.reason
9201
- );
9682
+ if (branch.kind !== "ok" || head.kind !== "ok" || operation.kind !== "ok") {
9683
+ return unknown("the checkout could not be read");
9202
9684
  }
9203
- const state = {
9685
+ let state = {
9204
9686
  branch: branch.value,
9205
9687
  head: head.value,
9206
- clean: clean.value,
9688
+ clean: readClean === null ? true : readClean.kind === "ok" && readClean.value,
9207
9689
  operation: operation.value,
9208
9690
  traits: classification.traits
9209
9691
  };
@@ -9222,13 +9704,21 @@ async function inspectMatchingCheckout(input) {
9222
9704
  contains = answer.kind === "ok" ? answer.value : null;
9223
9705
  }
9224
9706
  }
9225
- const { kind: reportKind, line: line2 } = matchingReport({
9707
+ const report = () => matchingReport({
9226
9708
  repository,
9227
9709
  remote: classification.remote,
9228
9710
  snapshot: snapshot ? { version: snapshot.version, source: snapshot.source } : null,
9229
9711
  state,
9230
9712
  contains
9231
9713
  });
9714
+ let { kind: reportKind, line: line2 } = report();
9715
+ if (readClean === null && reportKind === "behind") {
9716
+ const content = await hasNoTrackedContentChanges(root, deadline);
9717
+ if (content.kind === "ok") {
9718
+ state = { ...state, clean: content.value };
9719
+ ({ kind: reportKind, line: line2 } = report());
9720
+ }
9721
+ }
9232
9722
  return {
9233
9723
  classification,
9234
9724
  state,
@@ -9302,30 +9792,6 @@ function matchingReport(input) {
9302
9792
  if (contains === true) {
9303
9793
  return { kind: "current", line: null };
9304
9794
  }
9305
- let condition;
9306
- if (state.operation !== null) {
9307
- condition = { kind: "operation", operation: state.operation };
9308
- } else if (state.branch === null) {
9309
- condition = { kind: "detached" };
9310
- } else if (state.branch !== repository.ref) {
9311
- condition = {
9312
- kind: "other-branch",
9313
- branch: show(state.branch, MAX.ref)
9314
- };
9315
- } else if (!state.clean) {
9316
- condition = { kind: "dirty" };
9317
- } else {
9318
- condition = {
9319
- kind: "clean",
9320
- pull: [
9321
- "git",
9322
- "pull",
9323
- "--ff-only",
9324
- shellQuote(show(remote, MAX.remote)),
9325
- shellQuote(ref)
9326
- ].join(" ")
9327
- };
9328
- }
9329
9795
  return {
9330
9796
  kind: "behind",
9331
9797
  line: outcomeLine("behind", {
@@ -9334,10 +9800,43 @@ function matchingReport(input) {
9334
9800
  ref,
9335
9801
  notFetched: contains !== false,
9336
9802
  traits: traitNames(state.traits),
9337
- condition
9803
+ condition: behindCondition(state, repository.ref, remote)
9338
9804
  })
9339
9805
  };
9340
9806
  }
9807
+ function behindCondition(state, branchRef, remote) {
9808
+ if (state.operation !== null) {
9809
+ return { kind: "operation", operation: state.operation };
9810
+ }
9811
+ if (state.branch === null) {
9812
+ return { kind: "detached" };
9813
+ }
9814
+ if (state.branch !== branchRef) {
9815
+ return { kind: "other-branch", branch: show(state.branch, MAX.ref) };
9816
+ }
9817
+ if (!state.clean) {
9818
+ return { kind: "dirty" };
9819
+ }
9820
+ return {
9821
+ kind: "clean",
9822
+ pull: [
9823
+ "git",
9824
+ "pull",
9825
+ "--ff-only",
9826
+ shellQuote(show(remote, MAX.remote)),
9827
+ shellQuote(show(branchRef, MAX.ref))
9828
+ ].join(" ")
9829
+ };
9830
+ }
9831
+ function directBehindLine(input) {
9832
+ return outcomeLine("behind-direct", {
9833
+ repo: repositoryName(input.repository),
9834
+ sha7: show(input.commitSha.slice(0, 7), 7),
9835
+ ref: show(input.ref, MAX.ref),
9836
+ traits: traitNames(input.state.traits),
9837
+ condition: behindCondition(input.state, input.ref, input.remote)
9838
+ });
9839
+ }
9341
9840
  function classLine(classification, repository) {
9342
9841
  const name = repository ? repositoryName(repository) : "its repository";
9343
9842
  switch (classification.class) {
@@ -9371,7 +9870,11 @@ function classLine(classification, repository) {
9371
9870
  }
9372
9871
  async function inspectCheckout(input) {
9373
9872
  const classification = await classifyCheckout(input);
9374
- return reportForClassification({ ...input, classification });
9873
+ return reportForClassification({
9874
+ contentChanges: "always",
9875
+ ...input,
9876
+ classification
9877
+ });
9375
9878
  }
9376
9879
  async function reportForClassification(input) {
9377
9880
  const { classification, repository } = input;
@@ -9384,6 +9887,7 @@ async function reportForClassification(input) {
9384
9887
  try {
9385
9888
  return await inspectMatchingCheckout({
9386
9889
  ...input,
9890
+ contentChanges: input.contentChanges ?? "always",
9387
9891
  classification,
9388
9892
  repository
9389
9893
  });
@@ -9436,7 +9940,7 @@ function providerLoginCommand(provider, gitCommand) {
9436
9940
  }
9437
9941
  }
9438
9942
  function folderForCommand(dest, relative) {
9439
- const inside = relative.split(path9.sep).join("/") || ".";
9943
+ const inside = relative.split(path11.sep).join("/") || ".";
9440
9944
  const typed = dest?.replace(/\\/g, "/").replace(/\/+$/, "");
9441
9945
  return shellQuote(
9442
9946
  typed === void 0 || typed === "" ? inside : inside === "." ? typed : `${typed}/${inside}`
@@ -9478,9 +9982,9 @@ function adoptionFor(input) {
9478
9982
  repo,
9479
9983
  where: folderForCommand(
9480
9984
  input.rerun.dest,
9481
- path9.relative(
9985
+ path11.relative(
9482
9986
  input.destination,
9483
- path9.join(
9987
+ path11.join(
9484
9988
  classification.toplevel,
9485
9989
  classification.rootPath
9486
9990
  )
@@ -9512,8 +10016,9 @@ function adoptionFor(input) {
9512
10016
  }
9513
10017
 
9514
10018
  // src/lib/instructions/agent-mcp-config.ts
9515
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
9516
- import path10 from "path";
10019
+ import { readFile as readFile4, stat as stat2 } from "fs/promises";
10020
+ import path12 from "path";
10021
+ var LEGACY_SERVER_NAME = "fabric";
9517
10022
  var MAX_CONFIG_BYTES = 16 * 1024 * 1024;
9518
10023
  async function readText(file) {
9519
10024
  try {
@@ -9521,7 +10026,7 @@ async function readText(file) {
9521
10026
  if (!info.isFile() || info.size > MAX_CONFIG_BYTES) {
9522
10027
  return { kind: "unusable" };
9523
10028
  }
9524
- return { kind: "text", text: await readFile3(file, "utf8") };
10029
+ return { kind: "text", text: await readFile4(file, "utf8") };
9525
10030
  } catch (error) {
9526
10031
  return error.code === "ENOENT" ? { kind: "absent" } : { kind: "unusable" };
9527
10032
  }
@@ -9533,13 +10038,17 @@ function projectKey(folder, platform) {
9533
10038
  const slashed = folder.replace(/\\/g, "/").replace(/\/+$/, "");
9534
10039
  return platform === "win32" ? slashed.toLowerCase() : slashed;
9535
10040
  }
9536
- function serverIn(servers, name) {
9537
- const server = field(servers, name);
9538
- if (typeof server !== "object" || server === null) {
9539
- return void 0;
10041
+ function serversIn(servers) {
10042
+ if (typeof servers !== "object" || servers === null) {
10043
+ return [];
9540
10044
  }
9541
- const url = field(server, "url");
9542
- return { url: typeof url === "string" ? url : null };
10045
+ return Object.entries(servers).flatMap(([name, server]) => {
10046
+ if (typeof server !== "object" || server === null) {
10047
+ return [];
10048
+ }
10049
+ const url = field(server, "url");
10050
+ return [{ name, url: typeof url === "string" ? url : null }];
10051
+ });
9543
10052
  }
9544
10053
  async function readClaudeServers(input) {
9545
10054
  const directory = input.env.CLAUDE_CONFIG_DIR ?? input.home;
@@ -9547,7 +10056,7 @@ async function readClaudeServers(input) {
9547
10056
  return { state: "unlocated" };
9548
10057
  }
9549
10058
  const servers = [];
9550
- const config2 = await readText(path10.join(directory, ".claude.json"));
10059
+ const config2 = await readText(path12.join(directory, ".claude.json"));
9551
10060
  if (config2.kind === "unusable") {
9552
10061
  return { state: "unreadable" };
9553
10062
  }
@@ -9563,29 +10072,24 @@ async function readClaudeServers(input) {
9563
10072
  if (typeof projects === "object" && projects !== null) {
9564
10073
  for (const [key, project] of Object.entries(projects)) {
9565
10074
  if (projectKey(key, input.platform) === wanted) {
9566
- const found2 = serverIn(
9567
- field(project, "mcpServers"),
9568
- input.name
9569
- );
9570
- if (found2 !== void 0) {
9571
- servers.push({ scope: "local", ...found2 });
10075
+ for (const found of serversIn(
10076
+ field(project, "mcpServers")
10077
+ )) {
10078
+ servers.push({ scope: "local", ...found });
9572
10079
  }
9573
10080
  }
9574
10081
  }
9575
10082
  }
9576
- const found = serverIn(field(parsed, "mcpServers"), input.name);
9577
- if (found !== void 0) {
10083
+ for (const found of serversIn(field(parsed, "mcpServers"))) {
9578
10084
  servers.push({ scope: "user", ...found });
9579
10085
  }
9580
10086
  }
9581
- const shared = await readText(path10.join(input.cwd, ".mcp.json"));
10087
+ const shared = await readText(path12.join(input.cwd, ".mcp.json"));
9582
10088
  if (shared.kind === "text") {
9583
10089
  try {
9584
- const found = serverIn(
9585
- field(JSON.parse(shared.text), "mcpServers"),
9586
- input.name
9587
- );
9588
- if (found !== void 0) {
10090
+ for (const found of serversIn(
10091
+ field(JSON.parse(shared.text), "mcpServers")
10092
+ )) {
9589
10093
  servers.push({ scope: "project", ...found });
9590
10094
  }
9591
10095
  } catch {
@@ -9599,11 +10103,11 @@ var CODEX_SERVER_SUBTABLE = /^\[mcp_servers\.(?:"[^"]+"|'[^']+'|[A-Za-z0-9_-]+)\
9599
10103
  var CODEX_OTHER_HEADER = /^\[{1,2}\s*mcp_servers\b/;
9600
10104
  var CODEX_OTHER_KEY = /^mcp_servers\b/;
9601
10105
  async function readCodexServers(input) {
9602
- const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 : path10.join(input.home, ".codex"));
10106
+ const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 : path12.join(input.home, ".codex"));
9603
10107
  if (directory === void 0) {
9604
10108
  return { state: "unlocated" };
9605
10109
  }
9606
- const config2 = await readText(path10.join(directory, "config.toml"));
10110
+ const config2 = await readText(path12.join(directory, "config.toml"));
9607
10111
  if (config2.kind === "absent") {
9608
10112
  return { state: "read", servers: [] };
9609
10113
  }
@@ -9639,32 +10143,122 @@ async function readCodexServers(input) {
9639
10143
  servers: [...servers].map(([name, url]) => ({ name, url }))
9640
10144
  };
9641
10145
  }
9642
- async function readRegistration(input) {
9643
- if (input.tool === "codex") {
9644
- const read2 = await readCodexServers(input);
9645
- if (read2.state !== "read") {
9646
- return read2.state === "unlocated" ? "missing" : "unreadable";
9647
- }
9648
- if (read2.servers.some((server) => server.url === input.url)) {
9649
- return "registered";
10146
+
10147
+ // src/lib/instructions/agent-mcp-servers.ts
10148
+ var MAX_PROBES = 4;
10149
+ var SCOPE_PRECEDENCE = ["local", "project", "user"];
10150
+ var PLAIN_NAME = /^[A-Za-z0-9_-]{1,64}$/;
10151
+ function shownName(name) {
10152
+ return PLAIN_NAME.test(name) ? name : "(a name with unusual characters)";
10153
+ }
10154
+ function describeTarget(url) {
10155
+ if (url === null) {
10156
+ return "a local command";
10157
+ }
10158
+ try {
10159
+ const { host } = new URL(url);
10160
+ return host !== "" && isSafeArgument(host) ? host : "another address";
10161
+ } catch {
10162
+ return "another address";
10163
+ }
10164
+ }
10165
+ function effectiveFlags(servers) {
10166
+ const winners = /* @__PURE__ */ new Map();
10167
+ for (const server of servers) {
10168
+ const current = winners.get(server.name);
10169
+ if (server.scope !== null && (current === void 0 || SCOPE_PRECEDENCE.indexOf(server.scope) < SCOPE_PRECEDENCE.indexOf(current))) {
10170
+ winners.set(server.name, server.scope);
9650
10171
  }
9651
- return read2.servers.some((server) => server.name === input.name) ? "elsewhere" : "missing";
9652
10172
  }
9653
- const read = await readClaudeServers(input);
9654
- if (read.state !== "read") {
9655
- return read.state === "unlocated" ? "missing" : "unreadable";
10173
+ return servers.map(
10174
+ (server) => server.scope === null || winners.get(server.name) === server.scope
10175
+ );
10176
+ }
10177
+ async function classifyServers(servers, context) {
10178
+ const flags = effectiveFlags(servers);
10179
+ const probed = /* @__PURE__ */ new Map();
10180
+ const classified = [];
10181
+ for (const [index, server] of servers.entries()) {
10182
+ const points = describeTarget(server.url);
10183
+ const effective = flags[index] === true;
10184
+ const base = {
10185
+ name: server.name,
10186
+ scope: server.scope,
10187
+ points,
10188
+ effective
10189
+ };
10190
+ if (server.url === null) {
10191
+ classified.push({ ...base, kind: "command" });
10192
+ continue;
10193
+ }
10194
+ const relation = classifyGatewayUrl(
10195
+ context.origin,
10196
+ context.projectId,
10197
+ server.url
10198
+ );
10199
+ if (relation !== "unfamiliar") {
10200
+ classified.push({ ...base, kind: relation });
10201
+ continue;
10202
+ }
10203
+ const url = server.url;
10204
+ let answer = probed.get(url);
10205
+ if (answer === void 0) {
10206
+ answer = context.probe === void 0 || probed.size >= MAX_PROBES ? Promise.resolve("unknown") : context.probe(url).catch(() => "unknown");
10207
+ probed.set(url, answer);
10208
+ }
10209
+ classified.push({ ...base, kind: await answer });
9656
10210
  }
9657
- if (read.servers.some((server) => server.url === input.url)) {
9658
- return "registered";
10211
+ return classified;
10212
+ }
10213
+ async function inspectServers(input) {
10214
+ if (input.tool === "codex") {
10215
+ const read2 = await readCodexServers(input);
10216
+ return read2.state === "read" ? {
10217
+ state: "read",
10218
+ servers: await classifyServers(
10219
+ read2.servers.map((server) => ({
10220
+ ...server,
10221
+ scope: null
10222
+ })),
10223
+ input
10224
+ )
10225
+ } : { state: read2.state };
9659
10226
  }
9660
- return read.servers.length > 0 ? "elsewhere" : "missing";
10227
+ const read = await readClaudeServers(input);
10228
+ return read.state === "read" ? { state: "read", servers: await classifyServers(read.servers, input) } : { state: read.state };
10229
+ }
10230
+ function names(servers) {
10231
+ return [...new Set(servers.map((server) => server.name))];
10232
+ }
10233
+ function projectServersOf(servers) {
10234
+ return servers.filter(
10235
+ (server) => server.effective && server.kind === "this-project"
10236
+ );
10237
+ }
10238
+ function orgWideServersOf(servers) {
10239
+ return servers.filter(
10240
+ (server) => server.effective && server.kind === "org-wide"
10241
+ );
10242
+ }
10243
+ function registrationOf(servers, name) {
10244
+ const here = names(projectServersOf(servers));
10245
+ const sameName2 = servers.filter((server) => server.name === name);
10246
+ const state = here.length === 0 ? sameName2.length > 0 ? "elsewhere" : "missing" : !here.includes(name) && here.every((n) => n === LEGACY_SERVER_NAME) ? "legacy" : "registered";
10247
+ return {
10248
+ state,
10249
+ projectServers: here.map(shownName),
10250
+ orgWide: names(orgWideServersOf(servers)).map(shownName),
10251
+ foreignSameName: sameName2.some(
10252
+ (server) => server.effective && (server.kind === "foreign" || server.kind === "command" || server.kind === "unknown")
10253
+ )
10254
+ };
9661
10255
  }
9662
10256
 
9663
10257
  // src/lib/instructions/hook.ts
9664
10258
  import { createHash as createHash4 } from "crypto";
9665
10259
  import { mkdir as mkdir3 } from "fs/promises";
9666
10260
  import { tmpdir } from "os";
9667
- import path11 from "path";
10261
+ import path13 from "path";
9668
10262
 
9669
10263
  // src/lib/instructions/identifiers.ts
9670
10264
  var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -9673,11 +10267,11 @@ function isIdentifier(value) {
9673
10267
  }
9674
10268
 
9675
10269
  // src/lib/instructions/hook.ts
9676
- var CLAUDE_SETTINGS_RELATIVE_PATH = path11.join(
10270
+ var CLAUDE_SETTINGS_RELATIVE_PATH = path13.join(
9677
10271
  ".claude",
9678
10272
  "settings.local.json"
9679
10273
  );
9680
- var CODEX_HOOKS_RELATIVE_PATH = path11.join(".codex", "hooks.json");
10274
+ var CODEX_HOOKS_RELATIVE_PATH = path13.join(".codex", "hooks.json");
9681
10275
  var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
9682
10276
  var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
9683
10277
  var HOOK_TIMEOUT_SECONDS = 15;
@@ -9694,10 +10288,10 @@ function subcommandsForEvent(event) {
9694
10288
  return event === "Stop" ? STOP_SUBCOMMANDS : SESSION_START_SUBCOMMANDS;
9695
10289
  }
9696
10290
  async function withHookConfigLock(root, posixPath, work) {
9697
- const directory = path11.join(tmpdir(), "fabric-instructions-hook-locks");
10291
+ const directory = path13.join(tmpdir(), "fabric-instructions-hook-locks");
9698
10292
  await mkdir3(directory, { recursive: true });
9699
- const identity = createHash4("sha256").update(path11.resolve(root)).update("\0").update(posixPath).digest("hex");
9700
- const lockPath2 = path11.join(directory, `${identity}.lock`);
10293
+ const identity = createHash4("sha256").update(path13.resolve(root)).update("\0").update(posixPath).digest("hex");
10294
+ const lockPath2 = path13.join(directory, `${identity}.lock`);
9701
10295
  try {
9702
10296
  return await withExclusiveLock(work, {
9703
10297
  lockPath: lockPath2,
@@ -9752,7 +10346,7 @@ async function assertKeyStaysOutside(destination, configPath) {
9752
10346
  }
9753
10347
  }
9754
10348
  async function readHookConfig(root, hookTarget) {
9755
- const settingsPath = path11.join(root, hookTarget.relativePath);
10349
+ const settingsPath = path13.join(root, hookTarget.relativePath);
9756
10350
  let existingRaw;
9757
10351
  try {
9758
10352
  const existing = await readFileSafely(root, hookTarget.posixPath, {
@@ -9831,7 +10425,7 @@ function pruneMatching(groups, projectId, subcommands) {
9831
10425
  async function mergeCommandHook(input) {
9832
10426
  const hookTarget = hookTargetFor(input.tool ?? "claude-code");
9833
10427
  return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
9834
- const settingsPath = path11.join(input.root, hookTarget.relativePath);
10428
+ const settingsPath = path13.join(input.root, hookTarget.relativePath);
9835
10429
  const { settings, existingRaw } = await readHookConfig(
9836
10430
  input.root,
9837
10431
  hookTarget
@@ -9878,7 +10472,7 @@ async function mergeSessionStartHook(input) {
9878
10472
  async function removeCommandHook(input) {
9879
10473
  const hookTarget = hookTargetFor(input.tool ?? "claude-code");
9880
10474
  return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
9881
- const settingsPath = path11.join(input.root, hookTarget.relativePath);
10475
+ const settingsPath = path13.join(input.root, hookTarget.relativePath);
9882
10476
  const { settings, existingRaw } = await readHookConfig(
9883
10477
  input.root,
9884
10478
  hookTarget
@@ -10072,9 +10666,9 @@ function isRecord2(value) {
10072
10666
  }
10073
10667
 
10074
10668
  // src/lib/instructions/agent-mcp.ts
10075
- var CLAUDE_SERVER_NAME = "fabric";
10076
10669
  var WRITE_TIMEOUT_MS = 2e4;
10077
10670
  var LOGIN_TIMEOUT_MS2 = 5 * 6e4;
10671
+ var STATUS_TIMEOUT_MS = 5e3;
10078
10672
  var TOOL_LABEL2 = {
10079
10673
  "claude-code": "Claude Code",
10080
10674
  codex: "Codex"
@@ -10083,11 +10677,23 @@ var TOOL_COMMAND = {
10083
10677
  "claude-code": "claude",
10084
10678
  codex: "codex"
10085
10679
  };
10680
+ function isRegistered(result) {
10681
+ return result.outcome.kind === "registered" || result.outcome.kind === "replaced" || result.outcome.kind === "already";
10682
+ }
10086
10683
  function mcpRegistrationComplete(results) {
10087
- return results.every(
10088
- (result) => result.outcome.kind === "registered" || result.outcome.kind === "replaced" || result.outcome.kind === "already"
10684
+ return results.every(isRegistered);
10685
+ }
10686
+ function mcpSetupFailed(results) {
10687
+ return results.some(
10688
+ (result) => !isRegistered(result) && result.outcome.kind !== "manual"
10089
10689
  );
10090
10690
  }
10691
+ function mcpManualSteps(results) {
10692
+ return results.filter((result) => result.outcome.kind === "manual").map((result) => ({
10693
+ tool: result.tool,
10694
+ registerLine: result.registerLine
10695
+ }));
10696
+ }
10091
10697
  function mcpAuthenticationPending(results) {
10092
10698
  return results.some(
10093
10699
  (result) => result.login === "printed" || result.login === "not-finished"
@@ -10098,12 +10704,8 @@ function codexServerName(projectId) {
10098
10704
  const suffix = projectId.toLowerCase().replace(/[^a-z0-9]+/g, "").slice(-SERVER_ID_SUFFIX_LENGTH);
10099
10705
  return `fabric-${suffix === "" ? projectId : suffix}`;
10100
10706
  }
10101
- function serverNameFor(tool, projectId) {
10102
- return tool === "codex" ? codexServerName(projectId) : CLAUDE_SERVER_NAME;
10103
- }
10104
- var PLAIN_NAME = /^[A-Za-z0-9_-]{1,64}$/;
10105
- function shownName(name) {
10106
- return PLAIN_NAME.test(name) ? name : "(a name with unusual characters)";
10707
+ function serverNameFor(projectId) {
10708
+ return codexServerName(projectId);
10107
10709
  }
10108
10710
  function registerArguments(tool, name, url) {
10109
10711
  return tool === "codex" ? ["mcp", "add", name, "--url", url] : ["mcp", "add", "--scope", "local", "--transport", "http", name, url];
@@ -10129,19 +10731,26 @@ async function agentRegistrationFacts(input) {
10129
10731
  if (hooks.state !== "ok" || hooks.commands.length === 0) {
10130
10732
  continue;
10131
10733
  }
10132
- const name = serverNameFor(tool, input.projectId);
10734
+ const name = serverNameFor(input.projectId);
10735
+ const inspected = await inspectServers({
10736
+ tool,
10737
+ origin: input.origin,
10738
+ projectId: input.projectId,
10739
+ probe: input.probe,
10740
+ cwd: input.cwd,
10741
+ home: input.home,
10742
+ env: input.env,
10743
+ platform: input.platform
10744
+ });
10133
10745
  facts.push({
10134
10746
  tool,
10135
10747
  name,
10136
- state: await readRegistration({
10137
- tool,
10138
- name,
10139
- url,
10140
- cwd: input.cwd,
10141
- home: input.home,
10142
- env: input.env,
10143
- platform: input.platform
10144
- }),
10748
+ ...inspected.state === "read" ? registrationOf(inspected.servers, name) : {
10749
+ state: inspected.state === "unlocated" ? "missing" : "unreadable",
10750
+ projectServers: [],
10751
+ orgWide: [],
10752
+ foreignSameName: false
10753
+ },
10145
10754
  registerLine: lineOf(
10146
10755
  TOOL_COMMAND[tool],
10147
10756
  registerArguments(tool, name, url)
@@ -10153,36 +10762,65 @@ async function agentRegistrationFacts(input) {
10153
10762
  function succeeded(run3) {
10154
10763
  return run3.kind === "exited" && run3.code === 0;
10155
10764
  }
10156
- function isOurs(origin, url) {
10157
- return url !== null && isGatewayUrlOf(origin, url);
10765
+ function sameServerUrl(left, right) {
10766
+ if (left === null) {
10767
+ return false;
10768
+ }
10769
+ const normal = normalizeServerUrl(left);
10770
+ return normal === null ? left === right : normal === normalizeServerUrl(right);
10771
+ }
10772
+ function surroundingsOf(servers) {
10773
+ const orgWide = orgWideServersOf(servers)[0];
10774
+ const unverified = servers.filter((server) => server.effective && server.kind === "unknown").map((server) => ({
10775
+ name: shownName(server.name),
10776
+ points: server.points
10777
+ }));
10778
+ return {
10779
+ ...orgWide === void 0 ? {} : { orgWide: shownName(orgWide.name) },
10780
+ unverified
10781
+ };
10782
+ }
10783
+ function foundUnder(here, name) {
10784
+ return (here.find((server) => server.name === name) ?? here.find((server) => server.name === LEGACY_SERVER_NAME) ?? here[0])?.name;
10158
10785
  }
10159
10786
  async function registerWithClaude(run3, context) {
10160
- const name = CLAUDE_SERVER_NAME;
10161
- const read = await readClaudeServers({
10162
- name,
10787
+ const name = serverNameFor(context.projectId);
10788
+ const inspected = await inspectServers({
10789
+ tool: "claude-code",
10790
+ origin: context.origin,
10791
+ projectId: context.projectId,
10792
+ probe: context.probe,
10163
10793
  cwd: context.cwd,
10164
10794
  home: context.home,
10165
10795
  env: context.env,
10166
10796
  platform: context.platform
10167
10797
  });
10168
- if (read.state !== "read") {
10798
+ if (inspected.state !== "read") {
10169
10799
  return { outcome: { kind: "failed" }, name };
10170
10800
  }
10171
- const existing = read.servers;
10172
- if (existing.some((server) => server.url === context.url)) {
10173
- return { outcome: { kind: "already" }, name };
10801
+ const servers = inspected.servers;
10802
+ const around = surroundingsOf(servers);
10803
+ const alreadyAs = foundUnder(projectServersOf(servers), name);
10804
+ if (alreadyAs !== void 0) {
10805
+ return { outcome: { kind: "already" }, name: alreadyAs, ...around };
10174
10806
  }
10175
- if (existing.some((server) => !isOurs(context.origin, server.url))) {
10176
- return { outcome: { kind: "left", reason: "foreign" }, name };
10807
+ const local = servers.find(
10808
+ (server) => server.scope === "local" && server.name === name
10809
+ );
10810
+ if (local !== void 0 && local.kind !== "other-project") {
10811
+ return {
10812
+ outcome: { kind: "left", scope: "local", points: local.points },
10813
+ name,
10814
+ ...around
10815
+ };
10177
10816
  }
10178
- const replacing = existing.length > 0;
10179
- if (replacing) {
10180
- if (!existing.some((server) => server.scope === "local")) {
10181
- return { outcome: { kind: "left", reason: "other-scope" }, name };
10182
- }
10817
+ const stale = servers.filter(
10818
+ (server) => server.scope === "local" && server.kind === "other-project" && (server.name === name || server.name === LEGACY_SERVER_NAME)
10819
+ ).map((server) => server.name);
10820
+ for (const staleName of stale) {
10183
10821
  const removed = await run3(
10184
10822
  "claude",
10185
- ["mcp", "remove", name, "--scope", "local"],
10823
+ ["mcp", "remove", staleName, "--scope", "local"],
10186
10824
  { cwd: context.cwd, timeoutMs: WRITE_TIMEOUT_MS }
10187
10825
  );
10188
10826
  if (removed.kind === "missing") {
@@ -10200,33 +10838,45 @@ async function registerWithClaude(run3, context) {
10200
10838
  if (added.kind === "missing") {
10201
10839
  return { outcome: { kind: "skipped" }, name };
10202
10840
  }
10203
- return {
10204
- outcome: succeeded(added) ? { kind: replacing ? "replaced" : "registered" } : { kind: "failed" },
10205
- name
10206
- };
10841
+ return succeeded(added) ? {
10842
+ outcome: { kind: stale.length > 0 ? "replaced" : "registered" },
10843
+ name,
10844
+ ...around,
10845
+ shadowed: servers.flatMap(
10846
+ (server) => server.name === name && (server.scope === "user" || server.scope === "project") ? [{ scope: server.scope, points: server.points }] : []
10847
+ )
10848
+ } : { outcome: { kind: "failed" }, name };
10207
10849
  }
10208
10850
  async function registerWithCodex(run3, context, interactive) {
10209
- const read = await readCodexServers({
10851
+ const inspected = await inspectServers({
10852
+ tool: "codex",
10853
+ origin: context.origin,
10854
+ projectId: context.projectId,
10855
+ probe: context.probe,
10856
+ cwd: context.cwd,
10210
10857
  home: context.home,
10211
- env: context.env
10858
+ env: context.env,
10859
+ platform: context.platform
10212
10860
  });
10213
- if (read.state !== "read") {
10861
+ if (inspected.state !== "read") {
10214
10862
  return { outcome: { kind: "failed" }, name: context.name };
10215
10863
  }
10216
- const servers = read.servers;
10217
- const sameUrl = servers.find((server) => server.url === context.url);
10218
- if (sameUrl !== void 0) {
10219
- return { outcome: { kind: "already" }, name: sameUrl.name };
10864
+ const servers = inspected.servers;
10865
+ const around = surroundingsOf(servers);
10866
+ const alreadyAs = foundUnder(projectServersOf(servers), context.name);
10867
+ if (alreadyAs !== void 0) {
10868
+ return { outcome: { kind: "already" }, name: alreadyAs, ...around };
10220
10869
  }
10221
10870
  const sameName2 = servers.find((server) => server.name === context.name);
10222
- if (sameName2 !== void 0 && !isOurs(context.origin, sameName2.url)) {
10871
+ if (sameName2 !== void 0 && sameName2.kind !== "other-project") {
10223
10872
  return {
10224
- outcome: { kind: "left", reason: "foreign" },
10225
- name: context.name
10873
+ outcome: { kind: "left", scope: null, points: sameName2.points },
10874
+ name: context.name,
10875
+ ...around
10226
10876
  };
10227
10877
  }
10228
10878
  if (!interactive) {
10229
- return { outcome: { kind: "manual" }, name: context.name };
10879
+ return { outcome: { kind: "manual" }, name: context.name, ...around };
10230
10880
  }
10231
10881
  const written = sameName2 === void 0 ? "registered" : "replaced";
10232
10882
  const added = await run3(
@@ -10241,7 +10891,8 @@ async function registerWithCodex(run3, context, interactive) {
10241
10891
  return {
10242
10892
  outcome: { kind: written },
10243
10893
  name: context.name,
10244
- login: "completed"
10894
+ login: "completed",
10895
+ ...around
10245
10896
  };
10246
10897
  }
10247
10898
  const after = await readCodexServers({
@@ -10249,27 +10900,30 @@ async function registerWithCodex(run3, context, interactive) {
10249
10900
  env: context.env
10250
10901
  });
10251
10902
  const registered = after.state === "read" && after.servers.some(
10252
- (server) => server.name === context.name && server.url === context.url
10903
+ (server) => server.name === context.name && sameServerUrl(server.url, context.url)
10253
10904
  );
10254
10905
  return registered ? {
10255
10906
  outcome: { kind: written },
10256
10907
  name: context.name,
10257
- login: "not-finished"
10908
+ login: "not-finished",
10909
+ ...around
10258
10910
  } : { outcome: { kind: "failed" }, name: context.name };
10259
10911
  }
10260
10912
  async function registerAgentMcp(input) {
10261
10913
  const url = projectResource(input.origin, "mcp", input.projectId);
10262
10914
  const where = {
10915
+ projectId: input.projectId,
10263
10916
  cwd: input.cwd,
10264
10917
  origin: input.origin,
10265
10918
  url,
10266
10919
  home: input.home,
10267
10920
  env: input.env,
10268
- platform: input.platform
10921
+ platform: input.platform,
10922
+ probe: input.probe
10269
10923
  };
10270
10924
  const results = [];
10271
10925
  for (const tool of input.tools) {
10272
- const intendedName = serverNameFor(tool, input.projectId);
10926
+ const intendedName = serverNameFor(input.projectId);
10273
10927
  const command = TOOL_COMMAND[tool];
10274
10928
  const registration = tool === "codex" ? await registerWithCodex(
10275
10929
  input.run,
@@ -10283,6 +10937,11 @@ async function registerAgentMcp(input) {
10283
10937
  expectedName: intendedName,
10284
10938
  url,
10285
10939
  outcome: registration.outcome,
10940
+ shadowed: registration.shadowed ?? [],
10941
+ orgWide: registration.orgWide ?? null,
10942
+ unverified: registration.unverified ?? [],
10943
+ status: "unchecked",
10944
+ host: describeTarget(url),
10286
10945
  login: registration.login ?? (registration.outcome.kind === "already" && loginLine !== null ? "hint" : "unneeded"),
10287
10946
  registerLine: lineOf(
10288
10947
  command,
@@ -10291,6 +10950,25 @@ async function registerAgentMcp(input) {
10291
10950
  loginLine
10292
10951
  });
10293
10952
  }
10953
+ for (const result of results) {
10954
+ if (!isRegistered(result)) {
10955
+ continue;
10956
+ }
10957
+ if (result.tool === "codex") {
10958
+ result.status = "unavailable";
10959
+ } else if (input.checkSignIn !== false) {
10960
+ result.status = await claudeStatus(
10961
+ input.run,
10962
+ result.name,
10963
+ input.cwd
10964
+ );
10965
+ }
10966
+ if (result.status === "connected") {
10967
+ result.login = "signed-in";
10968
+ } else if (result.status === "needs-sign-in" && result.outcome.kind === "already" && result.loginLine !== null) {
10969
+ result.login = "printed";
10970
+ }
10971
+ }
10294
10972
  for (const result of results) {
10295
10973
  if (result.login !== "unneeded" || result.outcome.kind !== "registered" && result.outcome.kind !== "replaced") {
10296
10974
  continue;
@@ -10312,6 +10990,24 @@ async function registerAgentMcp(input) {
10312
10990
  }
10313
10991
  return results;
10314
10992
  }
10993
+ var STATUS_LINE = /^\s*Status:\s*(?:[^\sA-Za-z0-9]+\s+)?(.+?)\s*$/m;
10994
+ async function claudeStatus(run3, name, cwd) {
10995
+ const answer = await run3("claude", ["mcp", "get", name], {
10996
+ cwd,
10997
+ timeoutMs: STATUS_TIMEOUT_MS
10998
+ });
10999
+ if (answer.kind !== "exited" || answer.code !== 0) {
11000
+ return "unavailable";
11001
+ }
11002
+ const status = STATUS_LINE.exec(answer.stdout)?.[1]?.toLowerCase() ?? "";
11003
+ if (status === "connected") {
11004
+ return "connected";
11005
+ }
11006
+ if (status === "needs authentication") {
11007
+ return "needs-sign-in";
11008
+ }
11009
+ return /^(?:failed|disconnected|not connected|error)\b/.test(status) ? "unreachable" : "unavailable";
11010
+ }
10315
11011
  function agentMcpLines(results) {
10316
11012
  const lines2 = [];
10317
11013
  for (const result of results) {
@@ -10335,7 +11031,7 @@ function agentMcpLines(results) {
10335
11031
  break;
10336
11032
  case "left":
10337
11033
  lines2.push(
10338
- outcome.reason === "foreign" ? `${label} already has a server named "${result.name}" that is not a Fabric gateway, so it was left alone. Remove it, then ${byHand(result.registerLine)}` : `${label} has a "${result.name}" server in another scope that points at a different Fabric gateway, so it was left alone. Remove it with: ${TOOL_COMMAND[result.tool]} mcp remove ${result.name}, then ${byHand(result.registerLine)}`
11034
+ `${label} already has a server named "${result.name}" ${outcome.scope === "local" ? "in this checkout's local settings" : "in its settings"} that points at ${outcome.points}, not at this project's gateway, so it was left alone. Remove it, then ${byHand(result.registerLine)}`
10339
11035
  );
10340
11036
  break;
10341
11037
  case "skipped":
@@ -10358,6 +11054,21 @@ function agentMcpLines(results) {
10358
11054
  return unreachable;
10359
11055
  }
10360
11056
  }
11057
+ if (result.orgWide !== null && isRegistered(result)) {
11058
+ lines2.push(
11059
+ `Your organization-wide Fabric server "${result.orgWide}" stays as it is; coding instructions use the project server "${result.name}".`
11060
+ );
11061
+ }
11062
+ for (const server of result.unverified) {
11063
+ lines2.push(
11064
+ `${label} has a server "${server.name}" at ${server.points} that could not be told apart from Fabric's, so it was left as it is.`
11065
+ );
11066
+ }
11067
+ for (const server of result.shadowed) {
11068
+ lines2.push(
11069
+ `${label} also has a "${result.name}" server in your ${server.scope} settings (${server.points}); in this checkout the project's gateway takes precedence.`
11070
+ );
11071
+ }
10361
11072
  if (result.login === "printed") {
10362
11073
  lines2.push(
10363
11074
  result.loginLine === null ? `To finish, sign ${label} in to the server with its own mcp login command.` : `To finish, sign ${label} in to it: ${result.loginLine}`
@@ -10376,14 +11087,42 @@ function agentMcpLines(results) {
10376
11087
  }
10377
11088
  return lines2;
10378
11089
  }
11090
+ function agentMcpSummaryLines(results) {
11091
+ return results.map((result) => {
11092
+ const label = TOOL_LABEL2[result.tool];
11093
+ const outcome = result.outcome;
11094
+ if (!isRegistered(result)) {
11095
+ return outcome.kind === "manual" ? `${label}: not registered yet, see above.` : `${label}: nothing usable for this project, see above.`;
11096
+ }
11097
+ const server = `"${result.name}"`;
11098
+ if (result.login === "completed" || result.login === "signed-in") {
11099
+ return `${label}: ${server} connected.`;
11100
+ }
11101
+ switch (result.status) {
11102
+ case "connected":
11103
+ return `${label}: ${server} connected.`;
11104
+ case "needs-sign-in":
11105
+ return `${label}: ${server} registered, needs sign-in.`;
11106
+ case "unreachable":
11107
+ return `${label}: ${server} registered, unreachable (${result.host}).`;
11108
+ case "unavailable":
11109
+ return result.tool === "codex" ? `${label}: ${server} registered; sign-in status not available for Codex.` : `${label}: ${server} registered; its status could not be read.`;
11110
+ case "unchecked":
11111
+ return `${label}: ${server} registered.`;
11112
+ default: {
11113
+ const unreachable = result.status;
11114
+ return unreachable;
11115
+ }
11116
+ }
11117
+ });
11118
+ }
10379
11119
 
10380
11120
  // src/lib/instructions/agent-run.ts
10381
11121
  import { spawn as spawn3 } from "child_process";
10382
- import path13 from "path";
10383
11122
 
10384
11123
  // src/lib/instructions/path-lookup.ts
10385
11124
  import { stat as stat3 } from "fs/promises";
10386
- import path12 from "path";
11125
+ import path14 from "path";
10387
11126
  var MAX_PATH_ENTRIES = 256;
10388
11127
  var MAX_PATHEXT_ENTRIES = 32;
10389
11128
  var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
@@ -10397,7 +11136,7 @@ function readVariable(lookup, name) {
10397
11136
  return key === void 0 ? void 0 : lookup.env[key];
10398
11137
  }
10399
11138
  function pathApi(platform) {
10400
- return platform === "win32" ? path12.win32 : path12.posix;
11139
+ return platform === "win32" ? path14.win32 : path14.posix;
10401
11140
  }
10402
11141
  function searchDirectories(lookup) {
10403
11142
  const api = pathApi(lookup.platform);
@@ -10489,10 +11228,6 @@ function launch(file, args, platform, env2) {
10489
11228
  }
10490
11229
  return { file, args: [...args], verbatim: false };
10491
11230
  }
10492
- function taskkillPath(env2 = process.env) {
10493
- const root = env2.SystemRoot ?? env2.SYSTEMROOT ?? env2.windir ?? "C:\\Windows";
10494
- return path13.win32.join(root, "System32", "taskkill.exe");
10495
- }
10496
11231
  function stop(child, signal) {
10497
11232
  if (process.platform === "win32" && child.pid !== void 0) {
10498
11233
  try {
@@ -10597,8 +11332,8 @@ import { createHash as createHash5 } from "crypto";
10597
11332
  import { chmod } from "fs/promises";
10598
11333
 
10599
11334
  // src/lib/instructions/lock.ts
10600
- import { readFile as readFile4 } from "fs/promises";
10601
- import path14 from "path";
11335
+ import { readFile as readFile5 } from "fs/promises";
11336
+ import path15 from "path";
10602
11337
  var LOCK_DIRECTORY = ".fabric";
10603
11338
  var LOCK_FILENAME = "instructions.lock";
10604
11339
  var LOCK_VERSION = 3;
@@ -10606,13 +11341,13 @@ var READABLE_LOCK_VERSIONS = [1, 2, LOCK_VERSION];
10606
11341
  var LOCK_RELATIVE_PATH = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
10607
11342
  var SHA256_HEX2 = /^[0-9a-f]{64}$/;
10608
11343
  function lockPath(destination) {
10609
- return path14.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
11344
+ return path15.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
10610
11345
  }
10611
11346
  async function readLock(destination) {
10612
11347
  const file = lockPath(destination);
10613
11348
  let raw;
10614
11349
  try {
10615
- raw = await readFile4(file, "utf8");
11350
+ raw = await readFile5(file, "utf8");
10616
11351
  } catch (error) {
10617
11352
  if (error.code === "ENOENT") {
10618
11353
  return null;
@@ -11214,11 +11949,11 @@ function extractBundle(archive, wanted) {
11214
11949
  }
11215
11950
 
11216
11951
  // src/lib/instructions/clone-target.ts
11217
- import { lstat as lstat5 } from "fs/promises";
11218
- import path15 from "path";
11952
+ import { lstat as lstat6 } from "fs/promises";
11953
+ import path16 from "path";
11219
11954
  async function resolveCloneFolder(base, folder) {
11220
- const target = path15.resolve(base, folder);
11221
- const stats = await lstat5(target).catch((error) => {
11955
+ const target = path16.resolve(base, folder);
11956
+ const stats = await lstat6(target).catch((error) => {
11222
11957
  if (error.code === "ENOENT") {
11223
11958
  return null;
11224
11959
  }
@@ -11236,7 +11971,7 @@ async function resolveCloneFolder(base, folder) {
11236
11971
  // src/lib/instructions/doctor.ts
11237
11972
  import { createHash as createHash9 } from "crypto";
11238
11973
  import { stat as stat4 } from "fs/promises";
11239
- import path17 from "path";
11974
+ import path18 from "path";
11240
11975
 
11241
11976
  // src/lib/instructions/file-downloads.ts
11242
11977
  import { createHash as createHash6 } from "crypto";
@@ -11583,11 +12318,11 @@ function isTooLarge(error) {
11583
12318
  import { createHash as createHash8 } from "crypto";
11584
12319
  import { mkdtemp, realpath as realpath3, rm } from "fs/promises";
11585
12320
  import { tmpdir as tmpdir2 } from "os";
11586
- import path16 from "path";
12321
+ import path17 from "path";
11587
12322
  var IN_MEMORY_SNAPSHOT_BYTES = 50 * 1024 * 1024;
11588
12323
  async function stageFilesByUrl(options) {
11589
12324
  const root = await realpath3(
11590
- await mkdtemp(path16.join(tmpdir2(), "fabric-instructions-"))
12325
+ await mkdtemp(path17.join(tmpdir2(), "fabric-instructions-"))
11591
12326
  );
11592
12327
  const files = /* @__PURE__ */ new Map();
11593
12328
  const dispose = () => rm(root, { recursive: true, force: true });
@@ -11633,11 +12368,12 @@ var MAX_LOCK_BYTES = 16 * 1024 * 1024;
11633
12368
  var MAX_DOWNLOAD_BYTES = 52428800 + 1048576 + 5e3 * (512 + 2 * 1024);
11634
12369
  var PROBE_TIMEOUT_MS = 5e3;
11635
12370
  var PROBE_BUDGET_MS = 15e3;
11636
- var MAX_PROBES = 20;
12371
+ var MAX_PROBES2 = 20;
11637
12372
  var PROBE_CONCURRENCY = 4;
11638
12373
  var CLI_INSTALL_COMMAND = "npm install -g @fabricorg/cli";
11639
12374
  var GIT_BUDGET_MS = 1e4;
11640
12375
  var LOCK_NOT_USED = "not used in a checkout of the repository";
12376
+ var LOCK_NOT_USED_DIRECT = "not used: this project reads its repository directly, so Fabric keeps no copy here";
11641
12377
  var LOCK_NOT_HERE = "not used here: this folder is not a checkout of the project's repository";
11642
12378
  var SKIP_AFTER_AUTH = "not evaluated: the API key check failed";
11643
12379
  var SKIP_AFTER_ACCESS = "not evaluated: the project could not be read";
@@ -11755,9 +12491,9 @@ function buildFabricCommand(args, platform = process.platform) {
11755
12491
  function sha256Hex(bytes) {
11756
12492
  return createHash9("sha256").update(bytes).digest("hex");
11757
12493
  }
11758
- function listNames(names) {
11759
- const shown2 = names.slice(0, MAX_NAMES_IN_FIX);
11760
- const rest = names.length - shown2.length;
12494
+ function listNames(names2) {
12495
+ const shown2 = names2.slice(0, MAX_NAMES_IN_FIX);
12496
+ const rest = names2.length - shown2.length;
11761
12497
  return rest > 0 ? `${shown2.join(", ")} and ${rest} more` : shown2.join(", ");
11762
12498
  }
11763
12499
  function capItems(items, status) {
@@ -11963,13 +12699,14 @@ async function resolveCheckout(input, access) {
11963
12699
  }
11964
12700
  const { repository, snapshot } = access.published;
11965
12701
  try {
11966
- return input.inspectCheckout ? await input.inspectCheckout({ repository, snapshot }) : await inspectCheckout({
12702
+ const report = input.inspectCheckout ? await input.inspectCheckout({ repository, snapshot }) : await inspectCheckout({
11967
12703
  destination: input.root,
11968
12704
  repository,
11969
12705
  snapshot,
11970
12706
  deadline: Date.now() + GIT_BUDGET_MS,
11971
12707
  remote: input.remote
11972
12708
  });
12709
+ return await withDirectContains(report, access.published);
11973
12710
  } catch {
11974
12711
  return reportFor(repository, {
11975
12712
  class: "unknown",
@@ -11977,6 +12714,20 @@ async function resolveCheckout(input, access) {
11977
12714
  });
11978
12715
  }
11979
12716
  }
12717
+ async function withDirectContains(report, published) {
12718
+ const direct = published.direct;
12719
+ const head = report.state?.head;
12720
+ if (report.classification.class !== "matching" || report.contains !== void 0 || direct?.availability !== "READY" || head === null || head === void 0) {
12721
+ return report;
12722
+ }
12723
+ const answer = await isAncestor(
12724
+ report.classification.toplevel,
12725
+ direct.currentCommitSha,
12726
+ head,
12727
+ Date.now() + GIT_BUDGET_MS
12728
+ );
12729
+ return { ...report, contains: answer.kind === "ok" ? answer.value : null };
12730
+ }
11980
12731
  async function checkAuth(input, client) {
11981
12732
  const id = "auth";
11982
12733
  const login = loginCommand2(input);
@@ -12197,6 +12948,28 @@ function checkPublished(access) {
12197
12948
  }
12198
12949
  const answer = access.published;
12199
12950
  const repository = answer.sourceOfTruth === "REPOSITORY";
12951
+ const direct = answer.direct;
12952
+ if (repository && answer.repository && direct?.availability === "READY") {
12953
+ const where = sanitizeDisplayText(
12954
+ `${answer.repository.host}/${answer.repository.path}`,
12955
+ 200
12956
+ );
12957
+ const commit = sanitizeDisplayText(direct.currentCommitSha, 40);
12958
+ return [
12959
+ makeCheck(
12960
+ id,
12961
+ "server",
12962
+ "pass",
12963
+ `read directly from ${where} at ${commit.slice(0, 12)}\u2026 on ${sanitizeDisplayText(answer.repository.ref, 200)}; Fabric copies nothing`,
12964
+ { repository: answer.repository }
12965
+ ),
12966
+ {
12967
+ kind: "direct",
12968
+ commitSha: direct.currentCommitSha,
12969
+ ref: answer.repository.ref
12970
+ }
12971
+ ];
12972
+ }
12200
12973
  const repositoryConfig = answer.repository ?? null;
12201
12974
  if (answer.published !== true) {
12202
12975
  return [
@@ -12317,7 +13090,12 @@ function checkCheckout(access, snapshot, checkout) {
12317
13090
  if (snapshot.kind === "unpublished") {
12318
13091
  return makeCheck(id, "machine", "skip", NOTHING_PUBLISHED);
12319
13092
  }
12320
- const source = snapshot.snapshot.source;
13093
+ const source = snapshot.kind === "direct" ? {
13094
+ kind: "REPOSITORY",
13095
+ ref: snapshot.ref,
13096
+ commitSha: snapshot.commitSha,
13097
+ current: true
13098
+ } : snapshot.snapshot.source;
12321
13099
  const repository = access.kind === "ok" ? access.published.repository ?? null : null;
12322
13100
  if (source === void 0 || source.kind !== "REPOSITORY" || repository === null) {
12323
13101
  return makeCheck(id, "machine", "skip", PUBLISHED_NOT_SYNCED);
@@ -12368,6 +13146,12 @@ async function checkLock(input, commands, snapshot, checkout) {
12368
13146
  notEvaluated
12369
13147
  ];
12370
13148
  }
13149
+ if (snapshot.kind === "direct") {
13150
+ return [
13151
+ makeCheck(id, "machine", "skip", LOCK_NOT_USED_DIRECT),
13152
+ notEvaluated
13153
+ ];
13154
+ }
12371
13155
  const read = await readLockBounded(input.root);
12372
13156
  if (read.kind === "unreadable") {
12373
13157
  return [
@@ -12516,6 +13300,9 @@ async function checkDrift(input, commands, snapshot, lockState, checkout) {
12516
13300
  if (snapshot.kind === "unpublished") {
12517
13301
  return makeCheck(id, "machine", "skip", NOTHING_PUBLISHED);
12518
13302
  }
13303
+ if (snapshot.kind === "direct") {
13304
+ return makeCheck(id, "machine", "skip", LOCK_NOT_USED_DIRECT);
13305
+ }
12519
13306
  if (checkout !== null && checkoutClass(checkout) !== "upload") {
12520
13307
  return checkoutDrift(snapshot.snapshot, checkout);
12521
13308
  }
@@ -13223,41 +14010,86 @@ var AGENT_TITLE = {
13223
14010
  codex: "Codex"
13224
14011
  };
13225
14012
  function agentRegistrationItems(input) {
13226
- return (input.agentMcp ?? []).map((fact) => {
13227
- const name = `Fabric server in ${AGENT_TITLE[fact.tool]}`;
13228
- switch (fact.state) {
13229
- case "registered":
13230
- return {
13231
- name,
13232
- status: "pass",
13233
- detail: "registered at this project's gateway"
13234
- };
13235
- case "elsewhere":
13236
- return {
13237
- name,
13238
- status: "warn",
13239
- detail: `"${fact.name}" is registered, but not at this project's gateway`
13240
- };
13241
- case "missing":
13242
- return {
13243
- name,
13244
- status: "skip",
13245
- detail: "not registered; init registers it, unless it was run with --no-mcp or, for Codex, with no terminal to sign in at"
13246
- };
13247
- case "unreadable":
13248
- return {
13249
- name,
13250
- status: "skip",
13251
- detail: "its configuration could not be read"
13252
- };
13253
- default: {
13254
- const unreachable = fact.state;
13255
- return unreachable;
13256
- }
14013
+ return (input.agentMcp ?? []).flatMap((fact) => [
14014
+ registrationItem(fact),
14015
+ ...registrationNotes(fact)
14016
+ ]);
14017
+ }
14018
+ function registrationNotes(fact) {
14019
+ const title = AGENT_TITLE[fact.tool];
14020
+ const notes = [];
14021
+ if (fact.orgWide.length > 0) {
14022
+ notes.push({
14023
+ name: `Organization-wide Fabric server in ${title}`,
14024
+ status: "skip",
14025
+ detail: `present as ${listNames(fact.orgWide.map((name) => `"${name}"`))}; left as it is, coding instructions use the project server`
14026
+ });
14027
+ }
14028
+ if (fact.foreignSameName && (fact.state === "registered" || fact.state === "legacy")) {
14029
+ notes.push({
14030
+ name: `Server named "${fact.name}" in ${title}`,
14031
+ status: "skip",
14032
+ detail: "not Fabric's; left as it is"
14033
+ });
14034
+ }
14035
+ return notes;
14036
+ }
14037
+ function registrationItem(fact) {
14038
+ const name = `Fabric server in ${AGENT_TITLE[fact.tool]}`;
14039
+ if (fact.projectServers.length > 1) {
14040
+ return {
14041
+ name,
14042
+ status: "warn",
14043
+ detail: `this project's gateway is registered under ${fact.projectServers.length} names (${fact.projectServers.map((server) => `"${server}"`).join(", ")}); one is enough`
14044
+ };
14045
+ }
14046
+ switch (fact.state) {
14047
+ case "registered":
14048
+ return {
14049
+ name,
14050
+ status: "pass",
14051
+ detail: "registered at this project's gateway"
14052
+ };
14053
+ case "legacy":
14054
+ return {
14055
+ name,
14056
+ status: "warn",
14057
+ detail: `registered at this project's gateway under the older name "fabric", which still works; init now registers "${fact.name}"`
14058
+ };
14059
+ case "elsewhere":
14060
+ return {
14061
+ name,
14062
+ status: "warn",
14063
+ detail: `"${fact.name}" is registered, but not at this project's gateway`
14064
+ };
14065
+ case "missing":
14066
+ return {
14067
+ name,
14068
+ status: "skip",
14069
+ detail: "not registered; init registers it, unless it was run with --no-mcp or, for Codex, with no terminal to sign in at"
14070
+ };
14071
+ case "unreadable":
14072
+ return {
14073
+ name,
14074
+ status: "skip",
14075
+ detail: "its configuration could not be read"
14076
+ };
14077
+ default: {
14078
+ const unreachable = fact.state;
14079
+ return unreachable;
13257
14080
  }
13258
- });
14081
+ }
13259
14082
  }
13260
14083
  function agentRegistrationFix(input) {
14084
+ const legacy = (input.agentMcp ?? []).find(
14085
+ (fact) => fact.state === "legacy" || fact.projectServers.length > 1 && fact.projectServers.includes(LEGACY_SERVER_NAME)
14086
+ );
14087
+ if (legacy !== void 0) {
14088
+ return fixOf(
14089
+ legacy.state === "legacy" ? `optional: remove the older "fabric" server from ${AGENT_TITLE[legacy.tool]} and run init, which registers "${legacy.name}" in its place, or leave it as it is` : `optional: remove the older "fabric" server from ${AGENT_TITLE[legacy.tool]}, which "${legacy.name}" already replaces, or leave it as it is`,
14090
+ `${legacy.tool === "codex" ? "codex" : "claude"} mcp remove fabric${legacy.tool === "codex" ? "" : " --scope local"}`
14091
+ );
14092
+ }
13261
14093
  const first = (input.agentMcp ?? []).find(
13262
14094
  (fact) => fact.state === "elsewhere"
13263
14095
  );
@@ -13419,13 +14251,13 @@ async function commandResolves(input, lookup, command) {
13419
14251
  if (TOOL_NAME.test(command)) {
13420
14252
  return await isOnPath(command, lookup) ? "found" : "missing";
13421
14253
  }
13422
- const api = input.platform === "win32" ? path17.win32 : path17.posix;
14254
+ const api = input.platform === "win32" ? path18.win32 : path18.posix;
13423
14255
  if (api.isAbsolute(command)) {
13424
14256
  return await isExecutableFile(command, input.platform) ? "found" : "missing";
13425
14257
  }
13426
14258
  if (command.includes("/") || command.includes("\\")) {
13427
14259
  return await isExecutableFile(
13428
- path17.resolve(input.root, command),
14260
+ path18.resolve(input.root, command),
13429
14261
  input.platform
13430
14262
  ) ? "found" : "missing";
13431
14263
  }
@@ -13439,7 +14271,7 @@ async function runProbes(probes, items, fetchImpl) {
13439
14271
  while (next < probes.length) {
13440
14272
  const probe = probes[next++];
13441
14273
  const remaining = PROBE_BUDGET_MS - (Date.now() - started);
13442
- if (sent >= MAX_PROBES || remaining <= 0) {
14274
+ if (sent >= MAX_PROBES2 || remaining <= 0) {
13443
14275
  continue;
13444
14276
  }
13445
14277
  sent++;
@@ -13602,7 +14434,7 @@ function formatDoctorText(report, destination) {
13602
14434
  // src/lib/instructions/drop-lock.ts
13603
14435
  import { createHash as createHash10 } from "crypto";
13604
14436
  import { rmdir } from "fs/promises";
13605
- import path18 from "path";
14437
+ import path19 from "path";
13606
14438
  var LOCK_RELATIVE = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
13607
14439
  var MAX_LOCK_BYTES2 = 16 * 1024 * 1024;
13608
14440
  async function dropOwnLock(root, projectId) {
@@ -13622,7 +14454,7 @@ async function dropOwnLock(root, projectId) {
13622
14454
  if (outcome !== "deleted") {
13623
14455
  return false;
13624
14456
  }
13625
- await rmdir(path18.join(root, LOCK_DIRECTORY)).catch(() => void 0);
14457
+ await rmdir(path19.join(root, LOCK_DIRECTORY)).catch(() => void 0);
13626
14458
  return true;
13627
14459
  } catch {
13628
14460
  return false;
@@ -13715,7 +14547,7 @@ function replacePath(text3, target, replacement) {
13715
14547
 
13716
14548
  // src/lib/instructions/fast-forward-run.ts
13717
14549
  import { mkdir as mkdir6, realpath as realpath4 } from "fs/promises";
13718
- import path21 from "path";
14550
+ import path22 from "path";
13719
14551
 
13720
14552
  // src/lib/instructions/fast-forward.ts
13721
14553
  function fastForwardEligibility(facts) {
@@ -13780,7 +14612,7 @@ function fabricCopyLag(input) {
13780
14612
  return "pending";
13781
14613
  }
13782
14614
  function isBehindReason(reason) {
13783
- return reason === "dirty" || reason === "wrong-branch" || reason === "detached" || reason === "operation";
14615
+ return reason === "wrong-branch" || reason === "detached" || reason === "operation";
13784
14616
  }
13785
14617
  function skipped(text3) {
13786
14618
  return `${HOOK_PREFIX} sync skipped: ${text3}`;
@@ -13855,7 +14687,7 @@ function fastForwardLines(outcome, context) {
13855
14687
  login: context.login,
13856
14688
  commands: context.commands
13857
14689
  });
13858
- if (outcome.reason === "auth") {
14690
+ if (outcome.reason === "auth" || outcome.reason === "old-git") {
13859
14691
  stdout.push(text3);
13860
14692
  } else {
13861
14693
  stderr.push(skipped(text3));
@@ -13865,13 +14697,12 @@ function fastForwardLines(outcome, context) {
13865
14697
  }
13866
14698
  case "merge-failed": {
13867
14699
  const text3 = outcomeLine("ff-merge-failed", {
13868
- reason: outcome.reason,
13869
- files: outcome.files,
14700
+ failure: outcome,
13870
14701
  ref: context.ref,
13871
14702
  remote: context.remote,
13872
14703
  commands: context.commands
13873
14704
  });
13874
- if (outcome.reason === "diverged" || outcome.reason === "local-changes") {
14705
+ if (isUserActionableMergeFailure(outcome.reason)) {
13875
14706
  stdout.push(text3);
13876
14707
  otherLine();
13877
14708
  } else {
@@ -13905,10 +14736,10 @@ function fastForwardLines(outcome, context) {
13905
14736
  }
13906
14737
 
13907
14738
  // src/lib/instructions/fast-forward-memory.ts
13908
- import { mkdir as mkdir4, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
13909
- import path19 from "path";
14739
+ import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
14740
+ import path20 from "path";
13910
14741
  function noticeFile(commonDir2) {
13911
- return path19.join(commonDir2, "fabric", "ff-notice.json");
14742
+ return path20.join(commonDir2, "fabric", "ff-notice.json");
13912
14743
  }
13913
14744
  function isRecord4(value) {
13914
14745
  if (typeof value !== "object" || value === null) {
@@ -13919,7 +14750,7 @@ function isRecord4(value) {
13919
14750
  }
13920
14751
  async function isNewNotice(file, key) {
13921
14752
  try {
13922
- const parsed = JSON.parse(await readFile5(file, "utf8"));
14753
+ const parsed = JSON.parse(await readFile6(file, "utf8"));
13923
14754
  return !(isRecord4(parsed) && parsed.projectId === key.projectId && parsed.publishedVersion === key.publishedVersion && parsed.directCommitSha === key.directCommitSha && parsed.reason === key.reason);
13924
14755
  } catch {
13925
14756
  return true;
@@ -13927,7 +14758,7 @@ async function isNewNotice(file, key) {
13927
14758
  }
13928
14759
  async function rememberNotice(file, key) {
13929
14760
  try {
13930
- await mkdir4(path19.dirname(file), { recursive: true });
14761
+ await mkdir4(path20.dirname(file), { recursive: true });
13931
14762
  const temporary = `${file}.${process.pid}.tmp`;
13932
14763
  const record = { v: 1, ...key };
13933
14764
  await writeFile2(temporary, `${JSON.stringify(record)}
@@ -13944,22 +14775,27 @@ async function forgetNotice(file) {
13944
14775
 
13945
14776
  // src/lib/instructions/hook-timing.ts
13946
14777
  var hookTiming = {
13947
- deadlineMs: 1e4,
14778
+ deadlineMs: 9500,
13948
14779
  gitMarginMs: 750,
13949
14780
  selfUpdateBudgetMs: 4e3,
13950
- selfUpdateMarginMs: 1e3
14781
+ selfUpdateMarginMs: 1e3,
14782
+ processStartedAt: void 0,
14783
+ exitWhenDone: false
13951
14784
  };
14785
+ function sinceProcessStart(now = Date.now()) {
14786
+ return hookTiming.processStartedAt === void 0 ? 0 : Math.max(0, now - hookTiming.processStartedAt);
14787
+ }
13952
14788
 
13953
14789
  // src/lib/instructions/hook-trace.ts
13954
- import { mkdir as mkdir5, readFile as readFile6, rename as rename3, writeFile as writeFile3 } from "fs/promises";
13955
- import path20 from "path";
14790
+ import { mkdir as mkdir5, readFile as readFile7, rename as rename3, writeFile as writeFile3 } from "fs/promises";
14791
+ import path21 from "path";
13956
14792
  var TRACE_LIMIT = 50;
13957
14793
  function traceFile(configDir) {
13958
- return path20.join(configDir, "traces", "instructions-hook.jsonl");
14794
+ return path21.join(configDir, "traces", "instructions-hook.jsonl");
13959
14795
  }
13960
14796
  async function appendTrace(file, entry) {
13961
14797
  try {
13962
- const previous = await readFile6(file, "utf8").catch(() => "");
14798
+ const previous = await readFile7(file, "utf8").catch(() => "");
13963
14799
  const kept = previous.split("\n").filter((line2) => {
13964
14800
  try {
13965
14801
  return typeof JSON.parse(line2)?.outcome === "string";
@@ -13968,7 +14804,7 @@ async function appendTrace(file, entry) {
13968
14804
  }
13969
14805
  }).slice(-(TRACE_LIMIT - 1));
13970
14806
  kept.push(JSON.stringify(entry));
13971
- await mkdir5(path20.dirname(file), { recursive: true, mode: 448 });
14807
+ await mkdir5(path21.dirname(file), { recursive: true, mode: 448 });
13972
14808
  const temporary = `${file}.${process.pid}.tmp`;
13973
14809
  await writeFile3(temporary, `${kept.join("\n")}
13974
14810
  `, { mode: 384 });
@@ -13981,54 +14817,57 @@ async function appendTrace(file, entry) {
13981
14817
  var MERGE_RESERVE_MS = 1500;
13982
14818
  var MERGE_FLOOR_MS = 500;
13983
14819
  var FF_LOCK_STALE_MS = 3e4;
14820
+ var Phases = class {
14821
+ constructor() {
14822
+ this.totals = {};
14823
+ }
14824
+ async time(name, work) {
14825
+ const started = Date.now();
14826
+ try {
14827
+ return await work();
14828
+ } finally {
14829
+ this.totals[name] = (this.totals[name] ?? 0) + (Date.now() - started);
14830
+ }
14831
+ }
14832
+ };
13984
14833
  var show2 = (value, max = 200) => sanitizeDisplayText(value, max);
13985
14834
  async function samePath(a, b) {
13986
- const resolve = async (value) => realpath4(value).catch(() => path21.resolve(value));
14835
+ const resolve = async (value) => realpath4(value).catch(() => path22.resolve(value));
13987
14836
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
13988
14837
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
13989
14838
  }
13990
- async function readState(root, traits, deadline) {
13991
- const branch = await currentBranch(root, deadline);
13992
- const head = await headSha(root, deadline);
13993
- const clean = await isClean(root, deadline);
13994
- const operation = await operationInProgress(root, deadline);
13995
- if (branch.kind !== "ok" || head.kind !== "ok" || clean.kind !== "ok" || operation.kind !== "ok") {
13996
- return null;
13997
- }
13998
- return {
13999
- branch: branch.value,
14000
- head: head.value,
14001
- clean: clean.value,
14002
- operation: operation.value,
14003
- traits
14004
- };
14005
- }
14006
14839
  async function readFacts(run3, deadline) {
14007
14840
  const { classification, repository } = run3;
14008
14841
  const root = classification.toplevel;
14009
- const state = await readState(root, classification.traits, deadline);
14010
- if (state === null) {
14842
+ const [branch, head, operation, locks, holders] = await Promise.all([
14843
+ currentBranch(root, deadline),
14844
+ headSha(root, deadline),
14845
+ operationInProgress(root, deadline),
14846
+ lockFilesPresent(root, deadline),
14847
+ worktreesOnBranch(root, repository.ref, deadline)
14848
+ ]);
14849
+ if (branch.kind !== "ok" || head.kind !== "ok" || operation.kind !== "ok" || locks.kind !== "ok" || holders.kind !== "ok") {
14011
14850
  return null;
14012
14851
  }
14013
- const upstream = state.branch === null ? { kind: "ok", value: null } : await upstreamOf(root, state.branch, deadline);
14014
- const locks = await lockFilesPresent(root, deadline);
14015
- const holders = await worktreesOnBranch(root, repository.ref, deadline);
14016
- if (upstream.kind !== "ok" || locks.kind !== "ok" || holders.kind !== "ok") {
14852
+ const upstream = branch.value === null ? { kind: "ok", value: null } : await upstreamOf(root, branch.value, deadline);
14853
+ if (upstream.kind !== "ok") {
14017
14854
  return null;
14018
14855
  }
14019
- let heldElsewhere = false;
14020
- for (const holder of holders.value) {
14021
- if (!await samePath(holder, root)) {
14022
- heldElsewhere = true;
14023
- }
14024
- }
14856
+ const elsewhere = await Promise.all(
14857
+ holders.value.map(async (holder) => !await samePath(holder, root))
14858
+ );
14025
14859
  return {
14026
14860
  ref: repository.ref,
14027
14861
  remote: classification.remote,
14028
- state,
14862
+ state: {
14863
+ branch: branch.value,
14864
+ head: head.value,
14865
+ operation: operation.value,
14866
+ traits: classification.traits
14867
+ },
14029
14868
  upstream: upstream.value,
14030
14869
  lockFiles: locks.value,
14031
- heldElsewhere
14870
+ heldElsewhere: elsewhere.some(Boolean)
14032
14871
  };
14033
14872
  }
14034
14873
  function unanswered(deadline, head, stage) {
@@ -14050,7 +14889,10 @@ async function underLock(run3, first) {
14050
14889
  const root = classification.toplevel;
14051
14890
  const remote = classification.remote;
14052
14891
  const ref = repository.ref;
14053
- const facts = await readFacts(run3, deadline);
14892
+ const facts = await run3.phases.time(
14893
+ "facts",
14894
+ () => readFacts(run3, deadline)
14895
+ );
14054
14896
  if (facts === null) {
14055
14897
  return unanswered(deadline, first.state.head, "read");
14056
14898
  }
@@ -14072,14 +14914,25 @@ async function underLock(run3, first) {
14072
14914
  if (Date.now() >= fetchDeadline) {
14073
14915
  return { outcome: { kind: "deadline", stage: "fetch" }, head };
14074
14916
  }
14075
- const fetched = run3.gitTransport ? await fetchRefFromUrl(
14076
- root,
14077
- run3.gitTransport.url,
14078
- remote,
14079
- ref,
14080
- fetchDeadline,
14081
- run3.gitTransport
14082
- ) : await fetchRef(root, remote, ref, fetchDeadline);
14917
+ let transport;
14918
+ if (run3.gitTransport) {
14919
+ try {
14920
+ transport = await run3.gitTransport();
14921
+ } catch {
14922
+ return { outcome: { kind: "fetch-failed", reason: "auth" }, head };
14923
+ }
14924
+ }
14925
+ const fetched = await run3.phases.time(
14926
+ "fetch",
14927
+ () => transport ? fetchRefFromUrl(
14928
+ root,
14929
+ transport.url,
14930
+ remote,
14931
+ ref,
14932
+ fetchDeadline,
14933
+ transport
14934
+ ) : fetchRef(root, remote, ref, fetchDeadline)
14935
+ );
14083
14936
  switch (fetched.kind) {
14084
14937
  case "timed-out":
14085
14938
  return { outcome: { kind: "deadline", stage: "fetch" }, head };
@@ -14105,11 +14958,10 @@ async function underLock(run3, first) {
14105
14958
  return fetched;
14106
14959
  }
14107
14960
  const tip = fetched.tip;
14108
- const relation = await tipRelation(root, head, tip, deadline);
14109
- const changed = await factsStillMatch(run3, facts, deadline);
14110
- if (changed !== null) {
14111
- return changed;
14112
- }
14961
+ const relation = await run3.phases.time(
14962
+ "relate",
14963
+ () => tipRelation(root, head, tip, deadline)
14964
+ );
14113
14965
  switch (relation) {
14114
14966
  case "current":
14115
14967
  return { outcome: { kind: "already-current" }, head };
@@ -14123,10 +14975,20 @@ async function underLock(run3, first) {
14123
14975
  default:
14124
14976
  return relation;
14125
14977
  }
14978
+ const changed = await run3.phases.time(
14979
+ "facts",
14980
+ () => factsStillMatch(run3, facts, deadline)
14981
+ );
14982
+ if (changed !== null) {
14983
+ return changed;
14984
+ }
14126
14985
  if (deadline - Date.now() < MERGE_FLOOR_MS) {
14127
14986
  return { outcome: { kind: "deadline", stage: "merge" }, head };
14128
14987
  }
14129
- const merged = await fastForwardTo(root, tip, deadline);
14988
+ const merged = await run3.phases.time(
14989
+ "merge",
14990
+ () => fastForwardTo(root, tip, deadline)
14991
+ );
14130
14992
  switch (merged.kind) {
14131
14993
  case "merged":
14132
14994
  return {
@@ -14152,11 +15014,11 @@ async function underLock(run3, first) {
14152
15014
  };
14153
15015
  }
14154
15016
  return {
14155
- outcome: {
15017
+ outcome: merged.reason === "local-changes" ? {
14156
15018
  kind: "merge-failed",
14157
15019
  reason: merged.reason,
14158
- ...merged.files === void 0 ? {} : { files: merged.files }
14159
- },
15020
+ files: merged.files
15021
+ } : { kind: "merge-failed", reason: merged.reason },
14160
15022
  head
14161
15023
  };
14162
15024
  default:
@@ -14184,13 +15046,13 @@ async function factsStillMatch(run3, expected, deadline) {
14184
15046
  return null;
14185
15047
  }
14186
15048
  async function decide(run3) {
14187
- const { classification, deadline, report } = run3;
15049
+ const { classification, deadline, report, phases } = run3;
14188
15050
  const root = classification.toplevel;
14189
15051
  const head = report.state?.head ?? null;
14190
15052
  if (run3.optedOut) {
14191
15053
  return { outcome: { kind: "opted-out" }, head };
14192
15054
  }
14193
- const facts = await readFacts(run3, deadline);
15055
+ const facts = await phases.time("facts", () => readFacts(run3, deadline));
14194
15056
  if (facts === null) {
14195
15057
  return unanswered(deadline, head, "read");
14196
15058
  }
@@ -14201,15 +15063,32 @@ async function decide(run3) {
14201
15063
  head: facts.state.head
14202
15064
  };
14203
15065
  }
14204
- const common = await commonDir(root, deadline);
15066
+ if (run3.directCommitSha !== void 0 && facts.state.head !== null) {
15067
+ const holds = await phases.time(
15068
+ "contains",
15069
+ () => isAncestor(
15070
+ root,
15071
+ run3.directCommitSha,
15072
+ facts.state.head,
15073
+ deadline
15074
+ )
15075
+ );
15076
+ if (holds.kind === "ok" && holds.value) {
15077
+ return {
15078
+ outcome: { kind: "already-current" },
15079
+ head: facts.state.head
15080
+ };
15081
+ }
15082
+ }
15083
+ const common = await run3.commonDir;
14205
15084
  if (common.kind !== "ok") {
14206
15085
  return unanswered(deadline, head, "read");
14207
15086
  }
14208
- const directory = path21.join(common.value, "fabric");
15087
+ const directory = path22.join(common.value, "fabric");
14209
15088
  try {
14210
15089
  await mkdir6(directory, { recursive: true });
14211
15090
  return await withExclusiveLock(() => underLock(run3, facts), {
14212
- lockPath: path21.join(directory, "ff.lock"),
15091
+ lockPath: path22.join(directory, "ff.lock"),
14213
15092
  staleMs: FF_LOCK_STALE_MS,
14214
15093
  waitMs: 0
14215
15094
  });
@@ -14219,7 +15098,7 @@ async function decide(run3) {
14219
15098
  return {
14220
15099
  outcome: error.abandoned ? {
14221
15100
  kind: "abandoned-lock",
14222
- lockPath: path21.join(directory, "ff.lock")
15101
+ lockPath: path22.join(directory, "ff.lock")
14223
15102
  } : { kind: "locked" },
14224
15103
  head: now.kind === "ok" ? now.value : head
14225
15104
  };
@@ -14243,9 +15122,12 @@ function noticeReason(outcome, lag) {
14243
15122
  if (outcome.kind === "not-safe") {
14244
15123
  return `not-safe:${outcome.reason}`;
14245
15124
  }
14246
- if (outcome.kind === "merge-failed" && (outcome.reason === "diverged" || outcome.reason === "local-changes")) {
15125
+ if (outcome.kind === "merge-failed" && isUserActionableMergeFailure(outcome.reason)) {
14247
15126
  return `merge-failed:${outcome.reason}`;
14248
15127
  }
15128
+ if (outcome.kind === "fetch-failed" && outcome.reason === "old-git") {
15129
+ return "fetch-failed:old-git";
15130
+ }
14249
15131
  if ((outcome.kind === "fast-forwarded" || outcome.kind === "already-current") && lag !== null) {
14250
15132
  return `fabric-lags:${lag}`;
14251
15133
  }
@@ -14266,7 +15148,9 @@ async function runFastForward(run3) {
14266
15148
  const started = Date.now();
14267
15149
  const { repository, snapshot, classification } = run3;
14268
15150
  const root = classification.toplevel;
14269
- const decision = await decide(run3);
15151
+ const phases = new Phases();
15152
+ const commonDir2 = commonDir(root, run3.deadline);
15153
+ const decision = await decide({ ...run3, phases, commonDir: commonDir2 });
14270
15154
  const { outcome } = decision;
14271
15155
  const source = snapshot?.source;
14272
15156
  const snapshotSha = source?.kind === "REPOSITORY" && source.current && isCommitSha(source.commitSha) ? source.commitSha : null;
@@ -14274,7 +15158,7 @@ async function runFastForward(run3) {
14274
15158
  const moved = outcome.kind === "fast-forwarded" || outcome.kind === "already-current";
14275
15159
  const head = outcome.kind === "fast-forwarded" ? outcome.to : decision.head;
14276
15160
  let publishedInHistory = null;
14277
- if (moved && publishedSha !== null && head !== null && publishedSha !== head) {
15161
+ if (moved && snapshot !== void 0 && publishedSha !== null && head !== null && publishedSha !== head) {
14278
15162
  const answer = await isAncestor(
14279
15163
  root,
14280
15164
  publishedSha,
@@ -14303,10 +15187,13 @@ async function runFastForward(run3) {
14303
15187
  version: outcome.kind === "fast-forwarded" && publishedSha === outcome.to && snapshot ? Number(snapshot.version) : null,
14304
15188
  lag,
14305
15189
  head,
14306
- gaveUpAfter: describeDuration(hookTiming.deadlineMs)
15190
+ gaveUpAfter: describeDuration(
15191
+ hookTiming.processStartedAt === void 0 ? hookTiming.deadlineMs : Math.round(sinceProcessStart() / 100) * 100
15192
+ )
14307
15193
  };
14308
15194
  let { stdout, stderr } = fastForwardLines(outcome, context);
14309
- const common = await commonDir(
15195
+ const early = await commonDir2;
15196
+ const common = early.kind === "ok" ? early : await commonDir(
14310
15197
  root,
14311
15198
  Math.max(run3.deadline, Date.now() + 500)
14312
15199
  );
@@ -14336,17 +15223,90 @@ async function runFastForward(run3) {
14336
15223
  projectId: run3.projectId,
14337
15224
  outcome: outcome.kind,
14338
15225
  reason: reasonOf(outcome),
14339
- ms: Date.now() - started
15226
+ ms: Date.now() - started,
15227
+ totalMs: sinceProcessStart(),
15228
+ phases: { ...run3.timings, ...phases.totals }
14340
15229
  });
14341
15230
  }
14342
15231
  return { outcome, stdout, stderr };
14343
15232
  }
14344
15233
 
15234
+ // src/lib/instructions/gateway-probe.ts
15235
+ var PROBE_TIMEOUT_MS2 = 3e3;
15236
+ var MAX_METADATA_CHARS = 64 * 1024;
15237
+ var CHALLENGE_METADATA = /resource_metadata="([^"]+)"/;
15238
+ function sameOrigin(left, right) {
15239
+ const a = normalizeServerUrl(left);
15240
+ const b = normalizeServerUrl(right);
15241
+ return a !== null && b !== null && new URL(a).origin === new URL(b).origin;
15242
+ }
15243
+ async function discard(response) {
15244
+ try {
15245
+ await response.body?.cancel();
15246
+ } catch {
15247
+ }
15248
+ }
15249
+ function request(fetcher, url) {
15250
+ return fetcher(url, {
15251
+ method: "GET",
15252
+ redirect: "manual",
15253
+ credentials: "omit",
15254
+ headers: { accept: "application/json" },
15255
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS2)
15256
+ });
15257
+ }
15258
+ function gatewayProbeFor(input) {
15259
+ const fetcher = input.fetch ?? ((url, init) => fetch(url, init));
15260
+ return async (url) => {
15261
+ if (!sameOrigin(url, input.origin)) {
15262
+ return "unknown";
15263
+ }
15264
+ try {
15265
+ const challenged = await request(fetcher, url);
15266
+ await discard(challenged);
15267
+ if (challenged.status >= 500) {
15268
+ return "unknown";
15269
+ }
15270
+ const metadataUrl = challenged.status === 401 ? CHALLENGE_METADATA.exec(
15271
+ challenged.headers.get("www-authenticate") ?? ""
15272
+ )?.[1] : void 0;
15273
+ if (metadataUrl === void 0) {
15274
+ return "foreign";
15275
+ }
15276
+ if (!sameOrigin(metadataUrl, input.origin)) {
15277
+ return "unknown";
15278
+ }
15279
+ const metadata = await request(fetcher, metadataUrl);
15280
+ if (metadata.status !== 200) {
15281
+ await discard(metadata);
15282
+ return "unknown";
15283
+ }
15284
+ const text3 = await metadata.text();
15285
+ if (text3.length > MAX_METADATA_CHARS) {
15286
+ return "unknown";
15287
+ }
15288
+ const parsed = JSON.parse(text3);
15289
+ const resource = typeof parsed === "object" && parsed !== null ? Reflect.get(parsed, "resource") : void 0;
15290
+ if (typeof resource !== "string") {
15291
+ return "unknown";
15292
+ }
15293
+ const relation = classifyGatewayResource(
15294
+ input.origin,
15295
+ input.projectId,
15296
+ resource
15297
+ );
15298
+ return relation === "unfamiliar" ? "unknown" : relation;
15299
+ } catch {
15300
+ return "unknown";
15301
+ }
15302
+ };
15303
+ }
15304
+
14345
15305
  // src/lib/instructions/git-exclude.ts
14346
- import { appendFile, lstat as lstat6, mkdir as mkdir7, readFile as readFile7 } from "fs/promises";
14347
- import path22 from "path";
15306
+ import { appendFile, lstat as lstat7, mkdir as mkdir7, readFile as readFile8 } from "fs/promises";
15307
+ import path23 from "path";
14348
15308
  function relativeToTopLevel(toplevel, directory, file) {
14349
- return path22.relative(toplevel, path22.join(directory, file)).split(path22.sep).join("/");
15309
+ return path23.relative(toplevel, path23.join(directory, file)).split(path23.sep).join("/");
14350
15310
  }
14351
15311
  function isLiteralEntry(entry) {
14352
15312
  return !/[\n\r*?[\]\\!]/.test(entry) && !entry.startsWith("#");
@@ -14380,8 +15340,8 @@ async function excludeLocalFiles(input) {
14380
15340
  if (file.kind !== "ok") {
14381
15341
  return { kind: "failed", entries: missing };
14382
15342
  }
14383
- await mkdir7(path22.dirname(file.value), { recursive: true });
14384
- const existing = await lstat6(file.value).catch((error) => {
15343
+ await mkdir7(path23.dirname(file.value), { recursive: true });
15344
+ const existing = await lstat7(file.value).catch((error) => {
14385
15345
  if (error.code === "ENOENT") {
14386
15346
  return null;
14387
15347
  }
@@ -14390,7 +15350,7 @@ async function excludeLocalFiles(input) {
14390
15350
  if (existing !== null && !existing.isFile()) {
14391
15351
  return { kind: "failed", entries: missing };
14392
15352
  }
14393
- const text3 = existing === null ? "" : await readFile7(file.value, "utf8");
15353
+ const text3 = existing === null ? "" : await readFile8(file.value, "utf8");
14394
15354
  const present = new Set(text3.split(/\r?\n/));
14395
15355
  const toAdd = missing.filter((entry) => !present.has(entry));
14396
15356
  if (toAdd.length === 0) {
@@ -14406,8 +15366,8 @@ async function excludeLocalFiles(input) {
14406
15366
  }
14407
15367
 
14408
15368
  // src/lib/instructions/hook-launcher.ts
14409
- import { mkdir as mkdir8, readFile as readFile8, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
14410
- import path23 from "path";
15369
+ import { mkdir as mkdir8, readFile as readFile9, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
15370
+ import path24 from "path";
14411
15371
  var NOT_ON_PATH_WARNING = "`fabric` is not on this machine's PATH, so this session hook will not run. Run the setup line from the project's Connect dialog instead: it keeps a copy of this deployment's CLI where the hook finds it.";
14412
15372
  var COPY_IN_USE_WARNING = "The CLI copy the session hook runs is in use and could not be replaced, so the earlier copy was kept. Run this again when no coding session is starting.";
14413
15373
  var UNQUOTABLE_PATH = "The CLI's config folder has characters in its path that cannot be written safely into a hook command. Set XDG_CONFIG_HOME (APPDATA on Windows) to a plain path and run this again.";
@@ -14415,7 +15375,7 @@ function deploymentFolder(origin) {
14415
15375
  return origin.replace(/^(https?):\/\//, "$1-").replace(/[^A-Za-z0-9.-]+/g, "-");
14416
15376
  }
14417
15377
  function bundleCopyPath(configDirectory, origin) {
14418
- return path23.join(
15378
+ return path24.join(
14419
15379
  configDirectory,
14420
15380
  "cli",
14421
15381
  deploymentFolder(origin),
@@ -14423,11 +15383,11 @@ function bundleCopyPath(configDirectory, origin) {
14423
15383
  );
14424
15384
  }
14425
15385
  function samePath2(a, b) {
14426
- const [left, right] = [path23.resolve(a), path23.resolve(b)];
15386
+ const [left, right] = [path24.resolve(a), path24.resolve(b)];
14427
15387
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
14428
15388
  }
14429
15389
  async function writeCopy(bytes, destination) {
14430
- await mkdir8(path23.dirname(destination), { recursive: true });
15390
+ await mkdir8(path24.dirname(destination), { recursive: true });
14431
15391
  const temporary = `${destination}.${process.pid}.tmp`;
14432
15392
  await writeFile4(temporary, bytes, { mode: 420 });
14433
15393
  try {
@@ -14435,7 +15395,7 @@ async function writeCopy(bytes, destination) {
14435
15395
  return true;
14436
15396
  } catch (error) {
14437
15397
  await rm3(temporary, { force: true });
14438
- const existing = await readFile8(destination).catch(() => null);
15398
+ const existing = await readFile9(destination).catch(() => null);
14439
15399
  if (existing === null) {
14440
15400
  throw error;
14441
15401
  }
@@ -14458,7 +15418,7 @@ async function resolveHookLauncher(input) {
14458
15418
  return { prefix: `node ${word}`, warning: null };
14459
15419
  }
14460
15420
  const current = await writeCopy(
14461
- await readFile8(input.bundle.scriptPath),
15421
+ await readFile9(input.bundle.scriptPath),
14462
15422
  destination
14463
15423
  );
14464
15424
  return {
@@ -14512,7 +15472,7 @@ async function installHooks(input) {
14512
15472
  import { createHash as createHash11 } from "crypto";
14513
15473
  import { createReadStream } from "fs";
14514
15474
  import { mkdir as mkdir9, readdir as readdir2, stat as stat5, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
14515
- import path24 from "path";
15475
+ import path25 from "path";
14516
15476
  import { createInterface } from "readline";
14517
15477
  function parseStopHookInput(text3) {
14518
15478
  let parsed;
@@ -14610,7 +15570,7 @@ async function pruneOldMarkers(markerDir, now) {
14610
15570
  const cutoff = now.getTime() - MARKER_MAX_AGE_MS;
14611
15571
  await Promise.all(
14612
15572
  entries.map(async (name) => {
14613
- const entryPath = path24.join(markerDir, name);
15573
+ const entryPath = path25.join(markerDir, name);
14614
15574
  try {
14615
15575
  const stats = await stat5(entryPath);
14616
15576
  if (stats.mtimeMs < cutoff) {
@@ -14670,7 +15630,7 @@ async function runLessonPromptInner(input) {
14670
15630
  debugLog(`marker prune failed: ${describeError(error)}`);
14671
15631
  });
14672
15632
  const key = markerKey(parsedInput.sessionId, projectId);
14673
- const markerPath = path24.join(input.markerDir, key);
15633
+ const markerPath = path25.join(input.markerDir, key);
14674
15634
  if (await markerExists(markerPath)) {
14675
15635
  return { output: null, why: "already-asked" };
14676
15636
  }
@@ -14767,16 +15727,16 @@ function assertLockMatchesManifest(lock, manifest) {
14767
15727
  const published = new Map(
14768
15728
  manifest.map((entry) => [entry.path, entry.sha256])
14769
15729
  );
14770
- for (const [path29, entry] of Object.entries(lock.files)) {
14771
- const expected = published.get(path29);
15730
+ for (const [path30, entry] of Object.entries(lock.files)) {
15731
+ const expected = published.get(path30);
14772
15732
  if (expected === void 0) {
14773
15733
  refuseStaleLedger(
14774
- `it lists ${path29}, which version ${lock.snapshotVersion} does not contain`
15734
+ `it lists ${path30}, which version ${lock.snapshotVersion} does not contain`
14775
15735
  );
14776
15736
  }
14777
15737
  if (expected !== entry.sha256) {
14778
15738
  refuseStaleLedger(
14779
- `its record of ${path29} is not the published file`
15739
+ `its record of ${path30} is not the published file`
14780
15740
  );
14781
15741
  }
14782
15742
  }
@@ -15201,14 +16161,14 @@ function splitMessage(message) {
15201
16161
  }
15202
16162
 
15203
16163
  // src/lib/instructions/pull-request-verdict.ts
15204
- function pushPullRequest(pullRequest, timedOut) {
16164
+ function pushPullRequest(pullRequest, timedOut2) {
15205
16165
  const branch = pullRequest.branch ?? null;
15206
16166
  return {
15207
16167
  operationId: pullRequest.operationId,
15208
16168
  state: pullRequest.state,
15209
16169
  url: branch?.pullRequest?.url ?? pullRequest.url,
15210
16170
  failure: pullRequest.failure,
15211
- timedOut,
16171
+ timedOut: timedOut2,
15212
16172
  branch,
15213
16173
  append: pullRequest.append ?? null
15214
16174
  };
@@ -15591,7 +16551,7 @@ async function confirm(question, streams = { input: process.stdin, output: proce
15591
16551
  }
15592
16552
 
15593
16553
  // src/lib/instructions/resolve-project.ts
15594
- import path25 from "path";
16554
+ import path26 from "path";
15595
16555
  var MAX_CANDIDATES = 8;
15596
16556
  var MAX_CANDIDATE_CHARS = 512;
15597
16557
  var MAX_LABEL_CHARS = 80;
@@ -15621,15 +16581,15 @@ async function resolveProjectFromCheckout(input) {
15621
16581
  if (remotes2.value.length === 0) {
15622
16582
  throw outcomeFailure("no-remote", { verb: input.verb });
15623
16583
  }
15624
- const names = input.remote === void 0 ? remotes2.value : remotes2.value.filter((name) => name === input.remote);
15625
- if (names.length === 0) {
16584
+ const names2 = input.remote === void 0 ? remotes2.value : remotes2.value.filter((name) => name === input.remote);
16585
+ if (names2.length === 0) {
15626
16586
  throw outcomeFailure("remote-missing", {
15627
16587
  remote: sanitizeDisplayText(input.remote ?? "", 100)
15628
16588
  });
15629
16589
  }
15630
16590
  const candidates = [];
15631
16591
  let identity = null;
15632
- for (const name of names) {
16592
+ for (const name of names2) {
15633
16593
  const url = await effectiveFetchUrl(toplevel, name, deadline);
15634
16594
  if (url.kind !== "ok") {
15635
16595
  throw unreadable(url);
@@ -15660,7 +16620,7 @@ async function resolveProjectFromCheckout(input) {
15660
16620
  for (const match of matches2) {
15661
16621
  if (await sameDirectory(
15662
16622
  destination,
15663
- path25.join(toplevel, match.rootPath)
16623
+ path26.join(toplevel, match.rootPath)
15664
16624
  )) {
15665
16625
  here.push(match);
15666
16626
  }
@@ -15683,9 +16643,17 @@ async function resolveProjectFromCheckout(input) {
15683
16643
  }
15684
16644
 
15685
16645
  // src/lib/instructions/self-update.ts
16646
+ import { spawn as spawn4 } from "child_process";
15686
16647
  import { createHash as createHash14 } from "crypto";
15687
- import { mkdir as mkdir10, readFile as readFile9, realpath as realpath5, writeFile as writeFile6 } from "fs/promises";
15688
- import path26 from "path";
16648
+ import {
16649
+ mkdir as mkdir10,
16650
+ readFile as readFile10,
16651
+ realpath as realpath5,
16652
+ rm as rm4,
16653
+ stat as stat6,
16654
+ writeFile as writeFile6
16655
+ } from "fs/promises";
16656
+ import path27 from "path";
15689
16657
 
15690
16658
  // src/lib/instructions/npm-tarball.ts
15691
16659
  import { gunzipSync } from "zlib";
@@ -15803,7 +16771,7 @@ function transportIsSafe(origin) {
15803
16771
  const url = new URL(origin);
15804
16772
  return url.protocol === "https:" || url.protocol === "http:" && (LOOPBACK_HOST.test(url.hostname) || url.hostname.endsWith(".localhost"));
15805
16773
  }
15806
- function reasonToStayQuiet(input, now) {
16774
+ function reasonToStayQuiet(input) {
15807
16775
  if (isFlagSet(input.env[SELF_UPDATE_OPT_OUT])) {
15808
16776
  return "opted-out";
15809
16777
  }
@@ -15813,22 +16781,22 @@ function reasonToStayQuiet(input, now) {
15813
16781
  if (!transportIsSafe(input.origin)) {
15814
16782
  return "insecure-origin";
15815
16783
  }
15816
- if (input.deadlineAt - now < input.timing.budgetMs + input.timing.marginMs) {
15817
- return "no-time";
15818
- }
15819
16784
  return null;
15820
16785
  }
16786
+ function outOfTime(input, now) {
16787
+ return input.deadlineAt - now < input.timing.budgetMs + input.timing.marginMs;
16788
+ }
15821
16789
  async function isKeptCopy(script, copy) {
15822
16790
  const actual = await realpath5(copy).catch(() => null);
15823
16791
  if (actual === null) {
15824
16792
  return false;
15825
16793
  }
15826
- const running = await realpath5(script).catch(() => path26.resolve(script));
16794
+ const running = await realpath5(script).catch(() => path27.resolve(script));
15827
16795
  return samePath2(running, actual);
15828
16796
  }
15829
- async function readState2(file) {
16797
+ async function readState(file) {
15830
16798
  try {
15831
- const parsed = JSON.parse(await readFile9(file, "utf8"));
16799
+ const parsed = JSON.parse(await readFile10(file, "utf8"));
15832
16800
  return typeof parsed.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && typeof parsed.tarball === "string" ? { checkedAt: parsed.checkedAt, tarball: parsed.tarball } : null;
15833
16801
  } catch {
15834
16802
  return null;
@@ -15836,7 +16804,7 @@ async function readState2(file) {
15836
16804
  }
15837
16805
  async function writeState(file, state) {
15838
16806
  try {
15839
- await mkdir10(path26.dirname(file), { recursive: true });
16807
+ await mkdir10(path27.dirname(file), { recursive: true });
15840
16808
  await writeFile6(file, `${JSON.stringify(state)}
15841
16809
  `);
15842
16810
  return true;
@@ -15844,6 +16812,30 @@ async function writeState(file, state) {
15844
16812
  return false;
15845
16813
  }
15846
16814
  }
16815
+ async function claimSlot(file, now, timing) {
16816
+ try {
16817
+ await mkdir10(path27.dirname(file), { recursive: true });
16818
+ for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
16819
+ try {
16820
+ await writeFile6(file, `${now}
16821
+ `, { flag: "wx" });
16822
+ return true;
16823
+ } catch (error) {
16824
+ if (error.code !== "EEXIST") {
16825
+ return false;
16826
+ }
16827
+ const held = await stat6(file).catch(() => null);
16828
+ if (held !== null && now - held.mtimeMs < timing.budgetMs + timing.marginMs) {
16829
+ return false;
16830
+ }
16831
+ await rm4(file, { force: true });
16832
+ }
16833
+ }
16834
+ } catch {
16835
+ return false;
16836
+ }
16837
+ return false;
16838
+ }
15847
16839
  function askedRecently(state, now) {
15848
16840
  const elapsed = now - state.checkedAt;
15849
16841
  return elapsed >= 0 && elapsed < SELF_UPDATE_INTERVAL_MS;
@@ -15974,7 +16966,7 @@ async function attempt({
15974
16966
  }
15975
16967
  async function refreshKeptCopy(input, deps = {}) {
15976
16968
  const now = deps.now ?? Date.now;
15977
- const quiet = reasonToStayQuiet(input, now());
16969
+ const quiet = reasonToStayQuiet(input);
15978
16970
  if (quiet !== null) {
15979
16971
  return skipped2(quiet);
15980
16972
  }
@@ -15986,45 +16978,91 @@ async function refreshKeptCopy(input, deps = {}) {
15986
16978
  if (!await isKeptCopy(input.script, copy)) {
15987
16979
  return skipped2("not-a-kept-copy");
15988
16980
  }
15989
- const stateFile = path26.join(path26.dirname(copy), STATE_FILE);
15990
- const previous = await readState2(stateFile);
16981
+ const stateFile = path27.join(path27.dirname(copy), STATE_FILE);
16982
+ const previous = await readState(stateFile);
15991
16983
  if (previous !== null && askedRecently(previous, now())) {
15992
16984
  return skipped2("checked-recently");
15993
16985
  }
15994
- if (!await writeState(stateFile, { checkedAt: now(), tarball: running })) {
15995
- return skipped2("state-unwritable");
16986
+ if (outOfTime(input, now())) {
16987
+ return skipped2("no-time");
16988
+ }
16989
+ const claimFile = `${stateFile}.claim`;
16990
+ if (!await claimSlot(claimFile, now(), input.timing)) {
16991
+ return skipped2("checked-recently");
15996
16992
  }
15997
- let signal;
15998
- let outcome = { kind: "current" };
15999
16993
  try {
16000
- await withDeadline(input.timing.budgetMs, async (deadline) => {
16001
- signal = deadline;
16002
- outcome = await attempt({
16003
- input,
16004
- copy,
16005
- stateFile,
16006
- now,
16007
- fetchImpl: deps.fetchImpl ?? fetch,
16008
- replace: deps.replace ?? writeCopy,
16009
- signal: deadline,
16010
- running
16994
+ const latest = await readState(stateFile);
16995
+ if (latest !== null && askedRecently(latest, now())) {
16996
+ return skipped2("checked-recently");
16997
+ }
16998
+ if (!await writeState(stateFile, {
16999
+ checkedAt: now(),
17000
+ tarball: running
17001
+ })) {
17002
+ return skipped2("state-unwritable");
17003
+ }
17004
+ return await runAttempt(running);
17005
+ } finally {
17006
+ await rm4(claimFile, { force: true }).catch(() => void 0);
17007
+ }
17008
+ async function runAttempt(running2) {
17009
+ let signal;
17010
+ let outcome = { kind: "current" };
17011
+ try {
17012
+ await withDeadline(input.timing.budgetMs, async (deadline) => {
17013
+ signal = deadline;
17014
+ outcome = await attempt({
17015
+ input,
17016
+ copy,
17017
+ stateFile,
17018
+ now,
17019
+ fetchImpl: deps.fetchImpl ?? fetch,
17020
+ replace: deps.replace ?? writeCopy,
17021
+ signal: deadline,
17022
+ running: running2
17023
+ });
16011
17024
  });
16012
- });
16013
- } catch (error) {
16014
- if (signal?.aborted) {
16015
- return {
17025
+ } catch (error) {
17026
+ if (signal?.aborted) {
17027
+ return {
17028
+ kind: "failed",
17029
+ reason: "it ran out of time",
17030
+ cause: error
17031
+ };
17032
+ }
17033
+ return error instanceof SelfUpdateFailure ? { kind: "failed", reason: error.message, cause: error.cause } : {
16016
17034
  kind: "failed",
16017
- reason: "it ran out of time",
17035
+ reason: "it failed unexpectedly",
16018
17036
  cause: error
16019
17037
  };
16020
17038
  }
16021
- return error instanceof SelfUpdateFailure ? { kind: "failed", reason: error.message, cause: error.cause } : {
16022
- kind: "failed",
16023
- reason: "it failed unexpectedly",
16024
- cause: error
16025
- };
17039
+ return outcome;
17040
+ }
17041
+ }
17042
+ function startBackgroundSelfUpdate(input, spawnImpl = spawn4) {
17043
+ try {
17044
+ const child = spawnImpl(
17045
+ process.execPath,
17046
+ [
17047
+ input.script,
17048
+ "instructions",
17049
+ "self-update",
17050
+ "--base-url",
17051
+ input.origin
17052
+ ],
17053
+ {
17054
+ detached: true,
17055
+ stdio: "ignore",
17056
+ windowsHide: true,
17057
+ env: input.env
17058
+ }
17059
+ );
17060
+ child.on("error", () => void 0);
17061
+ child.unref();
17062
+ return true;
17063
+ } catch {
17064
+ return false;
16026
17065
  }
16027
- return outcome;
16028
17066
  }
16029
17067
  function selfUpdateLine(outcome) {
16030
17068
  switch (outcome.kind) {
@@ -16043,19 +17081,19 @@ function selfUpdateLine(outcome) {
16043
17081
  }
16044
17082
 
16045
17083
  // src/lib/instructions/tools.ts
16046
- import { stat as stat6 } from "fs/promises";
16047
- import path27 from "path";
17084
+ import { stat as stat7 } from "fs/promises";
17085
+ import path28 from "path";
16048
17086
  var TOOLS = [
16049
17087
  { tool: "claude-code", directory: ".claude", command: "claude" },
16050
17088
  { tool: "codex", directory: ".codex", command: "codex" }
16051
17089
  ];
16052
17090
  async function isDirectory(target) {
16053
- return (await stat6(target).catch(() => null))?.isDirectory() ?? false;
17091
+ return (await stat7(target).catch(() => null))?.isDirectory() ?? false;
16054
17092
  }
16055
17093
  async function detectTools(input) {
16056
17094
  const present = [];
16057
17095
  for (const facts of TOOLS) {
16058
- const found = await isDirectory(path27.join(input.root, facts.directory)) || input.home !== null && await isDirectory(path27.join(input.home, facts.directory)) || await isOnPath(facts.command, input.lookup);
17096
+ const found = await isDirectory(path28.join(input.root, facts.directory)) || input.home !== null && await isDirectory(path28.join(input.home, facts.directory)) || await isOnPath(facts.command, input.lookup);
16059
17097
  if (found) {
16060
17098
  present.push(facts.tool);
16061
17099
  }
@@ -16082,6 +17120,13 @@ function buildInstructionsCommand() {
16082
17120
  const instructions = new Command13("instructions").description(
16083
17121
  "Keep a checkout current with a project's published coding instructions"
16084
17122
  );
17123
+ instructions.command("self-update", { hidden: true }).description("Refresh the kept copy of the served CLI, out of band").option("--base-url <origin>", BASE_URL_HELP).action(async (opts) => {
17124
+ await refreshKeptCopyAfterHook(
17125
+ opts,
17126
+ Date.now() + hookTiming.selfUpdateBudgetMs + hookTiming.selfUpdateMarginMs + BACKGROUND_UPDATE_SLACK_MS,
17127
+ { mayDefer: false }
17128
+ );
17129
+ });
16085
17130
  instructions.command("check").description(
16086
17131
  "Report whether the local copy is behind the published one"
16087
17132
  ).option("--project <id>", PROJECT_HELP).option("--base-url <origin>", BASE_URL_HELP).option("--remote <name>", REMOTE_HELP).option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
@@ -16212,15 +17257,20 @@ async function run2(opts, verb, body) {
16212
17257
  takeAuthFailure();
16213
17258
  takeUpgradeNotice();
16214
17259
  let hookDeadlineAt;
17260
+ hookPhases = {};
17261
+ hookTraced = false;
16215
17262
  try {
16216
17263
  if (opts.hook) {
16217
- const totalMs = hookDeadlineMs();
16218
- activeDeadlineAt = Date.now() + totalMs;
17264
+ hookPhases.node = sinceProcessStart();
17265
+ activeDeadlineAt = (hookTiming.processStartedAt ?? Date.now()) + hookDeadlineMs();
16219
17266
  hookDeadlineAt = activeDeadlineAt;
16220
- await withDeadline(totalMs, (signal) => {
16221
- activeDeadline = signal;
16222
- return body();
16223
- }).finally(() => {
17267
+ await withDeadline(
17268
+ Math.max(0, activeDeadlineAt - Date.now()),
17269
+ (signal) => {
17270
+ activeDeadline = signal;
17271
+ return body();
17272
+ }
17273
+ ).finally(() => {
16224
17274
  activeDeadline = void 0;
16225
17275
  activeDeadlineAt = void 0;
16226
17276
  });
@@ -16239,7 +17289,7 @@ async function run2(opts, verb, body) {
16239
17289
  const signedOutOf = error instanceof NotSignedInError ? error.origin : authFailure?.origin ?? (failure.exitCode === 3 ? describedOrigin(opts) : null);
16240
17290
  if (signedOutOf !== null) {
16241
17291
  line(
16242
- `${HOOK_PREFIX}: ${outcomeLine("hook-signed-out", { origin: signedOutOf || describedOrigin(opts), project: opts.project })}`
17292
+ `${HOOK_PREFIX}: ${outcomeLine("hook-signed-out", { origin: signedOutOf || describedOrigin(opts), project: opts.project, otherProjects: signedInForOtherProjects(signedOutOf || describedOrigin(opts), opts.project) })}`
16243
17293
  );
16244
17294
  } else if (error instanceof UpgradeRequiredFailure) {
16245
17295
  line(error.line);
@@ -16249,10 +17299,17 @@ async function run2(opts, verb, body) {
16249
17299
  `
16250
17300
  );
16251
17301
  }
17302
+ await traceHookFailure(opts, {
17303
+ signedOut: signedOutOf !== null,
17304
+ upgrade: error instanceof UpgradeRequiredFailure,
17305
+ timedOut: /gave up after/.test(failure.message),
17306
+ exitCode: failure.exitCode
17307
+ });
16252
17308
  showUpgradeNotice(true);
16253
17309
  if (error instanceof UpgradeRequiredFailure) {
16254
17310
  await refreshKeptCopyAfterHook(opts, hookDeadlineAt);
16255
17311
  }
17312
+ await endHookProcess();
16256
17313
  process.exit(0);
16257
17314
  }
16258
17315
  process.stderr.write(`\u2717 ${failure.message}
@@ -16263,17 +17320,58 @@ async function run2(opts, verb, body) {
16263
17320
  showUpgradeNotice(Boolean(opts.hook));
16264
17321
  if (opts.hook) {
16265
17322
  await refreshKeptCopyAfterHook(opts, hookDeadlineAt);
17323
+ await endHookProcess();
17324
+ }
17325
+ }
17326
+ var hookPhases = {};
17327
+ var hookTraced = false;
17328
+ async function timedPhase(name, work) {
17329
+ const started = Date.now();
17330
+ try {
17331
+ return await work();
17332
+ } finally {
17333
+ hookPhases[name] = (hookPhases[name] ?? 0) + (Date.now() - started);
17334
+ }
17335
+ }
17336
+ async function traceHookFailure(opts, why) {
17337
+ if (hookTraced) {
17338
+ return;
17339
+ }
17340
+ const reason = why.signedOut ? "not-signed-in" : why.upgrade ? "upgrade-required" : why.timedOut ? "deadline" : `failed-${why.exitCode}`;
17341
+ await appendTrace(traceFile(path29.dirname(getConfigPath())), {
17342
+ at: (/* @__PURE__ */ new Date()).toISOString(),
17343
+ projectId: opts.project ?? "",
17344
+ outcome: "skipped",
17345
+ reason,
17346
+ ms: sinceProcessStart(),
17347
+ totalMs: sinceProcessStart(),
17348
+ phases: { ...hookPhases }
17349
+ });
17350
+ }
17351
+ async function endHookProcess() {
17352
+ if (!hookTiming.exitWhenDone) {
17353
+ return;
16266
17354
  }
17355
+ await Promise.all(
17356
+ [process.stdout, process.stderr].map(
17357
+ (stream) => new Promise((resolve) => {
17358
+ stream.write("", () => resolve());
17359
+ })
17360
+ )
17361
+ );
17362
+ process.exit(0);
16267
17363
  }
16268
- async function refreshKeptCopyAfterHook(opts, deadlineAt) {
17364
+ var BACKGROUND_UPDATE_SLACK_MS = 5e3;
17365
+ async function refreshKeptCopyAfterHook(opts, deadlineAt, { mayDefer = true } = {}) {
16269
17366
  const origin = opts.baseUrl === void 0 ? null : normalizeOrigin(opts.baseUrl);
16270
17367
  if (origin === null || deadlineAt === void 0) {
16271
17368
  return;
16272
17369
  }
16273
- const outcome = await refreshKeptCopy({
17370
+ const started = Date.now();
17371
+ const input = {
16274
17372
  origin,
16275
17373
  script: bundleScriptPath(),
16276
- configDirectory: path28.dirname(getConfigPath()),
17374
+ configDirectory: path29.dirname(getConfigPath()),
16277
17375
  runningTarball: bundleTarballPath(),
16278
17376
  env: process.env,
16279
17377
  deadlineAt,
@@ -16281,7 +17379,25 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt) {
16281
17379
  budgetMs: hookTiming.selfUpdateBudgetMs,
16282
17380
  marginMs: hookTiming.selfUpdateMarginMs
16283
17381
  }
16284
- });
17382
+ };
17383
+ const outcome = await refreshKeptCopy(input);
17384
+ if (mayDefer && outcome.kind === "skipped" && outcome.reason === "no-time") {
17385
+ startBackgroundSelfUpdate({
17386
+ script: input.script,
17387
+ origin,
17388
+ env: process.env
17389
+ });
17390
+ }
17391
+ if (outcome.kind !== "skipped") {
17392
+ await appendTrace(traceFile(path29.dirname(getConfigPath())), {
17393
+ at: (/* @__PURE__ */ new Date()).toISOString(),
17394
+ projectId: "",
17395
+ outcome: "self-update",
17396
+ reason: outcome.kind,
17397
+ ms: Date.now() - started,
17398
+ totalMs: sinceProcessStart()
17399
+ });
17400
+ }
16285
17401
  if (outcome.kind === "failed" && process.env.FABRIC_DEBUG) {
16286
17402
  process.stderr.write(
16287
17403
  `debug: ${describeError(outcome.cause ?? outcome.reason)}
@@ -16333,7 +17449,7 @@ function gitDeadline() {
16333
17449
  return activeDeadlineAt === void 0 ? Date.now() + GIT_BUDGET_MS2 : activeDeadlineAt - hookTiming.gitMarginMs;
16334
17450
  }
16335
17451
  function destinationOf(opts) {
16336
- return path28.resolve(opts.dest ?? process.cwd());
17452
+ return path29.resolve(opts.dest ?? process.cwd());
16337
17453
  }
16338
17454
  function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
16339
17455
  if (!getApiKey(deploymentOrigin(opts), opts.project)) {
@@ -16364,9 +17480,19 @@ function deploymentOrigin(opts) {
16364
17480
  function describedOrigin(opts) {
16365
17481
  return normalizeOrigin(opts.baseUrl ?? getBaseUrl() ?? DEFAULT_ORIGIN) ?? DEFAULT_ORIGIN;
16366
17482
  }
17483
+ function signedInForOtherProjects(origin, project) {
17484
+ return project !== void 0 && listProjectSignIns(origin).some((entry) => entry.projectId !== project);
17485
+ }
16367
17486
  var NotSignedInError = class extends CliFailure {
16368
17487
  constructor(origin, project) {
16369
- super(outcomeLine("not-signed-in", { origin, project }), 3);
17488
+ super(
17489
+ outcomeLine("not-signed-in", {
17490
+ origin,
17491
+ project,
17492
+ otherProjects: signedInForOtherProjects(origin, project)
17493
+ }),
17494
+ 3
17495
+ );
16370
17496
  this.origin = origin;
16371
17497
  this.name = "NotSignedInError";
16372
17498
  }
@@ -16383,7 +17509,7 @@ async function ensureSignedIn(opts) {
16383
17509
  if (opts.hook || runningInCi()) {
16384
17510
  throw new NotSignedInError(origin, opts.project);
16385
17511
  }
16386
- const forProject = opts.project === void 0 ? "" : ` for project ${opts.project}`;
17512
+ const forProject2 = opts.project === void 0 ? "" : ` for project ${opts.project}`;
16387
17513
  try {
16388
17514
  const who2 = await signInWithBrowser({
16389
17515
  baseUrl: opts.baseUrl ?? getBaseUrl() ?? origin,
@@ -16393,7 +17519,7 @@ async function ensureSignedIn(opts) {
16393
17519
  ...opts.project === void 0 ? {} : { project: opts.project },
16394
17520
  announce: (url) => {
16395
17521
  process.stderr.write(
16396
- `Opening your browser to sign in to ${origin}${forProject}.
17522
+ `Opening your browser to sign in to ${origin}${forProject2}.
16397
17523
  If it does not open, visit:
16398
17524
 
16399
17525
  ${url}
@@ -16405,7 +17531,7 @@ Waiting up to 5 minutes for the sign-in to finish.
16405
17531
  }
16406
17532
  });
16407
17533
  process.stderr.write(
16408
- `Signed in to ${origin}${forProject} as ${sanitizeDisplayText(who2.name, 80)}.
17534
+ `Signed in to ${origin}${forProject2} as ${sanitizeDisplayText(who2.name, 80)}.
16409
17535
  `
16410
17536
  );
16411
17537
  } catch {
@@ -16479,6 +17605,30 @@ async function directGitTransport(opts, direct) {
16479
17605
  }
16480
17606
  return { url: base.toString(), authorization: `Bearer ${token}` };
16481
17607
  }
17608
+ function lazyGitTransport(opts, direct) {
17609
+ if (!opts.hook || direct.gitGateway?.version !== "v1") {
17610
+ return void 0;
17611
+ }
17612
+ let failure;
17613
+ let pending;
17614
+ return {
17615
+ resolve: () => {
17616
+ pending ??= timedPhase(
17617
+ "token",
17618
+ () => directGitTransport(opts, direct)
17619
+ ).catch((error) => {
17620
+ failure = error;
17621
+ throw error;
17622
+ });
17623
+ return pending;
17624
+ },
17625
+ rethrow: () => {
17626
+ if (failure !== void 0) {
17627
+ throw failure;
17628
+ }
17629
+ }
17630
+ };
17631
+ }
16482
17632
  function hasSignInFor(origin, project) {
16483
17633
  if (project === void 0) {
16484
17634
  return Boolean(getApiKey(origin));
@@ -16548,14 +17698,17 @@ async function fetchPublished(client, opts, sinceDigest, guard) {
16548
17698
  const org = orgSlugFor(opts);
16549
17699
  let published;
16550
17700
  try {
16551
- published = await client.instructions.getPublished(opts.project, {
16552
- org,
16553
- sinceDigest
16554
- });
17701
+ published = await timedPhase(
17702
+ "api",
17703
+ () => client.instructions.getPublished(opts.project, {
17704
+ org,
17705
+ sinceDigest
17706
+ })
17707
+ );
16555
17708
  } catch (error) {
16556
17709
  throw fixedFailure(error);
16557
17710
  }
16558
- await guard?.admit(published);
17711
+ await timedPhase("classify", async () => guard?.admit(published));
16559
17712
  return published;
16560
17713
  }
16561
17714
  var SourceGuard = class {
@@ -16608,19 +17761,23 @@ function directRepositoryRead(published) {
16608
17761
  const direct = published.direct;
16609
17762
  return published.sourceOfTruth === "REPOSITORY" && published.repository !== void 0 && published.repository !== null && direct?.availability === "READY" ? direct : null;
16610
17763
  }
17764
+ var NO_VERIFY_FOR_DIRECT = "--verify has nothing to check: this project reads its repository directly, so Fabric keeps no copy here. Git is the record of what this checkout holds.";
16611
17765
  async function directRepositoryCheckoutStatus(input) {
16612
17766
  const commit = input.direct.currentCommitSha.slice(0, 7);
16613
17767
  const repository = repositoryName(input.repository);
16614
17768
  if (input.checkout.class !== "matching") {
16615
17769
  return {
16616
17770
  current: false,
17771
+ verdict: "no-checkout",
16617
17772
  line: `Coding instructions are read directly from ${repository} at ${commit}. Open a matching native checkout before reading them locally.`
16618
17773
  };
16619
17774
  }
16620
- const head = input.report.state?.head;
16621
- if (head === null || head === void 0) {
17775
+ const state = input.report.state;
17776
+ const head = state?.head;
17777
+ if (state === null || head === null || head === void 0) {
16622
17778
  return {
16623
17779
  current: false,
17780
+ verdict: "unknown",
16624
17781
  line: `Coding instructions are read directly from ${repository} at ${commit}; this checkout could not be compared to that commit.`
16625
17782
  };
16626
17783
  }
@@ -16633,6 +17790,7 @@ async function directRepositoryCheckoutStatus(input) {
16633
17790
  if (contains.kind === "ok" && contains.value) {
16634
17791
  return {
16635
17792
  current: true,
17793
+ verdict: "current",
16636
17794
  line: `Coding instructions are read directly from ${repository} at ${commit}; this checkout already contains that commit.`
16637
17795
  };
16638
17796
  }
@@ -16646,12 +17804,20 @@ async function directRepositoryCheckoutStatus(input) {
16646
17804
  if (behind.kind === "ok") {
16647
17805
  return {
16648
17806
  current: false,
16649
- line: behind.value ? `Coding instructions are read directly from ${repository} at ${commit}; this checkout is behind. Run git pull --ff-only.` : `Coding instructions are read directly from ${repository} at ${commit}; this checkout has diverged. Inspect its Git history before choosing how to update it.`
17807
+ verdict: behind.value ? "behind" : "diverged",
17808
+ line: behind.value ? directBehindLine({
17809
+ repository: input.repository,
17810
+ commitSha: input.direct.currentCommitSha,
17811
+ ref: input.direct.ref,
17812
+ remote: input.checkout.remote,
17813
+ state
17814
+ }) : `Coding instructions are read directly from ${repository} at ${commit}; this checkout has diverged. Inspect its Git history before choosing how to update it.`
16650
17815
  };
16651
17816
  }
16652
17817
  }
16653
17818
  return {
16654
17819
  current: false,
17820
+ verdict: "not-fetched",
16655
17821
  line: `Coding instructions are read directly from ${repository} at ${commit}; fetch that commit with your normal Git remote before continuing.`
16656
17822
  };
16657
17823
  }
@@ -16743,13 +17909,26 @@ async function runCheck(opts, format) {
16743
17909
  projectId: opts.project,
16744
17910
  destination,
16745
17911
  direct,
16746
- checkout: checkout?.json ?? null
17912
+ // The same words and verdict as the text mode: the checkout
17913
+ // block's own line knows nothing of a direct read.
17914
+ checkout: checkout === null ? null : {
17915
+ ...checkout.json,
17916
+ line: status.current ? null : status.line,
17917
+ verdict: status.verdict
17918
+ },
17919
+ verified: false
16747
17920
  },
16748
17921
  { format: "json" }
16749
17922
  );
16750
- } else if (!opts.hook || !status.current) {
16751
- process.stdout.write(`${status.line}
17923
+ } else {
17924
+ if (!opts.hook || !status.current) {
17925
+ process.stdout.write(`${status.line}
17926
+ `);
17927
+ }
17928
+ if (opts.verify) {
17929
+ process.stdout.write(`${NO_VERIFY_FOR_DIRECT}
16752
17930
  `);
17931
+ }
16753
17932
  }
16754
17933
  return;
16755
17934
  }
@@ -16907,7 +18086,8 @@ async function runDoctorCommand(opts, format) {
16907
18086
  origin,
16908
18087
  home: machine.home(),
16909
18088
  env: machine.env(),
16910
- platform: machine.platform()
18089
+ platform: machine.platform(),
18090
+ probe: opts.probeNetwork ? gatewayProbeFor({ origin, projectId: opts.project }) : void 0
16911
18091
  }),
16912
18092
  client: () => instructionsClient({ ...opts, hook: false }, CHECK_TIMEOUT_MS),
16913
18093
  createDownloadUrl: (projectId, options) => downloadUrlClient({
@@ -17019,25 +18199,34 @@ async function syncOnce(opts) {
17019
18199
  7
17020
18200
  );
17021
18201
  }
17022
- const report = await reportForClassification({
17023
- classification: guard.checkout,
17024
- repository: directRepository,
17025
- snapshot: void 0,
17026
- deadline: gitDeadline()
17027
- });
17028
- const status = await directRepositoryCheckoutStatus({
17029
- repository: directRepository,
17030
- direct,
17031
- checkout: guard.checkout,
17032
- report
17033
- });
17034
- const gitTransport = opts.hook && direct.gitGateway?.version === "v1" ? await directGitTransport(opts, direct) : void 0;
18202
+ const checkout = guard.checkout;
18203
+ const report = await timedPhase(
18204
+ "report",
18205
+ () => reportForClassification({
18206
+ classification: checkout,
18207
+ repository: directRepository,
18208
+ snapshot: void 0,
18209
+ deadline: gitDeadline(),
18210
+ contentChanges: opts.hook ? "when-behind" : "always"
18211
+ })
18212
+ );
18213
+ const status = await timedPhase(
18214
+ "status",
18215
+ () => directRepositoryCheckoutStatus({
18216
+ repository: directRepository,
18217
+ direct,
18218
+ checkout,
18219
+ report
18220
+ })
18221
+ );
18222
+ const lazyTransport = lazyGitTransport(opts, direct);
17035
18223
  const fastForward = opts.hook && guard.checkout.class === "matching" ? await runFastForward({
17036
18224
  classification: guard.checkout,
17037
18225
  repository: directRepository,
17038
18226
  snapshot: void 0,
17039
18227
  directCommitSha: direct.currentCommitSha,
17040
- ...gitTransport === void 0 ? {} : { gitTransport },
18228
+ ...lazyTransport === void 0 ? {} : { gitTransport: lazyTransport.resolve },
18229
+ timings: hookPhases,
17041
18230
  report: {
17042
18231
  ...report,
17043
18232
  reportKind: status.current ? "current" : "behind",
@@ -17046,8 +18235,10 @@ async function syncOnce(opts) {
17046
18235
  projectId: opts.project,
17047
18236
  deadline: gitDeadline(),
17048
18237
  optedOut: opts.fastForward === false,
17049
- traceFile: traceFile(path28.dirname(getConfigPath()))
18238
+ traceFile: traceFile(path29.dirname(getConfigPath()))
17050
18239
  }) : null;
18240
+ hookTraced = fastForward !== null;
18241
+ lazyTransport?.rethrow();
17051
18242
  return {
17052
18243
  kind: "reported",
17053
18244
  destination: existing,
@@ -17066,12 +18257,17 @@ async function syncOnce(opts) {
17066
18257
  if (refused !== null) {
17067
18258
  throw refused;
17068
18259
  }
17069
- const report = await reportForClassification({
17070
- classification: guard.checkout,
17071
- repository: published.repository,
17072
- snapshot: published.snapshot,
17073
- deadline: gitDeadline()
17074
- });
18260
+ const checkout = guard.checkout;
18261
+ const report = await timedPhase(
18262
+ "report",
18263
+ () => reportForClassification({
18264
+ classification: checkout,
18265
+ repository: published.repository,
18266
+ snapshot: published.snapshot,
18267
+ deadline: gitDeadline(),
18268
+ contentChanges: opts.hook ? "when-behind" : "always"
18269
+ })
18270
+ );
17075
18271
  const fastForward = opts.hook && guard.checkout.class === "matching" && published.repository ? await runFastForward({
17076
18272
  classification: guard.checkout,
17077
18273
  repository: published.repository,
@@ -17080,7 +18276,8 @@ async function syncOnce(opts) {
17080
18276
  projectId: opts.project,
17081
18277
  deadline: gitDeadline(),
17082
18278
  optedOut: opts.fastForward === false,
17083
- traceFile: traceFile(path28.dirname(getConfigPath()))
18279
+ traceFile: traceFile(path29.dirname(getConfigPath())),
18280
+ timings: hookPhases
17084
18281
  }) : null;
17085
18282
  return {
17086
18283
  kind: "reported",
@@ -17800,7 +18997,7 @@ async function runInit(opts, format) {
17800
18997
  );
17801
18998
  cloned = true;
17802
18999
  if (cloneFolder !== void 0 && checkout.class === "unmapped") {
17803
- destination = path28.join(checkout.toplevel, checkout.rootPath);
19000
+ destination = path29.join(checkout.toplevel, checkout.rootPath);
17804
19001
  existing = await resolveExistingRoot(destination);
17805
19002
  checkout = await classifyCheckout({
17806
19003
  destination: existing,
@@ -17868,12 +19065,12 @@ async function runInit(opts, format) {
17868
19065
  throw outcomeFailure("lessons-need-claude", {});
17869
19066
  }
17870
19067
  if (matching !== null) {
17871
- const prefix = path28.relative(matching.toplevel, root);
19068
+ const prefix = path29.relative(matching.toplevel, root);
17872
19069
  const relativeSetupPaths = [
17873
19070
  ...tools.tools.map(
17874
- (tool) => path28.join(prefix, hookPathFor(tool)).replaceAll(path28.sep, "/")
19071
+ (tool) => path29.join(prefix, hookPathFor(tool)).replaceAll(path29.sep, "/")
17875
19072
  ),
17876
- path28.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path28.sep, "/")
19073
+ path29.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path29.sep, "/")
17877
19074
  ];
17878
19075
  const tracked = await trackedPaths(
17879
19076
  matching.toplevel,
@@ -17912,7 +19109,7 @@ async function runInit(opts, format) {
17912
19109
  const applies = opts.reportOnly ? false : matching !== null ? true : Boolean(opts.apply);
17913
19110
  const launcher = await resolveHookLauncher({
17914
19111
  origin: deploymentOrigin(opts),
17915
- configDirectory: path28.dirname(getConfigPath()),
19112
+ configDirectory: path29.dirname(getConfigPath()),
17916
19113
  lookup: { env: machine.env(), platform: machine.platform() },
17917
19114
  bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
17918
19115
  });
@@ -17955,6 +19152,10 @@ async function runInit(opts, format) {
17955
19152
  tools: tools.tools,
17956
19153
  projectId: opts.project,
17957
19154
  origin: deploymentOrigin(opts),
19155
+ probe: gatewayProbeFor({
19156
+ origin: deploymentOrigin(opts),
19157
+ projectId: opts.project
19158
+ }),
17958
19159
  cwd: toplevel ?? root,
17959
19160
  home: machine.home(),
17960
19161
  env: machine.env(),
@@ -17965,9 +19166,12 @@ async function runInit(opts, format) {
17965
19166
  platform: machine.platform()
17966
19167
  }
17967
19168
  }),
17968
- interactive: format !== "json" && canPrompt() && process.stdout.isTTY === true && !runningInCi()
19169
+ interactive: format !== "json" && canPrompt() && process.stdout.isTTY === true && !runningInCi(),
19170
+ checkSignIn: format !== "json"
17969
19171
  });
17970
19172
  const mcpComplete = !mcpRequested || mcpRegistrationComplete(mcp);
19173
+ const mcpFailed = mcpRequested && mcpSetupFailed(mcp);
19174
+ const mcpManual = mcpManualSteps(mcp);
17971
19175
  const mcpAuthPending = mcpAuthenticationPending(mcp);
17972
19176
  const first = hooks[0];
17973
19177
  if (format === "json") {
@@ -17989,12 +19193,14 @@ async function runInit(opts, format) {
17989
19193
  lessonsHook: Boolean(opts.lessons),
17990
19194
  mcpRequested,
17991
19195
  mcpComplete,
19196
+ manualSteps: mcpManual,
17992
19197
  mcpAuthenticationPending: mcpAuthPending,
17993
19198
  mcp: mcp.map((result) => ({
17994
19199
  tool: result.tool,
17995
19200
  name: result.name,
17996
19201
  url: result.url,
17997
19202
  outcome: result.outcome.kind,
19203
+ status: result.status,
17998
19204
  login: result.login,
17999
19205
  registerLine: result.registerLine,
18000
19206
  loginLine: result.loginLine
@@ -18006,7 +19212,7 @@ async function runInit(opts, format) {
18006
19212
  },
18007
19213
  { format: "json" }
18008
19214
  );
18009
- if (!mcpComplete) {
19215
+ if (mcpFailed) {
18010
19216
  throw new CliFailure(
18011
19217
  "Coding tool setup is incomplete: the Fabric MCP server was not registered for every selected tool. Complete the action shown above, then run init again.",
18012
19218
  7
@@ -18059,7 +19265,10 @@ async function runInit(opts, format) {
18059
19265
  if (launcher.warning !== null) {
18060
19266
  line(launcher.warning);
18061
19267
  }
18062
- for (const mcpLine of agentMcpLines(mcp)) {
19268
+ for (const mcpLine of [
19269
+ ...agentMcpLines(mcp),
19270
+ ...agentMcpSummaryLines(mcp)
19271
+ ]) {
18063
19272
  line(mcpLine);
18064
19273
  }
18065
19274
  if (mcpAuthPending) {
@@ -18072,7 +19281,7 @@ async function runInit(opts, format) {
18072
19281
  outcomeLine("cloned-into", {
18073
19282
  where: folderForCommand(
18074
19283
  opts.dest,
18075
- path28.relative(
19284
+ path29.relative(
18076
19285
  await resolveExistingRoot(destinationOf(opts)),
18077
19286
  root
18078
19287
  )
@@ -18080,7 +19289,7 @@ async function runInit(opts, format) {
18080
19289
  })
18081
19290
  );
18082
19291
  }
18083
- if (!mcpComplete) {
19292
+ if (mcpFailed) {
18084
19293
  line(
18085
19294
  "The session hook was set up, but the Fabric MCP server was not registered for every selected tool."
18086
19295
  );
@@ -18118,8 +19327,8 @@ async function runLessonPromptCommand(opts) {
18118
19327
  stdinText = await readAllStdin(process.stdin);
18119
19328
  } catch {
18120
19329
  }
18121
- const markerDir = path28.join(
18122
- path28.dirname(getConfigPath()),
19330
+ const markerDir = path29.join(
19331
+ path29.dirname(getConfigPath()),
18123
19332
  "lesson-prompts"
18124
19333
  );
18125
19334
  const result = await runLessonPrompt({
@@ -19021,6 +20230,8 @@ function buildWorkspacesCommand() {
19021
20230
  }
19022
20231
 
19023
20232
  // src/bin/fabric.ts
20233
+ hookTiming.processStartedAt = Math.round(Date.now() - process.uptime() * 1e3);
20234
+ hookTiming.exitWhenDone = true;
19024
20235
  var program = new Command22();
19025
20236
  program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
19026
20237
  "--format <format>",