@mrclrchtr/supi-consulting 11.1.0 → 11.3.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 ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [11.3.0]
4
+
5
+ - Added package-specific release notes for bounded agent consultations.
6
+ - Consultations now use an explicit read-only Antigravity tool selection for web search, direct URL reading, and workspace inspection, with structured completion and observed source evidence.
7
+ - Follow-ups that ask the agent not to use tools no longer fail with an unverifiable answer.
8
+ - Reading cached web pages no longer reports workspace use or a misleading workspace warning.
9
+ - Requires Antigravity CLI `1.4.9` or newer. Start a new Consultation after upgrading: existing default-agent Conversation Handles cannot gain the new tools; they are rejected before process startup, retired, and recorded as `info`-level `consultation.rejected` Debug events instead of errors.
package/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - Keep agent-specific models, authentication, process code, protocol parsing, and tool-activity classification in `src/agents/antigravity/`. Shared workflow uses the private interface in `src/agents/types.ts`.
10
10
  - Keep each Model Catalogue immutable until the next session start or reload. Register `consulting_run` only after agent discovery succeeds.
11
11
  - Require explicit agent, model, and Workspace Access for a new Consultation. Keep `new` and `continue` exact-one; follow-ups inherit all selections and their working directory.
12
- - Acquire a handle before process work. Report process start, including hook probes, through the adapter interface. Reject overlap before startup; retire only after a started follow-up fails or is canceled.
12
+ - Acquire a handle before process work. Report process start, including hook probes, through the adapter interface. Reject overlap before startup; retire after a started follow-up fails or is canceled, or when the adapter rejects incompatible continuation state (which can never succeed).
13
13
  - Keep raw conversation IDs opaque to shared policy. Persist only new consulting handle records and results; old Antigravity state has no migration or legacy read path.
14
14
  - Use `<PI agent dir>/supi/consulting/antigravity/` for Antigravity state. Leave the old `supi/antigravity/` tree untouched.
15
15
  - Use the Isolated Antigravity Home for every `agy` process. On macOS, create and unlock its private keychain, exposed as the login keychain, before startup. Do not copy the normal profile, credentials, or PI provider environment.
package/README.md CHANGED
@@ -44,7 +44,7 @@ cd "<consultation-workspace>" &&
44
44
  HOME="<isolated-home>" AGY_CLI_DISABLE_AUTO_UPDATE=true agy
45
45
  ```
46
46
 
47
- On macOS, the adapter creates and unlocks a private keychain inside the Isolated Antigravity Home. It exposes that keychain as the login keychain without prompting for the normal keychain password. Run the displayed command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.1.24`.
47
+ On macOS, the adapter creates and unlocks a private keychain inside the Isolated Antigravity Home. It exposes that keychain as the login keychain without prompting for the normal keychain password. Run the displayed command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.4.9`.
48
48
 
49
49
  If macOS displays `antigravity.` in a warning, the final `.` is sentence punctuation, not part of a keychain name.
50
50
 
@@ -65,7 +65,7 @@ A new Consultation requires the agent, an available model, and Workspace Access:
65
65
 
66
66
  The Model Catalogue is the intersection of the curated Antigravity models and the models available to the signed-in account. It stays fixed until the next session start or reload. There is no default model or automatic agent fallback.
67
67
 
68
- Set `workspace` to `true` only when the Consultation needs repository evidence. This exposes the current PI workspace. Set it to `false` to use the empty Consultation Workspace.
68
+ Set `workspace` to `true` only when the Consultation needs repository evidence. This exposes the current PI workspace. Set it to `false` to use the empty Consultation Workspace. Explicitly configured additional read-only directories remain accessible with either selection.
69
69
 
70
70
  A follow-up supplies only a returned Conversation Handle:
71
71
 
@@ -76,38 +76,92 @@ A follow-up supplies only a returned Conversation Handle:
76
76
  }
77
77
  ```
78
78
 
79
- Supply exactly one of `new` and `continue`. Follow-ups retain the original agent, model, working directory, and Workspace Access. A handle is opaque and branch-aware. It is retired if a follow-up fails or is canceled after process work starts. Overlapping follow-ups on the same handle are rejected.
79
+ Supply exactly one of `new` and `continue`. Follow-ups retain the original agent, model, working directory, and Workspace Access. A handle is opaque and branch-aware. It is retired if a follow-up fails or is canceled after process work starts, or if its continuation state is incompatible. Overlapping follow-ups on the same handle are rejected.
80
80
 
81
81
  Results report web use, workspace use, observed and claimed references, warnings, token use, and the Conversation Handle. An answer can claim a reference without matching tool activity; SuPi labels that reference as claimed, not observed.
82
82
 
