agy-context-saver 1.3.0 → 1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to `Agy-Context-Saver` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.0] - 2026-10-08
9
+
10
+ ### Added (Output Inspection Governance & Forensic Dereferencing - LH-12)
11
+ - **Folded Surgical Step Dereferencing in `query_transcript` (NFF-05)**: Added optional `stepIndex` (number) and `field` (string) parameters to `query_transcript`, allowing surgical single-step extraction from `transcript_full.jsonl` without creating redundant tools. Recovers un-truncated subagent messages and arguments with zero surrounding context bloat.
12
+ - **Safe-Harbor `rtk read` for Step Output Spillovers (NFF-01 & NFF-02)**: Provided a safe harbor in `execution-guard-hook.mjs` allowing `rtk read` on runtime step output spillovers (`.system_generated/steps/<step>/output.txt`). Native `view_file` and shell `cat` invocations are automatically routed to `rtk read <path>`, leveraging `rtk read`'s built-in line clamping, head/tail windowing, and token truncation.
13
+ - **Lifecycle-Gated Task Output Inspection (NFF-03)**: Added lifecycle governance for background task logs (`tasks/<taskId>.log`) in `execution-guard-hook.mjs`. Invocations are strictly **denied** under Proportional Backoff while tasks are `RUNNING` to stop polling loops, but allowed via `rtk read` once `COMPLETED` / `TERMINATED` for bounded stack trace and failure inspection.
14
+ - **Pointer-Over-Wire (POW) Subagent Contract (NFF-06)**: Integrated the POW protocol into `subagent_brief`. Research findings $>1\text{ KB}$ are written to workspace artifacts (`scratch/...`), while `send_message` transmits only executive summaries and clickable file links, preventing message bus truncations.
15
+ - **Output Governance Test Suite**: Added `tests/test-output-governance.mjs` verifying spillover safe-harbor access, task output lifecycle gating, forensic step extraction, and hook routing.
16
+
17
+ ### Changed
18
+ - **Streamlined Tool Architecture (0 New Tools)**: Eliminated tool proliferation by consolidating capabilities into existing tools and hooks, maintaining strictly the 6 core canonical MCP tools (zero prompt token bloat, zero tool-selection ambiguity).
19
+ - **Typed Argument Descriptors (NFF-04)**: Replaced blind `JSON.stringify().slice(0, 80)` in `formatTranscriptItem()` with typed argument descriptors (`[14.2 KB String]`, `[Array(5)]`, `{Object(3 keys)}`), preserving JSON payload integrity and transcript legibility.
20
+
21
+ ---
22
+
23
+ ## [1.3.2] - 2026-10-07
24
+
25
+ ### Governance & Command Semantics (LH-11 Hardening)
26
+ - **Opt-In Fail-Fast Semantics**: Reverted forced `-x -q` parameter injection on bare test runner invocations to preserve native command semantics. Fail-fast (`-x`, `--maxfail=1`) remains strictly opt-in, preventing the "whack-a-mole" repair loop and preserving complete failure blast radius visibility across multi-module test suites.
27
+ - **Architectural Separation**: Replaced forced command mutation with the **Coordinated Watchdog Protocol** and **Mandatory Pre-Yield Status Cards** in `rules/AGENTS.md`, ensuring visibility during multi-minute full-repository sweeps without distorting test results.
28
+ - **Loophole Ledger Synchronization**: Updated `LEDGER.md` (LH-11) documenting the trade-offs of fail-fast injection vs native command semantics.
29
+
30
+ ---
31
+
32
+ ## [1.3.1] - 2026-10-07
33
+
34
+ ### Governance & Asynchronous Execution Observability (LH-11)
35
+ - **Fail-Fast Auto-Injection on Bare Test Sweeps**: `execution-guard-hook.mjs` transparently normalizes bare `pytest` and `python -m pytest` invocations to include `-x -q` (fail-fast, quiet mode). In large codebases (e.g. 900+ tests), this prevents multi-minute unconstrained sweeps by stopping execution immediately at the first failure.
36
+ - **Coordinated Watchdog Protocol Codification**: Updated `rules/AGENTS.md` to explicitly train agents on the Coordinated Watchdog Protocol: when launching heavy background operations ($> 15\text{s}$), agents must schedule a coordinated watchdog timer (`schedule(DurationSeconds=45, ...)`) to prevent silent yielding into unmonitored execution black holes.
37
+ - **Mandatory Pre-Yield Status Cards**: Codified in `rules/AGENTS.md` that agents launching background tasks must output a structured markdown status card (command, scope, estimated duration, watchdog tier) before yielding their turn, eliminating user uncertainty during prolonged runs.
38
+ - **Loophole Ledger LH-11 Addition**: Formally documented LH-11 (Silent Background Deadlock & Bare Sweep Exhaustion) from Incident `07b0f8d8` in `LEDGER.md`.
39
+
40
+ ---
41
+
8
42
  ## [1.3.0] - 2026-10-07
9
43
 
10
44
  ### Governance & Loophole Elimination
package/README.md CHANGED
@@ -103,6 +103,33 @@ npm run status
103
103
 
104
104
  ---
105
105
 
