amsd-pipeline-shared 0.5.41 → 0.5.43

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.
@@ -225,6 +225,7 @@ f64fdd6bc6df7e13d742e9e53a470be4 scripts/lib/release-lines.sh
225
225
  1cc1978d614fca875ad1117e9ac42f93 scripts/lib/render-engine-prompt.sh
226
226
  6894e248c7a86d07626b7e981d534354 scripts/lib/render-prompt-section.js
227
227
  9768775dcd035b39932f3ebce243857e scripts/lib/resume-semantics.sh
228
+ c2761c77c580a4f7c5bc36d6dfe797cf scripts/lib/roster-seams.js
228
229
  9216939efdf63c244d42d91ba6fb466c scripts/lib/run-checkpoint.sh
229
230
  886860d643c5db8fb3bfcf5bd3efaf9d scripts/lib/run-modes.sh
230
231
  912511e457b0a6f4801c479400b1c3a3 scripts/lib/seam-invocation.js
@@ -233,6 +233,7 @@ scripts/lib/model-ladders.sh
233
233
  scripts/lib/orchestration-resume.sh
234
234
  scripts/lib/phase-exit.sh
235
235
  scripts/lib/pipeline-commit.sh
236
+ scripts/lib/pipeline-tools-mcp.js
236
237
  scripts/lib/pipeline-tools.js
237
238
  scripts/lib/plan-hash.js
238
239
  scripts/lib/plan-of-record.sh
@@ -252,6 +253,8 @@ scripts/lib/release-lines.sh
252
253
  scripts/lib/render-engine-prompt.sh
253
254
  scripts/lib/render-prompt-section.js
254
255
  scripts/lib/resume-semantics.sh
256
+ scripts/lib/roster-mint-cycle.js
257
+ scripts/lib/roster-seams.js
255
258
  scripts/lib/reviewer-model.js
256
259
  scripts/lib/run-checkpoint.sh
257
260
  scripts/lib/run-modes.sh
@@ -24,9 +24,10 @@
24
24
  "'implementer'; a CORRECT, investigator-rich survey made it label everything 'investigator'.",
25
25
  "Improving the survey made the roster worse.",
26
26
  "2026-08-17: the name rule said 'ending in \"-engineer\" or \"-specialist\"'. The invocation registry has twenty name-shape rules and `specialist` is in none of them, so every name of that shape threw at resolveSeam and killed the run at mint — a coin flip on the model's wording. The rule is now __NAME_RULE__, derived from the registry's own seamPatterns, so the mint can only be offered a shape that routes. The examples above lost their suffix for the same reason: they were a fourth copy of the same fact.",
27
- "v2 (operator, 2026-10-24: the roster minted for AMSD-2974 named every agent after the ticket's subject (<codeline>-toast-detective, toast-accessibility-engineer) and its briefs described that one ticket; the roster and its prompts are cached per codeline and reused for every later story, so the next ticket on these codelines would inherit them). A role is the codeline's, for every story it will ever be given."
27
+ "v2 (operator, 2026-10-24: the roster minted for AMSD-2974 named every agent after the ticket's subject (<codeline>-toast-detective, toast-accessibility-engineer) and its briefs described that one ticket; the roster and its prompts are cached per codeline and reused for every later story, so the next ticket on these codelines would inherit them). A role is the codeline's, for every story it will ever be given.",
28
+ "v3 (operator, 2026-10-24: \"The purpose of re caching was to remove user story references in profiles and prompts\" — zero, not \"zero except marked examples\"): the sets are kept per codeline and reused for every later story; any reference to the ticket in scope, however labelled, is inherited by the next story's agents. Run 14's fresh mint had codeline-level names and briefs of seven \"worked example: for AMSD-2974 …\" sentences each, approved under the earlier wording."
28
29
  ],
29
- "body": "You are a senior engineering manager designing an AI agent team for a software project.\n\nThe following __FIXED_ROLE_COUNT__ fixed roles are ALWAYS present and must NOT be reproduced:\n__FIXED_ROLE_LIST__\n\nGiven the project manifest, tech stack, and clarification answers, propose 2-6 project-specific engineering agent roles that will implement the user stories. Each role should map to a distinct domain of the project — one per domain, e.g. a react-frontend role, a postgres-data role, an auth-security role. The exact name shape is fixed below.\n\nEvery role is one of TWO KINDS, and you must state which:\n\n- \"implementer\" — WRITES the change. This is the role that edits source and makes the work\n happen. An implementer spans the project, so its codeline is \"*\".\n- \"investigator\" — READS one codeline and characterises what is there for the implementer. It\n never writes. An investigator names exactly ONE codeline, spelled as listed in scope.\n\nYOU MUST PROPOSE AT LEAST ONE IMPLEMENTER. A team of investigators can describe the work and\ncannot do it: every story is assigned to an implementer, so a roster without one leaves every\nstory with nobody to write it and the run cannot proceed. Investigators are optional and\nadditive — propose them where a codeline genuinely needs its own reader.\n\nThe survey above recommends both: which codelines need investigating, and which need work\nwritten. Those are recommendations about the team, not instructions — you decide the roster.\n\nFor each role provide:\n__NAME_RULE__\n- kind: exactly \"implementer\" or \"investigator\"\n- codeline: for an investigator, the ONE codeline it reads; for an implementer, \"*\"\n- systemPrompt: A detailed system prompt (150-300 words) describing the agent's expertise, the project's conventions, key files/directories they own, coding patterns they follow, and tools they use. Be specific to this project.\n- rationale: One sentence explaining why THIS project needs THIS role. It is read by a human\n reviewing the team and seeds the agent's knowledge base, so it must say something.\n\nRespond with ONLY valid JSON (no markdown fences):\n{\n \"proposedAgents\": [\n { \"name\": \"<string>\", \"kind\": \"<implementer|investigator>\", \"codeline\": \"<name or *>\", \"systemPrompt\": \"<string>\", \"rationale\": \"<string>\" }\n ]\n}\n\nA ROLE OUTLIVES THE TICKET. The roster is kept with its codeline and reused for every story that codeline is given later, so each\nrole is minted for the codeline's work, not for the work in scope now: its name says what kind of engineer or investigator it is and\nwhich codeline it serves, never the subject, component or ticket of the current story; its system prompt describes the codeline — its\nstack, conventions, surfaces and how to verify there — and the kind of work the role does, and names the current ticket's subject,\ncomponents or files only as examples marked as such. A role that only makes sense for the story in scope is a defect.\n",
30
+ "body": "You are a senior engineering manager designing an AI agent team for a software project.\n\nThe following __FIXED_ROLE_COUNT__ fixed roles are ALWAYS present and must NOT be reproduced:\n__FIXED_ROLE_LIST__\n\nGiven the project manifest, tech stack, and clarification answers, propose 2-6 project-specific engineering agent roles that will implement the user stories. Each role should map to a distinct domain of the project — one per domain, e.g. a react-frontend role, a postgres-data role, an auth-security role. The exact name shape is fixed below.\n\nEvery role is one of TWO KINDS, and you must state which:\n\n- \"implementer\" — WRITES the change. This is the role that edits source and makes the work\n happen. An implementer spans the project, so its codeline is \"*\".\n- \"investigator\" — READS one codeline and characterises what is there for the implementer. It\n never writes. An investigator names exactly ONE codeline, spelled as listed in scope.\n\nYOU MUST PROPOSE AT LEAST ONE IMPLEMENTER. A team of investigators can describe the work and\ncannot do it: every story is assigned to an implementer, so a roster without one leaves every\nstory with nobody to write it and the run cannot proceed. Investigators are optional and\nadditive — propose them where a codeline genuinely needs its own reader.\n\nThe survey above recommends both: which codelines need investigating, and which need work\nwritten. Those are recommendations about the team, not instructions — you decide the roster.\n\nFor each role provide:\n__NAME_RULE__\n- kind: exactly \"implementer\" or \"investigator\"\n- codeline: for an investigator, the ONE codeline it reads; for an implementer, \"*\"\n- systemPrompt: A detailed system prompt (150-300 words) describing the agent's expertise, the project's conventions, key files/directories they own, coding patterns they follow, and tools they use. Be specific to this project.\n- rationale: One sentence explaining why THIS project needs THIS role. It is read by a human\n reviewing the team and seeds the agent's knowledge base, so it must say something.\n\nRespond with ONLY valid JSON (no markdown fences):\n{\n \"proposedAgents\": [\n { \"name\": \"<string>\", \"kind\": \"<implementer|investigator>\", \"codeline\": \"<name or *>\", \"systemPrompt\": \"<string>\", \"rationale\": \"<string>\" }\n ]\n}\n\nA ROLE OUTLIVES THE TICKET — ZERO REFERENCES TO THE TICKET IN SCOPE. This is kept with the codeline and reused for every later story on it, so it refers to no\nticket at all — not the key, title, subject, components, requirements or files of the story in scope, not even as an example. Examples\ncome from the codeline itself: its structure, conventions, manifests and how to verify there. Anything that could only have been written\nwith this ticket in view is a defect.\nA role's name says what kind of engineer or investigator it is and which codeline it serves; its system prompt describes the codeline\nand the kind of work the role does.",
30
31
  "description": "Phase B of scaffolding: propose this project's own engineering agent roles. THE MINT PROMPT.",
31
32
  "id": "agent-proposal",
32
33
  "placeholders": [
@@ -34,7 +35,7 @@
34
35
  "__FIXED_ROLE_LIST__",
35
36
  "__NAME_RULE__"
36
37
  ],
37
- "version": 2,
38
+ "version": 3,
38
39
  "seams": [
39
40
  "agent-mint"
40
41
  ]
@@ -20,7 +20,8 @@
20
20
  "prose rewrites prompts that were correct, and the placeholder contract already covers",
21
21
  "structure. Only a claim that can be checked against the repository is in scope.",
22
22
  "2026-10-06, AMSD-2974: metrolinx's repro-test-writer was generated as \"write a spec file for a Toast component at cx-shared\" — the generator completed sentences its slots complete at run time, using the one ticket it was shown, and the reviewer passed it because every claim was true. The prompt serves every story and call; a slot's place is the pipeline's; narrowing and a displaced slot are findings.",
23
- "v4 (operator, 2026-10-24: gear each prompt to its use case — strip what is not needed, add net new): the generator may now leave out a segment that cannot apply to this project and add guidance the template lacks (project-prompt-contract.js); the reviewer is shown both (__USE_CASE_EDITS__) and judges them as it judges every other claim."
23
+ "v4 (operator, 2026-10-24: gear each prompt to its use case — strip what is not needed, add net new): the generator may now leave out a segment that cannot apply to this project and add guidance the template lacks (project-prompt-contract.js); the reviewer is shown both (__USE_CASE_EDITS__) and judges them as it judges every other claim.",
24
+ "v5 (operator, 2026-10-24: \"The purpose of re caching was to remove user story references in profiles and prompts\" — zero, not \"zero except marked examples\"): the sets are kept per codeline and reused for every later story; any reference to the ticket in scope, however labelled, is inherited by the next story's agents. Run 14's fresh mint had codeline-level names and briefs of seven \"worked example: for AMSD-2974 …\" sentences each, approved under the earlier wording."
24
25
  ],
25
26
  "description": "Falsifies what a generated project prompt claims about this project, before any agent inherits it.",
26
27
  "seams": [
@@ -28,8 +29,8 @@
28
29
  ],
29
30
  "consumer": "engine",
30
31
  "id": "prompt-review",
