agent-conveyor 0.1.4 → 0.1.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.
package/README.md CHANGED
@@ -132,6 +132,19 @@ command exits 0 and the JSON result reports `"ok": true`.
132
132
  Before publishing `agent-conveyor` to npm, use
133
133
  [`docs/package-release.md`](docs/package-release.md).
134
134
 
135
+ For common manager setups, start with
136
+ [`docs/manager-recipes.md`](docs/manager-recipes.md). It maps natural-language
137
+ requests such as GoalBuddy conveyor runs, test coverage loops, UX polish loops,
138
+ what-next nudging, and PR/CI/merge Ralph loops to concrete `manager-config`
139
+ settings, permissions, evidence gates, cleanup behavior, and example
140
+ manager/Dispatch/worker interactions. Use `conveyor manager-recipes --list`
141
+ or `conveyor manager-recipes --show goalbuddy-conveyor --json` for a
142
+ machine-readable setup preview.
143
+ For a package-facing overview of these modes, open
144
+ [`docs/landing-page.html`](docs/landing-page.html) locally or host it as a
145
+ static landing page. From the repo, `npm run docs:landing` serves it at
146
+ `http://127.0.0.1:8765/`.
147
+
135
148
  After install, the intended Codex app entry point is natural language. Open a
136
149
  new Codex app session in the target repo and say:
137
150
 
@@ -163,6 +176,10 @@ Use `conveyor qa-plan adversarial-triggers` to verify natural-language
163
176
  manager prompts activate Ralph-loop adversarial gates.
164
177
  Use `conveyor qa-plan goalbuddy-conveyor` when a broad request should become
165
178
  sequential GoalBuddy child boards with PR/CI/merge receipts.
179
+ Before cutting a manager loose, have it resolve the freeform setup request to a
180
+ named recipe from `docs/manager-recipes.md` or an explicit `custom` setup, then
181
+ show the saved mode, permissions, evidence gates, cleanup policy, and disallowed
182
+ actions.
166
183
  For manual QA, launch the dashboard with Dispatch enforcement so the page can
167
184
  show live proof:
168
185
 
@@ -411,6 +428,11 @@ tmux attach -t codex-live-test
411
428
  Use `--require` when a manager command should fail closed. Use
412
429
  `--require-handoff` before worker compact/clear style instructions so visible
413
430
  context is persisted first.
431
+ - `manager-recipes --list|--show RECIPE [--json]` — List or show built-in
432
+ manager setup recipes. Recipe JSON includes the supervision mode,
433
+ permissions, expected tools, epilogues, evidence gates, cleanup behavior,
434
+ disallowed actions, locked setup summary template, and suggested
435
+ `manager-config` command. Use this before cutting a manager loose.
414
436
  - `worker-ack <task> --from-stdin|--json [--correlation-id ID]` /
415
437
  `manager-ack <task> --from-stdin|--json [--correlation-id ID]` — Persist or
416
438
  read the latest structured acknowledgement from the worker or manager. Acks
