glasspane-mcp 1.16.3 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,9 +36,13 @@ MCP 客户端配置:
36
36
 
37
37
  ## What ships in this package / 包里有什么
38
38
 
39
- `dist/index.js` 是 esbuild 单文件产物:`@iterate/kernel`(schema 真源)在发布时
40
- 已捆绑内联,包内不存在指向源码仓库的 `file:` 依赖;`schemas/` 是 kernel schema
41
- 的发布副本,每次 `npm run bundle` 重新同步。
39
+ `dist/index.js` 是 esbuild 单文件产物:已发布的 `iterate-kernel`(schema 真源,devDependency,
40
+ 只按 `iterate-kernel/<module>` 子路径引入)在发布时已捆绑内联,包内不存在指向源码仓库的
41
+ `file:` 依赖,也不残留一个裸的 `import "iterate-kernel"`——后者会让装到本机的产物去解析一份
42
+ 没人拿这份产物对照过的内核,比 `ERR_MODULE_NOT_FOUND` 安静,但不更安全;发布档的
43
+ bundle-shape 闸专拒这一条。`schemas/` 是内核契约的发布副本,每次 `npm run bundle` 由
44
+ `scripts/bundle-kernel-schemas.mjs` 从**装好的那一份包**重新同步,并核
45
+ manifest 声明 = 装到的 = fork 的 pin 三者同号。
42
46
 
43
47
  两个入口,各自是什么就按什么用 / two entries, each with one job:
44
48
 
@@ -26,7 +26,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-06-18";
26
26
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
27
27
  export declare const SERVER_INFO: {
28
28
  readonly name: "glasspane-mcp";
29
- readonly version: "1.16.3";
29
+ readonly version: "1.18.0";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -92,6 +92,22 @@ export declare class McpServer {
92
92
  private asParams;
93
93
  private isRequestBody;
94
94
  private result;
95
+ /**
96
+ * The example for the two `tools/call` **shape** refusals, taken from the list this server
97
+ * actually answers. A JSON-RPC reader cannot open a source symbol, and an example naming a
98
+ * tool this surface does not publish would be a second refusal waiting to happen — so the
99
+ * preference is a published tool that is legal to call with an empty `arguments`.
100
+ * `test/dispatch.test.mjs` checks every name in this sentence against `tools/list`, and
101
+ * checks it again on a server deliberately built with a one-tool list.
102
+ */
103
+ private callShapeRemedy;
104
+ /**
105
+ * The names this server actually answers with, nearest the caller's guess first. Every
106
+ * name offered here is one `tools/list` just advertised: a suggestion the caller cannot
107
+ * call is worse than no suggestion, and the point of the sentence is to end the guessing
108
+ * loop a bare `Unknown tool: gp_acti` left the caller in.
109
+ */
110
+ private publishedNamesNear;
95
111
  private error;
96
112
  }
97
113
  /**
@@ -184,6 +184,28 @@ export declare const RESTORE_BASE_DEADLINE_MS = 30000;
184
184
  export declare function isReplayUnsafeMethod(method: string): boolean;
185
185
  /** The non-replayable methods, for gates that must see the set change on purpose. */
186
186
  export declare const REPLAY_UNSAFE_METHOD_NAMES: readonly string[];
187
+ /**
188
+ * The answer this shell gives when the daemon's error frame carries no remedy at all.
189
+ *
190
+ * It used to be one sentence for every method — "check the daemon log (stderr) and retry" —
191
+ * which made the transport order by words the re-send it refuses by behaviour: `act`,
192
+ * `restore` and `attach` are the methods {@link REPLAY_UNSAFE_REASONS} exists to keep off
193
+ * the wire a second time, `slowEngineRemedy` says *do NOT re-issue* for them, and
194
+ * `writeBacklogRemedy` refuses even to name a probe while one is outstanding. An error
195
+ * frame is the daemon's answer to *this* request, so the request did reach the service —
196
+ * and whether the change took effect before the error was raised is a thing no error frame
197
+ * says, which is exactly why the sentence may not spend that unknown on a second copy.
198
+ * `snapshot` is the third case and the one a blanket "retry" also got wrong: nothing is
199
+ * pressed twice, but every accepted copy writes another entry into the daemon's bounded
200
+ * ring and can evict the baseline an earlier snapshot returned — the one a later restore
201
+ * still needs. Membership comes from the two lists this file already keeps, so a method
202
+ * moving between them moves this answer with it.
203
+ *
204
+ * `null` is the unattributed case: an id-less frame this client could not tie to a request
205
+ * it still tracks, where naming a method would be a guess and authorising a re-send on top
206
+ * of the guess would be worse.
207
+ */
208
+ export declare function noRemedyFallback(method: string | null): string;
187
209
  /**
188
210
  * `hello.capabilities` 的第一个读者(2026-10-11)。
189
211
  *
@@ -410,6 +432,13 @@ export declare class EngineJsonRpcClient {
410
432
  * unexposed; `index.ts` supplies the real list.
411
433
  */
412
434
  expectedDaemonMethods?: readonly string[]);