31
- "version": 4,
32
- "body": "You are reviewing a PROJECT PROMPT that was generated by specialising a generic template for this specific project. Another agent will receive it as its system prompt and act on every claim in it.\n\nTHE GENERIC TEMPLATE IT WAS DERIVED FROM:\n__TEMPLATE_BODY__\n\nTHE GENERATED PROJECT PROMPT UNDER REVIEW (id: __PROMPT_ID__):\n__GENERATED_BODY__\n\nWHAT THE GENERATOR LEFT OUT OF THE TEMPLATE FOR THIS PROJECT, AND WHAT IT ADDED:\n__USE_CASE_EDITS__\n\nTHE CODELINES THIS PROJECT WORKS ON, and what each one declares:\n__CODELINE_BLOCK__\n\nTHE WORK THE PROJECT WAS ASKED TO DO:\n__TICKET_BLOCK__\n\n__TOOL_LINE__\n\n# What is in scope\n\nTHREE QUESTIONS ONLY:\n1. Does the generated prompt state anything about this project that is FALSE?\n2. Is the generated prompt NARROWED TO ONE STORY OR CALL, or does a slot NO LONGER FIT ITS SENTENCE? (See \"A prompt narrowed to one call\" below.)\n3. Was any part of the template LEFT OUT although it applies to this project, or was anything ADDED that is false of this project or serves only one story or call? A part left out is a finding when its reason is false of the codelines or the kind of project; quote the template's words that were left out as the claim.\n\nA claim is in scope when it can be checked against a repository — a path, a file, a directory, a dependency, a command, a framework, a convention, a symbol, an ownership statement ('X owns Y'). Open the codelines and check it.\n\nExamples of a finding:\n- it names a test directory, file suffix or framework this codeline does not use\n- it names a dependency, script or command the manifest does not declare\n- it asserts a file, module or symbol exists that does not\n- it attributes behaviour to a file that does not contain it\n- it describes a codeline this project does not have, or confuses two that it does\n- it instructs the agent to run something this codeline cannot run\n\n# A prompt narrowed to one call\n\nThe generated prompt is not written for the ticket in scope now. It is reused for every story and every call this project makes to the agent, and each call's specifics — the role for the turn, the file to write, the command to run, the diff, the story — arrive through the slots (the SCREAMING_SNAKE tokens between double underscores) at run time. Compare it with the template, slot by slot, and report as a finding:\n- a statement true only for one story or one call — this ticket's component, a file or path one story will write, a command aimed at one story's file — written where the template leaves that to a slot. It is a finding even when it is true today: the next story receives it as an instruction.\n- a slot that no longer fits its sentence: in the template the slot completes or sits inside a sentence; in the generated prompt the words around it were moved, completed by hand, or rewritten, so the run-time value would land in the wrong place or be said twice. Quote the template's words around that slot and the generated prompt's.\nFor these findings `claim` quotes the generated text, `checked` names the template slot you compared it with, and `found` says what the template has there. Give no `verification` field: this is read from the two texts, not from the repository.\n\n# Before you call anything absent: SEARCH for it\n\nA path in a generated prompt is frequently ABBREVIATED rather than wrong. Looking at the literal\nstring, finding nothing there, and reporting a false claim is the single most common way this\nreview does damage — it rejects a prompt that was RIGHT about the project, the generator rewrites\nthe whole thing to escape a finding that was not real, and in rewriting it drops the placeholders\nthe pipeline depends on. Three attempts of that aborts the run.\n\nSo, for every path, file, directory or symbol the prompt names:\n\n1. SEARCH the codeline for it by BASENAME before concluding it does not exist. `src/routes/checkouts/[checkoutId].ts`\n and `src/pages/api/checkouts/[checkoutId].ts` are both a SHORTENED form of\n `src/pages/api/v1/ecommerce/checkouts/[checkoutId].ts`, which exists — that is a near-miss path,\n NOT a false claim, and it is not a finding.\n2. An IMPRECISE path that RESOLVES to a real file is not a finding. Neither is a partial path, a\n path missing intermediate directories, or a file named without its directory.\n A NAME MAY BE A DIRECTORY. A file glob such as **/Name* matches files, not directories, and a\n text search matches contents, not folder names — neither can show you a directory. Before you\n call a component, module or folder absent, list its parent directory and look for it there.\n3. Report it ONLY when the thing genuinely does not exist anywhere in the codeline — no file of\n that name, no such dependency in the manifest, no such symbol in the sources.\n\nAbsence at one exact path is not absence from the repository. If you did not search, you do not\nknow, and \"I did not find it where the prompt said\" is not a finding you may report.\n\n# What is NOT in scope\n\nDo not review wording, tone, length, structure or ambition. Do not propose improvements. Do not object that the prompt is too close to, or too far from, the template — how far it may diverge is decided elsewhere, and the placeholder contract is already enforced mechanically. (A prompt narrowed to one call, or a slot that no longer fits its sentence, is not a question of distance — report it as above.)\n\nA prompt that says nothing false and serves every call is SOUND, however plain it is.\n\n# Evidence\n\nReturn every defect you can evidence, and nothing you cannot. For each finding, say what you CHECKED and what you FOUND, naming the file or manifest you read. A finding without a check is an opinion, and an opinion about a prompt is not a defect.\n\nWhere a finding turns on whether a named path or dependency is present, fill in the verification field as well as the prose. The pipeline re-runs that exact check against the repository and DISCARDS any finding the check refutes — so a careless reading costs nothing, and a correct one is confirmed independently of how convincingly you argued it.\n\nIf every claim checks out, return no findings. An empty finding list from a review that RAN is the correct answer for a sound prompt.\n\n# How to return the verdict\n\nEmit the verdict INLINE as the last thing you write, wrapped in these exact tags. Do not write it\nto a file, and do not use a tool to deliver it — a verdict delivered any other way is not read, and\nthe prompt is then installed unreviewed.\n\n<PROMPT_REVIEW>\n{\"falseClaims\": []}\n</PROMPT_REVIEW>\n\n`falseClaims` is a list of the defects you evidenced. Each entry is an object:\n\n- `claim` — the false statement, quoted from the generated prompt\n- `checked` — what you opened or ran to test it, naming the file or manifest\n- `found` — what was actually there\n- `verification` — when the finding turns on a named path or dependency being present or absent,\n an object stating what to re-check, so the pipeline can confirm it independently\n\nTHE ROLES THIS PROJECT MINTED, which is the ground truth for any claim naming an agent:\n\n__ROSTER_BLOCK__\n\nA prompt naming one of these is stating a FACT, not inventing one. Check the list before calling a\nrole false — an unverifiable claim is not a false claim, and rejecting one costs a regeneration and\nteaches the generator nothing.\n\nTHE BLOCK IS THE WHOLE ANSWER. Nothing you write outside it is read — the consumer matches the\ntags and discards everything else. Do not narrate your investigation, do not summarise what you\nchecked, and do not restate the prompt back. Put your evidence INSIDE `checked` and `found` on the\nfinding it belongs to, where it is kept; anywhere else it is thrown away.\n\nMeasured 2026-08-27: reviews averaged 3,366 output tokens to deliver a 13-token verdict. Every one\nof those tokens was billed and 99% of them were discarded unread.\n\nAn empty list is the correct verdict for a sound prompt. Emit the block either way: the block is\nhow a review that RAN is distinguished from one that did not run at all.\n",
32
+ "version": 5,
33
+ "body": "You are reviewing a PROJECT PROMPT that was generated by specialising a generic template for this specific project. Another agent will receive it as its system prompt and act on every claim in it.\n\nTHE GENERIC TEMPLATE IT WAS DERIVED FROM:\n__TEMPLATE_BODY__\n\nTHE GENERATED PROJECT PROMPT UNDER REVIEW (id: __PROMPT_ID__):\n__GENERATED_BODY__\n\nWHAT THE GENERATOR LEFT OUT OF THE TEMPLATE FOR THIS PROJECT, AND WHAT IT ADDED:\n__USE_CASE_EDITS__\n\nTHE CODELINES THIS PROJECT WORKS ON, and what each one declares:\n__CODELINE_BLOCK__\n\nTHE WORK THE PROJECT WAS ASKED TO DO:\n__TICKET_BLOCK__\n\n__TOOL_LINE__\n\n# What is in scope\n\nFOUR QUESTIONS ONLY:\n1. Does the generated prompt state anything about this project that is FALSE?\n2. Is the generated prompt NARROWED TO ONE STORY OR CALL, or does a slot NO LONGER FIT ITS SENTENCE? (See \"A prompt narrowed to one call\" below.)\n3. Was any part of the template LEFT OUT although it applies to this project, or was anything ADDED that is false of this project or serves only one story or call? A part left out is a finding when its reason is false of the codelines or the kind of project; quote the template's words that were left out as the claim.\n4. Does the generated prompt refer to the TICKET IN SCOPE at all — its key, title, subject, components, requirements or files, or a path that belongs to this run (a run folder, a copy of a codeline) — in any wording, including as an example? This prompt is kept with the codeline and reused for every later story, so any such reference is a finding; quote it as the claim. Only the codeline's own structure, conventions and manifests may be named.\n\nA claim is in scope when it can be checked against a repository — a path, a file, a directory, a dependency, a command, a framework, a convention, a symbol, an ownership statement ('X owns Y'). Open the codelines and check it.\n\nExamples of a finding:\n- it names a test directory, file suffix or framework this codeline does not use\n- it names a dependency, script or command the manifest does not declare\n- it asserts a file, module or symbol exists that does not\n- it attributes behaviour to a file that does not contain it\n- it describes a codeline this project does not have, or confuses two that it does\n- it instructs the agent to run something this codeline cannot run\n\n# A prompt narrowed to one call\n\nThe generated prompt is not written for the ticket in scope now. It is reused for every story and every call this project makes to the agent, and each call's specifics — the role for the turn, the file to write, the command to run, the diff, the story — arrive through the slots (the SCREAMING_SNAKE tokens between double underscores) at run time. Compare it with the template, slot by slot, and report as a finding:\n- a statement true only for one story or one call — this ticket's component, a file or path one story will write, a command aimed at one story's file — written where the template leaves that to a slot. It is a finding even when it is true today: the next story receives it as an instruction.\n- a slot that no longer fits its sentence: in the template the slot completes or sits inside a sentence; in the generated prompt the words around it were moved, completed by hand, or rewritten, so the run-time value would land in the wrong place or be said twice. Quote the template's words around that slot and the generated prompt's.\nFor these findings `claim` quotes the generated text, `checked` names the template slot you compared it with, and `found` says what the template has there. Give no `verification` field: this is read from the two texts, not from the repository.\n\n# Before you call anything absent: SEARCH for it\n\nA path in a generated prompt is frequently ABBREVIATED rather than wrong. Looking at the literal\nstring, finding nothing there, and reporting a false claim is the single most common way this\nreview does damage — it rejects a prompt that was RIGHT about the project, the generator rewrites\nthe whole thing to escape a finding that was not real, and in rewriting it drops the placeholders\nthe pipeline depends on. Three attempts of that aborts the run.\n\nSo, for every path, file, directory or symbol the prompt names:\n\n1. SEARCH the codeline for it by BASENAME before concluding it does not exist. `src/routes/checkouts/[checkoutId].ts`\n and `src/pages/api/checkouts/[checkoutId].ts` are both a SHORTENED form of\n `src/pages/api/v1/ecommerce/checkouts/[checkoutId].ts`, which exists — that is a near-miss path,\n NOT a false claim, and it is not a finding.\n2. An IMPRECISE path that RESOLVES to a real file is not a finding. Neither is a partial path, a\n path missing intermediate directories, or a file named without its directory.\n A NAME MAY BE A DIRECTORY. A file glob such as **/Name* matches files, not directories, and a\n text search matches contents, not folder names — neither can show you a directory. Before you\n call a component, module or folder absent, list its parent directory and look for it there.\n3. Report it ONLY when the thing genuinely does not exist anywhere in the codeline — no file of\n that name, no such dependency in the manifest, no such symbol in the sources.\n\nAbsence at one exact path is not absence from the repository. If you did not search, you do not\nknow, and \"I did not find it where the prompt said\" is not a finding you may report.\n\n# What is NOT in scope\n\nDo not review wording, tone, length, structure or ambition. Do not propose improvements. Do not object that the prompt is too close to, or too far from, the template — how far it may diverge is decided elsewhere, and the placeholder contract is already enforced mechanically. (A prompt narrowed to one call, or a slot that no longer fits its sentence, is not a question of distance — report it as above.)\n\nA prompt that says nothing false and serves every call is SOUND, however plain it is.\n\n# Evidence\n\nReturn every defect you can evidence, and nothing you cannot. For each finding, say what you CHECKED and what you FOUND, naming the file or manifest you read. A finding without a check is an opinion, and an opinion about a prompt is not a defect.\n\nWhere a finding turns on whether a named path or dependency is present, fill in the verification field as well as the prose. The pipeline re-runs that exact check against the repository and DISCARDS any finding the check refutes — so a careless reading costs nothing, and a correct one is confirmed independently of how convincingly you argued it.\n\nIf every claim checks out, return no findings. An empty finding list from a review that RAN is the correct answer for a sound prompt.\n\n# How to return the verdict\n\nEmit the verdict INLINE as the last thing you write, wrapped in these exact tags. Do not write it\nto a file, and do not use a tool to deliver it — a verdict delivered any other way is not read, and\nthe prompt is then installed unreviewed.\n\n<PROMPT_REVIEW>\n{\"falseClaims\": []}\n</PROMPT_REVIEW>\n\n`falseClaims` is a list of the defects you evidenced. Each entry is an object:\n\n- `claim` — the false statement, quoted from the generated prompt\n- `checked` — what you opened or ran to test it, naming the file or manifest\n- `found` — what was actually there\n- `verification` — when the finding turns on a named path or dependency being present or absent,\n an object stating what to re-check, so the pipeline can confirm it independently\n\nTHE ROLES THIS PROJECT MINTED, which is the ground truth for any claim naming an agent:\n\n__ROSTER_BLOCK__\n\nA prompt naming one of these is stating a FACT, not inventing one. Check the list before calling a\nrole false — an unverifiable claim is not a false claim, and rejecting one costs a regeneration and\nteaches the generator nothing.\n\nTHE BLOCK IS THE WHOLE ANSWER. Nothing you write outside it is read — the consumer matches the\ntags and discards everything else. Do not narrate your investigation, do not summarise what you\nchecked, and do not restate the prompt back. Put your evidence INSIDE `checked` and `found` on the\nfinding it belongs to, where it is kept; anywhere else it is thrown away.\n\nMeasured 2026-08-27: reviews averaged 3,366 output tokens to deliver a 13-token verdict. Every one\nof those tokens was billed and 99% of them were discarded unread.\n\nAn empty list is the correct verdict for a sound prompt. Emit the block either way: the block is\nhow a review that RAN is distinguished from one that did not run at all.\n",
33
34
  "placeholders": [
34
35
  "__CODELINE_BLOCK__",
35
36
  "__GENERATED_BODY__",
@@ -21,9 +21,10 @@
21
21
  "already declares - the artefacts this run requires, the seam that produces each, and which",
22
22
  "roster member resolves to which seam - and asked whether the team covers them. No role or kind",
23
23
  "is named in this prompt; the coverage block is derived.",
24
- "v2 (operator, 2026-10-24: the roster minted for AMSD-2974 named every agent after the ticket's subject (<codeline>-toast-detective, toast-accessibility-engineer) and its briefs described that one ticket; the roster and its prompts are cached per codeline and reused for every later story, so the next ticket on these codelines would inherit them). A role is the codeline's, for every story it will ever be given."
24
+ "v2 (operator, 2026-10-24: the roster minted for AMSD-2974 named every agent after the ticket's subject (<codeline>-toast-detective, toast-accessibility-engineer) and its briefs described that one ticket; the roster and its prompts are cached per codeline and reused for every later story, so the next ticket on these codelines would inherit them). A role is the codeline's, for every story it will ever be given.",
25
+ "v3 (operator, 2026-10-24: \"The purpose of re caching was to remove user story references in profiles and prompts\" — zero, not \"zero except marked examples\"): the sets are kept per codeline and reused for every later story; any reference to the ticket in scope, however labelled, is inherited by the next story's agents. Run 14's fresh mint had codeline-level names and briefs of seven \"worked example: for AMSD-2974 …\" sentences each, approved under the earlier wording."
25
26
  ],
