@cjhyy/code-shell-arena 0.8.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 +46 -0
- package/dist/arena.d.ts +45 -0
- package/dist/arena.js +362 -0
- package/dist/capability.d.ts +43 -0
- package/dist/capability.js +453 -0
- package/dist/context/context-tools.d.ts +16 -0
- package/dist/context/context-tools.js +272 -0
- package/dist/context/within-root.d.ts +7 -0
- package/dist/context/within-root.js +15 -0
- package/dist/detect-mode.d.ts +20 -0
- package/dist/detect-mode.js +78 -0
- package/dist/digest-builder.d.ts +25 -0
- package/dist/digest-builder.js +120 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/index.runtime.d.ts +13 -0
- package/dist/index.runtime.js +11 -0
- package/dist/iterate/convergence.d.ts +25 -0
- package/dist/iterate/convergence.js +103 -0
- package/dist/iterate/formats/index.d.ts +22 -0
- package/dist/iterate/formats/index.js +283 -0
- package/dist/iterate/index.d.ts +11 -0
- package/dist/iterate/index.js +9 -0
- package/dist/iterate/iterative-arena.d.ts +31 -0
- package/dist/iterate/iterative-arena.js +252 -0
- package/dist/iterate/parse.d.ts +42 -0
- package/dist/iterate/parse.js +123 -0
- package/dist/iterate/phases/argue.d.ts +22 -0
- package/dist/iterate/phases/argue.js +165 -0
- package/dist/iterate/phases/revise.d.ts +16 -0
- package/dist/iterate/phases/revise.js +62 -0
- package/dist/iterate/phases/tournament.d.ts +34 -0
- package/dist/iterate/phases/tournament.js +113 -0
- package/dist/iterate/tools/web-tools.d.ts +13 -0
- package/dist/iterate/tools/web-tools.js +54 -0
- package/dist/iterate/types.d.ts +152 -0
- package/dist/iterate/types.js +8 -0
- package/dist/ledger.d.ts +47 -0
- package/dist/ledger.js +159 -0
- package/dist/lenses/architecture.d.ts +5 -0
- package/dist/lenses/architecture.js +22 -0
- package/dist/lenses/engineering.d.ts +5 -0
- package/dist/lenses/engineering.js +22 -0
- package/dist/lenses/general.d.ts +5 -0
- package/dist/lenses/general.js +20 -0
- package/dist/lenses/index.d.ts +16 -0
- package/dist/lenses/index.js +47 -0
- package/dist/lenses/product.d.ts +5 -0
- package/dist/lenses/product.js +22 -0
- package/dist/model-presets.d.ts +23 -0
- package/dist/model-presets.js +44 -0
- package/dist/phases/adjudication.d.ts +25 -0
- package/dist/phases/adjudication.js +142 -0
- package/dist/phases/build-consensus.d.ts +30 -0
- package/dist/phases/build-consensus.js +85 -0
- package/dist/phases/claim-registry.d.ts +26 -0
- package/dist/phases/claim-registry.js +60 -0
- package/dist/phases/cross-review.d.ts +47 -0
- package/dist/phases/cross-review.js +224 -0
- package/dist/phases/debate-rounds.d.ts +28 -0
- package/dist/phases/debate-rounds.js +161 -0
- package/dist/phases/participant-research.d.ts +39 -0
- package/dist/phases/participant-research.js +322 -0
- package/dist/phases/planning-detail-expansion.d.ts +39 -0
- package/dist/phases/planning-detail-expansion.js +120 -0
- package/dist/planner.d.ts +28 -0
- package/dist/planner.js +312 -0
- package/dist/providers/docs.d.ts +7 -0
- package/dist/providers/docs.js +111 -0
- package/dist/providers/git.d.ts +8 -0
- package/dist/providers/git.js +174 -0
- package/dist/providers/index.d.ts +32 -0
- package/dist/providers/index.js +132 -0
- package/dist/providers/none.d.ts +7 -0
- package/dist/providers/none.js +11 -0
- package/dist/providers/repo.d.ts +7 -0
- package/dist/providers/repo.js +258 -0
- package/dist/render/session.d.ts +17 -0
- package/dist/render/session.js +190 -0
- package/dist/render/terminal.d.ts +34 -0
- package/dist/render/terminal.js +286 -0
- package/dist/strategies/discussion.d.ts +25 -0
- package/dist/strategies/discussion.js +143 -0
- package/dist/strategies/index.d.ts +15 -0
- package/dist/strategies/index.js +28 -0
- package/dist/strategies/language-wrapper.d.ts +17 -0
- package/dist/strategies/language-wrapper.js +102 -0
- package/dist/strategies/lens-wrapper.d.ts +16 -0
- package/dist/strategies/lens-wrapper.js +236 -0
- package/dist/strategies/planning.d.ts +30 -0
- package/dist/strategies/planning.js +225 -0
- package/dist/strategies/review.d.ts +26 -0
- package/dist/strategies/review.js +168 -0
- package/dist/strategies/utils.d.ts +36 -0
- package/dist/strategies/utils.js +603 -0
- package/dist/tools/selector.d.ts +17 -0
- package/dist/tools/selector.js +61 -0
- package/dist/transitions.d.ts +53 -0
- package/dist/transitions.js +97 -0
- package/dist/types.d.ts +518 -0
- package/dist/types.js +27 -0
- package/package.json +45 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — evidence collectors for Arena.
|
|
3
|
+
*/
|
|
4
|
+
import { gitProvider } from "./git.js";
|
|
5
|
+
import { repoProvider } from "./repo.js";
|
|
6
|
+
import { docsProvider } from "./docs.js";
|
|
7
|
+
import { noneProvider } from "./none.js";
|
|
8
|
+
import { logger } from "@cjhyy/code-shell-core/extension";
|
|
9
|
+
export { gitProvider } from "./git.js";
|
|
10
|
+
export { repoProvider } from "./repo.js";
|
|
11
|
+
export { docsProvider } from "./docs.js";
|
|
12
|
+
export { noneProvider } from "./none.js";
|
|
13
|
+
const PROVIDER_MAP = {
|
|
14
|
+
git: gitProvider,
|
|
15
|
+
repo: repoProvider,
|
|
16
|
+
docs: docsProvider,
|
|
17
|
+
web: noneProvider, // web deferred to Phase 4 — falls back to none for now
|
|
18
|
+
none: noneProvider,
|
|
19
|
+
};
|
|
20
|
+
// Hard ceiling for any single provider. The repo provider can fan
|
|
21
|
+
// out 5+ recursive greps; without this, one slow grep on a large
|
|
22
|
+
// tree would freeze the entire arena startup. Tuned to be generous
|
|
23
|
+
// for normal cases but tight enough that the user doesn't sit
|
|
24
|
+
// staring at "Arena {}" for a minute.
|
|
25
|
+
const PROVIDER_TIMEOUT_MS = 8_000;
|
|
26
|
+
/**
|
|
27
|
+
* Collect all evidence according to the plan's sources.
|
|
28
|
+
* Returns unified ArenaArtifact[] and quickFacts.
|
|
29
|
+
*
|
|
30
|
+
* Providers run in parallel (independent of one another) with a
|
|
31
|
+
* per-provider timeout, so a single slow source can't stall the
|
|
32
|
+
* whole arena. Heartbeats are emitted around each provider so the
|
|
33
|
+
* UI can show "collecting from repo..." instead of a blank pause.
|
|
34
|
+
*/
|
|
35
|
+
export async function collectEvidence(plan, topic, options = {}) {
|
|
36
|
+
const artifacts = [];
|
|
37
|
+
const quickFacts = [];
|
|
38
|
+
const seenSources = new Set();
|
|
39
|
+
const { onProgress, signal } = options;
|
|
40
|
+
const tasks = [];
|
|
41
|
+
for (const sourceSpec of plan.sources) {
|
|
42
|
+
if (seenSources.has(sourceSpec.kind))
|
|
43
|
+
continue;
|
|
44
|
+
seenSources.add(sourceSpec.kind);
|
|
45
|
+
const provider = PROVIDER_MAP[sourceSpec.kind];
|
|
46
|
+
if (!provider)
|
|
47
|
+
continue;
|
|
48
|
+
onProgress?.({ type: "evidence_started", source: sourceSpec.kind });
|
|
49
|
+
tasks.push(runProviderWithTimeout(provider, plan, topic, signal));
|
|
50
|
+
}
|
|
51
|
+
const results = await Promise.all(tasks);
|
|
52
|
+
for (const r of results) {
|
|
53
|
+
artifacts.push(...r.artifacts);
|
|
54
|
+
logger.info("arena.evidence_collected", {
|
|
55
|
+
source: r.kind,
|
|
56
|
+
count: r.artifacts.length,
|
|
57
|
+
durationMs: r.durationMs,
|
|
58
|
+
timedOut: r.timedOut || undefined,
|
|
59
|
+
});
|
|
60
|
+
onProgress?.({
|
|
61
|
+
type: "evidence_collected",
|
|
62
|
+
source: r.kind,
|
|
63
|
+
count: r.artifacts.length,
|
|
64
|
+
durationMs: r.durationMs,
|
|
65
|
+
timedOut: r.timedOut,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Build quick facts from artifacts
|
|
69
|
+
const branchArtifact = artifacts.find((a) => a.id === "git-branch");
|
|
70
|
+
if (branchArtifact)
|
|
71
|
+
quickFacts.push({ label: "Branch", value: branchArtifact.preview });
|
|
72
|
+
const changedFilesArtifact = artifacts.find((a) => a.id === "git-changed-files");
|
|
73
|
+
if (changedFilesArtifact?.metadata?.totalCount) {
|
|
74
|
+
quickFacts.push({ label: "Changed Files", value: String(changedFilesArtifact.metadata.totalCount) });
|
|
75
|
+
}
|
|
76
|
+
const docCount = artifacts.filter((a) => a.source === "docs").length;
|
|
77
|
+
if (docCount > 0)
|
|
78
|
+
quickFacts.push({ label: "Documents", value: String(docCount) });
|
|
79
|
+
quickFacts.push({ label: "Sources", value: [...seenSources].join(", ") });
|
|
80
|
+
quickFacts.push({ label: "Lenses", value: plan.lenses.map((l) => l.name).join(", ") });
|
|
81
|
+
return { artifacts, quickFacts };
|
|
82
|
+
}
|
|
83
|
+
async function runProviderWithTimeout(provider, plan, topic, signal) {
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
let timedOut = false;
|
|
86
|
+
// Hold the timer outside the timeout Promise so the work-winner
|
|
87
|
+
// branch can clear it. Previously the timer kept ticking after
|
|
88
|
+
// work resolved and fired a spurious "timeout" warn 8s later.
|
|
89
|
+
let timer;
|
|
90
|
+
let abortHandler;
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
if (timer) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
timer = undefined;
|
|
95
|
+
}
|
|
96
|
+
if (abortHandler) {
|
|
97
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
98
|
+
abortHandler = undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const work = Promise.resolve().then(() => provider.collect(plan, topic));
|
|
102
|
+
const timeout = new Promise((resolve) => {
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
timedOut = true;
|
|
105
|
+
logger.warn("arena.provider.timeout", { source: provider.kind, ms: PROVIDER_TIMEOUT_MS });
|
|
106
|
+
resolve([]);
|
|
107
|
+
}, PROVIDER_TIMEOUT_MS);
|
|
108
|
+
if (typeof timer.unref === "function")
|
|
109
|
+
timer.unref();
|
|
110
|
+
if (signal) {
|
|
111
|
+
abortHandler = () => resolve([]);
|
|
112
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
let artifacts;
|
|
116
|
+
try {
|
|
117
|
+
artifacts = await Promise.race([work, timeout]);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
logger.warn("arena.provider.error", { source: provider.kind, error: err.message });
|
|
121
|
+
artifacts = [];
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: provider.kind,
|
|
128
|
+
artifacts,
|
|
129
|
+
durationMs: Date.now() - started,
|
|
130
|
+
timedOut,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoneProvider — returns no external evidence.
|
|
3
|
+
* Used for pure topic discussions, brainstorming, or debates
|
|
4
|
+
* where no external context is needed.
|
|
5
|
+
*/
|
|
6
|
+
import type { ArenaContextProvider } from "../types.js";
|
|
7
|
+
export declare const noneProvider: ArenaContextProvider;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoneProvider — returns no external evidence.
|
|
3
|
+
* Used for pure topic discussions, brainstorming, or debates
|
|
4
|
+
* where no external context is needed.
|
|
5
|
+
*/
|
|
6
|
+
export const noneProvider = {
|
|
7
|
+
kind: "none",
|
|
8
|
+
collect(_plan, _topic) {
|
|
9
|
+
return [];
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepoProvider — collects evidence from the code repository:
|
|
3
|
+
* directory trees, key files, grep results.
|
|
4
|
+
*/
|
|
5
|
+
import type { ArenaContextProvider } from "../types.js";
|
|
6
|
+
export declare const repoProvider: ArenaContextProvider;
|
|
7
|
+
export declare function truncate(content: string): string;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepoProvider — collects evidence from the code repository:
|
|
3
|
+
* directory trees, key files, grep results.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { readdir, stat } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { logger } from "@cjhyy/code-shell-core/extension";
|
|
11
|
+
const MAX_FILE_CHARS = 8_000;
|
|
12
|
+
// Per-grep ceiling. We run up to 5 hints in parallel, so the worst-case
|
|
13
|
+
// wall time is ~3s rather than the previous 5×10s = 50s in series.
|
|
14
|
+
const GREP_TIMEOUT_MS = 3_000;
|
|
15
|
+
// Hard cap on tree-walk work — large monorepos (or recursive symlinks)
|
|
16
|
+
// could otherwise blow up readdirSync/statSync time.
|
|
17
|
+
const MAX_TREE_NODES = 400;
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
export const repoProvider = {
|
|
20
|
+
kind: "repo",
|
|
21
|
+
async collect(plan, topic) {
|
|
22
|
+
const artifacts = [];
|
|
23
|
+
const targets = plan.sources.find((s) => s.kind === "repo")?.targets ?? [];
|
|
24
|
+
// Project tree overview
|
|
25
|
+
const tree = await buildTree(".", 2);
|
|
26
|
+
if (tree) {
|
|
27
|
+
artifacts.push({
|
|
28
|
+
id: "repo-tree",
|
|
29
|
+
kind: "tree",
|
|
30
|
+
source: "repo",
|
|
31
|
+
title: "Project Structure",
|
|
32
|
+
preview: tree,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// If specific targets provided, read them
|
|
36
|
+
for (const target of targets) {
|
|
37
|
+
if (!existsSync(target))
|
|
38
|
+
continue;
|
|
39
|
+
const st = statSync(target);
|
|
40
|
+
if (st.isDirectory()) {
|
|
41
|
+
const dirTree = await buildTree(target, 3);
|
|
42
|
+
if (dirTree) {
|
|
43
|
+
artifacts.push({
|
|
44
|
+
id: `repo-tree-${target}`,
|
|
45
|
+
kind: "tree",
|
|
46
|
+
source: "repo",
|
|
47
|
+
title: `Directory: ${target}`,
|
|
48
|
+
preview: dirTree,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Read entry files
|
|
52
|
+
for (const file of (await collectEntryFiles(target)).slice(0, 3)) {
|
|
53
|
+
const content = safeReadFile(file);
|
|
54
|
+
if (content) {
|
|
55
|
+
artifacts.push({
|
|
56
|
+
id: `repo-file-${file}`,
|
|
57
|
+
kind: "file",
|
|
58
|
+
source: "repo",
|
|
59
|
+
title: file,
|
|
60
|
+
ref: file,
|
|
61
|
+
preview: truncate(content),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const content = safeReadFile(target);
|
|
68
|
+
if (content) {
|
|
69
|
+
artifacts.push({
|
|
70
|
+
id: `repo-file-${target}`,
|
|
71
|
+
kind: "file",
|
|
72
|
+
source: "repo",
|
|
73
|
+
title: target,
|
|
74
|
+
ref: target,
|
|
75
|
+
preview: truncate(content),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Search for topic-related files if no specific targets.
|
|
81
|
+
// Fan out hints in parallel — the previous serial loop was the
|
|
82
|
+
// single biggest contributor to Arena's startup latency.
|
|
83
|
+
if (targets.length === 0) {
|
|
84
|
+
const hints = extractSearchHints(topic).slice(0, 5);
|
|
85
|
+
const grepResults = await Promise.all(hints.map((h) => safeGrep(h)));
|
|
86
|
+
for (let i = 0; i < hints.length; i++) {
|
|
87
|
+
const hint = hints[i];
|
|
88
|
+
const result = grepResults[i] ?? "";
|
|
89
|
+
if (!result)
|
|
90
|
+
continue;
|
|
91
|
+
const files = result.split("\n").filter(Boolean).slice(0, 10);
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (!artifacts.some((a) => a.ref === file)) {
|
|
94
|
+
artifacts.push({
|
|
95
|
+
id: `repo-grep-${hint}-${file}`,
|
|
96
|
+
kind: "grep",
|
|
97
|
+
source: "repo",
|
|
98
|
+
title: `Match: ${hint} → ${file}`,
|
|
99
|
+
ref: file,
|
|
100
|
+
preview: file,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Recent git activity — only if git source is not already active (avoids duplicate)
|
|
107
|
+
const hasGitSource = plan.sources.some((s) => s.kind === "git");
|
|
108
|
+
if (!hasGitSource) {
|
|
109
|
+
const recentLog = await gitLog();
|
|
110
|
+
if (recentLog) {
|
|
111
|
+
artifacts.push({
|
|
112
|
+
id: "repo-recent-activity",
|
|
113
|
+
kind: "doc",
|
|
114
|
+
source: "repo",
|
|
115
|
+
title: "Recent Git Activity",
|
|
116
|
+
preview: recentLog,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
logger.info("arena.provider.repo", { artifactCount: artifacts.length });
|
|
121
|
+
return artifacts;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
async function buildTree(dir, maxDepth) {
|
|
125
|
+
// Async + node-capped walk. The previous *Sync variant blocked the
|
|
126
|
+
// event loop while the new collectEvidence runs providers in
|
|
127
|
+
// parallel, partially defeating the parallelism win.
|
|
128
|
+
const lines = [];
|
|
129
|
+
const counter = { n: 0 };
|
|
130
|
+
await walkTree(dir, maxDepth, 0, "", lines, counter);
|
|
131
|
+
return lines.filter(Boolean).join("\n");
|
|
132
|
+
}
|
|
133
|
+
async function walkTree(dir, maxDepth, depth, prefix, out, counter) {
|
|
134
|
+
if (depth >= maxDepth)
|
|
135
|
+
return;
|
|
136
|
+
if (counter.n >= MAX_TREE_NODES)
|
|
137
|
+
return;
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = (await readdir(dir)).filter((e) => !e.startsWith(".") && e !== "node_modules" && e !== "dist" && e !== "__pycache__");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries.slice(0, 30)) {
|
|
146
|
+
if (counter.n >= MAX_TREE_NODES)
|
|
147
|
+
return;
|
|
148
|
+
counter.n++;
|
|
149
|
+
const fullPath = join(dir, entry);
|
|
150
|
+
let isDir = false;
|
|
151
|
+
try {
|
|
152
|
+
isDir = (await stat(fullPath)).isDirectory();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
out.push(`${prefix}${isDir ? "/" : ""} ${entry}`);
|
|
158
|
+
if (isDir && depth < maxDepth - 1) {
|
|
159
|
+
await walkTree(fullPath, maxDepth, depth + 1, prefix + " ", out, counter);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function collectEntryFiles(dir) {
|
|
164
|
+
const files = [];
|
|
165
|
+
try {
|
|
166
|
+
const entries = await readdir(dir);
|
|
167
|
+
const priority = ["index.ts", "index.js", "main.ts", "main.js", "mod.ts", "__init__.py"];
|
|
168
|
+
for (const p of priority) {
|
|
169
|
+
if (entries.includes(p))
|
|
170
|
+
files.push(join(dir, p));
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const full = join(dir, entry);
|
|
174
|
+
if (/\.(ts|js|tsx|jsx|py)$/.test(entry) && !files.includes(full)) {
|
|
175
|
+
files.push(full);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { /* ignore */ }
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
function safeReadFile(filePath) {
|
|
183
|
+
try {
|
|
184
|
+
if (!existsSync(filePath))
|
|
185
|
+
return null;
|
|
186
|
+
const st = statSync(filePath);
|
|
187
|
+
if (st.size > 500_000)
|
|
188
|
+
return null;
|
|
189
|
+
return readFileSync(filePath, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function truncate(content) {
|
|
196
|
+
if (content.length <= MAX_FILE_CHARS)
|
|
197
|
+
return content;
|
|
198
|
+
const t = content.slice(0, MAX_FILE_CHARS);
|
|
199
|
+
const lastNl = t.lastIndexOf("\n");
|
|
200
|
+
// No newline in the window (e.g. a minified file) → lastNl is -1 and
|
|
201
|
+
// slice(0,-1) would drop just the last char; cut at MAX_FILE_CHARS instead.
|
|
202
|
+
const body = lastNl > 0 ? t.slice(0, lastNl) : t;
|
|
203
|
+
return body + `\n... (truncated, ${content.length} chars total)`;
|
|
204
|
+
}
|
|
205
|
+
async function safeGrep(pattern) {
|
|
206
|
+
try {
|
|
207
|
+
const { stdout } = await execFileAsync("grep", [
|
|
208
|
+
"-rl",
|
|
209
|
+
"--include=*.ts", "--include=*.js", "--include=*.py", "--include=*.md",
|
|
210
|
+
"-i", pattern, ".",
|
|
211
|
+
], { encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: GREP_TIMEOUT_MS });
|
|
212
|
+
return stdout.trim();
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
// grep exits 1 when there are no matches — that's the expected
|
|
216
|
+
// "no hits" path. Anything else (signal kill on timeout, ENOENT
|
|
217
|
+
// because grep isn't installed, permission denied, etc.) is a
|
|
218
|
+
// real failure that the caller should know about, otherwise
|
|
219
|
+
// Arena silently degrades with zero artifacts.
|
|
220
|
+
const e = err;
|
|
221
|
+
if (e?.code === 1)
|
|
222
|
+
return "";
|
|
223
|
+
logger.warn("arena.provider.grep_failed", {
|
|
224
|
+
pattern,
|
|
225
|
+
code: e?.code,
|
|
226
|
+
killed: e?.killed,
|
|
227
|
+
signal: e?.signal,
|
|
228
|
+
});
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function gitLog() {
|
|
233
|
+
try {
|
|
234
|
+
const { stdout } = await execFileAsync("git", ["log", "--oneline", "-10"], {
|
|
235
|
+
encoding: "utf-8",
|
|
236
|
+
maxBuffer: 1024 * 1024,
|
|
237
|
+
timeout: 10_000,
|
|
238
|
+
});
|
|
239
|
+
return stdout.trim();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function extractSearchHints(topic) {
|
|
246
|
+
const stopWords = new Set([
|
|
247
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "have", "has",
|
|
248
|
+
"do", "does", "did", "will", "would", "could", "should", "may", "might",
|
|
249
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from", "about",
|
|
250
|
+
"and", "but", "or", "not", "this", "that", "my", "your", "what", "how",
|
|
251
|
+
"review", "discuss", "plan", "help", "please", "look", "check",
|
|
252
|
+
"一下", "看看", "帮", "我", "的", "是否", "怎么", "如何", "讨论", "规划", "审查",
|
|
253
|
+
]);
|
|
254
|
+
return topic
|
|
255
|
+
.split(/[\s,;:]+/)
|
|
256
|
+
.map((w) => w.toLowerCase().replace(/[^a-z0-9_\-/]/g, ""))
|
|
257
|
+
.filter((w) => w.length > 2 && !stopWords.has(w));
|
|
258
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session renderer — formats ArenaResultV2 into markdown suitable for
|
|
3
|
+
* displaying as assistant_text and as context for follow-up LLM turns.
|
|
4
|
+
*
|
|
5
|
+
* Scene-aware output order:
|
|
6
|
+
* 1. Subject Summary — factual framing / what changed / current scope
|
|
7
|
+
* 2. Overall assessment
|
|
8
|
+
* 3. Roadmap (planning-first when present)
|
|
9
|
+
* 4. Consensus sections ordered by output emphasis
|
|
10
|
+
* 5. Next Actions
|
|
11
|
+
* 6. Per-participant findings (collapsed)
|
|
12
|
+
*/
|
|
13
|
+
import type { ArenaResultV2 } from "../types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Format an ArenaResultV2 into markdown text.
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatArenaResultForSession(result: ArenaResultV2): string;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session renderer — formats ArenaResultV2 into markdown suitable for
|
|
3
|
+
* displaying as assistant_text and as context for follow-up LLM turns.
|
|
4
|
+
*
|
|
5
|
+
* Scene-aware output order:
|
|
6
|
+
* 1. Subject Summary — factual framing / what changed / current scope
|
|
7
|
+
* 2. Overall assessment
|
|
8
|
+
* 3. Roadmap (planning-first when present)
|
|
9
|
+
* 4. Consensus sections ordered by output emphasis
|
|
10
|
+
* 5. Next Actions
|
|
11
|
+
* 6. Per-participant findings (collapsed)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Format an ArenaResultV2 into markdown text.
|
|
15
|
+
*/
|
|
16
|
+
export function formatArenaResultForSession(result) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
const modeLabel = result.mode.charAt(0).toUpperCase() + result.mode.slice(1);
|
|
19
|
+
lines.push(`## Arena ${modeLabel} Result`);
|
|
20
|
+
lines.push(`**Topic:** ${result.topic}`);
|
|
21
|
+
lines.push(`**Models:** ${result.participants.join(" vs ")}`);
|
|
22
|
+
lines.push("");
|
|
23
|
+
const c = result.consensus;
|
|
24
|
+
// 1. Subject Summary — factual overview (adapts label per mode)
|
|
25
|
+
if (c.subjectSummary) {
|
|
26
|
+
const overviewLabel = result.plan.outputShape.overviewLabel
|
|
27
|
+
?? (result.mode === "review" ? "What Changed"
|
|
28
|
+
: result.mode === "planning" ? "Current Scope"
|
|
29
|
+
: "Problem Framing");
|
|
30
|
+
lines.push(`### ${overviewLabel}`);
|
|
31
|
+
lines.push(c.subjectSummary);
|
|
32
|
+
lines.push("");
|
|
33
|
+
}
|
|
34
|
+
// 2. Overall assessment
|
|
35
|
+
if (c.summary) {
|
|
36
|
+
lines.push("### Overall Assessment");
|
|
37
|
+
lines.push(c.summary);
|
|
38
|
+
lines.push("");
|
|
39
|
+
}
|
|
40
|
+
// 3. Roadmap — surfaced early for planning scenes
|
|
41
|
+
if (result.mode === "planning" && c.roadmap.length > 0) {
|
|
42
|
+
lines.push("### Roadmap");
|
|
43
|
+
for (const [index, phase] of c.roadmap.entries()) {
|
|
44
|
+
lines.push(...formatRoadmapPhase(index, phase));
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
// 3b. Implementation Details — expanded phase details
|
|
48
|
+
if (c.roadmapDetails && c.roadmapDetails.length > 0) {
|
|
49
|
+
lines.push("### Implementation Details");
|
|
50
|
+
for (const detail of c.roadmapDetails) {
|
|
51
|
+
lines.push(...formatPhaseDetail(detail));
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 4. Consensus sections ordered by output emphasis
|
|
57
|
+
for (const section of getOrderedConsensusSections(result)) {
|
|
58
|
+
lines.push(`### ${section.title}`);
|
|
59
|
+
for (const item of section.items) {
|
|
60
|
+
lines.push(`- **${item.title}**: ${item.summary}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
// 5. Roadmap for non-planning scenes
|
|
65
|
+
if (result.mode !== "planning" && c.roadmap.length > 0) {
|
|
66
|
+
lines.push("### Roadmap");
|
|
67
|
+
for (const [index, phase] of c.roadmap.entries()) {
|
|
68
|
+
lines.push(...formatRoadmapPhase(index, phase));
|
|
69
|
+
}
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
// 6. Next Actions
|
|
73
|
+
if (c.nextActions.length > 0) {
|
|
74
|
+
lines.push("### Next Actions");
|
|
75
|
+
for (const action of c.nextActions) {
|
|
76
|
+
const pri = action.priority === "high" ? "🔴" : action.priority === "medium" ? "🟡" : "⚪";
|
|
77
|
+
lines.push(`- ${pri} **${action.title}**: ${action.rationale}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("");
|
|
80
|
+
}
|
|
81
|
+
// 7. Per-participant findings (detail section)
|
|
82
|
+
if (result.reports.some((r) => r.findings.length > 0)) {
|
|
83
|
+
lines.push("---");
|
|
84
|
+
lines.push("### Participant Findings");
|
|
85
|
+
for (const report of result.reports) {
|
|
86
|
+
if (report.findings.length === 0) {
|
|
87
|
+
lines.push(`\n**${report.participant}**: ${report.contextSummary || "(no findings)"}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
lines.push(`\n**${report.participant}** (${report.findings.length} findings)`);
|
|
91
|
+
if (report.contextSummary) {
|
|
92
|
+
lines.push(`> ${report.contextSummary}`);
|
|
93
|
+
}
|
|
94
|
+
for (const f of report.findings) {
|
|
95
|
+
const severity = f.severity ? `/${f.severity}` : "";
|
|
96
|
+
lines.push(`- [${f.kind}${severity}] **${f.title}**: ${f.summary}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
function formatRoadmapPhase(index, phase) {
|
|
103
|
+
const pri = phase.priority === "high" ? "High" : phase.priority === "medium" ? "Medium" : "Low";
|
|
104
|
+
const lines = [`#### Phase ${index + 1}: ${phase.title} (${pri} priority)`];
|
|
105
|
+
if (phase.goal) {
|
|
106
|
+
lines.push(`Goal: ${phase.goal}`);
|
|
107
|
+
}
|
|
108
|
+
if (phase.scope.length > 0) {
|
|
109
|
+
lines.push(`Scope: ${phase.scope.join("; ")}`);
|
|
110
|
+
}
|
|
111
|
+
if (phase.deliverables.length > 0) {
|
|
112
|
+
lines.push(`Deliverables: ${phase.deliverables.join("; ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (phase.dependencies.length > 0) {
|
|
115
|
+
lines.push(`Dependencies: ${phase.dependencies.join("; ")}`);
|
|
116
|
+
}
|
|
117
|
+
if (phase.risks.length > 0) {
|
|
118
|
+
lines.push(`Risks: ${phase.risks.join("; ")}`);
|
|
119
|
+
}
|
|
120
|
+
if (phase.successCriteria.length > 0) {
|
|
121
|
+
lines.push(`Success Criteria: ${phase.successCriteria.join("; ")}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
function formatPhaseDetail(detail) {
|
|
127
|
+
const effort = detail.effort === "large" ? "Large" : detail.effort === "small" ? "Small" : "Medium";
|
|
128
|
+
const lines = [`#### ${detail.phaseTitle} (${effort} effort)`];
|
|
129
|
+
if (detail.objective) {
|
|
130
|
+
lines.push(detail.objective);
|
|
131
|
+
}
|
|
132
|
+
if (detail.targetFiles.length > 0) {
|
|
133
|
+
lines.push(`**Target files:** ${detail.targetFiles.map((f) => "`" + f + "`").join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
if (detail.codeChanges.length > 0) {
|
|
136
|
+
lines.push("**Code changes:**");
|
|
137
|
+
for (const change of detail.codeChanges) {
|
|
138
|
+
lines.push(`- ${change}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (detail.interfaces.length > 0) {
|
|
142
|
+
lines.push(`**Interfaces:** ${detail.interfaces.join("; ")}`);
|
|
143
|
+
}
|
|
144
|
+
if (detail.migrationSteps.length > 0) {
|
|
145
|
+
lines.push("**Migration steps:**");
|
|
146
|
+
for (const [i, step] of detail.migrationSteps.entries()) {
|
|
147
|
+
lines.push(`${i + 1}. ${step}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (detail.validation.length > 0) {
|
|
151
|
+
lines.push(`**Validation:** ${detail.validation.join("; ")}`);
|
|
152
|
+
}
|
|
153
|
+
if (detail.blockers.length > 0) {
|
|
154
|
+
lines.push(`**Blockers:** ${detail.blockers.join("; ")}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
function getOrderedConsensusSections(result) {
|
|
160
|
+
const VALID_KINDS = new Set(["strength", "improvement", "risk", "question"]);
|
|
161
|
+
const defaultOrder = ["risk", "improvement", "strength", "question"];
|
|
162
|
+
const emphasize = (result.plan?.outputShape?.emphasize ?? []).filter((k) => VALID_KINDS.has(k));
|
|
163
|
+
const orderedKinds = dedupeFindingKinds([...emphasize, ...defaultOrder]);
|
|
164
|
+
const sections = orderedKinds.map((kind) => {
|
|
165
|
+
switch (kind) {
|
|
166
|
+
case "strength":
|
|
167
|
+
return { kind, title: "✓ Strengths", items: result.consensus.strengths };
|
|
168
|
+
case "improvement":
|
|
169
|
+
return { kind, title: "→ Improvements", items: result.consensus.improvements };
|
|
170
|
+
case "risk":
|
|
171
|
+
return { kind, title: "⚠ Risks", items: result.consensus.risks };
|
|
172
|
+
case "question":
|
|
173
|
+
return { kind, title: "? Open Questions", items: result.consensus.openQuestions };
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return sections
|
|
177
|
+
.filter((section) => section != null)
|
|
178
|
+
.filter((section) => section.items.length > 0);
|
|
179
|
+
}
|
|
180
|
+
function dedupeFindingKinds(kinds) {
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const result = [];
|
|
183
|
+
for (const kind of kinds) {
|
|
184
|
+
if (!seen.has(kind)) {
|
|
185
|
+
seen.add(kind);
|
|
186
|
+
result.push(kind);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer — renders ArenaConsensus and ArenaResultV2.
|
|
3
|
+
*
|
|
4
|
+
* All render functions return strings instead of printing directly,
|
|
5
|
+
* so callers can decide where to display (ctx.addStatus vs console.log).
|
|
6
|
+
*
|
|
7
|
+
* Output sections:
|
|
8
|
+
* 1. Summary
|
|
9
|
+
* 2. Roadmap (surfaced early for planning scenes)
|
|
10
|
+
* 3. Consensus sections ordered by output emphasis
|
|
11
|
+
* 4. Next Actions
|
|
12
|
+
*/
|
|
13
|
+
import { type Colorizer } from "@cjhyy/code-shell-core/extension";
|
|
14
|
+
import type { ArenaResultV2, ArenaProgressEvent } from "../types.js";
|
|
15
|
+
/** Output sink — callers provide this to control where text goes. */
|
|
16
|
+
export type OutputSink = (text: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Format the full arena result as a styled string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatArenaResult(result: ArenaResultV2, c?: Colorizer): string;
|
|
21
|
+
/**
|
|
22
|
+
* Print the full arena result directly to stdout.
|
|
23
|
+
* Used by the CLI `code-shell arena` command (non-REPL).
|
|
24
|
+
*/
|
|
25
|
+
export declare function printArenaResult(result: ArenaResultV2, c?: Colorizer): void;
|
|
26
|
+
/**
|
|
27
|
+
* Create a progress renderer that sends output to a sink.
|
|
28
|
+
* For REPL: pass ctx.addStatus. For CLI: pass console.log.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createProgressRenderer(sink: OutputSink, c?: Colorizer): (event: ArenaProgressEvent) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Legacy: direct console.log progress renderer for CLI mode.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderProgress(event: ArenaProgressEvent, c?: Colorizer): void;
|