@os-eco/overstory-cli 0.9.4 → 0.11.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 +50 -19
- package/agents/builder.md +19 -9
- package/agents/coordinator.md +6 -6
- package/agents/lead.md +204 -87
- package/agents/merger.md +25 -14
- package/agents/reviewer.md +22 -16
- package/agents/scout.md +17 -12
- package/package.json +6 -3
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/headless-mail-injector.test.ts +448 -0
- package/src/agents/headless-mail-injector.ts +219 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +514 -14
- package/src/agents/hooks-deployer.ts +141 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/overlay.test.ts +60 -4
- package/src/agents/overlay.ts +63 -8
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/commands/agents.ts +9 -0
- package/src/commands/clean.ts +54 -0
- package/src/commands/coordinator.test.ts +254 -0
- package/src/commands/coordinator.ts +273 -8
- package/src/commands/dashboard.test.ts +188 -0
- package/src/commands/dashboard.ts +14 -4
- package/src/commands/doctor.ts +3 -1
- package/src/commands/group.test.ts +94 -0
- package/src/commands/group.ts +49 -20
- package/src/commands/init.test.ts +8 -0
- package/src/commands/init.ts +8 -1
- package/src/commands/log.test.ts +187 -11
- package/src/commands/log.ts +171 -71
- package/src/commands/mail.test.ts +162 -0
- package/src/commands/mail.ts +64 -9
- package/src/commands/merge.test.ts +230 -1
- package/src/commands/merge.ts +68 -12
- package/src/commands/nudge.test.ts +351 -4
- package/src/commands/nudge.ts +356 -34
- package/src/commands/run.test.ts +43 -7
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +408 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1323 -0
- package/src/commands/serve/rest.ts +708 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +565 -0
- package/src/commands/sling.test.ts +177 -1
- package/src/commands/sling.ts +243 -71
- package/src/commands/status.test.ts +9 -0
- package/src/commands/status.ts +12 -4
- package/src/commands/stop.test.ts +255 -1
- package/src/commands/stop.ts +107 -8
- package/src/commands/watch.test.ts +43 -0
- package/src/commands/watch.ts +153 -28
- package/src/config.ts +23 -0
- package/src/doctor/consistency.test.ts +106 -0
- package/src/doctor/consistency.ts +48 -1
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/types.ts +2 -1
- package/src/doctor/watchdog.ts +57 -1
- package/src/events/tailer.test.ts +234 -1
- package/src/events/tailer.ts +90 -0
- package/src/index.ts +57 -6
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.ts +29 -0
- package/src/logging/theme.ts +4 -0
- package/src/mail/client.ts +15 -2
- package/src/mail/store.test.ts +82 -0
- package/src/mail/store.ts +41 -4
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/resolver.ts +1 -1
- package/src/mulch/client.ts +3 -3
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/claude.test.ts +791 -1
- package/src/runtimes/claude.ts +323 -1
- package/src/runtimes/connections.test.ts +141 -1
- package/src/runtimes/connections.ts +73 -4
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/types.ts +10 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.test.ts +657 -29
- package/src/sessions/store.ts +286 -23
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/types.ts +107 -2
- package/src/utils/pid.test.ts +85 -1
- package/src/utils/pid.ts +86 -1
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/watchdog/daemon.test.ts +1607 -376
- package/src/watchdog/daemon.ts +462 -88
- package/src/watchdog/health.test.ts +282 -0
- package/src/watchdog/health.ts +126 -27
- package/src/worktree/manager.test.ts +218 -1
- package/src/worktree/manager.ts +55 -0
- package/src/worktree/process.test.ts +71 -0
- package/src/worktree/process.ts +25 -5
- package/src/worktree/tmux.test.ts +28 -0
- package/src/worktree/tmux.ts +27 -3
- package/templates/CLAUDE.md.tmpl +19 -8
- package/templates/overlay.md.tmpl +5 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect-free conflict prediction for `ov merge --dry-run`.
|
|
3
|
+
*
|
|
4
|
+
* Uses `git merge-tree --write-tree --merge-base=<base> <ours> <theirs>` to
|
|
5
|
+
* compute the conflict set without mutating HEAD, the working tree, or the
|
|
6
|
+
* merge lock. Each conflict file is classified into a predicted resolution
|
|
7
|
+
* tier by reusing the same primitives that the actual resolver uses
|
|
8
|
+
* (`hasContentfulCanonical`, `checkMergeUnion`), so prediction stays in lock
|
|
9
|
+
* step with how `ov merge` would actually behave at runtime.
|
|
10
|
+
*
|
|
11
|
+
* Requires git >= 2.38 for `merge-tree --write-tree`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { MergeError } from "../errors.ts";
|
|
15
|
+
import type { MulchClient } from "../mulch/client.ts";
|
|
16
|
+
import type { ConflictPrediction, MergeEntry } from "../types.ts";
|
|
17
|
+
import {
|
|
18
|
+
buildConflictHistory,
|
|
19
|
+
checkMergeUnion,
|
|
20
|
+
hasContentfulCanonical,
|
|
21
|
+
parseConflictPatterns,
|
|
22
|
+
} from "./resolver.ts";
|
|
23
|
+
|
|
24
|
+
/** Run a git command in the given repo root. Returns stdout, stderr, exit code. */
|
|
25
|
+
async function runGit(
|
|
26
|
+
repoRoot: string,
|
|
27
|
+
args: string[],
|
|
28
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
29
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
30
|
+
cwd: repoRoot,
|
|
31
|
+
stdout: "pipe",
|
|
32
|
+
stderr: "pipe",
|
|
33
|
+
});
|
|
34
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
35
|
+
new Response(proc.stdout).text(),
|
|
36
|
+
new Response(proc.stderr).text(),
|
|
37
|
+
proc.exited,
|
|
38
|
+
]);
|
|
39
|
+
return { stdout, stderr, exitCode };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolve a ref to its commit OID. Throws MergeError when the ref is missing. */
|
|
43
|
+
async function resolveRef(repoRoot: string, ref: string): Promise<string> {
|
|
44
|
+
const { stdout, stderr, exitCode } = await runGit(repoRoot, ["rev-parse", "--verify", ref]);
|
|
45
|
+
if (exitCode !== 0) {
|
|
46
|
+
throw new MergeError(`Failed to resolve ref "${ref}": ${stderr.trim()}`, {
|
|
47
|
+
branchName: ref,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return stdout.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find the merge-base of two refs.
|
|
55
|
+
* Returns the merge-base OID, or null when the refs share no history.
|
|
56
|
+
*/
|
|
57
|
+
async function findMergeBase(
|
|
58
|
+
repoRoot: string,
|
|
59
|
+
ours: string,
|
|
60
|
+
theirs: string,
|
|
61
|
+
): Promise<string | null> {
|
|
62
|
+
const { stdout, exitCode } = await runGit(repoRoot, ["merge-base", ours, theirs]);
|
|
63
|
+
if (exitCode !== 0) return null;
|
|
64
|
+
const oid = stdout.trim();
|
|
65
|
+
return oid.length > 0 ? oid : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface MergeTreeOutput {
|
|
69
|
+
treeOid: string;
|
|
70
|
+
conflictPaths: string[];
|
|
71
|
+
exitCode: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run `git merge-tree --write-tree` and parse the conflict info section.
|
|
76
|
+
*
|
|
77
|
+
* Output format (per git-merge-tree(1) --write-tree):
|
|
78
|
+
* <tree-oid>
|
|
79
|
+
* <Conflicted file info>*
|
|
80
|
+
* <blank line>
|
|
81
|
+
* <Informational messages>*
|
|
82
|
+
*
|
|
83
|
+
* Each conflicted file info line is `<mode> <object> <stage>\t<path>`.
|
|
84
|
+
*/
|
|
85
|
+
async function runMergeTree(
|
|
86
|
+
repoRoot: string,
|
|
87
|
+
base: string,
|
|
88
|
+
ours: string,
|
|
89
|
+
theirs: string,
|
|
90
|
+
): Promise<MergeTreeOutput> {
|
|
91
|
+
const { stdout, stderr, exitCode } = await runGit(repoRoot, [
|
|
92
|
+
"merge-tree",
|
|
93
|
+
"--write-tree",
|
|
94
|
+
`--merge-base=${base}`,
|
|
95
|
+
ours,
|
|
96
|
+
theirs,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
if (exitCode > 1) {
|
|
100
|
+
throw new MergeError(
|
|
101
|
+
`git merge-tree failed (exit ${exitCode}): ${stderr.trim() || "no error output"}`,
|
|
102
|
+
{ branchName: theirs },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lines = stdout.split("\n");
|
|
107
|
+
const treeOid = (lines[0] ?? "").trim();
|
|
108
|
+
if (treeOid.length === 0) {
|
|
109
|
+
throw new MergeError(`git merge-tree returned empty output for ${ours} vs ${theirs}`, {
|
|
110
|
+
branchName: theirs,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const conflictPaths = new Set<string>();
|
|
115
|
+
// Conflict info section lives between line 1 and the first blank line.
|
|
116
|
+
for (let i = 1; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
if (line === undefined || line === "") break;
|
|
119
|
+
const tabIdx = line.indexOf("\t");
|
|
120
|
+
if (tabIdx === -1) continue;
|
|
121
|
+
const path = line.substring(tabIdx + 1);
|
|
122
|
+
if (path.length > 0) conflictPaths.add(path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
treeOid,
|
|
127
|
+
conflictPaths: [...conflictPaths],
|
|
128
|
+
exitCode,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read a file's content from a tree OID. Returns "" when the file is absent. */
|
|
133
|
+
async function readFromTree(repoRoot: string, treeOid: string, path: string): Promise<string> {
|
|
134
|
+
const { stdout, exitCode } = await runGit(repoRoot, ["show", `${treeOid}:${path}`]);
|
|
135
|
+
if (exitCode !== 0) return "";
|
|
136
|
+
return stdout;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Predict how `ov merge` would resolve `entry.branchName` into `canonicalBranch`.
|
|
141
|
+
*
|
|
142
|
+
* Side-effect-free: runs `git merge-tree --write-tree` against committed refs.
|
|
143
|
+
* Does not touch HEAD, the working tree, or the merge lock.
|
|
144
|
+
*
|
|
145
|
+
* @param entry The merge entry under consideration. Only `branchName` is used here.
|
|
146
|
+
* @param canonicalBranch The target branch (e.g. "main").
|
|
147
|
+
* @param repoRoot Absolute path to the repo.
|
|
148
|
+
* @param mulchClient Optional. When provided, mulch history is consulted for
|
|
149
|
+
* skip-tier escalation: if `ai-resolve` has historical failures for any
|
|
150
|
+
* overlapping conflict file, the prediction bumps to `reimagine`.
|
|
151
|
+
*/
|
|
152
|
+
export async function predictConflicts(
|
|
153
|
+
entry: MergeEntry,
|
|
154
|
+
canonicalBranch: string,
|
|
155
|
+
repoRoot: string,
|
|
156
|
+
mulchClient?: MulchClient,
|
|
157
|
+
): Promise<ConflictPrediction> {
|
|
158
|
+
// Validate refs upfront so a missing branch produces a clear error
|
|
159
|
+
// instead of a confusing merge-base or merge-tree failure.
|
|
160
|
+
const oursOid = await resolveRef(repoRoot, canonicalBranch);
|
|
161
|
+
const theirsOid = await resolveRef(repoRoot, entry.branchName);
|
|
162
|
+
|
|
163
|
+
const baseOid = await findMergeBase(repoRoot, oursOid, theirsOid);
|
|
164
|
+
if (baseOid === null) {
|
|
165
|
+
throw new MergeError(`No common ancestor between ${canonicalBranch} and ${entry.branchName}`, {
|
|
166
|
+
branchName: entry.branchName,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Already-merged ancestor: branch tip is reachable from canonical.
|
|
171
|
+
// merge-tree would also report clean here, but short-circuiting avoids
|
|
172
|
+
// the extra spawn and produces a more informative reason string.
|
|
173
|
+
if (baseOid === theirsOid) {
|
|
174
|
+
return {
|
|
175
|
+
predictedTier: "clean-merge",
|
|
176
|
+
conflictFiles: [],
|
|
177
|
+
wouldRequireAgent: false,
|
|
178
|
+
reason: `${entry.branchName} is already an ancestor of ${canonicalBranch}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const mergeOutput = await runMergeTree(repoRoot, baseOid, oursOid, theirsOid);
|
|
183
|
+
|
|
184
|
+
if (mergeOutput.exitCode === 0 || mergeOutput.conflictPaths.length === 0) {
|
|
185
|
+
return {
|
|
186
|
+
predictedTier: "clean-merge",
|
|
187
|
+
conflictFiles: [],
|
|
188
|
+
wouldRequireAgent: false,
|
|
189
|
+
reason: "no conflicts: merge-tree reports clean merge",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const conflictFiles = mergeOutput.conflictPaths;
|
|
194
|
+
const blockingFiles: string[] = [];
|
|
195
|
+
|
|
196
|
+
for (const file of conflictFiles) {
|
|
197
|
+
// merge=union files are auto-resolvable by Tier 2's union driver even
|
|
198
|
+
// when merge-tree surfaces them as conflicts (the .gitattributes may
|
|
199
|
+
// only exist in the working tree; check-attr respects that).
|
|
200
|
+
if (await checkMergeUnion(repoRoot, file)) continue;
|
|
201
|
+
|
|
202
|
+
const merged = await readFromTree(repoRoot, mergeOutput.treeOid, file);
|
|
203
|
+
if (hasContentfulCanonical(merged)) {
|
|
204
|
+
blockingFiles.push(file);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (blockingFiles.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
predictedTier: "auto-resolve",
|
|
211
|
+
conflictFiles,
|
|
212
|
+
wouldRequireAgent: false,
|
|
213
|
+
reason: "all conflict files are merge=union or have whitespace-only canonical",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const baseReason =
|
|
218
|
+
blockingFiles.length === 1
|
|
219
|
+
? `${blockingFiles[0]} has contentful canonical`
|
|
220
|
+
: `${blockingFiles.length} files have contentful canonical (e.g. ${blockingFiles[0]})`;
|
|
221
|
+
|
|
222
|
+
let prediction: ConflictPrediction = {
|
|
223
|
+
predictedTier: "ai-resolve",
|
|
224
|
+
conflictFiles,
|
|
225
|
+
wouldRequireAgent: true,
|
|
226
|
+
reason: baseReason,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (mulchClient) {
|
|
230
|
+
try {
|
|
231
|
+
const searchOutput = await mulchClient.search("merge-conflict", { sortByScore: true });
|
|
232
|
+
const patterns = parseConflictPatterns(searchOutput);
|
|
233
|
+
const history = buildConflictHistory(patterns, conflictFiles);
|
|
234
|
+
if (history.skipTiers.includes("ai-resolve")) {
|
|
235
|
+
prediction = {
|
|
236
|
+
predictedTier: "reimagine",
|
|
237
|
+
conflictFiles,
|
|
238
|
+
wouldRequireAgent: true,
|
|
239
|
+
reason: `ai-resolve has historical failures for these files; ${baseReason}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Mulch failures must never block prediction — fall through with the
|
|
244
|
+
// base ai-resolve prediction.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return prediction;
|
|
249
|
+
}
|
package/src/merge/resolver.ts
CHANGED
|
@@ -207,7 +207,7 @@ export function hasContentfulCanonical(content: string): boolean {
|
|
|
207
207
|
* Check if a file has the `merge=union` gitattribute set.
|
|
208
208
|
* Returns true if `git check-attr merge -- <file>` ends with ": merge: union".
|
|
209
209
|
*/
|
|
210
|
-
async function checkMergeUnion(repoRoot: string, filePath: string): Promise<boolean> {
|
|
210
|
+
export async function checkMergeUnion(repoRoot: string, filePath: string): Promise<boolean> {
|
|
211
211
|
const { stdout, exitCode } = await runGit(repoRoot, ["check-attr", "merge", "--", filePath]);
|
|
212
212
|
if (exitCode !== 0) return false;
|
|
213
213
|
return stdout.trim().endsWith(": merge: union");
|
package/src/mulch/client.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface MulchClient {
|
|
|
60
60
|
classification?: string;
|
|
61
61
|
stdin?: boolean;
|
|
62
62
|
evidenceBead?: string;
|
|
63
|
-
outcomeStatus?: "success" | "failure";
|
|
63
|
+
outcomeStatus?: "success" | "failure" | "partial";
|
|
64
64
|
outcomeDuration?: number;
|
|
65
65
|
outcomeTestResults?: string;
|
|
66
66
|
outcomeAgent?: string;
|
|
@@ -306,7 +306,7 @@ function buildExpertiseRecord(options: {
|
|
|
306
306
|
tags?: string[];
|
|
307
307
|
classification?: string;
|
|
308
308
|
evidenceBead?: string;
|
|
309
|
-
outcomeStatus?: "success" | "failure";
|
|
309
|
+
outcomeStatus?: "success" | "failure" | "partial";
|
|
310
310
|
outcomeDuration?: number;
|
|
311
311
|
outcomeTestResults?: string;
|
|
312
312
|
outcomeAgent?: string;
|
|
@@ -322,7 +322,7 @@ function buildExpertiseRecord(options: {
|
|
|
322
322
|
outcomes: options.outcomeStatus
|
|
323
323
|
? [
|
|
324
324
|
{
|
|
325
|
-
status: options.outcomeStatus
|
|
325
|
+
status: options.outcomeStatus,
|
|
326
326
|
duration: options.outcomeDuration,
|
|
327
327
|
test_results: options.outcomeTestResults,
|
|
328
328
|
agent: options.outcomeAgent,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Fixture: emits a known sequence of Claude stream-json lines to stdout then exits.
|
|
2
|
+
// Used by the ClaudeRuntime.parseEvents() integration test.
|
|
3
|
+
const lines = [
|
|
4
|
+
JSON.stringify({ type: "system", subtype: "init", session_id: "sess-123" }),
|
|
5
|
+
JSON.stringify({
|
|
6
|
+
type: "assistant",
|
|
7
|
+
message: {
|
|
8
|
+
model: "claude-sonnet-4-6",
|
|
9
|
+
content: [{ type: "text", text: "hello" }],
|
|
10
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
11
|
+
},
|
|
12
|
+
}),
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
type: "result",
|
|
15
|
+
session_id: "sess-123",
|
|
16
|
+
result: "done",
|
|
17
|
+
is_error: false,
|
|
18
|
+
duration_ms: 1234,
|
|
19
|
+
num_turns: 1,
|
|
20
|
+
}),
|
|
21
|
+
];
|
|
22
|
+
for (const l of lines) process.stdout.write(`${l}\n`);
|