@zhn2605/topo 0.2.0 → 0.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/README.md +24 -3
- package/dist/topo.js +783 -231
- package/dist/web/assets/answer-3GChwEmV.js +2 -0
- package/dist/web/assets/{elk.bundled-BUXl0iSx.js → elk.bundled-Bx4dyQBB.js} +1 -1
- package/dist/web/assets/index-CX9S_hg9.js +23 -0
- package/dist/web/assets/index-D-MaOEzL.css +1 -0
- package/dist/web/index.html +2 -2
- package/dist/web/snapshot.html +43 -42
- package/package.json +1 -1
- package/dist/web/assets/answer-CbyA0kan.js +0 -2
- package/dist/web/assets/index-51XzhJVO.css +0 -1
- package/dist/web/assets/index-IpMwIh-N.js +0 -22
package/dist/topo.js
CHANGED
|
@@ -923,8 +923,8 @@ function parseUnifiedDiff(text) {
|
|
|
923
923
|
if (line.startsWith("diff --git ")) {
|
|
924
924
|
flush();
|
|
925
925
|
const both = line.slice("diff --git ".length);
|
|
926
|
-
const
|
|
927
|
-
cur = { oldPath:
|
|
926
|
+
const path29 = unquote(both.slice(0, (both.length - 1) / 2));
|
|
927
|
+
cur = { oldPath: path29, newPath: path29, status: "modified", oldRanges: [], newRanges: [] };
|
|
928
928
|
} else if (!cur) {
|
|
929
929
|
} else if (line.startsWith("new file mode")) {
|
|
930
930
|
cur.status = "added";
|
|
@@ -974,10 +974,10 @@ function getFileDiffs(repo, baseSha, headRef) {
|
|
|
974
974
|
const diffs = parseUnifiedDiff(git(repo, args).toString());
|
|
975
975
|
if (headRef) return diffs;
|
|
976
976
|
const untracked = git(repo, ["ls-files", "--others", "--exclude-standard", "-z"]).toString().split("\0").filter((p) => p && !p.endsWith("/"));
|
|
977
|
-
for (const
|
|
978
|
-
const n = lineCount(join(repo,
|
|
977
|
+
for (const path29 of untracked) {
|
|
978
|
+
const n = lineCount(join(repo, path29));
|
|
979
979
|
diffs.push({
|
|
980
|
-
newPath:
|
|
980
|
+
newPath: path29,
|
|
981
981
|
status: "added",
|
|
982
982
|
oldRanges: [],
|
|
983
983
|
newRanges: n ? [{ startLine: 1, endLine: n }] : []
|
|
@@ -1016,10 +1016,10 @@ import fs3 from "node:fs";
|
|
|
1016
1016
|
import os from "node:os";
|
|
1017
1017
|
import path3 from "node:path";
|
|
1018
1018
|
function requireSpec(arg) {
|
|
1019
|
-
const
|
|
1020
|
-
if (
|
|
1021
|
-
const relative2 = /^(\.\.?|@self)\//.test(
|
|
1022
|
-
return { ...relative2 && { relative: relative2 }, tokens:
|
|
1019
|
+
const str3 = /^(["'`])(.*)\1$/.exec(arg)?.[2];
|
|
1020
|
+
if (str3 !== void 0) {
|
|
1021
|
+
const relative2 = /^(\.\.?|@self)\//.test(str3) ? str3 : void 0;
|
|
1022
|
+
return { ...relative2 && { relative: relative2 }, tokens: str3.split("/") };
|
|
1023
1023
|
}
|
|
1024
1024
|
const tokens = (arg.match(/[A-Za-z_][\w-]*/g) ?? []).filter((t) => !LOOKUPS.has(t));
|
|
1025
1025
|
const chain = /^script((?:\.Parent)*)((?:\.[A-Za-z_]\w*)+)$/.exec(arg.replace(/\s+/g, ""));
|
|
@@ -1789,8 +1789,8 @@ function calleeName(call) {
|
|
|
1789
1789
|
if (ts2.isPropertyAccessExpression(c) && ts2.isIdentifier(c.name)) return c.name.text;
|
|
1790
1790
|
return void 0;
|
|
1791
1791
|
}
|
|
1792
|
-
function segsOf(
|
|
1793
|
-
return
|
|
1792
|
+
function segsOf(path29) {
|
|
1793
|
+
return path29.split("/").filter(Boolean).map((s) => s.includes("*") || s.startsWith("[...") || s.startsWith("[[...") ? "*" : s).map((s) => /^[:{[]/.test(s) ? ":" : s);
|
|
1794
1794
|
}
|
|
1795
1795
|
function urlPath(e) {
|
|
1796
1796
|
let text;
|
|
@@ -1855,8 +1855,8 @@ function routeCall(r, call, site, target, handlerOf) {
|
|
|
1855
1855
|
if (to) r.servers.push({ method: shape.method, segs: segsOf(shape.path), to });
|
|
1856
1856
|
if (to !== void 0) return;
|
|
1857
1857
|
}
|
|
1858
|
-
const
|
|
1859
|
-
if (!
|
|
1858
|
+
const path29 = arg && urlPath(arg);
|
|
1859
|
+
if (!path29) return;
|
|
1860
1860
|
const m = name.toLowerCase();
|
|
1861
1861
|
let method;
|
|
1862
1862
|
if (ts2.isIdentifier(call.expression) && CLIENT_FNS.has(name)) method = fetchMethod(call);
|
|
@@ -1864,9 +1864,9 @@ function routeCall(r, call, site, target, handlerOf) {
|
|
|
1864
1864
|
method = m.toUpperCase();
|
|
1865
1865
|
else if (target && (target.kind === "function" || target.kind === "method")) method = "ANY";
|
|
1866
1866
|
else return;
|
|
1867
|
-
const segs = segsOf(
|
|
1867
|
+
const segs = segsOf(path29);
|
|
1868
1868
|
if (segs.some((s) => s !== ":" && s !== "*"))
|
|
1869
|
-
r.clients.push({ ...site, method, segs, raw:
|
|
1869
|
+
r.clients.push({ ...site, method, segs, raw: path29 });
|
|
1870
1870
|
}
|
|
1871
1871
|
function serverLiteral(r, lit, to, prefix = false) {
|
|
1872
1872
|
const m = isStr(lit) ? ROUTE_LITERAL.exec(lit.text) : null;
|
|
@@ -3471,8 +3471,8 @@ import path8 from "node:path";
|
|
|
3471
3471
|
import { Language as Language3, Parser as Parser4 } from "web-tree-sitter";
|
|
3472
3472
|
function docstring(body) {
|
|
3473
3473
|
const first = body?.namedChildren[0];
|
|
3474
|
-
const
|
|
3475
|
-
return
|
|
3474
|
+
const str3 = first?.type === "expression_statement" ? first.namedChildren[0] : void 0;
|
|
3475
|
+
return str3?.type === "string" ? stripQuotes(str3.text) : void 0;
|
|
3476
3476
|
}
|
|
3477
3477
|
function dotted(n) {
|
|
3478
3478
|
if (!n) return void 0;
|
|
@@ -3690,7 +3690,7 @@ function buildGraph3(parsed) {
|
|
|
3690
3690
|
roots,
|
|
3691
3691
|
(to, line) => add(fileId(f.file), "imports", fileId(to), ["static"], 1, { line, inLoop: false })
|
|
3692
3692
|
);
|
|
3693
|
-
const inModule = (file,
|
|
3693
|
+
const inModule = (file, path29) => byFile.get(file)?.byQname.get(path29.join("."));
|
|
3694
3694
|
const resolve2 = (callee, fnScopes, classQ) => {
|
|
3695
3695
|
const [head, ...rest] = callee;
|
|
3696
3696
|
if (!head) return void 0;
|
|
@@ -4170,7 +4170,7 @@ function walk(dir, skip, want, out = []) {
|
|
|
4170
4170
|
function references(config) {
|
|
4171
4171
|
if (!fs11.existsSync(config)) return [];
|
|
4172
4172
|
try {
|
|
4173
|
-
const json2 = fs11.readFileSync(config, "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_,
|
|
4173
|
+
const json2 = fs11.readFileSync(config, "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str3) => str3 ?? "").replace(/,(\s*[}\]])/g, "$1");
|
|
4174
4174
|
const refs = JSON.parse(json2).references ?? [];
|
|
4175
4175
|
return refs.map((r) => {
|
|
4176
4176
|
const p = path11.resolve(path11.dirname(config), r.path);
|
|
@@ -5914,42 +5914,314 @@ Options:
|
|
|
5914
5914
|
}
|
|
5915
5915
|
});
|
|
5916
5916
|
|
|
5917
|
+
// packages/cli/src/permissions.ts
|
|
5918
|
+
import fs14 from "node:fs";
|
|
5919
|
+
import path16 from "node:path";
|
|
5920
|
+
function parseSettings(raw) {
|
|
5921
|
+
const p = isRecord2(raw) ? raw.permissions : void 0;
|
|
5922
|
+
return {
|
|
5923
|
+
version: 1,
|
|
5924
|
+
permissions: PERMISSION_MODES.includes(p) ? p : DEFAULT_SETTINGS.permissions
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
function readSettings(repo) {
|
|
5928
|
+
try {
|
|
5929
|
+
return parseSettings(JSON.parse(fs14.readFileSync(settingsFile(repo), "utf8")));
|
|
5930
|
+
} catch {
|
|
5931
|
+
return DEFAULT_SETTINGS;
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
function writeSettings(repo, s) {
|
|
5935
|
+
writeJsonAtomic(settingsFile(repo), s);
|
|
5936
|
+
}
|
|
5937
|
+
function writeJsonAtomic(file, value) {
|
|
5938
|
+
fs14.mkdirSync(path16.dirname(file), { recursive: true });
|
|
5939
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
5940
|
+
fs14.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
5941
|
+
`);
|
|
5942
|
+
fs14.renameSync(tmp, file);
|
|
5943
|
+
}
|
|
5944
|
+
function workerPermissionArgs(mode) {
|
|
5945
|
+
if (mode === "never") return ["--dangerously-skip-permissions"];
|
|
5946
|
+
const allowed = ["mcp__topo__*", ...mode === "safe" ? SAFE_RULES : []];
|
|
5947
|
+
return [
|
|
5948
|
+
"--permission-mode",
|
|
5949
|
+
"acceptEdits",
|
|
5950
|
+
"--allowedTools",
|
|
5951
|
+
allowed.join(","),
|
|
5952
|
+
"--permission-prompt-tool",
|
|
5953
|
+
"mcp__topo__permission_prompt"
|
|
5954
|
+
];
|
|
5955
|
+
}
|
|
5956
|
+
function ruleFor(toolName, input) {
|
|
5957
|
+
if (toolName !== "Bash") return toolName;
|
|
5958
|
+
const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
|
|
5959
|
+
if (!cmd || COMPOUND.test(cmd)) return void 0;
|
|
5960
|
+
const words = cmd.split(/\s+/);
|
|
5961
|
+
const [first = "", second] = words;
|
|
5962
|
+
if (second === void 0) return `Bash(${first})`;
|
|
5963
|
+
if (second.startsWith("-")) return `Bash(${cmd})`;
|
|
5964
|
+
const prefix = SUBCOMMANDS.has(first) ? `${first} ${second}` : first;
|
|
5965
|
+
return `Bash(${prefix} *)`;
|
|
5966
|
+
}
|
|
5967
|
+
function ruleMatches(rule, toolName, input) {
|
|
5968
|
+
const m = /^([^(]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
5969
|
+
if (!m || m[1] !== toolName) return false;
|
|
5970
|
+
const pattern = m[2];
|
|
5971
|
+
if (pattern === void 0) return true;
|
|
5972
|
+
if (toolName !== "Bash") return false;
|
|
5973
|
+
const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
|
|
5974
|
+
if (!cmd || COMPOUND.test(cmd)) return false;
|
|
5975
|
+
const prefix = /^(.*?)(?::\*| \*)$/.exec(pattern)?.[1];
|
|
5976
|
+
if (prefix === void 0) return cmd === pattern;
|
|
5977
|
+
return cmd === prefix || cmd.startsWith(`${prefix} `);
|
|
5978
|
+
}
|
|
5979
|
+
function readLocalSettings(repo) {
|
|
5980
|
+
try {
|
|
5981
|
+
const raw = JSON.parse(fs14.readFileSync(localSettingsFile(repo), "utf8"));
|
|
5982
|
+
return isRecord2(raw) ? raw : {};
|
|
5983
|
+
} catch {
|
|
5984
|
+
return {};
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
function readLocalAllow(repo) {
|
|
5988
|
+
const perms = readLocalSettings(repo).permissions;
|
|
5989
|
+
const allow = isRecord2(perms) ? perms.allow : void 0;
|
|
5990
|
+
return Array.isArray(allow) ? allow.filter((r) => typeof r === "string") : [];
|
|
5991
|
+
}
|
|
5992
|
+
function withAllowRule(doc, rule) {
|
|
5993
|
+
const perms = isRecord2(doc.permissions) ? doc.permissions : {};
|
|
5994
|
+
const allow = Array.isArray(perms.allow) ? perms.allow : [];
|
|
5995
|
+
if (allow.includes(rule)) return doc;
|
|
5996
|
+
return { ...doc, permissions: { ...perms, allow: [...allow, rule] } };
|
|
5997
|
+
}
|
|
5998
|
+
function addLocalAllowRule(repo, rule) {
|
|
5999
|
+
const file = localSettingsFile(repo);
|
|
6000
|
+
let doc = {};
|
|
6001
|
+
if (fs14.existsSync(file)) {
|
|
6002
|
+
const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
6003
|
+
if (!isRecord2(raw)) throw new Error(`${file} isn't a JSON object; not changing it`);
|
|
6004
|
+
doc = raw;
|
|
6005
|
+
}
|
|
6006
|
+
writeJsonAtomic(file, withAllowRule(doc, rule));
|
|
6007
|
+
}
|
|
6008
|
+
var PERMISSION_MODES, DEFAULT_SETTINGS, SAFE_RULES, isRecord2, settingsFile, SUBCOMMANDS, COMPOUND, localSettingsFile;
|
|
6009
|
+
var init_permissions = __esm({
|
|
6010
|
+
"packages/cli/src/permissions.ts"() {
|
|
6011
|
+
"use strict";
|
|
6012
|
+
PERMISSION_MODES = ["safe", "ask", "never"];
|
|
6013
|
+
DEFAULT_SETTINGS = { version: 1, permissions: "safe" };
|
|
6014
|
+
SAFE_RULES = [
|
|
6015
|
+
"Bash(ls *)",
|
|
6016
|
+
"Bash(pwd)",
|
|
6017
|
+
"Bash(cat *)",
|
|
6018
|
+
"Bash(head *)",
|
|
6019
|
+
"Bash(tail *)",
|
|
6020
|
+
"Bash(wc *)",
|
|
6021
|
+
"Bash(grep *)",
|
|
6022
|
+
"Bash(rg *)",
|
|
6023
|
+
"Bash(find *)",
|
|
6024
|
+
"Bash(git status *)",
|
|
6025
|
+
"Bash(git diff *)",
|
|
6026
|
+
"Bash(git log *)",
|
|
6027
|
+
"Bash(git show *)",
|
|
6028
|
+
"Bash(git blame *)"
|
|
6029
|
+
];
|
|
6030
|
+
isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6031
|
+
settingsFile = (repo) => path16.join(repo, ".topo", "settings.json");
|
|
6032
|
+
SUBCOMMANDS = /* @__PURE__ */ new Set(["git", "npm", "pnpm", "yarn", "bun", "npx", "cargo", "go", "docker"]);
|
|
6033
|
+
COMPOUND = /&&|\|\||[;|`<>]|\$\(/;
|
|
6034
|
+
localSettingsFile = (repo) => path16.join(repo, ".claude", "settings.local.json");
|
|
6035
|
+
}
|
|
6036
|
+
});
|
|
6037
|
+
|
|
6038
|
+
// packages/cli/src/adapters.ts
|
|
6039
|
+
var AGENT_IDS, mcpUrl, str, rec, claude, GEMINI_EDIT_TOOLS, gemini, codex, ADAPTERS;
|
|
6040
|
+
var init_adapters = __esm({
|
|
6041
|
+
"packages/cli/src/adapters.ts"() {
|
|
6042
|
+
"use strict";
|
|
6043
|
+
init_hook();
|
|
6044
|
+
init_permissions();
|
|
6045
|
+
AGENT_IDS = ["claude", "codex", "gemini"];
|
|
6046
|
+
mcpUrl = (port) => `http://127.0.0.1:${port}/mcp`;
|
|
6047
|
+
str = (v) => typeof v === "string" ? v : void 0;
|
|
6048
|
+
rec = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
6049
|
+
claude = {
|
|
6050
|
+
id: "claude",
|
|
6051
|
+
name: "Claude",
|
|
6052
|
+
bin: "claude",
|
|
6053
|
+
asksMidRun: true,
|
|
6054
|
+
resumes: true,
|
|
6055
|
+
worker: (t) => ({
|
|
6056
|
+
argv: [
|
|
6057
|
+
"-p",
|
|
6058
|
+
t.prompt,
|
|
6059
|
+
"--output-format",
|
|
6060
|
+
"stream-json",
|
|
6061
|
+
"--verbose",
|
|
6062
|
+
"--mcp-config",
|
|
6063
|
+
"{file:mcp}",
|
|
6064
|
+
"--settings",
|
|
6065
|
+
"{file:settings}",
|
|
6066
|
+
...workerPermissionArgs(t.mode),
|
|
6067
|
+
// Without this, workers run on the user's default model (often the priciest one).
|
|
6068
|
+
"--model",
|
|
6069
|
+
process.env.TOPO_WORKER_MODEL ?? "sonnet",
|
|
6070
|
+
...t.resume ? ["--resume", t.resume] : []
|
|
6071
|
+
],
|
|
6072
|
+
json: {
|
|
6073
|
+
mcp: {
|
|
6074
|
+
mcpServers: {
|
|
6075
|
+
topo: { type: "http", url: mcpUrl(t.port), headers: { "x-topo-run": t.runId } }
|
|
6076
|
+
}
|
|
6077
|
+
},
|
|
6078
|
+
// Its edits come in through the same PostToolUse hook `topo hook` prints.
|
|
6079
|
+
settings: hookSettings(t.port)
|
|
6080
|
+
}
|
|
6081
|
+
}),
|
|
6082
|
+
parse: (o) => {
|
|
6083
|
+
if (o.type === "system" && o.subtype === "init" && str(o.session_id))
|
|
6084
|
+
return [{ kind: "started", session: o.session_id }];
|
|
6085
|
+
if (o.type === "result")
|
|
6086
|
+
return [
|
|
6087
|
+
{
|
|
6088
|
+
kind: "finished",
|
|
6089
|
+
failed: o.is_error === true,
|
|
6090
|
+
summary: str(o.result),
|
|
6091
|
+
costUsd: typeof o.total_cost_usd === "number" ? o.total_cost_usd : void 0
|
|
6092
|
+
}
|
|
6093
|
+
];
|
|
6094
|
+
return [];
|
|
6095
|
+
}
|
|
6096
|
+
};
|
|
6097
|
+
GEMINI_EDIT_TOOLS = /* @__PURE__ */ new Set(["write_file", "replace", "edit"]);
|
|
6098
|
+
gemini = {
|
|
6099
|
+
id: "gemini",
|
|
6100
|
+
name: "Gemini",
|
|
6101
|
+
bin: "gemini",
|
|
6102
|
+
asksMidRun: false,
|
|
6103
|
+
resumes: false,
|
|
6104
|
+
// --resume takes "latest" or a list index, not a session id
|
|
6105
|
+
worker: (t) => ({
|
|
6106
|
+
argv: [
|
|
6107
|
+
"-p",
|
|
6108
|
+
t.prompt,
|
|
6109
|
+
"--output-format",
|
|
6110
|
+
"stream-json",
|
|
6111
|
+
"--skip-trust",
|
|
6112
|
+
"--approval-mode",
|
|
6113
|
+
t.mode === "never" ? "yolo" : "auto_edit",
|
|
6114
|
+
...process.env.TOPO_GEMINI_MODEL ? ["--model", process.env.TOPO_GEMINI_MODEL] : []
|
|
6115
|
+
],
|
|
6116
|
+
env: { GEMINI_CLI_SYSTEM_SETTINGS_PATH: "{file:settings}" },
|
|
6117
|
+
json: {
|
|
6118
|
+
settings: {
|
|
6119
|
+
mcpServers: {
|
|
6120
|
+
topo: { httpUrl: mcpUrl(t.port), headers: { "x-topo-run": t.runId }, trust: true }
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
}),
|
|
6125
|
+
parse: (o) => {
|
|
6126
|
+
if (o.type === "init" && str(o.session_id))
|
|
6127
|
+
return [{ kind: "started", session: o.session_id }];
|
|
6128
|
+
if (o.type === "message" && o.role === "assistant" && str(o.content))
|
|
6129
|
+
return [{ kind: "text", text: o.content, append: o.delta === true }];
|
|
6130
|
+
if (o.type === "tool_use" && GEMINI_EDIT_TOOLS.has(str(o.tool_name) ?? "")) {
|
|
6131
|
+
const file = str(rec(o.parameters).file_path);
|
|
6132
|
+
return file ? [{ kind: "edit", file }] : [];
|
|
6133
|
+
}
|
|
6134
|
+
if (o.type === "result") return [{ kind: "finished", failed: o.status !== "success" }];
|
|
6135
|
+
return [];
|
|
6136
|
+
}
|
|
6137
|
+
};
|
|
6138
|
+
codex = {
|
|
6139
|
+
id: "codex",
|
|
6140
|
+
name: "Codex",
|
|
6141
|
+
bin: "codex",
|
|
6142
|
+
asksMidRun: false,
|
|
6143
|
+
resumes: true,
|
|
6144
|
+
worker: (t) => {
|
|
6145
|
+
const opts = [
|
|
6146
|
+
"--json",
|
|
6147
|
+
"-c",
|
|
6148
|
+
`mcp_servers.topo.url=${JSON.stringify(mcpUrl(t.port))}`,
|
|
6149
|
+
"-c",
|
|
6150
|
+
`mcp_servers.topo.http_headers={ "x-topo-run" = ${JSON.stringify(t.runId)} }`,
|
|
6151
|
+
...t.mode === "never" ? ["--dangerously-bypass-approvals-and-sandbox"] : ["-s", "workspace-write", "-c", 'approval_policy="never"'],
|
|
6152
|
+
...process.env.TOPO_CODEX_MODEL ? ["-m", process.env.TOPO_CODEX_MODEL] : []
|
|
6153
|
+
];
|
|
6154
|
+
return {
|
|
6155
|
+
argv: t.resume ? ["exec", "resume", ...opts, t.resume, t.prompt] : ["exec", ...opts, t.prompt]
|
|
6156
|
+
};
|
|
6157
|
+
},
|
|
6158
|
+
parse: (o) => {
|
|
6159
|
+
if (o.type === "thread.started" && str(o.thread_id))
|
|
6160
|
+
return [{ kind: "started", session: o.thread_id }];
|
|
6161
|
+
if (o.type === "item.completed") {
|
|
6162
|
+
const item = rec(o.item);
|
|
6163
|
+
if (item.type === "agent_message" && str(item.text))
|
|
6164
|
+
return [{ kind: "text", text: item.text }];
|
|
6165
|
+
if (item.type === "file_change" && Array.isArray(item.changes))
|
|
6166
|
+
return item.changes.flatMap((c) => {
|
|
6167
|
+
const file = str(rec(c).path);
|
|
6168
|
+
return file ? [{ kind: "edit", file }] : [];
|
|
6169
|
+
});
|
|
6170
|
+
}
|
|
6171
|
+
if (o.type === "turn.completed") return [{ kind: "finished", failed: false }];
|
|
6172
|
+
if (o.type === "turn.failed")
|
|
6173
|
+
return [{ kind: "finished", failed: true, summary: str(rec(o.error).message) }];
|
|
6174
|
+
return [];
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
ADAPTERS = { claude, codex, gemini };
|
|
6178
|
+
}
|
|
6179
|
+
});
|
|
6180
|
+
|
|
5917
6181
|
// packages/cli/src/agents.ts
|
|
5918
6182
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
5919
|
-
import
|
|
6183
|
+
import fs15 from "node:fs";
|
|
5920
6184
|
import os2 from "node:os";
|
|
5921
|
-
import
|
|
6185
|
+
import path17 from "node:path";
|
|
6186
|
+
function describeContext(c) {
|
|
6187
|
+
const roots = c.roots ?? [];
|
|
6188
|
+
const what = c.mode === "review" ? "the review of the changes" : c.mode === "flow" ? "a flow" : "a map";
|
|
6189
|
+
const depth = c.depth === void 0 ? "" : c.depth === 0 ? ", just those blocks" : `, with what they call and what calls them ${c.depth} hop(s) out`;
|
|
6190
|
+
const more = roots.length > MAX_CONTEXT_ROOTS ? ` and ${roots.length - MAX_CONTEXT_ROOTS} more` : "";
|
|
6191
|
+
const sel = c.selected?.length ? ` Selected: ${c.selected.join(", ")}.` : "";
|
|
6192
|
+
return roots.length ? `The human's map right now: ${what}, rooted at ${roots.length} block(s)${depth}: ${roots.slice(0, MAX_CONTEXT_ROOTS).join(", ")}${more}.${sel}` : `The human's map right now: ${what}, empty.${sel}`;
|
|
6193
|
+
}
|
|
5922
6194
|
function resolveBin(bin) {
|
|
5923
6195
|
const tryExec = (p) => {
|
|
5924
6196
|
try {
|
|
5925
|
-
|
|
6197
|
+
fs15.accessSync(p, fs15.constants.X_OK);
|
|
5926
6198
|
return p;
|
|
5927
6199
|
} catch {
|
|
5928
6200
|
return void 0;
|
|
5929
6201
|
}
|
|
5930
6202
|
};
|
|
5931
|
-
if (bin.includes(
|
|
5932
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
6203
|
+
if (bin.includes(path17.sep)) return tryExec(bin);
|
|
6204
|
+
const dirs = (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean);
|
|
5933
6205
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
|
|
5934
6206
|
for (const dir of dirs) {
|
|
5935
6207
|
for (const ext of exts) {
|
|
5936
|
-
const hit = tryExec(
|
|
6208
|
+
const hit = tryExec(path17.join(dir, bin + ext));
|
|
5937
6209
|
if (hit) return hit;
|
|
5938
6210
|
}
|
|
5939
6211
|
}
|
|
5940
6212
|
return void 0;
|
|
5941
6213
|
}
|
|
5942
6214
|
function writeTempJson(prefix, value) {
|
|
5943
|
-
const file =
|
|
6215
|
+
const file = path17.join(
|
|
5944
6216
|
os2.tmpdir(),
|
|
5945
6217
|
`${prefix}-${process.pid}-${Math.random().toString(36).slice(2)}.json`
|
|
5946
6218
|
);
|
|
5947
|
-
|
|
6219
|
+
fs15.writeFileSync(file, JSON.stringify(value));
|
|
5948
6220
|
return file;
|
|
5949
6221
|
}
|
|
5950
6222
|
function rmQuiet(file) {
|
|
5951
6223
|
try {
|
|
5952
|
-
|
|
6224
|
+
fs15.unlinkSync(file);
|
|
5953
6225
|
} catch {
|
|
5954
6226
|
}
|
|
5955
6227
|
}
|
|
@@ -5979,6 +6251,11 @@ function createAgentManager(opts) {
|
|
|
5979
6251
|
const spawn3 = opts.spawn ?? nodeSpawn;
|
|
5980
6252
|
const claudeBin = opts.claudeBin ?? "claude";
|
|
5981
6253
|
const resolved = resolveBin(claudeBin);
|
|
6254
|
+
const installed = /* @__PURE__ */ new Map();
|
|
6255
|
+
for (const id of AGENT_IDS) {
|
|
6256
|
+
const bin = id === "claude" ? resolved : resolveBin(opts.bins?.[id] ?? ADAPTERS[id].bin);
|
|
6257
|
+
if (bin) installed.set(id, bin);
|
|
6258
|
+
}
|
|
5982
6259
|
const runs = /* @__PURE__ */ new Map();
|
|
5983
6260
|
const children = /* @__PURE__ */ new Map();
|
|
5984
6261
|
let runSeq = 0;
|
|
@@ -6001,13 +6278,14 @@ function createAgentManager(opts) {
|
|
|
6001
6278
|
opts.bus.subscribe((e) => {
|
|
6002
6279
|
if ((e.type === "explain" || e.type === "focus" || e.type === "lens") && e.command)
|
|
6003
6280
|
updateExchange(e.command, () => ({ view: e }));
|
|
6281
|
+
else if (e.type === "ask" && e.command) updateExchange(e.command, () => ({ ask: e.questions }));
|
|
6004
6282
|
});
|
|
6005
6283
|
function setTerritoryStatus(territoryId, patch) {
|
|
6006
6284
|
const territories = opts.getTerritories();
|
|
6007
6285
|
const next = territories.map((t) => t.id === territoryId ? { ...t, ...patch } : t);
|
|
6008
6286
|
opts.saveTerritories(next);
|
|
6009
6287
|
}
|
|
6010
|
-
function launch(territoryId, reply) {
|
|
6288
|
+
function launch(territoryId, reply, agent) {
|
|
6011
6289
|
const territory = opts.getTerritories().find((t) => t.id === territoryId);
|
|
6012
6290
|
if (!territory) throw new Error(`no territory "${territoryId}"`);
|
|
6013
6291
|
const territoryRuns = [...runs.values()].filter((r) => r.territoryId === territoryId);
|
|
@@ -6017,99 +6295,97 @@ function createAgentManager(opts) {
|
|
|
6017
6295
|
if (!reply && !territory.note?.trim()) {
|
|
6018
6296
|
throw new Error(`"${territoryId}" has no note; write the task before launching`);
|
|
6019
6297
|
}
|
|
6020
|
-
const
|
|
6298
|
+
const wanted = agent ?? territory.runner;
|
|
6299
|
+
if (wanted !== void 0 && !AGENT_IDS.includes(wanted)) {
|
|
6300
|
+
throw new Error(`unknown agent "${wanted}" (one of ${AGENT_IDS.join(", ")})`);
|
|
6301
|
+
}
|
|
6302
|
+
const adapter = wanted ? ADAPTERS[wanted] : ADAPTERS[[...installed.keys()][0] ?? "claude"];
|
|
6303
|
+
const bin = adapter && installed.get(adapter.id);
|
|
6304
|
+
if (!adapter || !bin) throw new Error(`no \`${adapter?.bin ?? "claude"}\` CLI found on PATH`);
|
|
6305
|
+
const prefix = `${adapter.id}:`;
|
|
6306
|
+
const resumeFrom = reply ? territoryRuns.findLast((r) => r.agent?.startsWith(prefix))?.agent?.slice(prefix.length) : void 0;
|
|
6307
|
+
if (reply && !adapter.resumes) {
|
|
6308
|
+
throw new Error(`${adapter.name} can't continue an earlier run; launch it again instead`);
|
|
6309
|
+
}
|
|
6021
6310
|
if (reply && !resumeFrom) throw new Error(`"${territoryId}" has no earlier run to continue`);
|
|
6022
|
-
if (
|
|
6311
|
+
if (agent && territory.runner !== adapter.id) {
|
|
6312
|
+
setTerritoryStatus(territoryId, { runner: adapter.id });
|
|
6313
|
+
}
|
|
6023
6314
|
const id = `run-${++runSeq}`;
|
|
6024
|
-
const mcpConfig = writeTempJson("topo-mcp", {
|
|
6025
|
-
mcpServers: {
|
|
6026
|
-
topo: {
|
|
6027
|
-
type: "http",
|
|
6028
|
-
url: `http://127.0.0.1:${opts.port()}/mcp`,
|
|
6029
|
-
headers: { "x-topo-run": id }
|
|
6030
|
-
}
|
|
6031
|
-
}
|
|
6032
|
-
});
|
|
6033
|
-
const settings = writeTempJson("topo-settings", hookSettings(opts.port()));
|
|
6034
6315
|
const prompt = reply ?? [
|
|
6035
6316
|
territory.label,
|
|
6036
6317
|
territory.note,
|
|
6037
6318
|
`Call get_territory ${territory.id} first. Stay inside it; if you need blocks outside, call request_territory. After changing a block, call report_change with one sentence on why.`
|
|
6038
6319
|
].filter((s) => !!s).join("\n\n");
|
|
6320
|
+
const spec = adapter.worker({
|
|
6321
|
+
runId: id,
|
|
6322
|
+
prompt,
|
|
6323
|
+
port: opts.port(),
|
|
6324
|
+
mode: opts.permissionMode?.() ?? "safe",
|
|
6325
|
+
resume: resumeFrom
|
|
6326
|
+
});
|
|
6327
|
+
const files = new Map(
|
|
6328
|
+
Object.entries(spec.json ?? {}).map(([k, v]) => [k, writeTempJson(`topo-${k}`, v)])
|
|
6329
|
+
);
|
|
6330
|
+
const fill = (s) => s.replace(/^\{file:(\w+)\}$/, (_, k) => files.get(k) ?? s);
|
|
6039
6331
|
const run8 = {
|
|
6040
6332
|
id,
|
|
6041
6333
|
role: "worker",
|
|
6042
6334
|
territoryId,
|
|
6043
|
-
label:
|
|
6335
|
+
label: `${adapter.name} \xB7 ${territory.label}`,
|
|
6044
6336
|
status: "starting",
|
|
6045
6337
|
startedAt: Date.now()
|
|
6046
6338
|
};
|
|
6047
6339
|
runs.set(id, run8);
|
|
6048
|
-
const child = spawn3(
|
|
6049
|
-
|
|
6050
|
-
[
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
"--settings",
|
|
6059
|
-
settings,
|
|
6060
|
-
...opts.skipPermissions ? ["--dangerously-skip-permissions"] : [
|
|
6061
|
-
"--permission-mode",
|
|
6062
|
-
"acceptEdits",
|
|
6063
|
-
// Topo's own tools never prompt (otherwise every get_territory would wait in the
|
|
6064
|
-
// inbox); everything acceptEdits doesn't cover goes to the inbox via the prompt tool.
|
|
6065
|
-
"--allowedTools",
|
|
6066
|
-
"mcp__topo__*",
|
|
6067
|
-
"--permission-prompt-tool",
|
|
6068
|
-
"mcp__topo__permission_prompt"
|
|
6069
|
-
],
|
|
6070
|
-
// Without this, workers run on the user's default model (often the priciest one).
|
|
6071
|
-
"--model",
|
|
6072
|
-
process.env.TOPO_WORKER_MODEL ?? "sonnet",
|
|
6073
|
-
...resumeFrom ? ["--resume", resumeFrom] : []
|
|
6074
|
-
],
|
|
6075
|
-
{ cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
|
|
6076
|
-
);
|
|
6340
|
+
const child = spawn3(bin, spec.argv.map(fill), {
|
|
6341
|
+
cwd: opts.repo,
|
|
6342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6343
|
+
...spec.env && {
|
|
6344
|
+
env: {
|
|
6345
|
+
...process.env,
|
|
6346
|
+
...Object.fromEntries(Object.entries(spec.env).map(([k, v]) => [k, fill(v)]))
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
});
|
|
6077
6350
|
children.set(id, child);
|
|
6078
6351
|
let gotResult = false;
|
|
6352
|
+
let summary;
|
|
6079
6353
|
const cleanup = () => {
|
|
6080
6354
|
children.delete(id);
|
|
6081
|
-
rmQuiet(
|
|
6082
|
-
rmQuiet(settings);
|
|
6355
|
+
for (const f of files.values()) rmQuiet(f);
|
|
6083
6356
|
};
|
|
6084
6357
|
child.stdout?.on(
|
|
6085
6358
|
"data",
|
|
6086
6359
|
makeLineFeed((obj) => {
|
|
6087
|
-
const
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6360
|
+
for (const ev of adapter.parse(obj)) {
|
|
6361
|
+
if (ev.kind === "started") {
|
|
6362
|
+
run8.agent = `${prefix}${ev.session}`;
|
|
6363
|
+
run8.status = "running";
|
|
6364
|
+
setTerritoryStatus(territoryId, { agent: run8.agent, status: "active" });
|
|
6365
|
+
emitRuns();
|
|
6366
|
+
} else if (ev.kind === "text") {
|
|
6367
|
+
summary = ev.append ? (summary ?? "") + ev.text : ev.text;
|
|
6368
|
+
} else if (ev.kind === "edit") {
|
|
6369
|
+
if (run8.agent) opts.onEdit?.(run8.agent, ev.file);
|
|
6370
|
+
} else if (ev.kind === "finished") {
|
|
6371
|
+
gotResult = true;
|
|
6372
|
+
run8.status = ev.failed ? "failed" : "done";
|
|
6373
|
+
run8.summary = ev.summary ?? summary;
|
|
6374
|
+
run8.costUsd = ev.costUsd;
|
|
6375
|
+
run8.endedAt = Date.now();
|
|
6376
|
+
setTerritoryStatus(territoryId, { status: "review" });
|
|
6377
|
+
opts.inbox.add({
|
|
6378
|
+
kind: "finished",
|
|
6379
|
+
title: `${territory.label}: ${run8.status === "failed" ? "failed" : "finished"}`,
|
|
6380
|
+
detail: run8.summary,
|
|
6381
|
+
agent: run8.agent,
|
|
6382
|
+
label: run8.label,
|
|
6383
|
+
runId: id,
|
|
6384
|
+
territoryId,
|
|
6385
|
+
nodeIds: territory.nodeIds
|
|
6386
|
+
});
|
|
6387
|
+
emitRuns();
|
|
6388
|
+
}
|
|
6113
6389
|
}
|
|
6114
6390
|
})
|
|
6115
6391
|
);
|
|
@@ -6146,7 +6422,7 @@ function createAgentManager(opts) {
|
|
|
6146
6422
|
const id = `cmd-${++cmdSeq}`;
|
|
6147
6423
|
const contextLine = context ? `
|
|
6148
6424
|
|
|
6149
|
-
|
|
6425
|
+
${describeContext(context)}` : "";
|
|
6150
6426
|
exchanges = [
|
|
6151
6427
|
...exchanges.slice(-(MAX_EXCHANGES - 1)),
|
|
6152
6428
|
{ id, text, status: "running", tools: [], at: Date.now() }
|
|
@@ -6248,8 +6524,17 @@ Context: ${JSON.stringify(context)}` : "";
|
|
|
6248
6524
|
}
|
|
6249
6525
|
return {
|
|
6250
6526
|
get available() {
|
|
6527
|
+
return installed.size > 0;
|
|
6528
|
+
},
|
|
6529
|
+
get director() {
|
|
6251
6530
|
return resolved !== void 0;
|
|
6252
6531
|
},
|
|
6532
|
+
agents: () => [...installed.keys()].map((id) => ({
|
|
6533
|
+
id,
|
|
6534
|
+
name: ADAPTERS[id].name,
|
|
6535
|
+
asksMidRun: ADAPTERS[id].asksMidRun,
|
|
6536
|
+
resumes: ADAPTERS[id].resumes
|
|
6537
|
+
})),
|
|
6253
6538
|
list: () => [...runs.values()],
|
|
6254
6539
|
get: (runId) => runs.get(runId),
|
|
6255
6540
|
launch,
|
|
@@ -6259,20 +6544,25 @@ Context: ${JSON.stringify(context)}` : "";
|
|
|
6259
6544
|
newConversation
|
|
6260
6545
|
};
|
|
6261
6546
|
}
|
|
6262
|
-
var DIRECTOR_SYSTEM_PROMPT, MAX_EXCHANGES, isActive;
|
|
6547
|
+
var DIRECTOR_SYSTEM_PROMPT, MAX_EXCHANGES, MAX_CONTEXT_ROOTS, isActive;
|
|
6263
6548
|
var init_agents = __esm({
|
|
6264
6549
|
async "packages/cli/src/agents.ts"() {
|
|
6265
6550
|
"use strict";
|
|
6266
6551
|
await init_src();
|
|
6267
|
-
|
|
6552
|
+
init_adapters();
|
|
6268
6553
|
DIRECTOR_SYSTEM_PROMPT = [
|
|
6269
6554
|
"You answer the human's questions about this codebase and drive the Topo map for them, through the topo tools only. You never edit code.",
|
|
6270
|
-
"For a question like how does X work, where is X, or what happens when Y: trace it with find, callees, callers and get_block (read the source of the blocks that matter), then call explain once with
|
|
6555
|
+
"For a question like how does X work, where is X, or what happens when Y: trace it with find, callees, callers and get_block (read the source of the blocks that matter), then call explain once with the steps in execution order. Size it to the question: about 5 steps for the gist, every step that matters when asked for all of it or the details. Pick only blocks that matter to the question; leave out UI, logging and plumbing that merely touches it. If explain reports unresolved steps, fix them and call it again.",
|
|
6556
|
+
"Each message ends with a line saying what the human's map shows right now. For walk me through this, the current map, these changes, or what changed: the blocks are that map's roots (for every change instead, call view with no roots and depth 1 and use its roots). Read them with get_block, then call explain with them in run order (callers before what they call). Each step's text is 2-4 sentences: what the block does, and for a changed block what changed there and why it matters. A tour or walkthrough request always ends in explain, never in only a view change.",
|
|
6271
6557
|
"For a request to change the view (a lens, a flow, focusing something), territories, agents or the inbox: use those tools and do it.",
|
|
6558
|
+
"For a request to change the code (fix, add, rename, refactor, remove): the request is the go-ahead, so hand it to a worker without asking. Find the blocks it touches, create_territory with a short label, those blocks, and a note that states the task in full (the worker sees only the note and its blocks), then launch_agent on it. Reply in a line or two: what the worker will do, and that its edits will show on the map. Ask first (with ask_user) only when the request is too vague to write the note.",
|
|
6559
|
+
"To plan a change (plan a change to X, how would I add Y): trace the blocks involved, then call explain with the plan as steps on the blocks it touches (what changes in each and why), and create_territory for each piece of work with a note that states it in full. Use ask_user where the plan forks. Don't launch_agent until the human says go.",
|
|
6560
|
+
`When a request is ambiguous in a way that changes what you'd do (how many agents, which territories to create, how long a walkthrough, which of several areas), call ask_user with concrete options and end your turn; the answer comes as the next message. Don't ask when a sensible default exists, and never ask for a go-ahead on a change already requested. Never end a reply by offering choices in prose ("I can do A, or B. Which?"): put them in ask_user so the human can click one.`,
|
|
6272
6561
|
"To sort the code into layers (or regions, areas, parts): call get_layers, read what the files hold, then set_layers with 3-8 layers named by what the code does, so every source file lands in one. Say what each layer is in a line.",
|
|
6273
6562
|
"Then reply in plain prose: a few sentences, or a numbered list that mirrors the steps. No headings, no tables, no preamble about which tools you called."
|
|
6274
6563
|
].join("\n\n");
|
|
6275
6564
|
MAX_EXCHANGES = 50;
|
|
6565
|
+
MAX_CONTEXT_ROOTS = 60;
|
|
6276
6566
|
isActive = (status) => status === "starting" || status === "running";
|
|
6277
6567
|
}
|
|
6278
6568
|
});
|
|
@@ -6314,7 +6604,7 @@ var init_attribution = __esm({
|
|
|
6314
6604
|
|
|
6315
6605
|
// packages/cli/src/editor.ts
|
|
6316
6606
|
import { spawn } from "node:child_process";
|
|
6317
|
-
import
|
|
6607
|
+
import path18 from "node:path";
|
|
6318
6608
|
function editorTemplate(flag) {
|
|
6319
6609
|
return flag ?? process.env.TOPO_EDITOR ?? DEFAULT_EDITOR;
|
|
6320
6610
|
}
|
|
@@ -6343,10 +6633,10 @@ function splitTemplate(template) {
|
|
|
6343
6633
|
return out;
|
|
6344
6634
|
}
|
|
6345
6635
|
function resolveInRepo(repo, file) {
|
|
6346
|
-
const root =
|
|
6347
|
-
const abs =
|
|
6348
|
-
const rel =
|
|
6349
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
6636
|
+
const root = path18.resolve(repo);
|
|
6637
|
+
const abs = path18.resolve(root, file);
|
|
6638
|
+
const rel = path18.relative(root, abs);
|
|
6639
|
+
if (rel === "" || rel.startsWith("..") || path18.isAbsolute(rel)) {
|
|
6350
6640
|
throw new Error(`path outside repository: ${file}`);
|
|
6351
6641
|
}
|
|
6352
6642
|
return abs;
|
|
@@ -6408,6 +6698,18 @@ function createInbox(bus, onDecision) {
|
|
|
6408
6698
|
return {
|
|
6409
6699
|
list: () => items,
|
|
6410
6700
|
add(input) {
|
|
6701
|
+
if (input.kind === "out_of_territory" && input.agent !== void 0) {
|
|
6702
|
+
const idx = items.findIndex(
|
|
6703
|
+
(it) => it.kind === input.kind && it.agent === input.agent && it.status === "open"
|
|
6704
|
+
);
|
|
6705
|
+
if (idx !== -1) {
|
|
6706
|
+
const [prev] = items.splice(idx, 1);
|
|
6707
|
+
const item2 = { ...prev, ...input, at: Date.now() };
|
|
6708
|
+
items.unshift(item2);
|
|
6709
|
+
bus.emit({ type: "inbox", item: item2 });
|
|
6710
|
+
return item2;
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6411
6713
|
const item = {
|
|
6412
6714
|
...input,
|
|
6413
6715
|
id: `inbox-${++counter}`,
|
|
@@ -6550,6 +6852,72 @@ var init_conflicts = __esm({
|
|
|
6550
6852
|
}
|
|
6551
6853
|
});
|
|
6552
6854
|
|
|
6855
|
+
// packages/cli/src/reviews.ts
|
|
6856
|
+
import fs16 from "node:fs";
|
|
6857
|
+
import path19 from "node:path";
|
|
6858
|
+
function readDoc(repo) {
|
|
6859
|
+
try {
|
|
6860
|
+
const doc = JSON.parse(fs16.readFileSync(reviewsFile(repo), "utf8"));
|
|
6861
|
+
if (doc?.version === 1 && doc.reviews && typeof doc.reviews === "object") return doc;
|
|
6862
|
+
} catch {
|
|
6863
|
+
}
|
|
6864
|
+
return { version: 1, reviews: {} };
|
|
6865
|
+
}
|
|
6866
|
+
function readReviews(repo, key) {
|
|
6867
|
+
const saved = readDoc(repo).reviews[key];
|
|
6868
|
+
if (!saved || typeof saved !== "object") return /* @__PURE__ */ new Map();
|
|
6869
|
+
return new Map(Object.entries(saved).filter(([, r]) => isReview(r)));
|
|
6870
|
+
}
|
|
6871
|
+
function writeReviews(repo, key, reviews) {
|
|
6872
|
+
const doc = readDoc(repo);
|
|
6873
|
+
delete doc.reviews[key];
|
|
6874
|
+
if (reviews.size) doc.reviews[key] = Object.fromEntries(reviews);
|
|
6875
|
+
for (const old of Object.keys(doc.reviews).slice(0, -KEEP_REVIEWS)) delete doc.reviews[old];
|
|
6876
|
+
const file = reviewsFile(repo);
|
|
6877
|
+
fs16.mkdirSync(path19.dirname(file), { recursive: true });
|
|
6878
|
+
const ignore = path19.join(path19.dirname(file), ".gitignore");
|
|
6879
|
+
const ignored = fs16.existsSync(ignore) ? fs16.readFileSync(ignore, "utf8") : "";
|
|
6880
|
+
if (!/^reviews\.json$/m.test(ignored))
|
|
6881
|
+
fs16.appendFileSync(ignore, `${ignored && !ignored.endsWith("\n") ? "\n" : ""}reviews.json
|
|
6882
|
+
`);
|
|
6883
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
6884
|
+
fs16.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
6885
|
+
`);
|
|
6886
|
+
fs16.renameSync(tmp, file);
|
|
6887
|
+
}
|
|
6888
|
+
function fingerprint(change, head) {
|
|
6889
|
+
return `${change.changeType}:${head?.bodyHash ?? ""}`;
|
|
6890
|
+
}
|
|
6891
|
+
function currentMark(mark, change, head) {
|
|
6892
|
+
if (!mark) return { review: "pending" };
|
|
6893
|
+
if (mark.decision === "viewed" && mark.fp !== void 0 && mark.fp !== fingerprint(change, head))
|
|
6894
|
+
return { review: "pending", stale: true };
|
|
6895
|
+
return { review: mark.decision, ...mark.note && { note: mark.note } };
|
|
6896
|
+
}
|
|
6897
|
+
function askTerritory(agent, territories) {
|
|
6898
|
+
return agent && agent !== "you" ? territories.find((t) => t.agent === agent)?.id : void 0;
|
|
6899
|
+
}
|
|
6900
|
+
function askReply(asks) {
|
|
6901
|
+
return [
|
|
6902
|
+
"I reviewed your change in Topo. Please make these changes:",
|
|
6903
|
+
...asks.map((a) => `- ${a.nodeId}: ${a.note ?? "needs another look"}`),
|
|
6904
|
+
"After each one, call report_change with one sentence on what you did."
|
|
6905
|
+
].join("\n");
|
|
6906
|
+
}
|
|
6907
|
+
var KEEP_REVIEWS, reviewKey, reviewsFile, isReview;
|
|
6908
|
+
var init_reviews = __esm({
|
|
6909
|
+
"packages/cli/src/reviews.ts"() {
|
|
6910
|
+
"use strict";
|
|
6911
|
+
KEEP_REVIEWS = 20;
|
|
6912
|
+
reviewKey = (cs) => `${cs.baseSha}..${cs.headRef}`;
|
|
6913
|
+
reviewsFile = (repo) => path19.join(repo, ".topo", "reviews.json");
|
|
6914
|
+
isReview = (r) => {
|
|
6915
|
+
const x = r;
|
|
6916
|
+
return !!x && (x.decision === "viewed" || x.decision === "changes_requested") && (x.note === void 0 || typeof x.note === "string") && (x.fp === void 0 || typeof x.fp === "string");
|
|
6917
|
+
};
|
|
6918
|
+
}
|
|
6919
|
+
});
|
|
6920
|
+
|
|
6553
6921
|
// packages/cli/src/live.ts
|
|
6554
6922
|
function createEventBus() {
|
|
6555
6923
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -6604,8 +6972,19 @@ function labelFor(attribution, agent) {
|
|
|
6604
6972
|
}
|
|
6605
6973
|
function annotateChangeSet(changeSet, opts) {
|
|
6606
6974
|
const blocks = changeSet.blocks.map((raw) => {
|
|
6607
|
-
const
|
|
6608
|
-
|
|
6975
|
+
const mark = currentMark(
|
|
6976
|
+
opts.tracking.reviews.get(raw.nodeId),
|
|
6977
|
+
raw,
|
|
6978
|
+
opts.index.byId.get(raw.nodeId)
|
|
6979
|
+
);
|
|
6980
|
+
const agentTerritory = askTerritory(opts.tracking.blockAgent.get(raw.nodeId), opts.territories);
|
|
6981
|
+
const b = {
|
|
6982
|
+
...raw,
|
|
6983
|
+
review: mark.review,
|
|
6984
|
+
...mark.note && { reviewNote: mark.note },
|
|
6985
|
+
...mark.stale && { viewedStale: true },
|
|
6986
|
+
...agentTerritory && { agentTerritory }
|
|
6987
|
+
};
|
|
6609
6988
|
const rationale = opts.tracking.rationale.get(b.nodeId);
|
|
6610
6989
|
const agent = opts.tracking.blockAgent.get(b.nodeId);
|
|
6611
6990
|
const territory = agent ? opts.territories.find((t) => t.agent === agent) : void 0;
|
|
@@ -6637,13 +7016,17 @@ function createLive(opts) {
|
|
|
6637
7016
|
const territory = territories.find(
|
|
6638
7017
|
(t) => t.agent === activity.agent || activity.session !== void 0 && t.agent === activity.session
|
|
6639
7018
|
);
|
|
7019
|
+
const open = opts.inbox.list().find(
|
|
7020
|
+
(it) => it.kind === "out_of_territory" && it.agent === activity.agent && it.status === "open"
|
|
7021
|
+
);
|
|
7022
|
+
const all = [.../* @__PURE__ */ new Set([...open?.nodeIds ?? [], ...outOfTerritory])];
|
|
6640
7023
|
opts.inbox.add({
|
|
6641
7024
|
kind: "out_of_territory",
|
|
6642
|
-
title: `${ownLabel} edited ${
|
|
7025
|
+
title: `${ownLabel} edited ${all.length} block${all.length === 1 ? "" : "s"} outside ${territory?.label ?? "its territory"}`,
|
|
6643
7026
|
agent: activity.agent,
|
|
6644
7027
|
label: activity.label,
|
|
6645
7028
|
territoryId: territory?.id,
|
|
6646
|
-
nodeIds:
|
|
7029
|
+
nodeIds: all
|
|
6647
7030
|
});
|
|
6648
7031
|
}
|
|
6649
7032
|
const changed = nodeIds.map((nodeId2) => ({
|
|
@@ -6737,12 +7120,13 @@ var init_live2 = __esm({
|
|
|
6737
7120
|
await init_src();
|
|
6738
7121
|
await init_attribution();
|
|
6739
7122
|
await init_conflicts();
|
|
7123
|
+
init_reviews();
|
|
6740
7124
|
}
|
|
6741
7125
|
});
|
|
6742
7126
|
|
|
6743
7127
|
// packages/cli/src/mcp.ts
|
|
6744
|
-
import
|
|
6745
|
-
import
|
|
7128
|
+
import fs17 from "node:fs";
|
|
7129
|
+
import path20 from "node:path";
|
|
6746
7130
|
function qualifiedName(n) {
|
|
6747
7131
|
const i = n.id.indexOf("#");
|
|
6748
7132
|
return i === -1 ? n.name : n.id.slice(i + 1);
|
|
@@ -6805,6 +7189,38 @@ function requireString(args, key) {
|
|
|
6805
7189
|
if (typeof v !== "string" || !v) throw new Error(`missing required "${key}"`);
|
|
6806
7190
|
return v;
|
|
6807
7191
|
}
|
|
7192
|
+
function parseQuestions(raw) {
|
|
7193
|
+
if (!Array.isArray(raw) || raw.length < 1 || raw.length > 4)
|
|
7194
|
+
throw new Error('"questions" must hold 1-4 {question, header?, options, multiSelect?} items');
|
|
7195
|
+
const problems = [];
|
|
7196
|
+
const questions = raw.map((q, i) => {
|
|
7197
|
+
const at = `question ${i + 1}`;
|
|
7198
|
+
const r = isRecord3(q) ? q : {};
|
|
7199
|
+
const question = str2(r.question);
|
|
7200
|
+
const header = str2(r.header);
|
|
7201
|
+
const opts = Array.isArray(r.options) ? r.options : [];
|
|
7202
|
+
if (!question) problems.push(`${at}: missing "question"`);
|
|
7203
|
+
if (header.length > 12) problems.push(`${at}: "header" is over 12 characters`);
|
|
7204
|
+
if (opts.length < 2 || opts.length > 4) problems.push(`${at}: give 2-4 options`);
|
|
7205
|
+
const options = opts.map((o, j) => {
|
|
7206
|
+
const label = str2(isRecord3(o) ? o.label : void 0);
|
|
7207
|
+
const description = str2(isRecord3(o) ? o.description : void 0);
|
|
7208
|
+
if (!label) problems.push(`${at}, option ${j + 1}: missing "label"`);
|
|
7209
|
+
return description ? { label, description } : { label };
|
|
7210
|
+
});
|
|
7211
|
+
if (new Set(options.map((o) => o.label)).size < options.length)
|
|
7212
|
+
problems.push(`${at}: option labels repeat`);
|
|
7213
|
+
return {
|
|
7214
|
+
question,
|
|
7215
|
+
...header && { header },
|
|
7216
|
+
options,
|
|
7217
|
+
...r.multiSelect === true && { multiSelect: true }
|
|
7218
|
+
};
|
|
7219
|
+
});
|
|
7220
|
+
if (problems.length)
|
|
7221
|
+
throw new Error(["nothing asked; fix these and call ask_user again:", ...problems].join("\n"));
|
|
7222
|
+
return questions;
|
|
7223
|
+
}
|
|
6808
7224
|
function isToolAllowed(name, ctx) {
|
|
6809
7225
|
const tool = TOOLS[name];
|
|
6810
7226
|
if (!tool) return false;
|
|
@@ -6886,7 +7302,7 @@ function shapeViewTool(deps, args, ctx) {
|
|
|
6886
7302
|
label,
|
|
6887
7303
|
...typeof args.mode === "string" && { mode: args.mode },
|
|
6888
7304
|
...roots && { roots },
|
|
6889
|
-
view:
|
|
7305
|
+
view: isRecord3(args.view) ? args.view : {}
|
|
6890
7306
|
};
|
|
6891
7307
|
const problem = lensProblem(candidate);
|
|
6892
7308
|
if (problem) throw new Error(`invalid lens: ${problem}`);
|
|
@@ -6911,25 +7327,25 @@ function shapeViewTool(deps, args, ctx) {
|
|
|
6911
7327
|
return `proposed lens "${name}"${savedNote}${skipNote}; the human will approve or deny it`;
|
|
6912
7328
|
}
|
|
6913
7329
|
function saveLensToFile(repo, lens) {
|
|
6914
|
-
const file =
|
|
7330
|
+
const file = path20.join(repo, ".topo", "lenses.json");
|
|
6915
7331
|
let existing = [];
|
|
6916
7332
|
try {
|
|
6917
|
-
const raw = JSON.parse(
|
|
7333
|
+
const raw = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
6918
7334
|
const list = Array.isArray(raw) ? raw : raw?.lenses;
|
|
6919
7335
|
if (Array.isArray(list)) existing = list;
|
|
6920
7336
|
} catch {
|
|
6921
7337
|
}
|
|
6922
7338
|
const byName = new Map(existing.map((l) => [l.name, l]));
|
|
6923
7339
|
byName.set(lens.name, lens);
|
|
6924
|
-
|
|
7340
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
6925
7341
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6926
|
-
|
|
7342
|
+
fs17.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
|
|
6927
7343
|
`);
|
|
6928
|
-
|
|
7344
|
+
fs17.renameSync(tmp, file);
|
|
6929
7345
|
}
|
|
6930
7346
|
async function callTool(deps, params, ctx) {
|
|
6931
7347
|
const name = params.name;
|
|
6932
|
-
const args =
|
|
7348
|
+
const args = isRecord3(params.arguments) ? params.arguments : {};
|
|
6933
7349
|
if (typeof name !== "string" || !TOOLS[name]) {
|
|
6934
7350
|
return { content: [{ type: "text", text: `unknown tool "${String(name)}"` }], isError: true };
|
|
6935
7351
|
}
|
|
@@ -6954,13 +7370,13 @@ function createMcpHandler(deps) {
|
|
|
6954
7370
|
} catch {
|
|
6955
7371
|
return { status: 200, body: rpcError(null, -32700, "Parse error") };
|
|
6956
7372
|
}
|
|
6957
|
-
if (!
|
|
6958
|
-
const id2 =
|
|
7373
|
+
if (!isRecord3(msg) || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
|
|
7374
|
+
const id2 = isRecord3(msg) && typeof msg.id !== "undefined" ? msg.id : null;
|
|
6959
7375
|
return { status: 200, body: rpcError(id2, -32600, "Invalid Request") };
|
|
6960
7376
|
}
|
|
6961
7377
|
if (!("id" in msg)) return { status: 202 };
|
|
6962
7378
|
const id = msg.id;
|
|
6963
|
-
const params =
|
|
7379
|
+
const params = isRecord3(msg.params) ? msg.params : {};
|
|
6964
7380
|
switch (msg.method) {
|
|
6965
7381
|
case "ping":
|
|
6966
7382
|
return { status: 200, body: rpcResult(id, {}) };
|
|
@@ -6997,21 +7413,23 @@ function createMcpHandler(deps) {
|
|
|
6997
7413
|
}
|
|
6998
7414
|
};
|
|
6999
7415
|
}
|
|
7000
|
-
var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS,
|
|
7416
|
+
var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord3, isStrArr, isDirection, isStatus, str2, MAX_LAYER_FILES, withCommand, MAX_STEPS, TOOLS, rpcResult, rpcError;
|
|
7001
7417
|
var init_mcp = __esm({
|
|
7002
7418
|
async "packages/cli/src/mcp.ts"() {
|
|
7003
7419
|
"use strict";
|
|
7004
7420
|
await init_src();
|
|
7421
|
+
init_permissions();
|
|
7005
7422
|
PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
7006
7423
|
DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
7007
7424
|
INSTRUCTIONS = "This is the live code map the human reviewing this repo is looking at right now. Prefer these tools over grep when you need to know who calls something, what would break if you change it, or where something lives \u2014 they're backed by a resolved call graph, not text search.";
|
|
7008
|
-
|
|
7425
|
+
isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7009
7426
|
isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
7010
7427
|
isDirection = (v) => v === "out" || v === "in" || v === "both";
|
|
7011
7428
|
isStatus = (v) => v === "proposed" || v === "active" || v === "review" || v === "done";
|
|
7429
|
+
str2 = (v) => typeof v === "string" ? v.trim() : "";
|
|
7012
7430
|
MAX_LAYER_FILES = 300;
|
|
7013
7431
|
withCommand = (ctx) => ctx.command ? { command: ctx.command } : {};
|
|
7014
|
-
MAX_STEPS =
|
|
7432
|
+
MAX_STEPS = 40;
|
|
7015
7433
|
TOOLS = {
|
|
7016
7434
|
find: {
|
|
7017
7435
|
description: "Search blocks (files, classes, functions, methods) by name or text. Use this instead of grep to locate where something lives before reading or changing it.",
|
|
@@ -7190,21 +7608,21 @@ var init_mcp = __esm({
|
|
|
7190
7608
|
}
|
|
7191
7609
|
},
|
|
7192
7610
|
review_feedback: {
|
|
7193
|
-
description: "The human's review of changed blocks: which need changes (with their note) and how many they
|
|
7611
|
+
description: "The human's review of changed blocks: which need changes (with their note) and how many they've looked at. Use when you finish, or when told your change was reviewed. An agent Topo launched sees only its own territory and the blocks it changed.",
|
|
7194
7612
|
inputSchema: { type: "object", properties: {} },
|
|
7195
7613
|
run: (deps, _args, ctx) => {
|
|
7196
7614
|
const index = deps.getIndex();
|
|
7197
7615
|
const run8 = ctx.runId ? deps.agents?.get(ctx.runId) : void 0;
|
|
7198
7616
|
const territory = run8 && deps.getTerritories().find((t) => t.id === run8.territoryId);
|
|
7199
7617
|
const mine = (id) => !territory || deps.tracking.blockAgent.get(id) === run8?.agent || outsideTerritory(index, territory.nodeIds, [id]).length === 0;
|
|
7200
|
-
const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "
|
|
7618
|
+
const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "viewed" || b.review === "changes_requested").filter((b) => mine(b.nodeId));
|
|
7201
7619
|
if (!reviewed.length) return "no review feedback yet";
|
|
7202
7620
|
const asks = reviewed.filter((b) => b.review === "changes_requested");
|
|
7203
7621
|
return [
|
|
7204
7622
|
...asks.map(
|
|
7205
7623
|
(b) => `${b.nodeId} CHANGES REQUESTED${b.reviewNote ? `: ${b.reviewNote}` : ""}`
|
|
7206
7624
|
),
|
|
7207
|
-
`${reviewed.length - asks.length} block(s)
|
|
7625
|
+
`${reviewed.length - asks.length} block(s) viewed, no changes asked`
|
|
7208
7626
|
].join("\n");
|
|
7209
7627
|
}
|
|
7210
7628
|
},
|
|
@@ -7223,16 +7641,23 @@ var init_mcp = __esm({
|
|
|
7223
7641
|
const input = args.input;
|
|
7224
7642
|
const run8 = deps.agents?.get(ctx.runId);
|
|
7225
7643
|
const label = run8?.label ?? "An agent";
|
|
7226
|
-
const detail = toolName === "Bash" &&
|
|
7644
|
+
const detail = toolName === "Bash" && isRecord3(input) && typeof input.command === "string" ? input.command : JSON.stringify(input);
|
|
7645
|
+
const allow = JSON.stringify({ behavior: "allow", updatedInput: input });
|
|
7646
|
+
if ((deps.autoAllowRules?.() ?? []).some((r) => ruleMatches(r, toolName, input)))
|
|
7647
|
+
return allow;
|
|
7648
|
+
const rule = ruleFor(toolName, input);
|
|
7227
7649
|
const item = deps.inbox.add({
|
|
7228
7650
|
kind: "permission",
|
|
7229
|
-
|
|
7651
|
+
// The command goes in detail, so the card can show it on one line as code.
|
|
7652
|
+
title: `${label} wants to run ${toolName}`,
|
|
7653
|
+
detail,
|
|
7230
7654
|
agent: run8?.agent,
|
|
7231
7655
|
label: run8?.label,
|
|
7232
|
-
runId: ctx.runId
|
|
7656
|
+
runId: ctx.runId,
|
|
7657
|
+
...rule && { rule, actions: ["approve", "always", "deny"] }
|
|
7233
7658
|
});
|
|
7234
7659
|
const status = await deps.inbox.wait(item.id, 10 * 60 * 1e3);
|
|
7235
|
-
return status === "approved" ?
|
|
7660
|
+
return status === "approved" ? allow : JSON.stringify({ behavior: "deny", message: "The human denied this in Topo." });
|
|
7236
7661
|
}
|
|
7237
7662
|
},
|
|
7238
7663
|
list_territories: {
|
|
@@ -7300,7 +7725,7 @@ var init_mcp = __esm({
|
|
|
7300
7725
|
label,
|
|
7301
7726
|
...typeof args.mode === "string" && { mode: args.mode },
|
|
7302
7727
|
...roots.length && { roots },
|
|
7303
|
-
view:
|
|
7728
|
+
view: isRecord3(args.view) ? args.view : {}
|
|
7304
7729
|
};
|
|
7305
7730
|
const problem = lensProblem(candidate);
|
|
7306
7731
|
if (problem) throw new Error(`invalid lens: ${problem}`);
|
|
@@ -7333,7 +7758,7 @@ var init_mcp = __esm({
|
|
|
7333
7758
|
},
|
|
7334
7759
|
explain: {
|
|
7335
7760
|
access: "director",
|
|
7336
|
-
description:
|
|
7761
|
+
description: `Walk the human through how something works, or through a change: the map tours these blocks in step order, numbered, with a card per step. Use for 'how does X work / where is X / what happens when / walk me through this or the changes' requests, after tracing with find/callees/callers/get_block. Give the steps in execution (or reading) order, sized to the question (about 5 for the gist, every step that matters for all of it, at most ${MAX_STEPS}), only blocks that matter; each step's text says what happens there in 2-4 sentences (for a changed block, also what changed and why it matters).`,
|
|
7337
7762
|
inputSchema: {
|
|
7338
7763
|
type: "object",
|
|
7339
7764
|
properties: {
|
|
@@ -7359,8 +7784,8 @@ var init_mcp = __esm({
|
|
|
7359
7784
|
const steps = [];
|
|
7360
7785
|
const problems = [];
|
|
7361
7786
|
raw.forEach((s, i) => {
|
|
7362
|
-
const ref =
|
|
7363
|
-
const text =
|
|
7787
|
+
const ref = isRecord3(s) && typeof s.block === "string" ? s.block : "";
|
|
7788
|
+
const text = isRecord3(s) && typeof s.text === "string" ? s.text.trim() : "";
|
|
7364
7789
|
const r = resolveOrText(index, ref);
|
|
7365
7790
|
if ("node" in r) steps.push({ nodeId: r.node.id, text });
|
|
7366
7791
|
else problems.push(`step ${i + 1} (${ref || "no block"}): ${r.text}`);
|
|
@@ -7374,6 +7799,45 @@ var init_mcp = __esm({
|
|
|
7374
7799
|
return `showing "${title}": ${steps.length} step(s) on the map`;
|
|
7375
7800
|
}
|
|
7376
7801
|
},
|
|
7802
|
+
ask_user: {
|
|
7803
|
+
access: "director",
|
|
7804
|
+
description: "Ask the human 1-4 multiple-choice questions, shown as buttons under your reply (they can also type their own answer). Use it when the request is ambiguous in a way that changes what you'd do: how many agents, which territories to make, how long a walkthrough, which of several areas. Each option is a concrete choice you can act on. After calling it, end your turn with at most a line of reply; the answer arrives as the human's next message.",
|
|
7805
|
+
inputSchema: {
|
|
7806
|
+
type: "object",
|
|
7807
|
+
properties: {
|
|
7808
|
+
questions: {
|
|
7809
|
+
type: "array",
|
|
7810
|
+
minItems: 1,
|
|
7811
|
+
maxItems: 4,
|
|
7812
|
+
items: {
|
|
7813
|
+
type: "object",
|
|
7814
|
+
properties: {
|
|
7815
|
+
question: { type: "string" },
|
|
7816
|
+
header: { type: "string", description: "a label of 12 characters at most" },
|
|
7817
|
+
options: {
|
|
7818
|
+
type: "array",
|
|
7819
|
+
minItems: 2,
|
|
7820
|
+
maxItems: 4,
|
|
7821
|
+
items: {
|
|
7822
|
+
type: "object",
|
|
7823
|
+
properties: { label: { type: "string" }, description: { type: "string" } },
|
|
7824
|
+
required: ["label"]
|
|
7825
|
+
}
|
|
7826
|
+
},
|
|
7827
|
+
multiSelect: { type: "boolean" }
|
|
7828
|
+
},
|
|
7829
|
+
required: ["question", "options"]
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
},
|
|
7833
|
+
required: ["questions"]
|
|
7834
|
+
},
|
|
7835
|
+
run: (deps, args, ctx) => {
|
|
7836
|
+
const questions = parseQuestions(args.questions);
|
|
7837
|
+
deps.bus.emit({ type: "ask", questions, ...withCommand(ctx) });
|
|
7838
|
+
return `asked ${questions.length} question(s); they show under your reply. End your turn now: the human's choice comes back as their next message.`;
|
|
7839
|
+
}
|
|
7840
|
+
},
|
|
7377
7841
|
get_layers: {
|
|
7378
7842
|
description: "The repo's layers (.topo/layers.json: named groups of files by path glob, which color the map) and which files are in none. With no layers yet, a folder-based draft. Lists every source file with its main blocks, so you can group them by what the code does.",
|
|
7379
7843
|
inputSchema: { type: "object", properties: {} },
|
|
@@ -7545,16 +8009,17 @@ ${loose.length} file(s) in no layer: ${loose.slice(0, 20).join(", ")}${loose.len
|
|
|
7545
8009
|
},
|
|
7546
8010
|
launch_agent: {
|
|
7547
8011
|
access: "director",
|
|
7548
|
-
description: "Launch a worker agent on a territory. Use when the human asks to start work.",
|
|
8012
|
+
description: "Launch a worker agent on a territory. Use when the human asks to start work or asks for a code change (you can't edit code; the worker can). `agent` picks the CLI (claude, codex, gemini) when the human names one; otherwise the territory's own or the default.",
|
|
7549
8013
|
inputSchema: {
|
|
7550
8014
|
type: "object",
|
|
7551
|
-
properties: { territory: { type: "string" } },
|
|
8015
|
+
properties: { territory: { type: "string" }, agent: { type: "string" } },
|
|
7552
8016
|
required: ["territory"]
|
|
7553
8017
|
},
|
|
7554
8018
|
run: (deps, args) => {
|
|
7555
8019
|
if (!deps.agents?.available) throw new Error("agent launching isn't available right now");
|
|
7556
8020
|
const territory = requireString(args, "territory");
|
|
7557
|
-
const
|
|
8021
|
+
const agent = typeof args.agent === "string" && args.agent ? args.agent : void 0;
|
|
8022
|
+
const run8 = deps.agents.launch(territory, void 0, agent);
|
|
7558
8023
|
return `launched ${run8.label} (${run8.id}) on territory "${territory}"`;
|
|
7559
8024
|
}
|
|
7560
8025
|
},
|
|
@@ -7629,11 +8094,11 @@ ${loose.length} file(s) in no layer: ${loose.slice(0, 20).join(", ")}${loose.len
|
|
|
7629
8094
|
});
|
|
7630
8095
|
|
|
7631
8096
|
// packages/cli/src/territories.ts
|
|
7632
|
-
import
|
|
7633
|
-
import
|
|
8097
|
+
import fs18 from "node:fs";
|
|
8098
|
+
import path21 from "node:path";
|
|
7634
8099
|
function readTerritories(repo) {
|
|
7635
8100
|
try {
|
|
7636
|
-
const doc = parseTerritoryDoc(JSON.parse(
|
|
8101
|
+
const doc = parseTerritoryDoc(JSON.parse(fs18.readFileSync(territoriesFile(repo), "utf8")));
|
|
7637
8102
|
if (doc) return doc;
|
|
7638
8103
|
} catch {
|
|
7639
8104
|
}
|
|
@@ -7641,11 +8106,11 @@ function readTerritories(repo) {
|
|
|
7641
8106
|
}
|
|
7642
8107
|
function writeTerritories(repo, doc) {
|
|
7643
8108
|
const file = territoriesFile(repo);
|
|
7644
|
-
|
|
8109
|
+
fs18.mkdirSync(path21.dirname(file), { recursive: true });
|
|
7645
8110
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7646
|
-
|
|
8111
|
+
fs18.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
7647
8112
|
`);
|
|
7648
|
-
|
|
8113
|
+
fs18.renameSync(tmp, file);
|
|
7649
8114
|
}
|
|
7650
8115
|
function parseTerritoryDoc(v) {
|
|
7651
8116
|
const o = v;
|
|
@@ -7661,6 +8126,7 @@ function parseTerritoryDoc(v) {
|
|
|
7661
8126
|
if (typeof r.label !== "string" || !isStrArr2(r.nodeIds)) return void 0;
|
|
7662
8127
|
if (r.agent !== void 0 && typeof r.agent !== "string") return void 0;
|
|
7663
8128
|
if (r.note !== void 0 && typeof r.note !== "string") return void 0;
|
|
8129
|
+
if (r.runner !== void 0 && typeof r.runner !== "string") return void 0;
|
|
7664
8130
|
if (r.status !== void 0 && !STATUSES.includes(r.status)) return void 0;
|
|
7665
8131
|
seen.add(r.id);
|
|
7666
8132
|
territories.push({
|
|
@@ -7669,6 +8135,7 @@ function parseTerritoryDoc(v) {
|
|
|
7669
8135
|
nodeIds: r.nodeIds,
|
|
7670
8136
|
...r.agent !== void 0 ? { agent: r.agent } : {},
|
|
7671
8137
|
...r.note !== void 0 ? { note: r.note } : {},
|
|
8138
|
+
...r.runner !== void 0 ? { runner: r.runner } : {},
|
|
7672
8139
|
...r.status !== void 0 ? { status: r.status } : {}
|
|
7673
8140
|
});
|
|
7674
8141
|
}
|
|
@@ -7679,7 +8146,7 @@ var init_territories = __esm({
|
|
|
7679
8146
|
"packages/cli/src/territories.ts"() {
|
|
7680
8147
|
"use strict";
|
|
7681
8148
|
SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7682
|
-
territoriesFile = (repo) =>
|
|
8149
|
+
territoriesFile = (repo) => path21.join(repo, ".topo", "territories.json");
|
|
7683
8150
|
isStrArr2 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
7684
8151
|
STATUSES = ["proposed", "active", "review", "done"];
|
|
7685
8152
|
}
|
|
@@ -7687,9 +8154,9 @@ var init_territories = __esm({
|
|
|
7687
8154
|
|
|
7688
8155
|
// packages/cli/src/server.ts
|
|
7689
8156
|
import crypto from "node:crypto";
|
|
7690
|
-
import
|
|
8157
|
+
import fs19 from "node:fs";
|
|
7691
8158
|
import http from "node:http";
|
|
7692
|
-
import
|
|
8159
|
+
import path22 from "node:path";
|
|
7693
8160
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7694
8161
|
function send(res, status, body, type) {
|
|
7695
8162
|
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
@@ -7728,27 +8195,27 @@ function parseView(v) {
|
|
|
7728
8195
|
}
|
|
7729
8196
|
function parseLayout(v) {
|
|
7730
8197
|
const o = v;
|
|
7731
|
-
if (!
|
|
8198
|
+
if (!isRecord4(o) || o.version !== 1 || !isRecord4(o.views) || !Object.values(o.views).every((pins) => isRecord4(pins) && Object.values(pins).every(isPos))) {
|
|
7732
8199
|
throw new HttpError(400, "invalid layout");
|
|
7733
8200
|
}
|
|
7734
8201
|
return { version: 1, views: o.views };
|
|
7735
8202
|
}
|
|
7736
8203
|
function serveStatic(res, staticDir, urlPath2) {
|
|
7737
|
-
const root =
|
|
7738
|
-
const index =
|
|
8204
|
+
const root = path22.resolve(staticDir);
|
|
8205
|
+
const index = path22.join(root, "index.html");
|
|
7739
8206
|
let decoded;
|
|
7740
8207
|
try {
|
|
7741
8208
|
decoded = decodeURIComponent(urlPath2);
|
|
7742
8209
|
} catch {
|
|
7743
8210
|
throw new HttpError(400, "bad path");
|
|
7744
8211
|
}
|
|
7745
|
-
let file =
|
|
7746
|
-
if (file !== root && !file.startsWith(root +
|
|
7747
|
-
if (!
|
|
7748
|
-
if (!
|
|
7749
|
-
const type = TYPES[
|
|
8212
|
+
let file = path22.resolve(root, `.${decoded}`);
|
|
8213
|
+
if (file !== root && !file.startsWith(root + path22.sep)) throw new HttpError(403, "forbidden");
|
|
8214
|
+
if (!fs19.statSync(file, { throwIfNoEntry: false })?.isFile()) file = index;
|
|
8215
|
+
if (!fs19.existsSync(file)) return send(res, 200, MISSING_DIST, TYPES[".html"]);
|
|
8216
|
+
const type = TYPES[path22.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
7750
8217
|
res.writeHead(200, { "content-type": type });
|
|
7751
|
-
|
|
8218
|
+
fs19.createReadStream(file).pipe(res);
|
|
7752
8219
|
}
|
|
7753
8220
|
function isLocalhostOrigin(origin) {
|
|
7754
8221
|
try {
|
|
@@ -7764,7 +8231,7 @@ function constantTimeEqual(a, b) {
|
|
|
7764
8231
|
}
|
|
7765
8232
|
function parseCommandContext(v) {
|
|
7766
8233
|
if (v === void 0) return void 0;
|
|
7767
|
-
if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.selected !== void 0 && !isStrArr3(v.selected)) {
|
|
8234
|
+
if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.depth !== void 0 && !Number.isInteger(v.depth) || v.selected !== void 0 && !isStrArr3(v.selected)) {
|
|
7768
8235
|
throw new HttpError(400, "invalid command context");
|
|
7769
8236
|
}
|
|
7770
8237
|
return v;
|
|
@@ -7844,15 +8311,32 @@ function createServer(opts) {
|
|
|
7844
8311
|
opts.liveEvents?.emit({ type: "territories", territories: doc.territories });
|
|
7845
8312
|
return json(res, 200, doc);
|
|
7846
8313
|
}
|
|
8314
|
+
if (route === "GET /api/settings" && opts.getSettings)
|
|
8315
|
+
return json(res, 200, opts.getSettings());
|
|
8316
|
+
if (route === "PUT /api/settings" && opts.saveSettings) {
|
|
8317
|
+
const body = await readJson(req);
|
|
8318
|
+
if (!PERMISSION_MODES.includes(body?.permissions)) {
|
|
8319
|
+
throw new HttpError(400, `expected {permissions: ${PERMISSION_MODES.join("|")}}`);
|
|
8320
|
+
}
|
|
8321
|
+
return json(res, 200, opts.saveSettings(body));
|
|
8322
|
+
}
|
|
7847
8323
|
if (route === "GET /api/inbox" && opts.inbox) return json(res, 200, opts.inbox.list());
|
|
7848
8324
|
const inboxDecide = req.method === "POST" && /^\/api\/inbox\/[^/]+$/.test(url.pathname);
|
|
7849
8325
|
if (inboxDecide && opts.inbox) {
|
|
7850
8326
|
const id = decodeURIComponent(url.pathname.slice("/api/inbox/".length));
|
|
7851
8327
|
const body = await readJson(req);
|
|
7852
|
-
|
|
7853
|
-
|
|
8328
|
+
const decision = body?.decision;
|
|
8329
|
+
if (decision !== "approve" && decision !== "always" && decision !== "deny" && decision !== "dismiss") {
|
|
8330
|
+
throw new HttpError(400, "expected {decision: approve|always|deny|dismiss}");
|
|
8331
|
+
}
|
|
8332
|
+
if (decision === "always") {
|
|
8333
|
+
const open = opts.inbox.list().find((it) => it.id === id);
|
|
8334
|
+
if (!open?.rule || !open.actions.includes("always") || !opts.allowAlways) {
|
|
8335
|
+
throw new HttpError(400, "this item has no rule to always allow");
|
|
8336
|
+
}
|
|
8337
|
+
opts.allowAlways(open);
|
|
7854
8338
|
}
|
|
7855
|
-
const item = opts.inbox.decide(id,
|
|
8339
|
+
const item = opts.inbox.decide(id, decision === "always" ? "approve" : decision);
|
|
7856
8340
|
if (!item) throw new HttpError(404, "no such inbox item");
|
|
7857
8341
|
return json(res, 200, item);
|
|
7858
8342
|
}
|
|
@@ -7862,10 +8346,11 @@ function createServer(opts) {
|
|
|
7862
8346
|
try {
|
|
7863
8347
|
const body = await readJson(req).catch(() => null);
|
|
7864
8348
|
const reply = typeof body?.reply === "string" && body.reply.trim() ? body.reply : void 0;
|
|
8349
|
+
const agent = typeof body?.agent === "string" && body.agent ? body.agent : void 0;
|
|
7865
8350
|
return json(
|
|
7866
8351
|
res,
|
|
7867
8352
|
200,
|
|
7868
|
-
opts.agents.launch(decodeURIComponent(launchMatch[1]), reply)
|
|
8353
|
+
opts.agents.launch(decodeURIComponent(launchMatch[1]), reply, agent)
|
|
7869
8354
|
);
|
|
7870
8355
|
} catch (e) {
|
|
7871
8356
|
throw new HttpError(400, e.message);
|
|
@@ -7922,11 +8407,11 @@ function createServer(opts) {
|
|
|
7922
8407
|
}
|
|
7923
8408
|
if (route === "POST /api/blocks/review" && opts.reviewBlock) {
|
|
7924
8409
|
const b = await readJson(req);
|
|
7925
|
-
const decision = b?.decision;
|
|
7926
|
-
if (typeof b?.nodeId !== "string" || decision !== "
|
|
8410
|
+
const decision = b?.decision === "approved" ? "viewed" : b?.decision;
|
|
8411
|
+
if (typeof b?.nodeId !== "string" || decision !== "viewed" && decision !== "changes_requested" && decision !== "pending" || b.note !== void 0 && (typeof b.note !== "string" || b.note.length > 4e3)) {
|
|
7927
8412
|
throw new HttpError(
|
|
7928
8413
|
400,
|
|
7929
|
-
"expected {nodeId, decision:
|
|
8414
|
+
"expected {nodeId, decision: viewed|changes_requested|pending, note?}"
|
|
7930
8415
|
);
|
|
7931
8416
|
}
|
|
7932
8417
|
if (!opts.reviewBlock(b.nodeId, decision, b.note)) {
|
|
@@ -7934,6 +8419,15 @@ function createServer(opts) {
|
|
|
7934
8419
|
}
|
|
7935
8420
|
return json(res, 200, { ok: true });
|
|
7936
8421
|
}
|
|
8422
|
+
if (route === "POST /api/blocks/ask" && opts.askChanges) {
|
|
8423
|
+
const b = await readJson(req);
|
|
8424
|
+
if (typeof b?.nodeId !== "string" || typeof b.note !== "string" || !b.note.trim() || b.note.length > 4e3) {
|
|
8425
|
+
throw new HttpError(400, "expected {nodeId, note} with a non-empty note");
|
|
8426
|
+
}
|
|
8427
|
+
const result = opts.askChanges(b.nodeId, b.note.trim());
|
|
8428
|
+
if (!result) throw new HttpError(404, "no such changed block");
|
|
8429
|
+
return json(res, 200, result);
|
|
8430
|
+
}
|
|
7937
8431
|
if (route === "POST /api/activity" && opts.postActivity) {
|
|
7938
8432
|
const body = await readJson(req);
|
|
7939
8433
|
const accepted = opts.postActivity(body);
|
|
@@ -7957,17 +8451,18 @@ function createServer(opts) {
|
|
|
7957
8451
|
}
|
|
7958
8452
|
});
|
|
7959
8453
|
}
|
|
7960
|
-
var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos,
|
|
8454
|
+
var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos, isRecord4, seenShapes, isRecordLike;
|
|
7961
8455
|
var init_server = __esm({
|
|
7962
8456
|
async "packages/cli/src/server.ts"() {
|
|
7963
8457
|
"use strict";
|
|
7964
8458
|
await init_src();
|
|
7965
8459
|
init_editor();
|
|
8460
|
+
init_permissions();
|
|
7966
8461
|
init_territories();
|
|
7967
8462
|
KEEPALIVE_MS = 15e3;
|
|
7968
8463
|
REPO_WEB_DIST = fileURLToPath3(new URL("../../../apps/web/dist/", import.meta.url));
|
|
7969
8464
|
DEFAULT_STATIC_DIR = [fileURLToPath3(new URL("./web/", import.meta.url)), REPO_WEB_DIST].find(
|
|
7970
|
-
(d) =>
|
|
8465
|
+
(d) => fs19.existsSync(path22.join(d, "index.html"))
|
|
7971
8466
|
) ?? REPO_WEB_DIST;
|
|
7972
8467
|
MAX_BODY = 1024 * 1024;
|
|
7973
8468
|
TYPES = {
|
|
@@ -7998,15 +8493,15 @@ var init_server = __esm({
|
|
|
7998
8493
|
};
|
|
7999
8494
|
isStrArr3 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
8000
8495
|
isPos = (p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
8001
|
-
|
|
8496
|
+
isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8002
8497
|
seenShapes = /* @__PURE__ */ new Set();
|
|
8003
8498
|
isRecordLike = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8004
8499
|
}
|
|
8005
8500
|
});
|
|
8006
8501
|
|
|
8007
8502
|
// packages/cli/src/watch.ts
|
|
8008
|
-
import
|
|
8009
|
-
import
|
|
8503
|
+
import fs20 from "node:fs";
|
|
8504
|
+
import path23 from "node:path";
|
|
8010
8505
|
function isIndexable(relPath) {
|
|
8011
8506
|
const parts = relPath.split(/[\\/]/).filter(Boolean);
|
|
8012
8507
|
const base = parts.at(-1);
|
|
@@ -8048,14 +8543,14 @@ function watchRepo(root, onBatch, opts = {}) {
|
|
|
8048
8543
|
if (watchers.has(dir)) return;
|
|
8049
8544
|
let w;
|
|
8050
8545
|
try {
|
|
8051
|
-
w =
|
|
8546
|
+
w = fs20.watch(dir, (_event, filename) => {
|
|
8052
8547
|
if (!filename) return;
|
|
8053
|
-
const abs =
|
|
8054
|
-
if (!watchers.has(abs) &&
|
|
8055
|
-
if (!isIgnoredDir(
|
|
8548
|
+
const abs = path23.join(dir, filename.toString());
|
|
8549
|
+
if (!watchers.has(abs) && fs20.statSync(abs, { throwIfNoEntry: false })?.isDirectory()) {
|
|
8550
|
+
if (!isIgnoredDir(path23.basename(abs))) walkDirs(abs);
|
|
8056
8551
|
return;
|
|
8057
8552
|
}
|
|
8058
|
-
const rel =
|
|
8553
|
+
const rel = path23.relative(root, abs).split(path23.sep).join("/");
|
|
8059
8554
|
if (!isIndexable(rel)) return;
|
|
8060
8555
|
pending.add(rel);
|
|
8061
8556
|
scheduleFlush();
|
|
@@ -8073,11 +8568,11 @@ function watchRepo(root, onBatch, opts = {}) {
|
|
|
8073
8568
|
watchDir(dir);
|
|
8074
8569
|
let entries = [];
|
|
8075
8570
|
try {
|
|
8076
|
-
entries =
|
|
8571
|
+
entries = fs20.readdirSync(dir, { withFileTypes: true });
|
|
8077
8572
|
} catch {
|
|
8078
8573
|
}
|
|
8079
8574
|
for (const e of entries)
|
|
8080
|
-
if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(
|
|
8575
|
+
if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path23.join(dir, e.name));
|
|
8081
8576
|
};
|
|
8082
8577
|
walkDirs(root);
|
|
8083
8578
|
return {
|
|
@@ -8102,8 +8597,8 @@ var init_watch = __esm({
|
|
|
8102
8597
|
|
|
8103
8598
|
// packages/cli/src/worktrees.ts
|
|
8104
8599
|
import { execFileSync } from "node:child_process";
|
|
8105
|
-
import
|
|
8106
|
-
import
|
|
8600
|
+
import fs21 from "node:fs";
|
|
8601
|
+
import path24 from "node:path";
|
|
8107
8602
|
function listWorktrees(repo) {
|
|
8108
8603
|
let out;
|
|
8109
8604
|
try {
|
|
@@ -8117,17 +8612,17 @@ function listWorktrees(repo) {
|
|
|
8117
8612
|
}
|
|
8118
8613
|
const roots = out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice("worktree ".length));
|
|
8119
8614
|
const own = roots.find((r) => isInside2(r, repo));
|
|
8120
|
-
return roots.filter((r) => r !== own).map((root) => ({ root, name:
|
|
8615
|
+
return roots.filter((r) => r !== own).map((root) => ({ root, name: path24.basename(root) }));
|
|
8121
8616
|
}
|
|
8122
8617
|
function mapWorktreePath(repo, worktrees, file) {
|
|
8123
8618
|
const wt = worktrees.filter((w) => isInside2(w.root, file)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
8124
8619
|
if (!wt) return void 0;
|
|
8125
8620
|
const mainRoot = gitRoot(repo);
|
|
8126
|
-
const sub = mainRoot ?
|
|
8127
|
-
const inTree =
|
|
8128
|
-
const rel = sub ?
|
|
8129
|
-
if (rel.startsWith("..") ||
|
|
8130
|
-
return { rel: rel.split(
|
|
8621
|
+
const sub = mainRoot ? path24.relative(mainRoot, repo) : "";
|
|
8622
|
+
const inTree = path24.relative(wt.root, file);
|
|
8623
|
+
const rel = sub ? path24.relative(sub, inTree) : inTree;
|
|
8624
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return void 0;
|
|
8625
|
+
return { rel: rel.split(path24.sep).join("/"), worktree: wt };
|
|
8131
8626
|
}
|
|
8132
8627
|
function gitRoot(dir) {
|
|
8133
8628
|
try {
|
|
@@ -8141,7 +8636,7 @@ function gitRoot(dir) {
|
|
|
8141
8636
|
}
|
|
8142
8637
|
}
|
|
8143
8638
|
function changedLines(base, changed) {
|
|
8144
|
-
if (!
|
|
8639
|
+
if (!fs21.existsSync(base) || !fs21.existsSync(changed)) return [];
|
|
8145
8640
|
let text = "";
|
|
8146
8641
|
try {
|
|
8147
8642
|
execFileSync("git", ["diff", "--no-index", "-U0", "--no-color", base, changed], {
|
|
@@ -8174,8 +8669,8 @@ var init_worktrees = __esm({
|
|
|
8174
8669
|
"packages/cli/src/worktrees.ts"() {
|
|
8175
8670
|
"use strict";
|
|
8176
8671
|
isInside2 = (dir, p) => {
|
|
8177
|
-
const rel =
|
|
8178
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8672
|
+
const rel = path24.relative(dir, p);
|
|
8673
|
+
return rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
8179
8674
|
};
|
|
8180
8675
|
}
|
|
8181
8676
|
});
|
|
@@ -8191,8 +8686,8 @@ __export(review_exports, {
|
|
|
8191
8686
|
});
|
|
8192
8687
|
import { spawn as spawn2 } from "node:child_process";
|
|
8193
8688
|
import crypto2 from "node:crypto";
|
|
8194
|
-
import
|
|
8195
|
-
import
|
|
8689
|
+
import fs22 from "node:fs";
|
|
8690
|
+
import path25 from "node:path";
|
|
8196
8691
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
8197
8692
|
function openBrowser(url) {
|
|
8198
8693
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
@@ -8231,7 +8726,7 @@ function run4(argv) {
|
|
|
8231
8726
|
if (!base) throw new Error(`missing <base> ref
|
|
8232
8727
|
|
|
8233
8728
|
${HELP4}`);
|
|
8234
|
-
const repo =
|
|
8729
|
+
const repo = path25.resolve(values.repo ?? process.cwd());
|
|
8235
8730
|
return serve({
|
|
8236
8731
|
compute: () => computeReview(repo, base, values.head),
|
|
8237
8732
|
repo,
|
|
@@ -8246,7 +8741,7 @@ ${HELP4}`);
|
|
|
8246
8741
|
}
|
|
8247
8742
|
function readLayout(repo) {
|
|
8248
8743
|
try {
|
|
8249
|
-
const doc = JSON.parse(
|
|
8744
|
+
const doc = JSON.parse(fs22.readFileSync(layoutFile(repo), "utf8"));
|
|
8250
8745
|
if (doc?.version === 1 && doc.views && typeof doc.views === "object") return doc;
|
|
8251
8746
|
} catch {
|
|
8252
8747
|
}
|
|
@@ -8254,12 +8749,12 @@ function readLayout(repo) {
|
|
|
8254
8749
|
}
|
|
8255
8750
|
function writeLayout(repo, doc) {
|
|
8256
8751
|
const file = layoutFile(repo);
|
|
8257
|
-
|
|
8752
|
+
fs22.mkdirSync(path25.dirname(file), { recursive: true });
|
|
8258
8753
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8259
8754
|
const text = JSON.stringify(doc, null, 2).replace(/\[\s+(-?\d+),\s+(-?\d+)\s+\]/g, "[$1, $2]");
|
|
8260
|
-
|
|
8755
|
+
fs22.writeFileSync(tmp, `${text}
|
|
8261
8756
|
`);
|
|
8262
|
-
|
|
8757
|
+
fs22.renameSync(tmp, file);
|
|
8263
8758
|
}
|
|
8264
8759
|
function serve(opts) {
|
|
8265
8760
|
const port = Number(opts.port);
|
|
@@ -8268,6 +8763,7 @@ function serve(opts) {
|
|
|
8268
8763
|
const { repo } = opts;
|
|
8269
8764
|
const editorCommand = editorTemplate(opts.editor);
|
|
8270
8765
|
const tracking = { blockAgent: /* @__PURE__ */ new Map(), rationale: /* @__PURE__ */ new Map(), reviews: /* @__PURE__ */ new Map() };
|
|
8766
|
+
let marksFor;
|
|
8271
8767
|
const compute = () => {
|
|
8272
8768
|
const review = opts.compute();
|
|
8273
8769
|
tagHubs(review.head);
|
|
@@ -8278,6 +8774,11 @@ function serve(opts) {
|
|
|
8278
8774
|
lenses: loadLenses(repo),
|
|
8279
8775
|
territories
|
|
8280
8776
|
};
|
|
8777
|
+
const key = reviewKey(payload.changeSet);
|
|
8778
|
+
if (key !== marksFor) {
|
|
8779
|
+
marksFor = key;
|
|
8780
|
+
tracking.reviews = readReviews(repo, key);
|
|
8781
|
+
}
|
|
8281
8782
|
payload.changeSet = annotateChangeSet(payload.changeSet, { territories, tracking, index });
|
|
8282
8783
|
return { review, payload, index };
|
|
8283
8784
|
};
|
|
@@ -8308,7 +8809,7 @@ function serve(opts) {
|
|
|
8308
8809
|
const nodeIds = blocksAt(
|
|
8309
8810
|
state.index,
|
|
8310
8811
|
parsed.file,
|
|
8311
|
-
changedLines(
|
|
8812
|
+
changedLines(path25.join(repo, parsed.file), parsed.absFile)
|
|
8312
8813
|
);
|
|
8313
8814
|
const run9 = agents.list().findLast((r) => r.agent === parsed.agent || r.agent === parsed.session);
|
|
8314
8815
|
const { absFile: _abs, worktree, ...activity } = parsed;
|
|
@@ -8340,6 +8841,7 @@ function serve(opts) {
|
|
|
8340
8841
|
return true;
|
|
8341
8842
|
};
|
|
8342
8843
|
const directorToken = crypto2.randomBytes(24).toString("hex");
|
|
8844
|
+
const permissionMode = () => opts.skipPermissions ? "never" : readSettings(repo).permissions;
|
|
8343
8845
|
let boundPort = port;
|
|
8344
8846
|
const agents = createAgentManager({
|
|
8345
8847
|
repo,
|
|
@@ -8350,7 +8852,9 @@ function serve(opts) {
|
|
|
8350
8852
|
saveTerritories: saveTerritoriesAndEmit,
|
|
8351
8853
|
directorToken,
|
|
8352
8854
|
getIndex: () => state.index,
|
|
8353
|
-
|
|
8855
|
+
permissionMode,
|
|
8856
|
+
// Agents without Claude's edit hook report their edits in their own stream.
|
|
8857
|
+
onEdit: (agent, file) => postActivity({ agent, file })
|
|
8354
8858
|
});
|
|
8355
8859
|
let live;
|
|
8356
8860
|
let watcher;
|
|
@@ -8389,16 +8893,45 @@ function serve(opts) {
|
|
|
8389
8893
|
saveTerritories: saveTerritoriesAndEmit,
|
|
8390
8894
|
inbox,
|
|
8391
8895
|
agents,
|
|
8392
|
-
tracking
|
|
8896
|
+
tracking,
|
|
8897
|
+
autoAllowRules: () => {
|
|
8898
|
+
const mode = permissionMode();
|
|
8899
|
+
return mode === "never" ? [] : [...mode === "safe" ? SAFE_RULES : [], ...readLocalAllow(repo)];
|
|
8900
|
+
}
|
|
8393
8901
|
});
|
|
8902
|
+
const markBlock = (nodeId2, decision, note) => {
|
|
8903
|
+
const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
|
|
8904
|
+
if (!block2) return false;
|
|
8905
|
+
if (decision === "pending") tracking.reviews.delete(nodeId2);
|
|
8906
|
+
else
|
|
8907
|
+
tracking.reviews.set(nodeId2, {
|
|
8908
|
+
decision,
|
|
8909
|
+
...note && { note },
|
|
8910
|
+
...decision === "viewed" && { fp: fingerprint(block2, state.index.byId.get(nodeId2)) }
|
|
8911
|
+
});
|
|
8912
|
+
block2.review = decision;
|
|
8913
|
+
delete block2.viewedStale;
|
|
8914
|
+
if (note && decision === "changes_requested") block2.reviewNote = note;
|
|
8915
|
+
else delete block2.reviewNote;
|
|
8916
|
+
try {
|
|
8917
|
+
writeReviews(repo, reviewKey(state.payload.changeSet), tracking.reviews);
|
|
8918
|
+
} catch (e) {
|
|
8919
|
+
console.error(
|
|
8920
|
+
`topo: couldn't save .topo/reviews.json: ${e instanceof Error ? e.message : e}`
|
|
8921
|
+
);
|
|
8922
|
+
}
|
|
8923
|
+
return true;
|
|
8924
|
+
};
|
|
8394
8925
|
const server = createServer({
|
|
8395
8926
|
getPayload: (refresh) => {
|
|
8396
8927
|
if (refresh) state = compute();
|
|
8397
8928
|
state.payload.inbox = inbox.list();
|
|
8398
8929
|
state.payload.runs = agents.list();
|
|
8399
|
-
state.payload.commandBar = agents.
|
|
8930
|
+
state.payload.commandBar = agents.director ? "agent" : "search";
|
|
8931
|
+
state.payload.agentClis = agents.agents();
|
|
8400
8932
|
state.payload.exchanges = agents.exchanges();
|
|
8401
8933
|
if (opts.skipPermissions) state.payload.skipPermissions = true;
|
|
8934
|
+
state.payload.agentPermissions = permissionMode();
|
|
8402
8935
|
const layers = readLayers(repo);
|
|
8403
8936
|
state.payload.layers = layers && layerMap(layers, state.review.head);
|
|
8404
8937
|
return state.payload;
|
|
@@ -8413,22 +8946,39 @@ function serve(opts) {
|
|
|
8413
8946
|
directorToken,
|
|
8414
8947
|
inbox,
|
|
8415
8948
|
agents,
|
|
8949
|
+
allowAlways: (item) => item.rule && addLocalAllowRule(repo, item.rule),
|
|
8950
|
+
getSettings: () => readSettings(repo),
|
|
8951
|
+
saveSettings: (raw) => {
|
|
8952
|
+
const s = parseSettings(raw);
|
|
8953
|
+
writeSettings(repo, s);
|
|
8954
|
+
return s;
|
|
8955
|
+
},
|
|
8416
8956
|
liveEvents: bus,
|
|
8417
8957
|
postActivity,
|
|
8418
|
-
reviewBlock:
|
|
8958
|
+
reviewBlock: markBlock,
|
|
8959
|
+
askChanges: (nodeId2, note) => {
|
|
8960
|
+
if (!markBlock(nodeId2, "changes_requested", note)) return void 0;
|
|
8419
8961
|
const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8962
|
+
try {
|
|
8963
|
+
const own = block2?.agentTerritory;
|
|
8964
|
+
if (own) {
|
|
8965
|
+
agents.launch(own, askReply([{ nodeId: nodeId2, note }]));
|
|
8966
|
+
return { to: "agent", territoryId: own };
|
|
8967
|
+
}
|
|
8968
|
+
const territories = getTerritories();
|
|
8969
|
+
const label = `Fix ${state.index.byId.get(nodeId2)?.name ?? nodeId2}`;
|
|
8970
|
+
const id = uniqueSlug(slugify(label), territories);
|
|
8971
|
+
saveTerritoriesAndEmit([
|
|
8972
|
+
...territories,
|
|
8973
|
+
{ id, label, nodeIds: [nodeId2], note: `${note}
|
|
8974
|
+
|
|
8975
|
+
The block: ${nodeId2}` }
|
|
8976
|
+
]);
|
|
8977
|
+
agents.launch(id);
|
|
8978
|
+
return { to: "worker", territoryId: id };
|
|
8979
|
+
} catch (e) {
|
|
8980
|
+
return { to: "saved", error: e instanceof Error ? e.message : String(e) };
|
|
8430
8981
|
}
|
|
8431
|
-
return true;
|
|
8432
8982
|
},
|
|
8433
8983
|
staticDir: DEFAULT_STATIC_DIR,
|
|
8434
8984
|
editorCommand,
|
|
@@ -8467,6 +9017,8 @@ var init_review2 = __esm({
|
|
|
8467
9017
|
init_inbox();
|
|
8468
9018
|
await init_live2();
|
|
8469
9019
|
await init_mcp();
|
|
9020
|
+
init_permissions();
|
|
9021
|
+
init_reviews();
|
|
8470
9022
|
await init_server();
|
|
8471
9023
|
init_territories();
|
|
8472
9024
|
init_watch();
|
|
@@ -8488,7 +9040,7 @@ Options:
|
|
|
8488
9040
|
--dangerously-skip-permissions launched agents run with no permission prompts (like Claude Code's
|
|
8489
9041
|
flag of the same name): nothing waits in the Inbox. For this server session only.
|
|
8490
9042
|
-h, --help`;
|
|
8491
|
-
layoutFile = (repo) =>
|
|
9043
|
+
layoutFile = (repo) => path25.join(repo, ".topo", "layout.json");
|
|
8492
9044
|
}
|
|
8493
9045
|
});
|
|
8494
9046
|
|
|
@@ -8499,8 +9051,8 @@ __export(export_exports, {
|
|
|
8499
9051
|
run: () => run5,
|
|
8500
9052
|
snapshotPage: () => snapshotPage
|
|
8501
9053
|
});
|
|
8502
|
-
import
|
|
8503
|
-
import
|
|
9054
|
+
import fs23 from "node:fs";
|
|
9055
|
+
import path26 from "node:path";
|
|
8504
9056
|
import { parseArgs as parseArgs5 } from "node:util";
|
|
8505
9057
|
function snapshotPage(template, data) {
|
|
8506
9058
|
const json2 = JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
@@ -8530,7 +9082,7 @@ function run5(argv) {
|
|
|
8530
9082
|
if (!base) throw new Error(`missing <base> ref
|
|
8531
9083
|
|
|
8532
9084
|
${HELP5}`);
|
|
8533
|
-
const repo =
|
|
9085
|
+
const repo = path26.resolve(values.repo ?? process.cwd());
|
|
8534
9086
|
const review = computeReview(repo, base, values.head);
|
|
8535
9087
|
const payload = { ...toPayload(repo, review, ""), live: void 0 };
|
|
8536
9088
|
const sources = {};
|
|
@@ -8540,14 +9092,14 @@ ${HELP5}`);
|
|
|
8540
9092
|
if (s) sources[`${side} ${b.nodeId}`] = s;
|
|
8541
9093
|
}
|
|
8542
9094
|
}
|
|
8543
|
-
const templateFile =
|
|
8544
|
-
if (!
|
|
9095
|
+
const templateFile = path26.join(DEFAULT_STATIC_DIR, "snapshot.html");
|
|
9096
|
+
if (!fs23.existsSync(templateFile)) {
|
|
8545
9097
|
throw new Error("the web UI isn't built: run `pnpm --filter @topo/web build` first");
|
|
8546
9098
|
}
|
|
8547
|
-
const template =
|
|
8548
|
-
const out =
|
|
8549
|
-
|
|
8550
|
-
const kb = Math.round(
|
|
9099
|
+
const template = fs23.readFileSync(templateFile, "utf8");
|
|
9100
|
+
const out = path26.resolve(values.out);
|
|
9101
|
+
fs23.writeFileSync(out, snapshotPage(template, { payload, sources }));
|
|
9102
|
+
const kb = Math.round(fs23.statSync(out).size / 1024);
|
|
8551
9103
|
console.log(`topo: ${review.changeSet.blocks.length} changed block(s) \u2192 ${out} (${kb} kB)`);
|
|
8552
9104
|
}
|
|
8553
9105
|
var HELP5;
|
|
@@ -8576,8 +9128,8 @@ __export(layers_exports, {
|
|
|
8576
9128
|
HELP: () => HELP6,
|
|
8577
9129
|
run: () => run6
|
|
8578
9130
|
});
|
|
8579
|
-
import
|
|
8580
|
-
import
|
|
9131
|
+
import fs24 from "node:fs";
|
|
9132
|
+
import path27 from "node:path";
|
|
8581
9133
|
import { parseArgs as parseArgs6 } from "node:util";
|
|
8582
9134
|
function run6(argv) {
|
|
8583
9135
|
const { values, positionals } = parseArgs6({
|
|
@@ -8589,18 +9141,18 @@ function run6(argv) {
|
|
|
8589
9141
|
console.log(HELP6);
|
|
8590
9142
|
return;
|
|
8591
9143
|
}
|
|
8592
|
-
const repo =
|
|
9144
|
+
const repo = path27.resolve(positionals[0] ?? process.cwd());
|
|
8593
9145
|
const graph = indexRepo(repo);
|
|
8594
9146
|
let doc = readLayers(repo);
|
|
8595
9147
|
if (values.suggest) {
|
|
8596
9148
|
if (doc) {
|
|
8597
9149
|
console.log(
|
|
8598
|
-
`topo: ${
|
|
9150
|
+
`topo: ${path27.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
|
|
8599
9151
|
);
|
|
8600
9152
|
} else {
|
|
8601
9153
|
doc = suggestLayers(graph);
|
|
8602
|
-
|
|
8603
|
-
|
|
9154
|
+
fs24.mkdirSync(path27.dirname(layersFile(repo)), { recursive: true });
|
|
9155
|
+
fs24.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
|
|
8604
9156
|
`);
|
|
8605
9157
|
console.log(
|
|
8606
9158
|
`topo: wrote ${doc.layers.length} suggested layer(s) to ${layersFile(repo)}; name them, merge them, and add rules.`
|
|
@@ -8650,7 +9202,7 @@ __export(map_exports, {
|
|
|
8650
9202
|
HELP: () => HELP7,
|
|
8651
9203
|
run: () => run7
|
|
8652
9204
|
});
|
|
8653
|
-
import
|
|
9205
|
+
import path28 from "node:path";
|
|
8654
9206
|
import { parseArgs as parseArgs7 } from "node:util";
|
|
8655
9207
|
function run7(argv) {
|
|
8656
9208
|
const { values, positionals } = parseArgs7({
|
|
@@ -8673,7 +9225,7 @@ function run7(argv) {
|
|
|
8673
9225
|
return;
|
|
8674
9226
|
}
|
|
8675
9227
|
if (!values.cache) process.env.TOPO_NO_CACHE = "1";
|
|
8676
|
-
const dir =
|
|
9228
|
+
const dir = path28.resolve(positionals[0] ?? process.cwd());
|
|
8677
9229
|
return serve({
|
|
8678
9230
|
compute: () => computeExplore(dir),
|
|
8679
9231
|
repo: dir,
|