26
- "body": "THE ROSTER JUST MINTED FOR THIS PROJECT — review it, both as individual briefs and AS A TEAM.\n\nWHAT THIS RUN NEEDS THE TEAM TO PRODUCE, and who in this roster can produce it:\n__COVERAGE_BLOCK__\n\nA roster can be made entirely of well-formed agents and still be unable to do the work. Where a\nline above says NOBODY, no one in this roster can produce something the run requires — that is a\ndefect of the ROSTER, and it is invisible to reading briefs one at a time, because an absent role\nhas no brief to find fault with. Report it as a blocking finding naming what cannot be produced\nand what kind of role would produce it.\n\nNow review every brief below:\n\n__BRIEF_BLOCK__\n\nTHE CODELINES THESE BRIEFS DESCRIBE, and what each one declares:\n__CODELINE_BLOCK__\n\nTHE WORK THE PROJECT WAS ASKED TO DO:\n__TICKET_BLOCK__\n\n__DOC_SECTION__\n\n__TOOL_LINE__\n\nTHE PIPELINE STAGES THAT RUN ALONGSIDE THIS ROSTER:\n\n__PIPELINE_STAGES__\n\nA ROSTER IS NOT THE WHOLE PIPELINE. A brief may correctly defer work to a stage above rather than\nto a role beside it, and a brief that tells its implementer to leave such work alone is describing\nthe pipeline accurately, not inventing cover. Check any such claim against the list above before\ncalling it false: the absence of a matching ROLE is not evidence when the brief named a STAGE.\n\nWhere the list above does not contain the stage a brief names, that IS a finding, and a blocking\none — the implementer has been told to rely on something that will never run.\n\nReturn every defect you can evidence, and nothing you cannot.\n\nWhere a finding turns on whether a named dependency or path is present in a codeline, fill in\nthe verification field as well as writing the prose. The pipeline re-runs that exact check\nagainst the repository and DISCARDS any finding the check refutes — so a careless reading\ncosts nothing, and a correct one is confirmed independently of how convincingly you argued it.\n\nA ROLE OUTLIVES THE TICKET. This roster is kept with its codelines and reused for every story they are given later. A role minted for\nthe work in scope now rather than for the codeline — named after the current story's subject, component or ticket, or briefed so that\nit would mislead an implementer on a different story of the same codeline — is a blocking finding: name the role, quote the words,\nand say what the codeline-level role would be. A brief that names the current ticket's subject or files only as marked examples of the\ncodeline's conventions is not this finding.\n",
27
+ "body": "THE ROSTER JUST MINTED FOR THIS PROJECT — review it, both as individual briefs and AS A TEAM.\n\nWHAT THIS RUN NEEDS THE TEAM TO PRODUCE, and who in this roster can produce it:\n__COVERAGE_BLOCK__\n\nA roster can be made entirely of well-formed agents and still be unable to do the work. Where a\nline above says NOBODY, no one in this roster can produce something the run requires — that is a\ndefect of the ROSTER, and it is invisible to reading briefs one at a time, because an absent role\nhas no brief to find fault with. Report it as a blocking finding naming what cannot be produced\nand what kind of role would produce it.\n\nNow review every brief below:\n\n__BRIEF_BLOCK__\n\nTHE CODELINES THESE BRIEFS DESCRIBE, and what each one declares:\n__CODELINE_BLOCK__\n\nTHE WORK THE PROJECT WAS ASKED TO DO:\n__TICKET_BLOCK__\n\n__DOC_SECTION__\n\n__TOOL_LINE__\n\nTHE PIPELINE STAGES THAT RUN ALONGSIDE THIS ROSTER:\n\n__PIPELINE_STAGES__\n\nA ROSTER IS NOT THE WHOLE PIPELINE. A brief may correctly defer work to a stage above rather than\nto a role beside it, and a brief that tells its implementer to leave such work alone is describing\nthe pipeline accurately, not inventing cover. Check any such claim against the list above before\ncalling it false: the absence of a matching ROLE is not evidence when the brief named a STAGE.\n\nWhere the list above does not contain the stage a brief names, that IS a finding, and a blocking\none — the implementer has been told to rely on something that will never run.\n\nReturn every defect you can evidence, and nothing you cannot.\n\nWhere a finding turns on whether a named dependency or path is present in a codeline, fill in\nthe verification field as well as writing the prose. The pipeline re-runs that exact check\nagainst the repository and DISCARDS any finding the check refutes — so a careless reading\ncosts nothing, and a correct one is confirmed independently of how convincingly you argued it.\n\nA ROLE OUTLIVES THE TICKET — ZERO REFERENCES TO THE TICKET IN SCOPE. This is kept with the codeline and reused for every later story on it, so it refers to no\nticket at all — not the key, title, subject, components, requirements or files of the story in scope, not even as an example. Examples\ncome from the codeline itself: its structure, conventions, manifests and how to verify there. Anything that could only have been written\nwith this ticket in view is a defect.\nA role or brief that refers to the current ticket — by key, subject, component, requirement or file, in any wording, including \"as an\nexample\" — is a BLOCKING finding: name the role, quote the words, and say what the codeline-level wording would be.",
27
28
  "description": "Review every brief in a freshly minted roster and report evidenced defects.",
28
29
  "id": "roster-review",
29
30
  "placeholders": [
@@ -35,7 +36,7 @@
35
36
  "__TICKET_BLOCK__",
36
37
  "__TOOL_LINE__"
37
38
  ],
38
- "version": 2,
39
+ "version": 3,
39
40
  "seams": [
40
41
  "roster-review"
41
42
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "set-fidelity-check",
3
- "version": 2,
3
+ "version": 3,
4
4
  "seams": [],
5
5
  "layer": "engine",
6
6
  "description": "A judge reads a codeline's minted roster and reusable prompts against the ticket they were minted for and reports what is geared to that ticket rather than to the codeline.",
@@ -12,7 +12,8 @@
12
12
  ],
13
13
  "$why": [
14
14
  "v1 (operator, 2026-10-24: \"We need some post pause 1 checkpoints to ensure no contamination in codeline level prompts and profiles\"): run 13's roster named every agent after the ticket's subject and ~half its reusable prompts described that one ticket; the sets are cached per codeline and reused for every later story. Judged by an agent, not a word list; run by the pre-launch of the leg after pause 1 and reported at pause 1.",
15
- "v2 (live, 2026-10-24): a codeline's whole prompt set in one call was 908 KB and the judge answered about its own context limit; the prompts are judged one by one by the pipeline's own reviewer as they are built (each copy records its judges), so this judge reads the roster only."
15
+ "v2 (live, 2026-10-24): a codeline's whole prompt set in one call was 908 KB and the judge answered about its own context limit; the prompts are judged one by one by the pipeline's own reviewer as they are built (each copy records its judges), so this judge reads the roster only.",
16
+ "v3 (operator, 2026-10-24: \"The purpose of re caching was to remove user story references in profiles and prompts\" — zero, not \"zero except marked examples\"): the sets are kept per codeline and reused for every later story; any reference to the ticket in scope, however labelled, is inherited by the next story's agents. Run 14's fresh mint had codeline-level names and briefs of seven \"worked example: for AMSD-2974 …\" sentences each, approved under the earlier wording."
16
17
  ],
