omp-conductor 0.3.24 → 0.4.0

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/unblock.ts CHANGED
@@ -40,6 +40,11 @@ export interface UnblockOutcome {
40
40
  continuationsUsed: number;
41
41
  /** Newest attempt, when the store has one for this issue at all. */
42
42
  latest?: RunRecord;
43
+ /** Set when nothing was cleared because the newest attempt's work exists
44
+ * only in its worktree. Carries the salvage failure verbatim. */
45
+ refused?: string;
46
+ /** Set when `--force` recorded an operator's acceptance of that loss. */
47
+ forced?: true;
43
48
  }
44
49
 
45
50
  /**
@@ -81,11 +86,35 @@ export async function unblockIssue(
81
86
  tracker: Tracker,
82
87
  store: Store,
83
88
  issue: number,
89
+ opts: { force?: boolean } = {},
84
90
  ): Promise<UnblockOutcome> {
85
91
  // Read before any label is touched: terminality is the whole of the argument
86
92
  // for clearing in-progress, so the row that carries it decides the set.
87
93
  const latest = store.latestRun(project.name, issue);
88
94
  const terminal = latest !== undefined && !LIVE_STATES.includes(latest.state);
95
+ const counts = {
96
+ attemptsUsed: store.attemptsFor(project.name, issue),
97
+ failuresUsed: store.failuresFor(project.name, issue),
98
+ continuationsUsed: store.continuationsFor(project.name, issue),
99
+ };
100
+
101
+ // The one case where this verb refuses. Clearing the labels here re-queues an
102
+ // issue whose next claim starts by force-removing the worktree that holds the
103
+ // only copy of the last attempt's work (#118) — and unlike every other state
104
+ // this function reasons about, that is unrecoverable. `--force` is the way
105
+ // through, and it is deliberately a separate keystroke rather than a prompt
106
+ // the operator can wave past: it records, on the row, that a human accepted
107
+ // the loss or recovered the tree themselves. Narrowed to the row rather than
108
+ // a boolean so the reason printed and the acknowledgement written are
109
+ // provably about the same run.
110
+ const held =
111
+ latest !== undefined && latest.salvageError !== undefined && latest.salvageAckAt === undefined
112
+ ? latest
113
+ : undefined;
114
+ if (held !== undefined && opts.force !== true) {
115
+ return { cleared: [], ...counts, latest: held, refused: held.salvageError };
116
+ }
117
+ if (held !== undefined) store.updateRun(held.id, { salvageAckAt: Date.now() });
89
118
 
90
119
  const cleared: string[] = [];
91
120
  for (const label of new Set([
@@ -99,10 +128,9 @@ export async function unblockIssue(
99
128
 
100
129
  return {
101
130
  cleared,
102
- attemptsUsed: store.attemptsFor(project.name, issue),
103
- failuresUsed: store.failuresFor(project.name, issue),
104
- continuationsUsed: store.continuationsFor(project.name, issue),
131
+ ...counts,
105
132
  ...(latest === undefined ? {} : { latest }),
133
+ ...(held === undefined ? {} : { forced: true as const }),
106
134
  };
107
135
  }
108
136
 
@@ -116,6 +144,11 @@ export async function unblockIssue(
116
144
  * run row at all, where that label may still be sitting there unread. #18 was
117
145
  * filed against this function saying `next tick eligible again` in a case where
118
146
  * it was not, so the wording is a contract rather than prose.
147
+ *
148
+ * A refusal is the loudest thing this verb prints, and it prints instead of
149
+ * everything else: the operator asked to re-queue an issue whose only copy of
150
+ * real work is a directory, and the next line they read has to be the one that
151
+ * stops them typing the same command again with `--force` on the end.
119
152
  */
120
153
  export function formatUnblock(
121
154
  issue: number,
@@ -124,7 +157,24 @@ export function formatUnblock(
124
157
  caps: Caps,
125
158
  ): string {
126
159
  const latest = o.latest;
160
+
161
+ if (o.refused !== undefined) {
162
+ return [
163
+ `#${issue}: REFUSED — nothing was cleared`,
164
+ ` reason attempt ${latest?.attempt ?? "?"} could not commit its work: ${o.refused}`,
165
+ ` only copy ${latest?.worktree === undefined || latest.worktree === "" ? "(worktree path not recorded)" : latest.worktree}`,
166
+ " why re-claiming this issue removes that worktree, and the work is not on any ref",
167
+ ` recover inspect the tree and commit or copy what matters, then re-run with --force`,
168
+ ].join("\n");
169
+ }
170
+
127
171
  const lines = [`#${issue}: cleared ${o.cleared.join(", ")}`];
172
+ if (o.forced === true) {
173
+ lines.push(
174
+ ` forced the unsalvaged worktree was accepted as lost or already recovered by hand — ` +
175
+ "the next claim removes it",
176
+ );
177
+ }
128
178
 
129
179
  if (latest === undefined) {
130
180
  lines.push(" runs none recorded — the terminal labels were cleared anyway; eligibility is read off the tracker");