amsd-pipeline-shared 0.5.41 → 0.5.42
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.
- package/docs/gateway-files.txt +3 -0
- package/orchestrations/prompts/templates/agent-proposal.json +4 -3
- package/orchestrations/prompts/templates/prompt-review.json +4 -3
- package/orchestrations/prompts/templates/roster-review.json +4 -3
- package/orchestrations/prompts/templates/set-fidelity-check.json +4 -3
- package/orchestrations/scripts/lib/engine-prompt.js +3 -4
- package/orchestrations/scripts/lib/llm-settings-resolve.js +2 -0
- package/orchestrations/scripts/lib/pipeline-tools-mcp.js +102 -0
- package/orchestrations/scripts/lib/project-prompt-builder.js +16 -1
- package/orchestrations/scripts/lib/roster-mint-cycle.js +113 -0
- package/orchestrations/scripts/lib/roster-seams.js +189 -0
- package/orchestrations/scripts/mint-agents-step.js +10 -117
- package/orchestrations/scripts/refresh-project-prompts.js +1 -1
- package/orchestrations/scripts/spec-mode-runner.js +40 -90
- package/package.json +1 -1
package/docs/gateway-files.txt
CHANGED
|
@@ -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.
|
|
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":
|
|
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":
|
|
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\
|
|
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
|
|
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":
|
|
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":
|
|
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
|
|
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
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
4137
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.42",
|
|
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": {
|