435
+ /**
436
+ * The state root the daemon named in its own `hello` (additive since 1.15.0), or null
437
+ * when no handshake answered that key. `null` means "this daemon does not report it" —
438
+ * never "therefore it is the home default"; callers say which of the two they used.
439
+ */
440
+ private helloStateRoot;
441
+ get reportedStateRoot(): string | null;
413
442
  /**
414
443
  * Sink for facts that cannot be attached to any request of the caller: a
415
444
  * late reply, a frame with an id this client never issued, a daemon that
@@ -1,4 +1,4 @@
1
- import type { EvidencePack } from "@iterate/kernel";
1
+ import type { EvidencePack } from "iterate-kernel/evidence-pack";
2
2
  /**
3
3
  * What a report may be handed: a validated pack, or the same structure with
4
4
  * `schemaVersion` widened so it can carry provenance text
@@ -269,6 +269,20 @@ function isReplayUnsafeMethod(method) {
269
269
  return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
270
270
  }
271
271
  var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
272
+ function noRemedyFallback(method) {
273
+ const readTheLog = "the engine sent no remedy for this error code, and the daemon's own log (stderr) is the only side that can say what it refused; this shell cannot see past the frame it was handed";
274
+ if (method === null) {
275
+ return `${readTheLog}. This frame was not attributed to a request this client still tracks, so nothing here says which method it answers, and this line authorises no re-send`;
276
+ }
277
+ const unsafe = REPLAY_UNSAFE_REASONS[method];
278
+ if (unsafe !== void 0) {
279
+ return `${readTheLog}. Do not re-issue '${method}' on the strength of this answer: the frame is proof the request reached the daemon, and no error frame says whether the change took effect before it was raised \u2014 this shell never replays it on its own either, because ${unsafe}`;
280
+ }
281
+ if (STATE_CHANGING_ON_RETRY_METHOD_NAMES.includes(method)) {
282
+ return `${readTheLog}. A second '${method}' is not a cheaper copy of the first: it writes daemon state again, the snapshot ring the daemon keeps is bounded, and one more entry can evict the baseline an earlier snapshot returned \u2014 the one a later restore still needs. Read what the first answer said before asking for another`;
283
+ }
284
+ return `${readTheLog}, and a retry is the plain next step: '${method}' is not a method this shell refuses to replay, so nothing is performed twice. Change what the request carries if a different answer is wanted \u2014 nothing here says the daemon will answer this one differently`;
285
+ }
272
286
  function describeCapabilityDrift(advertised, forwarded) {
273
287
  if (advertised === null) {
274
288
  return "engine self-report carries no capabilities list, so this shell cannot say which daemon methods it measured; every tool result in this session is unverified against the daemon's own method table (an engine older than the capability list, or a hello that was cut short)";
@@ -429,6 +443,15 @@ var EngineJsonRpcClient = class {
429
443
  lateReplyHandler: null
430
444
  };
431
445
  pending = /* @__PURE__ */ new Map();
