agentweaver 0.1.3 → 0.1.4

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.
@@ -483,32 +483,62 @@ export class InteractiveUi {
483
483
  const lines = [flow.label, ""];
484
484
  for (const item of this.visiblePhaseItems(flow, flowState)) {
485
485
  if (item.kind === "group") {
486
- lines.push(`${this.symbolForGroup(flow.id, item.phases, flowState)} ${item.label}`);
487
- for (const phase of item.phases) {
486
+ const visiblePhases = item.phases.filter((phase) => this.shouldDisplayPhase(flow, flowState, phase));
487
+ if (visiblePhases.length === 0) {
488
+ continue;
489
+ }
490
+ lines.push(`${this.symbolForGroup(flow.id, flow, visiblePhases, flowState)} ${item.label}`);
491
+ for (const phase of visiblePhases) {
488
492
  const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
489
- lines.push(` ${this.symbolForStatus(flow.id, phaseState?.status ?? "pending")} ${this.displayPhaseId(phase)}`);
493
+ const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
494
+ lines.push(` ${this.symbolForStatus(flow.id, phaseStatus)} ${this.displayPhaseId(phase)}`);
490
495
  for (const step of phase.steps) {
491
496
  const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
492
- lines.push(` ${this.symbolForStatus(flow.id, stepState?.status ?? "pending")} ${step.id}`);
497
+ const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
498
+ lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${step.id}`);
493
499
  }
494
500
  }
495
501
  lines.push("");
496
502
  continue;
497
503
  }
498
504
  const phase = item.phase;
505
+ if (!this.shouldDisplayPhase(flow, flowState, phase)) {
506
+ continue;
507
+ }
499
508
  const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
500
- lines.push(`${this.symbolForStatus(flow.id, phaseState?.status ?? "pending")} ${phase.id}`);
509
+ const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
510
+ lines.push(`${this.symbolForStatus(flow.id, phaseStatus)} ${this.displayPhaseId(phase)}`);
501
511
  for (const step of phase.steps) {
502
512
  const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
503
- lines.push(` ${this.symbolForStatus(flow.id, stepState?.status ?? "pending")} ${step.id}`);
513
+ const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
514
+ lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${step.id}`);
504
515
  }
505
516
  lines.push("");
506
517
  }
507
518
  if (flowState?.terminated) {
508
- lines.push(`Stopped: ${flowState.terminationReason ?? "flow terminated"}`);
519
+ lines.push(`✓ Flow completed successfully`);
520
+ lines.push(`Reason: ${flowState.terminationReason ?? "flow terminated"}`);
509
521
  }
510
522
  this.progress.setContent(lines.join("\n").trimEnd());
511
523
  }
524
+ displayStatusForPhase(flowState, flow, phase, actualStatus) {
525
+ if (actualStatus) {
526
+ return actualStatus;
527
+ }
528
+ if (!flowState?.terminated) {
529
+ return "pending";
530
+ }
531
+ return this.isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
532
+ }
533
+ displayStatusForStep(flowState, flow, phase, actualStatus) {
534
+ if (actualStatus) {
535
+ return actualStatus;
536
+ }
537
+ if (!flowState?.terminated) {
538
+ return "pending";
539
+ }
540
+ return this.isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
541
+ }
512
542
  symbolForStatus(flowId, status) {
513
543
  if (status === "done") {
514
544
  return "✓";
@@ -522,8 +552,8 @@ export class InteractiveUi {
522
552
  }
523
553
  return "○";
524
554
  }
525
- symbolForGroup(flowId, phases, flowState) {
526
- const statuses = phases.map((phase) => flowState?.phases.find((candidate) => candidate.id === phase.id)?.status ?? "pending");
555
+ symbolForGroup(flowId, flow, phases, flowState) {
556
+ const statuses = phases.map((phase) => this.displayStatusForPhase(flowState, flow, phase, flowState?.phases.find((candidate) => candidate.id === phase.id)?.status ?? null));
527
557
  if (statuses.some((status) => status === "running")) {
528
558
  return this.symbolForStatus(flowId, "running");
529
559
  }
@@ -568,9 +598,13 @@ export class InteractiveUi {
568
598
  const pendingSeries = new Set();
569
599
  return this.groupPhases(flow).filter((item) => {
570
600
  if (item.kind === "phase") {
571
- return true;
601
+ return this.shouldDisplayPhase(flow, flowState, item.phase);
602
+ }
603
+ const visiblePhases = item.phases.filter((phase) => this.shouldDisplayPhase(flow, flowState, phase));
604
+ const hasState = visiblePhases.some((phase) => flowState?.phases.some((candidate) => candidate.id === phase.id));
605
+ if (visiblePhases.length === 0) {
606
+ return false;
572
607
  }
573
- const hasState = item.phases.some((phase) => flowState?.phases.some((candidate) => candidate.id === phase.id));
574
608
  if (hasState) {
575
609
  return true;
576
610
  }
@@ -585,6 +619,42 @@ export class InteractiveUi {
585
619
  const entries = Object.entries(repeatVars).sort(([left], [right]) => left.localeCompare(right));
586
620
  return JSON.stringify(entries);
587
621
  }
622
+ shouldDisplayPhase(flow, flowState, phase) {
623
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id) ?? null;
624
+ if (!flowState) {
625
+ if (Object.keys(phase.repeatVars).length > 0) {
626
+ return false;
627
+ }
628
+ return !this.hasPreviousRepeatPhase(flow, phase);
629
+ }
630
+ if (Object.keys(phase.repeatVars).length === 0) {
631
+ if (!phaseState) {
632
+ return false;
633
+ }
634
+ if (phaseState?.status === "skipped" && flowState.terminated && this.isAfterTermination(flowState, flow, phase)) {
635
+ return false;
636
+ }
637
+ return true;
638
+ }
639
+ if (!phaseState) {
640
+ return false;
641
+ }
642
+ if (phaseState.status === "skipped" && flowState.terminated && this.isAfterTermination(flowState, flow, phase)) {
643
+ return false;
644
+ }
645
+ return true;
646
+ }
647
+ hasPreviousRepeatPhase(flow, phase) {
648
+ for (const candidate of flow.phases) {
649
+ if (candidate.id === phase.id) {
650
+ return false;
651
+ }
652
+ if (Object.keys(candidate.repeatVars).length > 0) {
653
+ return true;
654
+ }
655
+ }
656
+ return false;
657
+ }
588
658
  repeatSeriesKey(phases) {
589
659
  const repeatVarNames = Object.keys(phases[0]?.repeatVars ?? {}).sort();
590
660
  const phaseNames = phases.map((phase) => this.displayPhaseId(phase));
@@ -594,7 +664,7 @@ export class InteractiveUi {
594
664
  });
595
665
  }
596
666
  repeatLabel(repeatVars) {
597
- const entries = Object.entries(repeatVars);
667
+ const entries = Object.entries(repeatVars).filter(([key]) => !key.endsWith("_minus_one"));
598
668
  if (entries.length === 0) {
599
669
  return null;
600
670
  }
@@ -606,7 +676,10 @@ export class InteractiveUi {
606
676
  }
607
677
  displayPhaseId(phase) {
608
678
  let result = phase.id;
609
- for (const value of Object.values(phase.repeatVars)) {
679
+ const values = Object.entries(phase.repeatVars)
680
+ .filter(([key]) => !key.endsWith("_minus_one"))
681
+ .map(([, value]) => value);
682
+ for (const value of values) {
610
683
  const suffix = `_${String(value)}`;
611
684
  if (result.endsWith(suffix)) {
612
685
  result = result.slice(0, -suffix.length);
@@ -614,6 +687,20 @@ export class InteractiveUi {
614
687
  }
615
688
  return result;
616
689
  }
690
+ isAfterTermination(flowState, flow, phase) {
691
+ const terminationReason = flowState.terminationReason ?? "";
692
+ const match = /^Stopped by ([^:]+):/.exec(terminationReason);
693
+ if (!match) {
694
+ return false;
695
+ }
696
+ const stoppedPhaseId = match[1];
697
+ const stoppedIndex = flow.phases.findIndex((candidate) => candidate.id === stoppedPhaseId);
698
+ const currentIndex = flow.phases.findIndex((candidate) => candidate.id === phase.id);
699
+ if (stoppedIndex < 0 || currentIndex < 0) {
700
+ return false;
701
+ }
702
+ return currentIndex > stoppedIndex;
703
+ }
617
704
  openConfirm() {
618
705
  const flow = this.flowMap.get(this.selectedFlowId);
619
706
  if (!flow) {
@@ -1,5 +1,6 @@
1
1
  import { requireArtifacts } from "../artifacts.js";
2
2
  import { TaskRunnerError } from "../errors.js";
3
+ import { validateStructuredArtifacts } from "../structured-artifacts.js";
3
4
  export function runNodeChecks(checks) {
4
5
  for (const check of checks) {
5
6
  if (check.kind === "require-artifacts") {
@@ -10,6 +11,10 @@ export function runNodeChecks(checks) {
10
11
  requireArtifacts([check.path], check.message);
11
12
  continue;
12
13
  }
14
+ if (check.kind === "require-structured-artifacts") {
15
+ validateStructuredArtifacts(check.items, check.message);
16
+ continue;
17
+ }
13
18
  throw new TaskRunnerError(`Unsupported node check: ${check.kind ?? "unknown"}`);
14
19
  }
15
20
  }
@@ -76,6 +76,22 @@ function resolveExpectation(expectation, context) {
76
76
  message: expectation.message,
77
77
  };
78
78
  }
79
+ if (expectation.kind === "require-structured-artifacts") {
80
+ return {
81
+ kind: "require-structured-artifacts",
82
+ items: expectation.items.map((item) => {
83
+ const value = resolveValue(item.path, context);
84
+ if (typeof value !== "string") {
85
+ throw new TaskRunnerError("Expectation 'require-structured-artifacts' item path must resolve to string");
86
+ }
87
+ return {
88
+ path: value,
89
+ schemaId: item.schemaId,
90
+ };
91
+ }),
92
+ message: expectation.message,
93
+ };
94
+ }
79
95
  if (expectation.kind === "require-file") {
80
96
  const value = resolveValue(expectation.path, context);
81
97
  if (typeof value !== "string") {
@@ -123,11 +123,25 @@
123
123
  "id": "test_after_implement",
124
124
  "steps": [
125
125
  {
126
- "id": "verify_build_after_implement",
127
- "node": "verify-build",
126
+ "id": "run_linter_loop_after_implement",
127
+ "node": "flow-run",
128
128
  "params": {
129
+ "fileName": { "const": "run-linter-loop.json" },
130
+ "labelText": { "const": "Running run-linter-loop after implement" },
131
+ "taskKey": { "ref": "params.taskKey" },
129
132
  "dockerComposeFile": { "ref": "params.dockerComposeFile" },
130
- "labelText": { "const": "Running build verification in isolated Docker" }
133
+ "extraPrompt": { "ref": "params.extraPrompt" }
134
+ }
135
+ },
136
+ {
137
+ "id": "run_tests_loop_after_implement",
138
+ "node": "flow-run",
139
+ "params": {
140
+ "fileName": { "const": "run-tests-loop.json" },
141
+ "labelText": { "const": "Running run-tests-loop after implement" },
142
+ "taskKey": { "ref": "params.taskKey" },
143
+ "dockerComposeFile": { "ref": "params.dockerComposeFile" },
144
+ "extraPrompt": { "ref": "params.extraPrompt" }
131
145
  }
132
146
  }
133
147
  ]
@@ -0,0 +1,140 @@
1
+ {
2
+ "kind": "bug-analyze-flow",
3
+ "version": 1,
4
+ "phases": [
5
+ {
6
+ "id": "bug_analyze",
7
+ "steps": [
8
+ {
9
+ "id": "fetch_jira",
10
+ "node": "jira-fetch",
11
+ "params": {
12
+ "jiraApiUrl": { "ref": "params.jiraApiUrl" },
13
+ "outputFile": {
14
+ "artifact": {
15
+ "kind": "jira-task-file",
16
+ "taskKey": { "ref": "params.taskKey" }
17
+ }
18
+ }
19
+ },
20
+ "expect": [
21
+ {
22
+ "kind": "require-file",
23
+ "path": {
24
+ "artifact": {
25
+ "kind": "jira-task-file",
26
+ "taskKey": { "ref": "params.taskKey" }
27
+ }
28
+ },
29
+ "message": "Jira fetch node did not produce the Jira task file."
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "id": "run_codex_bug_analyze",
35
+ "node": "codex-local-prompt",
36
+ "prompt": {
37
+ "templateRef": "bug-analyze",
38
+ "vars": {
39
+ "jira_task_file": {
40
+ "artifact": {
41
+ "kind": "jira-task-file",
42
+ "taskKey": { "ref": "params.taskKey" }
43
+ }
44
+ },
45
+ "bug_analyze_file": {
46
+ "artifact": {
47
+ "kind": "bug-analyze-file",
48
+ "taskKey": { "ref": "params.taskKey" }
49
+ }
50
+ },
51
+ "bug_analyze_json_file": {
52
+ "artifact": {
53
+ "kind": "bug-analyze-json-file",
54
+ "taskKey": { "ref": "params.taskKey" }
55
+ }
56
+ },
57
+ "bug_fix_design_file": {
58
+ "artifact": {
59
+ "kind": "bug-fix-design-file",
60
+ "taskKey": { "ref": "params.taskKey" }
61
+ }
62
+ },
63
+ "bug_fix_design_json_file": {
64
+ "artifact": {
65
+ "kind": "bug-fix-design-json-file",
66
+ "taskKey": { "ref": "params.taskKey" }
67
+ }
68
+ },
69
+ "bug_fix_plan_file": {
70
+ "artifact": {
71
+ "kind": "bug-fix-plan-file",
72
+ "taskKey": { "ref": "params.taskKey" }
73
+ }
74
+ },
75
+ "bug_fix_plan_json_file": {
76
+ "artifact": {
77
+ "kind": "bug-fix-plan-json-file",
78
+ "taskKey": { "ref": "params.taskKey" }
79
+ }
80
+ }
81
+ },
82
+ "extraPrompt": { "ref": "params.extraPrompt" },
83
+ "format": "task-prompt"
84
+ },
85
+ "params": {
86
+ "labelText": { "const": "Running Codex bug analysis mode" },
87
+ "model": { "const": "gpt-5.4" }
88
+ },
89
+ "expect": [
90
+ {
91
+ "kind": "require-artifacts",
92
+ "when": { "not": { "ref": "context.dryRun" } },
93
+ "paths": {
94
+ "artifactList": {
95
+ "kind": "bug-analyze-artifacts",
96
+ "taskKey": { "ref": "params.taskKey" }
97
+ }
98
+ },
99
+ "message": "Bug analyze mode did not produce the required artifacts."
100
+ },
101
+ {
102
+ "kind": "require-structured-artifacts",
103
+ "when": { "not": { "ref": "context.dryRun" } },
104
+ "items": [
105
+ {
106
+ "path": {
107
+ "artifact": {
108
+ "kind": "bug-analyze-json-file",
109
+ "taskKey": { "ref": "params.taskKey" }
110
+ }
111
+ },
112
+ "schemaId": "bug-analysis/v1"
113
+ },
114
+ {
115
+ "path": {
116
+ "artifact": {
117
+ "kind": "bug-fix-design-json-file",
118
+ "taskKey": { "ref": "params.taskKey" }
119
+ }
120
+ },
121
+ "schemaId": "bug-fix-design/v1"
122
+ },
123
+ {
124
+ "path": {
125
+ "artifact": {
126
+ "kind": "bug-fix-plan-json-file",
127
+ "taskKey": { "ref": "params.taskKey" }
128
+ }
129
+ },
130
+ "schemaId": "bug-fix-plan/v1"
131
+ }
132
+ ],
133
+ "message": "Bug analyze mode produced invalid structured artifacts."
134
+ }
135
+ ]
136
+ }
137
+ ]
138
+ }
139
+ ]
140
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "kind": "bug-fix-flow",
3
+ "version": 1,
4
+ "phases": [
5
+ {
6
+ "id": "bug_fix",
7
+ "steps": [
8
+ {
9
+ "id": "run_codex_bug_fix",
10
+ "node": "codex-local-prompt",
11
+ "prompt": {
12
+ "templateRef": "bug-fix",
13
+ "vars": {
14
+ "bug_analyze_json_file": {
15
+ "artifact": {
16
+ "kind": "bug-analyze-json-file",
17
+ "taskKey": { "ref": "params.taskKey" }
18
+ }
19
+ },
20
+ "bug_fix_design_json_file": {
21
+ "artifact": {
22
+ "kind": "bug-fix-design-json-file",
23
+ "taskKey": { "ref": "params.taskKey" }
24
+ }
25
+ },
26
+ "bug_fix_plan_json_file": {
27
+ "artifact": {
28
+ "kind": "bug-fix-plan-json-file",
29
+ "taskKey": { "ref": "params.taskKey" }
30
+ }
31
+ }
32
+ },
33
+ "extraPrompt": { "ref": "params.extraPrompt" },
34
+ "format": "task-prompt"
35
+ },
36
+ "params": {
37
+ "labelText": { "const": "Running Codex bug fix mode locally" },
38
+ "model": { "const": "gpt-5.4" }
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "kind": "mr-description-flow",
3
+ "version": 1,
4
+ "phases": [
5
+ {
6
+ "id": "mr_description",
7
+ "steps": [
8
+ {
9
+ "id": "run_codex_mr_description",
10
+ "node": "codex-local-prompt",
11
+ "prompt": {
12
+ "templateRef": "mr-description",
13
+ "vars": {
14
+ "jira_task_file": {
15
+ "artifact": {
16
+ "kind": "jira-task-file",
17
+ "taskKey": { "ref": "params.taskKey" }
18
+ }
19
+ },
20
+ "mr_description_file": {
21
+ "artifact": {
22
+ "kind": "mr-description-file",
23
+ "taskKey": { "ref": "params.taskKey" }
24
+ }
25
+ }
26
+ },
27
+ "extraPrompt": { "ref": "params.extraPrompt" },
28
+ "format": "task-prompt"
29
+ },
30
+ "params": {
31
+ "labelText": { "const": "Running Codex MR description mode" },
32
+ "model": { "const": "gpt-5.4" },
33
+ "requiredArtifacts": {
34
+ "list": [
35
+ {
36
+ "artifact": {
37
+ "kind": "mr-description-file",
38
+ "taskKey": { "ref": "params.taskKey" }
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ },
44
+ "expect": [
45
+ {
46
+ "kind": "require-file",
47
+ "when": { "not": { "ref": "context.dryRun" } },
48
+ "path": {
49
+ "artifact": {
50
+ "kind": "mr-description-file",
51
+ "taskKey": { "ref": "params.taskKey" }
52
+ }
53
+ },
54
+ "message": "MR description mode did not produce the MR description artifact."
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ ]
61
+ }
@@ -0,0 +1,149 @@
1
+ {
2
+ "kind": "run-linter-loop-flow",
3
+ "version": 1,
4
+ "phases": [
5
+ {
6
+ "id": "run_linter_try_1",
7
+ "steps": [
8
+ {
9
+ "id": "run_linter",
10
+ "node": "verify-build",
11
+ "params": {
12
+ "dockerComposeFile": { "ref": "params.dockerComposeFile" },
13
+ "labelText": {
14
+ "const": "Running run_linter.sh in isolated Docker (attempt 1)"
15
+ },
16
+ "service": { "const": "run-linter" }
17
+ },
18
+ "stopFlowIf": {
19
+ "equals": [
20
+ { "ref": "steps.run_linter_try_1.run_linter.outputs.parsed.ok" },
21
+ { "const": true }
22
+ ]
23
+ }
24
+ },
25
+ {
26
+ "id": "fix_linter",
27
+ "when": {
28
+ "equals": [
29
+ { "ref": "steps.run_linter_try_1.run_linter.outputs.parsed.ok" },
30
+ { "const": false }
31
+ ]
32
+ },
33
+ "node": "codex-local-prompt",
34
+ "prompt": {
35
+ "templateRef": "run-linter-loop-fix",
36
+ "extraPrompt": {
37
+ "appendPrompt": {
38
+ "base": { "ref": "params.extraPrompt" },
39
+ "suffix": {
40
+ "template": "Последний результат run_linter.sh: exitCode={exit_code}, summary={summary}. Исправь только то, что мешает успешному прохождению проверки.",
41
+ "vars": {
42
+ "exit_code": { "ref": "steps.run_linter_try_1.run_linter.outputs.parsed.exitCode" },
43
+ "summary": { "ref": "steps.run_linter_try_1.run_linter.outputs.parsed.summary" }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "format": "task-prompt"
49
+ },
50
+ "params": {
51
+ "labelText": {
52
+ "const": "Running Codex linter loop fix (attempt 1)"
53
+ },
54
+ "model": { "const": "gpt-5.4" }
55
+ }
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ "repeat": {
61
+ "var": "attempt",
62
+ "from": 2,
63
+ "to": 5
64
+ },
65
+ "phases": [
66
+ {
67
+ "id": "run_linter_try_${attempt}",
68
+ "when": {
69
+ "equals": [
70
+ { "ref": "steps.run_linter_try_${attempt_minus_one}.run_linter.outputs.parsed.ok" },
71
+ { "const": false }
72
+ ]
73
+ },
74
+ "steps": [
75
+ {
76
+ "id": "run_linter",
77
+ "node": "verify-build",
78
+ "params": {
79
+ "dockerComposeFile": { "ref": "params.dockerComposeFile" },
80
+ "labelText": {
81
+ "template": "Running run_linter.sh in isolated Docker (attempt ${attempt})"
82
+ },
83
+ "service": { "const": "run-linter" }
84
+ },
85
+ "stopFlowIf": {
86
+ "equals": [
87
+ { "ref": "steps.run_linter_try_${attempt}.run_linter.outputs.parsed.ok" },
88
+ { "const": true }
89
+ ]
90
+ }
91
+ },
92
+ {
93
+ "id": "fix_linter",
94
+ "when": {
95
+ "equals": [
96
+ { "ref": "steps.run_linter_try_${attempt}.run_linter.outputs.parsed.ok" },
97
+ { "const": false }
98
+ ]
99
+ },
100
+ "node": "codex-local-prompt",
101
+ "prompt": {
102
+ "templateRef": "run-linter-loop-fix",
103
+ "extraPrompt": {
104
+ "appendPrompt": {
105
+ "base": { "ref": "params.extraPrompt" },
106
+ "suffix": {
107
+ "template": "Последний результат run_linter.sh: exitCode={exit_code}, summary={summary}. Исправь только то, что мешает успешному прохождению проверки.",
108
+ "vars": {
109
+ "exit_code": { "ref": "steps.run_linter_try_${attempt}.run_linter.outputs.parsed.exitCode" },
110
+ "summary": { "ref": "steps.run_linter_try_${attempt}.run_linter.outputs.parsed.summary" }
111
+ }
112
+ }
113
+ }
114
+ },
115
+ "format": "task-prompt"
116
+ },
117
+ "params": {
118
+ "labelText": {
119
+ "template": "Running Codex linter loop fix (attempt ${attempt})"
120
+ },
121
+ "model": { "const": "gpt-5.4" }
122
+ }
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ },
128
+ {
129
+ "id": "run_linter_failed",
130
+ "steps": [
131
+ {
132
+ "id": "assert_run_linter_success",
133
+ "node": "file-check",
134
+ "params": {
135
+ "path": { "ref": "params.dockerComposeFile" }
136
+ },
137
+ "expect": [
138
+ {
139
+ "kind": "step-output",
140
+ "value": { "ref": "steps.run_linter_try_5.run_linter.outputs.parsed.ok" },
141
+ "equals": { "const": true },
142
+ "message": "run-linter-loop exhausted all attempts without a successful run_linter.sh execution."
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ }
148
+ ]
149
+ }