@skeletiq/mcp 0.2.0 → 0.3.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 +1 -1
  2. package/dist/index.js +329 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,7 +85,7 @@ 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. |
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
89
  | `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. |
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`. |
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.3.0";
9
9
  var DEFAULT_API_URL = "https://api.skeletiq.com";
10
10
  var TOKEN_PREFIX = "skq_";
11
11
  var ConfigError = class extends Error {
@@ -331,10 +331,35 @@ var RequirementLedgerRowSchema = z.looseObject({
331
331
  * value added server-side must not stop an older connector parsing the row. */
332
332
  status: z.string().optional(),
333
333
  verifier_checked: z.boolean().optional(),
334
+ /** Which check answered this line: `simulation` | `cost` | `rules` | `structure` | `verifier`.
335
+ * Four are deterministic; `verifier` is the model check, and it only owns a row when it gave a
336
+ * reason. A string rather than a union, for the reason `status` is one. Absent from older APIs. */
337
+ check_method: z.string().nullable().optional(),
338
+ /** What that check said: `fails` | `passes` | `no_gap_found` | `not_checkable` | `not_checked`.
339
+ * `passes` is only ever a measurement; `no_gap_found` is the weaker "nothing contradicted it".
340
+ * Reported beside `status`, which stays populated. Absent from older APIs. */
341
+ verdict: z.string().nullable().optional(),
342
+ /** Why, in one sentence, with the figure or the finding behind it. */
343
+ verdict_reason: z.string().nullable().optional(),
344
+ /** The model behind a `verifier` verdict. Null on every deterministic row — naming a model
345
+ * beside a number the server computed would be a false attribution. */
346
+ checked_by: z.string().nullable().optional(),
334
347
  cited_by_components: z.array(z.string()).nullable().optional(),
335
348
  /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
336
349
  cited_by_decisions: z.array(z.number()).nullable().optional(),
337
- added_in_version: z.number().nullable().optional()
350
+ added_in_version: z.number().nullable().optional(),
351
+ /** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
352
+ * recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
353
+ superseded_by_change_request: z.string().nullable().optional(),
354
+ superseded_in_version: z.number().nullable().optional(),
355
+ /** What a person said about this line: `confirmed` ("yes, that one is real") or `removed`
356
+ * ("that is not a requirement"). Null or absent means nobody has said, which is neither. A
357
+ * `removed` row is listed under `RequirementLedger.removed` and never in `rows`, so a design
358
+ * is not held to it; it keeps its id so a citation naming it still resolves. `confirmed`
359
+ * changes no count and no verdict. Absent from older APIs. */
360
+ decision: z.string().nullable().optional(),
361
+ /** The version that was on screen when that decision was recorded. */
362
+ decided_in_version: z.number().nullable().optional()
338
363
  });
339
364
  var RequirementLedgerCountsSchema = z.looseObject({
340
365
  total: z.number().optional(),
@@ -343,10 +368,33 @@ var RequirementLedgerCountsSchema = z.looseObject({
343
368
  unmet: z.number().optional(),
344
369
  uncited: z.number().optional(),
345
370
  unchecked: z.number().optional(),
346
- cited: z.number().optional()
371
+ cited: z.number().optional(),
372
+ /** Rows citing no component and no decision (overlaps `unmet`). Absent from older APIs. */
373
+ untraced: z.number().optional(),
374
+ /** The verdict partition, which sums to `total` exactly as the status partition above does —
375
+ * two readings of one set of rows, not two sets. Absent from older APIs. */
376
+ fails: z.number().optional(),
377
+ passes: z.number().optional(),
378
+ no_gap_found: z.number().optional(),
379
+ not_checkable: z.number().optional(),
380
+ not_checked: z.number().optional(),
381
+ /** Rows a person has confirmed are real requirements. In no partition — it overlaps all of
382
+ * them, because confirming a line says nothing about whether the design meets it. */
383
+ confirmed: z.number().optional(),
384
+ /** Ids a person said are **not** requirements. Not part of `total`: those rows are not in
385
+ * `rows` at all. Absent from older APIs. */
386
+ removed: z.number().optional()
347
387
  });
348
388
  var RequirementLedgerSchema = z.looseObject({
349
389
  rows: z.array(RequirementLedgerRowSchema).nullable().optional(),
390
+ /** Ids that were instructions, not requirements. Counted nowhere; listed so an id a component
391
+ * or decision still cites resolves to what became of it. Absent from older APIs. */
392
+ superseded: z.array(RequirementLedgerRowSchema).nullable().optional(),
393
+ /** Ids a person said are not requirements, so the design is not held to them. Kept apart from
394
+ * `superseded` because it is a different statement by a different author. Counted nowhere;
395
+ * listed for the same reason, so a citation naming one still resolves. Absent from older
396
+ * APIs. */
397
+ removed: z.array(RequirementLedgerRowSchema).nullable().optional(),
350
398
  analysis_version: z.number().nullable().optional(),
351
399
  analysis_degraded: z.boolean().optional(),
352
400
  /** `grounded` | `not_grounded` | `diverged`. */
@@ -400,6 +448,12 @@ var BuildOrderSchema = z.looseObject({
400
448
  steps: z.array(BuildStepSchema),
401
449
  release: ReleaseFactsSchema
402
450
  });
451
+ var UnavailableSourceSchema = z.looseObject({
452
+ source: z.string(),
453
+ reason: z.string(),
454
+ /** `failed` | `missing`. A string, not a union: a value added server-side must still parse. */
455
+ kind: z.string().optional()
456
+ });
403
457
  var ReadinessRowSchema = z.looseObject({
404
458
  key: z.string(),
405
459
  label: z.string(),
@@ -438,7 +492,16 @@ var ReadinessSchema = z.looseObject({
438
492
  * Optional here only so a server that predates the field still parses — it is required on the
439
493
  * wire, and an empty array is the server saying it looked and found nothing.
440
494
  */
441
- release_warnings: z.array(z.string()).optional()
495
+ release_warnings: z.array(z.string()).optional(),
496
+ /**
497
+ * Sources this checklist could not speak for, exactly as `/todo` reports them.
498
+ *
499
+ * A failed read reaches each row builder as that source's own empty value — no questions, no
500
+ * decisions, no ledger — and every one of those empties reads as *clear*, so an outage could
501
+ * answer `ready: true` with `release_warnings: []`. Each one also has a row of its own now;
502
+ * this is the same fact in the shape a caller showing both lists can join on.
503
+ */
504
+ unavailable_sources: z.array(UnavailableSourceSchema).optional()
442
505
  });
443
506
  var DesignGapSchema = z.looseObject({
444
507
  gap_id: z.string(),
@@ -447,7 +510,13 @@ var DesignGapSchema = z.looseObject({
447
510
  resolved: z.boolean(),
448
511
  action: z.string().nullable().optional(),
449
512
  note: z.string().nullable().optional(),
450
- adr_id: z.string().nullable().optional()
513
+ adr_id: z.string().nullable().optional(),
514
+ /** `gap:{gap_id}` — the identity the review, the advisor and the decision suggestions share,
515
+ * and the key a dismissal is recorded against. Absent from servers before 0.3.0. */
516
+ problem_key: z.string().optional(),
517
+ /** An answer is recorded and this version was not built with it, so iterating the design
518
+ * would change something. False once a generation has been handed the answer. */
519
+ answer_pending: z.boolean().optional()
451
520
  });
452
521
  var OrphanedAnswerSchema = z.looseObject({
453
522
  gap_id: z.string(),
@@ -465,29 +534,6 @@ var DesignGapListSchema = z.looseObject({
465
534
  /** Answers whose question is not in this version — recorded, not silently dropped. */
466
535
  orphaned_answers: z.array(OrphanedAnswerSchema).optional()
467
536
  });
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
537
  var CritiqueFindingSchema = z.looseObject({
492
538
  message: z.string(),
493
539
  pattern_id: z.string(),
@@ -495,7 +541,17 @@ var CritiqueFindingSchema = z.looseObject({
495
541
  severity: z.string().optional(),
496
542
  affected_components: z.array(z.string()).optional(),
497
543
  evidence: z.string().optional(),
498
- remediation: z.string().optional()
544
+ remediation: z.string().optional(),
545
+ /** Reported in full and never charged against the score — advice, not a problem to fix. */
546
+ advisory_only: z.boolean().optional(),
547
+ /**
548
+ * The problem this finding is about. Stable across rewording, and shared with the advisor
549
+ * suggestion and decision record raised by the same problem — the key a dismissal is recorded
550
+ * against.
551
+ */
552
+ problem_key: z.string().optional(),
553
+ /** The project set this problem aside, with a reason. Still reported; never set on an unsaved payload. */
554
+ dismissed: z.boolean().optional()
499
555
  });
500
556
  var PayloadCritiqueSchema = z.looseObject({
501
557
  architecture_score: z.number(),
@@ -513,6 +569,96 @@ var PayloadCritiqueSchema = z.looseObject({
513
569
  compliance_note: z.string().nullable().optional(),
514
570
  exposure_assessed: z.string().optional()
515
571
  });
572
+ var TodoDecisionContextSchema = z.looseObject({
573
+ context: z.string().nullable().optional(),
574
+ consequences: z.string().nullable().optional(),
575
+ alternatives: z.string().nullable().optional(),
576
+ /** `records` | `open` | `tradeoff`. A string, not a union: a value added server-side must still parse. */
577
+ disposition: z.string(),
578
+ supersedes_adr_id: z.string().nullable().optional(),
579
+ source_version: z.number()
580
+ });
581
+ var TodoSourceSchema = z.looseObject({
582
+ source: z.string(),
583
+ message: z.string(),
584
+ detail: z.string().nullable().optional(),
585
+ /** A handle to act on through that source: a suggestion id, a gap id, an R-id. */
586
+ reference_id: z.string().nullable().optional(),
587
+ /** Present on a `critique` or `compliance` source; null on every other kind. */
588
+ finding: CritiqueFindingSchema.nullable().optional(),
589
+ /** Present on a `decisions` source; null on every other kind. */
590
+ decision: TodoDecisionContextSchema.nullable().optional()
591
+ });
592
+ var TodoActionSchema = z.looseObject({
593
+ action: z.string(),
594
+ available: z.boolean(),
595
+ reason: z.string().nullable().optional(),
596
+ reference_id: z.string().nullable().optional()
597
+ });
598
+ var TodoItemSchema = z.looseObject({
599
+ problem_key: z.string(),
600
+ group: z.string(),
601
+ /** `problem` | `question` | `assumption` | `decision` | `verdict`. A string, not a union: a
602
+ * value added server-side must not stop an older connector parsing the card. */
603
+ kind: z.string(),
604
+ title: z.string(),
605
+ severity: z.string(),
606
+ detail: z.string().nullable().optional(),
607
+ components: z.array(z.string()).optional(),
608
+ sources: z.array(TodoSourceSchema).optional(),
609
+ actions: z.array(TodoActionSchema).optional(),
610
+ carried: z.boolean().optional(),
611
+ /** The oldest earlier version known to have had this problem. Null with `carried: true` means
612
+ * it goes back further than the server looked — "an earlier version", never version zero. */
613
+ carried_from_version: z.number().nullable().optional(),
614
+ dismissed: z.boolean().optional(),
615
+ dismissed_reason: z.string().nullable().optional()
616
+ });
617
+ var TodoGroupSchema = z.looseObject({
618
+ key: z.string(),
619
+ label: z.string(),
620
+ items: z.array(TodoItemSchema),
621
+ bulk_action: TodoActionSchema.nullable().optional()
622
+ });
623
+ var TodoSchema = z.looseObject({
624
+ architecture_id: z.string(),
625
+ version: z.number(),
626
+ groups: z.array(TodoGroupSchema),
627
+ dismissed: z.array(TodoItemSchema).optional(),
628
+ counts: z.record(z.string(), z.number()).optional(),
629
+ /** Rows the sources raised before grouping. The gap between this and `counts.total` is the
630
+ * duplication the endpoint removes, reported so the claim stays checkable. */
631
+ raw_row_count: z.number().optional(),
632
+ unavailable_sources: z.array(UnavailableSourceSchema).optional(),
633
+ generation_running: z.boolean().optional(),
634
+ can_edit: z.boolean().optional(),
635
+ /** The project's newest version. Above `version` when this list is for an older one — a
636
+ * release resolved behind newer drafts, or a version asked for by number. Null when unread. */
637
+ latest_version: z.number().nullable().optional()
638
+ });
639
+ var ComponentRefSchema = z.looseObject({
640
+ id: z.string(),
641
+ name: z.string(),
642
+ type: z.string()
643
+ });
644
+ var UnknownIdSchema = z.looseObject({
645
+ id: z.string(),
646
+ suggestions: z.array(ComponentRefSchema).optional()
647
+ });
648
+ var DriftSchema = z.looseObject({
649
+ architecture_id: z.string(),
650
+ version: z.number(),
651
+ in_sync: z.boolean(),
652
+ missing: z.array(ComponentRefSchema),
653
+ unreported: z.array(ComponentRefSchema),
654
+ partial: z.array(ComponentRefSchema),
655
+ elsewhere: z.array(ComponentRefSchema),
656
+ unknown_ids: z.array(UnknownIdSchema),
657
+ extra_components: z.array(z.looseObject({ name: z.string() })),
658
+ extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
659
+ counts: z.record(z.string(), z.number()),
660
+ release: ReleaseFactsSchema
661
+ });
516
662
  var ClarifyingQuestionSchema = z.looseObject({
517
663
  id: z.string(),
518
664
  question: z.string(),
@@ -581,7 +727,9 @@ var PROJECT_PAGE_SIZE = 50;
581
727
  var ARCHITECTURE_PAGE_SIZE = 100;
582
728
  var DesignResolver = class {
583
729
  client;
730
+ /** Version summaries per project — enough to choose a version, never to describe one. */
584
731
  versions = /* @__PURE__ */ new Map();
732
+ /** Full records from the single-version read, the requirements ledger included. */
585
733
  designs = /* @__PURE__ */ new Map();
586
734
  constructor(client) {
587
735
  this.client = client;
@@ -610,7 +758,11 @@ var DesignResolver = class {
610
758
  if (exact.length === 1) return exact[0];
611
759
  throw new AmbiguousProjectError(idOrQuery, candidates);
612
760
  }
613
- /** Every version of a project, newest first. Cached: one call serves a whole session. */
761
+ /**
762
+ * Every version of a project, newest first. Cached: one call serves a whole session.
763
+ *
764
+ * These are summaries. They seed nothing in `designs` — see the file header for why.
765
+ */
614
766
  async versionsFor(projectId) {
615
767
  const cached = this.versions.get(projectId);
616
768
  if (cached) return cached;
@@ -619,7 +771,6 @@ var DesignResolver = class {
619
771
  });
620
772
  const items = [...ArchitecturePageSchema.parse(body).items].sort((a, b) => b.version - a.version);
621
773
  this.versions.set(projectId, items);
622
- for (const item of items) this.designs.set(item.id, item);
623
774
  return items;
624
775
  }
625
776
  /** The version rule. */
@@ -659,10 +810,10 @@ var DesignResolver = class {
659
810
  };
660
811
  }
661
812
  /**
662
- * The full design for one version.
813
+ * The full design for one version, from the single-version read.
663
814
  *
664
- * The list response already carries `architecture_json`, so the cache seeded by
665
- * `versionsFor` usually answers this without a second request.
815
+ * One request per version a session actually describes, not one per version in the list:
816
+ * an overview and any number of component slices of the same version share it.
666
817
  */
667
818
  async design(architectureId) {
668
819
  const cached = this.designs.get(architectureId);
@@ -757,7 +908,9 @@ var input = z2.object({
757
908
  target: z2.string().max(255),
758
909
  protocol: z2.string().max(64).optional()
759
910
  })
760
- ).max(500).optional().describe("Edges in the code the design does not have. Endpoints may be component ids or names.")
911
+ ).max(500).optional().describe(
912
+ "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
+ )
761
914
  });
762
915
  var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
763
916
  var output = z2.object({
@@ -893,7 +1046,10 @@ var output2 = z3.object({
893
1046
  category: z3.string(),
894
1047
  message: z3.string(),
895
1048
  affected_components: z3.array(z3.string()),
896
- remediation: z3.string()
1049
+ remediation: z3.string(),
1050
+ advisory_only: z3.boolean().describe(
1051
+ "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
+ )
897
1053
  })
898
1054
  ),
899
1055
  finding_count: z3.number(),
@@ -932,7 +1088,9 @@ function registerCritique(server, client) {
932
1088
  category: finding.category,
933
1089
  message: finding.message,
934
1090
  affected_components: finding.affected_components ?? [],
935
- remediation: finding.remediation ?? ""
1091
+ remediation: finding.remediation ?? "",
1092
+ // A server too old to send the flag predates it, and counted every finding.
1093
+ advisory_only: finding.advisory_only ?? false
936
1094
  }));
937
1095
  const complianceAssessed = critique.compliance_assessed ?? false;
938
1096
  const exposureAssessed = critique.exposure_assessed ?? "public_internet";
@@ -966,8 +1124,12 @@ function registerCritique(server, client) {
966
1124
  "",
967
1125
  findings.length === 0 ? "No findings." : `${findings.length} finding(s):`,
968
1126
  ...findings.map(
969
- (finding) => `- [${finding.severity}] ${finding.message}` + (finding.affected_components.length ? ` (${finding.affected_components.join(", ")})` : "") + (finding.remediation ? `
1127
+ (finding) => (
1128
+ // Said on the line, because a severity alone reads as a blocker: an
1129
+ // advisory critical is one the score and the app's verdict both ignore.
1130
+ `- [${finding.severity}${finding.advisory_only ? " \xB7 advisory, not scored" : ""}] ${finding.message}` + (finding.affected_components.length ? ` (${finding.affected_components.join(", ")})` : "") + (finding.remediation ? `
970
1131
  Fix: ${finding.remediation}` : "")
1132
+ )
971
1133
  )
972
1134
  ];
973
1135
  return ok(structured, lines.join("\n"));
@@ -1083,8 +1245,12 @@ var constraintsInput = z4.strictObject({
1083
1245
  preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
1084
1246
  preferred_technologies: z4.array(z4.string()).optional(),
1085
1247
  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."),
1248
+ read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
1249
+ "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
+ ),
1251
+ burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
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 the tier default peak.'
1253
+ ),
1088
1254
  data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
1089
1255
  consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
1090
1256
  data_residency: z4.enum(["any", "us", "eu"]).optional()
@@ -1159,7 +1325,7 @@ function registerGenerate(server, client, resolver) {
1159
1325
  "generate_architecture",
1160
1326
  {
1161
1327
  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.",
1328
+ 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. A prompt that names no system to design comes back as clarification_required, and so does one that names a system but no scale to build it for \u2014 how many users or requests it has to serve. Answer with clarification_answers; there is no way to skip the questions, because a design built on an invented scale is sized, priced and judged against a number nobody chose.",
1163
1329
  inputSchema: generateInput,
1164
1330
  outputSchema: generateOutput,
1165
1331
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1273,7 +1439,7 @@ function clarificationResult(questions) {
1273
1439
  options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
1274
1440
  }));
1275
1441
  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.",
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 scale it runs at. Nothing was generated and nothing was charged.",
1277
1443
  "",
1278
1444
  ...structured.flatMap((question) => [
1279
1445
  `${question.id}: ${question.question}`,
@@ -1416,7 +1582,10 @@ function sliceComponent(design, componentId, ledger) {
1416
1582
  incoming: connections.filter((c) => c.target === component.id),
1417
1583
  related_decisions: related,
1418
1584
  assumed_decisions: assumedAmong(design, related),
1419
- ...ledger ? { cited_requirements: citedRequirements(ledger, component.id) } : {}
1585
+ ...ledger ? {
1586
+ cited_requirements: citedRequirements(ledger, component.id),
1587
+ withdrawn_requirements: withdrawnRequirements(ledger, component.id)
1588
+ } : {}
1420
1589
  };
1421
1590
  }
1422
1591
  function citedRequirements(ledger, componentId) {
@@ -1424,13 +1593,30 @@ function citedRequirements(ledger, componentId) {
1424
1593
  (ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
1425
1594
  );
1426
1595
  }
1427
- var STATUS_ORDER = ["unmet", "uncited", "unchecked", "cited"];
1596
+ function withdrawnRequirements(ledger, componentId) {
1597
+ return [...ledger.removed ?? [], ...ledger.superseded ?? []].filter(
1598
+ (row) => (row.cited_by_components ?? []).includes(componentId)
1599
+ );
1600
+ }
1601
+ var VERDICT_ORDER = ["fails", "unmet", "not_checked", "not_checkable", "unchecked", "no_gap_found", "passes", "cited"];
1602
+ function isCitedRequirement(row) {
1603
+ return (row.cited_by_components?.length ?? 0) > 0 || (row.cited_by_decisions?.length ?? 0) > 0;
1604
+ }
1605
+ var CITATION_ONLY_METHOD = "structure";
1606
+ function requirementVerdict(row) {
1607
+ const verdict = (row.verdict ?? row.status ?? "").trim();
1608
+ if (verdict === "fails" && (row.check_method ?? "").trim() === CITATION_ONLY_METHOD) return "";
1609
+ return verdict;
1610
+ }
1428
1611
  function orderRequirementRows(rows) {
1429
- return [...rows].sort((a, b) => statusRank(a) - statusRank(b));
1612
+ return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
1430
1613
  }
1431
- function statusRank(row) {
1432
- const index = STATUS_ORDER.indexOf(row.status ?? "");
1433
- return index === -1 ? STATUS_ORDER.length : index;
1614
+ function verdictRank(row) {
1615
+ const verdict = requirementVerdict(row);
1616
+ if (verdict === "fails" || verdict === "unmet") return 0;
1617
+ if (!isCitedRequirement(row)) return 1;
1618
+ const index = VERDICT_ORDER.indexOf(verdict);
1619
+ return index === -1 ? VERDICT_ORDER.length + 1 : index + 2;
1434
1620
  }
1435
1621
  function decisionTexts(design) {
1436
1622
  return decisionEntries(design).map((entry) => entry.text);
@@ -1494,11 +1680,11 @@ function normalize(value) {
1494
1680
  }
1495
1681
 
1496
1682
  // src/tools/get-design.ts
1497
- var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps"];
1683
+ var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
1498
1684
  var input3 = z5.object({
1499
1685
  project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
1500
1686
  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."
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."
1502
1688
  ),
1503
1689
  component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
1504
1690
  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 +1712,7 @@ function registerGetDesign(server, client, resolver) {
1526
1712
  "get_design",
1527
1713
  {
1528
1714
  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.',
1715
+ 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
1716
  inputSchema: input3,
1531
1717
  outputSchema: output3,
1532
1718
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
@@ -1603,6 +1789,11 @@ function registerGetDesign(server, client, resolver) {
1603
1789
  // Same order as the overview: what was asked for, then how the design
1604
1790
  // answered.
1605
1791
  ...requirementsServed(slice.cited_requirements),
1792
+ // Said only when the design still carries the id. A component's
1793
+ // `requirement_ids` are never rewritten when a requirement is
1794
+ // withdrawn, so without this the row would leave the answer in
1795
+ // silence and the id on the box would resolve to nothing again.
1796
+ ...requirementsWithdrawn(slice.withdrawn_requirements),
1606
1797
  // Labelled, and only when there are any. These bullets used to be
1607
1798
  // printed bare under "Called by:", so a decision about the component
1608
1799
  // read as a continuation of its edge list — and a design with no
@@ -1649,6 +1840,33 @@ function registerGetDesign(server, client, resolver) {
1649
1840
  versionLine(facts)
1650
1841
  ].filter(Boolean).join("\n"));
1651
1842
  }
1843
+ case "todo": {
1844
+ const body = await client.request(`/architectures/${resolved.architectureId}/todo`);
1845
+ const todo = TodoSchema.parse(body);
1846
+ const facts = factsFrom(resolved);
1847
+ const groups = todo.groups ?? [];
1848
+ const blocking = groups.find((group) => group.key === "blocks_handoff");
1849
+ const total = groups.reduce((count, group) => count + group.items.length, 0);
1850
+ return ok({ ...base, ...facts, data: todo }, [
1851
+ 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:"),
1852
+ ...groups.flatMap(todoGroupBlock),
1853
+ // A source that could not be read is stated, never left to look like a
1854
+ // shorter list. An agent acting on "nothing is outstanding" when the
1855
+ // simulation results failed to load is the exact harm this prevents.
1856
+ ...todo.unavailable_sources?.length ? [
1857
+ "",
1858
+ "Some of this version could not be read, so the list above is incomplete:",
1859
+ ...todo.unavailable_sources.map((entry) => `- ${entry.source}: ${entry.reason}`)
1860
+ ] : [],
1861
+ "",
1862
+ // The actions are the person's. An agent that "accepts" an assumption
1863
+ // has decided something on their behalf, which is the one thing this
1864
+ // surface must never do — and the server refuses it anyway, because an
1865
+ // agent token reaches no write route.
1866
+ "Fix what you can in the design. Answering, accepting and dismissing are the person's to do \u2014 ask, do not decide for them.",
1867
+ versionLine(facts)
1868
+ ].join("\n"));
1869
+ }
1652
1870
  case "readiness": {
1653
1871
  const body = await client.request(`/architectures/${resolved.architectureId}/readiness`);
1654
1872
  const readiness = ReadinessSchema.parse(body);
@@ -1656,7 +1874,10 @@ function registerGetDesign(server, client, resolver) {
1656
1874
  const gates = readiness.rows.filter((row) => row.category === "gate");
1657
1875
  const open = gates.filter((row) => row.gating);
1658
1876
  const unanswered = gates.filter((row) => row.state === "unknown");
1659
- const advisories = readiness.rows.filter((row) => row.category !== "gate");
1877
+ const accepts = readiness.rows.filter((row) => row.category === "accept");
1878
+ const advisories = readiness.rows.filter(
1879
+ (row) => row.category !== "gate" && row.category !== "accept"
1880
+ );
1660
1881
  const unrun = new Set(readiness.unrun_checks ?? []);
1661
1882
  const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
1662
1883
  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 +1885,22 @@ function registerGetDesign(server, client, resolver) {
1664
1885
  return ok({ ...base, ...facts, data: readiness }, [
1665
1886
  headline,
1666
1887
  ...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
1888
+ ...accepts.length ? [
1889
+ "",
1890
+ "To accept (the person agrees to these rather than fixing them; accepting is free and never blocks):",
1891
+ ...accepts.map((row) => `- ${row.label}: ${describeRow(row)}`)
1892
+ ] : [],
1667
1893
  "",
1668
1894
  "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.
1895
+ // An advisory with no result is marked, not counted — "nothing to count
1896
+ // here" is a fact an agent being handed work is entitled to, and it is
1897
+ // invisible in a bare count of open items. The mark claims no more than
1898
+ // that: an advisory can be unanswered because no result is *stored* for
1899
+ // this version (the advisor row since the server stopped writing on
1900
+ // read), not only because nothing ever ran, and the server's own
1901
+ // sentence, when it sends one, says which.
1672
1902
  ...advisories.map(
1673
- (row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) ? " (never run on this version)" : ""}`
1903
+ (row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) && !row.detail ? " (no result for this version)" : ""}`
1674
1904
  ),
1675
1905
  // The same list, under the same heading, as the app shows beside its
1676
1906
  // Release button. A person clicking Release is told what the release
@@ -1683,7 +1913,7 @@ function registerGetDesign(server, client, resolver) {
1683
1913
  ...readiness.release_warnings.map((warning) => `- ${warning}`)
1684
1914
  ] : [],
1685
1915
  "",
1686
- "Only a person can clear a gate. Ask; do not decide on their behalf.",
1916
+ '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
1917
  versionLine(facts)
1688
1918
  ].join("\n"));
1689
1919
  }
@@ -1745,14 +1975,20 @@ function registerGetDesign(server, client, resolver) {
1745
1975
  function requirementBlock(ledger) {
1746
1976
  const rows = orderRequirementRows(ledger?.rows ?? []);
1747
1977
  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}`);
1978
+ const attention = [
1979
+ [rows.filter((row) => requirementVerdict(row) === "fails").length, "failing"],
1980
+ [rows.filter((row) => !isCitedRequirement(row)).length, "cited by nothing"],
1981
+ [rows.filter((row) => requirementVerdict(row) === "not_checked" || requirementVerdict(row) === "not_checkable").length, "unchecked"]
1982
+ ].filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
1749
1983
  return [
1750
1984
  "",
1751
1985
  `Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
1752
1986
  ...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.",
1987
+ "Verdicts: `fails` \u2014 a named check measured the design as not meeting it, and says which and why;",
1988
+ "`passes` \u2014 a check measured it against a figure the person stated; `no_gap_found` \u2014 nothing",
1989
+ "contradicted it, which is not the same as proven; `not_checked` / `not_checkable` \u2014 nobody",
1990
+ "established either way, and neither is fine. `cited by nothing` means no component and no",
1991
+ "decision in the design claims to address it.",
1756
1992
  // Two facts about how far the statuses can be trusted, said only when the server states
1757
1993
  // them. An absent `verification_state` is an older server, not a verifier that did not run,
1758
1994
  // and claiming the second would be this package inventing a fact about someone's design.
@@ -1776,14 +2012,29 @@ function requirementsServed(rows) {
1776
2012
  "",
1777
2013
  "Requirements the design cites this component for:",
1778
2014
  ...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."
2015
+ "`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."
2017
+ ];
2018
+ }
2019
+ function requirementsWithdrawn(rows) {
2020
+ if (!rows?.length) return [];
2021
+ return [
2022
+ "",
2023
+ "This component still cites these ids, and they are no longer requirements \u2014 do not build to them:",
2024
+ ...rows.map(
2025
+ (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"}`}]`
2026
+ )
1781
2027
  ];
1782
2028
  }
1783
2029
  function requirementLine(row) {
1784
- const status = row.status ? ` [${row.status}]` : "";
2030
+ const verdict = requirementVerdict(row);
2031
+ const marks = [verdict, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
2032
+ const label = marks.length > 0 ? ` [${marks.join(" \xB7 ")}]` : "";
1785
2033
  const text = (row.text ?? "").replace(/\s+/g, " ").trim();
1786
- return `- ${row.id}${status} ${text || "(no text recorded for this requirement)"}`;
2034
+ const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
2035
+ const by = (row.checked_by ?? "").trim();
2036
+ const because = reason ? ` \u2014 ${reason}${by ? ` (checked by ${by})` : ""}` : by ? ` \u2014 checked by ${by}` : "";
2037
+ return `- ${row.id}${label} ${text || "(no text recorded for this requirement)"}${because}`;
1787
2038
  }
1788
2039
  function settledAnswer(gap) {
1789
2040
  const note = (gap.note ?? "").trim();
@@ -1795,8 +2046,20 @@ function settledAnswer(gap) {
1795
2046
  if (gap.action === "confirmed") return "confirmed as it stands";
1796
2047
  return "answered, but no wording was recorded \u2014 ask before relying on it";
1797
2048
  }
2049
+ function todoGroupBlock(group) {
2050
+ if (group.items.length === 0) return [];
2051
+ return ["", `${group.label} (${group.items.length}):`, ...group.items.map(todoLine)];
2052
+ }
2053
+ function todoLine(item) {
2054
+ const severity = item.severity && item.severity !== "none" ? `[${item.severity}] ` : "";
2055
+ const raisedBy = [...new Set((item.sources ?? []).map((source) => source.source))];
2056
+ const raised = raisedBy.length > 0 ? ` \u2014 raised by ${raisedBy.join(", ")}` : "";
2057
+ const carried = item.carried ? item.carried_from_version ? ` \u2014 carried from v${item.carried_from_version}` : " \u2014 carried from an earlier version" : "";
2058
+ const components = item.components?.length ? ` \u2014 affects ${item.components.join(", ")}` : "";
2059
+ return `- ${severity}${item.title.replace(/\s+/g, " ").trim()}${components}${raised}${carried}`;
2060
+ }
1798
2061
  function describeRow(row) {
1799
- if (row.state === "unknown") return "not checked";
2062
+ if (row.state === "unknown") return `not checked${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1800
2063
  return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1801
2064
  }
1802
2065
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skeletiq/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",