@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.
- package/README.md +5 -4
- package/dist/index.js +702 -181
- 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.
|
|
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
|
|
37
|
-
return `${url.origin}${
|
|
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(
|
|
205
|
-
const response = await this.fetch(
|
|
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(
|
|
211
|
-
const url = this.url(
|
|
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(
|
|
236
|
-
const url = new URL(`${this.config.apiUrl}${API_PREFIX}${
|
|
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 =
|
|
933
|
-
project_id:
|
|
934
|
-
version:
|
|
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
|
-
|
|
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:
|
|
941
|
-
|
|
942
|
-
id:
|
|
943
|
-
name:
|
|
944
|
-
status:
|
|
945
|
-
note:
|
|
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:
|
|
949
|
-
|
|
950
|
-
name:
|
|
951
|
-
type:
|
|
952
|
-
technology:
|
|
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:
|
|
956
|
-
|
|
957
|
-
source:
|
|
958
|
-
target:
|
|
959
|
-
protocol:
|
|
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 =
|
|
966
|
-
var output =
|
|
967
|
-
architecture_id:
|
|
968
|
-
version:
|
|
969
|
-
is_released:
|
|
970
|
-
newer_release_exists:
|
|
971
|
-
resolved_by:
|
|
972
|
-
in_sync:
|
|
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:
|
|
978
|
-
counts:
|
|
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
|
|
1065
|
-
var
|
|
1066
|
-
architecture_json:
|
|
1067
|
-
title:
|
|
1068
|
-
description:
|
|
1069
|
-
components:
|
|
1070
|
-
connections:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1087
|
-
architecture_score:
|
|
1088
|
-
score_basis:
|
|
1089
|
-
security_score:
|
|
1090
|
-
performance_score:
|
|
1091
|
-
resilience_score:
|
|
1092
|
-
data_score:
|
|
1093
|
-
findings:
|
|
1094
|
-
|
|
1095
|
-
severity:
|
|
1096
|
-
category:
|
|
1097
|
-
message:
|
|
1098
|
-
affected_components:
|
|
1099
|
-
remediation:
|
|
1100
|
-
advisory_only:
|
|
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:
|
|
1106
|
-
compliance_assessed:
|
|
1107
|
-
frameworks_checked:
|
|
1108
|
-
compliance_note:
|
|
1109
|
-
exposure_assessed:
|
|
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:
|
|
1121
|
-
outputSchema:
|
|
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
|
-
`
|
|
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
|
|
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 =
|
|
1289
|
-
budget:
|
|
1290
|
-
max_latency_ms:
|
|
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:
|
|
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:
|
|
1300
|
-
preferred_technologies:
|
|
1301
|
-
max_components:
|
|
1302
|
-
read_write_mix:
|
|
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:
|
|
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:
|
|
1309
|
-
consistency:
|
|
1310
|
-
data_residency:
|
|
1311
|
-
transfer_band:
|
|
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:
|
|
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 =
|
|
1319
|
-
prompt:
|
|
1320
|
-
project_id:
|
|
1321
|
-
wait:
|
|
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:
|
|
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 =
|
|
1332
|
-
status:
|
|
1333
|
-
project_id:
|
|
1334
|
-
architecture_id:
|
|
1335
|
-
version:
|
|
1336
|
-
job_id:
|
|
1337
|
-
title:
|
|
1338
|
-
component_count:
|
|
1339
|
-
assistant_message:
|
|
1340
|
-
degradations:
|
|
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:
|
|
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:
|
|
1355
|
-
|
|
1356
|
-
id:
|
|
1357
|
-
question:
|
|
1358
|
-
why:
|
|
1359
|
-
options:
|
|
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 =
|
|
1364
|
-
job_id:
|
|
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 =
|
|
1367
|
-
job_id:
|
|
1368
|
-
status:
|
|
1369
|
-
project_id:
|
|
1370
|
-
architecture_id:
|
|
1371
|
-
error:
|
|
1372
|
-
queue_state:
|
|
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:
|
|
1740
|
+
refusal_code: z6.string().nullable(),
|
|
1379
1741
|
/** Non-empty only for a `CLARIFICATION_REQUIRED` refusal. */
|
|
1380
|
-
clarifying_questions:
|
|
1381
|
-
|
|
1382
|
-
id:
|
|
1383
|
-
question:
|
|
1384
|
-
why:
|
|
1385
|
-
options:
|
|
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 {
|
|
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
|
|
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
|
|
1790
|
-
project_id:
|
|
1791
|
-
mode:
|
|
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:
|
|
1795
|
-
version:
|
|
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
|
|
1798
|
-
project_id:
|
|
1799
|
-
architecture_id:
|
|
1800
|
-
version:
|
|
1801
|
-
is_released:
|
|
1802
|
-
newer_release_exists:
|
|
1803
|
-
resolved_by:
|
|
1804
|
-
mode:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
1822
|
-
outputSchema:
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
2221
|
-
var
|
|
2222
|
-
query:
|
|
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
|
|
2225
|
-
projects:
|
|
2226
|
-
|
|
2227
|
-
id:
|
|
2228
|
-
title:
|
|
2229
|
-
description:
|
|
2230
|
-
architecture_count:
|
|
2231
|
-
updated_at:
|
|
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:
|
|
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:
|
|
2243
|
-
outputSchema:
|
|
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
|
|