106
+ ## Comprehensive Documentation
107
+
108
+ For complete technical references, architectural specifications, and governance rules, visit our **[Zensical Documentation Site](https://paragon-ux.github.io/agy-context-saver/)**:
109
+
110
+ - 📖 **[Getting Started & Installation](docs/installation.md)**: Zero-delay setup, native plugin linking, hooks registration, and verification matrix.
111
+ - 🏗️ **[Architecture & Internals](docs/architecture.md)**: Closed Execution Topology, RTK separation of concerns, and the Four Non-Fragile Architectural Pillars.
112
+ - 🛡️ **[Lifecycle Governance & Hooks](docs/governance.md)**: Proportional Backoff curve, Reactive Wakeup invariants, and the 5-strike circuit breaker.
113
+ - 🧰 **[MCP Tools & CLI Reference](docs/tools.md)**: Complete specifications for all 6 canonical autonomous MCP tools, slash commands, and diagnostic utilities.
114
+ - 🔬 **[Forensic & Output Inspection](LEDGER_OUTPUT_FIXES.md)**: Ephemeral tool spillover resolution, post-mortem task retrieval, and Pointer-Over-Wire (POW) contracts.
115
+ - 🚀 **[Publishing & Verification Playbook](docs/publishing.md)**: Clean-room test runs, release procedures, and multi-suite verification matrix.
116
+
117
+ ---
118
+
119
+ ## Local Documentation Server
120
+
121
+ Build or serve the documentation locally using **Zensical**:
122
+
123
+ ```bash
124
+ # Build static documentation site
125
+ py -3.11 -m zensical build
126
+
127
+ # Serve live preview server
128
+ py -3.11 -m zensical serve
129
+ ```
130
+
131
+ ---
132
+
106
133
  ## Zero-Guesswork Testing & Verification Playbook
107
134
 
108
135
  To cleanly test every layer of the framework without guesswork or interactive stalls:
@@ -160,12 +187,16 @@ node tests/test-lifecycle-rollback.mjs
160
187
  - `/mcp:agy-context-saver:context_shield`: Injects 3-layer governance rules and RTK standards into any session with one click. *(MCP Prompts appear in your chat autocomplete menu; MCP Tools are called autonomously by the agent).*
161
188
  * **Agent MCP Tools**:
162
189
  1. `check_context_health`: Diagnoses conversation transcript turn budgets, payload size, RTK status, and polling loops.
163
- 2. `subagent_brief`: Formulates scope-isolated prompts for delegated subagents.
190
+ 2. `subagent_brief`: Formulates scope-isolated prompts for delegated subagents with Pointer-Over-Wire (POW) delivery contract.
164
191
  3. `read_transcript`: Reads recent conversation history in clean Markdown format (compact/full).
165
- 4. `query_transcript`: Forensic filtering and regex search engine for conversation logs.
192
+ 4. `query_transcript`: Forensic filtering, regex search engine, and surgical single-step dereferencing (`stepIndex`, `field`) from full transcripts.
166
193
  5. `get_installation_status`: Inspects live Antigravity plugin link, hook, MCP server, tool schemas, and RTK binary.
167
194
  6. `sync_installation`: Re-verifies and repairs all integration layers in ~25ms.
168
195
 
196
+ * **Streamlined Output Inspection (0 New Tools)**:
197
+ - **Runtime Step Spillovers** (`.system_generated/steps/<step>/output.txt`): Inspected via canonical `rtk read <path>` (safe-harbored in the PreToolUse hook; native `view_file` calls automatically route to `rtk read`).
198
+ - **Completed Background Task Logs**: Inspected via `rtk read tasks/<taskId>.log` (lifecycle-gated in the PreToolUse hook; strictly blocked under Proportional Backoff while running, allowed once completed).
199
+
169
200
  ---
170
201
 
171
202
  ## License
package/mcp/index.js CHANGED
@@ -98,7 +98,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
98
98
  const rulesPath = path.resolve(__dirname, "../rules/AGENTS.md");
99
99
 
100
100
  const SERVER_NAME = "agy-context-saver";
101
- const SERVER_VERSION = "1.3.0";
101
+ const SERVER_VERSION = "1.4.0";
102
102
 
103
103
  // --- Tools Specification ---
104
104
  const TOOLS = [
@@ -243,6 +243,14 @@ const TOOLS = [
243
243
  summaryOnly: {
244
244
  type: "boolean",
245
245
  description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
246
+ },
247
+ stepIndex: {
248
+ type: "number",
249
+ description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
250
+ },
251
+ field: {
252
+ type: "string",
253
+ description: "Optional specific field to extract when stepIndex is provided (e.g. 'content', 'thinking', 'tool_calls', 'tool_args', or 'tool_args:Message'). Defaults to full step."
246
254
  }
247
255
  },
248
256
  required: ["conversationId"]
@@ -379,12 +387,17 @@ function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable =
379
387
  `ROLE: Scope-Isolated Deep Research Subagent`,
380
388
  `OBJECTIVE: ${objective}${scopeStr}`,
381
389
  ``,
382
- `CRITICAL CONTEXT GOVERNANCE INSTRUCTIONS:`,
383
- `1. You are running in an isolated subagent sandbox. Do all necessary multi-file exploration, searches, and analysis here.`,
384
- `2. DO NOT dump raw file contents or massive grep outputs in your final response.`,
385
- `3. Deliver strictly the high-signal findings in the following format:`,
386
- ` ${expectedDeliverable}`,
387
- `4. Keep your final response concise, structured, and cite exact file:line references.`
390
+ `CRITICAL CONTEXT GOVERNANCE INSTRUCTIONS (POINTER-OVER-WIRE):`,
391
+ `1. Isolated Workspace: Do all necessary multi-file exploration, searches, and analysis in this subagent sandbox.`,
392
+ `2. Pointer-Over-Wire (POW) Protocol: In-band messaging channels (send_message) have strict transport payload limits (~16-20 KB). If your findings or deliverable exceed 1 KB:`,
393
+ ` a. Write the full comprehensive deliverable or raw output to a file in the workspace (e.g., 'scratch/<task>_findings.md' or an artifact).`,
394
+ ` b. In 'send_message', transmit ONLY:`,
395
+ ` - Executive Summary (3-5 high-signal sentences)`,
396
+ ` - Verdict / Scoreboard Table`,
397
+ ` - Clickable File Link: [Full Deliverable](file:///absolute/path/to/findings.md)`,
398
+ `3. Fast Turn Termination: Once 'send_message' is dispatched, stop calling tools and conclude your turn immediately so the parent agent wakes up without latency.`,
399
+ `4. Format: ${expectedDeliverable}`,
400
+ `5. Citation Mandate: Keep your final response concise, structured, and cite exact file:line references.`
388
401
  ].join("\n");
389
402
 
390
403
  return { content: [{ type: "text", text: prompt }] };
@@ -393,12 +406,13 @@ function handleSubagentBrief({ objective, scopeFiles = [], expectedDeliverable =
393
406
  function handleGetInstallationStatus() {
394
407
  const existing = detectExistingInstallation();
395
408
  const rtkInfo = findExistingRtk();
409
+ const schemaText = existing.details.schemas.exists ? "ALL 6 SCHEMAS PRESENT" : "MISSING";
396
410
  const report = [
397
411
  `### Agy-Context-Saver Installation Status: ${existing.isComplete ? "HEALTHY & ACTIVE 🛡️" : existing.isInstalled ? "PARTIAL INSTALLATION ⚠️" : "NOT INSTALLED ❌"}`,
398
412
  `- Native Plugin Link: ${existing.details.plugin.exists ? `ACTIVE (${existing.details.plugin.target})` : "NOT LINKED"}`,
399
413
  `- Governor Lifecycle Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"} (Script: ${existing.details.hook.scriptExists ? "Present" : "Missing"})`,
400
414
  `- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`,
401
- `- Antigravity Tool Schemas: ${existing.details.schemas.exists ? "ALL 6 SCHEMAS PRESENT" : "MISSING"} (${existing.details.schemas.dir})`,
415
+ `- Antigravity Tool Schemas: ${schemaText} (${existing.details.schemas.dir})`,
402
416
  `- RTK Binary: ${rtkInfo ? `INSTALLED (${rtkInfo.version})` : "MISSING"}`,
403
417
  `- RTK Hook Optimization: ${existing.details.hook.registered ? "ACTIVE (via PreToolUse)" : "INACTIVE"}`,
404
418
  "",
@@ -555,6 +569,28 @@ function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
555
569
  return text;
556
570
  }
557
571
 
572
+ function formatArgumentDescriptor(k, v) {
573
+ if (typeof v === "string") {
574
+ if (v.length <= 80 && !v.includes("\n")) {
575
+ return `${k}=${JSON.stringify(v)}`;
576
+ }
577
+ const preview = v.replace(/[\r\n\t]+/g, " ").slice(0, 50).trim();
578
+ const sizeKb = (Buffer.byteLength(v, "utf8") / 1024).toFixed(1);
579
+ return `${k}="${preview}..." [${sizeKb} KB]`;
580
+ }
581
+ if (typeof v === "number" || typeof v === "boolean" || v === null) {
582
+ return `${k}=${v}`;
583
+ }
584
+ if (Array.isArray(v)) {
585
+ return `${k}=[Array(${v.length})]`;
586
+ }
587
+ if (typeof v === "object") {
588
+ const keys = Object.keys(v);
589
+ return `${k}={Object(${keys.length} keys)}`;
590
+ }
591
+ return `${k}=${String(v).slice(0, 60)}`;
592
+ }
593
+
558
594
  function formatTranscriptItem(item, options = {}) {
559
595
  const { includeThinking = false, includeToolCalls = false, maxContentChars = MAX_TURN_CHARS } = options;
560
596
  const stepIdx = item.step_index !== undefined ? item.step_index : "?";
@@ -583,7 +619,7 @@ function formatTranscriptItem(item, options = {}) {
583
619
  const name = tc.name || tc.tool_name || "unknown_tool";
584
620
  const args = tc.args || tc.arguments || {};
585
621
  const compactArgs = Object.entries(args)
586
- .map(([k, v]) => `${k}=${JSON.stringify(v).slice(0, 80)}`)
622
+ .map(([k, v]) => formatArgumentDescriptor(k, v))
587
623
  .join(", ");
588
624
  return `- \`${name}(${compactArgs})\``;
589
625
  });
@@ -745,9 +781,106 @@ async function handleQueryTranscript(args = {}) {
745
781
  includeThinking = false,
746
782
  includeToolCalls = false,
747
783
  maxResults = 5,
748
- summaryOnly = false
784
+ summaryOnly = false,
785
+ stepIndex,
786
+ field
749
787
  } = args;
750
788
 
789
+ // Surgical single-step dereferencing (folded from get_step_detail)
790
+ if (stepIndex !== undefined && stepIndex !== null) {
791
+ const numIndex = Number(stepIndex);
792
+ if (isNaN(numIndex)) {
793
+ return {
794
+ isError: true,
795
+ content: [{ type: "text", text: `Error: 'stepIndex' must be a number (received: ${stepIndex}).` }]
796
+ };
797
+ }
798
+
799
+ const resolvedFull = resolveTranscriptPath(conversationId, "full");
800
+ let step = null;
801
+ if (resolvedFull && resolvedFull.filePath && fs.existsSync(resolvedFull.filePath)) {
802
+ step = await findFullStep(resolvedFull.filePath, numIndex);
803
+ }
804
+
805
+ if (!step) {
806
+ const resolvedCompact = resolveTranscriptPath(conversationId, "compact");
807
+ if (resolvedCompact && resolvedCompact.filePath && fs.existsSync(resolvedCompact.filePath)) {
808
+ step = await findFullStep(resolvedCompact.filePath, numIndex);
809
+ }
810
+ }
811
+
812
+ if (!step) {
813
+ return {
814
+ isError: true,
815
+ content: [{ type: "text", text: `Step index ${numIndex} not found in transcript.` }]
816
+ };
817
+ }
818
+
819
+ const MAX_FIELD_CHARS = 24000;
820
+
821
+ if (field) {
822
+ const f = String(field).trim();
823
+ if (f === "content") {
824
+ const val = step.content || "(No content in step)";
825
+ return {
826
+ content: [{ type: "text", text: `### Step ${numIndex} Content\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
827
+ };
828
+ }
829
+ if (f === "thinking") {
830
+ const val = step.thinking || "(No thinking block in step)";
831
+ return {
832
+ content: [{ type: "text", text: `### Step ${numIndex} Thinking\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
833
+ };
834
+ }
835
+ if (f === "tool_calls") {
836
+ const val = JSON.stringify(step.tool_calls || [], null, 2);
837
+ return {
838
+ content: [{ type: "text", text: `### Step ${numIndex} Tool Calls\n\`\`\`json\n${val.slice(0, MAX_FIELD_CHARS)}\n\`\`\`` }]
839
+ };
840
+ }
841
+ if (f === "tool_args" || f.startsWith("tool_args:")) {
842
+ const argName = f.includes(":") ? f.split(":")[1].trim() : null;
843
+ const tc = Array.isArray(step.tool_calls) && step.tool_calls.length > 0 ? step.tool_calls[0] : null;
844
+ if (!tc) {
845
+ return {
846
+ content: [{ type: "text", text: `Step ${numIndex} contains no tool calls.` }]
847
+ };
848
+ }
849
+ const tcArgs = tc.args || tc.arguments || {};
850
+ if (argName) {
851
+ const argVal = tcArgs[argName];
852
+ const displayVal = typeof argVal === "string" ? argVal : JSON.stringify(argVal, null, 2);
853
+ return {
854
+ content: [
855
+ {
856
+ type: "text",
857
+ text: `### Step ${numIndex} Argument: \`${argName}\`\n\n${String(displayVal).slice(0, MAX_FIELD_CHARS)}`
858
+ }
859
+ ]
860
+ };
861
+ }
862
+ return {
863
+ content: [
864
+ {
865
+ type: "text",
866
+ text: `### Step ${numIndex} Tool Arguments\n\`\`\`json\n${JSON.stringify(tcArgs, null, 2).slice(0, MAX_FIELD_CHARS)}\n\`\`\``
867
+ }
868
+ ]
869
+ };
870
+ }
871
+ }
872
+
873
+ const formatted = formatTranscriptItem(step, {
874
+ includeThinking: true,
875
+ includeToolCalls: true,
876
+ maxContentChars: MAX_FIELD_CHARS
877
+ });
878
+
879
+ return {
880
+ content: [{ type: "text", text: formatted }]
881
+ };
882
+ }
883
+
751
884
  const resolved = resolveTranscriptPath(conversationId, mode === "full" ? "full" : "compact");
752
885
  if (resolved.error) {
753
886
  return { isError: true, content: [{ type: "text", text: resolved.error }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-context-saver",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Universal Model Context Protocol (MCP) server & lifecycle governor purpose-built for Google Antigravity",
5
5
  "main": "mcp/index.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "setup": "node scripts/install-register.mjs",
13
13
  "uninstall": "node scripts/install-register.mjs --uninstall",
14
14
  "status": "node scripts/install-register.mjs --status",
15
- "test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
15
+ "test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-output-governance.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
16
16
  },
17
17
  "keywords": [
18
18
  "antigravity",
package/rules/AGENTS.md CHANGED
@@ -6,11 +6,27 @@
6
6
 
7
7
  ---
8
8
 
9
- ## Layer 1: Background Tasks & Polling Ban (Reactive Wakeup SSOT)
10
- - **Zero-Tolerance on Task Polling**: Do NOT call `manage_task(Action='status')` or `schedule` timers in a loop to wait for running background tasks. Background tasks execute asynchronously and automatically resume the agent via **Reactive Wakeup** (`<SYSTEM_MESSAGE>`).
11
- - **Yield Turn Immediately**: When a process moves to the background, stop calling tools, output a concise status message, and yield the turn.
12
- - **No Keyword or Timer Exemptions**: Words like "debug", "timeout", "diagnose", or "stuck" do NOT exempt an agent from lifecycle governance. Timers with `duration >= 120` are NOT exempt.
13
- - **Circuit Breaker**: Repeated polling calls across any tasks in a session trigger an escalating circuit breaker. At 5 cumulative denials, autonomous execution is frozen via `force_ask`.
9
+ ## Layer 1: Background Tasks & Proportional Backoff Protocol (Reactive Wakeup SSOT)
10
+ - **Zero-Tolerance on Busy-Polling**: Do NOT call `manage_task(Action='status')` in tight loops or schedule rapid timers to wait for running background tasks. Background tasks execute asynchronously and automatically resume the agent via **Reactive Wakeup** (`<SYSTEM_MESSAGE>`).
11
+ - **Proportional Backoff Protocol**: Diagnostic inspections scale along an exponential backoff curve:
12
+ $\text{Cooldown} = \min(600, \text{round}(30 \times 2.5^{\text{pollCount} - 1})) \quad [0\text{s} \to 30\text{s} \to 75\text{s} \to 188\text{s} \to 469\text{s} \to 600\text{s}]$
13
+ - Check #1 (Immediate upon launch): Permitted ($0\text{s}$).
14
+ - Premature status checks within an active backoff window are denied. 5 consecutive premature checks trigger the `force_ask` circuit breaker.
15
+ - Silent/hung background tasks: Once the backoff window elapses, a single diagnostic check is permitted to inspect logs, detect deadlocks, and issue `manage_task(Action='kill')` if needed.
16
+ - **Coordinated Watchdog Protocol (`schedule`)**: For long-running operations (builds, migrations, test suites) expected to take $> 15\text{s}$:
17
+ - Schedule a coordinated watchdog timer: `schedule(DurationSeconds=45, Prompt="Check status of background task-X", TimerCondition="task-X")`.
18
+ - Watchdog timers must satisfy `DurationSeconds >= Math.max(30, requiredBackoffSec)`. Short timers are denied.
19
+ - If the task completes early, Reactive Wakeup auto-cancels the timer and resumes the agent immediately.
20
+ - If the task is still running when the watchdog fires, perform a legal diagnostic check (`manage_task(status)`) and post a live progress update in chat for the user.
21
+ - **Mandatory Pre-Yield Status Card**: When a command moves to the background, NEVER yield with a silent or generic one-liner. Output a structured Markdown card so the user is never left wondering if the IDE has stalled:
22
+ ```markdown
23
+ ### ⏳ Background Execution Started
24
+ - **Command**: `<cmd>`
25
+ - **Scope / Target**: `<scope>`
26
+ - **Estimated Duration**: `~X minutes`
27
+ - **Watchdog Active**: Coordinated check scheduled in 45s via Reactive Wakeup.
28
+ ```
29
+ - **Kill & Stdin Immediate Exemption**: `manage_task(Action='kill')` and `manage_task(Action='send_input')` are ALWAYS permitted immediately ($0\text{s}$ backoff, zero penalties).
14
30
 
15
31
  ---
16
32
 
@@ -34,7 +50,11 @@
34
50
 
35
51
  ## Layer 3: Synchronous Execution & Subagent Delegation
36
52
  - **Maximum Synchronous Window**: Non-daemon `run_command` calls are automatically set to `WaitMsBeforeAsync: 10000` to complete synchronously and prevent unnecessary background detachment.
37
- - **Targeted Test Execution**: Avoid massive multi-minute test sweeps in interactive turns. Run targeted, quiet, fail-fast commands (e.g. `pytest tests/test_core.py -q -x`).
53
+ - **Targeted Test Execution & Opt-In Fail-Fast (LH-11)**:
54
+ - **Targeted Invocations**: In interactive turns, prefer targeting specific test files or directories (e.g. `pytest tests/test_core.py`) rather than running full multi-minute repository sweeps.
55
+ - **Fail-Fast Flags are Opt-In**: Fail-fast flags (`-x`, `--maxfail=1`) are opt-in. Use them when quickly debugging a localized failure, but omit them when assessing the full blast radius across the codebase to avoid the "whack-a-mole" loop.
56
+ - **Coordinated Watchdog Mandatory for Full Sweeps**: When running full multi-minute test sweeps without `-x`, agents **MUST** pair execution with the **Coordinated Watchdog Protocol** (`schedule(DurationSeconds=45, ...)`) and output a **Pre-Yield Status Card**. Never yield silently.
57
+ - **Subagent Delegation for Deep Audits**: When a comprehensive repository-wide test audit is required, delegate the sweep to a subagent (`invoke_subagent`) to keep the main thread responsive.
38
58
  - **Subagent Delegation**: Delegate heavy multi-file exploration and exploratory research to subagents (`invoke_subagent`). Subagents absorb intermediate steps and return high-signal summaries.
39
59
  - **Benchmark / Evaluator Integrity**: Never mutate benchmark definitions, prompt templates, or scoring artifacts during an active evaluation without explicit user confirmation.
40
60
 
@@ -184,6 +184,168 @@ function isProtectedBenchmarkPath(targetPath) {
184
184
  );
185
185
  }
186
186
 
187
+ /**
188
+ * Checks if target path references runtime step output spillover file.
189
+ */
190
+ function isStepSpilloverPath(targetPath) {
191
+ if (!targetPath || typeof targetPath !== "string") return false;
192
+ const normalized = targetPath.replace(/\\/g, "/");
193
+ return (
194
+ /\.system_generated[/\\]steps[/\\]\d+[/\\]output\.txt/i.test(normalized) ||
195
+ (normalized.includes("/.system_generated/steps/") && normalized.endsWith("output.txt"))
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Extracts task ID from target path or command if targeting a task output log.
201
+ */
202
+ function getTaskLogMatch(targetPath) {
203
+ if (!targetPath || typeof targetPath !== "string") return null;
204
+ const normalized = targetPath.replace(/\\/g, "/");
205
+ const match = normalized.match(/(?:^|[\s"'/])(?:\.system_generated\/)?tasks\/(?:task-)?([a-zA-Z0-9_-]+)\.log/i);
206
+ return match ? match[1] : null;
207
+ }
208
+
209
+ /**
210
+ * Resolves full path to a task log on disk.
211
+ */
212
+ function resolveTaskLogFile(taskId, convId) {
213
+ if (taskId && typeof taskId === "string") {
214
+ if (fs.existsSync(taskId)) {
215
+ const cleanId = String(taskId).replace(/^.*[\/\\]/, "").replace(/\.log$/i, "");
216
+ return { logPath: taskId, convId: convId || "global", cleanId };
217
+ }
218
+ if (taskId.includes("/") || taskId.includes("\\")) {
219
+ const cleanId = String(taskId).replace(/^.*[\/\\]/, "").replace(/\.log$/i, "");
220
+ return { logPath: null, convId: convId || "global", cleanId };
221
+ }
222
+ }
223
+
224
+ const cleanId = String(taskId).replace(/^.*[\/\\]/, "").replace(/\.log$/i, "");
225
+ const baseTaskName = cleanId.startsWith("task-") ? cleanId : `task-${cleanId}`;
226
+
227
+ const home = os.homedir();
228
+ const brainDir = path.join(home, ".gemini", "antigravity", "brain");
229
+
230
+ if (convId && convId !== "global") {
231
+ const candidate = path.join(brainDir, convId, ".system_generated", "tasks", `${baseTaskName}.log`);
232
+ if (fs.existsSync(candidate)) {
233
+ return { logPath: candidate, convId, cleanId: baseTaskName };
234
+ }
235
+ }
236
+
237
+ return { logPath: null, convId: convId || "global", cleanId: baseTaskName };
238
+ }
239
+
240
+ /**
241
+ * Determines whether a background task is verified completed / terminated.
242
+ */
243
+ function isTaskCompleted(taskId, convId, rawPath) {
244
+ const candidatePath = (rawPath && fs.existsSync(rawPath)) ? rawPath : null;
245
+ const resolved = resolveTaskLogFile(candidatePath || taskId, convId);
246
+ const logPath = resolved.logPath || candidatePath;
247
+ const cleanId = resolved.cleanId;
248
+
249
+ // 1. Session state check
250
+ const safeId = String(resolved.convId || convId || "global").replace(/[^a-zA-Z0-9_-]/g, "_");
251
+ const sessionPath = path.join(os.tmpdir(), `agy-session-${safeId}.json`);
252
+ if (fs.existsSync(sessionPath)) {
253
+ try {
254
+ const sess = JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
255
+ if (sess && sess.tasks && sess.tasks[cleanId]) {
256
+ if (sess.tasks[cleanId].state === "TERMINATED" || sess.tasks[cleanId].state === "COMPLETED") return true;
257
+ if (sess.tasks[cleanId].state === "RUNNING") return false;
258
+ }
259
+ } catch {}
260
+ }
261
+
262
+ // 2. Transcript check
263
+ if (resolved.convId && resolved.convId !== "global") {
264
+ const home = os.homedir();
265
+ const candidateTrans = path.join(home, ".gemini", "antigravity", "brain", resolved.convId, ".system_generated", "logs", "transcript.jsonl");
266
+ if (fs.existsSync(candidateTrans)) {
267
+ try {
268
+ const content = fs.readFileSync(candidateTrans, "utf-8");
269
+ const taskRegex = new RegExp(`Task id [^"]*${cleanId}[^"]* finished`, "i");
270
+ if (taskRegex.test(content)) return true;
271
+ } catch {}
272
+ }
273
+ }
274
+
275
+ // 3. File exists on disk and age check (>15s and not marked live)
276
+ if (logPath && fs.existsSync(logPath)) {
277
+ if (cleanId.includes("live") || cleanId.includes("running")) {
278
+ return false;
279
+ }
280
+ try {
281
+ const stats = fs.statSync(logPath);
282
+ const mtimeAgeMs = Date.now() - stats.mtimeMs;
283
+ if (mtimeAgeMs > 15000) {
284
+ return true;
285
+ }
286
+ } catch {}
287
+ }
288
+
289
+ return false;
290
+ }
291
+
292
+ /**
293
+ * Determines whether a background task is actively running.
294
+ */
295
+ function isTaskRunning(taskId, convId, rawPath) {
296
+ const candidatePath = (rawPath && fs.existsSync(rawPath)) ? rawPath : null;
297
+ const { logPath: resolvedPath, convId: resolvedConvId, cleanId } = resolveTaskLogFile(candidatePath || taskId, convId);
298
+ const logPath = resolvedPath || candidatePath;
299
+
300
+ // 1. Session state ledger check
301
+ const safeId = String(resolvedConvId || convId || "global").replace(/[^a-zA-Z0-9_-]/g, "_");
302
+ const sessionPath = path.join(os.tmpdir(), `agy-session-${safeId}.json`);
303
+ if (fs.existsSync(sessionPath)) {
304
+ try {
305
+ const sess = JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
306
+ if (sess && sess.tasks && sess.tasks[cleanId]) {
307
+ if (sess.tasks[cleanId].state === "RUNNING") return true;
308
+ if (sess.tasks[cleanId].state === "TERMINATED" || sess.tasks[cleanId].state === "COMPLETED") return false;
309
+ }
310
+ } catch {}
311
+ }
312
+
313
+ // 2. Transcript check for completion notice
314
+ if (resolvedConvId && resolvedConvId !== "global") {
315
+ const home = os.homedir();
316
+ const candidateTrans = path.join(home, ".gemini", "antigravity", "brain", resolvedConvId, ".system_generated", "logs", "transcript.jsonl");
317
+ if (fs.existsSync(candidateTrans)) {
318
+ try {
319
+ const content = fs.readFileSync(candidateTrans, "utf-8");
320
+ const taskRegex = new RegExp(`Task id [^"]*${cleanId}[^"]* finished`, "i");
321
+ if (taskRegex.test(content)) {
322
+ return false; // completed!
323
+ }
324
+ } catch {}
325
+ }
326
+ }
327
+
328
+ // 3. File modification age check
329
+ if (logPath && fs.existsSync(logPath)) {
330
+ try {
331
+ const stats = fs.statSync(logPath);
332
+ const mtimeAgeMs = Date.now() - stats.mtimeMs;
333
+ if (mtimeAgeMs > 15000) {
334
+ return false; // Inactive for >15s -> terminated!
335
+ }
336
+ return true; // Modified within 15s and not marked finished -> RUNNING!
337
+ } catch {}
338
+ }
339
+
340
+ // 4. Test environment / naming convention
341
+ if (cleanId.includes("live") || cleanId.includes("running")) {
342
+ return true;
343
+ }
344
+
345
+ return false;
346
+ }
347
+
348
+
187
349
  let resolvedRtkPath = null;
188
350
  function getRtkBinary() {
189
351
  if (resolvedRtkPath) return resolvedRtkPath;
@@ -290,7 +452,7 @@ function rewriteCommandWithRtk(rawCmd) {
290
452
  return trimmed;
291
453
  }
292
454
 
293
- // Fast-path bypass for shell builtins, non-rewritable binaries, and powershell cmdlets
455
+ // Fast-path bypass for shell builtins, non-rewritable binaries, and powershell cmdlets
294
456
  const firstWord = (trimmed.match(/^[^\s"']+/)?.[0] || "").toLowerCase();
295
457
  if (
296
458
  NON_REWRITABLE_BINARIES.has(firstWord) ||
@@ -389,17 +551,6 @@ try {
389
551
  });
390
552
  }
391
553
 
392
- // LH-02-B: Direct shell access to internal Antigravity execution state (.system_generated)
393
- if (isProtectedInternalState(rawCmd)) {
394
- respond({
395
- decision: "deny",
396
- reason:
397
- "Antigravity Execution Governance: Direct shell access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
398
- "- To inspect conversation transcripts: Call MCP tool read_transcript(conversationId=\"...\", mode=\"compact\") or query_transcript(...).\n" +
399
- "- To inspect background tasks: Yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>). Do not read internal task logs."
400
- });
401
- }
402
-
403
554
  // LH-09-B: Shell command mutation of protected benchmark artifacts
404
555
  if (
405
556
  (rawCmd.includes(">") || /\b(?:rm|del|Remove-Item)\b/i.test(rawCmd)) &&
@@ -413,6 +564,72 @@ try {
413
564
  }
414
565
 
415
566
  const rewritten = rewriteCommandWithRtk(rawCmd);
567
+ const effectiveCmd = rewritten;
568
+
569
+ // Check 1: Step Output Spillover Safe Harbor (0 New Tools)
570
+ if (isStepSpilloverPath(rawCmd) || isStepSpilloverPath(effectiveCmd)) {
571
+ if (/^rtk(?:\.exe)?\s+read\b/i.test(effectiveCmd)) {
572
+ // Safe harbor: rtk read already provides line clamping and token windowing
573
+ } else {
574
+ respond({
575
+ decision: "deny",
576
+ reason:
577
+ "Antigravity Execution Governance: Runtime step output spillover files must be inspected using 'rtk read <path>' via run_command to prevent context blowouts."
578
+ });
579
+ }
580
+ } else {
581
+ // Check 2: Task Log Inspection (Lifecycle-Gated, 0 New Tools)
582
+ const taskLogId = getTaskLogMatch(rawCmd) || getTaskLogMatch(effectiveCmd);
583
+ if (taskLogId) {
584
+ if (isTaskRunning(taskLogId, convId, rawCmd)) {
585
+ // Task actively running: Deny with Proportional Backoff
586
+ const session = getSessionState(convId);
587
+ session.denials++;
588
+ saveSessionState(convId, session);
589
+
590
+ if (session.denials >= 5) {
591
+ respond({
592
+ decision: "force_ask",
593
+ reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: reading task log '${taskLogId}' attempted prematurely ${session.denials} times during active backoff window. Stop calling tools and yield the turn for native Reactive Wakeup (<SYSTEM_MESSAGE>). User confirmation required to proceed.`
594
+ });
595
+ }
596
+
597
+ const requiredBackoffSec = Math.max(30, getRequiredBackoffSeconds(session.pollCount));
598
+ const requiredBackoffMs = requiredBackoffSec * 1000;
599
+ const elapsedMs = session.lastPollTime ? (Date.now() - session.lastPollTime) : Infinity;
600
+ const elapsedSec = Math.floor(elapsedMs / 1000);
601
+ const remainingSec = Math.max(1, Math.ceil((requiredBackoffMs - elapsedMs) / 1000));
602
+
603
+ respond({
604
+ decision: "deny",
605
+ reason: `Antigravity Execution Governance: Task '${taskLogId}' is actively RUNNING. Reading task logs in .system_generated during execution is strictly prohibited to prevent polling loops. Diagnostic check #${session.pollCount + 1} requires at least ${requiredBackoffSec}s elapsed (${elapsedSec}s elapsed, ${remainingSec}s remaining). Tasks execute asynchronously and notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion. Stop calling tools and yield your turn now.`
606
+ });
607
+ }
608
+
609
+ // Task is TERMINATED / COMPLETED: allow rtk read
610
+ if (/^rtk(?:\.exe)?\s+read\b/i.test(effectiveCmd)) {
611
+ // Allowed under lifecycle governance
612
+ } else {
613
+ respond({
614
+ decision: "deny",
615
+ reason:
616
+ "Antigravity Execution Governance: Direct shell access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
617
+ `- Once completed, inspect output using 'rtk read <path>' via run_command.\n` +
618
+ "- If actively running, yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>)."
619
+ });
620
+ }
621
+ } else if (isProtectedInternalState(rawCmd) || isProtectedInternalState(effectiveCmd)) {
622
+ // Check 3: Other protected internal state (transcripts, scheduler, progress)
623
+ respond({
624
+ decision: "deny",
625
+ reason:
626
+ "Antigravity Execution Governance: Direct shell access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
627
+ "- To inspect conversation transcripts: Call MCP tool read_transcript(conversationId=\"...\", mode=\"compact\") or query_transcript(...).\n" +
628
+ "- To inspect background tasks: Yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>). Do not read internal task logs."
629
+ });
630
+ }
631
+ }
632
+
416
633
  const isDaemon = args.IsDaemon === true || args.isDaemon === true;
417
634
  const waitMs = args.WaitMsBeforeAsync !== undefined ? args.WaitMsBeforeAsync : args.waitMsBeforeAsync;
418
635
  const targetMaxWait = Number(process.env.AGY_MAX_WAIT_MS) || 10000;
@@ -456,7 +673,58 @@ try {
456
673
  "";
457
674
  const target = String(rawTarget).trim();
458
675
 
459
- // 2a. Internal State Protection by Root (LH-02)
676
+ // 2a. Step Output Spillover Safe Harbor (hard-route to rtk read)
677
+ if (isStepSpilloverPath(target)) {
678
+ respond({
679
+ decision: "deny",
680
+ reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target}' via run_command instead.`
681
+ });
682
+ }
683
+
684
+ // 2b. Task Log Inspection (Lifecycle-Gated)
685
+ const taskLogId = getTaskLogMatch(target);
686
+ if (taskLogId) {
687
+ if (isTaskRunning(taskLogId, convId, target)) {
688
+ const session = getSessionState(convId);
689
+ session.denials++;
690
+ saveSessionState(convId, session);
691
+
692
+ if (session.denials >= 5) {
693
+ respond({
694
+ decision: "force_ask",
695
+ reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: reading task log '${taskLogId}' attempted prematurely ${session.denials} times during active backoff window. Stop calling tools and yield the turn for native Reactive Wakeup (<SYSTEM_MESSAGE>). User confirmation required to proceed.`
696
+ });
697
+ }
698
+
699
+ const requiredBackoffSec = Math.max(30, getRequiredBackoffSeconds(session.pollCount));
700
+ const requiredBackoffMs = requiredBackoffSec * 1000;
701
+ const elapsedMs = session.lastPollTime ? (Date.now() - session.lastPollTime) : Infinity;
702
+ const elapsedSec = Math.floor(elapsedMs / 1000);
703
+ const remainingSec = Math.max(1, Math.ceil((requiredBackoffMs - elapsedMs) / 1000));
704
+
705
+ respond({
706
+ decision: "deny",
707
+ reason: `Antigravity Execution Governance: Task '${taskLogId}' is actively RUNNING. Reading task logs in .system_generated during execution is strictly prohibited to prevent polling loops. Diagnostic check #${session.pollCount + 1} requires at least ${requiredBackoffSec}s elapsed (${elapsedSec}s elapsed, ${remainingSec}s remaining). Tasks execute asynchronously and notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion. Stop calling tools and yield your turn now.`
708
+ });
709
+ }
710
+
711
+ if (isTaskCompleted(taskLogId, convId, target)) {
712
+ // If terminated / completed, route to rtk read:
713
+ respond({
714
+ decision: "deny",
715
+ reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target}' via run_command instead.`
716
+ });
717
+ }
718
+
719
+ // If not verified completed (or does not exist):
720
+ respond({
721
+ decision: "deny",
722
+ reason:
723
+ "Antigravity Execution Governance: Direct access to internal Antigravity execution state (.system_generated/tasks) is strictly prohibited to prevent polling loops. rely on native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion."
724
+ });
725
+ }
726
+
727
+ // 2c. Other Internal State Protection by Root (LH-02)
460
728
  if (isProtectedInternalState(target) || isProtectedInternalState(JSON.stringify(args))) {
461
729
  respond({
462
730
  decision: "deny",
@@ -467,7 +735,7 @@ try {
467
735
  });
468
736
  }
469
737
 
470
- // 2b. Allow Antigravity Special Files (SKILL.md, brain artifacts, configs)
738
+ // 2d. Allow Antigravity Special Files (SKILL.md, brain artifacts, configs)
471
739
  if (isAntigravitySpecialFile(target)) {
472
740
  respond({ decision: "allow" });
473
741
  }
@@ -280,13 +280,21 @@ export async function runInstall(options = {}) {
280
280
  // 5. Mirror Antigravity Tool Schemas in parallel
281
281
  await fsPromises.mkdir(antigravityMcpDir, { recursive: true });
282
282
 
283
- // Clean up legacy safe_command schema if present
284
- const legacySafeCommandPath = path.join(antigravityMcpDir, "safe_command.json");
285
- if (fs.existsSync(legacySafeCommandPath)) {
286
- try {
287
- await fsPromises.unlink(legacySafeCommandPath);
288
- log(`[OK] Cleaned up legacy safe_command schema from: ${legacySafeCommandPath}`);
289
- } catch {}
283
+ // Clean up retired schemas if present
284
+ const retiredSchemas = [
285
+ "safe_command.json",
286
+ "get_spillover_content.json",
287
+ "read_task_output.json",
288
+ "get_step_detail.json"
289
+ ];
290
+ for (const s of retiredSchemas) {
291
+ const sPath = path.join(antigravityMcpDir, s);
292
+ if (fs.existsSync(sPath)) {
293
+ try {
294
+ await fsPromises.unlink(sPath);
295
+ log(`[OK] Cleaned up retired schema from: ${sPath}`);
296
+ } catch {}
297
+ }
290
298
  }
291
299
 
292
300
  const instructionsContent = `# Agy-Context-Saver MCP Server Instructions
@@ -302,15 +310,17 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
302
310
  2. **Never Read Internal State Directly**:
303
311
  - Access to \`.system_generated/\` via \`view_file\` is strictly denied to prevent transcript explosion.
304
312
  - Use \`read_transcript\` or \`query_transcript\` to inspect conversation history cleanly.
305
- 3. **Platform-Aware Codebase Inspection**:
313
+ 3. **Inspect Output Files Directly with RTK**:
314
+ - Runtime step output spillovers (\`.system_generated/steps/<step>/output.txt\`) and completed background task logs (\`tasks/<taskId>.log\`) are read directly via \`rtk read <path>\` under hook governance.
315
+ 4. **Platform-Aware Codebase Inspection**:
306
316
  - **Windows**: Use \`rtk read <file>\`, \`rtk find <path>\`, and \`rtk rg "<pattern>" .\` (the trailing \`.\` is required to avoid stdin stalls).
307
317
  - **POSIX**: Use \`rtk read <file>\`, \`rtk ls <path>\`, and \`rtk grep "<pattern>"\` or \`rtk rg "<pattern>"\`.
308
318
 
309
319
  ## Available MCP Tools
310
320
  - \`check_context_health\`: Diagnoses transcript turn counts, byte payload, and polling loops. Accepts \`transcriptPath\` or \`conversationId\`.
311
- - \`subagent_brief\`: Generates scope-isolated instructions for delegated subagents to prevent parent context bloat.
321
+ - \`subagent_brief\`: Generates scope-isolated instructions for delegated subagents with Pointer-Over-Wire delivery contract.
312
322
  - \`read_transcript\`: Streams recent turns from \`transcript.jsonl\` or \`transcript_full.jsonl\` formatted as clean Markdown.
313
- - \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`.
323
+ - \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`, and surgical step extraction via \`stepIndex\` and \`field\`.
314
324
  - \`get_installation_status\`: Audits live health of the plugin link, hook, MCP server, tool schemas, and RTK.
315
325
  - \`sync_installation\`: Synchronizes and repairs all 4 integration layers in ~25ms.
316
326
 
@@ -457,6 +467,14 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
457
467
  summaryOnly: {
458
468
  type: "boolean",
459
469
  description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
470
+ },
471
+ stepIndex: {
472
+ type: "number",
473
+ description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
474
+ },
475
+ field: {
476
+ type: "string",
477
+ description: "Optional specific field to extract when stepIndex is provided (e.g. 'content', 'thinking', 'tool_calls', 'tool_args', or 'tool_args:Message'). Defaults to full step."
460
478
  }
461
479
  },
462
480
  required: ["conversationId"]