17
- "body": "You judge the agent roster a pipeline minted for ONE codeline and will REUSE for every later story on it: its agents' names and\nbriefs. It was minted while this ticket was in scope:\n\nTicket __TICKET_KEY__: __TICKET_TITLE__\nCodeline: __CODELINE__\n\nTHE ROSTER (each agent: name, kind, brief):\n__ROSTER__\n\nReport every agent that is geared to THIS ticket rather than to the codeline: named after the ticket's subject, component or key, or\nbriefed so that it takes this ticket's subject, components, files or requirements as its subject or as standing instruction — an\nimplementer on a DIFFERENT story of this codeline would be misled. A mention of the ticket's subject or files only as a marked example\nof the codeline's conventions is not geared. Quote the words. Report nothing you cannot quote. Answer in the structured form you are\ngiven; `geared` is empty when nothing is geared."
18
+ "body": "You judge the agent roster a pipeline minted for ONE codeline and will REUSE for every later story on it: its agents' names and\nbriefs. It was minted while this ticket was in scope:\n\nTicket __TICKET_KEY__: __TICKET_TITLE__\nCodeline: __CODELINE__\n\nTHE ROSTER (each agent: name, kind, brief):\n__ROSTER__\n\nReport every agent whose name or brief refers to THIS ticket in any way — its key, title, subject, components, requirements or files —\nincluding a mention labelled as an example. The roster is reused for every later story on this codeline, so no reference to this ticket\nmay be in it; a codeline's own conventions, structure and manifests are the only examples allowed. Quote the words. Report nothing you\ncannot quote. Answer in the structured form you are\ngiven; `geared` is empty when nothing is geared."
18
19
  }
@@ -202,10 +202,9 @@ function templateLayerOf(id) {
202
202
  try {
203
203
  const own = JSON.parse(fs.readFileSync(path.join(templatesDir(), `${id}.json`), 'utf8'));
204
204
  if (own && own.layer === 'project') return 'project';
205
- // A TEMPLATE THAT DECLARES ITSELF BOOTSTRAP runs inside the mint, before this project's prompt layer exists (survey-review,
206
- // test-ownership: read by the estate survey and the roster mint), and renders from the template as the registry's bootstrap
207
- // seams do — run 14, 2026-10-24: a project never minted could not be minted ("project-authority prompt missing")
208
- if (own && own.layer === 'bootstrap') return 'bootstrap';
205
+ // Any other layer a template declares (bootstrap: survey-review, test-ownership — read INSIDE the mint before this project's prompt
206
+ // layer exists) leaves the decision to the registry below, which renders them from the template. Run 14, 2026-10-24: the two had
207
+ // declared `layer: project` and the routing above, added that morning, sent the mint to project copies that cannot exist yet.
209
208
  } catch { /* no template of that id here: the registry decides, as before */ }
210
209
  const registryPath = path.join(__dirname, '..', '..', 'agents', 'invocation-profiles.json');
211
210
  let reg;
@@ -267,6 +267,8 @@ function resolveRunner(runnerName, { projectConfigDir, defaultsFile } = {}) {
267
267
  toolNames: declared.toolNames && typeof declared.toolNames === 'object' ? declared.toolNames : {},
268
268
  // How this runner carries an agent's standing instructions, separate from the task.
269
269
  siChannel: declared.siChannel && typeof declared.siChannel === 'object' ? declared.siChannel : null,
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
+ hostedToolPlugins: Array.isArray(declared.hostedToolPlugins) ? declared.hostedToolPlugins : [],
270
272
  };
271
273
  }
272
274
  /** Every runner the active stack declares, by name. */
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // THE PIPELINE'S TOOLS AS CLAUDE CODE TOOLS — an MCP server over the hosted tool plugins (operator, 2026-10-24: "What about tools?"
4
+ // / "not using the proper interface is not permitted"). Until now a plain-Claude agent reached codegraph, the search and the
5
+ // package-symbol resolver only as shell scripts through Bash, named by variables (config/pipeline-tool-commands.json): a seam
6
+ // granted read-only (no Bash) could not call them at all, and in plan mode a Bash call outside Claude's read-only set is denied.
7
+ // Here the SAME plugin modules the codemie-web runner hosts (its runner declaration's hostedToolPlugins — plugins/*-plugin.js,
8
+ // each exporting tools[] of {name, description, permission, definition.inputSchema, execute}) are served over MCP stdio, so an
9
+ // agent is granted them by name (--tools mcp__<server>__<tool>), and a tool whose permission is 'safe' is annotated read-only,
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
+ //
12
+ // node pipeline-tools-mcp.js <runner-name> serve the runner's hostedToolPlugins (declaration read from the active set)
13
+ // node pipeline-tools-mcp.js --modules a.js,b.js serve these plugin modules (tests)
14
+ //
15
+ // Protocol: JSON-RPC 2.0, one message per line, MCP 2024-11-05 — initialize, tools/list, tools/call; nothing else.
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const readline = require('readline');
19
+
20
+ const ORCH = path.join(__dirname, '..', '..');
21
+
22
+ /** The plugin modules a runner hosts, from the active set's declaration (lib/llm-settings-resolve.js). */
23
+ function hostedModulesFor(runnerName) {
24
+ // eslint-disable-next-line global-require
25
+ const { resolveRunner } = require('./llm-settings-resolve.js');
26
+ const runner = resolveRunner(runnerName, { projectConfigDir: process.env.EPAM_PROJECT_CONFIG_DIR || undefined });
27
+ return ((runner && runner.hostedToolPlugins) || []).map((m) => path.resolve(ORCH, m));
28
+ }
29
+
30
+ /** Every tool the modules export, as MCP lists them: safe → read-only; the plugin's own input schema and description. */
31
+ function toolsOf(modules) {
32
+ const out = [];
33
+ for (const m of modules) {
34
+ // eslint-disable-next-line global-require, import/no-dynamic-require
35
+ const mod = require(m);
36
+ for (const t of ((mod && mod.tools) || [])) {
37
+ const name = t.name || (t.definition && t.definition.name);
38
+ if (!name) continue;
39
+ const def = t.definition || {};
40
+ out.push({
41
+ name,
42
+ description: t.description || def.description || '',
43
+ inputSchema: def.inputSchema || def.input_schema || { type: 'object', properties: {} },
44
+ annotations: { readOnlyHint: t.permission === 'safe', destructiveHint: t.permission !== 'safe' },
45
+ execute: t.execute,
46
+ });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ /** A plugin result as MCP content: the plugin returns {content, isError} (see codegraph-plugin.js). */
53
+ function asMcpResult(r) {
54
+ if (r && typeof r === 'object' && 'content' in r) {
55
+ const text = typeof r.content === 'string' ? r.content : JSON.stringify(r.content);
56
+ return { content: [{ type: 'text', text }], isError: !!r.isError };
57
+ }
58
+ return { content: [{ type: 'text', text: typeof r === 'string' ? r : JSON.stringify(r == null ? {} : r) }], isError: false };
59
+ }
60
+
61
+ function serve(modules, io = { input: process.stdin, write: (s) => process.stdout.write(s) }) {
62
+ const tools = toolsOf(modules);
63
+ const byName = new Map(tools.map((t) => [t.name, t]));
64
+ const send = (o) => io.write(`${JSON.stringify(o)}\n`);
65
+ const rl = readline.createInterface({ input: io.input });
66
+ rl.on('line', async (line) => {
67
+ let m; try { m = JSON.parse(line); } catch { return; }
68
+ if (m.method === 'initialize') {
69
+ return send({ jsonrpc: '2.0', id: m.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'pipeline', version: '1' } } });
70
+ }
71
+ if (m.method === 'notifications/initialized' || m.method === 'ping') return m.id !== undefined ? send({ jsonrpc: '2.0', id: m.id, result: {} }) : undefined;
72
+ if (m.method === 'tools/list') {
73
+ return send({ jsonrpc: '2.0', id: m.id, result: { tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, annotations })) } });
74
+ }
75
+ if (m.method === 'tools/call') {
76
+ const t = byName.get(m.params && m.params.name);
77
+ if (!t) return send({ jsonrpc: '2.0', id: m.id, error: { code: -32602, message: `unknown tool: ${m.params && m.params.name}` } });
78
+ try {
79
+ const r = await t.execute((m.params && m.params.arguments) || {});
80
+ return send({ jsonrpc: '2.0', id: m.id, result: asMcpResult(r) });
81
+ } catch (e) {
82
+ return send({ jsonrpc: '2.0', id: m.id, result: { content: [{ type: 'text', text: `tool ${t.name} failed: ${e && e.message}` }], isError: true } });
83
+ }
84
+ }
85
+ if (m.id !== undefined) send({ jsonrpc: '2.0', id: m.id, error: { code: -32601, message: `method not found: ${m.method}` } });
86
+ });
87
+ return { tools: tools.map((t) => t.name) };
88
+ }
89
+
90
+ /** The --mcp-config document the hub hands the CLI: this server, for this runner. */
91
+ function mcpConfig(runnerName, nodeBin = process.execPath) {
92
+ return { mcpServers: { pipeline: { command: nodeBin, args: [__filename, runnerName], env: { EPAM_PROJECT_CONFIG_DIR: process.env.EPAM_PROJECT_CONFIG_DIR || '' } } } };
93
+ }
94
+
95
+ if (require.main === module) {
96
+ 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); }
99
+ serve(modules);
100
+ }
101
+
102
+ module.exports = { hostedModulesFor, toolsOf, serve, mcpConfig, asMcpResult };
@@ -1433,17 +1433,32 @@ function clearCompletionMarker({ outDir, codeline } = {}) {
1433
1433
  * 2026-10-24): which kind of project this is — brownfield (existing code) or greenfield (built from nothing) — every codeline in
1434
1434
  * scope and whether its code exists yet, and the tickets, so it knows the kind of work (never the subject of the prompt).
1435
1435
  */
1436
+ /**
1437
+ * THE ROLES AS THE PROMPT GENERATOR IS TOLD THEM: name and kind, nothing else. A role's rationale is the mint's reason for
1438
+ * proposing it THIS run — written against the ticket in scope ("AMSD-2974 requires toast accessibility fixes in …", live
1439
+ * 2026-10-24, every one of six roles) — and the generator wrote it into prompts kept with the codeline. The rationale is run
1440
+ * provenance; it stays in the mint's own record (logs/agent-mint.json) and never enters codeline-level context.
1441
+ */
1442
+ function mintedRolesBlock(roles) {
1443
+ const list = Array.isArray(roles) ? roles.filter((m) => m && m.name) : [];
1444
+ return list.length ? list.map((m) => `- ${m.name} [${m.kind || ''}]`).join('\n') : '(none minted this run)';
1445
+ }
1446
+
1436
1447
  function useCaseContext({ projectConfigDir, stories, codelines, env = process.env }) {
1437
1448
  const kind = env.EPAM_BROWNFIELD === '1'
1438
1449
  ? 'brownfield — the work changes code that already exists in each codeline'
1439
1450
  : 'greenfield — the work builds the codeline from nothing; there is no existing code to read or preserve';
1440
1451
  const cl = (codelines || []).map((c) => `- ${c.name || c.codeline}: ${c.path && fs.existsSync(c.path) ? `code at ${c.path}` : 'no code yet'}`);
1452
+ // THE TICKETS ARE NOT NAMED. This line read "Tickets in scope: AMSD-2974: Toast Notification …", and the generator wrote
1453
+ // the ticket into prompts kept with the codeline (live 2026-10-24, two drafts of two). The use case is the kind of project
1454
+ // and its codelines; how many tickets reach them is all a reusable prompt may be told.
1441
1455
  return [`Project: ${path.basename(String(projectConfigDir || ''))}`, `Kind of project: ${kind}`,
1442
1456
  ...(cl.length ? ['Codelines in scope:', ...cl] : []),
1443
- `Tickets in scope: ${(stories || []).map((t) => `${t.jiraKey || t.id}: ${t.title || ''}`).join(' | ')}`].join('\n');
1457
+ `Tickets reaching these codelines: ${(stories || []).length} (what they ask for is given to the agent per call, never here)`].join('\n');
1444
1458
  }
