@skeletiq/mcp 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/index.js +522 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,8 +85,8 @@ change your provider keys, see billing, or delete your account, whatever scopes
85
85
  | Tool | What it does |
86
86
  |---|---|
87
87
  | `list_projects` | Find a project by name. Returns the candidates rather than guessing between them. |
88
- | `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. `overview` and `component` resolve the design's `R`-ids to the requirements they name, and what became of each. |
89
- | `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. |
88
+ | `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. Where a version records its requirements, `overview` and `component` resolve the design's `R`-ids to the requirements they name, and say what became of each. |
89
+ | `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. A prompt that states no load comes back with a question instead, and nothing is charged: ask your human, and answer "Not sure yet" when they do not know rather than guessing. |
90
90
  | `get_generation_status` | Poll a generation started with `wait: false`. |
91
91
  | `critique_architecture` | Check a design against SkeletIQ's rules. Deterministic, free, stores nothing. Tell it the `domain` and the `exposure`. |
92
92
  | `check_drift` | Report what you built; hear what is missing, half done, or not in the design. |
@@ -114,8 +114,9 @@ unknown ids with suggestions, they are suggestions — put them to a person rath
114
114
  mapping.
115
115
 
116
116
  **`critique_architecture`'s optional inputs are not neutral.** Omitting one does not skip a
117
- question; it answers it. With no `domain` and `secondary_domains`, no compliance framework applies,
118
- so no compliance finding is possible and the score comes back higher than the SkeletIQ app shows
117
+ question; it answers it. With no `domain`, no compliance framework applies (`secondary_domains`
118
+ select none — they add that domain's design checks as advice — so name a framework when it must
119
+ apply), so no compliance finding is possible and the score comes back higher than the SkeletIQ app shows
119
120
  for the same design — by up to 15 points. With no `exposure`, the design is assessed as
120
121
  internet-facing, which is how an air-gapped system gets told to add a CDN and a WAF. The response
121
122
  says what was actually used — `frameworks_checked` and `exposure_assessed` — and the text output
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
 
6
6
  // src/config.ts
7
7
  var SERVER_NAME = "skeletiq";
8
- var SERVER_VERSION = "0.2.0";
8
+ var SERVER_VERSION = "0.4.0";
9
9
  var DEFAULT_API_URL = "https://api.skeletiq.com";
10
10
  var TOKEN_PREFIX = "skq_";
