amsd-pipeline-shared 0.5.23 → 0.5.25

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.
@@ -20,6 +20,13 @@ const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
20
20
  const FILES = fs.readFileSync(path.join(ROOT, 'docs', 'gateway-files.txt'), 'utf8').trim().split('\n').filter(Boolean);
21
21
  const BASE = Object.fromEntries(fs.readFileSync(path.join(ROOT, 'docs', 'gateway-baseline-2.0.80.md5'), 'utf8').trim().split('\n')
22
22
  .map((l) => l.trim().split(/\s+/)).map(([sum, f]) => [f, sum]));
23
+ // KNOWN PREDECESSOR COPIES (docs/gateway-predecessors.md5): a copy an install may hold that is neither the release nor this package's — a bash
24
+ // repo change made after 2.0.80 — named with its source; replaceable like the released copy, restored by revert
25
+ const PRED = {};
26
+ for (const l of fs.readFileSync(path.join(ROOT, 'docs', 'gateway-predecessors.md5'), 'utf8').split('\n')) {
27
+ const m = l.match(/^([0-9a-f]{32})\s+(\S+)/); if (m) (PRED[m[2]] = PRED[m[2]] || []).push(m[1]);
28
+ }
29
+ const known = (f, sum) => sum === BASE[f] || (PRED[f] || []).includes(sum);
23
30
  const md5 = (f) => (fs.existsSync(f) ? crypto.createHash('md5').update(fs.readFileSync(f)).digest('hex') : null);
24
31
  const fail = (m, code = 1) => { process.stderr.write(`amsd-pipeline-shared: ${m}\n`); process.exit(code); };
25
32
 
@@ -62,7 +69,7 @@ if (cmd === 'revert') {
62
69
  if (fs.existsSync(b)) fs.copyFileSync(b, path.join(ORCH, f));
63
70
  else if (!BASE[f]) fs.rmSync(path.join(ORCH, f), { force: true }); // this package's own file: the release has none
64
71
  }
65
- const back = FILES.filter((f) => (BASE[f] ? md5(path.join(ORCH, f)) !== BASE[f] : fs.existsSync(path.join(ORCH, f))));
72
+ const back = FILES.filter((f) => (BASE[f] ? !known(f, md5(path.join(ORCH, f))) : fs.existsSync(path.join(ORCH, f))));
66
73
  if (back.length) fail(`revert incomplete — not the released copy:\n ${back.join('\n ')}`);
67
74
  fs.rmSync(STATE, { recursive: true, force: true });
68
75
  process.stdout.write(`amsd-pipeline-shared: reverted ${FILES.length} file(s) to the released copies\n`);
@@ -77,7 +84,7 @@ const plan = FILES.map((f) => {
77
84
  if (now && now !== ours && PREV[f] === now) return { f, now, ours, state: 'previous' };
78
85
  // a file the release never had (this package's own, e.g. lib/agent-messages.js): installed where absent
79
86
  if (!BASE[f]) return { f, now, ours, state: now === ours ? 'current' : (now === null ? 'new' : 'other') };
80
- return { f, now, ours, state: now === ours ? 'current' : (now === BASE[f] ? 'released' : 'other') };
87
+ return { f, now, ours, state: now === ours ? 'current' : (known(f, now) ? 'released' : 'other') };
81
88
  });
82
89
  const other = plan.filter((p) => p.state === 'other');
