@code-partner/codepipe-hub 0.14.1-dev.399.g96d6330f → 0.14.1-dev.405.gb14c9aa1

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.
@@ -27,7 +27,7 @@
27
27
  localStorage.getItem('theme') ?? 'dark'
28
28
  );
29
29
  </script>
30
- <script type="module" crossorigin src="/assets/index-Cpy8aKZb.js"></script>
30
+ <script type="module" crossorigin src="/assets/index-T-kK12wa.js"></script>
31
31
  <link rel="stylesheet" crossorigin href="/assets/index-CD8JrtmW.css">
32
32
  </head>
33
33
  <body>
package/hub/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var PLATFORM_VERSION;
13
13
  var init_version_generated = __esm({
14
14
  "../packages/shared/dist/version.generated.js"() {
15
15
  "use strict";
16
- PLATFORM_VERSION = "0.14.1-dev.399.g96d6330f";
16
+ PLATFORM_VERSION = "0.14.1-dev.405.gb14c9aa1";
17
17
  }
18
18
  });
19
19
 
@@ -394,22 +394,25 @@ var init_job = __esm({
394
394
  });
395
395
 
396
396
  // ../packages/shared/dist/llm-models.js
397
+ function modelForHarness(model, harness) {
398
+ if (!harness)
399
+ return model;
400
+ return HARNESS_MODEL_LADDER[harness]?.[model] ?? model;
401
+ }
397
402
  function parseWorkflowPreset(v) {
398
403
  return typeof v === "string" && WORKFLOW_PRESETS.includes(v.trim()) ? v.trim() : null;
399
404
  }
400
405
  function emptyModelConfig() {
401
406
  return { default: null, roles: {} };
402
407
  }
