amsd-pipeline-workflow 0.0.0-stage → 0.1.0

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.
Files changed (134) hide show
  1. package/README.md +29 -2
  2. package/bin/amsd-pipeline-workflow.js +19 -0
  3. package/bin/bounded +26 -0
  4. package/driver/assess-step.js +16 -0
  5. package/driver/bash.sh +26 -0
  6. package/driver/block-cap.js +26 -0
  7. package/driver/bridge-hook.js +21 -0
  8. package/driver/env.sh +22 -0
  9. package/driver/generate-assess.js +74 -0
  10. package/driver/generate-bridge.js +101 -0
  11. package/driver/launch-env.js +50 -0
  12. package/driver/phase-audits.js +53 -0
  13. package/driver/phase-gate.js +19 -0
  14. package/driver/pipeline.js +49 -0
  15. package/driver/release-lines.js +92 -0
  16. package/driver/run.js +535 -0
  17. package/driver/runtime.js +164 -0
  18. package/driver/session-cost.js +132 -0
  19. package/driver/shim.js +37 -0
  20. package/driver/stages.js +265 -0
  21. package/driver/story-guards.js +33 -0
  22. package/driver/writer-stamps.js +23 -0
  23. package/generator/agents-env.sh +41 -0
  24. package/generator/agents-seed.sh +58 -0
  25. package/generator/agents-workflow.js +105 -0
  26. package/generator/agents.js +134 -0
  27. package/generator/analyst-values.sh +66 -0
  28. package/generator/criteria-facts.sh +16 -0
  29. package/generator/features.json +65 -0
  30. package/generator/flows/writer-stage.json +125 -0
  31. package/generator/generate-agents.js +220 -0
  32. package/generator/generate-spec.js +431 -0
  33. package/generator/generate.js +601 -0
  34. package/generator/inventory.js +121 -0
  35. package/generator/ladder-resume.sh +29 -0
  36. package/generator/ladder.sh +101 -0
  37. package/generator/overlay-install.sh +14 -0
  38. package/generator/pipeline-env.sh +55 -0
  39. package/generator/pipeline-module.js +50 -0
  40. package/generator/pipeline.js +93 -0
  41. package/generator/plan-no-change.sh +33 -0
  42. package/generator/project-copy.sh +13 -0
  43. package/generator/project-flags.sh +18 -0
  44. package/generator/prompt-trim.sh +10 -0
  45. package/generator/reconcile-bootstrap.js +89 -0
  46. package/generator/recorded-issues.js +72 -0
  47. package/generator/render-engine.sh +9 -0
  48. package/generator/render-writer-si.sh +35 -0
  49. package/generator/render-writer.sh +52 -0
  50. package/generator/reviewer-values.sh +56 -0
  51. package/generator/role-may-write.sh +21 -0
  52. package/generator/spec-env.inc.sh +35 -0
  53. package/generator/spec-env.sh +8 -0
  54. package/generator/spec-prompts.js +326 -0
  55. package/generator/spec-runner.js +54 -0
  56. package/generator/spec-workflow.js +327 -0
  57. package/generator/story-acs.sh +8 -0
  58. package/generator/tc-slots.js +102 -0
  59. package/generator/writer-workflow.js +993 -0
  60. package/hooks/agents-sandbox.sh +113 -0
  61. package/hooks/agents-step-body.sh +59 -0
  62. package/hooks/agents-step.js +181 -0
  63. package/hooks/agents-text.js +28 -0
  64. package/hooks/analyst-checks.sh +128 -0
  65. package/hooks/analyst-record.js +36 -0
  66. package/hooks/answer-record.js +14 -0
  67. package/hooks/assess-round.sh +68 -0
  68. package/hooks/assess-script.sh +49 -0
  69. package/hooks/attempt-changes.sh +35 -0
  70. package/hooks/attempt-classify.sh +138 -0
  71. package/hooks/attempt-verify.sh +88 -0
  72. package/hooks/bin/claude +3 -0
  73. package/hooks/bin/codemie-claude +3 -0
  74. package/hooks/bin/docker +4 -0
  75. package/hooks/bin/podman +4 -0
  76. package/hooks/commit-story.sh +19 -0
  77. package/hooks/cpa-answer.js +23 -0
  78. package/hooks/cpa-round.sh +49 -0
  79. package/hooks/fetch-standin.js +15 -0
  80. package/hooks/kb-guidance.sh +14 -0
  81. package/hooks/kb-record.js +45 -0
  82. package/hooks/model-standin.js +102 -0
  83. package/hooks/orchestrator-libs.sh +9 -0
  84. package/hooks/pipeline-step.js +36 -0
  85. package/hooks/plan-answer.js +22 -0
  86. package/hooks/plan-round.sh +43 -0
  87. package/hooks/plan-script.sh +28 -0
  88. package/hooks/post-story.sh +44 -0
  89. package/hooks/publish-agent-outputs.sh +17 -0
  90. package/hooks/replay-answer.js +23 -0
  91. package/hooks/replay-hub.sh +39 -0
  92. package/hooks/replay-lib.sh +57 -0
  93. package/hooks/repro-checks.js +61 -0
  94. package/hooks/repro-prep.sh +166 -0
  95. package/hooks/repro-validate.sh +70 -0
  96. package/hooks/retry-ext.sh +73 -0
  97. package/hooks/review-inputs.sh +58 -0
  98. package/hooks/review-record.js +24 -0
  99. package/hooks/spec-step.js +696 -0
  100. package/hooks/spec-text.js +26 -0
  101. package/hooks/step-attempt-failure.sh +11 -0
  102. package/hooks/step-kb-fallback.sh +12 -0
  103. package/hooks/step-kb-section.sh +30 -0
  104. package/hooks/step-lint-fix-verify.sh +52 -0
  105. package/hooks/step-lint-gate.sh +110 -0
  106. package/hooks/step-no-change.sh +22 -0
  107. package/hooks/step-plan-fail.sh +2 -0
  108. package/hooks/step-plan-start.sh +2 -0
  109. package/hooks/step-pre-attempt.sh +42 -0
  110. package/hooks/step-pre-review.sh +37 -0
  111. package/hooks/step-repro-gate.sh +24 -0
  112. package/hooks/step-resolve-escalation.sh +29 -0
  113. package/hooks/step-retry-ext-apply.sh +2 -0
  114. package/hooks/step-retry-ext-prep.sh +2 -0
  115. package/hooks/step-review-refresh.sh +19 -0
  116. package/hooks/step-review-tests.sh +29 -0
  117. package/hooks/step-rung-entry.sh +29 -0
  118. package/hooks/step-spec-pause.sh +210 -0
  119. package/hooks/step-tc-apply.sh +25 -0
  120. package/hooks/step-tc-prep.sh +47 -0
  121. package/hooks/step-uit-check.sh +43 -0
  122. package/hooks/step-vc-prep.sh +33 -0
  123. package/hooks/story-changed.sh +16 -0
  124. package/hooks/tc-capture-hub.sh +14 -0
  125. package/hooks/tc-record.js +15 -0
  126. package/hooks/tool-record.js +48 -0
  127. package/hooks/tool-write.sh +24 -0
  128. package/hooks/uit-answer.js +16 -0
  129. package/hooks/uit-fence.js +18 -0
  130. package/hooks/uit-judge.sh +52 -0
  131. package/hooks/writer-checks.js +273 -0
  132. package/hooks/writer-process.sh +44 -0
  133. package/mcp/pipeline-tools.js +72 -0
  134. package/package.json +8 -4
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # amsd-pipeline-workflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The AMSD pipeline driven as Claude workflows. It runs **against an installed AMSD pipeline** (it calls the pipeline's
4
+ own scripts) and is installed by the pipeline's installer.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx amsd-pipeline@<version> --dest <dir> --ref v<version> --no-docker --workflows
10
+ ```
11
+
12
+ `--workflows` installs the `amsd-pipeline-workflow` version the pipeline release pins
13
+ (`orchestrations/config/claude-workflows.json`) into `<dir>/claude-workflows/`. `--workflows <file.tgz>` installs a
14
+ local package instead (for testing a build before it is published). `--check` verifies it; `--uninstall` removes it.
15
+
16
+ Needs: Node 20+, the `claude` CLI, `unshare` (util-linux) and `systemd-run --user` (the memory bound, `bin/bounded`).
17
+
18
+ ## Run
19
+
20
+ ```bash
21
+ <dir>/claude-workflows/bin/amsd-pipeline-workflow.js run --project <p> --run <id> --out <run dir> --codeline-map <lane=copy> ...
22
+ ```
23
+
24
+ `--install` defaults to the install it was installed into. The arguments are `driver/run.js`'s (see its header).
25
+ Langfuse tracing follows the install's own settings (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`): each agent call is
26
+ sent as one generation through the pipeline's own emitter.
27
+
28
+ ## Version
29
+
30
+ `amsd-pipeline-workflow version`. `package.json` `amsdPipeline.tested` names the pipeline release it was tested against.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // amsd-pipeline-workflow — the AMSD pipeline as Claude workflows. Installed by the pipeline's own installer
4
+ // (install.sh --workflows) into <install>/claude-workflows/, which writes install.json there naming the install.
5
+ // amsd-pipeline-workflow run <driver/run.js arguments> — --install defaults to the install this was installed into
6
+ // amsd-pipeline-workflow version
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { spawnSync } = require('child_process');
10
+
11
+ const ROOT = path.resolve(__dirname, '..');
12
+ const [cmd, ...rest] = process.argv.slice(2);
13
+ if (cmd === 'version') { process.stdout.write(`${require(path.join(ROOT, 'package.json')).version}\n`); process.exit(0); }
14
+ if (cmd !== 'run') { process.stderr.write('usage: amsd-pipeline-workflow run <driver arguments> | version\n'); process.exit(64); }
15
+ const args = [...rest];
16
+ const linked = path.join(ROOT, 'install.json');
17
+ if (!args.includes('--install') && fs.existsSync(linked)) args.unshift('--install', JSON.parse(fs.readFileSync(linked, 'utf8')).install);
18
+ const r = spawnSync(process.execPath, [path.join(ROOT, 'driver', 'run.js'), ...args], { stdio: 'inherit' });
19
+ process.exit(r.status === null ? 1 : r.status);
package/bin/bounded ADDED
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # bounded [--want <MB>] -- <command...>
3
+ #
4
+ # The ONLY sanctioned way to launch anything heavy from a Claude session on this host.
5
+ # The cap is derived from what the HOST can spare right now, never from what the job wants:
6
+ # cap = min(want, available - RESERVE) and the job is refused outright if cap < FLOOR.
7
+ # Two WSL restarts on 2026-09-11 came from MemoryMax values (12G, 6G) chosen against a 13G host
8
+ # that already held ~6G. A cap the host cannot honour is not a cap.
9
+ RESERVE_MB=${BOUNDED_RESERVE_MB:-3072} # always leave the host this much
10
+ FLOOR_MB=${BOUNDED_FLOOR_MB:-1536} # below this the job does not run
11
+ WANT_MB=4096
12
+ if [ "${1:-}" = "--want" ]; then WANT_MB="$2"; shift 2; fi
13
+ [ "${1:-}" = "--" ] && shift
14
+ [ $# -gt 0 ] || { echo "bounded: no command" >&2; exit 64; }
15
+
16
+ AVAIL_MB=$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo)
17
+ HEADROOM_MB=$(( AVAIL_MB - RESERVE_MB ))
18
+ if [ "$HEADROOM_MB" -lt "$FLOOR_MB" ]; then
19
+ echo "bounded: REFUSED — host has ${AVAIL_MB}MB available, reserve ${RESERVE_MB}MB leaves ${HEADROOM_MB}MB, floor is ${FLOOR_MB}MB." >&2
20
+ echo "bounded: free memory (stop stacks / batch smaller) before running this." >&2
21
+ exit 75
22
+ fi
23
+ CAP_MB=$HEADROOM_MB
24
+ [ "$CAP_MB" -gt "$WANT_MB" ] && CAP_MB=$WANT_MB
25
+ echo "bounded: cap=${CAP_MB}MB (host available ${AVAIL_MB}MB, reserve ${RESERVE_MB}MB, wanted ${WANT_MB}MB)" >&2
26
+ exec systemd-run --user --scope --quiet -p MemoryMax="${CAP_MB}M" -p MemorySwapMax=0 "$@"
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // THE PHASE ASSESSMENT'S STEP RUNNER HOOK (driver/generate-assess.js): on its structured answer, run the assessment round
4
+ // (hooks/assess-round.sh start, or answer --failed when the team lead's call got no answer) and put its result in the answer.
5
+ // assess-step.js --pipeline <assess-pipeline.json> --mode start|fail (stdin: the PreToolUse hook's JSON)
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const a = process.argv.slice(2); const arg = (n) => a[a.indexOf(`--${n}`) + 1];
11
+ let input = {}; try { input = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { /* nothing */ }
12
+ if (input.tool_name !== 'StructuredOutput') process.exit(0);
13
+ const round = path.join(__dirname, '..', 'hooks', 'assess-round.sh');
14
+ const r = spawnSync('bash', [round, ...(arg('mode') === 'start' ? ['start', arg('pipeline')] : ['answer', arg('pipeline'), '--failed'])], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
15
+ let result; try { result = JSON.parse(String(r.stdout).trim().split('\n').pop()); } catch { result = { error: true, reason: (r.stderr || `exit ${r.status}`).slice(-400) }; }
16
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput: { ...(input.tool_input || {}), result } } })}\n`);
package/driver/bash.sh ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # ONE OF THE PIPELINE'S OWN BASH FUNCTIONS, CALLED UNCHANGED in the run's environment. No AI.
3
+ # bash.sh <env.json> <function> [args...] (stdout and the exit code are the function's)
4
+ # Loads the libraries run-agent-orchestration.sh loads for the lane loop (RAO:144–222, 409, 497–504, 634, 769–775), as
5
+ # the orchestrator runs them: `set -e` off here because each call's status is the caller's to judge, no `set -u`, no
6
+ # pipefail (RAO:75). The pipeline's log lines go to stderr; stdout carries only what the function prints.
7
+ ENVF="${1:?env.json}"; shift
8
+ eval "$(jq -r 'to_entries[] | select(.key | test("^[A-Za-z_][A-Za-z0-9_]*$")) | "export \(.key)=\(.value | @sh)"' "$ENVF")" || exit 90
9
+ set +u +o pipefail
10
+ RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' WHITE='' NC=''
11
+ for _l in env-file jq-vals orch-common flags git-ops prd-state story-retry-state run-checkpoint checkpoints prd-integrity \
12
+ story-guards story-outputs codeline-lanes control-plane halt-recovery cost-ledger phase-exit resume-semantics \
13
+ render-engine-prompt plan-of-record before-the-writer run-modes orchestration-resume mint-and-spec qa-gate-evidence; do
14
+ # shellcheck source=/dev/null
15
+ [ -f "$SCRIPT_DIR/lib/$_l.sh" ] && { . "$SCRIPT_DIR/lib/$_l.sh" >/dev/null 2>&1 || { echo "lib/$_l.sh did not load" >&2; exit 91; }; }
16
+ done
17
+ declare -F log >/dev/null || log() { echo "$*" >&2; }
18
+ declare -F info >/dev/null || info() { echo "[INFO] $*" >&2; }
19
+ declare -F warning >/dev/null || warning() { echo "[WARNING] $*" >&2; }
20
+ declare -F error >/dev/null || error() { echo "[ERROR] $*" >&2; }
21
+ declare -F success >/dev/null || success() { echo "$*" >&2; }
22
+ declare -F step_emit >/dev/null || step_emit() { :; }
23
+ "$@"; _rc=$?
24
+ # BASH_CALL_ENV_OUT: what the function exported (apply_resume_if_requested, _read_estimation_manifest), as JSON
25
+ [ -n "${BASH_CALL_ENV_OUT:-}" ] && "$NODE_BIN" -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify(process.env))' "$BASH_CALL_ENV_OUT"
26
+ exit "$_rc"
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+ // THE SAME GATE BLOCKING THE SAME STORY, AGAIN AND AGAIN, STOPS THE PHASE (operator, 2026-10-16: "Stop after a few
3
+ // retries"). A recorded difference from the pipeline (backlog B39): its lane loop retries an exit-2 phase while HEAD moves,
4
+ // and every Writer run moves it, so a story a gate keeps blocking retries forever. Here a phase stops — failed, naming
5
+ // the gate — when its last BLOCK_CAP runs ended in the same block of the same story.
6
+ const BLOCK_CAP = 3;
7
+
8
+ /** What blocked a Writer run: the story and the gate (the reason up to its detail), or null when nothing blocked. */
9
+ function signature(story, res) {
10
+ if (!res || res.status !== 'blocked') return null;
11
+ return `${story}|${String(res.reason || '').split(' — ')[0]}`;
12
+ }
13
+
14
+ /** A tracker for one phase's runs: note(sig) after each run; stuck() when the last BLOCK_CAP runs share one block. */
15
+ function tracker() {
16
+ const seen = [];
17
+ return {
18
+ note(sig) { seen.push(sig || null); },
19
+ stuck() {
20
+ const last = seen.slice(-BLOCK_CAP);
21
+ return last.length === BLOCK_CAP && last[0] !== null && last.every((x) => x === last[0]) ? last[0] : null;
22
+ },
23
+ };
24
+ }
25
+
26
+ module.exports = { BLOCK_CAP, signature, tracker };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // THE BRIDGE'S ANSWER, JUDGED AS _run_codeline_bridge JUDGES IT (lib/codeline-lanes.sh :486–507) — a PreToolUse hook on
4
+ // the codeline-bridge-agent's StructuredOutput. No AI.
5
+ // bridge-hook.js --bridge <bridge.json> --attempt N --last 0|1 (stdin: the hook's JSON)
6
+ // The contract is the file the agent wrote; when it wrote none, its reply (`contract`) is the contract (:491–493). The
7
+ // verdict and, on failure, which correction the next attempt carries go back in the answer as `result`. Before the next
8
+ // attempt the call site removes the file (:487), so a failed attempt's file is removed here.
9
+ const fs = require('fs');
10
+ function args(argv) { const a = {}; for (let i = 0; i < argv.length; i += 2) a[argv[i].replace(/^--/, '')] = argv[i + 1]; return a; }
11
+ const a = args(process.argv.slice(2));
12
+ let input = {}; try { input = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { /* nothing */ }
13
+ if (input.tool_name && input.tool_name !== 'StructuredOutput') process.exit(0);
14
+ const ans = input.tool_input || {};
15
+ const b = JSON.parse(fs.readFileSync(a.bridge, 'utf8'));
16
+ const size = () => { try { return fs.statSync(b.bridgeOut).size; } catch { return -1; } };
17
+ if (size() <= 0 && typeof ans.contract === 'string' && ans.contract.trim()) fs.writeFileSync(b.bridgeOut, ans.contract);
18
+ const s = size();
19
+ const result = s > 0 ? { ok: true } : { ok: false, why: s === 0 ? 'empty' : 'missing' };
20
+ if (!result.ok) fs.rmSync(b.bridgeOut, { force: true });
21
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput: { ...ans, result } } })}\n`);
package/driver/env.sh ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # THE RUN'S ENVIRONMENT — what orchestrate.sh / run-agent-orchestration.sh export before `_run_jira_pipeline` and
3
+ # `_run_codeline_loop` run (the parent process, not a lane). Prints it as one JSON object. No AI.
4
+ # env.sh <install> <project> <run-id> <run-dir> <set>
5
+ # The project's own declarations come from its own loader (lib/env-file.sh load_project_env), over the launch settings
6
+ # already in the environment (a set key is never overwritten — the loader's rule). Everything the run writes is under
7
+ # <run-dir>: the project copy the run works in (EPAM_PROJECT_CONFIG_DIR, written by the driver), its log folder (LOG_DIR),
8
+ # the canonical PRD (PRD_FILE), and every pipeline write path the parent exports (RAO:470–492).
9
+ set -uo pipefail
10
+ INSTALL="$1" PROJECT="$2" RUN="$3" RD="$4" SET="${5:-claude}"
11
+ ORCH="$INSTALL/orchestrations"
12
+ export SCRIPT_DIR="$ORCH/scripts" AUTOMATION_DIR="$ORCH" NODE_BIN="${NODE_BIN:-$(command -v node)}" EPAM_PROVIDER_SET="$SET"
13
+ export EPAM_PROJECT_CONFIG_DIR="$RD/project"
14
+ # shellcheck source=/dev/null
15
+ . "$SCRIPT_DIR/lib/env-file.sh" && load_project_env "$EPAM_PROJECT_CONFIG_DIR" >/dev/null 2>&1 || { echo "project env did not load" >&2; exit 1; }
16
+ export ORCH_RUN_ID="$RUN" PRD_FILE="$RD/prd.json" MAIN_PRD_FILE="$RD/prd.json" LOG_DIR="$RD/logs"
17
+ export AGENTS_DIR="$ORCH/agents" EPAM_AGENTS_DIR="$ORCH/agents" AGENT_IO_DIR="$RD/logs/agent-io" TMPDIR="$RD/tmp"
18
+ export MONITOR_FILE="$RD/logs/agent-status.json" ACTIVITY_FILE="$RD/logs/agent-activity.jsonl" MESSAGES_JSONL="$RD/logs/agent-messages.jsonl"
19
+ export PHASE_COST_FILE="$RD/logs/phase-cost.jsonl" REVIEW_LOG="$RD/logs/code-reviews.jsonl" GATE_LOG="$RD/logs/phase-gates.jsonl"
20
+ export COST_LOG="$RD/logs/phase-cost.jsonl" MESSAGES_DIR="$RD/logs/messages" EPAM_AGENT_REPLY_LOG_DIR="$RD/agent-replies"
21
+ mkdir -p "$LOG_DIR" "$TMPDIR" "$EPAM_AGENT_REPLY_LOG_DIR"
22
+ "$NODE_BIN" -e 'process.stdout.write(JSON.stringify(process.env))'
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // STEPS 18 AND 24, THE PHASE SKILL ASSESSMENT (feature skill-assessment, decision 43; run-agent-orchestration.sh:3340–3358,
4
+ // :4318–4337) AS A NATIVE WORKFLOW the driver runs at phase level: the pipeline's own run_phase_assessment, run unchanged by
5
+ // hooks/assess-round.sh, its call to the team lead answered by this workflow's agent through the replay runner
6
+ // (hooks/replay-lib.sh); a retry it climbs to the next model for is asked on that model.
7
+ // node driver/generate-assess.js --install <dir> --codeline <copy> --out <dir> --label <step-18|step-24> [--set claude|codemie]
8
+ // --env <the lane's environment, JSON>
9
+ // Writes only under --out. No AI.
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const agents = require('../generator/agents.js');
13
+
14
+ function args(argv) { const a = {}; for (let i = 0; i < argv.length; i += 1) { if (argv[i].startsWith('--')) { a[argv[i].slice(2)] = argv[i + 1]; i += 1; } } return a; }
15
+ const q = (x) => `'${String(x).replace(/'/g, "'\\''")}'`;
16
+
17
+ function main(a) {
18
+ const env = JSON.parse(a.env || '{}');
19
+ const orch = path.join(a.install, 'orchestrations');
20
+ const out = path.resolve(a.out); fs.mkdirSync(out, { recursive: true });
21
+ const envFile = path.join(out, 'env.json'); fs.writeFileSync(envFile, JSON.stringify(env));
22
+ const pj = path.join(out, 'assess-pipeline.json');
23
+ fs.writeFileSync(pj, JSON.stringify({ state: path.join(out, 'state'), env: envFile, step: 'post', label: a.label || 'post' }));
24
+ const projectDir = env.EPAM_PROJECT_CONFIG_DIR;
25
+ const art = { orch, projectDir, checkpoint: projectDir, scripts: path.join(orch, 'scripts'), agentsDir: path.join(orch, 'agents'),
26
+ registry: path.join(orch, 'agents', 'invocation-profiles.json'), set: a.set || 'claude' };
27
+ const d = agents.definitionFor(art, 'team-lead-agent', path.resolve(a.codeline), fs.mkdtempSync(path.join(out, 'scratch-')), {});
28
+ const hook = (cmd) => ({ PreToolUse: [{ matcher: 'StructuredOutput', hooks: [{ type: 'command', command: cmd.map(q).join(' ') }] }] });
29
+ const step = (mode) => ({ description: `Runs the phase skill assessment's ${mode} step (its hook decides).`, model: d.frontmatter.model, maxTurns: 3, tools: [],
30
+ omitClaudeMd: true, hooks: hook([process.execPath, path.join(__dirname, 'assess-step.js'), '--pipeline', pj, '--mode', mode]),
31
+ prompt: 'Reply at once by giving your structured answer {"done": true}. Do nothing else: a hook runs the step, and its result is the answer.' });
32
+ const defs = {
33
+ 'assess-start': step('start'), 'assess-fail': step('fail'),
34
+ 'assess-team-lead': { ...d.frontmatter, name: undefined, description: `${d.frontmatter.description} Phase assessment (${a.label}).`,
35
+ hooks: hook([process.execPath, path.join(__dirname, '..', 'hooks', 'replay-answer.js'), 'answer', '--round', 'assess-round.sh', '--pipeline', pj]),
36
+ prompt: d.body.trim() },
37
+ };
38
+ delete defs['assess-team-lead'].name;
39
+ fs.writeFileSync(path.join(out, 'agents-assess.json'), JSON.stringify(defs, null, 2));
40
+ fs.mkdirSync(path.join(out, 'workflows'), { recursive: true });
41
+ fs.writeFileSync(path.join(out, 'workflows', 'assess.js'), emit({ label: a.label || 'post' }));
42
+ return { pj };
43
+ }
44
+
45
+ function emit(plan) {
46
+ return `export const meta = {
47
+ name: 'assess',
48
+ description: 'The phase skill assessment: the team lead reads the phase\\'s cost records and recommends role and profile changes',
49
+ phases: [{ title: 'Assess' }],
50
+ }
51
+ // GENERATED by driver/generate-assess.js — do not edit. lib/phase-assessment.sh run_phase_assessment, by hooks/assess-round.sh.
52
+ const PLAN = ${JSON.stringify(plan)}
53
+ // what lib/handlers/run-phase-assessment.py accepts (any object) and assess-apply.py reads
54
+ const SCHEMA = { type: 'object', properties: { notes: { type: 'string' }, agent_recommendations: { type: 'array', items: { type: 'string' } },
55
+ role_reassignments: { type: 'array', items: { type: 'object', properties: { story_id: { type: 'string' }, newAgentRole: { type: 'string' }, reason: { type: 'string' } } } } } }
56
+ const STEP = { type: 'object', properties: { done: { type: 'boolean' } } }
57
+ phase('Assess')
58
+ const run = async (agentType, label) => { const s = await agent('Reply now.', { agentType, schema: STEP, label, phase: 'Assess' }); return (s && s.result) || { error: true, reason: 'the step runner gave no result' } }
59
+ let r = await run('assess-start', \`\${PLAN.label}: the phase assessment\`)
60
+ while (r && r.pending) {
61
+ const q = r.pending
62
+ if (q.agent !== 'team-lead-agent') return { status: 'failed', exit: 0, reason: \`the assessment asked '\${q.agent}', which this workflow has no definition for\` }
63
+ const a = await agent(q.prompt, { agentType: 'assess-team-lead', ...(q.model ? { model: q.model } : {}), schema: SCHEMA, label: \`\${PLAN.label}: call \${q.n}\`, phase: 'Assess' })
64
+ r = a ? a.next : await run('assess-fail', \`\${PLAN.label}: call \${q.n} got no answer\`)
65
+ }
66
+ if (!r || r.error || !r.done) return { status: 'failed', exit: 0, reason: (r && r.reason) || 'no result' }
67
+ return { status: r.exit === 0 ? 'ok' : 'warn', exit: 0, calls: r.calls, outcome: r.outcome }
68
+ `;
69
+ }
70
+
71
+ module.exports = { main, emit };
72
+ if (require.main === module) {
73
+ try { process.stdout.write(`${JSON.stringify(main(args(process.argv.slice(2))))}\n`); } catch (e) { process.stderr.write(`${e.stack || e.message}\n`); process.exit(1); }
74
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // THE CODELINE BRIDGE AS A NATIVE WORKFLOW (decision 24: built in the launch layer). lib/codeline-lanes.sh
4
+ // _run_codeline_bridge (:464–534) after a lane completes in a run of more than one codeline: the engine template
5
+ // codeline-bridge rendered by the pipeline's own renderer (render_engine_prompt), asked of the codeline-bridge-agent seam,
6
+ // up to 1 + BRIDGE_MAX_RETRIES times with the call site's correction in front; the contract is the file the agent wrote
7
+ // or, failing that, its reply (:491–493).
8
+ //
9
+ // node driver/generate-bridge.js --install <dir> --project <p> --run <id> --lane <codeline> --codeline <copy>
10
+ // --prd <the lane's PRD> --out <dir> [--set claude|codemie] --env <{"LOG_DIR": the run's log folder}>
11
+ // The run's environment is this process's (the driver runs it so). Writes only under --out, and removes a stale
12
+ // contract at the bridge's own path (:487, the first attempt's `rm -f`). No AI.
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const agents = require('../generator/agents.js');
16
+ const pipe = require('../generator/pipeline.js');
17
+ const { bash } = require('./pipeline.js');
18
+
19
+ function args(argv) { const a = {}; for (let i = 0; i < argv.length; i += 1) { if (argv[i].startsWith('--')) { a[argv[i].slice(2)] = argv[i + 1]; i += 1; } } return a; }
20
+ const q = (x) => `'${String(x).replace(/'/g, "'\\''")}'`;
21
+
22
+ function main(a) {
23
+ const env = { ...process.env, ...JSON.parse(a.env || '{}') };
24
+ const orch = path.join(a.install, 'orchestrations');
25
+ const out = path.resolve(a.out); fs.mkdirSync(out, { recursive: true });
26
+ const bridgeOut = path.join(env.LOG_DIR, `cross-codeline-${a.lane}.md`); // :466
27
+ const maxRetries = Number(env.BRIDGE_MAX_RETRIES || 2); // :468
28
+ // :476–483 — the template, by the pipeline's own renderer, in the run's environment
29
+ const vals = path.join(out, 'bridge-vals.json');
30
+ fs.writeFileSync(vals, JSON.stringify({ __BRIDGE_OUT__: bridgeOut, __BPRD__: path.resolve(a.prd), __BCL__: a.lane, __BWT__: path.resolve(a.codeline) }));
31
+ const r = bash(env, 'render_engine_prompt', ['codeline-bridge', vals]);
32
+ if (r.status !== 0 || !r.stdout.trim()) throw new Error(`codeline-bridge did not render: ${r.stderr.slice(-600)}`);
33
+ const base = r.stdout.replace(/\n+$/, ''); // captured with $(...) at :482
34
+ fs.rmSync(bridgeOut, { force: true });
35
+ // the seam's agent, provisioned as every other agent (decision 13); the pause-linked roster is the run's project copy
36
+ const projectDir = env.EPAM_PROJECT_CONFIG_DIR;
37
+ const art = { orch, projectDir, checkpoint: projectDir, scripts: path.join(orch, 'scripts'), agentsDir: path.join(orch, 'agents'),
38
+ registry: path.join(orch, 'agents', 'invocation-profiles.json'), set: a.set || 'claude' };
39
+ const d = agents.definitionFor(art, 'codeline-bridge-agent', path.resolve(a.codeline), fs.mkdtempSync(path.join(out, 'scratch-')), {});
40
+ const pj = path.join(out, 'bridge.json');
41
+ fs.writeFileSync(pj, JSON.stringify({ bridgeOut, lane: a.lane }));
42
+ const defs = {};
43
+ for (let k = 0; k <= maxRetries; k += 1) {
44
+ const name = `codeline-bridge-agent-a${k}`;
45
+ defs[name] = { ...d.frontmatter, name: undefined, description: `${d.frontmatter.description} Bridge attempt ${k + 1}.`,
46
+ hooks: { PreToolUse: [{ matcher: 'StructuredOutput', hooks: [{ type: 'command',
47
+ command: [process.execPath, path.join(__dirname, 'bridge-hook.js'), '--bridge', pj, '--attempt', String(k), '--last', String(k === maxRetries ? 1 : 0)].map(q).join(' ') }] }] },
48
+ prompt: d.body.trim() };
49
+ delete defs[name].name;
50
+ }
51
+ // the hub's path rewrite of every prompt it hands a runner (llm-handler.sh:236–238)
52
+ const { relativizeLocalPaths } = require(path.join(orch, 'scripts', 'lib', 'relative-paths.js'));
53
+ const bases = [env.PROJECT_ROOT, env.JIRA_CODELINE_ROOT].filter(Boolean);
54
+ const word = (t) => relativizeLocalPaths(t.split('{out}').join(bridgeOut).split('{wt}').join(path.resolve(a.codeline)), bases);
55
+ const plan = { lane: a.lane, bridgeOut, maxRetries, base: relativizeLocalPaths(base, bases), bases, model: d.frontmatter.model,
56
+ blockOnFailure: env.CODELINE_BRIDGE_BLOCK_ON_FAILURE === 'true',
57
+ notes: {
58
+ // :499–507 — the correction for the next attempt, as the call site words it
59
+ failed: word('run_orch_prompt exited with code {rc}. You must call WriteFile to write the contract to {out} before finishing.'),
60
+ missing: word('The file {out} was NOT written. You MUST call WriteFile with path={out} to produce the contract — do not finish without writing it.'),
61
+ empty: word('The file {out} was written but is EMPTY. The source directory {wt} contains TypeScript files — read them and extract their exports before writing the contract.'),
62
+ },
63
+ codeline: path.resolve(a.codeline) };
64
+ fs.writeFileSync(path.join(out, 'agents-bridge.json'), JSON.stringify(defs, null, 2));
65
+ fs.mkdirSync(path.join(out, 'workflows'), { recursive: true });
66
+ fs.writeFileSync(path.join(out, 'workflows', 'bridge.js'), emit(plan));
67
+ fs.writeFileSync(path.join(out, 'bridge-plan.json'), JSON.stringify(plan, null, 2));
68
+ return plan;
69
+ }
70
+
71
+ function emit(plan) {
72
+ return `export const meta = {
73
+ name: 'bridge',
74
+ description: 'The codeline bridge: the completed codeline\\'s exported surface, as the cross-codeline contract the next lanes read',
75
+ phases: [{ title: 'Bridge' }],
76
+ }
77
+ // GENERATED by driver/generate-bridge.js — do not edit. lib/codeline-lanes.sh _run_codeline_bridge :464–534.
78
+ const PLAN = ${JSON.stringify(plan)}
79
+ const SCHEMA = { type: 'object', required: ['contract'], properties: { contract: { type: 'string' } } }
80
+ phase('Bridge')
81
+ let note = ''
82
+ for (let k = 0; k <= PLAN.maxRetries; k++) {
83
+ const prompt = (note ? \`CORRECTION REQUIRED — YOUR PREVIOUS ATTEMPT FAILED: \${note}\\n\\n\` : '') + PLAN.base
84
+ const a = await agent(prompt, { agentType: \`codeline-bridge-agent-a\${k}\`, schema: SCHEMA, model: PLAN.model, label: \`bridge \${PLAN.lane} #\${k + 1}\` })
85
+ // an agent that ended without an answer is the call that failed (:497); its exit code is the runtime's, not a runner's
86
+ const res = a && a.result ? a.result : { ok: false, why: 'failed' }
87
+ if (res.ok) return { status: 'ok', exit: 0, lane: PLAN.lane, contract: PLAN.bridgeOut, attempts: k + 1 }
88
+ note = (PLAN.notes[res.why] || PLAN.notes.failed).split('{rc}').join('(the agent ended without an answer)')
89
+ if (k < PLAN.maxRetries) log(\`[bridge] Attempt \${k + 1}/\${PLAN.maxRetries} failed for '\${PLAN.lane}' — retrying with corrective prompt\`)
90
+ }
91
+ // :516–533 — out of attempts: fatal only when the project asks for it
92
+ return PLAN.blockOnFailure
93
+ ? { status: 'failed', exit: 1, lane: PLAN.lane, reason: \`contract extraction failed after \${PLAN.maxRetries + 1} attempt(s) and CODELINE_BRIDGE_BLOCK_ON_FAILURE=true\` }
94
+ : { status: 'no-contract', exit: 0, lane: PLAN.lane, reason: \`contract extraction failed after \${PLAN.maxRetries + 1} attempt(s) — downstream codelines proceed without it\` }
95
+ `;
96
+ }
97
+
98
+ module.exports = { main, emit };
99
+ if (require.main === module) {
100
+ try { main(args(process.argv.slice(2))); } catch (e) { process.stderr.write(`${e.stack || e.message}\n`); process.exit(1); }
101
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+ // THE RUN'S LAUNCH SETTINGS, RECORDED AND REPLAYED (operator decision, 2026-10-16). The pipeline reads its behaviour from
3
+ // the environment it is launched with (pauses, skips, retries, provider set …); a resume launched with a different
4
+ // environment is a different run. The first launch records the settings it ran with (<run dir>/launch-env.json); every
5
+ // resume replays them — a setting given again on the resume's own command line wins, and each resume's record says which
6
+ // it changed. Secrets are never recorded. The Writer generator records the same settings in its plan (`launchEnv`); the
7
+ // driver reads that field and refuses a plan made under other settings (driver/stages.js), never re-derives it.
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // The families the pipeline reads as launch settings (lib/run-modes.sh snapshot_operator_env records every exported name;
12
+ // these are the ones the engine's own docs and config.env name). Exact names, not patterns, are added where a family
13
+ // would sweep in a credential.
14
+ const FAMILIES = /^(EPAM_|SKIP_|SPEC_|VC_|CODEGRAPH_|ORCH_|TICKET_LINK_|BRIDGE_|CODELINE_|REPRO_|SEAM_|STRICT_|RUNCLAUDE_|JIRA_)/;
15
+ const NEVER = /(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|_KEY$|AUTH|COOKIE|CREDENTIAL|EMAIL)/i; // secrets, and no PII
16
+ // named settings a credential pattern would otherwise sweep out
17
+ const ALSO = new Set(['JIRA_PROJECT_KEY']);
18
+ const RUN_LOCAL = new Set(['EPAM_RESUME_RUN', 'EPAM_DRIVER_LIVE_APPROVED', 'EPAM_OPERATOR_SET_VARS', 'EPAM_PROJECT_CONFIG_DIR']);
19
+ // launch settings outside the families: the Node heap every pipeline process inherits (run 9's launcher sets it,
20
+ // launch-live-claude.sh:12; a codeline's sibling build can run out of heap without it)
21
+ const NAMED = new Set(['NODE_OPTIONS']);
22
+
23
+ function pick(env) {
24
+ const keep = (k) => NAMED.has(k) || (FAMILIES.test(k) && (ALSO.has(k) || !NEVER.test(k)) && !RUN_LOCAL.has(k));
25
+ return Object.fromEntries(Object.entries(env).filter(([k]) => keep(k)).sort(([a], [b]) => a.localeCompare(b)));
26
+ }
27
+
28
+ function record(runDir, env, log) {
29
+ const f = path.join(runDir, 'launch-env.json');
30
+ if (fs.existsSync(f)) throw new Error(`${f} exists: this run was launched before — resume it (--resume), or use a new run folder`);
31
+ const values = pick(env);
32
+ fs.writeFileSync(f, JSON.stringify({ recordedAt: new Date().toISOString(), values }, null, 2));
33
+ if (log) log(`[launch] recorded ${Object.keys(values).length} launch setting(s) → ${f}`);
34
+ return { env: values, changed: [] };
35
+ }
36
+
37
+ function replay(runDir, env, log) {
38
+ const f = path.join(runDir, 'launch-env.json');
39
+ if (!fs.existsSync(f)) throw new Error(`no recorded launch settings at ${f}: this run was never launched by the driver`);
40
+ const rec = JSON.parse(fs.readFileSync(f, 'utf8')).values;
41
+ const now = pick(env);
42
+ const out = { ...rec }; const changed = [];
43
+ for (const [k, v] of Object.entries(now)) if (rec[k] !== v) { out[k] = v; changed.push({ key: k, was: rec[k] === undefined ? null : rec[k], now: v }); }
44
+ const n = fs.readdirSync(runDir).filter((x) => /^launch-env\.resume-\d+\.json$/.test(x)).length + 1;
45
+ fs.writeFileSync(path.join(runDir, `launch-env.resume-${n}.json`), JSON.stringify({ resumedAt: new Date().toISOString(), changed, values: out }, null, 2));
46
+ if (log) log(`[launch] replayed ${Object.keys(rec).length} recorded setting(s); changed on this resume: ${changed.map((c) => c.key).join(', ') || 'none'}`);
47
+ return { env: out, changed };
48
+ }
49
+
50
+ module.exports = { record, replay, pick };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+ // STEPS 11 AND 12, ONCE PER PHASE BEFORE ITS STORIES (run-agent-orchestration.sh:3094–3323), by the pipeline's own scans:
3
+ // Step 11 run_skills_audit_scan <profiles.json>: duplicate skill notes removed from the profiles; the self-contradictory
4
+ // ones would go to the skills-coordinator — whose seam is declared nowhere, so its call can never run
5
+ // (pipeline backlog B41): said, not run
6
+ // Step 12 run_tools_audit_scan <codeline>/.epam/dynamic-tools <log>: a broken tool would go to the tools-coordinator (the
7
+ // same: B41) and is recorded as the pipeline records it; a duplicate pair is flagged for manual review — both in
8
+ // <log>/tools-coordinator-audit.jsonl (RAO:3283–3318)
9
+ // A failed scan is a warning, never a stop (RAO:3100–3104, 3203–3207). SKIP_SKILLS_AUDIT / SKIP_TOOLS_AUDIT skip each.
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { bash } = require('./pipeline.js');
13
+
14
+ const truthy = (v) => /^(1|true|yes|on)$/i.test(String(v || ''));
15
+ const json = (t, fallback) => { try { return JSON.parse(String(t).trim().split('\n').pop()); } catch { return fallback; } };
16
+
17
+ function runPhaseAudits(env, { wt, phase }, log) {
18
+ const out = { skills: null, tools: null };
19
+ if (truthy(env.SKIP_SKILLS_AUDIT)) log('[orch] Step 11: Skills coordinator audit skipped (SKIP_SKILLS_AUDIT=1)');
20
+ else {
21
+ const r = bash(env, 'run_skills_audit_scan', [env.AGENT_PROFILES_FILE], { log });
22
+ const ok = r.status === 0;
23
+ const res = ok ? json(r.stdout, null) : null;
24
+ out.skills = { ok: ok && !!res, ...(res || { duplicates_removed: 0, contradictions: [] }) };
25
+ if (!out.skills.ok) log(` [SkillsAudit] scan failed — profiles.json was NOT audited this phase: ${(r.stdout + r.stderr).trim().slice(-300)}`);
26
+ if (out.skills.duplicates_removed > 0) log(` [SkillsAudit] Removed ${out.skills.duplicates_removed} duplicate skill note(s) from profiles.json`);
27
+ const n = (out.skills.contradictions || []).length;
28
+ if (n) log(` [SkillsAudit] ${n} suspected self-contradictory skill note(s) found — the skills-coordinator would rewrite them; its seam is declared nowhere, so the pipeline's call can never run (backlog B41) — not run`);
29
+ log(`[orch] Step 11: Skills coordinator audit → ${out.skills.ok ? 'pass' : 'warn'}`);
30
+ }
31
+ if (truthy(env.SKIP_TOOLS_AUDIT)) log('[orch] Step 12: Tools coordinator audit skipped (SKIP_TOOLS_AUDIT=1)');
32
+ else {
33
+ const r = bash(env, 'run_tools_audit_scan', [path.join(wt, '.epam', 'dynamic-tools'), env.LOG_DIR], { log });
34
+ const ok = r.status === 0;
35
+ const res = ok ? json(r.stdout, null) : null;
36
+ out.tools = { ok: ok && !!res, ...(res || { broken: [], duplicates: [] }) };
37
+ if (!out.tools.ok) log(` [ToolsAudit] scan failed — dynamic tools were NOT audited this phase: ${(r.stdout + r.stderr).trim().slice(-300)}`);
38
+ const audit = path.join(env.LOG_DIR, 'tools-coordinator-audit.jsonl');
39
+ const at = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
40
+ for (const b of out.tools.broken || []) {
41
+ log(` [ToolsAudit] broken tool ${b.tool}: ${b.reason} — the tools-coordinator's seam is declared nowhere (backlog B41) — not run`);
42
+ fs.appendFileSync(audit, `${JSON.stringify({ timestamp: at, phase: phase || 'unknown', tool: b.tool, reason: b.reason, event: 'broken_tool_rewrite' })}\n`);
43
+ }
44
+ for (const d of out.tools.duplicates || []) {
45
+ log(` [ToolsAudit] Duplicate tools detected: ${d.tool_a}.sh and ${d.tool_b}.sh solve overlapping problems — flagged for manual review (not auto-merged)`);
46
+ fs.appendFileSync(audit, `${JSON.stringify({ timestamp: at, phase: phase || 'unknown', tool_a: d.tool_a, tool_b: d.tool_b, event: 'duplicate_flagged' })}\n`);
47
+ }
48
+ log(`[orch] Step 12: Tools coordinator audit → ${out.tools.ok ? 'pass' : 'warn'}`);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ module.exports = { runPhaseAudits };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+ // RAO:4237–4311 — STEP 5, THE PHASE GATE, by the pipeline's own check-phase-gate.sh as the orchestrator runs it
3
+ // (SKIP_TESTS=true): review status, story completion, deliverables, cost variance. RETRY exits 1, ESCALATE exits 2; any
4
+ // other code goes on (the pipeline's `case` has no default). Returns the phase's exit: 1, 2, or 0.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ function phaseGate(env, phase, log) {
10
+ const pg = spawnSync('bash', [path.join(env.SCRIPT_DIR, 'check-phase-gate.sh'), phase], { encoding: 'utf8', env: { ...env, SKIP_TESTS: 'true' }, maxBuffer: 64 * 1024 * 1024 });
11
+ fs.mkdirSync(env.LOG_DIR, { recursive: true });
12
+ fs.writeFileSync(path.join(env.LOG_DIR, `phase-gate-${phase}.log`), `${pg.stdout || ''}${pg.stderr || ''}`);
13
+ log(`[orch] Step 5: phase gate for '${phase}' → ${pg.status}`);
14
+ if (pg.status === 1) { log('[orch] Phase gate: RETRY — pipeline blocked'); return 1; }
15
+ if (pg.status === 2) { log('[orch] Phase gate: ESCALATE'); return 2; }
16
+ return 0;
17
+ }
18
+
19
+ module.exports = { phaseGate };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+ // THE PIPELINE'S OWN CODE, CALLED UNCHANGED FROM THE DRIVER: its bash functions (driver/bash.sh, in the run's
3
+ // environment) and its node handlers (orchestrations/scripts/lib/handlers/*.js). No AI.
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ function writeEnv(dir, env) {
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ const f = path.join(dir, `env-${process.pid}-${Math.floor(Math.random() * 1e9)}.json`);
12
+ fs.writeFileSync(f, JSON.stringify(env));
13
+ return f;
14
+ }
15
+
16
+ /** One bash function; returns {status, stdout, stderr, env (after the call, when asked)}. */
17
+ function bash(env, fn, args = [], opts = {}) {
18
+ const tmp = env.TMPDIR || os.tmpdir();
19
+ const envFile = writeEnv(tmp, env);
20
+ const envOut = opts.exports ? path.join(tmp, `env-out-${process.pid}-${Date.now()}.json`) : '';
21
+ const r = spawnSync('bash', [path.join(__dirname, 'bash.sh'), envFile, fn, ...args.map(String)],
22
+ { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, ...(envOut ? { BASH_CALL_ENV_OUT: envOut } : {}) },
23
+ maxBuffer: 256 * 1024 * 1024, cwd: opts.cwd || env.PROJECT_ROOT || tmp });
24
+ fs.rmSync(envFile, { force: true });
25
+ let after = null;
26
+ if (envOut && fs.existsSync(envOut)) { after = JSON.parse(fs.readFileSync(envOut, 'utf8')); fs.rmSync(envOut, { force: true }); }
27
+ if (opts.log) opts.log(`[pipeline] ${fn} ${args.join(' ')} → ${r.status}${r.stderr ? `\n${r.stderr.trim()}` : ''}`);
28
+ return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '', env: after };
29
+ }
30
+
31
+ /** One node handler (lib/handlers/<name>.js). */
32
+ function handler(env, name, args = [], opts = {}) {
33
+ const r = spawnSync(env.NODE_BIN || process.execPath, [path.join(env.SCRIPT_DIR, 'lib', 'handlers', `${name}.js`), ...args.map(String)],
34
+ { encoding: 'utf8', env, maxBuffer: 256 * 1024 * 1024, cwd: opts.cwd || env.PROJECT_ROOT || env.TMPDIR });
35
+ if (opts.log) opts.log(`[pipeline] ${name} ${args.join(' ')} → ${r.status}${r.stderr ? `\n${r.stderr.trim()}` : ''}`);
36
+ return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
37
+ }
38
+
39
+ /** The env lines a function printed (KEY=VALUE), applied over env without overwriting a key already set (apply_run_mode). */
40
+ function applyAssignments(env, text) {
41
+ const out = { ...env };
42
+ for (const line of String(text).split('\n')) {
43
+ const i = line.indexOf('='); if (i <= 0) continue;
44
+ const k = line.slice(0, i); if (!(k in out) || out[k] === '') out[k] = line.slice(i + 1);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ module.exports = { bash, handler, applyAssignments };