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/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). */
@@ -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 !== undefined) {
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. `undefined` when either read fails or the tracker cannot
1358
- * produce the issue — the promotion is never blocked by its own feedback.
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
- | { lane: FileLane | undefined; malformed: string | undefined }
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(