@skeletiq/mcp 0.4.0 → 0.6.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 +714 -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.6.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,25 @@ 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(),
311
+ /**
312
+ * `existing` (the user already runs it) | `retiring` (this design replaces it). Absent means
313
+ * `new`: the API never sends `new`, and no design stored before the field existed carries it.
314
+ * Left as a string: a value this package has not seen is still shown.
315
+ */
316
+ lifecycle: z.string().nullish()
294
317
  });
295
318
  var ConnectionSchema = z.looseObject({
296
319
  id: z.string().nullable().optional(),
@@ -298,7 +321,11 @@ var ConnectionSchema = z.looseObject({
298
321
  target: z.string(),
299
322
  label: z.string().nullable().optional(),
300
323
  protocol: z.string().nullable().optional(),
301
- is_async: z.boolean().optional()
324
+ is_async: z.boolean().optional(),
325
+ /** TLS, timeout and breaker settings on the edge. */
326
+ policies: z.record(z.string(), z.unknown()).nullish(),
327
+ /** `realized` on an edge the platform added. */
328
+ platform_origin: z.string().nullish()
302
329
  });
303
330
  var GapEntrySchema = z.union([
304
331
  z.string(),
@@ -314,6 +341,7 @@ var GroundedDecisionSchema = z.looseObject({
314
341
  source_ids: z.array(z.string()).nullable().optional(),
315
342
  requirement_ids: z.array(z.string()).nullable().optional(),
316
343
  assumption: z.boolean().optional(),
344
+ answered: z.boolean().optional(),
317
345
  confidence: z.string().optional()
318
346
  });
319
347
  var ArchitectureJsonSchema = z.looseObject({
@@ -326,7 +354,11 @@ var ArchitectureJsonSchema = z.looseObject({
326
354
  trade_offs: z.array(z.string()).nullable().optional(),
327
355
  scalability_notes: z.string().nullable().optional(),
328
356
  assumptions: z.array(GapEntrySchema).nullable().optional(),
329
- open_questions: z.array(GapEntrySchema).nullable().optional()
357
+ open_questions: z.array(GapEntrySchema).nullable().optional(),
358
+ /** Drawn connections the platform layer replaced. */
359
+ bypassed_connections: z.array(ConnectionSchema).nullish(),
360
+ /** The `design_decisions` entries the platform layer appended. */
361
+ platform_decisions: z.array(z.string()).nullish()
330
362
  });
331
363
  var RequirementLedgerRowSchema = z.looseObject({
332
364
  id: z.string(),
@@ -367,6 +399,13 @@ var RequirementLedgerRowSchema = z.looseObject({
367
399
  cited_by_components: z.array(z.string()).nullable().optional(),
368
400
  /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
369
401
  cited_by_decisions: z.array(z.number()).nullable().optional(),
402
+ /** Ids of requirements merged into this one as near-duplicates. A component's `requirement_ids`
403
+ * may still name one of them, and it means this row. Absent from older APIs. */
404
+ aliases: z.array(z.string()).nullable().optional(),
405
+ /** `stated` in the person's own words, `proposed` by the analysis, or `accepted` — a proposal a
406
+ * person confirmed. Absent from older APIs. Left as a string: a value this package has not seen
407
+ * must not stop the whole design from parsing. */
408
+ origin: z.string().nullish(),
370
409
  added_in_version: z.number().nullable().optional(),
371
410
  /** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
372
411
  * recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
@@ -506,6 +545,14 @@ var ReadinessSchema = z.looseObject({
506
545
  * caller falls back to the boolean.
507
546
  */
508
547
  verdict: z.enum(["ready", "outstanding", "unanswered"]).optional(),
548
+ /** The verdict-first summary of the rows. Absent from servers that predate it. */
549
+ headline: z.looseObject({
550
+ met: z.number(),
551
+ total: z.number(),
552
+ verdict: z.enum(["ready", "outstanding", "unanswered"]),
553
+ score: z.number(),
554
+ rules_updated: z.boolean().optional()
555
+ }).nullable().optional(),
509
556
  unknown_gate_count: z.number().optional(),
510
557
  /** Keys of advisory rows whose check never ran. Named, never counted. */
511
558
  unrun_checks: z.array(z.string()).optional(),
@@ -694,7 +741,71 @@ var DriftSchema = z.looseObject({
694
741
  extra_components: z.array(z.looseObject({ name: z.string() })),
695
742
  extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
696
743
  counts: z.record(z.string(), z.number()),
697
- release: ReleaseFactsSchema
744
+ release: ReleaseFactsSchema,
745
+ /** True when the server read the evidence the connector checked. Absent from older servers. */
746
+ evidence_checked: z.boolean().optional(),
747
+ /** Covered components reported implemented with no verified citation. */
748
+ unverified: z.array(ComponentRefSchema).optional(),
749
+ fitness_failing: z.array(z.string()).optional(),
750
+ fitness_unverified: z.array(z.string()).optional(),
751
+ fitness_unknown: z.array(z.string()).optional(),
752
+ schema_version: z.number().optional()
753
+ });
754
+ var DecisionRequestSchema = z.looseObject({
755
+ id: z.string(),
756
+ project_id: z.string(),
757
+ architecture_id: z.string().nullable(),
758
+ version: z.number(),
759
+ decision_id: z.string(),
760
+ decision_title: z.string(),
761
+ proposed: z.string(),
762
+ why: z.string(),
763
+ status: z.enum(["pending", "accepted", "dismissed"]),
764
+ resolution_note: z.string().nullable(),
765
+ accepted_in_version: z.number().nullable(),
766
+ latest_version: z.number(),
767
+ decision_current: z.boolean().optional(),
768
+ schema_version: z.number().optional()
769
+ });
770
+ var FitnessTestSchema = z.looseObject({
771
+ id: z.string(),
772
+ source: z.string(),
773
+ obligation_id: z.string().nullable(),
774
+ subject_ids: z.array(z.string()),
775
+ subject_names: z.array(z.string()),
776
+ assertion: z.string(),
777
+ check: z.string(),
778
+ level: z.string(),
779
+ layer: z.string().nullable()
780
+ });
781
+ var FitnessTestsSchema = z.looseObject({
782
+ architecture_id: z.string(),
783
+ version: z.number(),
784
+ schema_version: z.number().optional(),
785
+ tests: z.array(FitnessTestSchema),
786
+ total: z.number(),
787
+ truncated: z.boolean(),
788
+ unavailable: z.string().nullable().optional()
789
+ });
790
+ var DecisionOptionSchema = z.looseObject({
791
+ id: z.string(),
792
+ label: z.string(),
793
+ rejected_because: z.string().nullable().optional()
794
+ });
795
+ var DecisionRecordViewSchema = z.looseObject({
796
+ id: z.string(),
797
+ title: z.string(),
798
+ chosen: z.string(),
799
+ why: z.string(),
800
+ options: z.array(DecisionOptionSchema),
801
+ basis_removed: z.array(z.string()).default([])
802
+ });
803
+ var DecisionsViewResponseSchema = z.looseObject({
804
+ architecture_id: z.string(),
805
+ version: z.number(),
806
+ status: z.string(),
807
+ reason: z.string().nullable().optional(),
808
+ records: z.array(DecisionRecordViewSchema).default([])
698
809
  });
699
810
  var ClarifyingOptionDetailSchema = z.looseObject({
700
811
  value: z.string(),
@@ -715,7 +826,11 @@ var ClarifyingQuestionSchema = z.looseObject({
715
826
  /** Whether more than one option may be picked at once. */
716
827
  multiple: z.boolean().optional(),
717
828
  /** What leaving the question unanswered does, in the product's words. */
718
- skip_effect: z.string().nullable().optional()
829
+ skip_effect: z.string().nullable().optional(),
830
+ /** The contract field this question resolves, e.g. `regimes.hipaa`; absent when it is not a fork. */
831
+ fork: z.string().nullish(),
832
+ /** Ask only when `question_id` was answered with one of `values`. */
833
+ asked_if: z.object({ question_id: z.string(), values: z.array(z.string()) }).nullish()
719
834
  });
720
835
  var IntentDecisionSchema = z.looseObject({
721
836
  intent: z.string().optional(),
@@ -896,7 +1011,80 @@ function isUuid(value) {
896
1011
  }
897
1012
 
898
1013
  // src/tools/check-drift.ts
1014
+ import * as z3 from "zod/v4";
1015
+
1016
+ // src/lib/evidence.ts
1017
+ import { realpath, readFile, stat } from "fs/promises";
1018
+ import path from "path";
899
1019
  import * as z2 from "zod/v4";
1020
+ var MAX_EVIDENCE_BYTES = 1e6;
1021
+ var MAX_REFS_PER_CLAIM = 5;
1022
+ var evidenceRef = z2.object({
1023
+ path: z2.string().min(1).max(512).describe("The file, relative to the repo root."),
1024
+ quote: z2.string().min(1).max(2e3).optional().describe("Text that must appear in the file. Checked locally, never sent."),
1025
+ symbol: z2.string().min(1).max(255).optional().describe("An identifier that must appear in the file. Checked locally, never sent.")
1026
+ }).refine((ref) => ref.quote !== void 0 || ref.symbol !== void 0, {
1027
+ message: "each evidence entry needs a quote or a symbol"
1028
+ });
1029
+ async function resolveRepoRoot(requested) {
1030
+ try {
1031
+ const root = await realpath(requested);
1032
+ return (await stat(root)).isDirectory() ? root : null;
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ }
1037
+ var MAX_PATH = 512;
1038
+ var MAX_REASON = 200;
1039
+ function collapse(text) {
1040
+ return text.replace(/\s+/g, " ").trim();
1041
+ }
1042
+ function escapeRegExp(text) {
1043
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1044
+ }
1045
+ function inside(root, target) {
1046
+ const relative = path.relative(root, target);
1047
+ return relative === "" || relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
1048
+ }
1049
+ async function verifyEvidence(root, ref) {
1050
+ const shown = ref.path.slice(0, MAX_PATH);
1051
+ const failed = (reason) => ({
1052
+ path: shown,
1053
+ status: "failed",
1054
+ reason: reason.slice(0, MAX_REASON)
1055
+ });
1056
+ try {
1057
+ const realRoot = await realpath(root);
1058
+ const lexical = path.resolve(realRoot, ref.path);
1059
+ if (!inside(realRoot, lexical)) return failed("outside the repository");
1060
+ let target;
1061
+ try {
1062
+ target = await realpath(lexical);
1063
+ } catch (error) {
1064
+ if (error.code === "ENOENT") return failed("file not found");
1065
+ throw error;
1066
+ }
1067
+ if (!inside(realRoot, target)) return failed("outside the repository");
1068
+ const info = await stat(target);
1069
+ if (!info.isFile()) return failed("not a regular file");
1070
+ if (info.size > MAX_EVIDENCE_BYTES) return failed(`file too large (over ${MAX_EVIDENCE_BYTES} bytes)`);
1071
+ const quote = ref.quote === void 0 ? "" : collapse(ref.quote);
1072
+ const symbol = ref.symbol?.trim() ?? "";
1073
+ if (!quote && !symbol) return failed("nothing cited to look for");
1074
+ const content = await readFile(target, "utf8");
1075
+ if (quote && !collapse(content).includes(quote)) {
1076
+ return failed("cited text not found in file");
1077
+ }
1078
+ if (symbol) {
1079
+ const pattern = new RegExp(`(?<![\\w$])${escapeRegExp(symbol)}(?![\\w$])`);
1080
+ if (!pattern.test(content)) return failed("symbol not found in file");
1081
+ }
1082
+ return { path: shown, status: "verified" };
1083
+ } catch (error) {
1084
+ const code = error?.code;
1085
+ return failed(typeof code === "string" ? `file could not be read (${code})` : "file could not be read");
1086
+ }
1087
+ }
900
1088
 
901
1089
  // src/lib/result.ts
902
1090
  function ok(structured, text) {
@@ -929,53 +1117,70 @@ function versionLine(facts) {
929
1117
  }
930
1118
 
931
1119
  // 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(
1120
+ var input = z3.object({
1121
+ project_id: z3.string().describe("A SkeletIQ project id, or its exact name."),
1122
+ version: z3.number().int().positive().optional().describe(
935
1123
  "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
1124
  ),
937
- covers: z2.array(z2.string()).max(500).optional().describe(
1125
+ repo_root: z3.string().optional().describe("The repository root that evidence paths are relative to. Defaults to the current directory."),
1126
+ covers: z3.array(z3.string()).max(500).optional().describe(
938
1127
  "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
1128
  ),
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()
1129
+ components: z3.array(
1130
+ z3.object({
1131
+ id: z3.string().max(200).describe("The component id from the design."),
1132
+ name: z3.string().max(255).optional().describe("The component name, which survives a regeneration when the id does not."),
1133
+ status: z3.enum(["implemented", "partial", "not_started"]),
1134
+ note: z3.string().max(2e3).optional(),
1135
+ 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
1136
  })
947
1137
  ).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()
1138
+ extra_components: z3.array(
1139
+ z3.object({
1140
+ name: z3.string().max(255),
1141
+ type: z3.string().max(64).optional(),
1142
+ technology: z3.string().max(255).optional()
953
1143
  })
954
1144
  ).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()
1145
+ extra_connections: z3.array(
1146
+ z3.object({
1147
+ source: z3.string().max(255),
1148
+ target: z3.string().max(255),
1149
+ protocol: z3.string().max(64).optional()
960
1150
  })
961
1151
  ).max(500).optional().describe(
962
1152
  "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
- )
1153
+ ),
1154
+ fitness: z3.array(
1155
+ z3.object({
1156
+ test_id: z3.string().max(300).describe("The id of a fitness test."),
1157
+ status: z3.enum(["pass", "fail", "not_written"]),
1158
+ test_path: z3.string().min(1).max(512).optional().describe("The file holding the test."),
1159
+ quote: z3.string().min(1).max(2e3).optional().describe("Text that must appear in that file. Checked locally, never sent.")
1160
+ })
1161
+ ).max(500).optional().describe("Your result for each fitness test you wrote or could not write.")
964
1162
  });
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(),
1163
+ var refs = z3.array(z3.object({ id: z3.string(), name: z3.string(), type: z3.string() }));
1164
+ var output = z3.object({
1165
+ architecture_id: z3.string(),
1166
+ version: z3.number(),
1167
+ is_released: z3.boolean(),
1168
+ newer_release_exists: z3.boolean(),
1169
+ resolved_by: z3.string(),
1170
+ in_sync: z3.boolean(),
973
1171
  missing: refs,
974
1172
  unreported: refs,
975
1173
  partial: refs,
976
1174
  elsewhere: refs,
977
- unknown_ids: z2.array(z2.object({ id: z2.string(), suggestions: refs })),
978
- counts: z2.record(z2.string(), z2.number())
1175
+ unknown_ids: z3.array(z3.object({ id: z3.string(), suggestions: refs })),
1176
+ counts: z3.record(z3.string(), z3.number()),
1177
+ evidence_checked: z3.boolean(),
1178
+ unverified: refs,
1179
+ fitness_failing: z3.array(z3.string()),
1180
+ fitness_unverified: z3.array(z3.string()),
1181
+ fitness_unknown: z3.array(z3.string()),
1182
+ checked_against: z3.string(),
1183
+ schema_version: z3.number().nullable()
979
1184
  });
980
1185
  function registerCheckDrift(server, client, resolver) {
981
1186
  server.registerTool(
@@ -987,7 +1192,23 @@ function registerCheckDrift(server, client, resolver) {
987
1192
  outputSchema: output,
988
1193
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
989
1194
  },
990
- async ({ project_id, version, covers, components, extra_components, extra_connections }) => guard(async () => {
1195
+ async ({ project_id, version, repo_root, covers, components, extra_components, extra_connections, fitness }) => guard(async () => {
1196
+ const requested = repo_root ?? process.cwd();
1197
+ const root = await resolveRepoRoot(requested);
1198
+ if (root === null) return failure(`repo_root ${requested} cannot be read`);
1199
+ const checked = await Promise.all(
1200
+ components.map(async ({ evidence, ...component }) => ({
1201
+ ...component,
1202
+ ...evidence ? { evidence: await Promise.all(evidence.map((ref) => verifyEvidence(root, ref))) } : {}
1203
+ }))
1204
+ );
1205
+ const fitnessChecked = fitness ? await Promise.all(
1206
+ fitness.map(async (entry) => ({
1207
+ test_id: entry.test_id,
1208
+ status: entry.status,
1209
+ evidence: entry.test_path && entry.quote ? [await verifyEvidence(root, { path: entry.test_path, quote: entry.quote })] : []
1210
+ }))
1211
+ ) : null;
991
1212
  const project = await resolver.resolveProject(project_id);
992
1213
  const resolved = await resolver.resolveVersion(project.id, version);
993
1214
  const report = DriftSchema.parse(
@@ -995,9 +1216,10 @@ function registerCheckDrift(server, client, resolver) {
995
1216
  method: "POST",
996
1217
  body: {
997
1218
  covers: covers ?? null,
998
- components,
1219
+ components: checked,
999
1220
  extra_components: extra_components ?? null,
1000
- extra_connections: extra_connections ?? null
1221
+ extra_connections: extra_connections ?? null,
1222
+ fitness: fitnessChecked
1001
1223
  }
1002
1224
  })
1003
1225
  );
@@ -1014,15 +1236,34 @@ function registerCheckDrift(server, client, resolver) {
1014
1236
  id: entry.id,
1015
1237
  suggestions: entry.suggestions ?? []
1016
1238
  })),
1017
- counts: report.counts
1239
+ counts: report.counts,
1240
+ evidence_checked: report.evidence_checked === true,
1241
+ unverified: report.unverified ?? [],
1242
+ fitness_failing: report.fitness_failing ?? [],
1243
+ fitness_unverified: report.fitness_unverified ?? [],
1244
+ fitness_unknown: report.fitness_unknown ?? [],
1245
+ checked_against: root,
1246
+ schema_version: report.schema_version ?? null
1018
1247
  };
1019
1248
  const lines = [];
1020
1249
  lines.push(
1021
1250
  report.in_sync ? "In sync \u2014 everything this repository covers is built as designed." : "Drift found:"
1022
1251
  );
1023
1252
  push(lines, "Not started", report.missing, report.counts.missing);
1253
+ push(lines, "Unverified", structured.unverified, report.counts.unverified);
1024
1254
  push(lines, "Half built", report.partial, report.counts.partial);
1025
1255
  push(lines, "You did not mention", report.unreported, report.counts.unreported);
1256
+ if (structured.fitness_failing.length) {
1257
+ lines.push(`Fitness failing (${structured.fitness_failing.length}): ${structured.fitness_failing.join(", ")}`);
1258
+ }
1259
+ if (structured.fitness_unverified.length) {
1260
+ lines.push(
1261
+ `Fitness unverified (${structured.fitness_unverified.length}): ${structured.fitness_unverified.join(", ")}`
1262
+ );
1263
+ }
1264
+ if (structured.fitness_unknown.length) {
1265
+ lines.push(`Fitness unknown (${structured.fitness_unknown.length}): ${structured.fitness_unknown.join(", ")}`);
1266
+ }
1026
1267
  if (report.extra_components.length) {
1027
1268
  lines.push(`In the code but not the design: ${report.extra_components.map((c) => c.name).join(", ")}`);
1028
1269
  }
@@ -1048,6 +1289,12 @@ function registerCheckDrift(server, client, resolver) {
1048
1289
  );
1049
1290
  }
1050
1291
  lines.push("");
1292
+ if (!structured.evidence_checked) {
1293
+ lines.push("This SkeletIQ server does not check evidence yet; these are unverified claims.");
1294
+ }
1295
+ lines.push(`Checked against ${root}`);
1296
+ const note = schemaNote(report.schema_version);
1297
+ if (note) lines.push(note);
1051
1298
  lines.push(versionLine(facts));
1052
1299
  return ok(structured, lines.join("\n"));
1053
1300
  })
@@ -1060,53 +1307,174 @@ function push(lines, label, refs2, total) {
1060
1307
  lines.push(`${label} (${total ?? refs2.length}): ${shown}${more}`);
1061
1308
  }
1062
1309
 
1310
+ // src/tools/request-decision-change.ts
1311
+ import * as z4 from "zod/v4";
1312
+ var REVIEW_LINE = "A person on the project reviews this in To-do; nothing changes until they accept.";
1313
+ var UNSUPPORTED_LINE = "This SkeletIQ server does not take decision requests yet.";
1314
+ var input2 = z4.object({
1315
+ request_id: z4.string().optional().describe("A request you filed earlier. Give this alone to read where it stands."),
1316
+ project_id: z4.string().optional().describe("A SkeletIQ project id, or its exact name. Needed to file a request."),
1317
+ version: z4.number().int().positive().optional().describe("The version you built from. Defaults to the latest released version."),
1318
+ decision_id: z4.string().max(64).optional().describe("The decision's topic id, e.g. database.primary. Needed to file a request."),
1319
+ proposed: z4.string().min(1).max(500).optional().describe("What you propose instead. Needed to file a request."),
1320
+ why: z4.string().min(1).max(2e3).optional().describe("Why the code needs the change. Needed to file a request."),
1321
+ 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."),
1322
+ repo_root: z4.string().optional().describe("The repository root that evidence paths are relative to. Defaults to the current directory.")
1323
+ });
1324
+ var output2 = z4.object({
1325
+ request_id: z4.string(),
1326
+ status: z4.enum(["pending", "accepted", "dismissed"]),
1327
+ decision_id: z4.string(),
1328
+ decision_title: z4.string(),
1329
+ proposed: z4.string(),
1330
+ version: z4.number(),
1331
+ latest_version: z4.number(),
1332
+ accepted_in_version: z4.number().nullable(),
1333
+ resolution_note: z4.string().nullable()
1334
+ });
1335
+ function registerRequestDecisionChange(server, client, resolver) {
1336
+ server.registerTool(
1337
+ "request_decision_change",
1338
+ {
1339
+ title: "Ask a person to change a design decision",
1340
+ 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.`,
1341
+ inputSchema: input2,
1342
+ outputSchema: output2,
1343
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1344
+ },
1345
+ async ({ request_id, project_id, version, decision_id, proposed, why, evidence, repo_root }) => guard(async () => {
1346
+ if (request_id !== void 0) return poll(client, request_id);
1347
+ if (!project_id || !decision_id || !proposed || !why) {
1348
+ return failure(
1349
+ "To file a request give project_id, decision_id, proposed and why. To read an answer, give only request_id."
1350
+ );
1351
+ }
1352
+ let checked = [];
1353
+ if (evidence?.length) {
1354
+ const requested = repo_root ?? process.cwd();
1355
+ const root = await resolveRepoRoot(requested);
1356
+ if (root === null) return failure(`repo_root ${requested} cannot be read`);
1357
+ checked = await Promise.all(evidence.map((ref) => verifyEvidence(root, ref)));
1358
+ }
1359
+ const project = await resolver.resolveProject(project_id);
1360
+ const resolved = await resolver.resolveVersion(project.id, version);
1361
+ let raw;
1362
+ try {
1363
+ raw = await client.request(`/architectures/${resolved.architectureId}/decision-requests`, {
1364
+ method: "POST",
1365
+ body: { decision_id, proposed, why, evidence: checked }
1366
+ });
1367
+ } catch (error) {
1368
+ return refused(error);
1369
+ }
1370
+ return answer(DecisionRequestSchema.parse(raw), true);
1371
+ })
1372
+ );
1373
+ }
1374
+ async function poll(client, requestId) {
1375
+ let raw;
1376
+ try {
1377
+ raw = await client.request(`/decision-requests/${encodeURIComponent(requestId)}`);
1378
+ } catch (error) {
1379
+ if (error instanceof SkeletiqApiError && error.status === 404 && error.message === "Decision request not found") {
1380
+ return failure(
1381
+ `There is no decision request ${requestId} on this server. Check the id; a request is removed when its project is deleted.`
1382
+ );
1383
+ }
1384
+ return refused(error);
1385
+ }
1386
+ return answer(DecisionRequestSchema.parse(raw), false);
1387
+ }
1388
+ function refused(error) {
1389
+ if (error instanceof SkeletiqApiError) {
1390
+ if (error.status === 404) return failure(UNSUPPORTED_LINE);
1391
+ if (error.status === 409) return failure(error.message);
1392
+ return failure(describeApiError(error));
1393
+ }
1394
+ throw error;
1395
+ }
1396
+ function answer(request, filed) {
1397
+ const structured = {
1398
+ request_id: request.id,
1399
+ status: request.status,
1400
+ decision_id: request.decision_id,
1401
+ decision_title: request.decision_title,
1402
+ proposed: request.proposed,
1403
+ version: request.version,
1404
+ latest_version: request.latest_version,
1405
+ accepted_in_version: request.accepted_in_version,
1406
+ resolution_note: request.resolution_note
1407
+ };
1408
+ const lines = [];
1409
+ const subject = `"${request.decision_title}" (${request.decision_id}, version ${request.version}) \u2192 ${request.proposed}`;
1410
+ if (request.status === "pending") {
1411
+ lines.push(`${filed ? "Request filed" : "Request pending"} (${request.id}): ${subject}`);
1412
+ lines.push(REVIEW_LINE);
1413
+ lines.push(`Poll it with request_decision_change(request_id: "${request.id}").`);
1414
+ } else if (request.status === "accepted") {
1415
+ lines.push(`Request ${request.id} accepted: ${subject}`);
1416
+ lines.push(
1417
+ 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 ? (
1418
+ // The server does not record which version the follow-up turn produced, so a newer one
1419
+ // is a reason to look, not proof the change is in it.
1420
+ `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`
1421
+ ) : `accepted at v${request.accepted_in_version}; the design changes when the follow-up turn completes (latest is v${request.latest_version})`
1422
+ );
1423
+ } else {
1424
+ lines.push(`Request ${request.id} dismissed: ${subject}`);
1425
+ lines.push("The design stays as it is.");
1426
+ }
1427
+ if (request.resolution_note) lines.push(`Note from the reviewer: ${request.resolution_note}`);
1428
+ return ok(structured, lines.join("\n"));
1429
+ }
1430
+
1063
1431
  // 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() }))
1432
+ import * as z5 from "zod/v4";
1433
+ var input3 = z5.object({
1434
+ architecture_json: z5.looseObject({
1435
+ title: z5.string(),
1436
+ description: z5.string(),
1437
+ components: z5.array(z5.looseObject({ id: z5.string(), name: z5.string(), type: z5.string() })),
1438
+ connections: z5.array(z5.looseObject({ source: z5.string(), target: z5.string() }))
1071
1439
  }).optional().describe(
1072
1440
  "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
1441
  ),
1074
- domain: z3.string().optional().describe(
1442
+ domain: z5.string().optional().describe(
1075
1443
  "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
1444
  ),
1077
- secondary_domains: z3.array(z3.string()).max(8).optional().describe(
1445
+ secondary_domains: z5.array(z5.string()).max(8).optional().describe(
1078
1446
  "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
1447
  ),
1080
- exposure: z3.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1448
+ exposure: z5.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
1081
1449
  "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
1450
  )
1083
1451
  });
1084
1452
  var SCORE_BASIS_FINDINGS_ONLY = "findings_only";
1085
1453
  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(
1454
+ var output3 = z5.object({
1455
+ architecture_score: z5.number(),
1456
+ score_basis: z5.literal(SCORE_BASIS_FINDINGS_ONLY).describe(SCORE_BASIS_NOTE),
1457
+ security_score: z5.number(),
1458
+ performance_score: z5.number(),
1459
+ resilience_score: z5.number(),
1460
+ data_score: z5.number(),
1461
+ findings: z5.array(
1462
+ z5.object({
1463
+ severity: z5.string(),
1464
+ category: z5.string(),
1465
+ message: z5.string(),
1466
+ affected_components: z5.array(z5.string()),
1467
+ remediation: z5.string(),
1468
+ advisory_only: z5.boolean().describe(
1101
1469
  "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
1470
  )
1103
1471
  })
1104
1472
  ),
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(
1473
+ finding_count: z5.number(),
1474
+ compliance_assessed: z5.boolean().describe("False means no compliance framework was checked, so no compliance finding was possible."),
1475
+ frameworks_checked: z5.array(z5.string()),
1476
+ compliance_note: z5.string().nullable(),
1477
+ exposure_assessed: z5.string().describe(
1110
1478
  "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
1479
  )
1112
1480
  });
@@ -1117,8 +1485,8 @@ function registerCritique(server, client) {
1117
1485
  {
1118
1486
  title: "Critique an architecture",
1119
1487
  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,
1488
+ inputSchema: input3,
1489
+ outputSchema: output3,
1122
1490
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1123
1491
  },
1124
1492
  async ({ architecture_json, domain, secondary_domains, exposure }) => guard(async () => {
@@ -1159,7 +1527,7 @@ function registerCritique(server, client) {
1159
1527
  exposure_assessed: exposureAssessed
1160
1528
  };
1161
1529
  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)}`,
1530
+ `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
1531
  SCORE_BASIS_NOTE,
1164
1532
  "",
1165
1533
  // An agent that reads only the text has to see this too. Without it, "the app
@@ -1209,7 +1577,7 @@ function round(value) {
1209
1577
 
1210
1578
  // src/tools/generate.ts
1211
1579
  import { randomUUID } from "crypto";
1212
- import * as z4 from "zod/v4";
1580
+ import * as z6 from "zod/v4";
1213
1581
 
1214
1582
  // src/http/sse.ts
1215
1583
  async function* readSseFrames(response) {
@@ -1285,65 +1653,65 @@ function frameDetail(frame) {
1285
1653
  }
1286
1654
  return parts.length > 0 ? parts.join(", ") : void 0;
1287
1655
  }
1288
- var constraintsInput = z4.strictObject({
1289
- budget: z4.enum(["low", "medium", "high", "unlimited"]).optional(),
1290
- max_latency_ms: z4.number().int().positive().optional(),
1656
+ var constraintsInput = z6.strictObject({
1657
+ budget: z6.enum(["low", "medium", "high", "unlimited"]).optional(),
1658
+ max_latency_ms: z6.number().int().positive().optional(),
1291
1659
  // Free strings, not an enum, and the description has to say why. SkeletIQ assesses five
1292
1660
  // frameworks; naming anything else is still worth doing, because an unassessed regime is
1293
1661
  // recorded and reaches the design brief as a requirement the agent still has to satisfy.
1294
1662
  // The old text read 'e.g. HIPAA, SOC2, PCI-DSS, GDPR' — it omitted SOX, which is the one
1295
1663
  // an agent is least likely to guess, and said nothing about what happens to the rest.
1296
- compliance: z4.array(z4.string()).optional().describe(
1664
+ compliance: z6.array(z6.string()).optional().describe(
1297
1665
  "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
1666
  ),
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(
1667
+ preferred_cloud: z6.enum(["aws", "gcp", "azure", "any"]).optional(),
1668
+ preferred_technologies: z6.array(z6.string()).optional(),
1669
+ max_components: z6.number().int().positive().optional(),
1670
+ read_write_mix: z6.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
1303
1671
  "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
1672
  ),
1305
- burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1673
+ burstiness: z6.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1306
1674
  '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
1675
  ),
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(
1676
+ data_retention: z6.enum(["days", "months", "years", "indefinite"]).optional(),
1677
+ consistency: z6.enum(["strict", "eventual", "mixed"]).optional(),
1678
+ data_residency: z6.enum(["any", "us", "eu"]).optional(),
1679
+ transfer_band: z6.enum(["light", "moderate", "heavy", "very_heavy", "massive"]).optional().describe(
1312
1680
  "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
1681
  ),
1314
- monthly_transfer_gb: z4.number().min(0).max(1e8).optional().describe(
1682
+ monthly_transfer_gb: z6.number().min(0).max(1e8).optional().describe(
1315
1683
  "The same thing as a figure: GB a month sent out to users. Set this or transfer_band, not both."
1316
1684
  )
1317
1685
  });
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(
1686
+ var generateInput = z6.object({
1687
+ prompt: z6.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
1688
+ project_id: z6.string().optional().describe("An existing SkeletIQ project id to add a version to. Omit to start a new project."),
1689
+ wait: z6.boolean().optional().describe(
1322
1690
  "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
1691
  ),
1324
- clarification_answers: z4.record(z4.string(), z4.string()).optional().describe(
1692
+ clarification_answers: z6.record(z6.string(), z6.string()).optional().describe(
1325
1693
  '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
1694
  ),
1327
1695
  constraints: constraintsInput.optional().describe(
1328
1696
  "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
1697
  )
1330
1698
  });
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()),
1699
+ var generateOutput = z6.object({
1700
+ status: z6.string().describe("completed | clarification_required, or the queue state on wait: false"),
1701
+ project_id: z6.string().nullable(),
1702
+ architecture_id: z6.string().nullable(),
1703
+ version: z6.number().nullable(),
1704
+ job_id: z6.string().nullable(),
1705
+ title: z6.string().nullable(),
1706
+ component_count: z6.number().nullable(),
1707
+ assistant_message: z6.string().nullable(),
1708
+ degradations: z6.array(z6.string()),
1341
1709
  /**
1342
1710
  * The server's sentence when part of what the model was given had to be cut to fit its input
1343
1711
  * window — a source left out, or the current design summarised — and null otherwise. The design
1344
1712
  * is still complete; this says what it was written without.
1345
1713
  */
1346
- prompt_trim_note: z4.string().nullable(),
1714
+ prompt_trim_note: z6.string().nullable(),
1347
1715
  /**
1348
1716
  * Present, and non-empty, only on `status: "clarification_required"`. A refusal is a
1349
1717
  * *result* here rather than an error: an error result may carry no `structuredContent` at
@@ -1351,38 +1719,46 @@ var generateOutput = z4.object({
1351
1719
  * hand the agent the one thing it cannot act on — a sentence saying detail is missing,
1352
1720
  * without saying which.
1353
1721
  */
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())
1722
+ clarifying_questions: z6.array(
1723
+ z6.object({
1724
+ id: z6.string(),
1725
+ question: z6.string(),
1726
+ why: z6.string(),
1727
+ options: z6.array(z6.string()),
1728
+ /** The contract field the answer resolves; null when the question is not a fork. */
1729
+ fork: z6.string().nullable(),
1730
+ /** Ask only once `question_id` is answered with one of `values`; null when always asked. */
1731
+ asked_if: z6.object({ question_id: z6.string(), values: z6.array(z6.string()) }).nullable()
1360
1732
  })
1361
1733
  )
1362
1734
  });
1363
- var statusInput = z4.object({
1364
- job_id: z4.string().describe("The job_id returned by generate_architecture with wait: false.")
1735
+ var statusInput = z6.object({
1736
+ job_id: z6.string().describe("The job_id returned by generate_architecture with wait: false.")
1365
1737
  });
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(),
1738
+ var statusOutput = z6.object({
1739
+ job_id: z6.string(),
1740
+ status: z6.string(),
1741
+ project_id: z6.string().nullable(),
1742
+ architecture_id: z6.string().nullable(),
1743
+ error: z6.string().nullable(),
1744
+ queue_state: z6.string().nullable(),
1373
1745
  /**
1374
1746
  * Set when the run was *declined* rather than broken — the same three refusals the inline
1375
1747
  * path answers with a 409. Nothing ran and nothing was charged, so the next call is a
1376
1748
  * normal one, not a retry.
1377
1749
  */
1378
- refusal_code: z4.string().nullable(),
1750
+ refusal_code: z6.string().nullable(),
1379
1751
  /** 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())
1752
+ clarifying_questions: z6.array(
1753
+ z6.object({
1754
+ id: z6.string(),
1755
+ question: z6.string(),
1756
+ why: z6.string(),
1757
+ options: z6.array(z6.string()),
1758
+ /** The contract field the answer resolves; null when the question is not a fork. */
1759
+ fork: z6.string().nullable(),
1760
+ /** Ask only once `question_id` is answered with one of `values`; null when always asked. */
1761
+ asked_if: z6.object({ question_id: z6.string(), values: z6.array(z6.string()) }).nullable()
1386
1762
  })
1387
1763
  )
1388
1764
  });
@@ -1453,7 +1829,7 @@ function decisionRefusal(progress) {
1453
1829
  return refusal.success ? refusal.data : void 0;
1454
1830
  }
1455
1831
  function refusalQuestions(refusal) {
1456
- return (refusal?.clarifying_questions ?? []).map(offeredQuestion);
1832
+ return orderQuestions(refusal?.clarifying_questions ?? []).map(offeredQuestion);
1457
1833
  }
1458
1834
  function offeredQuestion(question) {
1459
1835
  const options = answerOptions(question);
@@ -1469,18 +1845,50 @@ function offeredQuestion(question) {
1469
1845
  const said = detail.label && detail.label !== option ? `${option} (${detail.label})` : option;
1470
1846
  return [detail.detail ? `${said}: ${detail.detail}` : said];
1471
1847
  }),
1472
- skipEffect: question.skip_effect ?? ""
1848
+ skipEffect: question.skip_effect ?? "",
1849
+ fork: question.fork ?? "",
1850
+ askedIf: question.asked_if ?? void 0
1473
1851
  };
1474
1852
  }
1853
+ function orderQuestions(questions) {
1854
+ const ids = new Set(questions.map((question) => question.id));
1855
+ const placed = /* @__PURE__ */ new Set();
1856
+ const ordered = [];
1857
+ const place = (question) => {
1858
+ if (placed.has(question)) return;
1859
+ placed.add(question);
1860
+ ordered.push(question);
1861
+ for (const child of questions) {
1862
+ if (child.asked_if?.question_id === question.id) place(child);
1863
+ }
1864
+ };
1865
+ for (const question of questions) {
1866
+ if (!question.asked_if) place(question);
1867
+ }
1868
+ for (const question of questions) {
1869
+ if (question.asked_if && !ids.has(question.asked_if.question_id)) place(question);
1870
+ }
1871
+ for (const question of questions) place(question);
1872
+ return ordered;
1873
+ }
1475
1874
  function structuredQuestion(question) {
1476
- return { id: question.id, question: question.question, why: question.why, options: question.options };
1875
+ return {
1876
+ id: question.id,
1877
+ question: question.question,
1878
+ why: question.why,
1879
+ options: question.options,
1880
+ fork: question.fork || null,
1881
+ asked_if: question.askedIf ?? null
1882
+ };
1477
1883
  }
1478
1884
  function questionLines(questions) {
1479
1885
  return questions.flatMap((question) => [
1480
1886
  `${question.id}: ${question.question}`,
1481
1887
  ...question.why ? [` Why it matters: ${question.why}`] : [],
1482
1888
  ...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}`] : []
1889
+ ...question.skipEffect ? [` If it is left unanswered: ${question.skipEffect}`] : [],
1890
+ ...question.fork ? [` Resolves: ${question.fork}`] : [],
1891
+ ...question.askedIf ? [` Ask only if ${question.askedIf.question_id} is answered ${question.askedIf.values.join(" or ")}`] : []
1484
1892
  ]);
1485
1893
  }
1486
1894
  function answerOptions(question) {
@@ -1519,7 +1927,7 @@ function clarifyingQuestions(error) {
1519
1927
  return questions && questions.length > 0 ? questions : void 0;
1520
1928
  }
1521
1929
  function clarificationResult(questions) {
1522
- const offered = questions.map(offeredQuestion);
1930
+ const offered = orderQuestions(questions).map(offeredQuestion);
1523
1931
  const structured = offered.map(structuredQuestion);
1524
1932
  const lines = [
1525
1933
  "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 +2059,7 @@ async function describeCreated(resolver, architectureId) {
1651
2059
  }
1652
2060
 
1653
2061
  // src/tools/get-design.ts
1654
- import * as z5 from "zod/v4";
2062
+ import * as z7 from "zod/v4";
1655
2063
 
1656
2064
  // src/lib/slice.ts
1657
2065
  function sliceComponent(design, componentId, ledger) {
@@ -1773,6 +2181,8 @@ function overview(design, ledger) {
1773
2181
  // strings every existing caller reads.
1774
2182
  assumed_decisions: assumedAmong(design, decisions),
1775
2183
  trade_offs: design.trade_offs ?? [],
2184
+ bypassed_connections: design.bypassed_connections ?? [],
2185
+ platform_decisions: design.platform_decisions ?? [],
1776
2186
  // Carried verbatim, rows in the server's order, and only when the version has one. This is
1777
2187
  // a rebuild from named keys, so anything not named here is dropped without a trace — which
1778
2188
  // is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
@@ -1780,37 +2190,69 @@ function overview(design, ledger) {
1780
2190
  ...ledger ? { requirement_ledger: ledger } : {}
1781
2191
  };
1782
2192
  }
2193
+ var MAX_REPLACED_SHOWN = 10;
2194
+ function policySummary(policies) {
2195
+ return Object.entries(policies).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`).join(", ");
2196
+ }
2197
+ function platformPartLines(component) {
2198
+ const lines = [];
2199
+ const role = component.platform_role;
2200
+ const satisfies = component.satisfies ?? [];
2201
+ if (role || component.platform_origin || component.layer === "platform" || satisfies.length > 0) {
2202
+ const origin = component.platform_origin === "realized" ? "added for you" : component.platform_origin === "adopted" ? "adopted" : component.platform_origin;
2203
+ const label = origin ? ` (${origin})` : "";
2204
+ const satisfied = satisfies.length > 0 ? `; satisfies ${satisfies.join(", ")}` : "";
2205
+ lines.push(`Platform part${label}: ${role ?? "unspecified"}${satisfied}`);
2206
+ }
2207
+ const summary = component.policies ? policySummary(component.policies) : "";
2208
+ if (summary) lines.push(`Policies: ${summary}`);
2209
+ return lines;
2210
+ }
2211
+ function platformReplacedLines(design) {
2212
+ const bypassed = design.bypassed_connections ?? [];
2213
+ if (bypassed.length === 0) return [];
2214
+ const shown = bypassed.slice(0, MAX_REPLACED_SHOWN).map((c) => `- ${c.source} \u2192 ${c.target}`);
2215
+ const more = bypassed.length - MAX_REPLACED_SHOWN;
2216
+ return [
2217
+ "Connections the platform layer replaced:",
2218
+ ...shown,
2219
+ ...more > 0 ? [`and ${more} more`] : []
2220
+ ];
2221
+ }
1783
2222
  function normalize(value) {
1784
2223
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1785
2224
  }
1786
2225
 
1787
2226
  // 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."
2227
+ var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps", "fitness", "decisions"];
2228
+ var input4 = z7.object({
2229
+ project_id: z7.string().describe("A SkeletIQ project id, or its exact name."),
2230
+ mode: z7.enum(MODES).describe(
2231
+ "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
2232
  ),
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.")
2233
+ component_id: z7.string().optional().describe('Required for mode "component". The component id, or its name.'),
2234
+ 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
2235
  });
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(),
2236
+ var output4 = z7.object({
2237
+ project_id: z7.string(),
2238
+ architecture_id: z7.string(),
2239
+ version: z7.number(),
2240
+ is_released: z7.boolean(),
2241
+ newer_release_exists: z7.boolean(),
2242
+ resolved_by: z7.string(),
2243
+ mode: z7.string(),
1805
2244
  /** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
1806
- data: z5.unknown()
2245
+ data: z7.unknown()
1807
2246
  });
1808
2247
  function decisionsHeading(assumed, settled, withAssumptions) {
1809
2248
  return assumed.length > 0 ? withAssumptions : settled;
1810
2249
  }
1811
- function decisionLines(decisions, assumed) {
2250
+ function decisionLines(decisions, assumed, platform = []) {
1812
2251
  const marked = new Set(assumed);
1813
- return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
2252
+ const added = new Set(platform.map((text) => text.trim()));
2253
+ return decisions.map(
2254
+ (decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}${added.has(decision.trim()) ? " (platform)" : ""}`
2255
+ );
1814
2256
  }
1815
2257
  function registerGetDesign(server, client, resolver) {
1816
2258
  server.registerTool(
@@ -1818,8 +2260,8 @@ function registerGetDesign(server, client, resolver) {
1818
2260
  {
1819
2261
  title: "Read a SkeletIQ design",
1820
2262
  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,
2263
+ inputSchema: input4,
2264
+ outputSchema: output4,
1823
2265
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
1824
2266
  },
1825
2267
  async ({ project_id, mode, component_id, version }) => guard(async () => {
@@ -1843,6 +2285,7 @@ function registerGetDesign(server, client, resolver) {
1843
2285
  ...data.components.map(
1844
2286
  (c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
1845
2287
  ),
2288
+ ...data.bypassed_connections.length ? ["", ...platformReplacedLines(data)] : [],
1846
2289
  // Above the decisions because the two answer different questions, in
1847
2290
  // this order: requirements are what was asked for, decisions are how
1848
2291
  // the design answered.
@@ -1859,7 +2302,7 @@ function registerGetDesign(server, client, resolver) {
1859
2302
  "Decisions taken \u2014 these are settled, build to them:",
1860
2303
  "Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
1861
2304
  ),
1862
- ...decisionLines(data.design_decisions, data.assumed_decisions)
2305
+ ...decisionLines(data.design_decisions, data.assumed_decisions, data.platform_decisions)
1863
2306
  ] : [],
1864
2307
  ...data.trade_offs.length ? [
1865
2308
  "",
@@ -1888,6 +2331,7 @@ function registerGetDesign(server, client, resolver) {
1888
2331
  `## ${component.name} (${component.id})`,
1889
2332
  `Type: ${component.type}${component.technology ? ` \xB7 ${component.technology}` : ""}`,
1890
2333
  component.description ?? "",
2334
+ ...platformPartLines(component),
1891
2335
  "",
1892
2336
  outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
1893
2337
  incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
@@ -1986,10 +2430,16 @@ function registerGetDesign(server, client, resolver) {
1986
2430
  );
1987
2431
  const unrun = new Set(readiness.unrun_checks ?? []);
1988
2432
  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:`;
2433
+ 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
2434
  const listed = verdict === "unanswered" ? unanswered : open;
2435
+ const summary = readiness.headline;
1991
2436
  return ok({ ...base, ...facts, data: readiness }, [
1992
- headline,
2437
+ ...summary ? [
2438
+ `${summary.met} of ${summary.total} met \xB7 ${summary.verdict.charAt(0).toUpperCase()}${summary.verdict.slice(1)} \xB7 score ${Math.round(summary.score)}`,
2439
+ ...summary.rules_updated ? ["Rules updated since this check"] : [],
2440
+ ""
2441
+ ] : [],
2442
+ gateLine,
1993
2443
  ...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
1994
2444
  ...unconfirmedGates.length ? [
1995
2445
  "",
@@ -2045,6 +2495,80 @@ function registerGetDesign(server, client, resolver) {
2045
2495
  versionLine(facts)
2046
2496
  ].join("\n"));
2047
2497
  }
2498
+ case "fitness": {
2499
+ const facts = factsFrom(resolved);
2500
+ let body;
2501
+ try {
2502
+ body = await client.request(`/architectures/${resolved.architectureId}/fitness-tests`);
2503
+ } catch (error) {
2504
+ if (error instanceof SkeletiqApiError && error.status === 404) {
2505
+ return ok({ ...base, ...facts, data: { tests: [] } }, [
2506
+ "This SkeletIQ server does not serve fitness tests yet.",
2507
+ versionLine(facts)
2508
+ ].join("\n"));
2509
+ }
2510
+ throw error;
2511
+ }
2512
+ const fitness = FitnessTestsSchema.parse(body);
2513
+ if (fitness.unavailable) {
2514
+ return ok({ ...base, ...facts, data: fitness }, [fitness.unavailable, versionLine(facts)].join("\n"));
2515
+ }
2516
+ const note = schemaNote(fitness.schema_version);
2517
+ return ok({ ...base, ...facts, data: fitness }, [
2518
+ ...note ? [note, ""] : [],
2519
+ 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.",
2520
+ ...fitness.truncated ? [`Showing ${fitness.tests.length} of ${fitness.total}.`] : [],
2521
+ ...fitness.tests.flatMap((test) => [
2522
+ "",
2523
+ `- ${test.subject_names.join(", ") || test.subject_ids.join(", ") || "whole design"} [${test.level}]`,
2524
+ ` assert: ${test.assertion}`,
2525
+ ` check: ${test.check}`,
2526
+ ` test_id: ${test.id}`
2527
+ ]),
2528
+ "",
2529
+ versionLine(facts)
2530
+ ].join("\n"));
2531
+ }
2532
+ case "decisions": {
2533
+ const facts = factsFrom(resolved);
2534
+ let body;
2535
+ try {
2536
+ body = await client.request(`/architectures/${resolved.architectureId}/decisions`);
2537
+ } catch (error) {
2538
+ if (error instanceof SkeletiqApiError && error.status === 403) {
2539
+ return ok({ ...base, ...facts, data: { records: [] } }, [
2540
+ "This SkeletIQ server does not share decisions with tokens yet.",
2541
+ versionLine(facts)
2542
+ ].join("\n"));
2543
+ }
2544
+ throw error;
2545
+ }
2546
+ const decisions = DecisionsViewResponseSchema.parse(body);
2547
+ if (decisions.records.length === 0) {
2548
+ return ok({ ...base, ...facts, data: decisions }, [
2549
+ decisions.status === "unavailable" ? `This version's decisions could not be read (${decisions.reason ?? "unknown"}).` : "This version records no decisions.",
2550
+ versionLine(facts)
2551
+ ].join("\n"));
2552
+ }
2553
+ return ok({ ...base, ...facts, data: decisions }, [
2554
+ ...decisions.records.flatMap((record4) => {
2555
+ const chosen = record4.options.find((option) => option.id === record4.chosen);
2556
+ const rejected = record4.options.filter((option) => option.id !== record4.chosen);
2557
+ return [
2558
+ "",
2559
+ `- ${record4.id}: ${record4.title}`,
2560
+ ` chosen: ${chosen?.label ?? record4.chosen}`,
2561
+ ` why: ${record4.why}`,
2562
+ ...rejected.map(
2563
+ (option) => ` rejected: ${option.label}${option.rejected_because ? ` (${option.rejected_because})` : ""}`
2564
+ ),
2565
+ ...record4.basis_removed.length > 0 ? [` cites removed requirement ${record4.basis_removed.join(", ")}; ask before building on it`] : []
2566
+ ];
2567
+ }),
2568
+ "",
2569
+ versionLine(facts)
2570
+ ].join("\n"));
2571
+ }
2048
2572
  case "gaps": {
2049
2573
  const body = await client.request(`/projects/${project.id}/design-gaps`, {
2050
2574
  query: { architecture_id: resolved.architectureId }
@@ -2070,7 +2594,7 @@ function registerGetDesign(server, client, resolver) {
2070
2594
  ...orphaned.length ? [
2071
2595
  "",
2072
2596
  `${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}`)
2597
+ ...orphaned.filter((answer2) => answer2.adr_id).map((answer2) => `- [${answer2.kind}] ${answer2.action} \u2192 Decision ${answer2.adr_id}`)
2074
2598
  ] : [],
2075
2599
  "",
2076
2600
  "The unresolved ones are questions for a person, not for you to answer. Raise",
@@ -2103,6 +2627,7 @@ function requirementBlock(ledger) {
2103
2627
  "established either way, and neither is fine. `cited by nothing` means no component and no",
2104
2628
  "decision in the design claims to address it.",
2105
2629
  ...unconfirmedLegend(rows),
2630
+ ...proposedLegend(rows),
2106
2631
  // Two facts about how far the statuses can be trusted, said only when the server states
2107
2632
  // them. An absent `verification_state` is an older server, not a verifier that did not run,
2108
2633
  // and claiming the second would be this package inventing a fact about someone's design.
@@ -2120,6 +2645,9 @@ function requirementBlock(ledger) {
2120
2645
  ...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
2121
2646
  ];
2122
2647
  }
2648
+ function proposedLegend(rows) {
2649
+ return rows.some((row) => row.origin === "proposed") ? ["[proposed] = suggested by the design, not asked for; treat as optional."] : [];
2650
+ }
2123
2651
  function unconfirmedLegend(rows) {
2124
2652
  const words = new Set(rows.filter(isUnconfirmedRequirement).map(requirementVerdict));
2125
2653
  const reasons = new Set(
@@ -2156,7 +2684,8 @@ function requirementsServed(rows) {
2156
2684
  ...rows.map(requirementLine),
2157
2685
  "`fails` \u2014 a named check measured the design as not meeting it; `not_checked` \u2014 nobody",
2158
2686
  "established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven.",
2159
- ...unconfirmedLegend(rows)
2687
+ ...unconfirmedLegend(rows),
2688
+ ...proposedLegend(rows)
2160
2689
  ];
2161
2690
  }
2162
2691
  function requirementsWithdrawn(rows) {
@@ -2178,7 +2707,10 @@ function requirementLine(row) {
2178
2707
  const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
2179
2708
  const by = (row.checked_by ?? "").trim();
2180
2709
  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}`;
2710
+ const proposed = row.origin === "proposed" ? "[proposed] " : "";
2711
+ const aliases = (row.aliases ?? []).map((alias) => alias.replace(/\s+/g, " ").trim()).filter(Boolean);
2712
+ const also = aliases.length > 0 ? ` (also ${aliases.join(", ")})` : "";
2713
+ return `- ${row.id}${also}${label} ${proposed}${text || "(no text recorded for this requirement)"}${because}`;
2182
2714
  }
2183
2715
  function settledAnswer(gap) {
2184
2716
  const note = (gap.note ?? "").trim();
@@ -2217,21 +2749,21 @@ function describeRow(row) {
2217
2749
  }
2218
2750
 
2219
2751
  // 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.")
2752
+ import * as z8 from "zod/v4";
2753
+ var input5 = z8.object({
2754
+ query: z8.string().optional().describe("Filter by name or description. Omit to list everything the token can see.")
2223
2755
  });
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()
2756
+ var output5 = z8.object({
2757
+ projects: z8.array(
2758
+ z8.object({
2759
+ id: z8.string(),
2760
+ title: z8.string(),
2761
+ description: z8.string().nullable(),
2762
+ architecture_count: z8.number(),
2763
+ updated_at: z8.string().nullable()
2232
2764
  })
2233
2765
  ),
2234
- total: z6.number()
2766
+ total: z8.number()
2235
2767
  });
2236
2768
  function registerListProjects(server, resolver) {
2237
2769
  server.registerTool(
@@ -2239,8 +2771,8 @@ function registerListProjects(server, resolver) {
2239
2771
  {
2240
2772
  title: "List SkeletIQ projects",
2241
2773
  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,
2774
+ inputSchema: input5,
2775
+ outputSchema: output5,
2244
2776
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
2245
2777
  },
2246
2778
  async ({ query }) => guard(async () => {
@@ -2302,6 +2834,7 @@ function buildServer(client, resolver) {
2302
2834
  registerGenerate(server, client, resolver);
2303
2835
  registerCritique(server, client);
2304
2836
  registerCheckDrift(server, client, resolver);
2837
+ registerRequestDecisionChange(server, client, resolver);
2305
2838
  return server;
2306
2839
  }
2307
2840