amsd-pipeline-shared 0.5.43 → 0.5.44

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.
@@ -1,3 +1,6 @@
1
+ 5349c655265a56795c62a0327ba4e73b config/llm-defaults.claude.json
2
+ 5c2b348dbf4860aa6ad999eb7b882757 config/llm-defaults.mockserver.json
3
+ c90ae290edde83ae4e13236edb1e8fb5 config/llm-defaults.openrouter.json
1
4
  33364f057aa890497019737c7004db57 config/model-pricing.json
2
5
  8a1bb4df2d1b6e7fd135f757423fb2f2 config/repo-artifacts.json
3
6
  7ab104b57abcbc8cb3cd890c753d5d67 config/resume-preserves.json
@@ -2,6 +2,9 @@ config/call-policy.json
2
2
  config/doctor-heavy-jobs.json
3
3
  config/doctor-signatures.json
4
4
  config/finding-contracts.json
5
+ config/llm-defaults.claude.json
6
+ config/llm-defaults.mockserver.json
7
+ config/llm-defaults.openrouter.json
5
8
  config/model-pricing.json
6
9
  config/repo-artifacts.json
7
10
  config/resume-preserves.json
@@ -62,6 +65,8 @@ prompts/templates/phase-assessment-header.json
62
65
  prompts/templates/pipeline-tools-command.json
63
66
  prompts/templates/pipeline-tools-hosted.json
64
67
  prompts/templates/plan-corrective.json
68
+ prompts/templates/plan-execute.json
69
+ prompts/templates/plan-pass.json
65
70
  prompts/templates/plan-producer.json
66
71
  prompts/templates/plan-reviewer.json
67
72
  prompts/templates/post-failure-analyst.json
@@ -236,6 +241,7 @@ scripts/lib/pipeline-commit.sh
236
241
  scripts/lib/pipeline-tools-mcp.js
237
242
  scripts/lib/pipeline-tools.js
238
243
  scripts/lib/plan-hash.js
244
+ scripts/lib/plan-mode.js
239
245
  scripts/lib/plan-of-record.sh
240
246
  scripts/lib/plan-scope.js
241
247
  scripts/lib/prd-change-review.sh