446
+ /**
447
+ * The state root the daemon named in its own `hello` (additive since 1.15.0), or null
448
+ * when no handshake answered that key. `null` means "this daemon does not report it" —
449
+ * never "therefore it is the home default"; callers say which of the two they used.
450
+ */
451
+ helloStateRoot = null;
452
+ get reportedStateRoot() {
453
+ return this.helloStateRoot;
454
+ }
432
455
  /**
433
456
  * Sink for facts that cannot be attached to any request of the caller: a
434
457
  * late reply, a frame with an id this client never issued, a daemon that
@@ -599,7 +622,7 @@ var EngineJsonRpcClient = class {
599
622
  return;
600
623
  }
601
624
  if (frame.error) {
602
- this.reject(target, attribute(this.engineError(frame.error), target));
625
+ this.reject(target, attribute(this.engineError(frame.error, target.method), target));
603
626
  target.arrivalOrderConsumed = true;
604
627
  this.retainForLateReply();
605
628
  return;
@@ -607,20 +630,20 @@ var EngineJsonRpcClient = class {
607
630
  this.report(`engine sent an id-less frame with a result while '${target.method}' (id ${target.id}) is still waiting for its answer; an id-less answer from this daemon is an error frame, so this one was not delivered to any caller: ${truncate(line)}`);
608
631
  return;
609
632
  }
610
- this.report(frame.error ? `engine reported ${this.engineError(frame.error).code} with no request in flight: ${truncate(line)}` : `engine sent an id-less frame with no request in flight: ${truncate(line)}`);
633
+ this.report(frame.error ? `engine reported ${this.engineError(frame.error, null).code} with no request in flight: ${truncate(line)}` : `engine sent an id-less frame with no request in flight: ${truncate(line)}`);
611
634
  return;
612
635
  }
613
636
  this.report(`engine sent a frame with a non-numeric request id (${jsonOrString(id)}), which this client never issues: ${truncate(line)}`);
614
637
  }
615
638
  settleFromFrame(entry, frame) {
616
639
  if (frame.error) {
617
- this.reject(entry, this.engineError(frame.error));
640
+ this.reject(entry, this.engineError(frame.error, entry.method));
618
641
  return;
619
642
  }
620
- if (!("result" in frame)) {
643
+ if (!("result" in frame) || frame.result === null) {
621
644
  this.reject(entry, new EngineCallError(
622
645
  GP_E_INTERNAL,
623
- `engine answered '${entry.method}' with a frame carrying neither result nor error`,
646
+ frame.result === null ? `engine answered '${entry.method}' with a frame whose result is null: the key is present but carries no answer` : `engine answered '${entry.method}' with a frame carrying neither result nor error`,
624
647
  "do not read this call as successful: the engine answered nothing. Retry once, then check the daemon log (stderr) for what it died before answering"
625
648
  ));
626
649
  return;
@@ -654,11 +677,11 @@ var EngineJsonRpcClient = class {
654
677
  "narrow this request or ask for less of it; the tool's own schema names the parameters that can shrink a reply, and this shell rewrites this line into that advice when it can (the transport layer deliberately does not guess them)"
655
678
  ), entry, inferred));
656
679
  }
657
- engineError(body) {
680
+ engineError(body, method) {
658
681
  return new EngineCallError(
659
682
  String(body.code ?? GP_E_UNKNOWN),
660
683
  String(body.message ?? "the engine sent an error frame without a message"),
661
- typeof body.remedy === "string" && body.remedy !== "" ? body.remedy : "the engine sent no remedy for this error code; check the daemon log (stderr) and retry"
684
+ typeof body.remedy === "string" && body.remedy !== "" ? body.remedy : noRemedyFallback(method)
662
685
  );
663
686
  }
664
687
  /**
@@ -815,6 +838,10 @@ var EngineJsonRpcClient = class {
815
838
  return;
816
839
  }
817
840
  const version = typeof report.version === "string" ? report.version : null;
841
+ const reportedRoot = typeof report.stateRoot === "string" ? report.stateRoot.trim() : "";
842
+ if (reportedRoot !== "") {
843
+ this.helloStateRoot = reportedRoot;
844
+ }
818
845
  const wantVersion = this.identity?.version;
819
846
  if (version !== null && wantVersion !== void 0 && version !== wantVersion) {
820
847
  this.report(`version mismatch: engine self-reports '${version}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
@@ -1154,22 +1181,7 @@ function exponentialFromDigits(digits, power) {
1154
1181
  // src/tools.ts
1155
1182
  import { z as z10 } from "zod";
1156
1183
 
1157
- // ../kernel/dist/schemas.js
1158
- import { createRequire } from "node:module";
1159
- var nodeRequire = createRequire(import.meta.url);
1160
- function loadSchema(relativePath) {
1161
- return nodeRequire(relativePath);
1162
- }
1163
- var EVIDENCE_PACK_JSON_SCHEMA = loadSchema("../schemas/evidence-pack.schema.json");
1164
- var DECISION_LOG_ENTRY_JSON_SCHEMA = loadSchema("../schemas/decision-log-entry.schema.json");
1165
- var RECIPE_CONFIG_JSON_SCHEMA = loadSchema("../schemas/recipe-config.schema.json");
1166
- var RUN_PLAN_JSON_SCHEMA = loadSchema("../schemas/run-plan.schema.json");
1167
- var INVARIANT_REPORT_JSON_SCHEMA = loadSchema("../schemas/invariant-report.schema.json");
1168
- var REVERIFICATION_REQUEST_JSON_SCHEMA = loadSchema("../schemas/reverification-request.schema.json");
1169
- var CONVERGENCE_ROUND_STATE_JSON_SCHEMA = loadSchema("../schemas/convergence-round-state.schema.json");
1170
- var PROVIDER_REGISTRATION_JSON_SCHEMA = loadSchema("../schemas/provider-registration.schema.json");
1171
-
1172
- // ../kernel/dist/evidence-pack.js
1184
+ // ../node_modules/iterate-kernel/dist/evidence-pack.js
1173
1185
  import { z } from "zod";
1174
1186
  var OPERATION_ID_PATTERN = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
1175
1187
  var ISO_MILLIS_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -1338,10 +1350,28 @@ function normalizeEvidencePackForRead(input) {
1338
1350
  }
1339
1351
  var EvidencePackReadSchema = z.preprocess(normalizeEvidencePackForRead, EvidencePackSchema);
1340
1352
 
1341
- // ../kernel/dist/decision-log-entry.js
1353
+ // ../node_modules/iterate-kernel/dist/errors.js
1354
+ var KernelSchemaError = class extends Error {
1355
+ code = "KERNEL_E_SCHEMA";
1356
+ issues;
1357
+ constructor(label, issues) {
1358
+ const detail = issues.map((issue) => `${issue.path.length > 0 ? issue.path : "<root>"}: ${issue.message}`).join("; ");
1359
+ super(`invalid ${label}: ${detail}`);
1360
+ this.name = "KernelSchemaError";
1361
+ this.issues = issues;
1362
+ }
1363
+ };
1364
+ function schemaErrorFrom(label, error) {
1365
+ return new KernelSchemaError(label, error.issues.map((issue) => ({
1366
+ path: issue.path.map((segment) => String(segment)).join("."),
1367
+ message: issue.message
1368
+ })));
1369
+ }
1370
+
1371
+ // ../node_modules/iterate-kernel/dist/decision-log-entry.js
1342
1372
  import { z as z3 } from "zod";
1343
1373
 
1344
- // ../kernel/dist/canonical-json.js
1374
+ // ../node_modules/iterate-kernel/dist/canonical-json.js
1345
1375
  function canonicalJson(value) {
1346
1376
  if (value === null || typeof value !== "object") {
1347
1377
  return JSON.stringify(value);
@@ -1354,7 +1384,7 @@ function canonicalJson(value) {
1354
1384
  return "{" + keys.map((key) => JSON.stringify(key) + ":" + canonicalJson(record[key])).join(",") + "}";
1355
1385
  }
1356
1386
 
1357
- // ../kernel/dist/dimension-context.js
1387
+ // ../node_modules/iterate-kernel/dist/dimension-context.js
1358
1388
  import { z as z2 } from "zod";
1359
1389
  var DIMENSION_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1360
1390
  var DIMENSION_ID_MAX_LENGTH = 64;
@@ -1397,7 +1427,7 @@ var DimensionContextInputSchema = z2.strictObject({
1397
1427
  recorded: z2.record(z2.string().regex(DIMENSION_ID_PATTERN).max(DIMENSION_ID_MAX_LENGTH), DimensionEvidenceSchema)
1398
1428
  });
1399
1429
 
1400
- // ../kernel/dist/decision-log-entry.js
1430
+ // ../node_modules/iterate-kernel/dist/decision-log-entry.js
1401
1431
  var DECISION_ENTRY_ID_PATTERN = /^dl_[0-9A-HJKMNP-TV-Z]{26}$/;
1402
1432
  var PREV_ENTRY_HASH_PATTERN = /^([0-9a-f]{64})?$/;
1403
1433
  var SUMMARY_MAX_LENGTH = 2048;
@@ -1418,25 +1448,7 @@ var DecisionLogEntrySchema = z3.strictObject({
1418
1448
  payload: z3.record(z3.string(), z3.unknown()).optional().refine((value) => value === void 0 || canonicalJson(value).length <= PAYLOAD_MAX_CANONICAL_LENGTH, { message: `payload must stay within ${PAYLOAD_MAX_CANONICAL_LENGTH} characters of canonical JSON` })
1419
1449
  });
1420
1450
 
1421
- // ../kernel/dist/errors.js
1422
- var KernelSchemaError = class extends Error {
1423
- code = "KERNEL_E_SCHEMA";
1424
- issues;
1425
- constructor(label, issues) {
1426
- const detail = issues.map((issue) => `${issue.path.length > 0 ? issue.path : "<root>"}: ${issue.message}`).join("; ");
1427
- super(`invalid ${label}: ${detail}`);
1428
- this.name = "KernelSchemaError";
1429
- this.issues = issues;
1430
- }
1431
- };
1432
- function schemaErrorFrom(label, error) {
1433
- return new KernelSchemaError(label, error.issues.map((issue) => ({
1434
- path: issue.path.map((segment) => String(segment)).join("."),
1435
- message: issue.message
1436
- })));
1437
- }
1438
-
1439
- // ../kernel/dist/recipe-config.js
1451
+ // ../node_modules/iterate-kernel/dist/recipe-config.js
1440
1452
  import { z as z4 } from "zod";
1441
1453
  var RECIPE_SCHEMA_VERSION = "glasspane.recipe/0.1-draft";
1442
1454
  var RECIPE_NAME_MAX_LENGTH = 256;
@@ -1457,7 +1469,7 @@ var RecipeConfigSchema = z4.strictObject({
1457
1469
  steps: z4.array(RecipeStepSchema).min(RECIPE_MIN_STEPS).max(RECIPE_MAX_STEPS)
1458
1470
  });
1459
1471
 
1460
- // ../kernel/dist/run-plan.js
1472
+ // ../node_modules/iterate-kernel/dist/run-plan.js
1461
1473
  import { z as z5 } from "zod";
1462
1474
  var RUN_PLAN_SCHEMA_VERSION = "iterate.run-plan/0.1";
1463
1475
  var RUN_PLAN_SOURCES = ["config", "recipe", "session", "manual"];
@@ -1492,7 +1504,7 @@ var RunPlanSchema = z5.strictObject({
1492
1504
  });
1493
1505
  });
1494
1506
 
1495
- // ../kernel/dist/invariant-report.js
1507
+ // ../node_modules/iterate-kernel/dist/invariant-report.js
1496
1508
  import { z as z6 } from "zod";
1497
1509
  var INVARIANT_REPORT_SCHEMA_VERSION = "iterate.invariant-report/0.1";
1498
1510
  var REVERIFICATION_SCHEMA_VERSION = "iterate.reverification/0.1";
@@ -1552,7 +1564,7 @@ var ReverificationRequestSchema = z6.strictObject({
1552
1564
  targets: ReverificationTargetsSchema
1553
1565
  });
1554
1566
 
1555
- // ../kernel/dist/convergence.js
1567
+ // ../node_modules/iterate-kernel/dist/convergence.js
1556
1568
  import { z as z7 } from "zod";
1557
1569
  var CONVERGENCE_SCHEMA_VERSION = "iterate.convergence/0.1";
1558
1570
  var InvariantStateSchema = z7.enum(["pass", "fail", "error", "unmeasured"]);
@@ -1564,8 +1576,17 @@ var RoundStateSchema = z7.strictObject({
1564
1576
  openFindings: z7.number().int().min(0),
1565
1577
  /** False when the engine never got to evidence something (refused, crashed, unattached). */
