@bridge_gpt/mcp-server 0.1.17 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +333 -197
- package/build/agent-capabilities/cli.js +152 -0
- package/build/agent-capabilities/default-deps.js +45 -0
- package/build/agent-capabilities/probe-context.js +111 -0
- package/build/agent-capabilities/probes.js +278 -0
- package/build/agent-capabilities/reporter.js +50 -0
- package/build/agent-capabilities/runner.js +56 -0
- package/build/agent-capabilities/types.js +10 -0
- package/build/agent-launchers/claude.js +4 -4
- package/build/agents.generated.js +1 -1
- package/build/brainstorm-files.js +89 -0
- package/build/bridge-config.js +404 -0
- package/build/chain-orchestrator.js +247 -33
- package/build/commands.generated.js +5 -5
- package/build/credential-materialization.js +128 -0
- package/build/credential-store.js +232 -0
- package/build/decision-page-schema.js +39 -6
- package/build/decision-page-template.js +54 -18
- package/build/doctor.js +18 -2
- package/build/git-ignore-utils.js +63 -0
- package/build/index.js +1510 -560
- package/build/mcp-invoke.js +417 -0
- package/build/mcp-provisioning.js +249 -0
- package/build/mcp-registration-doctor.js +96 -0
- package/build/pipeline-orchestrator.js +9 -1
- package/build/pipeline-utils.js +33 -0
- package/build/pipelines.generated.js +36 -5
- package/build/schedule-run.js +6 -6
- package/build/start-tickets-prereqs.js +90 -1
- package/build/start-tickets.js +106 -14
- package/build/third-party-mcp-targets.js +75 -0
- package/build/version.generated.js +1 -1
- package/package.json +3 -3
- package/pipelines/full-automation.json +3 -1
- package/pipelines/implement-ticket.json +28 -2
- package/smoke-test/SMOKE-TEST.md +4 -2
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* - Stage 3 (start-tickets) is special-cased: the orchestrator does NOT call
|
|
16
16
|
* ``runPipeline``; it emits an ``agent_task`` envelope naming the exact
|
|
17
17
|
* ``/start-tickets ...`` command for the host agent to invoke in-session.
|
|
18
|
-
* - Cross-stage variables (
|
|
19
|
-
* reviewed_ticket_keys -> start-tickets) are resolved server-side.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* - Cross-stage variables (child_ticket_keys -> review fan-out ->
|
|
19
|
+
* reviewed_ticket_keys -> start-tickets) are resolved server-side. The
|
|
20
|
+
* idea-to-ticket stage emits a structured payload that keeps the Epic
|
|
21
|
+
* ``epic_parent_key`` separate from the implementable ``child_ticket_keys``,
|
|
22
|
+
* so the Epic parent is handed to NEITHER the review fan-out NOR
|
|
23
|
+
* start-tickets. Missing cross-stage variables produce a typed
|
|
24
|
+
* ``VALIDATION`` failure — never a placeholder value.
|
|
22
25
|
*/
|
|
23
26
|
import { runPipeline, resumePipeline, peekPipelineRun } from "./pipeline-orchestrator.js";
|
|
24
27
|
import { toStringVariable, substituteChainValue } from "./chain-utils.js";
|
|
@@ -174,6 +177,13 @@ const JIRA_KEY_EXACT = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
|
174
177
|
* keys merely *mentioned* by an earlier step (e.g. duplicate-detection search
|
|
175
178
|
* hits) can never be mistaken for this run's creations. A string / no-field
|
|
176
179
|
* input returns `[]`, which the caller treats as fail-closed.
|
|
180
|
+
*
|
|
181
|
+
* NOTE: This flat helper FLATTENS parent (`epic_key`) and `child_ticket_keys`
|
|
182
|
+
* into a single list. The full-automation chain fan-out must NOT use it — it
|
|
183
|
+
* would fold the Epic parent into the implementable list and hand the parent to
|
|
184
|
+
* review / start-tickets. The chain drives fan-out from the structured
|
|
185
|
+
* `extractCreatedTicketPayload` path instead; this helper remains only for
|
|
186
|
+
* non-chain compatibility callers that expect a flat single-ticket list.
|
|
177
187
|
*/
|
|
178
188
|
export function extractCreatedTicketKeys(source) {
|
|
179
189
|
const preferredKeys = [
|
|
@@ -267,24 +277,140 @@ function tryParseJson(raw) {
|
|
|
267
277
|
return undefined;
|
|
268
278
|
}
|
|
269
279
|
}
|
|
280
|
+
function isValidJiraKey(value) {
|
|
281
|
+
return typeof value === "string" && JIRA_KEY_EXACT.test(value);
|
|
282
|
+
}
|
|
270
283
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
284
|
+
* Normalize a PARSED, STRUCTURED upload payload into a {@link CreatedTicketPayload}.
|
|
285
|
+
* Accepts only a parsed JSON object — never a string, array, null, number, or
|
|
286
|
+
* free text — so Jira keys merely mentioned in prose can never become
|
|
287
|
+
* review/start-tickets targets. Two authoritative shapes are recognized:
|
|
288
|
+
*
|
|
289
|
+
* - Epic: ``{ epic_parent_key, child_ticket_keys }`` — ``epic_parent_key`` only
|
|
290
|
+
* (the legacy ``epic_key`` alias is rejected for a clean break); at least one
|
|
291
|
+
* valid child key required; the parent must not appear in the children.
|
|
292
|
+
* - Single-ticket: ``{ created_ticket_keys: [key] }`` — exactly one valid key,
|
|
293
|
+
* normalized into ``child_ticket_keys`` (with ``created_ticket_keys`` kept as
|
|
294
|
+
* a compatibility alias). A multi-key flat ``created_ticket_keys`` WITHOUT
|
|
295
|
+
* ``epic_parent_key`` is treated as malformed rather than guessing the first
|
|
296
|
+
* key is the parent.
|
|
297
|
+
*
|
|
298
|
+
* Returns ``{ ok: false, error }`` on any malformed shape so the caller can fail
|
|
299
|
+
* the chain with a clear validation message.
|
|
278
300
|
*/
|
|
279
|
-
export function
|
|
301
|
+
export function extractCreatedTicketPayload(source) {
|
|
302
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
error: "Upload payload must be a structured JSON object; the chain never " +
|
|
306
|
+
"harvests Jira keys from free text, arrays, null, or numbers.",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const obj = source;
|
|
310
|
+
const hasEpicParent = "epic_parent_key" in obj;
|
|
311
|
+
const hasLegacyEpicKey = "epic_key" in obj;
|
|
312
|
+
const hasCreated = "created_ticket_keys" in obj;
|
|
313
|
+
// Epic path — driven exclusively by the explicit `epic_parent_key` field.
|
|
314
|
+
if (hasEpicParent) {
|
|
315
|
+
const parent = obj.epic_parent_key;
|
|
316
|
+
if (!isValidJiraKey(parent)) {
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
error: `Epic payload "epic_parent_key" must be a valid Jira key; got ${JSON.stringify(parent)}.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const rawChildren = obj.child_ticket_keys;
|
|
323
|
+
if (!Array.isArray(rawChildren) || rawChildren.length === 0) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
error: 'Epic payload requires "child_ticket_keys" with at least one ' +
|
|
327
|
+
"implementable child ticket key.",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const children = [];
|
|
331
|
+
for (const c of rawChildren) {
|
|
332
|
+
if (!isValidJiraKey(c)) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
error: `Epic payload "child_ticket_keys" must contain only valid implementable Jira keys; got ${JSON.stringify(c)}.`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (c === parent) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
error: `Epic parent ${parent} must not appear in "child_ticket_keys" (parent leakage).`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
children.push(c);
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, payload: { epic_parent_key: parent, child_ticket_keys: children } };
|
|
347
|
+
}
|
|
348
|
+
// Reject the legacy `epic_key` alias explicitly: the chain accepts only
|
|
349
|
+
// `epic_parent_key` for the Epic parent (clean break, no alias recognition).
|
|
350
|
+
if (hasLegacyEpicKey) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
error: 'Epic payload must use "epic_parent_key"; the legacy "epic_key" alias ' +
|
|
354
|
+
"is not accepted by the chain.",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Single-ticket path — exactly one implementable key, normalized to children.
|
|
358
|
+
if (hasCreated) {
|
|
359
|
+
const raw = obj.created_ticket_keys;
|
|
360
|
+
if (!Array.isArray(raw) || raw.length !== 1 || !isValidJiraKey(raw[0])) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
error: 'Single-ticket payload "created_ticket_keys" must contain exactly ' +
|
|
364
|
+
"one valid implementable Jira key. A multi-key list is malformed " +
|
|
365
|
+
'(the legacy "first key is the Epic parent" convention is not accepted).',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const key = raw[0];
|
|
369
|
+
return { ok: true, payload: { child_ticket_keys: [key], created_ticket_keys: [key] } };
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
error: 'Upload payload has no recognized "epic_parent_key"/"child_ticket_keys" ' +
|
|
374
|
+
'or single-ticket "created_ticket_keys" fields.',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Derive the structured {@link CreatedTicketPayload} created by a completed
|
|
379
|
+
* idea-to-ticket child run AUTHORITATIVELY from the upload step's structured
|
|
380
|
+
* output — never by scanning the whole transcript. The upload step
|
|
381
|
+
* (`upload-and-track.md`) is the LAST step of the idea-to-ticket pipeline and the
|
|
382
|
+
* chain never passes `skip_steps`, so it is the final entry of `results`. Its
|
|
383
|
+
* `result` is parsed (with `parseStructuredAgentResult` when it is a string) and
|
|
384
|
+
* passed to {@link extractCreatedTicketPayload}. Returns a validation error if
|
|
385
|
+
* the payload is absent or malformed, so dedup-search hits from earlier steps can
|
|
386
|
+
* never become targets.
|
|
387
|
+
*/
|
|
388
|
+
export function extractCreatedTicketPayloadFromCompletedChild(childEnv) {
|
|
280
389
|
const results = childEnv.results;
|
|
281
|
-
if (!Array.isArray(results) || results.length === 0)
|
|
282
|
-
return
|
|
390
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
error: "idea-to-ticket child produced no results; cannot extract an upload payload.",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
283
396
|
const uploadResult = results[results.length - 1]?.result;
|
|
284
397
|
const payload = typeof uploadResult === "string"
|
|
285
398
|
? parseStructuredAgentResult(uploadResult)
|
|
286
399
|
: uploadResult;
|
|
287
|
-
return
|
|
400
|
+
return extractCreatedTicketPayload(payload);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Compatibility wrapper over {@link extractCreatedTicketPayloadFromCompletedChild}
|
|
404
|
+
* that returns ONLY the normalized implementable `child_ticket_keys` (never the
|
|
405
|
+
* Epic parent) on success, and `[]` on any structured-extraction failure — so
|
|
406
|
+
* legacy callers keep their flat-array contract without ever guessing
|
|
407
|
+
* parent/child roles.
|
|
408
|
+
*/
|
|
409
|
+
export function extractCreatedKeysFromCompletedChild(childEnv) {
|
|
410
|
+
const extraction = extractCreatedTicketPayloadFromCompletedChild(childEnv);
|
|
411
|
+
if (!extraction.ok)
|
|
412
|
+
return [];
|
|
413
|
+
return extraction.payload.child_ticket_keys;
|
|
288
414
|
}
|
|
289
415
|
/**
|
|
290
416
|
* True when ``token`` appears in a failed child envelope's failure-text fields:
|
|
@@ -305,12 +431,29 @@ function failedEnvelopeMentions(env, token) {
|
|
|
305
431
|
}
|
|
306
432
|
return false;
|
|
307
433
|
}
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Heuristic, no-LLM one-line stage summary. idea-to-ticket passes a structured
|
|
436
|
+
* {@link CreatedTicketPayload} so the summary distinguishes the Epic parent from
|
|
437
|
+
* implementable children; review / start-tickets pass a flat `string[]` of keys
|
|
438
|
+
* (wording preserved).
|
|
439
|
+
*/
|
|
440
|
+
export function summarizeStageCompletion(pipelineName, keysOrPayload) {
|
|
311
441
|
if (pipelineName === "idea-to-ticket") {
|
|
312
|
-
|
|
442
|
+
if (!Array.isArray(keysOrPayload)) {
|
|
443
|
+
const payload = keysOrPayload;
|
|
444
|
+
const children = payload.child_ticket_keys.join(", ");
|
|
445
|
+
if (payload.epic_parent_key) {
|
|
446
|
+
return `Created Epic ${payload.epic_parent_key} and implementable children: ${children}`;
|
|
447
|
+
}
|
|
448
|
+
return `Created ticket: ${children}`;
|
|
449
|
+
}
|
|
450
|
+
// Backward-compatible flat-array form.
|
|
451
|
+
return `Created tickets: ${keysOrPayload.join(", ")}`;
|
|
313
452
|
}
|
|
453
|
+
const keys = Array.isArray(keysOrPayload)
|
|
454
|
+
? keysOrPayload
|
|
455
|
+
: keysOrPayload.child_ticket_keys;
|
|
456
|
+
const joined = keys.join(", ");
|
|
314
457
|
if (pipelineName === "review-ticket") {
|
|
315
458
|
return `Reviewed tickets: ${joined}`;
|
|
316
459
|
}
|
|
@@ -504,7 +647,12 @@ function buildStageVariables(stage, args, extra = {}) {
|
|
|
504
647
|
}
|
|
505
648
|
return resolved;
|
|
506
649
|
}
|
|
507
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* Resolve a fan-out / cross-stage input list from prior stage outputs. This
|
|
652
|
+
* helper is intentionally KEY-SPECIFIC: it never searches alternate field names,
|
|
653
|
+
* so a recipe asking for `child_ticket_keys` will not silently fall back to
|
|
654
|
+
* `created_ticket_keys`.
|
|
655
|
+
*/
|
|
508
656
|
function resolveCrossStageList(row, stageIndex, key) {
|
|
509
657
|
for (let i = stageIndex - 1; i >= 0; i--) {
|
|
510
658
|
const outputs = row.stages[i]?.outputs;
|
|
@@ -514,6 +662,50 @@ function resolveCrossStageList(row, stageIndex, key) {
|
|
|
514
662
|
}
|
|
515
663
|
return null;
|
|
516
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Scan stage outputs STRICTLY BEFORE ``stageIndex`` and return the first valid
|
|
667
|
+
* string ``epic_parent_key``. Non-string / empty values are ignored. Used as a
|
|
668
|
+
* second-layer guard so the Epic parent can be filtered out of the start-tickets
|
|
669
|
+
* handoff even if an upstream persisted stage output is malformed.
|
|
670
|
+
*/
|
|
671
|
+
export function resolvePriorEpicParentKey(row, stageIndex) {
|
|
672
|
+
for (let i = 0; i < stageIndex; i++) {
|
|
673
|
+
const candidate = row.stages[i]?.outputs?.epic_parent_key;
|
|
674
|
+
if (typeof candidate === "string" && candidate.trim() !== "") {
|
|
675
|
+
return candidate;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Resolve the start-tickets stage's configured fan-out input and defensively
|
|
682
|
+
* filter out any key equal to the prior ``epic_parent_key``. Fails closed (a
|
|
683
|
+
* validation-oriented message) when the configured input is missing/empty, or
|
|
684
|
+
* when excluding the Epic parent leaves no implementable keys. This is the
|
|
685
|
+
* belt-and-suspenders second layer; the primary fix excludes the parent before
|
|
686
|
+
* review via the structured extractor.
|
|
687
|
+
*/
|
|
688
|
+
export function resolveStartTicketKeys(row, stageIndex, fanOutInput) {
|
|
689
|
+
const configured = resolveCrossStageList(row, stageIndex, fanOutInput);
|
|
690
|
+
if (!configured || configured.length === 0) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
error: `Missing cross-stage variable "${fanOutInput}" for start-tickets.`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const epicParent = resolvePriorEpicParentKey(row, stageIndex);
|
|
697
|
+
const filtered = epicParent
|
|
698
|
+
? configured.filter((k) => k !== epicParent)
|
|
699
|
+
: configured;
|
|
700
|
+
if (filtered.length === 0) {
|
|
701
|
+
return {
|
|
702
|
+
ok: false,
|
|
703
|
+
error: `No implementable tickets remain for start-tickets after excluding the ` +
|
|
704
|
+
`Epic parent ${epicParent}.`,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return { ok: true, keys: filtered };
|
|
708
|
+
}
|
|
517
709
|
// ---------------------------------------------------------------------------
|
|
518
710
|
// Child-pipeline envelope handling
|
|
519
711
|
// ---------------------------------------------------------------------------
|
|
@@ -633,22 +825,37 @@ async function handleChildPipelineEnvelope(persistence, recipe, row, childEnv, o
|
|
|
633
825
|
}
|
|
634
826
|
return { kind: "advanced", row: updated };
|
|
635
827
|
}
|
|
636
|
-
// Single-child stage (idea-to-ticket): finalize. Derive
|
|
637
|
-
// the upload step's structured
|
|
638
|
-
// duplicate-detection search hits cannot become review/start-tickets
|
|
639
|
-
|
|
640
|
-
|
|
828
|
+
// Single-child stage (idea-to-ticket): finalize. Derive the STRUCTURED payload
|
|
829
|
+
// ONLY from the upload step's structured output — never the whole transcript —
|
|
830
|
+
// so duplicate-detection search hits cannot become review/start-tickets
|
|
831
|
+
// targets. The Epic parent (`epic_parent_key`) is kept SEPARATE from the
|
|
832
|
+
// implementable `child_ticket_keys` so it is handed to NEITHER review NOR
|
|
833
|
+
// start-tickets. `created_ticket_keys` is persisted as an implementable-only
|
|
834
|
+
// compatibility alias (= children) and never carries the Epic parent.
|
|
835
|
+
const extraction = extractCreatedTicketPayloadFromCompletedChild(childEnv);
|
|
836
|
+
if (!extraction.ok) {
|
|
837
|
+
return {
|
|
838
|
+
kind: "fail",
|
|
839
|
+
envelope: await failChainValidation(persistence, recipe, row, extraction.error),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const payload = extraction.payload;
|
|
843
|
+
if (payload.child_ticket_keys.length === 0) {
|
|
641
844
|
return {
|
|
642
845
|
kind: "fail",
|
|
643
|
-
envelope: await failChainValidation(persistence, recipe, row, "idea-to-ticket produced no
|
|
846
|
+
envelope: await failChainValidation(persistence, recipe, row, "idea-to-ticket produced no implementable child tickets; full automation cannot continue without implementable tickets."),
|
|
644
847
|
};
|
|
645
848
|
}
|
|
646
849
|
const stages = cloneStages(row.stages);
|
|
647
850
|
const stage = stages[idx];
|
|
648
851
|
stage.status = "completed";
|
|
649
852
|
stage.pipeline_run_id = null;
|
|
650
|
-
stage.outputs = {
|
|
651
|
-
|
|
853
|
+
stage.outputs = {
|
|
854
|
+
child_ticket_keys: payload.child_ticket_keys,
|
|
855
|
+
created_ticket_keys: payload.child_ticket_keys,
|
|
856
|
+
...(payload.epic_parent_key ? { epic_parent_key: payload.epic_parent_key } : {}),
|
|
857
|
+
};
|
|
858
|
+
stage.summary = summarizeStageCompletion(stageRecipe.pipeline_name, payload);
|
|
652
859
|
let updated;
|
|
653
860
|
try {
|
|
654
861
|
updated = await persistence.patchRun(row.chain_run_id, {
|
|
@@ -815,14 +1022,18 @@ async function startStartTicketsStage(persistence, recipe, row) {
|
|
|
815
1022
|
const stageRecipe = recipe.stages[idx];
|
|
816
1023
|
const total = recipe.stages.length;
|
|
817
1024
|
const fanOutInput = stageRecipe.fan_out_input ?? "reviewed_ticket_keys";
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1025
|
+
// Second-layer guard: filter the Epic parent out of the start keys even if an
|
|
1026
|
+
// upstream persisted stage output is malformed (the primary exclusion happens
|
|
1027
|
+
// before review via the structured extractor).
|
|
1028
|
+
const resolution = resolveStartTicketKeys(row, idx, fanOutInput);
|
|
1029
|
+
if (!resolution.ok) {
|
|
1030
|
+
return failChainValidation(persistence, recipe, row, resolution.error);
|
|
821
1031
|
}
|
|
1032
|
+
const keys = resolution.keys;
|
|
822
1033
|
// Hands-off chain runs (auto_approve=true, the default) spawn the
|
|
823
1034
|
// implementation agents auto-approved too, so they don't stall on approval
|
|
824
1035
|
// gates. A --require-approval run (auto_approve=false) spawns them interactive.
|
|
825
|
-
const autoApproveFlag = row.args.auto_approve === true ? " --auto
|
|
1036
|
+
const autoApproveFlag = row.args.auto_approve === true ? " --auto" : "";
|
|
826
1037
|
const command = `/start-tickets ${keys.join(" ")}${autoApproveFlag}`;
|
|
827
1038
|
const stages = cloneStages(row.stages);
|
|
828
1039
|
stages[idx].status = "paused";
|
|
@@ -1018,7 +1229,10 @@ export async function resumeFullAutomation(deps, input) {
|
|
|
1018
1229
|
chain_total: total,
|
|
1019
1230
|
});
|
|
1020
1231
|
}
|
|
1021
|
-
|
|
1232
|
+
// Persist started_ticket_keys WITHOUT the Epic parent, using the same
|
|
1233
|
+
// defensive filter as startStartTicketsStage.
|
|
1234
|
+
const startResolution = resolveStartTicketKeys(row, idx, stageRecipe.fan_out_input ?? "reviewed_ticket_keys");
|
|
1235
|
+
const startedKeys = startResolution.ok ? startResolution.keys : [];
|
|
1022
1236
|
const stages = cloneStages(row.stages);
|
|
1023
1237
|
stages[idx].status = "completed";
|
|
1024
1238
|
stages[idx].pipeline_run_id = null;
|