@@ -0,0 +1,236 @@
1
+ {
2
+ "$comment": "THE CLAUDE PROVIDER SET — plain Claude Code on the operator's own tokens. The default stack.",
3
+ "$proven": "2026-08-25: plain `claude` honours ANTHROPIC_BASE_URL, reaches MockServer (POST /v1/messages in its request log) and PARSES the SSE it serves — is_error:false, result:'OK', stop_reason:'end_turn', RC=0.",
4
+ "$whyNotTheWrapper": "Plain `claude`, never codemie-claude. The wrapper authenticates through CodeMie SSO and spends CodeMie tokens; this set exists precisely because those are scarce and the operator has their own.",
5
+ "$whyTheSameLadders": "Deliberately identical to the codemie set. A rehearsal on different models proves the harness, not the run. Sharing the ladder means a mocked run climbs the chain a paid run would.",
6
+ "ladderTierOrder": [
7
+ "medium",
8
+ "high",
9
+ "highest"
10
+ ],
11
+ "ladders": {
12
+ "medium": {
13
+ "$why": "Enters cheap and tops out at Sonnet-5. A `mid` seam never reaches Opus: 14 seams resolve here, and measured turn counts are p50 4, p90 24 — work that does not need a frontier model.",
14
+ "startModel": "claude-haiku-4-5-20251001",
15
+ "modelLadder": [
16
+ {
17
+ "from": "claude-haiku-4-5-20251001",
18
+ "to": "claude-sonnet-5"
19
+ }
20
+ ],
21
+ "rungs": [
22
+ {
23
+ "rung": 0,
24
+ "maxIterations": 40,
25
+ "reasoningEffort": "medium"
26
+ },
27
+ {
28
+ "rung": 1,
29
+ "maxIterations": 120,
30
+ "reasoningEffort": "high"
31
+ }
32
+ ]
33
+ },
34
+ "high": {
35
+ "$why": "Same ENTRY as medium, higher CEILING. That is the 'different entry, same ceiling' rule applied between high and highest: what separates the tiers is how far they may climb, not where they start.",
36
+ "startModel": "claude-haiku-4-5-20251001",
37
+ "modelLadder": [
38
+ {
39
+ "from": "claude-haiku-4-5-20251001",
40
+ "to": "claude-sonnet-5"
41
+ },
42
+ {
43
+ "from": "claude-sonnet-5",
44
+ "to": "claude-opus-5-5"
45
+ }
46
+ ],
47
+ "rungs": [
48
+ {
49
+ "rung": 0,
50
+ "maxIterations": 40,
51
+ "reasoningEffort": "medium"
52
+ },
53
+ {
54
+ "rung": 1,
55
+ "maxIterations": 250,
56
+ "reasoningEffort": "high"
57
+ },
58
+ {
59
+ "rung": 2,
60
+ "maxIterations": 300,
61
+ "reasoningEffort": "high"
62
+ }
63
+ ]
64
+ },
65
+ "highest": {
66
+ "$why": "Skips Haiku. 25 seams resolve here and would waste a rung proving a small model cannot do the work. Same ceiling as high, entered two rungs up.",
67
+ "startModel": "claude-sonnet-5",
68
+ "modelLadder": [
69
+ {
70
+ "from": "claude-sonnet-5",
71
+ "to": "claude-opus-5-5"
72
+ }
73
+ ],
74
+ "rungs": [
75
+ {
76
+ "rung": 0,
77
+ "maxIterations": 250,
78
+ "reasoningEffort": "high"
79
+ },
80
+ {
81
+ "rung": 1,
82
+ "maxIterations": 250,
83
+ "reasoningEffort": "high"
84
+ },
85
+ {
86
+ "rung": 2,
87
+ "maxIterations": 300,
88
+ "reasoningEffort": "xhigh"
89
+ }
90
+ ]
91
+ }
92
+ },
93
+ "modelOverrides": {
94
+ "$whyAutoCompress": "150000 against a 200000 window is an OVERFLOW GUARD near the ceiling, never a token-saving device: compaction rewrites the prefix and destroys the prompt cache. Measured input is p50 13.3k, p90 19k, max 25.9k — about 13% of the window — so this should never fire. If it does, something has gone wrong upstream and that is worth knowing.",
95
+ "$whyNoTemperature": "codemie-claude exposes no temperature flag and Claude Code has no supported lever for it. Declaring one would be decoration. Deliberation is controlled by reasoningEffort and thinking instead.",
96
+ "$whyAdaptive": "Thinking is held STABLE per model. Anthropic's docs list effort and thinking-parameter changes as cache invalidators, so a config that changes between attempts discards the cache every retry — the opposite of what a 1h TTL is for.",
97
+ "claude-haiku-4-5": {
98
+ "matchOn": "model",
99
+ "matchSubstring": "claude-haiku-4-5",
100
+ "thinking": "adaptive",
101
+ "promptCacheTtl": "1h",
102
+ "autoCompressAt": 150000,
103
+ "maxOutputTokens": 32000,
104
+ "miniTier": true,
105
+ "charsPerToken": 4
106
+ },
107
+ "claude-sonnet-5": {
108
+ "matchOn": "model",
109
+ "matchSubstring": "claude-sonnet-5",
110
+ "thinking": "adaptive",
111
+ "promptCacheTtl": "1h",
112
+ "autoCompressAt": 150000,
113
+ "maxOutputTokens": 64000,
114
+ "charsPerToken": 4
115
+ },
116
+ "claude-opus-5-5": {
117
+ "matchOn": "model",
118
+ "matchSubstring": "claude-opus-5-5",
119
+ "thinking": "adaptive",
120
+ "promptCacheTtl": "1h",
121
+ "autoCompressAt": 150000,
122
+ "maxOutputTokens": 64000,
123
+ "charsPerToken": 4,
124
+ "$why": "Explicit, and BEFORE claude-opus-5: matching is by first substring hit, and 'claude-opus-5' is a substring of 'claude-opus-5-5' — without this entry Opus 5.5 silently took Opus 5's settings. Values as Opus 5; not vendor-checked."
125
+ },
126
+ "claude-opus-4-8": {
127
+ "matchOn": "model",
128
+ "matchSubstring": "claude-opus-4-8",
129
+ "thinking": "adaptive",
130
+ "promptCacheTtl": "1h",
131
+ "autoCompressAt": 150000,
132
+ "maxOutputTokens": 64000,
133
+ "charsPerToken": 4
134
+ },
135
+ "claude-opus-5": {
136
+ "matchOn": "model",
137
+ "matchSubstring": "claude-opus-5",
138
+ "thinking": "adaptive",
139
+ "promptCacheTtl": "1h",
140
+ "autoCompressAt": 150000,
141
+ "maxOutputTokens": 64000,
142
+ "charsPerToken": 4
143
+ },
144
+ "$miniTierWhy": "miniTier marks the cheap, fast rung — the models that are quick but limited in generation capacity. It was decided in engine code by sniffing vendor naming habits (-mini, -flash, -haiku, minimax-m2), which breaks in both directions: a cheap model named differently read as capable, and a capable model containing -flash read as cheap. The set that declares the model declares its tier.",
145
+ "$whyCharsPerToken": "INPUT CAPACITY IN CHARACTERS. autoCompressAt is the models input ceiling in TOKENS; callers that must size a payload measure it in characters. The ratio belongs with the model, because it is a property of that models tokenizer, not of any caller. Declared so a caller derives its budget instead of inventing one: the roster review carried a literal 60000 in code, which split a 276KB roster into six calls and failed the whole review whenever any one of them answered off-schema."
146
+ },
147
+ "finalFallback": {
148
+ "$why": "Applies after ladder exhaustion — the one point where the ladder has nothing left to say. Sonnet-5 differs from the top rung on high and highest, so it is a real second answer rather than a retry of the model that just failed.",
149
+ "model": "claude-sonnet-5",
150
+ "provider": "claude"
151
+ },
152
+ "runners": {
153
+ "claude": {
154
+ "siChannel": {
155
+ "flag": "--append-system-prompt-file"
156
+ },
157
+ "$whySiChannel": "Claude Code's own --append-system-prompt-file (claude --help, 2.1.282): the agent's SI as a system prompt, separate from the task on stdin.",
158
+ "$why": "Plain Claude Code. No CodeMie, no SSO, no spend.",
159
+ "alwaysFlags": [
160
+ "--setting-sources",
161
+ "",
162
+ "--strict-mcp-config",
163
+ "--disable-slash-commands",
164
+ "--settings",
165
+ "{\"includeGitInstructions\":false}"
166
+ ],
167
+ "$whyAlwaysFlags": "The agent receives the pipeline's SI and task, nothing of the host — through Claude Code's own interface, each piece by its own switch (proven on 2.1.310, 2026-10-24, with a CLAUDE.md canary in the working folder): --setting-sources \"\" loads no user, project or local settings (so none of the host's hooks, permissions or CLAUDE.md); CLAUDE_CODE_DISABLE_CLAUDE_MDS and CLAUDE_CODE_DISABLE_AUTO_MEMORY (env below) keep the working folder's CLAUDE.md and the host's auto-memory out; --disable-slash-commands drops the skills; includeGitInstructions:false drops the git status; --strict-mcp-config makes the pipeline's MCP server (hostedToolPlugins, lib/pipeline-tools-mcp.js) the ONLY server. --safe-mode did all of this in one switch and ALSO dropped every MCP server, --mcp-config included (proven: the tool was absent), so the pipeline's tools could not be the agent's tools under it. Sign-in survives (the subscription, not an API key); --bare would not.",
168
+ "env": {
169
+ "CLAUDE_CODE_DISABLE_CLAUDE_MDS": "hostClaudeMdOff",
170
+ "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "hostAutoMemoryOff",
171
+ "CLAUDE_CODE_PROMPT_CACHE_TTL": "promptCacheTtl",
172
+ "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL": "promptCacheTtl"
173
+ },
174
+ "flags": {
175
+ "--effort": "reasoningEffort",
176
+ "--autocompact": "autoCompressAt"
177
+ },
178
+ "$whyFlags": "Checked against the installed CLI's own --help, not assumed: --effort takes (low, medium, high, xhigh, max) and --autocompact takes auto or a token count. These were declared as CLAUDE_CODE_* env vars that the CLI does not document and that nothing exported.",
179
+ "$whyNoTurnCap": "maxIterations is deliberately absent. It binds the ReAct loop in src/agent/AgentRunner.ts, which only the `epam run` arm enters; a --print call has no turn cap and the loop is Claude Code's own. Declaring one here would name a knob nothing can turn.",
180
+ "$whyNoOutputCap": "maxOutputTokens is deliberately unmapped: the installed CLI documents no flag or env for it. It stays declared in modelOverrides so the intent is visible, and is not routed anywhere pretending to work.",
181
+ "$whyNoSilentFlag": "-s is codemie-claude's silent flag and plain `claude` REJECTS it: `echo x | claude -s --print` exits 1 with \"error: unknown option '-s'\". Proven by running it, 2026-08-26, after a --help probe proved nothing (--help and --version short-circuit before option validation, so an invalid flag exits 0 there too). With -s declared, the hub passed it to plain claude, the call failed, and the arm's 2>/dev/null hid the reason — codeline-discovery reported only 'Empty response from ai-run.sh' and the mock3 run aborted with 'codeline scope could not be resolved'. The codemie runner keeps -s: codemie-claude --help documents it as -s, --silent.",
182
+ "unsetEnv": [
183
+ "ANTHROPIC_API_KEY",
184
+ "ANTHROPIC_AUTH_TOKEN"
185
+ ],
186
+ "$whyUnsetEnv": "THE SUBSCRIPTION PAYS, AND THAT IS WHY THIS PIPELINE SHELLS OUT TO THE CLI INSTEAD OF THE SDK. Claude Code prefers ANTHROPIC_API_KEY over the OAuth credentials at ~/.claude/.credentials.json. The repo .env declares ANTHROPIC_API_KEY and run-agent-orchestration.sh loads .env, so every run on this stack billed API CREDITS — the exact outcome invoking the CLI was meant to avoid. It surfaced on 2026-08-26 only when the credits ran out mid-run: \"No JSON in LLM response: Credit balance is too low\". Seven runs had already billed the wrong account.\n\nDeclared per RUNNER rather than scrubbed globally: openrouter needs its key, and the free-run guard only scrubs when a run is declared free — a PAID run on this stack had no equivalent because nobody expected the key to be present at all.",
187
+ "emitsTraces": true,
188
+ "toolNames": {
189
+ "read_file": [
190
+ "Read"
191
+ ],
192
+ "list_files": [
193
+ "Glob"
194
+ ],
195
+ "search": [
196
+ "Grep"
197
+ ],
198
+ "fetch_url": [
199
+ "WebFetch"
200
+ ],
201
+ "bash": [
202
+ "Bash"
203
+ ],
204
+ "write_file": [
205
+ "Write",
206
+ "Edit"
207
+ ]
208
+ },
209
+ "$whyToolNames": "A SEAM'S TOOL GRANT IS BOUND AT THE RUNNER, LIKE ITS SCHEMA AND ITS BUDGET. Every seam declares a grant kind (none, read-only, execute, write-file...) and it travels as EPAM_ALLOWED_TOOLS; that variable was read in ONE place, src/agent/AgentRunner.ts on the `epam run` arm, so on this runner it bound NOTHING and every seam ran `--print --dangerously-skip-permissions` with the CLI's whole tool set. Run 20260916T225207Z (skyscanner): project-roster-review, declared read-only, ran `find / -iname prd.json` and read five earlier runs' cassettes out of the pipeline repo, then refused the roster for disagreeing with them. The 2026-09-12 roster specialiser, declared write-file, spent ~350 shell calls copying a repository. This map is the runner's own vocabulary for the pipeline's tool names (`claude --help`: --tools takes built-in tool names; \"\" disables all). llm-handler.sh translates the grant through it and passes --tools when the installed CLI advertises the flag. A pipeline tool with no entry here does not exist on this runner and is dropped from the list, said aloud on stderr. A seam with NO grant (kind none) is passed nothing and keeps the CLI's default set: --tools \"\" may also remove the StructuredOutput tool --json-schema requires, and that has not been proven either way, so it is not done blind.",
210
+ "$emitsTraces": "Whether model calls made through this runner reach Langfuse. TRUE for every runner: traces are emitted by lib/cost-emitter.js at the cost seam — the point every arm passes through with its model, tokens, cost and timing resolved — not by the TypeScript provider decorator, which only the epam arm reaches. Verified end-to-end 2026-09-04 by driving the real emitter and reading the session back out of Langfuse.",
211
+ "$whyEnv": "Each name is a top-level setting of this stack (hostClaudeMdOff, hostAutoMemoryOff, promptCacheTtl), resolved by lib/llm-settings-resolve.js runnerValues and exported for the CLI; an operator overrides one with EPAM_RUNNER_VALUE_<name>. The cache TTL: Claude Code's subagent and workflow calls default to a 5-minute prompt cache; the pipeline's calls repeat the same SI and context for an hour or more, so the documented 1h TTL is declared for both (docs: prompt-caching).",
212
+ "hostedToolPlugins": [
213
+ "plugins/codegraph-plugin.js",
214
+ "plugins/codeline-context-plugin.js",
215
+ "plugins/dependency-contract-plugin.js",
216
+ "plugins/dependency-scan-plugin.js",
217
+ "plugins/secret-scan-plugin.js",
218
+ "plugins/verification-plugin.js"
219
+ ],
220
+ "mcpServer": "pipeline",
221
+ "$whyHostedToolPlugins": "The pipeline's own tool plugins — the same modules the codemie-web runner hosts — served to this runner's agents as Claude Code tools over MCP (lib/pipeline-tools-mcp.js; the hub passes --mcp-config). Before this an agent on this runner reached codegraph, the search and the package-symbol resolver only as shell scripts through Bash: a read-only seam could not call them at all, and plan mode denies such Bash calls. A 'safe' plugin tool is annotated read-only, which is what lets plan mode run it.",
222
+ "planModeChannel": {
223
+ "modeFlag": "--permission-mode",
224
+ "planValue": "plan",
225
+ "sessionFlag": "--session-id",
226
+ "resumeFlag": "--resume"
227
+ },
228
+ "$whyPlanModeChannel": "How a plan pass is made on this runner (llm-handler.sh; lib/plan-mode.js decides WHETHER a seam plans): Claude Code's own plan mode. Proven on 2.1.310 (2026-10-24): `--permission-mode plan` under --print returns the plan as the result and writes nothing — the agent may read, search and call read-only tools, so the plan is grounded; the answer is then made in the same session with `--resume <session-id>` and the ordinary permissions. A runner that declares no channel gets the hub's prose pass (the plan written with no tools)."
229
+ }
230
+ },
231
+ "$why": "Built from the mockserver set by REMOVING the redirect, because that set was already a plain-`claude` stack pointed at localhost — same ladders, same budgets, same runner. Its header records the proof: plain `claude` honours ANTHROPIC_BASE_URL, reaches the endpoint it is given, and parses the SSE served back (is_error:false, RC=0). Taking the endpoint away is the whole difference between rehearsing and running.",
232
+ "hostClaudeMdOff": "1",
233
+ "hostAutoMemoryOff": "1",
234
+ "promptCacheTtl": "1h",
235
+ "$whyHostContextOff": "Values for the claude runner's env declaration (see runners.claude.$whyEnv): the host's CLAUDE.md and auto-memory stay out of every agent call; the prompt cache lives 1h."
236
+ }
@@ -0,0 +1,295 @@
1
+ {
2
+ "$comment": "THE MOCKSERVER PROVIDER SET — the CodeMie stack, answered by MockServer instead of a model. Same ladders and same budgets, so a rehearsal exercises the chain a paid run would climb; only the ENDPOINT differs.",
3
+ "$proven": "2026-08-25: plain `claude` honours ANTHROPIC_BASE_URL, reaches MockServer (POST /v1/messages in its request log) and PARSES the SSE it serves — is_error:false, result:'OK', stop_reason:'end_turn', RC=0.",
4
+ "$whyNotTheWrapper": "This runs PLAIN `claude`, not codemie-claude. The wrapper's --base-url selects an SSO PROFILE rather than redirecting, and fails with 'SSO credentials not found' for any URL it has no credentials for. A mocked run must authenticate against NOTHING, so it bypasses the wrapper entirely.",
5
+ "$whyTheSameLadders": "Deliberately identical to the codemie set. A rehearsal on different models proves the harness, not the run. Sharing the ladder means a mocked run climbs the chain a paid run would.",
6
+ "ladderTierOrder": [
7
+ "medium",
8
+ "high",
9
+ "highest"
10
+ ],
11
+ "ladders": {
12
+ "medium": {
13
+ "$why": "Enters cheap and tops out at Sonnet-5. A `mid` seam never reaches Opus: 14 seams resolve here, and measured turn counts are p50 4, p90 24 — work that does not need a frontier model.",
14
+ "startModel": "claude-haiku-4-5-20251001",
15
+ "modelLadder": [
16
+ {
17
+ "from": "claude-haiku-4-5-20251001",
18
+ "to": "claude-sonnet-5"
19
+ }
20
+ ],
21
+ "rungs": [
22
+ {
23
+ "rung": 0,
24
+ "maxIterations": 40,
25
+ "reasoningEffort": "medium"
26
+ },
27
+ {
28
+ "rung": 1,
29
+ "maxIterations": 120,
30
+ "reasoningEffort": "high"
31
+ }
32
+ ]
33
+ },
34
+ "high": {
35
+ "$why": "Same ENTRY as medium, higher CEILING. That is the 'different entry, same ceiling' rule applied between high and highest: what separates the tiers is how far they may climb, not where they start.",
36
+ "startModel": "claude-haiku-4-5-20251001",
37
+ "modelLadder": [
38
+ {
39
+ "from": "claude-haiku-4-5-20251001",
40
+ "to": "claude-sonnet-5"
41
+ },
42
+ {
43
+ "from": "claude-sonnet-5",
44
+ "to": "claude-opus-5"
45
+ }
46
+ ],
47
+ "rungs": [
48
+ {
49
+ "rung": 0,
50
+ "maxIterations": 40,
51
+ "reasoningEffort": "medium"
52
+ },
53
+ {
54
+ "rung": 1,
55
+ "maxIterations": 250,
56
+ "reasoningEffort": "high"
57
+ },
58
+ {
59
+ "rung": 2,
60
+ "maxIterations": 300,
61
+ "reasoningEffort": "high"
62
+ }
63
+ ]
64
+ },
65
+ "highest": {
66
+ "$why": "Skips Haiku. 25 seams resolve here and would waste a rung proving a small model cannot do the work. Same ceiling as high, entered two rungs up.",
67
+ "startModel": "claude-sonnet-5",
68
+ "modelLadder": [
69
+ {
70
+ "from": "claude-sonnet-5",
71
+ "to": "claude-opus-4-8"
72
+ },
73
+ {
74
+ "from": "claude-opus-4-8",
75
+ "to": "claude-opus-5"
76
+ }
77
+ ],
78
+ "rungs": [
79
+ {
80
+ "rung": 0,
81
+ "maxIterations": 250,
82
+ "reasoningEffort": "high"
83
+ },
84
+ {
85
+ "rung": 1,
86
+ "maxIterations": 250,
87
+ "reasoningEffort": "high"
88
+ },
89
+ {
90
+ "rung": 2,
91
+ "maxIterations": 300,
92
+ "reasoningEffort": "xhigh"
93
+ }
94
+ ]
95
+ }
96
+ },
97
+ "modelOverrides": {
98
+ "$whyAutoCompress": "150000 against a 200000 window is an OVERFLOW GUARD near the ceiling, never a token-saving device: compaction rewrites the prefix and destroys the prompt cache. Measured input is p50 13.3k, p90 19k, max 25.9k — about 13% of the window — so this should never fire. If it does, something has gone wrong upstream and that is worth knowing.",
99
+ "$whyNoTemperature": "codemie-claude exposes no temperature flag and Claude Code has no supported lever for it. Declaring one would be decoration. Deliberation is controlled by reasoningEffort and thinking instead.",
100
+ "$whyAdaptive": "Thinking is held STABLE per model. Anthropic's docs list effort and thinking-parameter changes as cache invalidators, so a config that changes between attempts discards the cache every retry — the opposite of what a 1h TTL is for.",
101
+ "claude-haiku-4-5": {
102
+ "matchOn": "model",
103
+ "matchSubstring": "claude-haiku-4-5",
104
+ "thinking": "adaptive",
105
+ "promptCacheTtl": "1h",
106
+ "autoCompressAt": 150000,
107
+ "maxOutputTokens": 32000,
108
+ "miniTier": true,
109
+ "charsPerToken": 4
110
+ },
111
+ "claude-sonnet-5": {
112
+ "matchOn": "model",
113
+ "matchSubstring": "claude-sonnet-5",
114
+ "thinking": "adaptive",
115
+ "promptCacheTtl": "1h",
116
+ "autoCompressAt": 150000,
117
+ "maxOutputTokens": 64000,
118
+ "charsPerToken": 4
119
+ },
120
+ "claude-opus-4-8": {
121
+ "matchOn": "model",
122
+ "matchSubstring": "claude-opus-4-8",
123
+ "thinking": "adaptive",
124
+ "promptCacheTtl": "1h",
125
+ "autoCompressAt": 150000,
126
+ "maxOutputTokens": 64000,
127
+ "charsPerToken": 4
128
+ },
129
+ "claude-opus-5": {
130
+ "matchOn": "model",
131
+ "matchSubstring": "claude-opus-5",
132
+ "thinking": "adaptive",
133
+ "promptCacheTtl": "1h",
134
+ "autoCompressAt": 150000,
135
+ "maxOutputTokens": 64000,
136
+ "charsPerToken": 4
137
+ },
138
+ "$miniTierWhy": "miniTier marks the cheap, fast rung — the models that are quick but limited in generation capacity. It was decided in engine code by sniffing vendor naming habits (-mini, -flash, -haiku, minimax-m2), which breaks in both directions: a cheap model named differently read as capable, and a capable model containing -flash read as cheap. The set that declares the model declares its tier.",
139
+ "$whyCharsPerToken": "INPUT CAPACITY IN CHARACTERS. autoCompressAt is the models input ceiling in TOKENS; callers that must size a payload measure it in characters. The ratio belongs with the model, because it is a property of that models tokenizer, not of any caller. Declared so a caller derives its budget instead of inventing one: the roster review carried a literal 60000 in code, which split a 276KB roster into six calls and failed the whole review whenever any one of them answered off-schema."
140
+ },
141
+ "finalFallback": {
142
+ "$why": "Applies after ladder exhaustion. The PROVIDER is plain `claude`, not codemie-claude, matching every other call this set makes — see $whyNotTheWrapper. That is CORRECT, not a paid escape: this set never talks to real Anthropic. runners.claude below declares ANTHROPIC_BASE_URL redirecting to MockServer and unsetEnv scrubbing every other vendor's credentials, and that redirect applies to every call routed under this set's own provider:\"claude\", finalFallback included — it never has a real endpoint to reach. Mockserver is a mock-LLM, no-pay testing mechanism by construction, not merely by convention (change-log/SEAM-CONSISTENCY-ANALYSIS.md Section 6, corrected 2026-09-03: the prior wording here mischaracterized this as an unresolved paid-escape risk).",
143
+ "$whyNotYetLive": "As of this writing, nothing in the runtime ladder-exhaustion path (llm-handler.sh, claude.sh, brownfield-repro-test-writer.sh) reads finalFallback — exhaustion retries the same rung instead. agent-check.js's dry-run cost estimator is the only reader, and even there this is unreachable while any ladder tier has a startModel (all of them do). Declared for correctness and for whenever that wiring exists.",
144
+ "model": "claude-sonnet-5",
145
+ "provider": "claude"
146
+ },
147
+ "runners": {
148
+ "claude": {
149
+ "siChannel": {
150
+ "flag": "--append-system-prompt-file"
151
+ },
152
+ "$whySiChannel": "Claude Code's own --append-system-prompt-file (claude --help, 2.1.282): the agent's SI as a system prompt, separate from the task on stdin.",
153
+ "$why": "Plain Claude Code. No CodeMie, no SSO, no spend.",
154
+ "$whyNoBaseUrlFlag": "--base-url is ABSENT on purpose: on the wrapper it means 'use this SSO profile'. The redirect is ANTHROPIC_BASE_URL, which plain claude honours.",
155
+ "alwaysFlags": [
156
+ "--setting-sources",
157
+ "",
158
+ "--strict-mcp-config",
159
+ "--disable-slash-commands",
160
+ "--settings",
161
+ "{\"includeGitInstructions\":false}"
162
+ ],
163
+ "$whyAlwaysFlags": "The agent receives the pipeline's SI and task, nothing of the host — through Claude Code's own interface, each piece by its own switch (proven on 2.1.310, 2026-10-24, with a CLAUDE.md canary in the working folder): --setting-sources \"\" loads no user, project or local settings (so none of the host's hooks, permissions or CLAUDE.md); CLAUDE_CODE_DISABLE_CLAUDE_MDS and CLAUDE_CODE_DISABLE_AUTO_MEMORY (env below) keep the working folder's CLAUDE.md and the host's auto-memory out; --disable-slash-commands drops the skills; includeGitInstructions:false drops the git status; --strict-mcp-config makes the pipeline's MCP server (hostedToolPlugins, lib/pipeline-tools-mcp.js) the ONLY server. --safe-mode did all of this in one switch and ALSO dropped every MCP server, --mcp-config included (proven: the tool was absent), so the pipeline's tools could not be the agent's tools under it. Sign-in survives (the subscription, not an API key); --bare would not.",
164
+ "env": {
165
+ "ANTHROPIC_BASE_URL": "mockBaseUrl",
166
+ "CLAUDE_CODE_DISABLE_CLAUDE_MDS": "hostClaudeMdOff",
167
+ "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "hostAutoMemoryOff",
168
+ "CLAUDE_CODE_PROMPT_CACHE_TTL": "promptCacheTtl",
169
+ "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL": "promptCacheTtl"
170
+ },
171
+ "flags": {
172
+ "--effort": "reasoningEffort",
173
+ "--autocompact": "autoCompressAt"
174
+ },
175
+ "$whyFlags": "Checked against the installed CLI's own --help, not assumed: --effort takes (low, medium, high, xhigh, max) and --autocompact takes auto or a token count. These were declared as CLAUDE_CODE_* env vars that the CLI does not document and that nothing exported.",
176
+ "$whyNoTurnCap": "maxIterations is deliberately absent. It binds the ReAct loop in src/agent/AgentRunner.ts, which only the `epam run` arm enters; a --print call has no turn cap and the loop is Claude Code's own. Declaring one here would name a knob nothing can turn.",
177
+ "$whyNoOutputCap": "maxOutputTokens is deliberately unmapped: the installed CLI documents no flag or env for it. It stays declared in modelOverrides so the intent is visible, and is not routed anywhere pretending to work.",
178
+ "$whyEnvKept": "ANTHROPIC_BASE_URL is a real, load-bearing env var: it is the mock redirect. It was declared here and never exported, so a free run would have looked redirected and reached the paid endpoint.",
179
+ "$whyNoSilentFlag": "-s is codemie-claude's silent flag and plain `claude` REJECTS it: `echo x | claude -s --print` exits 1 with \"error: unknown option '-s'\". Proven by running it, 2026-08-26, after a --help probe proved nothing (--help and --version short-circuit before option validation, so an invalid flag exits 0 there too). With -s declared, the hub passed it to plain claude, the call failed, and the arm's 2>/dev/null hid the reason — codeline-discovery reported only 'Empty response from ai-run.sh' and the mock3 run aborted with 'codeline scope could not be resolved'. The codemie runner keeps -s: codemie-claude --help documents it as -s, --silent.",
180
+ "unsetEnv": [
181
+ "CODEMIE_API_KEY",
182
+ "CODEMIE_AUTH_TOKEN",
183
+ "CODEMIE_KEY",
184
+ "CODEMIE_TOKEN",
185
+ "COPILOT_API_KEY",
186
+ "COPILOT_AUTH_TOKEN",
187
+ "COPILOT_KEY",
188
+ "COPILOT_TOKEN",
189
+ "CURSOR_API_KEY",
190
+ "CURSOR_AUTH_TOKEN",
191
+ "CURSOR_KEY",
192
+ "CURSOR_TOKEN",
193
+ "DASHSCOPE_API_KEY",
194
+ "DASHSCOPE_AUTH_TOKEN",
195
+ "DASHSCOPE_KEY",
196
+ "DASHSCOPE_TOKEN",
197
+ "EPAM_API_KEY_ANTHROPIC",
198
+ "EPAM_API_KEY_CLAUDE",
199
+ "EPAM_API_KEY_CODEMIE",
200
+ "EPAM_API_KEY_COPILOT",
201
+ "EPAM_API_KEY_CURSOR",
202
+ "EPAM_API_KEY_DASHSCOPE",
203
+ "EPAM_API_KEY_GEMINI",
204
+ "EPAM_API_KEY_MINIMAX",
205
+ "EPAM_API_KEY_OPENAI",
206
+ "EPAM_API_KEY_OPENROUTER",
207
+ "GEMINI_API_KEY",
208
+ "GEMINI_AUTH_TOKEN",
209
+ "GEMINI_KEY",
210
+ "GEMINI_TOKEN",
211
+ "MINIMAX_API_KEY",
212
+ "MINIMAX_AUTH_TOKEN",
213
+ "MINIMAX_KEY",
214
+ "MINIMAX_TOKEN",
215
+ "OPENAI_API_KEY",
216
+ "OPENAI_AUTH_TOKEN",
217
+ "OPENAI_KEY",
218
+ "OPENAI_TOKEN",
219
+ "OPENROUTER_API_KEY",
220
+ "OPENROUTER_AUTH_TOKEN",
221
+ "OPENROUTER_KEY",
222
+ "OPENROUTER_TOKEN"
223
+ ],
224
+ "$whyUnsetEnv": "A REHEARSAL MUST NOT BE ABLE TO SPEND SOMEWHERE ELSE. Under this set the writer once ran provider=minimax with MINIMAX_API_KEY present: the mockBaseUrl redirect covers Anthropic traffic and nothing else, so any other vendor's credential is a live route out. Those are removed. ANTHROPIC_API_KEY and CLAUDE_* are deliberately NOT removed: Claude Code falls back to the OAuth credentials on disk when it finds no key, and that bills for real — the exact inversion this is meant to prevent, recorded in seam-the-subscription-pays-not-the-api-key. The operator's fake key stays, and the redirect is what makes it harmless. The vendors come from llm-channel.json's credentialPattern, so one added there is covered without editing this.",
225
+ "emitsTraces": true,
226
+ "$emitsTraces": "Whether model calls made through this runner reach Langfuse. TRUE for every runner: traces are emitted by lib/cost-emitter.js at the cost seam — the point every arm passes through with its model, tokens, cost and timing resolved — not by the TypeScript provider decorator, which only the epam arm reaches. Verified end-to-end 2026-09-04 by driving the real emitter and reading the session back out of Langfuse.",
227
+ "toolNames": {
228
+ "read_file": [
229
+ "Read"
230
+ ],
231
+ "list_files": [
232
+ "Glob"
233
+ ],
234
+ "search": [
235
+ "Grep"
236
+ ],
237
+ "fetch_url": [
238
+ "WebFetch"
239
+ ],
240
+ "bash": [
241
+ "Bash"
242
+ ],
243
+ "write_file": [
244
+ "Write",
245
+ "Edit"
246
+ ]
247
+ },
248
+ "$whyToolNames": "A SEAM'S TOOL GRANT IS BOUND AT THE RUNNER, LIKE ITS SCHEMA AND ITS BUDGET. Every seam declares a grant kind (none, read-only, execute, write-file...) and it travels as EPAM_ALLOWED_TOOLS; that variable was read in ONE place, src/agent/AgentRunner.ts on the `epam run` arm, so on this runner it bound NOTHING and every seam ran `--print --dangerously-skip-permissions` with the CLI's whole tool set. Run 20260916T225207Z (skyscanner): project-roster-review, declared read-only, ran `find / -iname prd.json` and read five earlier runs' cassettes out of the pipeline repo, then refused the roster for disagreeing with them. The 2026-09-12 roster specialiser, declared write-file, spent ~350 shell calls copying a repository. This map is the runner's own vocabulary for the pipeline's tool names (`claude --help`: --tools takes built-in tool names; \"\" disables all). llm-handler.sh translates the grant through it and passes --tools when the installed CLI advertises the flag. A pipeline tool with no entry here does not exist on this runner and is dropped from the list, said aloud on stderr. A seam with NO grant (kind none) is passed nothing and keeps the CLI's default set: --tools \"\" may also remove the StructuredOutput tool --json-schema requires, and that has not been proven either way, so it is not done blind.",
249
+ "writeTool": {
250
+ "name": "Write",
251
+ "path": "file_path",
252
+ "content": "content"
253
+ },
254
+ "$whyWriteTool": "The rehearsal's stand-in writer delivers by calling the runner's own file-writing tool, and this is that tool's vocabulary for plain Claude Code (recorded sessions: 'Write' {file_path, content}). Declared here so the harness never spells a tool name.",
255
+ "readTool": {
256
+ "name": "Read",
257
+ "path": "file_path"
258
+ },
259
+ "$whyReadTool": "Plain Claude Code's Write refuses a file the session has not read ('File has not been read yet. Read it first before writing to it.', £0 brownfield harness run 10, 2026-09-14): a brownfield story's deliverables exist, so the stand-in writer reads each existing one through this tool before it writes, as a model does. Declared beside writeTool so the harness never spells this name either.",
260
+ "structuredOutputTool": "StructuredOutput",
261
+ "$whyStructuredOutputTool": "Under --json-schema plain Claude Code declares a tool of this name and REQUIRES the reply to call it ('[structured-output-enforce] You MUST call the StructuredOutput tool', recorded 2026-09-14); a text reply costs a second, enforcement turn per seam and never exercises the runner's structured-output path — the one a real model takes. The rehearsal answers a request that declares this tool by calling it with the seam's stand-in payload.",
262
+ "$whyEnv": "Each name is a top-level setting of this stack (hostClaudeMdOff, hostAutoMemoryOff, promptCacheTtl), resolved by lib/llm-settings-resolve.js runnerValues and exported for the CLI; an operator overrides one with EPAM_RUNNER_VALUE_<name>. The cache TTL: Claude Code's subagent and workflow calls default to a 5-minute prompt cache; the pipeline's calls repeat the same SI and context for an hour or more, so the documented 1h TTL is declared for both (docs: prompt-caching).",
263
+ "hostedToolPlugins": [
264
+ "plugins/codegraph-plugin.js",
265
+ "plugins/codeline-context-plugin.js",
266
+ "plugins/dependency-contract-plugin.js",
267
+ "plugins/dependency-scan-plugin.js",
268
+ "plugins/secret-scan-plugin.js",
269
+ "plugins/verification-plugin.js"
270
+ ],
271
+ "mcpServer": "pipeline",
272
+ "$whyHostedToolPlugins": "The pipeline's own tool plugins — the same modules the codemie-web runner hosts — served to this runner's agents as Claude Code tools over MCP (lib/pipeline-tools-mcp.js; the hub passes --mcp-config). Before this an agent on this runner reached codegraph, the search and the package-symbol resolver only as shell scripts through Bash: a read-only seam could not call them at all, and plan mode denies such Bash calls. A 'safe' plugin tool is annotated read-only, which is what lets plan mode run it.",
273
+ "planModeChannel": {
274
+ "modeFlag": "--permission-mode",
275
+ "planValue": "plan",
276
+ "sessionFlag": "--session-id",
277
+ "resumeFlag": "--resume"
278
+ },
279
+ "$whyPlanModeChannel": "How a plan pass is made on this runner (llm-handler.sh; lib/plan-mode.js decides WHETHER a seam plans): Claude Code's own plan mode. Proven on 2.1.310 (2026-10-24): `--permission-mode plan` under --print returns the plan as the result and writes nothing — the agent may read, search and call read-only tools, so the plan is grounded; the answer is then made in the same session with `--resume <session-id>` and the ordinary permissions. A runner that declares no channel gets the hub's prose pass (the plan written with no tools)."
280
+ }
281
+ },
282
+ "mockBaseUrl": "http://localhost:1080",
283
+ "$mockBaseUrl": "Where MockServer listens. Declared here so no script spells it — the same rule every other value in this layer follows.",
284
+ "rehearsal": true,
285
+ "$rehearsalWhy": "A run on this set spends nothing and answers from recordings and stand-ins. Every recorder stamps its traces rehearsal:true (lib/langfuse-emit.js), and the mock loader never replays a rehearsal's own traces as captures — on 2026-09-13 a stand-in reply recorded by one rehearsal was served to the next as a 'recorded' turn, and the roster stage failed on its own echo three launches running.",
286
+ "wire": {
287
+ "$comment": "HOW THE STAND-IN PUTS A TOOL CALL ON THE WIRE. A vendor streams a tool call as many `function.arguments` (or `input_json_delta`) fragments, and the body reaches the client in HTTP chunks that end wherever the socket ends them — inside an event as often as not. Until 2026-09-15 the stand-in served every call whole, in one read: a shape no vendor sends and the one shape the client's stream parser could never fail on. Run 20260915T101555Z lost 32 write_file calls' arguments to exactly that parser ($4.18, nothing delivered) while every harness cell was green. argumentDeltaChars is the fragment size; chunkBytes is the HTTP chunk size. Both are deliberately unaligned with any event length so boundaries fall inside events.",
288
+ "argumentDeltaChars": 97,
289
+ "chunkBytes": 333
290
+ },
291
+ "hostClaudeMdOff": "1",
292
+ "hostAutoMemoryOff": "1",
293
+ "promptCacheTtl": "1h",
294
+ "$whyHostContextOff": "Values for the claude runner's env declaration (see runners.claude.$whyEnv): the host's CLAUDE.md and auto-memory stay out of every agent call; the prompt cache lives 1h."
295
+ }
@@ -0,0 +1,279 @@
1
+ {
2
+ "$comment": "THE OPENROUTER/MINIMAX PROVIDER SET. Set-SPECIFIC declarations only — which models, which ladders, which runner. Set-INDEPENDENT budgets (effortTiers, roleOverrides, outputTokenFloors) stay in llm-defaults.json and are shared by every set, so swapping providers never changes how much budget a tier gets.",
3
+ "$provenance": "ladders, ladderTierOrder and modelOverrides were MOVED VERBATIM from projects/metrolinx/llm-settings.json on 2026-08-25 — generated from that file, not retyped. metrolinx is the live client codeline, so its chain is the one the openrouter stack keeps. mock3, hello-dolly and skyscanner previously topped out at glm-5.2/5.1 and now inherit glm-5.3: a deliberate convergence, recorded here rather than discovered later.",
4
+ "ladderTierOrder": [
5
+ "medium",
6
+ "high",
7
+ "highest"
8
+ ],
9
+ "ladders": {
10
+ "high": {
11
+ "modelLadder": [
12
+ {
13
+ "from": "MiniMax-M2.5",
14
+ "to": "MiniMax-M3"
15
+ },
16
+ {
17
+ "from": "MiniMax-M3",
18
+ "to": "z-ai/glm-5.2"
19
+ },
20
+ {
21
+ "from": "z-ai/glm-5.1",
22
+ "to": "z-ai/glm-5.2"
23
+ },
24
+ {
25
+ "from": "z-ai/glm-5.2",
26
+ "to": "moonshotai/kimi-k3"
27
+ },
28
+ {
29
+ "from": "moonshotai/kimi-k2.5",
30
+ "to": "moonshotai/kimi-k3"
31
+ }
32
+ ],
33
+ "startModel": "MiniMax-M3"
34
+ },
35
+ "medium": {
36
+ "modelLadder": [
37
+ {
38
+ "from": "MiniMax-M2.7-highspeed",
39
+ "to": "MiniMax-M3"
40
+ },
41
+ {
42
+ "from": "MiniMax-M2.7",
43
+ "to": "MiniMax-M3"
44
+ },
45
+ {
46
+ "from": "MiniMax-M2.5",
47
+ "to": "MiniMax-M3"
48
+ },
49
+ {
50
+ "from": "MiniMax-M3",
51
+ "to": "z-ai/glm-5.3"
52
+ },
53
+ {
54
+ "from": "z-ai/glm-5.1",
55
+ "to": "z-ai/glm-5.3"
56
+ },
57
+ {
58
+ "from": "z-ai/glm-5.2",
59
+ "to": "z-ai/glm-5.3"
60
+ }
61
+ ],
62
+ "startModel": "MiniMax-M2.7-highspeed",
63
+ "_why": "M2.5 is legacy. M2.7 scores materially higher on multi-file and long-horizon work (NL2Repo 26.6 -> 39.8, MLE Bench Lite +15pts) and the -highspeed variant is documented as producing IDENTICAL results, only faster — so it is a free latency win, not a quality trade. The chain ends at glm-5.3 rather than 5.2 because 5.3's gains concentrate in agent-shaped tasks, which is what a seam that has already escalated twice is struggling with."
64
+ },
65
+ "highest": {
66
+ "modelLadder": [
67
+ {
68
+ "from": "z-ai/glm-5.3",
69
+ "to": "moonshotai/kimi-k3"
70
+ },
71
+ {
72
+ "from": "z-ai/glm-5.2",
73
+ "to": "z-ai/glm-5.3"
74
+ },
75
+ {
76
+ "from": "z-ai/glm-5.1",
77
+ "to": "z-ai/glm-5.3"
78
+ },
79
+ {
80
+ "from": "MiniMax-M2.7-highspeed",
81
+ "to": "MiniMax-M3"
82
+ },
83
+ {
84
+ "from": "MiniMax-M2.7",
85
+ "to": "MiniMax-M3"
86
+ },
87
+ {
88
+ "from": "MiniMax-M2.5",
89
+ "to": "MiniMax-M3"
90
+ },
91
+ {
92
+ "from": "MiniMax-M3",
93
+ "to": "z-ai/glm-5.3"
94
+ },
95
+ {
96
+ "from": "moonshotai/kimi-k2.5",
97
+ "to": "moonshotai/kimi-k3"
98
+ }
99
+ ],
100
+ "startModel": "z-ai/glm-5.3",
101
+ "_why": "glm-5.3 shares 5.2's base model; every gain is post-training on executable environments and long-horizon tasks — Terminal-Bench 3.0 4.6 -> 28.3, DeepSWE 46.2 -> 66.9, SWE-Marathon 19.4 -> 42.5. It also reaches a higher score with ~22% FEWER output tokens on hard work, which offsets most of the 45% higher per-token price. All vendor-reported: no independent reproduction exists yet."
102
+ }
103
+ },
104
+ "modelOverrides": {
105
+ "minimax-m2.5": {
106
+ "matchOn": "model",
107
+ "matchSubstring": "MiniMax-M2.5",
108
+ "reasoningEffort": "medium",
109
+ "maxIterations": 45,
110
+ "autoCompressAt": 64000,
111
+ "autoCompressEveryNIterations": 15,
112
+ "topP": 0.85,
113
+ "temperature": 0.2,
114
+ "miniTier": true,
115
+ "charsPerToken": 4,
116
+ "maxOutputTokens": 128000,
117
+ "outputPricePerMillion": 1.08
118
+ },
119
+ "minimax-m3": {
120
+ "matchOn": "model",
121
+ "matchSubstring": "MiniMax-M3",
122
+ "reasoningEffort": "high",
123
+ "maxIterations": 120,
124
+ "autoCompressAt": 180000,
125
+ "topP": 0.85,
126
+ "temperature": 0.2,
127
+ "charsPerToken": 4,
128
+ "maxOutputTokens": 512000,
129
+ "outputPricePerMillion": 1.2
130
+ },
131
+ "glm-5.2": {
132
+ "matchOn": "model",
133
+ "matchSubstring": "glm-5.2",
134
+ "reasoningEffort": "high",
135
+ "maxIterations": 120,
136
+ "autoCompressAt": 180000,
137
+ "topP": 0.95,
138
+ "temperature": 0.9,
139
+ "providerOrder": [
140
+ "CoreWeave"
141
+ ],
142
+ "charsPerToken": 4,
143
+ "maxOutputTokens": 131072,
144
+ "outputPricePerMillion": 2.0416
145
+ },
146
+ "kimi-k2.5": {
147
+ "matchOn": "model",
148
+ "matchSubstring": "kimi-k2.5",
149
+ "temperature": 1,
150
+ "reasoningEffort": "medium",
151
+ "maxIterations": 60,
152
+ "autoCompressAt": 128000,
153
+ "autoCompressEveryNIterations": 20,
154
+ "topP": 1,
155
+ "temperatureLocked": true,
156
+ "charsPerToken": 4,
157
+ "maxOutputTokens": 235929,
158
+ "outputPricePerMillion": 2.25
159
+ },
160
+ "kimi-k3": {
161
+ "matchOn": "model",
162
+ "matchSubstring": "kimi-k3",
163
+ "temperature": 1,
164
+ "reasoningEffort": "max",
165
+ "maxIterations": 150,
166
+ "autoCompressAt": 400000,
167
+ "topP": 1,
168
+ "temperatureLocked": true,
169
+ "providerOrder": [
170
+ "Moonshot AI"
171
+ ],
172
+ "charsPerToken": 4,
173
+ "maxOutputTokens": 943718,
174
+ "outputPricePerMillion": 15.0
175
+ },
176
+ "minimax-m2.7": {
177
+ "matchOn": "model",
178
+ "matchSubstring": "MiniMax-M2.7",
179
+ "_why": "Replaces MiniMax-M2.5 as the medium tier's start. maxIterations is 90, not M2.5's 45: 45 was the outlier in this file (M3 120, glm 120, kimi-k3 150) and M2.7's advantage is precisely on the long-horizon work that needs room to converge — capping the stronger model at the weaker one's budget would throw the upgrade away. autoCompressAt is 150000 against a 204800 context: compaction destroys the cacheable prefix, so it must be an overflow guard near the ceiling, never a token-saving device. Substring is 'MiniMax-M2.7' so it matches the -highspeed variant too.",
180
+ "reasoningEffort": "medium",
181
+ "maxIterations": 90,
182
+ "autoCompressAt": 150000,
183
+ "autoCompressEveryNIterations": 15,
184
+ "topP": 0.85,
185
+ "temperature": 0.2,
186
+ "miniTier": true,
187
+ "charsPerToken": 4,
188
+ "maxOutputTokens": 131072,
189
+ "outputPricePerMillion": 1.2
190
+ },
191
+ "glm-5.3": {
192
+ "matchOn": "model",
193
+ "matchSubstring": "glm-5.3",
194
+ "_why": "Mirrors glm-5.2's sampling (same base model, so temperature/topP carry over) with more iteration room at 150, because this is now the highest tier's start and has only ONE rung above it. autoCompressAt raised to 300000 against a 1048576 context — 5.2's 180000 was conservative and every compaction destroys the cache. providerOrder is Z.AI because OpenRouter lists exactly one endpoint for this model; copying 5.2's CoreWeave pin would have failed every call.",
195
+ "reasoningEffort": "high",
196
+ "maxIterations": 150,
197
+ "autoCompressAt": 300000,
198
+ "topP": 0.95,
199
+ "temperature": 0.9,
200
+ "providerOrder": [
201
+ "Z.AI"
202
+ ],
203
+ "charsPerToken": 4,
204
+ "maxOutputTokens": 131072,
205
+ "outputPricePerMillion": 2.64
206
+ },
207
+ "$miniTierWhy": "miniTier marks the cheap, fast rung — the models that are quick but limited in generation capacity. It was decided in engine code by sniffing vendor naming habits (-mini, -flash, -haiku, minimax-m2), which breaks in both directions: a cheap model named differently read as capable, and a capable model containing -flash read as cheap. The set that declares the model declares its tier.",
208
+ "$whyCharsPerToken": "INPUT CAPACITY IN CHARACTERS. autoCompressAt is the models input ceiling in TOKENS; callers that must size a payload measure it in characters. The ratio belongs with the model, because it is a property of that models tokenizer, not of any caller. Declared so a caller derives its budget instead of inventing one: the roster review carried a literal 60000 in code, which split a 276KB roster into six calls and failed the whole review whenever any one of them answered off-schema.",
209
+ "glm-5.1": {
210
+ "matchOn": "model",
211
+ "matchSubstring": "glm-5.1",
212
+ "maxOutputTokens": 128000,
213
+ "_why": "created by refresh-model-limits.sh: a ladder rung with no override is capped by its tier, which is how truncation returns for one rung while the others are free.",
214
+ "outputPricePerMillion": 3.036
215
+ }
216
+ },
217
+ "runners": {
218
+ "claude": {
219
+ "siChannel": {
220
+ "flag": "--append-system-prompt-file"
221
+ },
222
+ "$whySiChannel": "Claude Code's own --append-system-prompt-file (claude --help, 2.1.282): the agent's SI as a system prompt, separate from the task on stdin.",
223
+ "$whyFlags": "The same two knobs every stack exposes, so a seam's declared effort and compaction reach the runner here exactly as they do on the others. Naming them per set rather than in engine code is what makes a swap a config edit.",
224
+ "alwaysFlags": [
225
+ "--setting-sources",
226
+ "",
227
+ "--strict-mcp-config",
228
+ "--disable-slash-commands",
229
+ "--settings",
230
+ "{\"includeGitInstructions\":false}"
231
+ ],
232
+ "$whyAlwaysFlags": "The agent receives the pipeline's SI and task, nothing of the host — through Claude Code's own interface, each piece by its own switch (proven on 2.1.310, 2026-10-24, with a CLAUDE.md canary in the working folder): --setting-sources \"\" loads no user, project or local settings (so none of the host's hooks, permissions or CLAUDE.md); CLAUDE_CODE_DISABLE_CLAUDE_MDS and CLAUDE_CODE_DISABLE_AUTO_MEMORY (env below) keep the working folder's CLAUDE.md and the host's auto-memory out; --disable-slash-commands drops the skills; includeGitInstructions:false drops the git status; --strict-mcp-config makes the pipeline's MCP server (hostedToolPlugins, lib/pipeline-tools-mcp.js) the ONLY server. --safe-mode did all of this in one switch and ALSO dropped every MCP server, --mcp-config included (proven: the tool was absent), so the pipeline's tools could not be the agent's tools under it. Sign-in survives (the subscription, not an API key); --bare would not.",
233
+ "env": {
234
+ "CLAUDE_CODE_DISABLE_CLAUDE_MDS": "hostClaudeMdOff",
235
+ "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "hostAutoMemoryOff",
236
+ "CLAUDE_CODE_PROMPT_CACHE_TTL": "promptCacheTtl",
237
+ "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL": "promptCacheTtl"
238
+ },
239
+ "flags": {
240
+ "--effort": "reasoningEffort",
241
+ "--autocompact": "autoCompressAt"
242
+ },
243
+ "$whyNoUnsetEnv": "Deliberately empty. Scrubbing ANTHROPIC_API_KEY sends Claude Code to the OAuth credentials on disk and bills for real — recorded in seam-the-subscription-pays-not-the-api-key. This stack authenticates with OPENROUTER_API_KEY and has no reason to take anything away.",
244
+ "unsetEnv": [],
245
+ "emitsTraces": true,
246
+ "$emitsTraces": "Whether model calls made through this runner reach Langfuse. TRUE for every runner: traces are emitted by lib/cost-emitter.js at the cost seam — the point every arm passes through with its model, tokens, cost and timing resolved — not by the TypeScript provider decorator, which only the epam arm reaches. Verified end-to-end 2026-09-04 by driving the real emitter and reading the session back out of Langfuse.",
247
+ "$whyEnv": "Each name is a top-level setting of this stack (hostClaudeMdOff, hostAutoMemoryOff, promptCacheTtl), resolved by lib/llm-settings-resolve.js runnerValues and exported for the CLI; an operator overrides one with EPAM_RUNNER_VALUE_<name>. The cache TTL: Claude Code's subagent and workflow calls default to a 5-minute prompt cache; the pipeline's calls repeat the same SI and context for an hour or more, so the documented 1h TTL is declared for both (docs: prompt-caching).",
248
+ "hostedToolPlugins": [
249
+ "plugins/codegraph-plugin.js",
250
+ "plugins/codeline-context-plugin.js",
251
+ "plugins/dependency-contract-plugin.js",
252
+ "plugins/dependency-scan-plugin.js",
253
+ "plugins/secret-scan-plugin.js",
254
+ "plugins/verification-plugin.js"
255
+ ],
256
+ "mcpServer": "pipeline",
257
+ "$whyHostedToolPlugins": "The pipeline's own tool plugins — the same modules the codemie-web runner hosts — served to this runner's agents as Claude Code tools over MCP (lib/pipeline-tools-mcp.js; the hub passes --mcp-config). Before this an agent on this runner reached codegraph, the search and the package-symbol resolver only as shell scripts through Bash: a read-only seam could not call them at all, and plan mode denies such Bash calls. A 'safe' plugin tool is annotated read-only, which is what lets plan mode run it.",
258
+ "planModeChannel": {
259
+ "modeFlag": "--permission-mode",
260
+ "planValue": "plan",
261
+ "sessionFlag": "--session-id",
262
+ "resumeFlag": "--resume"
263
+ },
264
+ "$whyPlanModeChannel": "How a plan pass is made on this runner (llm-handler.sh; lib/plan-mode.js decides WHETHER a seam plans): Claude Code's own plan mode. Proven on 2.1.310 (2026-10-24): `--permission-mode plan` under --print returns the plan as the result and writes nothing — the agent may read, search and call read-only tools, so the plan is grounded; the answer is then made in the same session with `--resume <session-id>` and the ordinary permissions. A runner that declares no channel gets the hub's prose pass (the plan written with no tools)."
265
+ }
266
+ },
267
+ "$whyRunners": "THE SWAP MUST NOT INHERIT THE PREVIOUS STACK'S RUNNER. apply_runner_settings is called with the BASENAME of CLAUDE_CMD, which is 'claude' under every set, so a set declaring no runner by that name resolves nothing and the flags, env and scrubs of whatever ran before are simply carried over. claude, codemie and mockserver each declared one; this set did not, which made hot swap 2 the one stack that silently kept another stack's settings.",
268
+ "finalFallback": {
269
+ "$why": "Applies after ladder exhaustion. The other 3 sets (claude, codemie, mockserver) all declare one; this set previously did not, an undocumented inconsistency (change-log/SEAM-CONSISTENCY-ANALYSIS.md Section 6). Routed WITHIN this set rather than escaping to paid claude: unlike mockserver (which is a no-pay mock endpoint by construction regardless of the provider string it declares — see its own finalFallback.$why), this is the live production openrouter/minimax stack, where a provider:\"claude\" fallback would be a real, unnoticed charge on a different vendor's account. MiniMax-M3 is chosen because it is a genuinely different model family from the top rung of every ladder here (high and highest both top out at moonshotai/kimi-k3, medium at z-ai/glm-5.3), so it is a real second answer rather than a retry of the model that just failed — mirroring the claude/codemie sets' own stated reasoning for choosing sonnet-5 over their own top rungs.",
270
+ "$whyNotYetLive": "As of this writing, nothing in the runtime ladder-exhaustion path (llm-handler.sh, claude.sh, brownfield-repro-test-writer.sh) reads finalFallback — exhaustion retries the same rung instead. agent-check.js's dry-run cost estimator is the only reader, and even there this is unreachable while any ladder tier has a startModel (all of them do). Declared for correctness and for whenever that wiring exists, not because a run can reach this today.",
271
+ "model": "MiniMax-M3",
272
+ "provider": "openrouter"
273
+ },
274
+ "$whyModelOutputLimits": "maxOutputTokens on each model override is the PROVIDER'S declared max_completion_tokens, written by scripts/refresh-model-limits.sh and never by hand. A cap below the model's own maximum has exactly one effect — truncation — because spend is bounded by costControls.storyBudgetHardLimitUsd, not by output length. Re-run the script when the provider's limits change.",
275
+ "hostClaudeMdOff": "1",
276
+ "hostAutoMemoryOff": "1",
277
+ "promptCacheTtl": "1h",
278
+ "$whyHostContextOff": "Values for the claude runner's env declaration (see runners.claude.$whyEnv): the host's CLAUDE.md and auto-memory stay out of every agent call; the prompt cache lives 1h."
279
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$why": [
3
+ "THE EXECUTE PASS — the fragment appended to the agent's task after its plan (llm-handler.sh). Moved out of the hub's printf lines 2026-10-24 with plan-pass.json."
4
+ ],
5
+ "body": "\n\n---\nYOUR PLAN (you wrote this a moment ago):\n__PLAN__\n\nNow produce the final answer, in exactly the format the task above\nrequires. If carrying out the plan showed it to be wrong, say so and\nanswer correctly rather than following it.\n",
6
+ "description": "Appended to an agent's task after its plan pass: the plan, and the instruction to answer now.",
7
+ "id": "plan-execute",
8
+ "placeholders": [
9
+ "__PLAN__"
10
+ ],
11
+ "version": 1,
12
+ "seams": [
13
+ "*"
14
+ ],
15
+ "layer": "engine"
16
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$why": [
3
+ "THE PLAN PASS — the fragment appended to an agent's task when its call plans first (llm-handler.sh; lib/plan-mode.js decides whether).",
4
+ "",
5
+ "It lived as printf lines inside the hub since the plan pass was built; a prompt in code is a prompt nothing reviews (one prompt = one JSON file).",
6
+ "Moved here 2026-10-24 when the pass gained Claude Code's own plan mode: the tool note differs by how the plan is made — in plan mode the agent may",
7
+ "read and search for real (nothing it does there changes anything); in the prose pass it has no tools and must not pretend to."
8
+ ],
9
+ "bodies": {
10
+ "prompt": "\n\n---\nBEFORE YOU ANSWER: state your PLAN, and nothing else.\n\nSay what you believe the answer is going to be and WHY, name the\nspecific targets you will examine or produce, and state how you will\nknow if you are wrong. If the task above sets a requirement your\nanswer must satisfy, say in one line how your plan satisfies it.\n\nDo NOT produce the final answer yet. Do NOT emit the output format\nthe task asks for. Plain prose, at most 200 words.\n\n__TOOL_NOTE__",
11
+ "tool-note-plan-mode": "You are in plan mode: you may read, search and query to ground the plan, and nothing you do here changes anything. The answer that follows this plan is made in a second step, with your full tools.",
12
+ "tool-note-no-tools": "You have NO tools available for THIS plan and cannot call any — do not\nemit tool-call syntax (<tool_call>, <tool_use>, <function_call>) or\ninvent what a tool would return. Nothing executes it, so any such text\nis fiction, not evidence — state your INTENT to examine a target, not a\nfabricated result from having done so. If tools are available for the\nanswer that follows this plan, you will use them for real then."
13
+ },
14
+ "description": "Appended to an agent's task for the plan pass; the tool note names how this plan is made.",
15
+ "id": "plan-pass",
16
+ "placeholders": [
17
+ "__TOOL_NOTE__"
18
+ ],
19
+ "version": 1,
20
+ "seams": [
21
+ "*"
22
+ ],
23
+ "layer": "engine"
24
+ }
@@ -269,6 +269,10 @@ function resolveRunner(runnerName, { projectConfigDir, defaultsFile } = {}) {
269
269
  siChannel: declared.siChannel && typeof declared.siChannel === 'object' ? declared.siChannel : null,
270
270
  // the pipeline's own tool plugins this runner hosts (plugins/*-plugin.js), served to the agent as its tools (lib/pipeline-tools-mcp.js)
271
271
  hostedToolPlugins: Array.isArray(declared.hostedToolPlugins) ? declared.hostedToolPlugins : [],
272
+ // the MCP server name the hub registers for those plugins (--mcp-config); a tool is then granted as mcp__<server>__<tool>
273
+ mcpServer: typeof declared.mcpServer === 'string' ? declared.mcpServer : '',
274
+ // how a plan pass is made on this runner (the CLI's own plan mode and session resume), or null for the hub's prose pass
275
+ planModeChannel: declared.planModeChannel && typeof declared.planModeChannel === 'object' ? declared.planModeChannel : null,
272
276
  };