1566
1578
  evidenceComplete: z7.boolean(),
1567
- /** The engine's own circuit-breaker level for this run; 3 is tripped. */
1568
- breakerLevel: z7.number().int().min(0).max(3)
1579
+ /**
1580
+ * The engine's own circuit-breaker level for this run; 3 is tripped.
1581
+ *
1582
+ * OPTIONAL, and absence is not the same claim as 0. `0` says "a breaker exists and it has
1583
+ * not tripped"; absence says "this run has no breaker channel at all" — which is the normal
1584
+ * shape of an iterate loop that never touched the GUI engine. The distinction changes no
1585
+ * verdict today (only `>= 3` halts), but collapsing it would force a producer that never
1586
+ * looked to assert a measurement it did not take, and that is the exact failure this
1587
+ * contract's `unmeasured` state exists to prevent.
1588
+ */
1589
+ breakerLevel: z7.number().int().min(0).max(3).optional()
1569
1590
  });
1570
1591
  var ConvergenceActionSchema = z7.enum(["fix", "verify", "converged", "halt"]);
1571
1592
  var CONVERGENCE_REASONS = {
@@ -1594,7 +1615,7 @@ var ConvergenceDecisionSchema = z7.strictObject({
1594
1615
  maxRounds: z7.number().int().min(1)
1595
1616
  });
1596
1617
 
1597
- // ../kernel/dist/provider-registration.js
1618
+ // ../node_modules/iterate-kernel/dist/provider-registration.js
1598
1619
  import { z as z8 } from "zod";
1599
1620
  var PROVIDER_SCHEMA_VERSION = "iterate.provider/0.1";
1600
1621
  var PROVIDER_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -1632,7 +1653,7 @@ var ProviderRegistrationSchema = z8.strictObject({
1632
1653
  });
1633
1654
  });