1445
1459
 
1446
1460
  module.exports = {
1461
+ mintedRolesBlock,
1447
1462
  useCaseContext, buildProjectPrompts, healContext, renderGeneratorPrompt, provisioningList, rolesIdentity, GEN_BODY_SLOT,
1448
1463
  PROJECT_PROMPT_GENERATOR_ID, answerBudgetChars, promptParts,
1449
1464
  writeCompletionMarker, clearCompletionMarker, promptInputsDigest, codelinePromptsComplete,
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+ // THE ROSTER REVIEW-AND-CORRECTION CYCLE, as the mint step runs it: the reviewer judges the minted roles as briefs and as a team; a
3
+ // blocking finding that names a role indicts it; the indicted are cleared and re-minted with the findings as corrective context, the
4
+ // retained kept; until a cycle has no blocking finding or the cycles run out. Extracted from mint-agents-step.js (2026-10-24) so the
5
+ // cycle is one unit the live proof can execute on a sample mint — before that, the only way to run it was the whole Agents stage.
6
+ // Behaviour unchanged: the step's own log lines, files (agent-mint-cycle<n>.json) and give-up rules.
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * @param {object} o
12
+ * @param {object} o.spec spec-mode-runner (reviewRoster, mintProjectAgents)
13
+ * @param {object} o.rosterLib lib/agent-roster.js (partitionRosterFindings, clearProjectRoster)
14
+ * @param {Array} o.mintedDetail the roles the mint just produced (this run)
15
+ * @param {number} o.maxCycles review cycles; the last one's findings are surfaced, not corrected
16
+ * @param {function} o.log one line of the step's log
17
+ * @returns {Promise<{mintedDetail: Array, mintedNames: string[], review: object}>} review.cycles = cycles run
18
+ */
19
+ async function reviewAndCorrectRoster({
20
+ spec, rosterLib, promptExec, stories, docs, deps, codelines, toolGrant, profilesPath, agentsDir, logDir, repoPath,
21
+ projectConfigDir, mintedDetail, maxCycles, log,
22
+ }) {
23
+ let _mintedDetail = Array.isArray(mintedDetail) ? mintedDetail : [];
24
+ let _mintedNames = _mintedDetail.map((m) => m.name);
25
+
26
+ const runRosterReview = async () => {
27
+ if (!_mintedDetail.length) return { verdict: 'nothing_to_review', findings: [], reviewed: 0 };
28
+ process.env.EPAM_AGENT_NAME = 'roster-review';
29
+ try {
30
+ const liveProfiles = JSON.parse(fs.readFileSync(profilesPath, 'utf8'));
31
+ let projectProfiles = {};
32
+ try {
33
+ const _pp = path.join(projectConfigDir || '', 'agent-profiles.json');
34
+ projectProfiles = JSON.parse(fs.readFileSync(_pp, 'utf8'));
35
+ } catch { /* a project with no minted profiles yet has none to add */ }
36
+ return await spec.reviewRoster({
37
+ promptExec, minted: _mintedDetail, profiles: liveProfiles, projectProfiles,
38
+ codelines, tickets: stories, referencedDocs: docs,
39
+ logDir, repoPath, toolGrant,
40
+ });
41
+ } catch (err) {
42
+ log(`roster review FAILED: ${err && err.message}`);
43
+ return { verdict: 'review_failed', findings: [], reviewed: 0, error: String(err && err.message) };
44
+ }
45
+ };
46
+
47
+ const reportReview = (rv, n) => {
48
+ const blocking = rv.findings.filter((f) => f && f.severity === 'blocking');
49
+ log(`roster review (cycle ${n}): ${rv.verdict} — ${rv.findings.length} finding(s), ${blocking.length} blocking`);
50
+ for (const f of rv.findings) {
51
+ log(` [${f.severity}] ${f.agent}: ${f.claim}`);
52
+ log(` checked: ${f.checked}`);
53
+ log(` found: ${f.found}`);
54
+ if (f.remedy) log(` remedy: ${f.remedy}`);
55
+ }
56
+ };
57
+
58
+ let cycle = 1;
59
+ let review = { verdict: 'not_run', findings: [], reviewed: 0 };
60
+
61
+ while (_mintedDetail.length && cycle <= maxCycles) {
62
+ review = await runRosterReview();
63
+ const blocking = review.findings.filter((f) => f && f.severity === 'blocking');
64
+ reportReview(review, cycle);
65
+ if (!blocking.length || cycle === maxCycles) break;
66
+
67
+ const { indicted: _indicted, retained: _retained, gaps: _gapFindings, superfluous: _superfluous } =
68
+ rosterLib.partitionRosterFindings(blocking, _mintedDetail);
69
+
70
+ log(`cycle ${cycle}: ${blocking.length} blocking finding(s) — ${_indicted.length} agent(s) indicted, ${_retained.length} retained`
71
+ + `${_gapFindings ? `, ${_gapFindings} finding(s) name no agent (roster gaps)` : ''}`
72
+ + `${_superfluous && _superfluous.length ? `, ${_superfluous.length} superfluous (no replacement needed)` : ''}`);
73
+
74
+ if (!_indicted.length && !_gapFindings) {
75
+ log(' no blocking finding names an agent in this roster — nothing to correct; surfacing the findings instead of re-minting');
76
+ break;
77
+ }
78
+
79
+ const cleared = rosterLib.clearProjectRoster(agentsDir, profilesPath, _indicted);
80
+ for (const c of cleared) {
81
+ const isSuper = _superfluous && _superfluous.includes(c);
82
+ log(` − ${c} (${isSuper ? 'superfluous — no replacement' : 'indicted, being replaced'})`);
83
+ }
84
+ for (const r of _retained) log(` = ${r.name} (passed review, kept)`);
85
+
86
+ const _replacedForMint = _mintedDetail.filter(
87
+ (m) => _indicted.includes(m.name) && !(_superfluous && _superfluous.includes(m.name)));
88
+ const _superfluousForMint = _mintedDetail.filter(
89
+ (m) => _superfluous && _superfluous.includes(m.name));
90
+
91
+ const remint = await spec.mintProjectAgents({
92
+ promptExec, tickets: stories, referencedDocs: docs, declaredDependencies: deps,
93
+ codelines, toolGrant, profilesPath, agentsDir,
94
+ logDir, repoPath, correctiveFindings: blocking,
95
+ retainedAgents: _retained,
96
+ replacedAgents: _replacedForMint,
97
+ superfluousAgents: _superfluousForMint,
98
+ });
99
+ _mintedDetail = [..._retained, ...remint.minted];
100
+ _mintedNames = _mintedDetail.map((m) => m.name);
101
+ fs.writeFileSync(path.join(logDir, `agent-mint-cycle${cycle + 1}.json`), JSON.stringify(remint, null, 2));
102
+ for (const m of remint.minted) {
103
+ const tag = m.kind === 'investigator' ? `investigator:${m.codeline || '(no codeline!)'}` : 'implementer';
104
+ log(` + [${tag}] ${m.name} — ${m.rationale}`);
105
+ }
106
+ cycle += 1;
107
+ }
108
+
109
+ review.cycles = cycle;
110
+ return { mintedDetail: _mintedDetail, mintedNames: _mintedNames, review };
111
+ }
112
+
113
+ module.exports = { reviewAndCorrectRoster };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * roster-seams.js — the two seams that derive a project's roster, as ONE piece of code.
3
+ *
4
+ * `produce` asks the roster specialiser for its delta and lands it where lib/project-roster.js
5
+ * composes it; `review` asks project-roster-review to falsify the composed roster. They were
6
+ * closures inside mint-agents-step.js; the seam harness (tools/seam-harness.js) needs to run
7
+ * exactly them on a recorded run's inputs, so they live here and the step calls them. Nothing
8
+ * about either seam changed in the move — the text below is the step's, verbatim.
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * rosterSeams(ctx) -> { produce, review }
17
+ * ctx: { spec, promptExec, projectConfigDir, LOG_DIR, AGENTS_DIR, REPO_PATH, codelines, stories,
18
+ * mintedDetail, survey, toolGrant }
19
+ */
20
+ /**
21
+ * THE CODELINES, WITH WHAT THE TICKETS DECLARE FOR THEM. On a brownfield tree the specialiser
22
+ * reads the code; on a greenfield one there is no code, and the only source of a path is the
23
+ * tickets' own declaration (technicalNotes.files and the paths their text names). Run
24
+ * 20260916T234139Z: handed ticket TITLES only, the specialiser wrote src/client.ts for a client the
25
+ * PRD declares at src/skyscanner/client.ts; the reviewer found the contradiction on every attempt.
26
+ * The declared paths are DATA the tickets already carry (lib/agent-roster.js declaredPathsOf, the
27
+ * same derivation the mint's brief check uses) — both seams receive them in the same line.
28
+ */
29
+ function codelineContext(codelines, stories) {
30
+ const declared = require('./agent-roster.js').declaredPathsOf(stories);
31
+ const suffix = declared.length ? ` — paths the tickets declare: ${declared.join(', ')}` : '';
32
+ return (Array.isArray(codelines) ? codelines : [])
33
+ .map((c) => `- ${c.name} (${c.path})${c.dependencies && c.dependencies.length ? ` deps: ${c.dependencies.join(', ')}` : ''}${suffix}`)
34
+ .join('\n');
35
+ }
36
+
37
+ function rosterSeams({ spec, promptExec, projectConfigDir, LOG_DIR, AGENTS_DIR, REPO_PATH, codelines, stories, prd, mintedDetail, survey, toolGrant }) {
38
+ const { seamInvocationEnv } = require('./seam-invocation.js');
39
+ // eslint-disable-next-line global-require
40
+ const { renderEngineTemplate } = require('./engine-prompt.js');
41
+ const { refusalBlock } = require('./refusal-block.js');
42
+ const renderSpecialisation = (vals) => renderEngineTemplate('roster-specialisation', vals);
43
+
44
+ // PRD configuration block — same derivation as spec-mode-runner.js prdConfigurationBlock.
45
+ // Strip $-prefixed comment keys; render empty string when the PRD declares no configuration.
46
+ const prdConfigurationBlock = (() => {
47
+ if (!prd || !prd.configuration) return '';
48
+ const cfg = Object.fromEntries(
49
+ Object.entries(prd.configuration).filter(([k]) => !k.startsWith('$')),
50
+ );
51
+ if (!Object.keys(cfg).length) return '';
52
+ return renderEngineTemplate('prd-configuration-block', {
53
+ __PRD_CONFIGURATION_JSON__: JSON.stringify(cfg, null, 2),
54
+ });
55
+ })();
56
+
57
+ // THE AGENT WRITES THE FILE. The pipeline hands it the canonical copy and a destination,
58
+ // then judges the artefact — it does not compose personas itself, because deciding what an
59
+ // agent must know about a codeline is judgement, not substitution.
60
+ const produce = async ({ canonicalCopyPath, outPath, refusal, attempt }) => {
61
+ process.env.EPAM_AGENT_NAME = 'roster-specialiser';
62
+ // The SAME context the prompt builder is given, computed the same way — a derivation that
63
+ // sees different facts than its sibling stage is two projects, not one.
64
+ const prompt = renderSpecialisation({
65
+ // THE VOCABULARY THE ANSWER IS JUDGED AGAINST. Read from the registry rather than written
66
+ // out here: a hand-kept second copy of a closed list only ever drifts from the list.
67
+ __DECLARED_SEAMS__: (() => {
68
+ try {
69
+ const _reg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'agents', 'invocation-profiles.json'), 'utf8'));
70
+ return Object.keys(_reg.profiles || {}).sort().map((s) => `- ${s}`).join('\n');
71
+ } catch { return ''; }
72
+ })(),
73
+ // THE PERSONAS THEMSELVES, NOT WHERE THEY LIE. Pointed at a folder under the install's logs,
74
+ // the agent was handed a local path (operator, 2026-10-01: relative paths only) that a CodeMie
75
+ // web plugin, rooted at the codelines, cannot even reach. The run's own copy, every entry.
76
+ __CANONICAL_PERSONAS__: (() => {
77
+ const c = JSON.parse(fs.readFileSync(canonicalCopyPath, 'utf8'));
78
+ return Object.keys(c).filter((n) => typeof c[n] === 'string' && c[n].trim())
79
+ .map((n) => `--- AGENT: ${n}\n${c[n].trim()}`).join('\n\n');
80
+ })(),
81
+ // the same use case the prompt generator is told (lib/project-prompt-builder.js useCaseContext): kind of project and its
82
+ // codelines, not the tickets — a persona is kept with the codeline (live 2026-10-24: personas carried the ticket's subject)
83
+ __PROJECT_CONTEXT__: [
84
+ require('./project-prompt-builder.js').useCaseContext({ projectConfigDir, stories, codelines }),
85
+ mintedDetail.length
86
+ ? `Agents this project minted: ${mintedDetail.map((mm) => `${mm.name} [${mm.kind}]`).join(', ')}`
87
+ : 'Agents this project minted: (none this run)',
88
+ ].join('\n'),
89
+ // WHAT THE SURVEY OBSERVED, not merely what the PRD declares. Everything else handed to
90
+ // this seam — paths, dependency lists, ticket titles — is a claim about the code rather
91
+ // than an observation of it, and the specialiser writes the project-facts paragraph that
92
+ // every persona inherits. Handed over WITH its caveat; see surveyLeadsBlock.
93
+ __SURVEY_LEADS__: spec.surveyTeamShapeBlock(survey),
94
+ __CODELINE_CONTEXT__: codelineContext(codelines, stories),
95
+ __PRD_CONFIGURATION_BLOCK__: prdConfigurationBlock,
96
+ // __STACK__ is deliberately ABSENT. It is a stack-fact key, and engine-prompt.js injects
97
+ // the ones a template declares — but only when the caller has not supplied them. Passing
98
+ // an empty string here would win, and starve the agent of the codeline's real facts.
99
+ // RENDERED FROM THE PROMPT LAYER, not written here. See lib/refusal-block.js: this text
100
+ // existed at three call sites in three wordings, none of them reviewable as a prompt.
101
+ __PREVIOUS_REFUSAL__: refusalBlock(refusal, 'roster'),
102
+ });
103
+ // spec.runClaude, like every other seam in this file. promptExec is the RUNNER handed to
104
+ // it, not something to call — invoking it directly threw "promptExec is not a function".
105
+ // I invented a call shape instead of matching the one already here, which is the third bug
106
+ // in this stage from that same habit.
107
+ //
108
+ // AGENTS_DIR, not LOG_DIR: seamInvocationEnv reads the invocation registry from the
109
+ // directory it is given, and handed the log folder it found none and resolved no ladder.
110
+ const seamEnv = {
111
+ // ATTEMPT N RUNS RUNG N-1. buildProjectRoster has always handed `attempt` to this producer
112
+ // and it was destructured away, so all three attempts re-ran the same model: the refusal
113
+ // was fed back to the one model that had just produced it. Fifth site of that same shape.
114
+ ...seamInvocationEnv('roster-specialiser', AGENTS_DIR,
115
+ { rung: Math.max(0, (Number(attempt) || 1) - 1) }),
116
+ EPAM_AGENT_NAME: 'roster-specialiser',
117
+ };
118
+ // The tool CHANNEL and the tool LIST travel together: granting one without the other gives
119
+ // an agent that quietly has nothing, and this one's whole job is to read the codeline and
120
+ // write a file.
121
+ if (seamEnv.EPAM_ALLOWED_TOOLS) seamEnv.AI_GATE_ALLOW_TOOLS = '1';
122
+ const ask = (text, log) => spec.runClaude(promptExec, text, path.join(LOG_DIR, log), seamEnv, { costAgent: 'roster-specialiser' });
123
+ // THE ROSTER IN TURNS ON A SET THAT CUTS LONG ANSWERS. A runner that declares `limits` returns at
124
+ // most one turn's worth of text and reports the cut answer as a success — live 2026-10-01 (AMSD-2974,
125
+ // codemie-web) the whole delta in one answer was cut three times and the mint refused the run. So one
126
+ // turn names the agents and one turn per agent writes its entry; the engine merges them. A set with
127
+ // no declared budget is asked once, as before.
128
+ const budget = require('./project-prompt-builder.js').answerBudgetChars();
129
+ let reply;
130
+ if (Number.isFinite(budget)) {
131
+ const { extractDeltaJson } = require('./project-roster.js');
132
+ const namesReply = String(await ask(prompt + renderEngineTemplate('roster-specialisation-plan', {}), 'roster-specialiser.log') || '');
133
+ let names = [];
134
+ try {
135
+ const m = namesReply.match(/\{[\s\S]*\}/);
136
+ const parsed = m ? JSON.parse(m[0]) : null;
137
+ names = Array.isArray(parsed && parsed.agents) ? parsed.agents.filter((n) => typeof n === 'string' && n.trim()) : [];
138
+ } catch { names = []; }
139
+ const agents = {};
140
+ for (const name of names) {
141
+ const part = String(await ask(prompt + renderEngineTemplate('roster-specialisation-one',
142
+ { __AGENT__: name, __BUDGET_CHARS__: String(budget) }), `roster-specialiser-${name}.log`) || '');
143
+ const d = extractDeltaJson(part);
144
+ if (d && d.agents && d.agents[name]) agents[name] = d.agents[name];
145
+ }
146
+ // WHOLE OR NOTHING: an agent the specialiser named but whose entry did not parse leaves no roster,
147
+ // so the attempt is refused and retried — never a roster quietly missing an agent it chose.
148
+ reply = names.length && Object.keys(agents).length === names.length ? JSON.stringify({ agents }) : '';
149
+ } else {
150
+ reply = await ask(prompt, 'roster-specialiser.log');
151
+ }
152
+ // THE ENGINE PERFORMS THE WRITE. The delta is the seam's ANSWER — the same delivery the
153
+ // prompt-builder seam uses — so the agent needs no write tool, and a rehearsal that answers
154
+ // from a recording or a contract stand-in delivers exactly what a live model does. Delivered
155
+ // by tool call, the seam was unrehearsable (a stand-in cannot write a file) and, live, three
156
+ // paid attempts on 2026-09-13 ended with "the agent wrote no roster". A file the agent wrote
157
+ // itself (the Claude Code arm may) is still accepted; the answer is written only when it did
158
+ // not. lib/project-roster.js composes the roster from whatever lands at outPath.
159
+ if (!fs.existsSync(outPath)) {
160
+ const { extractDeltaJson } = require('./project-roster.js');
161
+ const delta = extractDeltaJson(String(reply || ''));
162
+ if (delta) fs.writeFileSync(outPath, JSON.stringify(delta, null, 2));
163
+ }
164
+ };
165
+
166
+ // REVIEWED AGAINST BOTH. With only the roster a reviewer can judge plausibility; falsifying
167
+ // "is this ancestor close" and "was inherited structure quietly changed" needs the source.
168
+ const review = async ({ rosterPath, canonicalPath: copyPath }) => {
169
+ // The SEAM's own name. This announced 'roster-review' — a different seam, with a different
170
+ // template and a different job (certifying newly minted agents). The two would have
171
+ // resolved different ladders and filed their cost and KB under the wrong agent, which is
172
+ // the drift this file already warns about where roster-review is invoked properly.
173
+ process.env.EPAM_AGENT_NAME = 'project-roster-review';
174
+ try {
175
+ return await spec.reviewProjectRoster({
176
+ promptExec, rosterPath, canonicalPath: copyPath,
177
+ codelines, tickets: stories, logDir: LOG_DIR, repoPath: REPO_PATH, toolGrant,
178
+ });
179
+ } catch (err) {
180
+ // A review that cannot run must not read as "no defects".
181
+ process.stderr.write(`[mint-step] roster review FAILED: ${err && err.message}\n`);
182
+ return { verdict: 'review_failed', reason: String(err && err.message) };
183
+ }
184
+ };
185
+
186
+ return { produce, review };
187
+ }
188
+
189
+ module.exports = { rosterSeams, codelineContext };
@@ -899,122 +899,17 @@ if (require.main !== module) return;
899
899
  // passed adversarial review was as likely to come back subtly wrong as to come back the