273
277
  }
274
278
  /** Every runner the active stack declares, by name. */
@@ -331,8 +335,20 @@ function runnerValues(runnerName, { projectConfigDir, defaultsFile } = {}) {
331
335
  // silently skipped, so the declaration named knobs nothing could fill.
332
336
  // unsetEnv carries NAMES ONLY — there is no value to resolve, because the whole point is
333
337
  // that the variable must not be there. It passes through untouched.
338
+ // THE PIPELINE'S OWN TOOLS, BY THE NAME THE CLI KNOWS THEM. A runner that hosts the pipeline's tool plugins over an MCP
339
+ // server (lib/pipeline-tools-mcp.js) is granted them as mcp__<server>__<tool> — derived from the plugins it declares, so a
340
+ // seam's grant 'codegraph_query' reaches the CLI's --tools without anyone listing the name twice.
341
+ const toolNames = { ...runner.toolNames };
342
+ if (runner.mcpServer && runner.hostedToolPlugins.length) {
343
+ // eslint-disable-next-line global-require
344
+ const { toolsOf } = require('./pipeline-tools-mcp.js');
345
+ const orch = path.join(__dirname, '..', '..');
346
+ for (const t of toolsOf(runner.hostedToolPlugins.map((m) => path.resolve(orch, m)))) {
347
+ if (!toolNames[t.name]) toolNames[t.name] = [`mcp__${runner.mcpServer}__${t.name}`];
348
+ }
349
+ }
334
350
  const out = { env: {}, flags: {}, alwaysFlags: runner.alwaysFlags, unsetEnv: runner.unsetEnv,
335
- envNames: {}, flagNames: {}, toolNames: runner.toolNames, siChannel: runner.siChannel };
351
+ envNames: {}, flagNames: {}, toolNames, siChannel: runner.siChannel, mcpServer: runner.mcpServer, hostedToolPlugins: runner.hostedToolPlugins };
336
352
  for (const [k, name] of Object.entries(runner.env)) { out.env[k] = valueFor(name); out.envNames[k] = name; }
