amsd-pipeline-shared 0.5.0 → 0.5.2
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/docs/gateway-baseline-2.0.80.md5 +47 -0
- package/docs/gateway-files.txt +47 -0
- package/orchestrations/config/repo-artifacts.json +23 -0
- package/orchestrations/config/resume-preserves.json +25 -0
- package/orchestrations/config/run-modes.json +47 -0
- package/orchestrations/scripts/lib/checkpoints.sh +175 -0
- package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
- package/orchestrations/scripts/lib/codeline-packages.js +67 -0
- package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
- package/orchestrations/scripts/lib/common.sh +66 -0
- package/orchestrations/scripts/lib/control-plane.sh +506 -0
- package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
- package/orchestrations/scripts/lib/engine-paths.sh +52 -0
- package/orchestrations/scripts/lib/flags.sh +38 -0
- package/orchestrations/scripts/lib/git-ops.sh +975 -0
- package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
- package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
- package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
- package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
- package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
- package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
- package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
- package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
- package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
- package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
- package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
- package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
- package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
- package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
- package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
- package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
- package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
- package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
- package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
- package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
- package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
- package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
- package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
- package/orchestrations/scripts/lib/phase-exit.sh +90 -0
- package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
- package/orchestrations/scripts/lib/plan-hash.js +19 -0
- package/orchestrations/scripts/lib/plan-scope.js +73 -0
- package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
- package/orchestrations/scripts/lib/release-lines.sh +166 -0
- package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
- package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
- package/orchestrations/scripts/lib/run-modes.sh +93 -0
- package/orchestrations/scripts/lib/story-outputs.sh +322 -0
- package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
- package/package.json +1 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# control-plane.sh — moved verbatim out of run-agent-orchestration.sh by tools/split-main-into-modules.py
|
|
3
|
+
# (15 functions). Sourced by run-agent-orchestration.sh; SCRIPT_DIR and the globals it sets
|
|
4
|
+
# are in scope exactly as they were. A move, not an edit: every body is byte-identical to
|
|
5
|
+
# the golden recorded at the move (see the identity test).
|
|
6
|
+
|
|
7
|
+
# is_parent — true in the top-level orchestrator process. Guard project-wide work with this:
|
|
8
|
+
# anything that allocates a run-global resource, mutates shared state, or must happen exactly
|
|
9
|
+
# once per run regardless of how many codelines are in scope.
|
|
10
|
+
is_parent() { [ "$(orch_role)" = 'parent' ]; }
|
|
11
|
+
|
|
12
|
+
# REGISTERED HERE, NOT IN cleanup() — BECAUSE bash INSTALLS A HANDLER WHEN IT REACHES IT.
|
|
13
|
+
#
|
|
14
|
+
# The export used to ride along inside cleanup(), which is registered at the foot of this file.
|
|
15
|
+
# A run only ever had a cassette handler installed if it survived that far. The first `exit` in
|
|
16
|
+
# this script is thousands of lines earlier, so ingest, codeline discovery, spec-mode, the mint
|
|
17
|
+
# and roster derivation all failed into a trap that did not yet include the export.
|
|
18
|
+
#
|
|
19
|
+
# Measured 2026-09-11 by the free mockserver rehearsal: roster derivation failed, the write
|
|
20
|
+
# perimeter released (registered at the top of this file, so it was installed), cleanup never
|
|
21
|
+
# ran, and the run left nothing to replay. An early structural failure is exactly the run worth
|
|
22
|
+
# replaying, and it was the one guaranteed to leave no recording.
|
|
23
|
+
#
|
|
24
|
+
# Registered beside the recorder it uses: reaching the source line is the only precondition.
|
|
25
|
+
_epam_export_cassette() {
|
|
26
|
+
declare -F export_run_cassette >/dev/null 2>&1 || return 0
|
|
27
|
+
local _cas_run_id="${ORCH_RUN_ID:-${EPAM_RUN_ID:-${RUN_NUMBER:-}}}"
|
|
28
|
+
[ -n "$_cas_run_id" ] || return 0
|
|
29
|
+
export_run_cassette "$_cas_run_id" \
|
|
30
|
+
"$(basename "${EPAM_PROJECT_CONFIG_DIR:-project}")" \
|
|
31
|
+
"${EPAM_CASSETTE_DIR:-${AUTOMATION_DIR:-$SCRIPT_DIR/..}/cassettes}" || true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_resolve_control_plane_port() {
|
|
35
|
+
# Reads only the environment and the PRD — plus is_parent(), the single place the
|
|
36
|
+
# parent/lane role is derived. A harness testing this in isolation must carry those
|
|
37
|
+
# helpers with it; duplicating the role check here would be the very thing they exist
|
|
38
|
+
# to prevent.
|
|
39
|
+
local _base="${CONTROL_PLANE_BASE_PORT:-8094}"
|
|
40
|
+
# An explicit override always wins — the operator can pin a port.
|
|
41
|
+
if [ -n "${CONTROL_PLANE_PORT:-}" ]; then
|
|
42
|
+
printf '%s' "$CONTROL_PLANE_PORT"; return 0
|
|
43
|
+
fi
|
|
44
|
+
# THE PARENT AND ITS LANES ARE THE SAME SCRIPT, SO THEY MUST NOT DERIVE THE SAME PORT.
|
|
45
|
+
#
|
|
46
|
+
# This script is both the parent orchestrator and, re-invoked with JIRA_CODELINE_RUN=1,
|
|
47
|
+
# each lane — so every per-run resource is allocated twice. The lane is derived from the
|
|
48
|
+
# codeline whose outputDirs[].path matches project.outputDir, and the synthesizer sets
|
|
49
|
+
# project.outputDir = outputDirs[0].path, so the PARENT resolved to codeline index 0 —
|
|
50
|
+
# exactly what the FIRST LANE resolves to from its own filtered PRD.
|
|
51
|
+
#
|
|
52
|
+
# That collision is not benign: start_control_plane kills whatever already holds the port
|
|
53
|
+
# ("a stale process from a previous run") before binding. The first lane therefore killed
|
|
54
|
+
# the parent's control plane and took the port, and when the lane finished its cleanup
|
|
55
|
+
# stopped the control plane entirely — leaving the port dead while the parent still held a
|
|
56
|
+
# PID it believed was live.
|
|
57
|
+
#
|
|
58
|
+
# The parent reserves the base port. Lanes are offset past it, so no lane can ever land on
|
|
59
|
+
# it. Lanes keep their own control plane — each has its own LOG_DIR to serve.
|
|
60
|
+
if is_parent; then
|
|
61
|
+
printf '%s' "$_base"; return 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# EPAM_CODELINE is what the lane invocation actually exports; CODELINE_NAME is kept as the
|
|
65
|
+
# documented override. Reading only the latter meant every lane fell through to the PRD
|
|
66
|
+
# lookup, since nothing in this script has ever set it.
|
|
67
|
+
local _lane="${CODELINE_NAME:-${EPAM_CODELINE:-}}"
|
|
68
|
+
if [ -z "$_lane" ] && [ -n "${PRD_FILE:-}" ] && [ -f "${PRD_FILE}" ]; then
|
|
69
|
+
_lane=$(jq -r '.project as $p | (($p.outputDirs // []) | map(select(.path == $p.outputDir)) | .[0].codeline) // empty' \
|
|
70
|
+
"$PRD_FILE" 2>/dev/null)
|
|
71
|
+
fi
|
|
72
|
+
if [ -z "$_lane" ]; then
|
|
73
|
+
printf '%s' "$_base"; return 0
|
|
74
|
+
fi
|
|
75
|
+
local _idx=""
|
|
76
|
+
if [ -n "${PRD_FILE:-}" ] && [ -f "${PRD_FILE}" ]; then
|
|
77
|
+
_idx=$(jq -r --arg cl "$_lane" '((.project.outputDirs // []) | map(.codeline) | index($cl)) // empty' \
|
|
78
|
+
"$PRD_FILE" 2>/dev/null)
|
|
79
|
+
fi
|
|
80
|
+
if [ -z "$_idx" ] || [ "$_idx" = "null" ]; then
|
|
81
|
+
_idx=$(printf '%s' "$_lane" | cksum | awk '{print $1 % 64}')
|
|
82
|
+
fi
|
|
83
|
+
# +1 so lane 0 clears the parent's reserved base port.
|
|
84
|
+
printf '%s' "$(( _base + 1 + _idx ))"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
step_emit() {
|
|
88
|
+
local step_id="$1"
|
|
89
|
+
local status="$2"
|
|
90
|
+
local label="$3"
|
|
91
|
+
local reason="${4:-}"
|
|
92
|
+
local ts
|
|
93
|
+
ts=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)
|
|
94
|
+
|
|
95
|
+
_STEP_LABELS["$step_id"]="$label"
|
|
96
|
+
_STEP_STATUS["$step_id"]="$status"
|
|
97
|
+
# Only overwrite the stored reason when this call actually supplied one —
|
|
98
|
+
# many terminal-state calls (e.g. the final "pass") are emitted right
|
|
99
|
+
# after a "running" call with no reason, and would otherwise blank out
|
|
100
|
+
# a real detail a caller set moments earlier (found while wiring real
|
|
101
|
+
# per-step detail through to the dashboard, 2026-07-13).
|
|
102
|
+
[ -n "$reason" ] && _STEP_REASON["$step_id"]="$reason"
|
|
103
|
+
|
|
104
|
+
local icon
|
|
105
|
+
case "$status" in
|
|
106
|
+
pass) icon="${GREEN} ✓${NC}" ;;
|
|
107
|
+
skip) icon="${YELLOW} ⊘${NC}" ;;
|
|
108
|
+
fail) icon="${RED} ✗${NC}" ;;
|
|
109
|
+
warn) icon="${YELLOW} ⚠${NC}" ;;
|
|
110
|
+
running) icon="${CYAN} ▶${NC}" ;;
|
|
111
|
+
*) icon=" " ;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
114
|
+
local reason_str=""
|
|
115
|
+
[ -n "$reason" ] && reason_str=" ${YELLOW}[${reason}]${NC}"
|
|
116
|
+
echo -e "${icon} ${label}${reason_str}"
|
|
117
|
+
|
|
118
|
+
# Write JSON snapshot (atomic via tmp file)
|
|
119
|
+
local tmp_file="${STEP_STATUS_FILE}.tmp.$$"
|
|
120
|
+
{
|
|
121
|
+
echo "{"
|
|
122
|
+
echo " \"phase\": \"${PHASE:-unknown}\","
|
|
123
|
+
echo " \"updatedAt\": \"${ts}\","
|
|
124
|
+
echo " \"steps\": ["
|
|
125
|
+
local first=true
|
|
126
|
+
local _sid _slabel _sstatus _sreason
|
|
127
|
+
for _sid in \
|
|
128
|
+
"1:spec" "1a:openspec" "1b:speckit" "2:cpa" "3:skill-pre" "4:hybrid-coord" "5:regression" \
|
|
129
|
+
"6:mkdir" "7:model-coord" "8:main-stories" "9:auto-commit" "10:tc-writer" \
|
|
130
|
+
"11:skills-audit" "12:tools-audit" "13:worktrees" "14:primary" "15:independent" "16:wt-health" \
|
|
131
|
+
"17:wt-merge" "18:skill-post" "19:pre-review" "20:lint-gate" \
|
|
132
|
+
"21:review-stories" "22a:sast" "22b:spec-val" \
|
|
133
|
+
"22c:review-ranger" "22d:mutant-hunter" \
|
|
134
|
+
"22e:fuzz-weaver" "22f:perf-sentinel" "23:e2e"; do
|
|
135
|
+
local _key="${_sid%%:*}"
|
|
136
|
+
_slabel="$(_json_escape_str "${_STEP_LABELS[$_key]:-${_sid#*:}}")"
|
|
137
|
+
_sstatus="${_STEP_STATUS[$_key]:-pending}"
|
|
138
|
+
_sreason="$(_json_escape_str "${_STEP_REASON[$_key]:-}")"
|
|
139
|
+
[ "$first" = "true" ] && first=false || echo ","
|
|
140
|
+
printf ' {"id":"%s","label":"%s","status":"%s","detail":"%s"}' \
|
|
141
|
+
"$_key" "$_slabel" "$_sstatus" "$_sreason"
|
|
142
|
+
done
|
|
143
|
+
echo ""
|
|
144
|
+
echo " ]"
|
|
145
|
+
echo "}"
|
|
146
|
+
} > "$tmp_file" && mv "$tmp_file" "$STEP_STATUS_FILE" 2>/dev/null || true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Print full step checklist (called once at run start, after skip detection)
|
|
150
|
+
print_step_checklist() {
|
|
151
|
+
echo ""
|
|
152
|
+
echo -e "${MAGENTA}━━━ Pipeline Step Checklist ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
153
|
+
printf " %-6s %-32s %s\n" "Step" "Name" "Planned"
|
|
154
|
+
printf " %-6s %-32s %s\n" "------" "--------------------------------" "--------"
|
|
155
|
+
|
|
156
|
+
_checklist_row() {
|
|
157
|
+
local step="$1" name="$2" planned="$3" reason="${4:-}"
|
|
158
|
+
local color
|
|
159
|
+
case "$planned" in
|
|
160
|
+
ACTIVE) color="$GREEN" ;;
|
|
161
|
+
SKIP) color="$YELLOW" ;;
|
|
162
|
+
COND) color="$CYAN" ;;
|
|
163
|
+
*) color="$NC" ;;
|
|
164
|
+
esac
|
|
165
|
+
# A SWITCH ONLY MEANS SOMETHING ON A ROW THAT IS ACTUALLY OFF.
|
|
166
|
+
#
|
|
167
|
+
# The reason was printed unconditionally, and for the skip-toggle rows it is the variable
|
|
168
|
+
# that DISABLES the step — so a running gate rendered as
|
|
169
|
+
# 5 Regression guard ACTIVE (SKIP_REGRESSION_GUARD=true)
|
|
170
|
+
# which reads as "skipped, here is the proof". Eleven rows did this on every run: the
|
|
171
|
+
# status was computed and correct, and the text beside it contradicted the status.
|
|
172
|
+
#
|
|
173
|
+
# By SHAPE, not by a list of rows: several rows pass the resolved MODEL here, which is the
|
|
174
|
+
# most useful thing on the line and must survive. `NAME=value` is a switch; a model name
|
|
175
|
+
# never looks like one. So a switch is shown only when the row is not ACTIVE.
|
|
176
|
+
local reason_str=""
|
|
177
|
+
if [ -n "$reason" ] \
|
|
178
|
+
&& { [ "$planned" != "ACTIVE" ] || ! printf '%s' "$reason" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*='; }; then
|
|
179
|
+
reason_str=" (${reason})"
|
|
180
|
+
fi
|
|
181
|
+
printf " %-6s %-32s " "$step" "$name"
|
|
182
|
+
echo -e "${color}${planned}${reason_str}${NC}"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if [ -n "${RESOLVED_RUN_MODE:-}" ]; then
|
|
186
|
+
echo -e " ${CYAN}RUN MODE: ${RESOLVED_RUN_MODE}${NC} — the SKIP rows below are what it turns off"
|
|
187
|
+
fi
|
|
188
|
+
_checklist_row "1" "Specification pass" "$([ "${EPAM_SPEC_MODE:-1}" = "0" ] && echo SKIP || echo ACTIVE)" "EPAM_SPEC_MODE=0"
|
|
189
|
+
_checklist_row "1a" " openspec (elaboration)" "$([ "${EPAM_SPEC_MODE:-1}" = "0" ] && echo SKIP || echo ACTIVE)" "$(seam_model_or_fail "spec-agent" 2>/dev/null || printf '<unresolved>')"
|
|
190
|
+
_checklist_row "1b" " speckit (verification)" "$([ "${EPAM_SPEC_MODE:-1}" = "0" ] && echo SKIP || echo ACTIVE)" "$(seam_model_or_fail "spec-agent" 2>/dev/null || printf '<unresolved>')"
|
|
191
|
+
_checklist_row "2" "CPA pre-pass" "$(is_truthy "${SKIP_CPA:-}" && echo SKIP || echo ACTIVE)" "SKIP_CPA=1"
|
|
192
|
+
_checklist_row "3" "Pre-phase skill assess" "$(is_truthy "${SKIP_SKILL_ASSESSMENT:-}" && echo SKIP || echo ACTIVE)" "$(is_truthy "${SKIP_SKILL_ASSESSMENT:-}" && echo SKIP_SKILL_ASSESSMENT=1 || true)"
|
|
193
|
+
_checklist_row "4" "Hybrid pre-coord" "$([ "${RESOLVED_ORCH_MODE:-bash}" = "hybrid" ] && echo ACTIVE || echo SKIP)" "ORCH_MODE≠hybrid"
|
|
194
|
+
_checklist_row "5" "Regression guard" "$(is_truthy "${SKIP_REGRESSION_GUARD:-}" && echo SKIP || echo ACTIVE)" "SKIP_REGRESSION_GUARD=true"
|
|
195
|
+
_checklist_row "6" "mkdir src/ dirs" "ACTIVE"
|
|
196
|
+
_checklist_row "7" "PRD model coordinator" "$(is_truthy "${SKIP_PRD_MODEL_COORDINATOR:-}" && echo SKIP || echo ACTIVE)" "$(is_truthy "${SKIP_PRD_MODEL_COORDINATOR:-}" && echo SKIP_PRD_MODEL_COORDINATOR=1 || true)"
|
|
197
|
+
_checklist_row "8" "Main-branch stories" "ACTIVE"
|
|
198
|
+
_checklist_row "9" "Auto-commit" "COND" "if uncommitted changes"
|
|
199
|
+
_checklist_row "10" "TC writer gate" "$(is_truthy "${SKIP_TC_WRITER:-}" && echo SKIP || echo COND)" "SKIP_TC_WRITER=1 or no test stories"
|
|
200
|
+
_checklist_row "11" "Skills coordinator audit" "$(is_truthy "${SKIP_SKILLS_AUDIT:-}" && echo SKIP || echo ACTIVE)" "SKIP_SKILLS_AUDIT=1"
|
|
201
|
+
_checklist_row "12" "Tools coordinator audit" "$(is_truthy "${SKIP_TOOLS_AUDIT:-}" && echo SKIP || echo ACTIVE)" "SKIP_TOOLS_AUDIT=1"
|
|
202
|
+
_checklist_row "13" "Create worktrees" "COND" "if parallel stories exist"
|
|
203
|
+
_checklist_row "14" "Primary agent" "COND" "if primary stories"
|
|
204
|
+
_checklist_row "15" "Independent agent" "COND" "if independent stories"
|
|
205
|
+
_checklist_row "16" "Worktree health check" "COND" "if worktrees created"
|
|
206
|
+
_checklist_row "17" "Merge worktrees" "COND" "if worktrees created"
|
|
207
|
+
_checklist_row "18" "Post-parallel assessment" "$(is_truthy "${SKIP_SKILL_ASSESSMENT:-}" && echo SKIP || echo ACTIVE)" "$(is_truthy "${SKIP_SKILL_ASSESSMENT:-}" && echo SKIP_SKILL_ASSESSMENT=1 || true)"
|
|
208
|
+
_checklist_row "19" "Pre-review gate" "$(is_truthy "${SKIP_PRE_REVIEW_GATE:-}" && echo SKIP || echo ACTIVE)" "SKIP_PRE_REVIEW_GATE=true"
|
|
209
|
+
_checklist_row "20" "Lint gate" "$(is_truthy "${SKIP_LINT_GATE:-}" && echo SKIP || echo ACTIVE)" "SKIP_LINT_GATE=true"
|
|
210
|
+
_checklist_row "21" "Review stories" "COND" "if review stories exist"
|
|
211
|
+
_checklist_row "22a" "SAST sentinel" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
212
|
+
_checklist_row "22b" "Spec validator" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
213
|
+
_checklist_row "22c" "Review ranger" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
214
|
+
_checklist_row "22d" "Mutant hunter" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
215
|
+
_checklist_row "22e" "Fuzz-weaver" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
216
|
+
_checklist_row "22f" "Perf sentinel" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
217
|
+
_checklist_row "22g" "Runtime boundary" "$(is_truthy "${SKIP_TESTING_GATES:-}" && echo SKIP || echo ACTIVE)" "SKIP_TESTING_GATES=true"
|
|
218
|
+
_checklist_row "23" "Browser E2E routing" "$([ "${SKIP_BROWSER_E2E_ROUTING:-false}" = "true" ] && echo SKIP || echo COND)" "SKIP_BROWSER_E2E_ROUTING=true"
|
|
219
|
+
|
|
220
|
+
local skips=0
|
|
221
|
+
for key in "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "21" "22a" "22b" "22c" "22d" "22e" "22f" "23"; do
|
|
222
|
+
[ "${_STEP_STATUS[$key]:-}" = "skip" ] && skips=$((skips + 1))
|
|
223
|
+
done
|
|
224
|
+
echo ""
|
|
225
|
+
echo -e " ${YELLOW}SKIP bypass env vars active: SKIP_TESTING_GATES=${SKIP_TESTING_GATES:-false} SKIP_CPA=${SKIP_CPA:-0} SKIP_TC_WRITER=${SKIP_TC_WRITER:-0} SKIP_REGRESSION_GUARD=${SKIP_REGRESSION_GUARD:-false}${NC}"
|
|
226
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
227
|
+
echo ""
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
seed_runtime_logs() {
|
|
231
|
+
mkdir -p "$LOG_DIR" "$LOG_DIR/phase-improvements"
|
|
232
|
+
local files=(
|
|
233
|
+
"agent-activity.jsonl"
|
|
234
|
+
"agent-messages.jsonl"
|
|
235
|
+
"code-reviews.jsonl"
|
|
236
|
+
"cpa-review.jsonl"
|
|
237
|
+
"phase-cost.jsonl"
|
|
238
|
+
"phase-gates.jsonl"
|
|
239
|
+
"profiles-audit.jsonl"
|
|
240
|
+
"testing-gates.jsonl"
|
|
241
|
+
)
|
|
242
|
+
local f
|
|
243
|
+
for f in "${files[@]}"; do
|
|
244
|
+
[ -f "$LOG_DIR/$f" ] || : > "$LOG_DIR/$f"
|
|
245
|
+
done
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Kill a process and every descendant, leaves first. `pkill -P` reaches only direct children:
|
|
249
|
+
# `npx @11ty/eleventy` runs eleventy as ITS child, so killing npx's children and npx left eleventy
|
|
250
|
+
# alive after every run — five of them, 4GB resident, measured 2026-09-14 after five finished
|
|
251
|
+
# harness runs (one of which exited 0).
|
|
252
|
+
_epam_kill_tree() {
|
|
253
|
+
local _pid="$1" _child
|
|
254
|
+
for _child in $(pgrep -P "$_pid" 2>/dev/null); do
|
|
255
|
+
_epam_kill_tree "$_child"
|
|
256
|
+
done
|
|
257
|
+
kill "$_pid" 2>/dev/null || true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
stop_dashboards_watch() {
|
|
261
|
+
if [ "$DASHBOARD_WATCH_OWNED" != "true" ] || [ -z "$DASHBOARD_WATCH_PID" ]; then
|
|
262
|
+
return
|
|
263
|
+
fi
|
|
264
|
+
if ps -p "$DASHBOARD_WATCH_PID" > /dev/null 2>&1; then
|
|
265
|
+
info "Stopping dashboards watcher (PID $DASHBOARD_WATCH_PID)..."
|
|
266
|
+
_epam_kill_tree "$DASHBOARD_WATCH_PID"
|
|
267
|
+
wait "$DASHBOARD_WATCH_PID" 2>/dev/null || true
|
|
268
|
+
fi
|
|
269
|
+
rm -f "$DASHBOARD_WATCH_PID_FILE"
|
|
270
|
+
DASHBOARD_WATCH_PID=""
|
|
271
|
+
DASHBOARD_WATCH_OWNED=false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
start_control_plane() {
|
|
275
|
+
if [ "${EPAM_CONTROL_PLANE:-1}" != "1" ]; then
|
|
276
|
+
info "Control plane disabled (EPAM_CONTROL_PLANE=0)."
|
|
277
|
+
return
|
|
278
|
+
fi
|
|
279
|
+
local _node_bin
|
|
280
|
+
_node_bin=$(detect_node 2>/dev/null || true)
|
|
281
|
+
if [ -z "$_node_bin" ]; then
|
|
282
|
+
warning "Control plane: node binary not found — skipping."
|
|
283
|
+
return
|
|
284
|
+
fi
|
|
285
|
+
local cp_script="$SCRIPT_DIR/control-plane.js"
|
|
286
|
+
if [ ! -f "$cp_script" ]; then
|
|
287
|
+
warning "Control plane script not found at $cp_script — skipping."
|
|
288
|
+
return
|
|
289
|
+
fi
|
|
290
|
+
# Remove stale PAUSED sentinel from a previous run
|
|
291
|
+
rm -f "$LOG_DIR/PAUSED"
|
|
292
|
+
# Kill any stale process holding the control plane port from a previous run
|
|
293
|
+
local _stale_pid
|
|
294
|
+
_stale_pid=$(lsof -ti "tcp:${CONTROL_PLANE_PORT}" 2>/dev/null || true)
|
|
295
|
+
if [ -n "$_stale_pid" ]; then
|
|
296
|
+
warning "Killing stale process on port ${CONTROL_PLANE_PORT} (PID $_stale_pid)"
|
|
297
|
+
kill "$_stale_pid" 2>/dev/null || true
|
|
298
|
+
sleep 0.3
|
|
299
|
+
fi
|
|
300
|
+
CONTROL_PLANE_PORT="${CONTROL_PLANE_PORT}" \
|
|
301
|
+
LOG_DIR="$LOG_DIR" \
|
|
302
|
+
"$_node_bin" "$cp_script" >> "$CONTROL_PLANE_LOG" 2>&1 &
|
|
303
|
+
CONTROL_PLANE_PID=$!
|
|
304
|
+
sleep 0.5
|
|
305
|
+
if ! ps -p "$CONTROL_PLANE_PID" > /dev/null 2>&1; then
|
|
306
|
+
warning "Control plane exited immediately; see $CONTROL_PLANE_LOG"
|
|
307
|
+
CONTROL_PLANE_PID=""
|
|
308
|
+
return
|
|
309
|
+
fi
|
|
310
|
+
info "Control plane started (PID $CONTROL_PLANE_PID, port $CONTROL_PLANE_PORT)"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
stop_control_plane() {
|
|
314
|
+
if [ -z "$CONTROL_PLANE_PID" ]; then
|
|
315
|
+
return
|
|
316
|
+
fi
|
|
317
|
+
if ps -p "$CONTROL_PLANE_PID" > /dev/null 2>&1; then
|
|
318
|
+
kill "$CONTROL_PLANE_PID" 2>/dev/null || true
|
|
319
|
+
wait "$CONTROL_PLANE_PID" 2>/dev/null || true
|
|
320
|
+
fi
|
|
321
|
+
CONTROL_PLANE_PID=""
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
start_dashboards_watch() {
|
|
325
|
+
local dashboards_dir="$AUTOMATION_DIR/dashboards"
|
|
326
|
+
local config_path="$dashboards_dir/.eleventy.js"
|
|
327
|
+
local local_eleventy_bin="$dashboards_dir/node_modules/.bin/eleventy"
|
|
328
|
+
# THE INSTALL'S OWN ELEVENTY. The installer puts it in the install root's node_modules (it is a
|
|
329
|
+
# root dependency); dashboards/ has none, so every run fell to npx — a registry fetch, not the
|
|
330
|
+
# installed build.
|
|
331
|
+
[ -x "$local_eleventy_bin" ] || local_eleventy_bin="$(dirname "$AUTOMATION_DIR")/node_modules/.bin/eleventy"
|
|
332
|
+
|
|
333
|
+
if [ "${EPAM_DASH_AUTO_SERVE:-1}" != "1" ]; then
|
|
334
|
+
info "Dashboard auto-serve disabled (EPAM_DASH_AUTO_SERVE=0)."
|
|
335
|
+
return
|
|
336
|
+
fi
|
|
337
|
+
if [ ! -f "$config_path" ]; then
|
|
338
|
+
warning "Dashboards config not found at $config_path; skipping auto-serve."
|
|
339
|
+
return
|
|
340
|
+
fi
|
|
341
|
+
# A lane re-exec must never start its own watcher. The parent run already has
|
|
342
|
+
# one, and the pid-file check below is NOT atomic: three lanes launched
|
|
343
|
+
# together all read the file before any of them wrote it, so live metrolinx
|
|
344
|
+
# 2026-07-29 ran three Eleventy stacks rebuilding the same dashboard from the
|
|
345
|
+
# same LOG_DIR — ~120% CPU competing with the lanes for identical output.
|
|
346
|
+
# Sequential lanes hid this by never overlapping. A lock would also close the
|
|
347
|
+
# race; not starting it at all is simpler and strictly correct, because the
|
|
348
|
+
# lane has nothing to serve that the parent is not already serving.
|
|
349
|
+
if is_lane; then
|
|
350
|
+
return
|
|
351
|
+
fi
|
|
352
|
+
if [ -n "$DASHBOARD_WATCH_PID" ]; then
|
|
353
|
+
return
|
|
354
|
+
fi
|
|
355
|
+
if [ -f "$DASHBOARD_WATCH_PID_FILE" ]; then
|
|
356
|
+
local existing_pid
|
|
357
|
+
existing_pid="$(cat "$DASHBOARD_WATCH_PID_FILE" 2>/dev/null || true)"
|
|
358
|
+
if [ -n "$existing_pid" ] && ps -p "$existing_pid" > /dev/null 2>&1; then
|
|
359
|
+
info "Eleventy dashboards watcher already running (PID $existing_pid)."
|
|
360
|
+
DASHBOARD_WATCH_PID="$existing_pid"
|
|
361
|
+
return
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# NOTE: EPAM_PROJECT_OUTPUT_DIR is intentionally NOT exported to the Eleventy
|
|
366
|
+
# subprocess here. snapshot.js's agentActivity/agentStatus/phaseCost paths use
|
|
367
|
+
# process.env.EPAM_PROJECT_OUTPUT_DIR directly (not via resolveProjectOutputDir),
|
|
368
|
+
# and those files live in orchestrations/logs — which the dashboard's logs/ symlink
|
|
369
|
+
# already resolves correctly. Exporting EPAM_PROJECT_OUTPUT_DIR=OUTPUT_DIR would
|
|
370
|
+
# redirect those reads to the project output dir (wrong place). The .active-output-dir
|
|
371
|
+
# pointer written by pre-run-reset.sh is sufficient for resolveProjectOutputDir(),
|
|
372
|
+
# which only feeds healingEvents/storyFailures/guardedStepRetries (files in OUTPUT_DIR).
|
|
373
|
+
|
|
374
|
+
if [ -x "$local_eleventy_bin" ]; then
|
|
375
|
+
info "Starting Eleventy dashboards watcher (local binary)..."
|
|
376
|
+
(
|
|
377
|
+
cd "$PROJECT_ROOT" || exit 1
|
|
378
|
+
exec "$local_eleventy_bin" \
|
|
379
|
+
"--config=$config_path" \
|
|
380
|
+
"--input=$dashboards_dir" \
|
|
381
|
+
"--output=$dashboards_dir/live" \
|
|
382
|
+
--serve >> "$DASHBOARD_WATCH_LOG" 2>&1
|
|
383
|
+
) &
|
|
384
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
385
|
+
info "Starting Eleventy dashboards watcher (npx --prefix)..."
|
|
386
|
+
(
|
|
387
|
+
cd "$PROJECT_ROOT" || exit 1
|
|
388
|
+
exec npx --prefix "$dashboards_dir" @11ty/eleventy \
|
|
389
|
+
"--config=$config_path" \
|
|
390
|
+
"--input=$dashboards_dir" \
|
|
391
|
+
"--output=$dashboards_dir/live" \
|
|
392
|
+
--serve >> "$DASHBOARD_WATCH_LOG" 2>&1
|
|
393
|
+
) &
|
|
394
|
+
else
|
|
395
|
+
warning "Neither local Eleventy binary nor npx is available; skipping dashboard auto-serve."
|
|
396
|
+
return
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
DASHBOARD_WATCH_PID=$!
|
|
400
|
+
DASHBOARD_WATCH_OWNED=true
|
|
401
|
+
echo "$DASHBOARD_WATCH_PID" > "$DASHBOARD_WATCH_PID_FILE"
|
|
402
|
+
sleep 1
|
|
403
|
+
if ! ps -p "$DASHBOARD_WATCH_PID" > /dev/null 2>&1; then
|
|
404
|
+
warning "Dashboards watcher exited immediately; see $DASHBOARD_WATCH_LOG"
|
|
405
|
+
rm -f "$DASHBOARD_WATCH_PID_FILE"
|
|
406
|
+
DASHBOARD_WATCH_PID=""
|
|
407
|
+
DASHBOARD_WATCH_OWNED=false
|
|
408
|
+
fi
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Cleanup on exit
|
|
412
|
+
cleanup() {
|
|
413
|
+
local exit_code=$?
|
|
414
|
+
# THE RUN LEAVES A CASSETTE HOWEVER IT ENDS — and this is the only place that can promise it.
|
|
415
|
+
#
|
|
416
|
+
# Export-on-completion sat at the end of _run_codeline_loop(), which a run reaches only by
|
|
417
|
+
# running to the end. pause-before-writer ends the process with `exit 0` hundreds of lines
|
|
418
|
+
# earlier, and every failure path is a bare `exit 1`/`2`/`3`. pipeline-tests-48 is the one
|
|
419
|
+
# install that carried that hook and has no cassettes directory at all: the run paused, so the
|
|
420
|
+
# hook never ran, while Langfuse held all 286 traces the whole time. Langfuse records each call
|
|
421
|
+
# as it happens, so the recording was never pending — waiting for completion to harvest it was
|
|
422
|
+
# the defect.
|
|
423
|
+
#
|
|
424
|
+
# `trap ... EXIT` fires on every exit path bash controls, including ones added later, so this
|
|
425
|
+
# cannot be forgotten by whoever writes the next `exit`. FIRST in the trap, before the control
|
|
426
|
+
# plane is torn down, because the export reads a service that teardown is about to stop. It
|
|
427
|
+
# cannot change what the run reports: $? is already captured above, and export_run_cassette
|
|
428
|
+
# never fails the caller.
|
|
429
|
+
#
|
|
430
|
+
# NOT COVERED HERE: `kill -9` and an OOM kill bypass every trap. Only a harvest that does not
|
|
431
|
+
# depend on this process can cover those.
|
|
432
|
+
# The cassette export is registered beside lib/cassette-archive.sh, near the top of this
|
|
433
|
+
# file, so that a run failing before cleanup() is even registered still leaves a recording.
|
|
434
|
+
stop_control_plane
|
|
435
|
+
stop_dashboards_watch
|
|
436
|
+
if [ "$SKIP_CLEANUP" = "true" ]; then
|
|
437
|
+
warning "Skipping worktree cleanup (--skip-cleanup)"
|
|
438
|
+
return
|
|
439
|
+
fi
|
|
440
|
+
if [ $exit_code -ne 0 ]; then
|
|
441
|
+
error "Execution failed with exit code $exit_code"
|
|
442
|
+
fi
|
|
443
|
+
log "Cleaning up worktrees..."
|
|
444
|
+
"$CLAUDE_SH" --cleanup-worktrees 2>/dev/null || true
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# PUBLISH THE PLAN THIS PROCESS WILL WORK FROM — outside the spec-pass branch, deliberately.
|
|
448
|
+
#
|
|
449
|
+
# The detective renders its own answer (lib/producers/fix-plan.js) and it is published once, from
|
|
450
|
+
# THIS process's PRD, so consumers stop reading story.fixSiteAnalysis and inventing their own
|
|
451
|
+
# wording of it. Publishing from this PRD is what keeps a lane's writer on its own plan: a lane
|
|
452
|
+
# runs with its own scoped PRD, while the canonical one holds the UNION of every codeline — on
|
|
453
|
+
# AMSD-2041 that is 13 sites where gotransit has 4, including three conflicting prescriptions for
|
|
454
|
+
# one file.
|
|
455
|
+
#
|
|
456
|
+
# It sits OUTSIDE the spec-pass branch because a resume routinely skips the spec pass. Inside it,
|
|
457
|
+
# a resumed run would publish nothing, the plan would simply be absent, and the writer would go in
|
|
458
|
+
# blind — which looks exactly like a run that never had a plan.
|
|
459
|
+
_publish_agent_outputs() {
|
|
460
|
+
local _node="${NODE_CMD:-${HOME}/.nvm/versions/node/v20.20.0/bin/node}"
|
|
461
|
+
[ -x "$_node" ] || _node="$(command -v node 2>/dev/null || echo node)"
|
|
462
|
+
[ -f "${PRD_FILE:-}" ] || return 0
|
|
463
|
+
"$_node" "$SCRIPT_DIR/lib/producers/fix-plan.js" --publish "$PRD_FILE" \
|
|
464
|
+
|| warning "fix-plan publication failed — consumers will find no published plan"
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# ── Periodic checklist heartbeat ─────────────────────────────────────────────
|
|
468
|
+
# Prints a compact step-status summary every 60s so long-running phases stay
|
|
469
|
+
# visible in the log without requiring tail.
|
|
470
|
+
_checklist_heartbeat() {
|
|
471
|
+
while true; do
|
|
472
|
+
sleep 60
|
|
473
|
+
echo ""
|
|
474
|
+
echo -e "${MAGENTA}━━━ Step Status @ $(date +%H:%M:%S) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
475
|
+
for _sid in \
|
|
476
|
+
"1:Specification pass" "1a:openspec" "1b:speckit" "2:CPA pre-pass" \
|
|
477
|
+
"3:Skill assessment" "4:Hybrid pre-coord" "5:Regression guard" \
|
|
478
|
+
"6:mkdir src/ dirs" "7:PRD model coordinator" "8:Main-branch stories" \
|
|
479
|
+
"9:Auto-commit" "10:TC writer gate" "11:Skills coordinator audit" \
|
|
480
|
+
"12:Tools coordinator audit" "13:Create worktrees" "14:Primary agent" \
|
|
481
|
+
"15:Independent agent" "16:Worktree health" "17:Merge worktrees" \
|
|
482
|
+
"18:Post-parallel assessment" "19:Pre-review gate" "20:Lint gate" \
|
|
483
|
+
"21:Review stories" "22a:SAST sentinel" "22b:Spec validator" \
|
|
484
|
+
"22c:Review ranger" "22d:Mutant hunter" \
|
|
485
|
+
"22e:Fuzz-weaver" "22f:Perf sentinel" "23:Browser E2E"; do
|
|
486
|
+
local _key="${_sid%%:*}"
|
|
487
|
+
local _st="${_STEP_STATUS[$_key]:-pending}"
|
|
488
|
+
local _lbl="${_STEP_LABELS[$_key]:-${_sid#*:}}"
|
|
489
|
+
local _icon
|
|
490
|
+
case "$_st" in
|
|
491
|
+
pass) _icon="${GREEN}✓${NC}" ;;
|
|
492
|
+
skip) _icon="${YELLOW}⊘${NC}" ;;
|
|
493
|
+
fail) _icon="${RED}✗${NC}" ;;
|
|
494
|
+
warn) _icon="${YELLOW}⚠${NC}" ;;
|
|
495
|
+
running) _icon="${CYAN}▶${NC}" ;;
|
|
496
|
+
*) _icon="${WHITE}○${NC}" ;;
|
|
497
|
+
esac
|
|
498
|
+
printf " %b %-6s %s\n" "${_icon}" "$_key" "$_lbl"
|
|
499
|
+
done
|
|
500
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
501
|
+
echo ""
|
|
502
|
+
done
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Kill heartbeat on exit — ADDED, never replacing the handlers registered above.
|
|
506
|
+
_epam_kill_heartbeat() { kill "${_HEARTBEAT_PID:-}" 2>/dev/null || true; }
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECOSYSTEM REGISTRY — resolves providers at RUN TIME. The engine names no stack.
|
|
3
|
+
*
|
|
4
|
+
* REPLACES lib/ecosystem-registry.js, which was a table of "the ecosystems this engine knows". Six stacks
|
|
5
|
+
* were enumerated in engine source, so onboarding a seventh meant editing the pipeline — and every
|
|
6
|
+
* guard that consulted the table was generic only across the stacks already listed. That is the
|
|
7
|
+
* shape of a fact the pipeline must not hold.
|
|
8
|
+
*
|
|
9
|
+
* A provider is a module describing ONE ecosystem: the manifest file that identifies it, what it
|
|
10
|
+
* vendors, what it leaves behind, what a writer must not touch, and how to read its manifest.
|
|
11
|
+
* Those last parts are real parsers — JSON for a package manifest, section matching for TOML, line
|
|
12
|
+
* rules for a requirements file — so a provider is code, not a config row. It is loaded from a
|
|
13
|
+
* DIRECTORY, discovered by listing, never by a name written here.
|
|
14
|
+
*
|
|
15
|
+
* Adding an ecosystem is a new file in a provider directory. There is no engine change, no case
|
|
16
|
+
* statement, and no list to append to.
|
|
17
|
+
*
|
|
18
|
+
* Provider directories, in order:
|
|
19
|
+
* 1. orchestrations/ecosystems — the ones that ship
|
|
20
|
+
* 2. $EPAM_ECOSYSTEM_PROVIDERS — colon-separated, injected per run
|
|
21
|
+
*
|
|
22
|
+
* A later directory may REPLACE an earlier provider by declaring the same `file`, so a codeline
|
|
23
|
+
* that reads its manifest differently supplies its own provider without forking the engine.
|
|
24
|
+
*
|
|
25
|
+
* EPAM_CODELINE_MANIFESTS still extends the set for an ecosystem with no provider at all
|
|
26
|
+
* ("manifest:installDir,manifest:") — the minimum a scan needs to stop calling a repository
|
|
27
|
+
* `unknown`.
|
|
28
|
+
*
|
|
29
|
+
* NO FALLBACK TABLE. If no provider directory resolves, this returns an empty set and the callers
|
|
30
|
+
* that need an ecosystem must say they could not determine one. A built-in default would be a
|
|
31
|
+
* second definition of the same fact, and the second definition is the one that drifts.
|
|
32
|
+
*/
|
|
33
|
+
'use strict';
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
|
|
37
|
+
/** Where providers ship. Resolved from THIS file, so it holds no absolute path. */
|
|
38
|
+
const BUILTIN_DIR = path.resolve(__dirname, '../../ecosystems');
|
|
39
|
+
|
|
40
|
+
/** Provider directories, earliest first. Later entries win on a duplicate `file`. */
|
|
41
|
+
function providerDirs(env = process.env) {
|
|
42
|
+
const extra = String((env && env.EPAM_ECOSYSTEM_PROVIDERS) || '')
|
|
43
|
+
.split(':').map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
return [BUILTIN_DIR, ...extra];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every provider, discovered by listing the directories.
|
|
49
|
+
*
|
|
50
|
+
* A provider that throws on load is SKIPPED LOUDLY on stderr rather than silently dropped: an
|
|
51
|
+
* ecosystem that vanishes because its file has a syntax error would otherwise present as "this
|
|
52
|
+
* repository is not that stack", which is a wrong answer wearing the shape of a right one.
|
|
53
|
+
*/
|
|
54
|
+
function loadProviders(env = process.env) {
|
|
55
|
+
const byFile = new Map();
|
|
56
|
+
for (const dir of providerDirs(env)) {
|
|
57
|
+
let names = [];
|
|
58
|
+
try { names = fs.readdirSync(dir); } catch { continue; }
|
|
59
|
+
for (const n of names.filter((x) => x.endsWith('.js')).sort()) {
|
|
60
|
+
const p = path.join(dir, n);
|
|
61
|
+
let mod;
|
|
62
|
+
try { mod = require(p); } catch (e) {
|
|
63
|
+
process.stderr.write(`[ecosystem-registry] provider ${p} did not load: ${e.message}\n`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!mod || typeof mod.file !== 'string' || !mod.file) {
|
|
67
|
+
process.stderr.write(`[ecosystem-registry] provider ${p} declares no manifest file — ignored\n`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
byFile.set(mod.file, mod);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ORDERED BY DECLARED PRECEDENCE. Consumers resolve a repository's ecosystem with a
|
|
74
|
+
// first-match-wins `find`, so load order is behaviour: a Node repository that also carries a
|
|
75
|
+
// Gemfile must not resolve as ruby because of how a directory listing sorted. A provider that
|
|
76
|
+
// declares none sorts last, never silently ahead of one that did.
|
|
77
|
+
return [...byFile.values()].sort(
|
|
78
|
+
(a, b) => (a.precedence ?? Number.MAX_SAFE_INTEGER) - (b.precedence ?? Number.MAX_SAFE_INTEGER),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extra ecosystems without a provider: "manifest:installDir,manifest:" */
|
|
83
|
+
function extraManifests(env) {
|
|
84
|
+
const raw = (env && env.EPAM_CODELINE_MANIFESTS) || '';
|
|
85
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean).map((pair) => {
|
|
86
|
+
const [file, installDir] = pair.split(':');
|
|
87
|
+
// No `stack` is declared for a runtime addition, so the scan reports the manifest filename
|
|
88
|
+
// itself. Saying which file was found beats saying `unknown`.
|
|
89
|
+
return { file, stack: file, installDir: installDir || null, artifactDirs: installDir ? [installDir] : [], deps: () => [] };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function allManifests(env = process.env) {
|
|
94
|
+
const providers = loadProviders(env);
|
|
95
|
+
const extra = extraManifests(env).filter((e) => !providers.some((p) => p.file === e.file));
|
|
96
|
+
return [...providers, ...extra];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* EVERY DIRECTORY ANY RESOLVED ECOSYSTEM LEAVES BEHIND, deduped.
|
|
101
|
+
*
|
|
102
|
+
* The union, not the detected ecosystem's — a monorepo mixes stacks, and excluding a directory a
|
|
103
|
+
* repository does not have costs nothing.
|
|
104
|
+
*/
|
|
105
|
+
function allArtifactDirs(env = process.env) {
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const eco of allManifests(env)) for (const d of eco.artifactDirs || []) seen.add(d);
|
|
108
|
+
return [...seen];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* WHICH LOCKFILE THIS REPOSITORY CARRIES, first match wins — the same order that decides the
|
|
113
|
+
* package manager, so the two can never disagree. Takes an `exists` predicate rather than
|
|
114
|
+
* touching the filesystem.
|
|
115
|
+
*/
|
|
116
|
+
const lockfileFor = (eco, exists) => Object.keys((eco && eco.lockfiles) || {}).find((f) => exists(f)) || '';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* THE ECOSYSTEM A PRD DECLARES BEFORE ITS CODELINE EXISTS. A greenfield output directory carries no
|
|
120
|
+
* manifest until the scaffold story writes one; the PRD names the files that story creates, and
|
|
121
|
+
* the provider whose manifest is among them — in the providers' own precedence — is the codeline's
|
|
122
|
+
* ecosystem. One rule, read by the stack facts and by the rehearsal's stand-in writer, so the two
|
|
123
|
+
* cannot disagree about which stack a project is on.
|
|
124
|
+
*/
|
|
125
|
+
function declaredByStories(stories, env = process.env) {
|
|
126
|
+
const named = new Set();
|
|
127
|
+
for (const st of (Array.isArray(stories) ? stories : [])) {
|
|
128
|
+
const tn = st && st.technicalNotes;
|
|
129
|
+
for (const f of (tn && Array.isArray(tn.files) ? tn.files : [])) named.add(path.basename(String(f)));
|
|
130
|
+
}
|
|
131
|
+
if (!named.size) return null;
|
|
132
|
+
return allManifests(env).find((e) => [e.file, ...(Array.isArray(e.alsoMatches) ? e.alsoMatches : [])]
|
|
133
|
+
.some((n) => named.has(n))) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
lockfileFor,
|
|
138
|
+
declaredByStories,
|
|
139
|
+
allManifests,
|
|
140
|
+
extraManifests,
|
|
141
|
+
allArtifactDirs,
|
|
142
|
+
loadProviders,
|
|
143
|
+
providerDirs,
|
|
144
|
+
BUILTIN_DIR,
|
|
145
|
+
// Compatibility for importers that read a static list. Resolved on ACCESS, so it reflects the
|
|
146
|
+
// providers present at the moment it is read rather than a snapshot taken at require() time.
|
|
147
|
+
get MANIFESTS() { return allManifests(); },
|
|
148
|
+
};
|