1634
1655
 
1635
- // ../kernel/dist/parse.js
1656
+ // ../node_modules/iterate-kernel/dist/parse.js
1636
1657
  function parseEvidencePackRead(input) {
1637
1658
  const result = EvidencePackReadSchema.safeParse(input);
1638
1659
  if (result.success) {
@@ -1641,12 +1662,6 @@ function parseEvidencePackRead(input) {
1641
1662
  throw schemaErrorFrom("evidence pack", result.error);
1642
1663
  }
1643
1664
 
1644
- // ../kernel/dist/decision-log.js
1645
- var DecisionLogDraftSchema = DecisionLogEntrySchema.omit({
1646
- sequence: true,
1647
- prevEntryHash: true
1648
- });
1649
-
1650
1665
  // src/canonical.ts
1651
1666
  function canonicalJson2(value) {
1652
1667
  return JSON.stringify(sortKeys(value));
@@ -1691,6 +1706,7 @@ var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
1691
1706
  var MAX_PROJECTS = 128;
1692
1707
  var MAX_DISPLAY_NAME_LENGTH = 256;
1693
1708
  var MAX_FIELD_LENGTH = 1024;
1709
+ var BUNDLE_ID_MAX_LENGTH = 512;
1694
1710
  var PID_RANGE = [1, 2147483647];
1695
1711
  var PATH_FIELDS = [
1696
1712
  "recipeConfigPath",
@@ -1733,7 +1749,9 @@ function systemHome() {
1733
1749
  );
1734
1750
  }
1735
1751
  }
1736
- function daemonProjectsPath() {
1752
+ function daemonProjectsPath(daemonStateRoot) {
1753
+ const named2 = typeof daemonStateRoot === "string" ? daemonStateRoot.trim() : "";
1754
+ if (named2 !== "") return path2.join(named2, "projects.json");
1737
1755
  return path2.join(systemHome(), ".glasspane", "projects.json");
1738
1756
  }
1739
1757
  var FIRMLINK_PREFIX = "/System/Volumes/Data";
@@ -1751,16 +1769,16 @@ var ProjectSetArgs = z9.strictObject({
1751
1769
  // rule deliberately is not a check on this object — it holds of the MERGED
1752
1770
  // entry, and an update that clears one field while setting the other only
1753
1771
  // becomes legal after the merge (`assertSingleIdentity`).
1754
- bundleId: z9.string().max(256).nullish(),
1772
+ bundleId: z9.string().max(BUNDLE_ID_MAX_LENGTH).nullish(),
1755
1773
  pid: z9.number().int().min(PID_RANGE[0]).max(PID_RANGE[1]).nullish(),
1756
1774
  recipeConfigPath: z9.string().max(MAX_FIELD_LENGTH).nullish(),
1757
1775
  calibrationAssetsPath: z9.string().max(MAX_FIELD_LENGTH).nullish(),
1758
1776
  evidenceStoragePath: z9.string().max(MAX_FIELD_LENGTH).nullish()
1759
1777
  });
1760
- function projectsPath() {
1778
+ function projectsPath(daemonStateRoot) {
1761
1779
  const envPath = process.env[PROJECTS_FILE_ENV];
1762
1780
  if (envPath) return envPath;
1763
- return daemonProjectsPath();
1781
+ return daemonProjectsPath(daemonStateRoot);
1764
1782
  }
1765
1783
  function loadRegistry(filePath) {
1766
1784
  let raw;
@@ -1843,11 +1861,14 @@ function requireUsableRegistry(filePath, snapshot) {
1843
1861
  }
1844
1862
  return { projects: [], discarded };
1845
1863
  }
1864
+ function registryReaders(target) {
1865
+ return `\`/usr/bin/plutil -p ${target}\` or \`/bin/cat ${target}\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH`;
1866
+ }
1846
1867
  function unreadableMessage(filePath, failure) {
1847
- return `projects.json at ${filePath} ${failure}. gp_project_set refused to write it: replacing an unreadable registry discards every registration in it. See the damage with \`python3 -m json.tool ${filePath}\` and repair that file, or set ${FORCE_OVERWRITE_ENV}=1 and retry to rebuild the registry from scratch \u2014 the unloadable file is moved aside as ${filePath}.unreadable-<id> rather than deleted, and every existing project entry is lost.`;
1868
+ return `projects.json at ${filePath} ${failure}. gp_project_set refused to write it: replacing an unreadable registry discards every registration in it. See the damage with ${registryReaders(filePath)} and repair that file, or set ${FORCE_OVERWRITE_ENV}=1 and retry to rebuild the registry from scratch \u2014 the unloadable file is moved aside as ${filePath}.unreadable-<id> rather than deleted, and every existing project entry is lost.`;
1848
1869
  }
1849
1870
  function readFailureMessage(filePath, failure) {
1850
- return `projects.json at ${filePath} ${failure}. This is a read failure, not an empty registry: the entries that are registered cannot be listed. See the damage with \`python3 -m json.tool ${filePath}\`, then repair or replace that file. To rebuild the registry from scratch instead \u2014 which costs every existing entry \u2014 set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set again.`;
1871
+ return `projects.json at ${filePath} ${failure}. This is a read failure, not an empty registry: the entries that are registered cannot be listed. See the damage with ${registryReaders(filePath)}, then repair or replace that file. To rebuild the registry from scratch instead \u2014 which costs every existing entry \u2014 set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set again.`;
1851
1872
  }
1852
1873
  var STATE_DIR_MODE = 448;
1853
1874
  var STATE_FILE_MODE = 384;
@@ -1949,20 +1970,20 @@ function uniqueToken() {
1949
1970
  tokenCounter += 1;
1950
1971
  return `${process.pid}-${Date.now().toString(36)}-${tokenCounter.toString(36)}-${randomBytes(4).toString("hex")}`;
1951
1972
  }
1952
- function projectList() {
1953
- const filePath = projectsPath();
1973
+ function projectList(daemonStateRoot) {
1974
+ const filePath = projectsPath(daemonStateRoot);
1954
1975
  const snapshot = loadRegistry(filePath);
1955
1976
  if (snapshot.failure !== null) {
1956
1977
  throw new ProjectRegistryError(GP_E_INTERNAL, readFailureMessage(filePath, snapshot.failure));
1957
1978
  }
1958
1979
  return snapshot.projects;
1959
1980
  }
1960
- function projectGet(projectId) {
1961
- const projects = projectList();
1981
+ function projectGet(projectId, daemonStateRoot) {
1982
+ const projects = projectList(daemonStateRoot);
1962
1983
  return projects.find((p) => p.projectId === projectId);
1963
1984
  }
1964
- function projectSet(args) {
1965
- const filePath = projectsPath();
1985
+ function projectSet(args, daemonStateRoot) {
1986
+ const filePath = projectsPath(daemonStateRoot);
1966
1987
  assertStorablePid(args.pid);
1967
1988
  const snapshot = loadRegistry(filePath);
1968
1989
  const { projects, discarded } = requireUsableRegistry(filePath, snapshot);
@@ -1997,7 +2018,7 @@ function projectSet(args) {
1997
2018
  return {
1998
2019
  ...entry,
1999
2020
  requiresDaemonRestart: true,
2000
- daemonRestartNotice: daemonRestartNotice(filePath),
2021
+ daemonRestartNotice: daemonRestartNotice(filePath, daemonStateRoot),
2001
2022
  ...discarded !== void 0 ? { discardedUnreadableFile: discarded } : {}
2002
2023
  };
2003
2024
  }
@@ -2040,12 +2061,17 @@ function assertSingleIdentity(entry) {
2040
2061
  );
2041
2062
  }
2042
2063
  }
2043
- function daemonRestartNotice(filePath) {
2044
- const daemonPath = daemonProjectsPath();
2064
+ function daemonRestartNotice(filePath, daemonStateRoot) {
2065
+ const named2 = typeof daemonStateRoot === "string" && daemonStateRoot.trim() !== "" ? daemonStateRoot.trim() : null;
2066
+ const daemonPath = daemonProjectsPath(named2);
2067
+ const authority = named2 === null ? "its default root is the folder under this user's home record (where a service started with no --state-dir looks)" : `its root is the one the running daemon named in its hello (${named2})`;
2045
2068
  if (path2.resolve(filePath) !== path2.resolve(daemonPath)) {
2046
- return `Saved in ${filePath}. The background service does not read this path because of anything in its own configuration: its default root is the folder under this user's home record, where it loads ${daemonPath} once when it starts, and ${PROJECTS_FILE_ENV} \u2014 the variable that pointed this write here \u2014 is honoured by this MCP shell only, so as far as the service's own configuration goes it never reads that file, and no restart of the service will make it load this one. ${rootDiscriminator()} To land the entry where that default root looks: unset ${PROJECTS_FILE_ENV} for this shell (or point it at ${daemonPath}) and repeat gp_project_set, then restart the background service so it reloads \u2014 ${restartCommand()}`;
2069
+ return `Saved in ${filePath}. The background service does not read this path because of anything in its own configuration: ${authority}. It loads ${daemonPath} once when it starts, and ${PROJECTS_FILE_ENV} \u2014 the variable that pointed this write here \u2014 is honoured by this MCP shell only, so as far as the service's own configuration goes it never reads that file, and no restart of the service will make it load this one. ${rootDiscriminator()} To land the entry where that root looks: unset ${PROJECTS_FILE_ENV} for this shell (or point it at ${daemonPath}) and repeat gp_project_set, then restart the background service so it reloads \u2014 ${restartCommand()}`;
2047
2070
  }
2048
- return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
2071
+ if (named2 !== null) {
2072
+ return `Saved in ${filePath} \u2014 the projects file of the state root the running daemon reported in its own hello (${named2}), so this is the file that service loads. It read that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}`;
2073
+ }
2074
+ return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here, because this daemon's hello reported no stateRoot \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
2049
2075
  }
2050
2076
  function rootDiscriminator() {
2051
2077
  return 'Tell the two cases apart with the service\'s own answer: gp_project_list reads the file this call wrote, so an entry it lists is on disk, and gp_attach {projectId} is answered by the running service \u2014 an entry the list shows plus an attach that answers GP_E_NOT_FOUND means the service is loading a different projects.json than the one this write reached, not that the write failed. Which file the running service loads is not something this shell guesses from $HOME: `glasspaned --state-dir <root>` moves it to <root>/projects.json, and the daemon reports the root it resolved \u2014 `hello` names the state root under its `stateRoot` key. A daemon predating 1.15.0 omits the key, and an omitted key means "that daemon does not report it", never "so it must be the home default": for those binaries the discriminator below is the only read. Cross-check either way \u2014 `launchctl print gui/' + currentUid() + "/com.glasspane.daemon` prints the arguments the job was started with. If those arguments name a --state-dir, the entry has to go into that root's projects.json (repeat gp_project_set with " + PROJECTS_FILE_ENV + " pointed at <that root>/projects.json): no restart of the service will make it read anywhere else, so restarting again changes nothing. If they name no --state-dir, the service is on the default root and simply predates this write, and one restart with the command in this notice is what surfaces it.";
@@ -2236,7 +2262,7 @@ var PID_INT32_MAX = 2147483647;
2236
2262
  var OptionalPidSchema = z10.number().int().min(1).max(PID_INT32_MAX).optional();
2237
2263
  var OptionalDepthSchema = z10.number().int().min(1).max(10).optional();
2238
2264
  var OptionalRoleSchema = z10.string().min(1).max(SELECTOR_MAX_LENGTH).optional();
2239
- var OptionalBundleIdSchema = z10.string().max(512).optional();
2265
+ var OptionalBundleIdSchema = z10.string().max(BUNDLE_ID_MAX_LENGTH).optional();
2240
2266
  var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
2241
2267
  var OptionalOperationIdSchema = z10.string().regex(OPERATION_ID_PATTERN2).optional();
2242
2268
  var OptionalProjectIdSchema = z10.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
@@ -2352,7 +2378,7 @@ var TOOL_SPECS = [
2352
2378
  type: "object",
2353
2379
  additionalProperties: false,
2354
2380
  properties: {
2355
- bundleId: { type: "string", maxLength: 512 },
2381
+ bundleId: { type: "string", maxLength: BUNDLE_ID_MAX_LENGTH },
2356
2382
  pid: { type: "integer", minimum: 1, maximum: PID_INT32_MAX },
2357
2383
  projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
2358
2384
  },
@@ -2578,7 +2604,9 @@ var TOOL_SPECS = [
2578
2604
  displayName: { type: "string", minLength: 1, maxLength: 256 },
2579
2605
  // `null` clears the field on update, so these mirror
2580
2606
  // ProjectSetArgs' `.nullish()` rather than the old optional-string form.
2581
- bundleId: { type: ["string", "null"], maxLength: 256 },
2607
+ // The ceiling is `gp_attach`'s, from the daemon's own bound — the identity a
2608
+ // caller can attach with has to be one the registry can store.
2609
+ bundleId: { type: ["string", "null"], maxLength: BUNDLE_ID_MAX_LENGTH },
2582
2610
  // Matches ProjectSetArgs.pid, which is bounded to the same Int32 domain
2583
2611
  // the daemon stores (an out-of-range pid made Swift fail to decode the
2584
2612
  // whole registry file, which then got rewritten as an empty table).
@@ -2654,6 +2682,14 @@ function narrowingKnobsFor(spec, params) {
2654
2682
  knobs.push({ name, kind: "scale", minimum: schema.minimum, maximum: schema.maximum, sent });
2655
2683
  } else if (name === "limit") {
2656
2684
  knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
2685
+ } else if (name === "steps") {
2686
+ knobs.push({
2687
+ name,
2688
+ kind: "steps",
2689
+ minimum: schema.minItems,
2690
+ maximum: schema.maxItems,
2691
+ sent: Array.isArray(sentValue) ? sentValue.length : void 0
2692
+ });
2657
2693
  } else if (name === "selector" || name === "role" || name === "title") {
2658
2694
  const target = targetSelectingKnob(spec.name, name);
2659
2695
  knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
@@ -2681,6 +2717,8 @@ function knobSubject(kind) {
2681
2717
  return "the number of entries it collects";
2682
2718
  case "scale":
2683
2719
  return "the image";
2720
+ case "steps":
2721
+ return "the replay it performs";
2684
2722
  case "selector":
2685
2723
  return "the reply";
2686
2724
  case "target":
@@ -2701,6 +2739,8 @@ function describeKnob(knob) {
2701
2739
  return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
2702
2740
  case "scale":
2703
2741
  return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
2742
+ case "steps":
2743
+ return `a shorter steps array${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent} ${knob.sent === 1 ? "step" : "steps"}` : ""}`;
2704
2744
  case "target":
2705
2745
  return describeTargetKnob(knob);
2706
2746
  }
@@ -2870,9 +2910,9 @@ function mapEvidenceReadError(error) {
2870
2910
  }
2871
2911
  return void 0;
2872
2912
  }
2873
- async function projectListTool() {
2913
+ async function projectListTool(_args, context) {
2874
2914
  try {
2875
- const projects = projectList();
2915
+ const projects = projectList(context.engine.reportedStateRoot);
2876
2916
  return {
2877
2917
  content: [{ type: "text", text: canonicalJson2({ projects }) }],
2878
2918
  isError: false
@@ -2881,18 +2921,18 @@ async function projectListTool() {
2881
2921
  return mapProjectError(error);
2882
2922
  }
2883
2923
  }
2884
- async function projectSetTool(args) {
2924
+ async function projectSetTool(args, context) {
2885
2925
  try {
2886
- const entry = projectSet(args);
2926
+ const entry = projectSet(args, context.engine.reportedStateRoot);
2887
2927
  return { content: [{ type: "text", text: canonicalJson2({ project: entry }) }], isError: false };
2888
2928
  } catch (error) {
2889
2929
  return mapProjectError(error);
2890
2930
  }
2891
2931
  }
2892
- async function projectGetTool(args) {
2932
+ async function projectGetTool(args, context) {
2893
2933
  const argv = args;
2894
2934
  try {
2895
- const entry = projectGet(argv.projectId);
2935
+ const entry = projectGet(argv.projectId, context.engine.reportedStateRoot);
2896
2936
  if (entry === void 0) {
2897
2937
  return {
2898
2938
  content: [{ type: "text", text: formatToolError(
@@ -2945,7 +2985,7 @@ function projectErrorRemedy(code) {
2945
2985
  return "this process has no entry in the password database, so neither it nor the daemon can know which user's state directory is meant; run the shell as a user that has one (`id -u` / `dscl . -read /Users/<name> NFSHomeDirectory`). Nothing in ~/.glasspane needs repairing for this";
2946
2986
  }
2947
2987
  if (code === GP_E_INTERNAL) {
2948
- return `the projects file is the problem, not your arguments: the message above names its path, so read it with \`/usr/bin/plutil -p <that path>\` or \`/bin/cat <that path>\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH \u2014 repair or restore the damaged entry, and retry \u2014 or set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set to rebuild the registry from scratch (the unloadable file is moved aside as <path>.unreadable-<id>, never deleted, and every entry still in it is lost). Do not retry the same read-only call unchanged: it will keep failing.`;
2988
+ return `the projects file is the problem, not your arguments: the message above names its path, so read it with ${registryReaders("<that path>")}; repair or restore the damaged entry, and retry \u2014 or set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set to rebuild the registry from scratch (the unloadable file is moved aside as <path>.unreadable-<id>, never deleted, and every entry still in it is lost). Do not retry the same read-only call unchanged: it will keep failing.`;
2949
2989
  }
2950
2990
  return "check the tool's input schema and retry";
2951
2991
  }
@@ -3052,7 +3092,14 @@ function describeFetchFailure(error) {
3052
3092
  const mapped = mapEvidenceReadError(error);
3053
3093
  if (mapped !== void 0) {
3054
3094
  const [first] = mapped.content;
3055
- return first !== void 0 && first.type === "text" ? first.text : "the evidence read failed";
3095
+ if (first !== void 0 && first.type === "text") {
3096
+ return first.text;
3097
+ }
3098
+ return formatToolError(
3099
+ GP_E_INTERNAL,
3100
+ "a mapped evidence-read refusal reached gp_recent_reports without any text part, so its own message could not be carried",
3101
+ "read that operation on its own with gp_last_evidence {operationId}, which reports its own fault in the three-field shape; do not re-run gp_act"
3102
+ );
3056
3103
  }
3057
3104
  return formatToolError(
3058
3105
  GP_E_INTERNAL,
@@ -3585,8 +3632,8 @@ async function handleEvidenceAggregate(url, client, res) {
3585
3632
  res,
3586
3633
  404,
3587
3634
  GP_E_NO_EVIDENCE,
3588
- "none of the requested operations have evidence in the daemon history",
3589
- `the engine may have restarted, or those operationIds never produced evidence; each id named here was fetched and missed, and this gateway keeps no operation trail of its own, so it cannot list what ran. Producing a fresh pack means performing another action, which a read like this one does not do for you: see what the attached app is doing now with POST /v1/tools/observe, POST /v1/tools/assert_element or POST /v1/tools/probe_status, re-fetch one id with GET /v1/evidence/<operationId>, and the routes this surface answers are ${TOOL_ROUTES_TEXT}`
3635
+ `none of the ${ids.length} requested ${ids.length === 1 ? "operation has" : "operations have"} a pack in the daemon history: each id this request named was fetched and answered with that code`,
3636
+ `the daemon holding no pack for these operations is a fact about its bounded history, not evidence that they did not run, and this request measured nothing about a restart, so no cause is claimed here. Each id named above was fetched and missed, and this gateway keeps no operation trail of its own, so it cannot list what ran. Producing a fresh pack means performing another action, which a read like this one does not do for you: see what the attached app is doing now with POST /v1/tools/observe, POST /v1/tools/assert_element or POST /v1/tools/probe_status, re-fetch one id with GET /v1/evidence/<operationId>, ask again with fewer ids (at most ${MAX_EVIDENCE_IDS} per request), and the routes this surface answers are ${TOOL_ROUTES_TEXT}`
3590
3637
  );
3591
3638
  return;
3592
3639
  }