900
900
  // same, and the reviewer's own capped budget went on re-checking settled work. Only the
901
901
  // agents a blocking finding NAMES are replaced.
902
- const runRosterReview = async () => {
903
- // Same reason as reviewRoster: nothing was examined, so nothing may be certified.
904
- if (!_mintedDetail.length) return { verdict: 'nothing_to_review', findings: [], reviewed: 0 };
905
- process.env.EPAM_AGENT_NAME = 'roster-review';
906
- try {
907
- const liveProfiles = JSON.parse(fs.readFileSync(PROFILES_PATH, 'utf8'));
908
- // THE PROJECT'S OWN PROFILES TOO. The minted agents' briefs are written here, not into the
909
- // engine's canonical profiles — so passing only the latter handed the reviewer a header with
910
- // a blank line under it for every agent it was asked to judge. It reported them all "empty"
911
- // and blocked the roster twice, correctly about the text and wrongly about the roster.
912
- let projectProfiles = {};
913
- try {
914
- const _pp = path.join(process.env.EPAM_PROJECT_CONFIG_DIR || '', 'agent-profiles.json');
915
- projectProfiles = JSON.parse(fs.readFileSync(_pp, 'utf8'));
916
- } catch { /* a project with no minted profiles yet has none to add */ }
917
- return await spec.reviewRoster({
918
- promptExec, minted: _mintedDetail, profiles: liveProfiles, projectProfiles,
919
- codelines, tickets: stories, referencedDocs: docs,
920
- logDir: LOG_DIR, repoPath: REPO_PATH, toolGrant,
921
- });
922
- } catch (err) {
923
- // A review that cannot run must not read as "no defects" — that is the fail-open shape
924
- // these gates exist to prevent.
925
- process.stderr.write(`[mint-step] roster review FAILED: ${err && err.message}\n`);
926
- return { verdict: 'review_failed', findings: [], reviewed: 0, error: String(err && err.message) };
927
- }
928
- };
929
-
930
- const reportReview = (rv, n) => {
931
- const blocking = rv.findings.filter((f) => f && f.severity === 'blocking');
932
- process.stderr.write(
933
- `[mint-step] roster review (cycle ${n}): ${rv.verdict} — ${rv.findings.length} finding(s), ` +
934
- `${blocking.length} blocking\n`);
935
- for (const f of rv.findings) {
936
- process.stderr.write(`[mint-step] [${f.severity}] ${f.agent}: ${f.claim}\n`);
937
- process.stderr.write(`[mint-step] checked: ${f.checked}\n`);
938
- process.stderr.write(`[mint-step] found: ${f.found}\n`);
939
- if (f.remedy) process.stderr.write(`[mint-step] remedy: ${f.remedy}\n`);
940
- }
941
- };
942
-
943
- // THE CORRECTION BUDGET IS THE SEAM'S RETRY BUDGET — the same one every other seam retries on
944
- // (SEAM_MAX_RETRIES / SPEC_AGENT_MAX_RETRIES, default 3) plus the first attempt. It was a
945
- // literal 2: one correction, then refuse. A roster that needed two corrections was refused
946
- // with the second review's findings unanswered (run 20260916T233216Z).
902
+ // THE REVIEW-AND-CORRECTION CYCLE is one unit (lib/roster-mint-cycle.js), so the live proof can run it on a sample mint.
947
903
  const _seamRetries = Math.max(0, parseInt(process.env.SEAM_MAX_RETRIES || process.env.SPEC_AGENT_MAX_RETRIES || '3', 10) || 0);
