@fabricorg/cli 0.7.0 → 0.7.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 Command23 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@fabricorg/cli",
9
- version: "0.7.0",
9
+ version: "0.7.1",
10
10
  description: "CLI for the Fabric AI platform",
11
11
  keywords: [
12
12
  "fabric",
@@ -135,6 +135,57 @@ var DEFAULT_RETRY = {
135
135
  };
136
136
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
137
137
  var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
138
+ var RateLimitDelayError = class extends FabricError {
139
+ constructor(message, delayMs) {
140
+ super(message, 429);
141
+ this.delayMs = delayMs;
142
+ }
143
+ };
144
+ function retryAfterMs(value) {
145
+ if (value === null) {
146
+ return void 0;
147
+ }
148
+ if (/^\d+$/.test(value)) {
149
+ return Number(value) * 1e3;
150
+ }
151
+ const date = Date.parse(value);
152
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
153
+ }
154
+ async function boundedJson(res, maxBytes) {
155
+ const reader = res.body?.getReader();
156
+ if (!reader) {
157
+ throw new FabricError("Empty repository response", res.status);
158
+ }
159
+ const chunks = [];
160
+ let length = 0;
161
+ try {
162
+ for (; ; ) {
163
+ const next = await reader.read();
164
+ if (next.done) {
165
+ break;
166
+ }
167
+ length += next.value.byteLength;
168
+ if (length > maxBytes) {
169
+ await reader.cancel();
170
+ throw new FabricError(
171
+ "Repository response is too large",
172
+ res.status,
173
+ "RESPONSE_TOO_LARGE"
174
+ );
175
+ }
176
+ chunks.push(next.value);
177
+ }
178
+ } finally {
179
+ reader.releaseLock();
180
+ }
181
+ const bytes = new Uint8Array(length);
182
+ let offset = 0;
183
+ for (const chunk of chunks) {
184
+ bytes.set(chunk, offset);
185
+ offset += chunk.byteLength;
186
+ }
187
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
188
+ }
138
189
  function env(name) {
139
190
  if (typeof process === "undefined" || !process.env) {
140
191
  return void 0;
@@ -235,10 +286,10 @@ var FabricHttpClient = class _FabricHttpClient {
235
286
  * Inject default `org` / `personal` query params if the path doesn't already
236
287
  * carry one. Resources that explicitly pass `?org=other` are untouched.
237
288
  */
238
- buildUrl(path38) {
239
- const url = `${this.baseUrl}/api/v1${path38}`;
240
- const hasOrg = /[?&]org=/.test(path38);
241
- const hasPersonal = /[?&]personal=/.test(path38);
289
+ buildUrl(path42) {
290
+ const url = `${this.baseUrl}/api/v1${path42}`;
291
+ const hasOrg = /[?&]org=/.test(path42);
292
+ const hasPersonal = /[?&]personal=/.test(path42);
242
293
  if (hasOrg || hasPersonal) {
243
294
  return url;
244
295
  }
@@ -251,21 +302,22 @@ var FabricHttpClient = class _FabricHttpClient {
251
302
  if (additions.length === 0) {
252
303
  return url;
253
304
  }
254
- const sep = path38.includes("?") ? "&" : "?";
305
+ const sep = path42.includes("?") ? "&" : "?";
255
306
  return `${url}${sep}${additions.join("&")}`;
256
307
  }
257
- async request(method, path38, options = {}) {
258
- const url = this.buildUrl(path38);
308
+ async request(method, path42, options = {}) {
309
+ const url = this.buildUrl(path42);
259
310
  const isMutating = MUTATING_METHODS.has(method);
260
311
  const idempotencyKey = options.idempotencyKey ?? (isMutating ? randomId() : void 0);
261
- const maxAttempts = (options.retry ?? this.retry).maxRetries + 1;
312
+ const retries = (options.retry ?? this.retry).maxRetries;
313
+ const maxAttempts = (options.boundedRead ? Math.min(retries, 2) : retries) + 1;
262
314
  let lastError;
263
315
  for (let attempt2 = 1; attempt2 <= maxAttempts; attempt2++) {
264
316
  if (options.signal?.aborted) {
265
317
  throw options.signal.reason;
266
318
  }
267
319
  const start = Date.now();
268
- this.emit({ kind: "request", method, path: path38, attempt: attempt2 });
320
+ this.emit({ kind: "request", method, path: path42, attempt: attempt2 });
269
321
  try {
270
322
  const result = await this.executeOnce(
271
323
  method,
@@ -276,7 +328,7 @@ var FabricHttpClient = class _FabricHttpClient {
276
328
  this.emit({
277
329
  kind: "response",
278
330
  method,
279
- path: path38,
331
+ path: path42,
280
332
  attempt: attempt2,
281
333
  status: 200,
282
334
  durationMs: Date.now() - start
@@ -285,11 +337,11 @@ var FabricHttpClient = class _FabricHttpClient {
285
337
  } catch (err) {
286
338
  lastError = err;
287
339
  const status2 = err instanceof FabricError ? err.status : void 0;
288
- const retriable = attempt2 < maxAttempts && this.shouldRetry(method, err);
340
+ const retriable = attempt2 < maxAttempts && this.shouldRetry(method, err) && !(err instanceof RateLimitDelayError && err.delayMs > 6e4);
289
341
  this.emit({
290
342
  kind: retriable ? "retry" : "error",
291
343
  method,
292
- path: path38,
344
+ path: path42,
293
345
  attempt: attempt2,
294
346
  status: status2,
295
347
  durationMs: Date.now() - start,
@@ -301,7 +353,7 @@ var FabricHttpClient = class _FabricHttpClient {
301
353
  if (!retriable) {
302
354
  throw err;
303
355
  }
304
- const delay2 = Math.min(
356
+ const delay2 = err instanceof RateLimitDelayError ? err.delayMs : Math.min(
305
357
  this.retry.initialDelayMs * this.retry.multiplier ** (attempt2 - 1),
306
358
  this.retry.maxDelayMs
307
359
  );
@@ -354,22 +406,51 @@ var FabricHttpClient = class _FabricHttpClient {
354
406
  "NETWORK_ERROR"
355
407
  );
356
408
  }
357
- clearTimeout(timeoutHandle);
409
+ if (!options.boundedRead) {
410
+ clearTimeout(timeoutHandle);
411
+ }
358
412
  const upgradeNotice = res.headers?.get("X-Fabric-Cli-Upgrade");
359
413
  if (upgradeNotice) {
360
414
  this.notifyUpgrade(upgradeNotice);
361
415
  }
416
+ if (options.boundedRead && res.status === 429) {
417
+ try {
418
+ await res.body?.cancel();
419
+ } finally {
420
+ clearTimeout(timeoutHandle);
421
+ }
422
+ options.signal?.throwIfAborted();
423
+ throw new RateLimitDelayError(
424
+ "Too many requests. Wait a minute and try again.",
425
+ Math.max(
426
+ 1e3,
427
+ retryAfterMs(res.headers.get("Retry-After")) ?? 6e4
428
+ )
429
+ );
430
+ }
362
431
  let json2;
363
432
  try {
364
- json2 = await res.json();
365
- } catch {
433
+ json2 = options.boundedRead ? await boundedJson(res, options.boundedRead.maxResponseBytes) : await res.json();
434
+ } catch (error) {
366
435
  if (options.signal?.aborted) {
367
436
  throw options.signal.reason;
368
437
  }
438
+ if (options.boundedRead && controller.signal.aborted) {
439
+ throw new FabricError(
440
+ `Request timed out after ${this.timeoutMs}ms`,
441
+ 0,
442
+ "TIMEOUT"
443
+ );
444
+ }
445
+ if (options.boundedRead && error instanceof FabricError) {
446
+ throw error;
447
+ }
369
448
  throw new FabricError(
370
449
  `Unexpected response from server (status ${res.status})`,
371
450
  res.status
372
451
  );
452
+ } finally {
453
+ clearTimeout(timeoutHandle);
373
454
  }
374
455
  if (!res.ok) {
375
456
  const body = json2;
@@ -433,28 +514,28 @@ var FabricHttpClient = class _FabricHttpClient {
433
514
  } catch {
434
515
  }
435
516
  }
436
- get(path38, options = {}) {
517
+ get(path42, options = {}) {
437
518
  return this.request(
438
519
  "GET",
439
- path38,
520
+ path42,
440
521
  options.signal ? { signal: options.signal } : {}
441
522
  );
442
523
  }
443
- post(path38, body, options = {}) {
444
- return this.request("POST", path38, {
524
+ post(path42, body, options = {}) {
525
+ return this.request("POST", path42, {
445
526
  body,
446
527
  idempotencyKey: options.idempotencyKey,
447
528
  retry: options.retry
448
529
  });
449
530
  }
450
- patch(path38, body, options = {}) {
451
- return this.request("PATCH", path38, {
531
+ patch(path42, body, options = {}) {
532
+ return this.request("PATCH", path42, {
452
533
  body,
453
534
  idempotencyKey: options.idempotencyKey
454
535
  });
455
536
  }
456
- put(path38, body, options = {}) {
457
- return this.request("PUT", path38, {
537
+ put(path42, body, options = {}) {
538
+ return this.request("PUT", path42, {
458
539
  body,
459
540
  idempotencyKey: options.idempotencyKey,
460
541
  retry: options.retry
@@ -465,8 +546,8 @@ var FabricHttpClient = class _FabricHttpClient {
465
546
  * delete names the path and the version in its body). Without `body`,
466
547
  * nothing is sent, as before.
467
548
  */
468
- delete(path38, body, options = {}) {
469
- return this.request("DELETE", path38, {
549
+ delete(path42, body, options = {}) {
550
+ return this.request("DELETE", path42, {
470
551
  body,
471
552
  idempotencyKey: options.idempotencyKey
472
553
  });
@@ -830,6 +911,16 @@ function buildQuery2(opts) {
830
911
  return s ? `?${s}` : "";
831
912
  }
832
913
 
914
+ // ../sdk/src/resources/instruction-repository-files.ts
915
+ function readPinnedRepositoryFiles(http, url, input, signal) {
916
+ const deadline = AbortSignal.timeout(12e4);
917
+ return http.request("POST", url, {
918
+ body: input,
919
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
920
+ boundedRead: { maxResponseBytes: 4 * 1024 * 1024 }
921
+ });
922
+ }
923
+
833
924
  // ../sdk/src/resources/instructions.ts
834
925
  var InstructionsResource = class {
835
926
  constructor(http) {
@@ -862,9 +953,19 @@ var InstructionsResource = class {
862
953
  * commit pin; repeat calls must carry that pin so a moving branch cannot
863
954
  * mix tree metadata with later file bytes.
864
955
  */
865
- listRepositoryFiles(projectId, options = {}) {
956
+ listRepositoryFiles(projectId, options = {}, request2 = {}) {
866
957
  return this.http.get(
867
- `/projects/${encodeURIComponent(projectId)}/instructions/repository/files${buildQuery3(options)}`
958
+ `/projects/${encodeURIComponent(projectId)}/instructions/repository/files${buildQuery3(options)}`,
959
+ request2
960
+ );
961
+ }
962
+ /** Read-only POST, with at most two retries and a two-minute total budget. */
963
+ readRepositoryFiles(projectId, input, options = {}) {
964
+ return readPinnedRepositoryFiles(
965
+ this.http,
966
+ `/projects/${encodeURIComponent(projectId)}/instructions/repository/files/read${buildQuery3({ org: options.org })}`,
967
+ input,
968
+ options.signal
868
969
  );
869
970
  }
870
971
  /** Read one text file from an already pinned direct repository commit. */
@@ -1091,14 +1192,14 @@ function buildQuery3(opts) {
1091
1192
 
1092
1193
  // ../sdk/src/resources/integrations.ts
1093
1194
  var RESOURCE_BUILTINS = /* @__PURE__ */ new Set(["list", "constructor", "then"]);
1094
- function makeDotPath(http, slug, path38) {
1095
- const fn = (args) => http.post(`/integrations/${slug}/${path38.join(".")}`, args ?? {});
1195
+ function makeDotPath(http, slug, path42) {
1196
+ const fn = (args) => http.post(`/integrations/${slug}/${path42.join(".")}`, args ?? {});
1096
1197
  return new Proxy(fn, {
1097
1198
  get(target, prop) {
1098
1199
  if (prop in target || typeof prop !== "string") {
1099
1200
  return Reflect.get(target, prop);
1100
1201
  }
1101
- return makeDotPath(http, slug, [...path38, prop]);
1202
+ return makeDotPath(http, slug, [...path42, prop]);
1102
1203
  }
1103
1204
  });
1104
1205
  }
@@ -1784,6 +1885,102 @@ function buildQuery8(opts) {
1784
1885
  return s ? `?${s}` : "";
1785
1886
  }
1786
1887
 
1888
+ // ../sdk/src/plugin-mapping.ts
1889
+ var ALWAYS_ON_FILES = ["CLAUDE.md", "AGENTS.md"];
1890
+ function segmentsBelowRoot(instructionPath) {
1891
+ const segments = instructionPath.split("/");
1892
+ return segments[0]?.toLowerCase() === ".claude" ? segments.slice(1) : segments;
1893
+ }
1894
+ function isMarkdown(name) {
1895
+ return name?.toLowerCase().endsWith(".md") ?? false;
1896
+ }
1897
+ function pluginPathFor(instructionPath) {
1898
+ const rest = segmentsBelowRoot(instructionPath);
1899
+ const folder = rest[0]?.toLowerCase();
1900
+ if (rest.length < 2) {
1901
+ return null;
1902
+ }
1903
+ const below = rest.slice(1);
1904
+ switch (folder) {
1905
+ case "skills":
1906
+ return below.length >= 2 ? `skills/${below.join("/")}` : null;
1907
+ case "agents":
1908
+ return below.length === 1 && isMarkdown(below[0]) ? `agents/${below[0]}` : null;
1909
+ case "commands":
1910
+ return `commands/${below.join("/")}`;
1911
+ default:
1912
+ return null;
1913
+ }
1914
+ }
1915
+ function projectRulePathFor(instructionPath) {
1916
+ const rest = segmentsBelowRoot(instructionPath);
1917
+ if (rest.length < 2 || rest[0]?.toLowerCase() !== "rules" || !isMarkdown(rest[rest.length - 1])) {
1918
+ return null;
1919
+ }
1920
+ return rest.slice(1).join("/");
1921
+ }
1922
+ function reasonFor(instructionPath) {
1923
+ const rest = segmentsBelowRoot(instructionPath);
1924
+ if (rest.length >= 2) {
1925
+ switch (rest[0]?.toLowerCase()) {
1926
+ case "skills":
1927
+ return "skill-needs-folder";
1928
+ case "agents":
1929
+ return "agent-must-be-markdown-file";
1930
+ case "rules":
1931
+ return "rule-must-be-markdown";
1932
+ default:
1933
+ break;
1934
+ }
1935
+ }
1936
+ return "outside-plugin-folders";
1937
+ }
1938
+ var byPath = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
1939
+ function planPluginFiles(entries, tool) {
1940
+ const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] : ALWAYS_ON_FILES;
1941
+ const present = new Map(entries.map((entry) => [entry.path, entry]));
1942
+ const alwaysOn = order.find((name) => {
1943
+ const entry = present.get(name);
1944
+ return entry !== void 0 && entry.size !== 0;
1945
+ }) ?? null;
1946
+ const plan = {
1947
+ carried: [],
1948
+ rules: [],
1949
+ alwaysOn: alwaysOn === null ? null : { path: alwaysOn },
1950
+ leftOut: []
1951
+ };
1952
+ for (const entry of entries) {
1953
+ if (entry.path === alwaysOn) {
1954
+ continue;
1955
+ }
1956
+ if (ALWAYS_ON_FILES.includes(entry.path)) {
1957
+ plan.leftOut.push({
1958
+ path: entry.path,
1959
+ reason: "always-on-shadowed"
1960
+ });
1961
+ continue;
1962
+ }
1963
+ const place = pluginPathFor(entry.path);
1964
+ const rule = projectRulePathFor(entry.path);
1965
+ if (place === null && rule === null) {
1966
+ plan.leftOut.push({
1967
+ path: entry.path,
1968
+ reason: reasonFor(entry.path)
1969
+ });
1970
+ } else if (entry.isText === false) {
1971
+ plan.leftOut.push({ path: entry.path, reason: "not-text" });
1972
+ } else if (rule !== null) {
1973
+ plan.rules.push({ path: entry.path, as: rule });
1974
+ } else if (place !== null) {
1975
+ plan.carried.push({ path: entry.path, as: place });
1976
+ }
1977
+ }
1978
+ plan.carried.sort(byPath);
1979
+ plan.rules.sort(byPath);
1980
+ plan.leftOut.sort(byPath);
1981
+ return plan;
1982
+ }
1983
+
1787
1984
  // ../sdk/src/index.ts
1788
1985
  var FabricClient = class _FabricClient {
1789
1986
  constructor(optionsOrHttp = {}) {
@@ -1848,9 +2045,11 @@ import Conf from "conf";
1848
2045
  import { randomUUID } from "crypto";
1849
2046
  import {
1850
2047
  closeSync,
2048
+ constants,
2049
+ fstatSync,
2050
+ lstatSync,
1851
2051
  openSync,
1852
- readFileSync,
1853
- statSync,
2052
+ readSync,
1854
2053
  unlinkSync,
1855
2054
  writeSync
1856
2055
  } from "fs";
@@ -1864,6 +2063,13 @@ var ExclusiveLockBusyError = class extends Error {
1864
2063
  }
1865
2064
  };
1866
2065
  var DEFAULT_POLL_MS = 100;
2066
+ var MAX_LOCK_BYTES = 256;
2067
+ var LockIdentityChangedError = class extends Error {
2068
+ constructor() {
2069
+ super("The lock changed while it was being read.");
2070
+ this.name = "LockIdentityChangedError";
2071
+ }
2072
+ };
1867
2073
  function sleep2(ms) {
1868
2074
  return new Promise((resolve) => setTimeout(resolve, ms));
1869
2075
  }
@@ -1885,14 +2091,12 @@ function takeOnce(lockPath2, staleMs) {
1885
2091
  }
1886
2092
  }
1887
2093
  try {
1888
- if (Date.now() - statSync(lockPath2).mtimeMs > staleMs) {
1889
- if (ownerIsAlive(readFileSync(lockPath2, "utf8"))) {
1890
- return "busy";
1891
- }
1892
- return "abandoned";
2094
+ const read = readLock(lockPath2);
2095
+ if (Date.now() - read.stats.mtimeMs > staleMs) {
2096
+ return ownerIsAlive(read.owner) ? "busy" : "abandoned";
1893
2097
  }
1894
2098
  } catch (error) {
1895
- if (isNotFoundError(error)) {
2099
+ if (isNotFoundError(error) || error instanceof LockIdentityChangedError) {
1896
2100
  return "retry";
1897
2101
  }
1898
2102
  throw error;
@@ -1916,13 +2120,65 @@ function ownerIsAlive(owner) {
1916
2120
  return code === "EPERM" || code !== "ESRCH" && code !== "ENOENT";
1917
2121
  }
1918
2122
  }
2123
+ function readLock(lockPath2) {
2124
+ const descriptor = openSync(
2125
+ lockPath2,
2126
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
2127
+ );
2128
+ try {
2129
+ const stats = fstatSync(descriptor);
2130
+ if (!stats.isFile()) {
2131
+ throw new Error("The lock is not a regular file.");
2132
+ }
2133
+ if (stats.size > MAX_LOCK_BYTES) {
2134
+ throw new Error("The lock is too large.");
2135
+ }
2136
+ const buffer = Buffer.allocUnsafe(MAX_LOCK_BYTES + 1);
2137
+ let total = 0;
2138
+ while (total <= MAX_LOCK_BYTES) {
2139
+ const count = readSync(
2140
+ descriptor,
2141
+ buffer,
2142
+ total,
2143
+ MAX_LOCK_BYTES + 1 - total,
2144
+ total
2145
+ );
2146
+ if (count === 0) {
2147
+ break;
2148
+ }
2149
+ total += count;
2150
+ }
2151
+ if (total > MAX_LOCK_BYTES) {
2152
+ throw new Error("The lock is too large.");
2153
+ }
2154
+ const current = lstatSync(lockPath2);
2155
+ if (!current.isFile()) {
2156
+ throw new Error("The lock is not a regular file.");
2157
+ }
2158
+ if (current.size > MAX_LOCK_BYTES) {
2159
+ throw new Error("The lock is too large.");
2160
+ }
2161
+ if (current.dev !== stats.dev || current.ino !== stats.ino) {
2162
+ throw new LockIdentityChangedError();
2163
+ }
2164
+ return { owner: buffer.toString("utf8", 0, total), stats };
2165
+ } finally {
2166
+ closeSync(descriptor);
2167
+ }
2168
+ }
2169
+ function stillHeld(held, lockPath2) {
2170
+ const expected = fstatSync(held.descriptor);
2171
+ const read = readLock(lockPath2);
2172
+ return read.owner === held.owner && read.stats.dev === expected.dev && read.stats.ino === expected.ino;
2173
+ }
1919
2174
  function release(held, lockPath2) {
1920
- closeSync(held.descriptor);
1921
2175
  try {
1922
- if (readFileSync(lockPath2, "utf8") === held.owner) {
2176
+ if (stillHeld(held, lockPath2)) {
1923
2177
  unlinkSync(lockPath2);
1924
2178
  }
1925
2179
  } catch {
2180
+ } finally {
2181
+ closeSync(held.descriptor);
1926
2182
  }
1927
2183
  }
1928
2184
  async function withExclusiveLock(work, options) {
@@ -1948,6 +2204,10 @@ async function withExclusiveLock(work, options) {
1948
2204
  }
1949
2205
  }
1950
2206
  try {
2207
+ options.signal?.throwIfAborted();
2208
+ if (!stillHeld(held, lockPath2)) {
2209
+ throw new Error("The lock changed before work could start.");
2210
+ }
1951
2211
  return await work();
1952
2212
  } finally {
1953
2213
  release(held, lockPath2);
@@ -1978,6 +2238,9 @@ function withExclusiveLockSync(work, options) {
1978
2238
  }
1979
2239
  }
1980
2240
  try {
2241
+ if (!stillHeld(held, lockPath2)) {
2242
+ throw new Error("The lock changed before work could start.");
2243
+ }
1981
2244
  return work();
1982
2245
  } finally {
1983
2246
  release(held, lockPath2);
@@ -2414,8 +2677,8 @@ function sameOriginEndpoint(value, field2, issuer) {
2414
2677
  }
2415
2678
  function authorizationServerMetadataUrl(issuer) {
2416
2679
  const url = new URL(issuer);
2417
- const path38 = url.pathname.replace(/\/+$/, "");
2418
- return `${url.origin}/.well-known/oauth-authorization-server${path38}`;
2680
+ const path42 = url.pathname.replace(/\/+$/, "");
2681
+ return `${url.origin}/.well-known/oauth-authorization-server${path42}`;
2419
2682
  }
2420
2683
  async function discoverAuthorizationServer(baseUrl, options = {}) {
2421
2684
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
@@ -2482,11 +2745,26 @@ var CallbackError = class extends Error {
2482
2745
  this.name = "CallbackError";
2483
2746
  }
2484
2747
  };
2748
+ var DEFERRED_PAGE_TIMEOUT_MS = 60 * 1e3;
2749
+ var escapeHtml = (text3) => text3.replace(
2750
+ /[&<>"']/g,
2751
+ (char) => ({
2752
+ "&": "&amp;",
2753
+ "<": "&lt;",
2754
+ ">": "&gt;",
2755
+ '"': "&quot;",
2756
+ "'": "&#39;"
2757
+ })[char] ?? char
2758
+ );
2485
2759
  var FINAL_PAGE_HEADERS = {
2486
2760
  "Content-Type": "text/html; charset=utf-8",
2487
2761
  Connection: "close"
2488
2762
  };
2489
- 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>`;
2763
+ var PAGE = (rawTitle, rawBody) => {
2764
+ const title = escapeHtml(rawTitle);
2765
+ const body = escapeHtml(rawBody);
2766
+ return `<!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>`;
2767
+ };
2490
2768
  async function startLoopbackListener(options) {
2491
2769
  const callbackPath = options.callbackPath ?? CALLBACK_PATH;
2492
2770
  let settle = () => {
@@ -2502,6 +2780,32 @@ async function startLoopbackListener(options) {
2502
2780
  });
2503
2781
  result.catch(() => {
2504
2782
  });
2783
+ let closed = false;
2784
+ let held = null;
2785
+ let heldTimer;
2786
+ const release2 = () => {
2787
+ held = null;
2788
+ clearTimeout(heldTimer);
2789
+ if (closed) {
2790
+ server.closeAllConnections();
2791
+ }
2792
+ };
2793
+ const finish = (page) => {
2794
+ const response = held;
2795
+ if (!response) {
2796
+ return Promise.resolve();
2797
+ }
2798
+ held = null;
2799
+ clearTimeout(heldTimer);
2800
+ return new Promise((resolve) => {
2801
+ response.once("close", resolve);
2802
+ if (response.writableEnded || response.destroyed) {
2803
+ resolve();
2804
+ return;
2805
+ }
2806
+ response.writeHead(200, FINAL_PAGE_HEADERS).end(PAGE(page.title, page.body), () => resolve());
2807
+ }).finally(release2);
2808
+ };
2505
2809
  const server = createServer((request2, response) => {
2506
2810
  const url = new URL(request2.url ?? "/", "http://127.0.0.1");
2507
2811
  if (request2.method !== "GET" || url.pathname !== callbackPath) {
@@ -2543,6 +2847,22 @@ async function startLoopbackListener(options) {
2543
2847
  );
2544
2848
  return;
2545
2849
  }
2850
+ if (options.deferPage) {
2851
+ const { fallback, timeoutMs } = options.deferPage;
2852
+ held = response;
2853
+ response.once("close", () => {
2854
+ if (held === response) {
2855
+ release2();
2856
+ }
2857
+ });
2858
+ heldTimer = setTimeout(
2859
+ () => void finish(fallback),
2860
+ timeoutMs ?? DEFERRED_PAGE_TIMEOUT_MS
2861
+ );
2862
+ heldTimer.unref();
2863
+ settle({ code, params: url.searchParams });
2864
+ return;
2865
+ }
2546
2866
  response.writeHead(200, FINAL_PAGE_HEADERS).end(
2547
2867
  PAGE(
2548
2868
  options.successPage?.title ?? "Signed in",
@@ -2573,16 +2893,22 @@ async function startLoopbackListener(options) {
2573
2893
  const onAbort = () => settle({ error: new Error("Sign-in was cancelled.") });
2574
2894
  options.signal?.addEventListener("abort", onAbort, { once: true });
2575
2895
  const close = () => {
2896
+ closed = true;
2576
2897
  clearTimeout(timer);
2577
2898
  options.signal?.removeEventListener("abort", onAbort);
2578
- server.closeAllConnections();
2899
+ if (held) {
2900
+ server.closeIdleConnections();
2901
+ } else {
2902
+ server.closeAllConnections();
2903
+ }
2579
2904
  server.close();
2580
2905
  };
2581
2906
  result.then(close, close);
2582
2907
  return {
2583
2908
  redirectUri: `http://127.0.0.1:${port}${callbackPath}`,
2584
2909
  result,
2585
- close
2910
+ close,
2911
+ finish
2586
2912
  };
2587
2913
  }
2588
2914
 
@@ -2695,7 +3021,8 @@ var UNUSABLE_CLIENT_ERRORS = /* @__PURE__ */ new Set([
2695
3021
  "invalid_client",
2696
3022
  "client_disabled",
2697
3023
  "invalid_redirect",
2698
- "unauthorized_client"
3024
+ "unauthorized_client",
3025
+ "invalid_scope"
2699
3026
  ]);
2700
3027
  var OAuthLoginError = class extends Error {
2701
3028
  constructor(message) {
@@ -4795,10 +5122,10 @@ var AGENT_NAME = "Fabric";
4795
5122
  function registrationPath() {
4796
5123
  return join(dirname(getConfigPath()), "chatgpt-plan-host.json");
4797
5124
  }
4798
- async function loadRegistrationStore(path38 = registrationPath()) {
5125
+ async function loadRegistrationStore(path42 = registrationPath()) {
4799
5126
  let stored;
4800
5127
  try {
4801
- stored = JSON.parse(await readFile(path38, "utf8"));
5128
+ stored = JSON.parse(await readFile(path42, "utf8"));
4802
5129
  } catch {
4803
5130
  stored = {};
4804
5131
  }
@@ -4811,15 +5138,15 @@ async function loadRegistrationStore(path38 = registrationPath()) {
4811
5138
  accounts: stored.subject && stored.clientId ? { [stored.subject]: { clientId: stored.clientId } } : {}
4812
5139
  };
4813
5140
  }
4814
- async function saveAccountRegistration(account, path38 = registrationPath()) {
4815
- const store = await loadRegistrationStore(path38);
5141
+ async function saveAccountRegistration(account, path42 = registrationPath()) {
5142
+ const store = await loadRegistrationStore(path42);
4816
5143
  const { hostId, subject, ...registration } = account;
4817
5144
  const next = {
4818
5145
  hostId,
4819
5146
  accounts: { ...store.accounts, [subject]: registration }
4820
5147
  };
4821
- await mkdir(dirname(path38), { recursive: true, mode: 448 });
4822
- await writeFile(path38, JSON.stringify(next), { mode: 384 });
5148
+ await mkdir(dirname(path42), { recursive: true, mode: 448 });
5149
+ await writeFile(path42, JSON.stringify(next), { mode: 384 });
4823
5150
  }
4824
5151
  function selectRegistration(store, account) {
4825
5152
  if (account === void 0) {
@@ -4940,6 +5267,16 @@ function explainDenial(err) {
4940
5267
  `${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.`
4941
5268
  );
4942
5269
  }
5270
+ var CHECK_TERMINAL_PAGE = {
5271
+ title: "Check your terminal",
5272
+ body: "Fabric is taking longer than expected to answer. Your terminal shows whether the ChatGPT plan was connected. You can close this tab."
5273
+ };
5274
+ function notConnectedPage(reason) {
5275
+ return {
5276
+ title: "Not connected",
5277
+ body: `${reason} Your terminal shows the same message. You can close this tab.`
5278
+ };
5279
+ }
4943
5280
  async function attemptSignIn(registration, options) {
4944
5281
  const verifier = createCodeVerifier();
4945
5282
  const state = createState();
@@ -4947,7 +5284,10 @@ async function attemptSignIn(registration, options) {
4947
5284
  const listener = await startLoopbackListener({
4948
5285
  state,
4949
5286
  callbackPath: CHATGPT_CALLBACK_PATH,
4950
- preferredPort: options.preferredPort ?? CHATGPT_CALLBACK_PORT
5287
+ preferredPort: options.preferredPort ?? CHATGPT_CALLBACK_PORT,
5288
+ ...options.holdBrowserPage && {
5289
+ deferPage: { fallback: CHECK_TERMINAL_PAGE }
5290
+ }
4951
5291
  });
4952
5292
  try {
4953
5293
  options.open(
@@ -4961,27 +5301,38 @@ async function attemptSignIn(registration, options) {
4961
5301
  })
4962
5302
  );
4963
5303
  const { code, params } = await listener.result;
4964
- const clientId = issuedClientId(params, registration);
4965
- const tokens = await exchangeCode(
4966
- {
5304
+ try {
5305
+ const clientId = issuedClientId(params, registration);
5306
+ const tokens = await exchangeCode(
5307
+ {
5308
+ clientId,
5309
+ code,
5310
+ codeVerifier: verifier,
5311
+ redirectUri: listener.redirectUri
5312
+ },
5313
+ options.fetchImpl
5314
+ );
5315
+ const { sub, email } = checkIdToken(tokens.id_token ?? "", {
5316
+ nonce,
5317
+ subject: registration.subject
5318
+ });
5319
+ await options.save?.({
5320
+ hostId: registration.hostId,
5321
+ subject: sub,
4967
5322
  clientId,
4968
- code,
4969
- codeVerifier: verifier,
4970
- redirectUri: listener.redirectUri
4971
- },
4972
- options.fetchImpl
4973
- );
4974
- const { sub, email } = checkIdToken(tokens.id_token ?? "", {
4975
- nonce,
4976
- subject: registration.subject
4977
- });
4978
- await options.save?.({
4979
- hostId: registration.hostId,
4980
- subject: sub,
4981
- clientId,
4982
- ...email !== void 0 && { email }
4983
- });
4984
- return { tokens, clientId, subject: sub, email };
5323
+ ...email !== void 0 && { email }
5324
+ });
5325
+ return {
5326
+ tokens,
5327
+ clientId,
5328
+ subject: sub,
5329
+ email,
5330
+ finishBrowser: listener.finish
5331
+ };
5332
+ } catch (err) {
5333
+ await listener.finish(notConnectedPage(err.message));
5334
+ throw err;
5335
+ }
4985
5336
  } finally {
4986
5337
  listener.close();
4987
5338
  }
@@ -5139,7 +5490,7 @@ async function approveInFabric(origin, sharedOrganizationSlug) {
5139
5490
  function describeOrganizations(result) {
5140
5491
  const enabled = result.organizations.filter((org) => org.enabled);
5141
5492
  if (enabled.length === 0) {
5142
- return "Choose where to use it in Fabric settings \u2192 Account \u2192 AI providers.";
5493
+ return "Choose where to use it in Fabric: Settings \u2192 AI Providers \u2192 Personal.";
5143
5494
  }
5144
5495
  return `Your own AI work in ${enabled.map((org) => org.name).join(", ")} now uses your ChatGPT plan.`;
5145
5496
  }
@@ -5148,6 +5499,28 @@ function describeSharedAccount(result) {
5148
5499
  const { organization, created } = result.shared;
5149
5500
  return `${created ? "Connected" : "Reconnected"} the ChatGPT account${account} as a shared account of ${organization.name}. Choose which work it serves in ${organization.name}'s settings \u2192 AI providers.`;
5150
5501
  }
5502
+ function describeConnected(result) {
5503
+ if ("shared" in result) {
5504
+ return describeSharedAccount(result);
5505
+ }
5506
+ return `Connected your ChatGPT plan${result.email ? ` (${result.email})` : ""} to Fabric.`;
5507
+ }
5508
+ function connectedPage(result) {
5509
+ const where = "shared" in result ? "" : ` ${describeOrganizations(result)}`;
5510
+ return {
5511
+ title: "Connected",
5512
+ body: `${describeConnected(result)}${where} You can close this tab.`
5513
+ };
5514
+ }
5515
+ function notConnectedMessage(reason) {
5516
+ return `
5517
+ \u2717 Not connected: ${reason}
5518
+ `;
5519
+ }
5520
+ function failConnect(reason) {
5521
+ process.stderr.write(notConnectedMessage(reason));
5522
+ process.exit(1);
5523
+ }
5151
5524
  async function connect(opts) {
5152
5525
  const optionsError = connectOptionsError(opts);
5153
5526
  if (optionsError) {
@@ -5178,18 +5551,20 @@ async function connect(opts) {
5178
5551
  try {
5179
5552
  ticket = await approveInFabric(origin, sharedOrganizationSlug);
5180
5553
  } catch (err) {
5181
- printError(err.message, 1);
5554
+ failConnect(err.message);
5182
5555
  }
5183
5556
  process.stdout.write("Approved in Fabric.\n");
5557
+ let signIn;
5184
5558
  let result;
5185
5559
  try {
5186
- const signIn = await signInWithChatGpt({
5560
+ signIn = await signInWithChatGpt({
5187
5561
  registration,
5188
5562
  open: (url) => open(url, "sign in with ChatGPT"),
5189
5563
  ...!sharedOrganizationSlug && { save: saveAccountRegistration },
5190
5564
  onRetry: () => process.stdout.write(
5191
5565
  "ChatGPT refused this machine's saved registration; signing in again with a new one.\n"
5192
- )
5566
+ ),
5567
+ holdBrowserPage: true
5193
5568
  });
5194
5569
  result = await uploadChatGptPlan({
5195
5570
  origin,
@@ -5200,17 +5575,16 @@ async function connect(opts) {
5200
5575
  })
5201
5576
  });
5202
5577
  } catch (err) {
5203
- printError(err.message, 1);
5204
- }
5205
- if ("shared" in result) {
5206
- printSuccess(describeSharedAccount(result));
5207
- return;
5208
- }
5209
- printSuccess(
5210
- `Connected your ChatGPT plan${result.email ? ` (${result.email})` : ""} to Fabric.`
5211
- );
5212
- process.stdout.write(`${describeOrganizations(result)}
5578
+ const reason = err.message;
5579
+ await signIn?.finishBrowser(notConnectedPage(reason));
5580
+ failConnect(reason);
5581
+ }
5582
+ await signIn.finishBrowser(connectedPage(result));
5583
+ printSuccess(describeConnected(result));
5584
+ if (!("shared" in result)) {
5585
+ process.stdout.write(`${describeOrganizations(result)}
5213
5586
  `);
5587
+ }
5214
5588
  }
5215
5589
  function buildConnectCommand() {
5216
5590
  const connectCommand = new Command8("connect").description(
@@ -5314,7 +5688,7 @@ import path3 from "path";
5314
5688
 
5315
5689
  // src/lib/instructions/safe-write.ts
5316
5690
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
5317
- import { constants } from "fs";
5691
+ import { constants as constants2 } from "fs";
5318
5692
  import {
5319
5693
  lstat,
5320
5694
  mkdir as mkdir2,
@@ -5547,7 +5921,7 @@ async function openFileSafely(root, relativePath) {
5547
5921
  try {
5548
5922
  handle = await open2(
5549
5923
  check.path,
5550
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
5924
+ constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
5551
5925
  );
5552
5926
  } catch (error) {
5553
5927
  if (error.code === "ENOENT") {
@@ -5593,40 +5967,57 @@ async function readFileSafely(root, relativePath, options = {}) {
5593
5967
  mode: opened.stats.mode
5594
5968
  };
5595
5969
  }
5596
- if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
5597
- throw new Error(
5598
- "A safe file-read limit must be a non-negative integer."
5599
- );
5600
- }
5601
- if (opened.stats.size > options.maxBytes) {
5602
- throw new Error(
5603
- `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
5604
- );
5605
- }
5606
- const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
5607
- let total = 0;
5608
- while (total <= options.maxBytes) {
5609
- const { bytesRead } = await opened.handle.read(
5610
- buffer,
5611
- total,
5612
- options.maxBytes + 1 - total,
5613
- total
5614
- );
5615
- if (bytesRead === 0) {
5616
- break;
5617
- }
5618
- total += bytesRead;
5619
- }
5620
- if (total > options.maxBytes) {
5621
- throw new Error(
5622
- `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
5623
- );
5624
- }
5625
- return { bytes: buffer.subarray(0, total), mode: opened.stats.mode };
5970
+ return await readOpenedFileBounded(
5971
+ opened.handle,
5972
+ options.maxBytes,
5973
+ relativePath
5974
+ );
5626
5975
  } finally {
5627
5976
  await opened.handle.close();
5628
5977
  }
5629
5978
  }
5979
+ async function readOpenedFileBounded(handle, maxBytes, label) {
5980
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
5981
+ throw new Error(
5982
+ "A safe file-read limit must be a non-negative integer."
5983
+ );
5984
+ }
5985
+ const stats = await handle.stat();
5986
+ if (!stats.isFile()) {
5987
+ throw new Error(`${label} is not a regular file.`);
5988
+ }
5989
+ const tooLarge = () => new Error(
5990
+ `${label} is too large to read safely (maximum ${maxBytes} bytes).`
5991
+ );
5992
+ if (stats.size > maxBytes) {
5993
+ throw tooLarge();
5994
+ }
5995
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
5996
+ let total = 0;
5997
+ while (total <= maxBytes) {
5998
+ const { bytesRead } = await handle.read(
5999
+ buffer,
6000
+ total,
6001
+ maxBytes + 1 - total,
6002
+ total
6003
+ );
6004
+ if (bytesRead === 0) {
6005
+ break;
6006
+ }
6007
+ total += bytesRead;
6008
+ }
6009
+ if (total > maxBytes) {
6010
+ throw tooLarge();
6011
+ }
6012
+ const after = await handle.stat();
6013
+ if (after.size > maxBytes) {
6014
+ throw tooLarge();
6015
+ }
6016
+ if (!after.isFile() || after.mode !== stats.mode) {
6017
+ throw new Error(`${label} changed while it was being read.`);
6018
+ }
6019
+ return { bytes: buffer.subarray(0, total), mode: stats.mode };
6020
+ }
5630
6021
  var FINGERPRINT_BUFFER_BYTES = 64 * 1024;
5631
6022
  async function fingerprintFileSafely(root, relativePath) {
5632
6023
  const opened = await openFileSafely(root, relativePath);
@@ -5844,23 +6235,23 @@ function normalizeContextSourcePath(input) {
5844
6235
  if (slashed.startsWith("/") || DRIVE_LETTER_PREFIX.test(slashed)) {
5845
6236
  return { ok: false, reason: "absolute" };
5846
6237
  }
5847
- let path38 = slashed.replace(/\/{2,}/g, "/");
5848
- while (path38.startsWith("./")) {
5849
- path38 = path38.slice(2);
6238
+ let path42 = slashed.replace(/\/{2,}/g, "/");
6239
+ while (path42.startsWith("./")) {
6240
+ path42 = path42.slice(2);
5850
6241
  }
5851
- if (path38.length === 0) {
6242
+ if (path42.length === 0) {
5852
6243
  return { ok: false, reason: "empty" };
5853
6244
  }
5854
- if (path38.endsWith("/")) {
6245
+ if (path42.endsWith("/")) {
5855
6246
  return { ok: false, reason: "trailing-slash" };
5856
6247
  }
5857
- if (path38.split("/").some((segment) => segment === "." || segment === "..")) {
6248
+ if (path42.split("/").some((segment) => segment === "." || segment === "..")) {
5858
6249
  return { ok: false, reason: "dot-segment" };
5859
6250
  }
5860
- if (path38.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
6251
+ if (path42.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
5861
6252
  return { ok: false, reason: "too-long" };
5862
6253
  }
5863
- return { ok: true, sourcePath: path38 };
6254
+ return { ok: true, sourcePath: path42 };
5864
6255
  }
5865
6256
 
5866
6257
  // src/lib/context-sync/lock.ts
@@ -6058,7 +6449,7 @@ function buildContextIgnoreRules(input = {}) {
6058
6449
  // src/lib/context-sync/walk.ts
6059
6450
  import { lstat as lstat2, readdir } from "fs/promises";
6060
6451
  import path5 from "path";
6061
- function byPath(a, b) {
6452
+ function byPath2(a, b) {
6062
6453
  return a < b ? -1 : a > b ? 1 : 0;
6063
6454
  }
6064
6455
  async function walkContextDirectory(root, rules) {
@@ -6111,16 +6502,16 @@ async function walkContextDirectory(root, rules) {
6111
6502
  }
6112
6503
  }
6113
6504
  }
6114
- walk.files.sort((a, b) => byPath(a.path, b.path));
6115
- walk.symlinks.sort(byPath);
6116
- walk.special.sort(byPath);
6117
- walk.unmatchable.sort(byPath);
6118
- walk.ignored.sort((a, b) => byPath(a.path, b.path));
6505
+ walk.files.sort((a, b) => byPath2(a.path, b.path));
6506
+ walk.symlinks.sort(byPath2);
6507
+ walk.special.sort(byPath2);
6508
+ walk.unmatchable.sort(byPath2);
6509
+ walk.ignored.sort((a, b) => byPath2(a.path, b.path));
6119
6510
  return walk;
6120
6511
  }
6121
6512
 
6122
6513
  // src/lib/context-sync/plan.ts
6123
- function byPath2(a, b) {
6514
+ function byPath3(a, b) {
6124
6515
  return a < b ? -1 : a > b ? 1 : 0;
6125
6516
  }
6126
6517
  async function readContextIgnore(root) {
@@ -6255,15 +6646,15 @@ async function computeContextPlan(input) {
6255
6646
  skipped3.push({ path: entry.diskPath, reason: verdict.reason });
6256
6647
  continue;
6257
6648
  }
6258
- const sha256 = hashContextContent(verdict.content);
6649
+ const sha2562 = hashContextContent(verdict.content);
6259
6650
  const locked = lock?.files[entry.sourcePath];
6260
6651
  const plannedFile = {
6261
6652
  sourcePath: entry.sourcePath,
6262
6653
  diskPath: entry.diskPath,
6263
- sha256,
6654
+ sha256: sha2562,
6264
6655
  bytes: read.bytes.byteLength
6265
6656
  };
6266
- if (locked?.sha256 === sha256) {
6657
+ if (locked?.sha256 === sha2562) {
6267
6658
  unchangedLocal.push(entry.sourcePath);
6268
6659
  } else {
6269
6660
  push2.push(
@@ -6295,14 +6686,14 @@ async function computeContextPlan(input) {
6295
6686
  removed.push(sourcePath);
6296
6687
  }
6297
6688
  }
6298
- push2.sort((a, b) => byPath2(a.sourcePath, b.sourcePath));
6299
- removed.sort(byPath2);
6689
+ push2.sort((a, b) => byPath3(a.sourcePath, b.sourcePath));
6690
+ removed.sort(byPath3);
6300
6691
  const moves = pairMoves(lock, removed, push2);
6301
6692
  const moved = new Set(moves.map((move) => move.from));
6302
6693
  const arrived = new Set(moves.map((move) => move.to));
6303
- unchangedLocal.sort(byPath2);
6304
- forgotten.sort(byPath2);
6305
- skipped3.sort((a, b) => byPath2(a.path, b.path));
6694
+ unchangedLocal.sort(byPath3);
6695
+ forgotten.sort(byPath3);
6696
+ skipped3.sort((a, b) => byPath3(a.path, b.path));
6306
6697
  return {
6307
6698
  push: push2.filter((entry) => !arrived.has(entry.sourcePath)),
6308
6699
  moves,
@@ -6664,7 +7055,7 @@ async function pushContextPlan(input) {
6664
7055
  }
6665
7056
  }
6666
7057
  };
6667
- const deleteEntry = async (sourcePath, sha256) => {
7058
+ const deleteEntry = async (sourcePath, sha2562) => {
6668
7059
  const remove = (expectedContentHash) => input.client.contexts.deleteSyncedFile(
6669
7060
  input.projectId,
6670
7061
  { sourcePath, expectedContentHash },
@@ -6687,7 +7078,7 @@ async function pushContextPlan(input) {
6687
7078
  }
6688
7079
  };
6689
7080
  try {
6690
- return fromAnswer(await remove(sha256));
7081
+ return fromAnswer(await remove(sha2562));
6691
7082
  } catch (error) {
6692
7083
  const managed = repositoryManagedOf(error);
6693
7084
  if (managed !== null) {
@@ -6748,9 +7139,9 @@ async function pushContextPlan(input) {
6748
7139
  }
6749
7140
  };
6750
7141
  const pruneTargetsSoFar = () => {
6751
- const keptByMoves = results.map(moveKeptOldPath).filter((path38) => path38 !== null);
6752
- return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path38) => {
6753
- const locked = input.lock?.files[path38];
7142
+ const keptByMoves = results.map(moveKeptOldPath).filter((path42) => path42 !== null);
7143
+ return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path42) => {
7144
+ const locked = input.lock?.files[path42];
6754
7145
  return locked !== void 0 && locked.state !== "duplicate";
6755
7146
  }).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6756
7147
  };
@@ -6821,12 +7212,12 @@ function nextContextLock(input) {
6821
7212
  changed = true;
6822
7213
  }
6823
7214
  };
6824
- const confirm2 = (sourcePath, sha256, contextId) => {
7215
+ const confirm2 = (sourcePath, sha2562, contextId) => {
6825
7216
  const before = files[sourcePath];
6826
- if (before?.state === void 0 && before?.sha256 === sha256 && before.contextId === contextId) {
7217
+ if (before?.state === void 0 && before?.sha256 === sha2562 && before.contextId === contextId) {
6827
7218
  return;
6828
7219
  }
6829
- files[sourcePath] = { sha256, contextId };
7220
+ files[sourcePath] = { sha256: sha2562, contextId };
6830
7221
  changed = true;
6831
7222
  };
6832
7223
  for (const sourcePath of input.forgotten ?? []) {
@@ -7388,7 +7779,7 @@ function pruneSummary(results, targets) {
7388
7779
  (r) => r.status === "deleted" || r.status === "already-gone"
7389
7780
  ).map((r) => r.sourcePath)
7390
7781
  );
7391
- const notDeleted = targets.filter((path38) => !settled.has(path38)).length;
7782
+ const notDeleted = targets.filter((path42) => !settled.has(path42)).length;
7392
7783
  let text3 = `--prune: ${count("deleted")} deleted, ${count("already-gone")} already gone, ${notDeleted} not deleted`;
7393
7784
  const first = results.find((r) => r.status === "delete-failed");
7394
7785
  if (first && first.status === "delete-failed") {
@@ -7772,7 +8163,7 @@ function buildFramesCommand() {
7772
8163
 
7773
8164
  // src/commands/instructions/index.ts
7774
8165
  import { readdir as readdir4 } from "fs/promises";
7775
- import path36 from "path";
8166
+ import path39 from "path";
7776
8167
  import { Command as Command13 } from "commander";
7777
8168
 
7778
8169
  // src/lib/environment.ts
@@ -7785,15 +8176,15 @@ function runningInCi(env2 = process.env) {
7785
8176
  }
7786
8177
 
7787
8178
  // src/lib/instructions/adoption.ts
7788
- import path11 from "path";
8179
+ import path10 from "path";
7789
8180
 
7790
8181
  // src/lib/instructions/checkout.ts
7791
- import { realpath as realpath2 } from "fs/promises";
7792
- import path10 from "path";
8182
+ import { realpath as realpath3 } from "fs/promises";
8183
+ import path9 from "path";
7793
8184
 
7794
8185
  // src/lib/instructions/git.ts
7795
- import { lstat as lstat5, stat } from "fs/promises";
7796
- import path9 from "path";
8186
+ import { lstat as lstat4, stat } from "fs/promises";
8187
+ import path8 from "path";
7797
8188
 
7798
8189
  // src/lib/instructions/git-literals.ts
7799
8190
  var COMMIT_SHA = /^[0-9a-f]{40}$/;
@@ -8167,8 +8558,7 @@ async function changedTrackedPaths(root, deadline) {
8167
8558
  }
8168
8559
 
8169
8560
  // src/lib/instructions/git-write.ts
8170
- import { lstat as lstat4, readFile as readFile3 } from "fs/promises";
8171
- import path8 from "path";
8561
+ import { realpath as realpath2 } from "fs/promises";
8172
8562
  function fetchFailureOf(stderr) {
8173
8563
  if (/config-env/i.test(stderr) && /unknown option|usage:/i.test(stderr)) {
8174
8564
  return "old-git";
@@ -8380,24 +8770,22 @@ async function restorableBytes(root, file, entry, indexed, fileMode) {
8380
8770
  if (indexed === null || indexed === void 0 || entry.mode === "120000") {
8381
8771
  return null;
8382
8772
  }
8383
- const absolute = path8.join(root, file);
8384
- const info = await lstat4(absolute).catch(() => null);
8385
- if (info === null || !info.isFile() || info.size > MAX_COMPARED_BYTES) {
8773
+ const read = await readFileSafely(root, file, {
8774
+ maxBytes: MAX_COMPARED_BYTES
8775
+ }).catch(() => null);
8776
+ if (read === null) {
8386
8777
  return null;
8387
8778
  }
8388
- const executable = (info.mode & 73) !== 0;
8779
+ const executable = (read.mode & 73) !== 0;
8389
8780
  if (fileMode && executable !== (entry.mode === "100755")) {
8390
8781
  return null;
8391
8782
  }
8392
- const local = await readFile3(absolute).catch(() => null);
8393
- if (local === null) {
8394
- return null;
8395
- }
8783
+ const local = Buffer.from(read.bytes);
8396
8784
  const mine = local.toString("latin1");
8397
8785
  if (mine.includes("\0") || indexed.includes("\0")) {
8398
8786
  return null;
8399
8787
  }
8400
- return mine.replace(CRLF, "\n") === indexed.replace(CRLF, "\n") ? local : null;
8788
+ return mine.replace(CRLF, "\n") === indexed.replace(CRLF, "\n") ? { bytes: local, mode: read.mode } : null;
8401
8789
  }
8402
8790
  async function realBlockers(root, files, deadline) {
8403
8791
  const scope = ["--literal-pathspecs", "-c", "core.quotepath=false"];
@@ -8520,9 +8908,11 @@ async function realBlockers(root, files, deadline) {
8520
8908
  }
8521
8909
  var RESTORE_AND_MERGE_FLOOR_MS = 1500;
8522
8910
  async function stillUntouched(root, restorable) {
8523
- for (const [file, bytes] of restorable) {
8524
- const now2 = await readFile3(path8.join(root, file)).catch(() => null);
8525
- if (now2 === null || !now2.equals(bytes)) {
8911
+ for (const [file, previous] of restorable) {
8912
+ const now2 = await readFileSafely(root, file, {
8913
+ maxBytes: MAX_COMPARED_BYTES
8914
+ }).catch(() => null);
8915
+ if (now2 === null || now2.mode !== previous.mode || !Buffer.from(now2.bytes).equals(previous.bytes)) {
8526
8916
  return false;
8527
8917
  }
8528
8918
  }
@@ -8536,7 +8926,11 @@ async function fastForwardTo(root, sha, deadline) {
8536
8926
  if (first.kind !== "failed" || first.reason !== "local-changes" || first.files.length === 0) {
8537
8927
  return first;
8538
8928
  }
8539
- const judged = await realBlockers(root, first.files, deadline);
8929
+ const restoreRoot = await realpath2(root).catch(() => null);
8930
+ if (restoreRoot === null) {
8931
+ return first;
8932
+ }
8933
+ const judged = await realBlockers(restoreRoot, first.files, deadline);
8540
8934
  if (judged === "ran-out-of-time") {
8541
8935
  return { kind: "timed-out" };
8542
8936
  }
@@ -8546,11 +8940,11 @@ async function fastForwardTo(root, sha, deadline) {
8546
8940
  if (judged.real.length > 0) {
8547
8941
  return { kind: "failed", reason: "local-changes", files: judged.real };
8548
8942
  }
8549
- if (deadline - Date.now() < RESTORE_AND_MERGE_FLOOR_MS || !await stillUntouched(root, judged.restorable)) {
8943
+ if (deadline - Date.now() < RESTORE_AND_MERGE_FLOOR_MS || !await stillUntouched(restoreRoot, judged.restorable)) {
8550
8944
  return first;
8551
8945
  }
8552
8946
  const restored = await runGit(
8553
- root,
8947
+ restoreRoot,
8554
8948
  ["--literal-pathspecs", "checkout", "--", ...first.files],
8555
8949
  deadline,
8556
8950
  { write: true, unattended: true }
@@ -8558,7 +8952,7 @@ async function fastForwardTo(root, sha, deadline) {
8558
8952
  if (restored.kind !== "exited" || restored.code !== 0) {
8559
8953
  return first;
8560
8954
  }
8561
- return mergeOnce(root, sha, deadline);
8955
+ return mergeOnce(restoreRoot, sha, deadline);
8562
8956
  }
8563
8957
 
8564
8958
  // src/lib/instructions/git.ts
@@ -8575,13 +8969,13 @@ var OPERATION_MARKERS = [
8575
8969
  { marker: "sequencer", operation: "cherry-pick" }
8576
8970
  ];
8577
8971
  async function nearestExistingDirectory(dir) {
8578
- let current = path9.resolve(dir);
8972
+ let current = path8.resolve(dir);
8579
8973
  for (; ; ) {
8580
8974
  const found = await stat(current).catch(() => null);
8581
8975
  if (found?.isDirectory()) {
8582
8976
  return current;
8583
8977
  }
8584
- const parent = path9.dirname(current);
8978
+ const parent = path8.dirname(current);
8585
8979
  if (parent === current) {
8586
8980
  return null;
8587
8981
  }
@@ -8591,10 +8985,10 @@ async function nearestExistingDirectory(dir) {
8591
8985
  async function hasGitEntryAbove(dir) {
8592
8986
  let current = dir;
8593
8987
  for (; ; ) {
8594
- if (await lstat5(path9.join(current, ".git")).catch(() => null) !== null) {
8988
+ if (await lstat4(path8.join(current, ".git")).catch(() => null) !== null) {
8595
8989
  return true;
8596
8990
  }
8597
- const parent = path9.dirname(current);
8991
+ const parent = path8.dirname(current);
8598
8992
  if (parent === current) {
8599
8993
  return false;
8600
8994
  }
@@ -8640,7 +9034,7 @@ async function findWorkTree(dir, deadline) {
8640
9034
  kind: "ok",
8641
9035
  value: {
8642
9036
  toplevel,
8643
- gitDir: path9.resolve(cwd, gitDir),
9037
+ gitDir: path8.resolve(cwd, gitDir),
8644
9038
  shallow: shallow === "true",
8645
9039
  superproject: superproject ? superproject : null
8646
9040
  }
@@ -8766,7 +9160,7 @@ async function trackedPaths(root, relativePaths, deadline) {
8766
9160
  return { kind: "ok", value: [] };
8767
9161
  }
8768
9162
  if (relativePaths.some(
8769
- (relativePath) => relativePath === "" || relativePath.startsWith("-") || path9.isAbsolute(relativePath) || path9.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
9163
+ (relativePath) => relativePath === "" || relativePath.startsWith("-") || path8.isAbsolute(relativePath) || path8.win32.isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((part) => part === "..")
8770
9164
  )) {
8771
9165
  return {
8772
9166
  kind: "unavailable",
@@ -8852,8 +9246,8 @@ async function operationInProgress(root, deadline) {
8852
9246
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8853
9247
  }
8854
9248
  for (const [index, { operation }] of OPERATION_MARKERS.entries()) {
8855
- const marker = path9.resolve(root, paths[index]);
8856
- if (await lstat5(marker).catch(() => null) !== null) {
9249
+ const marker = path8.resolve(root, paths[index]);
9250
+ if (await lstat4(marker).catch(() => null) !== null) {
8857
9251
  return { kind: "ok", value: operation };
8858
9252
  }
8859
9253
  }
@@ -8943,7 +9337,7 @@ async function lockFilesPresent(root, deadline) {
8943
9337
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8944
9338
  }
8945
9339
  for (const lock of paths) {
8946
- if (await lstat5(path9.resolve(root, lock)).catch(() => null) !== null) {
9340
+ if (await lstat4(path8.resolve(root, lock)).catch(() => null) !== null) {
8947
9341
  return { kind: "ok", value: true };
8948
9342
  }
8949
9343
  }
@@ -8991,7 +9385,7 @@ async function commonDir(root, deadline) {
8991
9385
  if (result.value.code !== 0 || !dir) {
8992
9386
  return { kind: "unavailable", reason: "git answered unexpectedly" };
8993
9387
  }
8994
- return { kind: "ok", value: path9.resolve(root, dir) };
9388
+ return { kind: "ok", value: path8.resolve(root, dir) };
8995
9389
  }
8996
9390
  async function excludeFilePath(root, deadline) {
8997
9391
  const result = await simple(
@@ -9006,7 +9400,7 @@ async function excludeFilePath(root, deadline) {
9006
9400
  if (result.value.code !== 0 || !answer) {
9007
9401
  return { kind: "unavailable", reason: "git answered unexpectedly" };
9008
9402
  }
9009
- return { kind: "ok", value: path9.resolve(root, answer) };
9403
+ return { kind: "ok", value: path8.resolve(root, answer) };
9010
9404
  }
9011
9405
  async function isIgnored(root, relativePath, deadline) {
9012
9406
  if (relativePath.startsWith("-") || relativePath === "") {
@@ -9636,15 +10030,15 @@ function downloadsIn(classification, manual) {
9636
10030
  return manual && classification.class === "foreign";
9637
10031
  }
9638
10032
  function isSafeRootPath(rootPath) {
9639
- if (path10.posix.isAbsolute(rootPath) || path10.win32.isAbsolute(rootPath) || rootPath.startsWith("~")) {
10033
+ if (path9.posix.isAbsolute(rootPath) || path9.win32.isAbsolute(rootPath) || rootPath.startsWith("~")) {
9640
10034
  return false;
9641
10035
  }
9642
10036
  return !rootPath.split(/[\\/]/).some((segment) => segment === "..");
9643
10037
  }
9644
10038
  async function sameDirectory(a, b) {
9645
10039
  const [left, right] = await Promise.all([
9646
- realpath2(a).catch(() => null),
9647
- realpath2(b).catch(() => null)
10040
+ realpath3(a).catch(() => null),
10041
+ realpath3(b).catch(() => null)
9648
10042
  ]);
9649
10043
  return left !== null && left === right;
9650
10044
  }
@@ -9730,7 +10124,7 @@ async function classify({
9730
10124
  if (matching.length > 1) {
9731
10125
  return { class: "ambiguous", remotes: matching };
9732
10126
  }
9733
- const mapped = path10.join(root, repository.rootPath);
10127
+ const mapped = path9.join(root, repository.rootPath);
9734
10128
  if (!await sameDirectory(destination, mapped)) {
9735
10129
  return {
9736
10130
  class: "unmapped",
@@ -10033,7 +10427,7 @@ function providerLoginCommand(provider, gitCommand) {
10033
10427
  }
10034
10428
  }
10035
10429
  function folderForCommand(dest, relative) {
10036
- const inside = relative.split(path11.sep).join("/") || ".";
10430
+ const inside = relative.split(path10.sep).join("/") || ".";
10037
10431
  const typed = dest?.replace(/\\/g, "/").replace(/\/+$/, "");
10038
10432
  return shellQuote(
10039
10433
  typed === void 0 || typed === "" ? inside : inside === "." ? typed : `${typed}/${inside}`
@@ -10075,9 +10469,9 @@ function adoptionFor(input) {
10075
10469
  repo,
10076
10470
  where: folderForCommand(
10077
10471
  input.rerun.dest,
10078
- path11.relative(
10472
+ path10.relative(
10079
10473
  input.destination,
10080
- path11.join(
10474
+ path10.join(
10081
10475
  classification.toplevel,
10082
10476
  classification.rootPath
10083
10477
  )
@@ -10109,17 +10503,27 @@ function adoptionFor(input) {
10109
10503
  }
10110
10504
 
10111
10505
  // src/lib/instructions/agent-mcp-config.ts
10112
- import { readFile as readFile4, stat as stat2 } from "fs/promises";
10113
- import path12 from "path";
10506
+ import { constants as constants3 } from "fs";
10507
+ import { open as open3 } from "fs/promises";
10508
+ import path11 from "path";
10114
10509
  var LEGACY_SERVER_NAME = "fabric";
10115
10510
  var MAX_CONFIG_BYTES = 16 * 1024 * 1024;
10116
10511
  async function readText(file) {
10117
10512
  try {
10118
- const info = await stat2(file);
10119
- if (!info.isFile() || info.size > MAX_CONFIG_BYTES) {
10120
- return { kind: "unusable" };
10513
+ const handle = await open3(
10514
+ file,
10515
+ constants3.O_RDONLY | constants3.O_NONBLOCK
10516
+ );
10517
+ try {
10518
+ const read = await readOpenedFileBounded(
10519
+ handle,
10520
+ MAX_CONFIG_BYTES,
10521
+ file
10522
+ );
10523
+ return { kind: "text", text: read.bytes.toString("utf8") };
10524
+ } finally {
10525
+ await handle.close();
10121
10526
  }
10122
- return { kind: "text", text: await readFile4(file, "utf8") };
10123
10527
  } catch (error) {
10124
10528
  return error.code === "ENOENT" ? { kind: "absent" } : { kind: "unusable" };
10125
10529
  }
@@ -10149,7 +10553,7 @@ async function readClaudeServers(input) {
10149
10553
  return { state: "unlocated" };
10150
10554
  }
10151
10555
  const servers = [];
10152
- const config2 = await readText(path12.join(directory, ".claude.json"));
10556
+ const config2 = await readText(path11.join(directory, ".claude.json"));
10153
10557
  if (config2.kind === "unusable") {
10154
10558
  return { state: "unreadable" };
10155
10559
  }
@@ -10177,7 +10581,7 @@ async function readClaudeServers(input) {
10177
10581
  servers.push({ scope: "user", ...found });
10178
10582
  }
10179
10583
  }
10180
- const shared = await readText(path12.join(input.cwd, ".mcp.json"));
10584
+ const shared = await readText(path11.join(input.cwd, ".mcp.json"));
10181
10585
  if (shared.kind === "text") {
10182
10586
  try {
10183
10587
  for (const found of serversIn(
@@ -10196,11 +10600,11 @@ var CODEX_SERVER_SUBTABLE = /^\[mcp_servers\.(?:"[^"]+"|'[^']+'|[A-Za-z0-9_-]+)\
10196
10600
  var CODEX_OTHER_HEADER = /^\[{1,2}\s*mcp_servers\b/;
10197
10601
  var CODEX_OTHER_KEY = /^mcp_servers\b/;
10198
10602
  async function readCodexServers(input) {
10199
- const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 : path12.join(input.home, ".codex"));
10603
+ const directory = input.env.CODEX_HOME ?? (input.home === null ? void 0 : path11.join(input.home, ".codex"));
10200
10604
  if (directory === void 0) {
10201
10605
  return { state: "unlocated" };
10202
10606
  }
10203
- const config2 = await readText(path12.join(directory, "config.toml"));
10607
+ const config2 = await readText(path11.join(directory, "config.toml"));
10204
10608
  if (config2.kind === "absent") {
10205
10609
  return { state: "read", servers: [] };
10206
10610
  }
@@ -10351,7 +10755,7 @@ function registrationOf(servers, name) {
10351
10755
  import { createHash as createHash4 } from "crypto";
10352
10756
  import { mkdir as mkdir3 } from "fs/promises";
10353
10757
  import { tmpdir } from "os";
10354
- import path13 from "path";
10758
+ import path12 from "path";
10355
10759
 
10356
10760
  // src/lib/instructions/identifiers.ts
10357
10761
  var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -10360,11 +10764,11 @@ function isIdentifier(value) {
10360
10764
  }
10361
10765
 
10362
10766
  // src/lib/instructions/hook.ts
10363
- var CLAUDE_SETTINGS_RELATIVE_PATH = path13.join(
10767
+ var CLAUDE_SETTINGS_RELATIVE_PATH = path12.join(
10364
10768
  ".claude",
10365
10769
  "settings.local.json"
10366
10770
  );
10367
- var CODEX_HOOKS_RELATIVE_PATH = path13.join(".codex", "hooks.json");
10771
+ var CODEX_HOOKS_RELATIVE_PATH = path12.join(".codex", "hooks.json");
10368
10772
  var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
10369
10773
  var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
10370
10774
  var HOOK_TIMEOUT_SECONDS = 15;
@@ -10381,10 +10785,10 @@ function subcommandsForEvent(event) {
10381
10785
  return event === "Stop" ? STOP_SUBCOMMANDS : SESSION_START_SUBCOMMANDS;
10382
10786
  }
10383
10787
  async function withHookConfigLock(root, posixPath, work) {
10384
- const directory = path13.join(tmpdir(), "fabric-instructions-hook-locks");
10788
+ const directory = path12.join(tmpdir(), "fabric-instructions-hook-locks");
10385
10789
  await mkdir3(directory, { recursive: true });
10386
- const identity = createHash4("sha256").update(path13.resolve(root)).update("\0").update(posixPath).digest("hex");
10387
- const lockPath2 = path13.join(directory, `${identity}.lock`);
10790
+ const identity = createHash4("sha256").update(path12.resolve(root)).update("\0").update(posixPath).digest("hex");
10791
+ const lockPath2 = path12.join(directory, `${identity}.lock`);
10388
10792
  try {
10389
10793
  return await withExclusiveLock(work, {
10390
10794
  lockPath: lockPath2,
@@ -10439,7 +10843,7 @@ async function assertKeyStaysOutside(destination, configPath) {
10439
10843
  }
10440
10844
  }
10441
10845
  async function readHookConfig(root, hookTarget) {
10442
- const settingsPath = path13.join(root, hookTarget.relativePath);
10846
+ const settingsPath = path12.join(root, hookTarget.relativePath);
10443
10847
  let existingRaw;
10444
10848
  try {
10445
10849
  const existing = await readFileSafely(root, hookTarget.posixPath, {
@@ -10518,7 +10922,7 @@ function pruneMatching(groups, projectId, subcommands) {
10518
10922
  async function mergeCommandHook(input) {
10519
10923
  const hookTarget = hookTargetFor(input.tool ?? "claude-code");
10520
10924
  return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
10521
- const settingsPath = path13.join(input.root, hookTarget.relativePath);
10925
+ const settingsPath = path12.join(input.root, hookTarget.relativePath);
10522
10926
  const { settings, existingRaw } = await readHookConfig(
10523
10927
  input.root,
10524
10928
  hookTarget
@@ -10562,7 +10966,7 @@ async function mergeCommandHook(input) {
10562
10966
  async function updateHookConfig(input) {
10563
10967
  const hookTarget = hookTargetFor(input.tool);
10564
10968
  return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
10565
- const settingsPath = path13.join(input.root, hookTarget.relativePath);
10969
+ const settingsPath = path12.join(input.root, hookTarget.relativePath);
10566
10970
  const { settings, existingRaw } = await readHookConfig(
10567
10971
  input.root,
10568
10972
  hookTarget
@@ -10604,7 +11008,7 @@ async function mergeSessionStartHook(input) {
10604
11008
  async function removeCommandHook(input) {
10605
11009
  const hookTarget = hookTargetFor(input.tool ?? "claude-code");
10606
11010
  return withHookConfigLock(input.root, hookTarget.posixPath, async () => {
10607
- const settingsPath = path13.join(input.root, hookTarget.relativePath);
11011
+ const settingsPath = path12.join(input.root, hookTarget.relativePath);
10608
11012
  const { settings, existingRaw } = await readHookConfig(
10609
11013
  input.root,
10610
11014
  hookTarget
@@ -11253,8 +11657,8 @@ function agentMcpSummaryLines(results) {
11253
11657
  import { spawn as spawn3 } from "child_process";
11254
11658
 
11255
11659
  // src/lib/instructions/path-lookup.ts
11256
- import { stat as stat3 } from "fs/promises";
11257
- import path14 from "path";
11660
+ import { stat as stat2 } from "fs/promises";
11661
+ import path13 from "path";
11258
11662
  var MAX_PATH_ENTRIES = 256;
11259
11663
  var MAX_PATHEXT_ENTRIES = 32;
11260
11664
  var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
@@ -11268,7 +11672,7 @@ function readVariable(lookup, name) {
11268
11672
  return key === void 0 ? void 0 : lookup.env[key];
11269
11673
  }
11270
11674
  function pathApi(platform) {
11271
- return platform === "win32" ? path14.win32 : path14.posix;
11675
+ return platform === "win32" ? path13.win32 : path13.posix;
11272
11676
  }
11273
11677
  function searchDirectories(lookup) {
11274
11678
  const api = pathApi(lookup.platform);
@@ -11290,7 +11694,7 @@ function windowsExtensions(lookup) {
11290
11694
  const raw = readVariable(lookup, "PATHEXT") ?? DEFAULT_PATHEXT;
11291
11695
  return raw.split(";").map((extension) => extension.trim()).filter((extension) => /^\.[A-Za-z0-9]+$/.test(extension)).slice(0, MAX_PATHEXT_ENTRIES);
11292
11696
  }
11293
- async function isExecutableFile(candidate, platform, statCandidate = stat3) {
11697
+ async function isExecutableFile(candidate, platform, statCandidate = stat2) {
11294
11698
  try {
11295
11699
  const stats = await statCandidate(candidate);
11296
11700
  if (!stats.isFile()) {
@@ -11464,8 +11868,8 @@ import { createHash as createHash5 } from "crypto";
11464
11868
  import { chmod } from "fs/promises";
11465
11869
 
11466
11870
  // src/lib/instructions/lock.ts
11467
- import { readFile as readFile5 } from "fs/promises";
11468
- import path15 from "path";
11871
+ import { readFile as readFile3 } from "fs/promises";
11872
+ import path14 from "path";
11469
11873
  var LOCK_DIRECTORY = ".fabric";
11470
11874
  var LOCK_FILENAME = "instructions.lock";
11471
11875
  var LOCK_VERSION = 3;
@@ -11473,13 +11877,13 @@ var READABLE_LOCK_VERSIONS = [1, 2, LOCK_VERSION];
11473
11877
  var LOCK_RELATIVE_PATH = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
11474
11878
  var SHA256_HEX2 = /^[0-9a-f]{64}$/;
11475
11879
  function lockPath(destination) {
11476
- return path15.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
11880
+ return path14.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
11477
11881
  }
11478
- async function readLock(destination) {
11882
+ async function readLock2(destination) {
11479
11883
  const file = lockPath(destination);
11480
11884
  let raw;
11481
11885
  try {
11482
- raw = await readFile5(file, "utf8");
11886
+ raw = await readFile3(file, "utf8");
11483
11887
  } catch (error) {
11484
11888
  if (error.code === "ENOENT") {
11485
11889
  return null;
@@ -12081,11 +12485,11 @@ function extractBundle(archive, wanted) {
12081
12485
  }
12082
12486
 
12083
12487
  // src/lib/instructions/clone-target.ts
12084
- import { lstat as lstat6 } from "fs/promises";
12085
- import path16 from "path";
12488
+ import { lstat as lstat5 } from "fs/promises";
12489
+ import path15 from "path";
12086
12490
  async function resolveCloneFolder(base, folder) {
12087
- const target = path16.resolve(base, folder);
12088
- const stats = await lstat6(target).catch((error) => {
12491
+ const target = path15.resolve(base, folder);
12492
+ const stats = await lstat5(target).catch((error) => {
12089
12493
  if (error.code === "ENOENT") {
12090
12494
  return null;
12091
12495
  }
@@ -12102,8 +12506,8 @@ async function resolveCloneFolder(base, folder) {
12102
12506
 
12103
12507
  // src/lib/instructions/doctor.ts
12104
12508
  import { createHash as createHash9 } from "crypto";
12105
- import { stat as stat4 } from "fs/promises";
12106
- import path18 from "path";
12509
+ import { stat as stat3 } from "fs/promises";
12510
+ import path17 from "path";
12107
12511
 
12108
12512
  // src/lib/instructions/file-downloads.ts
12109
12513
  import { createHash as createHash6 } from "crypto";
@@ -12448,13 +12852,13 @@ function isTooLarge(error) {
12448
12852
 
12449
12853
  // src/lib/instructions/staged-files.ts
12450
12854
  import { createHash as createHash8 } from "crypto";
12451
- import { mkdtemp, realpath as realpath3, rm } from "fs/promises";
12855
+ import { mkdtemp, realpath as realpath4, rm } from "fs/promises";
12452
12856
  import { tmpdir as tmpdir2 } from "os";
12453
- import path17 from "path";
12857
+ import path16 from "path";
12454
12858
  var IN_MEMORY_SNAPSHOT_BYTES = 50 * 1024 * 1024;
12455
12859
  async function stageFilesByUrl(options) {
12456
- const root = await realpath3(
12457
- await mkdtemp(path17.join(tmpdir2(), "fabric-instructions-"))
12860
+ const root = await realpath4(
12861
+ await mkdtemp(path16.join(tmpdir2(), "fabric-instructions-"))
12458
12862
  );
12459
12863
  const files = /* @__PURE__ */ new Map();
12460
12864
  const dispose = () => rm(root, { recursive: true, force: true });
@@ -12496,7 +12900,7 @@ async function stageFilesByUrl(options) {
12496
12900
  // src/lib/instructions/doctor.ts
12497
12901
  var MAX_ITEMS = 50;
12498
12902
  var MAX_NAMES_IN_FIX = 20;
12499
- var MAX_LOCK_BYTES = 16 * 1024 * 1024;
12903
+ var MAX_LOCK_BYTES2 = 16 * 1024 * 1024;
12500
12904
  var MAX_DOWNLOAD_BYTES = 52428800 + 1048576 + 5e3 * (512 + 2 * 1024);
12501
12905
  var PROBE_TIMEOUT_MS = 5e3;
12502
12906
  var PROBE_BUDGET_MS = 15e3;
@@ -12654,7 +13058,7 @@ function loginCommand2(input) {
12654
13058
  ) ?? fabricCommand("auth login");
12655
13059
  }
12656
13060
  async function isRegularFile(file) {
12657
- return (await stat4(file).catch(() => null))?.isFile() ?? false;
13061
+ return (await stat3(file).catch(() => null))?.isFile() ?? false;
12658
13062
  }
12659
13063
  var Commands = class {
12660
13064
  constructor(input) {
@@ -13154,7 +13558,7 @@ function checkPublished(access) {
13154
13558
  }
13155
13559
  async function readLockBounded(root) {
13156
13560
  try {
13157
- const lock = await readLockSafely(root, { maxBytes: MAX_LOCK_BYTES });
13561
+ const lock = await readLockSafely(root, { maxBytes: MAX_LOCK_BYTES2 });
13158
13562
  return lock === null ? { kind: "absent" } : { kind: "ok", lock };
13159
13563
  } catch (error) {
13160
13564
  if (error instanceof LockReadRefusedError) {
@@ -13186,7 +13590,7 @@ function unreadableLockCheck(input, commands, reason) {
13186
13590
  "lock",
13187
13591
  "machine",
13188
13592
  "fail",
13189
- reason === "too-large" ? `the lock is larger than ${MAX_LOCK_BYTES} bytes` : "the lock is unreadable",
13593
+ reason === "too-large" ? `the lock is larger than ${MAX_LOCK_BYTES2} bytes` : "the lock is unreadable",
13190
13594
  {
13191
13595
  fix: fixOf(
13192
13596
  `the lock at ${where} is damaged: remove it, then run this sync to take a fresh copy`,
@@ -14383,13 +14787,13 @@ async function commandResolves(input, lookup, command) {
14383
14787
  if (TOOL_NAME.test(command)) {
14384
14788
  return await isOnPath(command, lookup) ? "found" : "missing";
14385
14789
  }
14386
- const api = input.platform === "win32" ? path18.win32 : path18.posix;
14790
+ const api = input.platform === "win32" ? path17.win32 : path17.posix;
14387
14791
  if (api.isAbsolute(command)) {
14388
14792
  return await isExecutableFile(command, input.platform) ? "found" : "missing";
14389
14793
  }
14390
14794
  if (command.includes("/") || command.includes("\\")) {
14391
14795
  return await isExecutableFile(
14392
- path18.resolve(input.root, command),
14796
+ path17.resolve(input.root, command),
14393
14797
  input.platform
14394
14798
  ) ? "found" : "missing";
14395
14799
  }
@@ -14566,13 +14970,13 @@ function formatDoctorText(report, destination) {
14566
14970
  // src/lib/instructions/drop-lock.ts
14567
14971
  import { createHash as createHash10 } from "crypto";
14568
14972
  import { rmdir } from "fs/promises";
14569
- import path19 from "path";
14973
+ import path18 from "path";
14570
14974
  var LOCK_RELATIVE = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
14571
- var MAX_LOCK_BYTES2 = 16 * 1024 * 1024;
14975
+ var MAX_LOCK_BYTES3 = 16 * 1024 * 1024;
14572
14976
  async function dropOwnLock(root, projectId) {
14573
14977
  try {
14574
14978
  const read = await readFileSafely(root, LOCK_RELATIVE, {
14575
- maxBytes: MAX_LOCK_BYTES2
14979
+ maxBytes: MAX_LOCK_BYTES3
14576
14980
  });
14577
14981
  if (read === null) {
14578
14982
  return false;
@@ -14581,12 +14985,12 @@ async function dropOwnLock(root, projectId) {
14581
14985
  if (lock.projectId !== projectId) {
14582
14986
  return false;
14583
14987
  }
14584
- const sha256 = createHash10("sha256").update(read.bytes).digest("hex");
14585
- const outcome = await deleteFileSafely(root, LOCK_RELATIVE, sha256);
14988
+ const sha2562 = createHash10("sha256").update(read.bytes).digest("hex");
14989
+ const outcome = await deleteFileSafely(root, LOCK_RELATIVE, sha2562);
14586
14990
  if (outcome !== "deleted") {
14587
14991
  return false;
14588
14992
  }
14589
- await rmdir(path19.join(root, LOCK_DIRECTORY)).catch(() => void 0);
14993
+ await rmdir(path18.join(root, LOCK_DIRECTORY)).catch(() => void 0);
14590
14994
  return true;
14591
14995
  } catch {
14592
14996
  return false;
@@ -14678,8 +15082,8 @@ function replacePath(text3, target, replacement) {
14678
15082
  }
14679
15083
 
14680
15084
  // src/lib/instructions/fast-forward-run.ts
14681
- import { mkdir as mkdir6, realpath as realpath4 } from "fs/promises";
14682
- import path22 from "path";
15085
+ import { mkdir as mkdir6, realpath as realpath5 } from "fs/promises";
15086
+ import path21 from "path";
14683
15087
 
14684
15088
  // src/lib/instructions/fast-forward.ts
14685
15089
  function fastForwardEligibility(facts) {
@@ -14868,10 +15272,10 @@ function fastForwardLines(outcome, context) {
14868
15272
  }
14869
15273
 
14870
15274
  // src/lib/instructions/fast-forward-memory.ts
14871
- import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
14872
- import path20 from "path";
15275
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
15276
+ import path19 from "path";
14873
15277
  function noticeFile(commonDir2) {
14874
- return path20.join(commonDir2, "fabric", "ff-notice.json");
15278
+ return path19.join(commonDir2, "fabric", "ff-notice.json");
14875
15279
  }
14876
15280
  function isRecord4(value) {
14877
15281
  if (typeof value !== "object" || value === null) {
@@ -14882,7 +15286,7 @@ function isRecord4(value) {
14882
15286
  }
14883
15287
  async function isNewNotice(file, key) {
14884
15288
  try {
14885
- const parsed = JSON.parse(await readFile6(file, "utf8"));
15289
+ const parsed = JSON.parse(await readFile4(file, "utf8"));
14886
15290
  return !(isRecord4(parsed) && parsed.projectId === key.projectId && parsed.publishedVersion === key.publishedVersion && parsed.directCommitSha === key.directCommitSha && parsed.reason === key.reason);
14887
15291
  } catch {
14888
15292
  return true;
@@ -14890,7 +15294,7 @@ async function isNewNotice(file, key) {
14890
15294
  }
14891
15295
  async function rememberNotice(file, key) {
14892
15296
  try {
14893
- await mkdir4(path20.dirname(file), { recursive: true });
15297
+ await mkdir4(path19.dirname(file), { recursive: true });
14894
15298
  const temporary = `${file}.${process.pid}.tmp`;
14895
15299
  const record = { v: 1, ...key };
14896
15300
  await writeFile2(temporary, `${JSON.stringify(record)}
@@ -14919,15 +15323,15 @@ function sinceProcessStart(now2 = Date.now()) {
14919
15323
  }
14920
15324
 
14921
15325
  // src/lib/instructions/hook-trace.ts
14922
- import { mkdir as mkdir5, readFile as readFile7, rename as rename3, writeFile as writeFile3 } from "fs/promises";
14923
- import path21 from "path";
15326
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
15327
+ import path20 from "path";
14924
15328
  var TRACE_LIMIT = 50;
14925
15329
  function traceFile(configDir) {
14926
- return path21.join(configDir, "traces", "instructions-hook.jsonl");
15330
+ return path20.join(configDir, "traces", "instructions-hook.jsonl");
14927
15331
  }
14928
15332
  async function appendTrace(file, entry) {
14929
15333
  try {
14930
- const previous = await readFile7(file, "utf8").catch(() => "");
15334
+ const previous = await readFile5(file, "utf8").catch(() => "");
14931
15335
  const kept = previous.split("\n").filter((line4) => {
14932
15336
  try {
14933
15337
  return typeof JSON.parse(line4)?.outcome === "string";
@@ -14936,7 +15340,7 @@ async function appendTrace(file, entry) {
14936
15340
  }
14937
15341
  }).slice(-(TRACE_LIMIT - 1));
14938
15342
  kept.push(JSON.stringify(entry));
14939
- await mkdir5(path21.dirname(file), { recursive: true, mode: 448 });
15343
+ await mkdir5(path20.dirname(file), { recursive: true, mode: 448 });
14940
15344
  const temporary = `${file}.${process.pid}.tmp`;
14941
15345
  await writeFile3(temporary, `${kept.join("\n")}
14942
15346
  `, { mode: 384 });
@@ -14964,7 +15368,7 @@ var Phases = class {
14964
15368
  };
14965
15369
  var show2 = (value, max = 200) => sanitizeDisplayText(value, max);
14966
15370
  async function samePath(a, b) {
14967
- const resolve = async (value) => realpath4(value).catch(() => path22.resolve(value));
15371
+ const resolve = async (value) => realpath5(value).catch(() => path21.resolve(value));
14968
15372
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
14969
15373
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
14970
15374
  }
@@ -15216,11 +15620,11 @@ async function decide(run4) {
15216
15620
  if (common.kind !== "ok") {
15217
15621
  return unanswered(deadline, head, "read");
15218
15622
  }
15219
- const directory = path22.join(common.value, "fabric");
15623
+ const directory = path21.join(common.value, "fabric");
15220
15624
  try {
15221
15625
  await mkdir6(directory, { recursive: true });
15222
15626
  return await withExclusiveLock(() => underLock(run4, facts), {
15223
- lockPath: path22.join(directory, "ff.lock"),
15627
+ lockPath: path21.join(directory, "ff.lock"),
15224
15628
  staleMs: FF_LOCK_STALE_MS,
15225
15629
  waitMs: 0
15226
15630
  });
@@ -15230,7 +15634,7 @@ async function decide(run4) {
15230
15634
  return {
15231
15635
  outcome: error.abandoned ? {
15232
15636
  kind: "abandoned-lock",
15233
- lockPath: path22.join(directory, "ff.lock")
15637
+ lockPath: path21.join(directory, "ff.lock")
15234
15638
  } : { kind: "locked" },
15235
15639
  head: now2.kind === "ok" ? now2.value : head
15236
15640
  };
@@ -15435,11 +15839,11 @@ function gatewayProbeFor(input) {
15435
15839
  }
15436
15840
 
15437
15841
  // src/lib/instructions/git-exclude.ts
15438
- import { constants as constants2 } from "fs";
15439
- import { lstat as lstat7, mkdir as mkdir7, open as open3 } from "fs/promises";
15440
- import path23 from "path";
15842
+ import { constants as constants4 } from "fs";
15843
+ import { lstat as lstat6, mkdir as mkdir7, open as open4 } from "fs/promises";
15844
+ import path22 from "path";
15441
15845
  function relativeToTopLevel(toplevel, directory, file) {
15442
- return path23.relative(toplevel, path23.join(directory, file)).split(path23.sep).join("/");
15846
+ return path22.relative(toplevel, path22.join(directory, file)).split(path22.sep).join("/");
15443
15847
  }
15444
15848
  function isLiteralEntry(entry) {
15445
15849
  return !/[\n\r*?[\]\\!]/.test(entry) && !entry.startsWith("#");
@@ -15473,16 +15877,16 @@ async function excludeLocalFiles(input) {
15473
15877
  if (file.kind !== "ok") {
15474
15878
  return { kind: "failed", entries: missing };
15475
15879
  }
15476
- await mkdir7(path23.dirname(file.value), { recursive: true });
15477
- const noFollow = constants2.O_NOFOLLOW;
15478
- const flags = constants2.O_RDWR | constants2.O_APPEND | (constants2.O_NONBLOCK ?? 0) | (noFollow ?? 0);
15479
- const handle = await open3(file.value, flags).catch((error) => {
15880
+ await mkdir7(path22.dirname(file.value), { recursive: true });
15881
+ const noFollow = constants4.O_NOFOLLOW;
15882
+ const flags = constants4.O_RDWR | constants4.O_APPEND | (constants4.O_NONBLOCK ?? 0) | (noFollow ?? 0);
15883
+ const handle = await open4(file.value, flags).catch((error) => {
15480
15884
  if (error.code !== "ENOENT") {
15481
15885
  throw error;
15482
15886
  }
15483
- return open3(
15887
+ return open4(
15484
15888
  file.value,
15485
- flags | constants2.O_CREAT | constants2.O_EXCL
15889
+ flags | constants4.O_CREAT | constants4.O_EXCL
15486
15890
  );
15487
15891
  });
15488
15892
  try {
@@ -15491,7 +15895,7 @@ async function excludeLocalFiles(input) {
15491
15895
  return { kind: "failed", entries: missing };
15492
15896
  }
15493
15897
  if (noFollow === void 0) {
15494
- const current = await lstat7(file.value, { bigint: true });
15898
+ const current = await lstat6(file.value, { bigint: true });
15495
15899
  if (!current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
15496
15900
  return { kind: "failed", entries: missing };
15497
15901
  }
@@ -15515,8 +15919,8 @@ async function excludeLocalFiles(input) {
15515
15919
  }
15516
15920
 
15517
15921
  // src/lib/instructions/hook-launcher.ts
15518
- import { mkdir as mkdir8, readFile as readFile8, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
15519
- import path24 from "path";
15922
+ import { mkdir as mkdir8, readFile as readFile6, rename as rename4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
15923
+ import path23 from "path";
15520
15924
  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.";
15521
15925
  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.";
15522
15926
  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.";
@@ -15524,7 +15928,7 @@ function deploymentFolder(origin) {
15524
15928
  return origin.replace(/^(https?):\/\//, "$1-").replace(/[^A-Za-z0-9.-]+/g, "-");
15525
15929
  }
15526
15930
  function bundleCopyPath(configDirectory, origin) {
15527
- return path24.join(
15931
+ return path23.join(
15528
15932
  configDirectory,
15529
15933
  "cli",
15530
15934
  deploymentFolder(origin),
@@ -15532,11 +15936,11 @@ function bundleCopyPath(configDirectory, origin) {
15532
15936
  );
15533
15937
  }
15534
15938
  function samePath2(a, b) {
15535
- const [left, right] = [path24.resolve(a), path24.resolve(b)];
15939
+ const [left, right] = [path23.resolve(a), path23.resolve(b)];
15536
15940
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
15537
15941
  }
15538
15942
  async function writeCopy(bytes, destination) {
15539
- await mkdir8(path24.dirname(destination), { recursive: true });
15943
+ await mkdir8(path23.dirname(destination), { recursive: true });
15540
15944
  const temporary = `${destination}.${process.pid}.tmp`;
15541
15945
  await writeFile4(temporary, bytes, { mode: 420 });
15542
15946
  try {
@@ -15544,7 +15948,7 @@ async function writeCopy(bytes, destination) {
15544
15948
  return true;
15545
15949
  } catch (error) {
15546
15950
  await rm3(temporary, { force: true });
15547
- const existing = await readFile8(destination).catch(() => null);
15951
+ const existing = await readFile6(destination).catch(() => null);
15548
15952
  if (existing === null) {
15549
15953
  throw error;
15550
15954
  }
@@ -15567,7 +15971,7 @@ async function resolveHookLauncher(input) {
15567
15971
  return { prefix: `node ${word}`, warning: null };
15568
15972
  }
15569
15973
  const current = await writeCopy(
15570
- await readFile8(input.bundle.scriptPath),
15974
+ await readFile6(input.bundle.scriptPath),
15571
15975
  destination
15572
15976
  );
15573
15977
  return {
@@ -15620,8 +16024,8 @@ async function installHooks(input) {
15620
16024
  // src/lib/instructions/lesson-prompt.ts
15621
16025
  import { createHash as createHash11 } from "crypto";
15622
16026
  import { createReadStream } from "fs";
15623
- import { mkdir as mkdir9, readdir as readdir2, stat as stat5, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
15624
- import path25 from "path";
16027
+ import { mkdir as mkdir9, readdir as readdir2, stat as stat4, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
16028
+ import path24 from "path";
15625
16029
  import { createInterface } from "readline";
15626
16030
  function parseStopHookInput(text3) {
15627
16031
  let parsed;
@@ -15719,9 +16123,9 @@ async function pruneOldMarkers(markerDir, now2) {
15719
16123
  const cutoff = now2.getTime() - MARKER_MAX_AGE_MS;
15720
16124
  await Promise.all(
15721
16125
  entries.map(async (name) => {
15722
- const entryPath = path25.join(markerDir, name);
16126
+ const entryPath = path24.join(markerDir, name);
15723
16127
  try {
15724
- const stats = await stat5(entryPath);
16128
+ const stats = await stat4(entryPath);
15725
16129
  if (stats.mtimeMs < cutoff) {
15726
16130
  await unlink2(entryPath);
15727
16131
  }
@@ -15732,7 +16136,7 @@ async function pruneOldMarkers(markerDir, now2) {
15732
16136
  }
15733
16137
  async function markerExists(markerPath) {
15734
16138
  try {
15735
- await stat5(markerPath);
16139
+ await stat4(markerPath);
15736
16140
  return true;
15737
16141
  } catch (error) {
15738
16142
  if (error.code === "ENOENT") {
@@ -15779,7 +16183,7 @@ async function runLessonPromptInner(input) {
15779
16183
  debugLog(`marker prune failed: ${describeError(error)}`);
15780
16184
  });
15781
16185
  const key = markerKey(parsedInput.sessionId, projectId);
15782
- const markerPath = path25.join(input.markerDir, key);
16186
+ const markerPath = path24.join(input.markerDir, key);
15783
16187
  if (await markerExists(markerPath)) {
15784
16188
  return { output: null, why: "already-asked" };
15785
16189
  }
@@ -15876,16 +16280,16 @@ function assertLockMatchesManifest(lock, manifest) {
15876
16280
  const published = new Map(
15877
16281
  manifest.map((entry) => [entry.path, entry.sha256])
15878
16282
  );
15879
- for (const [path38, entry] of Object.entries(lock.files)) {
15880
- const expected = published.get(path38);
16283
+ for (const [path42, entry] of Object.entries(lock.files)) {
16284
+ const expected = published.get(path42);
15881
16285
  if (expected === void 0) {
15882
16286
  refuseStaleLedger(
15883
- `it lists ${path38}, which version ${lock.snapshotVersion} does not contain`
16287
+ `it lists ${path42}, which version ${lock.snapshotVersion} does not contain`
15884
16288
  );
15885
16289
  }
15886
16290
  if (expected !== entry.sha256) {
15887
16291
  refuseStaleLedger(
15888
- `its record of ${path38} is not the published file`
16292
+ `its record of ${path42} is not the published file`
15889
16293
  );
15890
16294
  }
15891
16295
  }
@@ -16009,8 +16413,8 @@ async function materializePushChanges(input) {
16009
16413
  `Refusing to push: ${entry.path} disappeared after planning. Run \`${fabricCommand("instructions push")}\` again.`
16010
16414
  );
16011
16415
  }
16012
- const sha256 = sha256Of(read.bytes);
16013
- if (read.bytes.length !== entry.size || sha256 !== entry.sha256) {
16416
+ const sha2562 = sha256Of(read.bytes);
16417
+ if (read.bytes.length !== entry.size || sha2562 !== entry.sha256) {
16014
16418
  throw new Error(
16015
16419
  `Refusing to push: ${entry.path} changed after planning. Run \`${fabricCommand("instructions push")}\` again.`
16016
16420
  );
@@ -16700,7 +17104,7 @@ async function confirm(question, streams = { input: process.stdin, output: proce
16700
17104
  }
16701
17105
 
16702
17106
  // src/lib/instructions/resolve-project.ts
16703
- import path26 from "path";
17107
+ import path25 from "path";
16704
17108
  var MAX_CANDIDATES = 8;
16705
17109
  var MAX_CANDIDATE_CHARS = 512;
16706
17110
  var MAX_LABEL_CHARS = 80;
@@ -16769,7 +17173,7 @@ async function resolveProjectFromCheckout(input) {
16769
17173
  for (const match of matches2) {
16770
17174
  if (await sameDirectory(
16771
17175
  destination,
16772
- path26.join(toplevel, match.rootPath)
17176
+ path25.join(toplevel, match.rootPath)
16773
17177
  )) {
16774
17178
  here.push(match);
16775
17179
  }
@@ -16796,13 +17200,13 @@ import { spawn as spawn4 } from "child_process";
16796
17200
  import { createHash as createHash14 } from "crypto";
16797
17201
  import {
16798
17202
  mkdir as mkdir10,
16799
- readFile as readFile9,
16800
- realpath as realpath5,
17203
+ readFile as readFile7,
17204
+ realpath as realpath6,
16801
17205
  rm as rm4,
16802
- stat as stat6,
17206
+ stat as stat5,
16803
17207
  writeFile as writeFile6
16804
17208
  } from "fs/promises";
16805
- import path27 from "path";
17209
+ import path26 from "path";
16806
17210
 
16807
17211
  // src/lib/instructions/npm-tarball.ts
16808
17212
  import { gunzipSync } from "zlib";
@@ -16936,16 +17340,16 @@ function outOfTime(input, now2) {
16936
17340
  return input.deadlineAt - now2 < input.timing.budgetMs + input.timing.marginMs;
16937
17341
  }
16938
17342
  async function isKeptCopy(script, copy) {
16939
- const actual = await realpath5(copy).catch(() => null);
17343
+ const actual = await realpath6(copy).catch(() => null);
16940
17344
  if (actual === null) {
16941
17345
  return false;
16942
17346
  }
16943
- const running = await realpath5(script).catch(() => path27.resolve(script));
17347
+ const running = await realpath6(script).catch(() => path26.resolve(script));
16944
17348
  return samePath2(running, actual);
16945
17349
  }
16946
17350
  async function readState(file) {
16947
17351
  try {
16948
- const parsed = JSON.parse(await readFile9(file, "utf8"));
17352
+ const parsed = JSON.parse(await readFile7(file, "utf8"));
16949
17353
  return typeof parsed.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && typeof parsed.tarball === "string" ? { checkedAt: parsed.checkedAt, tarball: parsed.tarball } : null;
16950
17354
  } catch {
16951
17355
  return null;
@@ -16953,7 +17357,7 @@ async function readState(file) {
16953
17357
  }
16954
17358
  async function writeState(file, state) {
16955
17359
  try {
16956
- await mkdir10(path27.dirname(file), { recursive: true });
17360
+ await mkdir10(path26.dirname(file), { recursive: true });
16957
17361
  await writeFile6(file, `${JSON.stringify(state)}
16958
17362
  `);
16959
17363
  return true;
@@ -16963,7 +17367,7 @@ async function writeState(file, state) {
16963
17367
  }
16964
17368
  async function claimSlot(file, now2, timing) {
16965
17369
  try {
16966
- await mkdir10(path27.dirname(file), { recursive: true });
17370
+ await mkdir10(path26.dirname(file), { recursive: true });
16967
17371
  for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
16968
17372
  try {
16969
17373
  await writeFile6(file, `${now2}
@@ -16973,7 +17377,7 @@ async function claimSlot(file, now2, timing) {
16973
17377
  if (error.code !== "EEXIST") {
16974
17378
  return false;
16975
17379
  }
16976
- const held = await stat6(file).catch(() => null);
17380
+ const held = await stat5(file).catch(() => null);
16977
17381
  if (held !== null && now2 - held.mtimeMs < timing.budgetMs + timing.marginMs) {
16978
17382
  return false;
16979
17383
  }
@@ -17127,7 +17531,7 @@ async function refreshKeptCopy(input, deps = {}) {
17127
17531
  if (!await isKeptCopy(input.script, copy)) {
17128
17532
  return skipped2("not-a-kept-copy");
17129
17533
  }
17130
- const stateFile = path27.join(path27.dirname(copy), STATE_FILE);
17534
+ const stateFile = path26.join(path26.dirname(copy), STATE_FILE);
17131
17535
  const previous = await readState(stateFile);
17132
17536
  if (previous !== null && askedRecently(previous, now2())) {
17133
17537
  return skipped2("checked-recently");
@@ -17230,19 +17634,19 @@ function selfUpdateLine(outcome) {
17230
17634
  }
17231
17635
 
17232
17636
  // src/lib/instructions/tools.ts
17233
- import { stat as stat7 } from "fs/promises";
17234
- import path28 from "path";
17637
+ import { stat as stat6 } from "fs/promises";
17638
+ import path27 from "path";
17235
17639
  var TOOLS = [
17236
17640
  { tool: "claude-code", directory: ".claude", command: "claude" },
17237
17641
  { tool: "codex", directory: ".codex", command: "codex" }
17238
17642
  ];
17239
17643
  async function isDirectory(target) {
17240
- return (await stat7(target).catch(() => null))?.isDirectory() ?? false;
17644
+ return (await stat6(target).catch(() => null))?.isDirectory() ?? false;
17241
17645
  }
17242
17646
  async function detectTools(input) {
17243
17647
  const present = [];
17244
17648
  for (const facts of TOOLS) {
17245
- 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);
17649
+ 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);
17246
17650
  if (found) {
17247
17651
  present.push(facts.tool);
17248
17652
  }
@@ -17251,7 +17655,7 @@ async function detectTools(input) {
17251
17655
  }
17252
17656
 
17253
17657
  // src/commands/instructions/init-plugin.ts
17254
- import path35 from "path";
17658
+ import path38 from "path";
17255
17659
 
17256
17660
  // src/lib/memory/hooks.ts
17257
17661
  var HOOK_TIMEOUT_SECONDS2 = 15;
@@ -17277,6 +17681,9 @@ function kindOf(command) {
17277
17681
  if (args[0] === "instructions" && args[1] === "render") {
17278
17682
  return "plugin-render";
17279
17683
  }
17684
+ if (args[0] === "instructions" && args[1] === "rules") {
17685
+ return "rules-sync";
17686
+ }
17280
17687
  return null;
17281
17688
  }
17282
17689
  function isOurs(hook, projectId, kinds) {
@@ -17332,6 +17739,18 @@ function desiredHooks(input) {
17332
17739
  ];
17333
17740
  }
17334
17741
  return [
17742
+ {
17743
+ event: "SessionStart",
17744
+ kind: "rules-sync",
17745
+ command: words(
17746
+ input.launcher,
17747
+ "instructions rules",
17748
+ input.projectId,
17749
+ binding,
17750
+ ""
17751
+ ),
17752
+ timeout: RENDER_HOOK_TIMEOUT_SECONDS
17753
+ },
17335
17754
  {
17336
17755
  event: "SessionStart",
17337
17756
  kind: "memory-pull",
@@ -17401,7 +17820,12 @@ function without(groups, projectId, kinds) {
17401
17820
  }
17402
17821
  async function installPluginHooks(input) {
17403
17822
  const wanted = desiredHooks(input);
17404
- const allKinds = ["memory-pull", "memory-push", "plugin-render"];
17823
+ const allKinds = [
17824
+ "memory-pull",
17825
+ "memory-push",
17826
+ "plugin-render",
17827
+ "rules-sync"
17828
+ ];
17405
17829
  return updateHookConfig({
17406
17830
  root: input.root,
17407
17831
  tool: input.tool,
@@ -17438,7 +17862,7 @@ async function installPluginHooks(input) {
17438
17862
  }
17439
17863
 
17440
17864
  // src/lib/memory/project-root.ts
17441
- import path29 from "path";
17865
+ import path28 from "path";
17442
17866
  var MARKER_RELATIVE_PATH = ".fabric/project.json";
17443
17867
  var MARKER_VERSION = 1;
17444
17868
  var MAX_MARKER_BYTES = 64 * 1024;
@@ -17478,13 +17902,13 @@ async function writeProjectMarker(root, marker) {
17478
17902
  });
17479
17903
  }
17480
17904
  async function findProjectRoot(start) {
17481
- let current = path29.resolve(start);
17905
+ let current = path28.resolve(start);
17482
17906
  for (; ; ) {
17483
17907
  const marker = await readMarker(current);
17484
17908
  if (marker !== null) {
17485
17909
  return { root: current, marker };
17486
17910
  }
17487
- const parent = path29.dirname(current);
17911
+ const parent = path28.dirname(current);
17488
17912
  if (parent === current) {
17489
17913
  return null;
17490
17914
  }
@@ -17492,7 +17916,7 @@ async function findProjectRoot(start) {
17492
17916
  }
17493
17917
  }
17494
17918
  function comparable(folder) {
17495
- const resolved = path29.resolve(folder);
17919
+ const resolved = path28.resolve(folder);
17496
17920
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
17497
17921
  }
17498
17922
  function samePath3(left, right) {
@@ -17503,7 +17927,7 @@ function gitToplevelIsTheProjects(marker, root, found) {
17503
17927
  }
17504
17928
 
17505
17929
  // src/lib/memory/runtime.ts
17506
- import path30 from "path";
17930
+ import path29 from "path";
17507
17931
  var GIT_BUDGET_MS2 = 8e3;
17508
17932
  var MemoryNotSignedIn = class extends CliFailure {
17509
17933
  constructor(origin) {
@@ -17564,7 +17988,7 @@ function gitFor(root, marker) {
17564
17988
  async function createRuntime(options, signal) {
17565
17989
  const origin = originOf3(options.baseUrl);
17566
17990
  const start = await resolveExistingRoot(
17567
- path30.resolve(options.dest ?? process.cwd())
17991
+ path29.resolve(options.dest ?? process.cwd())
17568
17992
  );
17569
17993
  const found = await findProjectRoot(start);
17570
17994
  if (found === null) {
@@ -17604,44 +18028,95 @@ async function createRuntime(options, signal) {
17604
18028
  };
17605
18029
  }
17606
18030
 
17607
- // src/lib/plugin/gateway.ts
17608
- var REMOVE_TIMEOUT_MS = 3e4;
17609
- async function removeLocalClaudeGateway(input) {
17610
- const name = serverNameFor(input.projectId);
17611
- const inspected = await inspectServers({
17612
- tool: "claude-code",
17613
- origin: input.origin,
17614
- projectId: input.projectId,
17615
- cwd: input.cwd,
17616
- home: input.home,
17617
- env: input.env,
17618
- platform: input.platform
17619
- });
17620
- if (inspected.state !== "read") {
17621
- return { kind: "unchecked" };
17622
- }
17623
- const ours = inspected.servers.some(
17624
- (server) => server.scope === "local" && server.name === name && server.kind === "this-project"
17625
- );
17626
- if (!ours) {
17627
- return { kind: "none" };
18031
+ // src/lib/plugin/codex-install.ts
18032
+ import { stat as stat7 } from "fs/promises";
18033
+ import path32 from "path";
18034
+
18035
+ // src/lib/plugin/instruction-access.ts
18036
+ var INSTRUCTION_ACCESS_SKILL = "fabric-instruction-access";
18037
+ var INSTRUCTION_CATALOG = `skills/${INSTRUCTION_ACCESS_SKILL}/catalog.json`;
18038
+ function instructionAccessBootstrap(pluginName, tool) {
18039
+ const skill = tool === "claude-code" ? `/${pluginName}:${INSTRUCTION_ACCESS_SKILL}` : INSTRUCTION_ACCESS_SKILL;
18040
+ return `Fabric instruction access for plugin ${pluginName}: Load the generated ${skill} skill from this plugin before following references or choosing rules for a task. Its catalog lists every source instruction path, including rules beyond automatic delivery limits and files outside plugin folders. Read applicable rules and referenced files from their mapped local copies or this plugin's Fabric MCP server. Resolve @ references and relative paths in the original instruction tree, not by guessing paths in the user's checkout.
18041
+ `;
18042
+ }
18043
+ function instructionCatalog(input) {
18044
+ const paths = input.listed.map((entry) => entry.path);
18045
+ if (paths.some((entry) => !checkRelativePath(entry).ok) || findCollision(paths) !== null) {
18046
+ throw new Error(
18047
+ "The instruction catalog contains unsafe or colliding paths."
18048
+ );
17628
18049
  }
17629
- const removed = await input.run(
17630
- "claude",
17631
- ["mcp", "remove", name, "--scope", "local"],
17632
- { cwd: input.cwd, timeoutMs: REMOVE_TIMEOUT_MS }
17633
- );
17634
- return removed.kind === "exited" && removed.code === 0 ? { kind: "removed", name } : { kind: "failed", name };
18050
+ const entries = [...input.listed].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => ({
18051
+ path: entry.path,
18052
+ size: entry.size,
18053
+ isText: entry.isText,
18054
+ kind: entry.kind,
18055
+ blobId: entry.blobId,
18056
+ sha256: entry.sha256,
18057
+ mode: entry.mode,
18058
+ localPath: input.localPaths.get(entry.path) ?? null
18059
+ }));
18060
+ return `${JSON.stringify(
18061
+ {
18062
+ version: 1,
18063
+ projectId: input.projectId,
18064
+ source: input.source,
18065
+ entries
18066
+ },
18067
+ null,
18068
+ 2
18069
+ )}
18070
+ `;
18071
+ }
18072
+ function instructionAccessSkill() {
18073
+ return [
18074
+ "---",
18075
+ `name: ${INSTRUCTION_ACCESS_SKILL}`,
18076
+ "description: Discover and read this project's original coding instructions, referenced files and rules on demand, including files outside the plugin and rules beyond automatic delivery limits. Use before selecting applicable guidance or following instruction references.",
18077
+ "---",
18078
+ "",
18079
+ "# Fabric instruction access",
18080
+ "",
18081
+ "Search catalog.json beside this SKILL.md with your file search tool and read only matching entries, or use the MCP query below. It contains metadata for every listed instruction file, not file contents, and can be large. Do not load the whole catalog or tree into context. The catalog's projectId and source identify the instructions used to build this plugin; the plugin version is a render hash, not a repository pin.",
18082
+ "",
18083
+ "A non-null localPath is relative to this installed plugin's root (two directories above this skill folder). Read that file for the original bytes. A null localPath means the file is not stored in the plugin; automatic rule caps and unsupported layouts do not remove its catalog entry. Fabric reserves the fabric-instruction-access and fabric-living-memory skill folders; source files displaced by those names remain discoverable here and through MCP.",
18084
+ "",
18085
+ "## Select all applicable rules",
18086
+ "",
18087
+ "Automatic rule delivery can be incomplete. Find rule entries by kind and original rules/ or .claude/rules/ paths. Search the passive rule files under instructions/source/ and inspect the paths: frontmatter of each original rule to determine scope. Before affected work, read every unscoped rule and every rule whose paths match the files you will work on, including rules beyond automatic delivery limits. A filename or topic search alone cannot establish applicability. If applicable instructions cannot be read completely, report the blocked work instead of silently omitting them.",
18088
+ "",
18089
+ "## Read through this plugin's Fabric MCP server",
18090
+ "",
18091
+ "Use fabric_list_project_instructions with projectId from the catalog and query to match a path. For repository sources, also pass generation and commitSha from catalog.source. Optional prefix filters a folder; limit (1 to 500, default 200) controls the page. Read direct.generation and direct.commitSha in the response and check they match the catalog. Follow page.nextCursor by passing it as cursor with the same filters, generation and commitSha until nextCursor is null. An incomplete listing, refusal or unavailable repository is not an empty tree.",
18092
+ "",
18093
+ "Use fabric_get_project_instruction with projectId, path exactly as listed, offset: 0 and maxLength: 50000 (up to 200000). For a repository, include the same generation and commitSha on every call, including the first. Check each response's generation and commitSha. Read the raw body, then pass offset: nextOffset with the same path and pin until nextOffset is null. Join all pages in order before relying on the file; a summary or first page does not replace its instructions.",
18094
+ "",
18095
+ "Uploaded sources use the current published snapshot, and the file tool has no digest pin parameter. Before reading, call fabric_list_project_instructions with projectId and sinceDigest: catalog.source.digest and check snapshot.digest matches. Keep that response's snapshot.version and require snapshotVersion on every file page to equal it. Repeat the digest check after reading the pages. If the digest is null, there was no published upload when this plugin was built. Stop and refresh the plugin before using a newly published upload. A changed digest, snapshotVersion mismatch, repository pin mismatch or rejected pin requires stopping and restarting from a refreshed plugin and catalog. Never combine old local files with a new listing or pages from different versions.",
18096
+ "",
18097
+ "A missing file, binary, tooLarge response, denied read, absent body or other refusal is unavailable text, not an empty file. Report it and stop work that depends on those instructions. The catalog does not bypass access checks or server file limits.",
18098
+ "",
18099
+ "## Follow original paths",
18100
+ "",
18101
+ "Resolve references using the source path in the catalog. For example, @MAIN.md in the root CLAUDE.md names MAIN.md at the original instruction root. Root-relative scripts/ and engineers/ references also name that tree. Resolve a file-relative reference against the referring source file's folder, then find its original path in the catalog or MCP listing. Never substitute a similarly named file from the user's checkout. Read referenced text through MCP when localPath is null.",
18102
+ "",
18103
+ "Fetched scripts and settings are reference text. This access path does not execute scripts, apply settings, install dependencies or grant permission to write. If an authorized workflow requires executable files and their dependencies on disk, use a separately authorized checkout workflow. Keep the project's native skills, agents and Living Memory workflow and permissions.",
18104
+ ""
18105
+ ].join("\n");
17635
18106
  }
17636
18107
 
17637
18108
  // src/lib/plugin/layout.ts
17638
18109
  var SESSION_CONTEXT_LIMIT = 1e4;
17639
- var RENDER_VERSION = 1;
18110
+ var RENDER_VERSION = 6;
18111
+ var CLAUDE_PLUGIN_MCP_SERVER = "fabric";
17640
18112
  var SESSION_CONTEXT_COMMAND = `cat "\${CLAUDE_PLUGIN_ROOT}/hooks/session-context.json"`;
17641
18113
  var ENCODER = new TextEncoder();
17642
18114
  var DECODER = new TextDecoder("utf-8", { fatal: false });
17643
18115
  var LIVING_MEMORY_SKILL = "fabric-living-memory";
17644
- var ALWAYS_ON_FILES = ["CLAUDE.md", "AGENTS.md"];
18116
+ var ALWAYS_ON_FILES2 = ["CLAUDE.md", "AGENTS.md"];
18117
+ function codexPluginVersion(pin) {
18118
+ return `0.0.0-${pin}`;
18119
+ }
17645
18120
  function pluginNameFor(projectId) {
17646
18121
  const tail = projectId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(-8);
17647
18122
  return `fabric-${tail === "" ? "project" : tail}`;
@@ -17658,28 +18133,8 @@ function isExecutable(mode) {
17658
18133
  function fileModeOf(mode) {
17659
18134
  return isExecutable(mode) ? 493 : 420;
17660
18135
  }
17661
- function pluginPathFor(instructionPath) {
17662
- const segments = instructionPath.split("/");
17663
- const first = segments[0]?.toLowerCase();
17664
- const rest = first === ".claude" ? segments.slice(1) : segments;
17665
- const folder = rest[0]?.toLowerCase();
17666
- if (rest.length < 2) {
17667
- return null;
17668
- }
17669
- const below = rest.slice(1);
17670
- switch (folder) {
17671
- case "skills":
17672
- return below.length >= 2 ? `skills/${below.join("/")}` : null;
17673
- case "agents":
17674
- return below.length === 1 && below[0]?.toLowerCase().endsWith(".md") ? `agents/${below[0]}` : null;
17675
- case "commands":
17676
- return `commands/${below.join("/")}`;
17677
- default:
17678
- return null;
17679
- }
17680
- }
17681
18136
  function alwaysOnFile(files, tool = "claude-code") {
17682
- const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] : ALWAYS_ON_FILES;
18137
+ const order = tool === "codex" ? ["AGENTS.md", "CLAUDE.md"] : ALWAYS_ON_FILES2;
17683
18138
  for (const name of order) {
17684
18139
  const found = files.find((entry) => entry.path === name);
17685
18140
  if (found !== void 0) {
@@ -17696,8 +18151,8 @@ function truncationNotice(file, pluginName) {
17696
18151
 
17697
18152
  [Fabric: this file is longer than the ${SESSION_CONTEXT_LIMIT.toLocaleString("en-US")} characters a session start can carry, so it was cut here. Read the rest in full before relying on it: instructions/${file} inside the ${pluginName} plugin, for example ~/.claude/plugins/cache/${pluginName}/${pluginName}/*/instructions/${file}]`;
17698
18153
  }
17699
- function sessionContext(file, text3, pluginName) {
17700
- const heading = `Coding instructions for this project (${file}, from Fabric):
18154
+ function sessionContext(file, text3, pluginName, guidance = "") {
18155
+ const heading = `${guidance}${guidance === "" ? "" : "\n"}Coding instructions for this project (${file}, from Fabric):
17701
18156
 
17702
18157
  `;
17703
18158
  const whole = heading + text3;
@@ -17726,7 +18181,7 @@ function folderLine(folder) {
17726
18181
  return `- \`${folder.folder}/\` -- ${where}`;
17727
18182
  }
17728
18183
  function livingMemorySkill(memory, tool) {
17729
- const delivery = tool === "claude-code" ? "Fabric delivers them: a hook sends new and changed files when your turn ends, and brings other people's changes in when a session starts. Do not run git for these files." : "Fabric delivers them when the next session starts (this tool has no end-of-turn hook). To deliver sooner, call the `fabric_memory_write` tool of the Fabric MCP server. Do not run git for these files.";
18184
+ const delivery = tool === "claude-code" ? "Fabric delivers them: a hook sends new and changed files when your turn ends, and brings other people's changes in when a session starts. Do not run git for these files." : "Fabric delivers them when the next session starts (this tool has no end-of-turn hook). Do not run git for these files.";
17730
18185
  const header = [
17731
18186
  "---",
17732
18187
  `name: ${LIVING_MEMORY_SKILL}`,
@@ -17739,7 +18194,7 @@ function livingMemorySkill(memory, tool) {
17739
18194
  let body;
17740
18195
  if (memory.state === "unavailable") {
17741
18196
  body = [
17742
- "The list of this project's memory folders could not be read when this plugin was built. Run `fabric memory status` to see them, and do not save memory files until you know the folder."
18197
+ "The list of this project's memory folders could not be read when this plugin was built. Call the `fabric_memory_targets` tool of the Fabric MCP server to see them, and do not save memory files until you know the folder."
17743
18198
  ];
17744
18199
  } else if (memory.folders.length === 0) {
17745
18200
  body = [
@@ -17753,105 +18208,137 @@ function livingMemorySkill(memory, tool) {
17753
18208
  "",
17754
18209
  delivery,
17755
18210
  "",
17756
- "Never save memory files anywhere else. A file in a pull-request folder is not part of the project's Living Memory until the pull request is merged."
18211
+ "Never save memory files anywhere else. A file in a pull-request folder is not part of the project's Living Memory until the pull request is merged.",
18212
+ "",
18213
+ "Keep one work item's specs and tasks together in `docs/ai/specs/<work-item-id>/`, inside one of the folders above. To resume a work item, list that folder with the `fabric_memory_read` tool (pass `prefix`, then a file's `path` to read it), or read the local mirror: the same files, at the same paths, in the project folder.",
18214
+ "",
18215
+ "To check a delivery, read the `writeId` of each entry in the `outbox` of `.fabric/memory.lock` and pass it to the `fabric_memory_write_status` tool."
17757
18216
  ];
17758
18217
  }
17759
18218
  return `${[...header, ...body].join("\n")}
17760
18219
  `;
17761
18220
  }
17762
- function descriptionFor() {
17763
- return "Coding instructions for a Fabric project, rendered from the project's published instructions.";
18221
+ function descriptionFor(projectId, gatewayUrl) {
18222
+ let host = gatewayUrl;
18223
+ try {
18224
+ host = new URL(gatewayUrl).host;
18225
+ } catch {
18226
+ }
18227
+ return `Coding instructions for Fabric project ${projectId} on ${host}, rendered from its published instructions.`;
17764
18228
  }
17765
- function mcpConfiguration(tool, serverName, url) {
17766
- return json({
17767
- mcpServers: {
17768
- [serverName]: {
17769
- type: tool === "codex" ? "streamable-http" : "http",
17770
- url
17771
- }
17772
- }
17773
- });
18229
+ function mcpConfiguration(serverName, url) {
18230
+ return json({ mcpServers: { [serverName]: { type: "http", url } } });
17774
18231
  }
17775
18232
  function buildPlugin(inputs) {
17776
18233
  const name = pluginNameFor(inputs.projectId);
17777
18234
  const out = /* @__PURE__ */ new Map();
17778
- const put = (path38, bytes, mode = 420) => {
17779
- out.set(path38, { path: path38, bytes, mode });
18235
+ const put = (path42, bytes, mode = 420) => {
18236
+ out.set(path42, { path: path42, bytes, mode });
17780
18237
  };
18238
+ const sourceAtTarget = /* @__PURE__ */ new Map();
17781
18239
  for (const file of [...inputs.files].sort(
17782
18240
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
17783
18241
  )) {
17784
- const target = pluginPathFor(file.path);
17785
- if (target !== null) {
17786
- put(target, file.bytes, fileModeOf(file.mode));
18242
+ const nativeTarget = pluginPathFor(file.path);
18243
+ const target = file.path === "CLAUDE.md" || file.path === "AGENTS.md" ? `instructions/${file.path}` : projectRulePathFor(file.path) !== null ? `instructions/source/${file.path}` : nativeTarget;
18244
+ if (target === null || [LIVING_MEMORY_SKILL, INSTRUCTION_ACCESS_SKILL].some(
18245
+ (reserved) => target.toLowerCase().startsWith(`skills/${reserved}/`)
18246
+ )) {
18247
+ continue;
17787
18248
  }
18249
+ put(
18250
+ target,
18251
+ file.bytes,
18252
+ nativeTarget === null ? 420 : fileModeOf(file.mode)
18253
+ );
18254
+ sourceAtTarget.set(target, file.path);
17788
18255
  }
18256
+ const localPaths = new Map(
18257
+ [...sourceAtTarget].map(([target, source]) => [source, target])
18258
+ );
17789
18259
  put(
17790
18260
  `skills/${LIVING_MEMORY_SKILL}/SKILL.md`,
17791
18261
  bytesOf(livingMemorySkill(inputs.memory, inputs.tool))
17792
18262
  );
17793
- for (const path38 of [...out.keys()]) {
17794
- if (path38.startsWith(`skills/${LIVING_MEMORY_SKILL}/`) && path38 !== `skills/${LIVING_MEMORY_SKILL}/SKILL.md`) {
17795
- out.delete(path38);
17796
- }
17797
- }
18263
+ put(
18264
+ `skills/${INSTRUCTION_ACCESS_SKILL}/SKILL.md`,
18265
+ bytesOf(instructionAccessSkill())
18266
+ );
18267
+ put(
18268
+ INSTRUCTION_CATALOG,
18269
+ bytesOf(
18270
+ instructionCatalog({
18271
+ projectId: inputs.projectId,
18272
+ source: inputs.instructionAccess.source,
18273
+ listed: inputs.instructionAccess.listed,
18274
+ localPaths
18275
+ })
18276
+ )
18277
+ );
17798
18278
  if (inputs.tool === "claude-code") {
17799
18279
  put(
17800
18280
  ".claude-plugin/plugin.json",
17801
18281
  json({
17802
18282
  name,
17803
- description: descriptionFor(),
18283
+ description: descriptionFor(
18284
+ inputs.projectId,
18285
+ inputs.gatewayUrl
18286
+ ),
17804
18287
  author: { name: "Fabric" }
17805
18288
  })
17806
18289
  );
17807
18290
  put(
17808
18291
  ".mcp.json",
17809
- mcpConfiguration("claude-code", name, inputs.gatewayUrl)
18292
+ mcpConfiguration(CLAUDE_PLUGIN_MCP_SERVER, inputs.gatewayUrl)
17810
18293
  );
17811
18294
  const always = alwaysOnFile(inputs.files);
17812
- if (always !== null) {
17813
- put(`instructions/${always.file}`, bytesOf(always.text));
17814
- put(
17815
- "hooks/session-context.json",
17816
- json({
17817
- hookSpecificOutput: {
17818
- hookEventName: "SessionStart",
17819
- additionalContext: sessionContext(
17820
- always.file,
17821
- always.text,
17822
- name
17823
- )
17824
- }
17825
- })
17826
- );
17827
- put(
17828
- "hooks/hooks.json",
17829
- json({
17830
- hooks: {
17831
- SessionStart: [
17832
- {
17833
- hooks: [
17834
- {
17835
- type: "command",
17836
- command: SESSION_CONTEXT_COMMAND
17837
- }
17838
- ]
17839
- }
17840
- ]
17841
- }
17842
- })
17843
- );
17844
- }
18295
+ const guidance = instructionAccessBootstrap(name, inputs.tool);
18296
+ put(
18297
+ "hooks/session-context.json",
18298
+ json({
18299
+ hookSpecificOutput: {
18300
+ hookEventName: "SessionStart",
18301
+ additionalContext: always === null ? guidance : sessionContext(
18302
+ always.file,
18303
+ always.text,
18304
+ name,
18305
+ guidance
18306
+ )
18307
+ }
18308
+ })
18309
+ );
18310
+ put(
18311
+ "hooks/hooks.json",
18312
+ json({
18313
+ hooks: {
18314
+ SessionStart: [
18315
+ {
18316
+ hooks: [
18317
+ {
18318
+ type: "command",
18319
+ command: SESSION_CONTEXT_COMMAND
18320
+ }
18321
+ ]
18322
+ }
18323
+ ]
18324
+ }
18325
+ })
18326
+ );
17845
18327
  } else {
17846
18328
  put(
17847
- "plugin.json",
18329
+ ".codex-plugin/plugin.json",
17848
18330
  json({
17849
18331
  name,
17850
- version: `0.0.0-${inputs.pin}`,
17851
- description: descriptionFor()
18332
+ version: codexPluginVersion(inputs.pin),
18333
+ description: descriptionFor(
18334
+ inputs.projectId,
18335
+ inputs.gatewayUrl
18336
+ ),
18337
+ skills: "./skills/",
18338
+ mcpServers: "./.mcp.json"
17852
18339
  })
17853
18340
  );
17854
- put("mcp.json", mcpConfiguration("codex", name, inputs.gatewayUrl));
18341
+ put(".mcp.json", mcpConfiguration(name, inputs.gatewayUrl));
17855
18342
  }
17856
18343
  return [...out.values()].sort(
17857
18344
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
@@ -17875,8 +18362,113 @@ function isFabricAgentsOverride(text3) {
17875
18362
  }
17876
18363
 
17877
18364
  // src/lib/plugin/register.ts
17878
- import { mkdir as mkdir11, readFile as readFile10, rename as rename5, rm as rm5, writeFile as writeFile7 } from "fs/promises";
18365
+ import { mkdir as mkdir11, readFile as readFile9, rename as rename5, rm as rm5, writeFile as writeFile7 } from "fs/promises";
17879
18366
  import path31 from "path";
18367
+
18368
+ // src/lib/plugin/binding.ts
18369
+ import { readFile as readFile8 } from "fs/promises";
18370
+ import path30 from "path";
18371
+ var BINDING_FILE = ".fabric-binding.json";
18372
+ function isRecord6(value) {
18373
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18374
+ }
18375
+ function originOf4(value) {
18376
+ try {
18377
+ return new URL(value).origin;
18378
+ } catch {
18379
+ return null;
18380
+ }
18381
+ }
18382
+ function bindingText(binding) {
18383
+ return `${JSON.stringify(binding, null, 2)}
18384
+ `;
18385
+ }
18386
+ async function readJson(file) {
18387
+ try {
18388
+ return JSON.parse(await readFile8(file, "utf8"));
18389
+ } catch {
18390
+ return null;
18391
+ }
18392
+ }
18393
+ function valueAfter(words2, flag) {
18394
+ const at = words2.indexOf(flag);
18395
+ return at >= 0 ? words2[at + 1] ?? null : null;
18396
+ }
18397
+ async function bindingFromSidecar(directory) {
18398
+ const parsed = await readJson(path30.join(directory, BINDING_FILE));
18399
+ if (!isRecord6(parsed) || typeof parsed.projectId !== "string") {
18400
+ return null;
18401
+ }
18402
+ return {
18403
+ projectId: parsed.projectId,
18404
+ origin: typeof parsed.origin === "string" ? parsed.origin : null
18405
+ };
18406
+ }
18407
+ async function legacyClaudeBinding(directory) {
18408
+ const parsed = await readJson(
18409
+ path30.join(directory, ".claude-plugin", "marketplace.json")
18410
+ );
18411
+ if (!isRecord6(parsed) || !Array.isArray(parsed.plugins)) {
18412
+ return null;
18413
+ }
18414
+ for (const plugin of parsed.plugins) {
18415
+ const source = isRecord6(plugin) ? plugin.source : null;
18416
+ const command = isRecord6(source) ? source.command : null;
18417
+ if (typeof command !== "string") {
18418
+ continue;
18419
+ }
18420
+ const words2 = command.split(/\s+/);
18421
+ const projectId = valueAfter(words2, "--project");
18422
+ if (projectId !== null) {
18423
+ const baseUrl = valueAfter(words2, "--base-url");
18424
+ return {
18425
+ projectId,
18426
+ origin: baseUrl === null ? null : originOf4(baseUrl)
18427
+ };
18428
+ }
18429
+ }
18430
+ return null;
18431
+ }
18432
+ var GATEWAY_URL = /^(.+)\/api\/mcp-gateway\/projects\/([A-Za-z0-9_-]+)\/?$/;
18433
+ async function legacyCodexBinding(directory) {
18434
+ const parsed = await readJson(path30.join(directory, ".mcp.json"));
18435
+ if (!isRecord6(parsed) || !isRecord6(parsed.mcpServers)) {
18436
+ return null;
18437
+ }
18438
+ for (const server of Object.values(parsed.mcpServers)) {
18439
+ const url = isRecord6(server) ? server.url : null;
18440
+ if (typeof url !== "string") {
18441
+ continue;
18442
+ }
18443
+ const projectId = GATEWAY_URL.exec(url)?.[2];
18444
+ if (projectId !== void 0) {
18445
+ return { projectId, origin: originOf4(url) };
18446
+ }
18447
+ }
18448
+ return null;
18449
+ }
18450
+ async function readBinding(directory, location) {
18451
+ return await bindingFromSidecar(directory) ?? (location === "claude-marketplace" ? await legacyClaudeBinding(directory) : await legacyCodexBinding(directory));
18452
+ }
18453
+ async function assertLocationFree(input) {
18454
+ const existing = await readBinding(input.directory, input.location);
18455
+ if (existing === null) {
18456
+ return;
18457
+ }
18458
+ const otherProject = existing.projectId !== input.wanted.projectId;
18459
+ const otherDeployment = existing.origin !== null && input.wanted.origin !== null && existing.origin !== input.wanted.origin;
18460
+ if (!otherProject && !otherDeployment) {
18461
+ return;
18462
+ }
18463
+ const name = pluginNameFor(input.wanted.projectId);
18464
+ const removal = input.location === "claude-marketplace" ? `run \`claude plugin uninstall ${name}@${name} --scope local\` in the folders that use it, then delete ${input.directory}` : `delete ${input.directory} and remove ${name} from the Codex marketplace file that lists it`;
18465
+ throw new CliFailure(
18466
+ `The plugin name ${name} is already taken by project ${existing.projectId}${existing.origin === null ? "" : ` on ${existing.origin}`}, so it was not changed for project ${input.wanted.projectId}${input.wanted.origin === null ? "" : ` on ${input.wanted.origin}`}. Replacing it would repoint the folders already set up for that project. To use this one, remove the other setup first: ${removal}, then run this again.`,
18467
+ 7
18468
+ );
18469
+ }
18470
+
18471
+ // src/lib/plugin/register.ts
17880
18472
  var COMMAND_MAX_LENGTH = 500;
17881
18473
  var RENDER_TIMEOUT_SECONDS = 120;
17882
18474
  function marketplaceNameFor(projectId) {
@@ -17936,7 +18528,7 @@ function claudeMarketplace(input) {
17936
18528
  plugins: [
17937
18529
  {
17938
18530
  name: pluginNameFor(input.projectId),
17939
- description: "Coding instructions for a Fabric project, rendered on demand.",
18531
+ description: descriptionFor(input.projectId, input.origin),
17940
18532
  source: {
17941
18533
  source: "command",
17942
18534
  command: input.command,
@@ -17952,6 +18544,19 @@ async function writeClaudeMarketplace(input) {
17952
18544
  input.cacheDirectory,
17953
18545
  input.projectId
17954
18546
  );
18547
+ const binding = {
18548
+ projectId: input.projectId,
18549
+ origin: originOf4(input.origin)
18550
+ };
18551
+ await assertLocationFree({
18552
+ directory,
18553
+ location: "claude-marketplace",
18554
+ wanted: binding
18555
+ });
18556
+ await writeAtomically(
18557
+ path31.join(directory, BINDING_FILE),
18558
+ bindingText(binding)
18559
+ );
17955
18560
  await writeAtomically(
17956
18561
  path31.join(directory, ".claude-plugin", "marketplace.json"),
17957
18562
  `${JSON.stringify(claudeMarketplace(input), null, 2)}
@@ -17998,7 +18603,7 @@ var CODEX_MARKETPLACE_RELATIVE = path31.join(
17998
18603
  "marketplace.json"
17999
18604
  );
18000
18605
  var CODEX_MARKETPLACE_NAME = "personal";
18001
- function isRecord6(value) {
18606
+ function isRecord7(value) {
18002
18607
  return typeof value === "object" && value !== null && !Array.isArray(value);
18003
18608
  }
18004
18609
  function withCodexPlugin(marketplace, input) {
@@ -18016,7 +18621,7 @@ function withCodexPlugin(marketplace, input) {
18016
18621
  async function readCodexMarketplace(file) {
18017
18622
  let raw;
18018
18623
  try {
18019
- raw = await readFile10(file, "utf8");
18624
+ raw = await readFile9(file, "utf8");
18020
18625
  } catch (error) {
18021
18626
  if (error.code === "ENOENT") {
18022
18627
  return {
@@ -18039,7 +18644,7 @@ async function readCodexMarketplace(file) {
18039
18644
  7
18040
18645
  );
18041
18646
  }
18042
- if (!isRecord6(parsed) || typeof parsed.name !== "string" || !Array.isArray(parsed.plugins) || !parsed.plugins.every(isRecord6)) {
18647
+ if (!isRecord7(parsed) || typeof parsed.name !== "string" || !Array.isArray(parsed.plugins) || !parsed.plugins.every(isRecord7)) {
18043
18648
  throw new CliFailure(
18044
18649
  `${file} does not look like a plugin marketplace (a "name" and a "plugins" list). It was left untouched.`,
18045
18650
  7
@@ -18068,7 +18673,7 @@ async function registerCodexPlugin(input) {
18068
18673
  });
18069
18674
  const before = JSON.stringify(marketplace);
18070
18675
  const changed = JSON.stringify(next) !== before;
18071
- if (changed || !await readFile10(file).then(
18676
+ if (changed || !await readFile9(file).then(
18072
18677
  () => true,
18073
18678
  () => false
18074
18679
  )) {
@@ -18081,13 +18686,24 @@ async function registerCodexPlugin(input) {
18081
18686
  changed
18082
18687
  };
18083
18688
  }
18689
+ async function codexMarketplaceListing(input) {
18690
+ try {
18691
+ const { marketplace, existed } = await readCodexMarketplace(
18692
+ path31.join(input.home, CODEX_MARKETPLACE_RELATIVE)
18693
+ );
18694
+ const name = pluginNameFor(input.projectId);
18695
+ return existed && marketplace.plugins.some((plugin) => plugin.name === name) ? marketplace.name : null;
18696
+ } catch {
18697
+ return null;
18698
+ }
18699
+ }
18084
18700
  async function writeAgentsOverride(input) {
18085
18701
  const file = path31.join(input.root, "AGENTS.override.md");
18086
- const existing = await readFile10(file, "utf8").catch(() => null);
18702
+ const existing = await readFile9(file, "utf8").catch(() => null);
18087
18703
  if (existing !== null && !isFabricAgentsOverride(existing)) {
18088
18704
  return { kind: "left-alone", file };
18089
18705
  }
18090
- const tracked = await readFile10(
18706
+ const tracked = await readFile9(
18091
18707
  path31.join(input.root, "AGENTS.md"),
18092
18708
  "utf8"
18093
18709
  ).catch(() => null);
@@ -18102,31 +18718,467 @@ async function writeAgentsOverride(input) {
18102
18718
  return { kind: "written", file };
18103
18719
  }
18104
18720
 
18105
- // src/commands/instructions/render.ts
18106
- import os2 from "os";
18107
- import path34 from "path";
18721
+ // src/lib/plugin/codex-install.ts
18722
+ var INSTALL_TIMEOUT_MS = 12e4;
18723
+ var REFRESH_TIMEOUT_MS = 25e3;
18724
+ function codexPluginReference(projectId, marketplaceName) {
18725
+ return `${pluginNameFor(projectId)}@${marketplaceName}`;
18726
+ }
18727
+ async function installCodexPlugin(input) {
18728
+ const args = [
18729
+ "plugin",
18730
+ "add",
18731
+ codexPluginReference(input.projectId, input.marketplaceName)
18732
+ ];
18733
+ const result = await input.run("codex", args, {
18734
+ cwd: input.cwd,
18735
+ timeoutMs: INSTALL_TIMEOUT_MS
18736
+ });
18737
+ return result.kind === "exited" && result.code === 0 ? { kind: "installed" } : { kind: "manual", line: pasteableLine(["codex", ...args]) };
18738
+ }
18739
+ async function refreshCodexInstall(input) {
18740
+ try {
18741
+ const marketplaceName = await codexMarketplaceListing(input);
18742
+ if (marketplaceName === null) {
18743
+ return;
18744
+ }
18745
+ const installed = path32.join(
18746
+ input.codexHome,
18747
+ "plugins",
18748
+ "cache",
18749
+ marketplaceName,
18750
+ pluginNameFor(input.projectId),
18751
+ input.version
18752
+ );
18753
+ if (await stat7(installed).then(
18754
+ (found) => found.isDirectory(),
18755
+ () => false
18756
+ )) {
18757
+ return;
18758
+ }
18759
+ await input.run(
18760
+ "codex",
18761
+ [
18762
+ "plugin",
18763
+ "add",
18764
+ codexPluginReference(input.projectId, marketplaceName)
18765
+ ],
18766
+ { cwd: input.cwd, timeoutMs: REFRESH_TIMEOUT_MS }
18767
+ );
18768
+ } catch {
18769
+ }
18770
+ }
18771
+
18772
+ // src/lib/plugin/gateway.ts
18773
+ var REMOVE_TIMEOUT_MS = 3e4;
18774
+ async function removeLocalClaudeGateway(input) {
18775
+ const name = serverNameFor(input.projectId);
18776
+ const inspected = await inspectServers({
18777
+ tool: "claude-code",
18778
+ origin: input.origin,
18779
+ projectId: input.projectId,
18780
+ cwd: input.cwd,
18781
+ home: input.home,
18782
+ env: input.env,
18783
+ platform: input.platform
18784
+ });
18785
+ if (inspected.state !== "read") {
18786
+ return { kind: "unchecked" };
18787
+ }
18788
+ const ours = inspected.servers.some(
18789
+ (server) => server.scope === "local" && server.name === name && server.kind === "this-project"
18790
+ );
18791
+ if (!ours) {
18792
+ return { kind: "none" };
18793
+ }
18794
+ const removed = await input.run(
18795
+ "claude",
18796
+ ["mcp", "remove", name, "--scope", "local"],
18797
+ { cwd: input.cwd, timeoutMs: REMOVE_TIMEOUT_MS }
18798
+ );
18799
+ return removed.kind === "exited" && removed.code === 0 ? { kind: "removed", name } : { kind: "failed", name };
18800
+ }
18801
+
18802
+ // src/lib/plugin/rules-sync.ts
18803
+ import { createHash as createHash15 } from "crypto";
18804
+ import { rmdir as rmdir2 } from "fs/promises";
18805
+ import path33 from "path";
18806
+
18807
+ // src/lib/plugin/rules.ts
18808
+ var RULES_MAX_FILES = 64;
18809
+ var RULES_MAX_BYTES = 256 * 1024;
18810
+ var AGENTS_RULES_LIMIT = 24e3;
18811
+ var byPath4 = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
18812
+ function selectRules(read, tool) {
18813
+ const bytesOf2 = new Map(read.files.map((file) => [file.path, file.bytes]));
18814
+ const seen = /* @__PURE__ */ new Set();
18815
+ const rules = [];
18816
+ for (const rule of [...planPluginFiles(read.listed, tool).rules].sort(
18817
+ byPath4
18818
+ )) {
18819
+ const bytes = bytesOf2.get(rule.path);
18820
+ if (bytes === void 0 || seen.has(rule.as)) {
18821
+ continue;
18822
+ }
18823
+ seen.add(rule.as);
18824
+ rules.push({ path: rule.as, source: rule.path, bytes });
18825
+ }
18826
+ return rules.sort(byPath4);
18827
+ }
18828
+ function capRules(rules) {
18829
+ const kept = [];
18830
+ let total = 0;
18831
+ for (const rule of rules) {
18832
+ if (kept.length >= RULES_MAX_FILES || total + rule.bytes.byteLength > RULES_MAX_BYTES) {
18833
+ break;
18834
+ }
18835
+ kept.push(rule);
18836
+ total += rule.bytes.byteLength;
18837
+ }
18838
+ return { kept, omitted: rules.length - kept.length };
18839
+ }
18840
+ var DECODER2 = new TextDecoder("utf-8", { fatal: false });
18841
+ function unquote(value) {
18842
+ const trimmed = value.trim();
18843
+ return /^(["']).*\1$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed;
18844
+ }
18845
+ function parseRule(text3) {
18846
+ const match = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text3);
18847
+ if (match === null) {
18848
+ return { paths: [], body: text3 };
18849
+ }
18850
+ const paths = [];
18851
+ const lines2 = (match[1] ?? "").split(/\r?\n/);
18852
+ for (let index = 0; index < lines2.length; index++) {
18853
+ const head = /^paths:\s*(.*)$/.exec(lines2[index] ?? "");
18854
+ if (head === null) {
18855
+ continue;
18856
+ }
18857
+ const inline = (head[1] ?? "").trim();
18858
+ if (inline.startsWith("[")) {
18859
+ paths.push(
18860
+ ...inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter((entry) => entry !== "")
18861
+ );
18862
+ } else if (inline !== "") {
18863
+ paths.push(unquote(inline));
18864
+ } else {
18865
+ for (const next of lines2.slice(index + 1)) {
18866
+ const item = /^\s*-\s+(.*)$/.exec(next);
18867
+ if (item === null) {
18868
+ break;
18869
+ }
18870
+ paths.push(unquote(item[1] ?? ""));
18871
+ }
18872
+ }
18873
+ break;
18874
+ }
18875
+ return { paths, body: text3.slice(match[0].length) };
18876
+ }
18877
+ function agentsRulesSection(rules) {
18878
+ if (rules.length === 0) {
18879
+ return "";
18880
+ }
18881
+ const head = "## Project rules";
18882
+ const parts = [];
18883
+ let used = head.length;
18884
+ let included = 0;
18885
+ for (const rule of rules) {
18886
+ const { paths, body } = parseRule(DECODER2.decode(rule.bytes));
18887
+ const scope = paths.length === 0 ? "" : `
18888
+ _Applies when working on: ${paths.join(", ")}_
18889
+ `;
18890
+ const block = `### Rule: ${rule.path}${scope}
18891
+ ${body.trim()}`;
18892
+ if (used + block.length + 2 > AGENTS_RULES_LIMIT) {
18893
+ break;
18894
+ }
18895
+ parts.push(block);
18896
+ used += block.length + 2;
18897
+ included++;
18898
+ }
18899
+ const omitted = rules.length - included;
18900
+ if (omitted > 0) {
18901
+ parts.push(
18902
+ `[Fabric: ${omitted} more rule ${omitted === 1 ? "file was" : "files were"} left out to keep this file short: ${rules.slice(included).map((rule) => rule.path).join(
18903
+ ", "
18904
+ )}. Load fabric-instruction-access to find their original source paths and read mapped local copies or available text through the project's Fabric MCP tools.]`
18905
+ );
18906
+ }
18907
+ return `${head}
18908
+
18909
+ ${parts.join("\n\n")}`;
18910
+ }
18911
+ function agentsInstructions(memoryText, rules) {
18912
+ const sections = [
18913
+ memoryText === null ? "" : memoryText.trimEnd(),
18914
+ agentsRulesSection(rules)
18915
+ ].filter((section) => section !== "");
18916
+ return sections.length === 0 ? null : sections.join("\n\n");
18917
+ }
18918
+
18919
+ // src/lib/plugin/rules-sync.ts
18920
+ var MARKER_NAME = ".fabric-managed.json";
18921
+ var MARKER_VERSION2 = 1;
18922
+ var MAX_MARKER_BYTES2 = 256 * 1024;
18923
+ var MAX_RULE_READ_BYTES = 2 * 1024 * 1024;
18924
+ function rulesFolderFor(projectId) {
18925
+ return `.claude/rules/${pluginNameFor(projectId)}`;
18926
+ }
18927
+ var sha256 = (bytes) => createHash15("sha256").update(bytes).digest("hex");
18928
+ async function readMarker2(root, folder) {
18929
+ try {
18930
+ const read = await readFileSafely(root, `${folder}/${MARKER_NAME}`, {
18931
+ maxBytes: MAX_MARKER_BYTES2
18932
+ });
18933
+ if (read === null) {
18934
+ return {};
18935
+ }
18936
+ const parsed = JSON.parse(
18937
+ new TextDecoder().decode(read.bytes)
18938
+ );
18939
+ const files = parsed?.files;
18940
+ if (parsed?.version !== MARKER_VERSION2 || typeof files !== "object" || files === null || Array.isArray(files)) {
18941
+ return {};
18942
+ }
18943
+ const managed = {};
18944
+ for (const [file, hash] of Object.entries(files)) {
18945
+ if (typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash) && checkRelativePath(file).ok) {
18946
+ managed[file] = hash;
18947
+ }
18948
+ }
18949
+ return managed;
18950
+ } catch {
18951
+ return {};
18952
+ }
18953
+ }
18954
+ async function writeMarker(root, folder, managed) {
18955
+ const sorted = Object.fromEntries(
18956
+ Object.entries(managed).sort(
18957
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
18958
+ )
18959
+ );
18960
+ await writeFileSafely({
18961
+ root,
18962
+ relativePath: `${folder}/${MARKER_NAME}`,
18963
+ bytes: new TextEncoder().encode(
18964
+ `${JSON.stringify({ version: MARKER_VERSION2, files: sorted }, null, 2)}
18965
+ `
18966
+ ),
18967
+ mode: 420
18968
+ });
18969
+ }
18970
+ async function removeMarker(root, folder) {
18971
+ const relative = `${folder}/${MARKER_NAME}`;
18972
+ const current = await readFileSafely(root, relative, {
18973
+ maxBytes: MAX_MARKER_BYTES2
18974
+ });
18975
+ if (current !== null) {
18976
+ await deleteFileSafely(root, relative, sha256(current.bytes));
18977
+ }
18978
+ await pruneEmpty(root, folder, MARKER_NAME);
18979
+ }
18980
+ async function pruneEmpty(root, folder, file) {
18981
+ const parts = file.split("/").slice(0, -1);
18982
+ for (let depth = parts.length; depth >= 0; depth--) {
18983
+ const relative = [folder, ...parts.slice(0, depth)].join("/");
18984
+ try {
18985
+ await rmdir2(path33.join(root, relative));
18986
+ } catch {
18987
+ return;
18988
+ }
18989
+ }
18990
+ }
18991
+ async function syncProjectRules(input) {
18992
+ const folder = rulesFolderFor(input.projectId);
18993
+ const { kept: wanted, omitted } = capRules(input.rules);
18994
+ const result = {
18995
+ written: [],
18996
+ removed: [],
18997
+ unchanged: 0,
18998
+ kept: [],
18999
+ skipped: [],
19000
+ omitted,
19001
+ timedOut: false,
19002
+ refused: null
19003
+ };
19004
+ const folderCheck = await assertWritableTarget(
19005
+ input.root,
19006
+ `${folder}/${MARKER_NAME}`
19007
+ );
19008
+ if (!folderCheck.ok) {
19009
+ result.refused = `${folder} cannot be used (${folderCheck.reason}: ${folderCheck.detail})`;
19010
+ return result;
19011
+ }
19012
+ const previous = await readMarker2(input.root, folder);
19013
+ const next = {};
19014
+ const expired = () => input.deadline !== void 0 && Date.now() > input.deadline;
19015
+ const handled = /* @__PURE__ */ new Set();
19016
+ for (const rule of wanted) {
19017
+ const relative = rule.path;
19018
+ if (!checkRelativePath(relative).ok || relative === MARKER_NAME) {
19019
+ result.skipped.push({ path: relative, reason: "unsafe-path" });
19020
+ continue;
19021
+ }
19022
+ handled.add(relative);
19023
+ if (input.tracked?.has(relative)) {
19024
+ result.skipped.push({ path: relative, reason: "tracked" });
19025
+ continue;
19026
+ }
19027
+ if (expired()) {
19028
+ result.timedOut = true;
19029
+ if (previous[relative] !== void 0) {
19030
+ next[relative] = previous[relative];
19031
+ }
19032
+ continue;
19033
+ }
19034
+ const target = `${folder}/${relative}`;
19035
+ const wantedHash = sha256(rule.bytes);
19036
+ try {
19037
+ const check = await assertWritableTarget(input.root, target);
19038
+ if (!check.ok) {
19039
+ result.kept.push({ path: relative, reason: "unsafe" });
19040
+ continue;
19041
+ }
19042
+ const existing = await readFileSafely(input.root, target, {
19043
+ maxBytes: MAX_RULE_READ_BYTES
19044
+ }).catch(() => "unreadable");
19045
+ if (existing === "unreadable") {
19046
+ result.kept.push({ path: relative, reason: "unsafe" });
19047
+ continue;
19048
+ }
19049
+ if (existing === null) {
19050
+ await writeFileSafely({
19051
+ root: input.root,
19052
+ relativePath: target,
19053
+ bytes: rule.bytes,
19054
+ mode: 420
19055
+ });
19056
+ result.written.push(relative);
19057
+ next[relative] = wantedHash;
19058
+ continue;
19059
+ }
19060
+ const currentHash = sha256(existing.bytes);
19061
+ if (currentHash === wantedHash) {
19062
+ result.unchanged++;
19063
+ next[relative] = wantedHash;
19064
+ } else if (previous[relative] === currentHash) {
19065
+ await writeFileSafely({
19066
+ root: input.root,
19067
+ relativePath: target,
19068
+ bytes: rule.bytes,
19069
+ mode: 420
19070
+ });
19071
+ result.written.push(relative);
19072
+ next[relative] = wantedHash;
19073
+ } else {
19074
+ result.kept.push({
19075
+ path: relative,
19076
+ reason: previous[relative] === void 0 ? "not-managed" : "edited"
19077
+ });
19078
+ }
19079
+ } catch {
19080
+ result.kept.push({ path: relative, reason: "unsafe" });
19081
+ }
19082
+ }
19083
+ for (const [relative, hash] of Object.entries(previous)) {
19084
+ if (handled.has(relative)) {
19085
+ continue;
19086
+ }
19087
+ if (expired()) {
19088
+ result.timedOut = true;
19089
+ next[relative] = hash;
19090
+ continue;
19091
+ }
19092
+ try {
19093
+ const outcome = await deleteFileSafely(
19094
+ input.root,
19095
+ `${folder}/${relative}`,
19096
+ hash
19097
+ );
19098
+ if (outcome === "modified") {
19099
+ result.kept.push({ path: relative, reason: "edited" });
19100
+ } else {
19101
+ if (outcome === "deleted") {
19102
+ result.removed.push(relative);
19103
+ }
19104
+ await pruneEmpty(input.root, folder, relative);
19105
+ }
19106
+ } catch {
19107
+ result.kept.push({ path: relative, reason: "unsafe" });
19108
+ }
19109
+ }
19110
+ const changed = JSON.stringify(Object.entries(next).sort()) !== JSON.stringify(Object.entries(previous).sort());
19111
+ try {
19112
+ if (Object.keys(next).length > 0) {
19113
+ if (changed) {
19114
+ await writeMarker(input.root, folder, next);
19115
+ }
19116
+ } else if (Object.keys(previous).length > 0) {
19117
+ await removeMarker(input.root, folder);
19118
+ }
19119
+ } catch {
19120
+ result.refused = `${folder}/${MARKER_NAME} could not be updated`;
19121
+ }
19122
+ return result;
19123
+ }
19124
+ function rulesSyncLines(result) {
19125
+ const lines2 = [];
19126
+ if (result.refused !== null) {
19127
+ lines2.push(`rules not synced: ${result.refused}.`);
19128
+ }
19129
+ if (result.written.length > 0 || result.removed.length > 0) {
19130
+ lines2.push(
19131
+ `rules: ${result.written.length} written, ${result.removed.length} removed.`
19132
+ );
19133
+ }
19134
+ for (const entry of result.kept) {
19135
+ lines2.push(
19136
+ entry.reason === "edited" ? `rules: kept ${entry.path}, which you edited; it is no longer updated from Fabric.` : entry.reason === "not-managed" ? `rules: kept ${entry.path}, which is your own file at that path.` : `rules: left ${entry.path} alone because it could not be written safely.`
19137
+ );
19138
+ }
19139
+ for (const entry of result.skipped) {
19140
+ lines2.push(
19141
+ entry.reason === "tracked" ? `rules: skipped ${entry.path}, which this project's own git tracks.` : `rules: skipped ${entry.path}: not a path Fabric will write.`
19142
+ );
19143
+ }
19144
+ if (result.omitted > 0) {
19145
+ lines2.push(
19146
+ `rules: ${result.omitted} more rule files were not delivered automatically (limit: 64 files or 256 KiB). Load fabric-instruction-access to find their original paths and read mapped local copies or available text through the project's Fabric MCP tools.`
19147
+ );
19148
+ }
19149
+ if (result.timedOut) {
19150
+ lines2.push(
19151
+ "rules: ran out of time; the rest follow at the next session."
19152
+ );
19153
+ }
19154
+ return lines2;
19155
+ }
19156
+
19157
+ // src/commands/instructions/render.ts
19158
+ import os2 from "os";
19159
+ import path36 from "path";
18108
19160
 
18109
19161
  // src/lib/plugin/render.ts
18110
- import { createHash as createHash17, randomBytes as randomBytes4 } from "crypto";
19162
+ import { createHash as createHash18, randomBytes as randomBytes4 } from "crypto";
18111
19163
  import {
18112
19164
  chmod as chmod2,
18113
19165
  mkdir as mkdir13,
18114
19166
  readdir as readdir3,
18115
- readFile as readFile12,
19167
+ readFile as readFile11,
18116
19168
  rename as rename7,
18117
19169
  rm as rm7,
18118
19170
  stat as stat8,
18119
19171
  writeFile as writeFile9
18120
19172
  } from "fs/promises";
18121
- import path33 from "path";
19173
+ import path35 from "path";
18122
19174
 
18123
19175
  // src/lib/plugin/source.ts
18124
- import { createHash as createHash16 } from "crypto";
18125
- import { mkdir as mkdir12, readFile as readFile11, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
18126
- import path32 from "path";
19176
+ import { createHash as createHash17 } from "crypto";
19177
+ import { mkdir as mkdir12, readFile as readFile10, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
19178
+ import path34 from "path";
18127
19179
 
18128
19180
  // src/lib/git-blob.ts
18129
- import { createHash as createHash15 } from "crypto";
19181
+ import { createHash as createHash16 } from "crypto";
18130
19182
  function objectFormatOf(oid) {
18131
19183
  if (/^[0-9a-f]{40}$/.test(oid)) {
18132
19184
  return "sha1";
@@ -18137,7 +19189,7 @@ function objectFormatOf(oid) {
18137
19189
  return null;
18138
19190
  }
18139
19191
  function gitBlobOid(bytes, format = "sha1") {
18140
- return createHash15(format).update(`blob ${bytes.byteLength}\0`).update(bytes).digest("hex");
19192
+ return createHash16(format).update(`blob ${bytes.byteLength}\0`).update(bytes).digest("hex");
18141
19193
  }
18142
19194
  function matchesBlobOid(bytes, oid) {
18143
19195
  const format = objectFormatOf(oid);
@@ -18145,18 +19197,18 @@ function matchesBlobOid(bytes, oid) {
18145
19197
  }
18146
19198
 
18147
19199
  // src/lib/plugin/source.ts
18148
- var READ_CONCURRENCY = 6;
18149
- var MAX_PAGES_PER_FILE = 64;
19200
+ var READ_BATCH_SIZE = 200;
19201
+ var MAX_INLINE_BYTES = 256 * 1024;
18150
19202
  var ALWAYS_ON = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md"]);
18151
19203
  function isWantedPath(instructionPath) {
18152
- return ALWAYS_ON.has(instructionPath) || pluginPathFor(instructionPath) !== null;
19204
+ return ALWAYS_ON.has(instructionPath) || pluginPathFor(instructionPath) !== null || projectRulePathFor(instructionPath) !== null;
18153
19205
  }
18154
19206
  function blobPath(cacheDirectory, key) {
18155
- return path32.join(cacheDirectory, "blobs", key);
19207
+ return path34.join(cacheDirectory, "blobs", key);
18156
19208
  }
18157
19209
  async function cached(cacheDirectory, key, valid) {
18158
19210
  try {
18159
- const bytes = await readFile11(blobPath(cacheDirectory, key));
19211
+ const bytes = await readFile10(blobPath(cacheDirectory, key));
18160
19212
  return valid(bytes) ? bytes : null;
18161
19213
  } catch {
18162
19214
  return null;
@@ -18166,7 +19218,7 @@ async function remember(cacheDirectory, key, bytes) {
18166
19218
  const file = blobPath(cacheDirectory, key);
18167
19219
  const temporary = `${file}.${process.pid}.tmp`;
18168
19220
  try {
18169
- await mkdir12(path32.dirname(file), { recursive: true });
19221
+ await mkdir12(path34.dirname(file), { recursive: true });
18170
19222
  await writeFile8(temporary, bytes);
18171
19223
  await rename6(temporary, file);
18172
19224
  } catch {
@@ -18174,38 +19226,13 @@ async function remember(cacheDirectory, key, bytes) {
18174
19226
  }
18175
19227
  }
18176
19228
  function sha256Of2(bytes) {
18177
- return createHash16("sha256").update(bytes).digest("hex");
18178
- }
18179
- async function inBatches(items, work) {
18180
- const queue = items.values();
18181
- const failures = [];
18182
- const worker = async () => {
18183
- for (; ; ) {
18184
- const next = queue.next();
18185
- if (next.done || failures.length > 0) {
18186
- return;
18187
- }
18188
- try {
18189
- await work(next.value);
18190
- } catch (error) {
18191
- failures.push(error);
18192
- }
18193
- }
18194
- };
18195
- await Promise.all(
18196
- Array.from(
18197
- { length: Math.min(READ_CONCURRENCY, items.length) },
18198
- worker
18199
- )
18200
- );
18201
- if (failures.length > 0) {
18202
- throw failures[0];
18203
- }
19229
+ return createHash17("sha256").update(bytes).digest("hex");
18204
19230
  }
18205
19231
  async function readDirect(input) {
18206
19232
  const listing = await input.client.instructions.listRepositoryFiles(
18207
19233
  input.projectId,
18208
- { org: input.org }
19234
+ { org: input.org },
19235
+ { signal: input.signal }
18209
19236
  );
18210
19237
  if (listing.incomplete || listing.refusal !== null) {
18211
19238
  throw new Error(
@@ -18216,59 +19243,104 @@ async function readDirect(input) {
18216
19243
  generation: listing.generation,
18217
19244
  commitSha: listing.commitSha
18218
19245
  };
19246
+ const listed = listing.files.map((file) => ({
19247
+ path: file.path,
19248
+ size: file.size,
19249
+ kind: file.kind,
19250
+ blobId: file.blobId,
19251
+ mode: file.mode
19252
+ }));
18219
19253
  const wanted = listing.files.filter((file) => isWantedPath(file.path));
18220
19254
  const files = [];
18221
19255
  const skipped3 = [];
18222
- const readOne = async (file) => {
19256
+ const missing = [];
19257
+ if (listing.files.some((file) => !checkRelativePath(file.path).ok) || findCollision(listing.files.map((file) => file.path)) !== null) {
19258
+ throw new Error("The repository returned unsafe or colliding paths.");
19259
+ }
19260
+ for (const file of wanted) {
19261
+ input.signal?.throwIfAborted();
18223
19262
  const blobId = file.blobId;
18224
- if (blobId !== void 0) {
18225
- const hit = await cached(
18226
- input.cacheDirectory,
18227
- `git-${blobId}`,
18228
- (b) => matchesBlobOid(b, blobId)
19263
+ if (blobId === void 0 || objectFormatOf(blobId) === null) {
19264
+ throw new Error(
19265
+ `The repository did not identify the Git blob for ${file.path}.`
18229
19266
  );
18230
- if (hit !== null) {
18231
- files.push({
18232
- path: file.path,
18233
- bytes: hit,
18234
- mode: modeOf(file.mode)
18235
- });
18236
- return;
18237
- }
18238
19267
  }
18239
- const chunks = [];
18240
- let offset = 0;
18241
- for (let page = 0; page < MAX_PAGES_PER_FILE; page++) {
18242
- const read = await input.client.instructions.getRepositoryFile(
19268
+ const hit = await cached(
19269
+ input.cacheDirectory,
19270
+ `git-${blobId}`,
19271
+ (b) => b.byteLength <= MAX_INLINE_BYTES && matchesBlobOid(b, blobId)
19272
+ );
19273
+ if (hit !== null) {
19274
+ files.push({
19275
+ path: file.path,
19276
+ bytes: hit,
19277
+ mode: modeOf(file.mode)
19278
+ });
19279
+ continue;
19280
+ }
19281
+ missing.push(file);
19282
+ }
19283
+ for (let offset = 0; offset < missing.length; offset += READ_BATCH_SIZE) {
19284
+ const batch = missing.slice(offset, offset + READ_BATCH_SIZE);
19285
+ const paths = batch.map((file) => file.path);
19286
+ let startIndex = 0;
19287
+ for (; ; ) {
19288
+ input.signal?.throwIfAborted();
19289
+ const page = await input.client.instructions.readRepositoryFiles(
18243
19290
  input.projectId,
18244
- { ...pin, path: file.path, offset, org: input.org }
19291
+ { ...pin, paths, startIndex },
19292
+ { org: input.org, signal: input.signal }
18245
19293
  );
18246
- if (read.state !== "found") {
18247
- skipped3.push(file.path);
18248
- return;
19294
+ input.signal?.throwIfAborted();
19295
+ const end = page.nextIndex === null ? paths.length : page.nextIndex;
19296
+ if (page.generation !== pin.generation || page.commitSha !== pin.commitSha || !Number.isInteger(end) || end <= startIndex || end > paths.length || !Array.isArray(page.files) || page.files.length !== end - startIndex) {
19297
+ throw new Error(
19298
+ "The repository returned an invalid pinned batch."
19299
+ );
18249
19300
  }
18250
- chunks.push(read.body);
18251
- if (read.nextOffset === null) {
18252
- const bytes = new TextEncoder().encode(chunks.join(""));
18253
- if (blobId !== void 0 && matchesBlobOid(bytes, blobId)) {
18254
- await remember(
18255
- input.cacheDirectory,
18256
- `git-${blobId}`,
18257
- bytes
19301
+ for (const [index, result] of page.files.entries()) {
19302
+ const file = batch[startIndex + index];
19303
+ if (file === void 0 || result.path !== file.path) {
19304
+ throw new Error(
19305
+ "The repository returned unexpected or missing paths."
19306
+ );
19307
+ }
19308
+ const read = result.read;
19309
+ if (read.state === "binary" || read.state === "tooLarge") {
19310
+ skipped3.push(file.path);
19311
+ continue;
19312
+ }
19313
+ if (read.state !== "found" || typeof read.text !== "string") {
19314
+ throw new Error(
19315
+ `The repository could not read ${file.path}.`
19316
+ );
19317
+ }
19318
+ const bytes = new TextEncoder().encode(read.text);
19319
+ if (bytes.byteLength !== read.size || bytes.byteLength > MAX_INLINE_BYTES || file.blobId === void 0 || !matchesBlobOid(bytes, file.blobId)) {
19320
+ throw new Error(
19321
+ `The repository returned different bytes for ${file.path}.`
18258
19322
  );
18259
19323
  }
19324
+ await remember(
19325
+ input.cacheDirectory,
19326
+ `git-${file.blobId}`,
19327
+ bytes
19328
+ );
18260
19329
  files.push({ path: file.path, bytes, mode: modeOf(file.mode) });
18261
- return;
18262
19330
  }
18263
- offset = read.nextOffset;
19331
+ if (page.nextIndex === null) {
19332
+ break;
19333
+ }
19334
+ startIndex = end;
18264
19335
  }
18265
- throw new Error(`${file.path} is too long to read.`);
18266
- };
18267
- await inBatches(wanted, readOne);
19336
+ }
19337
+ input.signal?.throwIfAborted();
18268
19338
  return {
18269
19339
  pin: `repo:${pin.generation}:${pin.commitSha}`,
19340
+ source: { kind: "repository", ...pin },
18270
19341
  files,
18271
- skipped: skipped3.sort()
19342
+ skipped: skipped3.sort(),
19343
+ listed
18272
19344
  };
18273
19345
  }
18274
19346
  function modeOf(mode) {
@@ -18281,9 +19353,22 @@ function modeOf(mode) {
18281
19353
  async function readUploaded(input) {
18282
19354
  const { published } = input;
18283
19355
  if (!published.published || published.snapshot === void 0) {
18284
- return { pin: "upload:none", files: [], skipped: [] };
19356
+ return {
19357
+ pin: "upload:none",
19358
+ source: { kind: "upload", digest: null },
19359
+ files: [],
19360
+ skipped: [],
19361
+ listed: []
19362
+ };
18285
19363
  }
18286
19364
  const digest = published.snapshot.digest;
19365
+ const listed = (published.manifest ?? []).map((entry) => ({
19366
+ path: entry.path,
19367
+ size: entry.size,
19368
+ kind: entry.kind,
19369
+ sha256: entry.sha256,
19370
+ mode: entry.mode
19371
+ }));
18287
19372
  const manifest = (published.manifest ?? []).filter(
18288
19373
  (entry) => isWantedPath(entry.path)
18289
19374
  );
@@ -18302,7 +19387,7 @@ async function readUploaded(input) {
18302
19387
  }
18303
19388
  }
18304
19389
  if (missing.length > 0) {
18305
- const byPath3 = new Map(missing.map((entry) => [entry.path, entry]));
19390
+ const byPath5 = new Map(missing.map((entry) => [entry.path, entry]));
18306
19391
  await downloadFilesByUrl(
18307
19392
  {
18308
19393
  client: input.client,
@@ -18323,12 +19408,18 @@ async function readUploaded(input) {
18323
19408
  files.push({
18324
19409
  path: file.path,
18325
19410
  bytes,
18326
- mode: byPath3.get(file.path)?.mode ?? null
19411
+ mode: byPath5.get(file.path)?.mode ?? null
18327
19412
  });
18328
19413
  }
18329
19414
  );
18330
19415
  }
18331
- return { pin: `upload:${digest}`, files, skipped: [] };
19416
+ return {
19417
+ pin: `upload:${digest}`,
19418
+ source: { kind: "upload", digest },
19419
+ files,
19420
+ skipped: [],
19421
+ listed
19422
+ };
18332
19423
  }
18333
19424
  async function readInstructions(input) {
18334
19425
  const direct = input.published.direct;
@@ -18346,6 +19437,16 @@ async function readInstructions(input) {
18346
19437
  // src/lib/plugin/render.ts
18347
19438
  var KEPT_PINS = 3;
18348
19439
  var CODEX_PIN_MARKER = ".fabric-render";
19440
+ var LEFT_OUT_NAMED = 3;
19441
+ function leftOutLine(paths) {
19442
+ if (paths.length === 0) {
19443
+ return null;
19444
+ }
19445
+ const named = paths.slice(0, LEFT_OUT_NAMED).join(", ");
19446
+ const more = paths.length > LEFT_OUT_NAMED ? ` and ${paths.length - LEFT_OUT_NAMED} more` : "";
19447
+ const count = paths.length === 1 ? "1 instruction file is" : `${paths.length} instruction files are`;
19448
+ return `${count} not part of the plugin (${named}${more}). Their paths remain in the fabric-instruction-access skill's catalog; use the project's Fabric MCP tools to read available text on demand. The Coding Instructions tab lists the layout reasons.`;
19449
+ }
18349
19450
  function memoryFoldersOf(targets) {
18350
19451
  return {
18351
19452
  state: "known",
@@ -18369,7 +19470,7 @@ async function readMemoryFolders(input) {
18369
19470
  }
18370
19471
  }
18371
19472
  function pinOf(sourcePin, memory, tool) {
18372
- return createHash17("sha256").update(
19473
+ return createHash18("sha256").update(
18373
19474
  JSON.stringify({
18374
19475
  v: RENDER_VERSION,
18375
19476
  tool,
@@ -18386,8 +19487,8 @@ async function exists(file) {
18386
19487
  }
18387
19488
  async function writeTree(directory, files) {
18388
19489
  for (const file of files) {
18389
- const target = path33.join(directory, ...file.path.split("/"));
18390
- await mkdir13(path33.dirname(target), { recursive: true });
19490
+ const target = path35.join(directory, ...file.path.split("/"));
19491
+ await mkdir13(path35.dirname(target), { recursive: true });
18391
19492
  await writeFile9(target, file.bytes);
18392
19493
  await chmod2(target, file.mode).catch(() => void 0);
18393
19494
  }
@@ -18403,12 +19504,12 @@ async function prune(projectDirectory, keep) {
18403
19504
  const dated = await Promise.all(
18404
19505
  entries.filter((entry) => entry.isDirectory() && entry.name !== keep).map(async (entry) => ({
18405
19506
  name: entry.name,
18406
- modified: (await stat8(path33.join(projectDirectory, entry.name))).mtimeMs
19507
+ modified: (await stat8(path35.join(projectDirectory, entry.name))).mtimeMs
18407
19508
  }))
18408
19509
  );
18409
19510
  dated.sort((a, b) => b.modified - a.modified);
18410
19511
  for (const old of dated.slice(KEPT_PINS - 1)) {
18411
- await rm7(path33.join(projectDirectory, old.name), {
19512
+ await rm7(path35.join(projectDirectory, old.name), {
18412
19513
  recursive: true,
18413
19514
  force: true
18414
19515
  }).catch(() => void 0);
@@ -18417,8 +19518,8 @@ async function prune(projectDirectory, keep) {
18417
19518
  }
18418
19519
  }
18419
19520
  async function installClaude(projectDirectory, pin, files) {
18420
- const directory = path33.join(projectDirectory, pin);
18421
- if (await exists(path33.join(directory, ".claude-plugin", "plugin.json"))) {
19521
+ const directory = path35.join(projectDirectory, pin);
19522
+ if (await exists(path35.join(directory, ".claude-plugin", "plugin.json"))) {
18422
19523
  return { directory, reused: true };
18423
19524
  }
18424
19525
  await mkdir13(projectDirectory, { recursive: true });
@@ -18431,7 +19532,7 @@ async function installClaude(projectDirectory, pin, files) {
18431
19532
  await rm7(temporary, { recursive: true, force: true }).catch(
18432
19533
  () => void 0
18433
19534
  );
18434
- if (await exists(path33.join(directory, ".claude-plugin", "plugin.json"))) {
19535
+ if (await exists(path35.join(directory, ".claude-plugin", "plugin.json"))) {
18435
19536
  return { directory, reused: true };
18436
19537
  }
18437
19538
  throw error;
@@ -18439,20 +19540,30 @@ async function installClaude(projectDirectory, pin, files) {
18439
19540
  await prune(projectDirectory, pin);
18440
19541
  return { directory, reused: false };
18441
19542
  }
18442
- async function installCodex(directory, pin, files) {
18443
- const marker = await readFile12(
18444
- path33.join(directory, CODEX_PIN_MARKER),
19543
+ async function installCodex(directory, pin, files, binding) {
19544
+ await assertLocationFree({
19545
+ directory,
19546
+ location: "codex-plugin",
19547
+ wanted: binding
19548
+ });
19549
+ const marker = await readFile11(
19550
+ path35.join(directory, CODEX_PIN_MARKER),
18445
19551
  "utf8"
18446
19552
  ).catch(() => null);
18447
19553
  if (marker?.trim() === pin) {
18448
19554
  return { directory, reused: true };
18449
19555
  }
18450
- await mkdir13(path33.dirname(directory), { recursive: true });
19556
+ await mkdir13(path35.dirname(directory), { recursive: true });
18451
19557
  const temporary = temporaryNameFor(directory);
18452
19558
  const retired = temporaryNameFor(directory).replace(".tmp-", ".old-");
18453
19559
  try {
18454
19560
  await writeTree(temporary, [
18455
19561
  ...files,
19562
+ {
19563
+ path: BINDING_FILE,
19564
+ bytes: new TextEncoder().encode(bindingText(binding)),
19565
+ mode: 420
19566
+ },
18456
19567
  {
18457
19568
  path: CODEX_PIN_MARKER,
18458
19569
  bytes: new TextEncoder().encode(`${pin}
@@ -18493,7 +19604,15 @@ async function renderPlugin(input) {
18493
19604
  fetchImpl: input.fetchImpl
18494
19605
  });
18495
19606
  for (const skipped3 of read.skipped) {
18496
- input.log?.(`${skipped3} is not text, so the plugin leaves it out.`);
19607
+ input.log?.(
19608
+ `${skipped3} is unavailable as inline text, so the plugin leaves its bytes out. Its path remains in the fabric-instruction-access catalog; MCP reads may report a binary or size refusal.`
19609
+ );
19610
+ }
19611
+ const notPartOfPlugin = leftOutLine(
19612
+ planPluginFiles(read.listed, input.tool).leftOut.filter((entry) => entry.reason !== "always-on-shadowed").map((entry) => entry.path)
19613
+ );
19614
+ if (notPartOfPlugin !== null) {
19615
+ input.log?.(notPartOfPlugin);
18497
19616
  }
18498
19617
  const memory = await readMemoryFolders(input);
18499
19618
  const pin = pinOf(read.pin, memory, input.tool);
@@ -18503,25 +19622,39 @@ async function renderPlugin(input) {
18503
19622
  files: read.files,
18504
19623
  memory,
18505
19624
  gatewayUrl: projectResource(input.origin, "mcp", input.projectId),
18506
- pin
19625
+ pin,
19626
+ instructionAccess: { listed: read.listed, source: read.source }
18507
19627
  });
18508
19628
  const always = alwaysOnFile(read.files, input.tool);
19629
+ const guidance = instructionAccessBootstrap(
19630
+ pluginNameFor(input.projectId),
19631
+ input.tool
19632
+ );
18509
19633
  const installed = input.tool === "claude-code" ? await installClaude(
18510
- path33.join(input.cacheDirectory, "plugins", input.projectId),
19634
+ path35.join(input.cacheDirectory, "plugins", input.projectId),
18511
19635
  pin,
18512
19636
  files
18513
19637
  ) : await installCodex(
18514
- path33.join(
19638
+ path35.join(
18515
19639
  input.codexPluginsDirectory,
18516
19640
  pluginNameFor(input.projectId)
18517
19641
  ),
18518
19642
  pin,
18519
- files
19643
+ files,
19644
+ {
19645
+ projectId: input.projectId,
19646
+ origin: originOf4(input.origin)
19647
+ }
18520
19648
  );
18521
19649
  return {
18522
19650
  ...installed,
18523
19651
  pin,
18524
- alwaysOn: always === null ? null : { file: always.file, text: always.text },
19652
+ alwaysOn: {
19653
+ file: always?.file ?? "Fabric instruction access",
19654
+ text: `${guidance}${always === null ? "" : `
19655
+ ${always.text}`}`
19656
+ },
19657
+ rules: selectRules(read, input.tool),
18525
19658
  files
18526
19659
  };
18527
19660
  }
@@ -18537,7 +19670,7 @@ function toolOf(target) {
18537
19670
  }
18538
19671
  throw new CliFailure("--target is claude-code or codex", 2);
18539
19672
  }
18540
- async function renderForProject(input) {
19673
+ function connectForProject(input) {
18541
19674
  const origin = normalizeOrigin(
18542
19675
  input.baseUrl ?? getBaseUrl() ?? DEFAULT_ORIGIN
18543
19676
  );
@@ -18553,20 +19686,26 @@ async function renderForProject(input) {
18553
19686
  3
18554
19687
  );
18555
19688
  }
18556
- const client = getClient({
18557
- baseUrl: input.baseUrl,
18558
- project: input.project,
18559
- timeoutMs: REQUEST_TIMEOUT_MS
18560
- }).withoutContext();
18561
- const cacheDirectory = path34.join(path34.dirname(getConfigPath()), "cache");
19689
+ return {
19690
+ origin,
19691
+ client: getClient({
19692
+ baseUrl: input.baseUrl,
19693
+ project: input.project,
19694
+ timeoutMs: REQUEST_TIMEOUT_MS
19695
+ }).withoutContext(),
19696
+ cacheDirectory: path36.join(path36.dirname(getConfigPath()), "cache")
19697
+ };
19698
+ }
19699
+ async function renderForProject(input) {
19700
+ const { origin, client, cacheDirectory } = connectForProject(input);
18562
19701
  const rendered = await renderPlugin({
18563
19702
  client,
18564
19703
  projectId: input.project,
18565
19704
  org: input.org,
18566
19705
  origin,
18567
19706
  tool: input.tool,
18568
- cacheDirectory: input.out !== void 0 && input.tool === "claude-code" ? path34.resolve(input.out) : cacheDirectory,
18569
- codexPluginsDirectory: input.out !== void 0 && input.tool === "codex" ? path34.resolve(input.out) : path34.join(
19707
+ cacheDirectory: input.out !== void 0 && input.tool === "claude-code" ? path36.resolve(input.out) : cacheDirectory,
19708
+ codexPluginsDirectory: input.out !== void 0 && input.tool === "codex" ? path36.resolve(input.out) : path36.join(
18570
19709
  machine.home() ?? os2.homedir(),
18571
19710
  ".codex",
18572
19711
  "plugins"
@@ -18574,7 +19713,16 @@ async function renderForProject(input) {
18574
19713
  timeoutMs: REQUEST_TIMEOUT_MS,
18575
19714
  log: input.log
18576
19715
  });
18577
- return { directory: rendered.directory, alwaysOn: rendered.alwaysOn };
19716
+ return {
19717
+ directory: rendered.directory,
19718
+ reused: rendered.reused,
19719
+ pin: rendered.pin,
19720
+ alwaysOn: rendered.alwaysOn,
19721
+ agentsText: input.tool === "codex" ? agentsInstructions(
19722
+ rendered.alwaysOn?.text ?? null,
19723
+ rendered.rules
19724
+ ) : null
19725
+ };
18578
19726
  }
18579
19727
  async function refreshAgentsOverride(root, instructions) {
18580
19728
  const canonical = await resolveExistingRoot(root);
@@ -18621,12 +19769,26 @@ function registerRenderCommand(instructions) {
18621
19769
  log: (text3) => process.stderr.write(`fabric: ${text3}
18622
19770
  `)
18623
19771
  });
18624
- if (tool === "codex" && rendered.alwaysOn !== null) {
19772
+ if (tool === "codex" && rendered.agentsText !== null) {
18625
19773
  await refreshAgentsOverride(
18626
19774
  opts.dest ?? process.cwd(),
18627
- rendered.alwaysOn.text
19775
+ rendered.agentsText
18628
19776
  );
18629
19777
  }
19778
+ if (tool === "codex" && opts.out === void 0) {
19779
+ const env2 = machine.env();
19780
+ const home = machine.home() ?? os2.homedir();
19781
+ await refreshCodexInstall({
19782
+ run: createAgentRunner({
19783
+ lookup: { env: env2, platform: machine.platform() }
19784
+ }),
19785
+ home,
19786
+ codexHome: env2.CODEX_HOME ?? path36.join(home, ".codex"),
19787
+ projectId: opts.project,
19788
+ version: codexPluginVersion(rendered.pin),
19789
+ cwd: path36.resolve(opts.dest ?? process.cwd())
19790
+ });
19791
+ }
18630
19792
  if (!opts.hook) {
18631
19793
  process.stdout.write(`${rendered.directory}
18632
19794
  `);
@@ -18645,6 +19807,111 @@ function registerRenderCommand(instructions) {
18645
19807
  });
18646
19808
  }
18647
19809
 
19810
+ // src/commands/instructions/rules.ts
19811
+ import path37 from "path";
19812
+ var HOOK_BUDGET_MS = 4e4;
19813
+ var REQUEST_TIMEOUT_MS2 = 2e4;
19814
+ async function trackedBelow(toplevel, root, folder, deadline) {
19815
+ const prefix = path37.relative(toplevel, path37.join(root, folder)).split(path37.sep).join("/");
19816
+ const tracked = await trackedPaths(toplevel, [prefix], deadline);
19817
+ return new Set(
19818
+ tracked.kind === "ok" ? tracked.value.filter((entry) => entry.startsWith(`${prefix}/`)).map((entry) => entry.slice(prefix.length + 1)) : []
19819
+ );
19820
+ }
19821
+ async function syncRulesForProject(input) {
19822
+ const budget = input.budgetMs ?? HOOK_BUDGET_MS;
19823
+ const deadline = Date.now() + budget;
19824
+ const { client, cacheDirectory } = connectForProject(input);
19825
+ const published = await client.instructions.getPublished(input.project, {
19826
+ org: input.org
19827
+ });
19828
+ const read = await readInstructions({
19829
+ client,
19830
+ projectId: input.project,
19831
+ org: input.org,
19832
+ published,
19833
+ cacheDirectory,
19834
+ timeoutMs: REQUEST_TIMEOUT_MS2,
19835
+ signal: AbortSignal.timeout(budget)
19836
+ });
19837
+ const rules = selectRules(read, "claude-code");
19838
+ const root = await resolveExistingRoot(input.root);
19839
+ const folder = rulesFolderFor(input.project);
19840
+ const tree = await findWorkTree(root, Date.now() + 8e3);
19841
+ const toplevel = tree.kind === "ok" ? tree.value.toplevel : null;
19842
+ const result = await syncProjectRules({
19843
+ root,
19844
+ projectId: input.project,
19845
+ rules,
19846
+ tracked: toplevel === null ? void 0 : await trackedBelow(
19847
+ toplevel,
19848
+ root,
19849
+ folder,
19850
+ Date.now() + 8e3
19851
+ ),
19852
+ deadline
19853
+ });
19854
+ if (toplevel !== null && rules.length > 0 && result.refused === null) {
19855
+ await excludeLocalFiles({
19856
+ toplevel,
19857
+ directory: root,
19858
+ files: [folder],
19859
+ deadline: Date.now() + 8e3
19860
+ });
19861
+ }
19862
+ return result;
19863
+ }
19864
+ function registerRulesCommand(instructions) {
19865
+ instructions.command("rules").description(
19866
+ "Write the project's rules into .claude/rules/fabric-<id>/ for Claude Code (a plugin cannot carry rules)"
19867
+ ).requiredOption("--project <id>", "Project ID").option(
19868
+ "--base-url <origin>",
19869
+ "Deployment to talk to (default: the one you are signed in to)"
19870
+ ).option("--org <slug>", "Organization context").option(
19871
+ "--dest <dir>",
19872
+ "The project folder (default: the folder `init` set up, found from cwd)"
19873
+ ).option("--hook", "Run as a session hook: print nothing, never fail").action(async (opts) => {
19874
+ const note = (text3) => process.stderr.write(`fabric: ${text3}
19875
+ `);
19876
+ try {
19877
+ const project = opts.project;
19878
+ let root;
19879
+ if (opts.dest !== void 0) {
19880
+ root = await resolveDestinationRoot(
19881
+ path37.resolve(opts.dest)
19882
+ );
19883
+ } else {
19884
+ const found = await findProjectRoot(process.cwd());
19885
+ if (found === null || found.marker.projectId !== project) {
19886
+ throw new CliFailure(
19887
+ "this folder was not set up by `fabric instructions init` for this project; pass --dest",
19888
+ 2
19889
+ );
19890
+ }
19891
+ root = found.root;
19892
+ }
19893
+ const result = await syncRulesForProject({
19894
+ project,
19895
+ baseUrl: opts.baseUrl,
19896
+ org: opts.org,
19897
+ root
19898
+ });
19899
+ for (const text3 of rulesSyncLines(result)) {
19900
+ note(text3);
19901
+ }
19902
+ } catch (error) {
19903
+ note(
19904
+ `rules failed: ${error instanceof CliFailure ? error.message : describeError(error)}`
19905
+ );
19906
+ if (!opts.hook) {
19907
+ process.exit(
19908
+ error instanceof CliFailure ? error.exitCode : 1
19909
+ );
19910
+ }
19911
+ }
19912
+ });
19913
+ }
19914
+
18648
19915
  // src/commands/instructions/init-plugin.ts
18649
19916
  function wordForPaste(value) {
18650
19917
  return pathAsCommandWord(value) ?? value;
@@ -18673,6 +19940,25 @@ async function approveClaudePlugin(input) {
18673
19940
  );
18674
19941
  return installed.kind === "exited" && installed.code === 0 ? { kind: "approved" } : { kind: "manual", lines: [commands.install] };
18675
19942
  }
19943
+ async function deliverRules(options, root) {
19944
+ try {
19945
+ const result = await syncRulesForProject({
19946
+ project: options.project,
19947
+ baseUrl: options.origin,
19948
+ org: options.org,
19949
+ root
19950
+ });
19951
+ const notes = rulesSyncLines(result);
19952
+ return result.written.length + result.unchanged > 0 || notes.length > 0 ? [
19953
+ `Project rules are delivered to ${rulesFolderFor(options.project)}/ (kept out of commits; refreshed at each session start).`,
19954
+ ...notes
19955
+ ] : [];
19956
+ } catch (error) {
19957
+ return [
19958
+ `The project's rules could not be delivered now (${describeError(error)}); the session hook retries at each start.`
19959
+ ];
19960
+ }
19961
+ }
18676
19962
  function line(text3) {
18677
19963
  process.stdout.write(`${text3}
18678
19964
  `);
@@ -18701,7 +19987,7 @@ async function probeMemory(options, root) {
18701
19987
  }
18702
19988
  async function runPluginInit(options) {
18703
19989
  const explicit = options.tool === void 0 ? void 0 : toolOf2(options.tool);
18704
- const destination = path35.resolve(options.dest ?? process.cwd());
19990
+ const destination = path38.resolve(options.dest ?? process.cwd());
18705
19991
  const root = await resolveDestinationRoot(destination);
18706
19992
  try {
18707
19993
  await assertKeyStaysOutside(root, getConfigPath());
@@ -18719,7 +20005,7 @@ async function runPluginInit(options) {
18719
20005
  const tools = explicit === void 0 ? detected.tools : [explicit];
18720
20006
  const launcher = await resolveHookLauncher({
18721
20007
  origin: options.origin,
18722
- configDirectory: path35.dirname(getConfigPath()),
20008
+ configDirectory: path38.dirname(getConfigPath()),
18723
20009
  lookup: { env: machine.env(), platform: machine.platform() },
18724
20010
  bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
18725
20011
  });
@@ -18747,11 +20033,12 @@ async function runPluginInit(options) {
18747
20033
  org: options.org
18748
20034
  });
18749
20035
  marketplaceDirectory = await writeClaudeMarketplace({
18750
- cacheDirectory: path35.join(
18751
- path35.dirname(getConfigPath()),
20036
+ cacheDirectory: path38.join(
20037
+ path38.dirname(getConfigPath()),
18752
20038
  "cache"
18753
20039
  ),
18754
20040
  projectId: options.project,
20041
+ origin: options.origin,
18755
20042
  command
18756
20043
  });
18757
20044
  const registered = await registerClaudePlugin({
@@ -18760,8 +20047,9 @@ async function runPluginInit(options) {
18760
20047
  marketplaceDirectory
18761
20048
  });
18762
20049
  excludes.push(CLAUDE_SETTINGS_RELATIVE_PATH);
20050
+ steps.push(...await deliverRules(options, root));
18763
20051
  steps.push(
18764
- `Registered the ${pluginNameFor(options.project)} plugin in ${path35.relative(root, registered.settingsPath) || registered.settingsPath} (marketplace folder ${marketplaceDirectory}); Claude Code builds it from Fabric at the start of each session.`
20052
+ `Registered the ${pluginNameFor(options.project)} plugin in ${path38.relative(root, registered.settingsPath) || registered.settingsPath} (marketplace folder ${marketplaceDirectory}); Claude Code builds it from Fabric at the start of each session.`
18765
20053
  );
18766
20054
  } else {
18767
20055
  const registration = await registerCodexPlugin({
@@ -18770,13 +20058,22 @@ async function runPluginInit(options) {
18770
20058
  pluginDirectory: rendered.directory
18771
20059
  });
18772
20060
  steps.push(
18773
- `Rendered the ${pluginNameFor(options.project)} plugin to ${rendered.directory} and listed it in ${registration.marketplaceFile} (marketplace "${registration.marketplaceName}"). Install it from Codex's plugin browser if it is not on yet.`
20061
+ `Rendered the ${pluginNameFor(options.project)} plugin to ${rendered.directory} and listed it in ${registration.marketplaceFile} (marketplace "${registration.marketplaceName}").`
18774
20062
  );
18775
- if (rendered.alwaysOn !== null) {
18776
- await refreshAgentsOverride(root, rendered.alwaysOn.text);
20063
+ const installed = await installCodexPlugin({
20064
+ run: run4,
20065
+ projectId: options.project,
20066
+ marketplaceName: registration.marketplaceName,
20067
+ cwd: root
20068
+ });
20069
+ steps.push(
20070
+ installed.kind === "installed" ? `Installed the ${pluginNameFor(options.project)} plugin in Codex.` : `Codex did not install the plugin. Run: ${installed.line ?? "codex plugin add, with the plugin and marketplace names above"}`
20071
+ );
20072
+ if (rendered.agentsText !== null) {
20073
+ await refreshAgentsOverride(root, rendered.agentsText);
18777
20074
  excludes.push("AGENTS.override.md");
18778
20075
  steps.push(
18779
- "Wrote AGENTS.override.md (kept out of commits): the project's AGENTS.md, then the instructions."
20076
+ "Wrote AGENTS.override.md (kept out of commits): the project's AGENTS.md, then the instructions and the project's rules."
18780
20077
  );
18781
20078
  }
18782
20079
  }
@@ -18958,6 +20255,7 @@ function buildInstructionsCommand() {
18958
20255
  "Keep a checkout current with a project's published coding instructions"
18959
20256
  );
18960
20257
  registerRenderCommand(instructions);
20258
+ registerRulesCommand(instructions);
18961
20259
  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) => {
18962
20260
  await refreshKeptCopyAfterHook(
18963
20261
  opts,
@@ -19207,7 +20505,7 @@ async function traceHookFailure(opts, why) {
19207
20505
  return;
19208
20506
  }
19209
20507
  const reason = why.signedOut ? "not-signed-in" : why.upgrade ? "upgrade-required" : why.timedOut ? "deadline" : `failed-${why.exitCode}`;
19210
- await appendTrace(traceFile(path36.dirname(getConfigPath())), {
20508
+ await appendTrace(traceFile(path39.dirname(getConfigPath())), {
19211
20509
  at: (/* @__PURE__ */ new Date()).toISOString(),
19212
20510
  projectId: opts.project ?? "",
19213
20511
  outcome: "skipped",
@@ -19240,7 +20538,7 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt, { mayDefer = true } =
19240
20538
  const input = {
19241
20539
  origin,
19242
20540
  script: bundleScriptPath(),
19243
- configDirectory: path36.dirname(getConfigPath()),
20541
+ configDirectory: path39.dirname(getConfigPath()),
19244
20542
  runningTarball: bundleTarballPath(),
19245
20543
  env: process.env,
19246
20544
  deadlineAt,
@@ -19258,7 +20556,7 @@ async function refreshKeptCopyAfterHook(opts, deadlineAt, { mayDefer = true } =
19258
20556
  });
19259
20557
  }
19260
20558
  if (outcome.kind !== "skipped") {
19261
- await appendTrace(traceFile(path36.dirname(getConfigPath())), {
20559
+ await appendTrace(traceFile(path39.dirname(getConfigPath())), {
19262
20560
  at: (/* @__PURE__ */ new Date()).toISOString(),
19263
20561
  projectId: "",
19264
20562
  outcome: "self-update",
@@ -19318,7 +20616,7 @@ function gitDeadline() {
19318
20616
  return activeDeadlineAt === void 0 ? Date.now() + GIT_BUDGET_MS3 : activeDeadlineAt - hookTiming.gitMarginMs;
19319
20617
  }
19320
20618
  function destinationOf(opts) {
19321
- return path36.resolve(opts.dest ?? process.cwd());
20619
+ return path39.resolve(opts.dest ?? process.cwd());
19322
20620
  }
19323
20621
  function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
19324
20622
  if (!getApiKey(deploymentOrigin(opts), opts.project)) {
@@ -19691,7 +20989,7 @@ async function directRepositoryCheckoutStatus(input) {
19691
20989
  };
19692
20990
  }
19693
20991
  async function readLockForProject2(destination, projectId) {
19694
- const lock = await readLock(destination);
20992
+ const lock = await readLock2(destination);
19695
20993
  if (lock !== null && lock.projectId !== projectId) {
19696
20994
  throw outcomeFailure("lock-of-other-project", {});
19697
20995
  }
@@ -20104,7 +21402,7 @@ async function syncOnce(opts) {
20104
21402
  projectId: opts.project,
20105
21403
  deadline: gitDeadline(),
20106
21404
  optedOut: opts.fastForward === false,
20107
- traceFile: traceFile(path36.dirname(getConfigPath()))
21405
+ traceFile: traceFile(path39.dirname(getConfigPath()))
20108
21406
  }) : null;
20109
21407
  hookTraced = fastForward !== null;
20110
21408
  lazyTransport?.rethrow();
@@ -20145,7 +21443,7 @@ async function syncOnce(opts) {
20145
21443
  projectId: opts.project,
20146
21444
  deadline: gitDeadline(),
20147
21445
  optedOut: opts.fastForward === false,
20148
- traceFile: traceFile(path36.dirname(getConfigPath())),
21446
+ traceFile: traceFile(path39.dirname(getConfigPath())),
20149
21447
  timings: hookPhases
20150
21448
  }) : null;
20151
21449
  return {
@@ -20223,7 +21521,7 @@ async function syncOnce(opts) {
20223
21521
  try {
20224
21522
  for (let attempt2 = 0; plan.writes.length > 0 && attempt2 < 2; attempt2++) {
20225
21523
  try {
20226
- const byPath3 = new Map(
21524
+ const byPath5 = new Map(
20227
21525
  manifest.map((entry) => [entry.path, entry])
20228
21526
  );
20229
21527
  const large = manifest.reduce((total, entry) => total + entry.size, 0) > IN_MEMORY_SNAPSHOT_BYTES;
@@ -20235,7 +21533,7 @@ async function syncOnce(opts) {
20235
21533
  digest: published.snapshot.digest,
20236
21534
  files: plan.writes.map((entry) => ({
20237
21535
  path: entry.path,
20238
- size: byPath3.get(entry.path)?.size ?? -1,
21536
+ size: byPath5.get(entry.path)?.size ?? -1,
20239
21537
  sha256: entry.sha256 ?? ""
20240
21538
  })),
20241
21539
  timeoutMs: opts.hook ? hookDeadlineMs() : BUNDLE_TIMEOUT_MS,
@@ -20260,7 +21558,7 @@ async function syncOnce(opts) {
20260
21558
  archive,
20261
21559
  plan.writes.map((entry) => ({
20262
21560
  path: entry.path,
20263
- size: byPath3.get(entry.path)?.size ?? -1
21561
+ size: byPath5.get(entry.path)?.size ?? -1
20264
21562
  }))
20265
21563
  );
20266
21564
  }
@@ -20803,7 +22101,7 @@ async function cloneProjectRepository(opts, destination, repository, transport)
20803
22101
  }
20804
22102
  async function insideInstructionsCheckout(opts) {
20805
22103
  const destination = await resolveExistingRoot(destinationOf(opts));
20806
- if (await readLock(destination) !== null) {
22104
+ if (await readLock2(destination) !== null) {
20807
22105
  return true;
20808
22106
  }
20809
22107
  const guard = new SourceGuard(destination, opts.remote);
@@ -20880,7 +22178,7 @@ async function runInit(opts, format) {
20880
22178
  );
20881
22179
  cloned = true;
20882
22180
  if (cloneFolder !== void 0 && checkout.class === "unmapped") {
20883
- destination = path36.join(checkout.toplevel, checkout.rootPath);
22181
+ destination = path39.join(checkout.toplevel, checkout.rootPath);
20884
22182
  existing = await resolveExistingRoot(destination);
20885
22183
  checkout = await classifyCheckout({
20886
22184
  destination: existing,
@@ -20948,12 +22246,12 @@ async function runInit(opts, format) {
20948
22246
  throw outcomeFailure("lessons-need-claude", {});
20949
22247
  }
20950
22248
  if (matching !== null) {
20951
- const prefix = path36.relative(matching.toplevel, root);
22249
+ const prefix = path39.relative(matching.toplevel, root);
20952
22250
  const relativeSetupPaths = [
20953
22251
  ...tools.tools.map(
20954
- (tool) => path36.join(prefix, hookPathFor(tool)).replaceAll(path36.sep, "/")
22252
+ (tool) => path39.join(prefix, hookPathFor(tool)).replaceAll(path39.sep, "/")
20955
22253
  ),
20956
- path36.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path36.sep, "/")
22254
+ path39.join(prefix, LOCK_DIRECTORY, LOCK_FILENAME).replaceAll(path39.sep, "/")
20957
22255
  ];
20958
22256
  const tracked = await trackedPaths(
20959
22257
  matching.toplevel,
@@ -20992,7 +22290,7 @@ async function runInit(opts, format) {
20992
22290
  const applies = opts.reportOnly ? false : matching !== null ? true : Boolean(opts.apply);
20993
22291
  const launcher = await resolveHookLauncher({
20994
22292
  origin: deploymentOrigin(opts),
20995
- configDirectory: path36.dirname(getConfigPath()),
22293
+ configDirectory: path39.dirname(getConfigPath()),
20996
22294
  lookup: { env: machine.env(), platform: machine.platform() },
20997
22295
  bundle: isServedBundle() ? { scriptPath: bundleScriptPath() } : null
20998
22296
  });
@@ -21164,7 +22462,7 @@ async function runInit(opts, format) {
21164
22462
  outcomeLine("cloned-into", {
21165
22463
  where: folderForCommand(
21166
22464
  opts.dest,
21167
- path36.relative(
22465
+ path39.relative(
21168
22466
  await resolveExistingRoot(destinationOf(opts)),
21169
22467
  root
21170
22468
  )
@@ -21210,8 +22508,8 @@ async function runLessonPromptCommand(opts) {
21210
22508
  stdinText = await readAllStdin(process.stdin);
21211
22509
  } catch {
21212
22510
  }
21213
- const markerDir = path36.join(
21214
- path36.dirname(getConfigPath()),
22511
+ const markerDir = path39.join(
22512
+ path39.dirname(getConfigPath()),
21215
22513
  "lesson-prompts"
21216
22514
  );
21217
22515
  const result = await runLessonPrompt({
@@ -21373,6 +22671,9 @@ function buildMcpCommand() {
21373
22671
  // src/commands/memory/index.ts
21374
22672
  import { Command as Command15 } from "commander";
21375
22673
 
22674
+ // src/lib/memory/pull.ts
22675
+ import path41 from "path";
22676
+
21376
22677
  // src/lib/memory/context.ts
21377
22678
  var FETCH_CONCURRENCY = 4;
21378
22679
  function now(context) {
@@ -21403,11 +22704,11 @@ async function inParallel(items, work) {
21403
22704
  }
21404
22705
 
21405
22706
  // src/lib/memory/local.ts
21406
- import { createHash as createHash18 } from "crypto";
21407
- import { lstat as lstat8, readdir as readdir5 } from "fs/promises";
21408
- import path37 from "path";
22707
+ import { createHash as createHash19 } from "crypto";
22708
+ import { lstat as lstat7, readdir as readdir5 } from "fs/promises";
22709
+ import path40 from "path";
21409
22710
  function sha256Hex2(bytes) {
21410
- return createHash18("sha256").update(bytes).digest("hex");
22711
+ return createHash19("sha256").update(bytes).digest("hex");
21411
22712
  }
21412
22713
  async function scanFolder(root, folder) {
21413
22714
  const scan = { files: [], skipped: [] };
@@ -21416,7 +22717,7 @@ async function scanFolder(root, folder) {
21416
22717
  const directory = pending.pop();
21417
22718
  let names2;
21418
22719
  try {
21419
- names2 = await readdir5(path37.join(root, ...directory.split("/")));
22720
+ names2 = await readdir5(path40.join(root, ...directory.split("/")));
21420
22721
  } catch (error) {
21421
22722
  if (error.code === "ENOENT") {
21422
22723
  continue;
@@ -21428,8 +22729,8 @@ async function scanFolder(root, folder) {
21428
22729
  if (name.startsWith(".")) {
21429
22730
  continue;
21430
22731
  }
21431
- const stats = await lstat8(
21432
- path37.join(root, ...relative.split("/"))
22732
+ const stats = await lstat7(
22733
+ path40.join(root, ...relative.split("/"))
21433
22734
  ).catch(() => null);
21434
22735
  if (stats === null) {
21435
22736
  continue;
@@ -21491,7 +22792,7 @@ function emptyMemoryLock(projectId) {
21491
22792
  outbox: []
21492
22793
  };
21493
22794
  }
21494
- function isRecord7(value) {
22795
+ function isRecord8(value) {
21495
22796
  return typeof value === "object" && value !== null && !Array.isArray(value);
21496
22797
  }
21497
22798
  function damaged() {
@@ -21512,7 +22813,7 @@ async function readMemoryLock(root, projectId) {
21512
22813
  } catch {
21513
22814
  throw damaged();
21514
22815
  }
21515
- if (!isRecord7(parsed) || parsed.version !== MEMORY_LOCK_VERSION || typeof parsed.projectId !== "string" || !isRecord7(parsed.files) || !Array.isArray(parsed.outbox)) {
22816
+ if (!isRecord8(parsed) || parsed.version !== MEMORY_LOCK_VERSION || typeof parsed.projectId !== "string" || !isRecord8(parsed.files) || !Array.isArray(parsed.outbox)) {
21516
22817
  throw damaged();
21517
22818
  }
21518
22819
  if (parsed.projectId !== projectId) {
@@ -21713,14 +23014,14 @@ function planPull(input) {
21713
23014
  const remotePaths = /* @__PURE__ */ new Set();
21714
23015
  for (const remote of input.remote) {
21715
23016
  remotePaths.add(remote.path);
21716
- const path38 = remote.path;
21717
- if (input.tracked.has(path38)) {
21718
- actions.push({ kind: "tracked", path: path38 });
23017
+ const path42 = remote.path;
23018
+ if (input.tracked.has(path42)) {
23019
+ actions.push({ kind: "tracked", path: path42 });
21719
23020
  continue;
21720
23021
  }
21721
- const local = input.local.get(path38);
21722
- const entry = input.lock.files[path38];
21723
- const accept = input.acceptRemote.has(path38);
23022
+ const local = input.local.get(path42);
23023
+ const entry = input.lock.files[path42];
23024
+ const accept = input.acceptRemote.has(path42);
21724
23025
  if (entry === void 0) {
21725
23026
  if (local === void 0) {
21726
23027
  actions.push({ kind: "write", file: remote, reason: "new" });
@@ -21735,7 +23036,7 @@ function planPull(input) {
21735
23036
  } else {
21736
23037
  actions.push({
21737
23038
  kind: "keep",
21738
- path: path38,
23039
+ path: path42,
21739
23040
  reason: "untracked-differs"
21740
23041
  });
21741
23042
  }
@@ -21748,7 +23049,7 @@ function planPull(input) {
21748
23049
  actions.push(
21749
23050
  accept ? { kind: "write", file: remote, reason: "updated" } : {
21750
23051
  kind: "keep",
21751
- path: path38,
23052
+ path: path42,
21752
23053
  reason: "deleted-locally-changed-remotely"
21753
23054
  }
21754
23055
  );
@@ -21773,33 +23074,35 @@ function planPull(input) {
21773
23074
  } else if (accept) {
21774
23075
  actions.push({ kind: "write", file: remote, reason: "updated" });
21775
23076
  } else {
21776
- actions.push({ kind: "keep", path: path38, reason: "modified" });
23077
+ actions.push({ kind: "keep", path: path42, reason: "modified" });
21777
23078
  }
21778
23079
  }
21779
- for (const [path38, entry] of Object.entries(input.lock.files)) {
21780
- if (remotePaths.has(path38)) {
23080
+ for (const [path42, entry] of Object.entries(input.lock.files)) {
23081
+ if (remotePaths.has(path42)) {
21781
23082
  continue;
21782
23083
  }
21783
23084
  if (entry.origin === "push") {
21784
23085
  continue;
21785
23086
  }
21786
- const local = input.local.get(path38);
23087
+ const local = input.local.get(path42);
21787
23088
  if (local === void 0) {
21788
- actions.push({ kind: "forget", path: path38 });
21789
- } else if (local === entry.contentHash) {
21790
- actions.push({
21791
- kind: "delete",
21792
- path: path38,
21793
- expectedHash: entry.contentHash
21794
- });
23089
+ actions.push({ kind: "forget", path: path42 });
23090
+ } else if (local === entry.contentHash || input.acceptRemote.has(path42)) {
23091
+ actions.push({ kind: "delete", path: path42, expectedHash: local });
21795
23092
  } else {
21796
23093
  actions.push({
21797
23094
  kind: "keep",
21798
- path: path38,
23095
+ path: path42,
21799
23096
  reason: "modified-and-removed"
21800
23097
  });
21801
23098
  }
21802
23099
  }
23100
+ for (const path42 of input.acceptRemote) {
23101
+ const local = input.local.get(path42);
23102
+ if (local !== void 0 && !remotePaths.has(path42) && input.lock.files[path42] === void 0 && !input.tracked.has(path42)) {
23103
+ actions.push({ kind: "delete", path: path42, expectedHash: local });
23104
+ }
23105
+ }
21803
23106
  return actions.sort((a, b) => {
21804
23107
  const left = actionPath(a);
21805
23108
  const right = actionPath(b);
@@ -21809,13 +23112,13 @@ function planPull(input) {
21809
23112
  function actionPath(action) {
21810
23113
  return action.kind === "write" || action.kind === "adopt" ? action.file.path : action.path;
21811
23114
  }
21812
- function targetOf2(path38, targets) {
21813
- return targets.find((target) => path38.startsWith(`${target.folder}/`));
23115
+ function targetOf2(path42, targets) {
23116
+ return targets.find((target) => path42.startsWith(`${target.folder}/`));
21814
23117
  }
21815
- function recordFor(outbox, path38, contentHash) {
23118
+ function recordFor(outbox, path42, contentHash) {
21816
23119
  return outbox.find(
21817
23120
  (record) => record.paths.some(
21818
- (entry) => entry.path === path38 && entry.contentHash === contentHash
23121
+ (entry) => entry.path === path42 && entry.contentHash === contentHash
21819
23122
  )
21820
23123
  );
21821
23124
  }
@@ -21841,29 +23144,29 @@ function planPush(input) {
21841
23144
  }
21842
23145
  plan.changes.push(change);
21843
23146
  };
21844
- for (const [path38, hash] of input.local) {
21845
- const target = targetOf2(path38, input.targets);
23147
+ for (const [path42, hash] of input.local) {
23148
+ const target = targetOf2(path42, input.targets);
21846
23149
  if (target === void 0) {
21847
23150
  continue;
21848
23151
  }
21849
- const entry = input.lock.files[path38];
21850
- if (input.tracked.has(path38)) {
21851
- plan.skipped.push({ path: path38, reason: "tracked" });
23152
+ const entry = input.lock.files[path42];
23153
+ if (input.tracked.has(path42)) {
23154
+ plan.skipped.push({ path: path42, reason: "tracked" });
21852
23155
  continue;
21853
23156
  }
21854
23157
  if (entry === void 0) {
21855
23158
  consider(
21856
- { path: path38, targetId: target.id, kind: "new", contentHash: hash },
23159
+ { path: path42, targetId: target.id, kind: "new", contentHash: hash },
21857
23160
  hash
21858
23161
  );
21859
23162
  } else if (entry.contentHash !== hash) {
21860
23163
  if (entry.blobOid === null) {
21861
- plan.skipped.push({ path: path38, reason: "version-unknown" });
23164
+ plan.skipped.push({ path: path42, reason: "version-unknown" });
21862
23165
  continue;
21863
23166
  }
21864
23167
  consider(
21865
23168
  {
21866
- path: path38,
23169
+ path: path42,
21867
23170
  targetId: target.id,
21868
23171
  kind: "modified",
21869
23172
  ifMatchOid: entry.blobOid,
@@ -21873,21 +23176,21 @@ function planPush(input) {
21873
23176
  );
21874
23177
  }
21875
23178
  }
21876
- for (const [path38, entry] of Object.entries(input.lock.files)) {
21877
- if (input.local.has(path38)) {
23179
+ for (const [path42, entry] of Object.entries(input.lock.files)) {
23180
+ if (input.local.has(path42)) {
21878
23181
  continue;
21879
23182
  }
21880
- const target = targetOf2(path38, input.targets);
23183
+ const target = targetOf2(path42, input.targets);
21881
23184
  if (target === void 0) {
21882
23185
  continue;
21883
23186
  }
21884
23187
  if (entry.blobOid === null) {
21885
- plan.skipped.push({ path: path38, reason: "version-unknown" });
23188
+ plan.skipped.push({ path: path42, reason: "version-unknown" });
21886
23189
  continue;
21887
23190
  }
21888
23191
  consider(
21889
23192
  {
21890
- path: path38,
23193
+ path: path42,
21891
23194
  targetId: target.id,
21892
23195
  kind: "deleted",
21893
23196
  ifMatchOid: entry.blobOid
@@ -21976,10 +23279,16 @@ async function pull(context, options = {}) {
21976
23279
  }
21977
23280
  const folders = targets.map((target) => target.folder);
21978
23281
  const tracked = await context.git.tracked(folders) ?? /* @__PURE__ */ new Set();
23282
+ const acceptRemote = new Set(
23283
+ (options.acceptRemote ?? []).filter(
23284
+ (file) => path41.posix.normalize(file) === file && folders.some((folder) => file.startsWith(`${folder}/`))
23285
+ )
23286
+ );
21979
23287
  const local = /* @__PURE__ */ new Map();
21980
23288
  const relevant = /* @__PURE__ */ new Set([
21981
23289
  ...remote.map((file) => file.path),
21982
- ...Object.keys(lock.files)
23290
+ ...Object.keys(lock.files),
23291
+ ...acceptRemote
21983
23292
  ]);
21984
23293
  for (const file of relevant) {
21985
23294
  const hash = await hashLocalFile2(context.root, file);
@@ -21992,7 +23301,7 @@ async function pull(context, options = {}) {
21992
23301
  local,
21993
23302
  lock,
21994
23303
  tracked,
21995
- acceptRemote: new Set(options.acceptRemote ?? [])
23304
+ acceptRemote
21996
23305
  });
21997
23306
  await applyPull(context, lock, actions, result);
21998
23307
  await resolveOutbox(context, lock);
@@ -22405,10 +23714,10 @@ function recordLines(record) {
22405
23714
  const lines2 = [
22406
23715
  ` conflict in ${where}: nothing was written; these changed on the server since you last pulled:`
22407
23716
  ];
22408
- for (const path38 of record.paths) {
22409
- if (path38.verdict !== void 0) {
23717
+ for (const path42 of record.paths) {
23718
+ if (path42.verdict !== void 0) {
22410
23719
  lines2.push(
22411
- ` ${path38.path}: ${path38.verdict.code}${path38.verdict.currentOid ? ` (now ${path38.verdict.currentOid.slice(0, 10)})` : ""}`
23720
+ ` ${path42.path}: ${path42.verdict.code}${path42.verdict.currentOid ? ` (now ${path42.verdict.currentOid.slice(0, 10)})` : ""}`
22412
23721
  );
22413
23722
  }
22414
23723
  }
@@ -22420,7 +23729,7 @@ function recordLines(record) {
22420
23729
  case "REFUSED":
22421
23730
  return [
22422
23731
  ` refused in ${where}: ${record.detail ?? "see the server"}. Fix the files named below, then push again, or run \`fabric memory status --clear\` to dismiss.`,
22423
- ...record.paths.filter((path38) => path38.verdict !== void 0).map((path38) => ` ${path38.path}: ${path38.verdict?.code}`)
23732
+ ...record.paths.filter((path42) => path42.verdict !== void 0).map((path42) => ` ${path42.path}: ${path42.verdict?.code}`)
22424
23733
  ];
22425
23734
  case "FAILED":
22426
23735
  return [
@@ -22450,9 +23759,9 @@ function pullLines(result) {
22450
23759
  );
22451
23760
  }
22452
23761
  lines2.push(...keptLines(result));
22453
- for (const path38 of result.trackedByGit) {
23762
+ for (const path42 of result.trackedByGit) {
22454
23763
  lines2.push(
22455
- `skipped ${path38}: your repository tracks it; use git for that file.`
23764
+ `skipped ${path42}: your repository tracks it; use git for that file.`
22456
23765
  );
22457
23766
  }
22458
23767
  for (const settled of result.settled) {