@@ -109,6 +109,9 @@ export function runTypescriptRuntimeCommand(options) {
109
109
  if (parsed.command === "ralph-loop-presets") {
110
110
  return runRalphLoopPresetsCommand(parsed, options);
111
111
  }
112
+ if (parsed.command === "manager-recipes") {
113
+ return runManagerRecipesCommand(parsed);
114
+ }
112
115
  if (parsed.command === "loop-triggers") {
113
116
  return runLoopTriggersCommand(parsed, options);
114
117
  }
@@ -499,6 +502,7 @@ function parseRuntimeArgs(args, env) {
499
502
  managerObjective: null,
500
503
  managerPermissionsJson: null,
501
504
  managerPermit: [],
505
+ managerRecipe: null,
502
506
  managerReference: [],
503
507
  managerRequireAcks: false,
504
508
  managerTool: [],
@@ -593,6 +597,7 @@ function parseRuntimeArgs(args, env) {
593
597
  && command !== "runs"
594
598
  && command !== "loop-templates"
595
599
  && command !== "ralph-loop-presets"
600
+ && command !== "manager-recipes"
596
601
  && command !== "loop-triggers"
597
602
  && command !== "manager-permission"
598
603
  && command !== "continuation"
@@ -946,7 +951,7 @@ function parseRuntimeArgs(args, env) {
946
951
  index += 1;
947
952
  }
948
953
  else if (arg === "--show") {
949
- if (command !== "runs" && command !== "loop-templates" && command !== "ralph-loop-presets") {
954
+ if (command !== "runs" && command !== "loop-templates" && command !== "ralph-loop-presets" && command !== "manager-recipes") {
950
955
  return { command, enabled, error: "Unsupported TypeScript runtime option: --show", explicit, flags, task };
951
956
  }
952
957
  const value = valueAfter(queue, index, arg);
@@ -1417,6 +1422,17 @@ function parseRuntimeArgs(args, env) {
1417
1422
  flags.managerMode = value.value;
1418
1423
  index += 1;
1419
1424
  }
1425
+ else if (arg === "--manager-recipe") {
1426
+ if (command !== "pair") {
1427
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --manager-recipe", explicit, flags, task };
1428
+ }
1429
+ const value = valueAfter(queue, index, arg);
1430
+ if (value.error) {
1431
+ return { command, enabled, error: value.error, explicit, flags, task };
1432
+ }
1433
+ flags.managerRecipe = value.value;
1434
+ index += 1;
1435
+ }
1420
1436
  else if (arg === "--manager-objective") {
1421
1437
  if (command !== "pair") {
1422
1438
  return { command, enabled, error: "Unsupported TypeScript runtime option: --manager-objective", explicit, flags, task };
@@ -1551,6 +1567,17 @@ function parseRuntimeArgs(args, env) {
1551
1567
  flags.managerObjective = value.value;
1552
1568
  index += 1;
1553
1569
  }
1570
+ else if (arg === "--recipe") {
1571
+ if (command !== "manager-config") {
1572
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --recipe", explicit, flags, task };
1573
+ }
1574
+ const value = valueAfter(queue, index, arg);
1575
+ if (value.error) {
1576
+ return { command, enabled, error: value.error, explicit, flags, task };
1577
+ }
1578
+ flags.managerRecipe = value.value;
1579
+ index += 1;
1580
+ }
1554
1581
  else if (arg === "--guideline") {
1555
1582
  if (command !== "manager-config") {
1556
1583
  return { command, enabled, error: "Unsupported TypeScript runtime option: --guideline", explicit, flags, task };
@@ -3110,6 +3137,43 @@ function runRalphLoopPresetsCommand(parsed, options) {
3110
3137
  database.close();
3111
3138
  }
3112
3139
  }
3140
+ function runManagerRecipesCommand(parsed) {
3141
+ const unsupportedOptions = unsupportedLoopCommandOptions(parsed, {
3142
+ allowedFlags: new Set(["json", "list", "show"]),
3143
+ commandName: "manager-recipes",
3144
+ });
3145
+ if (unsupportedOptions) {
3146
+ return unsupportedRuntimeResult(parsed, unsupportedOptions);
3147
+ }
3148
+ const actionCount = [parsed.flags.list, parsed.flags.show !== null].filter(Boolean).length;
3149
+ if (actionCount !== 1) {
3150
+ return errorResult("Choose one of --list or --show");
3151
+ }
3152
+ if (parsed.flags.list) {
3153
+ const recipes = listManagerRecipes();
3154
+ if (parsed.flags.json) {
3155
+ return jsonResult({ recipes });
3156
+ }
3157
+ return textResult(recipes.map((recipe) => {
3158
+ const loop = recipe.loop_template ? ` loop=${recipe.loop_template}` : "";
3159
+ return `${recipe.name}\tmode=${recipe.mode}${loop}\t${recipe.description}`;
3160
+ }));
3161
+ }
3162
+ const recipe = managerRecipeSummary(parsed.flags.show ?? "");
3163
+ if (parsed.flags.json) {
3164
+ return jsonResult({ recipe });
3165
+ }
3166
+ const lines = [
3167
+ String(recipe.locked_summary_template),
3168
+ "",
3169
+ "manager config command:",
3170
+ ` ${recipe.manager_config_command.map(shellQuote).join(" ")}`,
3171
+ ];
3172
+ if (recipe.loop_template) {
3173
+ lines.push("", `loop template: ${recipe.loop_template}`);
3174
+ }
3175
+ return textResult(lines);
3176
+ }
3113
3177
  function runLoopTriggersCommand(parsed, _options) {
3114
3178
  const unsupported = unsupportedMigratedProofCliOptions(parsed);
3115
3179
  if (unsupported) {
@@ -5733,6 +5797,7 @@ function runPairCommand(parsed, options) {
5733
5797
  managerObjective: parsed.flags.managerObjective,
5734
5798
  managerPermissionsJson: parsed.flags.managerPermissionsJson,
5735
5799
  managerPermit: parsed.flags.managerPermit,
5800
+ managerRecipe: parsed.flags.managerRecipe,
5736
5801
  managerReference: parsed.flags.managerReference,
5737
5802
  managerRequireAcks: parsed.flags.managerRequireAcks,
5738
5803
  managerTool: parsed.flags.managerTool,
@@ -6012,6 +6077,7 @@ function emitPairTelemetry(database, options) {
6012
6077
  function ensurePairManagerConfig(database, options) {
6013
6078
  const existing = managerConfigSync(database, options.taskId);
6014
6079
  const requested = options.managerMode !== null
6080
+ || options.managerRecipe !== null
6015
6081
  || options.managerObjective !== null
6016
6082
  || options.managerGuideline.length > 0
6017
6083
  || options.managerAcceptance.length > 0
@@ -6032,6 +6098,7 @@ function ensurePairManagerConfig(database, options) {
6032
6098
  if (supervisionMode !== "light" && supervisionMode !== "guided" && supervisionMode !== "strict") {
6033
6099
  throw new Error("manager_mode must be light, guided, or strict");
6034
6100
  }
6101
+ const recipeName = cleanManagerRecipeName(options.managerRecipe, existing?.recipe_name ?? (existing === null ? "custom" : null));
6035
6102
  const objective = options.managerObjective !== null ? options.managerObjective : existing?.objective ?? null;
6036
6103
  const guidelines = options.managerGuideline.length > 0 ? options.managerGuideline : existing?.guidelines ?? [];
6037
6104
  const acceptanceCriteria = options.managerAcceptance.length > 0
@@ -6052,13 +6119,14 @@ function ensurePairManagerConfig(database, options) {
6052
6119
  const requireAcks = options.managerRequireAcks || (existing?.require_acks ?? false);
6053
6120
  database.prepare(`
6054
6121
  insert into manager_configs(
6055
- task_id, supervision_mode, objective, guidelines_json,
6122
+ task_id, recipe_name, supervision_mode, objective, guidelines_json,
6056
6123
  acceptance_criteria_json, reference_paths_json, permissions_json,
6057
6124
  tools_json, epilogues_json, nudge_on_completion, require_acks,
6058
6125
  revision, created_at, updated_at
6059
6126
  )
6060
- values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
6127
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
6061
6128
  on conflict(task_id) do update set
6129
+ recipe_name = excluded.recipe_name,
6062
6130
  supervision_mode = excluded.supervision_mode,
6063
6131
  objective = excluded.objective,
6064
6132
  guidelines_json = excluded.guidelines_json,
@@ -6070,6 +6138,7 @@ function ensurePairManagerConfig(database, options) {
6070
6138
  nudge_on_completion = excluded.nudge_on_completion,
6071
6139
  require_acks = excluded.require_acks,
6072
6140
  revision = case when
6141
+ manager_configs.recipe_name is not excluded.recipe_name or
6073
6142
  manager_configs.supervision_mode is not excluded.supervision_mode or
6074
6143
  manager_configs.objective is not excluded.objective or
6075
6144
  manager_configs.guidelines_json is not excluded.guidelines_json or
@@ -6082,7 +6151,7 @@ function ensurePairManagerConfig(database, options) {
6082
6151
  manager_configs.require_acks is not excluded.require_acks
6083
6152
  then manager_configs.revision + 1 else manager_configs.revision end,
6084
6153
  updated_at = excluded.updated_at
6085
- `).run(options.taskId, supervisionMode, objective, stableJson(guidelines), stableJson(acceptanceCriteria), stableJson(referencePaths), stableJson(permissions), stableJson(tools), stableJson(epilogues), nudgeOnCompletion, requireAcks ? 1 : 0, options.timestamp, options.timestamp);
6154
+ `).run(options.taskId, recipeName, supervisionMode, objective, stableJson(guidelines), stableJson(acceptanceCriteria), stableJson(referencePaths), stableJson(permissions), stableJson(tools), stableJson(epilogues), nudgeOnCompletion, requireAcks ? 1 : 0, options.timestamp, options.timestamp);
6086
6155
  const config = managerConfigSync(database, options.taskId);
6087
6156
  if (config === null) {
6088
6157
  throw new Error(`manager config was not recorded for task ${options.taskId}`);
@@ -11801,6 +11870,7 @@ function insertCompatEventSync(database, options) {
11801
11870
  }
11802
11871
  function managerConfigMutationRequested(parsed) {
11803
11872
  return parsed.flags.managerMode !== null
11873
+ || parsed.flags.managerRecipe !== null
11804
11874
  || parsed.flags.managerObjective !== null
11805
11875
  || parsed.flags.managerGuideline.length > 0
11806
11876
  || parsed.flags.managerAcceptance.length > 0
@@ -11819,6 +11889,7 @@ function upsertManagerConfigFromParsed(database, options) {
11819
11889
  const parsed = options.parsed;
11820
11890
  const existing = options.existing;
11821
11891
  const supervisionMode = parsed.flags.managerMode ?? existing?.supervision_mode ?? "guided";
11892
+ const recipeName = cleanManagerRecipeName(parsed.flags.managerRecipe, existing?.recipe_name ?? null);
11822
11893
  const objective = parsed.flags.managerObjective !== null ? parsed.flags.managerObjective : existing?.objective ?? null;
11823
11894
  const guidelines = parsed.flags.managerGuideline.length > 0 ? parsed.flags.managerGuideline : existing?.guidelines ?? [];
11824
11895
  const acceptanceCriteria = parsed.flags.managerAcceptance.length > 0
@@ -11839,13 +11910,14 @@ function upsertManagerConfigFromParsed(database, options) {
11839
11910
  const requireAcks = parsed.flags.managerRequireAcks || (existing?.require_acks ?? false);
11840
11911
  database.prepare(`
11841
11912
  insert into manager_configs(
11842
- task_id, supervision_mode, objective, guidelines_json,
11913
+ task_id, recipe_name, supervision_mode, objective, guidelines_json,
11843
11914
  acceptance_criteria_json, reference_paths_json, permissions_json,
11844
11915
  tools_json, epilogues_json, nudge_on_completion, require_acks,
11845
11916
  revision, created_at, updated_at
11846
11917
  )
11847
- values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
11918
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
11848
11919
  on conflict(task_id) do update set
11920
+ recipe_name = excluded.recipe_name,
11849
11921
  supervision_mode = excluded.supervision_mode,
11850
11922
  objective = excluded.objective,
11851
11923
  guidelines_json = excluded.guidelines_json,
@@ -11857,6 +11929,7 @@ function upsertManagerConfigFromParsed(database, options) {
11857
11929
  nudge_on_completion = excluded.nudge_on_completion,
11858
11930
  require_acks = excluded.require_acks,
11859
11931
  revision = case when
11932
+ manager_configs.recipe_name is not excluded.recipe_name or
11860
11933
  manager_configs.supervision_mode is not excluded.supervision_mode or
11861
11934
  manager_configs.objective is not excluded.objective or
11862
11935
  manager_configs.guidelines_json is not excluded.guidelines_json or
@@ -11869,7 +11942,7 @@ function upsertManagerConfigFromParsed(database, options) {
11869
11942
  manager_configs.require_acks is not excluded.require_acks
11870
11943
  then manager_configs.revision + 1 else manager_configs.revision end,
11871
11944
  updated_at = excluded.updated_at
11872
- `).run(options.taskId, supervisionMode, objective, stableJson(guidelines), stableJson(acceptanceCriteria), stableJson(referencePaths), stableJson(permissions), stableJson(tools), stableJson(epilogues), nudgeOnCompletion, requireAcks ? 1 : 0, options.timestamp, options.timestamp);
11945
+ `).run(options.taskId, recipeName, supervisionMode, objective, stableJson(guidelines), stableJson(acceptanceCriteria), stableJson(referencePaths), stableJson(permissions), stableJson(tools), stableJson(epilogues), nudgeOnCompletion, requireAcks ? 1 : 0, options.timestamp, options.timestamp);
11873
11946
  const config = managerConfigSync(database, options.taskId);
11874
11947
  if (config === null) {
11875
11948
  throw new Error(`manager config was not recorded for task ${options.taskId}`);
@@ -11892,6 +11965,16 @@ function cleanManagerNudgeOnCompletion(value) {
11892
11965
  }
11893
11966
  return value;
11894
11967
  }
11968
+ function cleanManagerRecipeName(value, existing) {
11969
+ if (value === null) {
11970
+ return existing;
11971
+ }
11972
+ const normalized = normalizeManagerRecipeName(value);
11973
+ if (normalized === "custom") {
11974
+ return null;
11975
+ }
11976
+ return managerRecipeDefinition(normalized).name;
11977
+ }
11895
11978
  function managerPermissionWarnings(permissions) {
11896
11979
  const warnings = [];
11897
11980
  for (const [key, value] of Object.entries(permissions ?? {})) {
@@ -13418,6 +13501,7 @@ function isDefaultRuntimeCommand(command) {
13418
13501
  || command === "loop-templates"
13419
13502
  || command === "loop-triggers"
13420
13503
  || command === "ralph-loop-presets"
13504
+ || command === "manager-recipes"
13421
13505
  || command === "qa-plan"
13422
13506
  || command === "qa-run"
13423
13507
  || command === "start"
@@ -15170,6 +15254,232 @@ function ralphLoopPresetMetadata(name, options) {
15170
15254
  ralphLoopPreset(name);
15171
15255
  return loopTemplateMetadata(name, options);
15172
15256
  }
15257
+ const MANAGER_RECIPES = {
15258
+ "goalbuddy-conveyor": {
15259
+ acceptance: [
15260
+ "Every child board has PR/CI/merge, satisfied_on_main, or blocker proof.",
15261
+ "Parent state records final status for every child.",
15262
+ ],
15263
+ cleanup: "compact between child boards after saved handoff",
15264
+ description: "Run broad work as one parent GoalBuddy board with one active child board at a time.",
15265
+ disallowedActions: [
15266
+ "Do not run two child boards at once.",
15267
+ "Do not merge without green CI.",
15268
+ "Do not compact or clear before a saved handoff.",
15269
+ ],
15270
+ displayName: "GoalBuddy Conveyor",
15271
+ epilogues: ["draft-pr", "record-handoff"],
15272
+ evidenceGates: [
15273
+ "child receipt with focused verification",
15274
+ "adversarial review",
15275
+ "PR/CI/merge or satisfied_on_main proof",
15276
+ "parent receipt update before the next child",
15277
+ ],
15278
+ guidelines: [
15279
+ "Keep exactly one child board active at a time.",
15280
+ "Before activating the next child, update the parent receipt.",
15281
+ ],
15282
+ loopTemplate: null,
15283
+ mode: "strict",
15284
+ name: "goalbuddy-conveyor",
15285
+ objective: "Run a one-child-at-a-time GoalBuddy conveyor until every child is merged, proven satisfied, or blocked with evidence.",
15286
+ permissions: ["repo.open_pr", "repo.merge_green_pr", "worker_session.compact", "worker_session.clear"],
15287
+ supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
15288
+ tools: ["verification.run_tests", "context.fetch_prs"],
15289
+ },
15290
+ "nudge-whats-next": {
15291
+ acceptance: [
15292
+ "Accepted criteria are satisfied or explicitly deferred.",
15293
+ "The final summary names commands run, changed files, and residual risk.",
15294
+ ],
15295
+ cleanup: "off by default",
15296
+ description: "Observe, ask useful status questions, negotiate criteria, and keep permissions minimal.",
15297
+ disallowedActions: ["Do not grant repo or worker-session mutation permissions by default."],
15298
+ displayName: "Nudge / What's Next Manager",
15299
+ epilogues: [],
15300
+ evidenceGates: ["manager decision", "worker receipt", "accepted criteria closure"],
15301
+ guidelines: [
15302
+ "Prefer wait over nudge while the worker is active.",
15303
+ "Ask for must-have current-task criteria versus follow-ups when scope changes.",
15304
+ ],
15305
+ loopTemplate: null,
15306
+ mode: "guided",
15307
+ name: "nudge-whats-next",
15308
+ objective: "Observe the worker, ask useful status and next-step questions, and finish only with evidence.",
15309
+ permissions: [],
15310
+ supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
15311
+ tools: [],
15312
+ },
15313
+ "pr-ci-merge-ralph-loop": {
15314
+ acceptance: [
15315
+ "PR URL, green CI, merge receipt, and adversarial proof are recorded.",
15316
+ "Worker handoff exists before compact or clear.",
15317
+ ],
15318
+ cleanup: "clear after saved handoff",
15319
+ description: "Drive delivery through PR readiness, CI, merge, handoff, and worker clear receipts.",
15320
+ disallowedActions: [
15321
+ "Do not open PRs before repo.open_pr is permitted.",
15322
+ "Do not merge before repo.merge_green_pr is permitted and CI is green.",
15323
+ "Do not clear before a saved handoff.",
15324
+ ],
15325
+ displayName: "PR/CI/Merge Ralph Loop",
15326
+ epilogues: ["draft-pr", "record-handoff"],
15327
+ evidenceGates: ["pr_url", "ci_green", "merge", "adversarial_check"],
15328
+ guidelines: ["Merge only after green CI and recorded manager decision evidence."],
15329
+ loopTemplate: "pr_ci_merge_loop",
15330
+ mode: "strict",
15331
+ name: "pr-ci-merge-ralph-loop",
15332
+ objective: "Drive the worker through PR readiness, CI, merge, handoff, and clear receipts.",
15333
+ permissions: ["repo.open_pr", "repo.merge_green_pr", "worker_session.compact", "worker_session.clear"],
15334
+ supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
15335
+ tools: ["verification.run_tests", "context.fetch_prs"],
15336
+ },
15337
+ "test-coverage-loop": {
15338
+ acceptance: [
15339
+ "Coverage or targeted test evidence is recorded before another worker pass.",
15340
+ "Structured adversarial proof names the strongest realistic failure mode.",
15341
+ ],
15342
+ cleanup: "clear by default",
15343
+ description: "Improve or prove test confidence with coverage evidence before another pass.",
15344
+ disallowedActions: ["Do not continue after only generic tests-passed text."],
15345
+ displayName: "Test Coverage Loop",
15346
+ epilogues: [],
15347
+ evidenceGates: ["test_coverage", "adversarial_check"],
15348
+ guidelines: ["Record coverage evidence before asking for another worker pass."],
15349
+ loopTemplate: "test_coverage_loop",
15350
+ mode: "strict",
15351
+ name: "test-coverage-loop",
15352
+ objective: "Improve or prove test coverage for the requested behavior.",
15353
+ permissions: ["worker_session.compact", "worker_session.clear"],
15354
+ supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
15355
+ tools: ["verification.run_tests"],
15356
+ },
15357
+ "ux-polish-loop": {
15358
+ acceptance: [
15359
+ "Reference artifact, candidate screenshot, visual diff report, and below-threshold evidence are recorded.",
15360
+ "Structured adversarial proof is recorded before another visual pass.",
15361
+ ],
15362
+ cleanup: "compact by default",
15363
+ description: "Iterate on visible UI quality using browser, screenshot, and visual-diff evidence.",
15364
+ disallowedActions: ["Do not approve a visual pass without screenshot or browser evidence."],
15365
+ displayName: "UX Polish Loop",
15366
+ epilogues: [],
15367
+ evidenceGates: [
15368
+ "reference_artifact",
15369
+ "candidate_screenshot",
15370
+ "visual_diff_report",
15371
+ "diff_below_threshold",
15372
+ "adversarial_check",
15373
+ ],
15374
+ guidelines: ["Compare visible output against references before requesting another pass."],
15375
+ loopTemplate: "visual_diff_loop",
15376
+ mode: "guided",
15377
+ name: "ux-polish-loop",
15378
+ objective: "Iterate on visible UI quality using browser and screenshot evidence.",
15379
+ permissions: ["worker_session.compact", "worker_session.clear"],
15380
+ supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
15381
+ tools: ["verification.run_playwright"],
15382
+ },
15383
+ };
15384
+ const MANAGER_RECIPE_ALIASES = {
15385
+ "goalbuddy conveyor": "goalbuddy-conveyor",
15386
+ goalbuddy: "goalbuddy-conveyor",
15387
+ "nudge / what's next manager": "nudge-whats-next",
15388
+ "nudge whats next": "nudge-whats-next",
15389
+ "pr ci merge ralph loop": "pr-ci-merge-ralph-loop",
15390
+ "pr/ci/merge ralph loop": "pr-ci-merge-ralph-loop",
15391
+ "ralph loop": "pr-ci-merge-ralph-loop",
15392
+ "test coverage": "test-coverage-loop",
15393
+ "test coverage loop": "test-coverage-loop",
15394
+ "ux polish": "ux-polish-loop",
15395
+ "ux polish loop": "ux-polish-loop",
15396
+ "visual polish": "ux-polish-loop",
15397
+ "what's next": "nudge-whats-next",
15398
+ "whats next": "nudge-whats-next",
15399
+ };
15400
+ function listManagerRecipes() {
15401
+ return Object.keys(MANAGER_RECIPES).sort().map((name) => managerRecipeSummary(name));
15402
+ }
15403
+ function managerRecipeDefinition(name) {
15404
+ const key = normalizeManagerRecipeName(name);
15405
+ const recipe = MANAGER_RECIPES[key];
15406
+ if (!recipe) {
15407
+ throw new Error(`Unknown manager recipe: ${name}; expected one of: ${Object.keys(MANAGER_RECIPES).sort().join(", ")}`);
15408
+ }
15409
+ return recipe;
15410
+ }
15411
+ function normalizeManagerRecipeName(name) {
15412
+ const normalized = name.trim().toLowerCase().split(/\s+/).join(" ");
15413
+ return MANAGER_RECIPE_ALIASES[normalized] ?? normalized.replace(/_/g, "-").replace(/ /g, "-");
15414
+ }
15415
+ function managerRecipeSummary(name) {
15416
+ const recipe = managerRecipeDefinition(name);
15417
+ return {
15418
+ acceptance: [...recipe.acceptance],
15419
+ cleanup: recipe.cleanup,
15420
+ description: recipe.description,
15421
+ disallowed_actions: [...recipe.disallowedActions],
15422
+ display_name: recipe.displayName,
15423
+ epilogues: [...recipe.epilogues],
15424
+ evidence_gates: [...recipe.evidenceGates],
15425
+ guidelines: [...recipe.guidelines],
15426
+ locked_summary_template: lockedManagerRecipeSummary(recipe),
15427
+ loop_template: recipe.loopTemplate,
15428
+ manager_config_command: managerRecipeConfigCommand(recipe),
15429
+ mode: recipe.mode,
15430
+ name: recipe.name,
15431
+ objective: recipe.objective,
15432
+ permissions: [...recipe.permissions],
15433
+ support_patterns: [...recipe.supportPatterns],
15434
+ tools: [...recipe.tools],
15435
+ };
15436
+ }
15437
+ function managerRecipeConfigCommand(recipe, taskPlaceholder = "<task>") {
15438
+ const command = ["conveyor", "manager-config", taskPlaceholder, "--mode", recipe.mode, "--objective", recipe.objective];
15439
+ for (const guideline of recipe.guidelines) {
15440
+ command.push("--guideline", guideline);
15441
+ }
15442
+ for (const acceptance of recipe.acceptance) {
15443
+ command.push("--acceptance", acceptance);
15444
+ }
15445
+ const permissions = new Set(recipe.permissions);
15446
+ if (permissions.has("worker_session.compact") && permissions.has("worker_session.clear")) {
15447
+ command.push("--allow-worker-compact-clear");
15448
+ permissions.delete("worker_session.compact");
15449
+ permissions.delete("worker_session.clear");
15450
+ }
15451
+ if (permissions.has("repo.open_pr")) {
15452
+ command.push("--allow-pr");
15453
+ permissions.delete("repo.open_pr");
15454
+ }
15455
+ if (permissions.has("repo.merge_green_pr")) {
15456
+ command.push("--allow-merge-green");
15457
+ permissions.delete("repo.merge_green_pr");
15458
+ }
15459
+ for (const permission of [...permissions].sort()) {
15460
+ command.push("--permit", permission);
15461
+ }
15462
+ for (const tool of recipe.tools) {
15463
+ command.push("--tool", tool);
15464
+ }
15465
+ for (const epilogue of recipe.epilogues) {
15466
+ command.push("--epilogue", epilogue);
15467
+ }
15468
+ return command;
15469
+ }
15470
+ function lockedManagerRecipeSummary(recipe) {
15471
+ return [
15472
+ `Selected recipe: ${recipe.displayName}`,
15473
+ `Mode: ${recipe.mode}`,
15474
+ `Permissions: ${recipe.permissions.length > 0 ? recipe.permissions.join(", ") : "none"}`,
15475
+ `Tools: ${recipe.tools.length > 0 ? recipe.tools.join(", ") : "none"}`,
15476
+ `Epilogues: ${recipe.epilogues.length > 0 ? recipe.epilogues.join(", ") : "none"}`,
15477
+ `Cleanup: ${recipe.cleanup}`,
15478
+ `Evidence gates: ${recipe.evidenceGates.length > 0 ? recipe.evidenceGates.join(", ") : "manager-reviewed evidence"}`,
15479
+ `Not allowed: ${recipe.disallowedActions.length > 0 ? recipe.disallowedActions.join("; ") : "unconfirmed custom actions"}`,
15480
+ "User confirmed: <yes|no>",
15481
+ ].join("\n");
15482
+ }
15173
15483
  const LOOP_TRIGGERS = [
15174
15484
  {
15175
15485
  acceptance: "Create or reuse a loop policy whose required_before_continue includes adversarial_check.",