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/CHANGELOG.md +33 -0
- package/README.md +33 -34
- package/mcp/index.js +135 -249
- package/mcp/instructions.md +8 -6
- package/package.json +2 -3
- package/plugin.json +3 -3
- package/rules/AGENTS.md +16 -20
- package/scripts/execution-guard-hook.mjs +104 -14
- package/scripts/install-register.mjs +30 -76
- package/scripts/provision-rtk.mjs +66 -0
- package/scripts/state-dir.mjs +51 -0
- package/skills/context-governance/SKILL.md +26 -32
package/rules/AGENTS.md
CHANGED
|
@@ -30,34 +30,32 @@
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
## Layer 2: Transparent Shell Execution & Codebase Inspection
|
|
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.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
* -
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
768
|
-
//
|
|
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).
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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.
|
|
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 (
|
|
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
|
+
}
|