@skeletiq/mcp 0.1.2 → 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 +473 -69
  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`. |
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.1.2";
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 {
@@ -322,6 +322,88 @@ var ArchitectureJsonSchema = z.looseObject({
322
322
  assumptions: z.array(GapEntrySchema).nullable().optional(),
323
323
  open_questions: z.array(GapEntrySchema).nullable().optional()
324
324
  });
325
+ var RequirementLedgerRowSchema = z.looseObject({
326
+ id: z.string(),
327
+ text: z.string().optional(),
328
+ kind: z.string().optional(),
329
+ source: z.string().optional(),
330
+ /** `unmet` | `uncited` | `unchecked` | `cited` — deliberately never `met`. Left as a string: a
331
+ * value added server-side must not stop an older connector parsing the row. */
332
+ status: z.string().optional(),
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(),
347
+ cited_by_components: z.array(z.string()).nullable().optional(),
348
+ /** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
349
+ cited_by_decisions: z.array(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()
363
+ });
364
+ var RequirementLedgerCountsSchema = z.looseObject({
365
+ total: z.number().optional(),
366
+ user_supplied: z.number().optional(),
367
+ llm_inferred: z.number().optional(),
368
+ unmet: z.number().optional(),
369
+ uncited: z.number().optional(),
370
+ unchecked: 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()
387
+ });
388
+ var RequirementLedgerSchema = z.looseObject({
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(),
398
+ analysis_version: z.number().nullable().optional(),
399
+ analysis_degraded: z.boolean().optional(),
400
+ /** `grounded` | `not_grounded` | `diverged`. */
401
+ grounding_state: z.string().optional(),
402
+ /** `ran` | `partial` | `not_run`. */
403
+ verification_state: z.string().optional(),
404
+ verified_version: z.number().nullable().optional(),
405
+ counts: RequirementLedgerCountsSchema.nullable().optional()
406
+ });
325
407
  var ArchitectureSchema = z.looseObject({
326
408
  id: z.string(),
327
409
  project_id: z.string(),
@@ -332,7 +414,8 @@ var ArchitectureSchema = z.looseObject({
332
414
  created_at: z.string().optional(),
333
415
  updated_at: z.string().optional(),
334
416
  is_released: z.boolean().optional(),
335
- released_at: z.string().nullable().optional()
417
+ released_at: z.string().nullable().optional(),
418
+ requirement_ledger: RequirementLedgerSchema.nullable().optional()
336
419
  });
337
420
  var ArchitecturePageSchema = z.looseObject({
338
421
  items: z.array(ArchitectureSchema),
@@ -365,6 +448,12 @@ var BuildOrderSchema = z.looseObject({
365
448
  steps: z.array(BuildStepSchema),
366
449
  release: ReleaseFactsSchema
367
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
+ });
368
457
  var ReadinessRowSchema = z.looseObject({
369
458
  key: z.string(),
370
459
  label: z.string(),
@@ -403,7 +492,16 @@ var ReadinessSchema = z.looseObject({
403
492
  * Optional here only so a server that predates the field still parses — it is required on the
404
493
  * wire, and an empty array is the server saying it looked and found nothing.
405
494
  */
406
- 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()
407
505
  });
408
506
  var DesignGapSchema = z.looseObject({
409
507
  gap_id: z.string(),
@@ -412,7 +510,13 @@ var DesignGapSchema = z.looseObject({
412
510
  resolved: z.boolean(),
413
511
  action: z.string().nullable().optional(),
414
512
  note: z.string().nullable().optional(),
415
- 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()
416
520
  });
417
521
  var OrphanedAnswerSchema = z.looseObject({
418
522
  gap_id: z.string(),
@@ -430,29 +534,6 @@ var DesignGapListSchema = z.looseObject({
430
534
  /** Answers whose question is not in this version — recorded, not silently dropped. */
431
535
  orphaned_answers: z.array(OrphanedAnswerSchema).optional()
432
536
  });
433
- var ComponentRefSchema = z.looseObject({
434
- id: z.string(),
435
- name: z.string(),
436
- type: z.string()
437
- });
438
- var UnknownIdSchema = z.looseObject({
439
- id: z.string(),
440
- suggestions: z.array(ComponentRefSchema).optional()
441
- });
442
- var DriftSchema = z.looseObject({
443
- architecture_id: z.string(),
444
- version: z.number(),
445
- in_sync: z.boolean(),
446
- missing: z.array(ComponentRefSchema),
447
- unreported: z.array(ComponentRefSchema),
448
- partial: z.array(ComponentRefSchema),
449
- elsewhere: z.array(ComponentRefSchema),
450
- unknown_ids: z.array(UnknownIdSchema),
451
- extra_components: z.array(z.looseObject({ name: z.string() })),
452
- extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
453
- counts: z.record(z.string(), z.number()),
454
- release: ReleaseFactsSchema
455
- });
456
537
  var CritiqueFindingSchema = z.looseObject({
457
538
  message: z.string(),
458
539
  pattern_id: z.string(),
@@ -460,7 +541,17 @@ var CritiqueFindingSchema = z.looseObject({
460
541
  severity: z.string().optional(),
461
542
  affected_components: z.array(z.string()).optional(),
462
543
  evidence: z.string().optional(),
463
- 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()
464
555
  });
465
556
  var PayloadCritiqueSchema = z.looseObject({
466
557
  architecture_score: z.number(),
@@ -478,6 +569,96 @@ var PayloadCritiqueSchema = z.looseObject({
478
569
  compliance_note: z.string().nullable().optional(),
479
570
  exposure_assessed: z.string().optional()
480
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
+ });
481
662
  var ClarifyingQuestionSchema = z.looseObject({
482
663
  id: z.string(),
483
664
  question: z.string(),
@@ -546,7 +727,9 @@ var PROJECT_PAGE_SIZE = 50;
546
727
  var ARCHITECTURE_PAGE_SIZE = 100;
547
728
  var DesignResolver = class {
548
729
  client;
730
+ /** Version summaries per project — enough to choose a version, never to describe one. */
549
731
  versions = /* @__PURE__ */ new Map();
732
+ /** Full records from the single-version read, the requirements ledger included. */
550
733
  designs = /* @__PURE__ */ new Map();
551
734
  constructor(client) {
552
735
  this.client = client;
@@ -575,7 +758,11 @@ var DesignResolver = class {
575
758
  if (exact.length === 1) return exact[0];
576
759
  throw new AmbiguousProjectError(idOrQuery, candidates);
577
760
  }
578
- /** 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
+ */
579
766
  async versionsFor(projectId) {
580
767
  const cached = this.versions.get(projectId);
581
768
  if (cached) return cached;
@@ -584,7 +771,6 @@ var DesignResolver = class {
584
771
  });
585
772
  const items = [...ArchitecturePageSchema.parse(body).items].sort((a, b) => b.version - a.version);
586
773
  this.versions.set(projectId, items);
587
- for (const item of items) this.designs.set(item.id, item);
588
774
  return items;
589
775
  }
590
776
  /** The version rule. */
@@ -624,10 +810,10 @@ var DesignResolver = class {
624
810
  };
625
811
  }
626
812
  /**
627
- * The full design for one version.
813
+ * The full design for one version, from the single-version read.
628
814
  *
629
- * The list response already carries `architecture_json`, so the cache seeded by
630
- * `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.
631
817
  */
