@bridge_gpt/mcp-server 0.1.16 → 0.1.17
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 +36 -1
- package/build/agent-launchers/claude.js +85 -0
- package/build/agent-launchers/index.js +17 -0
- package/build/agent-launchers/types.js +1 -0
- package/build/chain-orchestrator.js +1150 -0
- package/build/chain-utils.js +68 -0
- package/build/commands.generated.js +3 -1
- package/build/fetch-stub.js +139 -0
- package/build/index.js +137 -10
- package/build/pipeline-orchestrator.js +57 -0
- package/build/pipelines.generated.js +132 -3
- package/build/schedule-run.js +951 -0
- package/build/schedule-store.js +132 -0
- package/build/scheduler-backends/at-fallback.js +144 -0
- package/build/scheduler-backends/escaping.js +113 -0
- package/build/scheduler-backends/index.js +72 -0
- package/build/scheduler-backends/launchd.js +216 -0
- package/build/scheduler-backends/systemd-user.js +237 -0
- package/build/scheduler-backends/task-scheduler.js +219 -0
- package/build/scheduler-backends/types.js +23 -0
- package/build/start-tickets.js +119 -59
- package/build/version.generated.js +1 -1
- package/package.json +7 -7
- package/pipelines/full-automation.json +47 -0
- package/pipelines/idea-to-ticket.json +71 -0
- package/smoke-test/SMOKE-TEST.md +509 -0
- package/smoke-test/smoke-test-mcp.md +23 -0
package/build/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { execSync } from "child_process";
|
|
18
18
|
import { createRequire } from "module";
|
|
19
|
-
import { PIPELINES as BUNDLED_PIPELINES, INSTRUCTIONS as BUNDLED_INSTRUCTIONS } from "./pipelines.generated.js";
|
|
19
|
+
import { PIPELINES as BUNDLED_PIPELINES, INSTRUCTIONS as BUNDLED_INSTRUCTIONS, CHAIN_RECIPES } from "./pipelines.generated.js";
|
|
20
20
|
import { COMMANDS } from "./commands.generated.js";
|
|
21
21
|
import { AGENTS } from "./agents.generated.js";
|
|
22
22
|
import { VERSION } from "./version.generated.js";
|
|
@@ -25,9 +25,11 @@ import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils
|
|
|
25
25
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
26
26
|
import { runStartTicketsCli } from "./start-tickets.js";
|
|
27
27
|
import { runDoctorCli } from "./doctor.js";
|
|
28
|
+
import { runScheduleRunCli } from "./schedule-run.js";
|
|
28
29
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
29
30
|
import { DecisionPageInputShape } from "./decision-page-schema.js";
|
|
30
|
-
import { runPipeline, resumePipeline, listPipelineRuns, deletePipelineRun, } from "./pipeline-orchestrator.js";
|
|
31
|
+
import { runPipeline, resumePipeline, listPipelineRuns, deletePipelineRun, deriveIdeaHash, } from "./pipeline-orchestrator.js";
|
|
32
|
+
import { runFullAutomation, resumeFullAutomation, } from "./chain-orchestrator.js";
|
|
31
33
|
// Mutable pipeline/instruction state — starts with bundled, merged with user at startup
|
|
32
34
|
const PIPELINES = { ...BUNDLED_PIPELINES };
|
|
33
35
|
const INSTRUCTIONS = { ...BUNDLED_INSTRUCTIONS };
|
|
@@ -127,6 +129,8 @@ async function createTicketRequest(params) {
|
|
|
127
129
|
payload.labels = params.labels;
|
|
128
130
|
if (params.assignee)
|
|
129
131
|
payload.assignee = params.assignee;
|
|
132
|
+
if (params.parent_key)
|
|
133
|
+
payload.parent_key = params.parent_key;
|
|
130
134
|
const resp = await fetch(buildUrl("/create-ticket"), {
|
|
131
135
|
method: "POST",
|
|
132
136
|
headers: POST_HEADERS,
|
|
@@ -704,6 +708,12 @@ async function dispatchCliSubcommand(argv) {
|
|
|
704
708
|
if (argv[0] === "doctor") {
|
|
705
709
|
return runDoctorCli(argv.slice(1));
|
|
706
710
|
}
|
|
711
|
+
// The local-only `schedule-run` subcommand (BAPI-327) is likewise a positional
|
|
712
|
+
// subcommand that owns the rest of argv, routed before the --init / --upgrade
|
|
713
|
+
// flag guards and never starts the MCP server.
|
|
714
|
+
if (argv[0] === "schedule-run") {
|
|
715
|
+
return runScheduleRunCli(argv.slice(1));
|
|
716
|
+
}
|
|
707
717
|
// --init takes precedence over --upgrade; both are position-independent flags.
|
|
708
718
|
if (argv.includes("--init")) {
|
|
709
719
|
return runInitCli(cwd);
|
|
@@ -854,7 +864,7 @@ registerTool("get_project_standards", {
|
|
|
854
864
|
});
|
|
855
865
|
registerTool("get_tickets", {
|
|
856
866
|
description: "Search for and list Jira tickets from the configured project. " +
|
|
857
|
-
"Filters by query text, status name, or date. Returns up to 'limit' tickets ordered by most recently updated. " +
|
|
867
|
+
"Filters by query text, status name, label, or date. Returns up to 'limit' tickets ordered by most recently updated. " +
|
|
858
868
|
"All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",
|
|
859
869
|
inputSchema: {
|
|
860
870
|
query: z
|
|
@@ -867,6 +877,12 @@ registerTool("get_tickets", {
|
|
|
867
877
|
.string()
|
|
868
878
|
.optional()
|
|
869
879
|
.describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),
|
|
880
|
+
labels: z
|
|
881
|
+
.string()
|
|
882
|
+
.optional()
|
|
883
|
+
.describe("Comma-separated Jira labels. Filters tickets via JQL labels in (...) " +
|
|
884
|
+
"(matches tickets carrying any of the given labels). Labels cannot contain spaces. " +
|
|
885
|
+
"Example: \"bapi-idea-to-ticket-fa-1a2b3c\""),
|
|
870
886
|
limit: z
|
|
871
887
|
.number()
|
|
872
888
|
.optional()
|
|
@@ -882,12 +898,14 @@ registerTool("get_tickets", {
|
|
|
882
898
|
.optional()
|
|
883
899
|
.describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date"),
|
|
884
900
|
},
|
|
885
|
-
}, async ({ query, status, limit, offset, updated_since }) => {
|
|
901
|
+
}, async ({ query, status, labels, limit, offset, updated_since }) => {
|
|
886
902
|
const params = { repo_name: REPO_NAME };
|
|
887
903
|
if (query)
|
|
888
904
|
params.query = query;
|
|
889
905
|
if (status)
|
|
890
906
|
params.status = status;
|
|
907
|
+
if (labels)
|
|
908
|
+
params.labels = labels;
|
|
891
909
|
if (limit !== undefined)
|
|
892
910
|
params.limit = String(limit);
|
|
893
911
|
if (offset !== undefined && offset > 0)
|
|
@@ -936,7 +954,8 @@ registerTool("create_ticket", {
|
|
|
936
954
|
description: "Create a new Jira ticket in the configured project. Requires either description or file_path (or both — file_path takes precedence). " +
|
|
937
955
|
"Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. " +
|
|
938
956
|
"The ticket is created immediately in Jira — confirm details with the user before calling. " +
|
|
939
|
-
"The description field supports Jira markdown formatting."
|
|
957
|
+
"The description field supports Jira markdown formatting. " +
|
|
958
|
+
"Pass parent_key ONLY when creating a child ticket under an existing Jira Epic; omit it for standalone tickets and for Epic parent creation itself.",
|
|
940
959
|
inputSchema: {
|
|
941
960
|
summary: z.string().describe("Ticket title — keep under 100 characters"),
|
|
942
961
|
description: z
|
|
@@ -966,12 +985,17 @@ registerTool("create_ticket", {
|
|
|
966
985
|
.string()
|
|
967
986
|
.optional()
|
|
968
987
|
.describe("Jira username or account ID of the assignee. Omit to leave unassigned"),
|
|
988
|
+
parent_key: z
|
|
989
|
+
.string()
|
|
990
|
+
.optional()
|
|
991
|
+
.describe("Optional Jira Epic key to set as the parent of the newly created child issue. " +
|
|
992
|
+
"Omit for standalone tickets and Epic parent creation."),
|
|
969
993
|
},
|
|
970
|
-
}, async ({ summary, description, file_path, issue_type, priority, labels, assignee }) => {
|
|
994
|
+
}, async ({ summary, description, file_path, issue_type, priority, labels, assignee, parent_key }) => {
|
|
971
995
|
const resolved = await resolveTextOrFile(description, file_path, "description");
|
|
972
996
|
if (!resolved.ok)
|
|
973
997
|
return resolved.errorResponse;
|
|
974
|
-
const text = await createTicketRequest({ summary, description: resolved.text, issue_type, priority, labels, assignee });
|
|
998
|
+
const text = await createTicketRequest({ summary, description: resolved.text, issue_type, priority, labels, assignee, parent_key });
|
|
975
999
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
976
1000
|
});
|
|
977
1001
|
registerTool("get_plan", {
|
|
@@ -2044,6 +2068,7 @@ const VALID_CONFIG_FIELDS = [
|
|
|
2044
2068
|
"post_pr_target_status", "ci_check_config", "ci_followup_config",
|
|
2045
2069
|
"allow_mutating_smoke_ops",
|
|
2046
2070
|
"selected_mcp_slugs",
|
|
2071
|
+
"base_branch",
|
|
2047
2072
|
].join(", ");
|
|
2048
2073
|
registerTool("list_config_fields", {
|
|
2049
2074
|
description: "List all configurable fields available for reading and updating via the Bridge API. " +
|
|
@@ -2094,6 +2119,9 @@ registerTool("update_config_field", {
|
|
|
2094
2119
|
"The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings " +
|
|
2095
2120
|
"(e.g. [\"b2c-commerce-developer\", \"playwright-mcp\", \"pwa-kit-mcp\"]) — pass an array of strings, " +
|
|
2096
2121
|
"not a comma-delimited string; an empty array clears the selection. " +
|
|
2122
|
+
"The base_branch field is a string/null field controlling the development base branch used by PR " +
|
|
2123
|
+
"creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and " +
|
|
2124
|
+
"automations fall back to 'main'. " +
|
|
2097
2125
|
"For string fields, omit both value and file_path to set the field to NULL (clearing it). " +
|
|
2098
2126
|
"Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false " +
|
|
2099
2127
|
"(matching the API-layer coercion), so pass true/false explicitly."),
|
|
@@ -2791,7 +2819,7 @@ registerTool("list_pipelines", {
|
|
|
2791
2819
|
const list = Object.entries(PIPELINES).map(([key, pipeline]) => ({
|
|
2792
2820
|
name: key,
|
|
2793
2821
|
description: pipeline.description ?? "",
|
|
2794
|
-
variables: (pipeline.variables ?? []).filter((v) => v !== "docs_dir"),
|
|
2822
|
+
variables: (pipeline.variables ?? []).filter((v) => v !== "docs_dir" && v !== "idea_hash"),
|
|
2795
2823
|
source: userPipelineKeys.has(key) ? "user" : "bundled",
|
|
2796
2824
|
}));
|
|
2797
2825
|
return {
|
|
@@ -2809,7 +2837,7 @@ registerTool("get_pipeline_recipe", {
|
|
|
2809
2837
|
.string()
|
|
2810
2838
|
.describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
|
|
2811
2839
|
variables: z
|
|
2812
|
-
.record(z.string())
|
|
2840
|
+
.record(z.string(), z.string())
|
|
2813
2841
|
.optional()
|
|
2814
2842
|
.describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),
|
|
2815
2843
|
skip_steps: z
|
|
@@ -2860,6 +2888,11 @@ registerTool("get_pipeline_recipe", {
|
|
|
2860
2888
|
auto_approve: auto_approve ? "true" : "",
|
|
2861
2889
|
...(variables ?? {}),
|
|
2862
2890
|
};
|
|
2891
|
+
// Auto-inject the stable idea-hash (like docs_dir) for the standalone
|
|
2892
|
+
// /idea-to-ticket path, mirroring runPipeline.
|
|
2893
|
+
if ("idea" in mergedVariables) {
|
|
2894
|
+
mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
|
|
2895
|
+
}
|
|
2863
2896
|
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve);
|
|
2864
2897
|
return {
|
|
2865
2898
|
content: [{
|
|
@@ -2912,6 +2945,21 @@ function buildPipelineOrchestratorDeps() {
|
|
|
2912
2945
|
toolHandlers: TOOL_HANDLERS,
|
|
2913
2946
|
};
|
|
2914
2947
|
}
|
|
2948
|
+
// BAPI-326: dependency injection for the full-automation chain orchestrator.
|
|
2949
|
+
// Same shape as the pipeline deps plus the bundled chain-recipe registry. The
|
|
2950
|
+
// chain orchestrator never imports index.ts; everything flows through here.
|
|
2951
|
+
function buildChainOrchestratorDeps() {
|
|
2952
|
+
return {
|
|
2953
|
+
baseUrl: BASE_URL,
|
|
2954
|
+
apiKey: API_KEY,
|
|
2955
|
+
repoName: REPO_NAME,
|
|
2956
|
+
docsDir: BAPI_DOCS_DIR,
|
|
2957
|
+
pipelines: PIPELINES,
|
|
2958
|
+
chainRecipes: CHAIN_RECIPES,
|
|
2959
|
+
instructions: INSTRUCTIONS,
|
|
2960
|
+
toolHandlers: TOOL_HANDLERS,
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2915
2963
|
registerTool("run_pipeline", {
|
|
2916
2964
|
description: "Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, " +
|
|
2917
2965
|
"dispatching mcp_call steps in-process and pausing on agent_task steps with a " +
|
|
@@ -2929,7 +2977,7 @@ registerTool("run_pipeline", {
|
|
|
2929
2977
|
.string()
|
|
2930
2978
|
.describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
|
|
2931
2979
|
variables: z
|
|
2932
|
-
.record(z.string())
|
|
2980
|
+
.record(z.string(), z.string())
|
|
2933
2981
|
.optional()
|
|
2934
2982
|
.describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). " +
|
|
2935
2983
|
"Do NOT pass `auto_approve` here — use the top-level parameter."),
|
|
@@ -3021,6 +3069,85 @@ registerTool("delete_pipeline_run", {
|
|
|
3021
3069
|
};
|
|
3022
3070
|
});
|
|
3023
3071
|
// ---------------------------------------------------------------------------
|
|
3072
|
+
// Full Automation Chain Tools (BAPI-326)
|
|
3073
|
+
// ---------------------------------------------------------------------------
|
|
3074
|
+
//
|
|
3075
|
+
// run_full_automation / resume_full_automation drive an idea through three
|
|
3076
|
+
// chained stages (idea-to-ticket -> review-ticket fan-out -> start-tickets CLI
|
|
3077
|
+
// seam) via the existing pipeline runner. They share the chain envelope shape
|
|
3078
|
+
// (status: needs_agent_task | completed | failed) with a strict error_code
|
|
3079
|
+
// enum (VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Chain
|
|
3080
|
+
// state is persisted server-side via /jira/chain-runs/runs. Registered AFTER
|
|
3081
|
+
// TOOL_HANDLERS and the pipeline tools so the orchestrator can dispatch the
|
|
3082
|
+
// existing child pipelines in-process.
|
|
3083
|
+
registerTool("run_full_automation", {
|
|
3084
|
+
description: "Run the full-automation chain for an idea: create ticket(s) " +
|
|
3085
|
+
"(idea-to-ticket), review each created ticket (review-ticket fan-out), " +
|
|
3086
|
+
"then emit the exact `/start-tickets ...` command for you to invoke in " +
|
|
3087
|
+
"this same session. Returns the chain envelope keyed on `status`: " +
|
|
3088
|
+
"`needs_agent_task` (perform the `next_action.instruction`, then call " +
|
|
3089
|
+
"`resume_full_automation` with the result as `agent_result`), `completed`, " +
|
|
3090
|
+
"or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | " +
|
|
3091
|
+
"REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` " +
|
|
3092
|
+
"(mutually exclusive).",
|
|
3093
|
+
inputSchema: {
|
|
3094
|
+
idea: z.string().optional(),
|
|
3095
|
+
idea_file: z.string().optional(),
|
|
3096
|
+
auto_approve: z
|
|
3097
|
+
.union([z.boolean(), z.literal("true"), z.literal("false")])
|
|
3098
|
+
.optional(),
|
|
3099
|
+
scheduled_at: z.string().optional(),
|
|
3100
|
+
max_children: z.number().int().positive().optional(),
|
|
3101
|
+
allow_duplicate: z.boolean().optional(),
|
|
3102
|
+
agent: z.enum(["claude"]).optional(),
|
|
3103
|
+
ttl_seconds: z.number().int().positive().optional(),
|
|
3104
|
+
},
|
|
3105
|
+
}, async (input) => {
|
|
3106
|
+
const { idea, idea_file, ...rest } = input;
|
|
3107
|
+
if (idea !== undefined && idea_file !== undefined) {
|
|
3108
|
+
return {
|
|
3109
|
+
content: [{
|
|
3110
|
+
type: "text",
|
|
3111
|
+
text: JSON.stringify({
|
|
3112
|
+
error: "BAD_REQUEST",
|
|
3113
|
+
status: 400,
|
|
3114
|
+
message: "Provide exactly one of `idea` or `idea_file`, not both.",
|
|
3115
|
+
}),
|
|
3116
|
+
}],
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
const resolved = await resolveTextOrFile(idea, idea_file, "idea");
|
|
3120
|
+
if (!resolved.ok) {
|
|
3121
|
+
return resolved.errorResponse;
|
|
3122
|
+
}
|
|
3123
|
+
const result = await runFullAutomation(buildChainOrchestratorDeps(), {
|
|
3124
|
+
idea: resolved.text,
|
|
3125
|
+
...rest,
|
|
3126
|
+
});
|
|
3127
|
+
return {
|
|
3128
|
+
content: [
|
|
3129
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
3130
|
+
],
|
|
3131
|
+
};
|
|
3132
|
+
});
|
|
3133
|
+
registerTool("resume_full_automation", {
|
|
3134
|
+
description: "Resume a paused full-automation chain run. Provide the `chain_run_id` " +
|
|
3135
|
+
"returned by the prior needs_agent_task envelope and the string the " +
|
|
3136
|
+
"instruction asked you to produce as `agent_result`. Returns the same " +
|
|
3137
|
+
"chain envelope shape as `run_full_automation`.",
|
|
3138
|
+
inputSchema: {
|
|
3139
|
+
chain_run_id: z.string(),
|
|
3140
|
+
agent_result: z.string(),
|
|
3141
|
+
},
|
|
3142
|
+
}, async (input) => {
|
|
3143
|
+
const result = await resumeFullAutomation(buildChainOrchestratorDeps(), input);
|
|
3144
|
+
return {
|
|
3145
|
+
content: [
|
|
3146
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
3147
|
+
],
|
|
3148
|
+
};
|
|
3149
|
+
});
|
|
3150
|
+
// ---------------------------------------------------------------------------
|
|
3024
3151
|
// generate_decision_page
|
|
3025
3152
|
// ---------------------------------------------------------------------------
|
|
3026
3153
|
registerTool("generate_decision_page", {
|
|
@@ -16,7 +16,21 @@
|
|
|
16
16
|
* are inlined into the instruction string so the agent can review and
|
|
17
17
|
* confirm before resuming. (BAPI-275 decisions E-4 / E-36.)
|
|
18
18
|
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
19
20
|
import { resolveRecipe } from "./pipeline-utils.js";
|
|
21
|
+
/**
|
|
22
|
+
* Derive a STABLE, content-addressed hash of an idea — the basis for the
|
|
23
|
+
* cross-run idempotency label ``bapi-idea-hash-{idea_hash}``. Normalizes
|
|
24
|
+
* (trim + lowercase + collapse internal whitespace) so cosmetic edits don't
|
|
25
|
+
* change the hash, then takes the first 12 hex chars of the SHA-256. Unlike the
|
|
26
|
+
* per-run ``run_id`` label, this is identical every time the same idea is run,
|
|
27
|
+
* so a re-run of the same idea is reliably caught by label (not just fuzzy
|
|
28
|
+
* keyword search). Auto-injected as the ``idea_hash`` pipeline variable.
|
|
29
|
+
*/
|
|
30
|
+
export function deriveIdeaHash(idea) {
|
|
31
|
+
const normalized = String(idea ?? "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
32
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
33
|
+
}
|
|
20
34
|
class PipelinePersistenceError extends Error {
|
|
21
35
|
code;
|
|
22
36
|
constructor(code, message) {
|
|
@@ -175,6 +189,10 @@ const ORCHESTRATION_TOOLS = new Set([
|
|
|
175
189
|
"resume_pipeline",
|
|
176
190
|
"list_pipeline_runs",
|
|
177
191
|
"delete_pipeline_run",
|
|
192
|
+
// BAPI-326: chain orchestration tools must never be invoked from within a
|
|
193
|
+
// declarative pipeline step (avoids accidental orchestration recursion).
|
|
194
|
+
"run_full_automation",
|
|
195
|
+
"resume_full_automation",
|
|
178
196
|
]);
|
|
179
197
|
// ---------------------------------------------------------------------------
|
|
180
198
|
// runPipeline
|
|
@@ -196,6 +214,12 @@ export async function runPipeline(deps, input) {
|
|
|
196
214
|
auto_approve: autoApprove ? "true" : "",
|
|
197
215
|
...(input.variables ?? {}),
|
|
198
216
|
};
|
|
217
|
+
// Auto-inject the stable idea-hash (like docs_dir) whenever the pipeline was
|
|
218
|
+
// given an `idea`, so both the chain and standalone callers get the cross-run
|
|
219
|
+
// idempotency label without computing it themselves.
|
|
220
|
+
if ("idea" in mergedVariables) {
|
|
221
|
+
mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
|
|
222
|
+
}
|
|
199
223
|
let recipe;
|
|
200
224
|
try {
|
|
201
225
|
recipe = resolveRecipe(pipelineDef, deps.instructions, mergedVariables, undefined, autoApprove);
|
|
@@ -227,6 +251,39 @@ export async function runPipeline(deps, input) {
|
|
|
227
251
|
}
|
|
228
252
|
return continuePipelineExecution(deps, persistence, row, recipe, autoApprove);
|
|
229
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Read-only snapshot of a pipeline run's current persisted state — performs no
|
|
256
|
+
* execution and no state transition. Used by the chain orchestrator to decide,
|
|
257
|
+
* at a stage boundary, whether the inner pipeline still needs resuming
|
|
258
|
+
* (``paused``), has already reached a terminal state (``completed`` / ``failed``)
|
|
259
|
+
* so the stage can be advanced/failed idempotently, or is in an indeterminate
|
|
260
|
+
* state that cannot be safely resumed or recovered.
|
|
261
|
+
*/
|
|
262
|
+
export async function peekPipelineRun(deps, pipeline_run_id) {
|
|
263
|
+
const persistence = createPipelinePersistenceClient({
|
|
264
|
+
baseUrl: deps.baseUrl,
|
|
265
|
+
apiKey: deps.apiKey,
|
|
266
|
+
repoName: deps.repoName,
|
|
267
|
+
});
|
|
268
|
+
let row;
|
|
269
|
+
try {
|
|
270
|
+
row = await persistence.getRun(pipeline_run_id);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (err instanceof PipelinePersistenceError) {
|
|
274
|
+
return failedEnvelope(err.code, err.message, { pipeline_run_id });
|
|
275
|
+
}
|
|
276
|
+
return failedEnvelope("TOOL_ERROR", err instanceof Error ? err.message : String(err), { pipeline_run_id });
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
status: row.status,
|
|
280
|
+
pipeline_run_id: row.pipeline_run_id,
|
|
281
|
+
pipeline: row.pipeline_name,
|
|
282
|
+
total_steps: row.resolved_recipe.total_steps,
|
|
283
|
+
current_step_index: row.current_step_index,
|
|
284
|
+
results: row.results,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
230
287
|
// ---------------------------------------------------------------------------
|
|
231
288
|
// resumePipeline
|
|
232
289
|
// ---------------------------------------------------------------------------
|