948
904
  const maxCycles = Number(process.env.EPAM_ROSTER_REVIEW_CYCLES || String(_seamRetries + 1));
949
- let cycle = 1;
950
- let review = { verdict: 'not_run', findings: [], reviewed: 0 };
951
-
952
- while (_mintedDetail.length && cycle <= maxCycles) {
953
- review = await runRosterReview();
954
- const blocking = review.findings.filter((f) => f && f.severity === 'blocking');
955
- reportReview(review, cycle);
956
- if (!blocking.length || cycle === maxCycles) break;
957
-
958
- // Which agents does the reviewer actually indict? A finding names one in `agent`. A
959
- // finding that names nothing — or names something not in this roster — is a statement
960
- // about a GAP ("no one covers this codeline"), which indicts no brief and must remove
961
- // nothing. Those still feed the corrective prompt, where they read as work to add.
962
- const { indicted: _indicted, retained: _retained, gaps: _gapFindings, superfluous: _superfluous } =
963
- rosterLib.partitionRosterFindings(blocking, _mintedDetail);
964
-
965
- process.stderr.write(
966
- `[mint-step] cycle ${cycle}: ${blocking.length} blocking finding(s) — ` +
967
- `${_indicted.length} agent(s) indicted, ${_retained.length} retained` +
968
- `${_gapFindings ? `, ${_gapFindings} finding(s) name no agent (roster gaps)` : ''}` +
969
- `${_superfluous && _superfluous.length ? `, ${_superfluous.length} superfluous (no replacement needed)` : ''}\n`);
970
-
971
- // Nothing indicted and nothing to add would re-mint the same roster forever against the
972
- // same findings. Stop and let the operator see them rather than burn the budget.
973
- if (!_indicted.length && !_gapFindings) {
974
- process.stderr.write(
975
- '[mint-step] no blocking finding names an agent in this roster — nothing to correct; ' +
976
- 'surfacing the findings instead of re-minting\n');
977
- break;
978
- }
979
-
980
- const cleared = rosterLib.clearProjectRoster(AGENTS_DIR, PROFILES_PATH, _indicted);
981
- for (const c of cleared) {
982
- const isSuper = _superfluous && _superfluous.includes(c);
983
- process.stderr.write(`[mint-step] − ${c} (${isSuper ? 'superfluous — no replacement' : 'indicted, being replaced'})\n`);
984
- }
985
- for (const r of _retained) process.stderr.write(`[mint-step] = ${r.name} (passed review, kept)\n`);
986
-
987
- // Superfluous agents are removed without replacement: their work is already covered by
988
- // retained roles. Telling the minter to "cover EVERYTHING" for a superfluous role causes it
989
- // to create a new catchall that re-introduces the overlap the reviewer just flagged.
990
- const _replacedForMint = _mintedDetail.filter(
991
- (m) => _indicted.includes(m.name) && !(_superfluous && _superfluous.includes(m.name)));
992
- const _superfluousForMint = _mintedDetail.filter(
993
- (m) => _superfluous && _superfluous.includes(m.name));
994
-
995
- const remint = await spec.mintProjectAgents({
996
- promptExec, tickets: stories, referencedDocs: docs, declaredDependencies: deps,
997
- codelines, toolGrant, profilesPath: PROFILES_PATH, agentsDir: AGENTS_DIR,
998
- logDir: LOG_DIR, repoPath: REPO_PATH, correctiveFindings: blocking,
999
- retainedAgents: _retained,
1000
- replacedAgents: _replacedForMint,
1001
- superfluousAgents: _superfluousForMint,
1002
- });
1003
- // The next review sees the WHOLE roster, not just the replacements: a new brief can
1004
- // duplicate or contradict a retained one, and only a review of both would catch it.
1005
- _mintedDetail = [..._retained, ...remint.minted];
1006
- _mintedNames = _mintedDetail.map((m) => m.name);
1007
- fs.writeFileSync(path.join(LOG_DIR, `agent-mint-cycle${cycle + 1}.json`), JSON.stringify(remint, null, 2));
1008
- for (const m of remint.minted) {
1009
- const tag = m.kind === 'investigator' ? `investigator:${m.codeline || '(no codeline!)'}` : 'implementer';
1010
- process.stderr.write(`[mint-step] + [${tag}] ${m.name} — ${m.rationale}\n`);
1011
- }
1012
-
1013
-
1014
- cycle += 1;
1015
- }
1016
-
1017
- review.cycles = cycle;
905
+ const _cycled = await require('./lib/roster-mint-cycle.js').reviewAndCorrectRoster({
906
+ spec, rosterLib, promptExec, stories, docs, deps, codelines, toolGrant,
907
+ profilesPath: PROFILES_PATH, agentsDir: AGENTS_DIR, logDir: LOG_DIR, repoPath: REPO_PATH,
908
+ projectConfigDir: process.env.EPAM_PROJECT_CONFIG_DIR || '', mintedDetail: _mintedDetail, maxCycles,
909
+ log: (m) => process.stderr.write(`[mint-step] ${m}\n`),
910
+ });
911
+ _mintedDetail = _cycled.mintedDetail; _mintedNames = _cycled.mintedNames;
912
+ const review = _cycled.review;
1018
913
  fs.writeFileSync(path.join(LOG_DIR, 'roster-review.json'), JSON.stringify(review, null, 2));
1019
914
 
1020
915
  // THE REVIEW NOT RUNNING IS AS SERIOUS AS THE REVIEW FAILING THE ROSTER.
@@ -1352,9 +1247,7 @@ if (require.main !== module) return;
1352
1247
  projectContext: require('./lib/project-prompt-builder.js').useCaseContext({ projectConfigDir, stories, codelines }),
1353
1248
  // The same text the reviewer is handed, so the generator and its judge see one context.
1354
1249
  codelineContext: _codelineContext,
1355
- mintedRoles: _projectRoles.length
1356
- ? _projectRoles.map((m) => `- ${m.name} [${m.kind}] — ${m.rationale || ''}`).join('\n')
1357
- : '(none minted this run)',
1250
+ mintedRoles: require('./lib/project-prompt-builder.js').mintedRolesBlock(_projectRoles),
1358
1251
  // THE BUILDER RUNS AT ITS OWN SEAM, with the grants that seam declares.
1359
1252
  //
1360
1253
  // It passed {} as the environment, so it inherited whatever the mint process happened to
@@ -90,7 +90,7 @@ async function main() {
90
90
  attempts: Number(process.env.EPAM_PROMPT_ATTEMPTS || 5),
91
91
  projectContext: builder.useCaseContext({ projectConfigDir, stories, codelines }),
92
92
  codelineContext,
93
- mintedRoles: roles.length ? roles.map((m) => `- ${m.name} [${m.kind}] — ${m.rationale || ''}`).join('\n') : '(none minted this run)',
93
+ mintedRoles: builder.mintedRolesBlock(roles),
94
94
  ...calls,
95
95
  log: (m) => { lines.push(m); process.stderr.write(`[${codeline}] ${m}\n`); },
96
96
  });