632
818
  async design(architectureId) {
633
819
  const cached = this.designs.get(architectureId);
@@ -722,7 +908,9 @@ var input = z2.object({
722
908
  target: z2.string().max(255),
723
909
  protocol: z2.string().max(64).optional()
724
910
  })
725
- ).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
+ )
726
914
  });
727
915
  var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
728
916
  var output = z2.object({
@@ -858,7 +1046,10 @@ var output2 = z3.object({
858
1046
  category: z3.string(),
859
1047
  message: z3.string(),
860
1048
  affected_components: z3.array(z3.string()),
861
- 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
+ )
862
1053
  })
863
1054
  ),
864
1055
  finding_count: z3.number(),
@@ -897,7 +1088,9 @@ function registerCritique(server, client) {
897
1088
  category: finding.category,
898
1089
  message: finding.message,
899
1090
  affected_components: finding.affected_components ?? [],
900
- 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
901
1094
  }));
902
1095
  const complianceAssessed = critique.compliance_assessed ?? false;
903
1096
  const exposureAssessed = critique.exposure_assessed ?? "public_internet";
@@ -931,8 +1124,12 @@ function registerCritique(server, client) {
931
1124
  "",
932
1125
  findings.length === 0 ? "No findings." : `${findings.length} finding(s):`,
933
1126
  ...findings.map(
934
- (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 ? `
935
1131
  Fix: ${finding.remediation}` : "")
1132
+ )
936
1133
  )
