agy-context-saver 1.7.0 → 1.8.1

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/rules/AGENTS.md CHANGED
@@ -30,34 +30,32 @@
30
30
 
31
31
  ---
32
32
 
33
- ## Layer 2: Transparent Shell Execution & Codebase Inspection via RTK
33
+ ## Layer 2: Transparent Shell Execution & Unified 3-Tier Codebase Inspection
34
34
  - **Native Shell Commands**: Run shell commands normally using `run_command`. The PreToolUse hook transparently rewrites supported commands via RTK (`git`, `pytest`, `cargo`, `npm`, etc.) to produce ultra-compact outputs.
35
- - **Canonical Codebase Inspection**: Unbounded full-file reads and unoptimized search tools (`grep_search`, `find_by_name`, `list_dir`) are blocked for workspace files. Use bounded slices and RTK's canonical CLI interfaces:
36
- - `view_file` with bounded slices (`StartLine` and `EndLine`, max 150 lines): **Preferred for reading files**—preserves 1-indexed line numbers and avoids terminal stdout truncation. Pair with `waymark_ask` to locate exact symbol line ranges first.
37
- - `rtk find <path>`: Compact file search tree and directory listings (cross-platform, works on Windows & POSIX).
38
- - Search commands by platform:
39
- - **Windows**: Use `rtk rg "<pattern>" .` (explicit target `.` is required to prevent stdin pipe hanging) or `rg "<pattern>" .`.
40
- - **POSIX / Linux / macOS**: Use `rtk grep "<pattern>"` or `rtk rg "<pattern>"`.
41
- - Directory listing by platform:
42
- - **Windows**: Use `rtk find <path>` or PowerShell `dir` / `Get-ChildItem`. (Avoid `rtk ls` or `rtk tree` on Windows as they rely on POSIX binaries/flags).
43
- - **POSIX / Linux / macOS**: Use `rtk ls <path>` or `rtk tree`.
44
- - `rtk err <cmd>`: Run command and show only errors/warnings.
45
- - `rtk summary <cmd>`: Run command and produce a 2-line heuristic summary.
35
+ - **Canonical 3-Tier Codebase & File Inspection**: Unbounded full-file reads and unoptimized search tools (`grep_search`, `find_by_name`, `list_dir`) are blocked for workspace files. Follow the 3-tier hierarchy:
36
+ 1. **Tier 1 — Structural Discovery (`waymark_ask` with `plain: true`)**: Locate exact `[StartLine, EndLine]` coordinates via `waymark_ask(..., plain: true)` across all 23 supported formats (code ASTs + `markdown`, `yaml`, `json`, `toml`, `bash`). Always pass `plain: true` (~19–466 tokens, zero shell quoting or stdin hazards).
37
+ 2. **Tier 2 — Precision Workspace File Reading (`view_file` Hybrid Slice Policy)**: Pass the exact `StartLine` and `EndLine` from `waymark_ask` into `view_file` (bounded slices of `1–150` lines) to preserve 1-indexed line numbers for `replace_file_content` and avoid terminal stdout truncation. Partial slices of `151–800` lines are denied; an explicit one-pass full-file audit (`StartLine: 1, EndLine: <TotalLines>`) is permitted only when the file on disk is $\le 800$ lines and $\le 45\text{ KiB}$ (`46,080` bytes).
38
+ 3. **Tier 3 — RTK Boundaries (`rtk read`, `rtk find`, `rtk rg`, `rtk err`, `rtk summary`)**:
39
+ - **Spillovers & Task Logs**: Read `.system_generated/steps/<step>/output.txt` spillovers and completed `tasks/<taskId>.log` files via `rtk read <path>` (with `--head-lines` / `--tail-lines`). Never use `rtk read -l minimal|aggressive` or `-m` to derive line numbers for edits.
40
+ - **Directory / File Discovery**:
41
+ - **Windows**: Use `rtk find <path>` or PowerShell `dir` / `Get-ChildItem`. (Avoid `rtk ls` or `rtk tree` on Windows as they rely on POSIX binaries/flags).
42
+ - **POSIX / Linux / macOS**: Use `rtk find <path>`, `rtk ls <path>`, or `rtk tree`.
43
+ - **Prose / Regex Search (when symbol, heading, or config key is unknown)**:
44
+ - **Windows**: Use `rtk rg "<pattern>" .` (explicit target `.` is required to prevent stdin pipe hanging) or `rg "<pattern>" .`.
45
+ - **POSIX / Linux / macOS**: Use `rtk grep "<pattern>"` or `rtk rg "<pattern>"`.
46
+ - **Command Diagnostics**: `rtk err <cmd>` (errors/warnings only) and `rtk summary <cmd>` (2-line heuristic summary).
46
47
  - **Antigravity Special Files Safe Harbor**: Documented Antigravity special files (`SKILL.md`, brain artifacts, `.gemini/config/` configs) are permitted via `view_file` without slice bounds.
47
48
  - **Protected Internal State**: Direct native inspection of `.system_generated/` (transcripts, task logs, progress files) is strictly forbidden. Use Agy MCP tools `read_transcript` and `query_transcript`.
