omp-conductor 0.3.22 → 0.3.24
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 +89 -30
- package/package.json +1 -1
- package/src/approval-surface.ts +218 -0
- package/src/board.ts +292 -55
- package/src/briefs/orchestrator.md +17 -2
- package/src/daemon.ts +122 -15
- package/src/fleet.ts +17 -1
- package/src/orchestrator-tick.ts +130 -15
- package/src/tracker/github.ts +19 -6
- package/src/types.ts +25 -3
- package/src/unblock.ts +55 -17
package/src/unblock.ts
CHANGED
|
@@ -43,19 +43,38 @@ export interface UnblockOutcome {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Drop
|
|
46
|
+
* Drop the state labels the issue could be carrying, as far as the store can
|
|
47
|
+
* prove they are droppable.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
* which one is set and this process cannot read that
|
|
50
|
-
* port — inferring it from the newest run row would be
|
|
51
|
-
* exactly when a human has relabelled something by
|
|
52
|
-
* issue does not carry is a no-op: `gh issue edit
|
|
53
|
-
* absent label (verified against gh 2.97.0), and
|
|
54
|
-
* older paths return for one.
|
|
49
|
+
* The two terminal labels come off unconditionally, because the tracker is the
|
|
50
|
+
* only source of truth for which one is set and this process cannot read that
|
|
51
|
+
* back through the Tracker port — inferring it from the newest run row would be
|
|
52
|
+
* a guess that goes wrong exactly when a human has relabelled something by
|
|
53
|
+
* hand. Removing a label an issue does not carry is a no-op: `gh issue edit
|
|
54
|
+
* --remove-label` exits 0 on an absent label (verified against gh 2.97.0), and
|
|
55
|
+
* the adapter swallows the 404 older paths return for one.
|
|
55
56
|
*
|
|
56
|
-
* `agent:in-progress`
|
|
57
|
-
*
|
|
58
|
-
* from under a live run is how two workers end up on
|
|
57
|
+
* `agent:in-progress` comes off only when the newest run row is terminal, and
|
|
58
|
+
* that condition is the whole safety argument. The label means a worker process
|
|
59
|
+
* exists, and clearing it from under a live run is how two workers end up on
|
|
60
|
+
* one issue, so it used to be excluded outright. What that cost showed up on
|
|
61
|
+
* 2026-08-09: veltro#331's newest row had already settled to `failed` (its PR
|
|
62
|
+
* #332 was closed unmerged), `unblock 331` reported `cleared agent:blocked,
|
|
63
|
+
* agent:failed` and `next tick eligible again`, and an immediate authoritative
|
|
64
|
+
* `gh issue view 331` still showed `agent:in-progress` — the one label that
|
|
65
|
+
* actually decides eligibility, with no verb anywhere able to remove it (#18).
|
|
66
|
+
* A terminal newest row is proof the process is gone rather than an opinion
|
|
67
|
+
* about it: it is written when the worker exits, by `reconcileOrphanedRuns` at
|
|
68
|
+
* startup for a worker that died with its daemon, or by settlement once the PR
|
|
69
|
+
* resolved. Once it is there, the interlock has nothing left to guard.
|
|
70
|
+
*
|
|
71
|
+
* No run row at all is deliberately *not* that proof, so that case still leaves
|
|
72
|
+
* the label alone. A missing row means the store never saw the run, which is
|
|
73
|
+
* indistinguishable from here to a claim that raced a store write — and being
|
|
74
|
+
* wrong in that direction is the two-workers bug, where being wrong in the
|
|
75
|
+
* other direction is a stuck issue an operator is already looking at. On the
|
|
76
|
+
* paths the daemon does own it releases the label itself, next to the store
|
|
77
|
+
* transition that proves the run ended (`releaseInProgress` in `daemon.ts`).
|
|
59
78
|
*/
|
|
60
79
|
export async function unblockIssue(
|
|
61
80
|
project: ProjectConfig,
|
|
@@ -63,13 +82,21 @@ export async function unblockIssue(
|
|
|
63
82
|
store: Store,
|
|
64
83
|
issue: number,
|
|
65
84
|
): Promise<UnblockOutcome> {
|
|
85
|
+
// Read before any label is touched: terminality is the whole of the argument
|
|
86
|
+
// for clearing in-progress, so the row that carries it decides the set.
|
|
87
|
+
const latest = store.latestRun(project.name, issue);
|
|
88
|
+
const terminal = latest !== undefined && !LIVE_STATES.includes(latest.state);
|
|
89
|
+
|
|
66
90
|
const cleared: string[] = [];
|
|
67
|
-
for (const label of new Set([
|
|
91
|
+
for (const label of new Set([
|
|
92
|
+
project.stateLabels.blocked,
|
|
93
|
+
project.stateLabels.failed,
|
|
94
|
+
...(terminal ? [project.stateLabels.inProgress] : []),
|
|
95
|
+
])) {
|
|
68
96
|
await tracker.removeLabel(issue, label);
|
|
69
97
|
cleared.push(label);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
|
-
const latest = store.latestRun(project.name, issue);
|
|
73
100
|
return {
|
|
74
101
|
cleared,
|
|
75
102
|
attemptsUsed: store.attemptsFor(project.name, issue),
|
|
@@ -81,9 +108,14 @@ export async function unblockIssue(
|
|
|
81
108
|
|
|
82
109
|
/**
|
|
83
110
|
* What the operator reads back. It promises a re-claim only when one can
|
|
84
|
-
* actually happen
|
|
85
|
-
*
|
|
86
|
-
*
|
|
111
|
+
* actually happen, because a promise made blindly sends someone away believing
|
|
112
|
+
* work had resumed and they find out by waiting for it. Three things withhold
|
|
113
|
+
* it: a live run still owns the issue through `agent:in-progress`, a spent
|
|
114
|
+
* attempt budget makes the next tick escalate rather than dispatch, and — since
|
|
115
|
+
* the in-progress label is only released for a terminal run — an issue with no
|
|
116
|
+
* run row at all, where that label may still be sitting there unread. #18 was
|
|
117
|
+
* filed against this function saying `next tick eligible again` in a case where
|
|
118
|
+
* it was not, so the wording is a contract rather than prose.
|
|
87
119
|
*/
|
|
88
120
|
export function formatUnblock(
|
|
89
121
|
issue: number,
|
|
@@ -95,7 +127,7 @@ export function formatUnblock(
|
|
|
95
127
|
const lines = [`#${issue}: cleared ${o.cleared.join(", ")}`];
|
|
96
128
|
|
|
97
129
|
if (latest === undefined) {
|
|
98
|
-
lines.push(" runs none recorded — the labels were cleared anyway; eligibility is read off the tracker");
|
|
130
|
+
lines.push(" runs none recorded — the terminal labels were cleared anyway; eligibility is read off the tracker");
|
|
99
131
|
} else {
|
|
100
132
|
lines.push(` runs ${o.attemptsUsed}, newest ${latest.state}`);
|
|
101
133
|
lines.push(` failures ${o.failuresUsed} of ${caps.maxAttemptsPerIssue}`);
|
|
@@ -117,6 +149,12 @@ export function formatUnblock(
|
|
|
117
149
|
` next tick not eligible: the ${caps.maxContinuationsPerIssue}-continuation budget was exceeded. ` +
|
|
118
150
|
"Inspect progress or raise maxContinuationsPerIssue.",
|
|
119
151
|
);
|
|
152
|
+
} else if (latest === undefined) {
|
|
153
|
+
lines.push(
|
|
154
|
+
` next tick eligible once the issue carries "${project.queueLabel}" and no state label — with no ` +
|
|
155
|
+
`run row to prove the worker is gone, "${project.stateLabels.inProgress}" was left in place, and ` +
|
|
156
|
+
"on its own it keeps the issue ineligible",
|
|
157
|
+
);
|
|
120
158
|
} else {
|
|
121
159
|
lines.push(` next tick eligible again, as long as the issue still carries "${project.queueLabel}"`);
|
|
122
160
|
}
|