@taiga-ai/mcp-server 0.6.0 → 0.7.0-dev.396

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,871 @@ 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
+ const TURN_EVENT_TRIGGERS = [
440
+ "defined",
441
+ "plan_started",
442
+ "plan_completed",
443
+ "plan_failed",
444
+ "plan_approved",
445
+ "plan_rejected",
446
+ "build_started",
447
+ "pr_opened",
448
+ "pr_ready",
449
+ "build_completed",
450
+ "build_failed",
451
+ "build_stopped",
452
+ "pr_merged",
453
+ "pr_closed",
454
+ "edited",
455
+ "disposition_changed",
456
+ "archived",
457
+ "unarchived"
458
+ ];
459
+ /**
460
+ * The two blocking states are NOT the same kind of blocker, and collapsing them
461
+ * into one list is what caused a bug in each direction (PRO-1028, PRO-1039).
462
+ * They are separate constants so the difference is data a reader can see rather
463
+ * than a comment a reader can miss.
464
+ */
465
+ /**
466
+ * GLOBAL blockers: at most one per project, at any position. `active` is held
467
+ * by `uq_product_queue_active`, so an entry in this state blocks the whole
468
+ * queue WHEREVER it sits -- `reposition` lets a queued entry be dragged in
469
+ * front of the active one, and starting a second run then violates that index.
470
+ */
471
+ const QUEUE_GLOBAL_BLOCKING_STATES = ["active"];
472
+ /**
473
+ * POSITIONAL blockers: they stop what is BEHIND them and nothing in front.
474
+ * `halted` blocks by existing, which is how halt-on-failure works without a
475
+ * separate paused flag -- but only for the entries queued after it. That is
476
+ * what the UI has always promised, and it gives a halt a second exit: move it
477
+ * down and the rest of the queue carries on.
478
+ */
479
+ const QUEUE_POSITIONAL_BLOCKING_STATES = ["halted"];
480
+ [...QUEUE_GLOBAL_BLOCKING_STATES, ...QUEUE_POSITIONAL_BLOCKING_STATES];
481
+ Object.keys({
482
+ /**
483
+ * A PERSON stopped the plan (PRO-1144). Distinct from `planning_failed`
484
+ * because the two leave the initiative in the same state and mean opposite
485
+ * things: one is the agent falling over, this is somebody deciding.
486
+ *
487
+ * Halting is still right -- nothing should restart behind the back of a
488
+ * person who just stopped it -- but calling their decision a failure is not.
489
+ * Same wording as the initiative's own cancel copy (PRO-1071), so the two
490
+ * surfaces describe one event the same way.
491
+ */
492
+ planning_cancelled: {
493
+ message: "Planning was cancelled, so the queue stopped here.",
494
+ params: [],
495
+ clearedWhen: "planning is retried, or a re-plan replaces it",
496
+ exits: [
497
+ "auto",
498
+ "reorder",
499
+ "remove"
500
+ ]
501
+ },
502
+ /** The plan the entry needs could not be produced. */
503
+ planning_failed: {
504
+ message: "Planning failed for this initiative, so the queue stopped here.",
505
+ params: [],
506
+ clearedWhen: "the initiative leaves planningStatus \"failed\" (a retry or a re-plan)",
507
+ exits: [
508
+ "auto",
509
+ "reorder",
510
+ "remove"
511
+ ]
512
+ },
513
+ /**
514
+ * Nobody was running the plan: the worker was lost (PRO-1040, PRO-1521).
515
+ *
516
+ * The queue's wording for a plan that failed with cause `worker_lost` --
517
+ * reaped on a dead heartbeat, or found at 'planning' with no live job by the
518
+ * reconciler, which fails it with that cause on the tick that notices
519
+ * (`armPlanning` writes the status and creates the job in ONE transaction,
520
+ * so that shape is never a race). One of three halts that mirror
521
+ * `PLAN_FAILURE_CAUSES` one-to-one; see `PLAN_FAILURE_HALT`.
522
+ */
523
+ planning_stalled: {
524
+ message: "Planning stopped without finishing, so the queue stopped here.",
525
+ params: [],
526
+ clearedWhen: "planning is retried, or a re-plan replaces it",
527
+ exits: [
528
+ "auto",
529
+ "reorder",
530
+ "remove"
531
+ ]
532
+ },
533
+ /** The run errored out. */
534
+ run_failed: {
535
+ message: "The run failed, so the queue stopped here.",
536
+ params: [],
537
+ clearedWhen: "the run is retried (back to in-flight) or replaced by a re-plan",
538
+ exits: [
539
+ "auto",
540
+ "reorder",
541
+ "remove"
542
+ ]
543
+ },
544
+ /**
545
+ * A person stopped the run. Previously invisible: the halt check listed
546
+ * `failed` and `closed` by hand and silently missed this, so a stopped run
547
+ * left its entry active forever with no reason shown.
548
+ */
549
+ run_stopped: {
550
+ message: "The run was stopped, so the queue stopped here.",
551
+ params: [],
552
+ clearedWhen: "the run is resumed (back to in-flight) or replaced by a re-plan",
553
+ exits: [
554
+ "auto",
555
+ "reorder",
556
+ "remove"
557
+ ]
558
+ },
559
+ /**
560
+ * The run reached `completed` with nothing that could merge.
561
+ *
562
+ * `completed` carries two meanings -- merged, and "the review loop converged
563
+ * with nothing left to push". The queue advances on MERGES, so treating the
564
+ * second as success planned the next entry against a main that never got
565
+ * this one, which is why the cause exists.
566
+ *
567
+ * It is NARROWER than that now (PRO-1123). The second meaning is itself two
568
+ * states, and an open pull request waiting on a reviewer is the ordinary
569
+ * handoff, not a fault: `isAwaitingMerge` routes it away from here and the
570
+ * queue leaves that entry active. What still reaches this cause is a run that
571
+ * finished with no pull request at all, or one already closed.
572
+ */
573
+ run_unmerged: {
574
+ message: "The run finished without a pull request to merge, so the queue stopped here.",
575
+ params: [],
576
+ clearedWhen: "a pull request appears and merges, or a re-plan replaces the run",
577
+ exits: [
578
+ "auto",
579
+ "reorder",
580
+ "remove"
581
+ ]
582
+ },
583
+ /** The pull request was closed without merging. */
584
+ pr_closed: {
585
+ message: "The pull request was closed without merging, so the queue stopped here.",
586
+ params: [],
587
+ clearedWhen: "a re-plan replaces the abandoned run",
588
+ exits: [
589
+ "auto",
590
+ "reorder",
591
+ "remove"
592
+ ]
593
+ },
594
+ /**
595
+ * The project asked the queue to STOP at the plan (PRO-1295), and it has.
596
+ *
597
+ * Distinct from `armed_start_missing`, which is the same observable state --
598
+ * planned, unarmed, no run -- reached by accident rather than on purpose.
599
+ * Telling a person "the run never started" when they configured exactly that
600
+ * describes their own decision as a fault, and the two need different copy
601
+ * for the same reason `planning_cancelled` is not `planning_failed`.
602
+ *
603
+ * `auto` is honest here: this clears the moment a run appears, which is what
604
+ * pressing Run after reading the plan does. No grace period either -- the
605
+ * accidental case waits five minutes in case a run is merely slow to be
606
+ * created, but a deliberate stop has nothing to wait for.
607
+ */
608
+ awaiting_plan_review: {
609
+ message: "The plan is ready and waiting for you to read it, so the queue stopped here.",
610
+ params: [],
611
+ clearedWhen: "the run starts, which is what reading the plan and pressing Run does",
612
+ exits: [
613
+ "auto",
614
+ "reorder",
615
+ "remove"
616
+ ]
617
+ },
618
+ /**
619
+ * Planning finished and armed, but the run never appeared. Work that went
620
+ * wrong is every other cause; this is work that never began, and without it
621
+ * the entry waits forever for a merge that has no pull request to come from.
622
+ */
623
+ armed_start_missing: {
624
+ message: "Planning finished but the run never started, so the queue stopped here.",
625
+ params: [],
626
+ clearedWhen: "a run appears for the plan, or a re-plan re-arms it",
627
+ exits: [
628
+ "auto",
629
+ "reorder",
630
+ "remove"
631
+ ]
632
+ },
633
+ /**
634
+ * Activation was refused, and the refusal is a decision a person owes: an
635
+ * unsettled dependency, an unchosen UI direction, an agent already on the
636
+ * item. Deliberately NOT auto-clearing. Re-deriving it every tick would flip
637
+ * the entry queued/halted forever and log a refusal each time, which is how
638
+ * the codebase already ruled on this before the causes were data.
639
+ */
640
+ activation_refused: {
641
+ message: "{message}",
642
+ params: ["message"],
643
+ clearedWhen: "never on its own; the refusal names something a person has to settle",
644
+ exits: [
645
+ "resume",
646
+ "reorder",
647
+ "remove"
648
+ ]
649
+ },
650
+ /**
651
+ * The person who queued it was erased, so there is no principal to run as.
652
+ * Fail closed rather than run as somebody who did not ask (PRO-949), and
653
+ * there is nothing to observe that would change that.
654
+ */
655
+ queued_by_erased: {
656
+ message: "The person who queued this is no longer in the organisation, so it was not started.",
657
+ params: [],
658
+ clearedWhen: "never on its own; there is no principal left to run as",
659
+ exits: ["remove"]
660
+ }
661
+ }).filter((c) => c !== "awaiting_plan_review" && c !== "armed_start_missing");
662
+ //#endregion
663
+ //#region ../../packages/types/dist/build.js
664
+ const BUILD_RUN_STATUSES = [
665
+ "pending",
666
+ "cloning",
667
+ "implementing",
668
+ "hardening",
669
+ "pushing",
670
+ "ci_checking",
671
+ "ci_fixing",
672
+ "needs_review",
673
+ "reviewing",
674
+ "completed",
675
+ "failed",
676
+ "stopped",
677
+ "closed"
678
+ ];
679
+ /**
680
+ * Status -> disposition, as a `satisfies`-checked const map (rather than a
681
+ * switch) so the assignment is exhaustive at compile time AND the narrow
682
+ * per-status literal types survive for type-level derivation below
683
+ * (BuildRunActiveStatus / BuildRunTerminalStatus).
684
+ */
685
+ const RUN_DISPOSITION = {
686
+ pending: "in_flight",
687
+ cloning: "in_flight",
688
+ implementing: "in_flight",
689
+ hardening: "in_flight",
690
+ pushing: "in_flight",
691
+ ci_checking: "in_flight",
692
+ ci_fixing: "in_flight",
693
+ needs_review: "gated",
694
+ reviewing: "in_flight",
695
+ completed: "succeeded",
696
+ failed: "failed",
697
+ stopped: "stopped",
698
+ closed: "rejected"
699
+ };
700
+ /** How the run stands. Exhaustive via the RUN_DISPOSITION map. */
701
+ function runDisposition(status) {
702
+ return RUN_DISPOSITION[status];
703
+ }
704
+ /**
705
+ * Statuses where the coding agent (or review worker) is actively executing.
706
+ * Single source of truth shared by the tenant build feed's `active=true` filter
707
+ * (api) and the dashboard's "running" detection (web), which must agree.
708
+ * = in_flight minus 'pending' (queued is not yet executing).
709
+ */
710
+ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) === "in_flight" && s !== "pending");
711
+ BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
712
+ [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
713
+ [...TURN_EVENT_TRIGGERS];
714
+ //#endregion
715
+ //#region ../../packages/types/dist/maintenance.js
716
+ /**
717
+ * The kinds whose remediation is a CODE change rather than a version bump
718
+ * (PRO-1384).
719
+ *
720
+ * Shared rather than duplicated per side, because the two sides disagreeing is
721
+ * a silent dead feature: the API decides whether a remediation is ACCEPTED and
722
+ * the web decides whether the button is OFFERED, and when this split lived only
723
+ * in the API the client kept gating on `recommendedVersion` -- which a code
724
+ * finding never has -- so the endpoint was reachable and unreachable at the
725
+ * same time.
726
+ *
727
+ * A dependency is fixed by moving to a published version, so the absence of one
728
+ * is a real blocker. A code finding has no version by construction.
729
+ */
730
+ const CODE_REMEDIATION_KINDS = [
731
+ "secret",
732
+ "sast",
733
+ "iac_misconfig"
734
+ ];
735
+ new Map(Object.entries({
736
+ deps: ["vulnerable_dependency", "outdated_dependency"],
737
+ secrets: ["secret"],
738
+ sast: ["sast"],
739
+ iac: ["iac_misconfig"]
740
+ }).flatMap(([scanner, kinds]) => kinds.map((kind) => [kind, scanner])));
741
+ new Set(CODE_REMEDIATION_KINDS);
742
+ //#endregion
743
+ //#region ../../packages/types/dist/design-tokens.js
744
+ /**
745
+ * The brand grounding contract (PRO-929).
746
+ *
747
+ * A tenant's brand can come from more than one place -- the internal
748
+ * `design_standards` document authored in Design Standards, or a linked
749
+ * design-system repository, or (later) Figma Variables. Whichever source is
750
+ * authoritative, every consumer downstream wants the same thing: the resolved
751
+ * CSS custom properties the sandboxed artifact runtime reads.
752
+ *
753
+ * This module holds that vocabulary so the producers and the consumer cannot
754
+ * drift. The web token bridge maps the internal document's Material-3-flavoured
755
+ * keys onto these names; the API's repository reader lifts them straight out of
756
+ * a design system's own theme files (a shadcn-convention design system already
757
+ * speaks this vocabulary, so there is nothing to translate). Both must aim at
758
+ * one list, which is why the list lives here rather than beside either of them
759
+ * -- the same single-source rule PRO-847 applied to standing directions.
760
+ */
761
+ /**
762
+ * The CSS custom properties the artifact runtime consumes.
763
+ *
764
+ * Deliberately the shadcn convention rather than a Taiga invention: it is what
765
+ * the sandboxed runtime's Tailwind bridge already reads, and what a generated
766
+ * application's own theme will use, so a concept previewed in these variables
767
+ * is previewed in the same names the built product will carry.
768
+ */
769
+ const ARTIFACT_CSS_VARIABLES = [
770
+ "--background",
771
+ "--foreground",
772
+ "--card",
773
+ "--card-foreground",
774
+ "--popover",
775
+ "--popover-foreground",
776
+ "--primary",
777
+ "--primary-foreground",
778
+ "--primary-hover",
779
+ "--primary-hover-foreground",
780
+ "--secondary",
781
+ "--secondary-foreground",
782
+ "--muted",
783
+ "--muted-foreground",
784
+ "--accent",
785
+ "--accent-foreground",
786
+ "--destructive",
787
+ "--destructive-foreground",
788
+ "--success",
789
+ "--success-foreground",
790
+ "--warning",
791
+ "--warning-foreground",
792
+ "--border",
793
+ "--input",
794
+ "--ring",
795
+ "--radius"
796
+ ];
797
+ /** What the artifact document carries them as. */
798
+ const ARTIFACT_SCALE_FRAME_NAMES = [
799
+ {
800
+ source: "--spacing",
801
+ frame: "--ds-spacing",
802
+ theme: "--spacing"
803
+ },
804
+ {
805
+ source: "--font-mono",
806
+ frame: "--ds-font-mono",
807
+ theme: "--font-mono"
808
+ },
809
+ {
810
+ source: "--text-xs",
811
+ frame: "--ds-text-xs",
812
+ theme: "--text-xs"
813
+ },
814
+ {
815
+ source: "--text-sm",
816
+ frame: "--ds-text-sm",
817
+ theme: "--text-sm"
818
+ },
819
+ {
820
+ source: "--text-base",
821
+ frame: "--ds-text-base",
822
+ theme: "--text-base"
823
+ },
824
+ {
825
+ source: "--text-lg",
826
+ frame: "--ds-text-lg",
827
+ theme: "--text-lg"
828
+ },
829
+ {
830
+ source: "--text-xl",
831
+ frame: "--ds-text-xl",
832
+ theme: "--text-xl"
833
+ },
834
+ {
835
+ source: "--text-2xl",
836
+ frame: "--ds-text-2xl",
837
+ theme: "--text-2xl"
838
+ },
839
+ {
840
+ source: "--text-3xl",
841
+ frame: "--ds-text-3xl",
842
+ theme: "--text-3xl"
843
+ },
844
+ {
845
+ source: "--radius-sm",
846
+ frame: "--ds-radius-sm",
847
+ theme: "--radius-sm"
848
+ },
849
+ {
850
+ source: "--radius-md",
851
+ frame: "--ds-radius-md",
852
+ theme: "--radius-md"
853
+ },
854
+ {
855
+ source: "--radius-lg",
856
+ frame: "--ds-radius-lg",
857
+ theme: "--radius-lg"
858
+ },
859
+ {
860
+ source: "--radius-xl",
861
+ frame: "--ds-radius-xl",
862
+ theme: "--radius-xl"
863
+ },
864
+ {
865
+ source: "--radius-2xl",
866
+ frame: "--ds-radius-2xl",
867
+ theme: "--radius-2xl"
868
+ },
869
+ {
870
+ source: "--ease-out",
871
+ frame: "--ds-ease-out",
872
+ theme: "--ease-out"
873
+ },
874
+ {
875
+ source: "--ease-spring",
876
+ frame: "--ds-ease-spring",
877
+ theme: "--ease-spring"
878
+ },
879
+ {
880
+ source: "--default-transition-duration",
881
+ frame: "--ds-duration",
882
+ theme: "--default-transition-duration"
883
+ },
884
+ {
885
+ source: "--duration-normal",
886
+ frame: "--ds-duration-entrance",
887
+ theme: null
888
+ }
889
+ ].map((token) => token.frame);
890
+ [...ARTIFACT_CSS_VARIABLES, ...ARTIFACT_SCALE_FRAME_NAMES];
891
+ new Set(ARTIFACT_CSS_VARIABLES.filter((name) => name !== "--radius"));
892
+ Object.freeze({
893
+ productKinds: true,
894
+ applicationOwnership: true
895
+ });
896
+ //#endregion
897
+ //#region ../../packages/types/dist/document-types-for-kind.js
898
+ /**
899
+ * What a thing with no users of its own does not have.
900
+ *
901
+ * A library, an infrastructure module and a pipeline share one fact: their
902
+ * consumers are other products, not people. That is what decides all four
903
+ * exclusions, and it is why the three kinds share a list rather than each
904
+ * having their own -- three identical lists are three chances to change one.
905
+ *
906
+ * - `user_flow`: a journey through screens. There are no screens and there
907
+ * is no journey; the ux-planning agent asked for one invents both.
908
+ * - `privacy_assessment`: a GDPR Article 35 DPIA. Its sections are the legal
909
+ * basis for processing, the data subjects affected, retention, how each
910
+ * data-subject right is served, and an approval decision. A module has no
911
+ * data subjects and is not a controller, so every one of those sections is
912
+ * written from nothing. (A library that DOES touch personal data is still
913
+ * covered: the data flow below is kept, and the threat model reads it.)
914
+ * - `service_blueprint`: frontstage and backstage of a SERVICE, divided by a
915
+ * line of visibility that only exists where a customer is looking.
916
+ * - `ui_direction`: the look and the navigation. A Terraform module offered
917
+ * a look-and-feel wizard is the clearest single symptom of PRO-1866.
918
+ *
919
+ * The `data_flow` is deliberately KEPT, against the ticket's shorter list.
920
+ * Two reasons, and both are about the documents that remain rather than about
921
+ * the data flow itself. The threat model is STRIDE over trust boundaries and
922
+ * data stores, and the data flow is where those come from -- dropping it
923
+ * leaves the threat model and the risk register standing but ungrounded, which
924
+ * is a worse outcome than a document nobody wanted. And a data flow is not an
925
+ * application concept: a module's state file, provider credentials and remote
926
+ * backend cross trust boundaries, a pipeline moves artifacts and secrets
927
+ * between them, and a library receives and returns data it does not own.
928
+ *
929
+ * The technology decisions, architecture, threat model and risk register are
930
+ * kept for every kind. Each describes the thing itself, and a consumer of a
931
+ * library has more need of them, not less.
932
+ */
933
+ const NO_USERS_OF_ITS_OWN = [
934
+ "user_flow",
935
+ "privacy_assessment",
936
+ "service_blueprint",
937
+ "ui_direction"
938
+ ];
939
+ Object.freeze({
940
+ /** The kind every product was before the column existed. Nothing is excluded. */
941
+ application: [],
942
+ /** Code other products depend on, shipped as a published package. */
943
+ library: NO_USERS_OF_ITS_OWN,
944
+ /** A Terraform module, a Helm chart: consumed by other products' infrastructure. */
945
+ infrastructure_module: NO_USERS_OF_ITS_OWN,
946
+ /** A reusable workflow, an action, a build template. */
947
+ pipeline: NO_USERS_OF_ITS_OWN
948
+ });
949
+ Object.freeze({
950
+ user_flow: {
951
+ required: ["specification"],
952
+ optional: []
953
+ },
954
+ architecture: {
955
+ required: ["specification"],
956
+ optional: ["user_flow"]
957
+ },
958
+ technology_decisions: {
959
+ required: ["specification", "architecture"],
960
+ optional: []
961
+ },
962
+ data_flow: {
963
+ required: ["specification", "architecture"],
964
+ optional: []
965
+ },
966
+ privacy_assessment: {
967
+ required: ["specification", "architecture"],
968
+ optional: ["data_flow"]
969
+ },
970
+ threat_model: {
971
+ required: [
972
+ "specification",
973
+ "architecture",
974
+ "privacy_assessment"
975
+ ],
976
+ optional: ["data_flow"]
977
+ },
978
+ risk_register: {
979
+ required: ["specification"],
980
+ optional: [
981
+ "privacy_assessment",
982
+ "threat_model",
983
+ "technology_decisions",
984
+ "architecture"
985
+ ]
986
+ },
987
+ service_blueprint: {
988
+ required: [
989
+ "specification",
990
+ "user_flow",
991
+ "architecture"
992
+ ],
993
+ optional: ["ui_direction"]
994
+ }
995
+ });
996
+ //#endregion
997
+ //#region src/tools/catalogue.ts
998
+ /**
999
+ * What exists, from the one place that knows: the shared types package.
1000
+ *
1001
+ * Tool descriptions are what the model reads to decide what to call, and a
1002
+ * description that names a document type the catalogue lacks sends it down a
1003
+ * dead end (the walk-through that led here found `policy_acceptable_use` and
1004
+ * `policy_ip` named in a description and nowhere else). Every list of types,
1005
+ * origins and statuses a tool shows is derived from these constants, and a
1006
+ * test holds the descriptions to them.
1007
+ */
1008
+ /**
1009
+ * Display names for every product document type. The reviewable nine come
1010
+ * from the shared map the web uses; the three the web never asks a person to
1011
+ * review are named here, and the record type keeps the two sets complete.
1012
+ */
1013
+ const PRODUCT_DOCUMENT_NAMES = {
1014
+ ...PRODUCT_DOCUMENT_DISPLAY_NAMES,
1015
+ ui_direction: "Look & Feel",
1016
+ codebase_snapshot: "Codebase Snapshot",
1017
+ product_instructions: "Product Instructions"
1018
+ };
1019
+ function productDocumentName(type) {
1020
+ return PRODUCT_DOCUMENT_NAMES[type] ?? type;
1021
+ }
1022
+ /** The organization documents that are not policies: goals, standards, voice, instructions. */
1023
+ const OTHER_TENANT_DOCUMENT_TYPES = TENANT_DOCUMENT_TYPES.filter((type) => !POLICY_TYPES.includes(type));
1024
+ /** `a, b, c` for a description. */
1025
+ function named(types) {
1026
+ return types.join(", ");
1027
+ }
1028
+ /**
1029
+ * The two document timelines of a product (ADR-017), in the words the tools
1030
+ * and the server instructions use. One place, so the list tool, the read
1031
+ * tool and the instructions cannot explain them three different ways.
1032
+ */
1033
+ const ORIGIN_MEANING = {
1034
+ plan: "plan: the discovery documents, authored intent. Every greenfield product has them; they say what is meant to be built.",
1035
+ 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."
1036
+ };
1037
+ /**
1038
+ * Which timeline is a product's own (the API's documentOriginForSettings):
1039
+ * an imported product has no plan chain by design, so its documents are on
1040
+ * the reality side; everything else starts on the plan side. Derived from the
1041
+ * same settings key the API reads, so the two cannot disagree.
1042
+ */
1043
+ function isImportedProduct(product) {
1044
+ const settings = product?.settings;
1045
+ return typeof settings === "object" && settings !== null && settings.projectType === "brownfield";
1046
+ }
1047
+ /**
1048
+ * The document types no derivation ever writes: a person records or writes
1049
+ * them, so they live on the plan side of every product, imported or not.
1050
+ */
1051
+ const PLAN_ONLY_DOCUMENT_TYPES = PRODUCT_DOCUMENT_TYPES.filter((type) => !REALITY_DERIVED_DOCUMENT_TYPES.includes(type));
1052
+ /**
1053
+ * The timeline a read should default to: the plan side for a type only a
1054
+ * person writes, else the product's own side (reality for an imported
1055
+ * product, plan for every other).
1056
+ */
1057
+ function defaultOriginFor(product, type) {
1058
+ if (type !== void 0 && PLAN_ONLY_DOCUMENT_TYPES.includes(type)) return "plan";
1059
+ return isImportedProduct(product) ? "reality" : "plan";
1060
+ }
1061
+ /**
1062
+ * Where an imported product's import got to (`settings.importStatus`, written
1063
+ * by the API: pending, analyzing, complete, cancelled, failed). Unknown values
1064
+ * read as "not known", never as a promise.
1065
+ */
1066
+ function importStatusOf(product) {
1067
+ const settings = product?.settings;
1068
+ if (typeof settings !== "object" || settings === null) return null;
1069
+ const status = settings.importStatus;
1070
+ return typeof status === "string" ? status : null;
1071
+ }
1072
+ /** Why a side of the timeline is empty, for a list or a miss that shows one. */
1073
+ function emptyOriginNote(origin, product) {
1074
+ const imported = isImportedProduct(product);
1075
+ 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.";
1076
+ 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.";
1077
+ switch (importStatusOf(product)) {
1078
+ case "pending": return "No reality documents yet: the import has not started. Link the repository and run the import in Taiga.";
1079
+ case "analyzing":
1080
+ case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
1081
+ case "cancelled": return "No reality documents: the import was cancelled before it derived any. Run it again in Taiga.";
1082
+ case "failed": return "No reality documents: the import failed before it derived any. Run it again in Taiga.";
1083
+ default: return "No reality documents: the import derives them from the code, and none have been published.";
1084
+ }
1085
+ }
1086
+ //#endregion
216
1087
  //#region src/tools/errors.ts
217
1088
  /** A successful tool result: one JSON text block the model reads. */
218
1089
  function toolResult(value) {
@@ -263,17 +1134,6 @@ async function runTool(handler) {
263
1134
  }
264
1135
  }
265
1136
  //#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
1137
  //#region src/tools/remote.ts
278
1138
  const SCP_LIKE = /^(?:[^@\s/]+@)?([^:\s/]+):([^\s]+)$/;
279
1139
  function parseRemote(remote) {
@@ -335,37 +1195,63 @@ function webOrigin(url) {
335
1195
  }
336
1196
  }
337
1197
  //#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
- ];
1198
+ //#region src/tools/sections.ts
1199
+ function isRecord(value) {
1200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1201
+ }
1202
+ /** The `sections` map of a document, or null when the document is not section-shaped. */
1203
+ function sectionsOf(data) {
1204
+ return isRecord(data.sections) ? data.sections : null;
1205
+ }
1206
+ /** The text of one section, or null when the section does not exist. */
1207
+ function sectionContent(sections, sectionId) {
1208
+ const section = sections[sectionId];
1209
+ if (section === void 0 || section === null) return null;
1210
+ if (typeof section === "string") return section;
1211
+ return isRecord(section) && typeof section.content === "string" ? section.content : "";
1212
+ }
1213
+ /** The map with one section's text replaced (or the section added); every other section untouched. */
1214
+ function applySectionEdit(sections, sectionId, content) {
1215
+ const prior = sections[sectionId];
1216
+ return {
1217
+ ...sections,
1218
+ [sectionId]: isRecord(prior) ? {
1219
+ ...prior,
1220
+ content
1221
+ } : { content }
1222
+ };
1223
+ }
354
1224
  /**
355
- * Remove heavy content fields from each item in an array.
356
- * Preserves all metadata (id, name, description, type, dates, etc).
1225
+ * A document for reading, without its provenance: the top-level `derivedFrom`
1226
+ * tree and `outdated` source list (PRO-2359), and each section's own
1227
+ * `derivedFrom` (PRO-2158). They say which documents shaped this one, which
1228
+ * the editor needs and a reader does not; `source` and `priority` stay, the
1229
+ * server instructions explain them.
357
1230
  */
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
- });
1231
+ function stripProvenance(doc) {
1232
+ const { derivedFrom: _derivedFrom, outdated: _outdated, ...rest } = doc;
1233
+ if (!isRecord(rest.data)) return rest;
1234
+ const sections = sectionsOf(rest.data);
1235
+ if (!sections) return rest;
1236
+ const stripped = Object.fromEntries(Object.entries(sections).map(([id, section]) => {
1237
+ if (!isRecord(section)) return [id, section];
1238
+ const { derivedFrom: _sectionDerivedFrom, ...keep } = section;
1239
+ return [id, keep];
1240
+ }));
1241
+ return {
1242
+ ...rest,
1243
+ data: {
1244
+ ...rest.data,
1245
+ sections: stripped
1246
+ }
1247
+ };
364
1248
  }