83
+ ### Inspection tools and existing handles
84
+
85
+ New Antigravity conversations use a package-owned temporary agent definition with explicit web search (`search_web`), direct URL reading (`read_url_content`), read-only file inspection (`view_file`, `list_dir`, `find_by_name`, `grep_search`), and structured completion (`finish`). The default Antigravity agent's tool selection is not used. A permission such as `read_url(*)` authorizes access; it does not itself make a URL-reading tool available.
86
+
87
+ The definition lives in the Isolated Antigravity Home under a random name to avoid workspace-agent name collisions. SuPi removes it after the process, including ordinary failures, timeouts, and cancellation. A hard process crash can leave a temporary definition behind. Follow-ups resume the saved conversation without a new agent override; Antigravity retains the original definition even after its file is removed.
88
+
89
+ **Start a new Consultation after upgrading from the default-agent integration.** Old Conversation Handles cannot acquire the inspection tools by resuming. SuPi rejects legacy or incompatible profile continuations before starting any Antigravity process, retires the handle, and asks for a new Consultation. Profile compatibility is tracked privately; the calling tool's input shape is unchanged.
90
+
91
+ Follow-up prompts get one fixed line asking the agent to end with `finish` using the current schema. Without it, a follow-up such as "do not use any tools" can skip `finish`, and Antigravity then replays the previous turn's answer, which SuPi rejects as unbound.
92
+
93
+ Antigravity caches fetched pages inside its own conversation state and may read them back with `view_file`. File activity whose path resolves outside the selected workspace and configured read-only directories is treated as agent-internal: it does not count as workspace use or workspace evidence. File calls without a classifiable path remain counted.
94
+
95
+ Web activity alone does not prove a cited page was read. For research, the agent is instructed to search for sources and directly read relevant pages before citing them. SuPi still classifies references from observed activity, not from the agent's claims. The CLI's startup tool inventory is not treated as proof that a tool is active for the selected agent.
96
+
83
97
  Use `/supi-settings` → **Consulting** to disable the tool. Disabling it skips availability checks and removes `consulting_run` from the active tool list.
84
98
 
99
+ ## Additional read-only directories
100
+
101
+ Use `/supi-settings` → **Consulting** → **Additional read-only directories** to enter comma-separated absolute directory paths. The default is an empty list. No shell expansion or glob matching is performed. For paths containing commas, edit the JSON configuration instead.
102
+
103
+ Global configuration lives at `~/.pi/agent/supi/config.json`; project configuration lives at `.pi/supi/config.json`:
104
+
105
+ ```json
106
+ {
107
+ "consulting": {
108
+ "additionalReadOnlyDirectories": ["/tmp/octalog-filter-previews"]
109
+ }
110
+ }
111
+ ```
112
+
113
+ - Project values replace the global list, rather than extending it. An explicit `[]` removes inherited grants; Inherit/Reset deletes the scoped key. Untrusted project configuration cannot grant access; global grants still apply.
114
+ - Directories must exist and be accessible when a Consultation starts. Symlinks are resolved and duplicate roots are removed, so `/tmp` resolves to `/private/tmp` on macOS without needing both entries.
115
+ - Settings are read from the current Pi workspace before each new Consultation and follow-up. Changes apply to the next invocation without reloading. They do not modify an already-running process.
116
+ - The directories are registered with Antigravity per process using `--add-dir`, not persisted as permission grants in the shared profile. The fixed write, command, MCP, and URL-execution denials remain in force; `allowNonWorkspaceAccess` stays `false`.
117
+ - Configured directories are available even with `workspace: false`. Prefer task-specific artifact folders over all of `/tmp`, which may contain unrelated sensitive files. This setting does not authorize shell-based browser automation.
118
+ - Reads from extra directories can contribute inspection activity, but references outside the selected workspace are not reported as validated workspace-relative evidence.
119
+
120
+ Directory registration and deny-rule precedence follow Antigravity's [CLI reference](https://antigravity.google/docs/cli/reference/) and [permissions reference](https://antigravity.google/docs/permissions/). These are Antigravity policy restrictions, not a new operating-system security boundary.
121
+
85
122
  ## Failure diagnostics
86
123
 
87
124
  Execution failures report a fixed reason when the adapter can identify it:
88
125
 
89
- - Missing final result, missing structured answer, or answer schema mismatch.
126
+ - Missing final result, missing structured answer, answer schema mismatch, or an answer not bound to the current request.
90
127
  - Unsuccessful final result or data received after the final result.
91
128
  - Malformed JSON, a non-object event, or an output limit exceeded.
92
129
  - Missing conversation identifier, process failure, timeout, or cancellation.
93
130
 
94
- An observed tool permission denial is reported separately. It does not prove the cause of a later failure. A denial does not fail a Consultation that returns a valid answer. Workspace Access does not grant access to files outside the selected workspace.
131
+ Each invocation adds a unique response token to its private answer schema and validates the returned token locally before accepting the answer. The token is removed from public results and excluded from failure diagnostics. This prevents a resumed Antigravity conversation's previous structured answer from being accepted as the current result. Missing or mismatched tokens fail with `unbound-answer`; plain-text answers are not substituted for the structured contract. A started follow-up that fails retires its Conversation Handle; start a new Consultation.
95
132
 
