omp-conductor 0.19.6 → 0.19.7
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/REFERENCE.md +17 -1
- package/package.json +1 -1
- package/src/daemon.ts +123 -32
- package/src/doctor.ts +17 -12
- package/src/escalate.ts +39 -21
- package/src/fleet.ts +928 -140
- package/src/store.ts +42 -0
- package/src/types.ts +19 -0
- package/src/verbs/server.ts +92 -11
package/src/store.ts
CHANGED
|
@@ -1356,6 +1356,20 @@ CREATE TABLE IF NOT EXISTS orchestrator_incidents (
|
|
|
1356
1356
|
diverted INTEGER NOT NULL DEFAULT 0
|
|
1357
1357
|
);
|
|
1358
1358
|
|
|
1359
|
+
-- Conductor-owned Herdr worker workspaces (#1035 review). Herdr restores a
|
|
1360
|
+
-- session's workspaces and panes across a server restart but drops
|
|
1361
|
+
-- report-metadata tokens, so the live-only token cannot remain the only
|
|
1362
|
+
-- ownership authority: the restored surface would be invisible to discovery,
|
|
1363
|
+
-- a duplicate would be created beside it, and its stale panes could never be
|
|
1364
|
+
-- reconciled or removed. One row per workspace conductor created and still
|
|
1365
|
+
-- owns; forgotten when the workspace is removed or fails to create cleanly.
|
|
1366
|
+
CREATE TABLE IF NOT EXISTS herdr_worker_workspaces (
|
|
1367
|
+
project TEXT NOT NULL,
|
|
1368
|
+
workspace_id TEXT NOT NULL,
|
|
1369
|
+
created_at INTEGER NOT NULL,
|
|
1370
|
+
PRIMARY KEY (project, workspace_id)
|
|
1371
|
+
);
|
|
1372
|
+
|
|
1359
1373
|
-- Every stop/restart of the shared daemon, and who asked for it and why
|
|
1360
1374
|
-- (#378). Deliberately NOT partitioned by project: the daemon serves every
|
|
1361
1375
|
-- configured project, so a record written by one project's CLI must be
|
|
@@ -2954,6 +2968,18 @@ export function openStore(dbPath: string): Store {
|
|
|
2954
2968
|
WHERE project = ?
|
|
2955
2969
|
RETURNING project, mode, cause, since, diverted`,
|
|
2956
2970
|
);
|
|
2971
|
+
// Worker-workspace ownership rows (#1035 review). See the table comment:
|
|
2972
|
+
// the durable half of discovery, so a Herdr restart that drops metadata
|
|
2973
|
+
// tokens cannot orphan the surface conductor created.
|
|
2974
|
+
const selectHerdrWorkerWorkspaces = db.query<{ workspace_id: string }, [string]>(
|
|
2975
|
+
`SELECT workspace_id FROM herdr_worker_workspaces WHERE project = ? ORDER BY created_at ASC, workspace_id ASC`,
|
|
2976
|
+
);
|
|
2977
|
+
const insertHerdrWorkerWorkspace = db.query<unknown, [string, string, number]>(
|
|
2978
|
+
`INSERT OR IGNORE INTO herdr_worker_workspaces (project, workspace_id, created_at) VALUES (?, ?, ?)`,
|
|
2979
|
+
);
|
|
2980
|
+
const deleteHerdrWorkerWorkspace = db.query<unknown, [string, string]>(
|
|
2981
|
+
`DELETE FROM herdr_worker_workspaces WHERE project = ? AND workspace_id = ?`,
|
|
2982
|
+
);
|
|
2957
2983
|
const upsertDispatch = db.query<unknown, [string, string]>(
|
|
2958
2984
|
`INSERT INTO dispatch_summaries (project, summary) VALUES (?, ?)
|
|
2959
2985
|
ON CONFLICT(project) DO UPDATE SET summary = excluded.summary`,
|
|
@@ -4849,6 +4875,22 @@ export function openStore(dbPath: string): Store {
|
|
|
4849
4875
|
bumpOrchestratorIncident.run(by, project);
|
|
4850
4876
|
},
|
|
4851
4877
|
|
|
4878
|
+
/** The workspace ids conductor recorded as its own worker surface for
|
|
4879
|
+
* this project, oldest first (#1035 review). Durable across daemon AND
|
|
4880
|
+
* Herdr restarts, which is the point: Herdr restores workspaces without
|
|
4881
|
+
* their metadata tokens, so discovery needs a second authority. */
|
|
4882
|
+
workerWorkspaceIds(project: string): string[] {
|
|
4883
|
+
return selectHerdrWorkerWorkspaces.all(project).map((row) => row.workspace_id);
|
|
4884
|
+
},
|
|
4885
|
+
|
|
4886
|
+
rememberWorkerWorkspace(project: string, workspaceId: string): void {
|
|
4887
|
+
insertHerdrWorkerWorkspace.run(project, workspaceId, Date.now());
|
|
4888
|
+
},
|
|
4889
|
+
|
|
4890
|
+
forgetWorkerWorkspace(project: string, workspaceId: string): void {
|
|
4891
|
+
deleteHerdrWorkerWorkspace.run(project, workspaceId);
|
|
4892
|
+
},
|
|
4893
|
+
|
|
4852
4894
|
closeOrchestratorIncident(project: string, _at: number): OrchestratorIncident | undefined {
|
|
4853
4895
|
const row = deleteOrchestratorIncident.get(project);
|
|
4854
4896
|
return row === null ? undefined : toOrchestratorIncident(row);
|
package/src/types.ts
CHANGED
|
@@ -3187,6 +3187,13 @@ export interface Store {
|
|
|
3187
3187
|
/** Close the open incident and hand back what it accumulated, for the
|
|
3188
3188
|
* recovery page's downtime and diverted count. `undefined` when none. */
|
|
3189
3189
|
closeOrchestratorIncident(project: string, at: number): OrchestratorIncident | undefined;
|
|
3190
|
+
/** The workspace ids conductor recorded as its own worker surface for a
|
|
3191
|
+
* project, oldest first (#1035 review). Durable across daemon AND Herdr
|
|
3192
|
+
* restarts: Herdr restores workspaces without their metadata tokens, so
|
|
3193
|
+
* this record is the discovery leg that survives. */
|
|
3194
|
+
workerWorkspaceIds(project: string): string[];
|
|
3195
|
+
rememberWorkerWorkspace(project: string, workspaceId: string): void;
|
|
3196
|
+
forgetWorkerWorkspace(project: string, workspaceId: string): void;
|
|
3190
3197
|
/** Append one daemon stop/restart provenance line (#378). Host-wide — never
|
|
3191
3198
|
* partitioned by project, so any project's status reads the same records. */
|
|
3192
3199
|
recordDaemonStop(draft: DaemonStopDraft): DaemonStop;
|
|
@@ -3840,6 +3847,18 @@ export const VERB_REFUSALS = [
|
|
|
3840
3847
|
* verb refuses with an actionable syntax error instead of echoing a
|
|
3841
3848
|
* fail-open the heading contradicts. */
|
|
3842
3849
|
"file-lane-unparseable",
|
|
3850
|
+
/**
|
|
3851
|
+
* Adding the queue label was refused because the issue carries a durable
|
|
3852
|
+
* `promotable` grooming verdict whose dispatch brief no longer provably
|
|
3853
|
+
* matches the issue (#1036): the current write lane differs from the
|
|
3854
|
+
* verdict's `fileLane`, the evidence does not recover as a strict
|
|
3855
|
+
* PROMOTABLE result, or the issue could not be read to compare at all. The
|
|
3856
|
+
* durable verdict is the admission contract, so promotion fails closed
|
|
3857
|
+
* before any tracker mutation or daemon wake; the remediation is applying
|
|
3858
|
+
* the verdict's `proposedBrief` to the issue explicitly, never a silent
|
|
3859
|
+
* rewrite, and then adding the label again.
|
|
3860
|
+
*/
|
|
3861
|
+
"promotion-brief-mismatch",
|
|
3843
3862
|
/** The release grant does not permit this shape for this caller. */
|
|
3844
3863
|
"release-not-granted",
|
|
3845
3864
|
/** The artefact or environment is not one this project declared (#129). */
|
package/src/verbs/server.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
import { releaseRefusal } from "../release-policy.ts";
|
|
61
61
|
import { PR_LOOKUP_WINDOW_MS, REVISABLE_RUN_STATES, prReviewReadiness } from "../decisions.ts";
|
|
62
62
|
import { LIVE_STATES } from "../store.ts";
|
|
63
|
+
import { parseToSpecEvidence } from "../to-spec.ts";
|
|
63
64
|
import { DENIED_RELEASE_GRANTS } from "../types.ts";
|
|
64
65
|
import type {
|
|
65
66
|
FileLane,
|
|
@@ -1298,10 +1299,23 @@ async function labelVerb(
|
|
|
1298
1299
|
let malformed: string | undefined;
|
|
1299
1300
|
if (action === "add" && label === project.queueLabel) {
|
|
1300
1301
|
const lane = await laneForEcho(deps, ref.issue);
|
|
1301
|
-
if (lane
|
|
1302
|
+
if (lane.readable) {
|
|
1302
1303
|
malformed = lane.malformed;
|
|
1303
1304
|
echo = laneEcho(lane.lane);
|
|
1304
1305
|
}
|
|
1306
|
+
// The durable grooming verdict is an admission contract only while the
|
|
1307
|
+
// dispatch brief still matches it (#1036): a stale issue body once
|
|
1308
|
+
// promoted beside a PROMOTABLE verdict whose fileLane had moved on, and
|
|
1309
|
+
// the run dispatched on the stale lane. So before any tracker mutation or
|
|
1310
|
+
// daemon wake, a promotable row must verify against the same parse the
|
|
1311
|
+
// echo reports — exact in both directions, order-insensitive. The syntax
|
|
1312
|
+
// refusal below keeps precedence: fix the section first, then the contract.
|
|
1313
|
+
if (malformed === undefined) {
|
|
1314
|
+
const mismatch = promotionBriefRefusal(deps, project, ref.issue, lane);
|
|
1315
|
+
if (mismatch !== undefined) {
|
|
1316
|
+
return refuse("promotion-brief-mismatch", mismatch, ref.issue);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1305
1319
|
}
|
|
1306
1320
|
if (malformed !== undefined) {
|
|
1307
1321
|
return refuse(
|
|
@@ -1354,32 +1368,99 @@ async function labelVerb(
|
|
|
1354
1368
|
* The effective file lane admission will enforce for one issue, as the
|
|
1355
1369
|
* one-line echo (#724), plus the malformed write-lane section marker (#825):
|
|
1356
1370
|
* the body plus the whole comment thread, the same inputs `effectiveLane`
|
|
1357
|
-
* reads at admission. `
|
|
1358
|
-
*
|
|
1371
|
+
* reads at admission. `readable` says whether the issue and thread were read
|
|
1372
|
+
* at all — the echo stays best-effort (an unreadable issue never blocks its
|
|
1373
|
+
* own promotion, #724), but the promotion-brief gate (#1036) must be able to
|
|
1374
|
+
* tell "no lane declared" from "could not look", because behind a durable
|
|
1375
|
+
* `promotable` verdict the second is a fail-closed refusal rather than
|
|
1376
|
+
* feedback.
|
|
1359
1377
|
*/
|
|
1360
|
-
async function laneForEcho(deps: VerbDeps, issue: number): Promise<
|
|
1361
|
-
|
|
1362
|
-
| undefined
|
|
1363
|
-
|
|
1378
|
+
async function laneForEcho(deps: VerbDeps, issue: number): Promise<{
|
|
1379
|
+
readable: boolean;
|
|
1380
|
+
lane: FileLane | undefined;
|
|
1381
|
+
malformed: string | undefined;
|
|
1382
|
+
}> {
|
|
1364
1383
|
let body: string;
|
|
1365
1384
|
let comments: IssueComment[];
|
|
1366
1385
|
try {
|
|
1367
1386
|
const row = await deps.tracker.getIssue(issue);
|
|
1368
|
-
if (row === undefined) return undefined;
|
|
1387
|
+
if (row === undefined) return { readable: false, lane: undefined, malformed: undefined };
|
|
1369
1388
|
body = row.body;
|
|
1370
1389
|
comments = await deps.tracker.listComments(issue);
|
|
1371
1390
|
} catch {
|
|
1372
|
-
return undefined;
|
|
1391
|
+
return { readable: false, lane: undefined, malformed: undefined };
|
|
1373
1392
|
}
|
|
1374
1393
|
const lane = effectiveLane(body, comments);
|
|
1375
|
-
if (lane !== undefined) return { lane, malformed: undefined };
|
|
1394
|
+
if (lane !== undefined) return { readable: true, lane, malformed: undefined };
|
|
1376
1395
|
// No declaration parsed anywhere. If a clearly delimited write-lane section
|
|
1377
1396
|
// exists anyway, the section tried to declare and failed — the echo must
|
|
1378
1397
|
// refuse, never claim fail-open.
|
|
1379
1398
|
const heading =
|
|
1380
1399
|
writeLaneSectionHeading(body) ??
|
|
1381
1400
|
comments.map((c) => writeLaneSectionHeading(c.body)).find((h) => h !== undefined);
|
|
1382
|
-
return { lane: undefined, malformed: heading };
|
|
1401
|
+
return { readable: true, lane: undefined, malformed: heading };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* The promotion-brief gate (#1036): a durable `promotable` grooming verdict is
|
|
1406
|
+
* an admission contract only while the dispatch brief still matches it, so a
|
|
1407
|
+
* queue-label add must prove the match before any tracker mutation or daemon
|
|
1408
|
+
* wake. Compares the lane admission would enforce (the same parse the echo
|
|
1409
|
+
* reports) against the verdict's `fileLane` as deduplicated,
|
|
1410
|
+
* order-independent path sets, exact in both directions — missing and extra
|
|
1411
|
+
* paths refuse alike. Returns the refusal detail, or `undefined` when
|
|
1412
|
+
* promotion may proceed: no grooming row, a non-`promotable` row, or a
|
|
1413
|
+
* verified match.
|
|
1414
|
+
*
|
|
1415
|
+
* Fail-closed by design: evidence behind a `promotable` row that does not
|
|
1416
|
+
* recover as a strict PROMOTABLE result (`parseToSpecEvidence`, the same
|
|
1417
|
+
* restart round-trip the selection side trusts), or an issue that cannot be
|
|
1418
|
+
* read to compare at all, refuses the promotion rather than dispatching on an
|
|
1419
|
+
* unverifiable brief. The gate never edits the issue — it names both path
|
|
1420
|
+
* sets and points at the verdict's `proposedBrief`; correcting the dispatch
|
|
1421
|
+
* brief is an explicit orchestrator act.
|
|
1422
|
+
*/
|
|
1423
|
+
function promotionBriefRefusal(
|
|
1424
|
+
deps: VerbDeps,
|
|
1425
|
+
project: ProjectConfig,
|
|
1426
|
+
issue: number,
|
|
1427
|
+
read: { readable: boolean; lane: FileLane | undefined },
|
|
1428
|
+
): string | undefined {
|
|
1429
|
+
const row = deps.store.grooming(project.name, issue);
|
|
1430
|
+
if (row?.verdict !== "promotable") return undefined;
|
|
1431
|
+
const result = parseToSpecEvidence(row.evidence);
|
|
1432
|
+
if (result === undefined || result.verdict !== "PROMOTABLE") {
|
|
1433
|
+
return (
|
|
1434
|
+
`refused: ${project.queueLabel} was not added — #${issue}'s durable grooming row reads promotable, but its ` +
|
|
1435
|
+
"evidence does not recover as a strict PROMOTABLE to-spec result, so promotion cannot prove the dispatch " +
|
|
1436
|
+
"brief still matches what was groomed. Re-groom the issue so a fresh verdict replaces the row, then add the label again."
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
if (!read.readable) {
|
|
1440
|
+
return (
|
|
1441
|
+
`refused: ${project.queueLabel} was not added — #${issue} carries a durable PROMOTABLE grooming verdict, but ` +
|
|
1442
|
+
"the issue could not be read, so the current brief cannot be compared with the verdict's file lane. " +
|
|
1443
|
+
"Retry when the tracker responds."
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
// One closure so both sides of the comparison normalize in lockstep: a set
|
|
1447
|
+
// comparison whose halves dedupe differently lies silently (#1036).
|
|
1448
|
+
const normalizedPaths = (paths: readonly string[]): string[] =>
|
|
1449
|
+
[...new Set(paths.map((path) => path.trim()).filter((path) => path.length > 0))].sort();
|
|
1450
|
+
const brief = normalizedPaths(read.lane?.files ?? []);
|
|
1451
|
+
const verdict = normalizedPaths(result.fileLane);
|
|
1452
|
+
if (
|
|
1453
|
+
brief.length === verdict.length &&
|
|
1454
|
+
brief.every((path, index) => path === verdict[index])
|
|
1455
|
+
) {
|
|
1456
|
+
return undefined;
|
|
1457
|
+
}
|
|
1458
|
+
return (
|
|
1459
|
+
`refused: ${project.queueLabel} was not added — #${issue}'s current write lane [${brief.join(", ")}] disagrees ` +
|
|
1460
|
+
`with its durable PROMOTABLE verdict's file lane [${verdict.join(", ")}]. The durable verdict is the admission ` +
|
|
1461
|
+
"contract: apply the verdict's proposedBrief to the issue (its ## Exact write lane included), then add the " +
|
|
1462
|
+
"label again. The gate never rewrites the issue itself."
|
|
1463
|
+
);
|
|
1383
1464
|
}
|
|
1384
1465
|
|
|
1385
1466
|
async function releaseVerb(
|