@skeletiq/mcp 0.4.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 +5 -4
  2. package/dist/index.js +702 -181
  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.4.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
@@ -201,14 +206,14 @@ var SkeletiqClient = class {
201
206
  this.config = config;
202
207
  }
203
208
  /** A parsed JSON response, or a thrown `SkeletiqApiError` carrying the server's own words. */
204
- async request(path, options = {}) {
205
- const response = await this.fetch(path, options);
209
+ async request(path2, options = {}) {
210
+ const response = await this.fetch(path2, options);
206
211
  const text = await response.text();
207
212
  return text ? JSON.parse(text) : void 0;
208
213
  }
209
214
  /** The raw `Response`, for the one caller that reads a stream rather than a body. */
210
- async fetch(path, options = {}) {
211
- const url = this.url(path, options.query);
215
+ async fetch(path2, options = {}) {
216
+ const url = this.url(path2, options.query);
212
217
  const timeout = AbortSignal.timeout(READ_TIMEOUT_MS);
213
218
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
214
219
  let response;
@@ -232,8 +237,8 @@ var SkeletiqClient = class {
232
237
  if (!response.ok) throw await this.toError(response);
233
238
  return response;
234
239
  }
235
- url(path, query) {
236
- 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}`);
237
242
  for (const [key, value] of Object.entries(query ?? {})) {
238
243
  if (value !== void 0 && value !== "") url.searchParams.set(key, String(value));
239
244
  }
@@ -290,7 +295,19 @@ var ComponentSchema = z.looseObject({
290
295
  role: z.string().nullable().optional(),
291
296
  /** Why the component is in the design (`core`, `security`, …) — see the API's `component_concerns`. */
292
297
  concern: z.string().nullable().optional(),
293
- 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()
294
311
  });
295
312
  var ConnectionSchema = z.looseObject({
296
313
  id: z.string().nullable().optional(),
@@ -298,7 +315,11 @@ var ConnectionSchema = z.looseObject({
298
315
  target: z.string(),
299
316
  label: z.string().nullable().optional(),
300
317
  protocol: z.string().nullable().optional(),
301
- 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()
302
323
  });
303
324
  var GapEntrySchema = z.union([
304
325
  z.string(),
@@ -326,7 +347,11 @@ var ArchitectureJsonSchema = z.looseObject({
326
347
  trade_offs: z.array(z.string()).nullable().optional(),
327
348
  scalability_notes: z.string().nullable().optional(),
328
349
  assumptions: z.array(GapEntrySchema).nullable().optional(),
329
- 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()
330
355
  });
331
356
  var RequirementLedgerRowSchema = z.looseObject({
332
357
  id: z.string(),
@@ -367,6 +392,10 @@ var RequirementLedgerRowSchema = z.looseObject({
367
392
  cited_by_components: z.array(z.string()).nullable().optional(),
368
393
  /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
369
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(),
370
399
  added_in_version: z.number().nullable().optional(),
371
400
  /** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
372
401
  * recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
@@ -506,6 +535,14 @@ var ReadinessSchema = z.looseObject({
506
535
  * caller falls back to the boolean.
507
536
  */
508
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(),
509
546
  unknown_gate_count: z.number().optional(),
510
547
  /** Keys of advisory rows whose check never ran. Named, never counted. */
511
548
  unrun_checks: z.array(z.string()).optional(),
@@ -694,7 +731,71 @@ var DriftSchema = z.looseObject({
694
731
  extra_components: z.array(z.looseObject({ name: z.string() })),
695
732
  extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
696
733
  counts: z.record(z.string(), z.number()),
697
- 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([])
698
799
  });
699
800
  var ClarifyingOptionDetailSchema = z.looseObject({
700
801
  value: z.string(),
@@ -715,7 +816,11 @@ var ClarifyingQuestionSchema = z.looseObject({
715
816
  /** Whether more than one option may be picked at once. */
716
817
  multiple: z.boolean().optional(),
717
818
  /** What leaving the question unanswered does, in the product's words. */
718
- skip_effect: z.string().nullable().optional()
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()
719
824
  });
720
825
  var IntentDecisionSchema = z.looseObject({
721
826
  intent: z.string().optional(),
@@ -896,7 +1001,80 @@ function isUuid(value) {
896
1001
  }
897
1002
 
898
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";
899
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
+ }
900
1078
 
901
1079
  // src/lib/result.ts
902
1080
  function ok(structured, text) {
@@ -929,53 +1107,70 @@ function versionLine(facts) {
929
1107
  }
930
1108
 
931
1109
  // src/tools/check-drift.ts
932
- var input = z2.object({
933
- project_id: z2.string().describe("A SkeletIQ project id, or its exact name."),
934
- 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(
935
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."
936
1114
  ),
937
- 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(
938
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."
939
1118
  ),
940
- components: z2.array(
941
- z2.object({
942
- id: z2.string().max(200).describe("The component id from the design."),
943
- name: z2.string().max(255).optional().describe("The component name, which survives a regeneration when the id does not."),
944
- status: z2.enum(["implemented", "partial", "not_started"]),
945
- 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.")
946
1126
  })
947
1127
  ).max(500).describe("What you built. Report every component in `covers`, including the ones not started."),
948
- extra_components: z2.array(
949
- z2.object({
950
- name: z2.string().max(255),
951
- type: z2.string().max(64).optional(),
952
- 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()
953
1133
  })
954
1134
  ).max(200).optional().describe("Things in the code that the design does not contain."),
955
- extra_connections: z2.array(
956
- z2.object({
957
- source: z2.string().max(255),
958
- target: z2.string().max(255),
959
- 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()
960
1140
  })
961
1141
  ).max(500).optional().describe(
962
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."
963
- )
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.")
964
1152
  });
965
- var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
966
- var output = z2.object({
967
- architecture_id: z2.string(),
968
- version: z2.number(),
969
- is_released: z2.boolean(),
970
- newer_release_exists: z2.boolean(),
971
- resolved_by: z2.string(),
972
- 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(),
973
1161
  missing: refs,
974
1162
  unreported: refs,
975
1163
  partial: refs,
976
1164
  elsewhere: refs,
977
- unknown_ids: z2.array(z2.object({ id: z2.string(), suggestions: refs })),
978
- 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()
979
1174
  });
980
1175
  function registerCheckDrift(server, client, resolver) {
981
1176
  server.registerTool(
@@ -987,7 +1182,23 @@ function registerCheckDrift(server, client, resolver) {
987
1182
  outputSchema: output,
988
1183
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
989
1184
  },
990
- 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;
991
1202
  const project = await resolver.resolveProject(project_id);
992
1203
  const resolved = await resolver.resolveVersion(project.id, version);
993
1204
  const report = DriftSchema.parse(
@@ -995,9 +1206,10 @@ function registerCheckDrift(server, client, resolver) {
995
1206
  method: "POST",
996
1207
  body: {
997
1208
  covers: covers ?? null,
998
- components,
1209
+ components: checked,
999
1210
  extra_components: extra_components ?? null,
1000
- extra_connections: extra_connections ?? null
1211
+ extra_connections: extra_connections ?? null,
1212
+ fitness: fitnessChecked
1001
1213
  }
1002
1214
  })
1003
1215
  );
@@ -1014,15 +1226,34 @@ function registerCheckDrift(server, client, resolver) {
1014
1226
  id: entry.id,
1015
1227
  suggestions: entry.suggestions ?? []
1016
1228
  })),
1017
- 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
1018
1237
  };
1019
1238
  const lines = [];
1020
1239
  lines.push(
1021
1240
  report.in_sync ? "In sync \u2014 everything this repository covers is built as designed." : "Drift found:"
1022
1241
  );
1023
1242
  push(lines, "Not started", report.missing, report.counts.missing);
1243
+ push(lines, "Unverified", structured.unverified, report.counts.unverified);
1024
1244
  push(lines, "Half built", report.partial, report.counts.partial);
1025
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
+ }
1026
1257
  if (report.extra_components.length) {
1027
1258
  lines.push(`In the code but not the design: ${report.extra_components.map((c) => c.name).join(", ")}`);
1028
1259
  }
@@ -1048,6 +1279,12 @@ function registerCheckDrift(server, client, resolver) {
1048
1279
  );
1049
1280
  }
1050
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);
1051
1288
  lines.push(versionLine(facts));
1052
1289
  return ok(structured, lines.join("\n"));
1053
1290
  })
@@ -1060,53 +1297,174 @@ function push(lines, label, refs2, total) {
1060
1297
  lines.push(`${label} (${total ?? refs2.length}): ${shown}${more}`);
1061
1298
  }
1062
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
+
1063
1421
  // src/tools/critique.ts
1064
- import * as z3 from "zod/v4";
1065
- var input2 = z3.object({
1066
- architecture_json: z3.looseObject({
1067
- title: z3.string(),
1068
- description: z3.string(),
1069
- components: z3.array(z3.looseObject({ id: z3.string(), name: z3.string(), type: z3.string() })),
1070
- 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() }))
1071
1429
  }).optional().describe(
1072
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?}]."
1073
1431
  ),
1074
- domain: z3.string().optional().describe(
1432
+ domain: z5.string().optional().describe(
1075
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."
1076
1434
  ),
1077
- secondary_domains: z3.array(z3.string()).max(8).optional().describe(
1435
+ secondary_domains: z5.array(z5.string()).max(8).optional().describe(
1078
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."
1079
1437
  ),
1080
- exposure: z3.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1438
+ exposure: z5.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1081
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."
1082
1440
  )
1083
1441
  });
1084
1442
  var SCORE_BASIS_FINDINGS_ONLY = "findings_only";
1085
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.";
1086
- var output2 = z3.object({
1087
- architecture_score: z3.number(),
1088
- score_basis: z3.literal(SCORE_BASIS_FINDINGS_ONLY).describe(SCORE_BASIS_NOTE),
1089
- security_score: z3.number(),
1090
- performance_score: z3.number(),
1091
- resilience_score: z3.number(),
1092
- data_score: z3.number(),
1093
- findings: z3.array(
1094
- z3.object({
1095
- severity: z3.string(),
1096
- category: z3.string(),
1097
- message: z3.string(),
1098
- affected_components: z3.array(z3.string()),
1099
- remediation: z3.string(),
1100
- 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(
1101
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."
1102
1460
  )
1103
1461
  })
1104
1462
  ),
1105
- finding_count: z3.number(),
1106
- compliance_assessed: z3.boolean().describe("False means no compliance framework was checked, so no compliance finding was possible."),
1107
- frameworks_checked: z3.array(z3.string()),
1108
- compliance_note: z3.string().nullable(),
1109
- 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(
1110
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."
1111
1469
  )
1112
1470
  });
@@ -1117,8 +1475,8 @@ function registerCritique(server, client) {
1117
1475
  {
1118
1476
  title: "Critique an architecture",
1119
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.",
1120
- inputSchema: input2,
1121
- outputSchema: output2,
1478
+ inputSchema: input3,
1479
+ outputSchema: output3,
1122
1480
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1123
1481
  },
1124
1482
  async ({ architecture_json, domain, secondary_domains, exposure }) => guard(async () => {
@@ -1159,7 +1517,7 @@ function registerCritique(server, client) {
1159
1517
  exposure_assessed: exposureAssessed
1160
1518
  };
1161
1519
  const lines = [
1162
- `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)}`,
1163
1521
  SCORE_BASIS_NOTE,
1164
1522
  "",
1165
1523
  // An agent that reads only the text has to see this too. Without it, "the app
@@ -1209,7 +1567,7 @@ function round(value) {
1209
1567
 
1210
1568
  // src/tools/generate.ts
1211
1569
  import { randomUUID } from "crypto";
1212
- import * as z4 from "zod/v4";
1570
+ import * as z6 from "zod/v4";
1213
1571
 
1214
1572
  // src/http/sse.ts
1215
1573
  async function* readSseFrames(response) {
@@ -1285,65 +1643,65 @@ function frameDetail(frame) {
1285
1643
  }
1286
1644
  return parts.length > 0 ? parts.join(", ") : void 0;
1287
1645
  }
1288
- var constraintsInput = z4.strictObject({
1289
- budget: z4.enum(["low", "medium", "high", "unlimited"]).optional(),
1290
- 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(),
1291
1649
  // Free strings, not an enum, and the description has to say why. SkeletIQ assesses five
1292
1650
  // frameworks; naming anything else is still worth doing, because an unassessed regime is
1293
1651
  // recorded and reaches the design brief as a requirement the agent still has to satisfy.
1294
1652
  // The old text read 'e.g. HIPAA, SOC2, PCI-DSS, GDPR' — it omitted SOX, which is the one
1295
1653
  // an agent is least likely to guess, and said nothing about what happens to the rest.
1296
- compliance: z4.array(z4.string()).optional().describe(
1654
+ compliance: z6.array(z6.string()).optional().describe(
1297
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."
1298
1656
  ),
1299
- preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
1300
- preferred_technologies: z4.array(z4.string()).optional(),
1301
- max_components: z4.number().int().positive().optional(),
1302
- 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(
1303
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."
1304
1662
  ),
1305
- burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1663
+ burstiness: z6.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1306
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.'
1307
1665
  ),
1308
- data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
1309
- consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
1310
- data_residency: z4.enum(["any", "us", "eu"]).optional(),
1311
- transfer_band: z4.enum(["light", "moderate", "heavy", "very_heavy", "massive"]).optional().describe(
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(
1312
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."
1313
1671
  ),
1314
- monthly_transfer_gb: z4.number().min(0).max(1e8).optional().describe(
1672
+ monthly_transfer_gb: z6.number().min(0).max(1e8).optional().describe(
1315
1673
  "The same thing as a figure: GB a month sent out to users. Set this or transfer_band, not both."
1316
1674
  )
1317
1675
  });
1318
- var generateInput = z4.object({
1319
- prompt: z4.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
1320
- project_id: z4.string().optional().describe("An existing SkeletIQ project id to add a version to. Omit to start a new project."),
1321
- 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(
1322
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."
1323
1681
  ),
1324
- clarification_answers: z4.record(z4.string(), z4.string()).optional().describe(
1682
+ clarification_answers: z6.record(z6.string(), z6.string()).optional().describe(
1325
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.'
1326
1684
  ),
1327
1685
  constraints: constraintsInput.optional().describe(
1328
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."
1329
1687
  )
1330
1688
  });
1331
- var generateOutput = z4.object({
1332
- status: z4.string().describe("completed | clarification_required, or the queue state on wait: false"),
1333
- project_id: z4.string().nullable(),
1334
- architecture_id: z4.string().nullable(),
1335
- version: z4.number().nullable(),
1336
- job_id: z4.string().nullable(),
1337
- title: z4.string().nullable(),
1338
- component_count: z4.number().nullable(),
1339
- assistant_message: z4.string().nullable(),
1340
- 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()),
1341
1699
  /**
1342
1700
  * The server's sentence when part of what the model was given had to be cut to fit its input
1343
1701
  * window — a source left out, or the current design summarised — and null otherwise. The design
1344
1702
  * is still complete; this says what it was written without.
1345
1703
  */
1346
- prompt_trim_note: z4.string().nullable(),
1704
+ prompt_trim_note: z6.string().nullable(),
1347
1705
  /**
1348
1706
  * Present, and non-empty, only on `status: "clarification_required"`. A refusal is a
1349
1707
  * *result* here rather than an error: an error result may carry no `structuredContent` at
@@ -1351,38 +1709,46 @@ var generateOutput = z4.object({
1351
1709
  * hand the agent the one thing it cannot act on — a sentence saying detail is missing,
1352
1710
  * without saying which.
1353
1711
  */
1354
- clarifying_questions: z4.array(
1355
- z4.object({
1356
- id: z4.string(),
1357
- question: z4.string(),
1358
- why: z4.string(),
1359
- 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()
1360
1722
  })
1361
1723
  )
1362
1724
  });
1363
- var statusInput = z4.object({
1364
- 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.")
1365
1727
  });
1366
- var statusOutput = z4.object({
1367
- job_id: z4.string(),
1368
- status: z4.string(),
1369
- project_id: z4.string().nullable(),
1370
- architecture_id: z4.string().nullable(),
1371
- error: z4.string().nullable(),
1372
- 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(),
1373
1735
  /**
1374
1736
  * Set when the run was *declined* rather than broken — the same three refusals the inline
1375
1737
  * path answers with a 409. Nothing ran and nothing was charged, so the next call is a
1376
1738
  * normal one, not a retry.
1377
1739
  */
1378
- refusal_code: z4.string().nullable(),
1740
+ refusal_code: z6.string().nullable(),
1379
1741
  /** Non-empty only for a `CLARIFICATION_REQUIRED` refusal. */
1380
- clarifying_questions: z4.array(
1381
- z4.object({
1382
- id: z4.string(),
1383
- question: z4.string(),
1384
- why: z4.string(),
1385
- 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()
1386
1752
  })
1387
1753
  )
1388
1754
  });
@@ -1453,7 +1819,7 @@ function decisionRefusal(progress) {
1453
1819
  return refusal.success ? refusal.data : void 0;
1454
1820
  }
1455
1821
  function refusalQuestions(refusal) {
1456
- return (refusal?.clarifying_questions ?? []).map(offeredQuestion);
1822
+ return orderQuestions(refusal?.clarifying_questions ?? []).map(offeredQuestion);
1457
1823
  }
1458
1824
  function offeredQuestion(question) {
1459
1825
  const options = answerOptions(question);
@@ -1469,18 +1835,50 @@ function offeredQuestion(question) {
1469
1835
  const said = detail.label && detail.label !== option ? `${option} (${detail.label})` : option;
1470
1836
  return [detail.detail ? `${said}: ${detail.detail}` : said];
1471
1837
  }),
1472
- skipEffect: question.skip_effect ?? ""
1838
+ skipEffect: question.skip_effect ?? "",
1839
+ fork: question.fork ?? "",
1840
+ askedIf: question.asked_if ?? void 0
1473
1841
  };
1474
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
+ }
1475
1864
  function structuredQuestion(question) {
1476
- return { id: question.id, question: question.question, why: question.why, options: question.options };
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
+ };
1477
1873
  }
1478
1874
  function questionLines(questions) {
1479
1875
  return questions.flatMap((question) => [
1480
1876
  `${question.id}: ${question.question}`,
1481
1877
  ...question.why ? [` Why it matters: ${question.why}`] : [],
1482
1878
  ...question.explained.length > 0 ? [" Answers:", ...question.explained.map((line) => ` - ${line}`)] : question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : [],
1483
- ...question.skipEffect ? [` If it is left unanswered: ${question.skipEffect}`] : []
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 ")}`] : []
1484
1882
  ]);