403
- function resolveModelForRole(cfg, role, preset = DEFAULT_WORKFLOW_PRESET) {
408
+ function resolveModelForRole(cfg, role, preset = DEFAULT_WORKFLOW_PRESET, harness) {
404
409
  const explicit = cfg?.roles?.[role];
405
410
  if (typeof explicit === "string" && explicit.trim().length > 0)
406
411
  return explicit.trim();
407
412
  if (cfg?.default && cfg.default.trim().length > 0)
408
413
  return cfg.default.trim();
409
414
  const presetModel = PRESET_ROLE_MODELS[preset]?.[role];
410
- if (presetModel)
411
- return presetModel;
412
- return RECOMMENDED_ROLE_MODELS[role] ?? FALLBACK_MODEL;
415
+ return modelForHarness(presetModel ?? RECOMMENDED_ROLE_MODELS[role] ?? FALLBACK_MODEL, harness);
413
416
  }
414
417
  function resolveProvider(cfg) {
415
418
  const p = cfg?.provider;
@@ -532,7 +535,7 @@ function deserializeModelConfig(json) {
532
535
  return emptyModelConfig();
533
536
  }
534
537
  }
535
- var DEFAULT_LLM_PROVIDER, LLM_PROVIDER_SUGGESTIONS, MODEL_OPUS, MODEL_SONNET, MODEL_HAIKU, MODEL_ALIAS_SUGGESTIONS, BUILD_DISCOVERY, CODE_REVIEW_VERIFY, MODEL_SUB_ROLES, ALL_MODEL_ROLE_KEYS, RECOMMENDED_ROLE_MODELS, FALLBACK_MODEL, WORKFLOW_PRESETS, DEFAULT_WORKFLOW_PRESET, PRESET_ROLE_MODELS, DEFAULT_REGIMEN_ALIASES, EFFORT_LEVELS, EFFORT_LEVEL_SUGGESTIONS, RECOMMENDED_ROLE_EFFORT;
538
+ var DEFAULT_LLM_PROVIDER, LLM_PROVIDER_SUGGESTIONS, MODEL_OPUS, MODEL_SONNET, MODEL_HAIKU, MODEL_ALIAS_SUGGESTIONS, BUILD_DISCOVERY, CODE_REVIEW_VERIFY, MODEL_SUB_ROLES, ALL_MODEL_ROLE_KEYS, RECOMMENDED_ROLE_MODELS, FALLBACK_MODEL, HARNESS_MODEL_LADDER, WORKFLOW_PRESETS, DEFAULT_WORKFLOW_PRESET, PRESET_ROLE_MODELS, DEFAULT_REGIMEN_ALIASES, EFFORT_LEVELS, EFFORT_LEVEL_SUGGESTIONS, RECOMMENDED_ROLE_EFFORT;
536
539
  var init_llm_models = __esm({
537
540
  "../packages/shared/dist/llm-models.js"() {
538
541
  "use strict";
@@ -604,6 +607,13 @@ var init_llm_models = __esm({
604
607
  testing: MODEL_OPUS
605
608
  };
606
609
  FALLBACK_MODEL = MODEL_OPUS;
610
+ HARNESS_MODEL_LADDER = {
611
+ codex: {
612
+ [MODEL_OPUS]: "gpt-5.6-sol",
613
+ [MODEL_SONNET]: "gpt-5.6-terra",
614
+ [MODEL_HAIKU]: "gpt-5.6-terra"
615
+ }
616
+ };
607
617
  WORKFLOW_PRESETS = ["express", "balanced", "full"];
608
618
  DEFAULT_WORKFLOW_PRESET = "full";
609
619
  PRESET_ROLE_MODELS = {
@@ -711,6 +721,78 @@ var init_actions = __esm({
711
721
  }
712
722
  });
713
723
 
724
+ // ../packages/shared/dist/capability-registry.js
725
+ function harnessesForBackend(llm) {
726
+ switch (llm) {
727
+ case "claude-headless":
728
+ return ["claude-code"];
729
+ case "codex":
730
+ return ["codex"];
731
+ case "mock":
732
+ return ["mock", "claude-code", "codex"];
733
+ default:
734
+ return [];
735
+ }
736
+ }
737
+ var CAPABILITY_REGISTRY, SELECTABLE_HARNESSES;
738
+ var init_capability_registry = __esm({
739
+ "../packages/shared/dist/capability-registry.js"() {
740
+ "use strict";
741
+ CAPABILITY_REGISTRY = [
742
+ {
743
+ harness: "claude-code",
744
+ endpoint: "anthropic-managed",
745
+ modelPattern: "*",
746
+ credentialKinds: ["subscription_oauth", "api_key"],
747
+ status: "supported",
748
+ capabilities: {
749
+ headless: true,
750
+ // Claude Code can resume, but the product deliberately does not glue
751
+ // sessions across jobs (DEV-682) — so no resume is advertised.
752
+ resume: false,
753
+ streamEvents: true,
754
+ usage: "tokens",
755
+ toolPolicy: "vendor"
756
+ },
757
+ testedWith: { runtimeVersion: "claude-code 2.x (stream-json)", protocolVersion: 7 }
758
+ },
759
+ {
760
+ harness: "codex",
761
+ endpoint: "openai-managed",
762
+ modelPattern: "*",
763
+ credentialKinds: ["subscription_oauth", "api_key"],
764
+ // EXPERIMENTAL until a cross-harness eval run compares it with the Claude
765
+ // path on the same corpus (§12) — claiming "supported" before that would
766
+ // be a promise nothing backs.
767
+ status: "experimental",
768
+ capabilities: {
769
+ headless: true,
770
+ // The CLI can resume a session, but the adapter exposes no resume() and
771
+ // the product does not glue sessions across JOBS — the registry must
772
+ // say what the ADAPTER can do, or a job requiring resume would be
773
+ // matched to a worker that cannot. (DEV-897 uses `exec resume` WITHIN a
774
+ // chat job; that is one session, not glue between jobs.)
775
+ resume: false,
776
+ streamEvents: true,
777
+ usage: "quota",
778
+ toolPolicy: "vendor"
779
+ },
780
+ testedWith: { runtimeVersion: "codex-cli 0.152 (exec --json, exec resume)", protocolVersion: 7 }
781
+ },
782
+ {
783
+ harness: "mock",
784
+ endpoint: "none",
785
+ modelPattern: "*",
786
+ credentialKinds: [],
787
+ status: "supported",
788
+ capabilities: { headless: true, resume: false, streamEvents: false, usage: "none", toolPolicy: "vendor" },
789
+ testedWith: { runtimeVersion: "built-in stub", protocolVersion: 7 }
790
+ }
791
+ ];
792
+ SELECTABLE_HARNESSES = CAPABILITY_REGISTRY.filter((e) => e.harness !== "mock").map((e) => e.harness);
793
+ }
794
+ });
795
+
714
796
  // ../packages/shared/dist/project.js
715
797
  function slugify(input) {
716
798
  const lowered = input.toLowerCase();
@@ -1162,7 +1244,9 @@ var init_project_settings = __esm({
1162
1244
  "../packages/shared/dist/project-settings.js"() {
1163
1245
  "use strict";
1164
1246
  init_actions();
1247
+ init_capability_registry();
1165
1248
  init_git_flow();
1249
+ init_job();
1166
1250
  init_llm_models();
1167
1251
  init_project_map();
1168
1252
  init_project_language();
@@ -1313,10 +1397,58 @@ var init_project_settings = __esm({
1313
1397
  // DEV-896: the old text ("currently only claude is available") outlived
1314
1398
  // its truth and read as "there is nothing to choose" — the one thing
1315
1399
  // help must never say about a live control.
1316
- help: "The agent harness every role runs on by default. Per-role overrides (and the fallback agent) live in the shared config's routing block."
1400
+ // DEV-934: per-role agents are a control now, not a YAML instruction
1401
+ // point at it, and say which of the two wins, because they live in
1402
+ // different files and one is personal.
1403
+ help: "The agent every role runs on by default \u2014 your own choice, not the team's. \xABAgent by role\xBB below is shared and outranks it."
1317
1404
  }
1318
1405
  ],
1319
1406
  groups: [
1407
+ {
1408
+ // DEV-934: the model was pickable per role from the start (below) and
1409
+ // the AGENT was not — a project mixing vendors (analysis on one,
1410
+ // implementation on the other) could only be expressed by hand-editing
1411
+ // project.yaml, which put the whole DEV-759 routing feature out of
1412
+ // reach of anyone who does not edit YAML.
1413
+ id: "routing",
1414
+ label: "Agent by role",
1415
+ help: "Which agent runs which stage. Shared with the team (project.yaml). Empty \u21D2 the role follows the default below it, then your own provider setting above.",
1416
+ fields: [
1417
+ {
1418
+ id: "routing.default.harness",
1419
+ file: "project.yaml",
1420
+ path: ["routing", "default", "harness"],
1421
+ type: "enum",
1422
+ label: "default (whole project)",
1423
+ nullable: true,
1424
+ suggestions: [...SELECTABLE_HARNESSES],
1425
+ help: "The team-wide default. Set, it outranks each member's own provider setting."
1426
+ },
1427
+ ...ALL_JOB_ROLES.map((role) => ({
1428
+ id: `routing.${role}.harness`,
1429
+ file: "project.yaml",
1430
+ path: ["routing", role, "harness"],
1431
+ type: "enum",
1432
+ label: role,
1433
+ nullable: true,
1434
+ suggestions: [...SELECTABLE_HARNESSES],
1435
+ help: "Empty \u21D2 this role follows the default."
1436
+ })),
1437
+ {
1438
+ id: "routing.fallback.harness",
1439
+ file: "project.yaml",
1440
+ path: ["routing", "fallback", "harness"],
1441
+ type: "enum",
1442
+ label: "fallback (retry after a vendor failure)",
1443
+ nullable: true,
1444
+ suggestions: [...SELECTABLE_HARNESSES],
1445
+ // DEV-759 §9: the ONE sanctioned way to change vendor mid-task, and
1446
+ // only for infrastructure trouble — a wrong result is a rework, not
1447
+ // a reason to try the other vendor.
1448
+ help: "Where a retry goes when the vendor itself was unusable (rate limit, 5xx). Empty \u21D2 retries stay on the same agent."
1449
+ }
1450
+ ]
1451
+ },
1320
1452
  {
1321
1453
  id: "llm.models",
1322
1454
  label: "Models by role",
@@ -2988,25 +3120,6 @@ var init_credential_broker = __esm({
2988
3120
  }
2989
3121
  });
2990
3122
 
2991
- // ../packages/shared/dist/capability-registry.js
2992
- function harnessesForBackend(llm) {
2993
- switch (llm) {
2994
- case "claude-headless":
2995
- return ["claude-code"];
2996
- case "codex":
2997
- return ["codex"];
2998
- case "mock":
2999
- return ["mock", "claude-code", "codex"];
3000
- default:
3001
- return [];
3002
- }
3003
- }
3004
- var init_capability_registry = __esm({
3005
- "../packages/shared/dist/capability-registry.js"() {
3006
- "use strict";
3007
- }
3008
- });
3009
-
3010
3123
  // ../packages/shared/dist/structured-llm.js
3011
3124
  var init_structured_llm = __esm({
3012
3125
  "../packages/shared/dist/structured-llm.js"() {
@@ -3434,6 +3547,7 @@ function pgJobRepository(db, d = postgresDialect) {
3434
3547
  const harnessClause = q.harnesses.length > 0 ? `AND (${harnessField} IS NULL OR ${harnessField} IN (${q.harnesses.map(() => "?").join(",")}))` : `AND ${harnessField} IS NULL`;
3435
3548
  const heldClause = q.heldTaskIds.length > 0 ? `AND NOT (t.id IS NOT NULL AND j.role NOT IN (${CLUSTER_GATE_EXEMPT_SQL})
3436
3549
  AND t.id IN (${q.heldTaskIds.map(() => "?").join(",")}))` : "";
3550
+ const heldProjectClause = q.heldProjectIds.length > 0 ? `AND j.project_id NOT IN (${q.heldProjectIds.map(() => "?").join(",")})` : "";
3437
3551
  return await db.get(`SELECT ${jobColumns("j.")}
3438
3552
  FROM jobs j
3439
3553
  JOIN projects p ON p.id = j.project_id
@@ -3535,6 +3649,7 @@ function pgJobRepository(db, d = postgresDialect) {
3535
3649
  -- code-review (DEV-686: they judge the task's own delivery) are
3536
3650
  -- never gated. Empty held set this tick -> the clause is omitted.
3537
3651
  ${heldClause}
3652
+ ${heldProjectClause}
3538
3653
  -- DEV-686: eager build/code-review fill FREE slots only \u2014 an unheld
3539
3654
  -- implement-family job (the cluster's critical path) always outranks
3540
3655
  -- them; within a rank it stays FIFO.
@@ -3545,7 +3660,7 @@ function pgJobRepository(db, d = postgresDialect) {
3545
3660
  -- same queued row before either updates it. Lock the candidate (jobs
3546
3661
  -- side only \u2014 the LEFT-JOINed tasks row may be null) and let a
3547
3662
  -- parallel claimer skip past it instead of blocking.
3548
- ${d.claimLock}`, ...projectFilter.params, ...ownerFilter.params, ...laneFilter.params, ...q.harnesses, q.now, ...roleParams, ...roleParams, PLANNING_LANE_MAX_JOBS, ...implCap.params, ...q.heldTaskIds);
3663
+ ${d.claimLock}`, ...projectFilter.params, ...ownerFilter.params, ...laneFilter.params, ...q.harnesses, q.now, ...roleParams, ...roleParams, PLANNING_LANE_MAX_JOBS, ...implCap.params, ...q.heldTaskIds, ...q.heldProjectIds);
3549
3664
  },
3550
3665
  async markClaimed(jobId, sandboxId, executionId, now) {
3551
3666
  await db.run(`UPDATE jobs
@@ -6457,6 +6572,17 @@ function mergeSandboxCapabilities(stored, live) {
6457
6572
  }
6458
6573
  return merged;
6459
6574
  }
6575
+ async function claimCapabilities(db, sandboxId, stored, live) {
6576
+ const merged = mergeSandboxCapabilities(stored, live);
6577
+ if (JSON.stringify(merged) !== JSON.stringify(stored)) {
6578
+ await db.run(
6579
+ `UPDATE sandboxes SET capabilities_json = ? WHERE id = ?`,
6580
+ JSON.stringify(merged),
6581
+ sandboxId
6582
+ );
6583
+ }
6584
+ return merged;
6585
+ }
6460
6586
  async function refreshSandboxCapabilities(db, sandboxId, live) {
6461
6587
  const row = await db.get(
6462
6588
  `SELECT capabilities_json as capabilitiesJson FROM sandboxes WHERE id = ?`,
@@ -10691,6 +10817,63 @@ async function heldByTrancheDeferral(db) {
10691
10817
  );
10692
10818
  return rows.map((r) => r.id);
10693
10819
  }
10820
+ async function resolveJobAxes(db, projectId, role, taskWorkflowPreset) {
10821
+ const mc = await loadModelConfig(db, projectId);
10822
+ const provider = resolveProvider(mc);
10823
+ const { preset: projectPreset } = await loadWorkflowSettings(db, projectId);
10824
+ const workflowPreset = parseWorkflowPreset(taskWorkflowPreset) ?? projectPreset;
10825
+ const routing = resolveRoutingForRole(await loadRoutingConfig(db, projectId), role, {
10826
+ defaultHarness: harnessForProvider(provider)
10827
+ });
10828
+ return {
10829
+ routing,
10830
+ provider: HARNESS_PROVIDER[routing.harness] ?? provider,
10831
+ model: routing.model || resolveModelForRole(mc, role, workflowPreset, routing.harness),
10832
+ modelDiscovery: role === "build" ? resolveModelForRole(mc, "build-discovery", workflowPreset, routing.harness) : void 0
10833
+ };
10834
+ }
10835
+ function axesOnSpec(axes) {
10836
+ return {
10837
+ harness: axes.routing.harness,
10838
+ model: axes.model,
10839
+ endpoint: axes.routing.endpoint,
10840
+ account: axes.routing.account,
10841
+ // The harness a run needs is a capability of the worker (DEV-758 matches
10842
+ // on these).
10843
+ requiredCapabilities: [`harness:${axes.routing.harness}`, `provider:${axes.provider}`]
10844
+ };
10845
+ }
10846
+ async function restampQueuedJobs(db, projectId) {
10847
+ const rows = await db.all(
10848
+ `SELECT id, role, task_id AS taskId, spec_json AS specJson
10849
+ FROM jobs WHERE project_id = ? AND state = 'queued'`,
10850
+ projectId
10851
+ );
10852
+ let restamped = 0;
10853
+ for (const row of rows) {
10854
+ let spec;
10855
+ try {
10856
+ spec = JSON.parse(row.specJson);
10857
+ } catch {
10858
+ continue;
10859
+ }
10860
+ if (!spec.executionSpec) continue;
10861
+ const task = await getTask(db, row.taskId);
10862
+ const axes = await resolveJobAxes(db, projectId, row.role, task?.workflowPreset);
10863
+ const next = {
10864
+ ...spec,
10865
+ model: axes.model,
10866
+ provider: axes.provider,
10867
+ ...axes.modelDiscovery ? { modelDiscovery: axes.modelDiscovery } : {},
10868
+ executionSpec: { ...spec.executionSpec, ...axesOnSpec(axes) }
10869
+ };
10870
+ const nextJson = JSON.stringify(next);
10871
+ if (nextJson === row.specJson) continue;
10872
+ await engineStore(db).jobs.patchSpec(row.id, nextJson);
10873
+ restamped += 1;
10874
+ }
10875
+ return restamped;
10876
+ }
10694
10877
  async function createJob(db, input) {
10695
10878
  const id = `job_${nanoid12(16)}`;
10696
10879
  const now = Date.now();
@@ -10707,16 +10890,12 @@ async function createJob(db, input) {
10707
10890
  if (!isAutoLanguage(responseLanguage)) metadata["responseLanguage"] = responseLanguage;
10708
10891
  const attachDeliveries = input.role === "build" || input.role === "code-review" || input.attachDeliveries === true || input.role === "implement" && ((input.task.buildIteration ?? 0) > 0 || (input.task.reviewIteration ?? 0) > 0);
10709
10892
  const deliveries = attachDeliveries ? Object.entries(await loadDeliveries(db, input.task.id)).map(([name, d]) => ({ name, ...d })) : [];
10710
- const mc = await loadModelConfig(db, input.projectId);
10711
- const { preset: projectPreset } = await loadWorkflowSettings(db, input.projectId);
10712
- const workflowPreset = parseWorkflowPreset(input.task.workflowPreset) ?? projectPreset;
10713
- const model = resolveModelForRole(mc, input.role, workflowPreset);
10893
+ const axes = await resolveJobAxes(db, input.projectId, input.role, input.task.workflowPreset);
10894
+ const { modelDiscovery } = axes;
10714
10895
  const skillsRow = await db.get(
10715
10896
  `SELECT skills_json AS json FROM projects WHERE id = ?`,
10716
10897
  input.projectId
10717
10898
  );
10718
- const provider = resolveProvider(mc);
10719
- const modelDiscovery = input.role === "build" ? resolveModelForRole(mc, "build-discovery", workflowPreset) : void 0;
10720
10899
  const ec = await loadEffortConfig(db, input.projectId);
10721
10900
  const effort = resolveEffortForRole(ec, input.role);
10722
10901
  const effortDiscovery = input.role === "build" ? resolveEffortForRole(ec, "build-discovery") : void 0;
@@ -10725,23 +10904,13 @@ async function createJob(db, input) {
10725
10904
  const isReviewVerify = input.metadata?.["reviewVerify"] === true || input.metadata?.["externalReviewVerify"] === true;
10726
10905
  const councilPhase = input.role === "council" ? "questions" : input.role === "code-review" && !isReviewVerify ? "codeReview" : null;
10727
10906
  const council = councilPhase ? await loadCouncilSettings(db, input.projectId) : null;
10728
- const routing = resolveRoutingForRole(await loadRoutingConfig(db, input.projectId), input.role, {
10729
- defaultHarness: harnessForProvider(provider)
10730
- });
10731
- const effectiveModel = routing.model || model;
10732
- const effectiveProvider = HARNESS_PROVIDER[routing.harness] ?? provider;
10907
+ const { model: effectiveModel, provider: effectiveProvider } = axes;
10733
10908
  const executionSpec = {
10734
10909
  schemaVersion: EXECUTION_SPEC_SCHEMA_VERSION,
10735
10910
  jobId: id,
10736
10911
  role: input.role,
10737
- harness: routing.harness,
10738
- model: effectiveModel,
10739
- endpoint: routing.endpoint,
10740
- account: routing.account,
10741
- budgets: {},
10742
- // The harness a run needs is a capability of the worker (DEV-758 matches
10743
- // on these).
10744
- requiredCapabilities: [`harness:${routing.harness}`, `provider:${effectiveProvider}`]
10912
+ ...axesOnSpec(axes),
10913
+ budgets: {}
10745
10914
  };
10746
10915
  const spec = {
10747
10916
  jobId: id,
@@ -10879,6 +11048,14 @@ async function claimNextJob(db, sandboxId, capabilities, lane) {
10879
11048
  for (const id of await heldByClusterGate(db, projectId)) heldIds.push(id);
10880
11049
  }
10881
11050
  for (const id of await heldByTrancheDeferral(db)) heldIds.push(id);
11051
+ const heldProjectIds = [];
11052
+ const queuedProjects = await db.all(`SELECT DISTINCT j.project_id AS projectId
11053
+ FROM jobs j JOIN projects p ON p.id = j.project_id
11054
+ WHERE j.state = 'queued'
11055
+ AND ${projectFilter.clause} AND ${ownerFilter.clause} AND ${laneFilter.clause}`, ...projectFilter.params, ...ownerFilter.params, ...laneFilter.params);
11056
+ for (const { projectId } of queuedProjects) {
11057
+ if ((await unpinnedSourceRepos(db, projectId)).length > 0) heldProjectIds.push(projectId);
11058
+ }
10882
11059
  const claimed = await claimJob({
10883
11060
  store: pgEngineStore(db),
10884
11061
  audit: async (tx, e) => {
@@ -10952,7 +11129,7 @@ async function claimNextJob(db, sandboxId, capabilities, lane) {
10952
11129
  }
10953
11130
  return changed ? JSON.stringify(spec2) : null;
10954
11131
  }
10955
- }, { ...claimInput, heldTaskIds: heldIds, now: Date.now() });
11132
+ }, { ...claimInput, heldTaskIds: heldIds, heldProjectIds, now: Date.now() });
10956
11133
  if (!claimed) return null;
10957
11134
  const spec = JSON.parse(claimed.job.specJson);
10958
11135
  spec.executionId = claimed.executionId;
@@ -16569,7 +16746,7 @@ async function enqueueExternalReviewVerifyJob(db, task, meta, firstVerdict, log)
16569
16746
  metadata: { externalReviewVerify: true, reviewPanel: false, externalReview: true }
16570
16747
  });
16571
16748
  const patched = await patchSpecForExternalReview(db, task.projectId, spec, meta, {});
16572
- patched.model = resolveModelForRole(await loadModelConfig(db, task.projectId), "code-review-verify", (await loadWorkflowSettings(db, task.projectId)).preset);
16749
+ patched.model = resolveModelForRole(await loadModelConfig(db, task.projectId), "code-review-verify", (await loadWorkflowSettings(db, task.projectId)).preset, patched.executionSpec?.harness);
16573
16750
  const verifyEffort = resolveEffortForRole(
16574
16751
  await loadEffortConfig(db, task.projectId),
16575
16752
  "code-review-verify"
@@ -19563,7 +19740,9 @@ async function enqueueInternalReviewVerifyJob(db, task, firstVerdict, log) {
19563
19740
  const patched = {
19564
19741
  ...specRest,
19565
19742
  // DEV-660: the verifier runs on its own (cheaper) model key.
19566
- model: resolveModelForRole(await loadModelConfig(db, task.projectId), "code-review-verify", (await loadWorkflowSettings(db, task.projectId)).preset),
19743
+ // DEV-930: resolved for the harness THIS job was routed to — the cheaper
19744
+ // key is a tier, and the tier means a different model per vendor.
19745
+ model: resolveModelForRole(await loadModelConfig(db, task.projectId), "code-review-verify", (await loadWorkflowSettings(db, task.projectId)).preset, spec.executionSpec?.harness),
19567
19746
  ...verifyEffort !== void 0 ? { effort: verifyEffort } : {}
19568
19747
  };
19569
19748
  await engineStore(db).jobs.patchSpec(spec.jobId, JSON.stringify(patched));
@@ -25173,6 +25352,8 @@ async function rowToSummary3(db, row) {
25173
25352
  const mc = await loadModelConfig(db, row.id);
25174
25353
  summary.llmProvider = resolveProvider(mc);
25175
25354
  if (mc.default !== null || Object.keys(mc.roles).length > 0) summary.modelConfig = mc;
25355
+ const routing = await loadRoutingConfig(db, row.id);
25356
+ if (routing) summary.routing = routing;
25176
25357
  const ec = await loadEffortConfig(db, row.id);
25177
25358
  if (ec.default !== null || Object.keys(ec.roles).length > 0) summary.effortConfig = ec;
25178
25359
  const intake = await loadObserverIntakeSettings(db, row.id);
@@ -27209,6 +27390,7 @@ init_lifecycle();
27209
27390
  init_cluster_recovery();
27210
27391
 
27211
27392
  // src/projects/context-observation.ts
27393
+ init_queue2();
27212
27394
  init_prompts();
27213
27395
  init_prompt_cache();
27214
27396
  init_ws();
@@ -27257,17 +27439,29 @@ function installContextObserver(db, log, opts = {}) {
27257
27439
  } catch (err) {
27258
27440
  log.warn({ err, projectId }, "config_observation: project.yaml apply failed");
27259
27441
  }
27442
+ let regimenChanged = false;
27260
27443
  try {
27261
- await applyRegimenContent(
27444
+ const res = await applyRegimenContent(
27262
27445
  db,
27263
27446
  projectId,
27264
27447
  typeof obs.regimenYaml === "string" ? obs.regimenYaml : null,
27265
27448
  log,
27266
27449
  sameDoc ? sharedParsed : void 0
27267
27450
  );
27451
+ regimenChanged = res.changed;
27268
27452
  } catch (err) {
27269
27453
  log.warn({ err, projectId }, "config_observation: regimen.yaml apply failed");
27270
27454
  }
27455
+ if (regimenChanged) {
27456
+ try {
27457
+ const restamped = await restampQueuedJobs(db, projectId);
27458
+ if (restamped > 0) {
27459
+ log.info({ projectId, jobs: restamped }, "queued jobs re-stamped for the project's routing");
27460
+ }
27461
+ } catch (err) {
27462
+ log.warn({ err, projectId }, "config_observation: re-stamping queued jobs failed");
27463
+ }
27464
+ }
27271
27465
  try {
27272
27466
  applyActionsContent(projectId, obs.actions, obs.actionErrors);
27273
27467
  } catch (err) {
@@ -31049,6 +31243,7 @@ function latestQuestionsContent(artifacts) {
31049
31243
  // src/routes/jobs.ts
31050
31244
  init_context_outbox();
31051
31245
  init_deliveries();
31246
+ init_capabilities();
31052
31247
  init_sandbox_keys();
31053
31248
  init_sandbox2();
31054
31249
  init_ws();
@@ -31065,7 +31260,12 @@ async function jobsRoutes(app, deps) {
31065
31260
  app.log.warn({ sandboxId: sandbox.id }, "claim denied \u2014 SANDBOX protocol incompatible");
31066
31261
  return reply.status(204).send();
31067
31262
  }
31068
- const capabilities = JSON.parse(sandbox.capabilitiesJson);
31263
+ const capabilities = await claimCapabilities(
31264
+ db,
31265
+ sandbox.id,
31266
+ JSON.parse(sandbox.capabilitiesJson),
31267
+ req.body?.capabilities
31268
+ );
31069
31269
  const laneParam = req.query?.lane;
31070
31270
  const lane = laneParam === "planning" || laneParam === "implementation" ? laneParam : void 0;
31071
31271
  const deadline = Date.now() + config.jobClaimLongPollMs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-partner/codepipe-hub",
3
- "version": "0.14.1-dev.399.g96d6330f",
3
+ "version": "0.14.1-dev.405.gb14c9aa1",
4
4
  "description": "The CodePipe hub, packaged for one machine: the CLI installs it on demand for the local profile.",
5
5
  "type": "module",
6
6
  "main": "./hub/dist/index.js",