@tea-agent/loop-agent 0.3.0 → 0.5.0

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.
Files changed (57) hide show
  1. package/AGENTS.md +16 -14
  2. package/CHANGELOG.md +70 -53
  3. package/README.md +28 -25
  4. package/bin/agent-worker.js +22 -0
  5. package/dist/application/dag/validate-dag.js +14 -1
  6. package/dist/commands/init.js +220 -32
  7. package/dist/executors/config-core.js +3 -2
  8. package/dist/executors/dag-pi-executor.js +8 -1
  9. package/dist/executors/model-routing.js +43 -0
  10. package/dist/governance/manifest-types.js +9 -1
  11. package/dist/worker/cli.js +119 -0
  12. package/dist/worker/loop-agent/command-result.js +1 -0
  13. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  14. package/dist/worker/loop-agent/parse-json.js +14 -0
  15. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  16. package/dist/worker/pool/failure-routing.js +98 -0
  17. package/dist/worker/pool/run-store.js +125 -0
  18. package/dist/worker/pool/types.js +1 -0
  19. package/dist/worker/preflight.js +108 -0
  20. package/dist/worker/profile-mapping.js +76 -0
  21. package/dist/worker/progress-reporter.js +81 -0
  22. package/dist/worker/report/morning-report.js +69 -0
  23. package/dist/worker/repos/repo-resolver.js +23 -0
  24. package/dist/worker/run-task/run-task.js +359 -0
  25. package/dist/worker/runner/run-ready.js +216 -0
  26. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  27. package/dist/worker/task-graph/ready-queue.js +23 -0
  28. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  29. package/dist/worker/task-graph/types.js +1 -0
  30. package/dist/worker/task-graph/validate.js +188 -0
  31. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  32. package/dist/worker/task-spec/schema.js +116 -0
  33. package/dist/worker/task-spec/types.js +1 -0
  34. package/dist/worker/task-spec/validate.js +352 -0
  35. package/dist/workflows/dag/init-hybrid.js +4 -13
  36. package/dist/workflows/dag/skill-instructions.js +4 -0
  37. package/dist/workflows/dag/types.js +1 -1
  38. package/dist/workflows/dag/validate.js +3 -2
  39. package/docs/README.md +11 -7
  40. package/docs/development-principles.md +2 -0
  41. package/docs/exec-plans/active/README.md +1 -1
  42. package/docs/exec-plans/completed/README.md +8 -0
  43. package/docs/init-surface.manifest.json +199 -175
  44. package/docs/skills/vetted-skill-registry.md +4 -4
  45. package/docs/templates/agent-dag.base.json +1 -1
  46. package/docs/templates/agent-dag.final-verification.json +1 -1
  47. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  48. package/docs/templates/hybrid-dag.json +1 -1
  49. package/docs/templates/init-evolution-review.md +33 -33
  50. package/examples/example-dag.json +1 -1
  51. package/examples/hybrid-loop-agent-dag.json +1 -1
  52. package/harness.json +7 -32
  53. package/package.json +14 -12
  54. package/skills/init-capability-evolution/SKILL.md +69 -69
  55. package/skills/loop-agent/SKILL.md +2 -0
  56. package/skills/loop-agent/references/command-reference.md +63 -35
  57. package/skills/loop-agent/references/harness-policy.md +2 -1
@@ -6,6 +6,8 @@ import { copyDir } from "../shared/copy-dir.js";
6
6
  import { loadHarnessManifest } from "../governance/harness.js";
7
7
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
8
8
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
9
+ const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
10
+ const GITIGNORE_BLOCK_END = "# LOOP_AGENT_INIT_END";
9
11
  const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
10
12
  const CORE_DOC_FILES = [
11
13
  "README.md",
@@ -720,17 +722,88 @@ function mergeManagedBlock(existing, block) {
720
722
  }
721
723
  return `${existing.trimEnd()}\n\n${block}\n`;
722
724
  }