1249
+ //#endregion
1250
+ //#region src/tools/summarize.ts
365
1251
  /**
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.
1252
+ * Keep only the named fields of each item. List tools return summaries (the
1253
+ * fields a model needs to pick an item) and get tools return everything;
1254
+ * this is how a list drops the prose and the provenance trees.
369
1255
  */
370
1256
  function pickFields(items, fields) {
371
1257
  return items.map((item) => {
@@ -386,6 +1272,21 @@ const PRODUCT_SUMMARY_FIELDS = [
386
1272
  "kind",
387
1273
  "discoveryStep"
388
1274
  ];
1275
+ /**
1276
+ * What a document row needs to say for the model to pick one and read it in
1277
+ * full: which document, on which timeline, how current. Not the derived-from
1278
+ * tree, the data key count or the reviewer's name.
1279
+ */
1280
+ const DOCUMENT_SUMMARY_FIELDS$1 = [
1281
+ "type",
1282
+ "origin",
1283
+ "version",
1284
+ "isDraft",
1285
+ "hasDraft",
1286
+ "stage",
1287
+ "reviewedAt",
1288
+ "updatedAt"
1289
+ ];
389
1290
  /** The API's page ceiling (paginationSchema in apps/api). */
390
1291
  const PAGE_SIZE = 100;
391
1292
  /**
@@ -394,6 +1295,8 @@ const PAGE_SIZE = 100;
394
1295
  * matches is not a resolution the model can act on anyway.
395
1296
  */
396
1297
  const RESOLVE_PAGE_LIMIT = 10;
1298
+ const productId = z.string().uuid().describe("Product ID -- get from taiga_resolve_product (from a git remote) or taiga_list_products");
1299
+ 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
1300
  /**
398
1301
  * Every product linked to the repository, page by page, until the API runs
399
1302
  * out or the cap is hit. A page that fails fails the call, like every other
@@ -421,6 +1324,82 @@ async function linkedProducts(client, repository, repositoryHost) {
421
1324
  truncated: true
422
1325
  };
423
1326
  }
1327
+ /** A document list row as the model sees it: the summary fields plus a display name and the staleness fact. */
1328
+ function documentSummaries(rows) {
1329
+ const typed = rows;
1330
+ return pickFields(typed, DOCUMENT_SUMMARY_FIELDS$1).map((picked, index) => {
1331
+ const outdated = typed[index].outdated;
1332
+ const fact = picked.isDraft !== false ? void 0 : outdated === null ? false : Array.isArray(outdated?.sources) ? outdated.sources.length > 0 : void 0;
1333
+ return {
1334
+ type: picked.type,
1335
+ name: productDocumentName(String(picked.type)),
1336
+ ...picked,
1337
+ ...fact === void 0 ? {} : { outdated: fact }
1338
+ };
1339
+ });
1340
+ }
1341
+ /** The one sentence for a side the API would not list, with what it said. */
1342
+ function unlistableNote(origin, reason) {
1343
+ return `The ${origin} side could not be listed (${reason instanceof TaigaApiError ? `${reason.status} ${reason.code ?? ""}`.trim() : "the request failed"}).`;
1344
+ }
1345
+ /**
1346
+ * Both timelines of a product's documents, each a list of summaries, with
1347
+ * which one is the product's own and a note for an empty side. The product
1348
+ * decides what an empty side means (imported or not, how far its import got),
1349
+ * so a caller that already holds it passes it in.
1350
+ */
1351
+ async function documentTimelines(client, id, product) {
1352
+ const loaded = product ?? (await client.getProduct(id)).data;
1353
+ const settled = await Promise.allSettled(PRODUCT_DOCUMENT_ORIGINS.map((o) => client.listProductDocuments(id, {
1354
+ origin: o,
1355
+ limit: PAGE_SIZE
1356
+ })));
1357
+ if (settled.every((result) => result.status === "rejected")) throw settled[0].reason;
1358
+ const sides = {};
1359
+ const unlistable = {};
1360
+ PRODUCT_DOCUMENT_ORIGINS.forEach((o, index) => {
1361
+ const result = settled[index];
1362
+ if (result.status === "fulfilled") sides[o] = documentSummaries(result.value.data);
1363
+ else {
1364
+ sides[o] = null;
1365
+ unlistable[o] = unlistableNote(o, result.reason);
1366
+ }
1367
+ });
1368
+ const notes = PRODUCT_DOCUMENT_ORIGINS.flatMap((o) => {
1369
+ const side = sides[o];
1370
+ if (!side) return [unlistable[o]];
1371
+ return side.length === 0 ? [emptyOriginNote(o, loaded)] : [];
1372
+ });
1373
+ return {
1374
+ projectType: isImportedProduct(loaded) ? "brownfield" : "greenfield",
1375
+ defaultOrigin: defaultOriginFor(loaded),
1376
+ plan: sides.plan ?? null,
1377
+ reality: sides.reality ?? null,
1378
+ unlistable,
1379
+ notes
1380
+ };
1381
+ }
1382
+ const isPublished = (row) => row.isDraft === false;
1383
+ /** `specification (v2), architecture (draft)` for one side. */
1384
+ function sideList(rows) {
1385
+ return rows.map((row) => `${String(row.type)} (${isPublished(row) ? `v${String(row.version)}` : "draft"})`).join(", ");
1386
+ }
1387
+ /** What a missed document read should say: where the type does live, and what each side holds. */
1388
+ function describeMiss(type, wanted, timelines, product) {
1389
+ const name = productDocumentName(type);
1390
+ const other = wanted === "plan" ? "reality" : "plan";
1391
+ const parts = [`No published ${wanted} ${name} (${type}) for this product.`];
1392
+ const rows = (side) => timelines[side] ?? [];
1393
+ if (rows(other).some((row) => row.type === type && isPublished(row))) parts.push(`It is published on the ${other} side: call again with origin '${other}'.`);
1394
+ 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.`);
1395
+ for (const o of PRODUCT_DOCUMENT_ORIGINS) {
1396
+ const side = timelines[o];
1397
+ const label = `${o[0].toUpperCase()}${o.slice(1)} documents`;
1398
+ if (side === null) parts.push(timelines.unlistable[o]);
1399
+ else parts.push(side.length > 0 ? `${label}: ${sideList(side)}.` : emptyOriginNote(o, product));
1400
+ }
1401
+ return parts.join(" ");
1402
+ }
424
1403
  function registerProductTools(server, client) {
425
1404
  if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
426
1405
  title: "List factories",
@@ -433,14 +1412,10 @@ function registerProductTools(server, client) {
433
1412
  if (client.hasCapability("products:read")) {
434
1413
  server.registerTool("taiga_list_products", {
435
1414
  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).",
1415
+ 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
1416
  inputSchema: {
438
1417
  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."),
1418
+ 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
1419
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)")
445
1420
  },
446
1421
  annotations: READ_ONLY
@@ -449,11 +1424,22 @@ function registerProductTools(server, client) {
449
1424
  }));
450
1425
  server.registerTool("taiga_get_product", {
451
1426
  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") },
1427
+ 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.",
1428
+ inputSchema: {
1429
+ productId: productId.optional(),
1430
+ 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")
1431
+ },
454
1432
  annotations: READ_ONLY
455
1433
  }, (params) => runTool(async () => {
456
- return toolResult((await client.getProduct(params.productId)).data);
1434
+ if (params.productId && params.slug) return toolError("VALIDATION_FAILED", "Pass productId or slug, not both: they could name two products.");
1435
+ if (params.productId) return toolResult((await client.getProduct(params.productId)).data);
1436
+ if (!params.slug) return toolError("VALIDATION_FAILED", "Pass productId or slug.");
1437
+ const [product] = (await client.listProducts({
1438
+ slug: params.slug,
1439
+ limit: 1
1440
+ })).data;
1441
+ if (!product) return toolError("NOT_FOUND", `No product you can see has the slug "${params.slug}". taiga_list_products shows the ones you can.`);
1442
+ return toolResult(product);
457
1443
  }));
458
1444
  server.registerTool("taiga_resolve_product", {
459
1445
  title: "Resolve product from git remote",
@@ -474,57 +1460,61 @@ function registerProductTools(server, client) {
474
1460
  }));
475
1461
  server.registerTool("taiga_list_product_documents", {
476
1462
  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
- },
1463
+ 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.`,
1464
+ inputSchema: { productId },
482
1465
  annotations: READ_ONLY
483
1466
  }, (params) => runTool(async () => {
484
- return toolResult(summarizeList((await client.listProductDocuments(params.productId, { limit: params.limit })).data));
1467
+ const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
1468
+ return toolResult(timelines);
485
1469
  }));
486
1470
  server.registerTool("taiga_get_product_document", {
487
1471
  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).",
1472
+ 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
1473
  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")
1474
+ productId,
1475
+ type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
1476
+ origin: originInput
492
1477
  },
493
1478
  annotations: READ_ONLY
494
1479
  }, (params) => runTool(async () => {
495
- return toolResult((await client.getProductDocument(params.productId, params.type)).data);
1480
+ let product;
1481
+ if (!params.origin) product = (await client.getProduct(params.productId)).data;
1482
+ const origin = params.origin ?? defaultOriginFor(product, params.type);
1483
+ try {
1484
+ return toolResult(stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data));
1485
+ } catch (error) {
1486
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1487
+ product ??= (await client.getProduct(params.productId)).data;
1488
+ const timelines = await documentTimelines(client, params.productId, product);
1489
+ return toolError("NOT_FOUND", describeMiss(params.type, origin, timelines, product));
1490
+ }
496
1491
  }));