96
- When SuPi Debug is enabled, failed executions emit one `consulting/consultation.failed` event at `error` level. Cancellations emit `consulting/consultation.cancelled` at `info` level. Use the `debug` tool:
133
+ An observed tool permission denial is reported separately. It does not prove the cause of a later failure. A denial does not fail a Consultation that returns a valid answer. Workspace Access alone does not grant access to files outside the selected workspace; configure additional read-only directories explicitly.
134
+
135
+ Failure messages returned to the calling agent include the observed terminal status and denial counts by fixed operation category (`command`, `file-read`, `file-write`, `web-read`, `browser`, `mcp`, or `other`). They distinguish a successful agent terminal status from a usable final answer and recommend permitted inspection or a limited/blocked report rather than repeating denied actions unchanged. This feedback remains available when Debug is disabled.
136
+
137
+ When SuPi Debug is enabled, failed executions emit one `consulting/consultation.failed` event at `error` level. Cancellations emit `consulting/consultation.cancelled` at `info` level. Rejected incompatible Conversation Handles emit `consulting/consultation.rejected` at `info` level; no process was started for them. Use the `debug` tool:
97
138
 
98
139
  ```json
99
140
  { "source": "consulting", "level": "error", "limit": 10 }
100
141
  ```
101
142
 
102
- Omit `level` to include cancellations. Add `sessionFile` to inspect events persisted in a saved PI session. A response aborted before the adapter starts has no Consulting event.
143
+ Omit `level` to include cancellations and rejected handles. Add `sessionFile` to inspect events persisted in a saved PI session. A response aborted before the adapter starts has no Consulting event.
144
+
145
+ Events contain the execution phase, timing, selected model, workspace-access and follow-up flags, failure category and reason, and available event counts, terminal-state flags, permission-denial count, and exit code. `permissionDenialsByOperation` breaks down deduplicated denials by fixed category; unknown tool names become `other`. `deniedFileAccessOutsideReadRoots` counts denied file operations whose requested paths resolve outside the selected directory and configured additional read-only directories, following symlinks and existing parents of missing targets. Unavailable path information, dangling symlink destinations, and paths containing parent traversal (`..`) remain unclassified rather than risking an incorrect outside-root claim. This location observation does not identify which rule caused a denial.
146
+
147
+ Antigravity's terminal `denied_actions` summary can establish a denial even when its detailed tool-error update is absent. It establishes a minimum of one observation per operation category, not additional calls on top of already-correlated denials. Summary entries have no tool-call IDs or file parameters, so the adapter does not attribute them to nearby or unfinished file requests.
148
+
149
+ Failures also include bounded requested file-target metadata in both Debug and caller-visible feedback:
150
+
151
+ - `deniedResources` lists at most eight distinct `{ operation, requestedPath }` references for known file operations observed as denied. Targets come from structured parameters on the denied event or earlier updates correlated by tool-call ID, never from error text, command parsing, nearby calls, or transcripts.
152
+ - `requestedPath` preserves the supplied path, including significant whitespace. It is not a canonical filesystem path or the permission checker's resolved target, and does not identify a permanent path blacklist or the denying rule. Paths are limited to 1,024 UTF-16 characters; longer paths are unavailable rather than silently shortened.
153
+ - `deniedResourcesOmitted` counts file-denial observations whose known targets were omitted because the list was full, not unique omitted paths. Repeated denials for listed targets do not duplicate entries.
154
+ - `deniedResourcePathsUnavailable` counts known file-denial observations without an available, bounded structured target. Missing correlation or parameters remain unknown; the adapter does not infer a path from prose. Explicit replacement targets supersede earlier metadata, including when the replacement is invalid or overlong; only omitted path parameters preserve an earlier target.
155
+ - `unfinishedResources` separately lists at most eight distinct requested targets from ID-correlated file calls with no observed terminal outcome. These are **unknown outcomes, not confirmed denials**, even if a terminal summary reports the same operation category. Successful and already-denied calls are excluded. `unfinishedResourcesOmitted` counts capacity-omitted observations; `unfinishedResourcePathsUnavailable` counts observations lacking a usable target. The same path limits, replacement semantics, and deduplication rules apply as for denied resources.
156
+ - Caller-visible references are JSON-quoted, with terminal control characters and line separators escaped. **Denied and unfinished requested paths can contain sensitive metadata and are exposed to the calling agent and persisted with enabled Debug events.**
103
157
 
