@glrs-dev/cli 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +29 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +26 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +37 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +33 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +32 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +15 -20
- package/dist/vendor/harness-opencode/dist/chunk-57EOY72Y.js +174 -0
- package/dist/vendor/harness-opencode/dist/chunk-5TAMY7P6.js +67 -0
- package/dist/vendor/harness-opencode/dist/chunk-BKTFWXLG.js +204 -0
- package/dist/vendor/harness-opencode/dist/{chunk-XCZ3NOXR.js → chunk-CZMAJISX.js} +28 -0
- package/dist/vendor/harness-opencode/dist/chunk-KB7M7JXU.js +145 -0
- package/dist/vendor/harness-opencode/dist/chunk-RNRCXQ65.js +56 -0
- package/dist/vendor/harness-opencode/dist/{chunk-VVMP6QWS.js → chunk-WBBN7OVN.js} +162 -2
- package/dist/vendor/harness-opencode/dist/cli.js +964 -1383
- package/dist/vendor/harness-opencode/dist/index.js +2 -2
- package/dist/vendor/harness-opencode/dist/install-X5KEANRB.js +13 -0
- package/dist/vendor/harness-opencode/dist/paths-LT3QQKCF.js +18 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.d.ts +1 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.js +228 -0
- package/dist/vendor/harness-opencode/dist/pilot-config-7LJZ23YK.js +55 -0
- package/dist/vendor/harness-opencode/dist/runs-QWPL3TKV.js +18 -0
- package/dist/vendor/harness-opencode/dist/safety-gate-WM3EWOCY.js +10 -0
- package/dist/vendor/harness-opencode/dist/setup-hook-FHTXMAQL.js +88 -0
- package/dist/vendor/harness-opencode/dist/skills/adr/SKILL.md +328 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/SKILL.md +41 -10
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +27 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/qa-expectations.md +120 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/self-review.md +1 -1
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/touches-scope.md +34 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/verify-design.md +81 -13
- package/dist/vendor/harness-opencode/dist/tasks-KJ3WN2KY.js +32 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +1 -1
- package/dist/vendor/harness-opencode/dist/install-4EYR56OR.js +0 -9
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/pilot/paths.ts
|
|
2
|
+
import { promises as fs2 } from "fs";
|
|
3
|
+
import * as os2 from "os";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
|
|
6
|
+
// src/plan-paths.ts
|
|
7
|
+
import { execFile } from "child_process";
|
|
8
|
+
import * as fs from "fs/promises";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
function execFileP(file, args, opts = {}) {
|
|
12
|
+
const { cwd, timeoutMs = 5e3 } = opts;
|
|
13
|
+
return new Promise((resolve2, reject) => {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
execFile(
|
|
17
|
+
file,
|
|
18
|
+
args,
|
|
19
|
+
{ signal: controller.signal, cwd, encoding: "utf8" },
|
|
20
|
+
(err, stdout) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
if (err) {
|
|
23
|
+
reject(err);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve2(stdout ?? "");
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function expandTilde(p) {
|
|
32
|
+
if (p === "~") return os.homedir();
|
|
33
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
async function getRepoFolder(worktreeDir) {
|
|
37
|
+
let stdout;
|
|
38
|
+
try {
|
|
39
|
+
stdout = await execFileP(
|
|
40
|
+
"git",
|
|
41
|
+
["rev-parse", "--git-common-dir"],
|
|
42
|
+
{ cwd: worktreeDir }
|
|
43
|
+
);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
46
|
+
throw new Error(
|
|
47
|
+
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const gitCommonDir = stdout.trim();
|
|
51
|
+
if (!gitCommonDir) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const absCommonDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(worktreeDir, gitCommonDir);
|
|
57
|
+
const repoRoot = path.dirname(absCommonDir);
|
|
58
|
+
return path.basename(repoRoot);
|
|
59
|
+
}
|
|
60
|
+
async function getPlanDir(worktreeDir) {
|
|
61
|
+
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
62
|
+
const base = override ? expandTilde(override) : path.join(os.homedir(), ".glorious", "opencode");
|
|
63
|
+
const repoFolder = await getRepoFolder(worktreeDir);
|
|
64
|
+
const planDir = path.join(base, repoFolder, "plans");
|
|
65
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
66
|
+
return planDir;
|
|
67
|
+
}
|
|
68
|
+
async function migratePlans(worktreeDir, planDir) {
|
|
69
|
+
const oldDir = path.join(worktreeDir, ".agent", "plans");
|
|
70
|
+
const marker = path.join(oldDir, ".migrated");
|
|
71
|
+
try {
|
|
72
|
+
await fs.stat(oldDir);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await fs.stat(marker);
|
|
78
|
+
return;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await fs.readdir(oldDir);
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const planFiles = entries.filter(
|
|
88
|
+
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
89
|
+
);
|
|
90
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
91
|
+
for (const name of planFiles) {
|
|
92
|
+
const src = path.join(oldDir, name);
|
|
93
|
+
const dst = path.join(planDir, name);
|
|
94
|
+
let dstExists = false;
|
|
95
|
+
try {
|
|
96
|
+
await fs.stat(dst);
|
|
97
|
+
dstExists = true;
|
|
98
|
+
} catch {
|
|
99
|
+
dstExists = false;
|
|
100
|
+
}
|
|
101
|
+
if (!dstExists) {
|
|
102
|
+
await fs.rename(src, dst);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const [srcBuf, dstBuf] = await Promise.all([
|
|
106
|
+
fs.readFile(src),
|
|
107
|
+
fs.readFile(dst)
|
|
108
|
+
]);
|
|
109
|
+
if (srcBuf.equals(dstBuf)) {
|
|
110
|
+
await fs.unlink(src);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
115
|
+
`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
await fs.writeFile(marker, "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/pilot/paths.ts
|
|
122
|
+
function expandTilde2(p) {
|
|
123
|
+
if (p === "~") return os2.homedir();
|
|
124
|
+
if (p.startsWith("~/")) return path2.join(os2.homedir(), p.slice(2));
|
|
125
|
+
return p;
|
|
126
|
+
}
|
|
127
|
+
function resolveBaseDir() {
|
|
128
|
+
const pilotEnv = process.env.GLORIOUS_PILOT_DIR;
|
|
129
|
+
if (pilotEnv) return expandTilde2(pilotEnv);
|
|
130
|
+
const planEnv = process.env.GLORIOUS_PLAN_DIR;
|
|
131
|
+
if (planEnv) {
|
|
132
|
+
return path2.dirname(expandTilde2(planEnv));
|
|
133
|
+
}
|
|
134
|
+
return path2.join(os2.homedir(), ".glorious", "opencode");
|
|
135
|
+
}
|
|
136
|
+
async function getPilotDir(cwd) {
|
|
137
|
+
const base = resolveBaseDir();
|
|
138
|
+
const repoFolder = await getRepoFolder(cwd);
|
|
139
|
+
const dir = path2.join(base, repoFolder, "pilot");
|
|
140
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
141
|
+
return dir;
|
|
142
|
+
}
|
|
143
|
+
async function getPlansDir(cwd) {
|
|
144
|
+
const pilot = await getPilotDir(cwd);
|
|
145
|
+
const dir = path2.join(pilot, "plans");
|
|
146
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
147
|
+
return dir;
|
|
148
|
+
}
|
|
149
|
+
async function getRunDir(cwd, runId) {
|
|
150
|
+
if (!isSafeRunId(runId)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`getRunDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const pilot = await getPilotDir(cwd);
|
|
156
|
+
const dir = path2.join(pilot, "runs", runId);
|
|
157
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
158
|
+
return dir;
|
|
159
|
+
}
|
|
160
|
+
async function getStateDbPath(cwd, runId) {
|
|
161
|
+
const runDir = await getRunDir(cwd, runId);
|
|
162
|
+
return path2.join(runDir, "state.db");
|
|
163
|
+
}
|
|
164
|
+
async function getWorkerJsonlPath(cwd, runId, n) {
|
|
165
|
+
const runDir = await getRunDir(cwd, runId);
|
|
166
|
+
const workersDir = path2.join(runDir, "workers");
|
|
167
|
+
await fs2.mkdir(workersDir, { recursive: true });
|
|
168
|
+
const padded = n.toString().padStart(2, "0");
|
|
169
|
+
return path2.join(workersDir, `${padded}.jsonl`);
|
|
170
|
+
}
|
|
171
|
+
async function getTaskJsonlPath(cwd, runId, taskId) {
|
|
172
|
+
if (!isSafeRunId(runId)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`getTaskJsonlPath: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!isSafeTaskId(taskId)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`getTaskJsonlPath: taskId ${JSON.stringify(taskId)} is not a safe filesystem segment`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const runDir = await getRunDir(cwd, runId);
|
|
183
|
+
const taskDir = path2.join(runDir, "tasks", taskId);
|
|
184
|
+
await fs2.mkdir(taskDir, { recursive: true });
|
|
185
|
+
return path2.join(taskDir, "session.jsonl");
|
|
186
|
+
}
|
|
187
|
+
function isSafeRunId(runId) {
|
|
188
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(runId);
|
|
189
|
+
}
|
|
190
|
+
function isSafeTaskId(taskId) {
|
|
191
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
getPlanDir,
|
|
196
|
+
migratePlans,
|
|
197
|
+
resolveBaseDir,
|
|
198
|
+
getPilotDir,
|
|
199
|
+
getPlansDir,
|
|
200
|
+
getRunDir,
|
|
201
|
+
getStateDbPath,
|
|
202
|
+
getWorkerJsonlPath,
|
|
203
|
+
getTaskJsonlPath
|
|
204
|
+
};
|
|
@@ -59,6 +59,9 @@ var agentsMdWriterPrompt = readPrompt("agents-md-writer.md");
|
|
|
59
59
|
var pilotBuilderPrompt = readPrompt("pilot-builder.md");
|
|
60
60
|
var pilotPlannerPrompt = readPrompt("pilot-planner.md");
|
|
61
61
|
var researchPrompt = readPrompt("research.md");
|
|
62
|
+
var researchWebPrompt = readPrompt("research-web.md");
|
|
63
|
+
var researchLocalPrompt = readPrompt("research-local.md");
|
|
64
|
+
var researchAutoPrompt = readPrompt("research-auto.md");
|
|
62
65
|
function stripFrontmatter(md) {
|
|
63
66
|
if (!md.startsWith("---")) return md;
|
|
64
67
|
const end = md.indexOf("\n---", 3);
|
|
@@ -557,6 +560,9 @@ var AGENT_TIERS = {
|
|
|
557
560
|
"gap-analyzer": "deep",
|
|
558
561
|
"pilot-planner": "deep",
|
|
559
562
|
research: "deep",
|
|
563
|
+
"research-web": "deep",
|
|
564
|
+
"research-local": "deep",
|
|
565
|
+
"research-auto": "deep",
|
|
560
566
|
build: "mid",
|
|
561
567
|
"qa-reviewer": "mid",
|
|
562
568
|
"docs-maintainer": "mid",
|
|
@@ -641,6 +647,28 @@ function createAgents() {
|
|
|
641
647
|
model: "anthropic/claude-opus-4-7",
|
|
642
648
|
temperature: 0.3,
|
|
643
649
|
permission: RESEARCH_PERMISSIONS
|
|
650
|
+
}),
|
|
651
|
+
// Research subagents — thin shims that load the bundled skills
|
|
652
|
+
"research-web": agentFromPrompt(researchWebPrompt, {
|
|
653
|
+
description: "Research orchestrator subagent \u2014 Multi-agent web research orchestrator. Decomposes a research question into parallel agent workstreams, launches them, monitors progress, and synthesizes results. Use when user says 'research this topic', 'I need to understand', 'deep dive into', 'investigate the market for', 'what do we know about'. Provide the research topic and context.",
|
|
654
|
+
mode: "all",
|
|
655
|
+
model: "anthropic/claude-opus-4-7",
|
|
656
|
+
temperature: 0.3,
|
|
657
|
+
permission: RESEARCH_PERMISSIONS
|
|
658
|
+
}),
|
|
659
|
+
"research-local": agentFromPrompt(researchLocalPrompt, {
|
|
660
|
+
description: "Research orchestrator subagent \u2014 Deep codebase research using parallel Explore subagents. Decomposes a question about the local codebase into research tasks, launches parallel explorations, reviews for gaps, iterates, and synthesizes findings with specific file paths and line numbers. Use when user says 'how does X work in this codebase', 'where is Y implemented', 'trace the data flow for Z', 'what patterns does this repo use', 'explain the architecture of'. Provide the research topic as arguments.",
|
|
661
|
+
mode: "all",
|
|
662
|
+
model: "anthropic/claude-opus-4-7",
|
|
663
|
+
temperature: 0.3,
|
|
664
|
+
permission: RESEARCH_PERMISSIONS
|
|
665
|
+
}),
|
|
666
|
+
"research-auto": agentFromPrompt(researchAutoPrompt, {
|
|
667
|
+
description: "Research orchestrator subagent \u2014 Autonomous experimentation skill. Agent interviews the user, sets up a lab, then explores freely (think, test, reflect) until stopped or a target is hit. Works for any domain where you can measure or evaluate a result. Use when user says 'optimize this', 'experiment with', 'find the best approach', 'iterate on', 'research mode'. Do NOT use for binary validation tests (use /spec-lab instead). Based on ResearcherSkill v1.4.4 by krzysztofdudek.",
|
|
668
|
+
mode: "all",
|
|
669
|
+
model: "anthropic/claude-opus-4-7",
|
|
670
|
+
temperature: 0.3,
|
|
671
|
+
permission: RESEARCH_PERMISSIONS
|
|
644
672
|
})
|
|
645
673
|
};
|
|
646
674
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/pilot/worker/safety-gate.ts
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
var execFileP = promisify(execFile);
|
|
6
|
+
var SAFETY_GATE_TOLERATE = [
|
|
7
|
+
// opencode plugin installer churn. opencode upgrades its own plugin
|
|
8
|
+
// dependency in the background, which bumps the pinned version in
|
|
9
|
+
// .opencode/package.json + .opencode/package-lock.json. Users don't
|
|
10
|
+
// author those edits; refusing to start pilot because of them is a
|
|
11
|
+
// consistent friction.
|
|
12
|
+
".opencode/**",
|
|
13
|
+
// Next.js — `next build` regenerates this every run, often outside
|
|
14
|
+
// a pilot session.
|
|
15
|
+
"**/next-env.d.ts",
|
|
16
|
+
// Next.js app-router generated types.
|
|
17
|
+
"**/.next/types/**",
|
|
18
|
+
"**/.next/dev/types/**",
|
|
19
|
+
// TypeScript project-reference build info — tsc writes these.
|
|
20
|
+
"**/*.tsbuildinfo",
|
|
21
|
+
// Snapshot test files rewritten by `vitest -u` / `jest -u`.
|
|
22
|
+
"**/__snapshots__/**",
|
|
23
|
+
"**/*.snap"
|
|
24
|
+
];
|
|
25
|
+
async function git(cwd, args) {
|
|
26
|
+
for (const a of args) {
|
|
27
|
+
if (a.includes("\0")) {
|
|
28
|
+
throw new Error(`git arg contains null byte: ${JSON.stringify(a)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { stdout, stderr } = await execFileP("git", args, {
|
|
33
|
+
cwd,
|
|
34
|
+
timeout: 1e4,
|
|
35
|
+
maxBuffer: 1 << 20
|
|
36
|
+
});
|
|
37
|
+
return { stdout: stdout.toString(), stderr: stderr.toString(), ok: true };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const e = err;
|
|
40
|
+
return {
|
|
41
|
+
stdout: (e.stdout ?? "").toString(),
|
|
42
|
+
stderr: (e.stderr ?? "").toString(),
|
|
43
|
+
ok: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function headSha(cwd) {
|
|
48
|
+
const r = await git(cwd, ["rev-parse", "HEAD"]);
|
|
49
|
+
if (!r.ok) throw new Error(`git rev-parse HEAD failed: ${r.stderr.trim()}`);
|
|
50
|
+
return r.stdout.trim();
|
|
51
|
+
}
|
|
52
|
+
var FORBIDDEN_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
|
|
53
|
+
function parsePorcelainPaths(stdout) {
|
|
54
|
+
const paths = [];
|
|
55
|
+
for (const line of stdout.split("\n")) {
|
|
56
|
+
if (line.length < 4) continue;
|
|
57
|
+
let rest = line.slice(3);
|
|
58
|
+
const arrow = rest.indexOf(" -> ");
|
|
59
|
+
if (arrow !== -1) rest = rest.slice(arrow + 4);
|
|
60
|
+
if (rest.startsWith('"') && rest.endsWith('"')) {
|
|
61
|
+
rest = rest.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
paths.push(rest);
|
|
64
|
+
}
|
|
65
|
+
return paths;
|
|
66
|
+
}
|
|
67
|
+
async function checkCwdSafety(cwd) {
|
|
68
|
+
const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
69
|
+
if (!inside.ok || inside.stdout.trim() !== "true") {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: `not inside a git worktree: ${cwd}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const branchRes = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
76
|
+
if (!branchRes.ok) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: `cannot determine current branch: ${branchRes.stderr.trim()}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const branch = branchRes.stdout.trim();
|
|
83
|
+
if (FORBIDDEN_BRANCHES.has(branch)) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `refuse to run on protected branch: ${branch}. Switch to a feature branch first.`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const defaultRes = await git(cwd, [
|
|
90
|
+
"symbolic-ref",
|
|
91
|
+
"--short",
|
|
92
|
+
"refs/remotes/origin/HEAD"
|
|
93
|
+
]);
|
|
94
|
+
if (defaultRes.ok) {
|
|
95
|
+
const remoteDefault = defaultRes.stdout.trim().replace(/^origin\//, "");
|
|
96
|
+
if (remoteDefault && branch === remoteDefault) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: `refuse to run on the remote's default branch: ${branch}. Switch to a feature branch first.`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const statusRes = await git(cwd, ["status", "--porcelain"]);
|
|
104
|
+
if (!statusRes.ok) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: `git status failed: ${statusRes.stderr.trim()}`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const rawStdout = statusRes.stdout.replace(/\n$/, "");
|
|
111
|
+
if (rawStdout.length === 0) {
|
|
112
|
+
return { ok: true, warnings: [] };
|
|
113
|
+
}
|
|
114
|
+
const paths = parsePorcelainPaths(rawStdout);
|
|
115
|
+
const matchTolerate = picomatch([...SAFETY_GATE_TOLERATE], { dot: true });
|
|
116
|
+
const tolerated = [];
|
|
117
|
+
const genuine = [];
|
|
118
|
+
for (const p of paths) {
|
|
119
|
+
if (matchTolerate(p)) tolerated.push(p);
|
|
120
|
+
else genuine.push(p);
|
|
121
|
+
}
|
|
122
|
+
if (genuine.length > 0) {
|
|
123
|
+
const lines = rawStdout.split("\n").slice(0, 10).map((s) => " " + s).join("\n");
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: `working tree is dirty; pilot refuses to run on uncommitted changes.
|
|
127
|
+
Commit, stash, or discard them, then re-run.
|
|
128
|
+
First 10 lines of git status --porcelain:
|
|
129
|
+
${lines}`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const preview = tolerated.slice(0, 5);
|
|
133
|
+
const suffix = tolerated.length > preview.length ? ` (+${tolerated.length - preview.length} more)` : "";
|
|
134
|
+
const warnings = [
|
|
135
|
+
`working tree has ${tolerated.length} modified file(s) in framework-owned paths; treating tree as clean:
|
|
136
|
+
` + preview.map((p) => ` ${p}`).join("\n") + suffix
|
|
137
|
+
];
|
|
138
|
+
return { ok: true, warnings };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
SAFETY_GATE_TOLERATE,
|
|
143
|
+
headSha,
|
|
144
|
+
checkCwdSafety
|
|
145
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/pilot/mcp/session-registry.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
var writeFile2 = promisify(fs.writeFile);
|
|
6
|
+
var rename2 = promisify(fs.rename);
|
|
7
|
+
var unlink2 = promisify(fs.unlink);
|
|
8
|
+
var mkdir2 = promisify(fs.mkdir);
|
|
9
|
+
function getSessionsPath(runDir) {
|
|
10
|
+
return path.join(runDir, "sessions.json");
|
|
11
|
+
}
|
|
12
|
+
function readSessions(runDir) {
|
|
13
|
+
const sessionsPath = getSessionsPath(runDir);
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(sessionsPath, "utf8");
|
|
16
|
+
const parsed = JSON.parse(content);
|
|
17
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function registerSession(args) {
|
|
26
|
+
const { runDir, sessionId, runId, taskId } = args;
|
|
27
|
+
const sessionsPath = getSessionsPath(runDir);
|
|
28
|
+
await mkdir2(runDir, { recursive: true });
|
|
29
|
+
const current = readSessions(runDir);
|
|
30
|
+
const updated = {
|
|
31
|
+
...current,
|
|
32
|
+
[sessionId]: { runId, taskId }
|
|
33
|
+
};
|
|
34
|
+
const tempPath = `${sessionsPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
35
|
+
await writeFile2(tempPath, JSON.stringify(updated, null, 2), "utf8");
|
|
36
|
+
await rename2(tempPath, sessionsPath);
|
|
37
|
+
}
|
|
38
|
+
async function unregisterSession(args) {
|
|
39
|
+
const { runDir, sessionId } = args;
|
|
40
|
+
const sessionsPath = getSessionsPath(runDir);
|
|
41
|
+
const current = readSessions(runDir);
|
|
42
|
+
if (!(sessionId in current)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { [sessionId]: _, ...rest } = current;
|
|
46
|
+
const tempPath = `${sessionsPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
47
|
+
await writeFile2(tempPath, JSON.stringify(rest, null, 2), "utf8");
|
|
48
|
+
await rename2(tempPath, sessionsPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
getSessionsPath,
|
|
53
|
+
readSessions,
|
|
54
|
+
registerSession,
|
|
55
|
+
unregisterSession
|
|
56
|
+
};
|
|
@@ -257,7 +257,7 @@ async function requirePlugin() {
|
|
|
257
257
|
);
|
|
258
258
|
process.exit(1);
|
|
259
259
|
}
|
|
260
|
-
const { install: install2 } = await import("./install-
|
|
260
|
+
const { install: install2 } = await import("./install-X5KEANRB.js");
|
|
261
261
|
await install2({ nonInteractive: true });
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -505,6 +505,116 @@ function migrateHarnessKeyToPluginOptions(configPath) {
|
|
|
505
505
|
} catch {
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
|
+
function deepEqual(a, b) {
|
|
509
|
+
if (a === b) return true;
|
|
510
|
+
if (typeof a !== typeof b) return false;
|
|
511
|
+
if (a === null || b === null) return a === b;
|
|
512
|
+
if (typeof a !== "object") return false;
|
|
513
|
+
const aObj = a;
|
|
514
|
+
const bObj = b;
|
|
515
|
+
const aKeys = Object.keys(aObj);
|
|
516
|
+
const bKeys = Object.keys(bObj);
|
|
517
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
518
|
+
for (const key of aKeys) {
|
|
519
|
+
if (!bKeys.includes(key)) return false;
|
|
520
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
function writePluginOption(configPath, subKey, value, opts) {
|
|
525
|
+
try {
|
|
526
|
+
if (!fs3.existsSync(configPath)) {
|
|
527
|
+
return { changed: false };
|
|
528
|
+
}
|
|
529
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
530
|
+
const config = JSON.parse(raw);
|
|
531
|
+
if (!Array.isArray(config.plugin)) {
|
|
532
|
+
return { changed: false };
|
|
533
|
+
}
|
|
534
|
+
const pluginIdx = config.plugin.findIndex((entry) => {
|
|
535
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
536
|
+
return name === PLUGIN_NAME2 || String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
|
|
537
|
+
});
|
|
538
|
+
if (pluginIdx < 0) {
|
|
539
|
+
return { changed: false };
|
|
540
|
+
}
|
|
541
|
+
const current = config.plugin[pluginIdx];
|
|
542
|
+
const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME2;
|
|
543
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
|
|
544
|
+
if (deepEqual(existingOpts[subKey], value)) {
|
|
545
|
+
return { changed: false };
|
|
546
|
+
}
|
|
547
|
+
const newOpts = { ...existingOpts, [subKey]: value };
|
|
548
|
+
if (opts.dryRun) {
|
|
549
|
+
info(`[dry-run] Would reconfigure ${subKey} in plugin options`);
|
|
550
|
+
return { changed: true };
|
|
551
|
+
}
|
|
552
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
553
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
554
|
+
config.plugin[pluginIdx] = [existingName, newOpts];
|
|
555
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
556
|
+
ok(`Reconfigured ${subKey}`);
|
|
557
|
+
info(`Backup: ${bakPath}`);
|
|
558
|
+
return { changed: true, bakPath };
|
|
559
|
+
} catch {
|
|
560
|
+
return { changed: false };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function writeMcpToggles(configPath, enabledSet, opts) {
|
|
564
|
+
try {
|
|
565
|
+
if (!fs3.existsSync(configPath)) {
|
|
566
|
+
return { changed: false };
|
|
567
|
+
}
|
|
568
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
569
|
+
const config = JSON.parse(raw);
|
|
570
|
+
const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
|
|
571
|
+
const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
|
|
572
|
+
const newMcp = {};
|
|
573
|
+
let hasChanges = false;
|
|
574
|
+
for (const [key, val] of Object.entries(existingMcp)) {
|
|
575
|
+
if (!toggleNames.has(key)) {
|
|
576
|
+
newMcp[key] = val;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const toggleName of toggleNames) {
|
|
580
|
+
if (enabledSet.has(toggleName)) {
|
|
581
|
+
newMcp[toggleName] = { enabled: true };
|
|
582
|
+
if (!deepEqual(existingMcp[toggleName], { enabled: true })) {
|
|
583
|
+
hasChanges = true;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
if (existingMcp[toggleName] !== void 0) {
|
|
587
|
+
hasChanges = true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!hasChanges && Object.keys(newMcp).length === Object.keys(existingMcp).length) {
|
|
592
|
+
const allKeysMatch = Object.keys(newMcp).every(
|
|
593
|
+
(k) => deepEqual(newMcp[k], existingMcp[k])
|
|
594
|
+
);
|
|
595
|
+
if (allKeysMatch) {
|
|
596
|
+
return { changed: false };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (opts.dryRun) {
|
|
600
|
+
info(`[dry-run] Would reconfigure MCP toggles`);
|
|
601
|
+
return { changed: true };
|
|
602
|
+
}
|
|
603
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
604
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
605
|
+
if (Object.keys(newMcp).length > 0) {
|
|
606
|
+
config.mcp = newMcp;
|
|
607
|
+
} else {
|
|
608
|
+
delete config.mcp;
|
|
609
|
+
}
|
|
610
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
611
|
+
ok("Reconfigured MCPs");
|
|
612
|
+
info(`Backup: ${bakPath}`);
|
|
613
|
+
return { changed: true, bakPath };
|
|
614
|
+
} catch {
|
|
615
|
+
return { changed: false };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
508
618
|
async function install(opts = {}) {
|
|
509
619
|
const { dryRun = false, pin = false, nonInteractive = false } = opts;
|
|
510
620
|
const configPath = getOpencodeConfigPath2();
|
|
@@ -533,6 +643,10 @@ ${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
|
|
|
533
643
|
if (existingMcps.size > 0) {
|
|
534
644
|
ok(`MCPs: ${[...existingMcps].join(", ")} enabled`);
|
|
535
645
|
}
|
|
646
|
+
let reconfigureModels = false;
|
|
647
|
+
let reconfigureMcps = false;
|
|
648
|
+
let newModelsValue = null;
|
|
649
|
+
let newMcpEnabledSet = /* @__PURE__ */ new Set();
|
|
536
650
|
if (hasPlugin && (existingProvider || hasModels)) {
|
|
537
651
|
const unconfiguredMcps = MCP_TOGGLES.filter(
|
|
538
652
|
(t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
|
|
@@ -544,8 +658,20 @@ ${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
|
|
|
544
658
|
0
|
|
545
659
|
);
|
|
546
660
|
if (reconfigure === 1) {
|
|
661
|
+
reconfigureModels = true;
|
|
547
662
|
hasModels = false;
|
|
548
|
-
}
|
|
663
|
+
}
|
|
664
|
+
if (existingMcps.size > 0) {
|
|
665
|
+
const reconfigureMcpChoice = await promptChoice(
|
|
666
|
+
" Reconfigure MCPs?",
|
|
667
|
+
["No, keep current config", "Yes, reconfigure MCPs"],
|
|
668
|
+
0
|
|
669
|
+
);
|
|
670
|
+
if (reconfigureMcpChoice === 1) {
|
|
671
|
+
reconfigureMcps = true;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (!reconfigureModels && !reconfigureMcps && unconfiguredMcps.length === 0) {
|
|
549
675
|
console.log(`
|
|
550
676
|
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
551
677
|
`);
|
|
@@ -632,6 +758,11 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
632
758
|
mid: [preset.mid],
|
|
633
759
|
fast: [preset.fast]
|
|
634
760
|
};
|
|
761
|
+
newModelsValue = {
|
|
762
|
+
deep: [preset.deep],
|
|
763
|
+
mid: [preset.mid],
|
|
764
|
+
fast: [preset.fast]
|
|
765
|
+
};
|
|
635
766
|
ok(`Models configured`);
|
|
636
767
|
} else if (!pluginOpts._skipModels) {
|
|
637
768
|
info("Enter model IDs in <provider>/<model-id> format (e.g. amazon-bedrock/global.anthropic.claude-opus-4-7)");
|
|
@@ -645,6 +776,11 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
645
776
|
mid: [midModel || deepModel],
|
|
646
777
|
fast: [fastModel || midModel || deepModel]
|
|
647
778
|
};
|
|
779
|
+
newModelsValue = {
|
|
780
|
+
deep: [deepModel],
|
|
781
|
+
mid: [midModel || deepModel],
|
|
782
|
+
fast: [fastModel || midModel || deepModel]
|
|
783
|
+
};
|
|
648
784
|
ok("Models: custom");
|
|
649
785
|
} else {
|
|
650
786
|
ok("Models: OpenCode defaults");
|
|
@@ -653,6 +789,22 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
653
789
|
delete pluginOpts._skipModels;
|
|
654
790
|
console.log();
|
|
655
791
|
}
|
|
792
|
+
if (interactive && reconfigureMcps) {
|
|
793
|
+
console.log(`${c.dim}Reconfigure MCP servers${c.reset}`);
|
|
794
|
+
const currentEnabled = new Set(existingMcps);
|
|
795
|
+
const selected = await promptMulti(
|
|
796
|
+
" Select MCPs to enable:",
|
|
797
|
+
MCP_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
|
|
798
|
+
);
|
|
799
|
+
newMcpEnabledSet = new Set([...selected].map((i) => MCP_TOGGLES[i].name));
|
|
800
|
+
const names = [...newMcpEnabledSet].join(", ");
|
|
801
|
+
if (newMcpEnabledSet.size > 0) {
|
|
802
|
+
ok(`MCPs to enable: ${names}`);
|
|
803
|
+
} else {
|
|
804
|
+
ok("MCPs: all disabled");
|
|
805
|
+
}
|
|
806
|
+
console.log();
|
|
807
|
+
}
|
|
656
808
|
const pluginValue = Object.keys(pluginOpts).length > 0 ? [pluginEntry, pluginOpts] : pluginEntry;
|
|
657
809
|
const config = {
|
|
658
810
|
$schema: "https://opencode.ai/config.json",
|
|
@@ -683,6 +835,12 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
683
835
|
console.log();
|
|
684
836
|
}
|
|
685
837
|
}
|
|
838
|
+
if (reconfigureModels && newModelsValue) {
|
|
839
|
+
writePluginOption(configPath, "models", newModelsValue, { dryRun });
|
|
840
|
+
}
|
|
841
|
+
if (reconfigureMcps) {
|
|
842
|
+
writeMcpToggles(configPath, newMcpEnabledSet, { dryRun });
|
|
843
|
+
}
|
|
686
844
|
if (!fs3.existsSync(configPath)) {
|
|
687
845
|
if (dryRun) {
|
|
688
846
|
info(`[dry-run] Would create ${configPath}`);
|
|
@@ -727,5 +885,7 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
727
885
|
export {
|
|
728
886
|
requirePlugin,
|
|
729
887
|
MODEL_PRESETS,
|
|
888
|
+
writePluginOption,
|
|
889
|
+
writeMcpToggles,
|
|
730
890
|
install
|
|
731
891
|
};
|