1485
1883
  }
1486
1884
  function answerOptions(question) {
@@ -1519,7 +1917,7 @@ function clarifyingQuestions(error) {
1519
1917
  return questions && questions.length > 0 ? questions : void 0;
1520
1918
  }
1521
1919
  function clarificationResult(questions) {
1522
- const offered = questions.map(offeredQuestion);
1920
+ const offered = orderQuestions(questions).map(offeredQuestion);
1523
1921
  const structured = offered.map(structuredQuestion);
1524
1922
  const lines = [
1525
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.",
@@ -1651,7 +2049,7 @@ async function describeCreated(resolver, architectureId) {
1651
2049
  }
1652
2050
 
1653
2051
  // src/tools/get-design.ts
1654
- import * as z5 from "zod/v4";
2052
+ import * as z7 from "zod/v4";
1655
2053
 
1656
2054
  // src/lib/slice.ts
1657
2055
  function sliceComponent(design, componentId, ledger) {
@@ -1773,6 +2171,8 @@ function overview(design, ledger) {
1773
2171
  // strings every existing caller reads.
1774
2172
  assumed_decisions: assumedAmong(design, decisions),
1775
2173
  trade_offs: design.trade_offs ?? [],
2174
+ bypassed_connections: design.bypassed_connections ?? [],
2175
+ platform_decisions: design.platform_decisions ?? [],
1776
2176
  // Carried verbatim, rows in the server's order, and only when the version has one. This is
1777
2177
  // a rebuild from named keys, so anything not named here is dropped without a trace — which
1778
2178
  // is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
@@ -1780,37 +2180,69 @@ function overview(design, ledger) {
1780
2180
  ...ledger ? { requirement_ledger: ledger } : {}
1781
2181
  };
1782
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
+ }
1783
2212
  function normalize(value) {
1784
2213
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1785
2214
  }
1786
2215
 
1787
2216
  // src/tools/get-design.ts
1788
- var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
1789
- var input3 = z5.object({
1790
- project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
1791
- mode: z5.enum(MODES).describe(
1792
- "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."
1793
2222
  ),
1794
- component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
1795
- 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.")
1796
2225
  });
1797
- var output3 = z5.object({
1798
- project_id: z5.string(),
1799
- architecture_id: z5.string(),
1800
- version: z5.number(),
1801
- is_released: z5.boolean(),
1802
- newer_release_exists: z5.boolean(),
1803
- resolved_by: z5.string(),
1804
- 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(),
1805
2234
  /** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
1806
- data: z5.unknown()
2235
+ data: z7.unknown()
1807
2236
  });
1808
2237
  function decisionsHeading(assumed, settled, withAssumptions) {
1809
2238
  return assumed.length > 0 ? withAssumptions : settled;
1810
2239
  }
1811
- function decisionLines(decisions, assumed) {
2240
+ function decisionLines(decisions, assumed, platform = []) {
1812
2241
  const marked = new Set(assumed);
1813
- 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
+ );
1814
2246
  }
1815
2247
  function registerGetDesign(server, client, resolver) {
1816
2248
  server.registerTool(
@@ -1818,8 +2250,8 @@ function registerGetDesign(server, client, resolver) {
1818
2250
  {
1819
2251
  title: "Read a SkeletIQ design",
1820
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.',
1821
- inputSchema: input3,
1822
- outputSchema: output3,
2253
+ inputSchema: input4,
2254
+ outputSchema: output4,
1823
2255
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1824
2256
  },
1825
2257
  async ({ project_id, mode, component_id, version }) => guard(async () => {
@@ -1843,6 +2275,7 @@ function registerGetDesign(server, client, resolver) {
1843
2275
  ...data.components.map(
1844
2276
  (c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
1845
2277
  ),
2278
+ ...data.bypassed_connections.length ? ["", ...platformReplacedLines(data)] : [],
1846
2279
  // Above the decisions because the two answer different questions, in
1847
2280
  // this order: requirements are what was asked for, decisions are how
1848
2281
  // the design answered.
@@ -1859,7 +2292,7 @@ function registerGetDesign(server, client, resolver) {
1859
2292
  "Decisions taken \u2014 these are settled, build to them:",
1860
2293
  "Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
1861
2294
  ),
1862
- ...decisionLines(data.design_decisions, data.assumed_decisions)
2295
+ ...decisionLines(data.design_decisions, data.assumed_decisions, data.platform_decisions)
1863
2296
  ] : [],
1864
2297
  ...data.trade_offs.length ? [
1865
2298
  "",
@@ -1888,6 +2321,7 @@ function registerGetDesign(server, client, resolver) {
1888
2321
  `## ${component.name} (${component.id})`,
1889
2322
  `Type: ${component.type}${component.technology ? ` \xB7 ${component.technology}` : ""}`,
1890
2323
  component.description ?? "",
2324
+ ...platformPartLines(component),
1891
2325
  "",
1892
2326
  outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
1893
2327
  incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
@@ -1986,10 +2420,16 @@ function registerGetDesign(server, client, resolver) {
1986
2420
  );
1987
2421
  const unrun = new Set(readiness.unrun_checks ?? []);
1988
2422
  const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
1989
- 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:`;
1990
2424
  const listed = verdict === "unanswered" ? unanswered : open;
2425
+ const summary = readiness.headline;
1991
2426
  return ok({ ...base, ...facts, data: readiness }, [
1992
- 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,
1993
2433
  ...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
1994
2434
  ...unconfirmedGates.length ? [
1995
2435
  "",
@@ -2045,6 +2485,80 @@ function registerGetDesign(server, client, resolver) {
2045
2485
  versionLine(facts)
2046
2486
  ].join("\n"));
2047
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
+ }
2048
2562
  case "gaps": {
2049
2563
  const body = await client.request(`/projects/${project.id}/design-gaps`, {
2050
2564
  query: { architecture_id: resolved.architectureId }
@@ -2070,7 +2584,7 @@ function registerGetDesign(server, client, resolver) {
2070
2584
  ...orphaned.length ? [
2071
2585
  "",
2072
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.`,
2073
- ...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}`)
2074
2588
  ] : [],
2075
2589
  "",
2076
2590
  "The unresolved ones are questions for a person, not for you to answer. Raise",
@@ -2103,6 +2617,7 @@ function requirementBlock(ledger) {
2103
2617
  "established either way, and neither is fine. `cited by nothing` means no component and no",
2104
2618
  "decision in the design claims to address it.",
2105
2619
  ...unconfirmedLegend(rows),
2620
+ ...proposedLegend(rows),
2106
2621
  // Two facts about how far the statuses can be trusted, said only when the server states
2107
2622
  // them. An absent `verification_state` is an older server, not a verifier that did not run,
2108
2623
  // and claiming the second would be this package inventing a fact about someone's design.
@@ -2120,6 +2635,9 @@ function requirementBlock(ledger) {
2120
2635
  ...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
2121
2636
  ];
2122
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
+ }
2123
2641
  function unconfirmedLegend(rows) {
2124
2642
  const words = new Set(rows.filter(isUnconfirmedRequirement).map(requirementVerdict));
2125
2643
  const reasons = new Set(
@@ -2156,7 +2674,8 @@ function requirementsServed(rows) {
2156
2674
  ...rows.map(requirementLine),
2157
2675
  "`fails` \u2014 a named check measured the design as not meeting it; `not_checked` \u2014 nobody",
2158
2676
  "established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven.",
2159
- ...unconfirmedLegend(rows)
2677
+ ...unconfirmedLegend(rows),
2678
+ ...proposedLegend(rows)
2160
2679
  ];
2161
2680
  }
2162
2681
  function requirementsWithdrawn(rows) {
@@ -2178,7 +2697,8 @@ function requirementLine(row) {
2178
2697
  const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
2179
2698
  const by = (row.checked_by ?? "").trim();
2180
2699
  const because = reason ? ` \u2014 ${reason}${by ? ` (checked by ${by})` : ""}` : by ? ` \u2014 checked by ${by}` : "";
2181
- 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}`;
2182
2702
  }
2183
2703
  function settledAnswer(gap) {
2184
2704
  const note = (gap.note ?? "").trim();
@@ -2217,21 +2737,21 @@ function describeRow(row) {
2217
2737
  }
2218
2738
 
2219
2739
  // src/tools/list-projects.ts
2220
- import * as z6 from "zod/v4";
2221
- var input4 = z6.object({
2222
- 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.")
2223
2743
  });
2224
- var output4 = z6.object({
2225
- projects: z6.array(
2226
- z6.object({
2227
- id: z6.string(),
2228
- title: z6.string(),
2229
- description: z6.string().nullable(),
2230
- architecture_count: z6.number(),
2231
- 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()
2232
2752
  })
2233
2753
  ),
2234
- total: z6.number()
2754
+ total: z8.number()
2235
2755
  });
2236
2756
  function registerListProjects(server, resolver) {
2237
2757
  server.registerTool(
@@ -2239,8 +2759,8 @@ function registerListProjects(server, resolver) {
2239
2759
  {
2240
2760
  title: "List SkeletIQ projects",
2241
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.",
2242
- inputSchema: input4,
2243
- outputSchema: output4,
2762
+ inputSchema: input5,
2763
+ outputSchema: output5,
2244
2764
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
2245
2765
  },
2246
2766
  async ({ query }) => guard(async () => {
@@ -2302,6 +2822,7 @@ function buildServer(client, resolver) {
2302
2822
  registerGenerate(server, client, resolver);
2303
2823
  registerCritique(server, client);
2304
2824
  registerCheckDrift(server, client, resolver);
2825
+ registerRequestDecisionChange(server, client, resolver);
2305
2826
  return server;
2306
2827
  }
2307
2828