337
353
  for (const [k, name] of Object.entries(runner.flags)) { out.flags[k] = valueFor(name); out.flagNames[k] = name; }
338
354
  return out;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ // WHETHER THIS AGENT'S CALL PLANS FIRST (operator, 2026-10-24: "Plan mode can be used for any agent not just writer" / "If plan
3
+ // mode helps use it"). One answer for every seam, read from three places, highest first:
4
+ // 1. EPAM_PLAN_EXECUTE_SEAMS the operator's list of seams or agents that plan, comma-separated (a run's config.env)
5
+ // 2. the registry the seam's own `planMode: true|false` (agents/invocation-profiles.json)
6
+ // 3. EPAM_PLAN_EXECUTE the project-wide switch the hub has always read (installs set 0 since 2026-09-08)
7
+ // How the plan is made is the RUNNER's fact (the declaration's planModeChannel — Claude Code's own --permission-mode plan and
8
+ // --resume, or the hub's prose pass where a runner has none); this module only answers whether.
9
+ const CLI = process.argv[1] === __filename;
10
+
11
+ function planModeFor(agent, { env = process.env, registryFile } = {}) {
12
+ const listed = String(env.EPAM_PLAN_EXECUTE_SEAMS || '').split(',').map((s) => s.trim()).filter(Boolean);
13
+ let seam = '';
14
+ try {
15
+ // eslint-disable-next-line global-require
16
+ const si = require('./seam-invocation.js');
17
+ const file = registryFile || si.registryPath();
18
+ seam = agent ? si.resolveSeam(agent, file) : '';
19
+ if (listed.length && (listed.includes(agent) || listed.includes(seam))) return { plan: true, why: `EPAM_PLAN_EXECUTE_SEAMS names ${listed.includes(agent) ? agent : seam}` };
20
+ const profile = seam ? ((JSON.parse(require('fs').readFileSync(file, 'utf8')).profiles || {})[seam] || {}) : {};
21
+ if (profile.planMode === true || profile.planMode === false) return { plan: profile.planMode, why: `the registry: seam ${seam} planMode=${profile.planMode}` };
22
+ } catch (e) {
23
+ if (listed.length && listed.includes(agent)) return { plan: true, why: `EPAM_PLAN_EXECUTE_SEAMS names ${agent}` };
24
+ // an unresolved seam answers from the project-wide switch, as the hub always did
25
+ }
26
+ const global = String(env.EPAM_PLAN_EXECUTE === undefined ? '1' : env.EPAM_PLAN_EXECUTE) === '1';
27
+ return { plan: global, why: `EPAM_PLAN_EXECUTE=${env.EPAM_PLAN_EXECUTE === undefined ? '(unset → 1)' : env.EPAM_PLAN_EXECUTE}` };
28
+ }
29
+
30
+ if (CLI) {
31
+ const r = planModeFor(process.argv[2] || process.env.EPAM_AGENT_NAME || '');
32
+ process.stdout.write(r.plan ? '1' : '0');
33
+ if (process.argv.includes('--why')) process.stderr.write(`${r.why}\n`);
34
+ }
35
+
36
+ module.exports = { planModeFor };
@@ -38,6 +38,7 @@ declare -F resolve_operator_cli >/dev/null 2>&1 && CLAUDE_CMD="$(resolve_operato
38
38
  # Providers that are plugins (config/providers.json runnerPlugin) — see lib/provider-plugin.sh.
