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 +34 -0
- package/README.md +33 -2
- package/mcp/index.js +143 -10
- package/package.json +2 -2
- package/rules/AGENTS.md +26 -6
- package/scripts/execution-guard-hook.mjs +282 -14
- package/scripts/install-register.mjs +28 -10
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
|
|
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.
|
|
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.
|
|
384
|
-
`2.
|
|
385
|
-
`
|
|
386
|
-
`
|
|
387
|
-
`
|
|
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: ${
|
|
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]) =>
|
|
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
|
+
"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 &
|
|
10
|
-
- **Zero-Tolerance on
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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. **
|
|
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
|
|
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"]
|