claude-overnight 1.25.24 → 1.25.27
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/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/coach.d.ts +49 -0
- package/dist/coach.js +484 -0
- package/dist/index.js +88 -11
- package/dist/interactive-panel.d.ts +6 -15
- package/dist/interactive-panel.js +87 -50
- package/dist/merge.d.ts +17 -3
- package/dist/merge.js +82 -12
- package/dist/models.js +4 -9
- package/dist/planner-query.d.ts +8 -1
- package/dist/planner-query.js +70 -13
- package/dist/planner.js +83 -33
- package/dist/providers.js +17 -2
- package/dist/render.d.ts +19 -0
- package/dist/render.js +114 -61
- package/dist/run.d.ts +4 -0
- package/dist/run.js +243 -53
- package/dist/steering.js +17 -10
- package/dist/swarm.d.ts +12 -0
- package/dist/swarm.js +123 -15
- package/dist/test-coach.d.ts +1 -0
- package/dist/turns.d.ts +14 -0
- package/dist/turns.js +56 -0
- package/dist/types.d.ts +43 -1
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +122 -28
- package/package.json +1 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-overnight/skills/coach/SKILL.md +230 -0
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.27";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.27";
|
package/dist/coach.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PlannerLog } from "./planner-query.js";
|
|
2
|
+
import type { ProviderConfig } from "./providers.js";
|
|
3
|
+
export interface UserSettings {
|
|
4
|
+
skipCoach?: boolean;
|
|
5
|
+
lastCoachedAt?: number;
|
|
6
|
+
coachModel?: string;
|
|
7
|
+
coachProviderId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadUserSettings(): UserSettings;
|
|
10
|
+
export declare function saveUserSettings(s: UserSettings): void;
|
|
11
|
+
export declare const COACH_MODEL = "claude-haiku-4-5";
|
|
12
|
+
export type CoachPermMode = "auto" | "bypassPermissions" | "default";
|
|
13
|
+
export type CoachScope = "bugfix" | "feature-add" | "refactor" | "audit-and-fix" | "migration" | "research-and-implement" | "polish-and-verify";
|
|
14
|
+
export type ChecklistLevel = "blocking" | "warning" | "info";
|
|
15
|
+
export type ChecklistRemediation = "provider:anthropic" | "provider:cursor" | "git:dirty" | "git:branch" | "env:missing" | "port:busy" | "none";
|
|
16
|
+
export interface ChecklistItem {
|
|
17
|
+
id: string;
|
|
18
|
+
level: ChecklistLevel;
|
|
19
|
+
title: string;
|
|
20
|
+
detail: string;
|
|
21
|
+
remediation: ChecklistRemediation;
|
|
22
|
+
}
|
|
23
|
+
export interface CoachRecommended {
|
|
24
|
+
budget: number;
|
|
25
|
+
concurrency: number;
|
|
26
|
+
plannerModel: string;
|
|
27
|
+
workerModel: string;
|
|
28
|
+
fastModel: string | null;
|
|
29
|
+
flex: boolean;
|
|
30
|
+
usageCap: number | null;
|
|
31
|
+
permissionMode: CoachPermMode;
|
|
32
|
+
}
|
|
33
|
+
export interface CoachResult {
|
|
34
|
+
improvedObjective: string;
|
|
35
|
+
scope: CoachScope;
|
|
36
|
+
recommended: CoachRecommended;
|
|
37
|
+
checklist: ChecklistItem[];
|
|
38
|
+
rationale: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveCoachSkillPath(): string | null;
|
|
41
|
+
export declare function validateCoachOutput(raw: unknown): CoachResult | null;
|
|
42
|
+
export interface CoachContext {
|
|
43
|
+
providers: ProviderConfig[];
|
|
44
|
+
cliFlags: Record<string, string>;
|
|
45
|
+
log?: PlannerLog;
|
|
46
|
+
coachModel?: string;
|
|
47
|
+
coachProvider?: ProviderConfig;
|
|
48
|
+
}
|
|
49
|
+
export declare function runSetupCoach(rawObjective: string, cwd: string, ctx: CoachContext): Promise<CoachResult | null>;
|
package/dist/coach.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { runPlannerQuery, attemptJsonParse } from "./planner-query.js";
|
|
8
|
+
import { createTurn, beginTurn, endTurn } from "./turns.js";
|
|
9
|
+
import { selectKey, ask } from "./cli.js";
|
|
10
|
+
import { envFor } from "./providers.js";
|
|
11
|
+
// ── URL fetching for plan links in the objective ──
|
|
12
|
+
const URL_REGEX = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g;
|
|
13
|
+
async function fetchUrlContent(url, timeoutMs = 5_000) {
|
|
14
|
+
try {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
const resp = await fetch(url, { signal: controller.signal, redirect: "follow" });
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
if (!resp.ok)
|
|
20
|
+
return null;
|
|
21
|
+
const ct = resp.headers.get("content-type") || "";
|
|
22
|
+
if (ct.includes("json"))
|
|
23
|
+
return await resp.text();
|
|
24
|
+
// For HTML, extract body text (rough); for .md/.txt, return raw
|
|
25
|
+
if (ct.includes("text/html")) {
|
|
26
|
+
const html = await resp.text();
|
|
27
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 4000);
|
|
28
|
+
}
|
|
29
|
+
return (await resp.text()).slice(0, 4000);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ── User settings (~/.claude/claude-overnight/settings.json) ──
|
|
36
|
+
const SETTINGS_DIR = join(homedir(), ".claude", "claude-overnight");
|
|
37
|
+
const SETTINGS_PATH = join(SETTINGS_DIR, "settings.json");
|
|
38
|
+
export function loadUserSettings() {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function saveUserSettings(s) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
49
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2), "utf-8");
|
|
50
|
+
try {
|
|
51
|
+
chmodSync(SETTINGS_PATH, 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
}
|
|
57
|
+
// ── Coach model (separate from DEFAULT_MODEL so the coach can stay cheap) ──
|
|
58
|
+
export const COACH_MODEL = "claude-haiku-4-5";
|
|
59
|
+
const COACH_TIMEOUT_MS = 15_000;
|
|
60
|
+
const COACH_SOFT_STATUS_MS = 5_000;
|
|
61
|
+
// ── Raw schema matching the SKILL.md invocation contract ──
|
|
62
|
+
const COACH_SCHEMA = {
|
|
63
|
+
type: "json_schema",
|
|
64
|
+
schema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
required: ["scope", "improvedObjective", "rationale", "recommended", "checklist", "questions"],
|
|
68
|
+
properties: {
|
|
69
|
+
scope: { type: "string", enum: ["bugfix", "feature-add", "refactor", "audit-and-fix", "migration", "research-and-implement", "polish-and-verify"] },
|
|
70
|
+
improvedObjective: { type: "string" },
|
|
71
|
+
rationale: { type: "string" },
|
|
72
|
+
recommended: {
|
|
73
|
+
type: "object",
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
required: ["budget", "concurrency", "plannerModel", "workerModel", "fastModel", "flex", "usageCap", "permissionMode"],
|
|
76
|
+
properties: {
|
|
77
|
+
budget: { type: "integer", minimum: 1 },
|
|
78
|
+
concurrency: { type: "integer", minimum: 1, maximum: 12 },
|
|
79
|
+
plannerModel: { type: "string" },
|
|
80
|
+
workerModel: { type: "string" },
|
|
81
|
+
fastModel: { type: ["string", "null"] },
|
|
82
|
+
flex: { type: "boolean" },
|
|
83
|
+
usageCap: { type: ["number", "null"] },
|
|
84
|
+
permissionMode: { type: "string", enum: ["auto", "bypassPermissions", "default"] },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
checklist: {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: {
|
|
90
|
+
type: "object",
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
required: ["id", "level", "title", "detail", "remediation"],
|
|
93
|
+
properties: {
|
|
94
|
+
id: { type: "string" },
|
|
95
|
+
level: { type: "string", enum: ["blocking", "warning", "info"] },
|
|
96
|
+
title: { type: "string" },
|
|
97
|
+
detail: { type: "string" },
|
|
98
|
+
remediation: { type: "string", enum: ["provider:anthropic", "provider:cursor", "git:dirty", "git:branch", "env:missing", "port:busy", "none"] },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
questions: { type: "array", items: { type: "string" } },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
// ── Skill-file resolution ──
|
|
107
|
+
export function resolveCoachSkillPath() {
|
|
108
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
const installRoot = dirname(here); // <pkg>/dist → <pkg>
|
|
110
|
+
const candidates = [
|
|
111
|
+
join(installRoot, "plugins", "claude-overnight", "skills", "coach", "SKILL.md"),
|
|
112
|
+
join(here, "..", "plugins", "claude-overnight", "skills", "coach", "SKILL.md"),
|
|
113
|
+
];
|
|
114
|
+
for (const p of candidates) {
|
|
115
|
+
try {
|
|
116
|
+
if (existsSync(p))
|
|
117
|
+
return p;
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// ── Validation / coercion of the model output ──
|
|
124
|
+
export function validateCoachOutput(raw) {
|
|
125
|
+
if (!raw || typeof raw !== "object")
|
|
126
|
+
return null;
|
|
127
|
+
const r = raw;
|
|
128
|
+
const scopes = ["bugfix", "feature-add", "refactor", "audit-and-fix", "migration", "research-and-implement", "polish-and-verify"];
|
|
129
|
+
if (typeof r.scope !== "string" || !scopes.includes(r.scope))
|
|
130
|
+
return null;
|
|
131
|
+
if (typeof r.improvedObjective !== "string" || r.improvedObjective.trim().length < 5)
|
|
132
|
+
return null;
|
|
133
|
+
if (typeof r.rationale !== "string")
|
|
134
|
+
return null;
|
|
135
|
+
const rec = r.recommended;
|
|
136
|
+
if (!rec || typeof rec !== "object")
|
|
137
|
+
return null;
|
|
138
|
+
const budget = Number(rec.budget);
|
|
139
|
+
const concurrency = Number(rec.concurrency);
|
|
140
|
+
if (!Number.isFinite(budget) || budget < 1)
|
|
141
|
+
return null;
|
|
142
|
+
if (!Number.isFinite(concurrency) || concurrency < 1 || concurrency > 12)
|
|
143
|
+
return null;
|
|
144
|
+
if (typeof rec.plannerModel !== "string" || typeof rec.workerModel !== "string")
|
|
145
|
+
return null;
|
|
146
|
+
const fastModel = rec.fastModel == null ? null : (typeof rec.fastModel === "string" ? rec.fastModel : null);
|
|
147
|
+
if (typeof rec.flex !== "boolean")
|
|
148
|
+
return null;
|
|
149
|
+
const usageCap = rec.usageCap == null ? null : (typeof rec.usageCap === "number" && rec.usageCap > 0 && rec.usageCap <= 1 ? rec.usageCap : null);
|
|
150
|
+
const perms = ["auto", "bypassPermissions", "default"];
|
|
151
|
+
if (typeof rec.permissionMode !== "string" || !perms.includes(rec.permissionMode))
|
|
152
|
+
return null;
|
|
153
|
+
const rawChecklist = Array.isArray(r.checklist) ? r.checklist : [];
|
|
154
|
+
const checklist = [];
|
|
155
|
+
for (const item of rawChecklist) {
|
|
156
|
+
if (!item || typeof item !== "object")
|
|
157
|
+
continue;
|
|
158
|
+
const it = item;
|
|
159
|
+
if (typeof it.id !== "string" || typeof it.title !== "string" || typeof it.detail !== "string")
|
|
160
|
+
continue;
|
|
161
|
+
const levels = ["blocking", "warning", "info"];
|
|
162
|
+
if (typeof it.level !== "string" || !levels.includes(it.level))
|
|
163
|
+
continue;
|
|
164
|
+
const rems = ["provider:anthropic", "provider:cursor", "git:dirty", "git:branch", "env:missing", "port:busy", "none"];
|
|
165
|
+
const remediation = (typeof it.remediation === "string" && rems.includes(it.remediation))
|
|
166
|
+
? it.remediation : "none";
|
|
167
|
+
checklist.push({ id: it.id, level: it.level, title: it.title, detail: it.detail, remediation });
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
improvedObjective: r.improvedObjective.trim(),
|
|
171
|
+
scope: r.scope,
|
|
172
|
+
rationale: r.rationale.trim(),
|
|
173
|
+
recommended: {
|
|
174
|
+
budget: Math.round(budget),
|
|
175
|
+
concurrency: Math.round(concurrency),
|
|
176
|
+
plannerModel: rec.plannerModel,
|
|
177
|
+
workerModel: rec.workerModel,
|
|
178
|
+
fastModel,
|
|
179
|
+
flex: rec.flex,
|
|
180
|
+
usageCap,
|
|
181
|
+
permissionMode: rec.permissionMode,
|
|
182
|
+
},
|
|
183
|
+
checklist,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function collectRepoFacts(cwd) {
|
|
187
|
+
const readmeHead = (() => {
|
|
188
|
+
for (const name of ["README.md", "README", "readme.md"]) {
|
|
189
|
+
const p = join(cwd, name);
|
|
190
|
+
try {
|
|
191
|
+
if (existsSync(p))
|
|
192
|
+
return readFileSync(p, "utf-8").slice(0, 1500);
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
return "";
|
|
197
|
+
})();
|
|
198
|
+
const packageJson = (() => {
|
|
199
|
+
try {
|
|
200
|
+
const raw = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
201
|
+
const deps = { ...(raw.dependencies ?? {}), ...(raw.devDependencies ?? {}) };
|
|
202
|
+
const depNames = Object.keys(deps).slice(0, 40);
|
|
203
|
+
return {
|
|
204
|
+
name: typeof raw.name === "string" ? raw.name : undefined,
|
|
205
|
+
scripts: raw.scripts && typeof raw.scripts === "object" ? raw.scripts : undefined,
|
|
206
|
+
depSummary: depNames.join(", "),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
const safeExec = (cmd, timeoutMs = 1_500) => {
|
|
214
|
+
try {
|
|
215
|
+
return execSync(cmd, { cwd, timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const gitStatus = safeExec("git status --porcelain").slice(0, 800);
|
|
222
|
+
const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD").slice(0, 120);
|
|
223
|
+
const gitLog = safeExec("git log --oneline -20").slice(0, 2000);
|
|
224
|
+
const tree = (() => {
|
|
225
|
+
try {
|
|
226
|
+
return readdirSync(cwd).filter(n => !n.startsWith(".") || n === ".env").slice(0, 60);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
const hasEnv = [".env", ".env.local", ".env.development"].some(n => {
|
|
233
|
+
try {
|
|
234
|
+
return existsSync(join(cwd, n));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const hasTests = ["tests", "__tests__", "test", "spec"].some(n => {
|
|
241
|
+
try {
|
|
242
|
+
return existsSync(join(cwd, n)) && statSync(join(cwd, n)).isDirectory();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
const lockfiles = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"].filter(n => {
|
|
249
|
+
try {
|
|
250
|
+
return existsSync(join(cwd, n));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const priorRuns = (() => {
|
|
257
|
+
try {
|
|
258
|
+
const dir = join(cwd, ".claude-overnight", "runs");
|
|
259
|
+
return readdirSync(dir).length;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
265
|
+
const srcFileCount = (() => {
|
|
266
|
+
const out = safeExec("git ls-files", 2_000);
|
|
267
|
+
if (!out)
|
|
268
|
+
return 0;
|
|
269
|
+
const lines = out.split("\n").filter(Boolean);
|
|
270
|
+
return lines.filter(l => /^(src|app|lib)\//.test(l)).length;
|
|
271
|
+
})();
|
|
272
|
+
return { cwd, readmeHead, packageJson, gitStatus, gitBranch, gitLog, tree, hasEnv, hasTests, lockfiles, priorRuns, srcFileCount };
|
|
273
|
+
}
|
|
274
|
+
function renderProviders(providers) {
|
|
275
|
+
const lines = [];
|
|
276
|
+
const hasAnthropicKey = !!(process.env.ANTHROPIC_API_KEY?.trim() || process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim());
|
|
277
|
+
lines.push(`- Anthropic direct: ${hasAnthropicKey ? "available (env)" : "not configured (no ANTHROPIC_API_KEY / Claude session)"}`);
|
|
278
|
+
if (providers.length === 0) {
|
|
279
|
+
lines.push("- No custom providers saved in ~/.claude/claude-overnight/providers.json");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
for (const p of providers) {
|
|
283
|
+
const tag = p.cursorProxy ? " · cursor proxy" : (p.useJWT ? " · JWT" : "");
|
|
284
|
+
const keySrc = p.keyEnv ? `env ${p.keyEnv}` : (p.cursorApiKey || p.key ? "stored key" : "no key");
|
|
285
|
+
lines.push(`- ${p.displayName} → model="${p.model}"${tag} (${keySrc})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
function renderRepoFacts(f, rawObjective, providers, cliFlags, planContent) {
|
|
291
|
+
const sections = [];
|
|
292
|
+
sections.push(`# Raw user objective\n\n${rawObjective}`);
|
|
293
|
+
if (planContent)
|
|
294
|
+
sections.push(`# Linked plan (fetched from URL in objective)\n\n${planContent}`);
|
|
295
|
+
sections.push(`# Repo facts\n\n- cwd: ${f.cwd}\n- git branch: ${f.gitBranch || "(unknown)"}\n- source files (src|app|lib): ${f.srcFileCount}\n- prior claude-overnight runs: ${f.priorRuns}\n- .env present: ${f.hasEnv}\n- tests dir: ${f.hasTests}\n- lockfiles: ${f.lockfiles.join(", ") || "(none)"}`);
|
|
296
|
+
// CLI flags encode user intent — surface them so the coach can respect constraints.
|
|
297
|
+
if (Object.keys(cliFlags).length > 0) {
|
|
298
|
+
const flagLines = Object.entries(cliFlags).map(([k, v]) => ` --${k}=${v}`).join("\n");
|
|
299
|
+
sections.push(`# CLI flags (user-specified constraints)\n\n${flagLines}`);
|
|
300
|
+
}
|
|
301
|
+
sections.push(`# Available providers (recommend ONLY models the user can reach)\n\n${renderProviders(providers)}`);
|
|
302
|
+
if (f.packageJson) {
|
|
303
|
+
const scripts = f.packageJson.scripts ? Object.entries(f.packageJson.scripts).slice(0, 15).map(([k, v]) => ` ${k}: ${v}`).join("\n") : "(none)";
|
|
304
|
+
sections.push(`# package.json\n\nname: ${f.packageJson.name ?? "(none)"}\n\nscripts:\n${scripts}\n\ndeps: ${f.packageJson.depSummary ?? "(none)"}`);
|
|
305
|
+
}
|
|
306
|
+
if (f.gitStatus)
|
|
307
|
+
sections.push(`# git status --porcelain\n\n${f.gitStatus}`);
|
|
308
|
+
if (f.gitLog)
|
|
309
|
+
sections.push(`# git log (last 20)\n\n${f.gitLog}`);
|
|
310
|
+
if (f.readmeHead)
|
|
311
|
+
sections.push(`# README head\n\n${f.readmeHead}`);
|
|
312
|
+
if (f.tree.length)
|
|
313
|
+
sections.push(`# top-level entries\n\n${f.tree.join(", ")}`);
|
|
314
|
+
return sections.join("\n\n");
|
|
315
|
+
}
|
|
316
|
+
export async function runSetupCoach(rawObjective, cwd, ctx) {
|
|
317
|
+
const skillPath = resolveCoachSkillPath();
|
|
318
|
+
if (!skillPath) {
|
|
319
|
+
console.log(chalk.dim(" coach skipped: skill unavailable"));
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
let skill = "";
|
|
323
|
+
try {
|
|
324
|
+
skill = readFileSync(skillPath, "utf-8");
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
console.log(chalk.dim(" coach skipped: skill unreadable"));
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const facts = collectRepoFacts(cwd);
|
|
331
|
+
if (facts.srcFileCount > 1_000_000)
|
|
332
|
+
return null;
|
|
333
|
+
// Fetch any URLs found in the objective so the coach sees plan content, not dead links.
|
|
334
|
+
const urls = rawObjective.match(URL_REGEX) ?? [];
|
|
335
|
+
let planContent = null;
|
|
336
|
+
if (urls.length > 0) {
|
|
337
|
+
const results = await Promise.all(urls.map(u => fetchUrlContent(u, 4_000)));
|
|
338
|
+
const fetched = results.filter(Boolean);
|
|
339
|
+
if (fetched.length > 0) {
|
|
340
|
+
planContent = fetched.map((c, i) => `[URL ${i + 1}: ${urls[i]}]\n${c}`).join("\n\n---\n\n");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const userMessage = renderRepoFacts(facts, rawObjective, ctx.providers, ctx.cliFlags, planContent);
|
|
344
|
+
const prompt = `${skill}\n\n---\n\n${userMessage}\n\nRespond with the JSON object defined in "Invocation contract" only.`;
|
|
345
|
+
const model = ctx.coachModel ?? COACH_MODEL;
|
|
346
|
+
const startedAt = Date.now();
|
|
347
|
+
const spinner = setInterval(() => {
|
|
348
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
349
|
+
const hint = elapsed >= Math.round(COACH_SOFT_STATUS_MS / 1000) ? " · still thinking…" : "";
|
|
350
|
+
process.stdout.write(`\x1B[2K\r ${chalk.cyan("⚡")} ${chalk.dim(`coach ${elapsed}s${hint}`)}`);
|
|
351
|
+
}, 500);
|
|
352
|
+
let raw;
|
|
353
|
+
const turn = createTurn("coach", "Coach", "coach-0", model);
|
|
354
|
+
beginTurn(turn);
|
|
355
|
+
try {
|
|
356
|
+
const coachEnv = ctx.coachProvider ? envFor(ctx.coachProvider) : undefined;
|
|
357
|
+
const queryPromise = runPlannerQuery(prompt, {
|
|
358
|
+
cwd,
|
|
359
|
+
model,
|
|
360
|
+
permissionMode: "bypassPermissions",
|
|
361
|
+
outputFormat: COACH_SCHEMA,
|
|
362
|
+
transcriptName: "coach",
|
|
363
|
+
maxTurns: 3,
|
|
364
|
+
tools: [],
|
|
365
|
+
env: coachEnv,
|
|
366
|
+
turnId: turn.id,
|
|
367
|
+
}, () => { });
|
|
368
|
+
const timeout = new Promise((_, reject) => {
|
|
369
|
+
setTimeout(() => reject(new Error(`coach timed out after ${Math.round(COACH_TIMEOUT_MS / 1000)}s`)), COACH_TIMEOUT_MS);
|
|
370
|
+
});
|
|
371
|
+
raw = await Promise.race([queryPromise, timeout]);
|
|
372
|
+
endTurn(turn, "done");
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
clearInterval(spinner);
|
|
376
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
377
|
+
endTurn(turn, "error");
|
|
378
|
+
const msg = String(err?.message ?? err).toLowerCase();
|
|
379
|
+
const reason = msg.includes("timed out") ? "timeout"
|
|
380
|
+
: (msg.includes("401") || msg.includes("auth")) ? "auth"
|
|
381
|
+
: "network";
|
|
382
|
+
console.log(chalk.dim(` coach skipped: ${reason}`));
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
clearInterval(spinner);
|
|
386
|
+
const elapsedMs = Date.now() - startedAt;
|
|
387
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
388
|
+
const parsed = attemptJsonParse(raw);
|
|
389
|
+
const result = validateCoachOutput(parsed);
|
|
390
|
+
if (!result) {
|
|
391
|
+
console.log(chalk.dim(" coach output malformed — skipping"));
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
// The coach is advisory: provider issues surface as checklist items.
|
|
395
|
+
// We don't auto-spawn anything (cursor proxy, key prompts, etc.) because
|
|
396
|
+
// the user hasn't picked a provider yet — that happens in the pickers,
|
|
397
|
+
// and each provider flow already runs its own setup when actually selected.
|
|
398
|
+
renderCoachBlock(result, elapsedMs, model);
|
|
399
|
+
const choice = await selectKey("", [
|
|
400
|
+
{ key: "y", desc: " accept" },
|
|
401
|
+
{ key: "e", desc: "dit objective" },
|
|
402
|
+
{ key: "s", desc: "kip coach" },
|
|
403
|
+
{ key: "x", desc: " skip coach forever" },
|
|
404
|
+
]);
|
|
405
|
+
if (choice === "y") {
|
|
406
|
+
saveUserSettings({ ...loadUserSettings(), lastCoachedAt: Date.now() });
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
if (choice === "e") {
|
|
410
|
+
const amend = (await ask(`\n ${chalk.cyan(">")} what would you change? `)).trim();
|
|
411
|
+
if (!amend)
|
|
412
|
+
return null;
|
|
413
|
+
const amendedPrompt = `${prompt}\n\n---\n\nUser amendment (apply and return a revised JSON object):\n${amend}`;
|
|
414
|
+
const amendTurn = createTurn("coach", "Coach (amended)", "coach-amend-0", model);
|
|
415
|
+
beginTurn(amendTurn);
|
|
416
|
+
try {
|
|
417
|
+
const coachEnv = ctx.coachProvider ? envFor(ctx.coachProvider) : undefined;
|
|
418
|
+
const raw2 = await Promise.race([
|
|
419
|
+
runPlannerQuery(amendedPrompt, {
|
|
420
|
+
cwd, model, permissionMode: "bypassPermissions",
|
|
421
|
+
outputFormat: COACH_SCHEMA, transcriptName: "coach-retry", maxTurns: 3, tools: [],
|
|
422
|
+
env: coachEnv,
|
|
423
|
+
turnId: amendTurn.id,
|
|
424
|
+
}, () => { }),
|
|
425
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("coach amendment timed out")), COACH_TIMEOUT_MS)),
|
|
426
|
+
]);
|
|
427
|
+
endTurn(amendTurn, "done");
|
|
428
|
+
const parsed2 = attemptJsonParse(raw2);
|
|
429
|
+
const result2 = validateCoachOutput(parsed2);
|
|
430
|
+
if (result2) {
|
|
431
|
+
renderCoachBlock(result2, Date.now() - startedAt, model);
|
|
432
|
+
const confirm = await selectKey("", [
|
|
433
|
+
{ key: "y", desc: " accept" },
|
|
434
|
+
{ key: "s", desc: "kip coach" },
|
|
435
|
+
]);
|
|
436
|
+
if (confirm === "y") {
|
|
437
|
+
saveUserSettings({ ...loadUserSettings(), lastCoachedAt: Date.now() });
|
|
438
|
+
return result2;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
console.log(chalk.dim(" coach amendment malformed — falling through"));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
console.log(chalk.dim(" coach amendment failed — falling through"));
|
|
447
|
+
endTurn(amendTurn, "error");
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
if (choice === "x") {
|
|
452
|
+
saveUserSettings({ ...loadUserSettings(), skipCoach: true });
|
|
453
|
+
console.log(chalk.dim(" coach disabled — run `claude-overnight --coach` once to re-enable"));
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
// ── Rendering ──
|
|
459
|
+
function renderCoachBlock(r, elapsedMs, model) {
|
|
460
|
+
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
461
|
+
console.log(`\n ${chalk.cyan("⚡")} ${chalk.bold("Coach")} ${chalk.dim(`(${model}, ${elapsed}s)`)}\n`);
|
|
462
|
+
console.log(` ${chalk.cyan("✦")} ${chalk.bold("Objective")}`);
|
|
463
|
+
for (const line of r.improvedObjective.split("\n")) {
|
|
464
|
+
console.log(` ${line}`);
|
|
465
|
+
}
|
|
466
|
+
if (r.rationale)
|
|
467
|
+
console.log(` ${chalk.dim(r.rationale)}`);
|
|
468
|
+
const rec = r.recommended;
|
|
469
|
+
console.log(`\n ${chalk.cyan("⚙")} ${chalk.bold("Settings")}`);
|
|
470
|
+
const fastStr = rec.fastModel ? ` fast=${rec.fastModel}` : "";
|
|
471
|
+
console.log(` planner=${rec.plannerModel} worker=${rec.workerModel}${fastStr}`);
|
|
472
|
+
const capStr = rec.usageCap != null ? `${Math.round(rec.usageCap * 100)}%` : "unlimited";
|
|
473
|
+
console.log(` budget=${rec.budget} concurrency=${rec.concurrency} flex=${rec.flex ? "on" : "off"} cap=${capStr}`);
|
|
474
|
+
console.log(` scope: ${r.scope} perm=${rec.permissionMode}`);
|
|
475
|
+
if (r.checklist.length) {
|
|
476
|
+
console.log(`\n ${chalk.cyan("🔑")} ${chalk.bold("Preflight")}`);
|
|
477
|
+
for (const item of r.checklist) {
|
|
478
|
+
const mark = item.level === "blocking" ? chalk.red("✗")
|
|
479
|
+
: item.level === "warning" ? chalk.yellow("⚠") : chalk.green("✓");
|
|
480
|
+
console.log(` ${mark} ${item.title}${item.detail ? chalk.dim(` — ${item.detail}`) : ""}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
console.log("");
|
|
484
|
+
}
|