@selesai/code 0.13.37 → 0.13.39
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 +15 -0
- package/dist/extensions/jev/decisions.test.ts +18 -0
- package/dist/extensions/jev/decisions.ts +54 -6
- package/dist/extensions/jev-ask-tool.test.ts +371 -25
- package/dist/extensions/jev-ask-tool.ts +826 -167
- package/dist/extensions/jev-find-source.ts +190 -0
- package/dist/extensions/package.json +1 -1
- package/dist/extensions/pi-graft/cli.test.ts +5 -5
- package/dist/extensions/pi-graft/cli.ts +10 -5
- package/dist/extensions/pi-graft/commands.test.ts +5 -5
- package/dist/extensions/pi-graft/graft.integration.test.ts +1 -1
- package/dist/extensions/pi-graft/lifecycle.test.ts +26 -26
- package/dist/extensions/pi-graft/prompt.test.ts +1 -1
- package/dist/extensions/pi-graft/state.test.ts +9 -9
- package/dist/extensions/pi-graft/test-support.ts +1 -1
- package/dist/extensions/pi-graft/tools.test.ts +1 -1
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/docs/agents.md +1 -7
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +3 -4
- package/dist/extensions/pi-web-agent/src/extension.ts +11 -0
- package/dist/extensions/tokenin-onboarding.ts +138 -1
- package/docs/settings.md +8 -6
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jev-find-source — the source units `jev_find` asks Jev about and returns verbatim.
|
|
3
|
+
*
|
|
4
|
+
* A unit is a line range of one file. JavaScript and TypeScript files are split with the
|
|
5
|
+
* TypeScript parser into top-level statements (consecutive imports merged, and a class or
|
|
6
|
+
* interface longer than FIND_CHUNK_LINES split into its members, each keeping the header line),
|
|
7
|
+
* each with the comments directly above it. Every other file, and a script file when the parser
|
|
8
|
+
* is not installed, is split into blank-line-separated chunks of at most FIND_CHUNK_LINES lines.
|
|
9
|
+
* Line numbers are 1-based and always the file's own: excerpts are cut from the file, never
|
|
10
|
+
* reconstructed.
|
|
11
|
+
*/
|
|
12
|
+
import { extname } from "node:path";
|
|
13
|
+
import type * as TypeScript from "typescript";
|
|
14
|
+
|
|
15
|
+
type TypeScriptApi = typeof TypeScript;
|
|
16
|
+
|
|
17
|
+
/** ponytail: 40-line chunks for non-script files; raise once Jev is measured on longer units. */
|
|
18
|
+
export const FIND_CHUNK_LINES = 40;
|
|
19
|
+
const UNIT_NAME_CHARS = 40;
|
|
20
|
+
|
|
21
|
+
export interface SourceUnit {
|
|
22
|
+
/** What the unit is: a declaration name, `Class.member`, `imports`, or the chunk's first line. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** First line, 1-based, inclusive. */
|
|
25
|
+
start: number;
|
|
26
|
+
/** Last line, 1-based, inclusive. */
|
|
27
|
+
end: number;
|
|
28
|
+
/** Header line of the enclosing class or interface, kept with a member unit. */
|
|
29
|
+
header?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let typeScript: Promise<TypeScriptApi | undefined> | undefined;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The TypeScript compiler, or undefined when it is not installed: it is a devDependency, so a
|
|
36
|
+
* published install parses nothing and every file is chunked instead.
|
|
37
|
+
*/
|
|
38
|
+
export function loadTypeScript(): Promise<TypeScriptApi | undefined> {
|
|
39
|
+
typeScript ??= import("typescript").then(
|
|
40
|
+
(module: unknown) => {
|
|
41
|
+
const api = ((module as { default?: TypeScriptApi }).default ?? module) as TypeScriptApi;
|
|
42
|
+
return typeof api.createSourceFile === "function" ? api : undefined;
|
|
43
|
+
},
|
|
44
|
+
() => undefined,
|
|
45
|
+
);
|
|
46
|
+
return typeScript;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SCRIPT_KINDS: Record<string, "TS" | "TSX" | "JS" | "JSX"> = {
|
|
50
|
+
".ts": "TS",
|
|
51
|
+
".tsx": "TSX",
|
|
52
|
+
".js": "JS",
|
|
53
|
+
".jsx": "JSX",
|
|
54
|
+
".mjs": "JS",
|
|
55
|
+
".cjs": "JS",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Whether `path` is split with the TypeScript parser (when it is installed). */
|
|
59
|
+
export function isScriptPath(path: string): boolean {
|
|
60
|
+
return extname(path).toLowerCase() in SCRIPT_KINDS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A file's source units in file order; `ts` undefined chunks every file. */
|
|
64
|
+
export function splitSourceUnits(path: string, text: string, ts: TypeScriptApi | undefined): SourceUnit[] {
|
|
65
|
+
const kind = SCRIPT_KINDS[extname(path).toLowerCase()];
|
|
66
|
+
if (ts && kind) {
|
|
67
|
+
try {
|
|
68
|
+
const units = scriptUnits(ts, path, text, ts.ScriptKind[kind]);
|
|
69
|
+
if (units.length > 0) return units;
|
|
70
|
+
} catch {
|
|
71
|
+
// A parser crash on odd input falls back to chunks; the file is still readable.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return chunkUnits(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function firstLine(text: string): string {
|
|
78
|
+
const line = text.split("\n").find((row) => row.trim() !== "") ?? "";
|
|
79
|
+
return line.trim().slice(0, UNIT_NAME_CHARS);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function scriptUnits(ts: TypeScriptApi, path: string, text: string, kind: TypeScript.ScriptKind): SourceUnit[] {
|
|
83
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind);
|
|
84
|
+
const lineOf = (pos: number) => source.getLineAndCharacterOfPosition(pos).line + 1;
|
|
85
|
+
/**
|
|
86
|
+
* A node's first line, counting the comments directly above it: a comment separated by a blank
|
|
87
|
+
* line (a section banner) or sitting on the previous node's last line (`a(); // note`) is not its own.
|
|
88
|
+
*/
|
|
89
|
+
const startOf = (node: TypeScript.Node, floor: number): number => {
|
|
90
|
+
let start = lineOf(node.getStart(source));
|
|
91
|
+
const comments = ts.getLeadingCommentRanges(text, node.pos) ?? [];
|
|
92
|
+
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
93
|
+
const comment = comments[i];
|
|
94
|
+
const from = lineOf(comment.pos);
|
|
95
|
+
if (from <= floor || lineOf(comment.end) < start - 1) break;
|
|
96
|
+
start = from;
|
|
97
|
+
}
|
|
98
|
+
return start;
|
|
99
|
+
};
|
|
100
|
+
const nameOf = (node: TypeScript.Node): string => {
|
|
101
|
+
const named = (node as { name?: TypeScript.Node }).name;
|
|
102
|
+
if (named) return named.getText(source);
|
|
103
|
+
if (ts.isVariableStatement(node)) {
|
|
104
|
+
return node.declarationList.declarations.map((declaration) => declaration.name.getText(source)).join(", ");
|
|
105
|
+
}
|
|
106
|
+
if (ts.isConstructorDeclaration(node)) return "constructor";
|
|
107
|
+
if (ts.isExportAssignment(node)) return "export default";
|
|
108
|
+
return firstLine(node.getText(source));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const units: SourceUnit[] = [];
|
|
112
|
+
let floor = 0;
|
|
113
|
+
for (const statement of source.statements) {
|
|
114
|
+
const start = startOf(statement, floor);
|
|
115
|
+
const end = lineOf(statement.end);
|
|
116
|
+
floor = end;
|
|
117
|
+
if (ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) {
|
|
118
|
+
const last = units.at(-1);
|
|
119
|
+
if (last?.name === "imports" && last.end >= start - 1) last.end = end;
|
|
120
|
+
else units.push({ name: "imports", start, end });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const container =
|
|
124
|
+
ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) ? statement : undefined;
|
|
125
|
+
const members = container?.members;
|
|
126
|
+
if (container && members && members.length > 0 && end - start + 1 > FIND_CHUNK_LINES) {
|
|
127
|
+
const owner = nameOf(container);
|
|
128
|
+
const header = lineOf((container.name ?? container).getStart(source));
|
|
129
|
+
let memberFloor = header;
|
|
130
|
+
for (const member of members) {
|
|
131
|
+
const memberStart = startOf(member, memberFloor);
|
|
132
|
+
const memberEnd = lineOf(member.end);
|
|
133
|
+
memberFloor = memberEnd;
|
|
134
|
+
units.push({
|
|
135
|
+
name: `${owner}.${nameOf(member)}`,
|
|
136
|
+
start: memberStart,
|
|
137
|
+
end: memberEnd,
|
|
138
|
+
...(memberStart > header ? { header } : {}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
units.push({ name: nameOf(statement), start, end });
|
|
144
|
+
}
|
|
145
|
+
return units;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Blank-line-separated paragraphs packed into chunks of at most FIND_CHUNK_LINES lines. */
|
|
149
|
+
export function chunkUnits(text: string): SourceUnit[] {
|
|
150
|
+
const rows = text.split("\n");
|
|
151
|
+
const paragraphs: Array<{ start: number; end: number }> = [];
|
|
152
|
+
rows.forEach((row, index) => {
|
|
153
|
+
if (row.trim() === "") return;
|
|
154
|
+
const last = paragraphs.at(-1);
|
|
155
|
+
if (last && last.end === index) last.end = index + 1;
|
|
156
|
+
else paragraphs.push({ start: index + 1, end: index + 1 });
|
|
157
|
+
});
|
|
158
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
159
|
+
for (const paragraph of paragraphs) {
|
|
160
|
+
const current = ranges.at(-1);
|
|
161
|
+
if (current && paragraph.end - current.start + 1 <= FIND_CHUNK_LINES) {
|
|
162
|
+
current.end = paragraph.end;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// A paragraph longer than a chunk is cut into chunk-sized pieces.
|
|
166
|
+
for (let start = paragraph.start; start <= paragraph.end; start += FIND_CHUNK_LINES) {
|
|
167
|
+
ranges.push({ start, end: Math.min(paragraph.end, start + FIND_CHUNK_LINES - 1) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return ranges.map((range) => ({ ...range, name: firstLine(rows[range.start - 1]) }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Line windows around the best keyword lines (±`context` lines), merged where they overlap: the
|
|
175
|
+
* source returned when Jev could not judge a file's units. With no keyword line, the file's head.
|
|
176
|
+
*/
|
|
177
|
+
export function keywordWindows(lineCount: number, hits: readonly number[], context = 3, maxWindows = 3): SourceUnit[] {
|
|
178
|
+
if (lineCount <= 0) return [];
|
|
179
|
+
const anchors = [...new Set(hits.filter((line) => line >= 1 && line <= lineCount))].slice(0, maxWindows);
|
|
180
|
+
if (anchors.length === 0) return [{ name: "file head", start: 1, end: Math.min(lineCount, 2 * context + 1) }];
|
|
181
|
+
const windows: SourceUnit[] = [];
|
|
182
|
+
for (const line of anchors.sort((a, b) => a - b)) {
|
|
183
|
+
const start = Math.max(1, line - context);
|
|
184
|
+
const end = Math.min(lineCount, line + context);
|
|
185
|
+
const last = windows.at(-1);
|
|
186
|
+
if (last && start <= last.end + 1) last.end = Math.max(last.end, end);
|
|
187
|
+
else windows.push({ name: "keyword window", start, end });
|
|
188
|
+
}
|
|
189
|
+
return windows;
|
|
190
|
+
}
|
|
@@ -68,8 +68,8 @@ describe("installGraft", () => {
|
|
|
68
68
|
it("runs the compatible global npm install without a shell off Windows", async () => {
|
|
69
69
|
const { pi } = makePi(async () => execReturning(OK));
|
|
70
70
|
const run = await installGraft(pi.exec as never, "/repo");
|
|
71
|
-
expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.
|
|
72
|
-
expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.
|
|
71
|
+
expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.21");
|
|
72
|
+
expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.21");
|
|
73
73
|
const [command, args, options] = pi.exec.mock.calls[0] as [string, string[], { cwd: string; timeout: number }];
|
|
74
74
|
expect(command).toBe(process.platform === "win32" ? "cmd" : "npm");
|
|
75
75
|
expect(args).toContain("install");
|
|
@@ -118,7 +118,7 @@ describe("version handling", () => {
|
|
|
118
118
|
|
|
119
119
|
it("accepts the verified range and rejects the boundaries", () => {
|
|
120
120
|
expect(isCompatibleVersion(MIN_GRAFT_VERSION)).toBe(true);
|
|
121
|
-
expect(isCompatibleVersion("0.
|
|
121
|
+
expect(isCompatibleVersion("0.21.1")).toBe(true);
|
|
122
122
|
expect(isCompatibleVersion("0.99.0")).toBe(true);
|
|
123
123
|
expect(isCompatibleVersion("0.17.9")).toBe(false);
|
|
124
124
|
expect(isCompatibleVersion(MAX_GRAFT_VERSION_EXCLUSIVE)).toBe(false);
|
|
@@ -128,9 +128,9 @@ describe("version handling", () => {
|
|
|
128
128
|
|
|
129
129
|
describe("probeGraft", () => {
|
|
130
130
|
it("accepts a supported version and reports it", async () => {
|
|
131
|
-
const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.
|
|
131
|
+
const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.21.1\n" }));
|
|
132
132
|
const probe = await probeGraft(pi.exec as never, "/repo");
|
|
133
|
-
expect(probe).toMatchObject({ kind: "ok", version: "0.
|
|
133
|
+
expect(probe).toMatchObject({ kind: "ok", version: "0.21.1" });
|
|
134
134
|
if (probe.kind === "ok") expect(probe.argv.at(-1)).toBe("--version");
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Selesai model for deep enrichment, otherwise stay structural and local.
|
|
16
16
|
* - Extension-spawned commands default to telemetry opt-out (`DO_NOT_TRACK=1`).
|
|
17
17
|
*
|
|
18
|
-
* Verified against @nanonets/graft 0.
|
|
18
|
+
* Verified against @nanonets/graft 0.21.1 (see {@link MIN_GRAFT_VERSION}).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { truncateHead, type ExecOptions, type ExecResult } from "@selesai/code";
|
|
@@ -27,8 +27,13 @@ export const GRAFT_PACKAGE = "@nanonets/graft";
|
|
|
27
27
|
* Lowest Graft CLI whose documented surface this adapter was verified against:
|
|
28
28
|
* `ask --source/--json`, `skeleton`, `callers --direction/-d`, `grep --in/-i/--fixed`,
|
|
29
29
|
* `map --max-dirs`, `check --json`, `build [--deep]`, and `--version`.
|
|
30
|
+
*
|
|
31
|
+
* The floor sits on the 0.21 minor line this adapter is verified against
|
|
32
|
+
* (0.21.1). Upgrades are deliberate: an executable below the floor reports
|
|
33
|
+
* `incompatible`, and the adapter never overwrites it — the user upgrades with
|
|
34
|
+
* {@link INSTALL_COMMAND}.
|
|
30
35
|
*/
|
|
31
|
-
export const MIN_GRAFT_VERSION = "0.
|
|
36
|
+
export const MIN_GRAFT_VERSION = "0.21.0";
|
|
32
37
|
/** Upper bound is exclusive: a 1.x CLI is allowed to change its contract. */
|
|
33
38
|
export const MAX_GRAFT_VERSION_EXCLUSIVE = "1.0.0";
|
|
34
39
|
|
|
@@ -45,8 +50,8 @@ export const DEEP_BUILD_TIMEOUT_MS = 1_800_000;
|
|
|
45
50
|
/** Global npm installs can be slow on a cold cache. */
|
|
46
51
|
export const INSTALL_TIMEOUT_MS = 180_000;
|
|
47
52
|
|
|
48
|
-
/** Kept on the verified 0.
|
|
49
|
-
export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.
|
|
53
|
+
/** Kept on the verified 0.21 minor line; never install a floating latest CLI. */
|
|
54
|
+
export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.21`;
|
|
50
55
|
export const INSTALL_COMMAND = `npm install -g ${GRAFT_INSTALL_SPEC}`;
|
|
51
56
|
export const TELEMETRY_ENV = "DO_NOT_TRACK";
|
|
52
57
|
|
|
@@ -399,7 +404,7 @@ export interface AskHit {
|
|
|
399
404
|
}
|
|
400
405
|
|
|
401
406
|
/**
|
|
402
|
-
* `graft ask --json` result, as emitted by `AskResult` upstream (0.
|
|
407
|
+
* `graft ask --json` result, as emitted by `AskResult` upstream (0.21.1).
|
|
403
408
|
*
|
|
404
409
|
* Every field is optional on read: a CLI that changes shape degrades to raw
|
|
405
410
|
* text rather than to a wrong summary, and callers must never treat the parse
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
import { MODE_ENTRY_TYPE, type GraftState } from "./state.ts";
|
|
16
16
|
import { notified, makeCtx, makePi, type CtxStub } from "./test-support.ts";
|
|
17
17
|
|
|
18
|
-
const HEALTHY: GraftState = { name: "fresh-structural", version: "0.
|
|
18
|
+
const HEALTHY: GraftState = { name: "fresh-structural", version: "0.21.1" };
|
|
19
19
|
|
|
20
20
|
function installOf(overrides: Partial<GraftInstallRun> = {}): GraftInstallRun {
|
|
21
21
|
return {
|
|
22
|
-
argv: ["npm", "install", "-g", "@nanonets/graft@^0.
|
|
22
|
+
argv: ["npm", "install", "-g", "@nanonets/graft@^0.21"],
|
|
23
23
|
cwd: "/repo",
|
|
24
24
|
code: 0,
|
|
25
25
|
stdout: "",
|
|
@@ -75,7 +75,7 @@ function makeRuntime(
|
|
|
75
75
|
const recheck = vi.fn(async () => {});
|
|
76
76
|
const installCli = vi.fn(async () => {
|
|
77
77
|
const result = overrides.installResult ?? installOf();
|
|
78
|
-
if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.
|
|
78
|
+
if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.21.1" };
|
|
79
79
|
return result;
|
|
80
80
|
});
|
|
81
81
|
const runBuild = vi.fn(async () => overrides.buildResult ?? runOf());
|
|
@@ -134,7 +134,7 @@ describe("formatDoctor", () => {
|
|
|
134
134
|
it("reports repository, executable, graph, state, mode, telemetry and provider boundary", () => {
|
|
135
135
|
const text = formatDoctor(base);
|
|
136
136
|
expect(text).toContain("/repo (git ✓)");
|
|
137
|
-
expect(text).toContain("graft 0.
|
|
137
|
+
expect(text).toContain("graft 0.21.1");
|
|
138
138
|
expect(text).toContain("structural only");
|
|
139
139
|
expect(text).toContain("fresh-structural");
|
|
140
140
|
expect(text).toContain("nothing is injected");
|
|
@@ -188,7 +188,7 @@ describe("formatStatus", () => {
|
|
|
188
188
|
providerEnv: [],
|
|
189
189
|
});
|
|
190
190
|
expect(text.split("\n")).toHaveLength(4);
|
|
191
|
-
expect(text).toContain("graft: ● structural v0.
|
|
191
|
+
expect(text).toContain("graft: ● structural v0.21.1");
|
|
192
192
|
expect(text).toContain("hybrid");
|
|
193
193
|
expect(text).toContain("root: /repo");
|
|
194
194
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* adapter against the real executable, and it skips itself when no compatible
|
|
7
7
|
* Graft CLI is on PATH — which is the state of ordinary CI.
|
|
8
8
|
*
|
|
9
|
-
* To run it for real: `npm install -g @nanonets/graft@^0.
|
|
9
|
+
* To run it for real: `npm install -g @nanonets/graft@^0.21` then
|
|
10
10
|
* npx vitest run src/extensions/pi-graft/graft.integration.test.ts
|
|
11
11
|
*
|
|
12
12
|
* A structural build is tree-sitter only: no model, no API key, no network. The
|
|
@@ -39,7 +39,7 @@ function writeProjectSettings(value: unknown): void {
|
|
|
39
39
|
function exec(graftOutput = ASK_JSON) {
|
|
40
40
|
return async (command: string, args: string[]) => {
|
|
41
41
|
if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${repo}\n` };
|
|
42
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
42
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
43
43
|
return { ...OK, stdout: graftOutput };
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -88,7 +88,7 @@ describe("session start", () => {
|
|
|
88
88
|
it("resolves the Git working tree, probes the executable, and publishes a status line", async () => {
|
|
89
89
|
const session = await bootSession();
|
|
90
90
|
expect(session.commandsRun()).toEqual(["git rev-parse --show-toplevel", "graft --version"]);
|
|
91
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
91
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
|
|
92
92
|
await session.reset();
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -163,7 +163,7 @@ describe("session start", () => {
|
|
|
163
163
|
}
|
|
164
164
|
if (args.includes("--version")) {
|
|
165
165
|
if (!installed) throw new Error("spawn graft ENOENT");
|
|
166
|
-
return { ...OK, stdout: "0.
|
|
166
|
+
return { ...OK, stdout: "0.21.1\n" };
|
|
167
167
|
}
|
|
168
168
|
return { ...OK };
|
|
169
169
|
});
|
|
@@ -171,8 +171,8 @@ describe("session start", () => {
|
|
|
171
171
|
const ctx = makeCtx({ cwd: repo });
|
|
172
172
|
const start = handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>;
|
|
173
173
|
await start({ type: "session_start", reason: "startup" }, ctx);
|
|
174
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
175
|
-
expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.
|
|
174
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
175
|
+
expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.21");
|
|
176
176
|
|
|
177
177
|
await start({ type: "session_start", reason: "reload" }, ctx);
|
|
178
178
|
expect(harness.commandsRun().filter((command) => command.startsWith("npm install"))).toHaveLength(1);
|
|
@@ -202,7 +202,7 @@ describe("session start", () => {
|
|
|
202
202
|
writeProjectSettings({});
|
|
203
203
|
const harness = makePi(async (command: string) => {
|
|
204
204
|
if (command === "git") return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
|
|
205
|
-
return { ...OK, stdout: "0.
|
|
205
|
+
return { ...OK, stdout: "0.21.1\n" };
|
|
206
206
|
});
|
|
207
207
|
graftExtension(harness.pi as never);
|
|
208
208
|
const ctx = makeCtx({ cwd: repo });
|
|
@@ -233,14 +233,14 @@ describe("mutation awareness", () => {
|
|
|
233
233
|
it("marks the graph changed after a successful edit", async () => {
|
|
234
234
|
const session = await bootSession();
|
|
235
235
|
await onToolResult(session, "edit", { path: "src/a.ts" });
|
|
236
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
236
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
237
237
|
await session.reset();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
it("marks the graph changed after a clearly mutating bash command", async () => {
|
|
241
241
|
const session = await bootSession();
|
|
242
242
|
await onToolResult(session, "bash", { command: "rm -rf dist" });
|
|
243
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
243
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
244
244
|
await session.reset();
|
|
245
245
|
});
|
|
246
246
|
|
|
@@ -249,7 +249,7 @@ describe("mutation awareness", () => {
|
|
|
249
249
|
await onToolResult(session, "read", { path: "src/a.ts" });
|
|
250
250
|
await onToolResult(session, "bash", { command: "git status" });
|
|
251
251
|
await onToolResult(session, "edit", { path: "src/a.ts" }, true);
|
|
252
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
252
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
|
|
253
253
|
await session.reset();
|
|
254
254
|
});
|
|
255
255
|
});
|
|
@@ -270,7 +270,7 @@ describe("proactive refresh", () => {
|
|
|
270
270
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
271
271
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
272
272
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
273
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
273
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
274
274
|
if (args.includes("build")) {
|
|
275
275
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
276
276
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -283,7 +283,7 @@ describe("proactive refresh", () => {
|
|
|
283
283
|
{ type: "session_start", reason: "startup" },
|
|
284
284
|
ctx,
|
|
285
285
|
);
|
|
286
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
286
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
287
287
|
expect(harness.commandsRun()).toContain("graft build");
|
|
288
288
|
});
|
|
289
289
|
|
|
@@ -294,7 +294,7 @@ describe("proactive refresh", () => {
|
|
|
294
294
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
295
295
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
296
296
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
297
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
297
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
298
298
|
if (args.includes("build")) {
|
|
299
299
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
300
300
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -314,7 +314,7 @@ describe("proactive refresh", () => {
|
|
|
314
314
|
{ type: "session_start", reason: "startup" },
|
|
315
315
|
ctx,
|
|
316
316
|
);
|
|
317
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.
|
|
317
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
|
|
318
318
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
319
319
|
expect(build[1]).toEqual(["build", "--deep"]);
|
|
320
320
|
expect(build[2]).toMatchObject({
|
|
@@ -335,7 +335,7 @@ describe("proactive refresh", () => {
|
|
|
335
335
|
const models: string[] = [];
|
|
336
336
|
const harness = makePi(async (command: string, args: string[], options?: unknown) => {
|
|
337
337
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
338
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
338
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
339
339
|
if (args.includes("build")) {
|
|
340
340
|
const model = (options as { env?: Record<string, string> }).env?.GRAFT_MODEL ?? "";
|
|
341
341
|
models.push(model);
|
|
@@ -359,7 +359,7 @@ describe("proactive refresh", () => {
|
|
|
359
359
|
{ type: "session_start", reason: "startup" },
|
|
360
360
|
ctx,
|
|
361
361
|
);
|
|
362
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.
|
|
362
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
|
|
363
363
|
expect(models).toEqual(["deepseek-v4.1-flash", "glm-5.3-flash"]);
|
|
364
364
|
expect(notified(ctx)).toContain("retrying the deep pass");
|
|
365
365
|
});
|
|
@@ -371,7 +371,7 @@ describe("proactive refresh", () => {
|
|
|
371
371
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
372
372
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
373
373
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
374
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
374
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
375
375
|
if (args.includes("build")) {
|
|
376
376
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
377
377
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -388,7 +388,7 @@ describe("proactive refresh", () => {
|
|
|
388
388
|
{ type: "session_start", reason: "startup" },
|
|
389
389
|
ctx,
|
|
390
390
|
);
|
|
391
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
391
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
392
392
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
393
393
|
expect(build[1]).toEqual(["build"]);
|
|
394
394
|
});
|
|
@@ -403,11 +403,11 @@ describe("proactive refresh", () => {
|
|
|
403
403
|
{ type: "tool_result", toolName: "write", toolCallId: "c1", input: {}, content: [], isError: false },
|
|
404
404
|
session.ctx,
|
|
405
405
|
);
|
|
406
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
406
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
407
407
|
|
|
408
408
|
await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
|
|
409
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.
|
|
410
|
-
await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
409
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.21.1");
|
|
410
|
+
await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
411
411
|
expect(session.commandsRun()).toContain("graft build");
|
|
412
412
|
await session.reset();
|
|
413
413
|
});
|
|
@@ -434,7 +434,7 @@ describe("proactive refresh", () => {
|
|
|
434
434
|
writeProjectSettings({ refreshDebounceSeconds: 0 });
|
|
435
435
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
436
436
|
if (command === "git") return { ...OK, stdout: `${repo}\n` };
|
|
437
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
437
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
438
438
|
if (args.includes("build")) return { code: 1, stdout: "", stderr: "✗ index failed", killed: false };
|
|
439
439
|
return { ...OK, stdout: ASK_JSON };
|
|
440
440
|
});
|
|
@@ -449,7 +449,7 @@ describe("proactive refresh", () => {
|
|
|
449
449
|
ctx,
|
|
450
450
|
);
|
|
451
451
|
await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
|
|
452
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.
|
|
452
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.21.1"));
|
|
453
453
|
});
|
|
454
454
|
});
|
|
455
455
|
|
|
@@ -541,7 +541,7 @@ describe("retrieval strategy", () => {
|
|
|
541
541
|
writeProjectSettings({ mode: "push" });
|
|
542
542
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
543
543
|
if (command === "git") return { ...OK, stdout: `${repo}\n` };
|
|
544
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
544
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
545
545
|
return { code: 1, stdout: "", stderr: "✗ graph missing", killed: false };
|
|
546
546
|
});
|
|
547
547
|
graftExtension(harness.pi as never);
|
|
@@ -565,7 +565,7 @@ describe("retrieval strategy", () => {
|
|
|
565
565
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: { mode: "push" } }), "utf-8");
|
|
566
566
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
567
567
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
568
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
568
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
569
569
|
if (args.includes("build")) {
|
|
570
570
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
571
571
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -642,7 +642,7 @@ describe("tool readiness", () => {
|
|
|
642
642
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
643
643
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
644
644
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
645
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
645
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
646
646
|
if (args.includes("build")) {
|
|
647
647
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
648
648
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -671,7 +671,7 @@ describe("tool readiness", () => {
|
|
|
671
671
|
gitCalls += 1;
|
|
672
672
|
return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
|
|
673
673
|
}
|
|
674
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
674
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
675
675
|
return { ...OK, stdout: ASK_JSON };
|
|
676
676
|
});
|
|
677
677
|
graftExtension(harness.pi as never);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./prompt.ts";
|
|
14
14
|
import { DEFAULT_MIN_COVERAGE, type GraftState } from "./state.ts";
|
|
15
15
|
|
|
16
|
-
const FRESH: GraftState = { name: "fresh-structural", version: "0.
|
|
16
|
+
const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1" };
|
|
17
17
|
|
|
18
18
|
function payload(overrides: Partial<AskPayload> = {}): AskPayload {
|
|
19
19
|
return {
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
} from "./state.ts";
|
|
23
23
|
|
|
24
24
|
const UNBUILT: GraftState = { name: "unbuilt", detail: "no graph yet" };
|
|
25
|
-
const FRESH: GraftState = { name: "fresh-structural", version: "0.
|
|
26
|
-
const DEEP: GraftState = { name: "fresh-deep", version: "0.
|
|
25
|
+
const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1", deep: false };
|
|
26
|
+
const DEEP: GraftState = { name: "fresh-deep", version: "0.21.1", deep: true };
|
|
27
27
|
|
|
28
28
|
describe("reduceGraftState", () => {
|
|
29
29
|
it("only claims a repository once root resolution says there is a Git working tree", () => {
|
|
@@ -36,16 +36,16 @@ describe("reduceGraftState", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("derives built/fresh/deep from what is actually on disk", () => {
|
|
39
|
-
expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
39
|
+
expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: ABSENT_GRAPH })).toEqual({
|
|
40
40
|
name: "unbuilt",
|
|
41
|
-
version: "0.
|
|
41
|
+
version: "0.21.1",
|
|
42
42
|
detail: expect.stringContaining("/graft build"),
|
|
43
43
|
});
|
|
44
44
|
expect(
|
|
45
|
-
reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
45
|
+
reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: false } }),
|
|
46
46
|
).toEqual(FRESH);
|
|
47
47
|
expect(
|
|
48
|
-
reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
48
|
+
reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: true } }),
|
|
49
49
|
).toMatchObject({ name: "fresh-deep", deep: true });
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -91,7 +91,7 @@ describe("reduceGraftState", () => {
|
|
|
91
91
|
expect(reduceGraftState(FRESH, { type: "build-started", deep: true })).toMatchObject({
|
|
92
92
|
name: "building",
|
|
93
93
|
deep: true,
|
|
94
|
-
version: "0.
|
|
94
|
+
version: "0.21.1",
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -140,8 +140,8 @@ describe("state predicates and rendering", () => {
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it("renders a compact status line with the version when it is known", () => {
|
|
143
|
-
expect(statusText(FRESH)).toBe("graft: ● structural v0.
|
|
144
|
-
expect(statusText(DEEP)).toBe("graft: ● deep v0.
|
|
143
|
+
expect(statusText(FRESH)).toBe("graft: ● structural v0.21.1");
|
|
144
|
+
expect(statusText(DEEP)).toBe("graft: ● deep v0.21.1");
|
|
145
145
|
expect(statusText({ name: "unavailable" })).toBe("graft: ✕ no cli");
|
|
146
146
|
expect(statusText({ name: "changed" })).toBe("graft: ◐ changed");
|
|
147
147
|
});
|
|
@@ -190,7 +190,7 @@ export function makeCtx(
|
|
|
190
190
|
*/
|
|
191
191
|
export function healthyExec(overrides: { gitRoot?: string; version?: string; queryOutput?: string } = {}): ExecImpl {
|
|
192
192
|
const gitRoot = overrides.gitRoot ?? "/repo";
|
|
193
|
-
const version = overrides.version ?? "0.
|
|
193
|
+
const version = overrides.version ?? "0.21.1";
|
|
194
194
|
return async (command, args) => {
|
|
195
195
|
if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${gitRoot}\n` };
|
|
196
196
|
if (args.includes("--version")) return { ...OK, stdout: `${version}\n` };
|
|
@@ -64,7 +64,7 @@ function makeHarness(
|
|
|
64
64
|
const runtime: GraftToolRuntime = {
|
|
65
65
|
run: run as never,
|
|
66
66
|
settings: () => options.settings ?? {},
|
|
67
|
-
state: () => options.state ?? { name: "fresh-structural", version: "0.
|
|
67
|
+
state: () => options.state ?? { name: "fresh-structural", version: "0.21.1" },
|
|
68
68
|
noteQuery: ((_ctx: unknown, result: GraftRun) => {
|
|
69
69
|
noted.push(result);
|
|
70
70
|
}) as never,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: researcher
|
|
3
3
|
description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
|
|
4
|
-
tools: read, write,
|
|
4
|
+
tools: read, write, web_explore
|
|
5
5
|
thinking: medium
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
@@ -16,11 +16,10 @@ Given a question or topic, run focused web research and produce a concise, well-
|
|
|
16
16
|
|
|
17
17
|
Working rules:
|
|
18
18
|
- Break the problem into 2-4 distinct research angles.
|
|
19
|
-
- Use `
|
|
20
|
-
- Treat search-result summaries as discovery aids, not final evidence for important claims.
|
|
19
|
+
- Use `web_explore` once per research angle with a focused `query`; it handles search, fetch, and source ranking internally. Call it again with a narrower query to fill gaps.
|
|
20
|
+
- Treat search-result summaries as discovery aids, not final evidence for important claims. Re-check important, disputed, surprising, or decision-relevant claims against the original source with a narrower `web_explore` query.
|
|
21
21
|
- Prefer primary, official, authoritative, or directly relevant sources. Keep a smaller set of strong sources rather than many weak or redundant ones; reject stale, redundant, or SEO-heavy sources, and flag stale evidence when freshness materially affects the answer.
|
|
22
|
-
-
|
|
23
|
-
- `source_check` must be registered by the loaded provider before launch. If a registered `source_check` call fails, continue by fetching and inspecting the original source directly, and disclose the validation limitation rather than failing the research run.
|
|
22
|
+
- Verify decision-critical or disputed claims, benchmark/performance claims, pricing/licensing claims, security claims, and wording that could materially affect a recommendation against the original source. Do not do this for every trivial fact. Disclose any validation limitation rather than failing the research run.
|
|
24
23
|
- Label direct evidence, source interpretation, and researcher inference distinctly. Never present an inference as if the source stated it directly.
|
|
25
24
|
- Record contradictions instead of silently resolving them. Record missing evidence when a claim cannot be verified.
|
|
26
25
|
- Never invent dates, quotations, citations, or unsupported precision.
|