flowmind 1.5.3 → 1.5.6

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.
@@ -6,6 +6,7 @@
6
6
  const fs = require('fs');
7
7
  const os = require('os');
8
8
  const path = require('path');
9
+ const { callMcpTool } = require('../../core/mcp-http-client');
9
10
 
10
11
  const BUILTIN_WORKFLOWS = {
11
12
  'dev-workflow': {
@@ -72,19 +73,14 @@ module.exports = {
72
73
  const workflow = createWorkflowClient(context);
73
74
 
74
75
  if (params.action === 'define') {
76
+ const definition = buildWorkflowDefinitionSummary();
75
77
  return {
76
78
  type: 'result',
77
79
  skill: 'auto-flow',
78
80
  message: 'Define a workflow in YAML format',
79
81
  data: {
80
82
  format: 'YAML workflow definition',
81
- example: {
82
- name: 'My Workflow',
83
- steps: [
84
- { id: 'step1', action: 'run-command', command: 'echo hello' },
85
- { id: 'step2', skill: 'code-review', depends_on: ['step1'] }
86
- ]
87
- }
83
+ execution: definition
88
84
  },
89
85
  input,
90
86
  timestamp: new Date().toISOString()
@@ -96,7 +92,7 @@ module.exports = {
96
92
  }
97
93
 
98
94
  if (params.action === 'status') {
99
- await executeStatus(workflow, params, input);
95
+ const execution = await executeStatus(workflow, params, input);
100
96
  return {
101
97
  type: 'result',
102
98
  skill: 'auto-flow',
@@ -106,7 +102,8 @@ module.exports = {
106
102
  provider: workflow.provider,
107
103
  binding: workflow.binding,
108
104
  status: 'completed',
109
- mcpServer: getWorkflowMcpServer(workflow)
105
+ mcpServer: getWorkflowMcpServer(workflow),
106
+ execution: summarizeWorkflowExecution('status', params, execution)
110
107
  },
111
108
  input,
112
109
  timestamp: new Date().toISOString()
@@ -118,7 +115,7 @@ module.exports = {
118
115
  }
119
116
 
120
117
  if (params.action === 'list' || params.serviceNames.length > 0) {
121
- await workflow.client.listPipelines(buildListPipelineParams(params));
118
+ const execution = await workflow.client.listPipelines(buildListPipelineParams(params));
122
119
  return {
123
120
  type: 'result',
124
121
  skill: 'auto-flow',
@@ -128,7 +125,8 @@ module.exports = {
128
125
  provider: workflow.provider,
129
126
  binding: workflow.binding,
130
127
  status: 'completed',
131
- mcpServer: getWorkflowMcpServer(workflow)
128
+ mcpServer: getWorkflowMcpServer(workflow),
129
+ execution: summarizeWorkflowExecution('list', params, execution)
132
130
  },
133
131
  input,
134
132
  timestamp: new Date().toISOString()
@@ -143,7 +141,7 @@ module.exports = {
143
141
  message: `Workflow ready: ${workflowDef.name}`,
144
142
  data: {
145
143
  workflow: params.workflow,
146
- steps: workflowDef.steps,
144
+ execution: buildBuiltinWorkflowSummary(params.workflow, workflowDef),
147
145
  provider: workflow.provider
148
146
  },
149
147
  input,
@@ -164,7 +162,8 @@ module.exports = {
164
162
  ],
165
163
  builtinWorkflows: Object.keys(BUILTIN_WORKFLOWS),
166
164
  provider: workflow.provider,
167
- binding: workflow.binding
165
+ binding: workflow.binding,
166
+ execution: buildAutoFlowHintSummary(workflow)
168
167
  },
169
168
  input,
170
169
  timestamp: new Date().toISOString()
@@ -191,40 +190,391 @@ function createWorkflowClient(context) {
191
190
  return { client: null, provider: null, binding: null, transportBacked: false };
192
191
  }
193
192
 
193
+ const transport = buildWorkflowTransport(binding);
194
+ if (!transport.url) {
195
+ return { client: null, provider: binding.provider || null, binding, transportBacked: false };
196
+ }
197
+
194
198
  return {
195
199
  client: {
196
200
  providerName: binding.provider || 'workflow-binding',
197
201
  async listPipelines(params) {
198
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelines, params };
202
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.listPipelines, params);
199
203
  },
200
204
  async getCurrentIterate() {
201
- return { mcpServer: binding.mcpServer, tool: 'getCurrentIterate', params: {} };
205
+ return callWorkflowMcp(binding, transport, 'getCurrentIterate', {});
202
206
  },
203
207
  async listDeployChecklists(params) {
204
- return { mcpServer: binding.mcpServer, tool: 'listDeployChecklists', params };
208
+ return callWorkflowMcp(binding, transport, 'listDeployChecklists', params);
205
209
  },
206
210
  async orderList(params) {
207
- return { mcpServer: binding.mcpServer, tool: 'orderList', params };
211
+ return callWorkflowMcp(binding, transport, 'orderList', params);
208
212
  },
209
213
  async startPipelineRun(pipelineId) {
210
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startPipelineRun, params: { pipelineId } };
214
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.startPipelineRun, { pipelineId });
211
215
  },
212
216
  async startBatchPipelineRun(params) {
213
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startBatchPipelineRun, params };
217
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.startBatchPipelineRun, params);
214
218
  },
215
219
  async getPipelineRun(pipelineId, runId) {
216
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.getPipelineRun, params: { pipelineId, pipelineRunId: runId } };
220
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.getPipelineRun, { pipelineId, pipelineRunId: runId });
217
221
  },
218
222
  async listPipelineRuns(pipelineId, params) {
219
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelineRuns, params: { pipelineId, ...(params || {}) } };
223
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.listPipelineRuns, { pipelineId, ...(params || {}) });
220
224
  }
221
225
  },
