@cat-factory/server 0.49.2 → 0.49.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,11 @@
1
1
  import {} from '@cat-factory/kernel';
2
2
  import { CONTEXT_BUDGET, CredentialRequiredError, renderTaskContext, SUBSCRIPTION_VENDORS, isIndividualVendor, } from '@cat-factory/kernel';
3
3
  import { resolveInstanceTypeId } from '@cat-factory/contracts';
4
- import { agentTuningFor, composeBlockSystemPrompt, FINAL_ANSWER_IN_REPLY, FOLLOW_UP_GUIDANCE, isContainerBackedCompanion, isProxyableProvider, isReadOnlyAgentKind, registeredAgentStep, systemPromptFor, userPromptFor, webResearchGuidanceFor, } from '@cat-factory/agents';
4
+ import { agentTuningFor, isProxyableProvider, isReadOnlyAgentKind, webResearchGuidanceFor, } from '@cat-factory/agents';
5
5
  import { ModelRouter } from './ModelRouter.js';
6
6
  import { toRunResult } from './containerAgentResult.js';
7
- import { BLUEPRINTS_AGENT_KIND, CI_FIXER_AGENT_KIND, CONFLICT_RESOLVER_AGENT_KIND, FIXER_AGENT_KIND, MERGER_AGENT_KIND, ON_CALL_AGENT_KIND, SPEC_WRITER_AGENT_KIND, TESTER_AGENT_KIND, UI_TESTER_AGENT_KIND, } from '@cat-factory/orchestration';
7
+ import { buildKindBody } from './jobBody.js';
8
+ import { UI_TESTER_AGENT_KIND } from '@cat-factory/orchestration';
8
9
  import { RunnerJobClient } from './RunnerJobClient.js';
9
10
  /**
10
11
  * The repo spec every container job body carries: clone coordinates plus, for a
@@ -202,122 +203,6 @@ function buildContextFiles(context) {
202
203
  }
203
204
  /** Poll cadence for the non-durable `run()` fallback (the durable driver sleeps between polls itself). */
204
205
  const RUN_POLL_INTERVAL_MS = 5_000;
