amsd-pipeline-shared 0.5.44 → 0.5.46

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.
@@ -44,13 +44,32 @@ function renderPrompt(vars, profile) {
44
44
  }
45
45
 
46
46
  /** The command, prepared and not started: the CLI in print mode on the profile's family, in the work tree, memory-bounded when the host has the wrapper. */
47
+ /** THE RUNNER'S OWN ISOLATION FOR THE DOCTOR'S CALL (2026-10-25: the doctor's session loaded the host's MCP servers and skills — it is an
48
+ * agent like every other and gets the same declaration). The claude runner's alwaysFlags and env from the stack's declaration the
49
+ * profile names (config/llm-defaults.<set>.json runners.claude), read as the hub's resolver reads them; none when the file has none. */
50
+ function runnerIsolation(install, profile) {
51
+ try {
52
+ const file = String(profile.model.config || '').replace('{install}', install);
53
+ const settings = JSON.parse(fs.readFileSync(file, 'utf8'));
54
+ const r = settings.runners || {};
55
+ const d = r.claude || Object.values(r)[0] || {};
56
+ const flags = Array.isArray(d.alwaysFlags) ? d.alwaysFlags.filter((f) => f !== '--safe-mode') : [];
57
+ const env = {};
58
+ for (const [k, name] of Object.entries(d.env || {})) { const v = settings[name]; if (v !== undefined && v !== null && String(v) !== '') env[k] = String(v); }
59
+ return { flags, env };
60
+ } catch { return { flags: [], env: {} }; }
61
+ }
62
+
47
63
  function buildCommand({ install, profile, prompt, cwd, claudeBin, budgetUsd, model: rung }) {
48
64
  // a rung the failed-call step moved to is taken only within the profile's family; otherwise the family's own model
49
65
  const model = rung && rung.toLowerCase().includes(profile.model.family.toLowerCase()) ? rung : modelFor(install, profile);
66
+ const iso = runnerIsolation(install, profile);
67
+ // the declaration's switches (--setting-sources "", --strict-mcp-config, …) replace the former `--setting-sources project`; the
68
+ // doctor edits files, so its permission mode stays bypass
50
69
  const argv = [...hostWrap(), claudeBin || process.env.CLAUDE_BIN || 'claude', '-p', prompt, '--model', model, '--output-format', 'json',
51
- '--setting-sources', 'project', '--permission-mode', 'bypassPermissions',
70
+ ...(iso.flags.length ? iso.flags : ['--setting-sources', 'project']), '--permission-mode', 'bypassPermissions',
52
71
  ...(budgetUsd ? ['--max-budget-usd', String(budgetUsd)] : [])];
53
- return { cwd, argv, model };
72
+ return { cwd, argv, model, env: iso.env };
54
73
  }
55
74
 
56
75
  /** The answer's shape: repaired / cannot with the fields — anything else is no answer. */