222
226
  provider: binding.provider || 'workflow-binding',
223
227
  binding,
224
- transportBacked: false
228
+ transportBacked: true
229
+ };
230
+ }
231
+
232
+ function buildWorkflowTransport(binding) {
233
+ const connection = binding?.connection || {};
234
+ const headers = {};
235
+
236
+ if (connection.token) {
237
+ headers.Authorization = `Bearer ${connection.token}`;
238
+ }
239
+ if (connection.apiKey) {
240
+ headers['X-API-Key'] = connection.apiKey;
241
+ }
242
+
243
+ return {
244
+ url: connection.url || connection.address || connection.endpoint || null,
245
+ headers,
246
+ timeoutMs: connection.timeoutMs || 60000
247
+ };
248
+ }
249
+
250
+ function callWorkflowMcp(binding, transport, tool, params) {
251
+ return callMcpTool({
252
+ url: transport.url,
253
+ headers: transport.headers,
254
+ tool,
255
+ args: params || {},
256
+ timeoutMs: transport.timeoutMs || 60000,
257
+ serverName: binding.mcpServer || binding.provider || 'workflow'
258
+ });
259
+ }
260
+
261
+ function summarizeWorkflowExecution(action, params, execution) {
262
+ const payload = unwrapMcpResult(execution);
263
+ const records = extractRecords(payload);
264
+ const total = extractTotalCount(payload, records);
265
+
266
+ return compactObject({
267
+ action,
268
+ summary: buildWorkflowSummaryText(action, params, total, records),
269
+ total,
270
+ status: extractWorkflowStatus(payload, action),
271
+ target: buildWorkflowTargetLabel(params),
272
+ environment: params.environment || null,
273
+ items: records.slice(0, 5).map((record) => summarizeWorkflowRecord(record))
274
+ });
275
+ }
276
+
277
+ function summarizeDeployExecution(params, execution, context = {}) {
278
+ const payload = unwrapMcpResult(execution);
279
+ const payloadList = Array.isArray(payload) ? payload : [payload];
280
+ const primary = payloadList.find((item) => item !== null && item !== undefined) || null;
281
+ const summaryTarget = buildWorkflowTargetLabel(params) || buildDeployTargetLabel(context) || 'deployment';
282
+
283
+ return compactObject({
284
+ action: 'deploy',
285
+ summary: buildDeploySummaryText(summaryTarget, primary, context, payloadList.length),
286
+ status: extractDeployStatus(primary),
287
+ target: summaryTarget,
288
+ pipelineId: context.pipelineId || params.pipelineId || (Array.isArray(context.pipelineIds) && context.pipelineIds.length === 1 ? context.pipelineIds[0] : null),
289
+ pipelineIds: Array.isArray(context.pipelineIds) && context.pipelineIds.length > 0 ? context.pipelineIds : null,
290
+ source: context.source || null,
291
+ runId: extractDeployRunId(primary),
292
+ count: payloadList.length > 1 ? payloadList.length : null,
293
+ details: summarizeDeployDetails(primary)
294
+ });
295
+ }
296
+
297
+ function buildWorkflowDefinitionSummary() {
298
+ return {
299
+ action: 'define',
300
+ summary: 'Prepared a YAML workflow template',
301
+ format: 'YAML workflow definition',
302
+ example: {
303
+ name: 'My Workflow',
304
+ steps: [
305
+ { id: 'step1', action: 'run-command', command: 'echo hello' },
306
+ { id: 'step2', skill: 'code-review', depends_on: ['step1'] }
307
+ ]
308
+ }
225
309
  };
226
310
  }
