@taiga-ai/mcp-server 0.6.0-rc.393 → 0.7.0-dev.395

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -95,6 +95,7 @@ var TaigaClient = class {
95
95
  const params = new URLSearchParams();
96
96
  if (opts?.factoryId) params.set("factoryId", opts.factoryId);
97
97
  if (opts?.status) params.set("status", opts.status);
98
+ if (opts?.slug) params.set("slug", opts.slug);
98
99
  if (opts?.repository) params.set("repository", opts.repository);
99
100
  if (opts?.repositoryHost) params.set("repositoryHost", opts.repositoryHost);
100
101
  if (opts?.limit) params.set("limit", String(opts.limit));
@@ -145,12 +146,14 @@ var TaigaClient = class {
145
146
  }
146
147
  async listProductDocuments(productId, opts) {
147
148
  const params = new URLSearchParams();
149
+ if (opts?.origin) params.set("origin", opts.origin);
148
150
  if (opts?.limit) params.set("limit", String(opts.limit));
149
151
  const qs = params.toString();
150
152
  return this.request("GET", `/products/${seg(productId)}/documents${qs ? `?${qs}` : ""}`);
151
153
  }
152
- async getProductDocument(productId, type) {
153
- return this.request("GET", `/products/${seg(productId)}/documents/${seg(type)}`);
154
+ async getProductDocument(productId, type, origin) {
155
+ const qs = origin ? `?${new URLSearchParams({ origin })}` : "";
156
+ return this.request("GET", `/products/${seg(productId)}/documents/${seg(type)}${qs}`);
154
157
  }
155
158
  async listBacklogItems(productId, opts) {
156
159
  const params = new URLSearchParams();
@@ -178,6 +181,9 @@ var TaigaClient = class {
178
181
  async listBuildRunSteps(id) {
179
182
  return this.request("GET", `/builds/${seg(id)}/steps`);
180
183
  }
184
+ async getFactory(id) {
185
+ return this.request("GET", `/factories/${seg(id)}`);
186
+ }
181
187
  async listFactories(opts) {
182
188
  const params = new URLSearchParams();
183
189
  if (opts?.limit) params.set("limit", String(opts.limit));
@@ -189,7 +195,7 @@ var TaigaClient = class {
189
195
  //#region src/config.ts
190
196
  const envSchema = z.object({
191
197
  TAIGA_API_KEY: z.string().min(1),
192
- TAIGA_API_URL: z.string().default("https://api.taiga.app")
198
+ TAIGA_API_URL: z.string().default("https://api.eu.tai.ga")
193
199
  });
194
200
  /** Null when TAIGA_API_KEY is missing or empty: it is the one setting with no default. */
195
201
  function readConfig() {
@@ -213,6 +219,850 @@ const READ_ONLY = {
213
219
  openWorldHint: false
214
220
  };
215
221
  //#endregion
222
+ //#region ../../packages/types/dist/tenants.js
223
+ const TENANT_DOCUMENT_TYPES = [
224
+ "policy_security",
225
+ "policy_data",
226
+ "policy_sdlc",
227
+ "policy_ops",
228
+ "policy_tic",
229
+ "policy_risk",
230
+ "policy_architecture",
231
+ "policy_supply_chain",
232
+ "policy_cloud_governance",
233
+ "business_goals",
234
+ "design_standards",
235
+ "brand_standards",
236
+ "tone_of_voice",
237
+ "tenant_instructions"
238
+ ];
239
+ /** The nine policy documents: the tenant documents that define controls. */
240
+ const POLICY_TYPES = [
241
+ "policy_security",
242
+ "policy_data",
243
+ "policy_sdlc",
244
+ "policy_ops",
245
+ "policy_tic",
246
+ "policy_risk",
247
+ "policy_architecture",
248
+ "policy_supply_chain",
249
+ "policy_cloud_governance"
250
+ ];
251
+ const EU_COUNTRIES = [
252
+ {
253
+ code: "AT",
254
+ name: "Austria"
255
+ },
256
+ {
257
+ code: "BE",
258
+ name: "Belgium"
259
+ },
260
+ {
261
+ code: "BG",
262
+ name: "Bulgaria"
263
+ },
264
+ {
265
+ code: "HR",
266
+ name: "Croatia"
267
+ },
268
+ {
269
+ code: "CY",
270
+ name: "Cyprus"
271
+ },
272
+ {
273
+ code: "CZ",
274
+ name: "Czechia"
275
+ },
276
+ {
277
+ code: "DK",
278
+ name: "Denmark"
279
+ },
280
+ {
281
+ code: "EE",
282
+ name: "Estonia"
283
+ },
284
+ {
285
+ code: "FI",
286
+ name: "Finland"
287
+ },
288
+ {
289
+ code: "FR",
290
+ name: "France"
291
+ },
292
+ {
293
+ code: "DE",
294
+ name: "Germany"
295
+ },
296
+ {
297
+ code: "GR",
298
+ name: "Greece"
299
+ },
300
+ {
301
+ code: "HU",
302
+ name: "Hungary"
303
+ },
304
+ {
305
+ code: "IE",
306
+ name: "Ireland"
307
+ },
308
+ {
309
+ code: "IT",
310
+ name: "Italy"
311
+ },
312
+ {
313
+ code: "LV",
314
+ name: "Latvia"
315
+ },
316
+ {
317
+ code: "LT",
318
+ name: "Lithuania"
319
+ },
320
+ {
321
+ code: "LU",
322
+ name: "Luxembourg"
323
+ },
324
+ {
325
+ code: "MT",
326
+ name: "Malta"
327
+ },
328
+ {
329
+ code: "NL",
330
+ name: "Netherlands"
331
+ },
332
+ {
333
+ code: "PL",
334
+ name: "Poland"
335
+ },
336
+ {
337
+ code: "PT",
338
+ name: "Portugal"
339
+ },
340
+ {
341
+ code: "RO",
342
+ name: "Romania"
343
+ },
344
+ {
345
+ code: "SK",
346
+ name: "Slovakia"
347
+ },
348
+ {
349
+ code: "SI",
350
+ name: "Slovenia"
351
+ },
352
+ {
353
+ code: "ES",
354
+ name: "Spain"
355
+ },
356
+ {
357
+ code: "SE",
358
+ name: "Sweden"
359
+ }
360
+ ];
361
+ /** Countries available during closed beta. Expand as we open new markets. */
362
+ const BETA_COUNTRIES = ["FI"];
363
+ EU_COUNTRIES.filter((c) => BETA_COUNTRIES.includes(c.code));
364
+ Object.fromEntries(EU_COUNTRIES.map((c) => [c.code, "eu"]));
365
+ //#endregion
366
+ //#region ../../packages/types/dist/factories.js
367
+ const FACTORY_DOCUMENT_TYPES = ["factory_instructions"];
368
+ //#endregion
369
+ //#region ../../packages/types/dist/products.js
370
+ const PRODUCT_STATUSES = [
371
+ "draft",
372
+ "active",
373
+ "archived"
374
+ ];
375
+ const PRODUCT_DOCUMENT_TYPES = [
376
+ "specification",
377
+ "architecture",
378
+ "technology_decisions",
379
+ "data_flow",
380
+ "privacy_assessment",
381
+ "threat_model",
382
+ "risk_register",
383
+ "user_flow",
384
+ "service_blueprint",
385
+ "ui_direction",
386
+ "codebase_snapshot",
387
+ "product_instructions"
388
+ ];
389
+ /**
390
+ * The document types the codebase-intelligence agent can derive from a repo --
391
+ * the valid values for a single-document reality refresh (PRO-1141).
392
+ *
393
+ * Mirrors the agent's DocumentDerivation registry, which is the source of
394
+ * truth: everything in PRODUCT_DOCUMENT_TYPES except `ui_direction` (a human's
395
+ * recorded choice) and `product_instructions` (free-form text the user writes).
396
+ * The two lists are pinned to each other by
397
+ * apps/agents/codebase-intelligence/tests/test_reality_doc_types_sync.py.
398
+ * If they drift anyway, the failure is uneven: a subset request that mixes a
399
+ * drifted type with real ones reports it in the run's `errors` map, but a
400
+ * request naming ONLY drifted types derives nothing and fails the job after
401
+ * exhausting retries ("Codebase analysis produced no documents"), and a
402
+ * Python-side addition missing from this list is rejected with a 400 by the
403
+ * refresh route's schema before any job exists.
404
+ */
405
+ const REALITY_DERIVED_DOCUMENT_TYPES = [
406
+ "codebase_snapshot",
407
+ "specification",
408
+ "user_flow",
409
+ "architecture",
410
+ "technology_decisions",
411
+ "data_flow",
412
+ "privacy_assessment",
413
+ "threat_model",
414
+ "risk_register",
415
+ "service_blueprint"
416
+ ];
417
+ /**
418
+ * Human-readable document names, for text the SERVER has to compose -- inbox
419
+ * item titles in particular, which are stored strings and cannot reach the web
420
+ * app's i18n.
421
+ *
422
+ * Kept in `@taiga/types` rather than in the API so there is one list rather
423
+ * than a backend copy quietly drifting from the UI's. Matches the web's
424
+ * PIPELINE_DISPLAY_NAMES (apps/web/src/lib/pipeline-display-names.ts); change
425
+ * both together or neither.
426
+ */
427
+ const PRODUCT_DOCUMENT_DISPLAY_NAMES = {
428
+ specification: "Specification",
429
+ user_flow: "User Flows",
430
+ architecture: "Architecture",
431
+ technology_decisions: "Tech Decisions",
432
+ data_flow: "Data Flow",
433
+ privacy_assessment: "DPIA",
434
+ threat_model: "Threat Model",
435
+ risk_register: "Risk Register",
436
+ service_blueprint: "Service Blueprint"
437
+ };
438
+ const PRODUCT_DOCUMENT_ORIGINS = ["plan", "reality"];
439
+ /**
440
+ * The two blocking states are NOT the same kind of blocker, and collapsing them
441
+ * into one list is what caused a bug in each direction (PRO-1028, PRO-1039).
442
+ * They are separate constants so the difference is data a reader can see rather
443
+ * than a comment a reader can miss.
444
+ */
445
+ /**
446
+ * GLOBAL blockers: at most one per project, at any position. `active` is held
447
+ * by `uq_product_queue_active`, so an entry in this state blocks the whole
448
+ * queue WHEREVER it sits -- `reposition` lets a queued entry be dragged in
449
+ * front of the active one, and starting a second run then violates that index.
450
+ */
451
+ const QUEUE_GLOBAL_BLOCKING_STATES = ["active"];
452
+ /**
453
+ * POSITIONAL blockers: they stop what is BEHIND them and nothing in front.
454
+ * `halted` blocks by existing, which is how halt-on-failure works without a
455
+ * separate paused flag -- but only for the entries queued after it. That is
456
+ * what the UI has always promised, and it gives a halt a second exit: move it
457
+ * down and the rest of the queue carries on.
458
+ */
459
+ const QUEUE_POSITIONAL_BLOCKING_STATES = ["halted"];
460
+ [...QUEUE_GLOBAL_BLOCKING_STATES, ...QUEUE_POSITIONAL_BLOCKING_STATES];
461
+ Object.keys({
462
+ /**
463
+ * A PERSON stopped the plan (PRO-1144). Distinct from `planning_failed`
464
+ * because the two leave the initiative in the same state and mean opposite
465
+ * things: one is the agent falling over, this is somebody deciding.
466
+ *
467
+ * Halting is still right -- nothing should restart behind the back of a
468
+ * person who just stopped it -- but calling their decision a failure is not.
469
+ * Same wording as the initiative's own cancel copy (PRO-1071), so the two
470
+ * surfaces describe one event the same way.
471
+ */
472
+ planning_cancelled: {
473
+ message: "Planning was cancelled, so the queue stopped here.",
474
+ params: [],
475
+ clearedWhen: "planning is retried, or a re-plan replaces it",
476
+ exits: [
477
+ "auto",
478
+ "reorder",
479
+ "remove"
480
+ ]
481
+ },
482
+ /** The plan the entry needs could not be produced. */
483
+ planning_failed: {
484
+ message: "Planning failed for this initiative, so the queue stopped here.",
485
+ params: [],
486
+ clearedWhen: "the initiative leaves planningStatus \"failed\" (a retry or a re-plan)",
487
+ exits: [
488
+ "auto",
489
+ "reorder",
490
+ "remove"
491
+ ]
492
+ },
493
+ /**
494
+ * Nobody was running the plan: the worker was lost (PRO-1040, PRO-1521).
495
+ *
496
+ * The queue's wording for a plan that failed with cause `worker_lost` --
497
+ * reaped on a dead heartbeat, or found at 'planning' with no live job by the
498
+ * reconciler, which fails it with that cause on the tick that notices
499
+ * (`armPlanning` writes the status and creates the job in ONE transaction,
500
+ * so that shape is never a race). One of three halts that mirror
501
+ * `PLAN_FAILURE_CAUSES` one-to-one; see `PLAN_FAILURE_HALT`.
502
+ */
503
+ planning_stalled: {
504
+ message: "Planning stopped without finishing, so the queue stopped here.",
505
+ params: [],
506
+ clearedWhen: "planning is retried, or a re-plan replaces it",
507
+ exits: [
508
+ "auto",
509
+ "reorder",
510
+ "remove"
511
+ ]
512
+ },
513
+ /** The run errored out. */
514
+ run_failed: {
515
+ message: "The run failed, so the queue stopped here.",
516
+ params: [],
517
+ clearedWhen: "the run is retried (back to in-flight) or replaced by a re-plan",
518
+ exits: [
519
+ "auto",
520
+ "reorder",
521
+ "remove"
522
+ ]
523
+ },
524
+ /**
525
+ * A person stopped the run. Previously invisible: the halt check listed
526
+ * `failed` and `closed` by hand and silently missed this, so a stopped run
527
+ * left its entry active forever with no reason shown.
528
+ */
529
+ run_stopped: {
530
+ message: "The run was stopped, so the queue stopped here.",
531
+ params: [],
532
+ clearedWhen: "the run is resumed (back to in-flight) or replaced by a re-plan",
533
+ exits: [
534
+ "auto",
535
+ "reorder",
536
+ "remove"
537
+ ]
538
+ },
539
+ /**
540
+ * The run reached `completed` with nothing that could merge.
541
+ *
542
+ * `completed` carries two meanings -- merged, and "the review loop converged
543
+ * with nothing left to push". The queue advances on MERGES, so treating the
544
+ * second as success planned the next entry against a main that never got
545
+ * this one, which is why the cause exists.
546
+ *
547
+ * It is NARROWER than that now (PRO-1123). The second meaning is itself two
548
+ * states, and an open pull request waiting on a reviewer is the ordinary
549
+ * handoff, not a fault: `isAwaitingMerge` routes it away from here and the
550
+ * queue leaves that entry active. What still reaches this cause is a run that
551
+ * finished with no pull request at all, or one already closed.
552
+ */
553
+ run_unmerged: {
554
+ message: "The run finished without a pull request to merge, so the queue stopped here.",
555
+ params: [],
556
+ clearedWhen: "a pull request appears and merges, or a re-plan replaces the run",
557
+ exits: [
558
+ "auto",
559
+ "reorder",
560
+ "remove"
561
+ ]
562
+ },
563
+ /** The pull request was closed without merging. */
564
+ pr_closed: {
565
+ message: "The pull request was closed without merging, so the queue stopped here.",
566
+ params: [],
567
+ clearedWhen: "a re-plan replaces the abandoned run",
568
+ exits: [
569
+ "auto",
570
+ "reorder",
571
+ "remove"
572
+ ]
573
+ },
574
+ /**
575
+ * The project asked the queue to STOP at the plan (PRO-1295), and it has.
576
+ *
577
+ * Distinct from `armed_start_missing`, which is the same observable state --
578
+ * planned, unarmed, no run -- reached by accident rather than on purpose.
579
+ * Telling a person "the run never started" when they configured exactly that
580
+ * describes their own decision as a fault, and the two need different copy
581
+ * for the same reason `planning_cancelled` is not `planning_failed`.
582
+ *
583
+ * `auto` is honest here: this clears the moment a run appears, which is what
584
+ * pressing Run after reading the plan does. No grace period either -- the
585
+ * accidental case waits five minutes in case a run is merely slow to be
586
+ * created, but a deliberate stop has nothing to wait for.
587
+ */
588
+ awaiting_plan_review: {
589
+ message: "The plan is ready and waiting for you to read it, so the queue stopped here.",
590
+ params: [],
591
+ clearedWhen: "the run starts, which is what reading the plan and pressing Run does",
592
+ exits: [
593
+ "auto",
594
+ "reorder",
595
+ "remove"
596
+ ]
597
+ },
598
+ /**
599
+ * Planning finished and armed, but the run never appeared. Work that went
600
+ * wrong is every other cause; this is work that never began, and without it
601
+ * the entry waits forever for a merge that has no pull request to come from.
602
+ */
603
+ armed_start_missing: {
604
+ message: "Planning finished but the run never started, so the queue stopped here.",
605
+ params: [],
606
+ clearedWhen: "a run appears for the plan, or a re-plan re-arms it",
607
+ exits: [
608
+ "auto",
609
+ "reorder",
610
+ "remove"
611
+ ]
612
+ },
613
+ /**
614
+ * Activation was refused, and the refusal is a decision a person owes: an
615
+ * unsettled dependency, an unchosen UI direction, an agent already on the
616
+ * item. Deliberately NOT auto-clearing. Re-deriving it every tick would flip
617
+ * the entry queued/halted forever and log a refusal each time, which is how
618
+ * the codebase already ruled on this before the causes were data.
619
+ */
620
+ activation_refused: {
621
+ message: "{message}",
622
+ params: ["message"],
623
+ clearedWhen: "never on its own; the refusal names something a person has to settle",
624
+ exits: [
625
+ "resume",
626
+ "reorder",
627
+ "remove"
628
+ ]
629
+ },
630
+ /**
631
+ * The person who queued it was erased, so there is no principal to run as.
632
+ * Fail closed rather than run as somebody who did not ask (PRO-949), and
633
+ * there is nothing to observe that would change that.
634
+ */
635
+ queued_by_erased: {
636
+ message: "The person who queued this is no longer in the organisation, so it was not started.",
637
+ params: [],
638
+ clearedWhen: "never on its own; there is no principal left to run as",
639
+ exits: ["remove"]
640
+ }
641
+ }).filter((c) => c !== "awaiting_plan_review" && c !== "armed_start_missing");
642
+ //#endregion
643
+ //#region ../../packages/types/dist/build.js
644
+ const BUILD_RUN_STATUSES = [
645
+ "pending",
646
+ "cloning",
647
+ "implementing",
648
+ "hardening",
649
+ "pushing",
650
+ "ci_checking",
651
+ "ci_fixing",
652
+ "needs_review",
653
+ "reviewing",
654
+ "completed",
655
+ "failed",
656
+ "stopped",
657
+ "closed"
658
+ ];
659
+ /**
660
+ * Status -> disposition, as a `satisfies`-checked const map (rather than a
661
+ * switch) so the assignment is exhaustive at compile time AND the narrow
662
+ * per-status literal types survive for type-level derivation below
663
+ * (BuildRunActiveStatus / BuildRunTerminalStatus).
664
+ */
665
+ const RUN_DISPOSITION = {
666
+ pending: "in_flight",
667
+ cloning: "in_flight",
668
+ implementing: "in_flight",
669
+ hardening: "in_flight",
670
+ pushing: "in_flight",
671
+ ci_checking: "in_flight",
672
+ ci_fixing: "in_flight",
673
+ needs_review: "gated",
674
+ reviewing: "in_flight",
675
+ completed: "succeeded",
676
+ failed: "failed",
677
+ stopped: "stopped",
678
+ closed: "rejected"
679
+ };
680
+ /** How the run stands. Exhaustive via the RUN_DISPOSITION map. */
681
+ function runDisposition(status) {
682
+ return RUN_DISPOSITION[status];
683
+ }
684
+ /**
685
+ * Statuses where the coding agent (or review worker) is actively executing.
686
+ * Single source of truth shared by the tenant build feed's `active=true` filter
687
+ * (api) and the dashboard's "running" detection (web), which must agree.
688
+ * = in_flight minus 'pending' (queued is not yet executing).
689
+ */
690
+ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) === "in_flight" && s !== "pending");
691
+ BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
692
+ [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
693
+ //#endregion
694
+ //#region ../../packages/types/dist/maintenance.js
695
+ /**
696
+ * The kinds whose remediation is a CODE change rather than a version bump
697
+ * (PRO-1384).
698
+ *
699
+ * Shared rather than duplicated per side, because the two sides disagreeing is
700
+ * a silent dead feature: the API decides whether a remediation is ACCEPTED and
701
+ * the web decides whether the button is OFFERED, and when this split lived only
702
+ * in the API the client kept gating on `recommendedVersion` -- which a code
703
+ * finding never has -- so the endpoint was reachable and unreachable at the
704
+ * same time.
705
+ *
706
+ * A dependency is fixed by moving to a published version, so the absence of one
707
+ * is a real blocker. A code finding has no version by construction.
708
+ */
709
+ const CODE_REMEDIATION_KINDS = [
710
+ "secret",
711
+ "sast",
712
+ "iac_misconfig"
713
+ ];
714
+ new Map(Object.entries({
715
+ deps: ["vulnerable_dependency", "outdated_dependency"],
716
+ secrets: ["secret"],
717
+ sast: ["sast"],
718
+ iac: ["iac_misconfig"]
719
+ }).flatMap(([scanner, kinds]) => kinds.map((kind) => [kind, scanner])));
720
+ new Set(CODE_REMEDIATION_KINDS);
721
+ //#endregion
722
+ //#region ../../packages/types/dist/design-tokens.js
723
+ /**
724
+ * The brand grounding contract (PRO-929).
725
+ *
726
+ * A tenant's brand can come from more than one place -- the internal
727
+ * `design_standards` document authored in Design Standards, or a linked
728
+ * design-system repository, or (later) Figma Variables. Whichever source is
729
+ * authoritative, every consumer downstream wants the same thing: the resolved
730
+ * CSS custom properties the sandboxed artifact runtime reads.
731
+ *
732
+ * This module holds that vocabulary so the producers and the consumer cannot
733
+ * drift. The web token bridge maps the internal document's Material-3-flavoured
734
+ * keys onto these names; the API's repository reader lifts them straight out of
735
+ * a design system's own theme files (a shadcn-convention design system already
736
+ * speaks this vocabulary, so there is nothing to translate). Both must aim at
737
+ * one list, which is why the list lives here rather than beside either of them
738
+ * -- the same single-source rule PRO-847 applied to standing directions.
739
+ */
740
+ /**
741
+ * The CSS custom properties the artifact runtime consumes.
742
+ *
743
+ * Deliberately the shadcn convention rather than a Taiga invention: it is what
744
+ * the sandboxed runtime's Tailwind bridge already reads, and what a generated
745
+ * application's own theme will use, so a concept previewed in these variables
746
+ * is previewed in the same names the built product will carry.
747
+ */
748
+ const ARTIFACT_CSS_VARIABLES = [
749
+ "--background",
750
+ "--foreground",
751
+ "--card",
752
+ "--card-foreground",
753
+ "--popover",
754
+ "--popover-foreground",
755
+ "--primary",
756
+ "--primary-foreground",
757
+ "--primary-hover",
758
+ "--primary-hover-foreground",
759
+ "--secondary",
760
+ "--secondary-foreground",
761
+ "--muted",
762
+ "--muted-foreground",
763
+ "--accent",
764
+ "--accent-foreground",
765
+ "--destructive",
766
+ "--destructive-foreground",
767
+ "--success",
768
+ "--success-foreground",
769
+ "--warning",
770
+ "--warning-foreground",
771
+ "--border",
772
+ "--input",
773
+ "--ring",
774
+ "--radius"
775
+ ];
776
+ /** What the artifact document carries them as. */
777
+ const ARTIFACT_SCALE_FRAME_NAMES = [
778
+ {
779
+ source: "--spacing",
780
+ frame: "--ds-spacing",
781
+ theme: "--spacing"
782
+ },
783
+ {
784
+ source: "--font-mono",
785
+ frame: "--ds-font-mono",
786
+ theme: "--font-mono"
787
+ },
788
+ {
789
+ source: "--text-xs",
790
+ frame: "--ds-text-xs",
791
+ theme: "--text-xs"
792
+ },
793
+ {
794
+ source: "--text-sm",
795
+ frame: "--ds-text-sm",
796
+ theme: "--text-sm"
797
+ },
798
+ {
799
+ source: "--text-base",
800
+ frame: "--ds-text-base",
801
+ theme: "--text-base"
802
+ },
803
+ {
804
+ source: "--text-lg",
805
+ frame: "--ds-text-lg",
806
+ theme: "--text-lg"
807
+ },
808
+ {
809
+ source: "--text-xl",
810
+ frame: "--ds-text-xl",
811
+ theme: "--text-xl"
812
+ },
813
+ {
814
+ source: "--text-2xl",
815
+ frame: "--ds-text-2xl",
816
+ theme: "--text-2xl"
817
+ },
818
+ {
819
+ source: "--text-3xl",
820
+ frame: "--ds-text-3xl",
821
+ theme: "--text-3xl"
822
+ },
823
+ {
824
+ source: "--radius-sm",
825
+ frame: "--ds-radius-sm",
826
+ theme: "--radius-sm"
827
+ },
828
+ {
829
+ source: "--radius-md",
830
+ frame: "--ds-radius-md",
831
+ theme: "--radius-md"
832
+ },
833
+ {
834
+ source: "--radius-lg",
835
+ frame: "--ds-radius-lg",
836
+ theme: "--radius-lg"
837
+ },
838
+ {
839
+ source: "--radius-xl",
840
+ frame: "--ds-radius-xl",
841
+ theme: "--radius-xl"
842
+ },
843
+ {
844
+ source: "--radius-2xl",
845
+ frame: "--ds-radius-2xl",
846
+ theme: "--radius-2xl"
847
+ },
848
+ {
849
+ source: "--ease-out",
850
+ frame: "--ds-ease-out",
851
+ theme: "--ease-out"
852
+ },
853
+ {
854
+ source: "--ease-spring",
855
+ frame: "--ds-ease-spring",
856
+ theme: "--ease-spring"
857
+ },
858
+ {
859
+ source: "--default-transition-duration",
860
+ frame: "--ds-duration",
861
+ theme: "--default-transition-duration"
862
+ },
863
+ {
864
+ source: "--duration-normal",
865
+ frame: "--ds-duration-entrance",
866
+ theme: null
867
+ }
868
+ ].map((token) => token.frame);
869
+ [...ARTIFACT_CSS_VARIABLES, ...ARTIFACT_SCALE_FRAME_NAMES];
870
+ new Set(ARTIFACT_CSS_VARIABLES.filter((name) => name !== "--radius"));
871
+ Object.freeze({
872
+ productKinds: true,
873
+ applicationOwnership: true
874
+ });
875
+ //#endregion
876
+ //#region ../../packages/types/dist/document-types-for-kind.js
877
+ /**
878
+ * What a thing with no users of its own does not have.
879
+ *
880
+ * A library, an infrastructure module and a pipeline share one fact: their
881
+ * consumers are other products, not people. That is what decides all four
882
+ * exclusions, and it is why the three kinds share a list rather than each
883
+ * having their own -- three identical lists are three chances to change one.
884
+ *
885
+ * - `user_flow`: a journey through screens. There are no screens and there
886
+ * is no journey; the ux-planning agent asked for one invents both.
887
+ * - `privacy_assessment`: a GDPR Article 35 DPIA. Its sections are the legal
888
+ * basis for processing, the data subjects affected, retention, how each
889
+ * data-subject right is served, and an approval decision. A module has no
890
+ * data subjects and is not a controller, so every one of those sections is
891
+ * written from nothing. (A library that DOES touch personal data is still
892
+ * covered: the data flow below is kept, and the threat model reads it.)
893
+ * - `service_blueprint`: frontstage and backstage of a SERVICE, divided by a
894
+ * line of visibility that only exists where a customer is looking.
895
+ * - `ui_direction`: the look and the navigation. A Terraform module offered
896
+ * a look-and-feel wizard is the clearest single symptom of PRO-1866.
897
+ *
898
+ * The `data_flow` is deliberately KEPT, against the ticket's shorter list.
899
+ * Two reasons, and both are about the documents that remain rather than about
900
+ * the data flow itself. The threat model is STRIDE over trust boundaries and
901
+ * data stores, and the data flow is where those come from -- dropping it
902
+ * leaves the threat model and the risk register standing but ungrounded, which
903
+ * is a worse outcome than a document nobody wanted. And a data flow is not an
904
+ * application concept: a module's state file, provider credentials and remote
905
+ * backend cross trust boundaries, a pipeline moves artifacts and secrets
906
+ * between them, and a library receives and returns data it does not own.
907
+ *
908
+ * The technology decisions, architecture, threat model and risk register are
909
+ * kept for every kind. Each describes the thing itself, and a consumer of a
910
+ * library has more need of them, not less.
911
+ */
912
+ const NO_USERS_OF_ITS_OWN = [
913
+ "user_flow",
914
+ "privacy_assessment",
915
+ "service_blueprint",
916
+ "ui_direction"
917
+ ];
918
+ Object.freeze({
919
+ /** The kind every product was before the column existed. Nothing is excluded. */
920
+ application: [],
921
+ /** Code other products depend on, shipped as a published package. */
922
+ library: NO_USERS_OF_ITS_OWN,
923
+ /** A Terraform module, a Helm chart: consumed by other products' infrastructure. */
924
+ infrastructure_module: NO_USERS_OF_ITS_OWN,
925
+ /** A reusable workflow, an action, a build template. */
926
+ pipeline: NO_USERS_OF_ITS_OWN
927
+ });
928
+ Object.freeze({
929
+ user_flow: {
930
+ required: ["specification"],
931
+ optional: []
932
+ },
933
+ architecture: {
934
+ required: ["specification"],
935
+ optional: ["user_flow"]
936
+ },
937
+ technology_decisions: {
938
+ required: ["specification", "architecture"],
939
+ optional: []
940
+ },
941
+ data_flow: {
942
+ required: ["specification", "architecture"],
943
+ optional: []
944
+ },
945
+ privacy_assessment: {
946
+ required: ["specification", "architecture"],
947
+ optional: ["data_flow"]
948
+ },
949
+ threat_model: {
950
+ required: [
951
+ "specification",
952
+ "architecture",
953
+ "privacy_assessment"
954
+ ],
955
+ optional: ["data_flow"]
956
+ },
957
+ risk_register: {
958
+ required: ["specification"],
959
+ optional: [
960
+ "privacy_assessment",
961
+ "threat_model",
962
+ "technology_decisions",
963
+ "architecture"
964
+ ]
965
+ },
966
+ service_blueprint: {
967
+ required: [
968
+ "specification",
969
+ "user_flow",
970
+ "architecture"
971
+ ],
972
+ optional: ["ui_direction"]
973
+ }
974
+ });
975
+ //#endregion
976
+ //#region src/tools/catalogue.ts
977
+ /**
978
+ * What exists, from the one place that knows: the shared types package.
979
+ *
980
+ * Tool descriptions are what the model reads to decide what to call, and a
981
+ * description that names a document type the catalogue lacks sends it down a
982
+ * dead end (the walk-through that led here found `policy_acceptable_use` and
983
+ * `policy_ip` named in a description and nowhere else). Every list of types,
984
+ * origins and statuses a tool shows is derived from these constants, and a
985
+ * test holds the descriptions to them.
986
+ */
987
+ /**
988
+ * Display names for every product document type. The reviewable nine come
989
+ * from the shared map the web uses; the three the web never asks a person to
990
+ * review are named here, and the record type keeps the two sets complete.
991
+ */
992
+ const PRODUCT_DOCUMENT_NAMES = {
993
+ ...PRODUCT_DOCUMENT_DISPLAY_NAMES,
994
+ ui_direction: "Look & Feel",
995
+ codebase_snapshot: "Codebase Snapshot",
996
+ product_instructions: "Product Instructions"
997
+ };
998
+ function productDocumentName(type) {
999
+ return PRODUCT_DOCUMENT_NAMES[type] ?? type;
1000
+ }
1001
+ /** The organization documents that are not policies: goals, standards, voice, instructions. */
1002
+ const OTHER_TENANT_DOCUMENT_TYPES = TENANT_DOCUMENT_TYPES.filter((type) => !POLICY_TYPES.includes(type));
1003
+ /** `a, b, c` for a description. */
1004
+ function named(types) {
1005
+ return types.join(", ");
1006
+ }
1007
+ /**
1008
+ * The two document timelines of a product (ADR-017), in the words the tools
1009
+ * and the server instructions use. One place, so the list tool, the read
1010
+ * tool and the instructions cannot explain them three different ways.
1011
+ */
1012
+ const ORIGIN_MEANING = {
1013
+ plan: "plan: the discovery documents, authored intent. Every greenfield product has them; they say what is meant to be built.",
1014
+ reality: "reality: derived from the code on the main branch, what is actually built. A brownfield product has them from its import; a greenfield product gets them once its first initiative is done (a merged pull request marks it done) or the repository is pushed to, and they refresh as code lands."
1015
+ };
1016
+ /**
1017
+ * Which timeline is a product's own (the API's documentOriginForSettings):
1018
+ * an imported product has no plan chain by design, so its documents are on
1019
+ * the reality side; everything else starts on the plan side. Derived from the
1020
+ * same settings key the API reads, so the two cannot disagree.
1021
+ */
1022
+ function isImportedProduct(product) {
1023
+ const settings = product?.settings;
1024
+ return typeof settings === "object" && settings !== null && settings.projectType === "brownfield";
1025
+ }
1026
+ /**
1027
+ * The document types no derivation ever writes: a person records or writes
1028
+ * them, so they live on the plan side of every product, imported or not.
1029
+ */
1030
+ const PLAN_ONLY_DOCUMENT_TYPES = PRODUCT_DOCUMENT_TYPES.filter((type) => !REALITY_DERIVED_DOCUMENT_TYPES.includes(type));
1031
+ /**
1032
+ * The timeline a read should default to: the plan side for a type only a
1033
+ * person writes, else the product's own side (reality for an imported
1034
+ * product, plan for every other).
1035
+ */
1036
+ function defaultOriginFor(product, type) {
1037
+ if (type !== void 0 && PLAN_ONLY_DOCUMENT_TYPES.includes(type)) return "plan";
1038
+ return isImportedProduct(product) ? "reality" : "plan";
1039
+ }
1040
+ /**
1041
+ * Where an imported product's import got to (`settings.importStatus`, written
1042
+ * by the API: pending, analyzing, complete, cancelled, failed). Unknown values
1043
+ * read as "not known", never as a promise.
1044
+ */
1045
+ function importStatusOf(product) {
1046
+ const settings = product?.settings;
1047
+ if (typeof settings !== "object" || settings === null) return null;
1048
+ const status = settings.importStatus;
1049
+ return typeof status === "string" ? status : null;
1050
+ }
1051
+ /** Why a side of the timeline is empty, for a list or a miss that shows one. */
1052
+ function emptyOriginNote(origin, product) {
1053
+ const imported = isImportedProduct(product);
1054
+ if (origin === "plan") return imported ? `This product was imported, so no discovery chain was generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
1055
+ if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
1056
+ switch (importStatusOf(product)) {
1057
+ case "pending": return "No reality documents yet: the import has not started. Link the repository and run the import in Taiga.";
1058
+ case "analyzing":
1059
+ case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
1060
+ case "cancelled": return "No reality documents: the import was cancelled before it derived any. Run it again in Taiga.";
1061
+ case "failed": return "No reality documents: the import failed before it derived any. Run it again in Taiga.";
1062
+ default: return "No reality documents: the import derives them from the code, and none have been published.";
1063
+ }
1064
+ }
1065
+ //#endregion
216
1066
  //#region src/tools/errors.ts
217
1067
  /** A successful tool result: one JSON text block the model reads. */
218
1068
  function toolResult(value) {
@@ -263,17 +1113,6 @@ async function runTool(handler) {
263
1113
  }
264
1114
  }
265
1115
  //#endregion
266
- //#region src/tools/inputs.ts
267
- /**
268
- * A document type as the API names one: policy_security, factory_instructions,
269
- * specification. An identifier, never a path: the value is interpolated into
270
- * the route, and a type that carried a slash would reach a different
271
- * document's route than the tool's description promises. The API's own enum
272
- * stays the source of truth for which identifiers exist (it answers 400 with
273
- * the valid list); this only rules out what could not be a type at all.
274
- */
275
- const documentType = z.string().regex(/^[a-z][a-z0-9_]{0,63}$/, "A document type is a lowercase identifier such as policy_security");
276
- //#endregion
277
1116
  //#region src/tools/remote.ts
278
1117
  const SCP_LIKE = /^(?:[^@\s/]+@)?([^:\s/]+):([^\s]+)$/;
279
1118
  function parseRemote(remote) {
@@ -335,37 +1174,63 @@ function webOrigin(url) {
335
1174
  }
336
1175
  }
337
1176
  //#endregion
338
- //#region src/tools/summarize.ts
339
- /**
340
- * Strip heavy content from list responses to avoid context window bloat.
341
- *
342
- * List endpoints return summaries (id, name, description, metadata).
343
- * Get endpoints return the full content.
344
- *
345
- * This keeps list calls cheap (~100 tokens per item) and lets the AI
346
- * selectively load full content only for the items it needs.
347
- */
348
- /** Fields to strip from list responses -- these contain the bulk content. */
349
- const HEAVY_FIELDS = [
350
- "content",
351
- "data",
352
- "sections"
353
- ];
1177
+ //#region src/tools/sections.ts
1178
+ function isRecord(value) {
1179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1180
+ }
1181
+ /** The `sections` map of a document, or null when the document is not section-shaped. */
1182
+ function sectionsOf(data) {
1183
+ return isRecord(data.sections) ? data.sections : null;
1184
+ }
1185
+ /** The text of one section, or null when the section does not exist. */
1186
+ function sectionContent(sections, sectionId) {
1187
+ const section = sections[sectionId];
1188
+ if (section === void 0 || section === null) return null;
1189
+ if (typeof section === "string") return section;
1190
+ return isRecord(section) && typeof section.content === "string" ? section.content : "";
1191
+ }
1192
+ /** The map with one section's text replaced (or the section added); every other section untouched. */
1193
+ function applySectionEdit(sections, sectionId, content) {
1194
+ const prior = sections[sectionId];
1195
+ return {
1196
+ ...sections,
1197
+ [sectionId]: isRecord(prior) ? {
1198
+ ...prior,
1199
+ content
1200
+ } : { content }
1201
+ };
1202
+ }
354
1203
  /**
355
- * Remove heavy content fields from each item in an array.
356
- * Preserves all metadata (id, name, description, type, dates, etc).
1204
+ * A document for reading, without its provenance: the top-level `derivedFrom`
1205
+ * tree and `outdated` source list (PRO-2359), and each section's own
1206
+ * `derivedFrom` (PRO-2158). They say which documents shaped this one, which
1207
+ * the editor needs and a reader does not; `source` and `priority` stay, the
1208
+ * server instructions explain them.
357
1209
  */
358
- function summarizeList(items) {
359
- return items.map((item) => {
360
- const summary = { ...item };
361
- for (const field of HEAVY_FIELDS) if (field in summary) delete summary[field];
362
- return summary;
363
- });
1210
+ function stripProvenance(doc) {
1211
+ const { derivedFrom: _derivedFrom, outdated: _outdated, ...rest } = doc;
1212
+ if (!isRecord(rest.data)) return rest;
1213
+ const sections = sectionsOf(rest.data);
1214
+ if (!sections) return rest;
1215
+ const stripped = Object.fromEntries(Object.entries(sections).map(([id, section]) => {
1216
+ if (!isRecord(section)) return [id, section];
1217
+ const { derivedFrom: _sectionDerivedFrom, ...keep } = section;
1218
+ return [id, keep];
1219
+ }));
1220
+ return {
1221
+ ...rest,
1222
+ data: {
1223
+ ...rest.data,
1224
+ sections: stripped
1225
+ }
1226
+ };
364
1227
  }
1228
+ //#endregion
1229
+ //#region src/tools/summarize.ts
365
1230
  /**
366
- * Keep only the named fields of each item. For lists whose rows carry prose
367
- * (a backlog item's description, a run's metrics) that `summarizeList`'s
368
- * fixed field names would not catch.
1231
+ * Keep only the named fields of each item. List tools return summaries (the
1232
+ * fields a model needs to pick an item) and get tools return everything;
1233
+ * this is how a list drops the prose and the provenance trees.
369
1234
  */
370
1235
  function pickFields(items, fields) {
371
1236
  return items.map((item) => {
@@ -386,6 +1251,21 @@ const PRODUCT_SUMMARY_FIELDS = [
386
1251
  "kind",
387
1252
  "discoveryStep"
388
1253
  ];
1254
+ /**
1255
+ * What a document row needs to say for the model to pick one and read it in
1256
+ * full: which document, on which timeline, how current. Not the derived-from
1257
+ * tree, the data key count or the reviewer's name.
1258
+ */
1259
+ const DOCUMENT_SUMMARY_FIELDS$1 = [
1260
+ "type",
1261
+ "origin",
1262
+ "version",
1263
+ "isDraft",
1264
+ "hasDraft",
1265
+ "stage",
1266
+ "reviewedAt",
1267
+ "updatedAt"
1268
+ ];
389
1269
  /** The API's page ceiling (paginationSchema in apps/api). */
390
1270
  const PAGE_SIZE = 100;
391
1271
  /**
@@ -394,6 +1274,8 @@ const PAGE_SIZE = 100;
394
1274
  * matches is not a resolution the model can act on anyway.
395
1275
  */
396
1276
  const RESOLVE_PAGE_LIMIT = 10;
1277
+ const productId = z.string().uuid().describe("Product ID -- get from taiga_resolve_product (from a git remote) or taiga_list_products");
1278
+ const originInput = z.enum(PRODUCT_DOCUMENT_ORIGINS).optional().describe(`Which timeline. ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} Omit it to read the product's own timeline: reality for an imported product, plan for every other, and always plan for the documents a person writes (Look & Feel, Product Instructions).`);
397
1279
  /**
398
1280
  * Every product linked to the repository, page by page, until the API runs
399
1281
  * out or the cap is hit. A page that fails fails the call, like every other
@@ -421,6 +1303,82 @@ async function linkedProducts(client, repository, repositoryHost) {
421
1303
  truncated: true
422
1304
  };
423
1305
  }
1306
+ /** A document list row as the model sees it: the summary fields plus a display name and the staleness fact. */
1307
+ function documentSummaries(rows) {
1308
+ const typed = rows;
1309
+ return pickFields(typed, DOCUMENT_SUMMARY_FIELDS$1).map((picked, index) => {
1310
+ const outdated = typed[index].outdated;
1311
+ const fact = picked.isDraft !== false ? void 0 : outdated === null ? false : Array.isArray(outdated?.sources) ? outdated.sources.length > 0 : void 0;
1312
+ return {
1313
+ type: picked.type,
1314
+ name: productDocumentName(String(picked.type)),
1315
+ ...picked,
1316
+ ...fact === void 0 ? {} : { outdated: fact }
1317
+ };
1318
+ });
1319
+ }
1320
+ /** The one sentence for a side the API would not list, with what it said. */
1321
+ function unlistableNote(origin, reason) {
1322
+ return `The ${origin} side could not be listed (${reason instanceof TaigaApiError ? `${reason.status} ${reason.code ?? ""}`.trim() : "the request failed"}).`;
1323
+ }
1324
+ /**
1325
+ * Both timelines of a product's documents, each a list of summaries, with
1326
+ * which one is the product's own and a note for an empty side. The product
1327
+ * decides what an empty side means (imported or not, how far its import got),
1328
+ * so a caller that already holds it passes it in.
1329
+ */
1330
+ async function documentTimelines(client, id, product) {
1331
+ const loaded = product ?? (await client.getProduct(id)).data;
1332
+ const settled = await Promise.allSettled(PRODUCT_DOCUMENT_ORIGINS.map((o) => client.listProductDocuments(id, {
1333
+ origin: o,
1334
+ limit: PAGE_SIZE
1335
+ })));
1336
+ if (settled.every((result) => result.status === "rejected")) throw settled[0].reason;
1337
+ const sides = {};
1338
+ const unlistable = {};
1339
+ PRODUCT_DOCUMENT_ORIGINS.forEach((o, index) => {
1340
+ const result = settled[index];
1341
+ if (result.status === "fulfilled") sides[o] = documentSummaries(result.value.data);
1342
+ else {
1343
+ sides[o] = null;
1344
+ unlistable[o] = unlistableNote(o, result.reason);
1345
+ }
1346
+ });
1347
+ const notes = PRODUCT_DOCUMENT_ORIGINS.flatMap((o) => {
1348
+ const side = sides[o];
1349
+ if (!side) return [unlistable[o]];
1350
+ return side.length === 0 ? [emptyOriginNote(o, loaded)] : [];
1351
+ });
1352
+ return {
1353
+ projectType: isImportedProduct(loaded) ? "brownfield" : "greenfield",
1354
+ defaultOrigin: defaultOriginFor(loaded),
1355
+ plan: sides.plan ?? null,
1356
+ reality: sides.reality ?? null,
1357
+ unlistable,
1358
+ notes
1359
+ };
1360
+ }
1361
+ const isPublished = (row) => row.isDraft === false;
1362
+ /** `specification (v2), architecture (draft)` for one side. */
1363
+ function sideList(rows) {
1364
+ return rows.map((row) => `${String(row.type)} (${isPublished(row) ? `v${String(row.version)}` : "draft"})`).join(", ");
1365
+ }
1366
+ /** What a missed document read should say: where the type does live, and what each side holds. */
1367
+ function describeMiss(type, wanted, timelines, product) {
1368
+ const name = productDocumentName(type);
1369
+ const other = wanted === "plan" ? "reality" : "plan";
1370
+ const parts = [`No published ${wanted} ${name} (${type}) for this product.`];
1371
+ const rows = (side) => timelines[side] ?? [];
1372
+ if (rows(other).some((row) => row.type === type && isPublished(row))) parts.push(`It is published on the ${other} side: call again with origin '${other}'.`);
1373
+ else for (const side of [wanted, other]) if (rows(side).some((row) => row.type === type && !isPublished(row))) parts.push(`A draft of it exists on the ${side} side, unpublished.`);
1374
+ for (const o of PRODUCT_DOCUMENT_ORIGINS) {
1375
+ const side = timelines[o];
1376
+ const label = `${o[0].toUpperCase()}${o.slice(1)} documents`;
1377
+ if (side === null) parts.push(timelines.unlistable[o]);
1378
+ else parts.push(side.length > 0 ? `${label}: ${sideList(side)}.` : emptyOriginNote(o, product));
1379
+ }
1380
+ return parts.join(" ");
1381
+ }
424
1382
  function registerProductTools(server, client) {
425
1383
  if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
426
1384
  title: "List factories",
@@ -433,14 +1391,10 @@ function registerProductTools(server, client) {
433
1391
  if (client.hasCapability("products:read")) {
434
1392
  server.registerTool("taiga_list_products", {
435
1393
  title: "List products",
436
- description: "List products in the organization. Use this to find a product by name; inside a git checkout, taiga_resolve_product finds it from the remote URL instead. Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).",
1394
+ description: "List products in the organization, as summaries (id, name, slug, status, factory, kind). Use this to find a product by name; inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full.",
437
1395
  inputSchema: {
438
1396
  factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
439
- status: z.enum([
440
- "draft",
441
- "active",
442
- "archived"
443
- ]).optional().describe("Filter by status. Taiga no longer surfaces product status in its UI (PRO-543), so every product is currently \"draft\" and filtering by \"active\" or \"archived\" returns nothing. Omit this unless you are working with data set through the API directly."),
1397
+ status: z.enum(PRODUCT_STATUSES).optional().describe("Filter by status: draft until the product finishes setup in Taiga, active after that, archived when retired."),
444
1398
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)")
445
1399
  },
446
1400
  annotations: READ_ONLY
@@ -449,11 +1403,22 @@ function registerProductTools(server, client) {
449
1403
  }));
450
1404
  server.registerTool("taiga_get_product", {
451
1405
  title: "Get product",
452
- description: "Get one product in full: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
453
- inputSchema: { productId: z.string().uuid().describe("Product ID -- get from taiga_resolve_product (from a git remote) or taiga_list_products") },
1406
+ description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
1407
+ inputSchema: {
1408
+ productId: productId.optional(),
1409
+ slug: z.string().max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "A slug is lowercase words joined by hyphens").optional().describe("Product slug, as it appears in Taiga URLs -- an alternative to productId")
1410
+ },
454
1411
  annotations: READ_ONLY
455
1412
  }, (params) => runTool(async () => {
456
- return toolResult((await client.getProduct(params.productId)).data);
1413
+ if (params.productId && params.slug) return toolError("VALIDATION_FAILED", "Pass productId or slug, not both: they could name two products.");
1414
+ if (params.productId) return toolResult((await client.getProduct(params.productId)).data);
1415
+ if (!params.slug) return toolError("VALIDATION_FAILED", "Pass productId or slug.");
1416
+ const [product] = (await client.listProducts({
1417
+ slug: params.slug,
1418
+ limit: 1
1419
+ })).data;
1420
+ if (!product) return toolError("NOT_FOUND", `No product you can see has the slug "${params.slug}". taiga_list_products shows the ones you can.`);
1421
+ return toolResult(product);
457
1422
  }));
458
1423
  server.registerTool("taiga_resolve_product", {
459
1424
  title: "Resolve product from git remote",
@@ -474,57 +1439,61 @@ function registerProductTools(server, client) {
474
1439
  }));
475
1440
  server.registerTool("taiga_list_product_documents", {
476
1441
  title: "List product documents",
477
- description: "List documents for a specific product (names and types only). Product documents are generated during intake: specification (requirements), architecture (system design), technology decisions, data flow, privacy assessment, threat model (security risks), risk register, user flow, and service blueprint. Use taiga_get_product_document to read the full content.",
478
- inputSchema: {
479
- productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
480
- limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)")
481
- },
1442
+ description: `List a product's documents on both timelines, as summaries (type, name, version, draft state, review and update times), and say which timeline is the product's own (defaultOrigin). Two timelines: ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} An empty side comes with a note saying why; a side the API could not list is null, with a note. Use taiga_get_product_document with the type and origin to read one in full.`,
1443
+ inputSchema: { productId },
482
1444
  annotations: READ_ONLY
483
1445
  }, (params) => runTool(async () => {
484
- return toolResult(summarizeList((await client.listProductDocuments(params.productId, { limit: params.limit })).data));
1446
+ const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
1447
+ return toolResult(timelines);
485
1448
  }));
486
1449
  server.registerTool("taiga_get_product_document", {
487
1450
  title: "Get product document",
488
- description: "Get the full content of a specific product document. Use this when you need to understand what to build (specification), how the system is designed (architecture), what security risks exist (threat model), or what technology choices were made (technology_decisions). Types: specification, architecture, technology_decisions, data_flow, privacy_assessment, threat_model, risk_register, user_flow, service_blueprint, ui_direction, codebase_snapshot, and product_instructions (the product's standing instructions).",
1451
+ description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A read that finds nothing says which documents and timelines the product does have.",
489
1452
  inputSchema: {
490
- productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
491
- type: documentType.describe("Document type -- get available types from taiga_list_product_documents. Common: specification, architecture, threat_model, technology_decisions")
1453
+ productId,
1454
+ type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
1455
+ origin: originInput
492
1456
  },
493
1457
  annotations: READ_ONLY
494
1458
  }, (params) => runTool(async () => {
495
- return toolResult((await client.getProductDocument(params.productId, params.type)).data);
1459
+ let product;
1460
+ if (!params.origin) product = (await client.getProduct(params.productId)).data;
1461
+ const origin = params.origin ?? defaultOriginFor(product, params.type);
1462
+ try {
1463
+ return toolResult(stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data));
1464
+ } catch (error) {
1465
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1466
+ product ??= (await client.getProduct(params.productId)).data;
1467
+ const timelines = await documentTimelines(client, params.productId, product);
1468
+ return toolError("NOT_FOUND", describeMiss(params.type, origin, timelines, product));
1469
+ }
496
1470
  }));
497
1471
  }
498
1472
  }
499
1473
  //#endregion
500
- //#region src/tools/sections.ts
501
- function isRecord(value) {
502
- return typeof value === "object" && value !== null && !Array.isArray(value);
503
- }
504
- /** The `sections` map of a document, or null when the document is not section-shaped. */
505
- function sectionsOf(data) {
506
- return isRecord(data.sections) ? data.sections : null;
507
- }
508
- /** The text of one section, or null when the section does not exist. */
509
- function sectionContent(sections, sectionId) {
510
- const section = sections[sectionId];
511
- if (section === void 0 || section === null) return null;
512
- if (typeof section === "string") return section;
513
- return isRecord(section) && typeof section.content === "string" ? section.content : "";
1474
+ //#region src/tools/documents.ts
1475
+ /**
1476
+ * What a document row needs to say for the model to pick one and read it in
1477
+ * full. Not the content, not who wrote it.
1478
+ */
1479
+ const DOCUMENT_SUMMARY_FIELDS = [
1480
+ "type",
1481
+ "name",
1482
+ "description",
1483
+ "version",
1484
+ "isDraft",
1485
+ "hasDraft",
1486
+ "updatedAt"
1487
+ ];
1488
+ const summaries = (rows) => pickFields(rows, DOCUMENT_SUMMARY_FIELDS);
1489
+ /** What a lookup behind a miss said when it failed, so an unknown stays unknown. */
1490
+ function failedLookup(what, reason) {
1491
+ return `${what} could not be checked (${reason instanceof TaigaApiError ? `${reason.status} ${reason.code ?? ""}`.trim() : "the request failed"}).`;
514
1492
  }
515
- /** The map with one section's text replaced (or the section added); every other section untouched. */
516
- function applySectionEdit(sections, sectionId, content) {
517
- const prior = sections[sectionId];
518
- return {
519
- ...sections,
520
- [sectionId]: isRecord(prior) ? {
521
- ...prior,
522
- content
523
- } : { content }
524
- };
1493
+ /** `a (v3), b (v1)` for a not-found message. */
1494
+ function publishedList(rows) {
1495
+ return rows.filter((row) => row.isDraft === false).map((row) => `${row.type} (v${String(row.version)})`).join(", ");
525
1496
  }
526
- //#endregion
527
- //#region src/tools/documents.ts
528
1497
  const DRAFT_WRITE = {
529
1498
  readOnlyHint: false,
530
1499
  destructiveHint: false,
@@ -539,7 +1508,7 @@ const PUBLISH = {
539
1508
  };
540
1509
  const sectionId = z.string().min(1).describe("Section id -- the keys of `data.sections` in taiga_get_document, e.g. summary, scope, apiDesign. An id that does not exist yet adds a section.");
541
1510
  const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
542
- const tenantDocumentType = documentType.describe("Document type (e.g. policy_security, policy_sdlc) -- see taiga_list_documents");
1511
+ const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named(POLICY_TYPES)}) or ${named(OTHER_TENANT_DOCUMENT_TYPES)}`);
543
1512
  /** Everything the model needs back from a write, without the content it already holds. */
544
1513
  function documentSummary(doc) {
545
1514
  const { data, ...rest } = doc;
@@ -592,19 +1561,33 @@ function registerDocumentTools(server, client) {
592
1561
  if (client.hasCapability("tenant:documents:read")) {
593
1562
  server.registerTool("taiga_list_documents", {
594
1563
  title: "List company documents",
595
- description: "List company-level policy documents (names and types only). Policies define security, data privacy, SDLC, operations, architecture, and compliance rules that apply across all factories and products. Use taiga_get_document with the document type to read the full policy content.",
1564
+ description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
596
1565
  inputSchema: { limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)") },
597
1566
  annotations: READ_ONLY
598
1567
  }, (params) => runTool(async () => {
599
- return toolResult(summarizeList((await client.listTenantDocuments(params)).data));
1568
+ const result = await client.listTenantDocuments(params);
1569
+ return toolResult(summaries(result.data));
600
1570
  }));
601
1571
  server.registerTool("taiga_get_document", {
602
1572
  title: "Get company document",
603
- description: "Get the full content of a company policy document. Use this when you need to understand security requirements, data handling rules, architecture standards, or compliance controls that apply to your code. Types: policy_security, policy_data, policy_sdlc, policy_ops, policy_tic, policy_risk, policy_architecture, policy_supply_chain, policy_cloud_governance, policy_acceptable_use, policy_ip.",
604
- inputSchema: { type: documentType.describe("Document type -- get available types from taiga_list_documents. Common: policy_security, policy_data, policy_sdlc, policy_architecture") },
1573
+ description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A read that finds nothing says which documents are published and whether a draft exists.",
1574
+ inputSchema: { type: tenantDocumentType },
605
1575
  annotations: READ_ONLY
606
1576
  }, (params) => runTool(async () => {
607
- return toolResult((await client.getTenantDocument(params.type)).data);
1577
+ try {
1578
+ return toolResult(stripProvenance((await client.getTenantDocument(params.type)).data));
1579
+ } catch (error) {
1580
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1581
+ const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
1582
+ const parts = [`No published ${params.type}.`];
1583
+ if (draft.status === "fulfilled") parts.push(draft.value.data ? "A draft exists; taiga_publish_document publishes it once the person asks." : "There is no draft either.");
1584
+ else parts.push(failedLookup("The draft", draft.reason));
1585
+ if (all.status === "fulfilled") {
1586
+ const published = publishedList(all.value.data);
1587
+ parts.push(published ? `Published documents: ${published}.` : "Nothing is published yet.");
1588
+ } else parts.push(failedLookup("The published list", all.reason));
1589
+ return toolError("NOT_FOUND", parts.join(" "));
1590
+ }
608
1591
  }));
609
1592
  }
610
1593
  if (client.hasCapability("tenant:documents:read") && client.hasCapability("tenant:documents:write")) {
@@ -634,25 +1617,36 @@ function registerDocumentTools(server, client) {
634
1617
  if (client.hasCapability("factories:read")) {
635
1618
  server.registerTool("taiga_list_factory_documents", {
636
1619
  title: "List factory documents",
637
- description: "List documents for a specific factory (names and types only). A factory has one document: its instructions, the factory's free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Use taiga_get_factory_document to read the full content.",
1620
+ description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named(FACTORY_DOCUMENT_TYPES)}: its free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Drafts are listed too (isDraft true); an empty list means neither a publication nor a draft exists. Use taiga_get_factory_document to read the full content.`,
638
1621
  inputSchema: {
639
1622
  factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
640
1623
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)")
641
1624
  },
642
1625
  annotations: READ_ONLY
643
1626
  }, (params) => runTool(async () => {
644
- return toolResult(summarizeList((await client.listFactoryDocuments(params.factoryId, { limit: params.limit })).data));
1627
+ const result = await client.listFactoryDocuments(params.factoryId, { limit: params.limit });
1628
+ return toolResult(summaries(result.data));
645
1629
  }));
646
1630
  server.registerTool("taiga_get_factory_document", {
647
1631
  title: "Get factory instructions",
648
- description: "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow.",
1632
+ description: "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow. A read that finds nothing says whether a draft exists and how to write one.",
649
1633
  inputSchema: {
650
1634
  factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
651
- type: documentType.describe("Document type -- get available types from taiga_list_factory_documents. The factory instructions document is `factory_instructions`.")
1635
+ type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
652
1636
  },
653
1637
  annotations: READ_ONLY
654
1638
  }, (params) => runTool(async () => {
655
- return toolResult((await client.getFactoryDocument(params.factoryId, params.type)).data);
1639
+ try {
1640
+ return toolResult(stripProvenance((await client.getFactoryDocument(params.factoryId, params.type)).data));
1641
+ } catch (error) {
1642
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1643
+ await client.getFactory(params.factoryId);
1644
+ const detail = await client.getFactoryDocumentDraft(params.factoryId, params.type).then((draft) => draft.data ? "A draft exists; taiga_publish_factory_document publishes it once the person asks." : "There is no draft either: write one with taiga_update_factory_document, then publish it when the person asks.").catch((reason) => {
1645
+ if (reason instanceof TaigaApiError && reason.status === 404) throw reason;
1646
+ return failedLookup("The draft", reason);
1647
+ });
1648
+ return toolError("NOT_FOUND", `This factory has no published ${params.type} yet. ${detail}`);
1649
+ }
656
1650
  }));
657
1651
  }
