@skeletiq/mcp 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +9 -7
  2. package/dist/index.js +896 -222
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,12 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
 
6
6
  // src/config.ts
7
7
  var SERVER_NAME = "skeletiq";
8
- var SERVER_VERSION = "0.3.0";
8
+ var SERVER_VERSION = "0.5.0";
9
+ var AGENT_SCHEMA_VERSION = 2;
10
+ function schemaNote(serverVersion) {
11
+ if (serverVersion === void 0 || serverVersion <= AGENT_SCHEMA_VERSION) return null;
12
+ return "A newer @skeletiq/mcp understands more of this server; update when you can.";
13
+ }
9
14
  var DEFAULT_API_URL = "https://api.skeletiq.com";
10
15
  var TOKEN_PREFIX = "skq_";
11
16
  var ConfigError = class extends Error {
@@ -33,8 +38,8 @@ function normalizeApiUrl(raw) {
33
38
  } catch {
34
39
  throw new ConfigError(`SKELETIQ_API_URL is not a valid URL: ${value}`);
35
40
  }
36
- const path = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
37
- return `${url.origin}${path}`;
41
+ const path2 = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
42
+ return `${url.origin}${path2}`;
38
43
  }
39
44
 
40
45
  // src/server.ts
@@ -60,12 +65,12 @@ var SkeletiqApiError = class extends Error {
60
65
  /**
61
66
  * A `GET` may be retried after a 429; a `POST` that may have spent credits may not.
62
67
  *
63
- * A spent daily allowance is the one 429 that is *not* retryable: it refills on a clock
64
- * measured in hours, so a caller backing off and trying again is burning its budget on a
65
- * call that cannot succeed today.
68
+ * A spent daily allowance is the 429 that is *not* retryable — the account's, or a runtime's
69
+ * shared one: each refills on a clock measured in hours, so a caller backing off and trying
70
+ * again is burning its budget on a call that cannot succeed today.
66
71
  */
67
72
  get isRetryable() {
68
- if (this.code === "DAILY_LIMIT_REACHED") return false;
73
+ if (this.code === "DAILY_LIMIT_REACHED" || this.code === "RUNTIME_ALLOWANCE_SPENT") return false;
69
74
  return this.status === 429 || this.code === "IDEMPOTENCY_UNAVAILABLE";
70
75
  }
71
76
  };
@@ -138,6 +143,12 @@ function describeApiError(error) {
138
143
  if (upgradeUrl) lines.push(`A larger plan raises the daily limit: ${upgradeUrl}`);
139
144
  break;
140
145
  }
146
+ case "RUNTIME_ALLOWANCE_SPENT": {
147
+ lines.push(error.message);
148
+ const resetAt = asString(detail?.reset_at);
149
+ if (resetAt) lines.push(`The allowance resets at ${resetAt}.`);
150
+ break;
151
+ }
141
152
  case "PLAN_LIMIT_EXCEEDED":
142
153
  case "FEATURE_NOT_AVAILABLE":
