omp-conductor 0.2.2 → 0.3.2
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 +212 -35
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +33 -22
- package/src/briefs/orchestrator.md +46 -14
- package/src/briefs/worker.md +9 -2
- package/src/cli.ts +291 -13
- package/src/config.ts +127 -16
- package/src/daemon.ts +474 -50
- package/src/lifecycle.ts +6 -4
- package/src/orchestrator-tick.ts +209 -45
- package/src/plugin.ts +60 -0
- package/src/setup.ts +98 -4
- package/src/store.ts +20 -1
- package/src/tracker/github.ts +94 -2
- package/src/types.ts +56 -3
- package/src/unblock.ts +113 -0
- package/src/worker.ts +14 -0
- package/src/worktree.ts +119 -0
package/src/worktree.ts
CHANGED
|
@@ -268,6 +268,125 @@ export async function addWorktree(
|
|
|
268
268
|
return worktreePath;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* What one salvage attempt did. A failure is a *value* rather than a throw
|
|
273
|
+
* because the only caller is a run that is already ending badly: it has to log
|
|
274
|
+
* the outcome, word it into the escalation and still close its run record, and
|
|
275
|
+
* none of that may be skipped by an exception from the last-ditch step.
|
|
276
|
+
*/
|
|
277
|
+
export type SalvageOutcome =
|
|
278
|
+
| { kind: "salvaged"; sha: string; branch: string; pushed: boolean; pushError?: string }
|
|
279
|
+
/** Nothing uncommitted was there to save — a clean tree, or no tree at all. */
|
|
280
|
+
| { kind: "nothing" }
|
|
281
|
+
/** There was work and git would not commit it. This is the loud one. */
|
|
282
|
+
| { kind: "failed"; error: string };
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The salvage commit is the daemon's own, made unattended in a worktree whose
|
|
286
|
+
* config it does not control. A machine with no global git identity, a global
|
|
287
|
+
* `commit.gpgsign=true` whose key needs a passphrase nobody can type, or a repo
|
|
288
|
+
* pre-commit hook that rejects half-finished code are all ordinary states — and
|
|
289
|
+
* every one of them would turn "save the work" into "lose the work". So the
|
|
290
|
+
* commit brings its own identity, signs nothing, and skips hooks: it is a
|
|
291
|
+
* snapshot for a human to sort out, never something anyone merges.
|
|
292
|
+
*/
|
|
293
|
+
const SALVAGE_COMMIT_CONFIG = [
|
|
294
|
+
"-c",
|
|
295
|
+
"user.name=conductor",
|
|
296
|
+
"-c",
|
|
297
|
+
"user.email=conductor@invalid",
|
|
298
|
+
"-c",
|
|
299
|
+
"commit.gpgsign=false",
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Commits a dead run's uncommitted work to the run's own branch and pushes it,
|
|
304
|
+
* so that the tree the next attempt destroys is no longer the only copy.
|
|
305
|
+
*
|
|
306
|
+
* This closes a deliberate asymmetry. `addWorktree` preserves the run branch
|
|
307
|
+
* precisely because "that branch can hold the only copy of work attempt 1
|
|
308
|
+
* committed but never pushed", while `removeWorktree` runs `worktree remove
|
|
309
|
+
* --force` and `addWorktree` refuses to reuse a tree that "may hold a previous
|
|
310
|
+
* attempt's uncommitted work" — committed work is kept by design, uncommitted
|
|
311
|
+
* work is discarded by design. That trade is fair for a worker that *stops*:
|
|
312
|
+
* blocking is a decision it makes with turns left to commit first. It is not
|
|
313
|
+
* fair for one killed by the turns cap or the wall clock, or one that crashes:
|
|
314
|
+
* that end is external, unannounced, mid-sentence, and it lands hardest on the
|
|
315
|
+
* long refactors carrying the most unsaved work. So: non-graceful ends only.
|
|
316
|
+
*
|
|
317
|
+
* Never throws. Every outcome, including its own failure, comes back as a value
|
|
318
|
+
* for the caller to log and to put in front of a human.
|
|
319
|
+
*
|
|
320
|
+
* The push is best-effort and deliberately last, after the sha exists: a
|
|
321
|
+
* refused push (diverged branch, no credentials, no network) still leaves the
|
|
322
|
+
* commit in this host's mirror, which is strictly better than nothing. It is a
|
|
323
|
+
* plain fast-forward push — never a force — and if the run already had a PR
|
|
324
|
+
* open, that PR gains the WIP commit and re-runs its checks. That is the price
|
|
325
|
+
* of work outliving its host, and only a run that already failed ever pays it.
|
|
326
|
+
*/
|
|
327
|
+
export async function salvageWip(
|
|
328
|
+
worktree: string,
|
|
329
|
+
issue: number,
|
|
330
|
+
attempt: number,
|
|
331
|
+
reason: string,
|
|
332
|
+
): Promise<SalvageOutcome> {
|
|
333
|
+
try {
|
|
334
|
+
// A tree that is not there cannot be holding work. Checked before spawning
|
|
335
|
+
// git, because a missing cwd fails at spawn time rather than as an exit
|
|
336
|
+
// code, and "no tree" is not a salvage failure worth alarming anyone with.
|
|
337
|
+
if (!existsSync(worktree)) return { kind: "nothing" };
|
|
338
|
+
|
|
339
|
+
if ((await git(["status", "--porcelain"], worktree)) === "") return { kind: "nothing" };
|
|
340
|
+
|
|
341
|
+
// The tree's own branch, not one the caller believes it should be on: this
|
|
342
|
+
// string ends up in an escalation as the place to go looking.
|
|
343
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktree);
|
|
344
|
+
|
|
345
|
+
// `-A` on purpose: the losses this exists for were mostly *new* files.
|
|
346
|
+
await git(["add", "-A"], worktree);
|
|
347
|
+
await git(
|
|
348
|
+
[
|
|
349
|
+
...SALVAGE_COMMIT_CONFIG,
|
|
350
|
+
"commit",
|
|
351
|
+
"--no-verify",
|
|
352
|
+
"-m",
|
|
353
|
+
`wip(#${issue}): attempt ${attempt} killed by ${reason} — auto-salvaged`,
|
|
354
|
+
],
|
|
355
|
+
worktree,
|
|
356
|
+
);
|
|
357
|
+
const sha = await git(["rev-parse", "HEAD"], worktree);
|
|
358
|
+
|
|
359
|
+
if (branch === "HEAD") {
|
|
360
|
+
// Detached: the commit is real but reachable only by sha, and pushing
|
|
361
|
+
// `HEAD` from here would publish a branch literally named HEAD.
|
|
362
|
+
return {
|
|
363
|
+
kind: "salvaged",
|
|
364
|
+
sha,
|
|
365
|
+
branch,
|
|
366
|
+
pushed: false,
|
|
367
|
+
pushError: "detached HEAD — no branch to push",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const push = await runGit(["push", "origin", `HEAD:refs/heads/${branch}`], worktree);
|
|
372
|
+
if (push.code === 0) return { kind: "salvaged", sha, branch, pushed: true };
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
kind: "salvaged",
|
|
376
|
+
sha,
|
|
377
|
+
branch,
|
|
378
|
+
pushed: false,
|
|
379
|
+
pushError: (
|
|
380
|
+
push.stderr.trim() ||
|
|
381
|
+
push.stdout.trim() ||
|
|
382
|
+
`git push exited ${push.code}`
|
|
383
|
+
).replace(URL_USERINFO, "$1***@"),
|
|
384
|
+
};
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
271
390
|
/**
|
|
272
391
|
* Removes a run's worktree and its registration in the mirror. Idempotent: a
|
|
273
392
|
* path that is already gone resolves, so cleanup can be retried and can run on
|