@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 +1151 -128
- package/package.json +1 -1
- package/src/client.test.ts +19 -0
- package/src/client.ts +11 -3
- package/src/config.test.ts +1 -1
- package/src/config.ts +4 -1
- package/src/index.ts +3 -30
- package/src/server.integration.test.ts +644 -9
- package/src/tools/catalogue.test.ts +109 -0
- package/src/tools/catalogue.ts +165 -0
- package/src/tools/documents.ts +115 -35
- package/src/tools/instructions.ts +65 -0
- package/src/tools/products.ts +234 -43
- package/src/tools/sections.test.ts +34 -1
- package/src/tools/sections.ts +23 -1
- package/src/tools/summarize.ts +3 -34
- package/taiga-ai-mcp-server-0.7.0-dev.395.tgz +0 -0
- package/src/tools/inputs.test.ts +0 -19
- package/src/tools/inputs.ts +0 -13
- package/taiga-ai-mcp-server-0.6.0-rc.393.tgz +0 -0
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
|
-
|
|
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.
|
|
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/
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
|
|
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
|
-
*
|
|
356
|
-
*
|
|
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
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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.
|
|
367
|
-
*
|
|
368
|
-
*
|
|
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.
|
|
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: {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
491
|
-
type:
|
|
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
|
-
|
|
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/
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
/**
|
|
516
|
-
function
|
|
517
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
604
|
-
inputSchema: { type:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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})`);
|