11
11
  var ConfigError = class extends Error {
@@ -60,12 +60,12 @@ var SkeletiqApiError = class extends Error {
60
60
  /**
61
61
  * A `GET` may be retried after a 429; a `POST` that may have spent credits may not.
62
62
  *
63
- * A spent daily allowance is the one 429 that is *not* retryable: it refills on a clock
64
- * measured in hours, so a caller backing off and trying again is burning its budget on a
65
- * call that cannot succeed today.
63
+ * A spent daily allowance is the 429 that is *not* retryable — the account's, or a runtime's
64
+ * shared one: each refills on a clock measured in hours, so a caller backing off and trying
65
+ * again is burning its budget on a call that cannot succeed today.
66
66
  */
67
67
  get isRetryable() {
68
- if (this.code === "DAILY_LIMIT_REACHED") return false;
68
+ if (this.code === "DAILY_LIMIT_REACHED" || this.code === "RUNTIME_ALLOWANCE_SPENT") return false;
69
69
  return this.status === 429 || this.code === "IDEMPOTENCY_UNAVAILABLE";
70
70
  }
71
71
  };
@@ -138,6 +138,12 @@ function describeApiError(error) {
138
138
  if (upgradeUrl) lines.push(`A larger plan raises the daily limit: ${upgradeUrl}`);
139
139
  break;
140
140
  }
141
+ case "RUNTIME_ALLOWANCE_SPENT": {
142
+ lines.push(error.message);
143
+ const resetAt = asString(detail?.reset_at);
144
+ if (resetAt) lines.push(`The allowance resets at ${resetAt}.`);
145
+ break;
146
+ }
141
147
  case "PLAN_LIMIT_EXCEEDED":
142
148
  case "FEATURE_NOT_AVAILABLE":
143
149
  case "VERSION_LOCKED": {
@@ -331,10 +337,49 @@ var RequirementLedgerRowSchema = z.looseObject({
331
337
  * value added server-side must not stop an older connector parsing the row. */
332
338
  status: z.string().optional(),
333
339
  verifier_checked: z.boolean().optional(),
340
+ /** Which check answered this line: `simulation` | `cost` | `rules` | `structure` | `verifier`.
341
+ * Four are deterministic; `verifier` is the model check, and it only owns a row when it gave a
342
+ * reason. A string rather than a union, for the reason `status` is one. Absent from older APIs. */
343
+ check_method: z.string().nullable().optional(),
344
+ /** What that check said: `fails` | `passes` | `no_gap_found` | `not_checkable` | `not_checked`.
345
+ * `passes` is only ever a measurement; `no_gap_found` is the weaker "nothing contradicted it".
346
+ * Reported beside `status`, which stays populated. Absent from older APIs. */
347
+ verdict: z.string().nullable().optional(),
348
+ /** Why, in one sentence, with the figure or the finding behind it. */
349
+ verdict_reason: z.string().nullable().optional(),
350
+ /** The model behind a `verifier` verdict. Null on every deterministic row — naming a model
351
+ * beside a number the server computed would be a false attribution. */
352
+ checked_by: z.string().nullable().optional(),
353
+ /** Whether this row holds a handoff back: a `fails` a measurement reached (capacity, cost, the
354
+ * review's rules). A model check's `fails` does not block. Absent from older APIs, where
355
+ * `requirementBlocksHandoff` falls back to the method. */
356
+ blocks_handoff: z.boolean().optional(),
357
+ /** Why a `fails` does not block: `model_check` — only the model check failed it, nothing
358
+ * measured it; `unstated_input` — a shortfall that turns on an input nobody gave (an unstated
359
+ * split, or an assumed load — told apart by `unstated_input`); `advisory_finding` — only review
360
+ * advice failed it. Null on every other row. A string, for the reason `status` is one. Absent
361
+ * from older APIs. */
362
+ unconfirmed_reason: z.string().nullable().optional(),
363
+ /** True on a review-rules `fails` whose every finding is advice (`advisory_finding`). */
364
+ advisory_only: z.boolean().optional(),
365
+ /** The assumption an `unstated_input` row turns on, as a clause. Absent from older APIs. */
366
+ unstated_input: z.string().nullable().optional(),
334
367
  cited_by_components: z.array(z.string()).nullable().optional(),
335
368
  /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
336
369
  cited_by_decisions: z.array(z.number()).nullable().optional(),
337
- added_in_version: z.number().nullable().optional()
370
+ added_in_version: z.number().nullable().optional(),
371
+ /** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
372
+ * recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
373
+ superseded_by_change_request: z.string().nullable().optional(),
374
+ superseded_in_version: z.number().nullable().optional(),
375
+ /** What a person said about this line: `confirmed` ("yes, that one is real") or `removed`
376
+ * ("that is not a requirement"). Null or absent means nobody has said, which is neither. A
377
+ * `removed` row is listed under `RequirementLedger.removed` and never in `rows`, so a design
378
+ * is not held to it; it keeps its id so a citation naming it still resolves. `confirmed`
379
+ * changes no count and no verdict. Absent from older APIs. */
380
+ decision: z.string().nullable().optional(),
381
+ /** The version that was on screen when that decision was recorded. */
382
+ decided_in_version: z.number().nullable().optional()
338
383
  });
339
384
  var RequirementLedgerCountsSchema = z.looseObject({
340
385
  total: z.number().optional(),
@@ -343,10 +388,33 @@ var RequirementLedgerCountsSchema = z.looseObject({
343
388
  unmet: z.number().optional(),
344
389
  uncited: z.number().optional(),
345
390
  unchecked: z.number().optional(),
346
- cited: z.number().optional()
391
+ cited: z.number().optional(),
392
+ /** Rows citing no component and no decision (overlaps `unmet`). Absent from older APIs. */
393
+ untraced: z.number().optional(),
394
+ /** The verdict partition, which sums to `total` exactly as the status partition above does —
395
+ * two readings of one set of rows, not two sets. Absent from older APIs. */
396
+ fails: z.number().optional(),
397
+ passes: z.number().optional(),
398
+ no_gap_found: z.number().optional(),
399
+ not_checkable: z.number().optional(),
400
+ not_checked: z.number().optional(),
401
+ /** Rows a person has confirmed are real requirements. In no partition — it overlaps all of
402
+ * them, because confirming a line says nothing about whether the design meets it. */
403
+ confirmed: z.number().optional(),
404
+ /** Ids a person said are **not** requirements. Not part of `total`: those rows are not in
405
+ * `rows` at all. Absent from older APIs. */
406
+ removed: z.number().optional()
347
407
  });
348
408
  var RequirementLedgerSchema = z.looseObject({
349
409
  rows: z.array(RequirementLedgerRowSchema).nullable().optional(),
410
+ /** Ids that were instructions, not requirements. Counted nowhere; listed so an id a component
411
+ * or decision still cites resolves to what became of it. Absent from older APIs. */
412
+ superseded: z.array(RequirementLedgerRowSchema).nullable().optional(),
413
+ /** Ids a person said are not requirements, so the design is not held to them. Kept apart from
414
+ * `superseded` because it is a different statement by a different author. Counted nowhere;
415
+ * listed for the same reason, so a citation naming one still resolves. Absent from older
416
+ * APIs. */
417
+ removed: z.array(RequirementLedgerRowSchema).nullable().optional(),
350
418
  analysis_version: z.number().nullable().optional(),
351
419
  analysis_degraded: z.boolean().optional(),
352
420
  /** `grounded` | `not_grounded` | `diverged`. */
@@ -400,6 +468,12 @@ var BuildOrderSchema = z.looseObject({
400
468
  steps: z.array(BuildStepSchema),
401
469
  release: ReleaseFactsSchema
402
470
  });
471
+ var UnavailableSourceSchema = z.looseObject({
472
+ source: z.string(),
473
+ reason: z.string(),
474
+ /** `failed` | `missing`. A string, not a union: a value added server-side must still parse. */
475
+ kind: z.string().optional()
476
+ });
403
477
  var ReadinessRowSchema = z.looseObject({
404
478
  key: z.string(),
405
479
  label: z.string(),
@@ -409,7 +483,13 @@ var ReadinessRowSchema = z.looseObject({
409
483
  state: z.string(),
410
484
  category: z.string(),
411
485
  gating: z.boolean(),
412
- detail: z.string().nullable().optional()
486
+ detail: z.string().nullable().optional(),
487
+ /** On a row that could not be answered: what would answer it ("Saving a change runs the
488
+ * check on the new version."). Absent on a row that has an answer. */
489
+ clears: z.string().nullable().optional(),
490
+ /** True when stating the expected load is what would answer this row — `clears` then says
491
+ * how. Optional so a server that predates the field still parses. */
492
+ clears_by_load: z.boolean().optional()
413
493
  });
414
494
  var ReadinessSchema = z.looseObject({
415
495
  architecture_id: z.string(),
@@ -438,7 +518,16 @@ var ReadinessSchema = z.looseObject({
438
518
  * Optional here only so a server that predates the field still parses — it is required on the
439
519
  * wire, and an empty array is the server saying it looked and found nothing.
440
520
  */
441
- release_warnings: z.array(z.string()).optional()
521
+ release_warnings: z.array(z.string()).optional(),
522
+ /**
523
+ * Sources this checklist could not speak for, exactly as `/todo` reports them.
524
+ *
525
+ * A failed read reaches each row builder as that source's own empty value — no questions, no
526
+ * decisions, no ledger — and every one of those empties reads as *clear*, so an outage could
527
+ * answer `ready: true` with `release_warnings: []`. Each one also has a row of its own now;
528
+ * this is the same fact in the shape a caller showing both lists can join on.
529
+ */
530
+ unavailable_sources: z.array(UnavailableSourceSchema).optional()
442
531
  });
443
532
  var DesignGapSchema = z.looseObject({
444
533
  gap_id: z.string(),
@@ -447,7 +536,21 @@ var DesignGapSchema = z.looseObject({
447
536
  resolved: z.boolean(),
448
537
  action: z.string().nullable().optional(),
449
538
  note: z.string().nullable().optional(),
450
- adr_id: z.string().nullable().optional()
539
+ adr_id: z.string().nullable().optional(),
540
+ /** `gap:{gap_id}` — the identity the review, the advisor and the decision suggestions share,
541
+ * and the key a dismissal is recorded against. Absent from servers before 0.3.0. */
542
+ problem_key: z.string().optional(),
543
+ /** An answer is recorded and this version was not built with it, so iterating the design
544
+ * would change something. False once a generation has been handed the answer. */
545
+ answer_pending: z.boolean().optional(),
546
+ /** The read/write mix or peak this assumption states that the design is not sized on. Null when
547
+ * the two agree; absent from older servers. */
548
+ sizing_conflict: z.looseObject({
549
+ dimension: z.string(),
550
+ assumed: z.string(),
551
+ sized: z.string(),
552
+ resize_to: z.string().nullable().optional()
553
+ }).nullable().optional()
451
554
  });
452
555
  var OrphanedAnswerSchema = z.looseObject({
453
556
  gap_id: z.string(),
@@ -465,29 +568,6 @@ var DesignGapListSchema = z.looseObject({
465
568
  /** Answers whose question is not in this version — recorded, not silently dropped. */
466
569
  orphaned_answers: z.array(OrphanedAnswerSchema).optional()
467
570
  });
468
- var ComponentRefSchema = z.looseObject({
469
- id: z.string(),
470
- name: z.string(),
471
- type: z.string()
472
- });
473
- var UnknownIdSchema = z.looseObject({
474
- id: z.string(),
475
- suggestions: z.array(ComponentRefSchema).optional()
476
- });
477
- var DriftSchema = z.looseObject({
478
- architecture_id: z.string(),
479
- version: z.number(),
480
- in_sync: z.boolean(),
481
- missing: z.array(ComponentRefSchema),
482
- unreported: z.array(ComponentRefSchema),
483
- partial: z.array(ComponentRefSchema),
484
- elsewhere: z.array(ComponentRefSchema),
485
- unknown_ids: z.array(UnknownIdSchema),
486
- extra_components: z.array(z.looseObject({ name: z.string() })),
487
- extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
488
- counts: z.record(z.string(), z.number()),
489
- release: ReleaseFactsSchema
490
- });
491
571
  var CritiqueFindingSchema = z.looseObject({
492
572
  message: z.string(),
493
573
  pattern_id: z.string(),
@@ -495,7 +575,17 @@ var CritiqueFindingSchema = z.looseObject({
495
575
  severity: z.string().optional(),
496
576
  affected_components: z.array(z.string()).optional(),
497
577
  evidence: z.string().optional(),
498
- remediation: z.string().optional()
578
+ remediation: z.string().optional(),
579
+ /** Reported in full and never charged against the score — advice, not a problem to fix. */
580
+ advisory_only: z.boolean().optional(),
581
+ /**
582
+ * The problem this finding is about. Stable across rewording, and shared with the advisor
583
+ * suggestion and decision record raised by the same problem — the key a dismissal is recorded
584
+ * against.
585
+ */
586
+ problem_key: z.string().optional(),
587
+ /** The project set this problem aside, with a reason. Still reported; never set on an unsaved payload. */
588
+ dismissed: z.boolean().optional()
499
589
  });
500
590
  var PayloadCritiqueSchema = z.looseObject({
501
591
  architecture_score: z.number(),
@@ -513,13 +603,119 @@ var PayloadCritiqueSchema = z.looseObject({
513
603
  compliance_note: z.string().nullable().optional(),
514
604
  exposure_assessed: z.string().optional()
515
605
  });
606
+ var TodoDecisionContextSchema = z.looseObject({
607
+ context: z.string().nullable().optional(),
608
+ consequences: z.string().nullable().optional(),
609
+ alternatives: z.string().nullable().optional(),
610
+ /** `records` | `open` | `tradeoff`. A string, not a union: a value added server-side must still parse. */
611
+ disposition: z.string(),
612
+ supersedes_adr_id: z.string().nullable().optional(),
613
+ source_version: z.number()
614
+ });
615
+ var TodoSourceSchema = z.looseObject({
616
+ source: z.string(),
617
+ message: z.string(),
618
+ detail: z.string().nullable().optional(),
619
+ /** A handle to act on through that source: a suggestion id, a gap id, an R-id. */
620
+ reference_id: z.string().nullable().optional(),
621
+ /** Present on a `critique` or `compliance` source; null on every other kind. */
622
+ finding: CritiqueFindingSchema.nullable().optional(),
623
+ /** Present on a `decisions` source; null on every other kind. */
624
+ decision: TodoDecisionContextSchema.nullable().optional()
625
+ });
626
+ var TodoActionSchema = z.looseObject({
627
+ action: z.string(),
628
+ available: z.boolean(),
629
+ reason: z.string().nullable().optional(),
630
+ reference_id: z.string().nullable().optional()
631
+ });
632
+ var TodoItemSchema = z.looseObject({
633
+ problem_key: z.string(),
634
+ group: z.string(),
635
+ /** `problem` | `question` | `assumption` | `decision` | `verdict`. A string, not a union: a
636
+ * value added server-side must not stop an older connector parsing the card. */
637
+ kind: z.string(),
638
+ title: z.string(),
639
+ severity: z.string(),
640
+ detail: z.string().nullable().optional(),
641
+ components: z.array(z.string()).optional(),
642
+ sources: z.array(TodoSourceSchema).optional(),
643
+ actions: z.array(TodoActionSchema).optional(),
644
+ carried: z.boolean().optional(),
645
+ /** The oldest earlier version known to have had this problem. Null with `carried: true` means
646
+ * it goes back further than the server looked — "an earlier version", never version zero. */
647
+ carried_from_version: z.number().nullable().optional(),
648
+ dismissed: z.boolean().optional(),
649
+ dismissed_reason: z.string().nullable().optional(),
650
+ /** The key of the one readiness row this card is counted on, so the readiness counts can be
651
+ * traced to cards. Null on a card no row counts. */
652
+ counts_toward: z.string().nullable().optional()
653
+ });
654
+ var TodoGroupSchema = z.looseObject({
655
+ key: z.string(),
656
+ label: z.string(),
657
+ items: z.array(TodoItemSchema),
658
+ bulk_action: TodoActionSchema.nullable().optional()
659
+ });
660
+ var TodoSchema = z.looseObject({
661
+ architecture_id: z.string(),
662
+ version: z.number(),
663
+ groups: z.array(TodoGroupSchema),
664
+ dismissed: z.array(TodoItemSchema).optional(),
665
+ counts: z.record(z.string(), z.number()).optional(),
666
+ /** Rows the sources raised before grouping. The gap between this and `counts.total` is the
667
+ * duplication the endpoint removes, reported so the claim stays checkable. */
668
+ raw_row_count: z.number().optional(),
669
+ unavailable_sources: z.array(UnavailableSourceSchema).optional(),
670
+ generation_running: z.boolean().optional(),
671
+ can_edit: z.boolean().optional(),
672
+ /** The project's newest version. Above `version` when this list is for an older one — a
673
+ * release resolved behind newer drafts, or a version asked for by number. Null when unread. */
674
+ latest_version: z.number().nullable().optional()
675
+ });
676
+ var ComponentRefSchema = z.looseObject({
677
+ id: z.string(),
678
+ name: z.string(),
679
+ type: z.string()
680
+ });
681
+ var UnknownIdSchema = z.looseObject({
682
+ id: z.string(),
683
+ suggestions: z.array(ComponentRefSchema).optional()
684
+ });
685
+ var DriftSchema = z.looseObject({
686
+ architecture_id: z.string(),
687
+ version: z.number(),
688
+ in_sync: z.boolean(),
689
+ missing: z.array(ComponentRefSchema),
690
+ unreported: z.array(ComponentRefSchema),
691
+ partial: z.array(ComponentRefSchema),
692
+ elsewhere: z.array(ComponentRefSchema),
693
+ unknown_ids: z.array(UnknownIdSchema),
694
+ extra_components: z.array(z.looseObject({ name: z.string() })),
695
+ extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
696
+ counts: z.record(z.string(), z.number()),
697
+ release: ReleaseFactsSchema
698
+ });
699
+ var ClarifyingOptionDetailSchema = z.looseObject({
700
+ value: z.string(),
701
+ label: z.string(),
702
+ detail: z.string().optional()
703
+ });
516
704
  var ClarifyingQuestionSchema = z.looseObject({
517
705
  id: z.string(),
518
706
  question: z.string(),
519
707
  why: z.string().optional(),
520
708
  kind: z.string().optional(),
521
709
  options: z.array(z.string()).nullable().optional(),
522
- decline_options: z.array(z.string()).nullable().optional()
710
+ decline_options: z.array(z.string()).nullable().optional(),
711
+ /** Options that withdraw the instruction to be rewritten, not answer it ("I'll name the parts"). */
712
+ dismiss_options: z.array(z.string()).nullable().optional(),
713
+ /** Each option's label and what it means, keyed by the value in `options`. */
714
+ option_details: z.array(ClarifyingOptionDetailSchema).nullable().optional(),
715
+ /** Whether more than one option may be picked at once. */
716
+ multiple: z.boolean().optional(),
717
+ /** What leaving the question unanswered does, in the product's words. */
718
+ skip_effect: z.string().nullable().optional()
523
719
  });
524
720
  var IntentDecisionSchema = z.looseObject({
525
721
  intent: z.string().optional(),
@@ -581,7 +777,9 @@ var PROJECT_PAGE_SIZE = 50;
581
777
  var ARCHITECTURE_PAGE_SIZE = 100;
582
778
  var DesignResolver = class {
583
779
  client;
780
+ /** Version summaries per project — enough to choose a version, never to describe one. */
584
781
  versions = /* @__PURE__ */ new Map();
782
+ /** Full records from the single-version read, the requirements ledger included. */
585
783
  designs = /* @__PURE__ */ new Map();
586
784
  constructor(client) {
587
785
  this.client = client;
@@ -610,7 +808,11 @@ var DesignResolver = class {
610
808
  if (exact.length === 1) return exact[0];
611
809
  throw new AmbiguousProjectError(idOrQuery, candidates);
612
810
  }
613
- /** Every version of a project, newest first. Cached: one call serves a whole session. */
811
+ /**
812
+ * Every version of a project, newest first. Cached: one call serves a whole session.
813
+ *
814
+ * These are summaries. They seed nothing in `designs` — see the file header for why.
815
+ */
614
816
  async versionsFor(projectId) {
615
817
  const cached = this.versions.get(projectId);
616
818
  if (cached) return cached;
@@ -619,7 +821,6 @@ var DesignResolver = class {
619
821
  });
620
822
  const items = [...ArchitecturePageSchema.parse(body).items].sort((a, b) => b.version - a.version);
621
823
  this.versions.set(projectId, items);
622
- for (const item of items) this.designs.set(item.id, item);
623
824
  return items;
624
825
  }
625
826
  /** The version rule. */
@@ -659,10 +860,10 @@ var DesignResolver = class {
659
860
  };
660
861
  }
661
862
  /**
662
- * The full design for one version.
863
+ * The full design for one version, from the single-version read.
663
864
  *
664
- * The list response already carries `architecture_json`, so the cache seeded by
665
- * `versionsFor` usually answers this without a second request.
865
+ * One request per version a session actually describes, not one per version in the list:
866
+ * an overview and any number of component slices of the same version share it.
666
867
  */
667
868
  async design(architectureId) {
668
869
  const cached = this.designs.get(architectureId);
@@ -757,7 +958,9 @@ var input = z2.object({
757
958
  target: z2.string().max(255),
758
959
  protocol: z2.string().max(64).optional()
759
960
  })
760
- ).max(500).optional().describe("Edges in the code the design does not have. Endpoints may be component ids or names.")
961
+ ).max(500).optional().describe(
962
+ "Edges in the code the design does not have, each from the caller to the callee: the component that holds the client is the source, including a consumer reading a queue. Endpoints may be component ids or names."
963
+ )
761
964
  });
762
965
  var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
763
966
  var output = z2.object({
@@ -872,7 +1075,7 @@ var input2 = z3.object({
872
1075
  "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."
873
1076
  ),
874
1077
  secondary_domains: z3.array(z3.string()).max(8).optional().describe(
875
- "Further domains the design spans, when it spans more than one \u2014 a multi-tenant shop that takes payments is e-commerce plus fintech and saas. These select frameworks on top of the primary domain's, so leaving them off is why a design already checked against SOC2 and SOX in the app comes back here checked against neither, and scored higher for it. frameworks_checked says which were actually used."
1078
+ "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."
876
1079
  ),
877
1080
  exposure: z3.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
878
1081
  "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."
@@ -893,7 +1096,10 @@ var output2 = z3.object({
893
1096
  category: z3.string(),
894
1097
  message: z3.string(),
895
1098
  affected_components: z3.array(z3.string()),
896
- remediation: z3.string()
1099
+ remediation: z3.string(),
1100
+ advisory_only: z3.boolean().describe(
1101
+ "True for advice: reported and never charged against the score \u2014 a framework nobody named, or a check too uncertain to count. Not a problem to fix."
1102
+ )
897
1103
  })
898
1104
  ),
899
1105
  finding_count: z3.number(),
@@ -932,7 +1138,9 @@ function registerCritique(server, client) {
932
1138
  category: finding.category,
933
1139
  message: finding.message,
934
1140
  affected_components: finding.affected_components ?? [],
935
- remediation: finding.remediation ?? ""
1141
+ remediation: finding.remediation ?? "",
1142
+ // A server too old to send the flag predates it, and counted every finding.
1143
+ advisory_only: finding.advisory_only ?? false
936
1144
  }));
937
1145
  const complianceAssessed = critique.compliance_assessed ?? false;
938
1146
  const exposureAssessed = critique.exposure_assessed ?? "public_internet";
@@ -966,8 +1174,12 @@ function registerCritique(server, client) {
966
1174
  "",
967
1175
  findings.length === 0 ? "No findings." : `${findings.length} finding(s):`,
968
1176
  ...findings.map(
969
- (finding) => `- [${finding.severity}] ${finding.message}` + (finding.affected_components.length ? ` (${finding.affected_components.join(", ")})` : "") + (finding.remediation ? `
1177
+ (finding) => (
1178
+ // Said on the line, because a severity alone reads as a blocker: an
1179
+ // advisory critical is one the score and the app's verdict both ignore.
1180
+ `- [${finding.severity}${finding.advisory_only ? " \xB7 advisory, not scored" : ""}] ${finding.message}` + (finding.affected_components.length ? ` (${finding.affected_components.join(", ")})` : "") + (finding.remediation ? `
970
1181
  Fix: ${finding.remediation}` : "")
1182
+ )
971
1183
  )
972
1184
  ];
973
1185
  return ok(structured, lines.join("\n"));
@@ -1041,9 +1253,10 @@ var PROGRESS_LABELS = {
1041
1253
  refining: "Refining the design"
1042
1254
  };
1043
1255
  var PROGRESS_TOTAL = Object.keys(PROGRESS_LABELS).length + 1;
1256
+ var LOAD_NOT_SURE = "Not sure yet";
1044
1257
  var PROVENANCE_LABELS = {
1045
- user_supplied: "stated",
1046
- llm_inferred: "inferred",
1258
+ user_supplied: "you set this",
1259
+ llm_inferred: "read from your prompt",
1047
1260
  tier_default: "assumed",
1048
1261
  guardrail_clamped: "adjusted"
1049
1262
  };
@@ -1054,16 +1267,19 @@ function asText(value) {
1054
1267
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
1055
1268
  return typeof value === "string" && value.trim() && value !== "unknown" ? value.trim() : void 0;
1056
1269
  }
1270
+ var BARE_FIGURE = /^~?\d[\d,]*(\.\d+)?$/;
1057
1271
  function frameDetail(frame) {
1058
1272
  const data = asRecord2(frame.pipeline_step);
1059
1273
  if (!data) return void 0;
1060
1274
  const parts = [];
1061
1275
  const domain = asText(data.domain);
1062
1276
  if (domain) parts.push(domain);
1063
- const rps = asText(data.requests_per_second);
1064
- const figure = rps ? `${rps} rps` : asText(data.scale);
1277
+ const rawRps = asText(data.requests_per_second);
1278
+ const zeroRate = rawRps !== void 0 && Number(rawRps.replace(/[~,]/g, "")) === 0;
1279
+ const rps = zeroRate ? void 0 : rawRps;
1280
+ const figure = rps ? BARE_FIGURE.test(rps) ? `${rps}/s` : rps : asText(data.scale);
1065
1281
  if (figure) {
1066
- const source = asText(data.scale_source);
1282
+ const source = zeroRate ? void 0 : asText(data.scale_source);
1067
1283
  const label = source ? PROVENANCE_LABELS[source] ?? source : void 0;
1068
1284
  parts.push(label ? `${figure} (${label})` : figure);
1069
1285
  }
@@ -1083,11 +1299,21 @@ var constraintsInput = z4.strictObject({
1083
1299
  preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
1084
1300
  preferred_technologies: z4.array(z4.string()).optional(),
1085
1301
  max_components: z4.number().int().positive().optional(),
1086
- read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe("Nothing reads this from the prompt. Unset, the design assumes 80% reads."),
1087
- burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe("Nothing reads this from the prompt. Unset, the design assumes the tier default peak."),
1302
+ read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
1303
+ "Wins over any share stated in the prompt, which the server also reads. Unset and unstated, the design assumes 80% reads. It sizes components and weights read replicas, so on a read-heavy design it changes the instance counts and the cost."
1304
+ ),
1305
+ burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
1306
+ 'Wins over a peak the prompt states ("peaking at 2,000 per second"), which the server also reads. Unset and unstated, the design assumes a typical peak for its scale.'
1307
+ ),
1088
1308
  data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
1089
1309
  consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
1090
- data_residency: z4.enum(["any", "us", "eu"]).optional()
1310
+ data_residency: z4.enum(["any", "us", "eu"]).optional(),
1311
+ transfer_band: z4.enum(["light", "moderate", "heavy", "very_heavy", "massive"]).optional().describe(
1312
+ "How much data the system sends out to its users a month: light ~100 GB, moderate ~1 TB, heavy ~10 TB, very_heavy ~100 TB, massive ~1 PB. Priced at the cloud's published per-GB rates. Unset, data transfer is left out of the price and the estimate says so. Set this or monthly_transfer_gb, not both."
1313
+ ),
1314
+ monthly_transfer_gb: z4.number().min(0).max(1e8).optional().describe(
1315
+ "The same thing as a figure: GB a month sent out to users. Set this or transfer_band, not both."
1316
+ )
1091
1317
  });
1092
1318
  var generateInput = z4.object({
1093
1319
  prompt: z4.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
@@ -1096,7 +1322,7 @@ var generateInput = z4.object({
1096
1322
  "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."
1097
1323
  ),
1098
1324
  clarification_answers: z4.record(z4.string(), z4.string()).optional().describe(
1099
- '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.'
1325
+ '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.'
1100
1326
  ),
1101
1327
  constraints: constraintsInput.optional().describe(
1102
1328
  "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."
@@ -1112,6 +1338,12 @@ var generateOutput = z4.object({
1112
1338
  component_count: z4.number().nullable(),
1113
1339
  assistant_message: z4.string().nullable(),
1114
1340
  degradations: z4.array(z4.string()),
1341
+ /**
1342
+ * The server's sentence when part of what the model was given had to be cut to fit its input
1343
+ * window — a source left out, or the current design summarised — and null otherwise. The design
1344
+ * is still complete; this says what it was written without.
1345
+ */
1346
+ prompt_trim_note: z4.string().nullable(),
1115
1347
  /**
1116
1348
  * Present, and non-empty, only on `status: "clarification_required"`. A refusal is a
1117
1349
  * *result* here rather than an error: an error result may carry no `structuredContent` at
@@ -1159,7 +1391,7 @@ function registerGenerate(server, client, resolver) {
1159
1391
  "generate_architecture",
1160
1392
  {
1161
1393
  title: "Generate a SkeletIQ architecture",
1162
- description: "Design a system architecture from a prompt. This spends the account holder's credits and takes several minutes, so do not call it speculatively \u2014 if a design already exists, read it with get_design instead. Which model runs it is the account holder's stored setting; you do not choose it. Describe the system, what it must do, and the scale and constraints it runs under: whatever the prompt leaves out is assumed, and a prompt that names no system at all comes back as clarification_required with the questions to answer.",
1394
+ 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.`,
1163
1395
  inputSchema: generateInput,
1164
1396
  outputSchema: generateOutput,
1165
1397
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1203,7 +1435,7 @@ function registerGenerate(server, client, resolver) {
1203
1435
  error: job.error ?? null,
1204
1436
  queue_state: job.queue_state ?? null,
1205
1437
  refusal_code: refusal?.code ?? null,
1206
- clarifying_questions: questions
1438
+ clarifying_questions: questions.map(structuredQuestion)
1207
1439
  };
1208
1440
  const lines = refusal ? refusalLines(job.job_id, refusal, questions) : [`Generation ${job.job_id} is ${job.status}.`];
1209
1441
  if (job.status === "completed" && job.project_id) {
@@ -1221,14 +1453,39 @@ function decisionRefusal(progress) {
1221
1453
  return refusal.success ? refusal.data : void 0;
1222
1454
  }
1223
1455
  function refusalQuestions(refusal) {
1224
- return (refusal?.clarifying_questions ?? []).map((question) => ({
1456
+ return (refusal?.clarifying_questions ?? []).map(offeredQuestion);
1457
+ }
1458
+ function offeredQuestion(question) {
1459
+ const options = answerOptions(question);
1460
+ const details = new Map((question.option_details ?? []).map((detail) => [detail.value, detail]));
1461
+ return {
1225
1462
  id: question.id,
1226
1463
  question: question.question,
1227
1464
  why: question.why ?? "",
1228
- options: (question.options ?? []).filter(
1229
- (option) => !(question.decline_options ?? []).includes(option)
1230
- )
1231
- }));
1465
+ options,
1466
+ explained: options.flatMap((option) => {
1467
+ const detail = details.get(option);
1468
+ if (!detail) return [];
1469
+ const said = detail.label && detail.label !== option ? `${option} (${detail.label})` : option;
1470
+ return [detail.detail ? `${said}: ${detail.detail}` : said];
1471
+ }),
1472
+ skipEffect: question.skip_effect ?? ""
1473
+ };
1474
+ }
1475
+ function structuredQuestion(question) {
1476
+ return { id: question.id, question: question.question, why: question.why, options: question.options };
1477
+ }
1478
+ function questionLines(questions) {
1479
+ return questions.flatMap((question) => [
1480
+ `${question.id}: ${question.question}`,
1481
+ ...question.why ? [` Why it matters: ${question.why}`] : [],
1482
+ ...question.explained.length > 0 ? [" Answers:", ...question.explained.map((line) => ` - ${line}`)] : question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : [],
1483
+ ...question.skipEffect ? [` If it is left unanswered: ${question.skipEffect}`] : []
1484
+ ]);
1485
+ }
1486
+ function answerOptions(question) {
1487
+ const excluded = /* @__PURE__ */ new Set([...question.decline_options ?? [], ...question.dismiss_options ?? []]);
1488
+ return (question.options ?? []).filter((option) => !excluded.has(option));
1232
1489
  }
1233
1490
  function refusalLines(jobId, refusal, questions) {
1234
1491
  const preamble = `Generation ${jobId} was declined before it ran. Nothing was generated and nothing was charged.`;
@@ -1237,11 +1494,7 @@ function refusalLines(jobId, refusal, questions) {
1237
1494
  return [
1238
1495
  preamble,
1239
1496
  "",
1240
- ...questions.flatMap((question) => [
1241
- `${question.id}: ${question.question}`,
1242
- ...question.why ? [` Why it matters: ${question.why}`] : [],
1243
- ...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
1244
- ]),
1497
+ ...questionLines(questions),
1245
1498
  "",
1246
1499
  "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."
1247
1500
  ];
@@ -1266,22 +1519,14 @@ function clarifyingQuestions(error) {
1266
1519
  return questions && questions.length > 0 ? questions : void 0;
1267
1520
  }
1268
1521
  function clarificationResult(questions) {
1269
- const structured = questions.map((question) => ({
1270
- id: question.id,
1271
- question: question.question,
1272
- why: question.why ?? "",
1273
- options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
1274
- }));
1522
+ const offered = questions.map(offeredQuestion);
1523
+ const structured = offered.map(structuredQuestion);
1275
1524
  const lines = [
1276
- "SkeletIQ will not design from this prompt yet \u2014 it names no system to design. Nothing was generated and nothing was charged.",
1525
+ "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.",
1277
1526
  "",
1278
- ...structured.flatMap((question) => [
1279
- `${question.id}: ${question.question}`,
1280
- ...question.why ? [` Why it matters: ${question.why}`] : [],
1281
- ...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
1282
- ]),
1527
+ ...questionLines(offered),
1283
1528
  "",
1284
- "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."
1529
+ "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.` : "")
1285
1530
  ];
1286
1531
  return ok(
1287
1532
  {
@@ -1294,6 +1539,7 @@ function clarificationResult(questions) {
1294
1539
  component_count: null,
1295
1540
  assistant_message: null,
1296
1541
  degradations: [],
1542
+ prompt_trim_note: null,
1297
1543
  clarifying_questions: structured
1298
1544
  },
1299
1545
  lines.join("\n")
@@ -1314,6 +1560,7 @@ async function queueGeneration(client, body) {
1314
1560
  component_count: null,
1315
1561
  assistant_message: null,
1316
1562
  degradations: [],
1563
+ prompt_trim_note: null,
1317
1564
  clarifying_questions: []
1318
1565
  },
1319
1566
  `Queued as job ${queued.job_id}. Poll it with get_generation_status.`
@@ -1362,6 +1609,7 @@ async function streamGeneration(client, body, ctx, resolver) {
1362
1609
  const architectureId = typeof finished.architecture_id === "string" ? finished.architecture_id : null;
1363
1610
  const version = typeof finished.architecture_version === "number" ? finished.architecture_version : null;
1364
1611
  const degradations = Array.isArray(finished.degradations) ? finished.degradations.filter((d) => typeof d === "string") : [];
1612
+ const trimNote = asText(asRecord2(finished.run_summary)?.prompt_trim_note) ?? null;
1365
1613
  const created = architectureId ? await describeCreated(resolver, architectureId) : void 0;
1366
1614
  return ok(
1367
1615
  {
@@ -1374,6 +1622,7 @@ async function streamGeneration(client, body, ctx, resolver) {
1374
1622
  component_count: created?.componentCount ?? null,
1375
1623
  assistant_message: null,
1376
1624
  degradations,
1625
+ prompt_trim_note: trimNote,
1377
1626
  clarifying_questions: []
1378
1627
  },
1379
1628
  [
@@ -1383,7 +1632,8 @@ async function streamGeneration(client, body, ctx, resolver) {
1383
1632
  'Check get_design(mode: "readiness") before building \u2014 a fresh design is a draft, and',
1384
1633
  "its open questions are the ones worth asking now."
1385
1634
  ] : ["SkeletIQ answered in prose rather than producing a design."],
1386
- ...degradations.map((d) => `Degraded: ${d}`)
1635
+ ...degradations.map((d) => `Degraded: ${d}`),
1636
+ ...trimNote ? [`Input trimmed: ${trimNote}`] : []
1387
1637
  ].join("\n")
1388
1638
  );
1389
1639
  }
@@ -1416,7 +1666,10 @@ function sliceComponent(design, componentId, ledger) {
1416
1666
  incoming: connections.filter((c) => c.target === component.id),
1417
1667
  related_decisions: related,
1418
1668
  assumed_decisions: assumedAmong(design, related),
1419
- ...ledger ? { cited_requirements: citedRequirements(ledger, component.id) } : {}
1669
+ ...ledger ? {
1670
+ cited_requirements: citedRequirements(ledger, component.id),
1671
+ withdrawn_requirements: withdrawnRequirements(ledger, component.id)
1672
+ } : {}
1420
1673
  };
1421
1674
  }
1422
1675
  function citedRequirements(ledger, componentId) {
@@ -1424,13 +1677,51 @@ function citedRequirements(ledger, componentId) {
1424
1677
  (ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
1425
1678
  );
1426
1679
  }
1427
- var STATUS_ORDER = ["unmet", "uncited", "unchecked", "cited"];
1680
+ function withdrawnRequirements(ledger, componentId) {
1681
+ return [...ledger.removed ?? [], ...ledger.superseded ?? []].filter(
1682
+ (row) => (row.cited_by_components ?? []).includes(componentId)
1683
+ );
1684
+ }
1685
+ var VERDICT_ORDER = ["fails", "unmet", "not_checked", "not_checkable", "unchecked", "no_gap_found", "passes", "cited"];
1686
+ function isCitedRequirement(row) {
1687
+ return (row.cited_by_components?.length ?? 0) > 0 || (row.cited_by_decisions?.length ?? 0) > 0;
1688
+ }
1689
+ var CITATION_ONLY_METHOD = "structure";
1690
+ var MEASURED_METHODS = ["simulation", "cost", "rules"];
1691
+ var UNCONFIRMED_WORDS = {
1692
+ model_check: "unconfirmed (model check)",
1693
+ unstated_input: "unconfirmed (unstated split)",
1694
+ advisory_finding: "unconfirmed (review advice)"
1695
+ };
1696
+ var ASSUMED_LOAD_INPUT = "the load, which was assumed";
1697
+ var ASSUMED_LOAD_WORD = "unconfirmed (assumed load)";
1698
+ var UNCONFIRMED_ADVICE = "verify before building for it";
1699
+ function requirementBlocksHandoff(row) {
1700
+ if (typeof row.blocks_handoff === "boolean") return row.blocks_handoff;
1701
+ return (row.verdict ?? "").trim() === "fails" && MEASURED_METHODS.includes((row.check_method ?? "").trim());
1702
+ }
1703
+ function requirementVerdict(row) {
1704
+ const verdict = (row.verdict ?? row.status ?? "").trim();
1705
+ if (verdict !== "fails" || requirementBlocksHandoff(row)) return verdict;
1706
+ const method = (row.check_method ?? "").trim();
1707
+ if (method === CITATION_ONLY_METHOD) return "";
1708
+ const reason = row.unconfirmed_reason ?? "model_check";
1709
+ if (reason === "unstated_input" && row.unstated_input?.trim() === ASSUMED_LOAD_INPUT) return ASSUMED_LOAD_WORD;
1710
+ return UNCONFIRMED_WORDS[reason] ?? "unconfirmed";
1711
+ }
1712
+ function isUnconfirmedRequirement(row) {
1713
+ return requirementVerdict(row).startsWith("unconfirmed");
1714
+ }
1428
1715
  function orderRequirementRows(rows) {
1429
- return [...rows].sort((a, b) => statusRank(a) - statusRank(b));
1716
+ return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
1430
1717
  }
1431
- function statusRank(row) {
1432
- const index = STATUS_ORDER.indexOf(row.status ?? "");
1433
- return index === -1 ? STATUS_ORDER.length : index;
1718
+ function verdictRank(row) {
1719
+ const verdict = requirementVerdict(row);
1720
+ if (verdict === "fails" || verdict === "unmet") return 0;
1721
+ if (isUnconfirmedRequirement(row)) return 1;
1722
+ if (!isCitedRequirement(row)) return 2;
1723
+ const index = VERDICT_ORDER.indexOf(verdict);
1724
+ return index === -1 ? VERDICT_ORDER.length + 2 : index + 3;
1434
1725
  }
1435
1726
  function decisionTexts(design) {
1436
1727
  return decisionEntries(design).map((entry) => entry.text);
@@ -1494,11 +1785,11 @@ function normalize(value) {
1494
1785
  }
1495
1786
 
1496
1787
  // src/tools/get-design.ts
1497
- var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps"];
1788
+ var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
1498
1789
  var input3 = z5.object({
1499
1790
  project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
1500
1791
  mode: z5.enum(MODES).describe(
1501
- "overview: the whole design, and the requirements it was built from with what became of each. component: one component, its connections in both directions, the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. readiness: what still has to be decided before this design is worth building. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
1792
+ "overview: the whole design and, where the version records them, the requirements it was built from with what became of each. component: one component, its connections in both directions (the source calls the target), the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. todo: everything still to do on this version, one card per problem \u2014 what blocks a handoff, what is recommended, and what is waiting to be accepted. readiness: the verdict, gate by gate, on whether this design is worth building yet. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
1502
1793
  ),
1503
1794
  component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
1504
1795
  version: z5.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
@@ -1526,7 +1817,7 @@ function registerGetDesign(server, client, resolver) {
1526
1817
  "get_design",
1527
1818
  {
1528
1819
  title: "Read a SkeletIQ design",
1529
- description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "build_order" for the sequence and "component" for each piece as you build it. "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.',
1820
+ 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.',
1530
1821
  inputSchema: input3,
1531
1822
  outputSchema: output3,
1532
1823
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
@@ -1603,6 +1894,11 @@ function registerGetDesign(server, client, resolver) {
1603
1894
  // Same order as the overview: what was asked for, then how the design
1604
1895
  // answered.
1605
1896
  ...requirementsServed(slice.cited_requirements),
1897
+ // Said only when the design still carries the id. A component's
1898
+ // `requirement_ids` are never rewritten when a requirement is
1899
+ // withdrawn, so without this the row would leave the answer in
1900
+ // silence and the id on the box would resolve to nothing again.
1901
+ ...requirementsWithdrawn(slice.withdrawn_requirements),
1606
1902
  // Labelled, and only when there are any. These bullets used to be
1607
1903
  // printed bare under "Called by:", so a decision about the component
1608
1904
  // read as a continuation of its edge list — and a design with no
@@ -1649,6 +1945,33 @@ function registerGetDesign(server, client, resolver) {
1649
1945
  versionLine(facts)
1650
1946
  ].filter(Boolean).join("\n"));
1651
1947
  }
1948
+ case "todo": {
1949
+ const body = await client.request(`/architectures/${resolved.architectureId}/todo`);
1950
+ const todo = TodoSchema.parse(body);
1951
+ const facts = factsFrom(resolved);
1952
+ const groups = todo.groups ?? [];
1953
+ const blocking = groups.find((group) => group.key === "blocks_handoff");
1954
+ const total = groups.reduce((count, group) => count + group.items.length, 0);
1955
+ return ok({ ...base, ...facts, data: todo }, [
1956
+ total === 0 ? "Nothing is outstanding on this version." : `${total} thing(s) still to do on this version` + (blocking?.items.length ? `, ${blocking.items.length} of which block a handoff:` : ", none of which block a handoff:"),
1957
+ ...groups.flatMap(todoGroupBlock),
1958
+ // A source that could not be read is stated, never left to look like a
1959
+ // shorter list. An agent acting on "nothing is outstanding" when the
1960
+ // simulation results failed to load is the exact harm this prevents.
1961
+ ...todo.unavailable_sources?.length ? [
1962
+ "",
1963
+ "Some of this version could not be read, so the list above is incomplete:",
1964
+ ...todo.unavailable_sources.map((entry) => `- ${entry.source}: ${entry.reason}`)
1965
+ ] : [],
1966
+ "",
1967
+ // The actions are the person's. An agent that "accepts" an assumption
1968
+ // has decided something on their behalf, which is the one thing this
1969
+ // surface must never do — and the server refuses it anyway, because an
1970
+ // agent token reaches no write route.
1971
+ "Fix what you can in the design. Answering, accepting and dismissing are the person's to do \u2014 ask, do not decide for them.",
1972
+ versionLine(facts)
1973
+ ].join("\n"));
1974
+ }
1652
1975
  case "readiness": {
1653
1976
  const body = await client.request(`/architectures/${resolved.architectureId}/readiness`);
1654
1977
  const readiness = ReadinessSchema.parse(body);
@@ -1656,7 +1979,11 @@ function registerGetDesign(server, client, resolver) {
1656
1979
  const gates = readiness.rows.filter((row) => row.category === "gate");
1657
1980
  const open = gates.filter((row) => row.gating);
1658
1981
  const unanswered = gates.filter((row) => row.state === "unknown");
1659
- const advisories = readiness.rows.filter((row) => row.category !== "gate");
1982
+ const unconfirmedGates = gates.filter((row) => row.state === "unconfirmed");
1983
+ const accepts = readiness.rows.filter((row) => row.category === "accept");
1984
+ const advisories = readiness.rows.filter(
1985
+ (row) => row.category !== "gate" && row.category !== "accept"
1986
+ );
1660
1987
  const unrun = new Set(readiness.unrun_checks ?? []);
1661
1988
  const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
1662
1989
  const headline = verdict === "ready" ? "Every gate is clear \u2014 this design is ready to build." : verdict === "unanswered" ? `Nothing is undecided, but ${unanswered.length} check(s) could not be answered for this version:` : `${open.length} of ${gates.length} gate(s) still open:`;
@@ -1664,13 +1991,27 @@ function registerGetDesign(server, client, resolver) {
1664
1991
  return ok({ ...base, ...facts, data: readiness }, [
1665
1992
  headline,
1666
1993
  ...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
1994
+ ...unconfirmedGates.length ? [
1995
+ "",
1996
+ "Unconfirmed (the check ran, but its answer turns on an input nobody stated; never blocks):",
1997
+ ...unconfirmedGates.map((row) => `- ${row.label}: ${describeRow(row)}`)
1998
+ ] : [],
1999
+ ...accepts.length ? [
2000
+ "",
2001
+ "To accept (the person agrees to these rather than fixing them; accepting is free and never blocks):",
2002
+ ...accepts.map((row) => `- ${row.label}: ${describeRow(row)}`)
2003
+ ] : [],
1667
2004
  "",
1668
2005
  "Advisory checks (they never block, and some need a paid plan to clear):",
1669
- // An advisory that never ran is marked, not counted. "The advisor has
1670
- // not looked at this design" is a fact an agent being handed work is
1671
- // entitled to, and it is invisible in a bare count of open items.
2006
+ // An advisory with no result is marked, not counted — "nothing to count
2007
+ // here" is a fact an agent being handed work is entitled to, and it is
2008
+ // invisible in a bare count of open items. The mark claims no more than
2009
+ // that: an advisory can be unanswered because no result is *stored* for
2010
+ // this version (the advisor row since the server stopped writing on
2011
+ // read), not only because nothing ever ran, and the server's own
2012
+ // sentence, when it sends one, says which.
1672
2013
  ...advisories.map(
1673
- (row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) ? " (never run on this version)" : ""}`
2014
+ (row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) && !row.detail ? " (no result for this version)" : ""}`
1674
2015
  ),
1675
2016
  // The same list, under the same heading, as the app shows beside its
1676
2017
  // Release button. A person clicking Release is told what the release
@@ -1683,7 +2024,7 @@ function registerGetDesign(server, client, resolver) {
1683
2024
  ...readiness.release_warnings.map((warning) => `- ${warning}`)
1684
2025
  ] : [],
1685
2026
  "",
1686
- "Only a person can clear a gate. Ask; do not decide on their behalf.",
2027
+ 'Only a person can clear a gate. Ask; do not decide on their behalf. Use mode "todo" for the same work item by item.',
1687
2028
  versionLine(facts)
1688
2029
  ].join("\n"));
1689
2030
  }
@@ -1715,11 +2056,11 @@ function registerGetDesign(server, client, resolver) {
1715
2056
  const orphaned = gaps.orphaned_answers ?? [];
1716
2057
  return ok({ ...base, ...facts, data: gaps }, [
1717
2058
  unresolved.length === 0 ? "Every open question and assumption in this design has been dealt with." : `${unresolved.length} unresolved:`,
1718
- ...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}`),
2059
+ ...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}${sizingNote(gap)}`),
1719
2060
  ...settled.length > 0 ? [
1720
2061
  "",
1721
2062
  `${settled.length} settled during review \u2014 treat these as decided:`,
1722
- ...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}`)
2063
+ ...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}${sizingNote(gap)}`)
1723
2064
  ] : [],
1724
2065
  // A gap is identified by a hash of its own text, so a regeneration that
1725
2066
  // re-words a question strands the answer that settled it. The server
@@ -1745,14 +2086,23 @@ function registerGetDesign(server, client, resolver) {
1745
2086
  function requirementBlock(ledger) {
1746
2087
  const rows = orderRequirementRows(ledger?.rows ?? []);
1747
2088
  if (!ledger || rows.length === 0) return [];
1748
- const attention = ["unmet", "uncited", "unchecked"].map((status) => [rows.filter((row) => row.status === status).length, status]).filter(([count]) => count > 0).map(([count, status]) => `${count} ${status}`);
2089
+ const unconfirmed = rows.filter(isUnconfirmedRequirement).length;
2090
+ const attention = [
2091
+ [rows.filter((row) => requirementVerdict(row) === "fails").length, "failing"],
2092
+ [unconfirmed, "unconfirmed"],
2093
+ [rows.filter((row) => !isCitedRequirement(row)).length, "cited by nothing"],
2094
+ [rows.filter((row) => requirementVerdict(row) === "not_checked" || requirementVerdict(row) === "not_checkable").length, "unchecked"]
2095
+ ].filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
1749
2096
  return [
1750
2097
  "",
1751
2098
  `Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
1752
2099
  ...rows.map(requirementLine),
1753
- "Statuses: `unmet` \u2014 the verifier judged the design does not satisfy it; `uncited` \u2014 nothing",
1754
- "in the design claims to address it; `unchecked` \u2014 something cites it and the verifier did not",
1755
- "check it; `cited` \u2014 cited with no gap found, which is not the same as proven.",
2100
+ "Verdicts: `fails` \u2014 a named check measured the design as not meeting it, and says which and why;",
2101
+ "`passes` \u2014 a check measured it against a figure the person stated; `no_gap_found` \u2014 nothing",
2102
+ "contradicted it, which is not the same as proven; `not_checked` / `not_checkable` \u2014 nobody",
2103
+ "established either way, and neither is fine. `cited by nothing` means no component and no",
2104
+ "decision in the design claims to address it.",
2105
+ ...unconfirmedLegend(rows),
1756
2106
  // Two facts about how far the statuses can be trusted, said only when the server states
1757
2107
  // them. An absent `verification_state` is an older server, not a verifier that did not run,
1758
2108
  // and claiming the second would be this package inventing a fact about someone's design.
@@ -1770,20 +2120,65 @@ function requirementBlock(ledger) {
1770
2120
  ...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
1771
2121
  ];
1772
2122
  }
2123
+ function unconfirmedLegend(rows) {
2124
+ const words = new Set(rows.filter(isUnconfirmedRequirement).map(requirementVerdict));
2125
+ const reasons = new Set(
2126
+ rows.filter((row) => isUnconfirmedRequirement(row) && requirementVerdict(row) !== "unconfirmed (assumed load)").map((row) => row.unconfirmed_reason ?? "model_check")
2127
+ );
2128
+ return [
2129
+ ...reasons.has("model_check") ? [
2130
+ "`unconfirmed (model check)` \u2014 only a model check says the design may not meet it. Nothing",
2131
+ `measured it, so it does not block handoff: ${UNCONFIRMED_ADVICE} rather than adding a part.`
2132
+ ] : [],
2133
+ ...reasons.has("unstated_input") ? [
2134
+ "`unconfirmed (unstated split)` \u2014 capacity falls short only under an assumption nobody stated",
2135
+ "(the rate split evenly between entrances, or traffic weights that do not add up). Its reason",
2136
+ "names the split the design would need \u2014 judge whether that is realistic. It does not block",
2137
+ "handoff: ask how the traffic divides."
2138
+ ] : [],
2139
+ ...reasons.has("advisory_finding") ? [
2140
+ "`unconfirmed (review advice)` \u2014 only advice from the design review fails it: the findings it",
2141
+ "rests on are reported, not scored, so it does not block handoff. Read the advice and decide",
2142
+ "whether the design should take it."
2143
+ ] : [],
2144
+ ...words.has("unconfirmed (assumed load)") ? [
2145
+ "`unconfirmed (assumed load)` \u2014 the verdict rests on the load, which was assumed: nobody said how",
2146
+ "much traffic to expect, so the design was sized and priced on the typical load for this kind of",
2147
+ "system. It does not block handoff: ask your human for their load rather than guessing one."
2148
+ ] : []
2149
+ ];
2150
+ }
1773
2151
  function requirementsServed(rows) {
1774
2152
  if (!rows?.length) return [];
1775
2153
  return [
1776
2154
  "",
1777
2155
  "Requirements the design cites this component for:",
1778
2156
  ...rows.map(requirementLine),
1779
- "`unmet` \u2014 the verifier judged the design does not satisfy it; `unchecked` \u2014 the verifier did",
1780
- "not check it; `cited` \u2014 no gap found, which is not the same as proven."
2157
+ "`fails` \u2014 a named check measured the design as not meeting it; `not_checked` \u2014 nobody",
2158
+ "established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven.",
2159
+ ...unconfirmedLegend(rows)
2160
+ ];
2161
+ }
2162
+ function requirementsWithdrawn(rows) {
2163
+ if (!rows?.length) return [];
2164
+ return [
2165
+ "",
2166
+ "This component still cites these ids, and they are no longer requirements \u2014 do not build to them:",
2167
+ ...rows.map(
2168
+ (row) => `- ${row.id} ${(row.text ?? "").replace(/\s+/g, " ").trim() || "(no text recorded)"} [${row.decision === "removed" ? "removed by a person" : `was an instruction, now ${row.superseded_by_change_request ?? "a change request"}`}]`
2169
+ )
1781
2170
  ];
1782
2171
  }
1783
2172
  function requirementLine(row) {
1784
- const status = row.status ? ` [${row.status}]` : "";
2173
+ const verdict = requirementVerdict(row);
2174
+ const shown = isUnconfirmedRequirement(row) ? `${verdict} \u2014 ${UNCONFIRMED_ADVICE}` : verdict;
2175
+ const marks = [shown, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
2176
+ const label = marks.length > 0 ? ` [${marks.join(" \xB7 ")}]` : "";
1785
2177
  const text = (row.text ?? "").replace(/\s+/g, " ").trim();
1786
- return `- ${row.id}${status} ${text || "(no text recorded for this requirement)"}`;
2178
+ const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
2179
+ const by = (row.checked_by ?? "").trim();
2180
+ const because = reason ? ` \u2014 ${reason}${by ? ` (checked by ${by})` : ""}` : by ? ` \u2014 checked by ${by}` : "";
2181
+ return `- ${row.id}${label} ${text || "(no text recorded for this requirement)"}${because}`;
1787
2182
  }
1788
2183
  function settledAnswer(gap) {
1789
2184
  const note = (gap.note ?? "").trim();
@@ -1795,8 +2190,29 @@ function settledAnswer(gap) {
1795
2190
  if (gap.action === "confirmed") return "confirmed as it stands";
1796
2191
  return "answered, but no wording was recorded \u2014 ask before relying on it";
1797
2192
  }
2193
+ function sizingNote(gap) {
2194
+ const conflict = gap.sizing_conflict;
2195
+ return conflict ? ` (this design is sized for ${conflict.sized}, not ${conflict.assumed})` : "";
2196
+ }
2197
+ function todoGroupBlock(group) {
2198
+ if (group.items.length === 0) return [];
2199
+ return ["", `${group.label} (${group.items.length}):`, ...group.items.map(todoLine)];
2200
+ }
2201
+ function todoLine(item) {
2202
+ const severity = item.severity && item.severity !== "none" ? `[${item.severity}] ` : "";
2203
+ const raisedBy = [...new Set((item.sources ?? []).map((source) => source.source))];
2204
+ const raised = raisedBy.length > 0 ? ` \u2014 raised by ${raisedBy.join(", ")}` : "";
2205
+ const carried = item.carried ? item.carried_from_version ? ` \u2014 carried from v${item.carried_from_version}` : " \u2014 carried from an earlier version" : "";
2206
+ const components = item.components?.length ? ` \u2014 affects ${item.components.join(", ")}` : "";
2207
+ return `- ${severity}${item.title.replace(/\s+/g, " ").trim()}${components}${raised}${carried}`;
2208
+ }
1798
2209
  function describeRow(row) {
1799
- if (row.state === "unknown") return "not checked";
2210
+ if (row.state === "unknown") {
2211
+ const clears = row.clears ? ` ${row.clears}` : "";
2212
+ const detail = row.detail ? ` \u2014 ${row.detail}${clears && !/[.!?]$/.test(row.detail) ? "." : ""}` : clears ? " \u2014" : "";
2213
+ return `not checked${detail}${clears}`;
2214
+ }
2215
+ if (row.state === "unconfirmed") return `unconfirmed${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1800
2216
  return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1801
2217
  }
1802
2218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skeletiq/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Design in SkeletIQ, build with your coding agent. MCP server for the SkeletIQ architecture platform.",
5
5
  "license": "MIT",
6
6
  "author": "Sabhahith Works Private Limited",