937
1134
  ];
938
1135
  return ok(structured, lines.join("\n"));
@@ -1048,8 +1245,12 @@ var constraintsInput = z4.strictObject({
1048
1245
  preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
1049
1246
  preferred_technologies: z4.array(z4.string()).optional(),
1050
1247
  max_components: z4.number().int().positive().optional(),
1051
- read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe("Nothing reads this from the prompt. Unset, the design assumes 80% reads."),
1052
- 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
+ ),
1053
1254
  data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
1054
1255
  consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
1055
1256
  data_residency: z4.enum(["any", "us", "eu"]).optional()
@@ -1124,7 +1325,7 @@ function registerGenerate(server, client, resolver) {
1124
1325
  "generate_architecture",
1125
1326
  {
1126
1327
  title: "Generate a SkeletIQ architecture",
1127
- 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.",
1128
1329
  inputSchema: generateInput,
1129
1330
  outputSchema: generateOutput,
1130
1331
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1238,7 +1439,7 @@ function clarificationResult(questions) {
1238
1439
  options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
1239
1440
  }));
1240
1441
  const lines = [
1241
- "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.",
1242
1443
  "",
1243
1444
  ...structured.flatMap((question) => [
1244
1445
  `${question.id}: ${question.question}`,
@@ -1369,32 +1570,83 @@ async function describeCreated(resolver, architectureId) {
1369
1570
  import * as z5 from "zod/v4";
1370
1571
 
1371
1572
  // src/lib/slice.ts
1372
- function sliceComponent(design, componentId) {
1573
+ function sliceComponent(design, componentId, ledger) {
1373
1574
  const components = design.components ?? [];
1374
1575
  const component = components.find((c) => c.id === componentId) ?? components.find((c) => normalize(c.name) === normalize(componentId));
1375
1576
  if (!component) return null;
1376
1577
  const connections = design.connections ?? [];
1578
+ const related = relatedDecisions(design, component);
1377
1579
  return {
1378
1580
  component,
1379
1581
  outgoing: connections.filter((c) => c.source === component.id),
1380
1582
  incoming: connections.filter((c) => c.target === component.id),
1381
- related_decisions: relatedDecisions(design, component)
1583
+ related_decisions: related,
1584
+ assumed_decisions: assumedAmong(design, related),
1585
+ ...ledger ? {
1586
+ cited_requirements: citedRequirements(ledger, component.id),
1587
+ withdrawn_requirements: withdrawnRequirements(ledger, component.id)
1588
+ } : {}
1382
1589
  };
1383
1590
  }
1591
+ function citedRequirements(ledger, componentId) {
1592
+ return orderRequirementRows(
1593
+ (ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
1594
+ );
1595
+ }
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
+ }
1611
+ function orderRequirementRows(rows) {
1612
+ return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
1613
+ }
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;
1620
+ }
1384
1621
  function decisionTexts(design) {
1385
- const seen = /* @__PURE__ */ new Set();
1622
+ return decisionEntries(design).map((entry) => entry.text);
1623
+ }
1624
+ function decisionEntries(design) {
1625
+ const byText = /* @__PURE__ */ new Map();
1386
1626
  const out = [];
1387
- const push2 = (text) => {
1627
+ const push2 = (text, assumption) => {
1388
1628
  if (typeof text !== "string") return;
1389
1629
  const trimmed = text.trim();
1390
- if (trimmed.length === 0 || seen.has(trimmed)) return;
1391
- seen.add(trimmed);
1392
- out.push(trimmed);
1630
+ if (trimmed.length === 0) return;
1631
+ const existing = byText.get(trimmed);
1632
+ if (existing) {
1633
+ existing.assumption = existing.assumption || assumption;
1634
+ return;
1635
+ }
1636
+ const entry = { text: trimmed, assumption };
1637
+ byText.set(trimmed, entry);
1638
+ out.push(entry);
1393
1639
  };
1394
- for (const decision of design.design_decisions ?? []) push2(decision);
1395
- for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
1640
+ for (const decision of design.design_decisions ?? []) push2(decision, false);
1641
+ for (const decision of design.grounded_decisions ?? []) push2(decision?.text, decision?.assumption === true);
1396
1642
  return out;
1397
1643
  }
1644
+ function assumedAmong(design, texts) {
1645
+ const assumed = new Set(
1646
+ decisionEntries(design).filter((entry) => entry.assumption).map((entry) => entry.text)
1647
+ );
1648
+ return texts.filter((text) => assumed.has(text));
1649
+ }
1398
1650
  function relatedDecisions(design, component) {
1399
1651
  const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
1400
1652
  if (needles.length === 0) return [];
@@ -1403,14 +1655,24 @@ function relatedDecisions(design, component) {
1403
1655
  return needles.some((needle) => haystack.includes(needle));
1404
1656
  });
1405
1657
  }
1406
- function overview(design) {
1658
+ function overview(design, ledger) {
1659
+ const decisions = decisionTexts(design);
1407
1660
  return {
1408
1661
  title: design.title ?? "Untitled design",
1409
1662
  description: design.description ?? "",
1410
1663
  components: design.components ?? [],
1411
1664
  connections: design.connections ?? [],
1412
- design_decisions: decisionTexts(design),
1413
- trade_offs: design.trade_offs ?? []
1665
+ design_decisions: decisions,
1666
+ // A subset of `design_decisions`, not a second list of decisions: which of them the design
1667
+ // marks as resting on no cited source. Additive, so `design_decisions` stays the plain
1668
+ // strings every existing caller reads.
1669
+ assumed_decisions: assumedAmong(design, decisions),
1670
+ trade_offs: design.trade_offs ?? [],
1671
+ // Carried verbatim, rows in the server's order, and only when the version has one. This is
1672
+ // a rebuild from named keys, so anything not named here is dropped without a trace — which
1673
+ // is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
1674
+ // past would make "verbatim" untrue; the grouping belongs to the text that prints them.
1675
+ ...ledger ? { requirement_ledger: ledger } : {}
1414
1676
  };
1415
1677
  }
1416
1678
  function normalize(value) {
@@ -1418,11 +1680,11 @@ function normalize(value) {
1418
1680
  }
1419
1681
 
1420
1682
  // src/tools/get-design.ts
1421
- var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps"];
1683
+ var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
1422
1684
  var input3 = z5.object({
1423
1685
  project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
1424
1686
  mode: z5.enum(MODES).describe(
1425
- "overview: the whole design. component: one component, its connections in both directions, and the decisions that mention it. 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."
1426
1688
  ),
1427
1689
  component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
1428
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.")
@@ -1438,12 +1700,19 @@ var output3 = z5.object({
1438
1700
  /** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
1439
1701
  data: z5.unknown()
1440
1702
  });
1703
+ function decisionsHeading(assumed, settled, withAssumptions) {
1704
+ return assumed.length > 0 ? withAssumptions : settled;
1705
+ }
1706
+ function decisionLines(decisions, assumed) {
1707
+ const marked = new Set(assumed);
1708
+ return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
1709
+ }
1441
1710
  function registerGetDesign(server, client, resolver) {
1442
1711
  server.registerTool(
1443
1712
  "get_design",
1444
1713
  {
1445
1714
  title: "Read a SkeletIQ design",
1446
- 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. 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.',
1447
1716
  inputSchema: input3,
1448
1717
  outputSchema: output3,
1449
1718
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
@@ -1459,7 +1728,7 @@ function registerGetDesign(server, client, resolver) {
1459
1728
  switch (mode) {
1460
1729
  case "overview": {
1461
1730
  const design = await resolver.design(resolved.architectureId);
1462
- const data = overview(design.architecture_json);
1731
+ const data = overview(design.architecture_json, design.requirement_ledger);
1463
1732
  const facts = factsFrom(resolved);
1464
1733
  return ok({ ...base, ...facts, data }, [
1465
1734
  `# ${data.title}`,
@@ -1469,6 +1738,10 @@ function registerGetDesign(server, client, resolver) {
1469
1738
  ...data.components.map(
1470
1739
  (c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
1471
1740
  ),
1741
+ // Above the decisions because the two answer different questions, in
1742
+ // this order: requirements are what was asked for, decisions are how
1743
+ // the design answered.
1744
+ ...requirementBlock(data.requirement_ledger),
1472
1745
  // The design's own reasoning, which this text used to omit entirely
1473
1746
  // while carrying it in the structured half — so an agent working from
1474
1747
  // what it was shown rebuilt every choice from scratch, and could
@@ -1476,8 +1749,12 @@ function registerGetDesign(server, client, resolver) {
1476
1749
  // like this", and the overview is where that gets asked.
1477
1750
  ...data.design_decisions.length ? [
1478
1751
  "",
1479
- "Decisions taken \u2014 these are settled, build to them:",
1480
- ...data.design_decisions.map((decision) => `- ${decision}`)
1752
+ decisionsHeading(
1753
+ data.assumed_decisions,
1754
+ "Decisions taken \u2014 these are settled, build to them:",
1755
+ "Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
1756
+ ),
1757
+ ...decisionLines(data.design_decisions, data.assumed_decisions)
1481
1758
  ] : [],
1482
1759
  ...data.trade_offs.length ? [
1483
1760
  "",
@@ -1493,7 +1770,7 @@ function registerGetDesign(server, client, resolver) {
1493
1770
  return failure('mode "component" needs a component_id. Use mode "overview" or "build_order" to see the ids.');
1494
1771
  }
1495
1772
  const design = await resolver.design(resolved.architectureId);
1496
- const slice = sliceComponent(design.architecture_json, component_id);
1773
+ const slice = sliceComponent(design.architecture_json, component_id, design.requirement_ledger);
1497
1774
  if (!slice) {
1498
1775
  const known = (design.architecture_json.components ?? []).map((c) => c.id).join(", ");
1499
1776
  return failure(
@@ -1509,6 +1786,14 @@ function registerGetDesign(server, client, resolver) {
1509
1786
  "",
1510
1787
  outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
1511
1788
  incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
1789
+ // Same order as the overview: what was asked for, then how the design
1790
+ // answered.
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),
1512
1797
  // Labelled, and only when there are any. These bullets used to be
1513
1798
  // printed bare under "Called by:", so a decision about the component
1514
1799
  // read as a continuation of its edge list — and a design with no
@@ -1516,8 +1801,12 @@ function registerGetDesign(server, client, resolver) {
1516
1801
  // with no reasoning rather than as a decision that names other boxes.
1517
1802
  ...slice.related_decisions.length ? [
1518
1803
  "",
1519
- "Decisions that mention it:",
1520
- ...slice.related_decisions.map((decision) => `- ${decision}`)
1804
+ decisionsHeading(
1805
+ slice.assumed_decisions,
1806
+ "Decisions that mention it:",
1807
+ "Decisions that mention it \u2014 confirm the ones marked (assumption) first, they rest on no cited source:"
1808
+ ),
1809
+ ...decisionLines(slice.related_decisions, slice.assumed_decisions)
1521
1810
  ] : [
1522
1811
  "",
1523
1812
  "No decision in this design names this component. That is not the same as",
@@ -1551,6 +1840,33 @@ function registerGetDesign(server, client, resolver) {
1551
1840
  versionLine(facts)
1552
1841
  ].filter(Boolean).join("\n"));
1553
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
+ }
1554
1870
  case "readiness": {
1555
1871
  const body = await client.request(`/architectures/${resolved.architectureId}/readiness`);
1556
1872
  const readiness = ReadinessSchema.parse(body);
@@ -1558,7 +1874,10 @@ function registerGetDesign(server, client, resolver) {
1558
1874
  const gates = readiness.rows.filter((row) => row.category === "gate");
1559
1875
  const open = gates.filter((row) => row.gating);
1560
1876
  const unanswered = gates.filter((row) => row.state === "unknown");
1561
- 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
+ );
1562
1881
  const unrun = new Set(readiness.unrun_checks ?? []);
1563
1882
  const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
1564
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:`;
@@ -1566,13 +1885,22 @@ function registerGetDesign(server, client, resolver) {
1566
1885
  return ok({ ...base, ...facts, data: readiness }, [
1567
1886
  headline,
1568
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
+ ] : [],
1569
1893
  "",
1570
1894
  "Advisory checks (they never block, and some need a paid plan to clear):",
1571
- // An advisory that never ran is marked, not counted. "The advisor has
1572
- // not looked at this design" is a fact an agent being handed work is
1573
- // 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.
1574
1902
  ...advisories.map(
1575
- (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)" : ""}`
1576
1904
  ),
1577
1905
  // The same list, under the same heading, as the app shows beside its
1578
1906
  // Release button. A person clicking Release is told what the release
@@ -1585,7 +1913,7 @@ function registerGetDesign(server, client, resolver) {
1585
1913
  ...readiness.release_warnings.map((warning) => `- ${warning}`)
1586
1914
  ] : [],
1587
1915
  "",
1588
- "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.',
1589
1917
  versionLine(facts)
1590
1918
  ].join("\n"));
1591
1919
  }
@@ -1644,6 +1972,70 @@ function registerGetDesign(server, client, resolver) {
1644
1972
  })
1645
1973
  );
1646
1974
  }
1975
+ function requirementBlock(ledger) {
1976
+ const rows = orderRequirementRows(ledger?.rows ?? []);
1977
+ if (!ledger || rows.length === 0) return [];
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}`);
1983
+ return [
1984
+ "",
1985
+ `Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
1986
+ ...rows.map(requirementLine),
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.",
1992
+ // Two facts about how far the statuses can be trusted, said only when the server states
1993
+ // them. An absent `verification_state` is an older server, not a verifier that did not run,
1994
+ // and claiming the second would be this package inventing a fact about someone's design.
1995
+ ...ledger.grounding_state === "not_grounded" ? [
1996
+ "This version records no citations at all, so `uncited` here means nothing recorded what",
1997
+ "addresses the requirement \u2014 not that nothing in the design does."
1998
+ ] : [],
1999
+ ...ledger.verification_state === "partial" ? [
2000
+ "The verifier did not check every requirement in this set, so an `unchecked` row is",
2001
+ "neither confirmed nor flagged."
2002
+ ] : ledger.verification_state === "not_run" ? [
2003
+ "The verifier did not run on this version, so an `unchecked` row is neither confirmed",
2004
+ "nor flagged."
2005
+ ] : [],
2006
+ ...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
2007
+ ];
2008
+ }
2009
+ function requirementsServed(rows) {
2010
+ if (!rows?.length) return [];
2011
+ return [
2012
+ "",
2013
+ "Requirements the design cites this component for:",
2014
+ ...rows.map(requirementLine),
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
+ )
2027
+ ];
2028
+ }
2029
+ function requirementLine(row) {
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 ")}]` : "";
2033
+ const text = (row.text ?? "").replace(/\s+/g, " ").trim();
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}`;
2038
+ }
1647
2039
  function settledAnswer(gap) {
1648
2040
  const note = (gap.note ?? "").trim();
1649
2041
  if (gap.adr_id) {
@@ -1654,8 +2046,20 @@ function settledAnswer(gap) {
1654
2046
  if (gap.action === "confirmed") return "confirmed as it stands";
1655
2047
  return "answered, but no wording was recorded \u2014 ask before relying on it";
1656
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
+ }
1657
2061
  function describeRow(row) {
1658
- if (row.state === "unknown") return "not checked";
2062
+ if (row.state === "unknown") return `not checked${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1659
2063
  return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
1660
2064
  }
1661
2065
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skeletiq/mcp",
3
- "version": "0.1.2",
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",