@@ -80,7 +99,7 @@ function readAnswer(text) {
80
99
  /** Runs the session (the only model call the doctor makes). Returns the answer, the session's cost record and any model of another family it used. */
81
100
  function runAgent({ install, profile, prompt, cwd, sessionDir, env, claudeBin, budgetUsd, model }) {
82
101
  const cmd = buildCommand({ install, profile, prompt, cwd, claudeBin, budgetUsd, model });
83
- const r = spawnSync(cmd.argv[0], cmd.argv.slice(1), { cwd: cmd.cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: '0', ...process.env, ...(env || {}) }, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
102
+ const r = spawnSync(cmd.argv[0], cmd.argv.slice(1), { cwd: cmd.cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: '0', ...process.env, ...cmd.env, ...(env || {}) }, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
84
103
  try { fs.mkdirSync(sessionDir, { recursive: true }); fs.writeFileSync(path.join(sessionDir, 'agent.stderr'), String(r.stderr || '')); fs.writeFileSync(path.join(sessionDir, 'agent.stdout'), String(r.stdout || '')); } catch { /* evidence only */ }
85
104
  let out = null; try { out = JSON.parse(r.stdout); } catch { out = null; }
86
105
  const session = out && out.session_id ? { sessionId: out.session_id, modelUsage: out.modelUsage || {}, totalCostUsd: out.total_cost_usd || 0 } : null;
@@ -9,8 +9,12 @@
9
9
  // agent is granted them by name (--tools mcp__<server>__<tool>), and a tool whose permission is 'safe' is annotated read-only,
10
10
  // which is what lets it run in plan mode (proven 2026-10-24 on Claude Code 2.1.310: without the annotation plan mode refuses it).
11
11
  //
12
+ // node pipeline-tools-mcp.js --codeline <dir> serve the plugins THAT CODELINE provisions (.epam/settings.json tools — the
13
+ // pipeline's own grant rule, lib/agent-tools.js pluginToolsFor), run inside it
12
14
  // node pipeline-tools-mcp.js <runner-name> serve the runner's hostedToolPlugins (declaration read from the active set)
13
15
  // node pipeline-tools-mcp.js --modules a.js,b.js serve these plugin modules (tests)
16
+ // ONE SERVER FOR BOTH RUNTIMES (2026-10-25): the Claude workflows had their own (claude-workflows/mcp/pipeline-tools.js, per
17
+ // codeline) and the hub got this one (per runner) — duplicated code; the workflows now start this file in --codeline mode.
14
18
  //
15
19
  // Protocol: JSON-RPC 2.0, one message per line, MCP 2024-11-05 — initialize, tools/list, tools/call; nothing else.
16
20
  const fs = require('fs');
@@ -27,6 +31,13 @@ function hostedModulesFor(runnerName) {
27
31
  return ((runner && runner.hostedToolPlugins) || []).map((m) => path.resolve(ORCH, m));
28
32
  }
29
33
 
34
+ /** The plugin modules a codeline provisions (.epam/settings.json tools), as the pipeline's grant rule reads them. */
35
+ function codelineModulesOf(codeline) {
36
+ let settings;
37
+ try { settings = JSON.parse(fs.readFileSync(path.join(codeline, '.epam', 'settings.json'), 'utf8')); } catch { return []; }
38
+ return (Array.isArray(settings.tools) ? settings.tools : []).filter((e) => typeof e === 'string' && e);
39
+ }
40
+
30
41
  /** Every tool the modules export, as MCP lists them: safe → read-only; the plugin's own input schema and description. */
31
42
  function toolsOf(modules) {
32
43
  const out = [];
@@ -35,7 +46,7 @@ function toolsOf(modules) {
35
46
  const mod = require(m);
36
47
  for (const t of ((mod && mod.tools) || [])) {
37
48
  const name = t.name || (t.definition && t.definition.name);
38
- if (!name) continue;
49
+ if (!name || typeof t.execute !== 'function') continue;
39
50
  const def = t.definition || {};
40
51
  out.push({
41
52
  name,
@@ -94,9 +105,12 @@ function mcpConfig(runnerName, nodeBin = process.execPath) {
94
105
 
95
106
  if (require.main === module) {
96
107
  const a = process.argv.slice(2);
97
- const modules = a[0] === '--modules' ? String(a[1] || '').split(',').filter(Boolean).map((m) => path.resolve(m)) : hostedModulesFor(a[0]);
98
- if (!modules.length) { process.stderr.write(`[pipeline-tools-mcp] no hosted tool plugins for runner '${a[0] || ''}'\n`); process.exit(2); }
108
+ let modules;
109
+ if (a[0] === '--modules') modules = String(a[1] || '').split(',').filter(Boolean).map((m) => path.resolve(m));
110
+ else if (a[0] === '--codeline') { const cl = path.resolve(a[1] || process.cwd()); process.chdir(cl); modules = codelineModulesOf(cl); } // plugins work on the folder they run in
111
+ else modules = hostedModulesFor(a[0]);
112
+ if (!modules.length) { process.stderr.write(`[pipeline-tools-mcp] no tool plugins for ${a[0] === '--codeline' ? `codeline ${a[1]}` : `runner '${a[0] || ''}'`}\n`); process.exit(2); }
99
113
  serve(modules);
100
114
  }
101
115
 
102
- module.exports = { hostedModulesFor, toolsOf, serve, mcpConfig, asMcpResult };
116
+ module.exports = { hostedModulesFor, codelineModulesOf, toolsOf, serve, mcpConfig, asMcpResult };
@@ -4175,6 +4175,16 @@ PROPOSE TWO CLASSES OF AGENT.
4175
4175
  IMPLEMENTERS (kind: "implementer") author code, as described above. One roster of them spans
4176
4176
  every codeline.
4177
4177
 
4178
+ THE CODELINE'S STANDING ENGINEER, NOT A SPECIALITY. Propose ONE implementer per codeline that
4179
+ needs work written, named for that codeline (its engineer) and briefed on its WHOLE stack: its
4180
+ layout, build, conventions, tests and how to verify there. Any story on that codeline — a
4181
+ component change, a page, an integration, a localisation fix — must find this role fit to
4182
+ assign. Where codelines share one stack and structure, one engineer may cover them all, named
4183
+ for what they share. Propose a speciality (a narrower role) only where a codeline's structure
4184
+ genuinely splits it — a separate build, a separate deployable, a separately-owned subtree — and
4185
+ never for the kind of work one ticket happens to need: this roster outlives every ticket, and a
4186
+ roster of specialities leaves the next story with no one to assign.
4187
+
4178
4188
  Every proposal states a "codeline". An implementer spans the project and uses "*". An
4179
4189
  investigator names the ONE codeline it reads, exactly as spelled in scope above. There is no
4180
4190
  third option and no omitting it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
4
4
  "description": "The AMSD pipeline's shared components, one source for the bash pipeline and its Claude workflows: the model-call gateway (llm-handler.sh — retry, ladder, self-heal, cost) and the libraries it runs.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {