@jini-ai/daemon 0.3.0 → 0.3.2

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 (37) hide show
  1. package/dist/agent-executor.d.ts +216 -24
  2. package/dist/agent-executor.d.ts.map +1 -1
  3. package/dist/agent-executor.js +373 -38
  4. package/dist/agent-executor.js.map +1 -1
  5. package/dist/run-lifecycle.d.ts +36 -1
  6. package/dist/run-lifecycle.d.ts.map +1 -1
  7. package/dist/run-lifecycle.js +89 -0
  8. package/dist/run-lifecycle.js.map +1 -1
  9. package/package.json +5 -5
  10. package/dist/artifacts/index.d.ts +0 -14
  11. package/dist/artifacts/index.d.ts.map +0 -1
  12. package/dist/artifacts/index.js +0 -14
  13. package/dist/artifacts/index.js.map +0 -1
  14. package/dist/artifacts/manifest.d.ts +0 -90
  15. package/dist/artifacts/manifest.d.ts.map +0 -1
  16. package/dist/artifacts/manifest.js +0 -229
  17. package/dist/artifacts/manifest.js.map +0 -1
  18. package/dist/artifacts/publication-guard.d.ts +0 -28
  19. package/dist/artifacts/publication-guard.d.ts.map +0 -1
  20. package/dist/artifacts/publication-guard.js +0 -77
  21. package/dist/artifacts/publication-guard.js.map +0 -1
  22. package/dist/artifacts/runtime-compat.d.ts +0 -37
  23. package/dist/artifacts/runtime-compat.d.ts.map +0 -1
  24. package/dist/artifacts/runtime-compat.js +0 -33
  25. package/dist/artifacts/runtime-compat.js.map +0 -1
  26. package/dist/artifacts/store.d.ts +0 -85
  27. package/dist/artifacts/store.d.ts.map +0 -1
  28. package/dist/artifacts/store.js +0 -92
  29. package/dist/artifacts/store.js.map +0 -1
  30. package/dist/artifacts/stub-guard.d.ts +0 -73
  31. package/dist/artifacts/stub-guard.d.ts.map +0 -1
  32. package/dist/artifacts/stub-guard.js +0 -198
  33. package/dist/artifacts/stub-guard.js.map +0 -1
  34. package/dist/artifacts/text-suppression.d.ts +0 -68
  35. package/dist/artifacts/text-suppression.d.ts.map +0 -1
  36. package/dist/artifacts/text-suppression.js +0 -184
  37. package/dist/artifacts/text-suppression.js.map +0 -1
@@ -94,7 +94,7 @@ import { promises as fsPromises } from 'node:fs';
94
94
  import { homedir, tmpdir } from 'node:os';
95
95
  import { join } from 'node:path';
96
96
  import { redactSecrets } from '@jini-ai/core';
97
- import { applyAgentLaunchEnv, createClaudeStreamHandler, createCopilotStreamHandler, createJsonEventStreamHandler, createQoderStreamHandler, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
97
+ import { agentCapabilities, applyAgentLaunchEnv, createClaudeStreamHandler, createCopilotStreamHandler, createJsonEventStreamHandler, createQoderStreamHandler, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
98
98
  import { collectProcessTreePids, createCommandInvocation, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform';
99
99
  import { classifyRunCloseStatus } from './close-status.js';
100
100
  import { resolveContinuationTransport } from './continuation/continuation-transport.js';
@@ -743,6 +743,60 @@ export function mergeEnvContentMcpConfig(existingRaw, entry) {
743
743
  };
744
744
  return JSON.stringify({ ...doc, mcp });
745
745
  }
746
+ /**
747
+ * Merges a staged system-prompt overlay file's path into the `instructions` array of the same
748
+ * OpenCode-schema config document {@link mergeEnvContentMcpConfig} writes `mcp` into — for a
749
+ * `systemPromptDelivery: { strategy: 'config-instructions-file' }` def (`opencode` today).
750
+ *
751
+ * Confirmed live (2026-09-01, opencode-cli 1.17.10), not inferred from docs alone:
752
+ * 1. `instructions` is honored — a run configured with it visibly followed the file's directive
753
+ * (a required exact-token prefix), while an identical run without it did not.
754
+ * 2. It appends, never replaces: the same run that followed the custom instruction ALSO still
755
+ * answered correctly using opencode's own baked-in environment-context system prompt (asked
756
+ * for its cwd, with nothing about cwd anywhere in the custom instructions file) — proof
757
+ * opencode's own defaults survive alongside a custom `instructions` entry, not just proof the
758
+ * file was read at all.
759
+ * 3. Adding this key alongside `mcp` in the same `OPENCODE_CONFIG_CONTENT` document disturbs
760
+ * neither: in one combined run, the MCP bridge still got its connection attempt (logged
761
+ * `key=jini type=local`) AND the custom instruction was still followed — same as running each
762
+ * key alone.
763
+ * 4. `instructions` is re-read fresh from the env on every spawn, including a `-s <id>`-resumed
764
+ * turn (proved by swapping in a second instructions file between two turns of one resumed
765
+ * session and seeing the second turn immediately reflect it while still recalling
766
+ * conversation memory from turn one) — so this mechanism is safe to redeliver every turn like
767
+ * `'append-flag'`/`'env-var'`, exempt from the prompt-prefix fallback's create-only gating
768
+ * (see {@link resolveSystemPromptOverlayDelivery}'s doc): nothing here is ever baked into
769
+ * opencode's own persisted session state the way re-injecting fallback prompt text would be.
770
+ *
771
+ * @param existingRaw - Whatever the spawn env already held for this variable (already possibly
772
+ * carrying `mcp`, if `mergeEnvContentMcpConfig` ran first on the same value — order between the two
773
+ * doesn't matter, each only touches its own top-level key), or `undefined`.
774
+ * @param instructionsFilePath - The staged overlay file's absolute path (see
775
+ * {@link prepareSystemPromptOverlayFileIfNeeded}).
776
+ * @returns The full JSON string to set as the env var's value. Appends to, never clobbers, any
777
+ * `instructions` entries already present — the same "merge, never clobber" discipline
778
+ * {@link mergeEnvContentMcpConfig} applies to `mcp`, in case a host is already using this same
779
+ * config-content variable to carry the operator's own instruction files.
780
+ * @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
781
+ * @overallScore 100/100
782
+ */
783
+ export function mergeEnvContentInstructions(existingRaw, instructionsFilePath) {
784
+ let doc = {};
785
+ if (existingRaw !== undefined && existingRaw.length > 0) {
786
+ try {
787
+ const parsed = JSON.parse(existingRaw);
788
+ if (isRecord(parsed))
789
+ doc = parsed;
790
+ }
791
+ catch {
792
+ doc = {};
793
+ }
794
+ }
795
+ const existingInstructions = Array.isArray(doc.instructions)
796
+ ? doc.instructions.filter((entry) => typeof entry === 'string')
797
+ : [];
798
+ return JSON.stringify({ ...doc, instructions: [...existingInstructions, instructionsFilePath] });
799
+ }
746
800
  /**
747
801
  * TOML basic-string escaping for the narrow value shapes {@link buildCodexMcpServerToml} emits (a
748
802
  * command name, an argv token, an env var value — never multi-line or control-character-heavy
@@ -795,31 +849,61 @@ export function buildCodexMcpServerToml(entry) {
795
849
  return serverTable;
796
850
  return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
797
851
  }
852
+ /**
853
+ * Removes any pre-existing `[mcp_servers.{@link JINI_MCP_SERVER_KEY}]` table — and its
854
+ * `[mcp_servers.{@link JINI_MCP_SERVER_KEY}.*]` subtables (e.g. `.env`) — from a real Codex
855
+ * `config.toml`'s raw text, so {@link buildCodexHomeConfigToml} can append this run's own table
856
+ * without producing the duplicate TOML key Codex's parser rejects at startup.
857
+ *
858
+ * Line-oriented, not a real TOML parser (matching {@link buildCodexMcpServerToml}'s own
859
+ * no-TOML-dependency constraint): a table header is any line that is, after trimming, exactly
860
+ * `[...]`. Once such a header's key matches the jini table or one of its subtables, every following
861
+ * line is dropped until the next table header (of any name) or EOF. Every other table, key, and
862
+ * blank line is passed through untouched.
863
+ * @param existingRaw - The real Codex home's `config.toml` content, already known to be defined
864
+ * (callers pass `''` for a missing file).
865
+ * @returns `existingRaw` with any jini table/subtable removed.
866
+ * @complexity O(n) in the number of lines.
867
+ */
868
+ function stripExistingJiniMcpServerTable(existingRaw) {
869
+ const jiniTableKey = `mcp_servers.${JINI_MCP_SERVER_KEY}`;
870
+ const kept = [];
871
+ let skipping = false;
872
+ for (const line of existingRaw.split('\n')) {
873
+ const header = /^\s*\[([^[\]]+)\]\s*$/.exec(line);
874
+ if (header) {
875
+ const key = (header[1] ?? '').trim();
876
+ skipping = key === jiniTableKey || key.startsWith(`${jiniTableKey}.`);
877
+ if (skipping)
878
+ continue;
879
+ }
880
+ if (!skipping)
881
+ kept.push(line);
882
+ }
883
+ return kept.join('\n');
884
+ }
798
885
  /**
799
886
  * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
800
- * config, verbatim, with this run's `[mcp_servers.jini]` table appended.
887
+ * config, with any pre-existing `[mcp_servers.jini]` table removed (see
888
+ * {@link stripExistingJiniMcpServerTable}), then this run's own table appended.
801
889
  *
802
- * **Append-only by design, not a parse-and-merge.** `mergeMcpJsonContent`/`mergeEnvContentMcpConfig`
803
- * above can safely parse-merge-reserialize because their formats have a JS-native parser
804
- * (`JSON.parse`); this driver has no TOML parser in its dependency graph (see
805
- * `buildCodexMcpServerToml`'s doc), and every other setting a real Codex install carries — model
806
- * choice, sandbox policy, the trusted-project list, the operator's own other MCP servers — must
807
- * survive a spawn byte-for-byte. Appending preserves all of it; the one failure mode this trades
808
- * away is a PRE-EXISTING `[mcp_servers.jini]` table in the operator's own config, which would
809
- * produce a duplicate TOML key Codex rejects at startup. Accepted as vanishingly unlikely — `jini`
810
- * is this integration's own reserved server name (see {@link JINI_MCP_SERVER_KEY}), never suggested
811
- * to an operator for their own config — rather than solved with a full TOML parser for one
812
- * collision case.
890
+ * **Append-only by design, not a parse-and-merge, for everything but the jini table itself.**
891
+ * `mergeMcpJsonContent`/`mergeEnvContentMcpConfig` above can safely parse-merge-reserialize because
892
+ * their formats have a JS-native parser (`JSON.parse`); this driver has no general TOML parser in
893
+ * its dependency graph (see `buildCodexMcpServerToml`'s doc), and every other setting a real Codex
894
+ * install carries — model choice, sandbox policy, the trusted-project list, the operator's own
895
+ * other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
896
+ * owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
897
+ * ever removed, via the narrow line-oriented scan above, never a full TOML parse.
813
898
  * @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
814
899
  * exist (a fresh Codex install — degrades to "start from just this run's block", matching
815
900
  * {@link mergeMcpJsonContent}'s own "missing file" handling).
816
901
  * @param entry - The shared bridge entry.
817
902
  * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
818
903
  * @complexity O(n) in the existing config's length.
819
- * @overallScore 100/100
820
904
  */
821
905
  export function buildCodexHomeConfigToml(existingRaw, entry) {
822
- const base = existingRaw ?? '';
906
+ const base = stripExistingJiniMcpServerTable(existingRaw ?? '');
823
907
  const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
824
908
  return `${base}${separator}${buildCodexMcpServerToml(entry)}`;
825
909
  }
@@ -875,6 +959,8 @@ export function buildMcpBridgeDelivery(input) {
875
959
  return { kind: 'env-content', envVarName: ENV_CONTENT_VAR_BY_STRATEGY[strategy], serverEntry };
876
960
  case 'codex-toml':
877
961
  return { kind: 'codex-toml', serverEntry };
962
+ case 'env-passthrough':
963
+ return { kind: 'env-passthrough', serverEntry };
878
964
  }
879
965
  }
880
966
  function defaultReadMcpJsonFile(path) {
@@ -1810,28 +1896,47 @@ export async function resolveMcpBridgeForRun(input, deps) {
1810
1896
  }
1811
1897
  }
1812
1898
  /**
1813
- * Phase 6a/10c: the subprocess environment every env-riding MCP mechanism uses — mechanism 3+4
1899
+ * Phase 6a/10c: the subprocess environment every env-riding mechanism uses — mechanism 3+4
1814
1900
  * (`'opencode-env-content'`/`'mimo-env-content'`, merged into whatever the host already set there,
1815
1901
  * never a CLI argument: the config embeds `JINI_DAEMON_TOKEN`, and process arguments are readable
1816
- * by any other local user through `ps`) and mechanism 5 (`'codex-toml'`, `CODEX_HOME` relocation).
1817
- * Pure `codexHomeDir` arrives already staged by {@link prepareCodexHomeIfNeeded}, which is the
1818
- * one part of this mechanism that is NOT pure (a real `mkdtemp`).
1902
+ * by any other local user through `ps`), mechanism 5 (`'codex-toml'`, `CODEX_HOME` relocation),
1903
+ * mechanism 6 (`'env-passthrough'`, the bridge entry's flat env vars set directly with no carrier
1904
+ * document see {@link McpBridgeDelivery}'s own doc), a
1905
+ * `systemPromptDelivery: 'env-var'` def's overlay (`reasonix`'s `REASONIX_ACP_SYSTEM_APPEND` today
1906
+ * — see `resolveSystemPromptOverlayDelivery`'s own doc), and a `'config-instructions-file'` def's
1907
+ * staged overlay file (`opencode` today — see {@link mergeEnvContentInstructions}'s own doc). Pure
1908
+ * — `codexHomeDir` and `stagedInstructionsFile` arrive already staged by
1909
+ * {@link prepareCodexHomeIfNeeded} and {@link prepareSystemPromptOverlayFileIfNeeded} respectively,
1910
+ * the only parts of this mechanism that are NOT pure (real `mkdtemp`/`writeFile` calls).
1819
1911
  * @param spawnEnv - The env every other spawn-time step (launch-path resolution, `applyAgentLaunchEnv`) already computed.
1820
1912
  * @param mcpBridge - This run's resolved bridge delivery, or `null` for an unconfigured host / no-strategy def.
1821
1913
  * @param codexHomeDir - The staged scratch `CODEX_HOME` path for a `'codex-toml'` def, or `undefined` for every other run (including a `'codex-toml'` def when `mcpJsonInjection` was never configured — see `prepareCodexHomeIfNeeded`'s own gate).
1822
- * @complexity O(1) plus `mergeEnvContentMcpConfig`'s own `JSON.parse`/`JSON.stringify` cost.
1914
+ * @param systemPromptEnvOverrides - `resolveSystemPromptOverlayDelivery`'s `envOverrides` `{}` (default) for every def but an `'env-var'`-strategy one with an overlay present, in which case it carries that one var. Applied after `codexHomeDir`, so it can never be shadowed by it — the two never share a key (`CODEX_HOME` vs. e.g. `REASONIX_ACP_SYSTEM_APPEND`), so the ordering is a documentation choice, not a correctness one.
1915
+ * @param stagedInstructionsFile - `varName` (from the def's own `systemPromptDelivery` declaration) and the staged overlay file's `path`, or `undefined` for every def but a `'config-instructions-file'` one with an overlay present. Merged into `varName`'s value AFTER the `mcp` merge above (reading `envContentApplied`, not the original `spawnEnv`, for that same key) so both a `mcp` entry and an `instructions` entry from the two mechanisms survive together in one document — confirmed live this coexistence is safe (see {@link mergeEnvContentInstructions}'s doc).
1916
+ * @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
1823
1917
  * @overallScore 100/100
1824
1918
  */
1825
- export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir) {
1919
+ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile) {
1826
1920
  const envContentApplied = mcpBridge?.kind === 'env-content'
1827
1921
  ? {
1828
1922
  ...spawnEnv,
1829
1923
  [mcpBridge.envVarName]: mergeEnvContentMcpConfig(spawnEnv[mcpBridge.envVarName], mcpBridge.serverEntry),
1830
1924
  }
1831
1925
  : spawnEnv;
1832
- if (codexHomeDir === undefined)
1833
- return envContentApplied;
1834
- return { ...envContentApplied, CODEX_HOME: codexHomeDir };
1926
+ // `'env-passthrough'` (antigravity): no document, no named carrier variable — the bridge
1927
+ // entry's own `env` keys (`JINI_RUN_ID`/`JINI_DAEMON_URL`/`JINI_DAEMON_TOKEN`) are set directly
1928
+ // on the child's environment, for the spawned CLI to inherit down to its own globally
1929
+ // pre-registered MCP child in turn. See `McpBridgeDelivery`'s own doc for why this def has no
1930
+ // config document to merge into at all.
1931
+ const envPassthroughApplied = mcpBridge?.kind === 'env-passthrough' ? { ...envContentApplied, ...mcpBridge.serverEntry.env } : envContentApplied;
1932
+ const instructionsApplied = stagedInstructionsFile === undefined
1933
+ ? envPassthroughApplied
1934
+ : {
1935
+ ...envPassthroughApplied,
1936
+ [stagedInstructionsFile.varName]: mergeEnvContentInstructions(envPassthroughApplied[stagedInstructionsFile.varName], stagedInstructionsFile.path),
1937
+ };
1938
+ const codexHomeApplied = codexHomeDir === undefined ? instructionsApplied : { ...instructionsApplied, CODEX_HOME: codexHomeDir };
1939
+ return systemPromptEnvOverrides === undefined ? codexHomeApplied : { ...codexHomeApplied, ...systemPromptEnvOverrides };
1835
1940
  }
1836
1941
  /**
1837
1942
  * Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
@@ -1898,10 +2003,130 @@ export function buildAgentBuildArgsOptions(input, systemPromptOverlay) {
1898
2003
  ...(hasOverlay ? { systemPromptOverlay } : {}),
1899
2004
  };
1900
2005
  }
1901
- /** Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw. */
2006
+ /**
2007
+ * **The single dispatch point from a computed system-prompt overlay to its delivery mechanism** —
2008
+ * see `RuntimeAgentDef.systemPromptDelivery`'s own doc for the declared shape. Pure and
2009
+ * synchronous, mirroring {@link buildMcpBridgeDelivery}'s "keyed off the declared strategy, never
2010
+ * off the def's id" contract: a def earns overlay delivery by declaring a strategy, not by being
2011
+ * named in this file. That is what makes every def with no declaration work via the fallback
2012
+ * without any of their own files being touched.
2013
+ *
2014
+ * The fallback (no declared strategy — every def but `claude` today) prefixes the overlay directly
2015
+ * onto the composed prompt text, clearly delimited from the user's own request. It is gated on
2016
+ * session state, not merely on whether an overlay exists: a def that carries its own conversation
2017
+ * memory across spawns (`resumesSessionViaCli` / `resumesSessionViaAcpLoad`) persists whatever its
2018
+ * session-creating turn sends it — see `RuntimeContext.resumeSessionId`'s own doc: its presence on
2019
+ * a run means "continue a prior session", not "start one". Prefixing on every later turn of that
2020
+ * same session would therefore bake the overlay into the CLI's own stored history again and again,
2021
+ * compounding without bound turn over turn. So the fallback prefixes only when there is no resume
2022
+ * target yet (the session's own first turn, or a def with no session memory at all, which never
2023
+ * replays anything back at the CLI and so gets it on every turn).
2024
+ *
2025
+ * `'append-flag'` and `'env-var'` defs are the opposite case: the flag/env var is a fresh,
2026
+ * un-stored per-spawn directive — never part of what a resumed session replays — so it is set on
2027
+ * every turn unconditionally, exactly `claude`'s pre-existing (now-centralized) behavior before
2028
+ * this function existed.
2029
+ *
2030
+ * @param input.defId - Looks up this def's probed capabilities for an `'append-flag'` strategy's
2031
+ * `capabilityKey`. Otherwise unused — the dispatch itself is keyed off `systemPromptDelivery`, per
2032
+ * this function's own doc above, never off the id.
2033
+ * @param input.systemPromptDelivery - The def's declared strategy, or `undefined` for the fallback.
2034
+ * @param input.resumesSessionViaCli - The def's own flag (see `RuntimeAgentDef`'s doc).
2035
+ * @param input.resumesSessionViaAcpLoad - The def's own flag (see `RuntimeAgentDef`'s doc).
2036
+ * @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result. `null`/`undefined`/
2037
+ * empty short-circuits to "no delivery" — byte-identical to no `PromptAugmenter` configured at all.
2038
+ * @param input.prompt - The composed prompt `buildArgs` would otherwise receive verbatim.
2039
+ * @param input.resumeSessionId - This run's `RuntimeContext.resumeSessionId`; presence means an
2040
+ * existing session is being continued, not created.
2041
+ * @returns A {@link SystemPromptOverlayDelivery}: the prefix this run's prompt text must carry
2042
+ * (`''` unless the fallback applies), that prefix already applied to `input.prompt`, any extra argv
2043
+ * to append to whatever `buildArgs` itself returns, and any env var overrides to merge into the
2044
+ * spawn env (`{}` for every strategy but `'env-var'`). Every one of `run()`'s four prompt
2045
+ * transports consumes this same result — see the interface's own no-double-delivery note.
2046
+ * @complexity O(n) in the overlay/prompt lengths — string concatenation only, no I/O.
2047
+ * @overallScore 100/100
2048
+ */
2049
+ export function resolveSystemPromptOverlayDelivery(input) {
2050
+ const { defId, systemPromptDelivery, resumesSessionViaCli, resumesSessionViaAcpLoad, overlay, prompt, resumeSessionId } = input;
2051
+ if (typeof overlay !== 'string' || overlay.length === 0) {
2052
+ return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
2053
+ }
2054
+ if (systemPromptDelivery?.strategy === 'append-flag') {
2055
+ const capabilityKey = systemPromptDelivery.capabilityKey;
2056
+ // `!== false`, not a truthiness check: mirrors `claude.ts`'s own pre-existing
2057
+ // `agentCapabilities.get('claude') || {}` gate exactly (moved here, not changed) — an
2058
+ // undetected/never-probed capability defaults to allowed, and only an EXPLICIT `false` (the
2059
+ // `--help` probe ran and did not find the flag) withholds it. `capabilityKey === undefined`
2060
+ // (e.g. `pi`'s existing `--append-system-prompt`, trusted unconditionally) always passes, same
2061
+ // as an absent key.
2062
+ const capabilityOk = capabilityKey === undefined || agentCapabilities.get(defId)?.[capabilityKey] !== false;
2063
+ return { promptPrefix: '', prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
2064
+ }
2065
+ if (systemPromptDelivery?.strategy === 'env-var') {
2066
+ // No capability gate, unlike `'append-flag'`: an unrecognized env var is inert to a CLI (it
2067
+ // simply never reads it), never a fatal "unknown option" exit — there is no equivalent hazard
2068
+ // to probe-gate against here. Set verbatim, not merged with any existing value — a dedicated
2069
+ // single-purpose var, not a shared config channel (see this field's own `types.ts` doc).
2070
+ return { promptPrefix: '', prompt, extraArgs: [], envOverrides: { [systemPromptDelivery.varName]: overlay } };
2071
+ }
2072
+ if (systemPromptDelivery?.strategy === 'config-instructions-file') {
2073
+ // Delivered elsewhere, not here: unlike `'append-flag'`/`'env-var'`, this mechanism needs real
2074
+ // filesystem I/O (staging the overlay to a temp file — `opencode`'s `instructions` array only
2075
+ // accepts a file path or URL, confirmed live, never inline text), which this function's "pure
2076
+ // and synchronous" contract cannot perform. `prepareSystemPromptOverlayFileIfNeeded` (a separate
2077
+ // async phase in `run()`, gated on this same strategy check) stages the file, and
2078
+ // `computeChildEnv` merges its path into the config document via `mergeEnvContentInstructions`.
2079
+ // This branch's only job is to make sure the universal prefix fallback below does NOT ALSO run
2080
+ // for a def that already has this strategy declared — the same "no double delivery" concern
2081
+ // `imageDelivery`'s doc calls out for its own native-vs-fallback split.
2082
+ return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
2083
+ }
2084
+ const isContinuingExistingSession = (resumesSessionViaCli === true || resumesSessionViaAcpLoad === true) &&
2085
+ typeof resumeSessionId === 'string' &&
2086
+ resumeSessionId.length > 0;
2087
+ if (isContinuingExistingSession) {
2088
+ return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
2089
+ }
2090
+ // KNOWN TRADE-OFF, deliberate: for a resume-capable def with no `'append-flag'`/`'env-var'`
2091
+ // mechanism yet (`codex`, `codebuddy`, `opencode`, `amr` — all four presently on this fallback),
2092
+ // the overlay is therefore only injected on the SESSION-CREATING turn, not every turn. A host
2093
+ // whose `PromptAugmenter.systemOverlay()` result can change mid-conversation (e.g. a host that
2094
+ // lets an operator edit its own stored instructions and re-reads them before every run — see
2095
+ // `prompt-augmenter.ts`'s own doc for the seam) will see NO effect from such an edit until a NEW
2096
+ // session starts for one of these four defs specifically — a real, silent limitation, not a
2097
+ // theoretical one. This is the correct
2098
+ // trade against the alternative (re-injecting every turn would bake the overlay into that def's
2099
+ // own CLI-persisted session history again and again, compounding without bound) — do not change
2100
+ // this gating to "fix" the staleness. The actual fix is giving each of the four its own
2101
+ // `'append-flag'`-equivalent `systemPromptDelivery` (an argv flag or an env var, neither of which
2102
+ // is part of what a resumed session replays), which removes this limitation entirely for that
2103
+ // def. See `reasonix.ts`'s and `opencode.ts`'s module docs for the two already-identified,
2104
+ // not-yet-wired native mechanisms.
2105
+ const promptPrefix = `${overlay}\n\n---\n\n`;
2106
+ return { promptPrefix, prompt: `${promptPrefix}${prompt}`, extraArgs: [], envOverrides: {} };
2107
+ }
2108
+ /**
2109
+ * Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw.
2110
+ *
2111
+ * @param input.overlayDelivery - This run's already-resolved overlay decision, computed once in
2112
+ * `run()` rather than here. It is resolved upstream because `buildArgs` is only ONE of the four
2113
+ * channels that can carry the prompt: the staged prompt file is written *before* this function
2114
+ * runs, and stdin/ACP/pi-rpc send theirs *after* spawn. A decision made inside this function could
2115
+ * therefore only ever reach the 7 defs whose `buildArgs` reads its first argument at all — the
2116
+ * other 17 declare it `_prompt` and discard it, which is exactly how the overlay used to go missing.
2117
+ */
1902
2118
  export async function buildRunArgs(input, deps) {
1903
2119
  try {
1904
- return input.def.buildArgs(input.imageDelivery.prompt, [...(input.imagePaths ?? [])], input.imageDelivery.extraAllowedDirs === undefined ? undefined : [...input.imageDelivery.extraAllowedDirs], buildAgentBuildArgsOptions(input.runInput, input.systemPromptOverlay), input.runtimeContext);
2120
+ const delivery = input.overlayDelivery;
2121
+ const args = input.def.buildArgs(delivery.prompt, [...(input.imagePaths ?? [])], input.imageDelivery.extraAllowedDirs === undefined ? undefined : [...input.imageDelivery.extraAllowedDirs], buildAgentBuildArgsOptions(input.runInput, input.systemPromptOverlay), input.runtimeContext);
2122
+ // `'append-flag'` delivery's extra argv (empty for every other def/strategy) is appended after
2123
+ // whatever the def's own `buildArgs` returned — safe because it is only ever non-empty for a
2124
+ // `promptViaStdin` def with no trailing positional argv (`claude`/`pi` today; see
2125
+ // `resolveSystemPromptOverlayDelivery`'s doc for why a future 'append-flag' def must keep that
2126
+ // property too). `envOverrides` (non-empty only for `'env-var'` — `reasonix` today) is handed
2127
+ // back rather than applied here, since the spawn env isn't finalized until `computeChildEnv`
2128
+ // runs, later in `run()`.
2129
+ return { args: [...args, ...delivery.extraArgs], envOverrides: delivery.envOverrides };
1905
2130
  }
1906
2131
  catch (err) {
1907
2132
  await deps.releaseStagedResources();
@@ -1922,6 +2147,47 @@ export async function writeMcpJsonIfNeeded(input, deps) {
1922
2147
  return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}`);
1923
2148
  }
1924
2149
  }
2150
+ /**
2151
+ * Phase 10b1: `systemPromptDelivery: { strategy: 'config-instructions-file' }`'s one effect —
2152
+ * stages the computed overlay to a fresh, run-scoped temp file, so `computeChildEnv` has a real
2153
+ * path to merge into that def's `instructions` config array (see
2154
+ * {@link mergeEnvContentInstructions}'s own doc for the live verification this mechanism rests on).
2155
+ * `null` for every other strategy, an unset `systemPromptDelivery`, or no overlay present at all —
2156
+ * byte-identical to before this mechanism existed, matching {@link writeMcpJsonIfNeeded}'s and
2157
+ * {@link prepareCodexHomeIfNeeded}'s identical no-op-when-inapplicable gate.
2158
+ *
2159
+ * `opencode`'s `instructions` field only accepts a file path or a remote URL — confirmed live
2160
+ * (2026-09-01): a literal instruction string in the array is silently ignored (no error, just never
2161
+ * honored), so an inline-text shortcut is not available and this staging step is load-bearing, not
2162
+ * a defensive extra.
2163
+ * @param input.def - Only used for its `id`, in the failure message, and its `systemPromptDelivery` declaration.
2164
+ * @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result for this run.
2165
+ * @complexity O(1) plus one directory creation and one file write.
2166
+ * @overallScore 100/100
2167
+ */
2168
+ export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
2169
+ if (input.def.systemPromptDelivery?.strategy !== 'config-instructions-file' ||
2170
+ typeof input.overlay !== 'string' ||
2171
+ input.overlay.length === 0) {
2172
+ return null;
2173
+ }
2174
+ try {
2175
+ const safeRunId = input.runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
2176
+ const dir = await fsPromises.mkdtemp(join(tmpdir(), `jini-system-prompt-overlay-${safeRunId}-`));
2177
+ const filePath = join(dir, 'overlay.md');
2178
+ await fsPromises.writeFile(filePath, input.overlay, { encoding: 'utf8', mode: 0o600 });
2179
+ return {
2180
+ path: filePath,
2181
+ cleanup: async () => {
2182
+ await fsPromises.rm(dir, { recursive: true, force: true });
2183
+ },
2184
+ };
2185
+ }
2186
+ catch (err) {
2187
+ await deps.releaseStagedResources();
2188
+ return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}`);
2189
+ }
2190
+ }
1925
2191
  /**
1926
2192
  * Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
1927
2193
  * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
@@ -2125,13 +2391,46 @@ export function createAgentExecutor(options) {
2125
2391
  // defs' own native protocol already delivers the image and must never also get this
2126
2392
  // treatment (the double-delivery hazard this mechanism exists to avoid).
2127
2393
  const imageDelivery = await resolveImageDeliveryAndArgvBudget({ runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, { failBeforeSpawn });
2394
+ // Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
2395
+ // different channels carry a prompt in this driver, and they do not all run at the same point:
2396
+ // the `promptViaFile` staging below writes its file BEFORE `buildArgs`, `buildArgs` itself runs
2397
+ // mid-`run()`, and stdin/ACP/pi-rpc all send theirs AFTER spawn. Resolving the overlay once,
2398
+ // here, is what lets all four consume the same decision; resolving it later (as this used to,
2399
+ // inside `buildRunArgs`) could only ever reach `buildArgs`, so the 17 defs that discard that
2400
+ // argument, plus grok-build's staged file, silently received no overlay at all.
2401
+ //
2402
+ // `computeSystemPromptOverlay`'s third argument is the pre-staging `RuntimeContext`: it reads
2403
+ // only `hasPriorAssistantTurn`, which `computeRuntimeContext` never populates from any of the
2404
+ // staged-file/MCP inputs added to the fuller context built further down, so nothing between
2405
+ // here and there can change the overlay this returns. Passing the narrower context makes that
2406
+ // independence explicit rather than relying on the ordering staying lucky.
2407
+ //
2408
+ // `turnIndex` is a coarse 0/1 proxy (no exact turn counter exists on this driver) — sufficient
2409
+ // because every `PromptAugmenter.systemOverlay()` implementation this seam has today wants the
2410
+ // same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
2411
+ // numbering can track it itself and ignore this arg.
2412
+ const preStagingRuntimeContext = computeRuntimeContext(null, null, null, input.resumeSessionId, input.newSessionId);
2413
+ const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, preStagingRuntimeContext);
2414
+ const overlayDelivery = resolveSystemPromptOverlayDelivery({
2415
+ defId: def.id,
2416
+ systemPromptDelivery: def.systemPromptDelivery,
2417
+ resumesSessionViaCli: def.resumesSessionViaCli,
2418
+ resumesSessionViaAcpLoad: def.resumesSessionViaAcpLoad,
2419
+ overlay: systemPromptOverlay,
2420
+ prompt: imageDelivery.prompt,
2421
+ resumeSessionId: preStagingRuntimeContext?.resumeSessionId,
2422
+ });
2128
2423
  const resolvedEnv = resolveRunEnv(input, process.env);
2129
2424
  const launch = await resolveLaunch({ runId: input.runId, def, resolvedEnv }, { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn });
2130
2425
  const spawnEnv = applyAgentLaunchEnvFn({ ...resolvedEnv }, launch);
2131
2426
  // Stage a promptViaFile def's (grok-build) prompt to a temp file before buildArgs runs — its
2132
2427
  // buildArgs throws without runtimeContext.promptFilePath. A no-op (returns null) for every
2133
2428
  // def without promptViaFile: true (preparePromptFileForAgent's own guard).
2134
- const preparedPromptFile = await stagePromptFile({ runId: input.runId, def, prompt: imageDelivery.prompt }, { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn });
2429
+ //
2430
+ // `overlayDelivery.prompt`, not `imageDelivery.prompt`: for a `promptViaFile` def this file IS
2431
+ // the prompt transport — its `buildArgs` declares `_prompt` and passes only the path — so the
2432
+ // overlay has to be in the bytes written here or the CLI never sees it at all.
2433
+ const preparedPromptFile = await stagePromptFile({ runId: input.runId, def, prompt: overlayDelivery.prompt }, { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn });
2135
2434
  // Stage a needsAgentLogFile def's (antigravity) diagnostic-log path, on the same terms and at
2136
2435
  // the same point as the prompt file above: before buildArgs, since buildArgs is what turns the
2137
2436
  // path into a `--log-file <path>` argument. A no-op (returns null) for every def without
@@ -2159,6 +2458,14 @@ export function createAgentExecutor(options) {
2159
2458
  * Only the `'codex-toml'` mechanism stages a directory at all.
2160
2459
  */
2161
2460
  let preparedCodexHome = null;
2461
+ /**
2462
+ * Set once `prepareSystemPromptOverlayFileIfNeeded` has actually staged this run's overlay file
2463
+ * for a `'config-instructions-file'` def, so `cleanupStagedFiles` knows there is a temp
2464
+ * directory to remove. Cleared as it is consumed, matching `preparedCodexHome`'s identical
2465
+ * single-removal discipline. Only that one strategy stages a file this way — `null` for every
2466
+ * other def/strategy/no-overlay run.
2467
+ */
2468
+ let preparedSystemPromptOverlayFile = null;
2162
2469
  const cleanupStagedFiles = async () => {
2163
2470
  if (preparedPromptFile)
2164
2471
  await preparedPromptFile.cleanup();
@@ -2174,6 +2481,11 @@ export function createAgentExecutor(options) {
2174
2481
  preparedCodexHome = null;
2175
2482
  await codexHomeToRemove.cleanup();
2176
2483
  }
2484
+ if (preparedSystemPromptOverlayFile) {
2485
+ const overlayFileToRemove = preparedSystemPromptOverlayFile;
2486
+ preparedSystemPromptOverlayFile = null;
2487
+ await overlayFileToRemove.cleanup();
2488
+ }
2177
2489
  };
