omp-conductor 0.16.0 → 0.16.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 +25 -0
- package/REFERENCE.md +3 -0
- package/package.json +2 -1
- package/schema/config.schema.json +1 -0
- package/src/admission.ts +111 -7
- package/src/briefs/orchestrator.md +21 -6
- package/src/briefs/policy.md +13 -5
- package/src/cli.ts +110 -381
- package/src/command-help.ts +220 -0
- package/src/command-manifest.ts +471 -0
- package/src/commands/complete.ts +93 -0
- package/src/commands/context.ts +1 -0
- package/src/commands/setup.ts +68 -2
- package/src/commands/tail.ts +204 -44
- package/src/config-schema.ts +4 -0
- package/src/config.ts +31 -1
- package/src/daemon.ts +16 -0
- package/src/depends-on.ts +77 -28
- package/src/doctor.ts +120 -1
- package/src/escalate.ts +77 -4
- package/src/fleet.ts +127 -42
- package/src/setup-host.ts +29 -11
- package/src/setup-wizard.ts +10 -9
- package/src/transcript.ts +1 -1
- package/src/upgrade-verify.ts +25 -3
- package/src/upgrade.ts +61 -2
- package/src/worker.ts +196 -0
- package/src/worktree.ts +13 -1
- package/systemd/omp-conductor-recover.sh +1 -1
- package/systemd/recover-unit-test.sh +2 -2
package/README.md
CHANGED
|
@@ -114,6 +114,31 @@ For what the host needs before anything runs — `bun`, an authenticated `gh`,
|
|
|
114
114
|
`omp-telegram` for the escalation channel — see the
|
|
115
115
|
[Install prerequisites](https://github.com/TerrifiedBug/conductor/blob/main/omp/REFERENCE.md#install-prerequisites) in the reference.
|
|
116
116
|
|
|
117
|
+
### Shell completions
|
|
118
|
+
|
|
119
|
+
`omp-conductor complete` generates completions for zsh, bash, fish, and
|
|
120
|
+
PowerShell. Load them for the current shell:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
source <(omp-conductor complete zsh)
|
|
124
|
+
# or
|
|
125
|
+
source <(omp-conductor complete bash)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For a persistent zsh install:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
mkdir -p ~/.omp/conductor
|
|
132
|
+
omp-conductor complete zsh > ~/.omp/conductor/completions.zsh
|
|
133
|
+
echo '[ -f ~/.omp/conductor/completions.zsh ] && source ~/.omp/conductor/completions.zsh' >> ~/.zshrc
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use `bash` and `~/.bashrc` for the equivalent bash install. In fish, run
|
|
137
|
+
`omp-conductor complete fish | source`; in PowerShell, run
|
|
138
|
+
`omp-conductor complete powershell | Out-String | Invoke-Expression`.
|
|
139
|
+
After a successful interactive setup, the wizard offers to install zsh or bash
|
|
140
|
+
completions this way and adds the rc source line only when it is absent.
|
|
141
|
+
|
|
117
142
|
## Quick start
|
|
118
143
|
|
|
119
144
|
1. Install `omp-conductor` and `omp-telegram`. Pair the Telegram bot and enable its bridge.
|
package/REFERENCE.md
CHANGED
|
@@ -2240,6 +2240,8 @@ omp-conductor watch list [--project NAME]
|
|
|
2240
2240
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
2241
2241
|
omp-conductor resume [--project NAME]
|
|
2242
2242
|
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
2243
|
+
omp-conductor complete <zsh|bash|fish|powershell>
|
|
2244
|
+
omp-conductor complete -- <args...>
|
|
2243
2245
|
omp-conductor help
|
|
2244
2246
|
```
|
|
2245
2247
|
|
|
@@ -2285,6 +2287,7 @@ omp-conductor help
|
|
|
2285
2287
|
| `--retrofit` | — | Only for `brief-upgrade`. Propose (or with `--apply`, write) a `YOURS TO EDIT` banner before the first owned-topic heading on a hand-written brief. |
|
|
2286
2288
|
| `--apply` | — | Only for `brief-upgrade`. Confirms `--migrate` / `--retrofit`. On its own it exits `2`: the legacy single-file merge was removed in 0.4.3. |
|
|
2287
2289
|
| `--file PATH` | — | Only for `brief-upgrade`. Check a brief that is not where the wizard would have put it, on a host that may have no config at all. |
|
|
2290
|
+
| `complete <zsh\|bash\|fish\|powershell>` | none | Print a shell completion script. Source it directly for a session or save it and source the file from the shell rc. Interactive setup offers the persistent zsh/bash install; fish and PowerShell remain available manually. `complete -- <args...>` is the fast shell callback protocol: static verbs, subcommands, flags and setup areas come from the command manifest, while project names load from config and fail empty on any read error. |
|
|
2288
2291
|
| `help`, `--help`, `-h` | none | Print usage. An unknown or missing verb prints it too, and exits `2`. |
|
|
2289
2292
|
|
|
2290
2293
|
Pause is a sentinel under the state directory and survives a daemon restart.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"schema": "bun run src/generate-schema.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@bomb.sh/tab": "0.0.22",
|
|
36
37
|
"yaml": "^2.9.0",
|
|
37
38
|
"zod": "^4"
|
|
38
39
|
},
|
package/src/admission.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
} from "./types.ts";
|
|
30
30
|
import { readPlanUsage, type PlanUsageStatus, type UsageSource } from "./usage.ts";
|
|
31
31
|
import type { CriticalBaseProbe, CriticalBaseVerdict, RunLaneProbe } from "./gitops.ts";
|
|
32
|
+
import { repoSlugFor } from "./gitops.ts";
|
|
32
33
|
import { branchName, type Routed } from "./routing.ts";
|
|
33
34
|
import { parseDependsOn } from "./depends-on.ts";
|
|
34
35
|
|
|
@@ -51,6 +52,17 @@ export interface AdmissionDeps {
|
|
|
51
52
|
escalate(e: Escalation): Promise<void>;
|
|
52
53
|
probeCriticalBase?: CriticalBaseProbe;
|
|
53
54
|
probeWorktreeLane?: RunLaneProbe;
|
|
55
|
+
/**
|
|
56
|
+
* Reads one issue's tracker state in a repository the admission tracker is
|
|
57
|
+
* NOT bound to — the cross-repo half of the Depends-on interlock (#420).
|
|
58
|
+
* The tracker bound to `project.tracker.repo` can only answer same-repo
|
|
59
|
+
* references; a `owner/repo#n` prerequisite naming a different routed repo
|
|
60
|
+
* goes through this. Undefined means "could not tell" (same posture as
|
|
61
|
+
* {@link Tracker.issueSnapshot}); a routed prerequisite it cannot answer
|
|
62
|
+
* fails that candidate closed. Unset, admission fails a routed cross-repo
|
|
63
|
+
* prerequisite closed too — the daemon always wires it.
|
|
64
|
+
*/
|
|
65
|
+
probeIssueIn?: (repo: string, issue: number) => Promise<IssueSnapshot | undefined>;
|
|
54
66
|
}
|
|
55
67
|
/** `stops` are the operational ends that each require one resume. */
|
|
56
68
|
export function hasContinuationBudget(stops: number, maxContinuations: number): boolean {
|
|
@@ -190,6 +202,22 @@ function isPathLike(token: string): boolean {
|
|
|
190
202
|
return token !== "" && !/\s/.test(token) && (token.includes("/") || /\.[A-Za-z0-9]{1,10}$/.test(token));
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Where a `owner/repo` cross-repo reference can be read. Only repositories
|
|
207
|
+
* routed in THIS fleet are resolvable (#420): the issue source repo
|
|
208
|
+
* (`project.tracker.repo`) is answered by the existing tracker, and any routed
|
|
209
|
+
* work repo by {@link AdmissionDeps.probeIssueIn}. Anything else is unresolved
|
|
210
|
+
* — the caller fails closed rather than guess which repo was meant, and never
|
|
211
|
+
* silently resolves the reference against the candidate's own repo.
|
|
212
|
+
*/
|
|
213
|
+
function crossRepoTarget(project: ProjectConfig, ownerRepo: string): "tracker" | "routed" | undefined {
|
|
214
|
+
if (ownerRepo === project.tracker.repo) return "tracker";
|
|
215
|
+
for (const routed of Object.values(project.routing.repos)) {
|
|
216
|
+
if (repoSlugFor(routed) === ownerRepo) return "routed";
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
/**
|
|
194
222
|
* Which routed candidates get a worker this tick — in queue order, never more
|
|
195
223
|
* than `slots` of them. Every non-admission receives a stable reason code.
|
|
@@ -623,13 +651,14 @@ export async function admitCandidates(
|
|
|
623
651
|
continue;
|
|
624
652
|
}
|
|
625
653
|
|
|
626
|
-
// The Depends-on interlock (#419): a candidate declares the
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
// references
|
|
654
|
+
// The Depends-on interlock (#419/#420): a candidate declares the issues it
|
|
655
|
+
// must not be dispatched before — same-repo (`#123`) or cross-repo
|
|
656
|
+
// (`owner/repo#123`) — and any *open* prerequisite holds it until every
|
|
657
|
+
// referenced issue is closed. Prerequisite state is read fresh from the
|
|
658
|
+
// tracker at claim time, every pass — never cached across ticks — so a
|
|
659
|
+
// prerequisite that reopens re-holds on the next tick. Cross-repo
|
|
660
|
+
// references resolve only against repositories routed in this fleet;
|
|
661
|
+
// graph cycles are a later slice (#421).
|
|
633
662
|
const dependsOn = parseDependsOn(r.issue.body);
|
|
634
663
|
if (dependsOn.malformed.length > 0) {
|
|
635
664
|
// A marker line with no strict `#<n>` reference (e.g. `Depends-on:
|
|
@@ -686,6 +715,81 @@ export async function admitCandidates(
|
|
|
686
715
|
continue;
|
|
687
716
|
}
|
|
688
717
|
}
|
|
718
|
+
if (dependsOn.crossRefs.length > 0) {
|
|
719
|
+
// The cross-repo half (#420): each `owner/repo#n` is read from the repo
|
|
720
|
+
// it names. The fully qualified form travels in the `detail` string, so
|
|
721
|
+
// status/digest render it as `blocked by owner/repo#123` while same-repo
|
|
722
|
+
// refs stay compact `blocked by #5`.
|
|
723
|
+
let blocking: string | undefined;
|
|
724
|
+
let unreadable: string | undefined;
|
|
725
|
+
let unresolved: string | undefined;
|
|
726
|
+
for (const ref of dependsOn.crossRefs) {
|
|
727
|
+
const where = crossRepoTarget(project, ref.repo);
|
|
728
|
+
const full = `${ref.repo}#${ref.issue}`;
|
|
729
|
+
if (where === undefined) {
|
|
730
|
+
unresolved = full;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
let state: IssueSnapshot | undefined;
|
|
734
|
+
try {
|
|
735
|
+
if (where === "tracker") {
|
|
736
|
+
// The qualified form of the candidate's own tracker repo: the
|
|
737
|
+
// same state read the same-repo interlock uses.
|
|
738
|
+
state = await tracker.issueSnapshot(ref.issue);
|
|
739
|
+
} else if (d.probeIssueIn === undefined) {
|
|
740
|
+
// No resolver wired — fail closed, never guess the issue's state.
|
|
741
|
+
state = undefined;
|
|
742
|
+
} else {
|
|
743
|
+
state = await d.probeIssueIn(ref.repo, ref.issue);
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
state = undefined;
|
|
747
|
+
}
|
|
748
|
+
if (state?.state === "open") {
|
|
749
|
+
blocking = full;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (state === undefined) {
|
|
753
|
+
unreadable = full;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Same fail-closed posture as same-repo refs: an open prerequisite
|
|
758
|
+
// holds, an unreadable one holds (the next tick rereads it fresh), and
|
|
759
|
+
// an unresolved repo holds because the dispatcher cannot verify the
|
|
760
|
+
// dependency — it is never silently treated as closed. Only the
|
|
761
|
+
// unresolved repo also surfaces one material event for grooming, and it
|
|
762
|
+
// follows the malformed-declaration escalation's stable-summary
|
|
763
|
+
// dedupe, so repeated ticks page once, not per tick.
|
|
764
|
+
if (blocking !== undefined) {
|
|
765
|
+
hold(issue, "depends-on", `blocked by ${blocking}`);
|
|
766
|
+
log(`#${issue} held (depends-on): cross-repo prerequisite ${blocking} is open`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (unreadable !== undefined) {
|
|
770
|
+
hold(issue, "depends-on", `prerequisite ${unreadable} state unreadable`);
|
|
771
|
+
log(`#${issue} held (depends-on): cross-repo prerequisite ${unreadable} state unreadable`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (unresolved !== undefined) {
|
|
775
|
+
hold(issue, "depends-on", `blocked by ${unresolved} — repo not routed in this fleet`);
|
|
776
|
+
log(`#${issue} held (depends-on): cross-repo prerequisite ${unresolved} is not a routed repo`);
|
|
777
|
+
await safeEscalate(d, {
|
|
778
|
+
tier: 1,
|
|
779
|
+
project: project.name,
|
|
780
|
+
issue,
|
|
781
|
+
summary: `#${issue} depends on ${unresolved}, a repo not routed in this fleet — held until groomed`,
|
|
782
|
+
detail: [
|
|
783
|
+
r.issue.title,
|
|
784
|
+
r.issue.url,
|
|
785
|
+
`Unroutable prerequisite: ${unresolved}`,
|
|
786
|
+
"Depends-on cross-repo references must name a repository routed in this project",
|
|
787
|
+
"(`routing.repos` or the tracker repo). Groom the body or route the repo.",
|
|
788
|
+
].join("\n"),
|
|
789
|
+
});
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
689
793
|
|
|
690
794
|
// The file-lane interlock (#555): a candidate whose declared lane overlaps
|
|
691
795
|
// a live run's *actual* lane is held until that run's work has merged, so
|
|
@@ -381,12 +381,27 @@ did not arrive.
|
|
|
381
381
|
|
|
382
382
|
## Human messages
|
|
383
383
|
|
|
384
|
-
A human writing to you between ticks is not a tick
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
A human writing to you between ticks is not a tick, and how a reply reaches
|
|
385
|
+
them depends on which of three shapes this turn is — a shape the brief states,
|
|
386
|
+
never something a session infers from a tool's error:
|
|
387
|
+
|
|
388
|
+
- **A locally injected tick** has no inbound message and no topic to keep:
|
|
389
|
+
hand anything reportable to the outbox with `omp-conductor report`, and reach
|
|
390
|
+
the operator directly with `omp-conductor message --text "<the message>"`,
|
|
391
|
+
declaring the escalation category when you are asking. Your tick prompt names
|
|
392
|
+
this shape — it carries the delivery rule that says when a tick was injected
|
|
393
|
+
locally.
|
|
394
|
+
- **A turn that began as an inbound Telegram message** is answered with a
|
|
395
|
+
**single `telegram_send` call** — one message, the answer only, from evidence
|
|
396
|
+
you already hold or go and fetch — omitting **both** `chat_id` and `thread_id`
|
|
397
|
+
so the reply keeps the topic the message came from.
|
|
398
|
+
- **An interactive terminal session**, started by hand, is one the operator is
|
|
399
|
+
reading live: there, end-of-turn text **is** the delivery, and Telegram is
|
|
400
|
+
used only when it must demonstrably arrive — `telegram_send` resolves
|
|
401
|
+
`chat_id` from the last inbound message and refuses without one.
|
|
402
|
+
|
|
403
|
+
While handling any turn, produce no visible commentary between tool calls —
|
|
404
|
+
reasoning stays in thinking, actions stay in tools.
|
|
390
405
|
|
|
391
406
|
**Reply where the message arrived.** `thread_id` defaults to the active topic
|
|
392
407
|
*only while `chat_id` is omitted*, so a fleet whose Telegram is a forum topic
|
package/src/briefs/policy.md
CHANGED
|
@@ -85,11 +85,19 @@ authoritative. All four scopes, spelled out:
|
|
|
85
85
|
interrupt when operator availability permits; everything else waits for one
|
|
86
86
|
daily rollup.
|
|
87
87
|
|
|
88
|
-
**Delivery.**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
**Delivery.** A reply reaches the operator by the shape of the turn it answers.
|
|
89
|
+
A **locally injected tick** delivers through the outbox — `omp-conductor report`
|
|
90
|
+
for anything reportable, `omp-conductor message` to reach the operator directly —
|
|
91
|
+
because a report written as end-of-turn text on a tick reaches nobody. A turn
|
|
92
|
+
that **began as an inbound Telegram message** answers with `telegram_send`,
|
|
93
|
+
omitting `chat_id` and `thread_id` so the reply keeps its topic. An **interactive
|
|
94
|
+
terminal session** is read live by the operator, so end-of-turn text *is* the
|
|
95
|
+
delivery there; `telegram_send` resolves `chat_id` from the last inbound message
|
|
96
|
+
and refuses without one, so it is used only when the message must demonstrably
|
|
97
|
+
arrive. The provable delivery paths are `omp-conductor report` (persisted and
|
|
98
|
+
retried by the daemon) and `telegram_send` (direct messages); `telegram_ask` is
|
|
99
|
+
the decision primitive, not delivery evidence. Hand every reportable event to
|
|
100
|
+
the conductor's outbox:
|
|
93
101
|
|
|
94
102
|
```
|
|
95
103
|
omp-conductor report --text "<the whole report>" # a material event
|