@sorenllm/opencode-forge 0.3.3 → 0.4.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.
- package/README.md +119 -124
- package/dist/index.js +482 -1570
- package/package.json +64 -61
package/dist/index.js
CHANGED
|
@@ -12335,7 +12335,7 @@ function tool(input) {
|
|
|
12335
12335
|
tool.schema = exports_external;
|
|
12336
12336
|
// plugin.ts
|
|
12337
12337
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
12338
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync as
|
|
12338
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
12339
12339
|
import { isAbsolute, join as join5, relative } from "node:path";
|
|
12340
12340
|
import { tmpdir as tmpdir3, homedir } from "node:os";
|
|
12341
12341
|
|
|
@@ -14723,15 +14723,20 @@ function createLocator(platform = process.platform, posix = {}, win = {}) {
|
|
|
14723
14723
|
return platform === "win32" ? createWindowsLocator(win) : createPosixLocator(posix);
|
|
14724
14724
|
}
|
|
14725
14725
|
|
|
14726
|
-
// src/
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14726
|
+
// src/models-dev.ts
|
|
14727
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
14728
|
+
import { join as join4 } from "node:path";
|
|
14729
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
14730
|
+
var DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
|
|
14731
|
+
function parseIdentity(identity) {
|
|
14732
|
+
const slash = identity.indexOf("/");
|
|
14733
|
+
if (slash <= 0 || slash === identity.length - 1)
|
|
14734
|
+
return null;
|
|
14735
|
+
return { provider: identity.slice(0, slash), model: identity.slice(slash + 1) };
|
|
14736
|
+
}
|
|
14734
14737
|
function nativeLadder(catalog, identity) {
|
|
14738
|
+
if (!catalog)
|
|
14739
|
+
return null;
|
|
14735
14740
|
const parsed = parseIdentity(identity);
|
|
14736
14741
|
if (!parsed)
|
|
14737
14742
|
return null;
|
|
@@ -14741,352 +14746,174 @@ function nativeLadder(catalog, identity) {
|
|
|
14741
14746
|
const options = entry.reasoningOptions;
|
|
14742
14747
|
return Array.isArray(options) ? [...options] : null;
|
|
14743
14748
|
}
|
|
14744
|
-
function
|
|
14745
|
-
|
|
14746
|
-
if (!parsed)
|
|
14747
|
-
return;
|
|
14748
|
-
return catalog.providers?.[parsed.provider]?.models?.[parsed.model]?.cost;
|
|
14749
|
-
}
|
|
14750
|
-
function parseIdentity(identity) {
|
|
14751
|
-
const slash = identity.indexOf("/");
|
|
14752
|
-
if (slash <= 0 || slash === identity.length - 1)
|
|
14753
|
-
return null;
|
|
14754
|
-
return { provider: identity.slice(0, slash), model: identity.slice(slash + 1) };
|
|
14755
|
-
}
|
|
14756
|
-
function catalogKnowsIdentity(catalog, identity) {
|
|
14757
|
-
const parsed = parseIdentity(identity);
|
|
14758
|
-
if (!parsed)
|
|
14759
|
-
return false;
|
|
14760
|
-
return Boolean(catalog.providers?.[parsed.provider]?.models?.[parsed.model]);
|
|
14749
|
+
function modelsDevCacheDir(base) {
|
|
14750
|
+
return join4(base ?? join4(tmpdir2(), "opencode-forge"), "models-dev");
|
|
14761
14751
|
}
|
|
14762
|
-
function
|
|
14763
|
-
const
|
|
14764
|
-
const
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
findings.push({
|
|
14772
|
-
level: "warn",
|
|
14773
|
-
code: "tier-id-charset",
|
|
14774
|
-
message: `tier id "${id}" violates [a-z0-9-] (it materializes as forge-<tier> agent ids)`
|
|
14775
|
-
});
|
|
14776
|
-
}
|
|
14777
|
-
const shape = def.shape ?? "readonly";
|
|
14778
|
-
if (def.shape === undefined) {
|
|
14779
|
-
findings.push({
|
|
14780
|
-
level: "warn",
|
|
14781
|
-
code: "shape-defaulted",
|
|
14782
|
-
message: `custom tier "${id}" has no shape; defaulting to readonly (write requires explicit "shape": "write")`
|
|
14783
|
-
});
|
|
14784
|
-
}
|
|
14785
|
-
tiers[id] = { ...def, shape };
|
|
14786
|
-
}
|
|
14787
|
-
const roster = [];
|
|
14788
|
-
for (const entry of opts.userRoster ?? []) {
|
|
14789
|
-
const ladder = nativeLadder(catalog, entry.model);
|
|
14790
|
-
const verified = ladder !== null;
|
|
14791
|
-
if (verified && catalogKnowsIdentity(catalog, entry.model)) {
|
|
14792
|
-
const legal = ladder;
|
|
14793
|
-
if (legal.length === 0) {
|
|
14794
|
-
findings.push({
|
|
14795
|
-
level: "notice",
|
|
14796
|
-
code: "unnamed-levels-accepted",
|
|
14797
|
-
message: `roster entry ${entry.model} has no cataloged named reasoning levels (toggle/budget model); expose accepted verbatim without validation`
|
|
14798
|
-
});
|
|
14799
|
-
roster.push({ model: entry.model, expose: [...entry.expose ?? []], profiles: [...entry.profiles], verified: true });
|
|
14800
|
-
} else {
|
|
14801
|
-
const expose = entry.expose ?? legal;
|
|
14802
|
-
const illegal = expose.filter((d) => !legal.includes(d));
|
|
14803
|
-
if (illegal.length > 0) {
|
|
14804
|
-
findings.push({
|
|
14805
|
-
level: "error",
|
|
14806
|
-
code: "expose-unknown-level",
|
|
14807
|
-
message: `roster entry ${entry.model} exposes unknown level(s) ${illegal.map((d) => `"${d}"`).join(", ")}; legal ladder: ${legal.join(", ")}`
|
|
14808
|
-
});
|
|
14809
|
-
}
|
|
14810
|
-
roster.push({
|
|
14811
|
-
model: entry.model,
|
|
14812
|
-
expose: expose.filter((d) => legal.includes(d)),
|
|
14813
|
-
profiles: [...entry.profiles],
|
|
14814
|
-
verified: true
|
|
14815
|
-
});
|
|
14752
|
+
function namedLevels(rows) {
|
|
14753
|
+
const out = [];
|
|
14754
|
+
for (const r of rows) {
|
|
14755
|
+
if (r && typeof r === "object" && r.type === "effort") {
|
|
14756
|
+
const values = r.values;
|
|
14757
|
+
if (Array.isArray(values)) {
|
|
14758
|
+
for (const v of values)
|
|
14759
|
+
if (typeof v === "string")
|
|
14760
|
+
out.push(v);
|
|
14816
14761
|
}
|
|
14817
|
-
} else {
|
|
14818
|
-
findings.push({
|
|
14819
|
-
level: "notice",
|
|
14820
|
-
code: "unverified-identity",
|
|
14821
|
-
message: `roster entry ${entry.model} is not in the models.dev snapshot; expose accepted verbatim without validation`
|
|
14822
|
-
});
|
|
14823
|
-
roster.push({ model: entry.model, expose: [...entry.expose ?? []], profiles: [...entry.profiles], verified: false });
|
|
14824
|
-
}
|
|
14825
|
-
}
|
|
14826
|
-
const tierIds = new Set(Object.keys(tiers));
|
|
14827
|
-
for (const entry of roster) {
|
|
14828
|
-
if (!opts.configuredIdentities.includes(entry.model)) {
|
|
14829
|
-
findings.push({
|
|
14830
|
-
level: "warn",
|
|
14831
|
-
code: "dead-key",
|
|
14832
|
-
message: `roster entry ${entry.model} is not a configured provider/model identity (dead key)`
|
|
14833
|
-
});
|
|
14834
|
-
}
|
|
14835
|
-
const unknownTiers = entry.profiles.filter((p) => !tierIds.has(p));
|
|
14836
|
-
if (unknownTiers.length > 0) {
|
|
14837
|
-
findings.push({
|
|
14838
|
-
level: "warn",
|
|
14839
|
-
code: "unknown-tier-ref",
|
|
14840
|
-
message: `roster entry ${entry.model} references nonexistent tier(s): ${unknownTiers.join(", ")}`
|
|
14841
|
-
});
|
|
14842
14762
|
}
|
|
14843
14763
|
}
|
|
14844
|
-
|
|
14845
|
-
|
|
14764
|
+
return out;
|
|
14765
|
+
}
|
|
14766
|
+
function reduceModelsDev(raw) {
|
|
14767
|
+
const catalog = { providers: {} };
|
|
14768
|
+
const providers = raw?.constructor === Object ? raw : null;
|
|
14769
|
+
if (!providers)
|
|
14770
|
+
return catalog;
|
|
14771
|
+
for (const [providerID, provider] of Object.entries(providers)) {
|
|
14772
|
+
const models = provider.models;
|
|
14773
|
+
if (!models || typeof models !== "object")
|
|
14846
14774
|
continue;
|
|
14847
|
-
const
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14775
|
+
const out = { models: {} };
|
|
14776
|
+
for (const [modelID, model] of Object.entries(models)) {
|
|
14777
|
+
const m = model;
|
|
14778
|
+
const options = Array.isArray(m.reasoning_options) ? namedLevels(m.reasoning_options) : undefined;
|
|
14779
|
+
const cost = m.cost && typeof m.cost === "object" ? m.cost : undefined;
|
|
14780
|
+
out.models[modelID] = {
|
|
14781
|
+
...options ? { reasoningOptions: options } : {},
|
|
14782
|
+
...cost ? { cost } : {}
|
|
14783
|
+
};
|
|
14855
14784
|
}
|
|
14785
|
+
catalog.providers[providerID] = out;
|
|
14856
14786
|
}
|
|
14857
|
-
return
|
|
14787
|
+
return catalog;
|
|
14858
14788
|
}
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14789
|
+
async function loadModelsDevSnapshot(opts = {}) {
|
|
14790
|
+
const url2 = opts.url ?? process.env.OPENCODE_MODELS_URL ?? DEFAULT_MODELS_DEV_URL;
|
|
14791
|
+
const cacheDir = modelsDevCacheDir(opts.cacheDir);
|
|
14792
|
+
const cacheFile = join4(cacheDir, "snapshot.json");
|
|
14793
|
+
const fetcher = opts.fetcher;
|
|
14794
|
+
if (fetcher) {
|
|
14795
|
+
try {
|
|
14796
|
+
const res = await fetcher(url2, { signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
14797
|
+
if (res.ok)
|
|
14798
|
+
return { catalog: reduceModelsDev(JSON.parse(await res.text())), degraded: false, source: url2 };
|
|
14799
|
+
} catch {}
|
|
14800
|
+
return { catalog: { providers: {} }, degraded: true, source: "fetch-failed" };
|
|
14870
14801
|
}
|
|
14871
|
-
|
|
14802
|
+
try {
|
|
14803
|
+
const res = await fetch(url2, { signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
14804
|
+
if (res.ok) {
|
|
14805
|
+
const catalog = reduceModelsDev(JSON.parse(await res.text()));
|
|
14806
|
+
try {
|
|
14807
|
+
mkdirSync4(cacheDir, { recursive: true });
|
|
14808
|
+
writeFileSync4(cacheFile, JSON.stringify(catalog));
|
|
14809
|
+
} catch {}
|
|
14810
|
+
return { catalog, degraded: false, source: url2 };
|
|
14811
|
+
}
|
|
14812
|
+
} catch {}
|
|
14813
|
+
try {
|
|
14814
|
+
const cached2 = JSON.parse(readFileSync6(cacheFile, "utf8"));
|
|
14815
|
+
if (cached2 && typeof cached2 === "object" && cached2.providers)
|
|
14816
|
+
return { catalog: cached2, degraded: true, source: "cache" };
|
|
14817
|
+
} catch {}
|
|
14818
|
+
return { catalog: { providers: {} }, degraded: true, source: "unavailable" };
|
|
14872
14819
|
}
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
total.output += t.output ?? 0;
|
|
14878
|
-
total.reasoning += t.reasoning ?? 0;
|
|
14879
|
-
total.cache.read += t.cache?.read ?? 0;
|
|
14880
|
-
total.cache.write += t.cache?.write ?? 0;
|
|
14881
|
-
}
|
|
14882
|
-
return total;
|
|
14820
|
+
|
|
14821
|
+
// src/crew-gate.ts
|
|
14822
|
+
function normTitle(t) {
|
|
14823
|
+
return String(t ?? "").trim().toLowerCase();
|
|
14883
14824
|
}
|
|
14884
|
-
function
|
|
14885
|
-
|
|
14886
|
-
|
|
14825
|
+
function validateCrewReport(report, declared) {
|
|
14826
|
+
const gaps = [];
|
|
14827
|
+
if (!Array.isArray(report) || report.length === 0) {
|
|
14828
|
+
return { ok: false, gaps: ["the report lists no subtasks — a crew completion report must cover every declared subtask"] };
|
|
14887
14829
|
}
|
|
14888
|
-
|
|
14889
|
-
|
|
14830
|
+
const declaredTitles = new Set;
|
|
14831
|
+
for (const s of Array.isArray(declared) ? declared : []) {
|
|
14832
|
+
const t = normTitle(s?.title);
|
|
14833
|
+
if (t)
|
|
14834
|
+
declaredTitles.add(t);
|
|
14890
14835
|
}
|
|
14891
|
-
|
|
14892
|
-
|
|
14836
|
+
const covered = new Set;
|
|
14837
|
+
report.forEach((sub, i) => {
|
|
14838
|
+
const label = sub?.title?.trim() || `subtask #${i + 1}`;
|
|
14839
|
+
const key = normTitle(sub?.title);
|
|
14840
|
+
if (!sub?.title?.trim())
|
|
14841
|
+
gaps.push(`${label}: missing title`);
|
|
14842
|
+
else if (covered.has(key))
|
|
14843
|
+
gaps.push(`${label}: duplicate report entry for the same title`);
|
|
14844
|
+
covered.add(key);
|
|
14845
|
+
const verdict = String(sub?.verdict ?? "").toUpperCase();
|
|
14846
|
+
if (verdict !== "PASS" && verdict !== "FAIL")
|
|
14847
|
+
gaps.push(`${label}: verdict must be PASS or FAIL (got "${sub?.verdict ?? ""}")`);
|
|
14848
|
+
if (!String(sub?.evidence ?? "").trim())
|
|
14849
|
+
gaps.push(`${label}: missing evidence statement`);
|
|
14850
|
+
if (verdict === "FAIL" && (!Array.isArray(sub?.attempts) || sub.attempts.length < 2)) {
|
|
14851
|
+
gaps.push(`${label}: FAIL requires both failure reports (the attempt and the one retry) in attempts[]`);
|
|
14852
|
+
}
|
|
14853
|
+
if (key && declaredTitles.size > 0 && !declaredTitles.has(key)) {
|
|
14854
|
+
gaps.push(`${label}: not part of the plan registered at crew_begin (renegade work) — fold discoveries into an existing subtask's verdict notes, or discard and re-crew with the full plan`);
|
|
14855
|
+
}
|
|
14856
|
+
});
|
|
14857
|
+
for (const t of declaredTitles) {
|
|
14858
|
+
if (!covered.has(t)) {
|
|
14859
|
+
const original = (Array.isArray(declared) ? declared : []).find((s) => normTitle(s?.title) === t)?.title ?? t;
|
|
14860
|
+
gaps.push(`"${original}": declared at crew_begin but has no verdict in the report — a declared subtask was silently dropped`);
|
|
14861
|
+
}
|
|
14893
14862
|
}
|
|
14894
|
-
|
|
14895
|
-
return { costUsd: usd };
|
|
14863
|
+
return { ok: gaps.length === 0, gaps };
|
|
14896
14864
|
}
|
|
14897
14865
|
|
|
14898
14866
|
// src/dispatch-prompt.ts
|
|
14899
|
-
var
|
|
14867
|
+
var WORKER_MANDATES = [
|
|
14900
14868
|
"Mandates (non-negotiable):",
|
|
14901
14869
|
"- Use workspace-relative paths ONLY. Never write to or read from absolute paths outside this workspace; the workspace root is where you were started, not the filesystem root.",
|
|
14902
14870
|
"- If any tool call is refused or denied, report the refusal VERBATIM in your final answer and stop that line of work. Never improvise a workaround around a permission denial.",
|
|
14903
14871
|
"- End with conclusions backed by evidence references (file:line, command output, or the exact file/content you produced)."
|
|
14904
14872
|
].join(`
|
|
14905
14873
|
`);
|
|
14906
|
-
var READONLY_DISCIPLINE = `You are a readonly
|
|
14907
|
-
var WRITE_DISCIPLINE = `You are
|
|
14908
|
-
function
|
|
14909
|
-
const
|
|
14910
|
-
return [
|
|
14911
|
-
"[forge:dispatch] You were dispatched by the forge main agent as a scoped worker.",
|
|
14912
|
-
discipline,
|
|
14913
|
-
...input.role ? ["", "Your role:", input.role.trim()] : [],
|
|
14914
|
-
"",
|
|
14915
|
-
"Task:",
|
|
14916
|
-
input.prompt.trim(),
|
|
14917
|
-
"",
|
|
14918
|
-
COMMON_MANDATES,
|
|
14919
|
-
"",
|
|
14920
|
-
"Finish with a single concluding report (the main agent will read only this): what you did, what you found/produced, and the evidence."
|
|
14921
|
-
].join(`
|
|
14874
|
+
var READONLY_DISCIPLINE = (agent) => `You are forge-${agent}, a readonly worker: you gather, read, run read-only checks, and REPORT. Your restrictions are the design, not obstacles — do not attempt to work around them; report what you found instead.`;
|
|
14875
|
+
var WRITE_DISCIPLINE = (agent) => `You are forge-${agent}, a worker executing scoped tasks handed to you through the task tool. Do exactly the task, nothing else: no refactoring beyond scope, no unrelated files, no side quests.`;
|
|
14876
|
+
function materializeAgentPrompt(input) {
|
|
14877
|
+
const frame = input.shape === "readonly" ? READONLY_DISCIPLINE(input.agent) : WRITE_DISCIPLINE(input.agent);
|
|
14878
|
+
return [frame, "", "Your role:", input.role.trim(), "", WORKER_MANDATES].join(`
|
|
14922
14879
|
`);
|
|
14923
14880
|
}
|
|
14924
14881
|
|
|
14925
|
-
// src/dispatch-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
for (const entry of cfg.roster) {
|
|
14936
|
-
if (!entry.profiles.includes(tierId))
|
|
14937
|
-
continue;
|
|
14938
|
-
let reason;
|
|
14939
|
-
if (exclude.includes(entry.model))
|
|
14940
|
-
reason = "excluded after a failed attempt";
|
|
14941
|
-
else if (!available(entry.model))
|
|
14942
|
-
reason = "identity currently unavailable";
|
|
14943
|
-
else if (depth !== undefined && !entry.expose.includes(depth))
|
|
14944
|
-
reason = `depth "${depth}" not exposed`;
|
|
14945
|
-
else
|
|
14946
|
-
reason = "eligible";
|
|
14947
|
-
lines.push({ identity: entry.model, expose: [...entry.expose], verified: entry.verified, reason });
|
|
14948
|
-
}
|
|
14949
|
-
return lines;
|
|
14950
|
-
}
|
|
14951
|
-
function costRank(catalog, identity) {
|
|
14952
|
-
const cost = identityCost(catalog, identity);
|
|
14953
|
-
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number")
|
|
14954
|
-
return Number.POSITIVE_INFINITY;
|
|
14955
|
-
return cost.input + cost.output;
|
|
14956
|
-
}
|
|
14957
|
-
function resolveDispatch(cfg, req, available, catalog = { providers: {} }) {
|
|
14958
|
-
const tier = cfg.tiers[req.profile];
|
|
14959
|
-
if (!tier) {
|
|
14960
|
-
return {
|
|
14961
|
-
ok: false,
|
|
14962
|
-
error: {
|
|
14963
|
-
code: "unknown-tier",
|
|
14964
|
-
message: `unknown tier "${req.profile}"; known tiers: ${Object.keys(cfg.tiers).join(", ")}`,
|
|
14965
|
-
menu: [],
|
|
14966
|
-
vocabulary: exposedVocabulary(cfg)
|
|
14967
|
-
}
|
|
14968
|
-
};
|
|
14969
|
-
}
|
|
14970
|
-
const exclude = req.exclude ?? [];
|
|
14971
|
-
if (tier.model) {
|
|
14972
|
-
const pinnedIdentity = tier.model;
|
|
14973
|
-
const entry = cfg.roster.find((e) => e.model === pinnedIdentity);
|
|
14974
|
-
const expose = entry?.expose ?? [];
|
|
14975
|
-
const menu = [
|
|
14976
|
-
{ identity: pinnedIdentity, expose, verified: entry?.verified ?? false, reason: "pinned tier target" }
|
|
14977
|
-
];
|
|
14978
|
-
if (!available(pinnedIdentity)) {
|
|
14979
|
-
return {
|
|
14980
|
-
ok: false,
|
|
14981
|
-
error: {
|
|
14982
|
-
code: "pin-unavailable",
|
|
14983
|
-
message: `tier "${req.profile}" is pinned to ${pinnedIdentity}, which is currently unavailable; fix or unpin the tier — no fallback model will be used`,
|
|
14984
|
-
menu,
|
|
14985
|
-
vocabulary: exposedVocabulary(cfg)
|
|
14986
|
-
}
|
|
14987
|
-
};
|
|
14988
|
-
}
|
|
14989
|
-
const depth2 = req.depth ?? tier.depth;
|
|
14990
|
-
if (depth2 === undefined) {
|
|
14991
|
-
return {
|
|
14992
|
-
ok: false,
|
|
14993
|
-
error: {
|
|
14994
|
-
code: "depth-required",
|
|
14995
|
-
message: `tier "${req.profile}" is pinned without a depth and none was requested; pass depth explicitly`,
|
|
14996
|
-
menu,
|
|
14997
|
-
vocabulary: exposedVocabulary(cfg)
|
|
14998
|
-
}
|
|
14999
|
-
};
|
|
15000
|
-
}
|
|
15001
|
-
if (!expose.includes(depth2)) {
|
|
15002
|
-
return {
|
|
15003
|
-
ok: false,
|
|
15004
|
-
error: {
|
|
15005
|
-
code: "pin-depth-mismatch",
|
|
15006
|
-
message: `pinned model ${pinnedIdentity} does not expose depth "${depth2}" (expose: ${expose.join(", ") || "none"}); exact match only — no clamping`,
|
|
15007
|
-
menu,
|
|
15008
|
-
vocabulary: exposedVocabulary(cfg)
|
|
15009
|
-
}
|
|
15010
|
-
};
|
|
15011
|
-
}
|
|
15012
|
-
return { ok: true, identity: pinnedIdentity, depth: depth2, pinned: true };
|
|
15013
|
-
}
|
|
15014
|
-
const depth = req.depth ?? tier.defaultDepth;
|
|
15015
|
-
if (depth === undefined) {
|
|
15016
|
-
return {
|
|
15017
|
-
ok: false,
|
|
15018
|
-
error: {
|
|
15019
|
-
code: "depth-required",
|
|
15020
|
-
message: `tier "${req.profile}" has no usable default depth (its default was removed at validation); pass depth explicitly`,
|
|
15021
|
-
menu: menuFor(cfg, req.profile, undefined, exclude, available),
|
|
15022
|
-
vocabulary: exposedVocabulary(cfg)
|
|
15023
|
-
}
|
|
15024
|
-
};
|
|
15025
|
-
}
|
|
15026
|
-
const candidates = cfg.roster.map((entry, index) => ({ entry, index })).filter(({ entry }) => entry.profiles.includes(req.profile)).filter(({ entry }) => !exclude.includes(entry.model)).filter(({ entry }) => available(entry.model)).filter(({ entry }) => entry.expose.includes(depth)).sort((a, b) => a.index - b.index || costRank(catalog, a.entry.model) - costRank(catalog, b.entry.model));
|
|
15027
|
-
if (candidates.length === 0) {
|
|
15028
|
-
return {
|
|
15029
|
-
ok: false,
|
|
15030
|
-
error: {
|
|
15031
|
-
code: "no-candidate",
|
|
15032
|
-
message: `no candidate serves tier "${req.profile}" at depth "${depth}"; pick a depth from the menu's exposures (exact match — the plugin never clamps)`,
|
|
15033
|
-
menu: menuFor(cfg, req.profile, depth, exclude, available),
|
|
15034
|
-
vocabulary: exposedVocabulary(cfg)
|
|
15035
|
-
}
|
|
15036
|
-
};
|
|
15037
|
-
}
|
|
15038
|
-
return { ok: true, identity: candidates[0].entry.model, depth, pinned: false };
|
|
14882
|
+
// src/dispatch-tiers.ts
|
|
14883
|
+
var MUTATING_TOOLS = ["write", "edit", "bash"];
|
|
14884
|
+
var ROLE_PROMPTS = {
|
|
14885
|
+
research: "You are a research worker: search, read, and run read-only checks to answer the assigned question. Report findings as a compact brief with sources and file:line evidence; state plainly what could not be verified.",
|
|
14886
|
+
review: "You are a review worker: read the diff or files under review, run read-only checks/tests, and report findings as a prioritized list, each with file:line evidence and a concrete fix suggestion. End with a single-line verdict."
|
|
14887
|
+
};
|
|
14888
|
+
function rolePromptFor(agentId, def) {
|
|
14889
|
+
if (def?.prompt && def.prompt.trim().length > 0)
|
|
14890
|
+
return def.prompt;
|
|
14891
|
+
return ROLE_PROMPTS[agentId] ?? `You are a ${agentId} worker. Execute the assigned task exactly as scoped.`;
|
|
15039
14892
|
}
|
|
15040
|
-
function
|
|
15041
|
-
const
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
}
|
|
15051
|
-
if (!def.model || typeof def.model !== "string") {
|
|
15052
|
-
return {
|
|
15053
|
-
ok: false,
|
|
15054
|
-
error: {
|
|
15055
|
-
code: "pin-unavailable",
|
|
15056
|
-
message: `agent "${req.agent}" has no usable model (model: ${JSON.stringify(def.model)}); fix its "model" in forge.json — no fallback model will be used`
|
|
15057
|
-
}
|
|
15058
|
-
};
|
|
15059
|
-
}
|
|
15060
|
-
if (!available(def.model)) {
|
|
15061
|
-
return {
|
|
15062
|
-
ok: false,
|
|
15063
|
-
error: {
|
|
15064
|
-
code: "pin-unavailable",
|
|
15065
|
-
message: `agent "${req.agent}" is pinned to ${def.model}, which is not configured on this host; fix the agent's "model" in forge.json — no fallback model will be used`
|
|
15066
|
-
}
|
|
15067
|
-
};
|
|
15068
|
-
}
|
|
15069
|
-
const depths = Array.isArray(def.depths) ? def.depths : [];
|
|
15070
|
-
if (depths.length === 0) {
|
|
15071
|
-
return {
|
|
15072
|
-
ok: false,
|
|
15073
|
-
error: {
|
|
15074
|
-
code: "depth-not-in-set",
|
|
15075
|
-
message: `agent "${req.agent}" defines no depths; give it at least one depth in forge.json (canonical words: none, low, medium, high, max — or the model's native level names)`
|
|
15076
|
-
}
|
|
15077
|
-
};
|
|
15078
|
-
}
|
|
15079
|
-
const depth = req.depth ?? depths[0];
|
|
15080
|
-
if (!depths.includes(depth)) {
|
|
15081
|
-
return {
|
|
15082
|
-
ok: false,
|
|
15083
|
-
error: {
|
|
15084
|
-
code: "depth-not-in-set",
|
|
15085
|
-
message: `agent "${req.agent}" does not allow depth "${depth}" (allowed: ${depths.join(", ")}) — exact match only, the plugin never clamps`
|
|
15086
|
-
}
|
|
15087
|
-
};
|
|
14893
|
+
function agentDescriptionFor(agentId, def) {
|
|
14894
|
+
const shape = def.shape === "write" ? "write" : "readonly";
|
|
14895
|
+
return `forge subagent "${agentId}" (${shape}, pinned ${def.model}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
|
|
14896
|
+
}
|
|
14897
|
+
function forgeAgentDef(agentId, def) {
|
|
14898
|
+
const permission = { task: "deny" };
|
|
14899
|
+
const shape = def.shape ?? "readonly";
|
|
14900
|
+
if (shape === "readonly") {
|
|
14901
|
+
for (const t of MUTATING_TOOLS)
|
|
14902
|
+
permission[t] = "deny";
|
|
15088
14903
|
}
|
|
15089
|
-
|
|
14904
|
+
Object.assign(permission, def.permission ?? {});
|
|
14905
|
+
permission.task = "deny";
|
|
14906
|
+
return {
|
|
14907
|
+
description: agentDescriptionFor(agentId, def),
|
|
14908
|
+
mode: "subagent",
|
|
14909
|
+
model: def.model,
|
|
14910
|
+
prompt: materializeAgentPrompt({
|
|
14911
|
+
agent: agentId,
|
|
14912
|
+
shape,
|
|
14913
|
+
role: rolePromptFor(agentId, def)
|
|
14914
|
+
}),
|
|
14915
|
+
permission
|
|
14916
|
+
};
|
|
15090
14917
|
}
|
|
15091
14918
|
|
|
15092
14919
|
// src/dispatch-depth.ts
|
|
@@ -15147,633 +14974,13 @@ function applyDepthTranslation(options, t) {
|
|
|
15147
14974
|
options.thinking = { type: "enabled" };
|
|
15148
14975
|
}
|
|
15149
14976
|
}
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
quick: "You are forge-quick, a mechanical small-change worker. Make the smallest correct edit the task names (rename, constant, one-liner fix). No refactoring, no drive-by improvements."
|
|
15158
|
-
};
|
|
15159
|
-
function tierAgentPrompt(tierId) {
|
|
15160
|
-
return TIER_PROMPTS[tierId] ?? `You are forge-${tierId}, a dispatched worker. Execute the dispatched task exactly as scoped.`;
|
|
15161
|
-
}
|
|
15162
|
-
var ROLE_PROMPTS = {
|
|
15163
|
-
research: "You are a research worker: search, read, and run read-only checks to answer the dispatched question. Report findings as a compact brief with sources and file:line evidence; state plainly what could not be verified.",
|
|
15164
|
-
review: "You are a review worker: read the diff or files under review, run read-only checks/tests, and report findings as a prioritized list, each with file:line evidence and a concrete fix suggestion. End with a single-line verdict."
|
|
15165
|
-
};
|
|
15166
|
-
function rolePromptFor(agentId, def) {
|
|
15167
|
-
if (def?.prompt && def.prompt.trim().length > 0)
|
|
15168
|
-
return def.prompt;
|
|
15169
|
-
return ROLE_PROMPTS[agentId] ?? `You are forge-${agentId}, a dispatched worker. Execute the dispatched task exactly as scoped.`;
|
|
15170
|
-
}
|
|
15171
|
-
function forgeAgentDef(agentId, def) {
|
|
15172
|
-
const permission = { task: "deny" };
|
|
15173
|
-
const shape = def.shape ?? "readonly";
|
|
15174
|
-
if (shape === "readonly") {
|
|
15175
|
-
for (const t of MUTATING_TOOLS)
|
|
15176
|
-
permission[t] = "deny";
|
|
15177
|
-
}
|
|
15178
|
-
Object.assign(permission, def.permission ?? {});
|
|
15179
|
-
permission.task = "deny";
|
|
15180
|
-
const shapeLine = shape === "readonly" ? "Readonly agent: mutating tools are denied by design — report findings, never work around restrictions." : "Write agent: execute the scoped task with minimal, focused changes.";
|
|
15181
|
-
return {
|
|
15182
|
-
description: `forge dispatch agent "${agentId}" (${shape}) — spawned by forge_dispatch, not user-facing.`,
|
|
15183
|
-
mode: "subagent",
|
|
15184
|
-
hidden: true,
|
|
15185
|
-
prompt: `${rolePromptFor(agentId, def)}
|
|
15186
|
-
|
|
15187
|
-
${shapeLine}`,
|
|
15188
|
-
permission
|
|
15189
|
-
};
|
|
15190
|
-
}
|
|
15191
|
-
function tierAgentDef(tierId, tier) {
|
|
15192
|
-
const permission = { task: "deny" };
|
|
15193
|
-
if (tier.shape === "readonly") {
|
|
15194
|
-
for (const t of MUTATING_TOOLS)
|
|
15195
|
-
permission[t] = "deny";
|
|
15196
|
-
}
|
|
15197
|
-
const shapeLine = tier.shape === "readonly" ? "Readonly tier: mutating tools are denied by design — report findings, never work around restrictions." : "Write tier: execute the scoped task with minimal, focused changes.";
|
|
15198
|
-
const defaultLine = tier.defaultDepth ? ` Default reasoning depth: ${tier.defaultDepth}.` : "";
|
|
15199
|
-
return {
|
|
15200
|
-
description: `forge dispatch tier "${tierId}" (${tier.shape}) — spawned by forge_dispatch, not user-facing.${defaultLine}`,
|
|
15201
|
-
mode: "subagent",
|
|
15202
|
-
hidden: true,
|
|
15203
|
-
prompt: `${tierAgentPrompt(tierId)}
|
|
15204
|
-
|
|
15205
|
-
${shapeLine}`,
|
|
15206
|
-
permission
|
|
15207
|
-
};
|
|
15208
|
-
}
|
|
15209
|
-
|
|
15210
|
-
// src/dispatch-engine.ts
|
|
15211
|
-
var DEFAULT_DISPATCH_TIMEOUT_MS = 600000;
|
|
15212
|
-
var MAX_DISPATCH_TIMEOUT_MS = 600000;
|
|
15213
|
-
var DEFAULT_MAX_CONCURRENT = 4;
|
|
15214
|
-
var DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
15215
|
-
|
|
15216
|
-
class DispatchError extends Error {
|
|
15217
|
-
code;
|
|
15218
|
-
constructor(message, code) {
|
|
15219
|
-
super(message);
|
|
15220
|
-
this.name = "DispatchError";
|
|
15221
|
-
this.code = code;
|
|
15222
|
-
}
|
|
15223
|
-
}
|
|
15224
|
-
function textOf(messages) {
|
|
15225
|
-
const assistant = [...messages].reverse().find((m) => (m.info?.role ?? m.role) === "assistant");
|
|
15226
|
-
if (!assistant)
|
|
15227
|
-
return "";
|
|
15228
|
-
return (assistant.parts ?? []).filter((p) => p.type === "text" || typeof p.text === "string").map((p) => p.text ?? "").join(`
|
|
15229
|
-
`).trim();
|
|
15230
|
-
}
|
|
15231
|
-
function tokensOf(messages) {
|
|
15232
|
-
return messages.filter((m) => (m.info?.role ?? m.role) === "assistant" && m.info?.tokens).map((m) => m.info.tokens);
|
|
15233
|
-
}
|
|
15234
|
-
function createDispatchEngine(deps) {
|
|
15235
|
-
const maxConcurrent = deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
15236
|
-
const timeoutMs = Math.min(deps.timeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS, MAX_DISPATCH_TIMEOUT_MS);
|
|
15237
|
-
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
15238
|
-
const inFlight = new Set;
|
|
15239
|
-
let seq = 0;
|
|
15240
|
-
const api2 = async (path, init) => {
|
|
15241
|
-
const res = await deps.fetcher(`${deps.serverUrl}${path}`, {
|
|
15242
|
-
...init,
|
|
15243
|
-
headers: { "content-type": "application/json", ...init?.headers ?? {} }
|
|
15244
|
-
});
|
|
15245
|
-
const text = await res.text();
|
|
15246
|
-
if (!res.ok)
|
|
15247
|
-
throw new DispatchError(`${init?.method ?? "GET"} ${path} -> ${res.status}: ${text.slice(0, 300)}`, "host-error");
|
|
15248
|
-
return text ? JSON.parse(text) : undefined;
|
|
15249
|
-
};
|
|
15250
|
-
const fetchMessages = async (sessionID) => {
|
|
15251
|
-
const list = await api2(`/session/${sessionID}/message`);
|
|
15252
|
-
return Array.isArray(list) ? list : [];
|
|
15253
|
-
};
|
|
15254
|
-
const killControls = new Map;
|
|
15255
|
-
const resolveOrThrow = (profile, depth, exclude) => {
|
|
15256
|
-
const r = resolveDispatch(deps.cfg, { profile, depth, exclude }, deps.available, deps.catalog);
|
|
15257
|
-
if (!r.ok) {
|
|
15258
|
-
deps.sink({ ts: new Date().toISOString(), event: "resolve-error", tier: profile, outcome: r.error.code, note: r.error.message });
|
|
15259
|
-
throw new DispatchError(`${r.error.message}
|
|
15260
|
-
Menu:
|
|
15261
|
-
${r.error.menu.map((m) => `- ${m.identity} expose=[${m.expose.join(", ") || "none"}] ${m.reason}`).join(`
|
|
15262
|
-
`)}
|
|
15263
|
-
Vocabulary: ${r.error.vocabulary.join(", ") || "(none)"}`, r.error.code);
|
|
15264
|
-
}
|
|
15265
|
-
return r;
|
|
15266
|
-
};
|
|
15267
|
-
const resolveForThrow = (req, exclude) => {
|
|
15268
|
-
if (deps.legacy && req.agent in deps.cfg.tiers) {
|
|
15269
|
-
const r = resolveOrThrow(req.agent, req.depth, exclude);
|
|
15270
|
-
return {
|
|
15271
|
-
model: r.identity,
|
|
15272
|
-
depth: r.depth,
|
|
15273
|
-
agentPath: false,
|
|
15274
|
-
shape: deps.cfg.tiers[req.agent]?.shape ?? "readonly",
|
|
15275
|
-
role: tierAgentPrompt(req.agent)
|
|
15276
|
-
};
|
|
15277
|
-
}
|
|
15278
|
-
const a = resolveAgent(deps.agents(), { agent: req.agent, depth: req.depth }, deps.available);
|
|
15279
|
-
if (!a.ok) {
|
|
15280
|
-
deps.sink({ ts: new Date().toISOString(), event: "resolve-error", tier: req.agent, agent: req.agent, outcome: a.error.code, note: a.error.message });
|
|
15281
|
-
throw new DispatchError(a.error.message, a.error.code);
|
|
15282
|
-
}
|
|
15283
|
-
return {
|
|
15284
|
-
model: a.model,
|
|
15285
|
-
depth: a.depth,
|
|
15286
|
-
agentPath: true,
|
|
15287
|
-
shape: a.def?.shape === "write" ? "write" : "readonly",
|
|
15288
|
-
role: rolePromptFor(req.agent, a.def)
|
|
15289
|
-
};
|
|
15290
|
-
};
|
|
15291
|
-
const translationFor = (identity, depth) => {
|
|
15292
|
-
const providerID = identity.slice(0, identity.indexOf("/"));
|
|
15293
|
-
const family = deps.providerFamily(providerID) ?? "unknown";
|
|
15294
|
-
const ladder = nativeLadder(deps.catalog, identity);
|
|
15295
|
-
return translateDepth(depth, family, ladder);
|
|
15296
|
-
};
|
|
15297
|
-
const translationOrThrow = (identity, depth) => {
|
|
15298
|
-
const translation = translationFor(identity, depth);
|
|
15299
|
-
if (translation.kind === "no-mapping") {
|
|
15300
|
-
deps.sink({ ts: new Date().toISOString(), event: "resolve-error", agent: undefined, identity, depth, outcome: "no-native-mapping", note: translation.message });
|
|
15301
|
-
throw new DispatchError(translation.message, "no-native-mapping");
|
|
15302
|
-
}
|
|
15303
|
-
return translation;
|
|
15304
|
-
};
|
|
15305
|
-
async function runAttempt(r, req, parentSessionID, t0, ctl, onChild, dispatchId) {
|
|
15306
|
-
const providerID = r.model.slice(0, r.model.indexOf("/"));
|
|
15307
|
-
const translation = translationOrThrow(r.model, r.depth);
|
|
15308
|
-
let childID = "";
|
|
15309
|
-
const dir = encodeURIComponent(deps.workspace);
|
|
15310
|
-
let created;
|
|
15311
|
-
try {
|
|
15312
|
-
created = await api2(`/session?directory=${dir}`, { method: "POST", body: JSON.stringify(parentSessionID ? { parentID: parentSessionID } : {}) });
|
|
15313
|
-
} catch {
|
|
15314
|
-
created = await api2(`/session?directory=${dir}`, { method: "POST", body: JSON.stringify({}) });
|
|
15315
|
-
}
|
|
15316
|
-
childID = created?.id ?? "";
|
|
15317
|
-
if (!childID)
|
|
15318
|
-
throw new DispatchError("host returned no session id for the child session", "host-error");
|
|
15319
|
-
onChild?.(childID);
|
|
15320
|
-
deps.onDepth(childID, translation);
|
|
15321
|
-
const depthInjected = translation.disclose;
|
|
15322
|
-
let deadlineHit = false;
|
|
15323
|
-
const deadlinePromise = deps.sleep(Math.max(timeoutMs - (deps.now() - t0), 0)).then(() => {
|
|
15324
|
-
deadlineHit = true;
|
|
15325
|
-
});
|
|
15326
|
-
const turnPost = api2(`/session/${childID}/message`, {
|
|
15327
|
-
method: "POST",
|
|
15328
|
-
body: JSON.stringify({
|
|
15329
|
-
model: { providerID, modelID: r.model.slice(r.model.indexOf("/") + 1) },
|
|
15330
|
-
agent: `forge-${req.agent}`,
|
|
15331
|
-
parts: [{ type: "text", text: composeWorkerPrompt({ prompt: req.prompt, agent: req.agent, shape: r.shape, ...r.role ? { role: r.role } : {} }) }]
|
|
15332
|
-
})
|
|
15333
|
-
});
|
|
15334
|
-
await Promise.race([turnPost, deadlinePromise]);
|
|
15335
|
-
if (deadlineHit && deps.now() - t0 >= timeoutMs) {
|
|
15336
|
-
const elapsed = deps.now() - t0;
|
|
15337
|
-
deps.sink({
|
|
15338
|
-
ts: new Date().toISOString(),
|
|
15339
|
-
event: "timeout",
|
|
15340
|
-
tier: req.agent,
|
|
15341
|
-
agent: req.agent,
|
|
15342
|
-
identity: r.model,
|
|
15343
|
-
depth: r.depth,
|
|
15344
|
-
sessionID: childID,
|
|
15345
|
-
durationMs: elapsed,
|
|
15346
|
-
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15347
|
-
});
|
|
15348
|
-
throw new DispatchError(`forge_dispatch timed out after ${elapsed}ms (deadline ${timeoutMs}ms). Partial state: child session ${childID} is left for the host to reclaim; its transcript remains queryable via the host API.`, "timeout");
|
|
15349
|
-
}
|
|
15350
|
-
await turnPost;
|
|
15351
|
-
let poll = null;
|
|
15352
|
-
let messages = [];
|
|
15353
|
-
for (;; ) {
|
|
15354
|
-
if (ctl?.fired)
|
|
15355
|
-
throw new KillRequested;
|
|
15356
|
-
const elapsed = deps.now() - t0;
|
|
15357
|
-
if (elapsed >= timeoutMs) {
|
|
15358
|
-
deps.sink({
|
|
15359
|
-
ts: new Date().toISOString(),
|
|
15360
|
-
event: "timeout",
|
|
15361
|
-
tier: req.agent,
|
|
15362
|
-
agent: req.agent,
|
|
15363
|
-
identity: r.model,
|
|
15364
|
-
depth: r.depth,
|
|
15365
|
-
sessionID: childID,
|
|
15366
|
-
durationMs: elapsed,
|
|
15367
|
-
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15368
|
-
});
|
|
15369
|
-
throw new DispatchError(`forge_dispatch timed out after ${elapsed}ms (deadline ${timeoutMs}ms). Partial state: child session ${childID} is left for the host to reclaim; its transcript remains queryable via the host API.`, "timeout");
|
|
15370
|
-
}
|
|
15371
|
-
await deps.sleep(pollIntervalMs);
|
|
15372
|
-
messages = await (ctl ? Promise.race([fetchMessages(childID), ctl.promise.then(() => [])]) : fetchMessages(childID));
|
|
15373
|
-
if (ctl?.fired)
|
|
15374
|
-
throw new KillRequested;
|
|
15375
|
-
poll = completionVerdict(poll, { count: messages.length, text: textOf(messages) });
|
|
15376
|
-
if (poll.verdict === "complete" || poll.verdict === "empty")
|
|
15377
|
-
break;
|
|
15378
|
-
}
|
|
15379
|
-
const durationMs = deps.now() - t0;
|
|
15380
|
-
if (poll.verdict === "empty") {
|
|
15381
|
-
deps.sink({
|
|
15382
|
-
ts: new Date().toISOString(),
|
|
15383
|
-
event: "empty-response",
|
|
15384
|
-
tier: req.agent,
|
|
15385
|
-
agent: req.agent,
|
|
15386
|
-
identity: r.model,
|
|
15387
|
-
depth: r.depth,
|
|
15388
|
-
sessionID: childID,
|
|
15389
|
-
durationMs,
|
|
15390
|
-
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15391
|
-
});
|
|
15392
|
-
throw new DispatchError(`child session ${childID} returned an empty response (0-token assistant message). This is a known quirk of keyless endpoints combined with restricted tiers — see the README dispatch notes. The dispatch is NOT counted as success.`, "empty-response");
|
|
15393
|
-
}
|
|
15394
|
-
const tokens = sumTokens(tokensOf(messages));
|
|
15395
|
-
const price = priceFor(deps.catalog, r.model);
|
|
15396
|
-
const cost = computeCost(tokens, price?.spec ?? null);
|
|
15397
|
-
const result = {
|
|
15398
|
-
agent: req.agent,
|
|
15399
|
-
requested: { agent: req.agent, depth: req.depth },
|
|
15400
|
-
actual: { model: r.model, depth: r.depth },
|
|
15401
|
-
sessionID: childID,
|
|
15402
|
-
durationMs,
|
|
15403
|
-
tokens,
|
|
15404
|
-
costUsd: cost.costUsd,
|
|
15405
|
-
...cost.note ? { costNote: cost.note } : {},
|
|
15406
|
-
...price?.label ? { priceSnapshot: price.label } : {},
|
|
15407
|
-
text: textOf(messages),
|
|
15408
|
-
depthTranslation: depthInjected
|
|
15409
|
-
};
|
|
15410
|
-
deps.sink({
|
|
15411
|
-
ts: new Date().toISOString(),
|
|
15412
|
-
event: "completed",
|
|
15413
|
-
tier: req.agent,
|
|
15414
|
-
agent: req.agent,
|
|
15415
|
-
identity: r.model,
|
|
15416
|
-
depth: r.depth,
|
|
15417
|
-
sessionID: childID,
|
|
15418
|
-
outcome: "completed",
|
|
15419
|
-
tokens,
|
|
15420
|
-
costUsd: cost.costUsd,
|
|
15421
|
-
durationMs,
|
|
15422
|
-
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15423
|
-
});
|
|
15424
|
-
return result;
|
|
15425
|
-
}
|
|
15426
|
-
function bgTerminal(reg, dispatchId, identity, req, t0, e) {
|
|
15427
|
-
const report = {
|
|
15428
|
-
agent: req.agent,
|
|
15429
|
-
requested: { agent: req.agent, depth: req.depth },
|
|
15430
|
-
actual: { model: identity, depth: null },
|
|
15431
|
-
sessionID: "",
|
|
15432
|
-
outcome: "error",
|
|
15433
|
-
durationMs: deps.now() - t0,
|
|
15434
|
-
error: e instanceof Error ? e.message : String(e)
|
|
15435
|
-
};
|
|
15436
|
-
if (e instanceof DispatchError && e.code === "timeout") {
|
|
15437
|
-
report.outcome = "timeout";
|
|
15438
|
-
reg.markTimeout(dispatchId, report);
|
|
15439
|
-
deps.onTerminal?.(reg.get(dispatchId).parentSessionID);
|
|
15440
|
-
return;
|
|
15441
|
-
}
|
|
15442
|
-
deps.sink({ ts: new Date().toISOString(), event: "error", tier: req.agent, identity, outcome: "error", note: report.error });
|
|
15443
|
-
reg.markError(dispatchId, report);
|
|
15444
|
-
deps.onTerminal?.(reg.get(dispatchId).parentSessionID);
|
|
15445
|
-
}
|
|
15446
|
-
async function dispatch(req, parentSessionID, opts) {
|
|
15447
|
-
const t0 = deps.now();
|
|
15448
|
-
if (inFlight.size >= maxConcurrent) {
|
|
15449
|
-
deps.sink({ ts: new Date().toISOString(), event: "refused-cap", tier: req.agent, agent: req.agent, note: `${inFlight.size} in flight` });
|
|
15450
|
-
throw new DispatchError(`forge_dispatch refused: ${inFlight.size} dispatches already in flight (cap ${maxConcurrent}). Wait for a completion and retry.`, "cap-refused");
|
|
15451
|
-
}
|
|
15452
|
-
const bg = opts?.background === true;
|
|
15453
|
-
if (bg && !deps.registry)
|
|
15454
|
-
throw new DispatchError("background dispatch requires the dispatch registry (internal error)", "host-error");
|
|
15455
|
-
const first = resolveForThrow(req, undefined);
|
|
15456
|
-
translationOrThrow(first.model, first.depth);
|
|
15457
|
-
if (bg) {
|
|
15458
|
-
const reg = deps.registry;
|
|
15459
|
-
const { dispatchId } = reg.submit({ sessionID: "", identity: first.model, depth: first.depth, tier: req.agent, parentSessionID: parentSessionID ?? "" });
|
|
15460
|
-
let fire = () => {};
|
|
15461
|
-
const promise2 = new Promise((res) => {
|
|
15462
|
-
fire = res;
|
|
15463
|
-
});
|
|
15464
|
-
const ctl = { fired: false, promise: promise2, fire: () => {
|
|
15465
|
-
ctl.fired = true;
|
|
15466
|
-
fire();
|
|
15467
|
-
} };
|
|
15468
|
-
killControls.set(dispatchId, ctl);
|
|
15469
|
-
const slot = `d${++seq}`;
|
|
15470
|
-
inFlight.add(slot);
|
|
15471
|
-
(async () => {
|
|
15472
|
-
try {
|
|
15473
|
-
if (reg.get(dispatchId).state === "killed")
|
|
15474
|
-
return;
|
|
15475
|
-
reg.markRunning(dispatchId);
|
|
15476
|
-
let current = first;
|
|
15477
|
-
let exclude2 = undefined;
|
|
15478
|
-
for (let attemptNo = 0;; attemptNo++) {
|
|
15479
|
-
try {
|
|
15480
|
-
const result = await runAttempt(current, req, parentSessionID, t0, ctl, (sid) => {
|
|
15481
|
-
try {
|
|
15482
|
-
reg.get(dispatchId).sessionID = sid;
|
|
15483
|
-
} catch {}
|
|
15484
|
-
}, dispatchId);
|
|
15485
|
-
if (reg.get(dispatchId).state === "killed") {
|
|
15486
|
-
reg.noteLateCompletion(dispatchId, result);
|
|
15487
|
-
deps.sink({ ts: new Date().toISOString(), event: "kill-late-completion", tier: req.agent, agent: req.agent, identity: current.model, sessionID: result.sessionID, outcome: "kill-late-completion" });
|
|
15488
|
-
} else {
|
|
15489
|
-
reg.markCompleted(dispatchId, result);
|
|
15490
|
-
deps.onTerminal?.(reg.get(dispatchId).parentSessionID);
|
|
15491
|
-
}
|
|
15492
|
-
return;
|
|
15493
|
-
} catch (e) {
|
|
15494
|
-
if (e instanceof KillRequested)
|
|
15495
|
-
return;
|
|
15496
|
-
if (reg.get(dispatchId).state === "killed")
|
|
15497
|
-
return;
|
|
15498
|
-
const retryable = !current.agentPath && e instanceof DispatchError && (e.code === "host-error" || e.code === "empty-response");
|
|
15499
|
-
const alternative = retryable ? resolveDispatch(deps.cfg, { profile: req.agent, depth: req.depth, exclude: [current.model] }, deps.available, deps.catalog) : { ok: false };
|
|
15500
|
-
if (!retryable || attemptNo > 0 || !alternative.ok) {
|
|
15501
|
-
bgTerminal(reg, dispatchId, current.model, req, t0, e);
|
|
15502
|
-
return;
|
|
15503
|
-
}
|
|
15504
|
-
deps.sink({ ts: new Date().toISOString(), event: "retry-excluded", tier: req.agent, agent: req.agent, identity: current.model, note: `${current.model}: ${e.code}` });
|
|
15505
|
-
exclude2 = [current.model];
|
|
15506
|
-
current = resolveForThrow(req, exclude2);
|
|
15507
|
-
reg.get(dispatchId).identity = current.model;
|
|
15508
|
-
}
|
|
15509
|
-
}
|
|
15510
|
-
} finally {
|
|
15511
|
-
inFlight.delete(slot);
|
|
15512
|
-
killControls.delete(dispatchId);
|
|
15513
|
-
}
|
|
15514
|
-
})();
|
|
15515
|
-
return {
|
|
15516
|
-
dispatchId,
|
|
15517
|
-
agent: req.agent,
|
|
15518
|
-
requested: { agent: req.agent, depth: req.depth },
|
|
15519
|
-
resolved: first.model,
|
|
15520
|
-
depth: first.depth,
|
|
15521
|
-
queuedAt: reg.get(dispatchId).queuedAt
|
|
15522
|
-
};
|
|
15523
|
-
}
|
|
15524
|
-
let exclude = undefined;
|
|
15525
|
-
for (let attemptNo = 0;; attemptNo++) {
|
|
15526
|
-
const r = attemptNo === 0 ? first : resolveForThrow(req, exclude);
|
|
15527
|
-
const slot = `d${++seq}`;
|
|
15528
|
-
inFlight.add(slot);
|
|
15529
|
-
try {
|
|
15530
|
-
return await runAttempt(r, req, parentSessionID, t0, undefined, undefined);
|
|
15531
|
-
} catch (e) {
|
|
15532
|
-
const identity = r.model;
|
|
15533
|
-
const retryable = !r.agentPath && e instanceof DispatchError && (e.code === "host-error" || e.code === "empty-response");
|
|
15534
|
-
if (!retryable || attemptNo > 0)
|
|
15535
|
-
throw e;
|
|
15536
|
-
const alternative = resolveDispatch(deps.cfg, { profile: req.agent, depth: req.depth, exclude: [identity] }, deps.available, deps.catalog);
|
|
15537
|
-
if (!alternative.ok)
|
|
15538
|
-
throw e;
|
|
15539
|
-
deps.sink({ ts: new Date().toISOString(), event: "retry-excluded", tier: req.agent, agent: req.agent, identity, note: `${identity}: ${e.code}` });
|
|
15540
|
-
exclude = [identity];
|
|
15541
|
-
} finally {
|
|
15542
|
-
inFlight.delete(slot);
|
|
15543
|
-
}
|
|
15544
|
-
}
|
|
15545
|
-
}
|
|
15546
|
-
return {
|
|
15547
|
-
dispatch,
|
|
15548
|
-
kill: (dispatchId) => {
|
|
15549
|
-
const reg = deps.registry;
|
|
15550
|
-
if (!reg)
|
|
15551
|
-
throw new DispatchError("forge_dispatch_kill unavailable: no dispatch registry (internal error)", "kill-failed");
|
|
15552
|
-
const e = reg.get(dispatchId);
|
|
15553
|
-
if (e.state === "completed" || e.state === "timeout" || e.state === "error") {
|
|
15554
|
-
throw new DispatchError(`forge_dispatch_kill refused: dispatch ${dispatchId} already ${e.state}; use forge_dispatch_list for its result.`, "kill-failed");
|
|
15555
|
-
}
|
|
15556
|
-
if (e.state === "killed")
|
|
15557
|
-
throw new DispatchError(`dispatch ${dispatchId} is already killed.`, "kill-failed");
|
|
15558
|
-
reg.markKilled(dispatchId);
|
|
15559
|
-
deps.sink({ ts: new Date().toISOString(), event: "killed", tier: e.tier, identity: e.identity, depth: e.depth ?? undefined, sessionID: e.sessionID, outcome: "killed" });
|
|
15560
|
-
killControls.get(dispatchId)?.fire();
|
|
15561
|
-
},
|
|
15562
|
-
inFlightCount: () => inFlight.size,
|
|
15563
|
-
inFlightIDs: () => [...inFlight],
|
|
15564
|
-
dispose: () => {
|
|
15565
|
-
for (const slot of inFlight) {
|
|
15566
|
-
deps.sink({ ts: new Date().toISOString(), event: "lost-on-exit", note: `dispatch ${slot} in flight at host exit` });
|
|
15567
|
-
}
|
|
15568
|
-
inFlight.clear();
|
|
15569
|
-
}
|
|
15570
|
-
};
|
|
15571
|
-
}
|
|
15572
|
-
|
|
15573
|
-
class KillRequested extends Error {
|
|
15574
|
-
constructor() {
|
|
15575
|
-
super("kill requested");
|
|
15576
|
-
}
|
|
15577
|
-
}
|
|
15578
|
-
function priceFor(catalog, identity) {
|
|
15579
|
-
const slash = identity.indexOf("/");
|
|
15580
|
-
if (slash <= 0)
|
|
15581
|
-
return null;
|
|
15582
|
-
const model = catalog.providers?.[identity.slice(0, slash)]?.models?.[identity.slice(slash + 1)];
|
|
15583
|
-
const cost = model?.cost;
|
|
15584
|
-
if (!cost)
|
|
15585
|
-
return null;
|
|
15586
|
-
return {
|
|
15587
|
-
spec: {
|
|
15588
|
-
input: cost.input,
|
|
15589
|
-
output: cost.output,
|
|
15590
|
-
cacheRead: cost.cache_read,
|
|
15591
|
-
cacheWrite: cost.cache_write
|
|
15592
|
-
},
|
|
15593
|
-
label: "models.dev snapshot"
|
|
15594
|
-
};
|
|
15595
|
-
}
|
|
15596
|
-
|
|
15597
|
-
// src/dispatch-ledger.ts
|
|
15598
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
15599
|
-
import { dirname as dirname3 } from "node:path";
|
|
15600
|
-
var DISPATCH_LEDGER_MAX_ENTRIES = 200;
|
|
15601
|
-
var DISPATCH_LEDGER_MAX_BYTES = 1e6;
|
|
15602
|
-
function createDispatchLedger(logPath, maxEntries = DISPATCH_LEDGER_MAX_ENTRIES, maxBytes = DISPATCH_LEDGER_MAX_BYTES) {
|
|
15603
|
-
const append = (entry) => {
|
|
15604
|
-
try {
|
|
15605
|
-
mkdirSync4(dirname3(logPath), { recursive: true });
|
|
15606
|
-
let lines = [];
|
|
15607
|
-
try {
|
|
15608
|
-
lines = readFileSync6(logPath, "utf8").split(`
|
|
15609
|
-
`).filter((l) => l.trim().length > 0);
|
|
15610
|
-
} catch {}
|
|
15611
|
-
if (lines.length >= maxEntries)
|
|
15612
|
-
lines = lines.slice(lines.length - maxEntries + 1);
|
|
15613
|
-
lines.push(JSON.stringify(entry));
|
|
15614
|
-
let text = lines.join(`
|
|
15615
|
-
`) + `
|
|
15616
|
-
`;
|
|
15617
|
-
if (text.length > maxBytes) {
|
|
15618
|
-
const keep = Math.max(1, Math.floor(maxEntries / 2));
|
|
15619
|
-
text = lines.slice(-keep).join(`
|
|
15620
|
-
`) + `
|
|
15621
|
-
`;
|
|
15622
|
-
}
|
|
15623
|
-
writeFileSync4(logPath, text, "utf8");
|
|
15624
|
-
} catch {}
|
|
15625
|
-
};
|
|
15626
|
-
const entries = () => {
|
|
15627
|
-
try {
|
|
15628
|
-
return readFileSync6(logPath, "utf8").split(`
|
|
15629
|
-
`).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
15630
|
-
} catch {
|
|
15631
|
-
return [];
|
|
15632
|
-
}
|
|
15633
|
-
};
|
|
15634
|
-
return { append, entries, path: logPath };
|
|
15635
|
-
}
|
|
15636
|
-
|
|
15637
|
-
// src/dispatch-registry.ts
|
|
15638
|
-
var TERMINAL_CAP = 50;
|
|
15639
|
-
function createDispatchRegistry() {
|
|
15640
|
-
const entries = new Map;
|
|
15641
|
-
let seq = 0;
|
|
15642
|
-
const fail = (msg) => {
|
|
15643
|
-
throw new Error(`[forge:dispatch-registry] ${msg}`);
|
|
15644
|
-
};
|
|
15645
|
-
const isTerminal2 = (st) => st === "completed" || st === "timeout" || st === "killed" || st === "error";
|
|
15646
|
-
return {
|
|
15647
|
-
submit(entry) {
|
|
15648
|
-
const dispatchId = `bg-${++seq}`;
|
|
15649
|
-
entries.set(dispatchId, {
|
|
15650
|
-
...entry,
|
|
15651
|
-
dispatchId,
|
|
15652
|
-
background: true,
|
|
15653
|
-
queuedAt: entry.queuedAt ?? new Date().toISOString(),
|
|
15654
|
-
state: "queued",
|
|
15655
|
-
delivered: false
|
|
15656
|
-
});
|
|
15657
|
-
return { dispatchId };
|
|
15658
|
-
},
|
|
15659
|
-
get(dispatchId) {
|
|
15660
|
-
const e = entries.get(dispatchId);
|
|
15661
|
-
if (!e)
|
|
15662
|
-
throw new Error(`[forge:dispatch-registry] unknown dispatch id "${dispatchId}"`);
|
|
15663
|
-
return e;
|
|
15664
|
-
},
|
|
15665
|
-
markRunning(dispatchId) {
|
|
15666
|
-
const e = this.get(dispatchId);
|
|
15667
|
-
if (e.state !== "queued")
|
|
15668
|
-
fail(`illegal transition ${e.state} → running for ${dispatchId}`);
|
|
15669
|
-
e.state = "running";
|
|
15670
|
-
return e;
|
|
15671
|
-
},
|
|
15672
|
-
markCompleted(dispatchId, result) {
|
|
15673
|
-
return this.terminate(dispatchId, "completed", result);
|
|
15674
|
-
},
|
|
15675
|
-
markTimeout(dispatchId, report) {
|
|
15676
|
-
return this.terminate(dispatchId, "timeout", report);
|
|
15677
|
-
},
|
|
15678
|
-
markError(dispatchId, report) {
|
|
15679
|
-
return this.terminate(dispatchId, "error", report);
|
|
15680
|
-
},
|
|
15681
|
-
markKilled(dispatchId) {
|
|
15682
|
-
return this.terminate(dispatchId, "killed");
|
|
15683
|
-
},
|
|
15684
|
-
noteLateCompletion(dispatchId, result) {
|
|
15685
|
-
const e = this.get(dispatchId);
|
|
15686
|
-
if (e.state !== "killed")
|
|
15687
|
-
fail(`late completion only makes sense on a killed dispatch (${dispatchId} is ${e.state})`);
|
|
15688
|
-
e.lateCompletion = result;
|
|
15689
|
-
},
|
|
15690
|
-
terminate(dispatchId, state, result) {
|
|
15691
|
-
const e = this.get(dispatchId);
|
|
15692
|
-
if (isTerminal2(e.state))
|
|
15693
|
-
fail(`illegal transition ${e.state} → ${state} for ${dispatchId}`);
|
|
15694
|
-
e.state = state;
|
|
15695
|
-
if (result !== undefined)
|
|
15696
|
-
e.result = result;
|
|
15697
|
-
return e;
|
|
15698
|
-
},
|
|
15699
|
-
takeUndelivered(parentSessionID) {
|
|
15700
|
-
const out = [];
|
|
15701
|
-
for (const e of entries.values()) {
|
|
15702
|
-
if (parentSessionID !== undefined && e.parentSessionID !== parentSessionID)
|
|
15703
|
-
continue;
|
|
15704
|
-
if (isTerminal2(e.state) && !e.delivered && e.state !== "killed") {
|
|
15705
|
-
e.delivered = true;
|
|
15706
|
-
out.push(e);
|
|
15707
|
-
}
|
|
15708
|
-
}
|
|
15709
|
-
return out;
|
|
15710
|
-
},
|
|
15711
|
-
list() {
|
|
15712
|
-
const inFlight = [];
|
|
15713
|
-
const terminal = [];
|
|
15714
|
-
for (const e of entries.values()) {
|
|
15715
|
-
if (isTerminal2(e.state))
|
|
15716
|
-
terminal.push(e);
|
|
15717
|
-
else
|
|
15718
|
-
inFlight.push(e);
|
|
15719
|
-
}
|
|
15720
|
-
return { inFlight, terminal: terminal.slice(-TERMINAL_CAP) };
|
|
15721
|
-
}
|
|
15722
|
-
};
|
|
15723
|
-
}
|
|
15724
|
-
|
|
15725
|
-
// src/crew-gate.ts
|
|
15726
|
-
var CREW_DISPATCH_EVENTS = new Set(["completed", "timeout", "error", "killed", "empty-response"]);
|
|
15727
|
-
var DISPATCH_ID = /bg-\d+/g;
|
|
15728
|
-
function validateCrewReport(report, ledgerRows, crew) {
|
|
15729
|
-
const gaps = [];
|
|
15730
|
-
if (!Array.isArray(report) || report.length === 0) {
|
|
15731
|
-
return { ok: false, gaps: ["the report lists no subtasks — a crew completion report must cover every subtask"], dispatchIds: [] };
|
|
15732
|
-
}
|
|
15733
|
-
const rowById = new Map;
|
|
15734
|
-
for (const row of ledgerRows) {
|
|
15735
|
-
if (row.parentSessionID !== crew.sessionID)
|
|
15736
|
-
continue;
|
|
15737
|
-
if (row.ts < crew.startedAt)
|
|
15738
|
-
continue;
|
|
15739
|
-
if (row.event === "validation" || row.event === "resolve-error" || row.event === "refused-cap" || row.event === "models-dev-degraded" || row.event === "retry-excluded" || row.event === "lost-on-exit" || row.event === "kill-late-completion")
|
|
15740
|
-
continue;
|
|
15741
|
-
if (CREW_DISPATCH_EVENTS.has(row.event) && row.dispatchId)
|
|
15742
|
-
rowById.set(row.dispatchId, row);
|
|
15743
|
-
}
|
|
15744
|
-
const referenced = new Set;
|
|
15745
|
-
report.forEach((sub, i) => {
|
|
15746
|
-
const label = sub?.title?.trim() || `subtask #${i + 1}`;
|
|
15747
|
-
const verdict = String(sub?.verdict ?? "").toUpperCase();
|
|
15748
|
-
if (!sub?.title?.trim())
|
|
15749
|
-
gaps.push(`${label}: missing title`);
|
|
15750
|
-
if (verdict !== "PASS" && verdict !== "FAIL")
|
|
15751
|
-
gaps.push(`${label}: verdict must be PASS or FAIL (got "${sub?.verdict ?? ""}")`);
|
|
15752
|
-
const ids = [...String(sub?.evidence ?? "").matchAll(DISPATCH_ID)].map((m) => m[0]);
|
|
15753
|
-
if (!String(sub?.evidence ?? "").trim())
|
|
15754
|
-
gaps.push(`${label}: missing evidence reference`);
|
|
15755
|
-
else if (ids.length === 0)
|
|
15756
|
-
gaps.push(`${label}: evidence must reference a dispatch id (bg-N) that exists in the dispatch ledger`);
|
|
15757
|
-
for (const id of ids) {
|
|
15758
|
-
if (referenced.has(id))
|
|
15759
|
-
gaps.push(`${label}: dispatch ${id} is already cited by another subtask — each dispatch belongs to exactly one verdict`);
|
|
15760
|
-
referenced.add(id);
|
|
15761
|
-
const row = rowById.get(id);
|
|
15762
|
-
if (!row)
|
|
15763
|
-
gaps.push(`${label}: dispatch ${id} has no ledger row for this crew (fabricated, stale, or pre-crew)`);
|
|
15764
|
-
else if (verdict === "PASS" && row.event !== "completed") {
|
|
15765
|
-
gaps.push(`${label}: marked PASS but dispatch ${id} ended ${row.event} — a subtask whose attempt and retry both failed must be marked FAIL with both reports visible`);
|
|
15766
|
-
}
|
|
15767
|
-
}
|
|
15768
|
-
if (verdict === "FAIL" && (!Array.isArray(sub?.attempts) || sub.attempts.length < 2)) {
|
|
15769
|
-
gaps.push(`${label}: FAIL requires both failure reports (the attempt and the one retry) in attempts[]`);
|
|
15770
|
-
}
|
|
15771
|
-
});
|
|
15772
|
-
for (const [id, row] of rowById) {
|
|
15773
|
-
if (!referenced.has(id))
|
|
15774
|
-
gaps.push(`dispatch ${id} (${row.event}, ${row.ts}) has no verdict in the report — a dispatched subtask was silently dropped`);
|
|
15775
|
-
}
|
|
15776
|
-
return { ok: gaps.length === 0, gaps, dispatchIds: [...rowById.keys()] };
|
|
14977
|
+
function resolvePinnedDepth(thoughtLevel, family, ladder) {
|
|
14978
|
+
const t = translateDepth(thoughtLevel, family, ladder);
|
|
14979
|
+
if (t.kind === "no-mapping")
|
|
14980
|
+
return { translation: null, findingMessage: t.message };
|
|
14981
|
+
if (t.kind === "not-injected")
|
|
14982
|
+
return { translation: null };
|
|
14983
|
+
return { translation: t };
|
|
15777
14984
|
}
|
|
15778
14985
|
|
|
15779
14986
|
// src/forge-config.ts
|
|
@@ -15804,7 +15011,7 @@ function sanitizeJsonc(text) {
|
|
|
15804
15011
|
if (c === "/" && d === "*") {
|
|
15805
15012
|
out.push(" ", " ");
|
|
15806
15013
|
i += 2;
|
|
15807
|
-
while (i < n && !(text[i] === "*" && i + 1 < n
|
|
15014
|
+
while (i < n && !(text[i] === "*" && i + 1 < n)) {
|
|
15808
15015
|
out.push(text[i] === `
|
|
15809
15016
|
` ? `
|
|
15810
15017
|
` : " ");
|
|
@@ -15995,77 +15202,92 @@ function locateJsonError(text) {
|
|
|
15995
15202
|
ws();
|
|
15996
15203
|
return i >= n ? null : fail(`unexpected token '${text[i]}' after top-level value`);
|
|
15997
15204
|
}
|
|
15998
|
-
i++;
|
|
15999
|
-
continue;
|
|
15205
|
+
i++;
|
|
15206
|
+
continue;
|
|
15207
|
+
}
|
|
15208
|
+
return fail(`unexpected token '${c}'`);
|
|
15209
|
+
}
|
|
15210
|
+
}
|
|
15211
|
+
function parseForgeJsonc(text) {
|
|
15212
|
+
const sanitized = sanitizeJsonc(text);
|
|
15213
|
+
let parsed;
|
|
15214
|
+
try {
|
|
15215
|
+
parsed = JSON.parse(sanitized);
|
|
15216
|
+
} catch {
|
|
15217
|
+
const located = locateJsonError(sanitized);
|
|
15218
|
+
return {
|
|
15219
|
+
ok: false,
|
|
15220
|
+
error: located ?? { message: "invalid JSON", line: 1, column: 1 }
|
|
15221
|
+
};
|
|
15222
|
+
}
|
|
15223
|
+
return { ok: true, config: parsed };
|
|
15224
|
+
}
|
|
15225
|
+
var AGENT_ID_RE = /^[a-z0-9-]+$/;
|
|
15226
|
+
function isRecord(v) {
|
|
15227
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15228
|
+
}
|
|
15229
|
+
function nonEmptyString(v) {
|
|
15230
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
15231
|
+
}
|
|
15232
|
+
function validateAgentSet(raw) {
|
|
15233
|
+
const findings = [];
|
|
15234
|
+
const agents = {};
|
|
15235
|
+
if (raw === undefined)
|
|
15236
|
+
return { agents, findings };
|
|
15237
|
+
if (!isRecord(raw)) {
|
|
15238
|
+
findings.push({ level: "error", code: "agents-not-object", message: 'forge.json "agents" must be an object mapping agent ids to definitions — the agent set is empty' });
|
|
15239
|
+
return { agents, findings };
|
|
15240
|
+
}
|
|
15241
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
15242
|
+
const label = `agents.${id}`;
|
|
15243
|
+
if (!AGENT_ID_RE.test(id)) {
|
|
15244
|
+
findings.push({ level: "error", code: "agent-id-invalid", message: `${label}: agent id must match [a-z0-9-] — entry skipped` });
|
|
15245
|
+
continue;
|
|
15246
|
+
}
|
|
15247
|
+
if (!isRecord(entry)) {
|
|
15248
|
+
findings.push({ level: "error", code: "agent-not-object", message: `${label}: definition must be an object — entry skipped` });
|
|
15249
|
+
continue;
|
|
15250
|
+
}
|
|
15251
|
+
if (!nonEmptyString(entry.model)) {
|
|
15252
|
+
findings.push({ level: "error", code: "agent-model-missing", message: `${label}.model: a non-empty "provider/model" string is required — entry skipped, siblings still apply` });
|
|
15253
|
+
continue;
|
|
15254
|
+
}
|
|
15255
|
+
const def = { model: entry.model.trim() };
|
|
15256
|
+
if (entry.depths !== undefined) {
|
|
15257
|
+
findings.push({ level: "warn", code: "depths-deprecated", message: `${label}.depths is no longer used (static agents pin one thoughtLevel) — ignored; set "thoughtLevel" instead` });
|
|
15258
|
+
}
|
|
15259
|
+
if (entry.thoughtLevel !== undefined) {
|
|
15260
|
+
if (nonEmptyString(entry.thoughtLevel)) {
|
|
15261
|
+
def.thoughtLevel = entry.thoughtLevel.trim();
|
|
15262
|
+
} else {
|
|
15263
|
+
findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored, the agent still materializes` });
|
|
15264
|
+
}
|
|
15265
|
+
}
|
|
15266
|
+
if (entry.prompt !== undefined) {
|
|
15267
|
+
if (nonEmptyString(entry.prompt)) {
|
|
15268
|
+
def.prompt = entry.prompt;
|
|
15269
|
+
} else {
|
|
15270
|
+
findings.push({ level: "warn", code: "prompt-invalid", message: `${label}.prompt must be a non-empty string — field ignored` });
|
|
15271
|
+
}
|
|
15272
|
+
}
|
|
15273
|
+
if (entry.shape !== undefined) {
|
|
15274
|
+
if (entry.shape === "readonly" || entry.shape === "write") {
|
|
15275
|
+
def.shape = entry.shape;
|
|
15276
|
+
} else {
|
|
15277
|
+
findings.push({ level: "warn", code: "shape-invalid", message: `${label}.shape must be "readonly" or "write" — defaulting to readonly` });
|
|
15278
|
+
}
|
|
15279
|
+
}
|
|
15280
|
+
if (entry.permission !== undefined) {
|
|
15281
|
+
if (isRecord(entry.permission) && Object.values(entry.permission).every((v) => typeof v === "string")) {
|
|
15282
|
+
def.permission = entry.permission;
|
|
15283
|
+
} else {
|
|
15284
|
+
findings.push({ level: "warn", code: "permission-invalid", message: `${label}.permission must map tool names to string values — field ignored` });
|
|
15285
|
+
}
|
|
16000
15286
|
}
|
|
16001
|
-
|
|
16002
|
-
}
|
|
16003
|
-
}
|
|
16004
|
-
function parseForgeJsonc(text) {
|
|
16005
|
-
const sanitized = sanitizeJsonc(text);
|
|
16006
|
-
let parsed;
|
|
16007
|
-
try {
|
|
16008
|
-
parsed = JSON.parse(sanitized);
|
|
16009
|
-
} catch {
|
|
16010
|
-
const located = locateJsonError(sanitized);
|
|
16011
|
-
return {
|
|
16012
|
-
ok: false,
|
|
16013
|
-
error: located ?? { message: "invalid JSON", line: 1, column: 1 }
|
|
16014
|
-
};
|
|
15287
|
+
agents[id] = def;
|
|
16015
15288
|
}
|
|
16016
|
-
|
|
16017
|
-
return { ok: false, error: { message: "forge.json top level must be an object", line: 1, column: 1 } };
|
|
16018
|
-
}
|
|
16019
|
-
const agents = parsed.agents;
|
|
16020
|
-
if (agents === undefined)
|
|
16021
|
-
return { ok: true, config: { agents: {} } };
|
|
16022
|
-
if (typeof agents !== "object" || agents === null || Array.isArray(agents)) {
|
|
16023
|
-
return { ok: false, error: { message: 'forge.json "agents" must be an object mapping agent ids to definitions', line: 1, column: 1 } };
|
|
16024
|
-
}
|
|
16025
|
-
return { ok: true, config: { agents } };
|
|
16026
|
-
}
|
|
16027
|
-
function forgeRecipe(input) {
|
|
16028
|
-
const suggested = input.detectedIdentities[0] ?? "provider/model";
|
|
16029
|
-
const identityLines = input.detectedIdentities.length > 0 ? input.detectedIdentities.map((id) => ` - ${id}`).join(`
|
|
16030
|
-
`) : " (none detected — check the provider section of the host's opencode config)";
|
|
16031
|
-
const template = `{
|
|
16032
|
-
// forge dispatch agents — save the file; changes apply on the next dispatch, no restart
|
|
16033
|
-
"agents": {
|
|
16034
|
-
"research": {
|
|
16035
|
-
"model": "${suggested}", // exact "provider/model" string, pick from the detected list above
|
|
16036
|
-
"depths": ["low", "medium"], // first entry is the default; canonical words: none, low, medium, high, max — or the model's native level names, passed through verbatim
|
|
16037
|
-
// "prompt": "optional role prompt riding inside the discipline wrapper",
|
|
16038
|
-
// "shape": "write", // default readonly denies mutating tools; write agents need this
|
|
16039
|
-
// "permission": { "bash": "deny" } // optional override of the shape-derived permission
|
|
16040
|
-
}
|
|
16041
|
-
}
|
|
16042
|
-
}`;
|
|
16043
|
-
return [
|
|
16044
|
-
"[forge dispatch onboarding recipe]",
|
|
16045
|
-
'The dispatch agent you called is pinned to the placeholder model "Local/GPT Luna" — forge.json is not configured. Steps:',
|
|
16046
|
-
"",
|
|
16047
|
-
"1. Detected configured identities (use one as the exact model string):",
|
|
16048
|
-
identityLines,
|
|
16049
|
-
"",
|
|
16050
|
-
"2. Create ONE of these files (project-level wins; a single file fully applies — never merged):",
|
|
16051
|
-
` - ${input.projectPath} (project-level, versionable, team-shared)`,
|
|
16052
|
-
` - ${input.globalPath} (global)`,
|
|
16053
|
-
"",
|
|
16054
|
-
"3. Copy-paste template (JSONC — comments allowed) and edit model/depths:",
|
|
16055
|
-
"",
|
|
16056
|
-
template,
|
|
16057
|
-
"",
|
|
16058
|
-
'4. Verify: call the forge_dispatch tool again with agent "research" and a depth from its depths — the report shows the actual model and the depth translation.',
|
|
16059
|
-
"",
|
|
16060
|
-
'5. Extending: more agents are more entries under "agents"; agents that edit files need "shape": "write"; an explicit "permission" map overrides the shape-derived defaults — "task" is always denied, recursive dispatch stays impossible.'
|
|
16061
|
-
].join(`
|
|
16062
|
-
`);
|
|
15289
|
+
return { agents, findings };
|
|
16063
15290
|
}
|
|
16064
|
-
var PLACEHOLDER_IDENTITY = "Local/GPT Luna";
|
|
16065
|
-
var SEED_AGENTS = {
|
|
16066
|
-
research: { model: PLACEHOLDER_IDENTITY, depths: ["low", "medium"], shape: "readonly" },
|
|
16067
|
-
review: { model: PLACEHOLDER_IDENTITY, depths: ["medium", "high", "max"], shape: "readonly" }
|
|
16068
|
-
};
|
|
16069
15291
|
function forgeConfigPaths(opts) {
|
|
16070
15292
|
const sep2 = opts.projectDir?.includes("\\") && !opts.projectDir?.includes("/") ? "\\" : "/";
|
|
16071
15293
|
const project = opts.projectDir ? `${opts.projectDir}${sep2}.opencode${sep2}forge.json` : "";
|
|
@@ -16098,19 +15320,30 @@ function createForgeConfigLoader(deps) {
|
|
|
16098
15320
|
let result;
|
|
16099
15321
|
if (!parsed.ok) {
|
|
16100
15322
|
result = {
|
|
16101
|
-
agents: {
|
|
16102
|
-
source
|
|
16103
|
-
path
|
|
15323
|
+
agents: {},
|
|
15324
|
+
source,
|
|
15325
|
+
path,
|
|
16104
15326
|
findings: [
|
|
16105
15327
|
{
|
|
16106
15328
|
level: "error",
|
|
16107
15329
|
code: "config-parse-error",
|
|
16108
|
-
message: `${path}: ${parsed.error.message}${parsed.error.line !== undefined ? ` (line ${parsed.error.line}, column ${parsed.error.column})` : ""} —
|
|
15330
|
+
message: `${path}: ${parsed.error.message}${parsed.error.line !== undefined ? ` (line ${parsed.error.line}, column ${parsed.error.column})` : ""} — the agent set is empty until the file parses`
|
|
16109
15331
|
}
|
|
16110
15332
|
]
|
|
16111
15333
|
};
|
|
16112
15334
|
} else {
|
|
16113
|
-
|
|
15335
|
+
const top = parsed.config;
|
|
15336
|
+
if (top !== undefined && !isRecord(top)) {
|
|
15337
|
+
result = {
|
|
15338
|
+
agents: {},
|
|
15339
|
+
source,
|
|
15340
|
+
path,
|
|
15341
|
+
findings: [{ level: "error", code: "config-not-object", message: `${path}: top level must be an object — the agent set is empty` }]
|
|
15342
|
+
};
|
|
15343
|
+
} else {
|
|
15344
|
+
const validated = validateAgentSet(top === undefined ? undefined : top.agents);
|
|
15345
|
+
result = { agents: validated.agents, source, path, findings: validated.findings };
|
|
15346
|
+
}
|
|
16114
15347
|
}
|
|
16115
15348
|
cache.set(path, { mtimeMs: st.mtimeMs, size: st.size, result });
|
|
16116
15349
|
return result;
|
|
@@ -16123,104 +15356,18 @@ function createForgeConfigLoader(deps) {
|
|
|
16123
15356
|
const global = paths.global ? readCandidate(paths.global, "global") : null;
|
|
16124
15357
|
if (global)
|
|
16125
15358
|
return global;
|
|
16126
|
-
return {
|
|
16127
|
-
agents: { ...SEED_AGENTS },
|
|
16128
|
-
source: "seed",
|
|
16129
|
-
path: null,
|
|
16130
|
-
findings: [
|
|
16131
|
-
{
|
|
16132
|
-
level: "notice",
|
|
16133
|
-
code: "dispatch-unconfigured",
|
|
16134
|
-
message: "forge dispatch is unconfigured (no forge.json found): every forge_dispatch call will fail with the configuration recipe. Create .opencode/forge.json (project) or ~/.config/opencode/forge.json (global), or ask your session AI to configure dispatch — the recipe in the dispatch error tells it exactly what to write."
|
|
16135
|
-
}
|
|
16136
|
-
]
|
|
16137
|
-
};
|
|
15359
|
+
return { agents: {}, source: "none", path: null, findings: [] };
|
|
16138
15360
|
}
|
|
16139
15361
|
};
|
|
16140
15362
|
}
|
|
16141
15363
|
|
|
16142
|
-
// src/models-dev.ts
|
|
16143
|
-
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
16144
|
-
import { join as join4 } from "node:path";
|
|
16145
|
-
import { tmpdir as tmpdir2 } from "node:os";
|
|
16146
|
-
var DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
|
|
16147
|
-
function modelsDevCacheDir(base) {
|
|
16148
|
-
return join4(base ?? join4(tmpdir2(), "opencode-forge"), "models-dev");
|
|
16149
|
-
}
|
|
16150
|
-
function namedLevels(rows) {
|
|
16151
|
-
const out = [];
|
|
16152
|
-
for (const r of rows) {
|
|
16153
|
-
if (r && typeof r === "object" && r.type === "effort") {
|
|
16154
|
-
const values = r.values;
|
|
16155
|
-
if (Array.isArray(values)) {
|
|
16156
|
-
for (const v of values)
|
|
16157
|
-
if (typeof v === "string")
|
|
16158
|
-
out.push(v);
|
|
16159
|
-
}
|
|
16160
|
-
}
|
|
16161
|
-
}
|
|
16162
|
-
return out;
|
|
16163
|
-
}
|
|
16164
|
-
function reduceModelsDev(raw) {
|
|
16165
|
-
const catalog = { providers: {} };
|
|
16166
|
-
const providers = raw?.constructor === Object ? raw : null;
|
|
16167
|
-
if (!providers)
|
|
16168
|
-
return catalog;
|
|
16169
|
-
for (const [providerID, provider] of Object.entries(providers)) {
|
|
16170
|
-
const models = provider.models;
|
|
16171
|
-
if (!models || typeof models !== "object")
|
|
16172
|
-
continue;
|
|
16173
|
-
const out = { models: {} };
|
|
16174
|
-
for (const [modelID, model] of Object.entries(models)) {
|
|
16175
|
-
const m = model;
|
|
16176
|
-
const options = Array.isArray(m.reasoning_options) ? namedLevels(m.reasoning_options) : undefined;
|
|
16177
|
-
const cost = m.cost && typeof m.cost === "object" ? m.cost : undefined;
|
|
16178
|
-
out.models[modelID] = {
|
|
16179
|
-
...options ? { reasoningOptions: options } : {},
|
|
16180
|
-
...cost ? { cost } : {}
|
|
16181
|
-
};
|
|
16182
|
-
}
|
|
16183
|
-
catalog.providers[providerID] = out;
|
|
16184
|
-
}
|
|
16185
|
-
return catalog;
|
|
16186
|
-
}
|
|
16187
|
-
async function loadModelsDevSnapshot(opts = {}) {
|
|
16188
|
-
const url2 = opts.url ?? process.env.OPENCODE_MODELS_URL ?? DEFAULT_MODELS_DEV_URL;
|
|
16189
|
-
const cacheDir = modelsDevCacheDir(opts.cacheDir);
|
|
16190
|
-
const cacheFile = join4(cacheDir, "snapshot.json");
|
|
16191
|
-
const fetcher = opts.fetcher;
|
|
16192
|
-
if (fetcher) {
|
|
16193
|
-
try {
|
|
16194
|
-
const res = await fetcher(url2, { signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
16195
|
-
if (res.ok)
|
|
16196
|
-
return { catalog: reduceModelsDev(JSON.parse(await res.text())), degraded: false, source: url2 };
|
|
16197
|
-
} catch {}
|
|
16198
|
-
return { catalog: { providers: {} }, degraded: true, source: "fetch-failed" };
|
|
16199
|
-
}
|
|
16200
|
-
try {
|
|
16201
|
-
const res = await fetch(url2, { signal: AbortSignal.timeout(opts.timeoutMs ?? 8000) });
|
|
16202
|
-
if (res.ok) {
|
|
16203
|
-
const catalog = reduceModelsDev(JSON.parse(await res.text()));
|
|
16204
|
-
try {
|
|
16205
|
-
mkdirSync5(cacheDir, { recursive: true });
|
|
16206
|
-
writeFileSync5(cacheFile, JSON.stringify(catalog));
|
|
16207
|
-
} catch {}
|
|
16208
|
-
return { catalog, degraded: false, source: url2 };
|
|
16209
|
-
}
|
|
16210
|
-
} catch {}
|
|
16211
|
-
try {
|
|
16212
|
-
const cached2 = JSON.parse(readFileSync8(cacheFile, "utf8"));
|
|
16213
|
-
if (cached2 && typeof cached2 === "object" && cached2.providers)
|
|
16214
|
-
return { catalog: cached2, degraded: true, source: "cache" };
|
|
16215
|
-
} catch {}
|
|
16216
|
-
return { catalog: { providers: {} }, degraded: true, source: "unavailable" };
|
|
16217
|
-
}
|
|
16218
|
-
|
|
16219
15364
|
// plugin.ts
|
|
16220
15365
|
var FORGE_AGENT = "forge";
|
|
16221
15366
|
var FORGE_PROMPT = `You are forge — the single general-purpose coding agent. You handle every task directly: exploration, planning, implementation, and verification. There is no agent switching.
|
|
16222
15367
|
|
|
16223
|
-
Workflow modes are strictly user-initiated. Never enter plan or goal mode — and never call a plan_* or goal_* tool — unless the user ran /plan or /goal, unmistakably asked for that mode (e.g. "plan first", "set a goal"), or the session already carries a [forge:plan-notice] / [forge:goal-notice] for a mode they started. Ordinary task requests are normal work. Mode-specific rules arrive with those commands and notices; when a notice is present, follow it
|
|
15368
|
+
Workflow modes are strictly user-initiated. Never enter plan or goal mode — and never call a plan_* or goal_* tool — unless the user ran /plan or /goal, unmistakably asked for that mode (e.g. "plan first", "set a goal"), or the session already carries a [forge:plan-notice] / [forge:goal-notice] for a mode they started. Ordinary task requests are normal work. Mode-specific rules arrive with those commands and notices; when a notice is present, follow it.
|
|
15369
|
+
|
|
15370
|
+
Subagent dispatch (task tool): when the work suits a scoped worker, prefer a configured \`forge-*\` subagent whose description matches the job. If none matches, say so plainly to the user and dispatch through the native task channel instead.`;
|
|
16224
15371
|
var PLAN_COMMAND_TEMPLATE = [
|
|
16225
15372
|
'(forge plan harness routing. Argument: "$ARGUMENTS")',
|
|
16226
15373
|
"",
|
|
@@ -16279,22 +15426,14 @@ function effectiveWorktree(worktree, fallback) {
|
|
|
16279
15426
|
}
|
|
16280
15427
|
var sessions = new Map;
|
|
16281
15428
|
var forgeDisabled = false;
|
|
16282
|
-
var dispatchDisabled = false;
|
|
16283
|
-
var dispatchMaxConcurrent = 4;
|
|
16284
|
-
var dispatchTimeoutMs = 600000;
|
|
16285
|
-
var dispatchBase = null;
|
|
16286
15429
|
var forgeLoader = null;
|
|
16287
15430
|
var forgeLoaderDir = null;
|
|
16288
|
-
var forgeConfigState = null;
|
|
16289
|
-
var lastDetectedIdentities = [];
|
|
16290
|
-
var activeDispatchEngine = null;
|
|
16291
|
-
var dispatchLogDir = join5(tmpdir3(), "opencode-forge", "dispatch");
|
|
16292
|
-
var dispatchLedger = createDispatchLedger(join5(dispatchLogDir, "ledger.jsonl"));
|
|
16293
|
-
var activeDispatchRegistry = null;
|
|
16294
|
-
var dispatchClient = null;
|
|
16295
|
-
var dispatchWakeTimers = new Map;
|
|
16296
15431
|
var crews = new Map;
|
|
16297
|
-
var
|
|
15432
|
+
var modelsDevCatalog = null;
|
|
15433
|
+
var depthFindingNotes = new Map;
|
|
15434
|
+
var crewCommandSnapshot = null;
|
|
15435
|
+
var materializedSnapshot = {};
|
|
15436
|
+
var sessionDepths = new Map;
|
|
16298
15437
|
var OPENAI_FAMILY = new Set(["openai", "opencode", "openrouter", "groq", "xai", "azure", "github-copilot", "deepseek", "together", "vercel", "opus"]);
|
|
16299
15438
|
var ANTHROPIC_FAMILY = new Set(["anthropic"]);
|
|
16300
15439
|
var ZAI_FAMILY = new Set(["zai", "zai-coding-plan"]);
|
|
@@ -16307,32 +15446,6 @@ function dispatchProviderFamily(providerID) {
|
|
|
16307
15446
|
return "zai";
|
|
16308
15447
|
return null;
|
|
16309
15448
|
}
|
|
16310
|
-
function queueDispatchDepth(sessionID, translation) {
|
|
16311
|
-
const key = (t) => t.kind === "verbatim" ? `verbatim:${t.word}` : t.kind === "tiered" ? `tiered:${t.budgetTokens}` : t.kind;
|
|
16312
|
-
const existing = dispatchDepths.get(sessionID);
|
|
16313
|
-
if (existing)
|
|
16314
|
-
return key(existing) === key(translation);
|
|
16315
|
-
dispatchDepths.set(sessionID, translation);
|
|
16316
|
-
return true;
|
|
16317
|
-
}
|
|
16318
|
-
function configuredIdentitiesOf(cfg, catalog) {
|
|
16319
|
-
const providers = cfg.provider ?? {};
|
|
16320
|
-
const identities = [];
|
|
16321
|
-
for (const [providerID, provider] of Object.entries(providers)) {
|
|
16322
|
-
const modelKeys = provider?.models ? Object.keys(provider.models) : null;
|
|
16323
|
-
if (modelKeys && modelKeys.length > 0) {
|
|
16324
|
-
for (const m of modelKeys)
|
|
16325
|
-
identities.push(`${providerID}/${m}`);
|
|
16326
|
-
} else {
|
|
16327
|
-
const catModels = catalog.providers?.[providerID]?.models ?? {};
|
|
16328
|
-
const ids = Object.keys(catModels);
|
|
16329
|
-
if (ids.length > 0)
|
|
16330
|
-
for (const m of ids)
|
|
16331
|
-
identities.push(`${providerID}/${m}`);
|
|
16332
|
-
}
|
|
16333
|
-
}
|
|
16334
|
-
return identities;
|
|
16335
|
-
}
|
|
16336
15449
|
var jobsMode = "auto";
|
|
16337
15450
|
var jobsKeepBuiltinShell = false;
|
|
16338
15451
|
var jobsSurviveMode = "never";
|
|
@@ -16388,9 +15501,9 @@ var jobLogDir = jobsLogDir();
|
|
|
16388
15501
|
var jobLedgerPath = join5(jobLogDir, "ledger.jsonl");
|
|
16389
15502
|
function jobLedgerSink(entry) {
|
|
16390
15503
|
try {
|
|
16391
|
-
|
|
15504
|
+
mkdirSync5(jobLogDir, { recursive: true });
|
|
16392
15505
|
if (existsSync3(jobLedgerPath) && statSync4(jobLedgerPath).size > 1e6)
|
|
16393
|
-
|
|
15506
|
+
writeFileSync5(jobLedgerPath, "");
|
|
16394
15507
|
appendFileSync(jobLedgerPath, `${JSON.stringify(entry)}
|
|
16395
15508
|
`);
|
|
16396
15509
|
} catch {}
|
|
@@ -16451,7 +15564,7 @@ function readPlanDir(worktree) {
|
|
|
16451
15564
|
const dir = planDirOf(worktree);
|
|
16452
15565
|
if (!existsSync3(dir))
|
|
16453
15566
|
return [];
|
|
16454
|
-
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text:
|
|
15567
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join5(dir, name), "utf8") }));
|
|
16455
15568
|
}
|
|
16456
15569
|
function ensureSession(sessionID, worktree) {
|
|
16457
15570
|
const existing = sessions.get(sessionID);
|
|
@@ -16468,7 +15581,7 @@ function worktreeFor(context) {
|
|
|
16468
15581
|
}
|
|
16469
15582
|
function resolveActivePlan(state) {
|
|
16470
15583
|
if (state.planPath && existsSync3(state.planPath)) {
|
|
16471
|
-
const doc2 = parsePlanLoose(
|
|
15584
|
+
const doc2 = parsePlanLoose(readFileSync8(state.planPath, "utf8"));
|
|
16472
15585
|
if (doc2 && !isTerminal(doc2.status))
|
|
16473
15586
|
return { path: state.planPath, doc: doc2 };
|
|
16474
15587
|
state.planPath = undefined;
|
|
@@ -16510,12 +15623,12 @@ var planWriteTool = tool({
|
|
|
16510
15623
|
throw new PlanError(`A plan in ${active.doc.status} state is already active (${relFrom(state.worktree, active.path)}). Finish it with plan_close, or /plan discard it before planning something new.`);
|
|
16511
15624
|
} else {
|
|
16512
15625
|
const dir = planDirOf(state.worktree);
|
|
16513
|
-
|
|
15626
|
+
mkdirSync5(dir, { recursive: true });
|
|
16514
15627
|
path = join5(dir, planFileName(localDate(), slugify(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md"))));
|
|
16515
15628
|
mode = "created";
|
|
16516
15629
|
}
|
|
16517
15630
|
const text = renderPlan(args, now, created);
|
|
16518
|
-
|
|
15631
|
+
writeFileSync5(path, text);
|
|
16519
15632
|
state.planPath = path;
|
|
16520
15633
|
const doc2 = parsePlan(text);
|
|
16521
15634
|
context.metadata({ title: `${mode === "created" ? "Create" : "Revise"} plan: ${doc2.goal}` });
|
|
@@ -16543,8 +15656,8 @@ var planTickTool = tool({
|
|
|
16543
15656
|
if (active.doc.status !== "approved") {
|
|
16544
15657
|
throw new PlanError(`Plan status is ${active.doc.status}; only an approved plan can be ticked. Get user approval via plan_approve first.`);
|
|
16545
15658
|
}
|
|
16546
|
-
const next = tickTask(
|
|
16547
|
-
|
|
15659
|
+
const next = tickTask(readFileSync8(active.path, "utf8"), args.n, nowIso());
|
|
15660
|
+
writeFileSync5(active.path, next);
|
|
16548
15661
|
const doc2 = parsePlan(next);
|
|
16549
15662
|
const p = progressOf(doc2);
|
|
16550
15663
|
context.metadata({ title: `Tick task ${args.n} (${p.done}/${p.total})` });
|
|
@@ -16571,7 +15684,7 @@ var planApproveTool = tool({
|
|
|
16571
15684
|
throw new PlanError(`Plan status is ${active.doc.status}; only a draft plan can be approved.`);
|
|
16572
15685
|
}
|
|
16573
15686
|
await gate(context.ask, "plan_approve", `Approve plan: ${active.doc.goal}`);
|
|
16574
|
-
|
|
15687
|
+
writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "approved", nowIso()));
|
|
16575
15688
|
context.metadata({ title: `Plan approved: ${active.doc.goal}` });
|
|
16576
15689
|
return {
|
|
16577
15690
|
title: "plan approved",
|
|
@@ -16604,7 +15717,7 @@ var planCloseTool = tool({
|
|
|
16604
15717
|
Fix the implementation and retry, or revise the plan first.`);
|
|
16605
15718
|
}
|
|
16606
15719
|
await gate(context.ask, "plan_close", `Close plan: ${active.doc.goal}`);
|
|
16607
|
-
|
|
15720
|
+
writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "done", nowIso()));
|
|
16608
15721
|
context.metadata({ title: `Plan done: ${active.doc.goal}` });
|
|
16609
15722
|
return {
|
|
16610
15723
|
title: "plan done",
|
|
@@ -16622,7 +15735,7 @@ var planDiscardTool = tool({
|
|
|
16622
15735
|
const active = resolveActivePlan(state);
|
|
16623
15736
|
if (!active)
|
|
16624
15737
|
throw new PlanError("No plan to abandon in this workspace.");
|
|
16625
|
-
|
|
15738
|
+
writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "abandoned", nowIso()));
|
|
16626
15739
|
state.planPath = undefined;
|
|
16627
15740
|
context.metadata({ title: `Plan abandoned: ${active.doc.goal}` });
|
|
16628
15741
|
return { title: "plan abandoned", output: `Plan abandoned: ${relFrom(state.worktree, active.path)}. Write operations are restored.` };
|
|
@@ -16635,11 +15748,11 @@ function readGoalDir(worktree) {
|
|
|
16635
15748
|
const dir = goalDirOf(worktree);
|
|
16636
15749
|
if (!existsSync3(dir))
|
|
16637
15750
|
return [];
|
|
16638
|
-
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text:
|
|
15751
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join5(dir, name), "utf8") }));
|
|
16639
15752
|
}
|
|
16640
15753
|
function resolveSessionGoal(state) {
|
|
16641
15754
|
if (state.goalPath && existsSync3(state.goalPath)) {
|
|
16642
|
-
const doc2 = parseGoalLoose(
|
|
15755
|
+
const doc2 = parseGoalLoose(readFileSync8(state.goalPath, "utf8"));
|
|
16643
15756
|
if (doc2 && !isGoalTerminal(doc2.status))
|
|
16644
15757
|
return { path: state.goalPath, doc: doc2 };
|
|
16645
15758
|
state.goalPath = undefined;
|
|
@@ -16743,7 +15856,7 @@ var goalWriteTool = tool({
|
|
|
16743
15856
|
await gate(context.ask, "goal_write", `Arm goal: ${args.goal}`);
|
|
16744
15857
|
}
|
|
16745
15858
|
const dir = goalDirOf(state.worktree);
|
|
16746
|
-
|
|
15859
|
+
mkdirSync5(dir, { recursive: true });
|
|
16747
15860
|
const name = goalFileName(localDateNow(), slugifyGoal(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md")));
|
|
16748
15861
|
const path = join5(dir, name);
|
|
16749
15862
|
const text = renderGoal(input, {
|
|
@@ -16785,7 +15898,7 @@ var goalCheckTool = tool({
|
|
|
16785
15898
|
o.index = selected[i].n;
|
|
16786
15899
|
});
|
|
16787
15900
|
const runId = `${nowIso()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
16788
|
-
const next = appendCheckLog(
|
|
15901
|
+
const next = appendCheckLog(readFileSync8(goal.path, "utf8"), runId, outcomes, nowIso());
|
|
16789
15902
|
atomicWrite(goal.path, next);
|
|
16790
15903
|
const ok = outcomesAllOk(outcomes);
|
|
16791
15904
|
context.metadata({ title: `goal_check: ${outcomes.filter((o) => o.ok).length}/${outcomes.length} pass` });
|
|
@@ -16819,7 +15932,7 @@ var goalCompleteTool = tool({
|
|
|
16819
15932
|
const failures = outcomes.filter((o) => !o.ok);
|
|
16820
15933
|
if (failures.length > 0) {
|
|
16821
15934
|
const runId2 = `${nowIso()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
16822
|
-
atomicWrite(goal.path, appendCheckLog(
|
|
15935
|
+
atomicWrite(goal.path, appendCheckLog(readFileSync8(goal.path, "utf8"), runId2, outcomes, nowIso()));
|
|
16823
15936
|
throw new GoalError(`Completion gate: verification re-run failed (fail-closed). The goal stays active.
|
|
16824
15937
|
${formatOutcomes(failures)}
|
|
16825
15938
|
Fix the work and retry; recorded results never substitute for the gate's own re-run.`);
|
|
@@ -16832,7 +15945,7 @@ Fix the work and retry; recorded results never substitute for the gate's own re-
|
|
|
16832
15945
|
}
|
|
16833
15946
|
await gate(context.ask, "goal_complete", `Complete goal: ${goal.doc.goal}`);
|
|
16834
15947
|
const runId = `${nowIso()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
16835
|
-
let text = appendCheckLog(
|
|
15948
|
+
let text = appendCheckLog(readFileSync8(goal.path, "utf8"), runId, outcomes, nowIso());
|
|
16836
15949
|
text = transitionGoal(text, "completed", nowIso());
|
|
16837
15950
|
atomicWrite(goal.path, text);
|
|
16838
15951
|
context.metadata({ title: `Goal completed: ${goal.doc.goal}` });
|
|
@@ -16854,7 +15967,7 @@ var goalPauseTool = tool({
|
|
|
16854
15967
|
throw new GoalError(`No active goal to pause (status: ${goal?.doc.status ?? "none"}).`);
|
|
16855
15968
|
}
|
|
16856
15969
|
const stopReason = args.blocker ? "blocker" : "user";
|
|
16857
|
-
atomicWrite(goal.path, transitionGoal(
|
|
15970
|
+
atomicWrite(goal.path, transitionGoal(readFileSync8(goal.path, "utf8"), "paused", nowIso(), { stopReason }));
|
|
16858
15971
|
engineForgetSession(context.sessionID);
|
|
16859
15972
|
context.metadata({ title: `Goal paused (${stopReason}): ${goal.doc.goal}` });
|
|
16860
15973
|
return {
|
|
@@ -16884,7 +15997,7 @@ var goalResumeTool = tool({
|
|
|
16884
15997
|
}
|
|
16885
15998
|
const promoting = goal.doc.status === "queued";
|
|
16886
15999
|
await gate(context.ask, "goal_resume", `${promoting ? "Promote" : "Resume"} goal: ${goal.doc.goal}`);
|
|
16887
|
-
let text =
|
|
16000
|
+
let text = readFileSync8(goal.path, "utf8");
|
|
16888
16001
|
if (args.addTurns)
|
|
16889
16002
|
text = bumpBudget(text, args.addTurns, nowIso());
|
|
16890
16003
|
text = transitionGoal(text, "active", nowIso(), { session: context.sessionID });
|
|
@@ -16910,7 +16023,7 @@ var goalDiscardTool = tool({
|
|
|
16910
16023
|
if (!goal)
|
|
16911
16024
|
throw new GoalError("No goal to discard in this workspace.");
|
|
16912
16025
|
await gate(context.ask, "goal_discard", `Discard goal: ${goal.doc.goal}`);
|
|
16913
|
-
atomicWrite(goal.path, transitionGoal(
|
|
16026
|
+
atomicWrite(goal.path, transitionGoal(readFileSync8(goal.path, "utf8"), "abandoned", nowIso()));
|
|
16914
16027
|
state.goalPath = undefined;
|
|
16915
16028
|
engineForgetSession(context.sessionID);
|
|
16916
16029
|
context.metadata({ title: `Goal abandoned: ${goal.doc.goal}` });
|
|
@@ -17107,226 +16220,47 @@ ${p.newOutput || "(none in this window)"}`,
|
|
|
17107
16220
|
};
|
|
17108
16221
|
}
|
|
17109
16222
|
});
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
if (Number.isFinite(env) && env >= 0)
|
|
17113
|
-
return env;
|
|
17114
|
-
return idleDriven ? IDLE_DEBOUNCE_MS + 50 : 1500;
|
|
17115
|
-
}
|
|
17116
|
-
function scheduleDispatchWake(sessionID, idleDriven) {
|
|
17117
|
-
if (dispatchDisabled || forgeDisabled)
|
|
17118
|
-
return;
|
|
17119
|
-
if (dispatchWakeTimers.has(sessionID))
|
|
17120
|
-
return;
|
|
17121
|
-
const t = setTimeout(() => {
|
|
17122
|
-
dispatchWakeTimers.delete(sessionID);
|
|
17123
|
-
injectDispatchBrief(sessionID);
|
|
17124
|
-
}, dispatchWakeDelay(idleDriven));
|
|
17125
|
-
dispatchWakeTimers.set(sessionID, t);
|
|
17126
|
-
}
|
|
17127
|
-
function dispatchBriefText(entries) {
|
|
17128
|
-
const lines = [`[forge:dispatch-complete] ${entries.length} background dispatch(es) reached terminal state:`];
|
|
17129
|
-
for (const e of entries) {
|
|
17130
|
-
lines.push("", `- ${e.dispatchId} · agent ${e.tier} · ${e.identity}${e.depth ? ` @${e.depth}` : ""} · ${e.state.toUpperCase()} · session ${e.sessionID || "(not yet started)"}`);
|
|
17131
|
-
const r = e.result;
|
|
17132
|
-
if (r !== undefined) {
|
|
17133
|
-
lines.push(` result: ${JSON.stringify(r, null, 1).replace(/\n/g, `
|
|
17134
|
-
`).slice(0, 2000)}`);
|
|
17135
|
-
}
|
|
17136
|
-
}
|
|
17137
|
-
return lines.join(`
|
|
17138
|
-
`);
|
|
17139
|
-
}
|
|
17140
|
-
async function injectDispatchBrief(sessionID) {
|
|
17141
|
-
const reg = activeDispatchRegistry;
|
|
17142
|
-
if (!reg)
|
|
17143
|
-
return;
|
|
17144
|
-
const c0 = dispatchClient;
|
|
17145
|
-
if (typeof c0?.session.status === "function") {
|
|
17146
|
-
try {
|
|
17147
|
-
const st = await c0.session.status({ path: { id: sessionID } });
|
|
17148
|
-
const t = st?.[sessionID]?.type;
|
|
17149
|
-
if (t && t !== "idle")
|
|
17150
|
-
return;
|
|
17151
|
-
} catch {
|
|
17152
|
-
return;
|
|
17153
|
-
}
|
|
17154
|
-
}
|
|
17155
|
-
const entries = reg.takeUndelivered(sessionID);
|
|
17156
|
-
if (entries.length === 0)
|
|
17157
|
-
return;
|
|
17158
|
-
const c = dispatchClient;
|
|
17159
|
-
const send = c?.session.promptAsync ?? c?.session.prompt;
|
|
17160
|
-
if (typeof send !== "function")
|
|
17161
|
-
return;
|
|
17162
|
-
try {
|
|
17163
|
-
await send.call(c.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text: dispatchBriefText(entries) }] } });
|
|
17164
|
-
} catch {
|
|
17165
|
-
for (const e of entries)
|
|
17166
|
-
e.delivered = false;
|
|
17167
|
-
}
|
|
17168
|
-
}
|
|
17169
|
-
var forgeDispatchTool = tool({
|
|
17170
|
-
description: "Dispatch a scoped task to a worker subagent defined in forge.json. Takes {prompt, agent, depth}: agent names a dispatch agent (see forge_dispatch_config for the effective set); the agent's model is pinned by its definition — no fallback ever. depth comes from the canonical metalanguage (none, low, medium, high, max; default = the agent's first entry) or the model's native level names verbatim (escape hatch); the ONLY validation is membership in the agent's depths — the provider is the final judge and its raw errors flow back. The report discloses the depth translation (canonical -> native). Background mode targets live TUI sessions; under `opencode run` prefer sync. The result reports the actual model/depth, real token counts, a self-computed cost (null when unpriced), and the worker's concluding report. Refuses while a plan draft is active.",
|
|
17171
|
-
args: {
|
|
17172
|
-
prompt: tool.schema.string().describe("Complete, self-contained task for the worker (it cannot see this conversation)"),
|
|
17173
|
-
agent: tool.schema.string().describe("Dispatch agent id from forge.json (e.g. research, review, or a custom id; legacy inline tier ids still work when configured)"),
|
|
17174
|
-
depth: tool.schema.string().optional().describe("Reasoning depth: none | low | medium | high | max (canonical) or the model's native level name verbatim; omit to use the agent's first depths entry"),
|
|
17175
|
-
background: tool.schema.boolean().optional().describe("Run in the background: returns {dispatchId, resolved, queuedAt} immediately; the full result arrives later as a coalesced [forge:dispatch-complete] brief when the session is idle. Background mode targets live TUI sessions — under `opencode run` the session ends before any brief, so completions are ledger-only (read them via forge_dispatch_list) and sync mode is preferred.")
|
|
17176
|
-
},
|
|
17177
|
-
execute: async (args, context) => {
|
|
17178
|
-
const state = stateForBan(context.sessionID);
|
|
17179
|
-
const active = state ? resolveActivePlan(state) : null;
|
|
17180
|
-
if (active && active.doc.status === "draft") {
|
|
17181
|
-
throw new Error("[forge] A plan is in draft; dispatching workers is denied during planning. Call plan_approve for user approval, or /plan discard to abandon the plan.");
|
|
17182
|
-
}
|
|
17183
|
-
if (!args.prompt?.trim())
|
|
17184
|
-
throw new Error("forge_dispatch requires a non-empty prompt.");
|
|
17185
|
-
const engine = activeDispatchEngine;
|
|
17186
|
-
if (!engine)
|
|
17187
|
-
throw new Error("[forge] dispatch engine unavailable (disabled by dispatch.disable, or config still loading).");
|
|
17188
|
-
try {
|
|
17189
|
-
const r = await engine.dispatch({ prompt: args.prompt, agent: args.agent, ...args.depth !== undefined ? { depth: args.depth } : {} }, context.sessionID, args.background === true ? { background: true } : undefined);
|
|
17190
|
-
if ("dispatchId" in r) {
|
|
17191
|
-
context.metadata({ title: `dispatch ${r.agent} queued (${r.dispatchId})` });
|
|
17192
|
-
return {
|
|
17193
|
-
title: `dispatch ${r.agent} queued → ${r.resolved}${r.depth ? ` @${r.depth}` : ""}`,
|
|
17194
|
-
output: [
|
|
17195
|
-
`[forge:dispatch] accepted in the background: ${r.dispatchId}`,
|
|
17196
|
-
`agent: ${r.agent} resolved: ${r.resolved}${r.depth ? ` depth: ${r.depth}` : ""}`,
|
|
17197
|
-
`queuedAt: ${r.queuedAt}`,
|
|
17198
|
-
"",
|
|
17199
|
-
"The full result (tokens/cost/report — or the timeout/error report) arrives as a [forge:dispatch-complete] brief when this session is next idle. Track progress any time with forge_dispatch_list; cancel with forge_dispatch_kill."
|
|
17200
|
-
].join(`
|
|
17201
|
-
`)
|
|
17202
|
-
};
|
|
17203
|
-
}
|
|
17204
|
-
const tok = r.tokens;
|
|
17205
|
-
const costLine = r.costUsd !== null && r.costUsd !== undefined ? `$${r.costUsd.toFixed(6)}${r.priceSnapshot ? ` (${r.priceSnapshot})` : ""}` : `unpriced${r.costNote ? ` — ${r.costNote}` : ""}`;
|
|
17206
|
-
context.metadata({ title: `dispatch ${r.agent}: ${r.actual.model} @ ${r.actual.depth}` });
|
|
17207
|
-
return {
|
|
17208
|
-
title: `dispatch ${r.agent} → ${r.actual.model} @${r.actual.depth}`,
|
|
17209
|
-
output: [
|
|
17210
|
-
`[forge:dispatch] completed in ${r.durationMs}ms`,
|
|
17211
|
-
`agent: ${r.agent} model: ${r.actual.model} depth: ${r.actual.depth}${r.requested.depth !== undefined && r.requested.depth !== r.actual.depth ? ` (requested ${r.requested.depth})` : ""}`,
|
|
17212
|
-
`session: ${r.sessionID}`,
|
|
17213
|
-
`tokens: input ${tok.input ?? 0}, output ${tok.output ?? 0}, reasoning ${tok.reasoning ?? 0}, cache r/w ${tok.cache?.read ?? 0}/${tok.cache?.write ?? 0}`,
|
|
17214
|
-
`cost: ${costLine}`,
|
|
17215
|
-
`depthTranslation: ${r.depthTranslation}`,
|
|
17216
|
-
"",
|
|
17217
|
-
"Worker report:",
|
|
17218
|
-
r.text || "(empty worker report)"
|
|
17219
|
-
].join(`
|
|
17220
|
-
`)
|
|
17221
|
-
};
|
|
17222
|
-
} catch (err) {
|
|
17223
|
-
if (err instanceof DispatchError && err.code === "pin-unavailable") {
|
|
17224
|
-
throw new Error(`[forge:dispatch:pin-unavailable] ${err.message}
|
|
17225
|
-
|
|
17226
|
-
${dispatchRecipeText()}`);
|
|
17227
|
-
}
|
|
17228
|
-
if (err instanceof DispatchError)
|
|
17229
|
-
throw new Error(`[forge:dispatch:${err.code}] ${err.message}`);
|
|
17230
|
-
throw err;
|
|
17231
|
-
}
|
|
17232
|
-
}
|
|
17233
|
-
});
|
|
17234
|
-
function dispatchRecipeText() {
|
|
17235
|
-
const paths = forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
17236
|
-
return forgeRecipe({
|
|
17237
|
-
detectedIdentities: lastDetectedIdentities,
|
|
17238
|
-
projectPath: paths.project || "(project .opencode/forge.json)",
|
|
17239
|
-
globalPath: paths.global
|
|
17240
|
-
});
|
|
17241
|
-
}
|
|
17242
|
-
var forgeDispatchConfigTool = tool({
|
|
17243
|
-
description: "Read the effective forge dispatch configuration: {agents, knobs, findings}. agents mirrors the winning forge.json (project .opencode/forge.json > global ~/.config/opencode/forge.json > built-in seed) exactly — same keys, same shapes, writable back as valid forge.json agents content. knobs are the inline dispatch options {timeoutMs, maxConcurrent}. findings carry config state (config-parse-error, dispatch-unconfigured notice). Re-reads the file (mtime-cached) on every call, so edits are reflected immediately.",
|
|
17244
|
-
args: {},
|
|
17245
|
-
execute: async (_args, context) => {
|
|
17246
|
-
const loaded = forgeLoader?.load() ?? { agents: {}, source: "seed", path: null, findings: [] };
|
|
17247
|
-
context.metadata({ title: `dispatch config (${loaded.source})` });
|
|
17248
|
-
return {
|
|
17249
|
-
title: `dispatch config (${loaded.source}${loaded.path ? `: ${loaded.path}` : ""})`,
|
|
17250
|
-
output: JSON.stringify({
|
|
17251
|
-
agents: loaded.agents,
|
|
17252
|
-
knobs: { timeoutMs: dispatchTimeoutMs, maxConcurrent: dispatchMaxConcurrent },
|
|
17253
|
-
findings: loaded.findings
|
|
17254
|
-
}, null, 2)
|
|
17255
|
-
};
|
|
17256
|
-
}
|
|
17257
|
-
});
|
|
17258
|
-
var forgeDispatchListTool = tool({
|
|
17259
|
-
description: "List background forge_dispatch activity: in-flight dispatches (with dispatchIds) and recent terminal results (completed/timeout/error, full report objects). Use it to recover dispatch state after context compaction or to check on a background dispatch instead of waiting.",
|
|
17260
|
-
args: {},
|
|
17261
|
-
execute: async () => {
|
|
17262
|
-
const reg = activeDispatchRegistry;
|
|
17263
|
-
if (!reg)
|
|
17264
|
-
throw new Error("[forge] no background dispatches have been submitted.");
|
|
17265
|
-
const l = reg.list();
|
|
17266
|
-
if (l.inFlight.length === 0 && l.terminal.length === 0) {
|
|
17267
|
-
return { title: "no background dispatches", output: "[forge:dispatch-list] no background dispatches recorded in this session." };
|
|
17268
|
-
}
|
|
17269
|
-
const lines = [];
|
|
17270
|
-
if (l.inFlight.length > 0) {
|
|
17271
|
-
lines.push("In flight:");
|
|
17272
|
-
for (const e of l.inFlight) {
|
|
17273
|
-
lines.push(`- ${e.dispatchId} · agent ${e.tier} · ${e.identity}${e.depth ? ` @${e.depth}` : ""} · ${e.state} · queued ${e.queuedAt} · session ${e.sessionID || "(starting)"}`);
|
|
17274
|
-
}
|
|
17275
|
-
}
|
|
17276
|
-
if (l.terminal.length > 0) {
|
|
17277
|
-
lines.push("", "Recent terminal results:");
|
|
17278
|
-
for (const e of l.terminal.slice().reverse()) {
|
|
17279
|
-
lines.push(`- ${e.dispatchId} · agent ${e.tier} · ${e.identity}${e.depth ? ` @${e.depth}` : ""} · ${e.state.toUpperCase()} · session ${e.sessionID || "(not started)"}`);
|
|
17280
|
-
const r = e.result;
|
|
17281
|
-
if (r !== undefined)
|
|
17282
|
-
lines.push(` result: ${JSON.stringify(r, null, 1).slice(0, 2000)}`);
|
|
17283
|
-
if (e.lateCompletion !== undefined)
|
|
17284
|
-
lines.push(` (killed; a late result arrived after the kill and was discarded: ${JSON.stringify(e.lateCompletion).slice(0, 300)})`);
|
|
17285
|
-
}
|
|
17286
|
-
}
|
|
17287
|
-
return { title: `dispatches: ${l.inFlight.length} in flight, ${l.terminal.length} terminal`, output: lines.join(`
|
|
17288
|
-
`) };
|
|
17289
|
-
}
|
|
17290
|
-
});
|
|
17291
|
-
var forgeDispatchKillTool = tool({
|
|
17292
|
-
description: "Cancel an in-flight background forge_dispatch by dispatchId: polling stops, the completion brief is suppressed, and the outcome is ledgered as killed. Refuses honestly if the dispatch already finished (use forge_dispatch_list). The child session itself cannot be aborted on this host version and is left for the host to reclaim.",
|
|
17293
|
-
args: {
|
|
17294
|
-
dispatchId: tool.schema.string().describe("The dispatchId returned by forge_dispatch (background) or shown by forge_dispatch_list")
|
|
17295
|
-
},
|
|
17296
|
-
execute: async (args) => {
|
|
17297
|
-
const engine = activeDispatchEngine;
|
|
17298
|
-
if (!engine)
|
|
17299
|
-
throw new Error("[forge] dispatch engine unavailable.");
|
|
17300
|
-
try {
|
|
17301
|
-
engine.kill(args.dispatchId);
|
|
17302
|
-
} catch (err) {
|
|
17303
|
-
if (err instanceof DispatchError)
|
|
17304
|
-
throw new Error(`[forge:dispatch:${err.code}] ${err.message}`);
|
|
17305
|
-
throw err;
|
|
17306
|
-
}
|
|
17307
|
-
return {
|
|
17308
|
-
title: `${args.dispatchId} killed`,
|
|
17309
|
-
output: `[forge:dispatch] ${args.dispatchId}: killed — polling stopped, the completion brief is suppressed, and the outcome is ledgered. The child session cannot be aborted on this host and is left for the host to reclaim.`
|
|
17310
|
-
};
|
|
17311
|
-
}
|
|
17312
|
-
});
|
|
17313
|
-
var CREW_COMMAND_TEMPLATE = [
|
|
17314
|
-
"Objective: {args}",
|
|
16223
|
+
var CREW_INIT_TEMPLATE = [
|
|
16224
|
+
'(forge crew orchestration. Argument: "$ARGUMENTS")',
|
|
17315
16225
|
"",
|
|
17316
|
-
"
|
|
16226
|
+
"CREW IS NOT INITIALIZED: no forge subagents are configured (no usable forge.json agents). Refuse to enter orchestration and tell the user this feature needs a one-time setup, offering BOTH options:",
|
|
17317
16227
|
"",
|
|
17318
|
-
"1.
|
|
17319
|
-
"
|
|
17320
|
-
"
|
|
17321
|
-
"
|
|
17322
|
-
"
|
|
17323
|
-
"
|
|
16228
|
+
"1. The user configures it themselves: create ONE of these files (project-level wins, never merged):",
|
|
16229
|
+
"{paths}",
|
|
16230
|
+
"",
|
|
16231
|
+
"2. Or, ONLY if the user explicitly asks you to configure it in this conversation, you may write the file yourself through the normal write path. NEVER write forge.json without that explicit go-ahead.",
|
|
16232
|
+
"",
|
|
16233
|
+
"Template (JSONC — comments allowed; `model` is the exact provider/model identity, `thoughtLevel` is optional: none/low/medium/high/max or a native level name):",
|
|
16234
|
+
"{template}",
|
|
16235
|
+
"",
|
|
16236
|
+
"After the file is saved: NEW agents require a host restart to appear in the task tool; thoughtLevel edits on existing agents apply without restart. Until then, crew stays unavailable."
|
|
17324
16237
|
].join(`
|
|
17325
16238
|
`);
|
|
16239
|
+
function crewOrchestrationTemplate(agentIds) {
|
|
16240
|
+
return [
|
|
16241
|
+
'(forge crew orchestration. Argument: "$ARGUMENTS")',
|
|
16242
|
+
"",
|
|
16243
|
+
`Configured forge subagents (task-tool vocabulary): ${agentIds.map((id) => `forge-${id}`).join(", ")}`,
|
|
16244
|
+
"",
|
|
16245
|
+
"You are entering CREW ORCHESTRATION discipline for this session (single subject: do not switch agents). Follow it exactly:",
|
|
16246
|
+
"",
|
|
16247
|
+
"1. REGISTER: call `crew_begin` with the objective AND the declared subtask plan (subtasks[]: one {title} per subtask, optionally naming the intended forge-* agent). If crew_begin refuses (a crew is already active here, or a plan draft is active), STOP and tell the user why — never start a second crew. Reconnaissance first if you need it to decompose well; a wrong plan is fixed by discarding and re-crewing, never by improvising outside the declared plan.",
|
|
16248
|
+
"2. WAVES, NEVER FLOODS: execute subtasks in waves of parallel native `task` calls (subagent_type = the matching forge-* agent; a wave's results return within the calling turn). Launch the next wave ONLY after the previous wave's results are in.",
|
|
16249
|
+
"3. MISSING ROLE: when a subtask needs a role no configured forge-* agent matches, run that subtask through a native task call anyway AND tell the user about the gap — suggest configuring the missing role in forge.json.",
|
|
16250
|
+
"4. VERIFY: judge each subtask strictly against its acceptance-evidence statement from the task result text. A result that does not meet the statement is a failure even if the worker sounded confident.",
|
|
16251
|
+
"5. RETRY AT MOST ONCE: a failed subtask may be retried once with an adjusted prompt or agent. If the retry also fails, record the subtask as FAIL with BOTH failure reports visible (attempts[]). Do not retry twice, do not paper over a failure.",
|
|
16252
|
+
"6. CLOSE: when every declared subtask has a verdict, call `crew_close` with the full report (title/verdict/evidence per subtask; attempts[] for FAILs). The gate cross-checks your report against the declared plan — a declared subtask missing from the report refuses the close, and so does an undeclared one. The user confirms the closing dialog."
|
|
16253
|
+
].join(`
|
|
16254
|
+
`);
|
|
16255
|
+
}
|
|
17326
16256
|
var crewBeginTool = tool({
|
|
17327
|
-
description: "Register this session's crew (from /crew). Refuses when a crew is already active in the session
|
|
16257
|
+
description: "Register this session's crew (from /crew) with its DECLARED subtask plan. Refuses when a crew is already active in the session, while a plan draft is active, or when subtasks are missing/duplicated. Crew state is in-memory: a host restart ends it honestly.",
|
|
17328
16258
|
args: {
|
|
17329
|
-
objective: tool.schema.string().describe("One-line crew objective (from /crew)")
|
|
16259
|
+
objective: tool.schema.string().describe("One-line crew objective (from /crew)"),
|
|
16260
|
+
subtasks: tool.schema.array(tool.schema.object({
|
|
16261
|
+
title: tool.schema.string().describe("Short unique subtask title — the crew_close report must use these exact titles"),
|
|
16262
|
+
agent: tool.schema.string().optional().describe("Intended forge-* agent id for this subtask, when one matches")
|
|
16263
|
+
})).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list")
|
|
17330
16264
|
},
|
|
17331
16265
|
execute: async (args, context) => {
|
|
17332
16266
|
const state = stateForBan(context.sessionID);
|
|
@@ -17334,90 +16268,84 @@ var crewBeginTool = tool({
|
|
|
17334
16268
|
if (active && active.doc.status === "draft") {
|
|
17335
16269
|
throw new Error("[forge:crew] A plan is in draft; crew orchestration is denied during planning. Call plan_approve for user approval, or /plan discard to abandon the plan.");
|
|
17336
16270
|
}
|
|
16271
|
+
const loaded = forgeLoader?.load();
|
|
16272
|
+
if (!loaded || Object.keys(loaded.agents).length === 0) {
|
|
16273
|
+
const paths = forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
16274
|
+
throw new Error([
|
|
16275
|
+
"[forge:crew] CREW IS NOT INITIALIZED: no forge subagents are configured. Crew orchestration is unavailable until the user sets it up.",
|
|
16276
|
+
`Create ONE of these files (project-level wins): ${paths.project} OR ${paths.global}`,
|
|
16277
|
+
'Template (JSONC): {"agents": {"research": {"model": "provider/model", "thoughtLevel": "low"}}}',
|
|
16278
|
+
"The user may configure it themselves, or ask you to write the file — only on their explicit go-ahead. Newly added agents need a host restart; thoughtLevel edits apply without one."
|
|
16279
|
+
].join(`
|
|
16280
|
+
`));
|
|
16281
|
+
}
|
|
17337
16282
|
if (!args.objective?.trim())
|
|
17338
16283
|
throw new Error("crew_begin requires a non-empty objective (from /crew <objective>).");
|
|
16284
|
+
const raw = Array.isArray(args.subtasks) ? args.subtasks : [];
|
|
16285
|
+
if (raw.length === 0)
|
|
16286
|
+
throw new Error("[forge:crew] crew_begin requires subtasks[] — declare the plan before executing (decompose the objective into titled subtasks).");
|
|
16287
|
+
const seen = new Set;
|
|
16288
|
+
const subtasks = [];
|
|
16289
|
+
for (const s of raw) {
|
|
16290
|
+
const title = String(s?.title ?? "").trim();
|
|
16291
|
+
if (!title)
|
|
16292
|
+
throw new Error("[forge:crew] every subtask needs a non-empty title.");
|
|
16293
|
+
const key = title.toLowerCase();
|
|
16294
|
+
if (seen.has(key))
|
|
16295
|
+
throw new Error(`[forge:crew] duplicate subtask title "${title}" — titles must be unique (they are the crew_close matching key).`);
|
|
16296
|
+
seen.add(key);
|
|
16297
|
+
const agent = String(s?.agent ?? "").trim();
|
|
16298
|
+
subtasks.push(agent ? { title, agent } : { title });
|
|
16299
|
+
}
|
|
17339
16300
|
const existing = crews.get(context.sessionID);
|
|
17340
16301
|
if (existing) {
|
|
17341
16302
|
throw new Error(`[forge:crew] This session already has an active crew: "${existing.objective}" (started ${existing.startedAt}). Finish and close it with crew_close before starting another.`);
|
|
17342
16303
|
}
|
|
17343
|
-
crews.set(context.sessionID, { objective: args.objective.trim(), startedAt: nowIso() });
|
|
16304
|
+
crews.set(context.sessionID, { objective: args.objective.trim(), startedAt: nowIso(), subtasks });
|
|
17344
16305
|
return {
|
|
17345
16306
|
title: `crew registered: ${args.objective.trim().slice(0, 60)}`,
|
|
17346
|
-
output:
|
|
16307
|
+
output: [
|
|
16308
|
+
`[forge:crew] Crew registered for this session. Objective: ${args.objective.trim()}`,
|
|
16309
|
+
`Declared plan (${subtasks.length}):`,
|
|
16310
|
+
...subtasks.map((s, i) => ` ${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
|
|
16311
|
+
"Proceed with the discipline: waves of parallel native task calls (next wave after the previous results) → per-subtask evidence verdicts → at most one retry → crew_close with the full report covering every declared subtask."
|
|
16312
|
+
].join(`
|
|
16313
|
+
`)
|
|
17347
16314
|
};
|
|
17348
16315
|
}
|
|
17349
16316
|
});
|
|
17350
16317
|
var crewCloseTool = tool({
|
|
17351
|
-
description: "Close this session's crew with the final report (ask-level user confirmation). The gate cross-checks the report against the
|
|
16318
|
+
description: "Close this session's crew with the final report (ask-level user confirmation). The gate cross-checks the report against the declared subtask plan: every declared subtask needs a verdict (PASS/FAIL) and an evidence statement; an undeclared subtask in the report refuses the close; FAIL requires both failure reports (attempt + the one retry). On success the crew ends and the summary is the tool output (there is no persistent crew record).",
|
|
17352
16319
|
args: {
|
|
17353
16320
|
report: tool.schema.array(tool.schema.object({
|
|
17354
|
-
title: tool.schema.string().describe("Subtask title"),
|
|
16321
|
+
title: tool.schema.string().describe("Subtask title — must match a title declared at crew_begin"),
|
|
17355
16322
|
verdict: tool.schema.string().describe("PASS or FAIL"),
|
|
17356
|
-
evidence: tool.schema.string().describe("Acceptance evidence
|
|
16323
|
+
evidence: tool.schema.string().describe("Acceptance evidence: what the result contained that satisfies the acceptance statement"),
|
|
17357
16324
|
attempts: tool.schema.array(tool.schema.string()).optional().describe("For FAIL: both failure reports (attempt and retry)")
|
|
17358
|
-
})).describe("The full crew report — one entry per subtask")
|
|
16325
|
+
})).describe("The full crew report — one entry per declared subtask")
|
|
17359
16326
|
},
|
|
17360
16327
|
execute: async (args, context) => {
|
|
17361
16328
|
const crew = crews.get(context.sessionID);
|
|
17362
16329
|
if (!crew)
|
|
17363
16330
|
throw new Error("[forge:crew] No active crew in this session (crew state is in-memory and dies on host restart). Start one with /crew <objective>.");
|
|
17364
|
-
const
|
|
17365
|
-
|
|
17366
|
-
const file2 = readFileSync9(join5(dispatchLogDir, "ledger.jsonl"), "utf8");
|
|
17367
|
-
for (const line of file2.split(`
|
|
17368
|
-
`)) {
|
|
17369
|
-
const t = line.trim();
|
|
17370
|
-
if (!t)
|
|
17371
|
-
continue;
|
|
17372
|
-
try {
|
|
17373
|
-
rows.push(JSON.parse(t));
|
|
17374
|
-
} catch {}
|
|
17375
|
-
}
|
|
17376
|
-
} catch {}
|
|
17377
|
-
const check2 = validateCrewReport(args.report ?? [], rows, { objective: crew.objective, startedAt: crew.startedAt, sessionID: context.sessionID });
|
|
16331
|
+
const report = args.report ?? [];
|
|
16332
|
+
const check2 = validateCrewReport(report, crew.subtasks);
|
|
17378
16333
|
if (!check2.ok) {
|
|
17379
|
-
throw new Error(`[forge:crew] crew_close refused — the report does not match the
|
|
16334
|
+
throw new Error(`[forge:crew] crew_close refused — the report does not match the declared plan:
|
|
17380
16335
|
${check2.gaps.map((g) => `- ${g}`).join(`
|
|
17381
16336
|
`)}
|
|
17382
16337
|
The crew stays active; complete or correct the report and call crew_close again.`);
|
|
17383
16338
|
}
|
|
17384
|
-
let tokensIn = 0;
|
|
17385
|
-
let tokensOut = 0;
|
|
17386
|
-
let costSum = 0;
|
|
17387
|
-
let costKnown = 0;
|
|
17388
|
-
for (const row of rows) {
|
|
17389
|
-
if (typeof row.dispatchId !== "string" || !check2.dispatchIds.includes(row.dispatchId))
|
|
17390
|
-
continue;
|
|
17391
|
-
const tok = row.tokens;
|
|
17392
|
-
tokensIn += tok?.input ?? 0;
|
|
17393
|
-
tokensOut += (tok?.output ?? 0) + (tok?.reasoning ?? 0);
|
|
17394
|
-
if (typeof row.costUsd === "number") {
|
|
17395
|
-
costSum += row.costUsd;
|
|
17396
|
-
costKnown++;
|
|
17397
|
-
}
|
|
17398
|
-
}
|
|
17399
16339
|
await gate(context.ask, "crew_close", `Close crew: ${crew.objective}`);
|
|
17400
|
-
dispatchLedger.append({
|
|
17401
|
-
ts: nowIso(),
|
|
17402
|
-
event: "crew-summary",
|
|
17403
|
-
tier: "crew",
|
|
17404
|
-
outcome: "closed",
|
|
17405
|
-
sessionID: context.sessionID,
|
|
17406
|
-
note: crew.objective,
|
|
17407
|
-
tokens: { input: tokensIn, output: tokensOut, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
17408
|
-
costUsd: costKnown > 0 ? costSum : null,
|
|
17409
|
-
dispatchIds: check2.dispatchIds,
|
|
17410
|
-
subtasks: (args.report ?? []).map((r) => ({ title: r.title, verdict: String(r.verdict).toUpperCase(), evidence: r.evidence }))
|
|
17411
|
-
});
|
|
17412
16340
|
crews.delete(context.sessionID);
|
|
17413
|
-
const fails =
|
|
16341
|
+
const fails = report.filter((r) => String(r.verdict).toUpperCase() === "FAIL").length;
|
|
17414
16342
|
return {
|
|
17415
16343
|
title: `crew closed: ${crew.objective.slice(0, 60)}`,
|
|
17416
16344
|
output: [
|
|
17417
16345
|
`[forge:crew] Crew closed: ${crew.objective}`,
|
|
17418
|
-
`subtasks: ${
|
|
17419
|
-
|
|
17420
|
-
"
|
|
16346
|
+
`subtasks: ${report.length} (PASS ${report.length - fails}, FAIL ${fails})`,
|
|
16347
|
+
...report.map((r) => `- ${r.title}: ${String(r.verdict).toUpperCase()} — ${String(r.evidence).slice(0, 200)}`),
|
|
16348
|
+
"This output is the crew's record (crew state is in-memory; nothing persists)."
|
|
17421
16349
|
].join(`
|
|
17422
16350
|
`)
|
|
17423
16351
|
};
|
|
@@ -17441,11 +16369,7 @@ function forgeTools() {
|
|
|
17441
16369
|
tools.forge_shell = forgeShellTool;
|
|
17442
16370
|
tools.forge_jobs = forgeJobsTool;
|
|
17443
16371
|
}
|
|
17444
|
-
if (!
|
|
17445
|
-
tools.forge_dispatch = forgeDispatchTool;
|
|
17446
|
-
tools.forge_dispatch_config = forgeDispatchConfigTool;
|
|
17447
|
-
tools.forge_dispatch_list = forgeDispatchListTool;
|
|
17448
|
-
tools.forge_dispatch_kill = forgeDispatchKillTool;
|
|
16372
|
+
if (!forgeDisabled) {
|
|
17449
16373
|
tools.crew_begin = crewBeginTool;
|
|
17450
16374
|
tools.crew_close = crewCloseTool;
|
|
17451
16375
|
}
|
|
@@ -17518,7 +16442,7 @@ function wrapupBriefText(goal, reason) {
|
|
|
17518
16442
|
}
|
|
17519
16443
|
async function autoPauseGoal(client, state, goal, reason, wrapup) {
|
|
17520
16444
|
goalProbe(`auto-pause session=${state.sessionID} reason=${reason} wrapup=${wrapup}`);
|
|
17521
|
-
atomicWrite(goal.path, transitionGoal(
|
|
16445
|
+
atomicWrite(goal.path, transitionGoal(readFileSync8(goal.path, "utf8"), "paused", nowIso(), { stopReason: reason }));
|
|
17522
16446
|
engineForgetSession(state.sessionID);
|
|
17523
16447
|
if (wrapup && goal.doc.session) {
|
|
17524
16448
|
try {
|
|
@@ -17567,9 +16491,9 @@ async function continueIfEligible(client, sessionID) {
|
|
|
17567
16491
|
const ledgerPath = state.goalPath;
|
|
17568
16492
|
if (ledgerPath && existsSync3(ledgerPath)) {
|
|
17569
16493
|
try {
|
|
17570
|
-
const fresh = parseGoalLoose(
|
|
16494
|
+
const fresh = parseGoalLoose(readFileSync8(ledgerPath, "utf8"));
|
|
17571
16495
|
if (fresh && fresh.turnsUsed > 0) {
|
|
17572
|
-
atomicWrite(ledgerPath, appendLedger(
|
|
16496
|
+
atomicWrite(ledgerPath, appendLedger(readFileSync8(ledgerPath, "utf8"), { turn: fresh.turnsUsed, revision: fresh.revision, at: nowIso(), activity: turnHadActivity, writes: act?.writes ?? 0, checks: act?.checks ?? 0 }, nowIso()));
|
|
17573
16497
|
}
|
|
17574
16498
|
} catch (err) {
|
|
17575
16499
|
goalProbe(`ledger append failed session=${sessionID} err=${String(err)}`);
|
|
@@ -17622,10 +16546,7 @@ async function continueIfEligible(client, sessionID) {
|
|
|
17622
16546
|
}
|
|
17623
16547
|
}
|
|
17624
16548
|
try {
|
|
17625
|
-
const
|
|
17626
|
-
const briefText = wakeEntries.length > 0 ? `${dispatchBriefText(wakeEntries)}
|
|
17627
|
-
|
|
17628
|
-
${goalBriefText(state, goal)}` : goalBriefText(state, goal);
|
|
16549
|
+
const briefText = goalBriefText(state, goal);
|
|
17629
16550
|
await client.session.prompt({
|
|
17630
16551
|
path: { id: sessionID },
|
|
17631
16552
|
body: { parts: [{ type: "text", text: briefText }] }
|
|
@@ -17634,7 +16555,7 @@ ${goalBriefText(state, goal)}` : goalBriefText(state, goal);
|
|
|
17634
16555
|
pendingContinuationTurn.add(sessionID);
|
|
17635
16556
|
turnActivity.set(sessionID, { writes: 0, checks: 0 });
|
|
17636
16557
|
state.goalPath = goal.path;
|
|
17637
|
-
atomicWrite(goal.path, incTurns(
|
|
16558
|
+
atomicWrite(goal.path, incTurns(readFileSync8(goal.path, "utf8"), nowIso()));
|
|
17638
16559
|
goalProbe(`continued session=${sessionID} turn=${goal.doc.turnsUsed + 1}/${goal.doc.maxTurns}`);
|
|
17639
16560
|
} catch (err) {
|
|
17640
16561
|
const n = (transportFails.get(sessionID) ?? 0) + 1;
|
|
@@ -17665,7 +16586,6 @@ var server = async (input, options) => {
|
|
|
17665
16586
|
hostWorktree = effectiveWorktree(input.worktree, input.directory) || input.directory || "";
|
|
17666
16587
|
const client = input.client;
|
|
17667
16588
|
jobClient = input.client;
|
|
17668
|
-
dispatchClient = input.client;
|
|
17669
16589
|
const jobsOpts = options?.jobs;
|
|
17670
16590
|
if (jobsOpts?.mode === "auto" || jobsOpts?.mode === "forge" || jobsOpts?.mode === "native")
|
|
17671
16591
|
jobsMode = jobsOpts.mode;
|
|
@@ -17683,18 +16603,6 @@ var server = async (input, options) => {
|
|
|
17683
16603
|
if (wdStallRaw !== undefined && wdStallRaw !== wdStall) {
|
|
17684
16604
|
wdFallbacks.push(`watchdog.stallMs ${JSON.stringify(String(wdStallRaw))} adjusted to ${wdStall} (floor/default applied)`);
|
|
17685
16605
|
}
|
|
17686
|
-
const dpOpts = options?.dispatch;
|
|
17687
|
-
dispatchDisabled = dpOpts?.disable === true;
|
|
17688
|
-
if (Number.isInteger(dpOpts?.maxConcurrent) && dpOpts.maxConcurrent >= 1 && dpOpts.maxConcurrent <= 16) {
|
|
17689
|
-
dispatchMaxConcurrent = dpOpts.maxConcurrent;
|
|
17690
|
-
} else if (dpOpts?.maxConcurrent !== undefined) {
|
|
17691
|
-
dispatchMaxConcurrent = 4;
|
|
17692
|
-
}
|
|
17693
|
-
if (Number.isInteger(dpOpts?.timeoutMs) && dpOpts.timeoutMs >= 1000 && dpOpts.timeoutMs <= 600000) {
|
|
17694
|
-
dispatchTimeoutMs = dpOpts.timeoutMs;
|
|
17695
|
-
} else if (dpOpts?.timeoutMs !== undefined) {
|
|
17696
|
-
dispatchTimeoutMs = 600000;
|
|
17697
|
-
}
|
|
17698
16606
|
const watchdogLedger = createFileLedger(join5(watchdogLogDir(), "log.jsonl"));
|
|
17699
16607
|
const rawLocator = createLocator();
|
|
17700
16608
|
const probing = () => process.env.FORGE_WATCHDOG_PROBE === "1";
|
|
@@ -17750,7 +16658,6 @@ var server = async (input, options) => {
|
|
|
17750
16658
|
exitCleanup?.trigger("dispose");
|
|
17751
16659
|
jobManager.disposeAll();
|
|
17752
16660
|
watchdog.dispose();
|
|
17753
|
-
activeDispatchEngine?.dispose();
|
|
17754
16661
|
sessions.clear();
|
|
17755
16662
|
},
|
|
17756
16663
|
config: async (cfg) => {
|
|
@@ -17784,10 +16691,6 @@ var server = async (input, options) => {
|
|
|
17784
16691
|
template: PLAN_COMMAND_TEMPLATE,
|
|
17785
16692
|
description: "forge plan harness: no argument lists in-progress plans; resume continues the latest; discard abandons it; a goal enters planning discipline"
|
|
17786
16693
|
};
|
|
17787
|
-
cfg.command["crew"] ??= {
|
|
17788
|
-
template: CREW_COMMAND_TEMPLATE,
|
|
17789
|
-
description: "forge crew orchestration: decompose an objective into evidence-checked subtasks dispatched as paced background waves; at most one retry per subtask; crew_close cross-checks the report against the dispatch ledger"
|
|
17790
|
-
};
|
|
17791
16694
|
cfg.command["goal"] ??= {
|
|
17792
16695
|
template: GOAL_COMMAND_TEMPLATE,
|
|
17793
16696
|
description: "forge goal harness (autonomous, host-verified): no argument lists goals; pause/resume/discard drive the loop; add queues; next promotes; an objective drafts a contract and arms it (--check/--contains markers become verification items)"
|
|
@@ -17798,75 +16701,47 @@ var server = async (input, options) => {
|
|
|
17798
16701
|
if (permSection[gateKey] !== "deny")
|
|
17799
16702
|
permSection[gateKey] = "ask";
|
|
17800
16703
|
}
|
|
17801
|
-
if (!
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
if (!forgeLoader || forgeLoaderDir !== hostWorktree) {
|
|
17806
|
-
forgeLoader = createForgeConfigLoader({
|
|
17807
|
-
projectDir: hostWorktree,
|
|
17808
|
-
homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir()
|
|
17809
|
-
});
|
|
17810
|
-
forgeLoaderDir = hostWorktree;
|
|
17811
|
-
}
|
|
17812
|
-
const loaded = forgeLoader.load();
|
|
17813
|
-
forgeConfigState = loaded;
|
|
17814
|
-
for (const finding of loaded.findings) {
|
|
17815
|
-
dispatchLedger.append({ ts: new Date().toISOString(), event: "validation", level: finding.level, code: finding.code, note: finding.message });
|
|
17816
|
-
}
|
|
17817
|
-
for (const [agentId, def] of Object.entries(loaded.agents)) {
|
|
17818
|
-
const id = `forge-${agentId}`;
|
|
17819
|
-
if (agentSection[id] !== undefined)
|
|
17820
|
-
continue;
|
|
17821
|
-
agentSection[id] = forgeAgentDef(agentId, def);
|
|
17822
|
-
}
|
|
17823
|
-
const detected = configuredIdentitiesOf(cfg, snapshot.catalog);
|
|
17824
|
-
lastDetectedIdentities = detected;
|
|
17825
|
-
const built = buildDispatchConfig({
|
|
17826
|
-
configuredIdentities: detected,
|
|
17827
|
-
catalog: snapshot.catalog,
|
|
17828
|
-
userRoster: legacyInline ? dispatchOpts.roster ?? [] : [],
|
|
17829
|
-
userTiers: legacyInline ? dispatchOpts.tiers : undefined
|
|
16704
|
+
if (!forgeLoader || forgeLoaderDir !== hostWorktree) {
|
|
16705
|
+
forgeLoader = createForgeConfigLoader({
|
|
16706
|
+
projectDir: hostWorktree,
|
|
16707
|
+
homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir()
|
|
17830
16708
|
});
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
16709
|
+
forgeLoaderDir = hostWorktree;
|
|
16710
|
+
}
|
|
16711
|
+
const loaded = forgeLoader.load();
|
|
16712
|
+
for (const finding of loaded.findings) {
|
|
16713
|
+
console.warn(`[forge:config] ${finding.level}: ${finding.message}`);
|
|
16714
|
+
}
|
|
16715
|
+
materializedSnapshot = {};
|
|
16716
|
+
for (const [agentId, def] of Object.entries(loaded.agents)) {
|
|
16717
|
+
const id = `forge-${agentId}`;
|
|
16718
|
+
if (agentSection[id] !== undefined)
|
|
16719
|
+
continue;
|
|
16720
|
+
const entry = forgeAgentDef(agentId, def);
|
|
16721
|
+
agentSection[id] = entry;
|
|
16722
|
+
materializedSnapshot[id] = entry;
|
|
16723
|
+
}
|
|
16724
|
+
if (process.env.FORGE_TEST_NO_DISPATCH_FETCH === "1") {
|
|
16725
|
+
modelsDevCatalog = null;
|
|
16726
|
+
} else {
|
|
16727
|
+
try {
|
|
16728
|
+
const snapshot = await loadModelsDevSnapshot();
|
|
16729
|
+
modelsDevCatalog = snapshot.catalog;
|
|
16730
|
+
} catch {
|
|
16731
|
+
modelsDevCatalog = null;
|
|
17845
16732
|
}
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
17858
|
-
return roster.roster.some((e) => e.model === identity) || lastDetectedIdentities.includes(identity);
|
|
17859
|
-
},
|
|
17860
|
-
agents: () => forgeLoader?.load().agents ?? {},
|
|
17861
|
-
legacy: legacyInline,
|
|
17862
|
-
sink: (entry) => dispatchLedger.append(entry),
|
|
17863
|
-
registry: activeDispatchRegistry,
|
|
17864
|
-
onTerminal: (parentSessionID) => scheduleDispatchWake(parentSessionID, false),
|
|
17865
|
-
onDepth: queueDispatchDepth,
|
|
17866
|
-
providerFamily: dispatchProviderFamily,
|
|
17867
|
-
maxConcurrent: dispatchMaxConcurrent,
|
|
17868
|
-
timeoutMs: dispatchTimeoutMs
|
|
17869
|
-
});
|
|
16733
|
+
}
|
|
16734
|
+
cfg.command ??= {};
|
|
16735
|
+
const existingCrew = cfg.command["crew"];
|
|
16736
|
+
if (!existingCrew || String(existingCrew.template ?? "").startsWith("(forge crew")) {
|
|
16737
|
+
const agentIds = Object.keys(loaded.agents);
|
|
16738
|
+
const template = agentIds.length > 0 ? crewOrchestrationTemplate(agentIds) : CREW_INIT_TEMPLATE.replaceAll("{paths}", ` - ${forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).project}
|
|
16739
|
+
- ${forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global}`).replaceAll("{template}", JSON.stringify({ agents: { research: { model: "provider/model", thoughtLevel: "low" } } }, null, 2).split(`
|
|
16740
|
+
`).map((l) => " " + l).join(`
|
|
16741
|
+
`));
|
|
16742
|
+
const description = agentIds.length > 0 ? "forge crew orchestration: register a declared subtask plan, execute it in waves of parallel native task calls, close with an evidence-checked report" : "forge crew orchestration — NOT INITIALIZED: no forge subagents configured; invoking it shows the one-time setup guidance";
|
|
16743
|
+
cfg.command["crew"] = { template, description };
|
|
16744
|
+
crewCommandSnapshot = { template, description };
|
|
17870
16745
|
}
|
|
17871
16746
|
},
|
|
17872
16747
|
get tool() {
|
|
@@ -17897,32 +16772,52 @@ var server = async (input, options) => {
|
|
|
17897
16772
|
throw new Error(`[forge] A plan is in draft; write operations are denied (${input2.tool}). Present the plan summary and call plan_approve for user approval, or /plan discard to abandon it.`);
|
|
17898
16773
|
}
|
|
17899
16774
|
}
|
|
17900
|
-
if (input2.tool === "forge_dispatch") {
|
|
17901
|
-
const state = stateForBan(input2.sessionID);
|
|
17902
|
-
const active = state ? resolveActivePlan(state) : null;
|
|
17903
|
-
if (active && active.doc.status === "draft") {
|
|
17904
|
-
throw new Error("[forge] A plan is in draft; dispatching workers is denied during planning. Call plan_approve for user approval, or /plan discard to abandon the plan.");
|
|
17905
|
-
}
|
|
17906
|
-
}
|
|
17907
16775
|
},
|
|
17908
16776
|
"chat.params": async (input2, output) => {
|
|
17909
|
-
|
|
17910
|
-
|
|
16777
|
+
if (forgeDisabled)
|
|
16778
|
+
return;
|
|
16779
|
+
const raw = input2;
|
|
16780
|
+
const sessionID = raw.sessionID;
|
|
16781
|
+
if (!sessionID)
|
|
16782
|
+
return;
|
|
16783
|
+
const agentName = typeof raw.agent === "string" ? raw.agent : raw.agent?.name;
|
|
16784
|
+
if (typeof agentName !== "string" || !agentName.startsWith("forge-"))
|
|
16785
|
+
return;
|
|
16786
|
+
const frozen = sessionDepths.get(sessionID);
|
|
16787
|
+
if (frozen) {
|
|
16788
|
+
applyDepthTranslation(output.options ?? (output.options = {}), frozen.translation);
|
|
16789
|
+
return;
|
|
16790
|
+
}
|
|
16791
|
+
const agentId = agentName.slice("forge-".length);
|
|
16792
|
+
const def = forgeLoader?.load().agents[agentId];
|
|
16793
|
+
if (!def?.thoughtLevel)
|
|
16794
|
+
return;
|
|
16795
|
+
const providerID = typeof raw.model?.providerID === "string" ? raw.model.providerID : "";
|
|
16796
|
+
const modelID = typeof raw.model?.modelID === "string" ? raw.model.modelID : "";
|
|
16797
|
+
const family = dispatchProviderFamily(providerID);
|
|
16798
|
+
if (!family)
|
|
17911
16799
|
return;
|
|
17912
|
-
|
|
16800
|
+
const { translation, findingMessage } = resolvePinnedDepth(def.thoughtLevel, family, nativeLadder(modelsDevCatalog, `${providerID}/${modelID}`));
|
|
16801
|
+
if (!translation) {
|
|
16802
|
+
if (findingMessage && !depthFindingNotes.has(agentId)) {
|
|
16803
|
+
depthFindingNotes.set(agentId, findingMessage);
|
|
16804
|
+
console.warn(`[forge:depth] agent forge-${agentId}: ${findingMessage}`);
|
|
16805
|
+
}
|
|
16806
|
+
return;
|
|
16807
|
+
}
|
|
16808
|
+
sessionDepths.set(sessionID, { translation });
|
|
16809
|
+
applyDepthTranslation(output.options ?? (output.options = {}), translation);
|
|
17913
16810
|
},
|
|
17914
|
-
|
|
17915
|
-
queueDepth: queueDispatchDepth,
|
|
17916
|
-
depthState: (sessionID) => dispatchDepths.get(sessionID) ?? null,
|
|
17917
|
-
composePrompt: composeWorkerPrompt,
|
|
17918
|
-
registry: () => activeDispatchRegistry,
|
|
16811
|
+
__forgeSubagentsTest: {
|
|
17919
16812
|
crews: () => crews,
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
}
|
|
16813
|
+
depthState: (sessionID) => sessionDepths.get(sessionID)?.translation ?? null,
|
|
16814
|
+
depthFinding: (agentId) => depthFindingNotes.get(agentId) ?? null,
|
|
16815
|
+
catalog: () => modelsDevCatalog,
|
|
16816
|
+
setCatalog: (c) => {
|
|
16817
|
+
modelsDevCatalog = c;
|
|
16818
|
+
},
|
|
16819
|
+
crewCommand: () => crewCommandSnapshot,
|
|
16820
|
+
materialized: () => materializedSnapshot
|
|
17926
16821
|
},
|
|
17927
16822
|
"permission.ask": async (input2, output) => {
|
|
17928
16823
|
const meta = input2.metadata ?? {};
|
|
@@ -17961,13 +16856,13 @@ var server = async (input, options) => {
|
|
|
17961
16856
|
goalProbe(`idle event session=${sessionID}`);
|
|
17962
16857
|
scheduleIdleContinuation(client, sessionID);
|
|
17963
16858
|
deliverJobWakes(sessionID);
|
|
17964
|
-
scheduleDispatchWake(sessionID, true);
|
|
17965
16859
|
}
|
|
17966
16860
|
}
|
|
17967
16861
|
if (event.type === "session.deleted") {
|
|
17968
16862
|
const info = event.properties.info;
|
|
17969
16863
|
if (typeof info?.id === "string" && info.id) {
|
|
17970
16864
|
jobManager.onSessionEnd(info.id);
|
|
16865
|
+
sessionDepths.delete(info.id);
|
|
17971
16866
|
}
|
|
17972
16867
|
}
|
|
17973
16868
|
},
|
|
@@ -18083,13 +16978,30 @@ async function v2Setup(ctx) {
|
|
|
18083
16978
|
await ctx.agent.transform(async (draft) => {
|
|
18084
16979
|
if (typeof draft.get !== "function" || typeof draft.update !== "function")
|
|
18085
16980
|
return;
|
|
18086
|
-
if (draft.get(FORGE_AGENT)
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
}
|
|
16981
|
+
if (draft.get(FORGE_AGENT) === undefined) {
|
|
16982
|
+
draft.update(FORGE_AGENT, (agent) => {
|
|
16983
|
+
agent.description = "forge — the single general-purpose coding agent: takes implementation tasks directly; planning goes through /plan into the plan harness (plans land in .opencode/plan/, with approve/close confirmation gates and tick discipline enforced by tools and the permission layer).";
|
|
16984
|
+
agent.system = FORGE_PROMPT;
|
|
16985
|
+
agent.mode = "primary";
|
|
16986
|
+
});
|
|
16987
|
+
}
|
|
16988
|
+
try {
|
|
16989
|
+
const loader = createForgeConfigLoader({ projectDir: process.cwd(), homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
16990
|
+
const loaded = loader.load();
|
|
16991
|
+
for (const [agentId, def] of Object.entries(loaded.agents)) {
|
|
16992
|
+
const id = `forge-${agentId}`;
|
|
16993
|
+
if (draft.get(id) !== undefined)
|
|
16994
|
+
continue;
|
|
16995
|
+
const v1def = forgeAgentDef(agentId, def);
|
|
16996
|
+
draft.update(id, (agent) => {
|
|
16997
|
+
agent.description = v1def.description;
|
|
16998
|
+
agent.system = v1def.prompt;
|
|
16999
|
+
agent.mode = v1def.mode;
|
|
17000
|
+
agent.model = v1def.model;
|
|
17001
|
+
agent.permission = v1def.permission;
|
|
17002
|
+
});
|
|
17003
|
+
}
|
|
17004
|
+
} catch {}
|
|
18093
17005
|
});
|
|
18094
17006
|
}
|
|
18095
17007
|
}
|