@tekmidian/pai 0.42.6 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/{chain-BbcfP77K.mjs → chain-DAEC9AU7.mjs} +2 -2
  2. package/dist/{chain-BbcfP77K.mjs.map → chain-DAEC9AU7.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +5 -5
  4. package/dist/cli/program.mjs +5 -5
  5. package/dist/daemon/index.mjs +4 -4
  6. package/dist/{daemon-BNjbVsQU.mjs → daemon-BVbvL8IA.mjs} +4 -4
  7. package/dist/{daemon-BppziPi6.mjs → daemon-ebLDoKM3.mjs} +6 -6
  8. package/dist/{daemon-BppziPi6.mjs.map → daemon-ebLDoKM3.mjs.map} +1 -1
  9. package/dist/daemon-mcp/index.mjs +3 -3
  10. package/dist/{fallback-Cwt4hmh5.mjs → fallback-CxGDTXWt.mjs} +6 -4
  11. package/dist/fallback-CxGDTXWt.mjs.map +1 -0
  12. package/dist/hooks/load-project-context.mjs +51 -48
  13. package/dist/hooks/load-project-context.mjs.map +4 -4
  14. package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
  15. package/dist/hooks/whisper-reinject.mjs +8 -0
  16. package/dist/hooks/whisper-reinject.mjs.map +3 -3
  17. package/dist/hooks/worker-status-line.mjs +108 -41
  18. package/dist/hooks/worker-status-line.mjs.map +3 -3
  19. package/dist/{planner-pXRc-8yt.mjs → planner-B4DvNpXd.mjs} +3 -3
  20. package/dist/{planner-pXRc-8yt.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
  21. package/dist/{program-LLNPpxlj.mjs → program-cr5U1-DK.mjs} +1347 -65
  22. package/dist/program-cr5U1-DK.mjs.map +1 -0
  23. package/dist/{run-DDdT9teG.mjs → run-BgExF8Xg.mjs} +485 -60
  24. package/dist/run-BgExF8Xg.mjs.map +1 -0
  25. package/dist/{tools-BradfQk7.mjs → tools-CAG2sUjf.mjs} +2 -2
  26. package/dist/{tools-UrSHIe29.mjs → tools-DiRC5glA.mjs} +2 -2
  27. package/dist/{tools-UrSHIe29.mjs.map → tools-DiRC5glA.mjs.map} +1 -1
  28. package/dist/{wakeup-TnGjJY3I.mjs → wakeup-BfElduEU.mjs} +21 -8
  29. package/dist/wakeup-BfElduEU.mjs.map +1 -0
  30. package/docs/commands/README.md +5 -1
  31. package/docs/commands/audit.md +50 -2
  32. package/docs/commands/worker.md +2 -0
  33. package/package.json +1 -1
  34. package/scripts/build-hooks.mjs +2 -0
  35. package/src/hooks/ts/lib/context-fill.test.ts +40 -0
  36. package/src/hooks/ts/post-tool-use/whisper-reinject.test.ts +55 -0
  37. package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
  38. package/src/hooks/ts/session-start/load-project-context.ts +3 -3
  39. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -2
  40. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +0 -1
  41. package/src/hooks/ztk-auto.sh +28 -0
  42. package/src/hooks/ztk-auto.test.ts +72 -0
  43. package/src/hooks/ztk-passthrough.mjs +71 -0
  44. package/dist/fallback-Cwt4hmh5.mjs.map +0 -1
  45. package/dist/hooks/session-commands.mjs +0 -26
  46. package/dist/hooks/session-commands.mjs.map +0 -7
  47. package/dist/program-LLNPpxlj.mjs.map +0 -1
  48. package/dist/run-DDdT9teG.mjs.map +0 -1
  49. package/dist/wakeup-TnGjJY3I.mjs.map +0 -1
  50. package/src/hooks/ts/session-start/session-commands.ts +0 -30
@@ -1825,70 +1825,333 @@ function worktreeSystemPrompt(id, branch, dir) {
1825
1825
  ].join("\n");
1826
1826
  }
1827
1827
 
1828
+ //#endregion
1829
+ //#region src/workers/agentish.ts
1830
+ /**
1831
+ * agentish.ts — Agentish v2 (AG2), the wire format a worker's final report
1832
+ * uses by default: a kind line plus one-letter k=v lines instead of JSON
1833
+ * prose. Spec and validator live in the `aibroker` CLI
1834
+ * (see /Users/i052341/Daten/Cloud/Development/ai/AIBroker/docs/agentish.md);
1835
+ * this module shells out to it when present and falls back to a frozen copy
1836
+ * of the spec text otherwise, so a worker still gets a contract when
1837
+ * `aibroker` is not installed on its box.
1838
+ */
1839
+ /** Frozen copy of `aibroker agentish spec`'s first line, for when the CLI is absent. */
1840
+ const AG2_SPEC_FALLBACK = "AG2. msg=kind line+k=v lines. kinds T R S Q A X. keys i id g goal o own n forbid d steps p proof u out l limits r res c changes t tests G gate I inst m images # nums w worst x next z note(<200ch). sep |. outcomes + - ~ ? !. @n=path declared once then reused; @n:12=file:line. tests as Name+ Name-. no prose, no articles, never restate, unknown=?. r=+ only if all t +.";
1841
+ /** Frozen copy of the `y` (why) extension line. */
1842
+ const AG2_EXTENSIONS_FALLBACK = "y why(≤600ch)";
1843
+ let cachedSpec = null;
1844
+ /**
1845
+ * The AG2 spec + extensions lines: from `aibroker agentish spec` when the
1846
+ * CLI runs and its output looks right, else the frozen fallback. Memoised —
1847
+ * every caller in one process gets the same spec without re-spawning aibroker.
1848
+ */
1849
+ function ag2Spec() {
1850
+ if (cachedSpec) return cachedSpec;
1851
+ try {
1852
+ const lines = execFileSync("aibroker", ["agentish", "spec"], {
1853
+ timeout: 3e3,
1854
+ stdio: [
1855
+ "ignore",
1856
+ "pipe",
1857
+ "ignore"
1858
+ ],
1859
+ encoding: "utf8"
1860
+ }).split("\n").filter((l) => l.trim());
1861
+ if (lines[0]?.startsWith("AG2.")) {
1862
+ cachedSpec = {
1863
+ spec: lines[0],
1864
+ extensions: lines[1] ?? AG2_EXTENSIONS_FALLBACK,
1865
+ source: "aibroker"
1866
+ };
1867
+ return cachedSpec;
1868
+ }
1869
+ } catch {}
1870
+ cachedSpec = {
1871
+ spec: AG2_SPEC_FALLBACK,
1872
+ extensions: AG2_EXTENSIONS_FALLBACK,
1873
+ source: "builtin"
1874
+ };
1875
+ return cachedSpec;
1876
+ }
1877
+ /**
1878
+ * Parse one AG2 message: first non-empty line is the kind (T R S Q A X,
1879
+ * optionally followed by trailing text — only the first character is the
1880
+ * kind), later lines are `key=value` or `@n=path` symbol declarations.
1881
+ * ```-fenced lines are ignored so a message pasted inside a code fence still
1882
+ * parses. Returns null when the first non-empty line is not a known kind.
1883
+ */
1884
+ function parseAg2Report(text) {
1885
+ const kinds = new Set([
1886
+ "T",
1887
+ "R",
1888
+ "S",
1889
+ "Q",
1890
+ "A",
1891
+ "X"
1892
+ ]);
1893
+ const lines = (text ?? "").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
1894
+ if (!lines.length) return null;
1895
+ const kind = lines[0][0];
1896
+ if (!kinds.has(kind)) return null;
1897
+ const fields = {};
1898
+ const symbols = {};
1899
+ for (const line of lines.slice(1)) {
1900
+ const eq = line.indexOf("=");
1901
+ if (eq < 0) continue;
1902
+ const key = line.slice(0, eq).trim();
1903
+ const value = line.slice(eq + 1).trim();
1904
+ if (key.startsWith("@")) symbols[key.slice(1)] = value;
1905
+ else fields[key] = value;
1906
+ }
1907
+ return {
1908
+ kind,
1909
+ fields,
1910
+ symbols
1911
+ };
1912
+ }
1913
+ /** true iff the value ends with the AG2 pass outcome character. */
1914
+ function passed(entry) {
1915
+ return entry.endsWith("+");
1916
+ }
1917
+ /**
1918
+ * Map a parsed AG2 report onto the runner's WorkerReport shape, so the
1919
+ * viewer and `printResult` can render an AG2 `R` exactly like a JSON one.
1920
+ */
1921
+ function ag2ToWorkerReport(parsed) {
1922
+ const f = parsed.fields;
1923
+ const out = { format: "ag2" };
1924
+ if (f.c) out.changed = f.c.split("|").map((s) => s.trim()).filter(Boolean).map((entry) => {
1925
+ const colon = entry.indexOf(":");
1926
+ const space = entry.indexOf(" ");
1927
+ const cut = colon >= 0 && (space < 0 || colon < space) ? colon : space;
1928
+ if (cut < 0) return {
1929
+ path: entry,
1930
+ summary: ""
1931
+ };
1932
+ return {
1933
+ path: entry.slice(0, cut).trim(),
1934
+ summary: entry.slice(cut + 1).trim()
1935
+ };
1936
+ });
1937
+ if (f.t) out.checks = f.t.split(/\s+/).filter(Boolean).map((entry) => ({
1938
+ name: entry.slice(0, -1),
1939
+ ok: passed(entry),
1940
+ detail: entry.slice(-1)
1941
+ }));
1942
+ if (f.p) out.commands = f.p.split("|").map((s) => s.trim()).filter(Boolean);
1943
+ const open = [];
1944
+ if (f.x) open.push(f.x);
1945
+ for (const [k, v] of Object.entries(f)) if ((v.endsWith("!") || v.endsWith("?")) && k !== "r" && k !== "res") open.push(`${k}: ${v}`);
1946
+ if (open.length) out.open = open;
1947
+ if (f.z) out.notes = f.z;
1948
+ const res = f.r ?? f.res;
1949
+ if (res === "+" || res === "-" || res === "~" || res === "?" || res === "!") out.result = res;
1950
+ if (f.y) out.why = f.y;
1951
+ return out;
1952
+ }
1953
+ /**
1954
+ * Validate an AG2 message with `aibroker agentish check - --json`. Never
1955
+ * fails the caller: a missing/broken CLI reports ok:true, validator "none" —
1956
+ * a worker's run is never blocked on a local tooling gap, only its
1957
+ * `reportValid` marker goes unset.
1958
+ */
1959
+ function validateAg2(text) {
1960
+ try {
1961
+ const out = execFileSync("aibroker", [
1962
+ "agentish",
1963
+ "check",
1964
+ "-",
1965
+ "--json"
1966
+ ], {
1967
+ input: text,
1968
+ timeout: 5e3,
1969
+ stdio: [
1970
+ "pipe",
1971
+ "pipe",
1972
+ "ignore"
1973
+ ],
1974
+ encoding: "utf8"
1975
+ });
1976
+ const parsed = JSON.parse(out);
1977
+ const errors = (parsed.errors ?? []).map((e) => e.message ?? e.code ?? "unknown error");
1978
+ return {
1979
+ ok: parsed.ok !== false,
1980
+ errors,
1981
+ validator: "aibroker"
1982
+ };
1983
+ } catch (e) {
1984
+ const out = e.stdout;
1985
+ if (out) try {
1986
+ const parsed = JSON.parse(out.toString());
1987
+ const errors = (parsed.errors ?? []).map((er) => er.message ?? er.code ?? "unknown error");
1988
+ return {
1989
+ ok: parsed.ok !== false,
1990
+ errors,
1991
+ validator: "aibroker"
1992
+ };
1993
+ } catch {}
1994
+ return {
1995
+ ok: true,
1996
+ errors: [],
1997
+ validator: "none"
1998
+ };
1999
+ }
2000
+ }
2001
+
1828
2002
  //#endregion
1829
2003
  //#region src/workers/report.ts
1830
2004
  /**
1831
2005
  * report.ts — the worker contract: terse, structured final reports.
1832
2006
  *
1833
2007
  * Headless workers run with an appended system prompt that fixes their output
1834
- * shape: act, verify, then finish with ONE final message that is a JSON object
1835
- * and nothing else. The runner parses it out of the result; the viewer renders
1836
- * it as a compact block instead of a wall of text.
2008
+ * shape: act, verify, then finish with ONE final message — AG2 (see
2009
+ * agentish.ts) by default, JSON as the pre-AG2 fallback. The runner parses it
2010
+ * out of the result; the viewer renders it as a compact block instead of a
2011
+ * wall of text.
1837
2012
  */
1838
2013
  /** The marker the runner puts on operator messages the worker must answer. */
1839
2014
  const OPERATOR_MARK = "[operator]";
1840
- /** Appended to the caller's system prompt on every headless run (2h). */
1841
- const WORKER_CONTRACT_PROMPT = [
1842
- "You are a headless implementation worker, run non-interactively by an orchestrating session.",
1843
- "You are the worker: this task is yours to finish in this process. Never hand your whole task to a",
1844
- "single child worker and step back — that is delegation, not work. Child workers are for two things",
1845
- "only: (1) PARALLELISING independent parts of your task, each part a self-contained spec; (2) CHEAPER",
1846
- "bounded sub-tasks — probes, renders, test runs, lookups — run one tier down with --class spotcheck or",
1847
- "--class simple. Always pin --provider anthropic on children.",
1848
- "You own your children's results: run them in the foreground or poll until each has finished, read",
1849
- "their result, verify it, and fold it into your own report. When your turn ends this run ends and any",
1850
- "running child is killed, so never end with a child still running.",
1851
- "Ending your turn ends this run. There is no later wake-up: never call ScheduleWakeup, never say you",
1852
- "will pick something up later, never leave a background command as your final action. Finish, then",
1853
- "report.",
1854
- "No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
1855
- "Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:",
1856
- "{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}",
1857
- "changed: files you touched (path + one-line summary). commands: the commands that verify the work.",
1858
- "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.",
1859
- "notes: one line, the headline a reviewer reads first.",
1860
- "NEVER put a multi-line or quoted payload inline in a shell command — no heredocs (<<EOF), no long",
1861
- "-p '…' specs for child workers, no inline JSON, no inline scripts. ALWAYS write it to a file with",
1862
- "the Write tool first and pass the file: -p \"$(cat spec.txt)\", --file spec.txt, or < spec.txt.",
1863
- "Inline quoting fails silently or dies with a parse error and burns a worker launch; there are no",
1864
- "exceptions.",
1865
- "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
1866
- "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
1867
- "Write tool and run it (python3 script.py), or use jq for JSON inspection.",
1868
- "",
1869
- `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
1870
- "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
1871
- "a question gets its answer, an instruction gets one line naming what will change — then continue the",
1872
- "task you were on.",
1873
- "",
1874
- "Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
1875
- "(Bash tool). Your children run on their own provider and report back to you automatically when",
1876
- "they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
1877
- "suffice, the task is out of your scope, or a decision is needed:",
1878
- "`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
1879
- "question, blocker; results are sent for you when you finish). Sibling workers are not a",
1880
- "coordination path — there is no sideways channel; everything goes up to your parent."
1881
- ].join("\n");
1882
- /**
1883
- * Extract the JSON report from a worker's final message. Accepts a bare
1884
- * object, a ```json fenced block, or an object embedded in surrounding text.
1885
- * Only objects that look like the contract count (at least one of changed /
2015
+ /** Classes that get the short contract — cheap, bounded, single-answer tasks. */
2016
+ const SHORT_CONTRACT_CLASSES = new Set(["spotcheck", "simple"]);
2017
+ /**
2018
+ * The AG2 report block appended to a contract when format is "ag2" — field
2019
+ * set matches `aibroker agentish check`'s real R schema (verified 2026-09-20:
2020
+ * i/r/t/c are required, not optional as an early draft of this contract had
2021
+ * it; r=+ needs G=+ and non-empty p too, not just all-t-passing; R has no
2022
+ * `u`/out field at all — u is T-only — so the answer goes in z instead).
2023
+ */
2024
+ function ag2ReportBlock() {
2025
+ const spec = ag2Spec();
2026
+ return [
2027
+ spec.spec,
2028
+ spec.extensions,
2029
+ "Final message: one AG2 `R` message only — no prose, no code fence, no markdown.",
2030
+ "R requires i (id), r (+ - ~ ? !), t (tests, Name+ Name-), c (path summary|path summary).",
2031
+ "r=+ requires G=+ (gate) and p non-empty (proof command), and every t entry +. z= the answer/note,",
2032
+ "one line, always present. x= what is left (omit if nothing). y= one line why when r is not +.",
2033
+ "Example:",
2034
+ "R",
2035
+ "i=count-audit-ts",
2036
+ "r=+",
2037
+ "G=+",
2038
+ "c=src/audit summary of files counted",
2039
+ "t=Count+",
2040
+ "p=wc -l src/audit/*.ts",
2041
+ "z=19 files, 2523 lines"
2042
+ ];
2043
+ }
2044
+ /** The JSON report block appended to a contract when format is "json" (pre-AG2 fallback). */
2045
+ function jsonReportBlock(lead) {
2046
+ return [lead, "{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}"];
2047
+ }
2048
+ /** The two sentences that stop a worker from breaking on shell quoting — kept in both contracts. */
2049
+ function fileNotInlineBlock() {
2050
+ return [
2051
+ "NEVER put a multi-line or quoted payload inline in a shell command — no heredocs (<<EOF), no long",
2052
+ "-p '…' specs for child workers, no inline JSON, no inline scripts. ALWAYS write it to a file with",
2053
+ "the Write tool first and pass the file: -p \"$(cat spec.txt)\", --file spec.txt, or < spec.txt.",
2054
+ "Inline quoting fails silently or dies with a parse error and burns a worker launch; there are no",
2055
+ "exceptions.",
2056
+ "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
2057
+ "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
2058
+ "Write tool and run it (python3 script.py), or use jq for JSON inspection."
2059
+ ];
2060
+ }
2061
+ /** The file-not-inline rule, condensed for the short contract's tight length budget. */
2062
+ function fileNotInlineBlockShort() {
2063
+ return [
2064
+ "NEVER put a multi-line or quoted payload inline in a shell command (no heredocs, no inline",
2065
+ "scripts): write it to a file with the Write tool and pass it, e.g. -p \"$(cat spec.txt)\".",
2066
+ "Never inline code (python3 -c and friends) — write a script file and run it, or use jq."
2067
+ ];
2068
+ }
2069
+ /** spotcheck/simple: headless, bounded, no delegation language — under 1500 chars in ag2 format. */
2070
+ function shortContractPrompt(format) {
2071
+ const lines = [
2072
+ "You are a headless implementation worker, run non-interactively.",
2073
+ "This task is yours: answer the question or do the bounded task now. Run at most one command",
2074
+ "to confirm a result and never re-derive a number you already have. No child workers, no handoff.",
2075
+ ...fileNotInlineBlockShort()
2076
+ ];
2077
+ lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
2078
+ return lines.join("\n");
2079
+ }
2080
+ /** Every other class (and no class): the full worker/orchestrator contract. */
2081
+ function fullContractPrompt(format) {
2082
+ const lines = [
2083
+ "You are a headless implementation worker, run non-interactively by an orchestrating session.",
2084
+ "You are the worker: this task is yours to finish in this process. Never hand your whole task to a",
2085
+ "single child worker and step back — that is delegation, not work. Child workers are for two things",
2086
+ "only: (1) PARALLELISING independent parts of your task, each part a self-contained spec; (2) CHEAPER",
2087
+ "bounded sub-tasks — probes, renders, test runs, lookups — run one tier down with --class spotcheck or",
2088
+ "--class simple. Always pin --provider anthropic on children.",
2089
+ "You own your children's results: run them in the foreground or poll until each has finished, read",
2090
+ "their result, verify it, and fold it into your own report. When your turn ends this run ends and any",
2091
+ "running child is killed, so never end with a child still running.",
2092
+ "Ending your turn ends this run. There is no later wake-up: never call ScheduleWakeup, never say you",
2093
+ "will pick something up later, never leave a background command as your final action. Finish, then",
2094
+ "report.",
2095
+ "No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
2096
+ ...fileNotInlineBlock(),
2097
+ "",
2098
+ `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
2099
+ "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
2100
+ "a question gets its answer, an instruction gets one line naming what will change — then continue the",
2101
+ "task you were on.",
2102
+ "",
2103
+ "Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
2104
+ "(Bash tool). Your children run on their own provider and report back to you automatically when",
2105
+ "they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
2106
+ "suffice, the task is out of your scope, or a decision is needed:",
2107
+ "`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
2108
+ "question, blocker; results are sent for you when you finish). Sibling workers are not a",
2109
+ "coordination path — there is no sideways channel; everything goes up to your parent.",
2110
+ ""
2111
+ ];
2112
+ lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
2113
+ if (format === "json") lines.push("changed: files you touched (path + one-line summary). commands: the commands that verify the work.", "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.", "notes: one line, the headline a reviewer reads first.");
2114
+ return lines.join("\n");
2115
+ }
2116
+ /**
2117
+ * The system prompt appended to every headless run, chosen by task class and
2118
+ * report format. `spotcheck`/`simple` get a short contract (no delegation
2119
+ * language, under 1200 chars in json format / 1500 in ag2); every other class (and no class) gets the
2120
+ * full contract. Both end with the AG2 report block unless `format` is
2121
+ * "json", the pre-AG2 fallback.
2122
+ */
2123
+ function workerContractPrompt(cls, format) {
2124
+ return SHORT_CONTRACT_CLASSES.has(cls ?? "") ? shortContractPrompt(format) : fullContractPrompt(format);
2125
+ }
2126
+ /**
2127
+ * The line appended to a headless prompt's own text (never the system
2128
+ * prompt): models weight an instruction at the end of the user turn more
2129
+ * than one buried in a long system-prompt paragraph, and this is the
2130
+ * difference between a worker that returns a valid AG2/JSON report and one
2131
+ * that answers in prose despite the contract (measured 2026-09-20 on both
2132
+ * Haiku and Sonnet with the AG2 contract alone).
2133
+ */
2134
+ function promptTrailer(format) {
2135
+ return format === "ag2" ? "\n\nFinal message: one AG2 R message only, no prose (format in system prompt)." : "\n\nFinal message: a JSON object only, no prose before or after, no code fence (format in system prompt).";
2136
+ }
2137
+ /** The exact text sent to resend a final message that failed AG2 validation. */
2138
+ const AG2_REASK_TEXT = "Your final message was not a valid AG2 R message. Resend it now as a single AG2 R message only: first line R, then i=id r=result G=+ (gate, if r=+) t=tests(Name+) c=path summary p=proof z=the answer/note. No prose.";
2139
+ /** The full-contract AG2 prompt — the keepalive path's identical-construction baseline. */
2140
+ const WORKER_CONTRACT_PROMPT = fullContractPrompt("ag2");
2141
+ /**
2142
+ * Extract the report from a worker's final message: AG2 first (an `R`
2143
+ * message, see agentish.ts), then the JSON contract as a fallback for
2144
+ * workers still on the pre-AG2 shape. Accepts a bare JSON object, a
2145
+ * ```json fenced block, or an object embedded in surrounding text — only
2146
+ * objects that look like the contract count (at least one of changed /
1886
2147
  * checks / notes) — any other JSON falls through to null so the raw text is
1887
2148
  * kept as-is.
1888
2149
  */
1889
2150
  function parseWorkerReport(text) {
1890
2151
  const trimmed = (text ?? "").trim();
1891
2152
  if (!trimmed) return null;
2153
+ const ag2 = parseAg2Report(trimmed);
2154
+ if (ag2 && ag2.kind === "R") return ag2ToWorkerReport(ag2);
1892
2155
  const candidates = [];
1893
2156
  const fence = trimmed.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
1894
2157
  if (fence) candidates.push(fence[1]);
@@ -1898,7 +2161,10 @@ function parseWorkerReport(text) {
1898
2161
  if (first >= 0 && last > first) candidates.push(trimmed.slice(first, last + 1));
1899
2162
  for (const cand of candidates) try {
1900
2163
  const v = JSON.parse(cand);
1901
- if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return v;
2164
+ if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return {
2165
+ ...v,
2166
+ format: "json"
2167
+ };
1902
2168
  } catch {}
1903
2169
  return null;
1904
2170
  }
@@ -2204,6 +2470,14 @@ function codexInstalled() {
2204
2470
  * Auto-routed runs that die of a quota error before the first tool call are
2205
2471
  * restarted on the next provider in routing order (WORKER-REROUTE ledger line).
2206
2472
  */
2473
+ /**
2474
+ * ag2 unless the caller passed --report json or PAI_WORKER_REPORT=json is
2475
+ * set in the environment; an explicit flag always wins over the env default.
2476
+ */
2477
+ function resolveReportFormat(flag, env = process.env) {
2478
+ if (flag) return flag;
2479
+ return env.PAI_WORKER_REPORT === "json" ? "json" : "ag2";
2480
+ }
2207
2481
  /** A stream-json user message for the child's stdin. */
2208
2482
  function stdinUserMessage(text) {
2209
2483
  return JSON.stringify({
@@ -2312,7 +2586,44 @@ function modelArgs(model, callerPinned) {
2312
2586
  function modelFlagArgs(headless, model, callerPinned) {
2313
2587
  return headless ? modelArgs(model, callerPinned) : [];
2314
2588
  }
2589
+ /**
2590
+ * The prompt text actually sent to the worker and whether the trailer got
2591
+ * appended. An instruction at the END of the user turn outweighs one buried
2592
+ * in the system prompt (measured 2026-09-20: the AG2 contract alone was
2593
+ * ignored by both Haiku and Sonnet). Never applied to an interactive launch
2594
+ * (no -p) or when the caller brought its own --append-system-prompt — the
2595
+ * trailer's "format in system prompt" would then point at nothing.
2596
+ */
2597
+ function headlessPromptText(prompt, headless, callerSystemPrompt, format) {
2598
+ const applied = headless && prompt !== null && !callerSystemPrompt;
2599
+ return {
2600
+ text: applied ? prompt + promptTrailer(format) : prompt,
2601
+ applied
2602
+ };
2603
+ }
2604
+ /**
2605
+ * Whether an invalid final report earns the one bounded re-ask: only when a
2606
+ * real validator caught the problem (a missing/broken aibroker must never
2607
+ * block the run further), the caller did not opt out, and there is a session
2608
+ * to resume into. Cap of exactly one is enforced by the caller never calling
2609
+ * this twice for the same run.
2610
+ */
2611
+ function shouldRetryReport(validation, noReportRetry, hasSession) {
2612
+ return !validation.ok && validation.validator === "aibroker" && !noReportRetry && hasSession;
2613
+ }
2315
2614
  /** Context window announced by the init event, when the endpoint sends one. */
2615
+ /**
2616
+ * Did the run end well? A headless run must have produced a stream-json
2617
+ * result event that is not an error; an interactive launch IS the chat pane
2618
+ * and never emits one, so its clean exit (rc 0) is the whole verdict —
2619
+ * without this every interactive session that ends normally was recorded
2620
+ * as `failed rc=0` and supervision pinged the spawner about a non-failure.
2621
+ */
2622
+ function runSucceeded(headless, rc, resultEvent) {
2623
+ if (rc !== 0) return false;
2624
+ if (!headless) return true;
2625
+ return resultEvent !== null && !resultEvent.is_error;
2626
+ }
2316
2627
  function initContextWindow(e) {
2317
2628
  if (typeof e.context_window === "number" && e.context_window > 0) return e.context_window;
2318
2629
  if (e.model_info && typeof e.model_info.context_window === "number" && e.model_info.context_window > 0) return e.model_info.context_window;
@@ -2329,7 +2640,7 @@ async function runWorker(opts) {
2329
2640
  const logDir = workersLogDir(config);
2330
2641
  mkdirSync(logDir, { recursive: true });
2331
2642
  if (opts.className === "plan" && !opts._planner) {
2332
- const { runPlanner } = await import("./planner-pXRc-8yt.mjs");
2643
+ const { runPlanner } = await import("./planner-B4DvNpXd.mjs");
2333
2644
  return runPlanner(opts);
2334
2645
  }
2335
2646
  const parent = launchParent(opts.parent);
@@ -2342,6 +2653,7 @@ async function runWorker(opts) {
2342
2653
  const parsed = parseRunnerArgs(opts.claudeArgs);
2343
2654
  const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
2344
2655
  const model = resolveRunModel(target, opts.className, opts.modelFlag);
2656
+ const reportFormat = resolveReportFormat(opts.reportFormatFlag);
2345
2657
  try {
2346
2658
  if (target.provider.engine === "codex") return await executeCodexRun({
2347
2659
  config,
@@ -2360,7 +2672,8 @@ async function runWorker(opts) {
2360
2672
  onWorkerStart: opts.onWorkerStart,
2361
2673
  id: opts.id,
2362
2674
  worktreeFlag: opts.worktreeFlag,
2363
- className: opts.className
2675
+ className: opts.className,
2676
+ reportFormat
2364
2677
  });
2365
2678
  return await executeRun({
2366
2679
  config,
@@ -2382,7 +2695,9 @@ async function runWorker(opts) {
2382
2695
  worktreeFlag: opts.worktreeFlag,
2383
2696
  className: opts.className,
2384
2697
  reroutes: opts._reroutes ?? 0,
2385
- printCmd: opts.printCmd
2698
+ printCmd: opts.printCmd,
2699
+ reportFormat,
2700
+ noReportRetry: opts.noReportRetry ?? false
2386
2701
  });
2387
2702
  } catch (e) {
2388
2703
  if (e instanceof Error && e.message.startsWith("key file")) throw new Error(`provider "${target.providerName}": ${e.message}\nPut the token in that file (chmod 600) or point keyFile elsewhere.`);
@@ -2430,6 +2745,39 @@ function headlessToolsFlag(allowedTools) {
2430
2745
  return names.length ? ["--tools", names.join(",")] : [];
2431
2746
  }
2432
2747
  /**
2748
+ * A `--tools` value with no ToolSearch silently disables MCP tool deferral
2749
+ * (measured 2026-09-20: a run with `--tools "Bash,Read,Grep,Glob,Agent"` paid
2750
+ * 65,300 first-turn tokens with 5 MCP servers, 230,448 with 16, versus
2751
+ * 24,889 / 37,133 with ToolSearch present). Applied to the fully-assembled
2752
+ * argv so it catches a `--tools` from any source — the computed
2753
+ * `headlessToolsFlag`/project-pin flag, or one the caller put directly on
2754
+ * the command line.
2755
+ */
2756
+ function ensureToolSearch(cmd) {
2757
+ const flagIdx = cmd.findIndex((a) => a === "--tools");
2758
+ if (flagIdx >= 0 && flagIdx + 1 < cmd.length) {
2759
+ const value = cmd[flagIdx + 1];
2760
+ if (value && !value.split(",").includes("ToolSearch")) {
2761
+ const out = [...cmd];
2762
+ out[flagIdx + 1] = `${value},ToolSearch`;
2763
+ process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
2764
+ return out;
2765
+ }
2766
+ return cmd;
2767
+ }
2768
+ const eqIdx = cmd.findIndex((a) => a.startsWith("--tools="));
2769
+ if (eqIdx >= 0) {
2770
+ const value = cmd[eqIdx].slice(8);
2771
+ if (value && !value.split(",").includes("ToolSearch")) {
2772
+ const out = [...cmd];
2773
+ out[eqIdx] = `--tools=${value},ToolSearch`;
2774
+ process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
2775
+ return out;
2776
+ }
2777
+ }
2778
+ return cmd;
2779
+ }
2780
+ /**
2433
2781
  * The claude flag that turns on the browser bridge, when the run asked for it.
2434
2782
  *
2435
2783
  * The bridge is not an MCP server — it rides the Chrome native-host channel,
@@ -2520,14 +2868,16 @@ async function executeRun(a) {
2520
2868
  }
2521
2869
  const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
2522
2870
  const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
2523
- const cmd = ["claude"];
2871
+ const { text: promptText, applied: trailerApplied } = headlessPromptText(parsed.prompt, headless, parsed.callerSystemPrompt, a.reportFormat);
2872
+ let cmd = ["claude"];
2524
2873
  cmd.push(...modelFlagArgs(headless, model, Boolean(parsed.callerModel)));
2525
2874
  cmd.push(...chromeArgs, ...mcpArgs, ...toolArgs, ...toolsFlag, ...restArgs);
2526
2875
  if (headless) {
2527
2876
  cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
2528
- if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
2877
+ if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", workerContractPrompt(a.className, a.reportFormat));
2529
2878
  if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
2530
2879
  }
2880
+ cmd = ensureToolSearch(cmd);
2531
2881
  if (a.printCmd) {
2532
2882
  console.log(JSON.stringify({
2533
2883
  cmd,
@@ -2553,6 +2903,8 @@ async function executeRun(a) {
2553
2903
  secs: null,
2554
2904
  origin: headless ? "spawn" : "chat",
2555
2905
  outputFormat: parsed.outputFormat,
2906
+ ...headless ? { reportFormat: a.reportFormat } : {},
2907
+ ...trailerApplied ? { promptTrailer: true } : {},
2556
2908
  ...session ? { session } : {},
2557
2909
  ...spawnerSession ? { spawnerSession } : {},
2558
2910
  ...a.parent ? {
@@ -2663,8 +3015,8 @@ async function executeRun(a) {
2663
3015
  resultReport: null
2664
3016
  };
2665
3017
  if (headless) {
2666
- if (parsed.prompt !== null) try {
2667
- proc.stdin.write(stdinUserMessage(parsed.prompt) + "\n");
3018
+ if (promptText !== null) try {
3019
+ proc.stdin.write(stdinUserMessage(promptText) + "\n");
2668
3020
  } catch {}
2669
3021
  eventsFd = openSync(eventsPath(logDir, wid), "a");
2670
3022
  createInterface({ input: proc.stdout }).on("line", (line) => {
@@ -2717,12 +3069,46 @@ async function executeRun(a) {
2717
3069
  cleanup();
2718
3070
  const secs = Math.floor((Date.now() - t0) / 1e3);
2719
3071
  const resultEvent = ctx.resultEvent;
2720
- const ok = rc === 0 && resultEvent !== null && !resultEvent.is_error;
3072
+ const ok = runSucceeded(headless, rc, resultEvent);
2721
3073
  status.state = ok ? "done" : "failed";
2722
3074
  status.rc = rc;
2723
3075
  status.secs = secs;
2724
3076
  if (resultEvent) status.last = shortText(resultEvent.result ?? "", 90);
2725
3077
  if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
3078
+ if (headless && a.reportFormat === "ag2" && resultEvent?.result) {
3079
+ const v = validateAg2(resultEvent.result);
3080
+ status.reportValid = v.ok;
3081
+ status.reportErrors = v.errors;
3082
+ if (shouldRetryReport(v, a.noReportRetry ?? false, Boolean(status.claudeSession))) {
3083
+ status.reportRetried = true;
3084
+ saveStatus(logDir, status);
3085
+ appendLedger(ledger, "WORKER-REPORT-RETRY", {
3086
+ id: wid,
3087
+ reason: v.errors.join("; ") || "invalid AG2 message"
3088
+ });
3089
+ try {
3090
+ const reaskText = await reaskAg2Report({
3091
+ env,
3092
+ cwd: worktree?.dir ?? cwd,
3093
+ model,
3094
+ callerPinnedModel: Boolean(parsed.callerModel),
3095
+ chromeArgs,
3096
+ mcpArgs,
3097
+ toolArgs,
3098
+ toolsFlag,
3099
+ sessionId: status.claudeSession
3100
+ });
3101
+ const v2 = validateAg2(reaskText);
3102
+ status.reportValid = v2.ok;
3103
+ status.reportErrors = v2.errors;
3104
+ if (v2.ok) {
3105
+ resultEvent.result = reaskText;
3106
+ ctx.resultReport = parseWorkerReport(reaskText);
3107
+ if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
3108
+ }
3109
+ } catch {}
3110
+ }
3111
+ }
2726
3112
  saveStatus(logDir, status);
2727
3113
  appendLedger(ledger, "WORKER-END", {
2728
3114
  id: wid,
@@ -2791,6 +3177,44 @@ function worktreeExtras(s) {
2791
3177
  commits: s.commits ?? 0
2792
3178
  } : void 0;
2793
3179
  }
3180
+ /**
3181
+ * The one bounded re-ask, cap exactly one: `claude --resume <session>` with
3182
+ * AG2_REASK_TEXT, single-shot `--output-format json` (one turn, no streaming
3183
+ * plumbing needed). Same env/model/mcp/tool flags as the original launch —
3184
+ * only --resume and the fixed re-ask prompt are new — so the resumed turn
3185
+ * sees the same grants the worker ran with.
3186
+ */
3187
+ async function reaskAg2Report(a) {
3188
+ let cmd = ["claude"];
3189
+ cmd.push(...modelFlagArgs(true, a.model, a.callerPinnedModel));
3190
+ cmd.push(...a.chromeArgs, ...a.mcpArgs, ...a.toolArgs, ...a.toolsFlag);
3191
+ cmd.push("--resume", a.sessionId, "-p", AG2_REASK_TEXT, "--output-format", "json");
3192
+ cmd = ensureToolSearch(cmd);
3193
+ const proc = spawn(cmd[0], cmd.slice(1), {
3194
+ env: a.env,
3195
+ cwd: a.cwd,
3196
+ stdio: [
3197
+ "ignore",
3198
+ "pipe",
3199
+ "inherit"
3200
+ ]
3201
+ });
3202
+ const timer = setTimeout(() => {
3203
+ try {
3204
+ proc.kill("SIGKILL");
3205
+ } catch {}
3206
+ }, a.timeoutMs ?? 6e4);
3207
+ let out = "";
3208
+ proc.stdout.on("data", (chunk) => {
3209
+ out += chunk.toString("utf8");
3210
+ });
3211
+ await new Promise((resolve) => {
3212
+ proc.on("error", () => resolve());
3213
+ proc.on("close", () => resolve());
3214
+ });
3215
+ clearTimeout(timer);
3216
+ return resultFromOutput(out);
3217
+ }
2794
3218
  async function executeCodexRun(a) {
2795
3219
  const { config, logDir, target, model, label, parsed, noPane } = a;
2796
3220
  if (!parsed.headless || parsed.prompt === null) throw new Error(`provider "${target.providerName}" (engine codex) supports headless runs only: pass the task with -p '<prompt>'`);
@@ -2835,6 +3259,7 @@ async function executeCodexRun(a) {
2835
3259
  secs: null,
2836
3260
  origin: "spawn",
2837
3261
  outputFormat: parsed.outputFormat,
3262
+ reportFormat: "json",
2838
3263
  ...session ? { session } : {},
2839
3264
  ...spawnerSession ? { spawnerSession } : {},
2840
3265
  ...target.provider.contextWindow ? { contextWindow: target.provider.contextWindow } : {},
@@ -3113,5 +3538,5 @@ function resultFromOutput(out) {
3113
3538
  }
3114
3539
 
3115
3540
  //#endregion
3116
- export { newWorkerId as A, ageOf as C, isChatPane as D, contextPercent as E, longInlinePromptHint as F, parseRunnerArgs as I, shortText as L, saveStatus as M, setWorkerLabel as N, loadStatus as O, waitForTerminalStatus as P, describeMcp as R, UNLABELED as S, contextLabel as T, resolveSession as _, renderReport as a, appendLedger as b, handoffFromInside as c, workerDepth as d, checkPaneForWorker as f, itermForClaudeSession as g, currentTabKey as h, parseWorkerReport as i, nowStamp as j, loadStatuses as k, readInbox as l, openPaneForWorker as m, runWorker as n, discardWorker as o, openFollowPane as p, testProvider as r, mergeWorker as s, printResult as t, sayToWorker as u, sessionTag as v, alive as w, ledgerSummary as x, workerInScope as y, expandMcpNames as z };
3117
- //# sourceMappingURL=run-DDdT9teG.mjs.map
3541
+ export { loadStatus as A, describeMcp as B, ledgerSummary as C, contextLabel as D, alive as E, setWorkerLabel as F, waitForTerminalStatus as I, longInlinePromptHint as L, newWorkerId as M, nowStamp as N, contextPercent as O, saveStatus as P, parseRunnerArgs as R, appendLedger as S, ageOf as T, expandMcpNames as V, currentTabKey as _, projectLaunchConfig as a, sessionTag as b, discardWorker as c, readInbox as d, sayToWorker as f, openPaneForWorker as g, openFollowPane as h, usageContextTokens as i, loadStatuses as j, isChatPane as k, mergeWorker as l, checkPaneForWorker as m, runWorker as n, parseWorkerReport as o, workerDepth as p, testProvider as r, renderReport as s, printResult as t, handoffFromInside as u, itermForClaudeSession as v, UNLABELED as w, workerInScope as x, resolveSession as y, shortText as z };
3542
+ //# sourceMappingURL=run-BgExF8Xg.mjs.map