49
+ - **Git CRLF/LF Warning Hygiene**: Git's line-ending conversion notices (`warning: LF will be replaced by CRLF the next time Git touches it` / `warning: CRLF will be replaced by LF...`) are non-actionable diagnostic noise. Suppress them during repo operations (via `git -c core.safecrlf=false` or `git config --global core.safecrlf false`) and do not propagate them into task outputs or conversation context.
48
50
 
49
51
  ---
50
52
 
51
- ## Layer 3: Synchronous Execution & Subagent Delegation
53
+ ## Layer 3: Synchronous Execution
52
54
  - **Maximum Synchronous Window**: Non-daemon `run_command` calls are automatically set to `WaitMsBeforeAsync: 10000` to complete synchronously and prevent unnecessary background detachment.
53
55
  - **Targeted Test Execution & Opt-In Fail-Fast (LH-11)**:
54
56
  - **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
57
  - **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
58
  - **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.
58
- - **Subagent Delegation & Pointer-Over-Wire (POW)**:
59
- - Delegate heavy multi-file exploration, deep research, and repo sweeps to subagents (`invoke_subagent`). Subagents absorb intermediate steps and return high-signal summaries.
60
- - **POW Protocol & `write_to_file` Isolation**: Payloads or deliverables exceeding 1 KB must be written either to workspace scratch files (`<workspace>/scratch/<task>_findings.md` with `ArtifactMetadata` **omitted**) or to the **subagent's own** `<Artifact Directory Path>/scratch/<task>_findings.md` (with `ArtifactMetadata: { Summary, UserFacing: false, RequestFeedback: false }`). Never instruct a subagent to `write_to_file` into the parent conversation's `brain/<parent-uuid>/` directory, as Antigravity enforces per-conversation `brain/<uuid>` write isolation. Transmit **ONLY** an Executive Summary (3–5 sentences), a Scoreboard / Verdict table, and a clickable file link (`file:///...`) in `send_message` to prevent transport truncation and parent context bloat.
61
59
  - **Benchmark / Evaluator Integrity**: Never mutate benchmark definitions, prompt templates, or scoring artifacts during an active evaluation without explicit user confirmation.
62
60
 
63
61
  ---
@@ -77,7 +75,7 @@ When an agent or developer tests this framework, follow this exact sequence with
77
75
  ```bash
78
76
  npm test
79
77
  ```
80
- *Expected*: All 8 test suites pass (Hook unit tests, MCP protocol tests, output governance, E2E probes, performance probes, live system validation, lifecycle rollback, subagent trials).
78
+ *Expected*: All 10 test suites pass (hook unit tests, MCP protocol tests, output governance, E2E probes, performance probes, live system validation, lifecycle rollback, check-only read-only safety, malformed config preservation, RTK ledger fresh install).
81
79
 
82
80
  3. **Verify Individual Layers On-Demand**:
83
81
  - Governance Hook: `node tests/test-hook.mjs`
@@ -87,14 +85,12 @@ When an agent or developer tests this framework, follow this exact sequence with
87
85
  - Performance Probes: `node tests/test-performance-probes.mjs`
88
86
  - Live System Checks: `node tests/test-installed-verification.mjs`
89
87
  - Lifecycle & Rollback: `node tests/test-lifecycle-rollback.mjs`
90
- - Subagent Delegation Trials: `node tests/test-subagent-trials.mjs`
91
88
 
92
89
  4. **Verify MCP Tools in Antigravity**:
93
90
  - `get_installation_status`: Inspect live integration layers.
94
91
  - `check_context_health`: Pass `{ "transcriptPath": "<path-to-transcript.jsonl>" }`.
95
92
  - `read_transcript`: Pass `{ "conversationId": "<uuid>", "lastTurns": 3, "mode": "compact" }`.
96
93
  - `query_transcript`: Pass `{ "conversationId": "<uuid>", "query": "<pattern>", "summaryOnly": true }`.
97
- - `subagent_brief`: Pass `{ "objective": "<goal>", "role": "<role>", "scopeFiles": ["..."] }` or pass multiple via `{ "subagents": [...] }` to generate ready-to-use `invoke_subagent` JSON payloads.
98
94
 
99
95
  5. **Operational Anti-Patterns (What NOT to do)**:
100
96
  - ❌ **Do NOT run `npm start` or `node mcp/index.js` interactively**: It starts the stdio JSON-RPC daemon and waits indefinitely on stdin.
@@ -9,10 +9,11 @@
9
9
  * - Intercepts native run_command and calls `rtk rewrite <cmd>`.
10
10
  * - Non-daemon commands maintain WaitMsBeforeAsync=10000 for synchronous completion.
11
11
  * 2. Hard-Routed Native File/Search Inspection (LH-07):
12
- * - view_file, read_file, read_many_files -> Deny and route to `rtk read <file>`.
13
- * - grep_search -> Deny and route to `rtk grep <pattern>`.
12
+ * - view_file -> Allow bounded slices (<= 150 lines, paired with waymark_ask); deny unbounded dumps.
13
+ * - read_file, read_many_files -> Deny and route to bounded view_file or `rtk read <file>`.
14
+ * - grep_search -> Deny and route to `rtk rg "<pattern>" .` / `rtk grep <pattern>`.
14
15
  * - find_by_name -> Deny and route to `rtk find <path>`.