725
+ function mergeGitignoreManagedBlock(existing, block) {
726
+ const start = existing.indexOf(GITIGNORE_BLOCK_START);
727
+ const end = existing.indexOf(GITIGNORE_BLOCK_END);
728
+ if (start >= 0 && end > start) {
729
+ return `${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart()}`.trimEnd() + "\n";
730
+ }
731
+ if (!existing.trim())
732
+ return `${block}\n`;
733
+ return `${existing.trimEnd()}\n\n${block}\n`;
734
+ }
735
+ /** Shared ignore rules for loop-agent runtime facts; keep prompts/ and directory structure shareable. */
736
+ export function buildManagedGitignoreBlock() {
737
+ return [
738
+ GITIGNORE_BLOCK_START,
739
+ "# loop-agent runtime: ignore personal/session facts; keep prompts and directory placeholders shareable",
740
+ "",
741
+ "# tasks (source + runtime state per developer/session)",
742
+ ".harness/tasks/*",
743
+ "!.harness/tasks/.gitkeep",
744
+ "",
745
+ "# DAG runs",
746
+ ".harness/dag-runs/active/*",
747
+ "!.harness/dag-runs/active/.gitkeep",
748
+ ".harness/dag-runs/completed/*",
749
+ "!.harness/dag-runs/completed/.gitkeep",
750
+ ".harness/dag-runs/paused/*",
751
+ "!.harness/dag-runs/paused/.gitkeep",
752
+ "",
753
+ "# one-shot executor runs",
754
+ ".harness/runs/*",
755
+ "!.harness/runs/.gitkeep",
756
+ "",
757
+ "# session / cache / logs / recomputable surface state",
758
+ ".harness/live/",
759
+ ".harness/cache/",
760
+ ".harness/*.log",
761
+ ".harness/init-surface.json",
762
+ "",
763
+ "# worker task pool",
764
+ ".task-pool/",
765
+ "",
766
+ "# multi-worktree parallel mode",
767
+ ".worktrees/",
768
+ GITIGNORE_BLOCK_END,
769
+ ].join("\n");
770
+ }
771
+ function extractGitignoreManagedBlock(content) {
772
+ const start = content.indexOf(GITIGNORE_BLOCK_START);
773
+ const end = content.indexOf(GITIGNORE_BLOCK_END);
774
+ if (start < 0 || end <= start)
775
+ return undefined;
776
+ return content.slice(start, end + GITIGNORE_BLOCK_END.length);
777
+ }
723
778
  function buildHarness(input) {
724
- const modelOverride = input.provider || input.model
725
- ? {
726
- analyze: { provider: input.provider, model: input.model },
727
- plan: { provider: input.provider, model: input.model },
728
- implement: { provider: input.provider, model: input.model },
729
- verify: { provider: input.provider, model: input.model },
730
- retrospective: { provider: input.provider, model: input.model },
731
- }
732
- : undefined;
733
- return {
779
+ const templateExecutors = isRecord(input.template.executors)
780
+ ? { ...input.template.executors }
781
+ : {};
782
+ const existingExecutors = isRecord(input.existing.executors)
783
+ ? input.existing.executors
784
+ : {};
785
+ // Cursor executor is opt-in for package developers; do not project it to target apps.
786
+ // Keep it only when the target already configured executors.cursor.
787
+ if (!isRecord(existingExecutors.cursor)) {
788
+ delete templateExecutors.cursor;
789
+ }
790
+ const mergedExecutors = mergeRecord(templateExecutors, existingExecutors);
791
+ // requiresApiKey is Cursor-facing only; never project it on pi.
792
+ if (isRecord(mergedExecutors.pi) && "requiresApiKey" in mergedExecutors.pi) {
793
+ const pi = { ...mergedExecutors.pi };
794
+ delete pi.requiresApiKey;
795
+ mergedExecutors.pi = pi;
796
+ }
797
+ // Prefer DAG-facing executors.*.defaultModel over legacy top-level models.*.
798
+ // --provider/--model still apply as a convenience override for pi defaultModel.
799
+ if (input.provider || input.model) {
800
+ const piExisting = isRecord(mergedExecutors.pi) ? mergedExecutors.pi : {};
801
+ mergedExecutors.pi = {
802
+ ...piExisting,
803
+ ...(input.model ? { defaultModel: input.model } : {}),
804
+ };
805
+ }
806
+ const harness = {
734
807
  ...input.existing,
735
808
  version: 1,
736
809
  project: input.projectName,
@@ -771,11 +844,30 @@ function buildHarness(input) {
771
844
  standardVerify: "bash scripts/ci-governance.sh",
772
845
  fullVerify: "bash scripts/ci.sh",
773
846
  }),
774
- models: modelOverride ?? input.existing.models ?? input.template.models ?? {},
775
- modelProfiles: input.existing.modelProfiles ?? input.template.modelProfiles ?? {},
776
- modelRouting: input.existing.modelRouting ?? input.template.modelRouting ?? {},
777
- executors: mergeRecord(input.template.executors, input.existing.executors),
847
+ executors: mergedExecutors,
778
848
  };
849
+ // Do not project legacy step-routing models / modelProfiles / modelRouting into
850
+ // new or refreshed harness.json. Keep them only when the target already has them
851
+ // so old files remain loadable until the user migrates.
852
+ if (isRecord(input.existing.models) && Object.keys(input.existing.models).length > 0) {
853
+ harness.models = input.existing.models;
854
+ }
855
+ else {
856
+ delete harness.models;
857
+ }
858
+ if (isRecord(input.existing.modelProfiles) && Object.keys(input.existing.modelProfiles).length > 0) {
859
+ harness.modelProfiles = input.existing.modelProfiles;
860
+ }
861
+ else {
862
+ delete harness.modelProfiles;
863
+ }
864
+ if (isRecord(input.existing.modelRouting) && Object.keys(input.existing.modelRouting).length > 0) {
865
+ harness.modelRouting = input.existing.modelRouting;
866
+ }
867
+ else {
868
+ delete harness.modelRouting;
869
+ }
870
+ return harness;
779
871
  }