39
39
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/provider-plugin.sh"
40
40
  . "$_SCRIPT_DIR_AIRUN/lib/story-retry-state.sh"
41
+ . "$_SCRIPT_DIR_AIRUN/lib/render-engine-prompt.sh"
41
42
  # THE HUB READS ITS OWN CLOCKS. Every call path used EPAM_TIMEOUT_SECS / EPAM_PLAN_TIMEOUT_SECS and
42
43
  # relied on the CALLER having loaded them; a caller that had not (a harness, a one-off, a seam run
43
44
  # outside the orchestrator) died under `set -u` before the runner was reached. They are loaded
@@ -255,6 +256,18 @@ run_provider_once() {
255
256
  local model_args=()
256
257
  [ -n "$AI_MODEL" ] && model_args=(--model "$AI_MODEL")
257
258
 
259
+ # HOW THIS CALL IS PERMITTED. The plan pass of a runner with a plan-mode channel (its declaration, resolved by the parent
260
+ # into EPAM_PLAN_CHANNEL) runs in the CLI's own plan mode under a session id the parent minted — the CLI then returns the
261
+ # plan and changes nothing; the answer is made in that session (resume) with the ordinary permissions.
262
+ local _perm_args=(--dangerously-skip-permissions)
263
+ local _pm_mode _pm_value _pm_session _pm_resume
264
+ read -r _pm_mode _pm_value _pm_session _pm_resume <<< "${EPAM_PLAN_CHANNEL:-}"
265
+ if [ -n "${EPAM_PLAN_SESSION_NEW:-}" ] && [ -n "$_pm_mode" ]; then
266
+ _perm_args=("$_pm_mode" "$_pm_value" "$_pm_session" "$EPAM_PLAN_SESSION_NEW")
267
+ elif [ -n "${EPAM_PLAN_SESSION_RESUME:-}" ] && [ -n "$_pm_resume" ]; then
268
+ _perm_args+=("$_pm_resume" "$EPAM_PLAN_SESSION_RESUME")
269
+ fi
270
+
258
271
  # WHAT THE STACK DECLARED FOR THIS RUNNER — through the SHIM, not a second copy of it.
259
272
  #
260
273
  # lib/runner-settings.sh already resolves a runner declaration into exported env and a
@@ -345,6 +358,29 @@ run_provider_once() {
345
358
  fi
346
359
  fi
347
360
 
361
+ # THE PIPELINE'S OWN TOOLS AS THE CLI'S TOOLS (operator, 2026-10-24: "not using the proper interface is not permitted").
362
+ # A runner that hosts the pipeline's tool plugins over an MCP server (its declaration: hostedToolPlugins + mcpServer;
363
+ # lib/pipeline-tools-mcp.js) is handed that server with the flag the installed CLI advertises; the seam's grant names
364
+ # the tools (mcp__<server>__<tool>, derived in lib/llm-settings-resolve.js). The config names the runner, the project
365
+ # and this node, nothing of the host. --strict-mcp-config (the declaration's alwaysFlags) keeps it the ONLY server.
366
+ local _mcp_runner _mcp_file=""
367
+ _mcp_runner="$(runner_name_for "${PRIMARY_PROVIDER:-}" "$(basename "$CLAUDE_CMD")")"
368
+ # the declaration first (a file read), the CLI's --help only for a runner that declares a server
369
+ if [ -n "$_mcp_runner" ] \
370
+ && EPAM_RUNNER="$_mcp_runner" "${NODE_BIN:-node}" -e 'const r = require(process.argv[1] + "/lib/llm-settings-resolve.js").resolveRunner(process.env.EPAM_RUNNER, { projectConfigDir: process.env.EPAM_PROJECT_CONFIG_DIR || undefined }); process.exit(r && r.mcpServer && r.hostedToolPlugins.length ? 0 : 1);' "$_SCRIPT_DIR_AIRUN" 2>/dev/null \
371
+ && "$(runner_bin_for "${PRIMARY_PROVIDER:-}" "$CLAUDE_CMD")" --help 2>/dev/null | grep -q -- '--mcp-config'; then
372
+ _mcp_file="$(EPAM_RUNNER="$_mcp_runner" "${NODE_BIN:-node}" -e '
373
+ const r = require(process.argv[1] + "/lib/llm-settings-resolve.js").resolveRunner(process.env.EPAM_RUNNER, { projectConfigDir: process.env.EPAM_PROJECT_CONFIG_DIR || undefined });
374
+ if (!r || !r.mcpServer || !r.hostedToolPlugins.length) process.exit(0);
375
+ const fs = require("fs"); const path = require("path");
376
+ const dir = process.env.LOG_DIR && fs.existsSync(process.env.LOG_DIR) ? process.env.LOG_DIR : require("os").tmpdir();
377
+ const file = path.join(dir, `pipeline-mcp-${process.env.EPAM_RUNNER}.json`);
378
+ fs.writeFileSync(file, JSON.stringify(require(process.argv[1] + "/lib/pipeline-tools-mcp.js").mcpConfig(process.env.EPAM_RUNNER, process.execPath)));
379
+ process.stdout.write(file);
380
+ ' "$_SCRIPT_DIR_AIRUN" 2>/dev/null)" || _mcp_file=""
381
+ [ -n "$_mcp_file" ] && runner_args+=(--mcp-config "$_mcp_file")
382
+ fi
383
+
348
384
 
349
385
  # A PROVIDER THAT IS A PLUGIN answers through its plugin (config/providers.json runnerPlugin;
350
386
  # lib/provider-plugin.sh). Its vendor facts live in the plugin; this names no provider. The result
@@ -383,7 +419,7 @@ run_provider_once() {
383
419
  # aborted three steps later on "codeline scope could not be resolved". The actual
384
420
  # message was `error: unknown option '-s'`.
385
421
  local _cc_err; _cc_err="$(mktemp "${TMPDIR:-/tmp}/claude-err-XXXXXX")"
386
- "$CLAUDE_CMD" --print --output-format json --dangerously-skip-permissions "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} \
422
+ "$CLAUDE_CMD" --print --output-format json "${_perm_args[@]}" "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} \
387
423
  < "$PROMPT_FILE" > "$_json_out" 2>"$_cc_err"
388
424
  local _cc_rc=$?
389
425
  if [ ! -s "$_json_out" ] && [ -s "$_cc_err" ]; then
@@ -399,7 +435,7 @@ run_provider_once() {
399
435
  # are kept first; then the CLI's own status is the arm's.
400
436
  [ "$_cc_rc" -eq 0 ] || return "$_cc_rc"
401
437
  else
402
- "$CLAUDE_CMD" --print --output-format text --dangerously-skip-permissions "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} < "$PROMPT_FILE"
438
+ "$CLAUDE_CMD" --print --output-format text "${_perm_args[@]}" "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} < "$PROMPT_FILE"
403
439
  fi
404
440
  fi
405
441
  ;;
@@ -419,7 +455,7 @@ run_provider_once() {
419
455
  # or budget failure, not a format one)" and aborted the run, when the real fault was a
420
456
  # one-line parse error visible right here.
421
457
  local _cm_err; _cm_err="$(mktemp "${TMPDIR:-/tmp}/codemie-err-XXXXXX")"
422
- codemie-claude --print --output-format json --dangerously-skip-permissions "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} \
458
+ codemie-claude --print --output-format json "${_perm_args[@]}" "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} \
423
459
  < "$PROMPT_FILE" > "$_cm_json" 2>"$_cm_err"
424
460
  local _cm_rc=$?
425
461
 
@@ -468,7 +504,7 @@ run_provider_once() {
468
504
  # are kept first; then the CLI's own status is the arm's.
469
505
  [ "$_cm_rc" -eq 0 ] || return "$_cm_rc"
470
506
  else
471
- codemie-claude --print --output-format text --dangerously-skip-permissions "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} < "$PROMPT_FILE"
507
+ codemie-claude --print --output-format text "${_perm_args[@]}" "${model_args[@]}" ${runner_args[@]+"${runner_args[@]}"} < "$PROMPT_FILE"
472
508
  fi
473
509
  ;;
474
510
  codex)
@@ -742,92 +778,73 @@ if [ -n "$_LADDER_AGENT" ] && [ -n "${LOG_DIR:-}" ] && [ "${_EPAM_IN_PLAN_PASS:-
742
778
  fi
743
779
 
744
780
  _plan_cost_json=""
745
- if [ "${EPAM_PLAN_EXECUTE:-1}" = "1" ] && [ "${_EPAM_IN_PLAN_PASS:-0}" != "1" ]; then
781
+ # WHETHER THIS AGENT PLANS FIRST is one answer for every seam (lib/plan-mode.js: the operator's EPAM_PLAN_EXECUTE_SEAMS, the
782
+ # seam's registry planMode, then EPAM_PLAN_EXECUTE). HOW is the runner's fact: a declared planModeChannel is Claude Code's own
783
+ # plan mode — the plan made with the seam's read tools under a session the hub mints, the answer made in that session — and
784
+ # a runner without one gets the prose pass (the plan written with no tools), as before.
785
+ _plan_on="$("${NODE_BIN:-node}" "$_SCRIPT_DIR_AIRUN/lib/plan-mode.js" "${EPAM_AGENT_NAME:-}" 2>/dev/null || printf '%s' "${EPAM_PLAN_EXECUTE:-1}")"
786
+ if [ "$_plan_on" = "1" ] && [ "${_EPAM_IN_PLAN_PASS:-0}" != "1" ]; then
787
+ _plan_channel=""; _plan_session=""
788
+ _plan_runner="$(runner_name_for "${PRIMARY_PROVIDER:-}" "$(basename "${CLAUDE_CMD:-claude}")" 2>/dev/null || true)"
789
+ if [ -n "$_plan_runner" ]; then
790
+ _plan_channel="$(EPAM_RUNNER="$_plan_runner" "${NODE_BIN:-node}" -e '
791
+ const r = require(process.argv[1] + "/lib/llm-settings-resolve.js").resolveRunner(process.env.EPAM_RUNNER, { projectConfigDir: process.env.EPAM_PROJECT_CONFIG_DIR || undefined });
792
+ const c = r && r.planModeChannel; if (!c || !c.modeFlag || !c.planValue || !c.sessionFlag || !c.resumeFlag) process.exit(0);
793
+ process.stdout.write([c.modeFlag, c.planValue, c.sessionFlag, c.resumeFlag].join(" "));' "$_SCRIPT_DIR_AIRUN" 2>/dev/null || true)"
794
+ if [ -n "$_plan_channel" ] && "$(runner_bin_for "${PRIMARY_PROVIDER:-}" "${CLAUDE_CMD:-claude}")" --help 2>/dev/null | grep -q -- "${_plan_channel%% *}"; then
795
+ _plan_session="$(uuidgen 2>/dev/null || "${NODE_BIN:-node}" -e 'process.stdout.write(require("crypto").randomUUID())')"
796
+ else
797
+ _plan_channel=""
798
+ fi
799
+ fi
800
+ _plan_vals="$(mktemp)"; printf '{}' > "$_plan_vals"
801
+ if [ -n "$_plan_session" ]; then _plan_note="$(render_engine_prompt plan-pass "$_plan_vals" tool-note-plan-mode)"; else _plan_note="$(render_engine_prompt plan-pass "$_plan_vals" tool-note-no-tools)"; fi
802
+ jq -n --arg n "$_plan_note" '{__TOOL_NOTE__: $n}' > "$_plan_vals"
746
803
  _plan_file="$(mktemp)"
747
- {
748
- cat "$PROMPT_FILE"
749
- printf '\n\n---\n'
750
- printf 'BEFORE YOU ANSWER: state your PLAN, and nothing else.\n\n'
751
- printf 'Say what you believe the answer is going to be and WHY, name the\n'
752
- printf 'specific targets you will examine or produce, and state how you will\n'
753
- printf 'know if you are wrong. If the task above sets a requirement your\n'
754
- printf 'answer must satisfy, say in one line how your plan satisfies it.\n\n'
755
- printf 'Do NOT produce the final answer yet. Do NOT emit the output format\n'
756
- printf 'the task asks for. Plain prose, at most 200 words.\n\n'
757
- printf 'You have NO tools available for THIS plan and cannot call any — do not\n'
758
- printf 'emit tool-call syntax (<tool_call>, <tool_use>, <function_call>) or\n'
759
- printf 'invent what a tool would return. Nothing executes it, so any such text\n'
760
- printf 'is fiction, not evidence — state your INTENT to examine a target, not a\n'
761
- printf 'fabricated result from having done so. If tools are available for the\n'
762
- printf 'answer that follows this plan, you will use them for real then.\n'
763
- } > "$_plan_file"
764
-
804
+ { cat "$PROMPT_FILE"; render_engine_prompt plan-pass "$_plan_vals"; } > "$_plan_file"
765
805
  _plan_json=""
766
806
  [ -n "${ORCH_JSON_RESULT:-}" ] && _plan_json="$(mktemp)"
767
-
768
- # The plan pass is an ordinary invocation with planning suppressed, so it
769
- # cannot recurse.
770
- # A distinct agent label, so the planning call is a separate, identifiable
771
- # generation in Langfuse instead of a second trace with the same name. Both
772
- # passes previously inherited EPAM_AGENT_NAME, which made plan and answer
773
- # indistinguishable in the one place their cost and latency are comparable.
774
- # NO TOOLS, and its own short deadline.
775
- #
776
- # Inheriting the tool budget made the detective explore twice — seven calls to
777
- # plan, seven more to answer — for work already done. And because the agent's
778
- # timeout wraps this whole script, a second exploration pushed agents toward
779
- # deadlines set for one pass, manufacturing timeouts that read as model
780
- # failures.
781
- #
782
- # It does not need them: the prompt already carries pre-seeded CodeGraph
783
- # output, so a hypothesis can be formed without spending a call — and a plan
784
- # made BEFORE looking is the more useful artefact, because the answer can be
785
- # checked against it. The execute pass keeps the full budget.
786
- # The plan pass is this same agent: named "<agent>:plan", and told the SI file it inherits is its own.
807
+ # The plan pass is an ordinary invocation with planning suppressed, so it cannot recurse. Its own agent label
808
+ # ("<agent>:plan") keeps plan and answer apart in Langfuse and the ledger. In plan mode the seam's read tools stay (plan mode
809
+ # itself permits nothing else); in the prose pass it has NO tools and its own short deadline (the detective explored twice).
787
810
  _plan_agent="${EPAM_AGENT_NAME:-agent}:plan"
811
+ _plan_tools_env=(EPAM_ALLOWED_TOOLS= EPAM_MAX_TOOL_CALLS=0)
812
+ [ -n "$_plan_session" ] && _plan_tools_env=(EPAM_PLAN_SESSION_NEW="$_plan_session" EPAM_PLAN_CHANNEL="$_plan_channel")
788
813
  _plan_text="$(
789
- _EPAM_IN_PLAN_PASS=1 \
814
+ env _EPAM_IN_PLAN_PASS=1 \
790
815
  EPAM_SI_AGENT="$_plan_agent" \
791
816
  EPAM_AGENT_NAME="$_plan_agent" \
792
- EPAM_ALLOWED_TOOLS="" \
793
- EPAM_MAX_TOOL_CALLS=0 \
817
+ "${_plan_tools_env[@]}" \
794
818
  ORCH_JSON_RESULT="$_plan_json" \
795
819
  PROMPT_FILE="$_plan_file" \
796
820
  bash "$0" ${PRIMARY_PROVIDER:+--provider "$PRIMARY_PROVIDER"} ${AI_MODEL:+--model "$AI_MODEL"} \
797
821
  < "$_plan_file" 2>/dev/null || true
798
822
  )"
799
823
  rm -f "$_plan_file"
800
-
801
824
  if [ -n "$_plan_text" ]; then
802
825
  _plan_cost_json="$_plan_json"
803
-
804
- # Langfuse is off in mock1 and in any run without LANGFUSE_* configured, so
805
- # the plan is also written somewhere that always exists. Runs 8 and 9
806
- # produced byte-identical logs and different answers; without the plans
807
- # there is nothing to compare. Best-effort: a plan that cannot be recorded
808
- # must never break the agent call it belongs to.
826
+ # Recorded where it always exists (Langfuse is off in mock runs): runs 8 and 9 had identical logs and different answers.
809
827
  if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ]; then
810
828
  jq -cn --arg agent "${EPAM_AGENT_NAME:-agent}" \
811
829
  --arg story "${EPAM_STORY_ID:-}" \
812
830
  --arg model "${AI_MODEL:-}" \
813
831
  --arg phase "${PHASE:-unknown}" \
814
832
  --arg plan "$_plan_text" \
815
- '{ts:(now|todate), agent:$agent, story:$story, model:$model, phase:$phase, plan:$plan}' \
833
+ --arg session "$_plan_session" \
834
+ --arg mode "$([ -n "$_plan_session" ] && echo plan-mode || echo prose)" \
835
+ '{ts:(now|todate), agent:$agent, story:$story, model:$model, phase:$phase, mode:$mode, session:$session, plan:$plan}' \
816
836
  >> "$LOG_DIR/plans-${PHASE:-unknown}.jsonl" 2>/dev/null || true
817
837
  fi
838
+ jq -n --arg p "$_plan_text" '{__PLAN__: $p}' > "$_plan_vals"
818
839
  _exec_file="$(mktemp)"
819
- {
820
- cat "$PROMPT_FILE"
821
- printf '\n\n---\n'
822
- printf 'YOUR PLAN (you wrote this a moment ago):\n%s\n\n' "$_plan_text"
823
- printf 'Now produce the final answer, in exactly the format the task above\n'
824
- printf 'requires. If carrying out the plan showed it to be wrong, say so and\n'
825
- printf 'answer correctly rather than following it.\n'
826
- } > "$_exec_file"
840
+ { cat "$PROMPT_FILE"; render_engine_prompt plan-execute "$_plan_vals"; } > "$_exec_file"
827
841
  mv "$_exec_file" "$PROMPT_FILE"
842
+ # the answer is made in the plan's session, with the ordinary permissions
843
+ if [ -n "$_plan_session" ]; then export EPAM_PLAN_SESSION_RESUME="$_plan_session" EPAM_PLAN_CHANNEL="$_plan_channel"; fi
828
844
  else
829
845
  [ -n "$_plan_json" ] && rm -f "$_plan_json"
830
846
  fi
847
+ rm -f "$_plan_vals"
831
848
  fi
832
849
 
833
850
  # The plan pass billed real money. Fold its cost into the result the caller
@@ -5327,33 +5327,18 @@ async function reviewRoster({
5327
5327
  + (d.length ? d.join(', ') : '(no manifest configuration)');
5328
5328
  }).join('\n');
5329
5329
 
5330
- // THE WHOLE TICKET, not a summary. The reviewer decides whether a brief serves the work that
5331
- // was actually asked for, so it needs the same view of the request the mint had: components
5332
- // (which say how far the work reaches), the links, and the untruncated description.
5333
- const ticketBlock = (Array.isArray(tickets) ? tickets : []).map((t) => {
5334
- const head = '- ' + (t.jiraKey || t.id || '') + ': ' + (t.title || '');
5335
- const comps = Array.isArray(t.components) && t.components.length
5336
- ? '\n Components: ' + t.components.join(', ') : '';
5337
- const links = (Array.isArray(t.ticketLinks) ? t.ticketLinks : [])
5338
- .map((l) => (typeof l === 'string' ? l : (l && l.url))).filter(Boolean);
5339
- const linkLine = links.length ? '\n Links: ' + links.join(' , ') : '';
5340
- return head + comps + '\n ' + String(t.description || '').replace(/\s+/g, ' ') + linkLine;
5341
- }).join('\n');
5342
-
5343
- // THE DOCUMENTS THE BRIEFS WERE DERIVED FROM. Without them the reviewer can tell that a brief
5344
- // disagrees with the repository, but not WHY — and the why decides the remedy. A brief that
5345
- // followed the vendor's current guide into a symbol this pinned version lacks needs the
5346
- // version-correct instruction; one that simply invented something needs deleting.
5347
- const docBlock = (Array.isArray(referencedDocs) ? referencedDocs : [])
5348
- .filter((d) => d && d.fetchStatus === 'fetched')
5349
- .map((d) => {
5350
- if (Array.isArray(d.quotes) && d.quotes.length) {
5351
- return '- ' + d.url + '\n' + d.quotes.map((q) => ' "' + String(q).replace(/\s+/g, ' ') + '"').join('\n');
5352
- }
5353
- let body = typeof d.body === 'string' ? d.body : '';
5354
- if (!body && d.path) { try { body = fs.readFileSync(d.path, 'utf8'); } catch { body = ''; } }
5355
- return body ? '- ' + d.url + '\n' + body : '- ' + d.url + ' (retrieved, no readable text)';
5356
- }).join('\n\n');
5330
+ // THE WORK'S SHAPE, NOT ITS WORDS — the same rule as the mint's (mintProjectAgents). This reviewer was handed THE WHOLE TICKET
5331
+ // to judge coverage, and its findings were the mint's corrective context: live 2026-10-24 (zero-story-references P1, four
5332
+ // cycles) a finding read "the ticket (AMSD-2974) asks for accessibility enhancements to Toast Notification pop-ups … the roster
5333
+ // needs an implementer scoped to the cx-shared ToastNotification component", the re-mint wrote exactly that brief, and the
5334
+ // next cycle indicted it for naming the ticket's subject. A roster is kept with the codelines and reused for every later
5335
+ // ticket on them; whether THIS ticket's stories find a role is the assignment stage's question. The reviewer judges the
5336
+ // briefs against the codelines: true, owned tests, every codeline the work reaches covered by the kinds it needs.
5337
+ const _rvTickets = Array.isArray(tickets) ? tickets : [];
5338
+ const ticketBlock = `${_rvTickets.length} ticket(s) from the tracker reach the codelines listed. You are not told what they ask for: this roster is kept
5339
+ with the codelines and serves every later ticket on them, so judge each brief against ITS CODELINE, not against any one piece of work.`;
5340
+ const docBlock = '';
5341
+ void referencedDocs;
5357
5342
 
5358
5343
  const toolLine = toolGrant
5359
5344
  ? 'Your tools: ' + toolGrant + '. Open the repositories. Resolve the packages and symbols these '
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.43",
3
+ "version": "0.5.44",
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": {