205
- /** Role prompt the Blueprinter step's agent runs under (returns the tree as JSON). */
206
- const BLUEPRINT_SYSTEM_PROMPT = 'You are a Domain-Driven Design architect mapping this repository. Decompose it ' +
207
- 'into ONE top-level service and the modules inside it, where each module is a ' +
208
- 'DOMAIN — a cohesive area of the BUSINESS, in the language of the problem space ' +
209
- '(a DDD bounded context / aggregate / subdomain). Name modules after business ' +
210
- 'concepts, not technical layers. ' +
211
- 'A module MUST represent a business capability or domain model (e.g. Billing, ' +
212
- 'Catalog, Ordering, Identity), NOT a technical layer or shape: "api", "routes", ' +
213
- '"controllers", "utils", "helpers", "lib", "common", "config", "types", "models", ' +
214
- '"db" and the like are NOT domains and MUST NOT be modules. ' +
215
- 'Group the genuinely non-business, technical/cross-cutting plumbing (persistence ' +
216
- 'wiring, HTTP/transport, logging, configuration, auth middleware, build/deploy, ' +
217
- 'shared utilities) into a SINGLE module named "infrastructure" rather than ' +
218
- 'scattering it into many technical modules. ' +
219
- 'Prefer organising code by domain (the ubiquitous language) over organising by ' +
220
- 'file type. Anchor every node to the codebase with explicit repo-relative ' +
221
- 'file/directory references. Keep names short and descriptive. ' +
222
- 'Respond with ONLY a JSON object of shape {"type","name","summary","references":[],' +
223
- '"modules":[{"name","summary","references":[]}]} — no prose, no code fences. ' +
224
- FINAL_ANSWER_IN_REPLY;
225
- /** Role prompt the spec-writer step runs under (returns the spec doc as JSON). */
226
- const SPEC_WRITER_SYSTEM_PROMPT = 'You maintain the PRESCRIPTIVE specification for a service. READ the specification ' +
227
- 'already committed to the repository under `spec/` (the baseline): start with ' +
228
- '`spec/overview.md` for the module → feature index, then open the relevant ' +
229
- '`spec/modules/<module>/<feature>.json` shards for the detail you need. You are also ' +
230
- 'given the ' +
231
- 'requirements of ONE task. Apply that task as an INCREMENT onto the baseline: add ' +
232
- 'requirements for what the task introduces, and adjust existing requirements ONLY ' +
233
- 'where the task changes their expected behaviour. Leave every other part of the ' +
234
- 'baseline spec untouched. Translate ONLY what the task requirements state — do NOT ' +
235
- 'invent requirements, fill gaps, or design beyond them (missing requirements are the ' +
236
- 'requirements step’s job, not yours). ' +
237
- 'The spec captures ONLY BUSINESS requirements — externally-observable behaviour, ' +
238
- 'product rules and acceptance criteria. PURELY TECHNICAL work (a refactor, a ' +
239
- 'dependency bump, internal restructuring, build/infra or other non-functional change ' +
240
- 'that does NOT alter what the system does for its users) introduces no business ' +
241
- 'requirements, and "NO NEW SPECS" is a valid, correct outcome for it: do NOT invent ' +
242
- 'requirements to justify a change, and do NOT re-document technical/architecture ' +
243
- 'detail here. When this task is purely technical, leave the baseline spec untouched ' +
244
- 'and respond with ONLY {"noBusinessSpecs": true} (no other fields, no prose, no code ' +
245
- 'fences). Otherwise return the full document as below. ' +
246
- 'The spec is a two-level taxonomy: MODULES ' +
247
- '(domains, e.g. "Auth") each containing GROUPS (features, e.g. "Login"). Every ' +
248
- 'requirement AND every domain rule lives inside a specific feature group: a group ' +
249
- 'carries both its `requirements` and the `rules` scoped to it. There is NO catch-all — ' +
250
- 'a cross-cutting concern goes in a `common` or `infrastructure` module that is ITSELF ' +
251
- 'split into specific feature groups. CRUCIALLY, reuse the EXISTING taxonomy: place ' +
252
- 'each new requirement/rule into the closest-fitting existing module and feature, ' +
253
- 'reusing its EXACT name, and create a new module or feature ONLY when nothing fits — ' +
254
- 'never a near-duplicate of an existing one (no "Authentication" beside "Auth", no ' +
255
- '"User Login" beside "Login"). Each requirement is phrased as "The system SHALL …" ' +
256
- 'with a MoSCoW priority (must/should/could) and structured Given/When/Then acceptance ' +
257
- 'criteria. Acceptance-scenario coverage is a FIRST-CLASS deliverable: every ' +
258
- 'requirement the task adds or changes MUST carry complete acceptance criteria — the ' +
259
- 'happy path AND the invalid-input / error / edge / boundary cases the requirements ' +
260
- 'imply — since the Gherkin `.feature` files and the runnable tests are derived ' +
261
- 'mechanically from them. Preserve the baseline’s existing `sourceBlockIds`; tag the ' +
262
- 'requirements this task adds or changes with this task’s block id. Return the ' +
263
- 'COMPLETE updated specification (baseline plus this increment), not a diff. You have ' +
264
- 'NO repository write access and MUST NOT write, edit, or commit any file: the platform ' +
265
- 'persists the specification you return, so returning it IS the whole job. Respond ' +
266
- 'with ONLY a JSON object of ' +
267
- 'shape {"service","summary","modules":[{"name","summary","groups":[{"name","summary",' +
268
- '"requirements":[{"id","title","statement","kind","priority","sourceBlockIds":[],' +
269
- '"acceptance":[{"id","given","when","outcome"}]}],"rules":[{"id","rule","rationale",' +
270
- '"sourceBlockIds":[]}]}]}]} ' +
271
- '(each acceptance criterion is a Given/When/Then, with the Then clause in `outcome`) — ' +
272
- 'no prose, no code fences. ' +
273
- FINAL_ANSWER_IN_REPLY;
274
- /** Role prompt the `merger` step runs under (scores the PR; returns JSON only). */
275
- const MERGER_SYSTEM_PROMPT = 'You are a release manager assessing a pull request before merge. Inspect the ' +
276
- 'diff between the PR head branch and the base branch and judge three axes, each ' +
277
- 'as a number from 0 (trivial/safe) to 1 (severe): complexity (how intricate the ' +
278
- 'change is), risk (how likely it is to break something), and impact (blast radius ' +
279
- 'if it does). Be conservative. Respond with ONLY a JSON object of shape ' +
280
- '{"complexity":0.0,"risk":0.0,"impact":0.0,"rationale":"…"} — no prose, no code fences. ' +
281
- FINAL_ANSWER_IN_REPLY;
282
- /** Compact shape hint fed to the structured-output repair call for the blueprint tree. */
283
- const BLUEPRINT_SHAPE_HINT = 'Expected a service tree: {"type": string, "name": string, "summary": string, ' +
284
- '"references": string[], "modules": [{"name": string, "summary": string, ' +
285
- '"references": string[]}]}.';
286
- /** Compact shape hint fed to the structured-output repair call for the spec doc. */
287
- const SPEC_SHAPE_HINT = 'Expected a requirements document with a two-level taxonomy — module (domain) → ' +
288
- 'group (feature) — where each group carries BOTH its requirements and the domain ' +
289
- 'rules scoped to it: {"service": string, "summary": string, "modules": [{"name": ' +
290
- 'string, "summary": string, "groups": [{"name": string, "summary": string, ' +
291
- '"requirements": [{"id": string, "title": string, "statement": string, "kind": ' +
292
- 'string, "priority": string, "sourceBlockIds": string[], "acceptance": [{"given": ' +
293
- 'string, "when": string, "outcome": string}]}], "rules": [{"id": string, "rule": ' +
294
- 'string, "rationale": string, "sourceBlockIds": string[]}]}]}]}. For a purely ' +
295
- 'technical task with no business requirements, the document is instead just ' +
296
- '{"noBusinessSpecs": true}.';
297
- /** Compact shape hint fed to the structured-output repair call for the merger assessment. */
298
- const MERGE_ASSESSMENT_SHAPE_HINT = 'Expected a merge assessment: {"complexity": number 0..1, "risk": number 0..1, ' +
299
- '"impact": number 0..1, "rationale": string}.';
300
- /** Compact shape hint fed to the structured-output repair call for the on-call assessment. */
301
- const ON_CALL_ASSESSMENT_SHAPE_HINT = 'Expected an on-call assessment: {"culpritConfidence": number 0..1, "recommendation": ' +
302
- '"revert"|"hold"|"monitor", "rationale": string, "evidence": string[]}.';
303
- /** Compact shape hint fed to the structured-output repair call for the tester report. */
304
- const TEST_REPORT_SHAPE_HINT = 'Expected a test report: {"greenlight": boolean, "summary": string, "tested": string[], ' +
305
- '"outcomes": [{"name": string, "status": "passed"|"failed"|"skipped", "detail"?: string}], ' +
306
- '"concerns": [{"title": string, "detail": string, "severity": "low"|"medium"|"high"|"critical"}]}.';
307
- /** Shape hint for the UI tester: a test report that also lists captured screenshots. */
308
- const UI_TEST_REPORT_SHAPE_HINT = TEST_REPORT_SHAPE_HINT.replace(/\}\.$/, '') +
309
- ', "screenshots": [{"view": string, "artifactId": string, "hash"?: string}]}. Each ' +
310
- 'screenshot must be a distinct view you captured and uploaded to the artifact store.';
311
- const ON_CALL_SYSTEM_PROMPT = 'You are an on-call engineer investigating a possible post-release regression. A ' +
312
- 'recently merged pull request shipped, and the evidence below (alerting Datadog ' +
313
- 'monitors/SLOs and recent error logs) suggests the service regressed afterward. Read ' +
314
- 'the PR diff on the head branch and weigh whether THIS change is the likely cause — ' +
315
- 'beware correlation vs causation; a coincident deploy is not proof. You may read and ' +
316
- 'inspect any file, but you MUST NOT modify, commit or revert anything; a human decides ' +
317
- 'whether to revert. Respond with ONLY a JSON object of shape ' +
318
- '{"culpritConfidence":0.0,"recommendation":"revert"|"hold"|"monitor","rationale":"…",' +
319
- '"evidence":["…"]} — no prose, no code fences. ' +
320
- FINAL_ANSWER_IN_REPLY;
321
206
  /**
322
207
  * An {@link AgentExecutor} that performs implementation work in a real sandbox:
323
208
  * it dispatches a per-run container running the Pi coding agent (a per-run
@@ -672,7 +557,7 @@ export class ContainerAgentExecutor {
672
557
  webToolsGuidance: webResearchGuidanceFor(context.agentKind, { fetch: true }),
673
558
  ...(webSearchEnabled ? { webSearch: true } : {}),
674
559
  };
675
- const { body, kind } = this.buildKindBody(promptContext, {
560
+ const { body, kind } = buildKindBody(promptContext, {
676
561
  common,
677
562
  webTools,
678
563
  repo,
@@ -764,450 +649,5 @@ export class ContainerAgentExecutor {
764
649
  ...(subscriptionTokenId ? { subscriptionTokenId } : {}),
765
650
  };
766
651
  }
767
- /**
768
- * Build the per-kind harness job body: the shared `common` fields plus ONLY the delta
769
- * specific to this kind's harness endpoint (its prompts, the branch it runs on, and
770
- * any per-kind extras), and the matching dispatch `kind`. The web-search fields live
771
- * in `webTools` (shared by the kinds that allow web access). The dispatch precedence
772
- * matches the original if-ladder exactly: the specific kinds first, then any read-only
773
- * kind, then the default coder body.
774
- */
775
- buildKindBody(context, parts) {
776
- // `parts` (common/webTools/workBranch/workBranchReady) is consumed by
777
- // `buildRegisteredAgentBody`/`buildMigratedBuiltInBody`, not directly here.
778
- const baseRoleSystemPrompt = composeBlockSystemPrompt(systemPromptFor(context.agentKind), context.block);
779
- // When the future-looking Follow-up companion is enabled for this (coder) step, append
780
- // the guidance that tells the Coder to stream loose-ends / side-tasks / questions to the
781
- // sentinel file the harness tails. Only when enabled, so a disabled companion (or any
782
- // other kind) never writes the file.
783
- const roleSystemPrompt = context.followUpCompanion
784
- ? `${baseRoleSystemPrompt}\n\n${FOLLOW_UP_GUIDANCE}`
785
- : baseRoleSystemPrompt;
786
- // A registered (custom or migrated) kind that declares an `agent` step dispatches
787
- // through the generic, manifest-driven `agent` harness kind — no per-kind case here.
788
- // Built-in kinds (below) still carry their bespoke bodies until they are migrated.
789
- const registeredStep = registeredAgentStep(context.agentKind);
790
- if (registeredStep) {
791
- return this.buildRegisteredAgentBody(context, parts, registeredStep, roleSystemPrompt);
792
- }
793
- // Built-in container kinds migrated onto the generic, manifest-driven `agent` harness
794
- // kind (they dispatch `kind:'agent'` through `buildRegisteredAgentBody`, exactly like a
795
- // registered custom kind, with NO bespoke per-kind harness handler) — the Task-5
796
- // strangler. Today: blueprints/spec-writer (structured explore + render post-op), the
797
- // in-place fixers (`ci-fixer` / `fixer`, coding-on-PR), the JSON-assessment producers
798
- // (`merger` / `on-call`, read-only structured explore whose assessment is coerced
799
- // backend-side in `toRunResult`), the `tester` (read-only structured explore with
800
- // docker-compose infra stand-up), and the conflict-resolver (coding with a `mergeBase`).
801
- // The default coder dispatches the generic coding agent at the end of this method.
802
- const migrated = this.buildMigratedBuiltInBody(context, parts, roleSystemPrompt);
803
- if (migrated)
804
- return migrated;
805
- // Container-backed companions (reviewer / doc-reviewer): a read-only explore that clones
806
- // the producer's PR branch and reads the ACTUAL repository (changed files / committed
807
- // document) before rating it, returning the verdict as structured JSON. Surfaced to the
808
- // engine as `result.custom` (the default `toRunResult` branch) and parsed back into a
809
- // CompanionAssessment by `CompanionController.resolveContainerVerdict`. The companion
810
- // review system prompt (which already instructs the JSON shape and, for these kinds, to
811
- // read the checkout) wins in `systemPromptFor`, so no per-kind prompt wiring is needed.
812
- if (isContainerBackedCompanion(context.agentKind)) {
813
- return this.buildRegisteredAgentBody(context, parts, { surface: 'container-explore', clone: { branch: 'pr' }, output: { kind: 'structured' } }, roleSystemPrompt);
814
- }
815
- // Read-only agents (architect, analysis) explore a real checkout but never edit it:
816
- // they clone a branch, produce a prose report/proposal and return it as `output`,
817
- // making no commit and opening no PR (and — unlike a coding run — an edit-free run is
818
- // the expected, correct outcome, not a failure). They dispatch through the generic,
819
- // manifest-driven `agent` kind in `explore` mode — the SAME path a registered
820
- // `container-explore` kind takes — instead of a bespoke per-kind harness handler. A
821
- // synthesized read-only step (no clone target ⇒ the shared work-branch fallback, so
822
- // e.g. the architect reads the spec-writer's committed `spec/` and any in-progress
823
- // implementation, falling back to base when no work/PR branch exists) yields a body
824
- // byte-identical to the old `/explore` job, minus only the harness-internal temp-dir
825
- // label. This is the first built-in migrated onto the generic agent surface (the
826
- // Task-5 strangler); the now-dead `/explore` harness handler is deleted in a
827
- // follow-up once parity is confirmed on CI.
828
- if (isReadOnlyAgentKind(context.agentKind)) {
829
- return this.buildRegisteredAgentBody(context, parts, { surface: 'container-explore' }, roleSystemPrompt);
830
- }
831
- // The default coder (and any other write-and-PR kind): the build-phase role plus the
832
- // block's selected best-practice fragments. Dispatches the generic `container-coding`
833
- // agent onto the deterministic per-task work branch (`clone: 'work'` ⇒ branch off base,
834
- // push the work branch, open a PR). The work-branch name is deterministic per task
835
- // (block), NOT per dispatch — a retry mints a fresh executionId but keeps the blockId —
836
- // so every re-dispatch targets the SAME branch; `runCodingAgent` checkpoints commits to
837
- // it and RESUMES on it if it already exists, so an evicted/failed run's work survives.
838
- // This is behaviour-equivalent to the old bespoke `/run` body (handleAgent coding mode
839
- // is built on the same `runCodingAgent` primitive); the dead `/run` handler is removed
840
- // in the harness-cleanup step.
841
- return this.buildRegisteredAgentBody(context, parts, { surface: 'container-coding', clone: { branch: 'work' } }, roleSystemPrompt);
842
- }
843
- /**
844
- * Build the generic `agent` job body for a registered kind from its declarative
845
- * {@link AgentStepSpec} — the single dispatch path that replaces the per-kind cases as
846
- * built-ins migrate. `container-explore` clones a branch read-only and returns prose
847
- * (or, for `output.kind==='structured'`, a parsed `custom` JSON object the kind's
848
- * post-op renders from); `container-coding` clones, edits, pushes and (off the work
849
- * branch) opens a PR. The clone target maps `base`/`pr`/`work` to a concrete branch
850
- * exactly as the built-in bodies do.
851
- */
852
- buildRegisteredAgentBody(context, parts, step, roleSystemPrompt,
853
- /**
854
- * The concrete task prompt. Defaults to the generic `userPromptFor` (block context +
855
- * prior outputs) — the same prompt a registered custom kind gets. A migrated built-in
856
- * (merger / on-call) overrides it with its bespoke, JSON-instructing prompt so its
857
- * body matches the old per-kind handler's.
858
- */
859
- userPrompt = userPromptFor(context, { materialized: true })) {
860
- const { common, webTools, repo, workBranch, workBranchReady } = parts;
861
- const prBranch = context.block.pullRequest?.branch;
862
- const onPr = step.clone?.branch === 'pr';
863
- const exploreBranch = step.clone?.branch === 'base'
864
- ? repo.baseBranch
865
- : onPr
866
- ? (prBranch ?? repo.baseBranch)
867
- : workBranchReady
868
- ? workBranch
869
- : (prBranch ?? repo.baseBranch);
870
- if (step.surface === 'container-coding') {
871
- // `pr` clone ⇒ work in place on the PR branch and push back (fixer-like, no new PR);
872
- // otherwise branch off base onto the work branch, push it and open a PR (coder-like).
873
- return {
874
- kind: 'agent',
875
- body: {
876
- ...common,
877
- mode: 'coding',
878
- systemPrompt: roleSystemPrompt,
879
- userPrompt,
880
- branch: onPr ? (prBranch ?? repo.baseBranch) : repo.baseBranch,
881
- ...(onPr ? {} : { newBranch: workBranch }),
882
- pushBranch: onPr ? (prBranch ?? workBranch) : workBranch,
883
- ...(onPr
884
- ? { noChangesIsError: false }
885
- : {
886
- pr: {
887
- title: `${context.block.title} (${context.pipelineName})`,
888
- body: prBody(context),
889
- },
890
- }),
891
- ...(step.clone?.full ? { full: true } : {}),
892
- // The Coder (follow-up companion enabled) streams forward-looking items out via
893
- // the sentinel file; tell the harness to tail it. Only on the implementer path.
894
- ...(context.followUpCompanion && !onPr ? { streamFollowUps: true } : {}),
895
- ...webTools,
896
- },
897
- };
898
- }
899
- // container-explore (read-only): prose, or a structured JSON object as `custom`.
900
- return {
901
- kind: 'agent',
902
- body: {
903
- ...common,
904
- mode: 'explore',
905
- systemPrompt: roleSystemPrompt,
906
- userPrompt,
907
- branch: exploreBranch,
908
- ...(step.clone?.full ? { full: true } : {}),
909
- ...(step.output?.kind === 'structured'
910
- ? {
911
- output: {
912
- kind: 'structured',
913
- ...(step.output.shapeHint ? { shapeHint: step.output.shapeHint } : {}),
914
- ...(step.output.repair === false ? { repair: false } : {}),
915
- ...(step.output.failOnUnusableFinal ? { failOnUnusableFinal: true } : {}),
916
- },
917
- }
918
- : {}),
919
- ...webTools,
920
- },
921
- };
922
- }
923
- /**
924
- * Build the generic `agent` body for a BUILT-IN container kind being migrated onto the
925
- * manifest-driven path (the Task-5 strangler), or undefined when `context.agentKind` is
926
- * not a migrated built-in (the caller falls through to the remaining bespoke switch). Each
927
- * migrated kind is expressed as a synthesized {@link AgentStepSpec} routed through
928
- * {@link buildRegisteredAgentBody} — the SAME dispatch a registered custom kind takes — so
929
- * there is no bespoke harness handler:
930
- * - `ci-fixer` / `fixer`: coding-on-PR (clone the PR branch, push back, no new PR; a
931
- * no-op is non-fatal). Requires the implementation PR branch.
932
- * - `merger` / `on-call`: read-only structured explore (full clone) that returns ONLY a
933
- * JSON assessment; the conservative coercion that used to live in the harness runs
934
- * backend-side in {@link toRunResult}.
935
- * - `conflict-resolver`: coding (full clone of the PR branch) with a `mergeBase` — the
936
- * harness merges the base in to surface the conflicts, the agent resolves them, and the
937
- * harness completes the merge commit + pushes back onto the same branch (no new PR).
938
- */
939
- buildMigratedBuiltInBody(context, parts, roleSystemPrompt) {
940
- const { repo } = parts;
941
- const prBranch = context.block.pullRequest?.branch;
942
- switch (context.agentKind) {
943
- // The Blueprinter maps the repo into the service → modules tree. It now runs as a
944
- // read-only structured explore (clone the PR branch when present, else the default
945
- // branch — exactly its old `prBranch ?? baseBranch` clone), returning ONLY the tree
946
- // as JSON; the deterministic render + commit of the `blueprints/` artifact that used
947
- // to live in the harness `/blueprint` handler is the backend `blueprintPostOp` (run
948
- // from ExecutionService), and `toRunResult` coerces the JSON into `blueprintService`
949
- // for the board reconcile + that post-op.
950
- case BLUEPRINTS_AGENT_KIND:
951
- return this.buildRegisteredAgentBody(context, parts, {
952
- surface: 'container-explore',
953
- clone: { branch: 'pr' },
954
- output: { kind: 'structured', shapeHint: BLUEPRINT_SHAPE_HINT },
955
- }, BLUEPRINT_SYSTEM_PROMPT, blueprintUserPrompt());
956
- // The spec-writer maintains the prescriptive `spec/` document. It now runs as a
957
- // read-only structured explore on the per-block WORK branch (clone `work` — the
958
- // deterministic `cat-factory/<blockId>` the coder resumes, created from base when
959
- // absent; it runs BEFORE the coder, so it SEEDS that branch). The agent READS the
960
- // baseline spec from its own checkout (`spec/`), applies this ONE task as an increment,
961
- // and returns the COMPLETE tree as JSON; the deterministic SHARD + commit of the
962
- // `spec/` artifact that used to live in the harness `/spec` handler is the backend
963
- // `specPostOp` (run from ExecutionService), and `toRunResult` coerces the JSON into the
964
- // `spec` channel the engine strict-validates + that post-op renders/commits from. It
965
- // NEVER targets base: the spec is prescriptive for not-yet-landed work, so it merges
966
- // WITH the feature, never reaching `main` ahead of it.
967
- case SPEC_WRITER_AGENT_KIND:
968
- return this.buildRegisteredAgentBody(context, parts, {
969
- surface: 'container-explore',
970
- clone: { branch: 'work' },
971
- // The spec doc is handed onward to be sharded + committed by `specPostOp`, so a
972
- // final answer cut off at the output ceiling must FAIL LOUDLY (the bespoke `/spec`
973
- // handler's `unusableFinalAnswerCause` gate) rather than be laundered into a
974
- // half-baked spec by the structured repair — exactly what drove the old
975
- // spec-writer ⇄ companion rework loop.
976
- output: { kind: 'structured', shapeHint: SPEC_SHAPE_HINT, failOnUnusableFinal: true },
977
- }, SPEC_WRITER_SYSTEM_PROMPT, specWriterUserPrompt(context));
978
- // In-place fixers: clone the PR head branch, push fixes back onto it (no new PR);
979
- // a no-op run is a clean non-event (the gate/loop re-checks the real signal).
980
- case CI_FIXER_AGENT_KIND:
981
- if (!prBranch)
982
- throw new Error('CI-fixer needs the implementation PR branch to push fixes to');
983
- return this.buildRegisteredAgentBody(context, parts, { surface: 'container-coding', clone: { branch: 'pr' } }, roleSystemPrompt);
984
- case FIXER_AGENT_KIND:
985
- if (!prBranch)
986
- throw new Error('Fixer needs the implementation PR branch to push fixes to');
987
- return this.buildRegisteredAgentBody(context, parts, { surface: 'container-coding', clone: { branch: 'pr' } }, roleSystemPrompt);
988
- // The conflict-resolver clones the PR head branch (full history), merges the base in
989
- // to surface the conflicts, resolves them and pushes back onto the SAME branch (no new
990
- // branch / PR) so the PR becomes mergeable and CI re-runs. It dispatches the generic
991
- // coding agent with a `mergeBase` (the harness merges `origin/<mergeBase>` in before the
992
- // agent runs); the harness leads the prompt with the actual conflict hunks it discovers.
993
- //
994
- // Unlike the CI-fixer it is deliberately NOT given `userPromptFor(context)`: that renders
995
- // the full task brief + every prior agent's output (the spec-writer's whole spec, etc.),
996
- // which buries the one-line "resolve a conflict" role and drifts the model onto
997
- // re-implementing the feature (observed in prod: a resolver that returned a "test report
998
- // is ready" answer and never touched the markers). The backend supplies only a compact
999
- // task reference for intent.
1000
- case CONFLICT_RESOLVER_AGENT_KIND: {
1001
- if (!prBranch) {
1002
- throw new Error('Conflict-resolver needs the implementation PR branch to resolve conflicts on');
1003
- }
1004
- const description = context.block.description?.trim();
1005
- const built = this.buildRegisteredAgentBody(context, parts, { surface: 'container-coding', clone: { branch: 'pr', full: true } }, roleSystemPrompt, `Task: ${context.block.title}${description ? `\n\n${description}` : ''}`);
1006
- return { kind: built.kind, body: { ...built.body, mergeBase: repo.baseBranch } };
1007
- }
1008
- // The merger clones the PR head (full, to diff vs base) and returns ONLY the
1009
- // complexity/risk/impact assessment JSON; the engine performs the real merge.
1010
- case MERGER_AGENT_KIND:
1011
- return this.buildRegisteredAgentBody(context, parts, {
1012
- surface: 'container-explore',
1013
- clone: { branch: 'pr', full: true },
1014
- output: { kind: 'structured', shapeHint: MERGE_ASSESSMENT_SHAPE_HINT },
1015
- }, MERGER_SYSTEM_PROMPT, mergerUserPrompt(context, repo));
1016
- // The on-call agent clones the BASE branch (full, to locate + diff the merged
1017
- // release commit) and returns ONLY the regression assessment JSON.
1018
- case ON_CALL_AGENT_KIND:
1019
- return this.buildRegisteredAgentBody(context, parts, {
1020
- surface: 'container-explore',
1021
- clone: { branch: 'base', full: true },
1022
- output: { kind: 'structured', shapeHint: ON_CALL_ASSESSMENT_SHAPE_HINT },
1023
- }, ON_CALL_SYSTEM_PROMPT, onCallUserPrompt(context, repo));
1024
- // The tester clones the PR head branch (read-only — it makes NO commits), stands up
1025
- // its dependencies (locally via the service's docker-compose, or against the
1026
- // provisioned ephemeral env — the task's `tester.environment` config picks which) and
1027
- // returns ONLY a structured JSON report. It runs as a generic structured explore with
1028
- // an `infra` spec the harness uses to stand the docker-compose dependencies up for the
1029
- // run; `toRunResult` coerces the JSON into `testReport` (the conservative greenlight /
1030
- // blocking-concern rule the harness applied now runs backend-side, and the engine's
1031
- // TesterController re-applies it). The role prompt + the run-mode/ephemeral-URL guidance
1032
- // come from the standard `roleSystemPrompt` + `userPromptFor` (which already carry them),
1033
- // so the harness adds none. The engine loops the `fixer` on a withheld greenlight.
1034
- case TESTER_AGENT_KIND: {
1035
- const built = this.buildRegisteredAgentBody(context, parts, {
1036
- surface: 'container-explore',
1037
- clone: { branch: 'pr' },
1038
- output: { kind: 'structured', shapeHint: TEST_REPORT_SHAPE_HINT },
1039
- }, roleSystemPrompt);
1040
- return { kind: built.kind, body: { ...built.body, infra: testerInfraSpec(context) } };
1041
- }
1042
- // The UI tester is the Tester's browser-driven sibling: same read-only structured
1043
- // explore + infra stand-up, but it drives Playwright (supplied by the UI-tester
1044
- // image, routed via the `image:'ui'` dispatch option) to capture a non-redundant
1045
- // screenshot of each distinct view, uploads them to the artifact store, and reports
1046
- // them under `screenshots[]`. The role prompt carries the capture guidance.
1047
- case UI_TESTER_AGENT_KIND: {
1048
- const built = this.buildRegisteredAgentBody(context, parts, {
1049
- surface: 'container-explore',
1050
- clone: { branch: 'pr' },
1051
- output: { kind: 'structured', shapeHint: UI_TEST_REPORT_SHAPE_HINT },
1052
- }, roleSystemPrompt);
1053
- return { kind: built.kind, body: { ...built.body, infra: testerInfraSpec(context) } };
1054
- }
1055
- }
1056
- return undefined;
1057
- }
1058
- }
1059
- /**
1060
- * The Blueprinter's task prompt. The agent now reads any existing blueprint from its own
1061
- * read-only checkout (the harness no longer pre-injects the baseline tree), so the prompt
1062
- * tells it to read `blueprints/` and update-or-create, then return the complete tree as
1063
- * JSON. The backend `blueprintPostOp` renders + commits the artifact from that tree.
1064
- */
1065
- function blueprintUserPrompt() {
1066
- return [
1067
- 'Map this repository into the canonical service → modules blueprint, anchored to real ' +
1068
- 'file/directory references.',
1069
- '',
1070
- 'If a blueprint already exists in the repository (read `blueprints/blueprint.json` and ' +
1071
- '`blueprints/overview.md`), UPDATE it to reflect the current code: keep accurate ' +
1072
- 'modules, add new ones, and refine summaries + references. Otherwise create it from ' +
1073
- 'scratch. Return the COMPLETE tree (not a diff).',
1074
- '',
1075
- 'Respond with ONLY the JSON object for the service tree — no prose, no code fences.',
1076
- ].join('\n');
1077
- }
1078
- /**
1079
- * The spec-writer's task prompt — the instructions + baseline-read + taxonomy-reuse guidance
1080
- * the bespoke harness `/spec` handler used to build (`buildUserPrompt`/`renderTaxonomyInventory`,
1081
- * which used to inject the baseline doc + its module→feature inventory). The agent now reads
1082
- * the baseline from its own read-only checkout under `spec/`, so the prompt tells it to read +
1083
- * reuse the existing taxonomy rather than pre-injecting it. Carries ONLY this task's
1084
- * requirements (the block description IS the task's reworked/incorporated requirements), so an
1085
- * unmerged sibling task's work never bleeds in. The backend `specPostOp` shards + commits the
1086
- * returned tree.
1087
- */
1088
- function specWriterUserPrompt(context) {
1089
- const block = context.block;
1090
- const header = `### ${block.title || '(untitled task)'}${block.id ? ` (block ${block.id})` : ''}`;
1091
- // Honour an explicit human-set BUSINESS/TECHNICAL label: a task pinned business HAS
1092
- // business requirements, so the "no new specs" escape hatch is withdrawn; a task pinned
1093
- // technical is told the empty outcome is expected. Left unset, the writer self-determines.
1094
- const technicalGuidance = block.technical === false
1095
- ? 'This task is explicitly flagged BUSINESS: it HAS business requirements, so you MUST ' +
1096
- 'return the full updated specification. Do NOT respond with {"noBusinessSpecs": true}.'
1097
- : block.technical === true
1098
- ? 'This task is explicitly flagged TECHNICAL (a refactor / dependency bump / internal ' +
1099
- 'or non-functional change with NO new externally-observable behaviour): "no business ' +
1100
- 'requirements" is the expected outcome — respond with ONLY {"noBusinessSpecs": true} ' +
1101
- 'and change nothing, unless you find genuine externally-observable behaviour to spec.'
1102
- : 'If this task is purely TECHNICAL (a refactor / dependency bump / internal or ' +
1103
- 'non-functional change that introduces NO new externally-observable behaviour), it ' +
1104
- 'has no business requirements: respond with ONLY {"noBusinessSpecs": true} and ' +
1105
- 'change nothing.';
1106
- return [
1107
- 'Apply this ONE task as an INCREMENT onto the service specification.',
1108
- '',
1109
- 'First READ the specification already committed to the repository under `spec/` (the ' +
1110
- 'baseline as merged before this task): open `spec/overview.md` for the module → feature ' +
1111
- 'index, then the relevant `spec/modules/<module>/<feature>.json` shards. Keep every part ' +
1112
- 'of the baseline this task does not touch exactly as-is, preserving its `sourceBlockIds`; ' +
1113
- 'adjust an existing requirement only where this task changes its behaviour. Map each new ' +
1114
- 'requirement/rule into the closest-fitting EXISTING module and feature, reusing its EXACT ' +
1115
- 'name — create a new module or feature ONLY when nothing fits (never a near-duplicate). ' +
1116
- 'If no spec exists yet, start one as a module (domain) → feature (group) taxonomy.',
1117
- '',
1118
- 'Requirements for the ONE task to apply (its clarified description). Translate ONLY what ' +
1119
- 'these state into BUSINESS requirements (externally-observable behaviour, product rules, ' +
1120
- 'acceptance criteria) with COMPLETE acceptance-scenario coverage — do NOT invent ' +
1121
- 'requirements or fill gaps they leave:',
1122
- '',
1123
- `${header}\n\n${block.description?.trim() || '(no description)'}`,
1124
- '',
1125
- technicalGuidance +
1126
- ' Otherwise return the COMPLETE updated document (baseline plus this task’s ' +
1127
- 'increment), not a diff. Respond with ONLY the JSON object — no prose, no code fences.',
1128
- ].join('\n');
1129
- }
1130
- /**
1131
- * The merger's task prompt — the instructions + diff guidance the bespoke harness `/merge`
1132
- * handler used to build. Kept backend-side now that the merger dispatches the generic
1133
- * explore agent. Names the PR/branches so the agent diffs against the right base.
1134
- */
1135
- function mergerUserPrompt(context, repo) {
1136
- const prNumber = context.block.pullRequest?.number;
1137
- const branch = context.block.pullRequest?.branch ?? repo.baseBranch;
1138
- const pr = prNumber !== undefined ? ` (PR #${prNumber})` : '';
1139
- return [
1140
- 'Assess the pull request on the head branch against the base branch and return the ' +
1141
- 'complexity / risk / impact scores + rationale as JSON.',
1142
- '',
1143
- `The pull request${pr} is on branch \`${branch}\`; the base branch is ` +
1144
- `\`${repo.baseBranch}\`. Inspect the change (e.g. \`git fetch origin ${repo.baseBranch}\` ` +
1145
- `then \`git diff origin/${repo.baseBranch}...HEAD\`) and score complexity, risk and impact.`,
1146
- '',
1147
- 'Respond with ONLY a JSON object {"complexity":0.0,"risk":0.0,"impact":0.0,"rationale":"…"}.',
1148
- ].join('\n');
1149
- }
1150
- /**
1151
- * The on-call agent's task prompt — the regression evidence (the generic block/prior-output
1152
- * prompt) plus the locate-the-merged-commit guidance the bespoke harness `/on-call` handler
1153
- * used to build. The released PR already merged into the base branch (its work branch is
1154
- * gone), so the agent is on the base branch and is told how to find the merged commit.
1155
- */
1156
- function onCallUserPrompt(context, repo) {
1157
- const prNumber = context.block.pullRequest?.number;
1158
- const headBranch = context.block.pullRequest?.branch;
1159
- const pr = prNumber !== undefined ? `#${prNumber}` : '';
1160
- const locate = prNumber
1161
- ? `It merged as a commit referencing ${pr} — find it with \`git log --oneline -n 50\` ` +
1162
- `(squash/merge commits include \`(${pr})\`; a merge commit mentions \`#${prNumber}\`), then ` +
1163
- `inspect it with \`git show <sha>\`.`
1164
- : headBranch
1165
- ? `Its work branch was \`${headBranch}\` (now deleted) — find the merged commit in ` +
1166
- `\`git log --oneline -n 50\` and inspect it with \`git show <sha>\`.`
1167
- : `Find the most recent merge/feature commit with \`git log --oneline -n 50\` and inspect ` +
1168
- `it with \`git show <sha>\`.`;
1169
- return [
1170
- userPromptFor(context, { materialized: true }),
1171
- '',
1172
- `You are on the base branch \`${repo.baseBranch}\`, which already contains the released ` +
1173
- `pull request ${pr}. ${locate} Correlate that change with the regression evidence above. ` +
1174
- `Beware correlation vs causation.`,
1175
- '',
1176
- 'Respond with ONLY a JSON object {"culpritConfidence":0.0,"recommendation":"revert"|"hold"|"monitor","rationale":"…","evidence":["…"]}.',
1177
- ].join('\n');
1178
- }
1179
- /**
1180
- * The tester's infra stand-up spec for the generic agent job, from the block's
1181
- * `tester.environment` config + the resolved service: a `local` run carries the
1182
- * docker-compose path (or the explicit no-infra flag) for the harness to stand the
1183
- * dependencies up + tear them down around the run; an `ephemeral` run carries the
1184
- * provisioned environment URL. Byte-identical to the old bespoke `/test` body's `test`
1185
- * object — only the field name changed (`test` → `infra`).
1186
- */
1187
- function testerInfraSpec(context) {
1188
- const env = context.block.agentConfig?.['tester.environment'] === 'local' ? 'local' : 'ephemeral';
1189
- const service = context.service;
1190
- return {
1191
- environment: env,
1192
- ...(env === 'local'
1193
- ? {
1194
- noInfraDependencies: service?.noInfraDependencies === true,
1195
- ...(service?.testComposePath ? { composePath: service.testComposePath } : {}),
1196
- }
1197
- : {}),
1198
- ...(env === 'ephemeral' && context.environment?.url
1199
- ? { environmentUrl: context.environment.url }
1200
- : {}),
1201
- };
1202
- }
1203
- function prBody(context) {
1204
- const lines = [
1205
- `Automated implementation for block **${context.block.title}** (${context.block.type}).`,
1206
- '',
1207
- context.block.description || '(no description)',
1208
- '',
1209
- `Pipeline: ${context.pipelineName}`,
1210
- ];
1211
- return lines.join('\n');
1212
652
  }
1213
653
  //# sourceMappingURL=ContainerAgentExecutor.js.map