83
90
  if (other.length) {
@@ -244,3 +244,4 @@ d4992178558476af4c4c228e6aa63b2d scripts/lib/mint-and-spec.sh
244
244
  fac1b26443bb52ba45ba072f4d654920 scripts/lib/story-guards.sh
245
245
  ada237373615a26d924226184ed7614b scripts/lib/prd-change-review.sh
246
246
  c2dfd2381db0242fdbc487a948fd7feb scripts/lib/agent-si.js
247
+ 03bde875977ab741c54c3b38a236a496 scripts/pre-run-reset.sh
@@ -165,6 +165,7 @@ scripts/lib/content-retry.js
165
165
  scripts/lib/control-plane.sh
166
166
  scripts/lib/cost-emitter.js
167
167
  scripts/lib/cost-record.sh
168
+ scripts/lib/dashboard-target.sh
168
169
  scripts/lib/doctor/agent.js
169
170
  scripts/lib/doctor/classify.js
170
171
  scripts/lib/doctor/cli.js
@@ -254,6 +255,7 @@ scripts/lib/transcript-tool-calls.js
254
255
  scripts/lib/worktree-rungs.sh
255
256
  scripts/llm-handler.sh
256
257
  scripts/mint-agents-step.js
258
+ scripts/pre-run-reset.sh
257
259
  scripts/preflight-check.sh
258
260
  scripts/preflight-prd-integrity.sh
259
261
  scripts/preflight-static.sh
@@ -0,0 +1,3 @@
1
+ # Copies a pipeline install may hold that are neither the 2.0.80 release nor this package's, each with where it came from.
2
+ # The installer backs one up and replaces it as it does the released copy; revert puts it back. Any other copy is refused.
3
+ a4e6e7d7071dfc30dc4ae11eff9050db scripts/pre-run-reset.sh epam-cli a306218f7 (unreleased): ClickHouse removed from the agent-monitor recreate (operator: "CLICKHOUSE OUT")
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ # THE DASHBOARDS SERVE A RUN — pre-run-reset.sh's Step 1, lifted into a function so every pipeline points them the same way (operator,
3
+ # 2026-10-22: launch checklist item 12, "Dashboards all up and running, controlled by eleventy", in scope for the Claude workflows).
4
+ # The lines below are pre-run-reset.sh's Step 1 as released (2.0.80), unchanged: the active-PRD and active-output-dir pointers, the compose
5
+ # override mounting the run's PRD directory at /prd-dir and its LOG_DIR at /logs-dir, nginx's alias, and agent-monitor recreated.
6
+ # point_dashboards_at_run <prd_file> <log_dir>
7
+ # What pre-run-reset.sh sets before its Step 1 is taken from the caller when set, else derived from this install as pre-run-reset.sh derives it.
8
+ # CLICKHOUSE OUT (operator; epam-cli a306218f7): the recreate passes no ClickHouse port — the one change from the released step.
9
+ point_dashboards_at_run() {
10
+ local PRD_FILE="${1:?prd file}" LOG_DIR="${2:?log dir}"
11
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ local REPO_ROOT="${REPO_ROOT:-$(cd "$_here/../../.." && pwd)}"
13
+ local PRD_PRESENT=1; [ -f "$PRD_FILE" ] || PRD_PRESENT=0
14
+ if [ -z "${OBS_PROJECT:-}" ]; then
15
+ local _identity_lib="$REPO_ROOT/orchestrations-installer/lib/isolated-compose-identity.sh"
16
+ [ -f "$_identity_lib" ] && . "$_identity_lib" || { echo "[dashboards] missing $_identity_lib — cannot resolve the observability project name" >&2; return 1; }
17
+ local OBS_PROJECT; OBS_PROJECT="$(isolated_project_name "$REPO_ROOT" obs)"
18
+ fi
19
+ local _STATE_FILE="${PIPELINE_SERVICES_STATE_FILE:-$REPO_ROOT/.pipeline-services-state.env}"
20
+ if [ -z "${OBS_DASHBOARD_PORT:-}" ] && [ -f "$_STATE_FILE" ]; then set -a; . "$_STATE_FILE"; set +a; fi
21
+ local COMPOSE_BASE="${COMPOSE_BASE:-$REPO_ROOT/docker-compose.observability.yml}"
22
+ local COMPOSE_OVERRIDE="${COMPOSE_OVERRIDE:-$REPO_ROOT/docker-compose.observability.override.yml}"
23
+ local DASHBOARD_STATE_DIR="${DASHBOARD_STATE_DIR:-$REPO_ROOT/orchestrations/dashboards}"
24
+ declare -F info >/dev/null 2>&1 || info() { echo "[dashboards] $*" >&2; }
25
+ declare -F success >/dev/null 2>&1 || success() { echo "[dashboards] ✓ $*" >&2; }
26
+ # ── Step 1: Wire agent-monitor to serve the live PRD ─────────────────────────
27
+ # Generate a compose override that mounts the PRD's PARENT DIRECTORY (not the
28
+ # file itself) at /prd-dir inside the nginx container, and patch nginx.conf's
29
+ # alias to the correct basename. nginx.conf routes GET /prd.json to that path.
30
+ #
31
+ # Directory mount, not file mount — deliberately. A single-FILE bind mount is
32
+ # fixed to the inode present at mount time; since the orchestration pipeline
33
+ # always writes the PRD via atomic tmp+rename (jq ... > tmp && mv tmp file /
34
+ # fs.renameSync — correct, corruption-safe, used throughout this codebase),
35
+ # every write creates a NEW inode at that path, silently orphaning a file
36
+ # mount the moment the PRD is rewritten after this script runs (found live
37
+ # 2026-07-13: the dashboard had been serving a frozen, stale PRD snapshot
38
+ # indefinitely). A directory mount doesn't have this problem — renames INSIDE
39
+ # the mounted directory are visible immediately, no container restart needed
40
+ # for subsequent PRD writes during the run.
41
+ # THE LOGS MOUNT DOES NOT DEPEND ON THE PRD. This whole block used to be skipped when the PRD did
42
+ # not exist yet, taking /logs-dir down with it — but LOG_DIR is this run's own log directory and is
43
+ # knowable with or without a PRD. The consequence was a chain: a fresh install has no prd.json (it
44
+ # is REGENERATED by ingest, and no longer committed — see .gitignore), so the dashboard mount was
45
+ # skipped, so nginx had no /logs-dir, so pre-flight's own reachability check failed
46
+ # ("nginx /logs/healing-events.jsonl not reachable") and refused the launch — on an install where
47
+ # nothing was actually wrong.
48
+ #
49
+ # /prd-dir is the only part that genuinely needs the file, so that is the only part now conditional.
50
+ # THE PRD'S DIRECTORY IS KNOWABLE EVEN WHEN THE FILE IS NOT THERE YET, and mounting it is the whole
51
+ # reason this is a DIRECTORY mount rather than a file mount: a file appearing inside a mounted
52
+ # directory is visible immediately, with no container restart.
53
+ #
54
+ # Conditioning the /prd-dir mount on the file already existing was half a fix. On a fresh install
55
+ # the PRD does not exist when this runs — ingest writes it minutes LATER — so the mount was skipped,
56
+ # nothing re-mounted afterwards, and nginx served 404 for /prd.json for the whole run. The dashboard
57
+ # showed "data offline" against a perfectly healthy run (live 2026-09-04, pipeline-tests-13).
58
+ PRD_DIR="$(dirname "$PRD_FILE")"
59
+ PRD_BASENAME="$(basename "$PRD_FILE")"
60
+ # The directory itself must exist to be mounted; docker would otherwise create it as root.
61
+ mkdir -p "$PRD_DIR" 2>/dev/null || true
62
+
63
+ if [ "$PRD_PRESENT" = "0" ]; then
64
+ info "Configuring agent-monitor to serve $PRD_BASENAME (written by ingest during this run) and this run's logs..."
65
+ else
66
+ info "Configuring agent-monitor to serve: $(basename "$PRD_FILE")..."
67
+
68
+ # Ensure the PRD file is world-readable so nginx (non-root) can serve it
69
+ chmod o+r "$PRD_FILE"
70
+
71
+ # build/snapshot.js (the Eleventy watcher — a separate Node process, no access
72
+ # to the Docker mounts above) needs its own way to know which PRD is active.
73
+ # It reads this pointer file directly; falls back to the old dead
74
+ # orchestrations/prd.json symlink when absent so an unrun/older setup doesn't
75
+ # break. Truncate-write (>), not atomic rename — this file itself must never
76
+ # be subject to the inode-staleness bug it exists to work around.
77
+ echo -n "$PRD_FILE" > "$DASHBOARD_STATE_DIR/.active-prd-path"
78
+
79
+ # Same pointer-file pattern for the project output directory: snapshot.js
80
+ # (the Eleventy watcher) reads EPAM_PROJECT_OUTPUT_DIR from its process env,
81
+ # but since pre-run-reset.sh is invoked as a subprocess (not sourced), the
82
+ # export above does not propagate to the parent shell or to the Eleventy
83
+ # watcher started later by run-agent-orchestration.sh. Writing the path here
84
+ # lets snapshot.js read it without requiring the env var in the process chain.
85
+ echo -n "$LOG_DIR" > "$DASHBOARD_STATE_DIR/.active-output-dir"
86
+ fi # end: the PRD-only half (pointer files, chmod, nginx alias below)
87
+
88
+ # /logs-dir gets the same directory-mount treatment as /prd-dir, and for the
89
+ # identical reason: step-status.json/agent-status.json/agent-activity.jsonl/
90
+ # phase-cost.jsonl are all rewritten via atomic tmp+rename or repeated append
91
+ # during a run, and for external-project runs LOG_DIR is a directory the
92
+ # dashboard has otherwise never been told about at all.
93
+ #
94
+ # WRITTEN WHETHER OR NOT A PRD EXISTS YET — the /prd-dir line is the only part that needs one.
95
+ {
96
+ echo "services:"
97
+ echo " agent-monitor:"
98
+ echo " volumes:"
99
+ echo " - ${PRD_DIR}:/prd-dir:ro"
100
+ echo " - ${LOG_DIR}:/logs-dir:ro"
101
+ } > "$COMPOSE_OVERRIDE"
102
+
103
+ # nginx.conf's /prd.json alias basename must match whatever PRD was passed —
104
+ # patch it in place (truncate-write via sed -i, not atomic rename, so this
105
+ # single-file mount for nginx.conf itself is not subject to the same bug).
106
+ # Pointed whether or not the file exists yet: the basename is known from --prd, and the alias must
107
+ # already be correct when ingest writes the file into the mounted directory mid-run.
108
+ sed -i "s|alias /prd-dir/[^;]*;|alias /prd-dir/${PRD_BASENAME};|" \
109
+ "$REPO_ROOT/orchestrations/dashboards/nginx.conf"
110
+
111
+ # --force-recreate, not just `up -d`: nginx.conf itself is STILL a single-
112
+ # FILE bind mount (the sed -i just above rewrites it via a new inode, same as
113
+ # every other atomic-write case this session). Plain `up -d` only recreates a
114
+ # container when the COMPOSE CONFIG changed — it does not notice an existing
115
+ # bind-mounted file's inode changing underneath it, so without
116
+ # --force-recreate the container keeps serving the nginx.conf it started
117
+ # with, silently ignoring this run's PRD-basename patch (found live
118
+ # 2026-07-13 verifying this exact fix — even the fix's own docs file wasn't
119
+ # safe from the bug it documents).
120
+ # THE COMPOSE THAT MATCHES THE INSTALL, resolved once.
121
+ #
122
+ # This shelled out to `docker compose` literally. A podman install therefore never remounted
123
+ # agent-monitor, so the run's own log dir was never served and the pre-flight gate
124
+ # "nginx /logs/healing-events.jsonl not reachable" failed on every podman run -- after the
125
+ # installer's health check had already reported the box "ready", because that check probes host
126
+ # ports and cannot see a mount that was never made.
127
+ #
128
+ # container_compose() already resolves the runtime and knows podman needs PODMAN_COMPOSE_PROVIDER
129
+ # and a real runtime dir, and the installer lib ships inside every install. Sourced, never
130
+ # reimplemented: a second copy of the resolution is how this drifted from install.sh in the first
131
+ # place.
132
+ _obs_runtime_lib() {
133
+ local _c
134
+ for _c in "${INSTALLER_LIB:-}" \
135
+ "$(dirname "${BASH_SOURCE[0]}")/../../orchestrations-installer/lib/container-runtime.sh" \
136
+ "${REPO_ROOT:-}/orchestrations-installer/lib/container-runtime.sh"; do
137
+ [ -n "$_c" ] && [ -f "$_c" ] && { printf '%s' "$_c"; return 0; }
138
+ done
139
+ return 1
140
+ }
141
+ _obs_runtime_name() {
142
+ local _l; _l="$(_obs_runtime_lib)" || { printf 'none'; return 1; }
143
+ # shellcheck disable=SC1090
144
+ . "$_l" 2>/dev/null || { printf 'none'; return 1; }
145
+ container_runtime 2>/dev/null || printf 'none'
146
+ }
147
+ _obs_have_runtime() {
148
+ local _n; _n="$(_obs_runtime_name)"
149
+ [ -n "$_n" ] && [ "$_n" != "none" ]
150
+ }
151
+ _obs_compose() {
152
+ local _l; _l="$(_obs_runtime_lib)" || return 1
153
+ # shellcheck disable=SC1090
154
+ . "$_l" 2>/dev/null || return 1
155
+ container_compose "$@"
156
+ }
157
+
158
+ # THE DASHBOARD RESTART IS THE ONLY SLOW STEP, AND A TEST DOES NOT NEED IT.
159
+ #
160
+ # `docker compose up --force-recreate` costs ~3 seconds. Everything else this script does — the
161
+ # roster restore, the PRD restore, clearing generated artefacts and prompts — is file work that
162
+ # takes milliseconds. A test that exercises the RESET therefore paid three seconds per case for a
163
+ # container it never looks at: two test files spent 23 of the 24 seconds my tests take, purely
164
+ # here, and that is the difference between a suite that gets run and one that does not.
165
+ #
166
+ # Skipped only when asked. A run never sets this, so the live path is unchanged.
167
+ # REPLACING THE CONTAINER, NOT RESTARTING IT.
168
+ #
169
+ # --force-recreate is honoured by docker compose and IGNORED by podman-compose, which restarts the
170
+ # existing container instead. A restarted container keeps the volumes it was created with, so the
171
+ # override this script just wrote -- carrying THIS run's /prd-dir and /logs-dir -- was reported as
172
+ # applied and had no effect. Live 2026-09-08, pipeline-tests-41: override written 08:28,
173
+ # agent-monitor created 08:23 at install time and merely started at 08:28, mounts holding neither
174
+ # /logs-dir nor /prd-dir, and the run then died at a pre-flight check for a /logs path nginx could
175
+ # not serve.
176
+ #
177
+ # The same class this file already records for nginx.conf ("without --force-recreate the container
178
+ # keeps serving the nginx.conf it started with") -- one runtime further down. Removing the service
179
+ # first makes the replacement explicit and true on both runtimes; --force-recreate stays for
180
+ # docker, where it is meaningful and cheap.
181
+ _obs_replace_agent_monitor() {
182
+ EPAM_OBS_SUBNET="${OBS_SUBNET:-}" \
183
+ EPAM_OBS_LANGFUSE_PORT="${OBS_LANGFUSE_PORT:-}" \
184
+ EPAM_OBS_DASHBOARD_PORT="${OBS_DASHBOARD_PORT:-}" \
185
+ EPAM_OBS_GRAFANA_PORT="${OBS_GRAFANA_PORT:-}" \
186
+ EPAM_PRD_DIR="$PRD_DIR" EPAM_LOG_DIR="$LOG_DIR" \
187
+ _obs_compose -f "$COMPOSE_BASE" -f "$COMPOSE_OVERRIDE" -p "$OBS_PROJECT" \
188
+ rm -sf agent-monitor >/dev/null 2>&1 || true
189
+ EPAM_OBS_SUBNET="${OBS_SUBNET:-}" \
190
+ EPAM_OBS_LANGFUSE_PORT="${OBS_LANGFUSE_PORT:-}" \
191
+ EPAM_OBS_DASHBOARD_PORT="${OBS_DASHBOARD_PORT:-}" \
192
+ EPAM_OBS_GRAFANA_PORT="${OBS_GRAFANA_PORT:-}" \
193
+ EPAM_PRD_DIR="$PRD_DIR" EPAM_LOG_DIR="$LOG_DIR" \
194
+ _obs_compose -f "$COMPOSE_BASE" -f "$COMPOSE_OVERRIDE" -p "$OBS_PROJECT" \
195
+ up -d --force-recreate agent-monitor 2>/dev/null
196
+ }
197
+
198
+ if [ "${EPAM_SKIP_CONTAINER_RESTART:-0}" = "1" ]; then
199
+ info " Container restart skipped (EPAM_SKIP_CONTAINER_RESTART=1)"
200
+
201
+ elif _obs_replace_agent_monitor; then
202
+ if [ "$PRD_PRESENT" = "1" ]; then
203
+ success "agent-monitor restarted → /prd-dir = $PRD_DIR (serving $PRD_BASENAME), /logs-dir = $LOG_DIR"
204
+ else
205
+ success "agent-monitor restarted → /prd-dir = $PRD_DIR (serving $PRD_BASENAME once ingest writes it), /logs-dir = $LOG_DIR"
206
+ fi
207
+ elif ! _obs_have_runtime; then
208
+ info " no container runtime found — skipping container restart"
209
+ else
210
+ # NOT "docker is missing". The runtime IS here and the remount FAILED, which is a different
211
+ # fact and a different fix. The old text blamed an absent docker for every outcome, and on
212
+ # 2026-09-08 that sent the reader hunting containers "stuck in Created" while the real cause
213
+ # was this call naming a runtime the box does not use.
214
+ info " agent-monitor remount FAILED with $(_obs_runtime_name) — this run's /logs-dir and /prd-dir are NOT mounted;"
215
+ info " the pre-flight check 'nginx /logs/healing-events.jsonl not reachable' will fail because of this"
216
+ fi
217
+ return 0
218
+ }