104
- Events contain the execution phase, timing, selected model, workspace-access and follow-up flags, failure category and reason, and available event counts, terminal-state flags, permission-denial count, and exit code. They exclude prompts, answers, resource paths, conversation IDs, raw stdout, stderr, tool payloads, and error causes, even when raw Debug access is enabled. The event's `cwd` identifies the selected working directory. Failure messages remain specific when Debug is disabled.
158
+ Events otherwise exclude prompts, answers, arbitrary tool names, raw stdout, stderr, complete tool payloads, and error causes, even when raw Debug access is enabled. The event's `cwd` identifies the selected working directory. Successful Consultations do not gain a failure event merely because an action was denied.
105
159
 
106
160
  ## Security limits
107
161
 
108
- The Antigravity adapter enforces its Inspection Permission Set in the Isolated Antigravity Home. It allows `read_url(*)` and denies known file writes, commands, unsandboxed actions, MCP, and URL execution. These rules are **not an operating-system sandbox**.
162
+ The Antigravity adapter enforces its Inspection Permission Set in the Isolated Antigravity Home. It allows `read_url(*)` and denies known file writes, commands, MCP, and URL execution. The `command(*)` rule covers commands inside and outside Antigravity's terminal sandbox; the obsolete, ignored `unsandboxed(*)` rule is not emitted. These rules are **not an operating-system sandbox**.
109
163
 
110
- For workspace Consultations, project-local hooks can run commands, read the Antigravity transcript, and cause side effects outside these permission rules. The adapter probes hook state before the paid process and warns when hooks are active or unknown.
164
+ For Consultations with Workspace Access or additional read-only directories, project-local hooks can run commands, read the Antigravity transcript, and cause side effects outside these permission rules. The adapter probes hook state with the additional roots before the paid process and warns when hooks are active or unknown. Directory grants are intended for inspection artifacts, not untrusted project customizations.
111
165
 
112
166
  `read_url(*)` can reach local, private-network, and link-local endpoints that Antigravity accepts. It does not enforce public-internet-only access.
113
167
 
@@ -123,4 +177,4 @@ The live probe requires separate approval and sign-in. It never runs in normal t
123
177
  SUPI_CONSULTING_LIVE=1 pnpm exec jiti packages/supi-consulting/scripts/live-probe.ts