2178
2490
  // Resolve this run's MCP bridge delivery once, before buildArgs — the `'claude-mcp-json'`
2179
2491
  // variant's path has to be in `runtimeContext` for that def's own `--mcp-config` argv, and
@@ -2205,12 +2517,6 @@ export function createAgentExecutor(options) {
2205
2517
  releaseRuntimeLock();
2206
2518
  await cleanupStagedFiles();
2207
2519
  };
2208
- // Computed once per `run()`, not per-token/per-event: a system-prompt overlay is a spawn-time
2209
- // CLI arg, not something that varies mid-run. `turnIndex` is a coarse 0/1 proxy (no exact turn
2210
- // counter exists on this driver) — sufficient because every `PromptAugmenter.systemOverlay()`
2211
- // implementation this seam has today wants the same overlay on every turn, not a first-turn-only
2212
- // one; a caller that needs finer-grained turn numbering can track it itself and ignore this arg.
2213
- const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, runtimeContext);
2214
2520
  // Guarded, like every other step between staging and spawn: a `runtimeLock` def's `buildArgs` is
2215
2521
  // guarded precisely *because* it performs real filesystem writes (antigravity writes its model
2216
2522
  // choice into a shared settings file), so EACCES on a read-only home, ENOSPC, or a malformed
@@ -2218,7 +2524,7 @@ export function createAgentExecutor(options) {
2218
2524
  // `Error` — breaking this driver's "never a bare throw, always an `AgentExecutorError`" contract
2219
2525
  // — and left the run `'running'` forever while still holding the process-global mutex and both
2220
2526
  // staged files, so no later run of that def could ever acquire the lock either.
2221
- const args = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
2527
+ const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
2222
2528
  // Mechanism 1 of 5's one effect — stage this run's own MCP config file (run-scoped, see
2223
2529
  // `mcpJsonPathForRun`) before spawn so the `--mcp-config <path>` argv buildArgs just produced
2224
2530
  // points at a real file. Skipped entirely for the other four mechanisms and whenever no bridge
@@ -2231,12 +2537,21 @@ export function createAgentExecutor(options) {
2231
2537
  // so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
2232
2538
  // constraint of its own; it is placed here only to keep the two staging steps adjacent.
2233
2539
  preparedCodexHome = await prepareCodexHomeIfNeeded({ runId: input.runId, def, mcpBridge }, { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn });
2540
+ // `'config-instructions-file'`'s one effect — stage the overlay to a temp file so
2541
+ // `computeChildEnv` below has a real path to merge into that def's `instructions` array. A
2542
+ // no-op (`null`) for every other def/strategy or a run with no overlay at all. Independent of
2543
+ // `mcpBridge`/`preparedCodexHome` above (a different strategy field entirely), so placed here
2544
+ // only to stay adjacent to the other pre-`computeChildEnv` staging steps, not for any ordering
2545
+ // requirement between them.
2546
+ preparedSystemPromptOverlayFile = await prepareSystemPromptOverlayFileIfNeeded({ runId: input.runId, def, overlay: systemPromptOverlay }, { releaseStagedResources, failBeforeSpawn });
2234
2547
  // Computed only now, not right after `mcpBridge` resolution: mechanism 5's directory path is
2235
2548
  // not known until the staging step directly above actually runs `mkdtemp` (see
2236
2549
  // `McpBridgeDelivery`'s `'codex-toml'` variant doc for why it cannot be pre-computed the way
2237
2550
  // `'claude-mcp-json'`'s deterministic path is). Nothing between the old, earlier call site and
2238
2551
  // here ever read `childEnv`, so moving the call cost nothing.
2239
- const childEnv = computeChildEnv(spawnEnv, mcpBridge, preparedCodexHome?.path);
2552
+ const childEnv = computeChildEnv(spawnEnv, mcpBridge, preparedCodexHome?.path, systemPromptEnvOverrides, preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
2553
+ ? { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path }
2554
+ : undefined);
2240
2555
  // Post-buildArgs guard for argv-bound defs whose resolved binary is a
2241
2556
  // Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
2242
2557
  // budget can still expand past CreateProcess's command-line cap once
@@ -2289,7 +2604,14 @@ export function createAgentExecutor(options) {
2289
2604
  runId: input.runId,
2290
2605
  agentId: def.id,
2291
2606
  child,
2292
- prompt: input.prompt,
2607
+ // `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
2608
+ // this call site deliberately sends the raw input prompt rather than the image-delivery
2609
+ // rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
2610
+ // images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
2611
+ // is the part of the overlay decision that applies to whatever prompt text a transport was
2612
+ // already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
2613
+ // sending byte-identical text and never receive the overlay twice.
2614
+ prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2293
2615
  cwd: input.cwd,
2294
2616
  model: input.model,
2295
2617
  imagePaths: input.imagePaths ?? [],
@@ -2316,7 +2638,10 @@ export function createAgentExecutor(options) {
2316
2638
  runId: input.runId,
2317
2639
  agentId: def.id,
2318
2640
  child,
2319
- prompt: input.prompt,
2641
+ // Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
2642
+ // so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
2643
+ // rides `--append-system-prompt` instead, exactly once.
2644
+ prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2320
2645
  cwd: input.cwd,
2321
2646
  model: input.model,
2322
2647
  imagePaths: input.imagePaths ?? [],
@@ -2336,7 +2661,17 @@ export function createAgentExecutor(options) {
2336
2661
  });
2337
2662
  return;
2338
2663
  }
2339
- writePromptToStdin(def, child, imageDelivery.prompt, stdinHandle);
2664
+ // The overlay reaches stdin only for a def that declares stdin as its prompt transport. The
2665
+ // four `plain`-format defs that do not (`aider`/`antigravity`/`deepseek` put the prompt in
2666
+ // argv, `grok-build` in a staged file) are still written to and closed here exactly as before,
2667
+ // because this driver always spawns with `stdio: ['pipe','pipe','pipe']` and their CLIs need
2668
+ // the EOF — but their prompt already carried the overlay through argv or the staged file, so
2669
+ // sending the overlaid text here too would deliver it twice. This is the one place the
2670
+ // resolver's own `''`-prefix rule is not sufficient on its own: those four defs are on the
2671
+ // fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
2672
+ // channel for them is the def's declared transport, not the strategy.
2673
+ const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
2674
+ writePromptToStdin(def, child, stdinPrompt, stdinHandle);
2340
2675
  }
2341
2676
  return { run };
2342
2677
  }