780
872
  async function writeTextIfMissing(input) {
781
873
  const target = path.join(input.repoRoot, input.relativePath);
@@ -899,8 +991,11 @@ function inferInitSurfaceMode(relativePath) {
899
991
  relativePath.endsWith("/")) {
900
992
  return "directory";
901
993
  }
902
- if (relativePath === "README.md" || relativePath === "AGENTS.md")
994
+ if (relativePath === "README.md" ||
995
+ relativePath === "AGENTS.md" ||
996
+ relativePath === ".gitignore") {
903
997
  return "managed-block";
998
+ }
904
999
  if (relativePath === "harness.json" ||
905
1000
  relativePath.startsWith("scripts/") ||
906
1001
  relativePath.startsWith(".harness/prompts/") ||
@@ -934,6 +1029,9 @@ async function buildDesiredSurfaceContent(input) {
934
1029
  }),
935
1030
  };
936
1031
  }
1032
+ if (manifestPath === ".gitignore") {
1033
+ return { content: buildManagedGitignoreBlock() };
1034
+ }
937
1035
  if (manifestPath === "harness.json") {
938
1036
  const template = await readJsonIfExists(path.join(input.assetRoot, "harness.json"));
939
1037
  return {
@@ -953,6 +1051,16 @@ async function buildDesiredSurfaceContent(input) {
953
1051
  const name = path.basename(manifestPath);
954
1052
  return { content: COMPAT_PROMPTS[name] };
955
1053
  }
1054
+ // `.agents/skills/...` is a target-only mirror of the bundled `skills/...`
1055
+ // directory; the package ships `skills/`, never `.agents/skills/`.
1056
+ if (manifestPath.startsWith(".agents/skills/")) {
1057
+ const sourceManifestPath = manifestPath.slice(".agents/".length);
1058
+ const mirrorSourcePath = path.join(input.assetRoot, sourceManifestPath);
1059
+ if (await exists(mirrorSourcePath)) {
1060
+ return { content: await readFile(mirrorSourcePath, "utf-8"), sourcePath: mirrorSourcePath };
1061
+ }
1062
+ return {};
1063
+ }
956
1064
  if (manifestPath.startsWith("docs/")) {
957
1065
  const doc = manifestPath.slice("docs/".length);
958
1066
  const generated = buildGeneratedCoreDoc({
@@ -1031,7 +1139,10 @@ async function buildCurrentSurfaceState(input) {
1031
1139
  const current = await readFile(targetPath);
1032
1140
  const currentSha256 = sha256Buffer(current);
1033
1141
  if (entry.mode === "managed-block") {
1034
- const currentBlock = extractManagedBlock(current.toString("utf-8"));
1142
+ const text = current.toString("utf-8");
1143
+ const currentBlock = entry.path === ".gitignore" || targetRelativePath === ".gitignore"
1144
+ ? extractGitignoreManagedBlock(text)
1145
+ : extractManagedBlock(text);
1035
1146
  files[targetRelativePath] = {
1036
1147
  ...base,
1037
1148
  currentSha256,
@@ -1462,7 +1573,8 @@ export function buildInitInstructions(input) {
1462
1573
  "- After deterministic initialization, inspect README/config/build files (for example package.json, pyproject.toml, go.mod, Cargo.toml, pom.xml, Gradle files, Makefile, .sln/.csproj, or project-specific scripts) and adapt `scripts/ci-tests.sh` plus the verification matrix to the real project.",
1463
1574
  "- Enrich the root `README.md`: keep the deterministic project title and the loop-agent managed block intact, and fill the human-authored sections (项目概览, 技术栈与目录结构, 开发与验证) from the target project's actual files. The root README must serve both as a human-first project entry and as an agent work entry; replace the initialization-model supplement comments when the project files provide the information.",
1464
1575
  "- Populate `docs/verification-matrix.md` with the target project's actual quick, standard, and full verification commands derived from its real language and toolchain, keeping the governance rows intact.",
1465
- "- Copy repo-local skills by default so the target repo has auditable skill instructions.",
1576
+ "- Copy repo-local skills by default so the target repo has auditable skill instructions; full profile also mirrors skills into `.agents/skills/` for external agents.",
1577
+ "- Merge a loop-agent managed block into `.gitignore` that ignores harness runtime facts (tasks, dag-runs, runs, live, cache, init-surface.json, .task-pool) while keeping prompts and directory placeholders shareable.",
1466
1578
  "- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
1467
1579
  "- Add or update a loop-agent managed block in AGENTS.md.",
1468
1580
  "- The generated AGENTS.md must include documentation convergence and structured DAG write-boundary rules so target projects keep the same working discipline as this repository.",
@@ -1556,6 +1668,18 @@ export async function initializeLoopAgentProject(options) {
1556
1668
  written,
1557
1669
  skipped,
1558
1670
  });
1671
+ const gitignorePath = path.join(repoRoot, ".gitignore");
1672
+ const existingGitignore = (await exists(gitignorePath))
1673
+ ? await readFile(gitignorePath, "utf-8")
1674
+ : "";
1675
+ await writeText({
1676
+ repoRoot,
1677
+ relativePath: ".gitignore",
1678
+ content: mergeGitignoreManagedBlock(existingGitignore, buildManagedGitignoreBlock()),
1679
+ merge: true,
1680
+ written,
1681
+ skipped,
1682
+ });
1559
1683
  for (const doc of CORE_DOC_FILES) {
1560
1684
  const generated = buildGeneratedCoreDoc({ doc, projectName, governanceRoot });
1561
1685
  if (generated) {
@@ -1604,9 +1728,21 @@ export async function initializeLoopAgentProject(options) {
1604
1728
  targetRelativePath: "skills",
1605
1729
  written,
1606
1730
  });
1731
+ // Mirror bundled skills into the agent-compatible `.agents/skills/` path so
1732
+ // external agents (e.g. OpenCode) that auto-discover `.agents/skills/<name>/SKILL.md`
1733
+ // share the same skill content as loop-agent's primary `skills/` path.
1734
+ // The package still ships only `skills/`; `.agents/skills` is a target projection.
1735
+ await copyDirMerge({
1736
+ assetRoot,
1737
+ repoRoot,
1738
+ sourceRelativePath: "skills",
1739
+ targetRelativePath: ".agents/skills",
1740
+ written,
1741
+ });
1607
1742
  }
1608
1743
  else {
1609
1744
  skipped.push("skills/");
1745
+ skipped.push(".agents/skills/");
1610
1746
  }
1611
1747
  skipped.push("examples/");
1612
1748
  for (const [relativePath, content] of Object.entries(buildInitScriptFiles(governanceRoot))) {
@@ -1736,6 +1872,27 @@ export async function checkInitUpdate(input) {
1736
1872
  }
1737
1873
  }
1738
1874
  }
1875
+ // Safe harness hygiene: strip obsolete pi.requiresApiKey without full harness rewrite.
1876
+ // Cursor requiresApiKey is intentionally kept when present.
1877
+ const harnessPath = path.join(repoRoot, "harness.json");
1878
+ if (await exists(harnessPath)) {
1879
+ try {
1880
+ const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
1881
+ if (isRecord(harness) &&
1882
+ isRecord(harness.executors) &&
1883
+ isRecord(harness.executors.pi) &&
1884
+ Object.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
1885
+ deterministicActions.push({
1886
+ type: "strip-pi-requires-api-key",
1887
+ path: "harness.json",
1888
+ reason: "executors.pi.requiresApiKey is unused; strip it while keeping user model config and cursor.requiresApiKey",
1889
+ });
1890
+ }
1891
+ }
1892
+ catch {
1893
+ // leave malformed harness for model merge / doctor
1894
+ }
1895
+ }
1739
1896
  const partial = {
1740
1897
  repoRoot,
1741
1898
  controllerVersion: currentState.controllerVersion,
@@ -1759,6 +1916,29 @@ export async function checkInitUpdate(input) {
1759
1916
  };
1760
1917
  }
1761
1918
  async function applySafeAction(input) {
1919
+ if (input.action.type === "strip-pi-requires-api-key") {
1920
+ const target = path.join(input.repoRoot, "harness.json");
1921
+ if (!(await exists(target)))
1922
+ return false;
1923
+ const harness = JSON.parse(await readFile(target, "utf-8"));
1924
+ if (!isRecord(harness) || !isRecord(harness.executors) || !isRecord(harness.executors.pi)) {
1925
+ return false;
1926
+ }
1927
+ if (!Object.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
1928
+ return false;
1929
+ }
1930
+ const nextPi = { ...harness.executors.pi };
1931
+ delete nextPi.requiresApiKey;
1932
+ const next = {
1933
+ ...harness,
1934
+ executors: {
1935
+ ...harness.executors,
1936
+ pi: nextPi,
1937
+ },
1938
+ };
1939
+ await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
1940
+ return true;
1941
+ }
1762
1942
  const assetRoot = await findPackageRoot();
1763
1943
  const entry = {
1764
1944
  path: input.action.path,
@@ -1789,11 +1969,13 @@ async function applySafeAction(input) {
1789
1969
  await mkdir(path.dirname(target), { recursive: true });
1790
1970
  if (input.action.type === "refresh-managed-block") {
1791
1971
  const existing = (await exists(target)) ? await readFile(target, "utf-8") : "";
1792
- const next = input.action.path === "README.md" && existing.trim().length === 0
1793
- ? buildTargetReadme({ projectName: input.projectName, governanceRoot: input.governanceRoot })
1794
- : input.action.path === "AGENTS.md" && existing.trim().length === 0
1795
- ? `# AGENTS.md\n\n${desired.content}\n`
1796
- : mergeManagedBlock(existing, desired.content);
1972
+ const next = input.action.path === ".gitignore"
1973
+ ? mergeGitignoreManagedBlock(existing, desired.content)
1974
+ : input.action.path === "README.md" && existing.trim().length === 0
1975
+ ? buildTargetReadme({ projectName: input.projectName, governanceRoot: input.governanceRoot })
1976
+ : input.action.path === "AGENTS.md" && existing.trim().length === 0
1977
+ ? `# AGENTS.md\n\n${desired.content}\n`
1978
+ : mergeManagedBlock(existing, desired.content);
1797
1979
  await writeFile(target, next, "utf-8");
1798
1980
  return true;
1799
1981
  }
@@ -1826,16 +2008,18 @@ export async function applyInitUpdate(input) {
1826
2008
  skipped.push(action);
1827
2009
  continue;
1828
2010
  }
1829
- const currentState = await buildCurrentSurfaceState({
1830
- repoRoot,
1831
- projectName,
1832
- governanceRoot,
1833
- stateKind: "inferred-baseline",
1834
- });
1835
- const relationship = currentState.files[action.path]?.relationship;
1836
- if (relationship === "local-existing-unknown") {
1837
- skipped.push(action);
1838
- continue;
2011
+ if (action.type !== "strip-pi-requires-api-key") {
2012
+ const currentState = await buildCurrentSurfaceState({
2013
+ repoRoot,
2014
+ projectName,
2015
+ governanceRoot,
2016
+ stateKind: "inferred-baseline",
2017
+ });
2018
+ const relationship = currentState.files[action.path]?.relationship;
2019
+ if (relationship === "local-existing-unknown") {
2020
+ skipped.push(action);
2021
+ continue;
2022
+ }
1839
2023
  }
1840
2024
  if (await applySafeAction({ repoRoot, projectName, governanceRoot, action }))
1841
2025
  applied.push(action);
@@ -1909,6 +2093,10 @@ export async function runInitDoctor(input) {
1909
2093
  const readme = path.join(repoRoot, "README.md");
1910
2094
  add("README loop-agent block", (await exists(readme)) && (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
1911
2095
  add("repo-local skills", await exists(path.join(repoRoot, "skills", "loop-agent", "SKILL.md")), "skills/loop-agent/SKILL.md");
2096
+ add("agent-compatible skills mirror", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
2097
+ const gitignorePath = path.join(repoRoot, ".gitignore");
2098
+ const gitignoreContent = (await exists(gitignorePath)) ? await readFile(gitignorePath, "utf-8") : "";
2099
+ add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) && gitignoreContent.includes(".harness/tasks/*"), ".gitignore managed runtime ignores");
1912
2100
  const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
1913
2101
  const missingScripts = [];
1914
2102
  for (const script of requiredScripts) {
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_CURSOR_MODEL } from './cursor-executor.js';
2
- import { DEFAULT_DAG_MODELS } from './model-routing.js';
2
+ import { resolveExecutorModelMatrix } from './model-routing.js';
3
3
  export const TASK_COMPLEXITY_TO_DAG = {
4
4
  small: 'LOW',
5
5
  medium: 'MED',
@@ -13,7 +13,8 @@ export function resolveCursorModelForTaskConfig(taskConfig, cursorConfig) {
13
13
  return taskConfig.cursorModel.trim();
14
14
  }
15
15
  const dagLevel = TASK_COMPLEXITY_TO_DAG[taskConfig.complexity ?? 'medium'];
16
- return DEFAULT_DAG_MODELS[dagLevel] ?? resolveCursorModel({}, cursorConfig);
16
+ const matrix = resolveExecutorModelMatrix('cursor', cursorConfig);
17
+ return matrix[dagLevel] ?? resolveCursorModel({}, cursorConfig);
17
18
  }
18
19
  export function resolveTaskExecutor(taskConfig, override) {
19
20
  if (override)
@@ -148,7 +148,14 @@ export function buildDagPiUserMessage(task, persona, step) {
148
148
  ].join(" ");
149
149
  }
150
150
  function resolveDagPiModelConfig(model) {
151
- return { provider: DAG_PI_MODEL_PROVIDERS[model] ?? DEFAULT_DAG_PI_PROVIDER, model };
151
+ const provider = DAG_PI_MODEL_PROVIDERS[model] ?? DEFAULT_DAG_PI_PROVIDER;
152
+ return {
153
+ provider,
154
+ model,
155
+ ...(provider === "wizard-local" && model === "gpt-5.5"
156
+ ? { thinking: "low" }
157
+ : {}),
158
+ };
152
159
  }
153
160
  const SUMMARY_STDOUT_MAX = 4_000;
154
161
  const SUMMARY_STDERR_MAX = 2_000;
@@ -1,9 +1,52 @@
1
+ import { DEFAULT_DAG_EXECUTOR_MODELS, } from '../workflows/dag/types.js';
1
2
  export const DEFAULT_DAG_CURSOR_MODEL = "composer-2.5";
2
3
  export const DEFAULT_DAG_MODELS = {
3
4
  HIGH: "gpt-5.5",
4
5
  MED: "composer-2.5",
5
6
  LOW: "composer-2.5",
6
7
  };
8
+ /**
9
+ * DAG executor model tier keys that may carry a per-complexity override.
10
+ */
11
+ const EXECUTOR_MODEL_TIERS = ["LOW", "MED", "HIGH"];
12
+ /**
13
+ * Resolve the DAG executor model matrix for a single executor from its harness
14
+ * `executors.<name>` config.
15
+ *
16
+ * Priority per tier (LOW/MED/HIGH):
17
+ * 1. execConfig[tier] (truthy and !== "default" sentinel)
18
+ * 2. execConfig.defaultModel (truthy and !== "default" sentinel)
19
+ * 3. DEFAULT_DAG_EXECUTOR_MODELS[executor][tier]
20
+ *
21
+ * The "default" literal (injected by the schema `.default("default")`) and
22
+ * absent/undefined both mean "no override, fall through".
23
+ */
24
+ export function resolveExecutorModelMatrix(executor, execConfig) {
25
+ const tierValue = (tier) => {
26
+ const tierOverride = execConfig?.[tier];
27
+ if (tierOverride && tierOverride !== "default")
28
+ return tierOverride;
29
+ const defaultModel = execConfig?.defaultModel;
30
+ if (defaultModel && defaultModel !== "default")
31
+ return defaultModel;
32
+ return DEFAULT_DAG_EXECUTOR_MODELS[executor][tier];
33
+ };
34
+ return {
35
+ LOW: tierValue("LOW"),
36
+ MED: tierValue("MED"),
37
+ HIGH: tierValue("HIGH"),
38
+ };
39
+ }
40
+ /**
41
+ * Resolve both pi and cursor DAG executor model matrices from a harness manifest.
42
+ * Single entry point for DAG generation and --strict-models baseline resolution.
43
+ */
44
+ export function resolveExecutorModelMatrices(manifest) {
45
+ return {
46
+ pi: resolveExecutorModelMatrix("pi", manifest.executors?.pi),
47
+ cursor: resolveExecutorModelMatrix("cursor", manifest.executors?.cursor),
48
+ };
49
+ }
7
50
  export function resolveModelSelection(manifest, taskConfig, step, options) {
8
51
  const retryAttempt = options?.retryAttempt ?? 0;
9
52
  if (Object.keys(manifest.modelProfiles ?? {}).length === 0) {
@@ -21,7 +21,15 @@ export const executorManifestSchema = z.object({
21
21
  description: z.string().optional(),
22
22
  enabled: z.boolean().optional(),
23
23
  defaultModel: z.string().optional().default("default"),
24
- requiresApiKey: z.string().optional().default("CURSOR_API_KEY"),
24
+ /**
25
+ * Per-complexity DAG model overrides. When set (and not the "default" sentinel),
26
+ * these take priority over defaultModel for the matching DAG executor tier.
27
+ * "default" literal and absent/undefined both mean "no override, fall through".
28
+ */
29
+ LOW: z.string().optional(),
30
+ MED: z.string().optional(),
31
+ HIGH: z.string().optional(),
32
+ requiresApiKey: z.string().optional(),
25
33
  });
26
34
  export const workflowPolicyProfileNameSchema = z.enum([
27
35
  "minimal",
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import YAML from "yaml";
7
+ import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
8
+ import { resolveLoopAgentProfile } from "./profile-mapping.js";
9
+ import { writeMorningReport } from "./report/morning-report.js";
10
+ import { buildBatchRunId, runReadyTasks } from "./runner/run-ready.js";
11
+ import { createProgressReporter } from "./progress-reporter.js";
12
+ import { taskSpecSchema } from "./task-spec/schema.js";
13
+ import { validateTaskSpec } from "./task-spec/validate.js";
14
+ export function buildAgentWorkerProgram() {
15
+ const program = new Command();
16
+ program
17
+ .name("agent-worker")
18
+ .description("Local product-line worker utilities for TaskSpec validation")
19
+ .showHelpAfterError()
20
+ .showSuggestionAfterError();
21
+ const task = program.command("task").description("TaskSpec utilities");
22
+ const batch = program.command("batch").description("Task Pool batch utilities");
23
+ const report = program.command("report").description("Task Pool reporting utilities");
24
+ task
25
+ .command("validate")
26
+ .argument("<task-yaml>", "TaskSpec YAML file")
27
+ .description("Validate a TaskSpec and print JSON")
28
+ .action(async (taskYaml) => {
29
+ const taskSpecPath = path.resolve(taskYaml);
30
+ const raw = await readTaskYaml(taskSpecPath);
31
+ const result = await validateTaskSpec(raw, { taskSpecPath });
32
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
33
+ if (!result.ok)
34
+ process.exitCode = 1;
35
+ });
36
+ task
37
+ .command("explain-profile")
38
+ .argument("<task-yaml>", "TaskSpec YAML file")
39
+ .description("Explain business profile to loop-agent profile mapping")
40
+ .action(async (taskYaml) => {
41
+ const raw = await readTaskYaml(path.resolve(taskYaml));
42
+ const parsed = taskSpecSchema.parse(raw);
43
+ const result = resolveLoopAgentProfile(parsed);
44
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
45
+ });
46
+ batch
47
+ .command("run-ready")
48
+ .requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
49
+ .requiredOption("--repo <repo-root>", "Target repo root")
50
+ .option("--limit <count>", "Maximum ready tasks to run")
51
+ .option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
52
+ .option("--batch-run-id <id>", "Batch run id")
53
+ .option("--check-repo", "Run bash scripts/check-repo.sh during target repo preflight")
54
+ .option("--check-repo-command <command...>", "Override the check-repo preflight command")
55
+ .option("--quiet", "Suppress human-readable progress on stderr (JSON still goes to stdout)")
56
+ .option("--pi-model <model>", "Smoke override: force every pi executor node to this model (drops --strict-models)")
57
+ .description("Run ready TaskSpec tasks serially")
58
+ .action(async (options) => {
59
+ const repoRoot = path.resolve(options.repo);
60
+ const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
61
+ const client = new LoopAgentClient({
62
+ loopAgentBin: options.loopAgentBin,
63
+ artifactRoot: path.join(repoRoot, ".task-pool", "artifacts", batchRunId),
64
+ });
65
+ const progress = createProgressReporter({ quiet: options.quiet ?? false });
66
+ const result = await runReadyTasks({
67
+ repoRoot,
68
+ featureDir: path.resolve(options.featureDir),
69
+ ...(options.limit ? { limit: Number.parseInt(options.limit, 10) } : {}),
70
+ batchRunId,
71
+ client,
72
+ runCheckRepo: options.checkRepo ?? false,
73
+ ...(options.checkRepoCommand
74
+ ? { checkRepoCommand: options.checkRepoCommand }
75
+ : {}),
76
+ progress,
77
+ ...(options.piModel ? { piModel: options.piModel } : {}),
78
+ });
79
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
80
+ if (result.status !== "completed")
81
+ process.exitCode = 1;
82
+ });
83
+ report
84
+ .command("morning")
85
+ .requiredOption("--repo <repo-root>", "Target repo root")
86
+ .option("--batch-run-id <id>", "Filter to one batch run id")
87
+ .option("--output <path>", "Write markdown report to this path")
88
+ .description("Render a markdown morning report from Task Pool runs")
89
+ .action(async (options) => {
90
+ const repoRoot = path.resolve(options.repo);
91
+ const outputPath = options.output ??
92
+ path.join(repoRoot, ".task-pool", "reports", "morning-report.md");
93
+ await writeMorningReport({
94
+ repoRoot,
95
+ ...(options.batchRunId ? { batchRunId: options.batchRunId } : {}),
96
+ outputPath: path.resolve(outputPath),
97
+ });
98
+ process.stdout.write(`${JSON.stringify({ ok: true, outputPath }, null, 2)}\n`);
99
+ });
100
+ return program;
101
+ }
102
+ export async function main(argv = process.argv) {
103
+ await buildAgentWorkerProgram().parseAsync(argv);
104
+ }
105
+ async function readTaskYaml(taskSpecPath) {
106
+ return YAML.parse(await readFile(taskSpecPath, "utf-8"));
107
+ }
108
+ function isDirectRun() {
109
+ const entry = process.argv[1];
110
+ if (!entry)
111
+ return false;
112
+ return path.resolve(entry) === fileURLToPath(import.meta.url);
113
+ }
114
+ if (isDirectRun()) {
115
+ main().catch((error) => {
116
+ console.error(error instanceof Error ? error.message : String(error));
117
+ process.exit(1);
118
+ });
119
+ }
@@ -0,0 +1 @@
1
+ export {};