143
154
  case "VERSION_LOCKED": {
@@ -195,14 +206,14 @@ var SkeletiqClient = class {
195
206
  this.config = config;
196
207
  }
197
208
  /** A parsed JSON response, or a thrown `SkeletiqApiError` carrying the server's own words. */
198
- async request(path, options = {}) {
199
- const response = await this.fetch(path, options);
209
+ async request(path2, options = {}) {
210
+ const response = await this.fetch(path2, options);
200
211
  const text = await response.text();
201
212
  return text ? JSON.parse(text) : void 0;
202
213
  }
203
214
  /** The raw `Response`, for the one caller that reads a stream rather than a body. */
204
- async fetch(path, options = {}) {
205
- const url = this.url(path, options.query);
215
+ async fetch(path2, options = {}) {
216
+ const url = this.url(path2, options.query);
206
217
  const timeout = AbortSignal.timeout(READ_TIMEOUT_MS);
207
218
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
208
219
  let response;
@@ -226,8 +237,8 @@ var SkeletiqClient = class {
226
237
  if (!response.ok) throw await this.toError(response);
227
238
  return response;
228
239
  }
229
- url(path, query) {
230
- const url = new URL(`${this.config.apiUrl}${API_PREFIX}${path}`);
240
+ url(path2, query) {
241
+ const url = new URL(`${this.config.apiUrl}${API_PREFIX}${path2}`);
231
242
  for (const [key, value] of Object.entries(query ?? {})) {
232
243
  if (value !== void 0 && value !== "") url.searchParams.set(key, String(value));
233
244
  }
@@ -284,7 +295,19 @@ var ComponentSchema = z.looseObject({
284
295
  role: z.string().nullable().optional(),
285
296
  /** Why the component is in the design (`core`, `security`, …) — see the API's `component_concerns`. */
286
297
  concern: z.string().nullable().optional(),
287
- requirement_ids: z.array(z.string()).nullable().optional()
298
+ requirement_ids: z.array(z.string()).nullable().optional(),
299
+ /** Ids of the decision records this component realizes, e.g. `database.primary`. */
300
+ decision_ids: z.array(z.string()).nullable().optional(),
301
+ /** `domain` | `platform`. Left as a string: a value this package has not seen is still shown. */
302
+ layer: z.string().nullish(),
303
+ /** What a platform part is (`waf`, `api_gateway`, …). Left as a string for the same reason. */
304
+ platform_role: z.string().nullish(),
305
+ /** `realized` (the platform added it) | `adopted` (the design drew it and the platform kept it). */
306
+ platform_origin: z.string().nullish(),
307
+ /** Ids of the platform obligations this part meets. */
308
+ satisfies: z.array(z.string()).nullish(),
309
+ /** Cross-cutting settings realized on the component (encryption, backup, rate limit, …). */
310
+ policies: z.record(z.string(), z.unknown()).nullish()
288
311
  });
289
312
  var ConnectionSchema = z.looseObject({
290
313
  id: z.string().nullable().optional(),
@@ -292,7 +315,11 @@ var ConnectionSchema = z.looseObject({
292
315
  target: z.string(),
293
316
  label: z.string().nullable().optional(),
294
317
  protocol: z.string().nullable().optional(),
295
- is_async: z.boolean().optional()
318
+ is_async: z.boolean().optional(),
319
+ /** TLS, timeout and breaker settings on the edge. */
320
+ policies: z.record(z.string(), z.unknown()).nullish(),
321
+ /** `realized` on an edge the platform added. */
322
+ platform_origin: z.string().nullish()
296
323
  });
297
324
  var GapEntrySchema = z.union([
298
325
  z.string(),
@@ -320,7 +347,11 @@ var ArchitectureJsonSchema = z.looseObject({
320
347
  trade_offs: z.array(z.string()).nullable().optional(),
321
348
  scalability_notes: z.string().nullable().optional(),
322
349
  assumptions: z.array(GapEntrySchema).nullable().optional(),
323
- open_questions: z.array(GapEntrySchema).nullable().optional()
350
+ open_questions: z.array(GapEntrySchema).nullable().optional(),
351
+ /** Drawn connections the platform layer replaced. */
352
+ bypassed_connections: z.array(ConnectionSchema).nullish(),
353
+ /** The `design_decisions` entries the platform layer appended. */
354
+ platform_decisions: z.array(z.string()).nullish()
324
355
  });
325
356
  var RequirementLedgerRowSchema = z.looseObject({
326
357
  id: z.string(),
@@ -344,9 +375,27 @@ var RequirementLedgerRowSchema = z.looseObject({
344
375
  /** The model behind a `verifier` verdict. Null on every deterministic row — naming a model
345
376
  * beside a number the server computed would be a false attribution. */
346
377
  checked_by: z.string().nullable().optional(),
378
+ /** Whether this row holds a handoff back: a `fails` a measurement reached (capacity, cost, the
379
+ * review's rules). A model check's `fails` does not block. Absent from older APIs, where
380
+ * `requirementBlocksHandoff` falls back to the method. */
381
+ blocks_handoff: z.boolean().optional(),
382
+ /** Why a `fails` does not block: `model_check` — only the model check failed it, nothing
383
+ * measured it; `unstated_input` — a shortfall that turns on an input nobody gave (an unstated
384
+ * split, or an assumed load — told apart by `unstated_input`); `advisory_finding` — only review
385
+ * advice failed it. Null on every other row. A string, for the reason `status` is one. Absent
386
+ * from older APIs. */
387
+ unconfirmed_reason: z.string().nullable().optional(),
388
+ /** True on a review-rules `fails` whose every finding is advice (`advisory_finding`). */
389
+ advisory_only: z.boolean().optional(),
390
+ /** The assumption an `unstated_input` row turns on, as a clause. Absent from older APIs. */
391
+ unstated_input: z.string().nullable().optional(),
347
392
  cited_by_components: z.array(z.string()).nullable().optional(),
348
393
  /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
349
394
  cited_by_decisions: z.array(z.number()).nullable().optional(),
395
+ /** `stated` in the person's own words, `proposed` by the analysis, or `accepted` — a proposal a
396
+ * person confirmed. Absent from older APIs. Left as a string: a value this package has not seen
397
+ * must not stop the whole design from parsing. */
398
+ origin: z.string().nullish(),
350
399
  added_in_version: z.number().nullable().optional(),
351
400
  /** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
352
401
  * recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
@@ -463,7 +512,13 @@ var ReadinessRowSchema = z.looseObject({
463
512
  state: z.string(),
464
513
  category: z.string(),
465
514
  gating: z.boolean(),
466
- detail: z.string().nullable().optional()
515
+ detail: z.string().nullable().optional(),
516
+ /** On a row that could not be answered: what would answer it ("Saving a change runs the
517
+ * check on the new version."). Absent on a row that has an answer. */
518
+ clears: z.string().nullable().optional(),
519
+ /** True when stating the expected load is what would answer this row — `clears` then says
520
+ * how. Optional so a server that predates the field still parses. */
521
+ clears_by_load: z.boolean().optional()
467
522
  });
468
523
  var ReadinessSchema = z.looseObject({
469
524
  architecture_id: z.string(),
@@ -480,6 +535,14 @@ var ReadinessSchema = z.looseObject({
480
535
  * caller falls back to the boolean.
481
536
  */
482
537
  verdict: z.enum(["ready", "outstanding", "unanswered"]).optional(),
538
+ /** The verdict-first summary of the rows. Absent from servers that predate it. */
539
+ headline: z.looseObject({
540
+ met: z.number(),
541
+ total: z.number(),
542
+ verdict: z.enum(["ready", "outstanding", "unanswered"]),
543
+ score: z.number(),
544
+ rules_updated: z.boolean().optional()
545
+ }).nullable().optional(),
483
546
  unknown_gate_count: z.number().optional(),
484
547
  /** Keys of advisory rows whose check never ran. Named, never counted. */
485
548
  unrun_checks: z.array(z.string()).optional(),
@@ -516,7 +579,15 @@ var DesignGapSchema = z.looseObject({
516
579
  problem_key: z.string().optional(),
517
580
  /** An answer is recorded and this version was not built with it, so iterating the design
518
581
  * would change something. False once a generation has been handed the answer. */
519
- answer_pending: z.boolean().optional()
582
+ answer_pending: z.boolean().optional(),
583
+ /** The read/write mix or peak this assumption states that the design is not sized on. Null when
584
+ * the two agree; absent from older servers. */
585
+ sizing_conflict: z.looseObject({
586
+ dimension: z.string(),
587
+ assumed: z.string(),
588
+ sized: z.string(),
589
+ resize_to: z.string().nullable().optional()
590
+ }).nullable().optional()
520
591
  });
521
592
  var OrphanedAnswerSchema = z.looseObject({
522
593
  gap_id: z.string(),
@@ -612,7 +683,10 @@ var TodoItemSchema = z.looseObject({
612
683
  * it goes back further than the server looked — "an earlier version", never version zero. */
613
684
  carried_from_version: z.number().nullable().optional(),
614
685
  dismissed: z.boolean().optional(),
615
- dismissed_reason: z.string().nullable().optional()
686
+ dismissed_reason: z.string().nullable().optional(),
687
+ /** The key of the one readiness row this card is counted on, so the readiness counts can be
688
+ * traced to cards. Null on a card no row counts. */
689
+ counts_toward: z.string().nullable().optional()
616
690
  });
617
691
  var TodoGroupSchema = z.looseObject({
618
692
  key: z.string(),
@@ -657,7 +731,76 @@ var DriftSchema = z.looseObject({
657
731
  extra_components: z.array(z.looseObject({ name: z.string() })),
658
732
  extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
659
733
  counts: z.record(z.string(), z.number()),
660
- release: ReleaseFactsSchema
734
+ release: ReleaseFactsSchema,
735
+ /** True when the server read the evidence the connector checked. Absent from older servers. */
736
+ evidence_checked: z.boolean().optional(),
737
+ /** Covered components reported implemented with no verified citation. */
738
+ unverified: z.array(ComponentRefSchema).optional(),
739
+ fitness_failing: z.array(z.string()).optional(),
740
+ fitness_unverified: z.array(z.string()).optional(),
741
+ fitness_unknown: z.array(z.string()).optional(),
742
+ schema_version: z.number().optional()
743
+ });
744
+ var DecisionRequestSchema = z.looseObject({
745
+ id: z.string(),
746
+ project_id: z.string(),
747
+ architecture_id: z.string().nullable(),
748
+ version: z.number(),
749
+ decision_id: z.string(),
750
+ decision_title: z.string(),
751
+ proposed: z.string(),
752
+ why: z.string(),
753
+ status: z.enum(["pending", "accepted", "dismissed"]),
754
+ resolution_note: z.string().nullable(),
755
+ accepted_in_version: z.number().nullable(),
756
+ latest_version: z.number(),
757
+ decision_current: z.boolean().optional(),
758
+ schema_version: z.number().optional()
759
+ });
760
+ var FitnessTestSchema = z.looseObject({
761
+ id: z.string(),
762
+ source: z.string(),
763
+ obligation_id: z.string().nullable(),
764
+ subject_ids: z.array(z.string()),
765
+ subject_names: z.array(z.string()),
766
+ assertion: z.string(),
767
+ check: z.string(),
768
+ level: z.string(),
769
+ layer: z.string().nullable()
770
+ });
771
+ var FitnessTestsSchema = z.looseObject({
772
+ architecture_id: z.string(),
773
+ version: z.number(),
774
+ schema_version: z.number().optional(),
775
+ tests: z.array(FitnessTestSchema),
776
+ total: z.number(),
777
+ truncated: z.boolean(),
778
+ unavailable: z.string().nullable().optional()
779
+ });
780
+ var DecisionOptionSchema = z.looseObject({
781
+ id: z.string(),
782
+ label: z.string(),
783
+ rejected_because: z.string().nullable().optional()
784
+ });
785
+ var DecisionRecordViewSchema = z.looseObject({
786
+ id: z.string(),
787
+ title: z.string(),
788
+ chosen: z.string(),
789
+ why: z.string(),
790
+ options: z.array(DecisionOptionSchema),
791
+ basis_removed: z.array(z.string()).default([])
792
+ });
793
+ var DecisionsViewResponseSchema = z.looseObject({
794
+ architecture_id: z.string(),
795
+ version: z.number(),
796
+ status: z.string(),
797
+ reason: z.string().nullable().optional(),
798
+ records: z.array(DecisionRecordViewSchema).default([])
799
+ });
800
+ var ClarifyingOptionDetailSchema = z.looseObject({
801
+ value: z.string(),
802
+ label: z.string(),
803
+ detail: z.string().optional()
661
804
  });
662
805
  var ClarifyingQuestionSchema = z.looseObject({
663
806
  id: z.string(),
@@ -665,7 +808,19 @@ var ClarifyingQuestionSchema = z.looseObject({
665
808
  why: z.string().optional(),
666
809
  kind: z.string().optional(),
667
810
  options: z.array(z.string()).nullable().optional(),
668
- decline_options: z.array(z.string()).nullable().optional()
811
+ decline_options: z.array(z.string()).nullable().optional(),
812
+ /** Options that withdraw the instruction to be rewritten, not answer it ("I'll name the parts"). */
813
+ dismiss_options: z.array(z.string()).nullable().optional(),
814
+ /** Each option's label and what it means, keyed by the value in `options`. */
815
+ option_details: z.array(ClarifyingOptionDetailSchema).nullable().optional(),
816
+ /** Whether more than one option may be picked at once. */
817
+ multiple: z.boolean().optional(),
818
+ /** What leaving the question unanswered does, in the product's words. */
819
+ skip_effect: z.string().nullable().optional(),
820
+ /** The contract field this question resolves, e.g. `regimes.hipaa`; absent when it is not a fork. */
821
+ fork: z.string().nullish(),
822
+ /** Ask only when `question_id` was answered with one of `values`. */
823
+ asked_if: z.object({ question_id: z.string(), values: z.array(z.string()) }).nullish()
669
824
  });
670
825
  var IntentDecisionSchema = z.looseObject({
671
826
  intent: z.string().optional(),
@@ -846,7 +1001,80 @@ function isUuid(value) {
846
1001
  }
847
1002
 
848
1003
  // src/tools/check-drift.ts
1004
+ import * as z3 from "zod/v4";
1005
+
1006
+ // src/lib/evidence.ts
1007
+ import { realpath, readFile, stat } from "fs/promises";
1008
+ import path from "path";
849
1009
  import * as z2 from "zod/v4";
1010
+ var MAX_EVIDENCE_BYTES = 1e6;
1011
+ var MAX_REFS_PER_CLAIM = 5;
1012
+ var evidenceRef = z2.object({
1013
+ path: z2.string().min(1).max(512).describe("The file, relative to the repo root."),
1014
+ quote: z2.string().min(1).max(2e3).optional().describe("Text that must appear in the file. Checked locally, never sent."),
1015
+ symbol: z2.string().min(1).max(255).optional().describe("An identifier that must appear in the file. Checked locally, never sent.")
1016
+ }).refine((ref) => ref.quote !== void 0 || ref.symbol !== void 0, {
1017
+ message: "each evidence entry needs a quote or a symbol"
1018
+ });
1019
+ async function resolveRepoRoot(requested) {
1020
+ try {
1021
+ const root = await realpath(requested);
1022
+ return (await stat(root)).isDirectory() ? root : null;
1023
+ } catch {
1024
+ return null;
1025
+ }
1026
+ }
1027
+ var MAX_PATH = 512;
1028
+ var MAX_REASON = 200;
1029
+ function collapse(text) {
1030
+ return text.replace(/\s+/g, " ").trim();
1031
+ }
1032
+ function escapeRegExp(text) {
1033
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1034
+ }
1035
+ function inside(root, target) {
1036
+ const relative = path.relative(root, target);
1037
+ return relative === "" || relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
1038
+ }
1039
+ async function verifyEvidence(root, ref) {
1040
+ const shown = ref.path.slice(0, MAX_PATH);
1041
+ const failed = (reason) => ({
1042
+ path: shown,
1043
+ status: "failed",
1044
+ reason: reason.slice(0, MAX_REASON)
1045
+ });
1046
+ try {
1047
+ const realRoot = await realpath(root);
1048
+ const lexical = path.resolve(realRoot, ref.path);
1049
+ if (!inside(realRoot, lexical)) return failed("outside the repository");
1050
+ let target;
1051
+ try {
1052
+ target = await realpath(lexical);
1053
+ } catch (error) {
1054
+ if (error.code === "ENOENT") return failed("file not found");
1055
+ throw error;
1056
+ }
1057
+ if (!inside(realRoot, target)) return failed("outside the repository");
1058
+ const info = await stat(target);
1059
+ if (!info.isFile()) return failed("not a regular file");
1060
+ if (info.size > MAX_EVIDENCE_BYTES) return failed(`file too large (over ${MAX_EVIDENCE_BYTES} bytes)`);
1061
+ const quote = ref.quote === void 0 ? "" : collapse(ref.quote);
1062
+ const symbol = ref.symbol?.trim() ?? "";
1063
+ if (!quote && !symbol) return failed("nothing cited to look for");
1064
+ const content = await readFile(target, "utf8");
1065
+ if (quote && !collapse(content).includes(quote)) {
1066
+ return failed("cited text not found in file");
1067
+ }
1068
+ if (symbol) {
1069
+ const pattern = new RegExp(`(?<![\\w$])${escapeRegExp(symbol)}(?![\\w$])`);
1070
+ if (!pattern.test(content)) return failed("symbol not found in file");
1071
+ }
1072
+ return { path: shown, status: "verified" };
1073
+ } catch (error) {
1074
+ const code = error?.code;
1075
+ return failed(typeof code === "string" ? `file could not be read (${code})` : "file could not be read");
1076
+ }
1077
+ }
850
1078
 
851
1079
  // src/lib/result.ts
852
1080
  function ok(structured, text) {
@@ -879,53 +1107,70 @@ function versionLine(facts) {
879
1107
  }
880
1108
 
881
1109
  // src/tools/check-drift.ts
882
- var input = z2.object({
883
- project_id: z2.string().describe("A SkeletIQ project id, or its exact name."),
884
- version: z2.number().int().positive().optional().describe(
1110
+ var input = z3.object({
1111
+ project_id: z3.string().describe("A SkeletIQ project id, or its exact name."),
1112
+ version: z3.number().int().positive().optional().describe(
885
1113
  "The version you built from \u2014 the `version=` stamped in the AGENTS.md fence. Defaults to the latest released version. Component ids are only meaningful within one version."
886
1114
  ),
887
- covers: z2.array(z2.string()).max(500).optional().describe(
1115
+ repo_root: z3.string().optional().describe("The repository root that evidence paths are relative to. Defaults to the current directory."),
1116
+ covers: z3.array(z3.string()).max(500).optional().describe(
888
1117
  "The component ids this repository is responsible for. Omit only when this repo implements the whole design \u2014 otherwise everything you leave out is reported as missing."
889
1118
  ),
890
- components: z2.array(
891
- z2.object({
892
- id: z2.string().max(200).describe("The component id from the design."),
893
- name: z2.string().max(255).optional().describe("The component name, which survives a regeneration when the id does not."),
894
- status: z2.enum(["implemented", "partial", "not_started"]),
895
- note: z2.string().max(2e3).optional()
1119
+ components: z3.array(
1120
+ z3.object({
1121
+ id: z3.string().max(200).describe("The component id from the design."),
1122
+ name: z3.string().max(255).optional().describe("The component name, which survives a regeneration when the id does not."),
1123
+ status: z3.enum(["implemented", "partial", "not_started"]),
1124
+ note: z3.string().max(2e3).optional(),
1125
+ evidence: z3.array(evidenceRef).max(MAX_REFS_PER_CLAIM).optional().describe("Where in the code this component is built. Verified on this machine; only the path and outcome are sent.")
896
1126
  })
897
1127
  ).max(500).describe("What you built. Report every component in `covers`, including the ones not started."),
898
- extra_components: z2.array(
899
- z2.object({
900
- name: z2.string().max(255),
901
- type: z2.string().max(64).optional(),
902
- technology: z2.string().max(255).optional()
1128
+ extra_components: z3.array(
1129
+ z3.object({
1130
+ name: z3.string().max(255),
1131
+ type: z3.string().max(64).optional(),
1132
+ technology: z3.string().max(255).optional()
903
1133
  })
904
1134
  ).max(200).optional().describe("Things in the code that the design does not contain."),
905
- extra_connections: z2.array(
906
- z2.object({
907
- source: z2.string().max(255),
908
- target: z2.string().max(255),
909
- protocol: z2.string().max(64).optional()
1135
+ extra_connections: z3.array(
1136
+ z3.object({
1137
+ source: z3.string().max(255),
1138
+ target: z3.string().max(255),
1139
+ protocol: z3.string().max(64).optional()
910
1140
  })
911
1141
  ).max(500).optional().describe(
912
1142
  "Edges in the code the design does not have, each from the caller to the callee: the component that holds the client is the source, including a consumer reading a queue. Endpoints may be component ids or names."
913
- )
1143
+ ),
1144
+ fitness: z3.array(
1145
+ z3.object({
1146
+ test_id: z3.string().max(300).describe("The id of a fitness test."),
1147
+ status: z3.enum(["pass", "fail", "not_written"]),
1148
+ test_path: z3.string().min(1).max(512).optional().describe("The file holding the test."),
1149
+ quote: z3.string().min(1).max(2e3).optional().describe("Text that must appear in that file. Checked locally, never sent.")
1150
+ })
1151
+ ).max(500).optional().describe("Your result for each fitness test you wrote or could not write.")
914
1152
  });
915
- var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
916
- var output = z2.object({
917
- architecture_id: z2.string(),
918
- version: z2.number(),
919
- is_released: z2.boolean(),
920
- newer_release_exists: z2.boolean(),
921
- resolved_by: z2.string(),
922
- in_sync: z2.boolean(),
1153
+ var refs = z3.array(z3.object({ id: z3.string(), name: z3.string(), type: z3.string() }));
1154
+ var output = z3.object({
1155
+ architecture_id: z3.string(),
1156
+ version: z3.number(),
1157
+ is_released: z3.boolean(),
1158
+ newer_release_exists: z3.boolean(),
1159
+ resolved_by: z3.string(),
1160
+ in_sync: z3.boolean(),
923
1161
  missing: refs,
924
1162
  unreported: refs,
925
1163
  partial: refs,
926
1164
  elsewhere: refs,
927
- unknown_ids: z2.array(z2.object({ id: z2.string(), suggestions: refs })),
928
- counts: z2.record(z2.string(), z2.number())
1165
+ unknown_ids: z3.array(z3.object({ id: z3.string(), suggestions: refs })),
1166
+ counts: z3.record(z3.string(), z3.number()),
1167
+ evidence_checked: z3.boolean(),
1168
+ unverified: refs,
1169
+ fitness_failing: z3.array(z3.string()),
1170
+ fitness_unverified: z3.array(z3.string()),
1171
+ fitness_unknown: z3.array(z3.string()),
1172
+ checked_against: z3.string(),
1173
+ schema_version: z3.number().nullable()
929
1174
  });
930
1175
  function registerCheckDrift(server, client, resolver) {
931
1176
  server.registerTool(
@@ -937,7 +1182,23 @@ function registerCheckDrift(server, client, resolver) {
937
1182
  outputSchema: output,
938
1183
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
939
1184
  },
940
- async ({ project_id, version, covers, components, extra_components, extra_connections }) => guard(async () => {
1185
+ async ({ project_id, version, repo_root, covers, components, extra_components, extra_connections, fitness }) => guard(async () => {
1186
+ const requested = repo_root ?? process.cwd();
1187
+ const root = await resolveRepoRoot(requested);
1188
+ if (root === null) return failure(`repo_root ${requested} cannot be read`);
1189
+ const checked = await Promise.all(
1190
+ components.map(async ({ evidence, ...component }) => ({
1191
+ ...component,
1192
+ ...evidence ? { evidence: await Promise.all(evidence.map((ref) => verifyEvidence(root, ref))) } : {}
1193
+ }))
1194
+ );
1195
+ const fitnessChecked = fitness ? await Promise.all(
1196
+ fitness.map(async (entry) => ({
1197
+ test_id: entry.test_id,
1198
+ status: entry.status,
1199
+ evidence: entry.test_path && entry.quote ? [await verifyEvidence(root, { path: entry.test_path, quote: entry.quote })] : []
1200
+ }))
1201
+ ) : null;
941
1202
  const project = await resolver.resolveProject(project_id);
942
1203
  const resolved = await resolver.resolveVersion(project.id, version);
943
1204
  const report = DriftSchema.parse(
@@ -945,9 +1206,10 @@ function registerCheckDrift(server, client, resolver) {
945
1206
  method: "POST",
946
1207
  body: {
947
1208
  covers: covers ?? null,
948
- components,
1209
+ components: checked,
949
1210
  extra_components: extra_components ?? null,
950
- extra_connections: extra_connections ?? null
1211
+ extra_connections: extra_connections ?? null,
1212
+ fitness: fitnessChecked
951
1213
  }
952
1214
  })
953
1215
  );
@@ -964,15 +1226,34 @@ function registerCheckDrift(server, client, resolver) {
964
1226
  id: entry.id,
965
1227
  suggestions: entry.suggestions ?? []
966
1228
  })),
967
- counts: report.counts
1229
+ counts: report.counts,
1230
+ evidence_checked: report.evidence_checked === true,
1231
+ unverified: report.unverified ?? [],
1232
+ fitness_failing: report.fitness_failing ?? [],
1233
+ fitness_unverified: report.fitness_unverified ?? [],
1234
+ fitness_unknown: report.fitness_unknown ?? [],
1235
+ checked_against: root,
1236
+ schema_version: report.schema_version ?? null
968
1237
  };
969
1238
  const lines = [];
970
1239
  lines.push(
971
1240
  report.in_sync ? "In sync \u2014 everything this repository covers is built as designed." : "Drift found:"
972
1241
  );
973
1242
  push(lines, "Not started", report.missing, report.counts.missing);
1243
+ push(lines, "Unverified", structured.unverified, report.counts.unverified);
974
1244
  push(lines, "Half built", report.partial, report.counts.partial);
975
1245
  push(lines, "You did not mention", report.unreported, report.counts.unreported);
1246
+ if (structured.fitness_failing.length) {
1247
+ lines.push(`Fitness failing (${structured.fitness_failing.length}): ${structured.fitness_failing.join(", ")}`);
1248
+ }
1249
+ if (structured.fitness_unverified.length) {
1250
+ lines.push(
1251
+ `Fitness unverified (${structured.fitness_unverified.length}): ${structured.fitness_unverified.join(", ")}`
1252
+ );
1253
+ }
1254
+ if (structured.fitness_unknown.length) {
1255
+ lines.push(`Fitness unknown (${structured.fitness_unknown.length}): ${structured.fitness_unknown.join(", ")}`);
1256
+ }
976
1257
  if (report.extra_components.length) {
977
1258
  lines.push(`In the code but not the design: ${report.extra_components.map((c) => c.name).join(", ")}`);
978
1259
  }
@@ -998,6 +1279,12 @@ function registerCheckDrift(server, client, resolver) {
998
1279
  );
999
1280
  }
1000
1281
  lines.push("");
1282
+ if (!structured.evidence_checked) {
1283
+ lines.push("This SkeletIQ server does not check evidence yet; these are unverified claims.");
1284
+ }
1285
+ lines.push(`Checked against ${root}`);
1286
+ const note = schemaNote(report.schema_version);
1287
+ if (note) lines.push(note);
1001
1288
  lines.push(versionLine(facts));
1002
1289
  return ok(structured, lines.join("\n"));
1003
1290
  })
@@ -1010,53 +1297,174 @@ function push(lines, label, refs2, total) {
1010
1297
  lines.push(`${label} (${total ?? refs2.length}): ${shown}${more}`);
1011
1298
  }
1012
1299
 
1300
+ // src/tools/request-decision-change.ts
1301
+ import * as z4 from "zod/v4";
1302
+ var REVIEW_LINE = "A person on the project reviews this in To-do; nothing changes until they accept.";
1303
+ var UNSUPPORTED_LINE = "This SkeletIQ server does not take decision requests yet.";
1304
+ var input2 = z4.object({
1305
+ request_id: z4.string().optional().describe("A request you filed earlier. Give this alone to read where it stands."),
1306
+ project_id: z4.string().optional().describe("A SkeletIQ project id, or its exact name. Needed to file a request."),
1307
+ version: z4.number().int().positive().optional().describe("The version you built from. Defaults to the latest released version."),
1308
+ decision_id: z4.string().max(64).optional().describe("The decision's topic id, e.g. database.primary. Needed to file a request."),
1309
+ proposed: z4.string().min(1).max(500).optional().describe("What you propose instead. Needed to file a request."),
1310
+ why: z4.string().min(1).max(2e3).optional().describe("Why the code needs the change. Needed to file a request."),
1311
+ evidence: z4.array(evidenceRef).max(MAX_REFS_PER_CLAIM).optional().describe("Where in the code the need shows. Verified on this machine; only the path and outcome are sent."),
1312
+ repo_root: z4.string().optional().describe("The repository root that evidence paths are relative to. Defaults to the current directory.")
1313
+ });
1314
+ var output2 = z4.object({
1315
+ request_id: z4.string(),
1316
+ status: z4.enum(["pending", "accepted", "dismissed"]),
1317
+ decision_id: z4.string(),
1318
+ decision_title: z4.string(),
1319
+ proposed: z4.string(),
1320
+ version: z4.number(),
1321
+ latest_version: z4.number(),
1322
+ accepted_in_version: z4.number().nullable(),
1323
+ resolution_note: z4.string().nullable()
1324
+ });
1325
+ function registerRequestDecisionChange(server, client, resolver) {
1326
+ server.registerTool(
1327
+ "request_decision_change",
1328
+ {
1329
+ title: "Ask a person to change a design decision",
1330
+ description: `Ask for one of the design's decisions to change, with what you propose and why the code needs it. ${REVIEW_LINE} Call again with only \`request_id\` to see the answer. This never changes the design.`,
1331
+ inputSchema: input2,
1332
+ outputSchema: output2,
1333
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1334
+ },
1335
+ async ({ request_id, project_id, version, decision_id, proposed, why, evidence, repo_root }) => guard(async () => {
1336
+ if (request_id !== void 0) return poll(client, request_id);
1337
+ if (!project_id || !decision_id || !proposed || !why) {
1338
+ return failure(
1339
+ "To file a request give project_id, decision_id, proposed and why. To read an answer, give only request_id."
1340
+ );
1341
+ }
1342
+ let checked = [];
1343
+ if (evidence?.length) {
1344
+ const requested = repo_root ?? process.cwd();
1345
+ const root = await resolveRepoRoot(requested);
1346
+ if (root === null) return failure(`repo_root ${requested} cannot be read`);
1347
+ checked = await Promise.all(evidence.map((ref) => verifyEvidence(root, ref)));
1348
+ }
1349
+ const project = await resolver.resolveProject(project_id);
1350
+ const resolved = await resolver.resolveVersion(project.id, version);
1351
+ let raw;
1352
+ try {
1353
+ raw = await client.request(`/architectures/${resolved.architectureId}/decision-requests`, {
1354
+ method: "POST",
1355
+ body: { decision_id, proposed, why, evidence: checked }
1356
+ });
1357
+ } catch (error) {
1358
+ return refused(error);
1359
+ }
1360
+ return answer(DecisionRequestSchema.parse(raw), true);
1361
+ })
1362
+ );
1363
+ }
1364
+ async function poll(client, requestId) {
1365
+ let raw;
1366
+ try {
1367
+ raw = await client.request(`/decision-requests/${encodeURIComponent(requestId)}`);
1368
+ } catch (error) {
1369
+ if (error instanceof SkeletiqApiError && error.status === 404 && error.message === "Decision request not found") {
1370
+ return failure(
1371
+ `There is no decision request ${requestId} on this server. Check the id; a request is removed when its project is deleted.`
1372
+ );
1373
+ }
1374
+ return refused(error);
1375
+ }
1376
+ return answer(DecisionRequestSchema.parse(raw), false);
1377
+ }
1378
+ function refused(error) {
1379
+ if (error instanceof SkeletiqApiError) {
1380
+ if (error.status === 404) return failure(UNSUPPORTED_LINE);
1381
+ if (error.status === 409) return failure(error.message);
1382
+ return failure(describeApiError(error));
1383
+ }
1384
+ throw error;
1385
+ }
1386
+ function answer(request, filed) {
1387
+ const structured = {
1388
+ request_id: request.id,
1389
+ status: request.status,
1390
+ decision_id: request.decision_id,
1391
+ decision_title: request.decision_title,
1392
+ proposed: request.proposed,
1393
+ version: request.version,
1394
+ latest_version: request.latest_version,
1395
+ accepted_in_version: request.accepted_in_version,
1396
+ resolution_note: request.resolution_note
1397
+ };
1398
+ const lines = [];
1399
+ const subject = `"${request.decision_title}" (${request.decision_id}, version ${request.version}) \u2192 ${request.proposed}`;
1400
+ if (request.status === "pending") {
1401
+ lines.push(`${filed ? "Request filed" : "Request pending"} (${request.id}): ${subject}`);
1402
+ lines.push(REVIEW_LINE);
1403
+ lines.push(`Poll it with request_decision_change(request_id: "${request.id}").`);
1404
+ } else if (request.status === "accepted") {
1405
+ lines.push(`Request ${request.id} accepted: ${subject}`);
1406
+ lines.push(
1407
+ request.accepted_in_version === null ? `accepted; the server did not say which version it landed in (latest is v${request.latest_version})` : request.latest_version > request.accepted_in_version ? (
1408
+ // The server does not record which version the follow-up turn produced, so a newer one
1409
+ // is a reason to look, not proof the change is in it.
1410
+ `accepted at v${request.accepted_in_version}; v${request.latest_version} is newer \u2014 read its decisions with get_design (mode "decisions") to see whether it carries the change`
1411
+ ) : `accepted at v${request.accepted_in_version}; the design changes when the follow-up turn completes (latest is v${request.latest_version})`
1412
+ );
1413
+ } else {
1414
+ lines.push(`Request ${request.id} dismissed: ${subject}`);
1415
+ lines.push("The design stays as it is.");
1416
+ }
1417
+ if (request.resolution_note) lines.push(`Note from the reviewer: ${request.resolution_note}`);
1418
+ return ok(structured, lines.join("\n"));
1419
+ }
1420
+
1013
1421
  // src/tools/critique.ts
1014
- import * as z3 from "zod/v4";
1015
- var input2 = z3.object({
1016
- architecture_json: z3.looseObject({
1017
- title: z3.string(),
1018
- description: z3.string(),
1019
- components: z3.array(z3.looseObject({ id: z3.string(), name: z3.string(), type: z3.string() })),
1020
- connections: z3.array(z3.looseObject({ source: z3.string(), target: z3.string() }))
1422
+ import * as z5 from "zod/v4";
1423
+ var input3 = z5.object({
1424
+ architecture_json: z5.looseObject({
1425
+ title: z5.string(),
1426
+ description: z5.string(),
1427
+ components: z5.array(z5.looseObject({ id: z5.string(), name: z5.string(), type: z5.string() })),
1428
+ connections: z5.array(z5.looseObject({ source: z5.string(), target: z5.string() }))
1021
1429
  }).optional().describe(
1022
1430
  "A design to check. Free and deterministic \u2014 no model call. The shape SkeletIQ uses: title, description, components[{id,name,type,technology?}], connections[{source,target,protocol?}]."
1023
1431
  ),
1024
- domain: z3.string().optional().describe(
1432
+ domain: z5.string().optional().describe(
1025
1433
  "The design's domain \u2014 e-commerce, fintech, healthcare, saas, social, iot, gaming, infrastructure, streaming, logistics, ai-ml, search, data-analytics, content-platform, internal-tool. Worth sending: the domain is what selects the compliance frameworks to check against, and without it none apply, so no compliance finding is possible and the score comes back higher than the SkeletIQ app would show for the same design \u2014 by up to 15 points."
1026
1434
  ),
1027
- secondary_domains: z3.array(z3.string()).max(8).optional().describe(
1028
- "Further domains the design spans, when it spans more than one \u2014 a multi-tenant shop that takes payments is e-commerce plus fintech and saas. These select frameworks on top of the primary domain's, so leaving them off is why a design already checked against SOC2 and SOX in the app comes back here checked against neither, and scored higher for it. frameworks_checked says which were actually used."
1435
+ secondary_domains: z5.array(z5.string()).max(8).optional().describe(
1436
+ "Further domains the design spans, when it spans more than one \u2014 a multi-tenant shop that takes payments is e-commerce plus fintech and saas. These add the design checks that domain usually needs, as advice; they select no compliance frameworks. Only the primary domain's frameworks and the ones the user named are checked, so name a framework in the prompt when it must apply. frameworks_checked says which were used."
1029
1437
  ),
1030
- exposure: z3.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1438
+ exposure: z5.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1031
1439
  "Where the design runs. Worth sending: four checks \u2014 CDN, WAF, rate limiting and multi-region \u2014 ask whether traffic arriving from the public internet is handled safely, and they only apply to a system that takes any. Left unset, the design is assessed as internet-facing, which is why an air-gapped design comes back told to add a CDN. exposure_assessed says which exposure was actually used."
1032
1440
  )
1033
1441
  });
1034
1442
  var SCORE_BASIS_FINDINGS_ONLY = "findings_only";
1035
1443
  var SCORE_BASIS_NOTE = "This score is findings only. It is not held to a traceability ceiling, because this tool is given a design and no requirement set to trace it against. The app can show a lower number for the same stored design for exactly that reason.";
1036
- var output2 = z3.object({
1037
- architecture_score: z3.number(),
1038
- score_basis: z3.literal(SCORE_BASIS_FINDINGS_ONLY).describe(SCORE_BASIS_NOTE),
1039
- security_score: z3.number(),
1040
- performance_score: z3.number(),
1041
- resilience_score: z3.number(),
1042
- data_score: z3.number(),
1043
- findings: z3.array(
1044
- z3.object({
1045
- severity: z3.string(),
1046
- category: z3.string(),
1047
- message: z3.string(),
1048
- affected_components: z3.array(z3.string()),
1049
- remediation: z3.string(),
1050
- advisory_only: z3.boolean().describe(
1444
+ var output3 = z5.object({
1445
+ architecture_score: z5.number(),
1446
+ score_basis: z5.literal(SCORE_BASIS_FINDINGS_ONLY).describe(SCORE_BASIS_NOTE),
1447
+ security_score: z5.number(),
1448
+ performance_score: z5.number(),
1449
+ resilience_score: z5.number(),
1450
+ data_score: z5.number(),
1451
+ findings: z5.array(
1452
+ z5.object({
1453
+ severity: z5.string(),
1454
+ category: z5.string(),
1455
+ message: z5.string(),
1456
+ affected_components: z5.array(z5.string()),
1457
+ remediation: z5.string(),
1458
+ advisory_only: z5.boolean().describe(
1051
1459
  "True for advice: reported and never charged against the score \u2014 a framework nobody named, or a check too uncertain to count. Not a problem to fix."
1052
1460
  )
1053
1461
  })
1054
1462
  ),
1055
- finding_count: z3.number(),
1056
- compliance_assessed: z3.boolean().describe("False means no compliance framework was checked, so no compliance finding was possible."),
1057
- frameworks_checked: z3.array(z3.string()),
1058
- compliance_note: z3.string().nullable(),
1059
- exposure_assessed: z3.string().describe(
1463
+ finding_count: z5.number(),
1464
+ compliance_assessed: z5.boolean().describe("False means no compliance framework was checked, so no compliance finding was possible."),
1465
+ frameworks_checked: z5.array(z5.string()),
1466
+ compliance_note: z5.string().nullable(),
1467
+ exposure_assessed: z5.string().describe(
1060
1468
  "The exposure this critique was actually run under. An absent or unreadable exposure is assessed as public_internet, so a value the server could not read shows up here rather than arriving silently as four extra security findings."
1061
1469
  )
1062
1470
  });
@@ -1067,8 +1475,8 @@ function registerCritique(server, client) {
1067
1475
  {
1068
1476
  title: "Critique an architecture",
1069
1477
  description: "Check a design against SkeletIQ's architecture rules and get scored findings back. Deterministic and free \u2014 no model call, no credits, nothing stored. Useful on a design you drafted yourself before committing to it.",
1070
- inputSchema: input2,
1071
- outputSchema: output2,
1478
+ inputSchema: input3,
1479
+ outputSchema: output3,
1072
1480
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1073
1481
  },
1074
1482
  async ({ architecture_json, domain, secondary_domains, exposure }) => guard(async () => {
@@ -1109,7 +1517,7 @@ function registerCritique(server, client) {
1109
1517
  exposure_assessed: exposureAssessed
1110
1518
  };
1111
1519
  const lines = [
1112
- `Architecture ${round(critique.architecture_score)}/100 \xB7 security ${round(critique.security_score)} \xB7 performance ${round(critique.performance_score)} \xB7 resilience ${round(critique.resilience_score)} \xB7 data ${round(critique.data_score)}`,
1520
+ `Design quality ${round(critique.architecture_score)}/100 \xB7 security ${round(critique.security_score)} \xB7 performance ${round(critique.performance_score)} \xB7 resilience ${round(critique.resilience_score)} \xB7 data ${round(critique.data_score)}`,
1113
1521
  SCORE_BASIS_NOTE,
1114
1522
  "",
1115
1523
  // An agent that reads only the text has to see this too. Without it, "the app
@@ -1159,7 +1567,7 @@ function round(value) {
1159
1567
 
1160
1568
  // src/tools/generate.ts
1161
1569
  import { randomUUID } from "crypto";
1162
- import * as z4 from "zod/v4";
1570
+ import * as z6 from "zod/v4";
1163
1571
 
1164
1572
  // src/http/sse.ts
1165
1573
  async function* readSseFrames(response) {
@@ -1203,9 +1611,10 @@ var PROGRESS_LABELS = {
1203
1611
  refining: "Refining the design"
1204
1612
  };
1205
1613
  var PROGRESS_TOTAL = Object.keys(PROGRESS_LABELS).length + 1;
1614
+ var LOAD_NOT_SURE = "Not sure yet";
1206
1615
  var PROVENANCE_LABELS = {
1207
- user_supplied: "stated",
1208
- llm_inferred: "inferred",
1616
+ user_supplied: "you set this",
1617
+ llm_inferred: "read from your prompt",
1209
1618
  tier_default: "assumed",
1210
1619
  guardrail_clamped: "adjusted"
1211
1620
  };
@@ -1216,68 +1625,83 @@ function asText(value) {
1216
1625
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
1217
1626
  return typeof value === "string" && value.trim() && value !== "unknown" ? value.trim() : void 0;
1218
1627
  }
1628
+ var BARE_FIGURE = /^~?\d[\d,]*(\.\d+)?$/;
1219
1629
  function frameDetail(frame) {
1220
1630
  const data = asRecord2(frame.pipeline_step);
1221
1631
  if (!data) return void 0;
1222
1632
  const parts = [];
1223
1633
  const domain = asText(data.domain);
1224
1634
  if (domain) parts.push(domain);
1225
- const rps = asText(data.requests_per_second);
1226
- const figure = rps ? `${rps} rps` : asText(data.scale);
1635
+ const rawRps = asText(data.requests_per_second);
1636
+ const zeroRate = rawRps !== void 0 && Number(rawRps.replace(/[~,]/g, "")) === 0;
1637
+ const rps = zeroRate ? void 0 : rawRps;
1638
+ const figure = rps ? BARE_FIGURE.test(rps) ? `${rps}/s` : rps : asText(data.scale);
1227
1639
  if (figure) {
1228
- const source = asText(data.scale_source);
1640
+ const source = zeroRate ? void 0 : asText(data.scale_source);
1229
1641
  const label = source ? PROVENANCE_LABELS[source] ?? source : void 0;
1230
1642
  parts.push(label ? `${figure} (${label})` : figure);
1231
1643
  }
1232
1644
  return parts.length > 0 ? parts.join(", ") : void 0;
1233
1645
  }
1234
- var constraintsInput = z4.strictObject({
1235
- budget: z4.enum(["low", "medium", "high", "unlimited"]).optional(),
1236
- max_latency_ms: z4.number().int().positive().optional(),
1646
+ var constraintsInput = z6.strictObject({
1647
+ budget: z6.enum(["low", "medium", "high", "unlimited"]).optional(),
1648
+ max_latency_ms: z6.number().int().positive().optional(),
1237
1649
  // Free strings, not an enum, and the description has to say why. SkeletIQ assesses five
1238
1650
  // frameworks; naming anything else is still worth doing, because an unassessed regime is
1239
1651
  // recorded and reaches the design brief as a requirement the agent still has to satisfy.
1240
1652
  // The old text read 'e.g. HIPAA, SOC2, PCI-DSS, GDPR' — it omitted SOX, which is the one
1241
1653
  // an agent is least likely to guess, and said nothing about what happens to the rest.
1242
- compliance: z4.array(z4.string()).optional().describe(
1654
+ compliance: z6.array(z6.string()).optional().describe(
1243
1655
  "Regimes the design must satisfy. SOC2, HIPAA, PCI-DSS, GDPR and SOX are checked against requirement catalogues; any other regime is recorded and carried into the design brief as a requirement to satisfy, rather than checked."
1244
1656
  ),
1245
- preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
1246
- preferred_technologies: z4.array(z4.string()).optional(),
1247
- max_components: z4.number().int().positive().optional(),
1248
- read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
1657
+ preferred_cloud: z6.enum(["aws", "gcp", "azure", "any"]).optional(),
1658
+ preferred_technologies: z6.array(z6.string()).optional(),
1659
+ max_components: z6.number().int().positive().optional(),
1660
+ read_write_mix: z6.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
1249
1661
  "Wins over any share stated in the prompt, which the server also reads. Unset and unstated, the design assumes 80% reads. It sizes components and weights read replicas, so on a read-heavy design it changes the instance counts and the cost."
1250
1662
  ),
1251
- burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1252
- 'Wins over a peak the prompt states ("peaking at 2,000 per second"), which the server also reads. Unset and unstated, the design assumes the tier default peak.'
1663
+ burstiness: z6.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1664
+ 'Wins over a peak the prompt states ("peaking at 2,000 per second"), which the server also reads. Unset and unstated, the design assumes a typical peak for its scale.'
1665
+ ),
1666
+ data_retention: z6.enum(["days", "months", "years", "indefinite"]).optional(),
1667
+ consistency: z6.enum(["strict", "eventual", "mixed"]).optional(),
1668
+ data_residency: z6.enum(["any", "us", "eu"]).optional(),
1669
+ transfer_band: z6.enum(["light", "moderate", "heavy", "very_heavy", "massive"]).optional().describe(
1670
+ "How much data the system sends out to its users a month: light ~100 GB, moderate ~1 TB, heavy ~10 TB, very_heavy ~100 TB, massive ~1 PB. Priced at the cloud's published per-GB rates. Unset, data transfer is left out of the price and the estimate says so. Set this or monthly_transfer_gb, not both."
1253
1671
  ),
1254
- data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
1255
- consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
1256
- data_residency: z4.enum(["any", "us", "eu"]).optional()
1672
+ monthly_transfer_gb: z6.number().min(0).max(1e8).optional().describe(
1673
+ "The same thing as a figure: GB a month sent out to users. Set this or transfer_band, not both."
1674
+ )
1257
1675
  });
1258
- var generateInput = z4.object({
1259
- prompt: z4.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
1260
- project_id: z4.string().optional().describe("An existing SkeletIQ project id to add a version to. Omit to start a new project."),
1261
- wait: z4.boolean().optional().describe(
1676
+ var generateInput = z6.object({
1677
+ prompt: z6.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
1678
+ project_id: z6.string().optional().describe("An existing SkeletIQ project id to add a version to. Omit to start a new project."),
1679
+ wait: z6.boolean().optional().describe(
1262
1680
  "Default true: run inline and return the finished design (this takes several minutes). False queues it as a background job and returns a job_id to poll \u2014 not available on every deployment."
1263
1681
  ),
1264
- clarification_answers: z4.record(z4.string(), z4.string()).optional().describe(
1265
- 'Answers to the questions a previous call returned with status "clarification_required", keyed by their ids. Treated as authoritative requirements, so send what the user told you \u2014 ask them rather than guessing.'
1682
+ clarification_answers: z6.record(z6.string(), z6.string()).optional().describe(
1683
+ 'Answers to the questions a previous call returned with status "clarification_required", keyed by their ids. Treated as authoritative requirements, so send what the user told you \u2014 ask them rather than guessing. "Not sure yet" is a real answer to the load question and to the data-sent-out question when they do not know the figure.'
1266
1684
  ),
1267
1685
  constraints: constraintsInput.optional().describe(
1268
1686
  "Facts about the design that the prose does not have to carry. Anything omitted is assumed by SkeletIQ, and the design says which values were assumed."
1269
1687
  )
1270
1688
  });
1271
- var generateOutput = z4.object({
1272
- status: z4.string().describe("completed | clarification_required, or the queue state on wait: false"),
1273
- project_id: z4.string().nullable(),
1274
- architecture_id: z4.string().nullable(),
1275
- version: z4.number().nullable(),
1276
- job_id: z4.string().nullable(),
1277
- title: z4.string().nullable(),
1278
- component_count: z4.number().nullable(),
1279
- assistant_message: z4.string().nullable(),
1280
- degradations: z4.array(z4.string()),
1689
+ var generateOutput = z6.object({
1690
+ status: z6.string().describe("completed | clarification_required, or the queue state on wait: false"),
1691
+ project_id: z6.string().nullable(),
1692
+ architecture_id: z6.string().nullable(),
1693
+ version: z6.number().nullable(),
1694
+ job_id: z6.string().nullable(),
1695
+ title: z6.string().nullable(),
1696
+ component_count: z6.number().nullable(),
1697
+ assistant_message: z6.string().nullable(),
1698
+ degradations: z6.array(z6.string()),
1699
+ /**
1700
+ * The server's sentence when part of what the model was given had to be cut to fit its input
1701
+ * window — a source left out, or the current design summarised — and null otherwise. The design
1702
+ * is still complete; this says what it was written without.
1703
+ */
1704
+ prompt_trim_note: z6.string().nullable(),
1281
1705
  /**
1282
1706
  * Present, and non-empty, only on `status: "clarification_required"`. A refusal is a
1283
1707
  * *result* here rather than an error: an error result may carry no `structuredContent` at
@@ -1285,38 +1709,46 @@ var generateOutput = z4.object({
1285
1709
  * hand the agent the one thing it cannot act on — a sentence saying detail is missing,
1286
1710
  * without saying which.
1287
1711
  */
1288
- clarifying_questions: z4.array(
1289
- z4.object({
1290
- id: z4.string(),
1291
- question: z4.string(),
1292
- why: z4.string(),
1293
- options: z4.array(z4.string())
1712
+ clarifying_questions: z6.array(
1713
+ z6.object({
1714
+ id: z6.string(),
1715
+ question: z6.string(),
1716
+ why: z6.string(),
1717
+ options: z6.array(z6.string()),
1718
+ /** The contract field the answer resolves; null when the question is not a fork. */
1719
+ fork: z6.string().nullable(),
1720
+ /** Ask only once `question_id` is answered with one of `values`; null when always asked. */
1721
+ asked_if: z6.object({ question_id: z6.string(), values: z6.array(z6.string()) }).nullable()
1294
1722
  })
1295
1723
  )
1296
1724
  });
1297
- var statusInput = z4.object({
1298
- job_id: z4.string().describe("The job_id returned by generate_architecture with wait: false.")
1725
+ var statusInput = z6.object({
1726
+ job_id: z6.string().describe("The job_id returned by generate_architecture with wait: false.")
1299
1727
  });
1300
- var statusOutput = z4.object({
1301
- job_id: z4.string(),
1302
- status: z4.string(),
1303
- project_id: z4.string().nullable(),
1304
- architecture_id: z4.string().nullable(),
1305
- error: z4.string().nullable(),
1306
- queue_state: z4.string().nullable(),
1728
+ var statusOutput = z6.object({
1729
+ job_id: z6.string(),
1730
+ status: z6.string(),
1731
+ project_id: z6.string().nullable(),
1732
+ architecture_id: z6.string().nullable(),
1733
+ error: z6.string().nullable(),
1734
+ queue_state: z6.string().nullable(),
1307
1735
  /**
1308
1736
  * Set when the run was *declined* rather than broken — the same three refusals the inline
1309
1737
  * path answers with a 409. Nothing ran and nothing was charged, so the next call is a
1310
1738
  * normal one, not a retry.
1311
1739
  */
1312
- refusal_code: z4.string().nullable(),
1740
+ refusal_code: z6.string().nullable(),
1313
1741
  /** Non-empty only for a `CLARIFICATION_REQUIRED` refusal. */
1314
- clarifying_questions: z4.array(
1315
- z4.object({
1316
- id: z4.string(),
1317
- question: z4.string(),
1318
- why: z4.string(),
1319
- options: z4.array(z4.string())
1742
+ clarifying_questions: z6.array(
1743
+ z6.object({
1744
+ id: z6.string(),
1745
+ question: z6.string(),
1746
+ why: z6.string(),
1747
+ options: z6.array(z6.string()),
1748
+ /** The contract field the answer resolves; null when the question is not a fork. */
1749
+ fork: z6.string().nullable(),
1750
+ /** Ask only once `question_id` is answered with one of `values`; null when always asked. */
1751
+ asked_if: z6.object({ question_id: z6.string(), values: z6.array(z6.string()) }).nullable()
1320
1752
  })
1321
1753
  )
1322
1754
  });
@@ -1325,7 +1757,7 @@ function registerGenerate(server, client, resolver) {
1325
1757
  "generate_architecture",
1326
1758
  {
1327
1759
  title: "Generate a SkeletIQ architecture",
1328
- description: "Design a system architecture from a prompt. This spends the account holder's credits and takes several minutes, so do not call it speculatively \u2014 if a design already exists, read it with get_design instead. Which model runs it is the account holder's stored setting; you do not choose it. Describe the system, what it must do, and the scale and constraints it runs under. A prompt that names no system to design comes back as clarification_required, and so does one that names a system but no scale to build it for \u2014 how many users or requests it has to serve. Answer with clarification_answers; there is no way to skip the questions, because a design built on an invented scale is sized, priced and judged against a number nobody chose.",
1760
+ description: `Design a system architecture from a prompt. This spends the account holder's credits and takes several minutes, so do not call it speculatively \u2014 if a design already exists, read it with get_design instead. Which model runs it is the account holder's stored setting; you do not choose it. Describe the system, what it must do, the load it carries and the constraints it runs under. A prompt that names no system to design comes back as clarification_required, and so does one that states no load: a request rate ("about 300 requests a second"), a fleet (how many devices, and how often each one reports) or a volume ("2 million orders a day"). A headcount alone is not a load. Answer with clarification_answers, keyed by the question ids. Ask your human for the load first. If they do not know it, answer the load question "Not sure yet": the design is then sized on a typical load for its kind of system and marked assumed. Never guess a load or pick a band on their behalf \u2014 a design sized on an invented load is priced and judged against a number nobody chose.`,
1329
1761
  inputSchema: generateInput,
1330
1762
  outputSchema: generateOutput,
1331
1763
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1369,7 +1801,7 @@ function registerGenerate(server, client, resolver) {
1369
1801
  error: job.error ?? null,
1370
1802
  queue_state: job.queue_state ?? null,
1371
1803
  refusal_code: refusal?.code ?? null,
1372
- clarifying_questions: questions
1804
+ clarifying_questions: questions.map(structuredQuestion)
1373
1805
  };
1374
1806
  const lines = refusal ? refusalLines(job.job_id, refusal, questions) : [`Generation ${job.job_id} is ${job.status}.`];
1375
1807
  if (job.status === "completed" && job.project_id) {
@@ -1387,14 +1819,71 @@ function decisionRefusal(progress) {
1387
1819
  return refusal.success ? refusal.data : void 0;
1388
1820
  }
1389
1821
  function refusalQuestions(refusal) {
1390
- return (refusal?.clarifying_questions ?? []).map((question) => ({
1822
+ return orderQuestions(refusal?.clarifying_questions ?? []).map(offeredQuestion);
1823
+ }
1824
+ function offeredQuestion(question) {
1825
+ const options = answerOptions(question);
1826
+ const details = new Map((question.option_details ?? []).map((detail) => [detail.value, detail]));
1827
+ return {
1391
1828
  id: question.id,
1392
1829
  question: question.question,
1393
1830
  why: question.why ?? "",
1394
- options: (question.options ?? []).filter(
1395
- (option) => !(question.decline_options ?? []).includes(option)
1396
- )
1397
- }));
1831
+ options,
1832
+ explained: options.flatMap((option) => {
1833
+ const detail = details.get(option);
1834
+ if (!detail) return [];
1835
+ const said = detail.label && detail.label !== option ? `${option} (${detail.label})` : option;
1836
+ return [detail.detail ? `${said}: ${detail.detail}` : said];
1837
+ }),
1838
+ skipEffect: question.skip_effect ?? "",
1839
+ fork: question.fork ?? "",
1840
+ askedIf: question.asked_if ?? void 0
1841
+ };
1842
+ }
1843
+ function orderQuestions(questions) {
1844
+ const ids = new Set(questions.map((question) => question.id));
1845
+ const placed = /* @__PURE__ */ new Set();
1846
+ const ordered = [];
1847
+ const place = (question) => {
1848
+ if (placed.has(question)) return;
1849
+ placed.add(question);
1850
+ ordered.push(question);
1851
+ for (const child of questions) {
1852
+ if (child.asked_if?.question_id === question.id) place(child);
1853
+ }
1854
+ };
1855
+ for (const question of questions) {
1856
+ if (!question.asked_if) place(question);
1857
+ }
1858
+ for (const question of questions) {
1859
+ if (question.asked_if && !ids.has(question.asked_if.question_id)) place(question);
1860
+ }
1861
+ for (const question of questions) place(question);
1862
+ return ordered;
1863
+ }
1864
+ function structuredQuestion(question) {
1865
+ return {
1866
+ id: question.id,
1867
+ question: question.question,
1868
+ why: question.why,
1869
+ options: question.options,
1870
+ fork: question.fork || null,
1871
+ asked_if: question.askedIf ?? null
1872
+ };
1873
+ }
1874
+ function questionLines(questions) {
1875
+ return questions.flatMap((question) => [
1876
+ `${question.id}: ${question.question}`,
1877
+ ...question.why ? [` Why it matters: ${question.why}`] : [],
1878
+ ...question.explained.length > 0 ? [" Answers:", ...question.explained.map((line) => ` - ${line}`)] : question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : [],
1879
+ ...question.skipEffect ? [` If it is left unanswered: ${question.skipEffect}`] : [],
1880
+ ...question.fork ? [` Resolves: ${question.fork}`] : [],
1881
+ ...question.askedIf ? [` Ask only if ${question.askedIf.question_id} is answered ${question.askedIf.values.join(" or ")}`] : []
1882
+ ]);
1883
+ }
1884
+ function answerOptions(question) {
1885
+ const excluded = /* @__PURE__ */ new Set([...question.decline_options ?? [], ...question.dismiss_options ?? []]);
1886
+ return (question.options ?? []).filter((option) => !excluded.has(option));
1398
1887
  }
1399
1888
  function refusalLines(jobId, refusal, questions) {
1400
1889
  const preamble = `Generation ${jobId} was declined before it ran. Nothing was generated and nothing was charged.`;
@@ -1403,11 +1892,7 @@ function refusalLines(jobId, refusal, questions) {
1403
1892
  return [
1404
1893
  preamble,
1405
1894
  "",
1406
- ...questions.flatMap((question) => [
1407
- `${question.id}: ${question.question}`,
1408
- ...question.why ? [` Why it matters: ${question.why}`] : [],
1409
- ...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
1410
- ]),
1895
+ ...questionLines(questions),
1411
1896
  "",
1412
1897
  "Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. Take the answers from the user rather than inventing them."
1413
1898
  ];
@@ -1432,22 +1917,14 @@ function clarifyingQuestions(error) {
1432
1917
  return questions && questions.length > 0 ? questions : void 0;
1433
1918
  }
1434
1919
  function clarificationResult(questions) {
1435
- const structured = questions.map((question) => ({
1436
- id: question.id,
1437
- question: question.question,
1438
- why: question.why ?? "",
1439
- options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
1440
- }));
1920
+ const offered = orderQuestions(questions).map(offeredQuestion);
1921
+ const structured = offered.map(structuredQuestion);
1441
1922
  const lines = [
1442
- "SkeletIQ will not design from this prompt yet \u2014 it is missing something the design would otherwise have to invent, such as the system itself or the scale it runs at. Nothing was generated and nothing was charged.",
1923
+ "SkeletIQ will not design from this prompt yet \u2014 it is missing something the design would otherwise have to invent, such as the system itself or the load it carries. Nothing was generated and nothing was charged.",
1443
1924
  "",
1444
- ...structured.flatMap((question) => [
1445
- `${question.id}: ${question.question}`,
1446
- ...question.why ? [` Why it matters: ${question.why}`] : [],
1447
- ...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
1448
- ]),
1925
+ ...questionLines(offered),
1449
1926
  "",
1450
- "Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. The answers are used as authoritative requirements, so take them from the user rather than inventing them \u2014 and put anything you already know into constraints instead, where it is used as a fact rather than as prose to interpret."
1927
+ "Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. The answers are used as authoritative requirements, so take them from the user rather than inventing them \u2014 and put anything you already know into constraints instead, where it is used as a fact rather than as prose to interpret." + (offered.some((question) => question.options.includes(LOAD_NOT_SURE)) ? ` If they do not know a figure asked for, answer "${LOAD_NOT_SURE}" rather than picking one.` : "")
1451
1928
  ];
1452
1929
  return ok(
1453
1930
  {
@@ -1460,6 +1937,7 @@ function clarificationResult(questions) {
1460
1937
  component_count: null,
1461
1938
  assistant_message: null,
1462
1939
  degradations: [],
1940
+ prompt_trim_note: null,
1463
1941
  clarifying_questions: structured
1464
1942
  },
1465
1943
  lines.join("\n")
@@ -1480,6 +1958,7 @@ async function queueGeneration(client, body) {
1480
1958
  component_count: null,
1481
1959
  assistant_message: null,
1482
1960
  degradations: [],
1961
+ prompt_trim_note: null,
1483
1962
  clarifying_questions: []
1484
1963
  },
1485
1964
  `Queued as job ${queued.job_id}. Poll it with get_generation_status.`
@@ -1528,6 +2007,7 @@ async function streamGeneration(client, body, ctx, resolver) {
1528
2007
  const architectureId = typeof finished.architecture_id === "string" ? finished.architecture_id : null;
1529
2008
  const version = typeof finished.architecture_version === "number" ? finished.architecture_version : null;
1530
2009
  const degradations = Array.isArray(finished.degradations) ? finished.degradations.filter((d) => typeof d === "string") : [];
2010
+ const trimNote = asText(asRecord2(finished.run_summary)?.prompt_trim_note) ?? null;
1531
2011
  const created = architectureId ? await describeCreated(resolver, architectureId) : void 0;
1532
2012
  return ok(
1533
2013
  {
@@ -1540,6 +2020,7 @@ async function streamGeneration(client, body, ctx, resolver) {
1540
2020
  component_count: created?.componentCount ?? null,
1541
2021
  assistant_message: null,
1542
2022
  degradations,
2023
+ prompt_trim_note: trimNote,
1543
2024
  clarifying_questions: []
1544
2025
  },
1545
2026
  [
@@ -1549,7 +2030,8 @@ async function streamGeneration(client, body, ctx, resolver) {
1549
2030
  'Check get_design(mode: "readiness") before building \u2014 a fresh design is a draft, and',
1550
2031
  "its open questions are the ones worth asking now."
1551
2032
  ] : ["SkeletIQ answered in prose rather than producing a design."],
1552
- ...degradations.map((d) => `Degraded: ${d}`)
2033
+ ...degradations.map((d) => `Degraded: ${d}`),
2034
+ ...trimNote ? [`Input trimmed: ${trimNote}`] : []
1553
2035
  ].join("\n")
1554
2036
  );
1555
2037
  }
@@ -1567,7 +2049,7 @@ async function describeCreated(resolver, architectureId) {
1567
2049
  }
1568
2050
 
1569
2051
  // src/tools/get-design.ts
1570
- import * as z5 from "zod/v4";
2052
+ import * as z7 from "zod/v4";
1571
2053
 
1572
2054
  // src/lib/slice.ts
1573
2055
  function sliceComponent(design, componentId, ledger) {
@@ -1603,10 +2085,30 @@ function isCitedRequirement(row) {
1603
2085
  return (row.cited_by_components?.length ?? 0) > 0 || (row.cited_by_decisions?.length ?? 0) > 0;
1604
2086
  }
1605
2087
  var CITATION_ONLY_METHOD = "structure";
2088
+ var MEASURED_METHODS = ["simulation", "cost", "rules"];
2089
+ var UNCONFIRMED_WORDS = {
2090
+ model_check: "unconfirmed (model check)",
2091
+ unstated_input: "unconfirmed (unstated split)",
2092
+ advisory_finding: "unconfirmed (review advice)"
2093
+ };
2094
+ var ASSUMED_LOAD_INPUT = "the load, which was assumed";
2095
+ var ASSUMED_LOAD_WORD = "unconfirmed (assumed load)";
2096
+ var UNCONFIRMED_ADVICE = "verify before building for it";
2097
+ function requirementBlocksHandoff(row) {
2098
+ if (typeof row.blocks_handoff === "boolean") return row.blocks_handoff;
2099
+ return (row.verdict ?? "").trim() === "fails" && MEASURED_METHODS.includes((row.check_method ?? "").trim());
2100
+ }
1606
2101
  function requirementVerdict(row) {
1607
2102
  const verdict = (row.verdict ?? row.status ?? "").trim();
1608
- if (verdict === "fails" && (row.check_method ?? "").trim() === CITATION_ONLY_METHOD) return "";
1609
- return verdict;
2103
+ if (verdict !== "fails" || requirementBlocksHandoff(row)) return verdict;
2104
+ const method = (row.check_method ?? "").trim();
2105
+ if (method === CITATION_ONLY_METHOD) return "";
2106
+ const reason = row.unconfirmed_reason ?? "model_check";
2107
+ if (reason === "unstated_input" && row.unstated_input?.trim() === ASSUMED_LOAD_INPUT) return ASSUMED_LOAD_WORD;
2108
+ return UNCONFIRMED_WORDS[reason] ?? "unconfirmed";
2109
+ }
2110
+ function isUnconfirmedRequirement(row) {
2111
+ return requirementVerdict(row).startsWith("unconfirmed");
1610
2112
  }
1611
2113
  function orderRequirementRows(rows) {
1612
2114
  return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
@@ -1614,9 +2116,10 @@ function orderRequirementRows(rows) {
1614
2116
  function verdictRank(row) {
1615
2117
  const verdict = requirementVerdict(row);
1616
2118
  if (verdict === "fails" || verdict === "unmet") return 0;
1617
- if (!isCitedRequirement(row)) return 1;
2119
+ if (isUnconfirmedRequirement(row)) return 1;
2120
+ if (!isCitedRequirement(row)) return 2;
1618
2121
  const index = VERDICT_ORDER.indexOf(verdict);
1619
- return index === -1 ? VERDICT_ORDER.length + 1 : index + 2;
2122
+ return index === -1 ? VERDICT_ORDER.length + 2 : index + 3;
1620
2123
  }
1621
2124
  function decisionTexts(design) {
1622
2125
  return decisionEntries(design).map((entry) => entry.text);
@@ -1668,6 +2171,8 @@ function overview(design, ledger) {
1668
2171
  // strings every existing caller reads.
1669
2172
  assumed_decisions: assumedAmong(design, decisions),
1670
2173
  trade_offs: design.trade_offs ?? [],
2174
+ bypassed_connections: design.bypassed_connections ?? [],
2175
+ platform_decisions: design.platform_decisions ?? [],
1671
2176
  // Carried verbatim, rows in the server's order, and only when the version has one. This is
1672
2177
  // a rebuild from named keys, so anything not named here is dropped without a trace — which
1673
2178
  // is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
@@ -1675,37 +2180,69 @@ function overview(design, ledger) {
1675
2180
  ...ledger ? { requirement_ledger: ledger } : {}
1676
2181
  };
1677
2182
  }
2183
+ var MAX_REPLACED_SHOWN = 10;
2184
+ function policySummary(policies) {
2185
+ return Object.entries(policies).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`).join(", ");
2186
+ }
2187
+ function platformPartLines(component) {
2188
+ const lines = [];
2189
+ const role = component.platform_role;
2190
+ const satisfies = component.satisfies ?? [];
2191
+ if (role || component.platform_origin || component.layer === "platform" || satisfies.length > 0) {
2192
+ const origin = component.platform_origin === "realized" ? "added for you" : component.platform_origin === "adopted" ? "adopted" : component.platform_origin;
2193
+ const label = origin ? ` (${origin})` : "";
2194
+ const satisfied = satisfies.length > 0 ? `; satisfies ${satisfies.join(", ")}` : "";
2195
+ lines.push(`Platform part${label}: ${role ?? "unspecified"}${satisfied}`);
2196
+ }
2197
+ const summary = component.policies ? policySummary(component.policies) : "";
2198
+ if (summary) lines.push(`Policies: ${summary}`);
2199
+ return lines;
2200
+ }
2201
+ function platformReplacedLines(design) {
2202
+ const bypassed = design.bypassed_connections ?? [];
2203
+ if (bypassed.length === 0) return [];
2204
+ const shown = bypassed.slice(0, MAX_REPLACED_SHOWN).map((c) => `- ${c.source} \u2192 ${c.target}`);
2205
+ const more = bypassed.length - MAX_REPLACED_SHOWN;
2206
+ return [
2207
+ "Connections the platform layer replaced:",
2208
+ ...shown,
2209
+ ...more > 0 ? [`and ${more} more`] : []
2210
+ ];
2211
+ }
1678
2212
  function normalize(value) {
1679
2213
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1680
2214
  }
1681
2215
 
1682
2216
  // src/tools/get-design.ts
1683
- var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
1684
- var input3 = z5.object({
1685
- project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
1686
- mode: z5.enum(MODES).describe(
1687
- "overview: the whole design and, where the version records them, the requirements it was built from with what became of each. component: one component, its connections in both directions (the source calls the target), the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. todo: everything still to do on this version, one card per problem \u2014 what blocks a handoff, what is recommended, and what is waiting to be accepted. readiness: the verdict, gate by gate, on whether this design is worth building yet. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
2217
+ var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps", "fitness", "decisions"];
2218
+ var input4 = z7.object({
2219
+ project_id: z7.string().describe("A SkeletIQ project id, or its exact name."),
2220
+ mode: z7.enum(MODES).describe(
2221
+ "overview: the whole design and, where the version records them, the requirements it was built from with what became of each. component: one component, its connections in both directions (the source calls the target), the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. todo: everything still to do on this version, one card per problem \u2014 what blocks a handoff, what is recommended, and what is waiting to be accepted. readiness: the verdict, gate by gate, on whether this design is worth building yet. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design. fitness: the tests to write in your own repo, each with the id to report back through check_drift. decisions: the design's decisions, each with its id, choice, reasons and any removed basis."
1688
2222
  ),
1689
- component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
1690
- version: z5.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
2223
+ component_id: z7.string().optional().describe('Required for mode "component". The component id, or its name.'),
2224
+ version: z7.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
1691
2225
  });
1692
- var output3 = z5.object({
1693
- project_id: z5.string(),
1694
- architecture_id: z5.string(),
1695
- version: z5.number(),
1696
- is_released: z5.boolean(),
1697
- newer_release_exists: z5.boolean(),
1698
- resolved_by: z5.string(),
1699
- mode: z5.string(),
2226
+ var output4 = z7.object({
2227
+ project_id: z7.string(),
2228
+ architecture_id: z7.string(),
2229
+ version: z7.number(),
2230
+ is_released: z7.boolean(),
2231
+ newer_release_exists: z7.boolean(),
2232
+ resolved_by: z7.string(),
2233
+ mode: z7.string(),
1700
2234
  /** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
1701
- data: z5.unknown()
2235
+ data: z7.unknown()
1702
2236
  });
1703
2237
  function decisionsHeading(assumed, settled, withAssumptions) {
1704
2238
  return assumed.length > 0 ? withAssumptions : settled;
1705
2239
  }
1706
- function decisionLines(decisions, assumed) {
2240
+ function decisionLines(decisions, assumed, platform = []) {
1707
2241
  const marked = new Set(assumed);
1708
- return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
2242
+ const added = new Set(platform.map((text) => text.trim()));
2243
+ return decisions.map(
2244
+ (decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}${added.has(decision.trim()) ? " (platform)" : ""}`
2245
+ );
1709
2246
  }
1710
2247
  function registerGetDesign(server, client, resolver) {
1711
2248
  server.registerTool(
@@ -1713,8 +2250,8 @@ function registerGetDesign(server, client, resolver) {
1713
2250
  {
1714
2251
  title: "Read a SkeletIQ design",
1715
2252
  description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "todo" for what the design still needs, "build_order" for the sequence and "component" for each piece as you build it. Where a version records its requirements, "overview" and "component" also resolve the R-ids the design carries to the requirements they name, and say what became of each. Every answer states which version it came from and whether that version is a release.',
1716
- inputSchema: input3,
1717
- outputSchema: output3,
2253
+ inputSchema: input4,
2254
+ outputSchema: output4,
1718
2255
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1719
2256
  },
1720
2257
  async ({ project_id, mode, component_id, version }) => guard(async () => {
@@ -1738,6 +2275,7 @@ function registerGetDesign(server, client, resolver) {
1738
2275
  ...data.components.map(
1739
2276
  (c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
1740
2277
  ),
2278
+ ...data.bypassed_connections.length ? ["", ...platformReplacedLines(data)] : [],
1741
2279
  // Above the decisions because the two answer different questions, in
1742
2280
  // this order: requirements are what was asked for, decisions are how
1743
2281
  // the design answered.
@@ -1754,7 +2292,7 @@ function registerGetDesign(server, client, resolver) {
1754
2292
  "Decisions taken \u2014 these are settled, build to them:",
1755
2293
  "Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
1756
2294
  ),
1757
- ...decisionLines(data.design_decisions, data.assumed_decisions)
2295
+ ...decisionLines(data.design_decisions, data.assumed_decisions, data.platform_decisions)
1758
2296
  ] : [],
1759
2297
  ...data.trade_offs.length ? [
1760
2298
  "",
@@ -1783,6 +2321,7 @@ function registerGetDesign(server, client, resolver) {
1783
2321
  `## ${component.name} (${component.id})`,
1784
2322
  `Type: ${component.type}${component.technology ? ` \xB7 ${component.technology}` : ""}`,
1785
2323
  component.description ?? "",
2324
+ ...platformPartLines(component),
1786
2325
  "",
1787
2326
  outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
1788
2327
  incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
@@ -1874,17 +2413,29 @@ function registerGetDesign(server, client, resolver) {
1874
2413
  const gates = readiness.rows.filter((row) => row.category === "gate");
1875
2414
  const open = gates.filter((row) => row.gating);
1876
2415
  const unanswered = gates.filter((row) => row.state === "unknown");
2416
+ const unconfirmedGates = gates.filter((row) => row.state === "unconfirmed");
1877
2417
  const accepts = readiness.rows.filter((row) => row.category === "accept");
1878
2418
  const advisories = readiness.rows.filter(
1879
2419
  (row) => row.category !== "gate" && row.category !== "accept"
1880
2420
  );
1881
2421
  const unrun = new Set(readiness.unrun_checks ?? []);
1882
2422
  const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
1883
- const headline = verdict === "ready" ? "Every gate is clear \u2014 this design is ready to build." : verdict === "unanswered" ? `Nothing is undecided, but ${unanswered.length} check(s) could not be answered for this version:` : `${open.length} of ${gates.length} gate(s) still open:`;
2423
+ const gateLine = verdict === "ready" ? "Every gate is clear \u2014 this design is ready to build." : verdict === "unanswered" ? `Nothing is undecided, but ${unanswered.length} check(s) could not be answered for this version:` : `${open.length} of ${gates.length} gate(s) still open:`;
1884
2424
  const listed = verdict === "unanswered" ? unanswered : open;
2425
+ const summary = readiness.headline;
1885
2426
  return ok({ ...base, ...facts, data: readiness }, [
1886
- headline,
2427
+ ...summary ? [
2428
+ `${summary.met} of ${summary.total} met \xB7 ${summary.verdict.charAt(0).toUpperCase()}${summary.verdict.slice(1)} \xB7 score ${Math.round(summary.score)}`,
2429
+ ...summary.rules_updated ? ["Rules updated since this check"] : [],
2430
+ ""
2431
+ ] : [],
2432
+ gateLine,
1887
2433
  ...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
2434
+ ...unconfirmedGates.length ? [
2435
+ "",
2436
+ "Unconfirmed (the check ran, but its answer turns on an input nobody stated; never blocks):",
2437
+ ...unconfirmedGates.map((row) => `- ${row.label}: ${describeRow(row)}`)
2438
+ ] : [],
1888
2439
  ...accepts.length ? [
1889
2440
  "",
1890
2441
  "To accept (the person agrees to these rather than fixing them; accepting is free and never blocks):",
@@ -1934,6 +2485,80 @@ function registerGetDesign(server, client, resolver) {
1934
2485
  versionLine(facts)
1935
2486
  ].join("\n"));
1936
2487
  }
2488
+ case "fitness": {
2489
+ const facts = factsFrom(resolved);
2490
+ let body;
2491
+ try {
2492
+ body = await client.request(`/architectures/${resolved.architectureId}/fitness-tests`);
2493
+ } catch (error) {
2494
+ if (error instanceof SkeletiqApiError && error.status === 404) {
2495
+ return ok({ ...base, ...facts, data: { tests: [] } }, [
2496
+ "This SkeletIQ server does not serve fitness tests yet.",
2497
+ versionLine(facts)
2498
+ ].join("\n"));
2499
+ }
2500
+ throw error;
2501
+ }
2502
+ const fitness = FitnessTestsSchema.parse(body);
2503
+ if (fitness.unavailable) {
2504
+ return ok({ ...base, ...facts, data: fitness }, [fitness.unavailable, versionLine(facts)].join("\n"));
2505
+ }
2506
+ const note = schemaNote(fitness.schema_version);
2507
+ return ok({ ...base, ...facts, data: fitness }, [
2508
+ ...note ? [note, ""] : [],
2509
+ fitness.tests.length === 0 ? "This version has no fitness tests to write." : "Write these tests in your own repo. Each is a spec, not code. Report each result through check_drift, in `fitness`, under its test_id.",
2510
+ ...fitness.truncated ? [`Showing ${fitness.tests.length} of ${fitness.total}.`] : [],
2511
+ ...fitness.tests.flatMap((test) => [
2512
+ "",
2513
+ `- ${test.subject_names.join(", ") || test.subject_ids.join(", ") || "whole design"} [${test.level}]`,
2514
+ ` assert: ${test.assertion}`,
2515
+ ` check: ${test.check}`,
2516
+ ` test_id: ${test.id}`
2517
+ ]),
2518
+ "",
2519
+ versionLine(facts)
2520
+ ].join("\n"));
2521
+ }
2522
+ case "decisions": {
2523
+ const facts = factsFrom(resolved);
2524
+ let body;
2525
+ try {
2526
+ body = await client.request(`/architectures/${resolved.architectureId}/decisions`);
2527
+ } catch (error) {
2528
+ if (error instanceof SkeletiqApiError && error.status === 403) {
2529
+ return ok({ ...base, ...facts, data: { records: [] } }, [
2530
+ "This SkeletIQ server does not share decisions with tokens yet.",
2531
+ versionLine(facts)
2532
+ ].join("\n"));
2533
+ }
2534
+ throw error;
2535
+ }
2536
+ const decisions = DecisionsViewResponseSchema.parse(body);
2537
+ if (decisions.records.length === 0) {
2538
+ return ok({ ...base, ...facts, data: decisions }, [
2539
+ decisions.status === "unavailable" ? `This version's decisions could not be read (${decisions.reason ?? "unknown"}).` : "This version records no decisions.",
2540
+ versionLine(facts)
2541
+ ].join("\n"));
2542
+ }
2543
+ return ok({ ...base, ...facts, data: decisions }, [
2544
+ ...decisions.records.flatMap((record4) => {
2545
+ const chosen = record4.options.find((option) => option.id === record4.chosen);
2546
+ const rejected = record4.options.filter((option) => option.id !== record4.chosen);
2547
+ return [
2548
+ "",
2549
+ `- ${record4.id}: ${record4.title}`,
2550
+ ` chosen: ${chosen?.label ?? record4.chosen}`,
2551
+ ` why: ${record4.why}`,
2552
+ ...rejected.map(
2553
+ (option) => ` rejected: ${option.label}${option.rejected_because ? ` (${option.rejected_because})` : ""}`
2554
+ ),
2555
+ ...record4.basis_removed.length > 0 ? [` cites removed requirement ${record4.basis_removed.join(", ")}; ask before building on it`] : []
2556
+ ];
2557
+ }),
2558
+ "",
2559
+ versionLine(facts)
2560
+ ].join("\n"));
2561
+ }
1937
2562
  case "gaps": {
1938
2563
  const body = await client.request(`/projects/${project.id}/design-gaps`, {
1939
2564
  query: { architecture_id: resolved.architectureId }
@@ -1945,11 +2570,11 @@ function registerGetDesign(server, client, resolver) {
1945
2570
  const orphaned = gaps.orphaned_answers ?? [];
1946
2571
  return ok({ ...base, ...facts, data: gaps }, [
1947
2572
  unresolved.length === 0 ? "Every open question and assumption in this design has been dealt with." : `${unresolved.length} unresolved:`,
1948
- ...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}`),
2573
+ ...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}${sizingNote(gap)}`),
1949
2574
  ...settled.length > 0 ? [
1950
2575
  "",
1951
2576
  `${settled.length} settled during review \u2014 treat these as decided:`,
1952
- ...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}`)
2577
+ ...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}${sizingNote(gap)}`)
1953
2578
  ] : [],
1954
2579
  // A gap is identified by a hash of its own text, so a regeneration that
1955
2580
  // re-words a question strands the answer that settled it. The server
@@ -1959,7 +2584,7 @@ function registerGetDesign(server, client, resolver) {
1959
2584
  ...orphaned.length ? [
1960
2585
  "",
1961
2586
  `${orphaned.length} earlier answer(s) do not match any question in this version \u2014 re-worded or dropped when it was generated, or asked only in a different version. Any Decision they minted still stands.`,
1962
- ...orphaned.filter((answer) => answer.adr_id).map((answer) => `- [${answer.kind}] ${answer.action} \u2192 Decision ${answer.adr_id}`)
2587
+ ...orphaned.filter((answer2) => answer2.adr_id).map((answer2) => `- [${answer2.kind}] ${answer2.action} \u2192 Decision ${answer2.adr_id}`)
1963
2588
  ] : [],
1964
2589
  "",
1965
2590
  "The unresolved ones are questions for a person, not for you to answer. Raise",
@@ -1975,8 +2600,10 @@ function registerGetDesign(server, client, resolver) {
1975
2600
  function requirementBlock(ledger) {
1976
2601
  const rows = orderRequirementRows(ledger?.rows ?? []);
1977
2602
  if (!ledger || rows.length === 0) return [];
2603
+ const unconfirmed = rows.filter(isUnconfirmedRequirement).length;
1978
2604
  const attention = [
1979
2605
  [rows.filter((row) => requirementVerdict(row) === "fails").length, "failing"],
2606
+ [unconfirmed, "unconfirmed"],
1980
2607
  [rows.filter((row) => !isCitedRequirement(row)).length, "cited by nothing"],
1981
2608
  [rows.filter((row) => requirementVerdict(row) === "not_checked" || requirementVerdict(row) === "not_checkable").length, "unchecked"]
1982
2609
  ].filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
@@ -1989,6 +2616,8 @@ function requirementBlock(ledger) {
1989
2616
  "contradicted it, which is not the same as proven; `not_checked` / `not_checkable` \u2014 nobody",
1990
2617
  "established either way, and neither is fine. `cited by nothing` means no component and no",
1991
2618
  "decision in the design claims to address it.",
2619
+ ...unconfirmedLegend(rows),
2620
+ ...proposedLegend(rows),
1992
2621
  // Two facts about how far the statuses can be trusted, said only when the server states
1993
2622
  // them. An absent `verification_state` is an older server, not a verifier that did not run,
1994
2623
  // and claiming the second would be this package inventing a fact about someone's design.
@@ -2006,6 +2635,37 @@ function requirementBlock(ledger) {
2006
2635
  ...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
2007
2636
  ];
2008
2637
  }
2638
+ function proposedLegend(rows) {
2639
+ return rows.some((row) => row.origin === "proposed") ? ["[proposed] = suggested by the design, not asked for; treat as optional."] : [];
2640
+ }
2641
+ function unconfirmedLegend(rows) {
2642
+ const words = new Set(rows.filter(isUnconfirmedRequirement).map(requirementVerdict));
2643
+ const reasons = new Set(
2644
+ rows.filter((row) => isUnconfirmedRequirement(row) && requirementVerdict(row) !== "unconfirmed (assumed load)").map((row) => row.unconfirmed_reason ?? "model_check")
2645
+ );
2646
+ return [
2647
+ ...reasons.has("model_check") ? [
2648
+ "`unconfirmed (model check)` \u2014 only a model check says the design may not meet it. Nothing",
2649
+ `measured it, so it does not block handoff: ${UNCONFIRMED_ADVICE} rather than adding a part.`
2650
+ ] : [],
2651
+ ...reasons.has("unstated_input") ? [
2652
+ "`unconfirmed (unstated split)` \u2014 capacity falls short only under an assumption nobody stated",
2653
+ "(the rate split evenly between entrances, or traffic weights that do not add up). Its reason",
2654
+ "names the split the design would need \u2014 judge whether that is realistic. It does not block",
2655
+ "handoff: ask how the traffic divides."
2656
+ ] : [],
2657
+ ...reasons.has("advisory_finding") ? [
2658
+ "`unconfirmed (review advice)` \u2014 only advice from the design review fails it: the findings it",
2659
+ "rests on are reported, not scored, so it does not block handoff. Read the advice and decide",
2660
+ "whether the design should take it."
2661
+ ] : [],
2662
+ ...words.has("unconfirmed (assumed load)") ? [
2663
+ "`unconfirmed (assumed load)` \u2014 the verdict rests on the load, which was assumed: nobody said how",
2664
+ "much traffic to expect, so the design was sized and priced on the typical load for this kind of",
2665
+ "system. It does not block handoff: ask your human for their load rather than guessing one."
2666
+ ] : []
2667
+ ];
2668
+ }
2009
2669
  function requirementsServed(rows) {
2010
2670
  if (!rows?.length) return [];
2011
2671
  return [
@@ -2013,7 +2673,9 @@ function requirementsServed(rows) {
2013
2673
  "Requirements the design cites this component for:",
2014
2674
  ...rows.map(requirementLine),
2015
2675
  "`fails` \u2014 a named check measured the design as not meeting it; `not_checked` \u2014 nobody",
2016
- "established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven."
2676
+ "established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven.",
2677
+ ...unconfirmedLegend(rows),
2678
+ ...proposedLegend(rows)
2017
2679
  ];
2018
2680
  }
2019
2681
  function requirementsWithdrawn(rows) {
@@ -2028,13 +2690,15 @@ function requirementsWithdrawn(rows) {
2028
2690
  }
2029
2691
  function requirementLine(row) {
2030
2692
  const verdict = requirementVerdict(row);
2031
- const marks = [verdict, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
2693
+ const shown = isUnconfirmedRequirement(row) ? `${verdict} \u2014 ${UNCONFIRMED_ADVICE}` : verdict;
2694
+ const marks = [shown, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
2032
2695
  const label = marks.length > 0 ? ` [${marks.join(" \xB7 ")}]` : "";
2033
2696
  const text = (row.text ?? "").replace(/\s+/g, " ").trim();
2034
2697
  const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
2035
2698
  const by = (row.checked_by ?? "").trim();
2036
2699
  const because = reason ? ` \u2014 ${reason}${by ? ` (checked by ${by})` : ""}` : by ? ` \u2014 checked by ${by}` : "";
2037
- return `- ${row.id}${label} ${text || "(no text recorded for this requirement)"}${because}`;
2700
+ const proposed = row.origin === "proposed" ? "[proposed] " : "";
2701
+ return `- ${row.id}${label} ${proposed}${text || "(no text recorded for this requirement)"}${because}`;
2038
2702
  }
2039
2703
  function settledAnswer(gap) {
2040
2704
  const note = (gap.note ?? "").trim();
@@ -2046,6 +2710,10 @@ function settledAnswer(gap) {
2046
2710
  if (gap.action === "confirmed") return "confirmed as it stands";
2047
2711
  return "answered, but no wording was recorded \u2014 ask before relying on it";
2048
2712
  }
2713
+ function sizingNote(gap) {
2714
+ const conflict = gap.sizing_conflict;
2715
+ return conflict ? ` (this design is sized for ${conflict.sized}, not ${conflict.assumed})` : "";
2716
+ }
2049
2717
  function todoGroupBlock(group) {
2050
2718
  if (group.items.length === 0) return [];
2051
2719
  return ["", `${group.label} (${group.items.length}):`, ...group.items.map(todoLine)];
@@ -2059,26 +2727,31 @@ function todoLine(item) {
2059
2727
  return `- ${severity}${item.title.replace(/\s+/g, " ").trim()}${components}${raised}${carried}`;
2060
2728
  }
2061
2729
  function describeRow(row) {
2062
- if (row.state === "unknown") return `not checked${row.detail ? ` \u2014 ${row.detail}` : ""}`;
2730
+ if (row.state === "unknown") {
2731
+ const clears = row.clears ? ` ${row.clears}` : "";
2732
+ const detail = row.detail ? ` \u2014 ${row.detail}${clears && !/[.!?]$/.test(row.detail) ? "." : ""}` : clears ? " \u2014" : "";
2733
+ return `not checked${detail}${clears}`;
2734
+ }
2735
+ if (row.state === "unconfirmed") return `unconfirmed${row.detail ? ` \u2014 ${row.detail}` : ""}`;
2063
2736
  return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
2064
2737
  }
2065
2738
 
2066
2739
  // src/tools/list-projects.ts
2067
- import * as z6 from "zod/v4";
2068
- var input4 = z6.object({
2069
- query: z6.string().optional().describe("Filter by name or description. Omit to list everything the token can see.")
2740
+ import * as z8 from "zod/v4";
2741
+ var input5 = z8.object({
2742
+ query: z8.string().optional().describe("Filter by name or description. Omit to list everything the token can see.")
2070
2743
  });
2071
- var output4 = z6.object({
2072
- projects: z6.array(
2073
- z6.object({
2074
- id: z6.string(),
2075
- title: z6.string(),
2076
- description: z6.string().nullable(),
2077
- architecture_count: z6.number(),
2078
- updated_at: z6.string().nullable()
2744
+ var output5 = z8.object({
2745
+ projects: z8.array(
2746
+ z8.object({
2747
+ id: z8.string(),
2748
+ title: z8.string(),
2749
+ description: z8.string().nullable(),
2750
+ architecture_count: z8.number(),
2751
+ updated_at: z8.string().nullable()
2079
2752
  })
2080
2753
  ),
2081
- total: z6.number()
2754
+ total: z8.number()
2082
2755
  });
2083
2756
  function registerListProjects(server, resolver) {
2084
2757
  server.registerTool(
@@ -2086,8 +2759,8 @@ function registerListProjects(server, resolver) {
2086
2759
  {
2087
2760
  title: "List SkeletIQ projects",
2088
2761
  description: "List the SkeletIQ projects this token can see, optionally filtered by name. Use this first to turn a project a person named in conversation into an id. If more than one project matches, ask which \u2014 do not pick one.",
2089
- inputSchema: input4,
2090
- outputSchema: output4,
2762
+ inputSchema: input5,
2763
+ outputSchema: output5,
2091
2764
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
2092
2765
  },
2093
2766
  async ({ query }) => guard(async () => {
@@ -2149,6 +2822,7 @@ function buildServer(client, resolver) {
2149
2822
  registerGenerate(server, client, resolver);
2150
2823
  registerCritique(server, client);
2151
2824
  registerCheckDrift(server, client, resolver);
2825
+ registerRequestDecisionChange(server, client, resolver);
2152
2826
  return server;
2153
2827
  }
2154
2828