658
1652
  if (client.hasCapability("factories:read") && client.hasCapability("factories:documents:write")) {
@@ -663,7 +1657,7 @@ function registerDocumentTools(server, client) {
663
1657
  publish: () => client.publishFactoryDocument(factoryId, type)
664
1658
  });
665
1659
  const factoryId = z.string().uuid().describe("Factory ID -- get from taiga_list_factories");
666
- const factoryDocumentType = documentType.describe("Document type (`factory_instructions`)");
1660
+ const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
667
1661
  server.registerTool("taiga_update_factory_document", {
668
1662
  title: "Update a section of factory instructions",
669
1663
  description: "Replace the text of one section of a factory's instructions, in its draft. The instructions document has one section, `instructions`. Does not publish: products keep following the published version until taiga_publish_factory_document is called. Read the current text with taiga_get_factory_document first. Use this when the factory conventions, domain context or tech preferences need updating while working on the codebase.",
@@ -812,12 +1806,69 @@ function registerAllTools(server, client) {
812
1806
  registerBuildTools(server, client);
813
1807
  }
814
1808
  //#endregion
1809
+ //#region src/tools/instructions.ts
1810
+ /**
1811
+ * What the server tells a client at connect time: the knowledge base, the
1812
+ * order to read it in, the two document timelines, what section metadata
1813
+ * means, and when to write. Kept as data so a test can hold it to the
1814
+ * catalogue the same way it holds the tool descriptions.
1815
+ */
1816
+ const SERVER_INSTRUCTIONS = [
1817
+ "You have access to Taiga, a policy-aware software development platform: the organization's",
1818
+ "engineering knowledge base, its products, their backlog and what the coding agent has built.",
1819
+ "",
1820
+ "**Company documents** (taiga_list_documents / taiga_get_document):",
1821
+ `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
1822
+ `architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
1823
+ "carry the business goals, design and brand standards, tone of voice and standing instructions.",
1824
+ "Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
1825
+ "",
1826
+ "**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
1827
+ "A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
1828
+ "tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
1829
+ "builds software. A factory that has not written any yet has none; that is normal.",
1830
+ "",
1831
+ "**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
1832
+ `${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
1833
+ "A product has two timelines of these, and the list shows both:",
1834
+ `- ${ORIGIN_MEANING.plan}`,
1835
+ `- ${ORIGIN_MEANING.reality}`,
1836
+ "An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
1837
+ "reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
1838
+ "side of every product. Omit origin on a read and the server picks the right side (the list reports the",
1839
+ "product's own as defaultOrigin). To understand",
1840
+ "what to build, read the plan where there is one; to understand the code you are changing, read reality",
1841
+ "when it exists, the ground truth for architecture, data flow and threats as built.",
1842
+ "",
1843
+ "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
1844
+ "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
1845
+ "for them: runs, steps, commits and pull requests.",
1846
+ "",
1847
+ "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
1848
+ "provenance fields you can read past: `source` says where the text originally came from (template:",
1849
+ "Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>:",
1850
+ "rendered for the organization's cloud providers; any other tag names the derivation that wrote the",
1851
+ "text; a hand edit keeps the source it had) and",
1852
+ "`priority: core` marks the sections every organization keeps. Neither changes what the section",
1853
+ "requires of you.",
1854
+ "",
1855
+ "Workflow: inside a git checkout, start with taiga_resolve_product on the remote URL to find the product.",
1856
+ "Otherwise use taiga_list_factories and taiga_list_products. Then read the factory instructions,",
1857
+ "the product's specification and architecture, and the backlog item you are working on. A document",
1858
+ "read that finds nothing tells you what does exist and which tool reads or writes it.",
1859
+ "",
1860
+ "When you notice a policy or factory instructions are outdated, fix the section with",
1861
+ "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
1862
+ "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
1863
+ "This keeps the knowledge base accurate for everyone."
1864
+ ].join("\n");
1865
+ //#endregion
815
1866
  //#region src/index.ts
816
1867
  async function main() {
817
1868
  const config = readConfig();
818
1869
  if (!config) {
819
1870
  console.error("Error: TAIGA_API_KEY environment variable is required.");
820
- console.error("Create an API key at https://app.taiga.app/settings/api-keys");
1871
+ console.error("Create one at https://eu.tai.ga under Settings > Account > API keys.");
821
1872
  process.exit(1);
822
1873
  }
823
1874
  const client = new TaigaClient({
@@ -833,35 +1884,7 @@ async function main() {
833
1884
  const server = new McpServer({
834
1885
  name: "taiga",
835
1886
  version: SERVER_VERSION
836
- }, { instructions: [
837
- "You have access to Taiga -- a policy-aware software development platform.",
838
- "Taiga provides your organization's engineering knowledge base:",
839
- "",
840
- "**Company policies** (taiga_list_documents / taiga_get_document):",
841
- "Security, data privacy, SDLC, architecture, and compliance rules that apply to all code.",
842
- "Read the relevant policy BEFORE writing code that touches security, data handling, or infrastructure.",
843
- "",
844
- "**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
845
- "A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
846
- "tech preferences (approved stack, patterns), and conventions. Read these to understand HOW this factory builds software.",
847
- "",
848
- "**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
849
- "Specification (what to build), architecture (how it's designed), threat model (security risks),",
850
- "technology decisions, and data flow. Read these to understand the specific product.",
851
- "",
852
- "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
853
- "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
854
- "for them: runs, steps, commits and pull requests.",
855
- "",
856
- "Workflow: Inside a git checkout, start with taiga_resolve_product on the remote URL to find the product.",
857
- "Otherwise use taiga_list_factories and taiga_list_products. Then read the factory instructions,",
858
- "the product's specification and architecture, and the backlog item you are working on.",
859
- "",
860
- "When you notice a policy or factory instructions are outdated, fix the section with",
861
- "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
862
- "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
863
- "This keeps the knowledge base accurate for everyone."
864
- ].join("\n") });
1887
+ }, { instructions: SERVER_INSTRUCTIONS });
865
1888
  registerAllTools(server, client);
866
1889
  const toolCount = introspection.capabilities.length;
867
1890
  console.error(`Taiga MCP server started (${toolCount} capabilities, user: ${introspection.email}, role: ${introspection.role})`);