15
- * - list_dir -> Deny and route to `rtk ls` or `rtk tree`.
16
+ * - list_dir -> Deny and route to `rtk find` (Win) or `rtk ls` / `rtk tree` (POSIX).
16
17
  * - Safe Harbor: Documented Antigravity special files (SKILL.md, brain artifacts, configs) are allowed.
17
18
  * 3. Root-Based Antigravity Internal State Protection (LH-02):
18
19
  * - Hard denial for any inspection of `.system_generated/` (transcripts, task logs, progress).
@@ -56,12 +57,32 @@ const GOVERNED_TOOLS = new Set([
56
57
  const STATE_TTL_MS = 3600000; // 1 hour TTL
57
58
  const MAX_STATE_ENTRIES = 50;
58
59
 
60
+ // Per-user state folder, never the shared os.tmpdir(). Keep identical to scripts/state-dir.mjs:
61
+ // this hook is mirrored as a standalone file, so it can't import it.
62
+ function getStateDir() {
63
+ const profileHome = process.env.AGY_TEST_PROFILE_DIR;
64
+ if (os.platform() === "win32") {
65
+ const localAppData = profileHome
66
+ ? path.join(profileHome, "AppData", "Local")
67
+ : (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"));
68
+ return path.join(localAppData, "agy-context-saver");
69
+ }
70
+ const stateHome = profileHome
71
+ ? path.join(profileHome, ".local", "state")
72
+ : (process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"));
73
+ return path.join(stateHome, "agy-context-saver");
74
+ }
75
+
76
+ function ensureStateDir() {
77
+ try { fs.mkdirSync(getStateDir(), { recursive: true }); } catch {}
78
+ }
79
+
59
80
  function getStateFilePath(convId = "global") {
60
81
  if (!convId || convId === "global") {
61
- return path.join(os.tmpdir(), "agy-session-governance-state.json");
82
+ return path.join(getStateDir(), "agy-session-governance-state.json");
62
83
  }
63
84
  const safeId = String(convId).replace(/[^a-zA-Z0-9_-]/g, "_");
64
- return path.join(os.tmpdir(), `agy-session-${safeId}.json`);
85
+ return path.join(getStateDir(), `agy-session-${safeId}.json`);
65
86
  }
66
87
 
67
88
  function getSessionState(convId = "global") {
@@ -79,6 +100,7 @@ function getSessionState(convId = "global") {
79
100
 
80
101
  function saveSessionState(convId = "global", entry) {
81
102
  const file = getStateFilePath(convId);
103
+ ensureStateDir();
82
104
  try {
83
105
  const now = Date.now();
84
106
  entry.lastPollTime = now;
@@ -176,6 +198,7 @@ function isProtectedBenchmarkPath(targetPath) {
176
198
  );
177
199
  }
178
200
 
201
+
179
202
  /**
180
203
  * Checks if target path references runtime step output spillover file.
181
204
  */
@@ -355,7 +378,7 @@ const NON_REWRITABLE_BINARIES = new Set([
355
378
  "export"
356
379
  ]);
357
380
 
358
- const REWRITE_CACHE_FILE = path.join(os.tmpdir(), "agy-rtk-rewrite-cache.json");
381
+ const REWRITE_CACHE_FILE = path.join(getStateDir(), "agy-rtk-rewrite-cache.json");
359
382
  const REWRITE_CACHE_MAX = 100;
360
383
  let inMemoryRewriteCache = null;
361
384
 
@@ -403,6 +426,7 @@ function resolveCompatibleRtk() {
403
426
 
404
427
  cache.__envSig = envSig;
405
428
  cache.__rtkResolved = resolved;
429
+ ensureStateDir();
406
430
  try {
407
431
  const tmp = `${REWRITE_CACHE_FILE}.${process.pid}.tmp`;
408
432
  fs.writeFileSync(tmp, JSON.stringify(cache), "utf-8");
@@ -443,6 +467,7 @@ function rememberRewrite(cmd, rewritten) {
443
467
  for (const k of keys.slice(0, Math.max(0, keys.length - REWRITE_CACHE_MAX))) delete disk[k];
444
468
  if (envSig !== undefined) disk.__envSig = envSig;
445
469
  if (rtkResolved !== undefined) disk.__rtkResolved = rtkResolved;
470
+ ensureStateDir();
446
471
  const tmp = `${REWRITE_CACHE_FILE}.${process.pid}.tmp`;
447
472
  fs.writeFileSync(tmp, JSON.stringify(disk), "utf-8");
448
473
  try {
@@ -616,6 +641,7 @@ try {
616
641
  });
617
642
  }
618
643
 
644
+
619
645
  // LH-09-B: Shell command mutation of protected benchmark artifacts
620
646
  if (
621
647
  (rawCmd.includes(">") || /\b(?:rm|del|Remove-Item)\b/i.test(rawCmd)) &&
@@ -764,9 +790,56 @@ try {
764
790
  respond({ decision: "allow" });
765
791
  }
766
792
 
767
- // 2e. Bounded Slice Safe Harbor for view_file
768
- // Allow view_file IF AND ONLY IF it is a bounded slice inspection (StartLine and EndLine <= 150 lines)
793
+ // 2e. Hybrid Slice & Guarded Full-File Policy for view_file
794
+ // - 1..150 line slice -> ALLOW
795
+ // - Explicit full-file read (StartLine=1, EndLine=actualFileLines) of an existing file <= 800 lines AND <= 45 KiB -> ALLOW
796
+ // - Partial slice of 151..800+ lines -> DENY (discover target with waymark_ask and narrow the slice)
797
+ // - Explicit full-file read exceeding 800 lines or 45 KiB -> DENY
798
+ // - Unbounded view_file (missing StartLine/EndLine) -> DENY
769
799
  if (toolName === "view_file") {
800
+ const MAX_SAFE_SLICE_LINES = 150;
801
+ const MAX_FULL_FILE_LINES = 800;
802
+ const MAX_FULL_FILE_BYTES = 45 * 1024; // 45 KiB (46,080 bytes)
803
+
804
+ const waymarkAvailable = (() => {
805
+ if (process.env.AGY_FORCE_WAYMARK === "0") return false;
806
+ if (process.env.AGY_FORCE_WAYMARK === "1") return true;
807
+ try {
808
+ const home = process.env.AGY_TEST_PROFILE_DIR || os.homedir();
809
+ const schemaPath = path.join(home, ".gemini", "antigravity", "mcp", "waymark-engine", "waymark_ask.json");
810
+ if (fs.existsSync(schemaPath)) return true;
811
+ const mcpConfigPath = path.join(home, ".gemini", "config", "mcp_config.json");
812
+ if (fs.existsSync(mcpConfigPath)) {
813
+ const raw = fs.readFileSync(mcpConfigPath, "utf-8");
814
+ if (raw.includes('"waymark-engine"')) return true;
815
+ }
816
+ } catch {}
817
+ return false;
818
+ })();
819
+
820
+ const getFileMetrics = (filePath) => {
821
+ if (!filePath || typeof filePath !== "string") return null;
822
+ try {
823
+ if (!fs.existsSync(filePath)) return null;
824
+ const stat = fs.statSync(filePath);
825
+ if (!stat.isFile()) return null;
826
+ const bytes = stat.size;
827
+ const content = fs.readFileSync(filePath, "utf-8");
828
+ if (content.length === 0) {
829
+ return { bytes: 0, lineCount: 0, rawSplitCount: 1 };
830
+ }
831
+ const rawSplitCount = content.split(/\r?\n/).length;
832
+ const lineCount = /\r?\n$/.test(content) ? Math.max(1, rawSplitCount - 1) : rawSplitCount;
833
+ return { bytes, lineCount, rawSplitCount };
834
+ } catch {
835
+ return null;
836
+ }
837
+ };
838
+
839
+ const discoveryGuidance = waymarkAvailable
840
+ ? `Locate the target symbol, heading, or config key first with 'waymark_ask(..., plain: true)' and pass a precise bounded slice ('StartLine' and 'EndLine', max ${MAX_SAFE_SLICE_LINES} lines) to 'view_file'`
841
+ : `Provide bounded slices ('StartLine' and 'EndLine', max ${MAX_SAFE_SLICE_LINES} lines) via 'view_file', or run 'rtk read ${target || "<file>"}' via run_command instead`;
842
+
770
843
  const rawStart = args.StartLine !== undefined ? args.StartLine : args.start_line;
771
844
  const rawEnd = args.EndLine !== undefined ? args.EndLine : args.end_line;
772
845
  const hasSliceParams = typeof rawStart !== "undefined" || typeof rawEnd !== "undefined";
@@ -784,30 +857,47 @@ try {
784
857
  endLine >= startLine
785
858
  ) {
786
859
  const span = endLine - startLine + 1;
787
- const MAX_SAFE_SLICE_LINES = 150;
788
860
  if (span <= MAX_SAFE_SLICE_LINES) {
789
861
  respond({ decision: "allow" });
790
862
  }
863
+
864
+ const metrics = startLine === 1 ? getFileMetrics(target) : null;
865
+ const isExplicitFullFile = Boolean(
866
+ metrics &&
867
+ startLine === 1 &&
868
+ (endLine === metrics.lineCount || endLine === metrics.rawSplitCount)
869
+ );
870
+
871
+ if (isExplicitFullFile) {
872
+ if (metrics.lineCount <= MAX_FULL_FILE_LINES && metrics.bytes <= MAX_FULL_FILE_BYTES) {
873
+ respond({ decision: "allow" });
874
+ }
875
+ const kib = (metrics.bytes / 1024).toFixed(1);
876
+ respond({
877
+ decision: "deny",
878
+ reason: `Antigravity Governance: Explicit full-file read via 'view_file' (${metrics.lineCount} lines, ${kib} KiB) exceeds the full-file audit ceiling (max ${MAX_FULL_FILE_LINES} lines and 45 KiB). ${discoveryGuidance}.`
879
+ });
880
+ }
881
+
791
882
  respond({
792
883
  decision: "deny",
793
- reason: `Antigravity Governance: Oversized file slice via 'view_file' (${span} lines > ${MAX_SAFE_SLICE_LINES} lines max safe window). Reduce line range or run 'rtk read ${target || "<file>"}' via run_command instead.`
884
+ reason: `Antigravity Governance: Oversized partial file slice via 'view_file' (${span} lines > ${MAX_SAFE_SLICE_LINES} lines max safe slice window). Partial slices above ${MAX_SAFE_SLICE_LINES} lines are denied; full-file reads are only permitted when explicitly covering line 1 through the actual end of a file <= ${MAX_FULL_FILE_LINES} lines and <= 45 KiB. ${discoveryGuidance}.`
794
885
  });
795
886
  }
796
887
 
797
888
  respond({
798
889
  decision: "deny",
799
- reason: `Antigravity Governance: Invalid slice range via 'view_file'. Ensure StartLine and EndLine are positive integers with EndLine >= StartLine (max 150 lines), or run 'rtk read ${target || "<file>"}' via run_command instead.`
890
+ reason: `Antigravity Governance: Invalid slice range via 'view_file'. Ensure StartLine and EndLine are positive integers with EndLine >= StartLine. ${discoveryGuidance}.`
800
891
  });
801
892
  }
802
893
 
803
- // Default when no slice attempted: keep hook identical to original behavior (zero slice advertisement)
804
894
  respond({
805
895
  decision: "deny",
806
- reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target || "<file>"}' via run_command instead.`
896
+ reason: `Antigravity Governance: Full-file inspection via '${toolName}' without explicit bounds is denied. ${discoveryGuidance} (or pass explicit StartLine=1, EndLine=<TotalLines> for a one-pass audit when the file is <= ${MAX_FULL_FILE_LINES} lines and <= 45 KiB).`
807
897
  });
808
898
  }
809
899
 
810
- // 2f. Hard-Route Full-File / Other Workspace Code Inspection to Canonical RTK Commands (LH-07)
900
+ // 2f. Hard-Route Other Native File/Search Tools (LH-07)
811
901
  if (toolName === "read_file") {
812
902
  respond({
813
903
  decision: "deny",
@@ -17,6 +17,7 @@ import os from "node:os";
17
17
  import path from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { ensureRtkInstalled, findExistingRtk } from "./provision-rtk.mjs";
20
+ import { getStateDir, removeLegacyTmpState } from "./state-dir.mjs";
20
21
 
21
22
  const fsPromises = fs.promises;
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -115,7 +116,6 @@ export function detectExistingInstallation() {
115
116
  const scriptExists = fs.existsSync(destHook);
116
117
  const schemasExist =
117
118
  fs.existsSync(path.join(antigravityMcpDir, "check_context_health.json")) &&
118
- fs.existsSync(path.join(antigravityMcpDir, "subagent_brief.json")) &&
119
119
  fs.existsSync(path.join(antigravityMcpDir, "get_installation_status.json")) &&
120
120
  fs.existsSync(path.join(antigravityMcpDir, "sync_installation.json")) &&
121
121
  fs.existsSync(path.join(antigravityMcpDir, "read_transcript.json")) &&
@@ -338,7 +338,10 @@ export async function runInstall(options = {}) {
338
338
  "safe_command.json",
339
339
  "get_spillover_content.json",
340
340
  "read_task_output.json",
341
- "get_step_detail.json"
341
+ "get_step_detail.json",
342
+ // Subagent tools moved to the separate Agy-Squad package (2026-10-15)
343
+ "subagent_brief.json",
344
+ "dispatch_squad.json"
342
345
  ];
343
346
  for (const s of retiredSchemas) {
344
347
  const sPath = path.join(antigravityMcpDir, s);
@@ -350,39 +353,13 @@ export async function runInstall(options = {}) {
350
353
  }
351
354
  }
352
355
 
353
- const instructionsContent = `# Agy-Context-Saver MCP Server Instructions
354
-
355
- ## Overview
356
- Agy-Context-Saver is the universal Model Context Protocol server and lifecycle governor for Google Antigravity, paired with RTK (Rust Token Killer) for transparent CLI output reduction.
357
-
358
- ## Core Rules for Antigravity Agents
359
- 1. **Reactive Wakeup (Zero Task Polling)**:
360
- - When background tasks run, DO NOT call \`manage_task(Action='status')\` or \`schedule\` polling timers.
361
- - Stop calling tools and yield the turn. The system notifies you automatically via \`<SYSTEM_MESSAGE>\`.
362
- - Repeated polling triggers an escalating circuit breaker (frozen at 5 denials).
363
- 2. **Never Read Internal State Directly**:
364
- - Access to \`.system_generated/\` via \`view_file\` is strictly denied to prevent transcript explosion.
365
- - Use \`read_transcript\` or \`query_transcript\` to inspect conversation history cleanly.
366
- 3. **Inspect Output Files Directly with RTK**:
367
- - 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.
368
- 4. **Platform-Aware Codebase Inspection**:
369
- - **Windows**: Use \`rtk read <file>\`, \`rtk find <path>\`, and \`rtk rg "<pattern>" .\` (the trailing \`.\` is required to avoid stdin stalls).
370
- - **POSIX**: Use \`rtk read <file>\`, \`rtk ls <path>\`, and \`rtk grep "<pattern>"\` or \`rtk rg "<pattern>"\`.
371
-
372
- ## Available MCP Tools
373
- - \`check_context_health\`: Diagnoses transcript turn counts, byte payload, and polling loops. Accepts \`transcriptPath\` or \`conversationId\`.
374
- - \`subagent_brief\`: Formulates scope-isolated prompts and ready-to-use invoke_subagent JSON payloads for single or multiple delegated subagents with Pointer-Over-Wire delivery contract.
375
- - \`read_transcript\`: Streams recent turns from \`transcript.jsonl\` or \`transcript_full.jsonl\` formatted as clean Markdown.
376
- - \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`, and surgical step extraction via \`stepIndex\` and \`field\`.
377
- - \`get_installation_status\`: Audits live health of the plugin link, hook, MCP server, tool schemas, and RTK.
378
- - \`sync_installation\`: Synchronizes and repairs all 4 integration layers in ~25ms.
379
-
380
- ## Diagnostic & CLI Playbook
381
- - Audit Status: \`npm run status\` or \`node mcp/index.js status\`
382
- - Run Test Suite: \`npm test\`
383
- - CLI Help: \`node mcp/index.js --help\`
384
- - ⚠️ Warning: Do not run \`npm start\` or \`node mcp/index.js\` without subcommands interactively (it is a stdio JSON-RPC server).
385
- `;
356
+ // Versions up to 1.7.0 kept hook state in the shared temp folder; it now lives in getStateDir()
357
+ for (const legacy of removeLegacyTmpState()) {
358
+ log(`[OK] Removed legacy temp-folder state: ${legacy}`);
359
+ }
360
+
361
+ const instructionsSourcePath = path.join(repoRoot, "mcp", "instructions.md");
362
+ const instructionsContent = await fsPromises.readFile(instructionsSourcePath, "utf-8");
386
363
 
387
364
  const checkHealthSchema = {
388
365
  name: "check_context_health",
@@ -402,42 +379,6 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
402
379
  }
403
380
  };
404
381
 
405
- const subagentBriefSchema = {
406
- name: "subagent_brief",
407
- description: "Generates scope-isolated, anti-bloat prompts and ready-to-use invoke_subagent JSON payloads for single or multiple delegated subagents. Instructs subagents to absorb intermediate file reads/searches and return only high-signal findings.",
408
- parameters: {
409
- type: "object",
410
- properties: {
411
- objective: { type: "string", description: "The core research or investigative objective (for single subagent)." },
412
- role: { type: "string", description: "2-5 word role description (e.g. 'Comprehensive Test Engineer', 'Codebase Security Auditor'). Defaults to 'Scope-Isolated Deep Research Subagent'." },
413
- instructions: { type: "string", description: "Optional step-by-step instructions or specialized directives." },
414
- scopeFiles: { type: "array", items: { type: "string" }, description: "List of files or directories to investigate." },
415
- expectedDeliverable: { type: "string", description: "The concise format expected back by the parent agent." },
416
- typeName: { type: "string", description: "Subagent type name: 'self' (default, inherits parent tools/prompt/model) or 'research' (read-only)." },
417
- model: { type: "string", enum: ["inherit", "flash_lite", "flash", "pro"], description: "Model tier: 'inherit' (default), 'flash_lite', 'flash', or 'pro'." },
418
- workspace: { type: "string", enum: ["inherit", "branch", "share"], description: "Workspace isolation: 'inherit' (default), 'branch', or 'share'." },
419
- subagents: {
420
- type: "array",
421
- description: "Optional array of subagent configurations to formulate multiple subagents at once.",
422
- items: {
423
- type: "object",
424
- properties: {
425
- role: { type: "string", description: "2-5 word role description." },
426
- objective: { type: "string", description: "The core objective for this subagent." },
427
- instructions: { type: "string", description: "Optional step-by-step instructions." },
428
- scopeFiles: { type: "array", items: { type: "string" }, description: "Files/directories to investigate." },
429
- expectedDeliverable: { type: "string", description: "Expected deliverable format." },
430
- typeName: { type: "string", description: "Subagent type name: 'self' or 'research'." },
431
- model: { type: "string", enum: ["inherit", "flash_lite", "flash", "pro"], description: "Model tier." },
432
- workspace: { type: "string", enum: ["inherit", "branch", "share"], description: "Workspace isolation." }
433
- },
434
- required: ["objective"]
435
- }
436
- }
437
- }
438
- }
439
- };
440
-
441
382
  const getInstallationStatusSchema = {
442
383
  name: "get_installation_status",
443
384
  description: "Inspect the live installation status of Agy-Context-Saver across all Antigravity integration points (Native Plugin Link, Governor Hook, MCP Server, and Tool Schemas).",
@@ -493,7 +434,7 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
493
434
 
494
435
  const queryTranscriptSchema = {
495
436
  name: "query_transcript",
496
- description: "Granular query and forensic filtering engine for Antigravity conversation transcripts. Searches by keyword or regex, filters by role (user, assistant, tool, error), slices step ranges, and automatically dereferences full content when truncated.",
437
+ description: "Granular query and forensic filtering engine for Antigravity conversation transcripts. Searches by keyword or regex, filters by role (user, assistant, subagent, tool, error), slices step ranges, and automatically dereferences full content when truncated.",
497
438
  parameters: {
498
439
  type: "object",
499
440
  properties: {
@@ -508,7 +449,7 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
508
449
  roles: {
509
450
  type: "array",
510
451
  items: { type: "string" },
511
- description: "Filter steps by role/source: 'user', 'assistant', 'tool', 'error', or 'all' (default: ['user', 'assistant'])."
452
+ description: "Filter steps by role/source: 'user', 'assistant', 'subagent', 'tool', 'error', or 'all' (default: ['user', 'assistant'])."
512
453
  },
513
454
  mode: {
514
455
  type: "string",
@@ -559,7 +500,6 @@ Agy-Context-Saver is the universal Model Context Protocol server and lifecycle g
559
500
  await Promise.all([
560
501
  safeWriteFileIfChanged(path.join(antigravityMcpDir, "instructions.md"), instructionsContent),
561
502
  safeWriteFileIfChanged(path.join(antigravityMcpDir, "check_context_health.json"), JSON.stringify(checkHealthSchema, null, 2)),
562
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "subagent_brief.json"), JSON.stringify(subagentBriefSchema, null, 2)),
563
503
  safeWriteFileIfChanged(path.join(antigravityMcpDir, "get_installation_status.json"), JSON.stringify(getInstallationStatusSchema, null, 2)),
564
504
  safeWriteFileIfChanged(path.join(antigravityMcpDir, "sync_installation.json"), JSON.stringify(syncInstallationSchema, null, 2)),
565
505
  safeWriteFileIfChanged(path.join(antigravityMcpDir, "read_transcript.json"), JSON.stringify(readTranscriptSchema, null, 2)),
@@ -631,7 +571,21 @@ export async function runUninstall(options = {}) {
631
571
  }
632
572
  }
633
573
 
634
- // 6. Optional: Restore configuration files from backups if requested
574
+ // 6. Remove hook state: the per-user state folder and anything older versions left in the temp folder
575
+ const stateDir = getStateDir();
576
+ if (fs.existsSync(stateDir)) {
577
+ try {
578
+ await fsPromises.rm(stateDir, { recursive: true, force: true });
579
+ log(`[OK] Removed hook state folder: ${stateDir}`);
580
+ } catch (err) {
581
+ warn(`[WARN] Could not remove hook state folder: ${err.message}`);
582
+ }
583
+ }
584
+ for (const legacy of removeLegacyTmpState()) {
585
+ log(`[OK] Removed legacy temp-folder state: ${legacy}`);
586
+ }
587
+
588
+ // 7. Optional: Restore configuration files from backups if requested
635
589
  if (options.restoreBackups) {
636
590
  await runRestoreBackups(options);
637
591
  }
@@ -678,7 +632,7 @@ export function runStatus(options = {}) {
678
632
  log(`- Native Plugin Link: ${existing.details.plugin.exists ? "ACTIVE (" + pluginDest + ")" : "NOT LINKED"}`);
679
633
  log(`- Execution Governor Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"}`);
680
634
  log(`- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`);
681
- log(`- Antigravity Tool Schemas: ${existing.details.schemas.exists ? "PRESENT (6 Schemas)" : "MISSING"}`);
635
+ log(`- Antigravity Tool Schemas: ${existing.details.schemas.exists ? "PRESENT (5 Schemas)" : "MISSING"}`);
682
636
  log(`- RTK Command Optimizer: ${existing.details.rtk ? "INSTALLED (" + existing.details.rtk.version + ")" : "MISSING"}`);
683
637
 
684
638
  log(`\nOverall Status: ${existing.isComplete ? "HEALTHY & ACTIVE 🛡️" : existing.isInstalled ? "PARTIAL INSTALLATION" : "NOT INSTALLED"}\n`);
@@ -92,6 +92,7 @@ export async function ensureRtkInstalled(options = {}) {
92
92
  const log = options.silent ? () => {} : console.log;
93
93
  const existing = options.existingRtk || findExistingRtk(options);
94
94
  if (existing) {
95
+ ensureRtkConfigured(options);
95
96
  log(`✓ RTK is already installed and verified: ${existing.version} (${existing.path})`);
96
97
  return existing;
97
98
  }
@@ -160,10 +161,75 @@ export async function ensureRtkInstalled(options = {}) {
160
161
  throw new Error(`RTK installation verification failed: could not run ${targetExe}`);
161
162
  }
162
163
 
164
+ ensureRtkConfigured(options);
165
+
163
166
  log(`✓ Successfully installed and verified RTK: ${verified.version} at ${verified.path}`);
164
167
  return verified;
165
168
  }
166
169
 
170
+ export function getRtkConfigPath() {
171
+ const profileHome = process.env.AGY_TEST_PROFILE_DIR;
172
+ if (profileHome) {
173
+ if (os.platform() === "win32") {
174
+ return path.join(profileHome, "AppData", "Roaming", "rtk", "config.toml");
175
+ }
176
+ return path.join(profileHome, ".config", "rtk", "config.toml");
177
+ }
178
+ if (os.platform() === "win32") {
179
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
180
+ return path.join(appData, "rtk", "config.toml");
181
+ }
182
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
183
+ return path.join(configHome, "rtk", "config.toml");
184
+ }
185
+
186
+ export function ensureRtkConfigured(options = {}) {
187
+ const log = options.silent ? () => {} : console.log;
188
+ const configPath = getRtkConfigPath();
189
+ const configDir = path.dirname(configPath);
190
+
191
+ try {
192
+ if (!fs.existsSync(configDir)) {
193
+ fs.mkdirSync(configDir, { recursive: true });
194
+ }
195
+
196
+ if (!fs.existsSync(configPath)) {
197
+ const defaultContent = `[hooks]\nsuppress_hook_warning = true\n`;
198
+ fs.writeFileSync(configPath, defaultContent, "utf-8");
199
+ log(`✓ Initialized RTK configuration with suppress_hook_warning = true at ${configPath}`);
200
+ } else {
201
+ let content = fs.readFileSync(configPath, "utf-8");
202
+ if (/suppress_hook_warning\s*=\s*false/i.test(content)) {
203
+ content = content.replace(/suppress_hook_warning\s*=\s*false/i, "suppress_hook_warning = true");
204
+ fs.writeFileSync(configPath, content, "utf-8");
205
+ log(`✓ Updated RTK configuration to suppress_hook_warning = true in ${configPath}`);
206
+ } else if (!/suppress_hook_warning/i.test(content)) {
207
+ if (/\[hooks\]/i.test(content)) {
208
+ content = content.replace(/\[hooks\]/i, "[hooks]\nsuppress_hook_warning = true");
209
+ } else {
210
+ content = (content.trim() + "\n\n[hooks]\nsuppress_hook_warning = true\n").trimStart();
211
+ }
212
+ fs.writeFileSync(configPath, content, "utf-8");
213
+ log(`✓ Configured suppress_hook_warning = true in ${configPath}`);
214
+ }
215
+ }
216
+ } catch (err) {
217
+ if (!options.silent) {
218
+ console.warn(`[WARN] Could not standardize RTK config: ${err.message}`);
219
+ }
220
+ }
221
+
222
+ ensureGitLineEndingConfigured(options);
223
+ }
224
+
225
+ export function ensureGitLineEndingConfigured(options = {}) {
226
+ const log = options.silent ? () => {} : console.log;
227
+ try {
228
+ execSync("git config --global core.safecrlf false", { windowsHide: true, stdio: "ignore" });
229
+ log("✓ Standardized git core.safecrlf = false to suppress line-ending warning noise");
230
+ } catch {}
231
+ }
232
+
167
233
  // Auto-run if executed directly
168
234
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
169
235
  ensureRtkInstalled().catch((err) => {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-user state location for Agy-Context-Saver (governance ledger, RTK rewrite cache).
3
+ *
4
+ * State lives in a folder owned by this user, never in the shared os.tmpdir():
5
+ * Windows: %LOCALAPPDATA%\agy-context-saver
6
+ * POSIX: $XDG_STATE_HOME/agy-context-saver (default ~/.local/state/agy-context-saver)
7
+ * Under AGY_TEST_PROFILE_DIR the same layout is rooted in the test profile.
8
+ *
9
+ * The hook is mirrored into ~/.gemini/config/scripts as a standalone file, so it keeps an
10
+ * identical inline copy of getStateDir(). tests/test-hook.mjs checks that the two agree.
11
+ * No side effects on import.
12
+ */
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+
17
+ export function getStateDir() {
18
+ const profileHome = process.env.AGY_TEST_PROFILE_DIR;
19
+ if (os.platform() === "win32") {
20
+ const localAppData = profileHome
21
+ ? path.join(profileHome, "AppData", "Local")
22
+ : (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"));
23
+ return path.join(localAppData, "agy-context-saver");
24
+ }
25
+ const stateHome = profileHome
26
+ ? path.join(profileHome, ".local", "state")
27
+ : (process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"));
28
+ return path.join(stateHome, "agy-context-saver");
29
+ }
30
+
31
+ // Files that versions up to 1.7.0 left in the shared temp folder, plus their atomic-write temps.
32
+ const LEGACY_TMP_STATE = /^(agy-session-.+\.json|agy-rtk-rewrite-cache\.json)(\..+\.tmp)?$/;
33
+
34
+ /** Removes state files that older versions wrote to os.tmpdir(). Returns the paths removed. */
35
+ export function removeLegacyTmpState() {
36
+ const removed = [];
37
+ const tmp = os.tmpdir();
38
+ let names = [];
39
+ try { names = fs.readdirSync(tmp); } catch { return removed; }
40
+ for (const name of names) {
41
+ if (!LEGACY_TMP_STATE.test(name)) continue;
42
+ const p = path.join(tmp, name);
43
+ try {
44
+ if (fs.lstatSync(p).isFile()) {
45
+ fs.unlinkSync(p);
46
+ removed.push(p);
47
+ }
48
+ } catch {}
49
+ }
50
+ return removed;
51
+ }