227
311
 
312
+ function buildBuiltinWorkflowSummary(workflowName, workflowDef) {
313
+ return {
314
+ action: 'workflow',
315
+ summary: `Workflow ready: ${workflowDef.name}`,
316
+ workflow: workflowName,
317
+ name: workflowDef.name,
318
+ stepCount: Array.isArray(workflowDef.steps) ? workflowDef.steps.length : 0,
319
+ steps: (workflowDef.steps || []).map((step) => compactObject({
320
+ id: step.id,
321
+ skill: step.skill || null,
322
+ action: step.action || null,
323
+ name: step.name || null,
324
+ depends_on: Array.isArray(step.depends_on) ? step.depends_on : null
325
+ }))
326
+ };
327
+ }
328
+
329
+ function buildAutoFlowHintSummary(workflow) {
330
+ return {
331
+ action: 'hint',
332
+ summary: workflow?.binding
333
+ ? `Workflow service bound to ${workflow.binding.mcpServer}`
334
+ : 'Workflow service not configured',
335
+ provider: workflow?.provider || null,
336
+ binding: workflow?.binding || null,
337
+ configured: !!workflow?.client
338
+ };
339
+ }
340
+
341
+ function buildWorkflowSummaryText(action, params, total, records) {
342
+ const target = buildWorkflowTargetLabel(params) || 'all workflows';
343
+ const count = typeof total === 'number' ? total : records.length;
344
+
345
+ if (action === 'status') {
346
+ return `Checked ${target}${count !== null ? `, ${count} record(s) found` : ''}`;
347
+ }
348
+
349
+ if (action === 'list') {
350
+ return `Listed workflows for ${target}${count !== null ? `, ${count} pipeline(s) found` : ''}`;
351
+ }
352
+
353
+ return `Workflow ${action} completed`;
354
+ }
355
+
356
+ function buildWorkflowTargetLabel(params) {
357
+ if (params.pipelineId && params.runId) {
358
+ return `pipeline ${params.pipelineId} / run ${params.runId}`;
359
+ }
360
+ if (params.pipelineId) {
361
+ return `pipeline ${params.pipelineId}`;
362
+ }
363
+ if (params.serviceNames?.length > 0) {
364
+ return params.serviceNames.join(', ');
365
+ }
366
+ if (params.workflow) {
367
+ return params.workflow;
368
+ }
369
+ return params.environment || null;
370
+ }
371
+
372
+ function buildDeployTargetLabel(context) {
373
+ if (Array.isArray(context.pipelineIds) && context.pipelineIds.length > 0) {
374
+ return `pipeline ${context.pipelineIds.join(', ')}`;
375
+ }
376
+ if (context.pipelineId) {
377
+ return `pipeline ${context.pipelineId}`;
378
+ }
379
+ if (Array.isArray(context.services) && context.services.length > 0) {
380
+ return context.services.map((item) => item.serviceName || item.service || item.pipelineName || item.pipelineId).filter(Boolean).join(', ');
381
+ }
382
+ return null;
383
+ }
384
+
385
+ function buildDeploySummaryText(target, payload, context, count = 1) {
386
+ const status = extractDeployStatus(payload) || 'submitted';
387
+ const runId = extractDeployRunId(payload);
388
+ const source = context.source ? ` via ${context.source}` : '';
389
+ const countSuffix = count > 1 ? ` (${count} items)` : '';
390
+
391
+ if (runId) {
392
+ return `Deployment ${status} for ${target}${source}, run ${runId}${countSuffix}`;
393
+ }
394
+
395
+ return `Deployment ${status} for ${target}${source}${countSuffix}`;
396
+ }
397
+
398
+ function extractDeployStatus(payload) {
399
+ if (payload && typeof payload === 'object') {
400
+ const candidates = [
401
+ payload.status,
402
+ payload.state,
403
+ payload.result?.status,
404
+ payload.result?.state
405
+ ].filter(Boolean);
406
+ if (candidates.length > 0) {
407
+ return String(candidates[0]);
408
+ }
409
+
410
+ if (payload.submitted === true) {
411
+ return 'submitted';
412
+ }
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ function extractDeployRunId(payload) {
419
+ if (!payload || typeof payload !== 'object') {
420
+ return null;
421
+ }
422
+
423
+ const candidates = [
424
+ payload.pipelineRunId,
425
+ payload.pipeline_run_id,
426
+ payload.runId,
427
+ payload.run_id,
428
+ payload.result?.pipelineRunId,
429
+ payload.result?.pipeline_run_id,
430
+ payload.result?.runId,
431
+ payload.result?.run_id
432
+ ];
433
+
434
+ for (const candidate of candidates) {
435
+ if (candidate === null || candidate === undefined || candidate === '') continue;
436
+ return String(candidate);
437
+ }
438
+
439
+ return null;
440
+ }
441
+
442
+ function summarizeDeployDetails(payload) {
443
+ if (!payload || typeof payload !== 'object') {
444
+ return null;
445
+ }
446
+
447
+ const details = compactObject({
448
+ submitted: typeof payload.submitted === 'boolean' ? payload.submitted : null,
449
+ pipelineRunId: payload.pipelineRunId || payload.pipeline_run_id || null,
450
+ pipelineId: payload.pipelineId || payload.pipeline_id || null,
451
+ message: payload.message || payload.result?.message || null
452
+ });
453
+
454
+ return Object.keys(details).length > 0 ? details : null;
455
+ }
456
+
457
+ function unwrapMcpResult(payload) {
458
+ if (!payload || typeof payload !== 'object') {
459
+ return payload;
460
+ }
461
+
462
+ const textContent = Array.isArray(payload.content) ? payload.content : payload.result?.content;
463
+ if (Array.isArray(textContent)) {
464
+ const textItem = textContent.find((item) => item?.type === 'text' && typeof item.text === 'string');
465
+ if (textItem?.text) {
466
+ try {
467
+ return JSON.parse(textItem.text);
468
+ } catch (error) {
469
+ return payload;
470
+ }
471
+ }
472
+ }
473
+
474
+ return payload;
475
+ }
476
+
477
+ function extractRecords(payload) {
478
+ const normalized = unwrapMcpResult(payload);
479
+ if (!normalized) return [];
480
+ if (Array.isArray(normalized)) return normalized;
481
+
482
+ const candidates = [
483
+ normalized.data,
484
+ normalized.rows,
485
+ normalized.list,
486
+ normalized.records,
487
+ normalized.result?.data,
488
+ normalized.result?.rows,
489
+ normalized.result?.list,
490
+ normalized.result?.records
491
+ ];
492
+
493
+ for (const candidate of candidates) {
494
+ if (Array.isArray(candidate)) {
495
+ return candidate;
496
+ }
497
+ }
498
+
499
+ return [];
500
+ }
501
+
502
+ function extractTotalCount(payload, records) {
503
+ if (payload && typeof payload === 'object') {
504
+ const candidates = [payload.total, payload.count, payload.result?.total, payload.result?.count];
505
+ for (const candidate of candidates) {
506
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
507
+ return candidate;
508
+ }
509
+ if (typeof candidate === 'string' && candidate.trim() !== '' && !Number.isNaN(Number(candidate))) {
510
+ return Number(candidate);
511
+ }
512
+ }
513
+ }
514
+
515
+ return records.length;
516
+ }
517
+
518
+ function extractWorkflowStatus(payload, action) {
519
+ if (payload && typeof payload === 'object') {
520
+ const candidates = [
521
+ payload.status,
522
+ payload.state,
523
+ payload.result?.status,
524
+ payload.result?.state
525
+ ].filter(Boolean);
526
+ if (candidates.length > 0) {
527
+ return String(candidates[0]);
528
+ }
529
+ }
530
+
531
+ return action === 'status' ? 'completed' : 'completed';
532
+ }
533
+
534
+ function summarizeWorkflowRecord(record) {
535
+ const fields = parseWorkflowFields(record?.fields);
536
+ return compactObject({
537
+ pipelineName: record?.pipelineName || record?.name || fields?.pipelineName || fields?.name || null,
538
+ pipelineId: record?.pipelineId || fields?.pipelineId || extractIdFromFields(fields) || null,
539
+ service: extractWorkflowService(fields),
540
+ status: record?.status || fields?.status || fields?.state || null,
541
+ env: record?.env || fields?.env || fields?.environment || null
542
+ });
543
+ }
544
+
545
+ function parseWorkflowFields(fields) {
546
+ if (!fields) return null;
547
+ if (typeof fields === 'object') return fields;
548
+ if (typeof fields !== 'string') return null;
549
+
550
+ try {
551
+ return JSON.parse(fields);
552
+ } catch (error) {
553
+ return null;
554
+ }
555
+ }
556
+
557
+ function extractWorkflowService(fields) {
558
+ if (!fields) return null;
559
+ if (Array.isArray(fields.service) && fields.service.length > 0) {
560
+ return fields.service[0];
561
+ }
562
+ if (typeof fields.service === 'string') {
563
+ return fields.service;
564
+ }
565
+ return fields.serviceName || fields.service_name || null;
566
+ }
567
+
568
+ function extractIdFromFields(fields) {
569
+ if (!fields || typeof fields !== 'object') return null;
570
+ const candidates = [fields.pipelineId, fields.pipeline_id, fields.id];
571
+ for (const candidate of candidates) {
572
+ if (candidate === null || candidate === undefined || candidate === '') continue;
573
+ return String(candidate);
574
+ }
575
+ return null;
576
+ }
577
+
228
578
  function buildNoAdapterResult(input, params) {
229
579
  if (params.workflow && BUILTIN_WORKFLOWS[params.workflow]) {
230
580
  const workflow = BUILTIN_WORKFLOWS[params.workflow];
@@ -234,7 +584,7 @@ function buildNoAdapterResult(input, params) {
234
584
  message: `Workflow ready: ${workflow.name}`,
235
585
  data: {
236
586
  workflow: params.workflow,
237
- steps: workflow.steps
587
+ execution: buildBuiltinWorkflowSummary(params.workflow, workflow)
238
588
  },
239
589
  input,
240
590
  timestamp: new Date().toISOString()
@@ -247,7 +597,12 @@ function buildNoAdapterResult(input, params) {
247
597
  message: 'Workflow service not configured. Connect friday-auto-flow first.',
248
598
  data: {
249
599
  status: 'not_configured',
250
- hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`'
600
+ hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`',
601
+ execution: buildAutoFlowHintSummary({
602
+ client: null,
603
+ provider: null,
604
+ binding: null
605
+ })
251
606
  },
252
607
  input,
253
608
  timestamp: new Date().toISOString()
@@ -267,7 +622,10 @@ async function executeDeploy(workflow, params, input, context) {
267
622
  binding: workflow.binding,
268
623
  status: 'submitted',
269
624
  mcpServer: getWorkflowMcpServer(workflow),
270
- execution,
625
+ execution: summarizeDeployExecution(params, execution, {
626
+ source: 'direct-input',
627
+ pipelineId: params.pipelineId
628
+ }),
271
629
  resolution: {
272
630
  source: 'direct-input',
273
631
  pipelineId: params.pipelineId
@@ -295,7 +653,11 @@ async function executeDeploy(workflow, params, input, context) {
295
653
  binding: workflow.binding,
296
654
  status: 'submitted',
297
655
  mcpServer: getWorkflowMcpServer(workflow),
298
- execution: executions.length === 1 ? executions[0] : executions,
656
+ execution: summarizeDeployExecution(params, executions.length === 1 ? executions[0] : executions, {
657
+ source: resolvedTargets.source,
658
+ pipelineIds: resolvedTargets.pipelineIds,
659
+ services: resolvedTargets.resolutions
660
+ }),
299
661
  resolution: {
300
662
  source: resolvedTargets.source,
301
663
  pipelineIds: resolvedTargets.pipelineIds,
@@ -307,57 +669,22 @@ async function executeDeploy(workflow, params, input, context) {
307
669
  };
308
670
  }
309
671
 
310
- const listParams = buildListPipelineParams(params);
311
- const lookup = await workflow.client.listPipelines(listParams);
312
- const batchParams = buildBatchRunParams(params);
313
- const execution = await workflow.client.startBatchPipelineRun(batchParams);
314
-
315
- return {
316
- type: 'result',
317
- skill: 'auto-flow',
318
- message: 'Workflow status: deployment submitted',
319
- data: {
320
- action: 'deploy',
321
- provider: workflow.provider,
322
- binding: workflow.binding,
323
- status: 'submitted',
324
- mcpServer: getWorkflowMcpServer(workflow),
325
- execution,
326
- resolution: {
327
- source: 'batch-fallback',
328
- lookup
329
- }
330
- },
331
- input,
332
- timestamp: new Date().toISOString()
333
- };
334
- }
672
+ return buildUnresolvedDeployResult(workflow, params, input, context);
673
+ }
335
674
 
336
675
  async function executeStatus(workflow, params, input) {
676
+ let execution;
337
677
  if (params.pipelineId && params.runId) {
338
- await workflow.client.getPipelineRun(params.pipelineId, params.runId);
678
+ execution = await workflow.client.getPipelineRun(params.pipelineId, params.runId);
339
679
  } else if (params.pipelineId) {
340
- await workflow.client.listPipelineRuns(params.pipelineId, {
680
+ execution = await workflow.client.listPipelineRuns(params.pipelineId, {
341
681
  env: params.environment
342
682
  });
343
683
  } else {
344
- await workflow.client.listPipelines(buildListPipelineParams(params));
684
+ execution = await workflow.client.listPipelines(buildListPipelineParams(params));
345
685
  }
346
686
 
347
- return {
348
- type: 'result',
349
- skill: 'auto-flow',
350
- message: 'Workflow status: query completed',
351
- data: {
352
- action: 'status',
353
- provider: workflow.provider,
354
- binding: workflow.binding,
355
- status: 'completed',
356
- mcpServer: workflow.binding?.mcpServer || workflow.provider
357
- },
358
- input,
359
- timestamp: new Date().toISOString()
360
- };
687
+ return execution;
361
688
  }
362
689
 
363
690
  function parseFlowParams(input) {
@@ -541,7 +868,7 @@ function resolveLocalPipelineForService(serviceName, environment, context) {
541
868
  const entry = pipelineMap[serviceName];
542
869
  if (!entry) return null;
543
870
 
544
- const pipeline = selectPipelineFromEntry(entry, environment);
871
+ const pipeline = selectPipelineFromEntry(entry, environment, serviceName);
545
872
  if (!pipeline?.pipelineId) return null;
546
873
 
547
874
  return {
@@ -570,19 +897,13 @@ function readLocalPipelineMap(context) {
570
897
  return null;
571
898
  }
572
899
 
573
- function selectPipelineFromEntry(entry, environment) {
900
+ function selectPipelineFromEntry(entry, environment, serviceName) {
574
901
  const preferredKeys = getEnvLookupOrder(environment);
575
902
  for (const key of preferredKeys) {
576
903
  const candidate = normalizePipelineCollection(entry?.[key]);
577
- if (candidate.length > 0) {
578
- return candidate[0];
579
- }
580
- }
581
-
582
- for (const value of Object.values(entry || {})) {
583
- const candidate = normalizePipelineCollection(value);
584
- if (candidate.length > 0) {
585
- return candidate[0];
904
+ const matched = candidate.find((item) => matchesPipelineName(item.pipelineName, environment, serviceName));
905
+ if (matched) {
906
+ return matched;
586
907
  }
587
908
  }
588
909
 
@@ -741,49 +1062,12 @@ function matchesRemoteService(fields, serviceName, environment) {
741
1062
  }
742
1063
 
743
1064
  function selectPipelineFromFields(fields, environment, serviceName) {
744
- const indexedCandidate = selectIndexedPipelineForService(fields, environment, serviceName);
745
- if (indexedCandidate?.pipelineId) {
746
- return indexedCandidate;
747
- }
748
-
749
1065
  const candidates = collectFieldPipelineDescriptors(fields, environment);
750
- if (candidates.length > 0) {
751
- return candidates[0];
752
- }
753
-
754
- const pipelineIds = String(fields.pipelineId || '')
755
- .split(',')
756
- .map((item) => item.trim())
757
- .filter(Boolean);
758
-
759
- if (pipelineIds.length === 0) {
760
- return null;
761
- }
762
-
763
- return {
764
- pipelineName: buildPrefixedPipelineName(
765
- Array.isArray(fields.service) ? fields.service[0] : null,
766
- environment
767
- ),
768
- pipelineId: pipelineIds[0]
769
- };
770
- }
771
-
772
- function selectIndexedPipelineForService(fields, environment, serviceName) {
773
- if (!serviceName) return null;
774
-
775
- const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
776
- const serviceIndex = services.indexOf(serviceName);
777
- if (serviceIndex < 0) return null;
778
-
779
- for (const key of getFieldKeysForEnvironment(environment)) {
780
- const values = Array.isArray(fields[key]) ? fields[key] : [];
781
- const parsed = parsePipelineDescriptor(values[serviceIndex]);
782
- if (parsed?.pipelineId) {
783
- return parsed;
1066
+ for (const candidate of candidates) {
1067
+ if (candidate?.pipelineId && matchesPipelineName(candidate.pipelineName, environment, serviceName)) {
1068
+ return candidate;
784
1069
  }
785
1070
  }
786
-
787
1071
  return null;
788
1072
  }
789
1073
 
@@ -824,10 +1108,11 @@ function parsePipelineDescriptor(value) {
824
1108
  function getEnvLookupOrder(environment) {
825
1109
  const normalized = normalizeEnvironment(environment || '');
826
1110
  if (normalized === 'prod') return ['prod'];
827
- if (normalized === 'gray') return ['gray', 'prod'];
1111
+ if (normalized === 'gray') return ['gray'];
828
1112
  if (normalized === 'uat') return ['uat'];
829
- if (normalized === 'test') return ['test', 'dev', 'uat'];
830
- return ['uat', 'test', 'dev', 'gray', 'prod'];
1113
+ if (normalized === 'test') return ['test', 'dev'];
1114
+ if (normalized === 'dev') return ['dev'];
1115
+ return [];
831
1116
  }
832
1117
 
833
1118
  function buildPrefixedPipelineName(serviceName, environment) {
@@ -838,6 +1123,24 @@ function buildPrefixedPipelineName(serviceName, environment) {
838
1123
  return `${prefix}-${serviceName}`;
839
1124
  }
840
1125
 
1126
+ function matchesPipelineName(pipelineName, environment, serviceName) {
1127
+ if (!pipelineName || !serviceName) return false;
1128
+
1129
+ const normalized = String(pipelineName).toLowerCase();
1130
+ const candidatePrefixes = getAllowedPipelinePrefixes(environment);
1131
+ return candidatePrefixes.some((prefix) => normalized === `${prefix}-${serviceName}`.toLowerCase());
1132
+ }
1133
+
1134
+ function getAllowedPipelinePrefixes(environment) {
1135
+ const normalized = normalizeEnvironment(environment || '');
1136
+ if (normalized === 'test') return ['test', 'dev'];
1137
+ if (normalized === 'dev') return ['dev'];
1138
+ if (normalized === 'uat') return ['uat'];
1139
+ if (normalized === 'gray') return ['gray'];
1140
+ if (normalized === 'prod') return ['prod'];
1141
+ return [];
1142
+ }
1143
+
841
1144
  function inferEnvironmentFromPipelineName(pipelineName) {
842
1145
  const match = String(pipelineName || '').match(/^(test|dev|uat|gray|prod)-/i);
843
1146
  return match ? (ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1])) : null;
@@ -866,6 +1169,32 @@ function collectNestedValues(payload, values = []) {
866
1169
  return values;
867
1170
  }
868
1171
 
1172
+ function buildUnresolvedDeployResult(workflow, params, input, context) {
1173
+ const target = buildWorkflowTargetLabel(params) || buildDeployTargetLabel(context) || 'deployment';
1174
+ return {
1175
+ type: 'error',
1176
+ success: false,
1177
+ skill: 'auto-flow',
1178
+ message: `Unable to resolve deployment target for ${target}. Specify an exact environment or pipelineId.`,
1179
+ data: {
1180
+ action: 'deploy',
1181
+ provider: workflow.provider,
1182
+ binding: workflow.binding,
1183
+ status: 'not_resolved',
1184
+ mcpServer: getWorkflowMcpServer(workflow),
1185
+ target,
1186
+ environment: params.environment || null,
1187
+ resolution: {
1188
+ source: 'not-resolved',
1189
+ serviceNames: params.serviceNames || [],
1190
+ environment: params.environment || null
1191
+ }
1192
+ },
1193
+ input,
1194
+ timestamp: new Date().toISOString()
1195
+ };
1196
+ }
1197
+
869
1198
  function compactObject(value) {
870
1199
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
871
1200
  }