@@ -3777,6 +3777,26 @@ const TOOL_PROJECT_AGENTS = {
3777
3777
  * field collected and never shown is inert — this pipeline has produced several — so the
3778
3778
  * rendering is here, where a test executes it.
3779
3779
  */
3780
+ /** What the survey says about a codeline that a standing role may be told: whether work reaches it. Not what it found. */
3781
+ function surveyTeamShapeLine(c) {
3782
+ return `- ${c.codeline}: ${c.state}`;
3783
+ }
3784
+
3785
+ /** The survey as the TEAM's shape — which codelines the work reaches and the kinds of role each needs — for the specialiser. */
3786
+ function surveyTeamShapeBlock(estateSurvey) {
3787
+ const sv = estateSurvey && estateSurvey.ran === true && Array.isArray(estateSurvey.codelines) ? estateSurvey : null;
3788
+ if (!sv) return '';
3789
+ const lines = sv.codelines.filter(Boolean).map(surveyTeamShapeLine);
3790
+ if (!lines.length) return '';
3791
+ const inv = (sv.recommendedInvestigators || []).map((r) => r.codeline).filter(Boolean);
3792
+ const wr = (sv.recommendedWriters || []).map((r) => r.codeline).filter(Boolean);
3793
+ return ['WHICH CODELINES THE WORK REACHES, as a survey of the repositories reported (work_found / no_work_found /',
3794
+ 'not_investigated). You are not told what it found there: a persona is kept with the codeline and serves every later',
3795
+ 'ticket on it, so it is specialised to the codeline, never to this work.', '', ...lines,
3796
+ ...(inv.length ? ['', `Codelines that need their own investigator: ${inv.join(', ')}`] : []),
3797
+ ...(wr.length ? [`Codelines that need work written: ${wr.join(', ')}`] : []), ''].join('\n');
3798
+ }
3799
+
3780
3800
  function surveyLineFor(c) {
3781
3801
  const areas = c.surfaces && c.surfaces.length ? ` — areas: ${c.surfaces.join(', ')}` : '';
3782
3802
  const read = c.filesRead && c.filesRead.length
@@ -3854,40 +3874,16 @@ async function mintProjectAgents({
3854
3874
  }
3855
3875
  }
3856
3876
 
3857
- const ticketBlock = (Array.isArray(tickets) ? tickets : []).map((t) => {
3858
- const comps = Array.isArray(t.components) && t.components.length ? `\nComponents: ${t.components.join(', ')}` : '';
3859
- // THE LINK IS EVIDENCE EVEN WHEN THE FETCH FAILED. Live 2026-08-07: both linked documents
3860
- // came back empty, so they were dropped entirely — and the URLs themselves named the
3861
- // vendor the tickets never mentioned. The mint then invented a different one.
3862
- const links = (Array.isArray(t.ticketLinks) ? t.ticketLinks : [])
3863
- .map((l) => (typeof l === 'string' ? l : (l && l.url)))
3864
- .filter(Boolean);
3865
- const linkLine = links.length ? `\n Links on this ticket: ${links.join(' , ')}` : '';
3866
- return `- ${t.jiraKey || t.id || ''}: ${t.title || ''}${comps}\n ${String(t.description || '').replace(/\s+/g, ' ')}${linkLine}`;
3867
- }).join('\n');
3877
+ // THE WORK'S SHAPE, NOT ITS WORDS. The ticket's key, title, description, components and links were the first thing the
3878
+ // proposer read, and every roster it wrote was named after them — "accessible-toast-engineer", "site-toast-integration-
3879
+ // engineer" (live 2026-10-24, with a rule saying ZERO references in the same prompt). A roster is kept with the codeline and
3880
+ // reused for every later story on it, so the proposer is told how much work reaches these codelines and nothing of what
3881
+ // it is about. Which codelines the work reaches was settled by discovery: they are the codelines listed.
3882
+ const _tickets = Array.isArray(tickets) ? tickets : [];
3883
+ const workShapeBlock = `${_tickets.length} ticket(s) from the tracker reach the codelines listed below. You are not told what the tickets ask for,
3884
+ on purpose: the roster you propose is kept with these codelines and serves every later ticket on them, so each role is named
3885
+ and briefed for ITS CODELINE — its stack, structure, conventions, how it is built and tested — never for one piece of work.`;
3868
3886
 
3869
- // The vendor's own published contract, quoted verbatim by the link agent. This is the
3870
- // sharpest signal about which domains this work actually spans.
3871
- // QUOTES OR THE DOCUMENT ITSELF.
3872
- //
3873
- // This required d.quotes, which only the ticket-link AGENT produces — it runs in the spec
3874
- // pass, after this. fetchTicketDocuments returns {url, fetchStatus, path} and no quotes, so
3875
- // every successfully fetched document was filtered straight back out: 25KB of vendor
3876
- // documentation retrieved, named the real product, and reached the proposer as nothing.
3877
- // Verified before the run rather than discovered by it.
3878
- const docBlock = (Array.isArray(referencedDocs) ? referencedDocs : [])
3879
- .filter((d) => d && d.fetchStatus === 'fetched')
3880
- .map((d) => {
3881
- if (Array.isArray(d.quotes) && d.quotes.length) {
3882
- return `- ${d.url}\n${d.quotes.map((q) => ` "${String(q).replace(/\s+/g, ' ')}"`).join('\n')}`;
3883
- }
3884
- // Not truncated: this is the authoritative statement of what the work involves, and a
3885
- // cut copy is how a proposer ends up inferring the rest.
3886
- let body = typeof d.body === 'string' ? d.body : '';
3887
- if (!body && d.path) { try { body = fs.readFileSync(d.path, 'utf8'); } catch { body = ''; } }
3888
- return body ? `- ${d.url}\n${body}` : `- ${d.url} (retrieved, no readable text)`;
3889
- })
3890
- .join('\n\n');
3891
3887
 
3892
3888
  // WHAT THE CODELINE DECLARES IT USES. Ground truth about the stack, and the correction for
3893
3889
  // a specific live failure: on 2026-08-07 the tickets said only "CMS", both linked documents
@@ -4029,7 +4025,8 @@ async function mintProjectAgents({
4029
4025
  // ticket, its documents, the declared dependencies — is a claim about the code rather than
4030
4026
  // an observation of it, which is how briefs came to name modules that do not exist.
4031
4027
  const _sv = estateSurvey && Array.isArray(estateSurvey.codelines) ? estateSurvey : null;
4032
- const _svLines = _sv ? _sv.codelines.map(surveyLineFor) : [];
4028
+ // its surfaces, files and evidence are what the survey found for THIS ticket — a brief that names them is geared to it
4029
+ const _svLines = _sv ? _sv.codelines.filter(Boolean).map(surveyTeamShapeLine) : [];
4033
4030
  const surveyBlock = _svLines.length
4034
4031
  ? ['WHAT A SURVEY OF THESE REPOSITORIES REPORTED. These are LEADS, not settled facts.',
4035
4032
  '',
@@ -4050,11 +4047,9 @@ async function mintProjectAgents({
4050
4047
  ...(_sv.recommendedInvestigators.length
4051
4048
  ? ['SEPARATELY — and this is a recommendation about the TEAM, not something discovered',
4052
4049
  'about the code: a survey of the estate suggests these codelines need their own',
4053
- 'investigator. Propose one per codeline named here, with the stated focus:',
4050
+ 'investigator. Propose one per codeline named here, briefed on that codeline:',
4054
4051
  '',
4055
- ..._sv.recommendedInvestigators.map((r) =>
4056
- `- ${r.codeline}: focus on ${String(r.focus || '').replace(/\s+/g, ' ')}` +
4057
- `${r.why ? ` (because ${String(r.why).replace(/\s+/g, ' ')})` : ''}`),
4052
+ ..._sv.recommendedInvestigators.map((r) => `- ${r.codeline}`),
4058
4053
  '']
4059
4054
  : []),
4060
4055
  // THE OTHER HALF OF THE TEAM, WHICH THIS PROMPT NEVER MENTIONED.
@@ -4072,9 +4067,7 @@ async function mintProjectAgents({
4072
4067
  'work WRITTEN, not merely read. An investigator cannot change code; every story is',
4073
4068
  'assigned to an implementer, so propose at least one implementer that can build this:',
4074
4069
  '',
4075
- ..._sv.recommendedWriters.map((r) =>
4076
- `- ${r.codeline}: must be able to write ${String(r.focus || '').replace(/\s+/g, ' ')}` +
4077
- `${r.why ? ` (because ${String(r.why).replace(/\s+/g, ' ')})` : ''}`),
4070
+ ..._sv.recommendedWriters.map((r) => `- ${r.codeline}`),
4078
4071
  '']
4079
4072
  : [])].join('\n')
4080
4073
  : '';
@@ -4133,8 +4126,8 @@ async function mintProjectAgents({
4133
4126
 
4134
4127
  const prompt = `${correctiveBlock}${replacedBlock}${superfluousBlock}${retainedBlock}${existingRosterBlock}${surveyBlock}${vendorClaimRule}${testOwnershipRule}${basePrompt}
4135
4128
 
4136
- THE WORK THIS PROJECT HAS BEEN ASKED TO DO (real tickets from the tracker):
4137
- ${ticketBlock || '- (no tickets available)'}
4129
+ THE WORK THAT REACHES THESE CODELINES:
4130
+ ${workShapeBlock}
4138
4131
 
4139
4132
  ${depBlock ? `WHAT THIS CODELINE DECLARES IT DEPENDS ON (from its own manifest — ground truth
4140
4133
  about the stack, not inference from the ticket text). Where a ticket names a category
@@ -4143,10 +4136,7 @@ around a product that does not appear here:
4143
4136
  ${depBlock}
4144
4137
 
4145
4138
  ` : ''}
4146
- ${docBlock ? `DOCUMENTATION LINKED ON THESE TICKETS (fetched, quoted verbatim — the vendor's published contract):
4147
- ${docBlock}
4148
-
4149
- ` : ''}${codelineBlock}
4139
+ ${codelineBlock}
4150
4140
  ${toolGrant ? renderEngineTemplate('roster-tool-grant', { __TOOL_GRANT__: toolGrant }) : `You have NO tools on this call — you cannot open these
4151
4141
  repositories. Reason only from what is written above, and say in the rationale when a claim
4152
4142
  rests on documentation rather than on this codebase.`}
@@ -4488,7 +4478,7 @@ const TOOL_ROSTER_REVIEW = {
4488
4478
  required: ['name', 'outlivesTicket', 'why'],
4489
4479
  properties: {
4490
4480
  name: { type: 'string', description: 'The minted role, named exactly as in the roster.' },
4491
- outlivesTicket: { type: 'boolean', description: 'true only when this role — its name and its brief — would serve a story on this codeline about something OTHER than the current ticket: named for the kind of engineer/investigator and the codeline, briefed on the codeline, not on this ticket\'s subject, components or files (except as marked examples).' },
4481
+ outlivesTicket: { type: 'boolean', description: 'true only when this role\'s name and brief refer to the current ticket in NO way — not its key, title, subject, components, requirements or files, not even as an example — and would serve a story on this codeline about something else. Any such reference, in any wording, makes it false.' },
4492
4482
  why: { type: 'string', description: 'Quote the words that gear the role to the ticket, or say what makes it the codeline\'s.' },
4493
4483
  },
4494
4484
  },
@@ -4910,7 +4900,7 @@ function rolesOutliveTicketFindings(part) {
4910
4900
  agent: String(r.name), severity: 'blocking', claim: String(r.why || '(the reviewer gave no words)'),
4911
4901
  checked: 'whether this role, as named and briefed, would serve a story on its codeline about something other than the current ticket',
4912
4902
  found: 'minted for the ticket in scope, not for the codeline it is kept with',
4913
- remedy: 'a role named for its kind of work and its codeline, briefed on the codeline — its stack, conventions and how to verify there — naming the current ticket only as a marked example',
4903
+ remedy: 'a role named for its kind of work and its codeline, briefed on the codeline — its stack, conventions and how to verify there — never naming the current ticket, not even as an example',
4914
4904
  }));
4915
4905
  }
4916
4906
 
@@ -6691,46 +6681,6 @@ function prescriptionMissingSource(sites) {
6691
6681
  * survey that did not run, a codeline it never covered, or one it put out of scope — because a
6692
6682
  * fabricated starting point is worse than none.
6693
6683
  */
6694
- /**
6695
- * surveyLeadsBlock(estateSurvey) — what a survey of the repositories OBSERVED, with its caveat.
6696
- *
6697
- * THE OBSERVATION HALF, SEPARATED FROM THE INSTRUCTION HALF. mintProjectAgents builds a block that
6698
- * mixes the survey's findings with instructions to PROPOSE roles and investigators. Those
6699
- * instructions are about staffing a team and mean nothing to a seam that is specialising personas,
6700
- * but the findings mean everything to it: they are the only input in the whole roster stage
6701
- * derived from opening the repositories.
6702
- *
6703
- * WHY THIS EXISTS AT ALL. estateSurvey was consumed only by mintProjectAgents, so the seam that
6704
- * chooses WHICH agents to make saw the repositories and the seam that decides WHAT THEY BELIEVE
6705
- * did not. The specialiser was handed codeline names, paths and dependency lists, and asked to
6706
- * write the project-facts paragraph that goes into every persona. Live 2026-09-08: it wrote that
6707
- * this repo colocates .spec.tsx beside the component, the roster review refuted it (338 of 368
6708
- * files are under __tests__/), and three attempts escalating to opus-4-8 cost $8.23 plus $3.47 of
6709
- * review — to establish something `find` answers instantly, and which the survey had already read.
6710
- *
6711
- * THE CAVEAT IS PART OF THE EVIDENCE, never separable from it. A survey line handed over bare is
6712
- * worse than none: bare is exactly how "as this repo already does" became an instruction inherited
6713
- * by every agent. A survey has been wrong before — 2026-08-08 it reported a repository contained
6714
- * no reference to a package its own source uses in twenty files.
6715
- */
6716
- function surveyLeadsBlock(estateSurvey) {
6717
- const sv = estateSurvey && estateSurvey.ran === true && Array.isArray(estateSurvey.codelines)
6718
- ? estateSurvey : null;
6719
- if (!sv) return '';
6720
- const lines = sv.codelines.filter(Boolean).map(surveyLineFor);
6721
- if (!lines.length) return '';
6722
- return [
6723
- 'WHAT A SURVEY OF THESE REPOSITORIES REPORTED. These are LEADS, not settled facts.',
6724
- '',
6725
- 'This was read from the repositories before the roster existed. It has been wrong before, so',
6726
- 'treat every line below as what the survey BELIEVES. Do NOT restate any of it in an agent brief',
6727
- 'as established, verified or confirmed — a brief is inherited whole and re-checked by nothing,',
6728
- 'so a wrong lead written as a fact becomes an instruction every agent obeys.',
6729
- '',
6730
- ...lines,
6731
- '',
6732
- ].join('\n');
6733
- }
6734
6684
 
6735
6685
  /**
6736
6686
  * configSurfaceBlock — the configuration files this codeline carries that govern what its code is
@@ -11216,7 +11166,7 @@ module.exports = {
11216
11166
  buildRosterReviewPrompt,
11217
11167
  buildSurveyPrompt,
11218
11168
  surveyLineFor,
11219
- surveyLeadsBlock,
11169
+ surveyTeamShapeBlock,
11220
11170
  reconcileMintTally,
11221
11171
  SURVEY_STATES,
11222
11172
  TOOL_ROSTER_REVIEW,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.41",
3
+ "version": "0.5.43",
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": {