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