497
1492
  }
498
1493
  }
499
1494
  //#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 : "";
1495
+ //#region src/tools/documents.ts
1496
+ /**
1497
+ * What a document row needs to say for the model to pick one and read it in
1498
+ * full. Not the content, not who wrote it.
1499
+ */
1500
+ const DOCUMENT_SUMMARY_FIELDS = [
1501
+ "type",
1502
+ "name",
1503
+ "description",
1504
+ "version",
1505
+ "isDraft",
1506
+ "hasDraft",
1507
+ "updatedAt"
1508
+ ];
1509
+ const summaries = (rows) => pickFields(rows, DOCUMENT_SUMMARY_FIELDS);
1510
+ /** What a lookup behind a miss said when it failed, so an unknown stays unknown. */
1511
+ function failedLookup(what, reason) {
1512
+ return `${what} could not be checked (${reason instanceof TaigaApiError ? `${reason.status} ${reason.code ?? ""}`.trim() : "the request failed"}).`;
514
1513
  }
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
- };
1514
+ /** `a (v3), b (v1)` for a not-found message. */
1515
+ function publishedList(rows) {
1516
+ return rows.filter((row) => row.isDraft === false).map((row) => `${row.type} (v${String(row.version)})`).join(", ");
525
1517
  }
526
- //#endregion
527
- //#region src/tools/documents.ts
528
1518
  const DRAFT_WRITE = {
529
1519
  readOnlyHint: false,
530
1520
  destructiveHint: false,
@@ -539,7 +1529,7 @@ const PUBLISH = {
539
1529
  };
540
1530
  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
1531
  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");
1532
+ const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named(POLICY_TYPES)}) or ${named(OTHER_TENANT_DOCUMENT_TYPES)}`);
543
1533
  /** Everything the model needs back from a write, without the content it already holds. */
544
1534
  function documentSummary(doc) {
545
1535
  const { data, ...rest } = doc;
@@ -592,19 +1582,33 @@ function registerDocumentTools(server, client) {
592
1582
  if (client.hasCapability("tenant:documents:read")) {
593
1583
  server.registerTool("taiga_list_documents", {
594
1584
  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.",
1585
+ 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
1586
  inputSchema: { limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)") },
597
1587
  annotations: READ_ONLY
598
1588
  }, (params) => runTool(async () => {
599
- return toolResult(summarizeList((await client.listTenantDocuments(params)).data));
1589
+ const result = await client.listTenantDocuments(params);
1590
+ return toolResult(summaries(result.data));
600
1591
  }));
601
1592
  server.registerTool("taiga_get_document", {
602
1593
  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") },
1594
+ 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.",
1595
+ inputSchema: { type: tenantDocumentType },
605
1596
  annotations: READ_ONLY
606
1597
  }, (params) => runTool(async () => {
607
- return toolResult((await client.getTenantDocument(params.type)).data);
1598
+ try {
1599
+ return toolResult(stripProvenance((await client.getTenantDocument(params.type)).data));
1600
+ } catch (error) {
1601
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1602
+ const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
1603
+ const parts = [`No published ${params.type}.`];
1604
+ 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.");
1605
+ else parts.push(failedLookup("The draft", draft.reason));
1606
+ if (all.status === "fulfilled") {
1607
+ const published = publishedList(all.value.data);
1608
+ parts.push(published ? `Published documents: ${published}.` : "Nothing is published yet.");
1609
+ } else parts.push(failedLookup("The published list", all.reason));
1610
+ return toolError("NOT_FOUND", parts.join(" "));
1611
+ }
608
1612
  }));
609
1613
  }
610
1614
  if (client.hasCapability("tenant:documents:read") && client.hasCapability("tenant:documents:write")) {
@@ -634,25 +1638,36 @@ function registerDocumentTools(server, client) {
634
1638
  if (client.hasCapability("factories:read")) {
635
1639
  server.registerTool("taiga_list_factory_documents", {
636
1640
  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.",
1641
+ 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
1642
  inputSchema: {
639
1643
  factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
640
1644
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)")
641
1645
  },
642
1646
  annotations: READ_ONLY
643
1647
  }, (params) => runTool(async () => {
644
- return toolResult(summarizeList((await client.listFactoryDocuments(params.factoryId, { limit: params.limit })).data));
1648
+ const result = await client.listFactoryDocuments(params.factoryId, { limit: params.limit });
1649
+ return toolResult(summaries(result.data));
645
1650
  }));
646
1651
  server.registerTool("taiga_get_factory_document", {
647
1652
  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.",
1653
+ 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
1654
  inputSchema: {
650
1655
  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`.")
