amsd-pipeline-shared 0.5.21 → 0.5.23

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.
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * THE PROMPT LIBRARY. Resolves and renders an agent's PROJECT-AUTHORITY prompt.
4
+ *
5
+ * Operator mandate, 2026-08-11, verbatim:
6
+ * "WE WILL NEVER EVER run the template version of the prompts - NEVER. NEVER - no
7
+ * fallbacks. Only project authority prompts - the self-health mechanism is the only
8
+ * process permitted to mutate project level prompts. You will keep each prompt in a
9
+ * separate file (json) and never hide prompts in code ever again."
10
+ *
11
+ * So, precisely:
12
+ * - The TEMPLATE (orchestrations/prompts/templates/<id>.json) is the immutable generic
13
+ * source a project prompt is MINTED FROM. It is NEVER executed.
14
+ * - The PROJECT prompt (<project-config-dir>/prompts/<id>.json) is the ONLY thing
15
+ * rendered and sent to a model.
16
+ * - A missing project prompt is a HARD FAILURE. It never degrades to the template,
17
+ * because a silent degrade is how an engine-embedded default runs for a whole
18
+ * campaign without anyone noticing.
19
+ *
20
+ * WHY THIS EXISTS AT ALL: the failure-analyst prompt lived as a 44-line shell heredoc in
21
+ * claude.sh, generic, with no project-level variant — so the self-heal mechanism had
22
+ * nothing to mutate, and the prompt could not be corrected without editing the engine.
23
+ * Four more prompts are still embedded the same way (PROMPT_HEADER, PLAN_PROMPT_EOF,
24
+ * MODRES_EOF, COORD_PROMPT); this module is the pattern they migrate to.
25
+ *
26
+ * RENDERING IS TOTAL AND STRICT. Every declared placeholder must be supplied, and every
27
+ * placeholder appearing in the body must be declared. A prompt that reaches a model with
28
+ * a literal __STORY_ACS__ in it has silently lost its evidence, which is exactly the
29
+ * class of defect this pipeline keeps paying for.
30
+ */
31
+ 'use strict';
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ const TEMPLATE_DIR_REL = 'orchestrations/prompts/templates';
37
+ const PROJECT_PROMPT_SUBDIR = 'prompts';
38
+ // THE PLACEHOLDER RULE LIVES IN ONE PLACE.
39
+ //
40
+ // This regex existed in THREE files — here, engine-prompt.js and project-prompt-contract.js —
41
+ // and only one of them was fixed when adjacent placeholders turned out to merge. ${a}${b}
42
+ // becomes __A____B__ in a template, and a greedy match reads that as a single token. The two
43
+ // unfixed copies then disagreed with the fixed one about what a template declares, so a valid
44
+ // generated prompt was refused as "dropping placeholders" and a valid template was refused as
45
+ // "using undeclared placeholders". Three copies of a rule is three rules.
46
+ const { placeholdersIn: _placeholdersIn, substituteOnce } = require('./engine-prompt.js');
47
+
48
+ /** Repo root, derived from this file's location — never an env guess. */
49
+ function repoRoot() {
50
+ return path.resolve(__dirname, '..', '..', '..');
51
+ }
52
+
53
+ function templatePath(id) {
54
+ return path.join(repoRoot(), TEMPLATE_DIR_REL, `${id}.json`);
55
+ }
56
+
57
+ function projectPromptPath(id, projectConfigDir) {
58
+ return path.join(projectConfigDir, PROJECT_PROMPT_SUBDIR, `${id}.json`);
59
+ }
60
+
61
+ function readJson(file) {
62
+ let raw;
63
+ try {
64
+ raw = fs.readFileSync(file, 'utf8');
65
+ } catch (e) {
66
+ throw new Error(`prompt file unreadable (${file}): ${e && e.message}`);
67
+ }
68
+ try {
69
+ return JSON.parse(raw);
70
+ } catch (e) {
71
+ // Never "fall back to the template" here. An unparseable project prompt means the
72
+ // authority for this agent is broken, and running SOMETHING ELSE hides that.
73
+ throw new Error(`prompt file is not valid JSON (${file}): ${e && e.message}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load the project-authority prompt. NO FALLBACK — see the header.
79
+ * @param {string} id prompt id, e.g. 'failure-analyst'
80
+ * @param {string} projectConfigDir EPAM_PROJECT_CONFIG_DIR
81
+ */
82
+ function loadProjectPrompt(id, projectConfigDir, opts) {
83
+ if (!id || typeof id !== 'string') throw new Error('prompt id is required');
84
+ if (!projectConfigDir) {
85
+ throw new Error(
86
+ `no project config dir given for prompt '${id}' — EPAM_PROJECT_CONFIG_DIR must be set. ` +
87
+ 'The template is never executed, so there is nothing to fall back to.',
88
+ );
89
+ }
90
+ const file = projectPromptPath(id, projectConfigDir);
91
+ if (!fs.existsSync(file)) {
92
+ throw new Error(
93
+ `project-authority prompt missing: ${file}. Mint it from ` +
94
+ `${TEMPLATE_DIR_REL}/${id}.json — the template is NEVER executed directly.`,
95
+ );
96
+ }
97
+ const doc = readJson(file);
98
+
99
+ // A MULTI-PART PROMPT: ONE POLICY, SEVERAL AUDIENCES.
100
+ //
101
+ // Some rules bind more than one agent, and the two halves must never drift. "Tests are NOT
102
+ // your job" lived only in the writer's prompt; the reviewer had never heard of it and raised
103
+ // a blocker for missing tests, so the writer was ordered to create what it was forbidden to
104
+ // create. Splitting that across two prompt files would recreate the same defect with extra
105
+ // steps, so a prompt may declare `bodies: { <part>: text }` and each consumer asks for its
106
+ // own part of the SAME document.
107
+ //
108
+ // A part is REQUIRED when bodies exist: silently picking one would make the caller's audience
109
+ // an accident of ordering.
110
+ if (doc && doc.bodies && typeof doc.bodies === 'object') {
111
+ const part = opts && opts.part;
112
+ if (!part) {
113
+ throw new Error(
114
+ `prompt '${id}' declares parts (${Object.keys(doc.bodies).join(', ')}) — ask for one by name`);
115
+ }
116
+ if (typeof doc.bodies[part] !== 'string' || !doc.bodies[part].trim()) {
117
+ throw new Error(
118
+ `prompt '${id}' has no part '${part}' (declares: ${Object.keys(doc.bodies).join(', ')})`);
119
+ }
120
+ // THE PART'S CONTRACT IS THE PART'S PLACEHOLDERS. `placeholders` on a multi-part document
121
+ // is the UNION across parts — that is what the whole document declares. render() then
122
+ // compares it against ONE part's body and refuses it for "declaring placeholders it never
123
+ // uses", so every multi-part project prompt was unrenderable through this path.
124
+ //
125
+ // Surfaced when seam prompts started routing here instead of to the template renderer, which
126
+ // computes its declared set per body already. Narrowed rather than relaxing render(): a
127
+ // prompt that declares a placeholder its body never uses is still a real defect, and the
128
+ // check that catches it is worth keeping.
129
+ const used = new Set(placeholdersIn(doc.bodies[part]));
130
+ return {
131
+ ...doc,
132
+ body: doc.bodies[part],
133
+ placeholders: (doc.placeholders || []).filter((x) => used.has(x)),
134
+ };
135
+ }
136
+
137
+ if (!doc || typeof doc.body !== 'string' || !doc.body.trim()) {
138
+ throw new Error(`project-authority prompt has no body: ${file}`);
139
+ }
140
+ if (doc.authority && doc.authority !== 'project') {
141
+ throw new Error(`refusing to run a non-project prompt (authority='${doc.authority}'): ${file}`);
142
+ }
143
+ return doc;
144
+ }
145
+
146
+ /** Placeholders actually present in a body, deduped and sorted. */
147
+ function placeholdersIn(body) {
148
+ return [..._placeholdersIn(body)].sort();
149
+ }
150
+
151
+ /**
152
+ * Render a prompt body with values. Strict in both directions.
153
+ *
154
+ * Values are substituted with a replacer FUNCTION, not a string, so that `$&`, `$1` and
155
+ * friends inside a value (a diff, a regex, a test log — all routine here) are inserted
156
+ * literally instead of being interpreted as replacement patterns. That bug would corrupt
157
+ * evidence silently and only for values that happen to contain a dollar sign.
158
+ */
159
+ function render(doc, values) {
160
+ const body = doc.body;
161
+ const declared = Array.isArray(doc.placeholders) ? [...doc.placeholders].sort() : [];
162
+ const present = placeholdersIn(body);
163
+
164
+ const undeclared = present.filter((p) => !declared.includes(p));
165
+ if (undeclared.length) {
166
+ throw new Error(`prompt '${doc.id}' uses undeclared placeholders: ${undeclared.join(', ')}`);
167
+ }
168
+ const orphanDeclared = declared.filter((p) => !present.includes(p));
169
+ if (orphanDeclared.length) {
170
+ throw new Error(`prompt '${doc.id}' declares placeholders it never uses: ${orphanDeclared.join(', ')}`);
171
+ }
172
+
173
+ const supplied = values && typeof values === 'object' ? values : {};
174
+
175
+ // A VALUE NOTHING CONSUMES IS EVIDENCE THROWN AWAY.
176
+ //
177
+ // This checked three things and not this one, so a value supplied to a prompt with no
178
+ // matching placeholder was silently dropped. engine-prompt.js has always rejected exactly
179
+ // that — "was given values it does not use" — so the two renderers disagreed on the same
180
+ // class, and the lenient one lost evidence without a trace.
181
+ //
182
+ // Live 20260821T212250Z: prior-reviews.py produced the reviewer's history correctly and it
183
+ // was supplied as __PRIOR_REVIEW__. The project's prompt declares no such placeholder, so
184
+ // it went nowhere. All three review cycles ran with no memory of each other and the
185
+ // approval missed a regression the earlier cycle had caught. The run log mentions prior
186
+ // review zero times: a whole feedback loop absent, with nothing to show it.
187
+ //
188
+ // Loud, because the caller believed it sent something. A stale project prompt is now a
189
+ // failure at render rather than a quiet degrade — which is the same rule the rest of this
190
+ // file already applies in the other direction.
191
+ const unconsumed = Object.keys(supplied).filter((k) => /^__[A-Z0-9_]+__$/.test(k) && !present.includes(k));
192
+ if (unconsumed.length) {
193
+ // LOUD, NOT FATAL — and that asymmetry is deliberate.
194
+ //
195
+ // Throwing here kills the seam outright: the reviewer supplies __PRIOR_REVIEW__ to project
196
+ // prompts minted before that placeholder existed, so a hard failure would take the reviewer
197
+ // out of every such run. Losing one block of context is bad; losing the whole review is
198
+ // worse, and the operator has not asked for a re-mint.
199
+ //
200
+ // engine-prompt.js DOES throw on this, correctly: it renders templates, which are the
201
+ // in-repo source and can always be corrected. A project prompt is generated, may lag the
202
+ // template, and cannot be corrected from here.
203
+ //
204
+ // What was unacceptable was silence. This is the trace that was missing when the reviewer's
205
+ // entire prior-review history was dropped on 20260821T212250Z and the run log mentioned it
206
+ // zero times.
207
+ process.stderr.write(
208
+ `[prompt-library] '${doc.id}' was given values it does not use: ${unconsumed.join(', ')}. `
209
+ + 'That evidence is being DROPPED — the prompt has no placeholder for it. '
210
+ + 'Add the placeholder to this project prompt, or stop supplying the value.\n');
211
+ }
212
+
213
+ // A LOOKUP THAT FOUND NOTHING IS ABSENCE, NOT A VALUE. `p in supplied` is true for a key whose
214
+ // value is undefined, so the rule stated below — absent is not empty — was defeated by the one
215
+ // shape absence actually takes in JavaScript, and the prompt rendered with the literal word
216
+ // "undefined" where the evidence belonged. null is the same thing arriving from JSON.
217
+ const missing = present.filter((p) => !(p in supplied) || supplied[p] === undefined || supplied[p] === null);
218
+ if (missing.length) {
219
+ // An empty string is a legitimate value and must be passed explicitly. Absent is not
220
+ // empty: a prompt rendered without its evidence is worse than one that fails loudly.
221
+ throw new Error(`prompt '${doc.id}' is missing values for: ${missing.join(', ')}`);
222
+ }
223
+
224
+ // ONE PASS OVER THE BODY. Replacing each key in turn over the accumulating output meant every
225
+ // value was re-read by every later key: a diff that mentioned __B__ had __B__'s content
226
+ // spliced into it, and a diff carrying any double-underscore token — a Python dunder, a C
227
+ // macro — tripped the leftover check below and killed the render of a complete prompt. A
228
+ // single alternation pass inserts each value exactly where the BODY asked for it and never
229
+ // looks at what was inserted.
230
+ //
231
+ // Replacer FUNCTION, not a string, so a `$&` or `$1` inside a diff, a log or a JSON example
232
+ // is inserted literally instead of being read as a replacement pattern.
233
+ // AN EMPTY VALUE IS SILENCE, AND SILENCE REACHES THE MODEL AS A BLANK SECTION.
234
+ //
235
+ // The renderer refused a MISSING key and accepted a present-but-empty one, so `|| ''` — which
236
+ // fifteen call sites use — turned a failed lookup into a prompt that looked complete and said
237
+ // nothing. Live 2026-08-23: the roster reviewer was handed a header with a blank line under it
238
+ // for every agent, reported the briefs "entirely empty", and blocked the roster twice. The
239
+ // briefs existed; the lookup read the wrong map.
240
+ //
241
+ // A placeholder that may legitimately be empty says so IN THE TEMPLATE, as `mayBeEmpty`. A retry
242
+ // note absent on a first attempt is a real state; a brief is not. That distinction belongs with
243
+ // the prompt, which is reviewable, rather than in the producer, which is where it got lost.
244
+ const mayBeEmpty = new Set(Array.isArray(doc.mayBeEmpty) ? doc.mayBeEmpty : []);
245
+ const blank = present.filter((p) => !mayBeEmpty.has(p)
246
+ && typeof supplied[p] === 'string' && !supplied[p].trim());
247
+ if (blank.length) {
248
+ throw new Error(
249
+ `prompt '${doc.id}' was given EMPTY values for: ${blank.join(', ')}. An empty payload renders `
250
+ + 'as a blank section and the agent answers about silence — it cannot tell a failed lookup '
251
+ + 'from a genuinely absent one. Supply the value, or declare the placeholder in the '
252
+ + "template's `mayBeEmpty` if absent is a real state for it.");
253
+ }
254
+
255
+ const out = substituteOnce(body, present, supplied);
256
+
257
+ // The check that means something is on the BODY: a placeholder this pass did not cover.
258
+ // Scanning the OUTPUT could only ever find tokens that arrived as evidence.
259
+ const uncovered = placeholdersIn(body).filter((p) => !present.includes(p));
260
+ if (uncovered.length) {
261
+ throw new Error(`prompt '${doc.id}' has placeholders no value covered: ${uncovered.join(', ')}`);
262
+ }
263
+ return out;
264
+ }
265
+
266
+ /** Convenience: load + render in one call. */
267
+ function buildPrompt(id, projectConfigDir, values, opts) {
268
+ return render(loadProjectPrompt(id, projectConfigDir, opts), values);
269
+ }
270
+
271
+ module.exports = {
272
+ loadProjectPrompt,
273
+ render,
274
+ buildPrompt,
275
+ placeholdersIn,
276
+ templatePath,
277
+ projectPromptPath,
278
+ TEMPLATE_DIR_REL,
279
+ PROJECT_PROMPT_SUBDIR,
280
+ };
281
+
282
+ /**
283
+ * CLI, so shell can use the library without reimplementing resolution:
284
+ * node prompt-library.js render <id> <projectConfigDir> <values.json> [part]
285
+ * Values arrive as a JSON file rather than argv because they routinely contain newlines,
286
+ * quotes and megabytes of test output.
287
+ */
288
+ if (require.main === module) {
289
+ const [, , cmd, id, dir, valuesFile, part] = process.argv;
290
+ try {
291
+ if (cmd !== 'render') throw new Error(`unknown command '${cmd}' (expected: render)`);
292
+ const values = valuesFile ? readJson(valuesFile) : {};
293
+ process.stdout.write(buildPrompt(id, dir, values, part ? { part } : undefined));
294
+ } catch (e) {
295
+ process.stderr.write(`[prompt-library] ${e && e.message}\n`);
296
+ process.exit(1);
297
+ }
298
+ }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # THE COMPLETION MARKER'S NAME — ONE DERIVATION, FOR EVERY SHELL CALL SITE.
3
+ #
4
+ # `.prompt-cache/.complete-<codeline>` is the claim that a codeline's prompts are fully
5
+ # provisioned, and its presence makes the mint skip ENTIRELY. It is tested in three shell places
6
+ # (run-agent-orchestration.sh twice, pre-run-reset.sh) and written by project-prompt-builder.js.
7
+ #
8
+ # A brownfield run generates DIFFERENT prompts from the same seams, so it must not inherit a
9
+ # greenfield codeline's claim — it would skip provisioning and serve the greenfield prompts on
10
+ # disk, which on a cached codeline means the brownfield variant never executes at all.
11
+ #
12
+ # Greenfield's name is unchanged, byte for byte: no marker already on disk is invalidated.
13
+ # The JS side of this rule is _markerPath in lib/project-prompt-builder.js; the two are asserted
14
+ # to agree by test/unit/orchestration/a-brownfield-run-generates-its-own-prompts.test.ts.
15
+ prompt_marker_key() {
16
+ if [ "${EPAM_BROWNFIELD:-0}" = "1" ]; then
17
+ printf '.complete-%s.brownfield' "$1"
18
+ else
19
+ printf '.complete-%s' "$1"
20
+ fi
21
+ }
22
+
23
+ # codeline_prompts_complete <codeline> — were this codeline's prompts built from the CURRENT
24
+ # inputs? Answered by project-prompt-builder.js (the digest the marker records vs the digest
25
+ # of the template layer, registry and generator now). A marker's mere existence is not an
26
+ # answer: an empty one let a launch reuse prompts built from the previous day's templates
27
+ # (regintel 20260920T232518Z, $5.73). Says why on stderr when the answer is no.
28
+ codeline_prompts_complete() {
29
+ local _cl="${1:-}"
30
+ [ -n "$_cl" ] && [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ] || return 1
31
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ local _builder="${_here}/project-prompt-builder.js"
33
+ local _out
34
+ _out=$("${NODE_BIN:-node}" -e '
35
+ const b = require(process.argv[1]);
36
+ const r = b.codelinePromptsComplete({ projectConfigDir: process.argv[2], codeline: process.argv[3],
37
+ templatesDir: process.env.EPAM_PROMPT_TEMPLATES_DIR || undefined, registryFile: process.env.EPAM_SEAM_REGISTRY_FILE || undefined });
38
+ process.stdout.write((r.complete ? "yes" : "no") + "\t" + (r.reason || ""));
39
+ ' "$_builder" "$EPAM_PROJECT_CONFIG_DIR" "$_cl" 2>/dev/null) || return 1
40
+ case "$_out" in
41
+ yes*) return 0 ;;
42
+ *) echo "[prompts] codeline ${_cl}: ${_out#* }" >&2; return 1 ;;
43
+ esac
44
+ }
45
+
46
+ # THIS CODELINE'S MINTED AGENTS ON DISK — restored from what its completion recorded
47
+ # (.prompt-cache/<codeline>/minted, project-prompt-builder.js). Returns 1, touching nothing, when the
48
+ # codeline recorded none: a marker cannot vouch for agents it never saw (AMSD-2974 run 3, 2026-09-30).
49
+ codeline_minted_restore() {
50
+ local _cl="${1:-}"
51
+ [ -n "$_cl" ] && [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ] || return 1
52
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
53
+ "${NODE_BIN:-node}" -e '
54
+ const b = require(process.argv[1]);
55
+ process.exit(b.restoreMinted({ projectConfigDir: process.argv[2], codeline: process.argv[3] }) ? 0 : 1);
56
+ ' "${_here}/project-prompt-builder.js" "$EPAM_PROJECT_CONFIG_DIR" "$_cl" 2>/dev/null
57
+ }
58
+
59
+ # The mint's clean slate: every minted file (the one list, in project-prompt-builder.js), removed.
60
+ codeline_minted_clear() {
61
+ [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ] || return 0
62
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63
+ "${NODE_BIN:-node}" -e 'require(process.argv[1]).clearMinted({ projectConfigDir: process.argv[2] });' \
64
+ "${_here}/project-prompt-builder.js" "$EPAM_PROJECT_CONFIG_DIR" 2>/dev/null || true
65
+ }
66
+
67
+ # lane_codeline_set_restore <lane PRD> — A LANE WORKS FROM ITS OWN CODELINE'S SET (operator, 2026-10-22: "prompts/profiles BY
68
+ # codeline"). The lane's codeline is its PRD's project.outputDir folder; that codeline's recorded agents and prompts become the
69
+ # active set before the lane's agents run (project-prompt-builder.js restoreCodelineSet). A codeline with no recorded set returns 1,
70
+ # says why and changes nothing: the lane never runs another codeline's set or the project-wide one.
71
+ lane_codeline_set_restore() {
72
+ local _prd="${1:-}"
73
+ [ -n "$_prd" ] && [ -f "$_prd" ] && [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ] || { echo "[prompts] lane: no PRD or project to resolve its codeline's set from" >&2; return 1; }
74
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
75
+ local _out
76
+ _out=$("${NODE_BIN:-node}" -e '
77
+ const b = require(process.argv[1]);
78
+ const codeline = b.codelineOfPrd(process.argv[3]);
79
+ const r = codeline ? b.restoreCodelineSet({ projectConfigDir: process.argv[2], codeline }) : { ok: false, reason: "its PRD names no codeline (project.outputDir)" };
80
+ process.stdout.write((r.ok ? "yes" : "no") + "\t" + (codeline || "?") + "\t" + (r.reason || ""));
81
+ ' "${_here}/project-prompt-builder.js" "$EPAM_PROJECT_CONFIG_DIR" "$_prd" 2>&1) || { echo "[prompts] lane: its codeline's set could not be resolved: ${_out}" >&2; return 1; }
82
+ local _ok="${_out%% *}" _rest="${_out#* }"; local _cl="${_rest%% *}" _why="${_rest#* }"
83
+ if [ "$_ok" = "yes" ]; then
84
+ echo "[prompts] lane: working from codeline ${_cl}'s own recorded agents and prompts" >&2
85
+ return 0
86
+ fi
87
+ echo "[prompts] lane: codeline ${_cl} has no recorded set (${_why}) — the lane does not run another codeline's or the project-wide set" >&2
88
+ return 1
89
+ }
90
+
91
+ # scope_codeline_of_prd <PRD> — the codeline the mint's reuse is decided for (project-prompt-builder.js scopeCodelineFromPrd): the
92
+ # story's single codeline; for several, its own codeline when EVERY codeline of the story has its complete recorded set; else empty.
93
+ scope_codeline_of_prd() {
94
+ local _prd="${1:-}"
95
+ [ -n "$_prd" ] && [ -f "$_prd" ] || return 0
96
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
97
+ "${NODE_BIN:-node}" -e '
98
+ const b = require(process.argv[1]);
99
+ process.stdout.write(b.scopeCodelineFromPrd({ prdFile: process.argv[3], projectConfigDir: process.argv[2],
100
+ templatesDir: process.env.EPAM_PROMPT_TEMPLATES_DIR || undefined, registryFile: process.env.EPAM_SEAM_REGISTRY_FILE || undefined }) || "");
101
+ ' "${_here}/project-prompt-builder.js" "${EPAM_PROJECT_CONFIG_DIR:-}" "$_prd" 2>/dev/null || true
102
+ }
103
+
104
+ # parallel_lanes_codeline_set_restore <lane PRD>... — LANES THAT RUN TOGETHER SHARE ONE ACTIVE SET. Each can work from its own
105
+ # codeline's set only when those sets are the same, byte for byte (project-prompt-builder.js codelineSetsIdentical): then the set is
106
+ # restored once, before they start. Otherwise returns 1 and says which differ — no lane runs another codeline's set; run the lanes
107
+ # one at a time (EPAM_PARALLEL_CODELINES=0) and each restores its own.
108
+ parallel_lanes_codeline_set_restore() {
109
+ [ "$#" -gt 0 ] || return 0
110
+ local _here; _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
111
+ local _why
112
+ _why=$("${NODE_BIN:-node}" -e '
113
+ const b = require(process.argv[1]);
114
+ const codelines = process.argv.slice(3).map((f) => b.codelineOfPrd(f));
115
+ if (codelines.some((c) => !c)) { process.stdout.write("a lane PRD names no codeline (project.outputDir)"); process.exit(1); }
116
+ const r = b.codelineSetsIdentical({ projectConfigDir: process.argv[2], codelines });
117
+ if (!r.same) { process.stdout.write(r.reason); process.exit(1); }
118
+ ' "${_here}/project-prompt-builder.js" "${EPAM_PROJECT_CONFIG_DIR:-}" "$@" 2>&1) || {
119
+ echo "[prompts] parallel lanes: their codelines' recorded sets are not the same (${_why}) — run them one at a time (EPAM_PARALLEL_CODELINES=0) so each works from its own" >&2
120
+ return 1
121
+ }
122
+ lane_codeline_set_restore "$1"
123
+ }