@stigmer/sdk 3.0.9-dev.20260615153829 → 3.1.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/execution/__tests__/approval-provenance.test.d.ts +2 -0
- package/execution/__tests__/approval-provenance.test.d.ts.map +1 -0
- package/execution/__tests__/approval-provenance.test.js +42 -0
- package/execution/__tests__/approval-provenance.test.js.map +1 -0
- package/execution/__tests__/file-review-fold.corpus.test.d.ts +2 -0
- package/execution/__tests__/file-review-fold.corpus.test.d.ts.map +1 -0
- package/execution/__tests__/file-review-fold.corpus.test.js +82 -0
- package/execution/__tests__/file-review-fold.corpus.test.js.map +1 -0
- package/execution/__tests__/file-review-fold.test.d.ts +2 -0
- package/execution/__tests__/file-review-fold.test.d.ts.map +1 -0
- package/execution/__tests__/file-review-fold.test.js +184 -0
- package/execution/__tests__/file-review-fold.test.js.map +1 -0
- package/execution/__tests__/to-display-file-change.test.d.ts +2 -0
- package/execution/__tests__/to-display-file-change.test.d.ts.map +1 -0
- package/execution/__tests__/to-display-file-change.test.js +69 -0
- package/execution/__tests__/to-display-file-change.test.js.map +1 -0
- package/execution/__tests__/tool-view.file-changes.test.d.ts +2 -0
- package/execution/__tests__/tool-view.file-changes.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.file-changes.test.js +86 -0
- package/execution/__tests__/tool-view.file-changes.test.js.map +1 -0
- package/execution/__tests__/tool-view.fixtures.test.js +3 -0
- package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
- package/execution/__tests__/tool-view.output-ref.test.d.ts +2 -0
- package/execution/__tests__/tool-view.output-ref.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.output-ref.test.js +64 -0
- package/execution/__tests__/tool-view.output-ref.test.js.map +1 -0
- package/execution/__tests__/tool-view.search.test.d.ts +2 -0
- package/execution/__tests__/tool-view.search.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.search.test.js +200 -0
- package/execution/__tests__/tool-view.search.test.js.map +1 -0
- package/execution/__tests__/tool-view.shell.test.d.ts +2 -0
- package/execution/__tests__/tool-view.shell.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.shell.test.js +104 -0
- package/execution/__tests__/tool-view.shell.test.js.map +1 -0
- package/execution/approval-provenance.d.ts +30 -0
- package/execution/approval-provenance.d.ts.map +1 -0
- package/execution/approval-provenance.js +73 -0
- package/execution/approval-provenance.js.map +1 -0
- package/execution/execution-phases.d.ts +7 -0
- package/execution/execution-phases.d.ts.map +1 -0
- package/execution/execution-phases.js +21 -0
- package/execution/execution-phases.js.map +1 -0
- package/execution/file-review-fold.d.ts +44 -0
- package/execution/file-review-fold.d.ts.map +1 -0
- package/execution/file-review-fold.js +182 -0
- package/execution/file-review-fold.js.map +1 -0
- package/execution/to-display-file-change.d.ts +14 -0
- package/execution/to-display-file-change.d.ts.map +1 -0
- package/execution/to-display-file-change.js +57 -0
- package/execution/to-display-file-change.js.map +1 -0
- package/execution/tool-view.d.ts +8 -2
- package/execution/tool-view.d.ts.map +1 -1
- package/execution/tool-view.js +205 -21
- package/execution/tool-view.js.map +1 -1
- package/gen/agentexecution.d.ts +2 -1
- package/gen/agentexecution.d.ts.map +1 -1
- package/gen/agentexecution.js +8 -0
- package/gen/agentexecution.js.map +1 -1
- package/gen/mcpserver.d.ts +1 -0
- package/gen/mcpserver.d.ts.map +1 -1
- package/gen/mcpserver.js +1 -0
- package/gen/mcpserver.js.map +1 -1
- package/gen/workflowexecution.d.ts +2 -1
- package/gen/workflowexecution.d.ts.map +1 -1
- package/gen/workflowexecution.js +8 -0
- package/gen/workflowexecution.js.map +1 -1
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/package.json +2 -2
- package/src/execution/__tests__/approval-provenance.test.ts +60 -0
- package/src/execution/__tests__/file-review-fold.corpus.test.ts +127 -0
- package/src/execution/__tests__/file-review-fold.test.ts +281 -0
- package/src/execution/__tests__/to-display-file-change.test.ts +91 -0
- package/src/execution/__tests__/tool-view.file-changes.test.ts +100 -0
- package/src/execution/__tests__/tool-view.fixtures.test.ts +4 -0
- package/src/execution/__tests__/tool-view.output-ref.test.ts +76 -0
- package/src/execution/__tests__/tool-view.search.test.ts +264 -0
- package/src/execution/__tests__/tool-view.shell.test.ts +127 -0
- package/src/execution/approval-provenance.ts +80 -0
- package/src/execution/execution-phases.ts +23 -0
- package/src/execution/file-review-fold.ts +210 -0
- package/src/execution/to-display-file-change.ts +66 -0
- package/src/execution/tool-view.ts +261 -30
- package/src/gen/agentexecution.ts +7 -1
- package/src/gen/mcpserver.ts +2 -0
- package/src/gen/workflowexecution.ts +7 -1
- package/src/index.ts +12 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Covers search normalization's envelope handling: the Cursor SDK returns search
|
|
2
|
+
// results as a stringified JSON envelope (file-name search via {status,value} and
|
|
3
|
+
// grep/codebase search via {workspaceResults}), which the original parser dumped
|
|
4
|
+
// as a single fake "match". The cross-language fixture (result-views.json)
|
|
5
|
+
// asserts the happy-path `type`/`count`; these cover the `kind`/`truncated`
|
|
6
|
+
// presentation hints, the multi-workspace flatten, the content-match mapping, and
|
|
7
|
+
// the unrecognized-JSON -> json degradation that is not a shared scalar fact.
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { create, type JsonObject } from "@bufbuild/protobuf";
|
|
11
|
+
import { ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
12
|
+
import {
|
|
13
|
+
ToolCallStatus,
|
|
14
|
+
ToolKind,
|
|
15
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
import { normalizeToolResult } from "../tool-view";
|
|
17
|
+
|
|
18
|
+
function searchCall(args: Record<string, unknown>, result: string) {
|
|
19
|
+
return create(ToolCallSchema, {
|
|
20
|
+
id: "tc-search",
|
|
21
|
+
name: "Grep",
|
|
22
|
+
toolKind: ToolKind.SEARCH,
|
|
23
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
24
|
+
args: args as JsonObject,
|
|
25
|
+
result,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("normalizeToolResult — Cursor file-name search envelope", () => {
|
|
30
|
+
it("unwraps an empty {status,value} file search (the reported bug: not '1 match')", () => {
|
|
31
|
+
const view = normalizeToolResult(
|
|
32
|
+
searchCall(
|
|
33
|
+
{ pattern: "**/*.nope" },
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
status: "success",
|
|
36
|
+
value: { files: [], totalFiles: 0, clientTruncated: false, ripgrepTruncated: false },
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
expect(view.type).toBe("search");
|
|
41
|
+
if (view.type !== "search") return;
|
|
42
|
+
expect(view.kind).toBe("files");
|
|
43
|
+
expect(view.count).toBe(0);
|
|
44
|
+
expect(view.matches).toHaveLength(0);
|
|
45
|
+
expect(view.truncated).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("unwraps a non-empty file search into clickable file matches", () => {
|
|
49
|
+
const view = normalizeToolResult(
|
|
50
|
+
searchCall(
|
|
51
|
+
{ pattern: "Dockerfile" },
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
status: "success",
|
|
54
|
+
value: { files: ["Dockerfile"], totalFiles: 1, clientTruncated: false, ripgrepTruncated: false },
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
expect(view.type).toBe("search");
|
|
59
|
+
if (view.type !== "search") return;
|
|
60
|
+
expect(view.kind).toBe("files");
|
|
61
|
+
expect(view.count).toBe(1);
|
|
62
|
+
// Both file and text are set so a kind-unaware consumer still shows the path.
|
|
63
|
+
expect(view.matches[0]).toEqual({ file: "Dockerfile", text: "Dockerfile" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("carries the engine's truncation flags and authoritative totalFiles", () => {
|
|
67
|
+
const view = normalizeToolResult(
|
|
68
|
+
searchCall(
|
|
69
|
+
{ pattern: "*.ts" },
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
status: "success",
|
|
72
|
+
value: { files: ["a.ts", "b.ts"], totalFiles: 200, clientTruncated: true, ripgrepTruncated: false },
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
expect(view.type).toBe("search");
|
|
77
|
+
if (view.type !== "search") return;
|
|
78
|
+
// totalFiles is authoritative and can exceed the returned page.
|
|
79
|
+
expect(view.count).toBe(200);
|
|
80
|
+
expect(view.matches).toHaveLength(2);
|
|
81
|
+
expect(view.truncated).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("normalizeToolResult — Cursor grep workspaceResults envelope", () => {
|
|
86
|
+
it("unwraps the real {status,value:{workspaceResults}} grep shape (the reported bug)", () => {
|
|
87
|
+
// The grep payload nests workspaceResults INSIDE the {status,value} envelope —
|
|
88
|
+
// the shape the runner actually persists. The original parser only looked at
|
|
89
|
+
// the top level, so this fell through to a raw JSON dump.
|
|
90
|
+
const view = normalizeToolResult(
|
|
91
|
+
searchCall(
|
|
92
|
+
{ pattern: "pipeline" },
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
status: "success",
|
|
95
|
+
value: {
|
|
96
|
+
workspaceResults: {
|
|
97
|
+
"/work/demo": { type: "files", output: { files: [], count: 0 } },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
expect(view.type).toBe("search");
|
|
104
|
+
if (view.type !== "search") return;
|
|
105
|
+
expect(view.kind).toBe("files");
|
|
106
|
+
expect(view.count).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("still reads an un-enveloped top-level workspaceResults (defensive)", () => {
|
|
110
|
+
const view = normalizeToolResult(
|
|
111
|
+
searchCall(
|
|
112
|
+
{ pattern: "pipeline" },
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
workspaceResults: { "/work/demo": { type: "files", output: { files: [], count: 0 } } },
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
expect(view.type).toBe("search");
|
|
119
|
+
if (view.type !== "search") return;
|
|
120
|
+
expect(view.kind).toBe("files");
|
|
121
|
+
expect(view.count).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("flattens file matches across multiple workspaces and sums reported counts", () => {
|
|
125
|
+
const view = normalizeToolResult(
|
|
126
|
+
searchCall(
|
|
127
|
+
{ pattern: "TODO" },
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
status: "success",
|
|
130
|
+
value: {
|
|
131
|
+
workspaceResults: {
|
|
132
|
+
"/work/a": { type: "files", output: { files: ["a/x.ts"], count: 1 } },
|
|
133
|
+
"/work/b": { type: "files", output: { files: ["b/y.ts"], count: 1 } },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
expect(view.type).toBe("search");
|
|
140
|
+
if (view.type !== "search") return;
|
|
141
|
+
expect(view.kind).toBe("files");
|
|
142
|
+
expect(view.count).toBe(2);
|
|
143
|
+
expect(view.matches.map((m) => m.file)).toEqual(["a/x.ts", "b/y.ts"]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("renders a non-empty grep (type:files) as a file list, not 0 files", () => {
|
|
147
|
+
// The real wrapped shape with results present — proves a populated grep is
|
|
148
|
+
// shown (the user's concern that a non-empty response might render as empty).
|
|
149
|
+
const view = normalizeToolResult(
|
|
150
|
+
searchCall(
|
|
151
|
+
{ pattern: "pipeline" },
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
status: "success",
|
|
154
|
+
value: {
|
|
155
|
+
workspaceResults: {
|
|
156
|
+
"/work/demo": {
|
|
157
|
+
type: "files",
|
|
158
|
+
output: { files: [".tekton/pipeline.yaml", "README.md"], count: 2 },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
expect(view.type).toBe("search");
|
|
166
|
+
if (view.type !== "search") return;
|
|
167
|
+
expect(view.kind).toBe("files");
|
|
168
|
+
expect(view.count).toBe(2);
|
|
169
|
+
expect(view.matches.map((m) => m.file)).toEqual([".tekton/pipeline.yaml", "README.md"]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("falls back to JSON (never hides data) when a count is reported but nothing extracts", () => {
|
|
173
|
+
// Shape drift: the engine says count:5 but the matches live under a key we
|
|
174
|
+
// don't recognise. Surfacing the raw JSON beats a misleading "No matches".
|
|
175
|
+
const view = normalizeToolResult(
|
|
176
|
+
searchCall(
|
|
177
|
+
{ pattern: "x" },
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
status: "success",
|
|
180
|
+
value: {
|
|
181
|
+
workspaceResults: {
|
|
182
|
+
"/work/demo": { type: "weird", output: { hits: ["a", "b"], count: 5 } },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
expect(view.type).toBe("json");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("falls back to JSON when a file search reports totalFiles but no extractable paths", () => {
|
|
192
|
+
// files are objects, not strings (a drift) — but totalFiles says 3.
|
|
193
|
+
const view = normalizeToolResult(
|
|
194
|
+
searchCall(
|
|
195
|
+
{ pattern: "*.ts" },
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
status: "success",
|
|
198
|
+
value: { files: [{ path: "a.ts" }], totalFiles: 3 },
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
expect(view.type).toBe("json");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("maps line-bearing matches into grouped content matches", () => {
|
|
206
|
+
const view = normalizeToolResult(
|
|
207
|
+
searchCall(
|
|
208
|
+
{ pattern: "TODO" },
|
|
209
|
+
JSON.stringify({
|
|
210
|
+
status: "success",
|
|
211
|
+
value: {
|
|
212
|
+
workspaceResults: {
|
|
213
|
+
"/work/a": {
|
|
214
|
+
type: "matches",
|
|
215
|
+
output: {
|
|
216
|
+
matches: [
|
|
217
|
+
{ file: "a/x.ts", line: 12, text: "// TODO: fix" },
|
|
218
|
+
{ file: "a/x.ts", line: 30, text: "// TODO: later" },
|
|
219
|
+
],
|
|
220
|
+
count: 2,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
expect(view.type).toBe("search");
|
|
229
|
+
if (view.type !== "search") return;
|
|
230
|
+
expect(view.kind).toBe("content");
|
|
231
|
+
expect(view.count).toBe(2);
|
|
232
|
+
expect(view.matches[0]).toEqual({ file: "a/x.ts", line: 12, text: "// TODO: fix" });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("normalizeToolResult — search plain text and graceful degradation", () => {
|
|
237
|
+
it("keeps native grep line matches as content (unchanged behavior)", () => {
|
|
238
|
+
const view = normalizeToolResult(
|
|
239
|
+
searchCall(
|
|
240
|
+
{ pattern: "TODO" },
|
|
241
|
+
"\n/workspace/a.go:\n 12: // TODO: fix\n/workspace/b.go:\n 7: // TODO: later",
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
expect(view.type).toBe("search");
|
|
245
|
+
if (view.type !== "search") return;
|
|
246
|
+
expect(view.kind).toBe("content");
|
|
247
|
+
expect(view.count).toBe(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("treats native path/name lines as file matches", () => {
|
|
251
|
+
const view = normalizeToolResult(searchCall({ pattern: "*.go" }, "src/a.go\nsrc/b.go"));
|
|
252
|
+
expect(view.type).toBe("search");
|
|
253
|
+
if (view.type !== "search") return;
|
|
254
|
+
expect(view.kind).toBe("files");
|
|
255
|
+
expect(view.count).toBe(2);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("degrades an unrecognized JSON shape to a json view, not a fake match", () => {
|
|
259
|
+
const view = normalizeToolResult(
|
|
260
|
+
searchCall({ pattern: "x" }, JSON.stringify({ somethingElse: true })),
|
|
261
|
+
);
|
|
262
|
+
expect(view.type).toBe("json");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Covers shell normalization's `command` capture and result-shape handling: a
|
|
2
|
+
// terminal view models the whole session (prompt + output), so the command is
|
|
3
|
+
// echoed from args onto the view across every result shape — un-enveloped Cursor
|
|
4
|
+
// JSON, the Cursor {status,value} envelope, the exit-marker form, and plain
|
|
5
|
+
// text. The cross-language fixture (result-views.json) asserts the happy path;
|
|
6
|
+
// these cover the branch matrix, the envelope unwrap (the reported bug), and the
|
|
7
|
+
// missing-command edge.
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { create, type JsonObject } from "@bufbuild/protobuf";
|
|
11
|
+
import { ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
12
|
+
import {
|
|
13
|
+
ToolCallStatus,
|
|
14
|
+
ToolKind,
|
|
15
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
import { normalizeToolResult } from "../tool-view";
|
|
17
|
+
|
|
18
|
+
function shellCall(args: Record<string, unknown>, result: string) {
|
|
19
|
+
return create(ToolCallSchema, {
|
|
20
|
+
id: "tc-shell",
|
|
21
|
+
name: "shell",
|
|
22
|
+
toolKind: ToolKind.SHELL,
|
|
23
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
24
|
+
args: args as JsonObject,
|
|
25
|
+
result,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("normalizeToolResult — shell command capture", () => {
|
|
30
|
+
it("captures the command on the plain (no-marker) branch", () => {
|
|
31
|
+
const view = normalizeToolResult(shellCall({ command: "echo hi" }, "hi\n"));
|
|
32
|
+
expect(view.type).toBe("terminal");
|
|
33
|
+
if (view.type !== "terminal") return;
|
|
34
|
+
expect(view.command).toBe("echo hi");
|
|
35
|
+
expect(view.stdout).toBe("hi\n");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("captures the command on the exit-marker branch (success and failure)", () => {
|
|
39
|
+
const ok = normalizeToolResult(
|
|
40
|
+
shellCall({ command: "ls -la" }, "total 8\n[Command succeeded]"),
|
|
41
|
+
);
|
|
42
|
+
expect(ok.type).toBe("terminal");
|
|
43
|
+
if (ok.type !== "terminal") return;
|
|
44
|
+
expect(ok.command).toBe("ls -la");
|
|
45
|
+
expect(ok.exitCode).toBe(0);
|
|
46
|
+
|
|
47
|
+
const bad = normalizeToolResult(
|
|
48
|
+
shellCall({ command: "false" }, "boom\n[Command failed with exit code 2]"),
|
|
49
|
+
);
|
|
50
|
+
expect(bad.type).toBe("terminal");
|
|
51
|
+
if (bad.type !== "terminal") return;
|
|
52
|
+
expect(bad.command).toBe("false");
|
|
53
|
+
expect(bad.exitCode).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("captures the command on the structured Cursor JSON branch", () => {
|
|
57
|
+
const view = normalizeToolResult(
|
|
58
|
+
shellCall(
|
|
59
|
+
{ command: "npm test" },
|
|
60
|
+
JSON.stringify({ stdout: "ok", stderr: "", exitCode: 0 }),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
expect(view.type).toBe("terminal");
|
|
64
|
+
if (view.type !== "terminal") return;
|
|
65
|
+
expect(view.command).toBe("npm test");
|
|
66
|
+
expect(view.stdout).toBe("ok");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("unwraps the Cursor {status,value} envelope on success (the reported bug)", () => {
|
|
70
|
+
const view = normalizeToolResult(
|
|
71
|
+
shellCall(
|
|
72
|
+
{ command: "ls -la" },
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
status: "success",
|
|
75
|
+
value: {
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
signal: "",
|
|
78
|
+
stdout: "total 8\ndrwxr-xr-x 2 user user 4096 .\n",
|
|
79
|
+
stderr: "",
|
|
80
|
+
executionTime: 1176,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
expect(view.type).toBe("terminal");
|
|
86
|
+
if (view.type !== "terminal") return;
|
|
87
|
+
expect(view.command).toBe("ls -la");
|
|
88
|
+
// The real stdout surfaces — NOT the raw envelope JSON.
|
|
89
|
+
expect(view.stdout).toBe("total 8\ndrwxr-xr-x 2 user user 4096 .\n");
|
|
90
|
+
expect(view.exitCode).toBe(0);
|
|
91
|
+
// Regression guard: the envelope keys must never leak into the rendered output.
|
|
92
|
+
expect(view.stdout).not.toContain("status");
|
|
93
|
+
expect(view.stdout).not.toContain("value");
|
|
94
|
+
expect(view.stdout).not.toContain("executionTime");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("unwraps the Cursor envelope on failure (surfaces stderr and exit code)", () => {
|
|
98
|
+
const view = normalizeToolResult(
|
|
99
|
+
shellCall(
|
|
100
|
+
{ command: "ls /nope" },
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
status: "success",
|
|
103
|
+
value: {
|
|
104
|
+
exitCode: 2,
|
|
105
|
+
signal: "",
|
|
106
|
+
stdout: "",
|
|
107
|
+
stderr: "ls: /nope: No such file or directory\n",
|
|
108
|
+
executionTime: 5,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
expect(view.type).toBe("terminal");
|
|
114
|
+
if (view.type !== "terminal") return;
|
|
115
|
+
expect(view.command).toBe("ls /nope");
|
|
116
|
+
expect(view.stderr).toBe("ls: /nope: No such file or directory\n");
|
|
117
|
+
expect(view.exitCode).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("leaves command undefined when args carry no command", () => {
|
|
121
|
+
const view = normalizeToolResult(shellCall({}, "orphan output\n"));
|
|
122
|
+
expect(view.type).toBe("terminal");
|
|
123
|
+
if (view.type !== "terminal") return;
|
|
124
|
+
expect(view.command).toBeUndefined();
|
|
125
|
+
expect(view.stdout).toBe("orphan output\n");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Framework-agnostic authorization-provenance labels for every Stigmer surface.
|
|
2
|
+
//
|
|
3
|
+
// describeApprovalPolicySource maps the wire ApprovalPolicySource — *which policy
|
|
4
|
+
// layer decided a tool call's approval requirement* — to a short human phrase.
|
|
5
|
+
// It is shared by @stigmer/react (the ApprovalCard "why-gated" line and the
|
|
6
|
+
// tool-call detail view) and @stigmer/ink (the terminal approval prompt); the Go
|
|
7
|
+
// CLI mirrors it. The phrasing is intrinsic to the source's semantics so the
|
|
8
|
+
// same label reads correctly whether the call is still waiting (a gating source
|
|
9
|
+
// → "required by …") or already cleared (a bypass source → "auto-approved …").
|
|
10
|
+
|
|
11
|
+
import { ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
12
|
+
|
|
13
|
+
export { ApprovalPolicySource };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns a short human phrase describing a tool call's authorization
|
|
17
|
+
* provenance, or `null` for {@link ApprovalPolicySource.UNSPECIFIED} — an
|
|
18
|
+
* execution that predates this field, or a tool the gate never evaluated (e.g. a
|
|
19
|
+
* read-only built-in) — so callers render nothing rather than a misleading
|
|
20
|
+
* default.
|
|
21
|
+
*/
|
|
22
|
+
export function describeApprovalPolicySource(
|
|
23
|
+
source: ApprovalPolicySource,
|
|
24
|
+
): string | null {
|
|
25
|
+
switch (source) {
|
|
26
|
+
case ApprovalPolicySource.CLASSIFIER_DEFAULT:
|
|
27
|
+
return "required by the tool's default policy";
|
|
28
|
+
case ApprovalPolicySource.PINNED_OVERRIDE:
|
|
29
|
+
return "required by a pinned override";
|
|
30
|
+
case ApprovalPolicySource.AGENT_OVERRIDE:
|
|
31
|
+
return "required by agent override";
|
|
32
|
+
case ApprovalPolicySource.BUILTIN_CATEGORY:
|
|
33
|
+
return "required by built-in tool policy";
|
|
34
|
+
case ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN:
|
|
35
|
+
return "required: marked destructive by the server";
|
|
36
|
+
case ApprovalPolicySource.AUTO_APPROVE_ALL:
|
|
37
|
+
return "auto-approved by a run-wide bypass";
|
|
38
|
+
case ApprovalPolicySource.APPROVAL_LEASE:
|
|
39
|
+
return "auto-approved by a run lease";
|
|
40
|
+
case ApprovalPolicySource.UNSPECIFIED:
|
|
41
|
+
default:
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns whether a policy source carries information worth surfacing inline at
|
|
48
|
+
* the approval gate, versus the everyday default ("this tool category just
|
|
49
|
+
* requires approval") that is noise next to the action it is already gating.
|
|
50
|
+
*
|
|
51
|
+
* The default gating reasons — {@link ApprovalPolicySource.CLASSIFIER_DEFAULT}
|
|
52
|
+
* and {@link ApprovalPolicySource.BUILTIN_CATEGORY} — explain nothing the user
|
|
53
|
+
* does not already infer from the tool itself, so they are suppressed from the
|
|
54
|
+
* card (the full phrase stays available on hover where a surface chooses to show
|
|
55
|
+
* a chip). The remaining sources each change the user's understanding of *why*
|
|
56
|
+
* this particular call is held — an explicit override, a server tightening a
|
|
57
|
+
* destructive tool, or (post-execution) a bypass/lease that cleared it — and so
|
|
58
|
+
* are worth showing. {@link ApprovalPolicySource.UNSPECIFIED} is never
|
|
59
|
+
* informative (legacy / ungated).
|
|
60
|
+
*
|
|
61
|
+
* This is the headless policy behind the gate's "smart-suppress" provenance
|
|
62
|
+
* chip; rendering lives in the consuming surface.
|
|
63
|
+
*/
|
|
64
|
+
export function isInformativePolicySource(
|
|
65
|
+
source: ApprovalPolicySource,
|
|
66
|
+
): boolean {
|
|
67
|
+
switch (source) {
|
|
68
|
+
case ApprovalPolicySource.PINNED_OVERRIDE:
|
|
69
|
+
case ApprovalPolicySource.AGENT_OVERRIDE:
|
|
70
|
+
case ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN:
|
|
71
|
+
case ApprovalPolicySource.AUTO_APPROVE_ALL:
|
|
72
|
+
case ApprovalPolicySource.APPROVAL_LEASE:
|
|
73
|
+
return true;
|
|
74
|
+
case ApprovalPolicySource.CLASSIFIER_DEFAULT:
|
|
75
|
+
case ApprovalPolicySource.BUILTIN_CATEGORY:
|
|
76
|
+
case ApprovalPolicySource.UNSPECIFIED:
|
|
77
|
+
default:
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Framework-agnostic execution-phase predicates.
|
|
2
|
+
//
|
|
3
|
+
// Lives in @stigmer/sdk (the pure layer) so it is shared by @stigmer/react and
|
|
4
|
+
// @stigmer/ink and usable by pure logic here (e.g. the file-review fold's corpus
|
|
5
|
+
// parity test reproduces the server's terminal-phase gate). @stigmer/react
|
|
6
|
+
// re-exports it so its public API is unchanged.
|
|
7
|
+
|
|
8
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
9
|
+
|
|
10
|
+
const TERMINAL_PHASES: ReadonlySet<ExecutionPhase> = new Set([
|
|
11
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
12
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
13
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
14
|
+
ExecutionPhase.EXECUTION_TERMINATED,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns `true` when the given phase represents a final, immutable
|
|
19
|
+
* execution state — no further updates will arrive from the server.
|
|
20
|
+
*/
|
|
21
|
+
export function isTerminalPhase(phase: ExecutionPhase): boolean {
|
|
22
|
+
return TERMINAL_PHASES.has(phase);
|
|
23
|
+
}
|