1656
+ type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
652
1657
  },
653
1658
  annotations: READ_ONLY
654
1659
  }, (params) => runTool(async () => {
655
- return toolResult((await client.getFactoryDocument(params.factoryId, params.type)).data);
1660
+ try {
1661
+ return toolResult(stripProvenance((await client.getFactoryDocument(params.factoryId, params.type)).data));
1662
+ } catch (error) {
1663
+ if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1664
+ await client.getFactory(params.factoryId);
1665
+ 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) => {
1666
+ if (reason instanceof TaigaApiError && reason.status === 404) throw reason;
1667
+ return failedLookup("The draft", reason);
1668
+ });
1669
+ return toolError("NOT_FOUND", `This factory has no published ${params.type} yet. ${detail}`);
1670
+ }
656
1671
  }));
657
1672
  }
658
1673
  if (client.hasCapability("factories:read") && client.hasCapability("factories:documents:write")) {
@@ -663,7 +1678,7 @@ function registerDocumentTools(server, client) {
663
1678
  publish: () => client.publishFactoryDocument(factoryId, type)
664
1679
  });
665
1680
  const factoryId = z.string().uuid().describe("Factory ID -- get from taiga_list_factories");
666
- const factoryDocumentType = documentType.describe("Document type (`factory_instructions`)");
1681
+ const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
667
1682
  server.registerTool("taiga_update_factory_document", {
668
1683
  title: "Update a section of factory instructions",
669
1684
  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 +1827,69 @@ function registerAllTools(server, client) {
812
1827
  registerBuildTools(server, client);
813
1828
  }
814
1829
  //#endregion
1830
+ //#region src/tools/instructions.ts
1831
+ /**
1832
+ * What the server tells a client at connect time: the knowledge base, the
1833
+ * order to read it in, the two document timelines, what section metadata
1834
+ * means, and when to write. Kept as data so a test can hold it to the
1835
+ * catalogue the same way it holds the tool descriptions.
1836
+ */
1837
+ const SERVER_INSTRUCTIONS = [
1838
+ "You have access to Taiga, a policy-aware software development platform: the organization's",
1839
+ "engineering knowledge base, its products, their backlog and what the coding agent has built.",
1840
+ "",
1841
+ "**Company documents** (taiga_list_documents / taiga_get_document):",
1842
+ `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
1843
+ `architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
1844
+ "carry the business goals, design and brand standards, tone of voice and standing instructions.",
1845
+ "Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
1846
+ "",
1847
+ "**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
1848
+ "A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
1849
+ "tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
1850
+ "builds software. A factory that has not written any yet has none; that is normal.",
1851
+ "",
1852
+ "**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
1853
+ `${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
1854
+ "A product has two timelines of these, and the list shows both:",
1855
+ `- ${ORIGIN_MEANING.plan}`,
1856
+ `- ${ORIGIN_MEANING.reality}`,
1857
+ "An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
1858
+ "reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
1859
+ "side of every product. Omit origin on a read and the server picks the right side (the list reports the",
1860
+ "product's own as defaultOrigin). To understand",
1861
+ "what to build, read the plan where there is one; to understand the code you are changing, read reality",
1862
+ "when it exists, the ground truth for architecture, data flow and threats as built.",
1863
+ "",
1864
+ "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
1865
+ "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
1866
+ "for them: runs, steps, commits and pull requests.",
1867
+ "",
1868
+ "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
1869
+ "provenance fields you can read past: `source` says where the text originally came from (template:",
1870
+ "Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>:",
1871
+ "rendered for the organization's cloud providers; any other tag names the derivation that wrote the",
1872
+ "text; a hand edit keeps the source it had) and",
1873
+ "`priority: core` marks the sections every organization keeps. Neither changes what the section",
1874
+ "requires of you.",
1875
+ "",
1876
+ "Workflow: inside a git checkout, start with taiga_resolve_product on the remote URL to find the product.",
1877
+ "Otherwise use taiga_list_factories and taiga_list_products. Then read the factory instructions,",
1878
+ "the product's specification and architecture, and the backlog item you are working on. A document",
1879
+ "read that finds nothing tells you what does exist and which tool reads or writes it.",
1880
+ "",
1881
+ "When you notice a policy or factory instructions are outdated, fix the section with",
1882
+ "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
1883
+ "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
1884
+ "This keeps the knowledge base accurate for everyone."
1885
+ ].join("\n");
1886
+ //#endregion
815
1887
  //#region src/index.ts
816
1888
  async function main() {
817
1889
  const config = readConfig();
818
1890
  if (!config) {
819
1891
  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");
1892
+ console.error("Create one at https://eu.tai.ga under Settings > Account > API keys.");
821
1893
  process.exit(1);
822
1894
  }
823
1895
  const client = new TaigaClient({
@@ -833,35 +1905,7 @@ async function main() {
833
1905
  const server = new McpServer({
834
1906
  name: "taiga",
835
1907
  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") });
1908
+ }, { instructions: SERVER_INSTRUCTIONS });
865
1909
  registerAllTools(server, client);
866
1910
  const toolCount = introspection.capabilities.length;
867
1911
  console.error(`Taiga MCP server started (${toolCount} capabilities, user: ${introspection.email}, role: ${introspection.role})`);