124
178
  ```
125
179
 
126
- It uses the isolated profile and checks web evidence, follow-up context, workspace evidence, and byte-identical fixture state.
180
+ It uses the isolated profile and requires successful web search and direct URL reading, an observed source matching the fetched URL, and a schema-validated follow-up that retains context and the URL reader after the temporary definition has been removed. It also checks observed workspace evidence and byte-identical fixture state. The direct-reader checks use the private process interface; workspace checks use the Consulting Agent adapter. No paid capability checks run during normal discovery.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,110 @@
1
+ // Session-provided Web tools for SuPi child sessions (Agent Runs and Reviewer Sessions).
2
+ //
3
+ // The loaded supi-web extension answers a synchronous request on the parent event bus.
4
+ // Consumers never depend on supi-web; when it is absent or provides no tools, the
5
+ // request returns undefined and the child simply lacks Web tools.
6
+
7
+ import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
8
+
9
+ /** Web tools that child sessions may receive when the containing session provides them. */
10
+ export const CHILD_WEB_TOOL_NAMES = [
11
+ "web_fetch_md",
12
+ "web_search",
13
+ "web_docs_search",
14
+ "web_docs_fetch",
15
+ ] as const;
16
+
17
+ /** One session-provided Web tool name. */
18
+ export type ChildWebToolName = (typeof CHILD_WEB_TOOL_NAMES)[number];
19
+
20
+ /**
21
+ * Web tools currently callable in the containing session plus a child-only extension
22
+ * factory that registers them. The factory must not capture the parent session; it may
23
+ * capture plain data such as the containing session's configuration directory.
24
+ */
25
+ export interface ChildWebToolProvision {
26
+ readonly toolNames: readonly ChildWebToolName[];
27
+ /** Register the requested subset of `toolNames` in a child session. */
28
+ readonly createFactory: (toolNames: readonly ChildWebToolName[]) => ExtensionFactory;
29
+ }
30
+
31
+ /** Plain containing-session data supplied with a child Web tool request. */
32
+ export interface ChildWebToolsRequestContext {
33
+ /**
34
+ * Working directory whose SuPi configuration governs Web settings such as Firecrawl.
35
+ * Children with a different cwd (for example a frozen Review Workspace) keep this policy.
36
+ */
37
+ readonly configCwd: string;
38
+ }
39
+
40
+ type EventBusLike = Pick<ExtensionAPI["events"], "on" | "emit">;
41
+
42
+ const REQUEST_CHANNEL = "supi:child-web-tools:request";
43
+ const RESPONSE_CHANNEL = "supi:child-web-tools:response";
44
+
45
+ interface ChildWebToolsRequest extends ChildWebToolsRequestContext {
46
+ readonly requestId: string;
47
+ }
48
+
49
+ interface ChildWebToolsResponse {
50
+ readonly requestId: string;
51
+ readonly provision: ChildWebToolProvision;
52
+ }
53
+
54
+ let nextRequestId = 0;
55
+
56
+ /**
57
+ * Answer child Web tool requests on the parent event bus. The callback must be synchronous
58
+ * because Pi's event bus does not await listeners. Returns an unsubscribe function.
59
+ */
60
+ export function provideChildWebTools(
61
+ events: EventBusLike,
62
+ resolve: (context: ChildWebToolsRequestContext) => ChildWebToolProvision | undefined,
63
+ ): () => void {
64
+ return events.on(REQUEST_CHANNEL, (data) => {
65
+ const request = data as Partial<ChildWebToolsRequest> | undefined;
66
+ if (typeof request?.requestId !== "string" || typeof request.configCwd !== "string") return;
67
+ const { requestId, configCwd } = request;
68
+ const provision = resolve({ configCwd });
69
+ if (!provision || provision.toolNames.length === 0) return;
70
+ events.emit(RESPONSE_CHANNEL, { requestId, provision } satisfies ChildWebToolsResponse);
71
+ });
72
+ }
73
+
74
+ /** Capture the Web tools the containing session currently provides, if any. */
75
+ export function requestChildWebTools(
76
+ events: EventBusLike,
77
+ context: ChildWebToolsRequestContext,
78
+ ): ChildWebToolProvision | undefined {
79
+ const requestId = `child-web-${process.pid}-${++nextRequestId}`;
80
+ let provision: ChildWebToolProvision | undefined;
81
+ const unsubscribe = events.on(RESPONSE_CHANNEL, (data) => {
82
+ const response = data as Partial<ChildWebToolsResponse> | undefined;
83
+ if (response?.requestId === requestId && response.provision) provision = response.provision;
84
+ });
85
+ try {
86
+ events.emit(REQUEST_CHANNEL, { requestId, ...context } satisfies ChildWebToolsRequest);
87
+ } finally {
88
+ unsubscribe();
89
+ }
90
+ return provision;
91
+ }
92
+
93
+ /**
94
+ * Return the Web tools callable in a session under Pi's exposure rules: active `direct`
95
+ * tools, plus registered `codemode` and `deferred` tools. `model-only` and `hidden`
96
+ * tools are not callable.
97
+ */
98
+ export function callableChildWebToolNames(
99
+ pi: Pick<ExtensionAPI, "getAllTools" | "getActiveTools">,
100
+ ): ChildWebToolName[] {
101
+ const active = new Set(pi.getActiveTools());
102
+ const exposures = new Map(
103
+ pi.getAllTools().map((tool) => [tool.name, tool.exposure ?? "direct"] as const),
104
+ );
105
+ return CHILD_WEB_TOOL_NAMES.filter((name) => {
106
+ const exposure = exposures.get(name);
107
+ if (exposure === "direct") return active.has(name);
108
+ return exposure === "codemode" || exposure === "deferred";
109
+ });
110
+ }
@@ -1,5 +1,16 @@
1
1
  // supi-core session domain — session utilities and registries.
2
2
 
3
+ export type {
4
+ ChildWebToolName,
5
+ ChildWebToolProvision,
6
+ ChildWebToolsRequestContext,
7
+ } from "./child-web-tools.ts";
8
+ export {
9
+ CHILD_WEB_TOOL_NAMES,
10
+ callableChildWebToolNames,
11
+ provideChildWebTools,
12
+ requestChildWebTools,
13
+ } from "./child-web-tools.ts";
3
14
  export { createRegistry, createSessionStateRegistry } from "./registry-utils.ts";
4
15
  export type {
5
16
  SessionCapabilitySkill,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-consulting",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Bounded consultations with explicit agent and workspace selection and observed evidence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  ],
30
30
  "type": "module",
31
31
  "files": [
32
+ "CHANGELOG.md",
32
33
  "src/**/*.ts",
33
34
  "scripts/live-probe.ts",
34
35
  "README.md",
@@ -37,7 +38,7 @@
37
38
  "docs/adr/*.md"
38
39
  ],
39
40
  "dependencies": {
40
- "@mrclrchtr/supi-core": "11.1.0"
41
+ "@mrclrchtr/supi-core": "11.3.0"
41
42
  },
42
43
  "bundledDependencies": [
43
44
  "@mrclrchtr/supi-core"
@@ -4,11 +4,20 @@ import { tmpdir } from "node:os";
4
4
  import { join, relative } from "node:path";
5
5
  import { createAntigravityAdapter } from "../src/agents/antigravity/adapter.ts";
6
6
  import { getIsolatedAntigravityPaths } from "../src/agents/antigravity/isolated-home.ts";
7
- import { isHttpUrl, isSafeWorkspacePath } from "../src/tool/consulting_run/evidence.ts";
7
+ import { runAntigravityConversation } from "../src/agents/antigravity/process/runner.ts";
8
+ import { ANTIGRAVITY_ANSWER_SCHEMA } from "../src/agents/antigravity/structured-output.ts";
9
+ import type { AntigravityExecutionFacts } from "../src/agents/antigravity/types.ts";
10
+ import {
11
+ classifyAnswerEvidence,
12
+ isHttpUrl,
13
+ isSafeWorkspacePath,
14
+ } from "../src/tool/consulting_run/evidence.ts";
15
+ import type { ConsultationExecutionFacts } from "../src/types.ts";
8
16
 
9
17
  const LIVE_ENVIRONMENT = "SUPI_CONSULTING_LIVE";
10
18
  const MODEL = "gemini-3.8-flash-low";
11
19
  const CONTEXT_TOKEN = "supi-consulting-context-token";
20
+ const SOURCE_URL = "https://nodejs.org/api/child_process.html";
12
21
 
13
22
  async function main(): Promise<void> {
14
23
  if (process.env[LIVE_ENVIRONMENT] !== "1") {
@@ -21,23 +30,60 @@ async function main(): Promise<void> {
21
30
  throw new Error("The selected model is not available to the isolated Antigravity account.");
22
31
  }
23
32
  const consultationWorkspace = await realpath(paths.consultationWorkspace);
24
- const first = await adapter.execute({
25
- prompt: `Remember the exact token ${CONTEXT_TOKEN}. Name one current official Node.js documentation page about subprocess cancellation and cite it.`,
33
+ // The process interface exposes successful tool names without adding them to the
34
+ // public Consultation contract. Mere web activity or claimed URLs cannot pass.
35
+ const processOptions = {
36
+ paths,
26
37
  model: MODEL,
27
- canonicalWorkingDirectory: consultationWorkspace,
28
- workspaceAccess: false,
38
+ cwd: consultationWorkspace,
39
+ schema: ANTIGRAVITY_ANSWER_SCHEMA as Record<string, unknown>,
40
+ };
41
+ const first = await runAntigravityConversation({
42
+ ...processOptions,
43
+ prompt: `Remember the exact token ${CONTEXT_TOKEN}. Use search_web to find official Node.js subprocess cancellation documentation, then use read_url_content to read ${SOURCE_URL}. Explain cancellation briefly and cite that exact fetched URL.`,
29
44
  });
30
- requireWebEvidence(first);
45
+ if (!first.successfulToolNames.includes("search_web")) {
46
+ throw new Error("The live probe did not observe a successful web search.");
47
+ }
48
+ requireDirectRead(first, consultationWorkspace);
31
49
 
32
- const second = await adapter.execute({
33
- prompt: `What exact context token did I give you? Answer in one sentence and include ${CONTEXT_TOKEN}.`,
50
+ // The first run has already removed its temporary profile. Resume only by ID,
51
+ // proving the CLI retains the reader and finish capability in conversation state.
52
+ const second = await runAntigravityConversation({
53
+ ...processOptions,
54
+ prompt: `Read ${SOURCE_URL} again using read_url_content and cite that exact URL. In the answer field return only the exact context token I gave you in the preceding request.`,
55
+ conversationId: first.conversationId,
56
+ });
57
+ if (second.conversationId !== first.conversationId) {
58
+ throw new Error("The live probe resumed a different conversation.");
59
+ }
60
+ requireDirectRead(second, consultationWorkspace);
61
+
62
+ // A tool-free follow-up used to skip finish and replay the previous answer.
63
+ const toolFree = await adapter.execute({
64
+ prompt: "Return only the exact context token I gave you earlier. Do not use any tools.",
34
65
  model: MODEL,
35
66
  canonicalWorkingDirectory: consultationWorkspace,
36
67
  workspaceAccess: false,
37
- continuation: first.continuation,
68
+ continuation: (
69
+ await adapter.execute({
70
+ prompt: `Remember the exact token ${CONTEXT_TOKEN}. Use read_url_content to read ${SOURCE_URL} and cite it.`,
71
+ model: MODEL,
72
+ canonicalWorkingDirectory: consultationWorkspace,
73
+ workspaceAccess: false,
74
+ })
75
+ ).continuation,
38
76
  });
39
- if (!second.answer.answer.includes(CONTEXT_TOKEN)) {
40
- throw new Error("The follow-up did not retain the Conversation Handle context.");
77
+ if (toolFree.answer.answer.trim() !== CONTEXT_TOKEN) {
78
+ throw new Error("The tool-free follow-up did not return the current context token.");
79
+ }
80
+ if (toolFree.workspaceUsed) {
81
+ throw new Error("Cached page reads were misreported as workspace use.");
82
+ }
83
+ if (second.answer.answer.trim() !== CONTEXT_TOKEN) {
84
+ throw new Error(
85
+ "The follow-up did not return the requested current-turn answer from conversation context.",
86
+ );
41
87
  }
42
88
 
43
89
  const fixture = await realpath(await mkdtemp(join(tmpdir(), "supi-consulting-live-")));
@@ -53,7 +99,7 @@ async function main(): Promise<void> {
53
99
  canonicalWorkingDirectory: fixture,
54
100
  workspaceAccess: true,
55
101
  });
56
- requireWebEvidence(workspace);
102
+ requireWebEvidence(workspace, fixture);
57
103
  requireWorkspaceEvidence(workspace, fixture);
58
104
  if (!workspace.workspaceUsed) {
59
105
  throw new Error("The workspace probe did not observe workspace activity.");
@@ -65,40 +111,42 @@ async function main(): Promise<void> {
65
111
  }
66
112
  }
67
113
 
68
- function requireWebEvidence(result: { webUsed: boolean; answer: { sources: unknown[] } }): void {
69
- if (!result.webUsed || result.answer.sources.length === 0) {
114
+ type EvidenceFacts = Pick<
115
+ ConsultationExecutionFacts,
116
+ "answer" | "observedSourceHashes" | "observedWorkspacePathHashes"
117
+ >;
118
+
119
+ function requireDirectRead(result: AntigravityExecutionFacts, workspace: string): void {
120
+ if (!result.successfulToolNames.includes("read_url_content")) {
121
+ throw new Error("The live probe did not observe a successful direct URL read.");
122
+ }
123
+ requireWebEvidence(result, workspace);
124
+ const evidence = classifyAnswerEvidence(result.answer, result, workspace);
125
+ if (!evidence.observedSources.some((source) => source.url === SOURCE_URL)) {
126
+ throw new Error("The live probe did not return the fetched URL as observed evidence.");
127
+ }
128
+ }
129
+
130
+ function requireWebEvidence(result: EvidenceFacts, workspace: string): void {
131
+ const evidence = classifyAnswerEvidence(result.answer, result, workspace);
132
+ if (evidence.observedSources.length === 0) {
70
133
  throw new Error("The live probe did not observe web evidence.");
71
134
  }
72
- if (
73
- !result.answer.sources.every(
74
- (source) => isRecord(source) && typeof source.url === "string" && isHttpUrl(source.url),
75
- )
76
- ) {
135
+ if (!result.answer.sources.every((source) => isHttpUrl(source.url))) {
77
136
  throw new Error("The live probe returned an invalid source URL.");
78
137
  }
79
138
  }
80
139
 
81
- function requireWorkspaceEvidence(
82
- result: { answer: { workspaceEvidence: unknown[] } },
83
- workspace: string,
84
- ): void {
140
+ function requireWorkspaceEvidence(result: EvidenceFacts, workspace: string): void {
141
+ const evidence = classifyAnswerEvidence(result.answer, result, workspace);
85
142
  if (
86
- result.answer.workspaceEvidence.length === 0 ||
87
- !result.answer.workspaceEvidence.every(
88
- (item) =>
89
- isRecord(item) &&
90
- typeof item.path === "string" &&
91
- isSafeWorkspacePath(item.path, workspace),
92
- )
143
+ evidence.observedWorkspaceEvidence.length === 0 ||
144
+ !result.answer.workspaceEvidence.every((item) => isSafeWorkspacePath(item.path, workspace))
93
145
  ) {
94
- throw new Error("The live probe did not return valid workspace evidence.");
146
+ throw new Error("The live probe did not return observed, valid workspace evidence.");
95
147
  }
96
148
  }
97
149
 
98
- function isRecord(value: unknown): value is Record<string, unknown> {
99
- return typeof value === "object" && value !== null && !Array.isArray(value);
100
- }
101
-
102
150
  async function digestTree(directory: string): Promise<string> {
103
151
  const hash = createHash("sha256");
104
152
  await digestDirectory(directory, directory, hash);
@@ -9,6 +9,7 @@ import type {
9
9
  } from "../types.ts";
10
10
  import { isWebToolName, isWorkspaceToolName } from "./activity.ts";
11
11
  import { discoverAntigravityAvailability } from "./availability.ts";
12
+ import { INSPECTION_AGENT_FINGERPRINT } from "./inspection-agent.ts";
12
13
  import {
13
14
  getIsolatedAntigravityPaths,
14
15
  type IsolatedAntigravityPaths,
@@ -22,7 +23,12 @@ import { ANTIGRAVITY_ANSWER_SCHEMA } from "./structured-output.ts";
22
23
  const MAX_RETAINED_REFERENCE_HASHES = 512;
23
24
  const MAX_WARNING_COUNT = 24;
24
25
  const MAX_WARNING_CHARS = 500;
25
- const CONTINUATION_PREFIX = "agy1.";
26
+ // A resumed CLI retains its original profile and ignores --agent overrides.
27
+ // Reject pre-profile conversations and changed contracts before any process work.
28
+ const CONTINUATION_PREFIX = `agy2.${INSPECTION_AGENT_FINGERPRINT}.`;
29
+ // Follow-ups saying "do not use any tools" can skip finish; agy then replays the
30
+ // previous turn's structured answer, which the response token rejects. Tested live.
31
+ const FOLLOW_UP_RESPONSE_CONTRACT = "\n\n[Always end with finish using the current schema.]";
26
32
 
27
33
  type ExecutionPhase = "preparation" | "hook-probe" | "conversation" | "validation";
28
34
 
@@ -78,12 +84,12 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
78
84
  });
79
85
  } catch (error) {
80
86
  const failure = safeExecutionFailure(error);
81
- const canceled = failure.kind === "cancelled";
87
+ const outcome = failureOutcome(failure.kind);
82
88
  timer.finish(
83
89
  () => ({
84
90
  source: "consulting",
85
- level: canceled ? "info" : "error",
86
- category: canceled ? "consultation.cancelled" : "consultation.failed",
91
+ level: outcome.level,
92
+ category: outcome.category,
87
93
  message: failure.message,
88
94
  cwd: options.canonicalWorkingDirectory,
89
95
  data: {
@@ -111,18 +117,23 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
111
117
  ? decodeContinuation(options.continuation)
112
118
  : undefined;
113
119
  await prepareIsolatedAntigravityHome(this.#paths);
114
- if (options.workspaceAccess) onPhase("hook-probe");
115
- const hookWarning = options.workspaceAccess
120
+ const inspectHooks =
121
+ options.workspaceAccess || (options.additionalReadOnlyDirectories?.length ?? 0) > 0;
122
+ if (inspectHooks) onPhase("hook-probe");
123
+ const hookWarning = inspectHooks
116
124
  ? await inspectProjectHooks(this.#paths, options, options.onWarning)
117
125
  : undefined;
118
126
  onPhase("conversation");
119
127
  const rawFacts = await runAntigravityConversation({
120
128
  paths: this.#paths,
121
129
  cwd: options.canonicalWorkingDirectory,
122
- prompt: options.prompt,
130
+ prompt: expectedConversationId
131
+ ? `${options.prompt}${FOLLOW_UP_RESPONSE_CONTRACT}`
132
+ : options.prompt,
123
133
  model: options.model,
124
134
  ...(expectedConversationId ? { conversationId: expectedConversationId } : {}),
125
135
  ...(options.workspaceAccess ? { workspaceDirectory: options.canonicalWorkingDirectory } : {}),
136
+ additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
126
137
  schema: ANTIGRAVITY_ANSWER_SCHEMA as Record<string, unknown>,
127
138
  signal: options.signal,
128
139
  onActivity: options.onProgress,
@@ -161,6 +172,7 @@ async function inspectProjectHooks(
161
172
  const probe = await probeProjectHooks({
162
173
  paths,
163
174
  cwd: options.canonicalWorkingDirectory,
175
+ additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
164
176
  signal: options.signal,
165
177
  onProcessStart: options.onProcessStart,
166
178
  });
@@ -216,6 +228,19 @@ function decodeContinuation(value: string): string {
216
228
  return conversationId;
217
229
  }
218
230
 
231
+ /**
232
+ * Cancellations and stale handles are expected outcomes, not execution failures:
233
+ * an incompatible continuation is rejected before any process starts.
234
+ */
235
+ function failureOutcome(kind: ConsultingAgentFailureKind): {
236
+ level: "info" | "error";
237
+ category: string;
238
+ } {
239
+ if (kind === "cancelled") return { level: "info", category: "consultation.cancelled" };
240
+ if (kind === "invalid-continuation") return { level: "info", category: "consultation.rejected" };
241
+ return { level: "error", category: "consultation.failed" };
242
+ }
243
+
219
244
  /** Keep only fixed reasons and safe facts, including preparation and callback failures. */
220
245
  function safeExecutionFailure(error: unknown): ConsultingAgentError {
221
246
  if (error instanceof ConsultingAgentError) {
@@ -7,8 +7,8 @@ import { CURATED_MODELS, type CuratedModel, isCuratedModel } from "./models.ts";
7
7
  import type { AntigravityProbeResult } from "./process/runner.ts";
8
8
  import { AntigravityProcessError, runAntigravityProbe } from "./process/runner.ts";
9
9
 
10
- /** Minimum supported Antigravity CLI version. */
11
- export const MINIMUM_ANTIGRAVITY_VERSION = Object.freeze({ major: 1, minor: 1, patch: 24 });
10
+ /** Minimum CLI qualified for explicit inspection profiles, finish, and profile-free resumes. */
11
+ export const MINIMUM_ANTIGRAVITY_VERSION = Object.freeze({ major: 1, minor: 4, patch: 9 });
12
12
  const MAX_DISCOVERY_OUTPUT_BYTES = 128 * 1024;
13
13
 
14
14
  /** Parsed Antigravity CLI version. */