@momentiq/dark-factory-cli 0.7.0-alpha.9 → 1.0.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.
Files changed (79) hide show
  1. package/dist/cli.js +383 -29
  2. package/dist/cli.js.map +1 -1
  3. package/dist/handoff/accept-verb.d.ts +31 -0
  4. package/dist/handoff/accept-verb.d.ts.map +1 -0
  5. package/dist/handoff/accept-verb.js +165 -0
  6. package/dist/handoff/accept-verb.js.map +1 -0
  7. package/dist/handoff/args.d.ts +19 -0
  8. package/dist/handoff/args.d.ts.map +1 -0
  9. package/dist/handoff/args.js +68 -0
  10. package/dist/handoff/args.js.map +1 -0
  11. package/dist/handoff/assignees.d.ts +37 -0
  12. package/dist/handoff/assignees.d.ts.map +1 -0
  13. package/dist/handoff/assignees.js +46 -0
  14. package/dist/handoff/assignees.js.map +1 -0
  15. package/dist/handoff/handoff-verb.d.ts +34 -0
  16. package/dist/handoff/handoff-verb.d.ts.map +1 -0
  17. package/dist/handoff/handoff-verb.js +451 -0
  18. package/dist/handoff/handoff-verb.js.map +1 -0
  19. package/dist/handoff/handoffs-verb.d.ts +29 -0
  20. package/dist/handoff/handoffs-verb.d.ts.map +1 -0
  21. package/dist/handoff/handoffs-verb.js +84 -0
  22. package/dist/handoff/handoffs-verb.js.map +1 -0
  23. package/dist/handoff/index.d.ts +14 -143
  24. package/dist/handoff/index.d.ts.map +1 -1
  25. package/dist/handoff/index.js +28 -647
  26. package/dist/handoff/index.js.map +1 -1
  27. package/dist/handoff/iso.d.ts +9 -0
  28. package/dist/handoff/iso.d.ts.map +1 -0
  29. package/dist/handoff/iso.js +47 -0
  30. package/dist/handoff/iso.js.map +1 -0
  31. package/dist/handoff/links.d.ts +48 -0
  32. package/dist/handoff/links.d.ts.map +1 -0
  33. package/dist/handoff/links.js +211 -0
  34. package/dist/handoff/links.js.map +1 -0
  35. package/dist/handoff/markers.d.ts +50 -0
  36. package/dist/handoff/markers.d.ts.map +1 -0
  37. package/dist/handoff/markers.js +121 -0
  38. package/dist/handoff/markers.js.map +1 -0
  39. package/dist/handoff/ports.d.ts +126 -0
  40. package/dist/handoff/ports.d.ts.map +1 -0
  41. package/dist/handoff/ports.js +34 -0
  42. package/dist/handoff/ports.js.map +1 -0
  43. package/dist/handoff/real-clients.d.ts +43 -0
  44. package/dist/handoff/real-clients.d.ts.map +1 -0
  45. package/dist/handoff/real-clients.js +291 -0
  46. package/dist/handoff/real-clients.js.map +1 -0
  47. package/dist/handoff/rehydrate-core.d.ts +55 -0
  48. package/dist/handoff/rehydrate-core.d.ts.map +1 -0
  49. package/dist/handoff/rehydrate-core.js +243 -0
  50. package/dist/handoff/rehydrate-core.js.map +1 -0
  51. package/dist/handoff/rehydrate-render.d.ts +3 -0
  52. package/dist/handoff/rehydrate-render.d.ts.map +1 -0
  53. package/dist/handoff/rehydrate-render.js +107 -0
  54. package/dist/handoff/rehydrate-render.js.map +1 -0
  55. package/dist/handoff/rehydrate-verb.d.ts +31 -0
  56. package/dist/handoff/rehydrate-verb.d.ts.map +1 -0
  57. package/dist/handoff/rehydrate-verb.js +111 -0
  58. package/dist/handoff/rehydrate-verb.js.map +1 -0
  59. package/dist/handoff/scrub.d.ts +99 -0
  60. package/dist/handoff/scrub.d.ts.map +1 -0
  61. package/dist/handoff/scrub.js +127 -0
  62. package/dist/handoff/scrub.js.map +1 -0
  63. package/dist/handoff/strip-control.d.ts +4 -0
  64. package/dist/handoff/strip-control.d.ts.map +1 -0
  65. package/dist/handoff/strip-control.js +16 -0
  66. package/dist/handoff/strip-control.js.map +1 -0
  67. package/dist/mcp/server.d.ts +0 -8
  68. package/dist/mcp/server.d.ts.map +1 -1
  69. package/dist/mcp/server.js +1 -5
  70. package/dist/mcp/server.js.map +1 -1
  71. package/dist/mcp/tools/handoff.d.ts +16 -8
  72. package/dist/mcp/tools/handoff.d.ts.map +1 -1
  73. package/dist/mcp/tools/handoff.js +253 -303
  74. package/dist/mcp/tools/handoff.js.map +1 -1
  75. package/package.json +2 -2
  76. package/dist/handoff/cli.d.ts +0 -5
  77. package/dist/handoff/cli.d.ts.map +0 -1
  78. package/dist/handoff/cli.js +0 -227
  79. package/dist/handoff/cli.js.map +0 -1
@@ -0,0 +1,451 @@
1
+ // packages/cli/src/handoff/handoff-verb.ts
2
+ //
3
+ // /handoff verb — port of dark-factory-platform
4
+ // .claude/skills/handoff/scripts/handoff.sh@a6f711b (390 LOC bash → TS).
5
+ //
6
+ // PRESERVES THE gh-CALL SEQUENCE BYTE-FOR-FOR-BYTE. Task 17 ports the bash
7
+ // race/drift tests via FakeGhClient.calls(); they assert the relative order:
8
+ // 1) validate gh.issueView (slot 1 — body-bearing)
9
+ // 2) maybe gh.issueList (no-arg path; assignee partition)
10
+ // 3) maybe gh.prListByHead (auto-link single PR; createNew && no --link)
11
+ // 4) maybe gh.prView N (per --link, PR-first lookup in resolveLinkRef)
12
+ // 5) maybe gh.issueView N (per --link, issue fallback)
13
+ // 6) pre-PATCH gh.issueView (slot 2 — body-bearing, the race-safety seam)
14
+ // 7) gh.issueEditBody (the actual PATCH)
15
+ // 8) gh.ensureHandoffLabel
16
+ // 9) gh.issueAddLabel (load-bearing — failure = not on /handoffs stack)
17
+ // 10) gh.issueUnassignMe (idempotent, non-fatal)
18
+ //
19
+ // The slot-1 vs slot-2 split on issueView IS the race-safety seam — drift
20
+ // between them means a concurrent /accept landed in between. Do NOT collapse.
21
+ //
22
+ // Behavioral correction from advisor (overrides the spec's Phase C step 6
23
+ // prose): the claimed-by-other advisory fires ONLY on the create-new branch
24
+ // (FORCE_NEW || eligible_count==0). The bash code (handoff.sh:151-153)
25
+ // calls _emit_others_advisory only there; the eligible_count==1 picker
26
+ // branch (handoff.sh:154-163) emits NO advisory. The message text confirms:
27
+ // "…— creating a new handoff." is false in the pick-one-and-update case.
28
+ //
29
+ // Other v2 deltas vs the v1 src/handoff/index.ts runHandoff (which this
30
+ // supersedes at Task 22):
31
+ // - Dirty worktree → warn, not refuse (D4: no push step here).
32
+ // - Empty-note check uses byte length (bash [ -s ]), not v1's !body.trim().
33
+ // - Imports HandoffError + ports from ./ports.js, not the v1 module.
34
+ // - Uses GhClient/GitClient/Clock ports (Task 4), not GhRunner/GitRunner.
35
+ import { requireIssueNumber } from "./args.js";
36
+ import { assigneesOtherCsv, assigneesStatus, MeLoginCache, } from "./assignees.js";
37
+ import { canonicalizeLinkRef, extractLinkedItems, formatLinkEntry, resolveLinkRef, } from "./links.js";
38
+ import { MARKER_CLOSE, MARKER_OPEN, spliceAgentContextBlock, validateLatestBlock, } from "./markers.js";
39
+ import { HandoffError } from "./ports.js";
40
+ import { scrubBody, scrubString } from "./scrub.js";
41
+ const HANDOFF_LABEL = "handoff";
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers.
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Strip a pre-existing "Linked work items" section from the operator's
47
+ * note. Defensive — SKILL.md tells operators not to include one, but if they
48
+ * do, the script-maintained section replaces it.
49
+ *
50
+ * Mirrors the bash awk transform at handoff.sh:267-274. Five rules in
51
+ * order: header line turns skip on and is dropped; in-skip list-item /
52
+ * empty-marker / blank lines are dropped (blank ALSO turns skip off as a
53
+ * consumed terminator); in-skip arbitrary line turns skip off and falls
54
+ * through to the default print rule.
55
+ *
56
+ * The blank-line terminator is CONSUMED (`next` in awk), but the
57
+ * content-line terminator is PRINTED (fall through). Preserve this
58
+ * asymmetry — getting it wrong only surfaces in Task 21 parity snapshots.
59
+ */
60
+ function stripOperatorLinksSection(note) {
61
+ const lines = note.split("\n");
62
+ const out = [];
63
+ let skip = false;
64
+ for (const line of lines) {
65
+ if (/^\*\*Linked work items:\*\*/.test(line)) {
66
+ skip = true;
67
+ continue;
68
+ }
69
+ if (skip && /^- (pr|issue) /.test(line))
70
+ continue;
71
+ if (skip && /^_None linked\._/.test(line))
72
+ continue;
73
+ if (skip && /^[\t ]*$/.test(line)) {
74
+ skip = false;
75
+ continue; // blank-line terminator is consumed
76
+ }
77
+ if (skip) {
78
+ skip = false;
79
+ // FALL THROUGH and print this content-line terminator.
80
+ }
81
+ out.push(line);
82
+ }
83
+ return out.join("\n");
84
+ }
85
+ /**
86
+ * Inject a Linked-items section immediately before the close marker.
87
+ *
88
+ * PORT FROM bash awk (handoff.sh:277-287). On a line containing the close
89
+ * marker: emit blank, the section lines, blank, then the close marker line.
90
+ * All other lines pass through.
91
+ */
92
+ function injectLinksBeforeClose(noteStripped, linksSection) {
93
+ const lines = noteStripped.split("\n");
94
+ const sectionLines = linksSection.split("\n");
95
+ const out = [];
96
+ let injected = false;
97
+ for (const line of lines) {
98
+ if (!injected && line.includes(MARKER_CLOSE)) {
99
+ out.push("");
100
+ for (const s of sectionLines)
101
+ out.push(s);
102
+ out.push("");
103
+ out.push(line);
104
+ injected = true;
105
+ continue;
106
+ }
107
+ out.push(line);
108
+ }
109
+ return out.join("\n");
110
+ }
111
+ /** Compose the script-maintained `**Linked work items:**` section. */
112
+ function composeLinksSection(entries) {
113
+ const out = ["**Linked work items:**"];
114
+ if (entries.length === 0) {
115
+ out.push("_None linked._");
116
+ }
117
+ else {
118
+ for (const e of entries)
119
+ out.push(e);
120
+ }
121
+ return out.join("\n");
122
+ }
123
+ /**
124
+ * Parse a body entry `- (pr|issue) <ref> ...` → its kind + display, for
125
+ * --unlink matching. PORT FROM bash (handoff.sh:221-222): the bash uses
126
+ * `awk '{print $2}' / '{print $3}'` — field 2 = kind, field 3 = display.
127
+ * Returns "" for kind/display if the line is malformed (caller treats as
128
+ * non-matchable).
129
+ */
130
+ function entryKindAndDisplay(line) {
131
+ // Match bash awk default field-splitting (whitespace).
132
+ const fields = line.split(/\s+/);
133
+ return {
134
+ kind: fields[1] ?? "",
135
+ display: fields[2] ?? "",
136
+ };
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // runHandoff — the orchestrator.
140
+ // ---------------------------------------------------------------------------
141
+ /**
142
+ * Put a work-stream on the handoff stack. Upserts the operator's
143
+ * marker-bounded rehydration note as a dedicated handoff GitHub Issue body,
144
+ * maintains its Linked-items section + label + leaves it unassigned. NO
145
+ * `git push` (Decision D4).
146
+ *
147
+ * Phases mirror the bash:
148
+ * A: validate stdin — markers + secret-scrub, no gh
149
+ * B: dirty worktree — warn, not refuse
150
+ * C: resolve target — explicit issue OR no-arg eligibility partition
151
+ * D: linked-items — extract → --link (with title scrub) → --unlink → auto-link
152
+ * E: compose new note — strip operator's stale section, inject script section
153
+ * F: PATCH or CREATE — with pre-PATCH race-safety drift check
154
+ */
155
+ export async function runHandoff(opts) {
156
+ const { noteStdin, gh, git, clock } = opts;
157
+ const links = opts.link ?? [];
158
+ const unlinks = opts.unlink ?? [];
159
+ const forceNew = opts.forceNew === true;
160
+ const logs = [];
161
+ const meCache = new MeLoginCache();
162
+ // Belt-and-braces: requireIssueNumber would refuse a non-positive integer,
163
+ // but `opts.issue` is already typed `number | undefined`. The cli.ts layer
164
+ // (Task 25) is expected to parse the raw arg via requireIssueNumber; this
165
+ // call ensures programmatic callers (MCP tools, tests) get the same guard.
166
+ if (opts.issue !== undefined) {
167
+ requireIssueNumber(String(opts.issue));
168
+ }
169
+ // -------------------------------------------------------------------------
170
+ // Phase A — validate the note (handoff.sh:58-70). NO gh calls.
171
+ // -------------------------------------------------------------------------
172
+ // Bash `[ -s "$NOTE" ]` — byte length, not trimmed. A whitespace-only note
173
+ // must fall through to the marker error, not the empty-body error.
174
+ if (noteStdin.length === 0) {
175
+ throw new HandoffError("empty note body on stdin — nothing to post.");
176
+ }
177
+ if (!validateLatestBlock(noteStdin)) {
178
+ throw new HandoffError(`note is missing/malformed agent-context markers (need ${MARKER_OPEN} … ${MARKER_CLOSE}) — compose per SKILL.md (single block, well-formed).`);
179
+ }
180
+ const noteScrub = scrubBody(noteStdin, "<stdin>");
181
+ if (!noteScrub.ok) {
182
+ throw new HandoffError(noteScrub.refusal);
183
+ }
184
+ // -------------------------------------------------------------------------
185
+ // Phase B — dirty worktree → warn, do not refuse (handoff.sh:72-75). D4.
186
+ // -------------------------------------------------------------------------
187
+ if (await git.isDirty()) {
188
+ logs.push("uncommitted tracked changes in the worktree — they won't be on a linked PR's diff. Commit/push them yourself if part of this work-stream.");
189
+ }
190
+ // -------------------------------------------------------------------------
191
+ // Phase C — resolve the target issue (handoff.sh:77-168).
192
+ // -------------------------------------------------------------------------
193
+ let createNew = false;
194
+ let issueNum = opts.issue;
195
+ let existingBody = "";
196
+ let initialUpdatedAt;
197
+ if (opts.issue !== undefined) {
198
+ // Explicit issue path (bash:81-107). Capture state/labels/assignees/body/
199
+ // updatedAt in one body-bearing view (slot 1).
200
+ let view;
201
+ try {
202
+ view = await gh.issueView(opts.issue);
203
+ }
204
+ catch {
205
+ throw new HandoffError(`can't verify issue #${opts.issue} (gh issue view failed — does it exist in this repo?).`);
206
+ }
207
+ if (view.state === "CLOSED") {
208
+ throw new HandoffError(`issue #${opts.issue} is closed — the handoff was already accepted; start a fresh one (run \`/handoff\` with no argument).`);
209
+ }
210
+ const hasHandoffLabel = view.labels.some((l) => l.name === HANDOFF_LABEL);
211
+ existingBody = view.body;
212
+ if (!hasHandoffLabel) {
213
+ if (existingBody.length > 0) {
214
+ throw new HandoffError(`issue #${opts.issue} is not a handoff issue (no \`${HANDOFF_LABEL}\` label, non-empty body) — start a fresh handoff (\`/handoff\` with no argument), or pre-create an empty issue and apply the \`${HANDOFF_LABEL}\` label first.`);
215
+ }
216
+ // Empty shell — accept; label added later in Phase F.
217
+ }
218
+ const meLogin = await meCache.resolve(gh);
219
+ const status = assigneesStatus(view.assignees, meLogin);
220
+ if (status === "other") {
221
+ const others = assigneesOtherCsv(view.assignees, meLogin);
222
+ throw new HandoffError(`issue #${opts.issue} is currently assigned to @${others} — coordinate with them or ask them to re-handoff (which un-assigns).`);
223
+ }
224
+ initialUpdatedAt = view.updatedAt;
225
+ issueNum = opts.issue;
226
+ }
227
+ else {
228
+ // No-arg path (bash:108-168). Fail closed on list errors so we never
229
+ // silently fall through to creating a duplicate of an existing handoff.
230
+ let list;
231
+ try {
232
+ list = await gh.issueList({ state: "open", search: "author:@me" });
233
+ }
234
+ catch {
235
+ throw new HandoffError("could not query existing handoffs (`gh issue list` failed) — not creating/updating; re-run when gh recovers.");
236
+ }
237
+ const meLogin = await meCache.resolve(gh);
238
+ const eligible = list.filter((it) => assigneesStatus(it.assignees, meLogin) !== "other");
239
+ const claimedByOther = list.filter((it) => assigneesStatus(it.assignees, meLogin) === "other");
240
+ // Advisor correction: the claimed-by-other advisory fires ONLY on the
241
+ // create-new branch (bash:151-153). The eligible==1 picker branch
242
+ // (bash:154-163) emits NO advisory — the message text would be false
243
+ // ("…— creating a new handoff" is wrong when we're updating one).
244
+ const emitOthersAdvisory = () => {
245
+ for (const other of claimedByOther) {
246
+ const otherCsv = other.assignees.map((a) => a.login).join(",");
247
+ logs.push(`the handoff you created at #${other.number} is now claimed by @${otherCsv} — creating a new handoff. To update #${other.number}'s body coordinate with @${otherCsv}.`);
248
+ }
249
+ };
250
+ if (forceNew || eligible.length === 0) {
251
+ createNew = true;
252
+ emitOthersAdvisory();
253
+ }
254
+ else if (eligible.length === 1) {
255
+ const picked = eligible[0];
256
+ issueNum = picked.number;
257
+ // Re-fetch full snapshot via view (slot 1 — body-bearing). List body
258
+ // can lag the view body, AND we need state/assignees/updatedAt for
259
+ // the pre-PATCH drift check below.
260
+ let view;
261
+ try {
262
+ view = await gh.issueView(picked.number);
263
+ }
264
+ catch {
265
+ throw new HandoffError(`could not fetch state for #${picked.number} (gh issue view failed) — not PATCHing.`);
266
+ }
267
+ existingBody = view.body;
268
+ initialUpdatedAt = view.updatedAt;
269
+ logs.push(`updated #${picked.number} instead of creating new — pass \`--new\` to force a new issue.`);
270
+ }
271
+ else {
272
+ const nums = eligible.map((e) => `#${e.number}`).join(",");
273
+ throw new HandoffError(`multiple open handoffs owned by you: ${nums} — pick one (\`/handoff <issue>\`) or pass \`--new\` to force a new issue.`);
274
+ }
275
+ }
276
+ // -------------------------------------------------------------------------
277
+ // Phase D — compute linked-work-items entries (handoff.sh:170-247).
278
+ // -------------------------------------------------------------------------
279
+ let entries = [...extractLinkedItems(existingBody)];
280
+ // --link: resolve via gh, scrub title, dedup by "kind display" prefix.
281
+ for (const ref of links) {
282
+ const resolved = await resolveLinkRef(ref, gh);
283
+ const titleScrub = scrubString(resolved.title, `linked work-item title for ${resolved.display}`);
284
+ if (!titleScrub.ok) {
285
+ throw new HandoffError(titleScrub.refusal);
286
+ }
287
+ const newEntry = formatLinkEntry(resolved);
288
+ const dedupPrefix = `- ${resolved.kind} ${resolved.display} —`;
289
+ entries = entries.filter((e) => !e.startsWith(dedupPrefix));
290
+ entries.push(newEntry);
291
+ }
292
+ // --unlink: canonicalize ref (no gh fetch), drop matching entries.
293
+ for (const ref of unlinks) {
294
+ const canon = canonicalizeLinkRef(ref);
295
+ entries = entries.filter((e) => {
296
+ const { kind: eKind, display: eDisplay } = entryKindAndDisplay(e);
297
+ if (eDisplay !== canon.display)
298
+ return true;
299
+ // Display matched. Only drop if ref had no kind hint, OR kinds also match.
300
+ if (canon.kind === "" || canon.kind === eKind)
301
+ return false;
302
+ return true;
303
+ });
304
+ }
305
+ // Auto-link single matching open PR — only when creating new AND no --link.
306
+ if (createNew && links.length === 0) {
307
+ const branch = await git.currentBranch();
308
+ if (branch !== "" &&
309
+ branch !== "HEAD" &&
310
+ branch !== "main" &&
311
+ branch !== "master") {
312
+ const prList = await gh.prListByHead(branch);
313
+ if (prList.length === 1) {
314
+ const onlyPr = prList[0];
315
+ const titleScrub = scrubString(onlyPr.title, `auto-linked PR #${onlyPr.number} title`);
316
+ if (!titleScrub.ok) {
317
+ throw new HandoffError(titleScrub.refusal);
318
+ }
319
+ entries.push(`- pr #${onlyPr.number} — ${onlyPr.title}`);
320
+ }
321
+ }
322
+ }
323
+ // -------------------------------------------------------------------------
324
+ // Phase E — compose the new block (handoff.sh:249-287).
325
+ // -------------------------------------------------------------------------
326
+ const linksSection = composeLinksSection(entries);
327
+ const noteStripped = stripOperatorLinksSection(noteStdin);
328
+ const newBlockMd = injectLinksBeforeClose(noteStripped, linksSection);
329
+ // -------------------------------------------------------------------------
330
+ // Phase F — PATCH or CREATE (handoff.sh:289-390).
331
+ // -------------------------------------------------------------------------
332
+ if (createNew) {
333
+ // CREATE path (bash:290-326). Title from branch (or date), scrub it, and
334
+ // fall back to a date-only title if the branch name itself trips scrub.
335
+ const branch = await git.currentBranch();
336
+ const ymd = clock.todayYmd();
337
+ let title;
338
+ if (branch !== "" &&
339
+ branch !== "HEAD" &&
340
+ branch !== "main" &&
341
+ branch !== "master") {
342
+ title = `Handoff: ${branch}`;
343
+ }
344
+ else {
345
+ title = `Handoff: closeout @ ${ymd}`;
346
+ }
347
+ const titleScrub = scrubString(title, "generated issue title for new handoff");
348
+ if (!titleScrub.ok) {
349
+ title = `Handoff: ${ymd} (branch name redacted by scrub)`;
350
+ logs.push("branch name matched the secret-shaped pattern set — using a date-based title instead. Rename the branch if a descriptive title is needed.");
351
+ }
352
+ await gh.ensureHandoffLabel();
353
+ const created = await gh.issueCreate({
354
+ title,
355
+ bodyMd: newBlockMd,
356
+ label: HANDOFF_LABEL,
357
+ });
358
+ // gh issue create --label already applied the label; the explicit add
359
+ // step in the PATCH path doesn't apply here. Belt-and-braces unassign
360
+ // (idempotent; non-fatal — issueUnassignMe swallows "not assigned" per
361
+ // its contract on the GhClient port).
362
+ try {
363
+ await gh.issueUnassignMe(created.number);
364
+ }
365
+ catch (err) {
366
+ const msg = err instanceof Error ? err.message : String(err);
367
+ logs.push(`couldn't remove @me from #${created.number} (gh error): ${msg}.`);
368
+ }
369
+ logs.push(`created handoff issue #${created.number}: ${created.url}`);
370
+ return {
371
+ issueNumber: created.number,
372
+ noteUrl: created.url,
373
+ created: true,
374
+ logs,
375
+ };
376
+ }
377
+ // PATCH path (bash:327-390). Pre-PATCH drift check — the race-safety seam.
378
+ if (issueNum === undefined) {
379
+ // Defensive: createNew=false implies issueNum was set in Phase C. If we
380
+ // reach here with no issue, the orchestrator has a logic bug.
381
+ throw new HandoffError("internal error: PATCH path reached with no target issue number.");
382
+ }
383
+ let prePatch;
384
+ try {
385
+ // Slot 2 of issueView — body-bearing. Do NOT collapse with slot 1; that
386
+ // split IS the race-safety seam (Task 17 asserts this via FakeGhClient
387
+ // call-sequence). The drift check requires body to detect concurrent
388
+ // writers, so this is `issueView`, not `issueViewSlim`.
389
+ prePatch = await gh.issueView(issueNum);
390
+ }
391
+ catch {
392
+ throw new HandoffError(`could not re-fetch state for race check (gh error) — not PATCHing.`);
393
+ }
394
+ if (prePatch.state !== "OPEN") {
395
+ throw new HandoffError(`issue #${issueNum} changed between fetch and intended PATCH — state is now ${prePatch.state} (concurrent \`/accept\` may have closed it). Your note was NOT posted; re-run \`/handoff\` with a fresh target if appropriate.`);
396
+ }
397
+ const meLogin = await meCache.resolve(gh);
398
+ if (assigneesStatus(prePatch.assignees, meLogin) === "other") {
399
+ const others = assigneesOtherCsv(prePatch.assignees, meLogin);
400
+ throw new HandoffError(`issue #${issueNum} changed between fetch and intended PATCH — now assigned to @${others} (concurrent \`/accept\` claimed it). Your note was NOT posted.`);
401
+ }
402
+ if (prePatch.body !== existingBody) {
403
+ throw new HandoffError("issue body changed between fetch and intended PATCH — your note was NOT posted. Re-run `/handoff` to splice against the new body.");
404
+ }
405
+ // updatedAt drift sanity: if state+assignees+body all match but updatedAt
406
+ // changed, something else mutated (labels?) — usually benign; warn but
407
+ // proceed. (bash:360-362)
408
+ if (initialUpdatedAt !== undefined &&
409
+ prePatch.updatedAt !== initialUpdatedAt) {
410
+ logs.push(`note: issue #${issueNum}.updatedAt changed since initial fetch (state/assignees/body unchanged — likely labels/metadata only). Proceeding.`);
411
+ }
412
+ // PATCH: splice the new block into the existing body, edit, ensure label,
413
+ // ADD label (load-bearing — failure means invisible on /handoffs), then
414
+ // best-effort unassign (idempotent; non-fatal).
415
+ const newBody = spliceAgentContextBlock(existingBody, newBlockMd);
416
+ try {
417
+ await gh.issueEditBody(issueNum, newBody);
418
+ }
419
+ catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ throw new HandoffError(`gh issue edit failed — body was NOT patched: ${msg}`);
422
+ }
423
+ await gh.ensureHandoffLabel();
424
+ try {
425
+ await gh.issueAddLabel(issueNum, HANDOFF_LABEL);
426
+ }
427
+ catch {
428
+ // Label add is load-bearing: without it the issue won't show up on
429
+ // /handoffs and the protocol's "leave it visible for pickup" contract
430
+ // breaks. Hard-error so the operator knows to retry (the body PATCH
431
+ // already landed; re-running /handoff is safe and idempotent).
432
+ throw new HandoffError(`body patched but \`${HANDOFF_LABEL}\` label was NOT added (gh error) — issue #${issueNum} won't show up on /handoffs. Re-run \`/handoff ${issueNum}\` (idempotent) or add the label manually.`);
433
+ }
434
+ try {
435
+ await gh.issueUnassignMe(issueNum);
436
+ }
437
+ catch (err) {
438
+ const msg = err instanceof Error ? err.message : String(err);
439
+ logs.push(`couldn't remove @me from #${issueNum} (gh error): ${msg}. Verify the issue is unassigned with \`gh issue view ${issueNum}\`.`);
440
+ }
441
+ logs.push(`updated handoff issue #${issueNum}`);
442
+ return {
443
+ issueNumber: issueNum,
444
+ // We don't have the URL handy without an extra view; bash echoes
445
+ // `#$ISSUE`. The CLI can format it as a repo-relative ref.
446
+ noteUrl: `#${issueNum}`,
447
+ created: false,
448
+ logs,
449
+ };
450
+ }
451
+ //# sourceMappingURL=handoff-verb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-verb.js","sourceRoot":"","sources":["../../src/handoff/handoff-verb.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,gDAAgD;AAChD,yEAAyE;AACzE,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,sDAAsD;AACtD,gEAAgE;AAChE,4EAA4E;AAC5E,8EAA8E;AAC9E,2DAA2D;AAC3D,4EAA4E;AAC5E,iDAAiD;AACjD,6BAA6B;AAC7B,gFAAgF;AAChF,sDAAsD;AACtD,EAAE;AACF,0EAA0E;AAC1E,8EAA8E;AAC9E,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,uEAAuE;AACvE,uEAAuE;AACvE,4EAA4E;AAC5E,yEAAyE;AACzE,EAAE;AACF,wEAAwE;AACxE,0BAA0B;AAC1B,iEAAiE;AACjE,8EAA8E;AAC9E,uEAAuE;AACvE,4EAA4E;AAE5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAA6C,MAAM,YAAY,CAAC;AACrF,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,aAAa,GAAG,SAAS,CAAC;AAyBhC,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,SAAS,yBAAyB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,IAAI,GAAG,IAAI,CAAC;YACZ,SAAS;QACX,CAAC;QACD,IAAI,IAAI,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAClD,IAAI,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACpD,IAAI,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,IAAI,GAAG,KAAK,CAAC;YACb,SAAS,CAAC,oCAAoC;QAChD,CAAC;QACD,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,GAAG,KAAK,CAAC;YACb,uDAAuD;QACzD,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,sBAAsB,CAC7B,YAAoB,EACpB,YAAoB;IAEpB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7C,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACb,KAAK,MAAM,CAAC,IAAI,YAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC1C,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,sEAAsE;AACtE,SAAS,mBAAmB,CAAC,OAA0B;IACrD,MAAM,GAAG,GAAa,CAAC,wBAAwB,CAAC,CAAC;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC7B,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,OAAO;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,IAAY;IACvC,uDAAuD;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE;QACrB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE;KACzB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,iCAAiC;AACjC,8EAA8E;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAuB;IAEvB,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC;IACxC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IAEnC,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7B,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,4EAA4E;IAC5E,+DAA+D;IAC/D,4EAA4E;IAE5E,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,YAAY,CAAC,6CAA6C,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,YAAY,CACpB,yDAAyD,WAAW,MAAM,YAAY,uDAAuD,CAC9I,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAClD,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,4EAA4E;IAE5E,IAAI,MAAM,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,CACP,2IAA2I,CAC5I,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,0DAA0D;IAC1D,4EAA4E;IAE5E,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,QAAQ,GAAuB,IAAI,CAAC,KAAK,CAAC;IAC9C,IAAI,YAAY,GAAG,EAAE,CAAC;IACtB,IAAI,gBAAoC,CAAC;IAEzC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7B,0EAA0E;QAC1E,+CAA+C;QAC/C,IAAI,IAAI,CAAC;QACT,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,YAAY,CACpB,uBAAuB,IAAI,CAAC,KAAK,wDAAwD,CAC1F,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,YAAY,CACpB,UAAU,IAAI,CAAC,KAAK,uGAAuG,CAC5H,CAAC;QACJ,CAAC;QACD,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC;QAC1E,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,YAAY,CACpB,UAAU,IAAI,CAAC,KAAK,iCAAiC,aAAa,mIAAmI,aAAa,iBAAiB,CACpO,CAAC;YACJ,CAAC;YACD,sDAAsD;QACxD,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAC1D,MAAM,IAAI,YAAY,CACpB,UAAU,IAAI,CAAC,KAAK,8BAA8B,MAAM,uEAAuE,CAChI,CAAC;QACJ,CAAC;QACD,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC;QAClC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC;IACxB,CAAC;SAAM,CAAC;QACN,qEAAqE;QACrE,wEAAwE;QACxE,IAAI,IAAI,CAAC;QACT,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,YAAY,CACpB,8GAA8G,CAC/G,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAC1B,CAAC,EAAE,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,OAAO,CAC3D,CAAC;QACF,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAChC,CAAC,EAAE,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,OAAO,CAC3D,CAAC;QAEF,sEAAsE;QACtE,kEAAkE;QAClE,qEAAqE;QACrE,kEAAkE;QAClE,MAAM,kBAAkB,GAAG,GAAS,EAAE;YACpC,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;gBACnC,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC/D,IAAI,CAAC,IAAI,CACP,+BAA+B,KAAK,CAAC,MAAM,uBAAuB,QAAQ,yCAAyC,KAAK,CAAC,MAAM,4BAA4B,QAAQ,GAAG,CACvK,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtC,SAAS,GAAG,IAAI,CAAC;YACjB,kBAAkB,EAAE,CAAC;QACvB,CAAC;aAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;YAC5B,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;YACzB,qEAAqE;YACrE,mEAAmE;YACnE,mCAAmC;YACnC,IAAI,IAAI,CAAC;YACT,IAAI,CAAC;gBACH,IAAI,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3C,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,YAAY,CACpB,8BAA8B,MAAM,CAAC,MAAM,yCAAyC,CACrF,CAAC;YACJ,CAAC;YACD,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC;YACzB,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC;YAClC,IAAI,CAAC,IAAI,CACP,YAAY,MAAM,CAAC,MAAM,iEAAiE,CAC3F,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3D,MAAM,IAAI,YAAY,CACpB,wCAAwC,IAAI,4EAA4E,CACzH,CAAC;QACJ,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,oEAAoE;IACpE,4EAA4E;IAE5E,IAAI,OAAO,GAAa,CAAC,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAC;IAE9D,uEAAuE;IACvE,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAG,WAAW,CAC5B,QAAQ,CAAC,KAAK,EACd,8BAA8B,QAAQ,CAAC,OAAO,EAAE,CACjD,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACnB,MAAM,IAAI,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC7C,CAAC;QACD,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC3C,MAAM,WAAW,GAAG,KAAK,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,OAAO,IAAI,CAAC;QAC/D,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IAED,mEAAmE;IACnE,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACvC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;YAC7B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;YAClE,IAAI,QAAQ,KAAK,KAAK,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC5C,2EAA2E;YAC3E,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;gBAAE,OAAO,KAAK,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IAED,4EAA4E;IAC5E,IAAI,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,aAAa,EAAE,CAAC;QACzC,IACE,MAAM,KAAK,EAAE;YACb,MAAM,KAAK,MAAM;YACjB,MAAM,KAAK,MAAM;YACjB,MAAM,KAAK,QAAQ,EACnB,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;gBAC1B,MAAM,UAAU,GAAG,WAAW,CAC5B,MAAM,CAAC,KAAK,EACZ,mBAAmB,MAAM,CAAC,MAAM,QAAQ,CACzC,CAAC;gBACF,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;oBACnB,MAAM,IAAI,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBAC7C,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,MAAM,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,wDAAwD;IACxD,4EAA4E;IAE5E,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,sBAAsB,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAEtE,4EAA4E;IAC5E,kDAAkD;IAClD,4EAA4E;IAE5E,IAAI,SAAS,EAAE,CAAC;QACd,yEAAyE;QACzE,wEAAwE;QACxE,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,aAAa,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC7B,IAAI,KAAa,CAAC;QAClB,IACE,MAAM,KAAK,EAAE;YACb,MAAM,KAAK,MAAM;YACjB,MAAM,KAAK,MAAM;YACjB,MAAM,KAAK,QAAQ,EACnB,CAAC;YACD,KAAK,GAAG,YAAY,MAAM,EAAE,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,KAAK,GAAG,uBAAuB,GAAG,EAAE,CAAC;QACvC,CAAC;QACD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,uCAAuC,CAAC,CAAC;QAC/E,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACnB,KAAK,GAAG,YAAY,GAAG,kCAAkC,CAAC;YAC1D,IAAI,CAAC,IAAI,CACP,2IAA2I,CAC5I,CAAC;QACJ,CAAC;QACD,MAAM,EAAE,CAAC,kBAAkB,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC;YACnC,KAAK;YACL,MAAM,EAAE,UAAU;YAClB,KAAK,EAAE,aAAa;SACrB,CAAC,CAAC;QACH,sEAAsE;QACtE,sEAAsE;QACtE,uEAAuE;QACvE,sCAAsC;QACtC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,IAAI,CAAC,IAAI,CAAC,6BAA6B,OAAO,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,0BAA0B,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACtE,OAAO;YACL,WAAW,EAAE,OAAO,CAAC,MAAM;YAC3B,OAAO,EAAE,OAAO,CAAC,GAAG;YACpB,OAAO,EAAE,IAAI;YACb,IAAI;SACL,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,wEAAwE;QACxE,8DAA8D;QAC9D,MAAM,IAAI,YAAY,CACpB,iEAAiE,CAClE,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,wEAAwE;QACxE,uEAAuE;QACvE,qEAAqE;QACrE,wDAAwD;QACxD,QAAQ,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,YAAY,CACpB,oEAAoE,CACrE,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,YAAY,CACpB,UAAU,QAAQ,4DAA4D,QAAQ,CAAC,KAAK,iIAAiI,CAC9N,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1C,IAAI,eAAe,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,OAAO,EAAE,CAAC;QAC7D,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9D,MAAM,IAAI,YAAY,CACpB,UAAU,QAAQ,gEAAgE,MAAM,iEAAiE,CAC1J,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACnC,MAAM,IAAI,YAAY,CACpB,mIAAmI,CACpI,CAAC;IACJ,CAAC;IACD,0EAA0E;IAC1E,uEAAuE;IACvE,0BAA0B;IAC1B,IACE,gBAAgB,KAAK,SAAS;QAC9B,QAAQ,CAAC,SAAS,KAAK,gBAAgB,EACvC,CAAC;QACD,IAAI,CAAC,IAAI,CACP,gBAAgB,QAAQ,oHAAoH,CAC7I,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,wEAAwE;IACxE,gDAAgD;IAChD,MAAM,OAAO,GAAG,uBAAuB,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IAClE,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,MAAM,IAAI,YAAY,CAAC,gDAAgD,GAAG,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,EAAE,CAAC,kBAAkB,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;QACnE,sEAAsE;QACtE,oEAAoE;QACpE,+DAA+D;QAC/D,MAAM,IAAI,YAAY,CACpB,sBAAsB,aAAa,8CAA8C,QAAQ,kDAAkD,QAAQ,4CAA4C,CAChM,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,CACP,6BAA6B,QAAQ,gBAAgB,GAAG,yDAAyD,QAAQ,KAAK,CAC/H,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,0BAA0B,QAAQ,EAAE,CAAC,CAAC;IAChD,OAAO;QACL,WAAW,EAAE,QAAQ;QACrB,iEAAiE;QACjE,2DAA2D;QAC3D,OAAO,EAAE,IAAI,QAAQ,EAAE;QACvB,OAAO,EAAE,KAAK;QACd,IAAI;KACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { type Clock, type GhClient } from "./ports.js";
2
+ export interface HandoffStackRow {
3
+ readonly issueNumber: number;
4
+ readonly title: string;
5
+ readonly age: string;
6
+ readonly linkedCount: number;
7
+ readonly linkedDisplay: string;
8
+ }
9
+ export interface RunHandoffsResult {
10
+ readonly rows: readonly HandoffStackRow[];
11
+ /**
12
+ * Bash-compatible text output: header + one line per row + blank line +
13
+ * footer. No trailing newline — the CLI print layer adds it (Task 21
14
+ * snapshot parity).
15
+ */
16
+ readonly text: string;
17
+ readonly logs: readonly string[];
18
+ }
19
+ /**
20
+ * List the handoff stack (open + handoff-labeled + unassigned, sorted
21
+ * oldest → newest by updatedAt). Returns both structured rows and the
22
+ * rendered text so the CLI prints text and the MCP tool returns rows as
23
+ * structuredContent.
24
+ */
25
+ export declare function runHandoffs(opts: {
26
+ readonly gh: GhClient;
27
+ readonly clock: Clock;
28
+ }): Promise<RunHandoffsResult>;
29
+ //# sourceMappingURL=handoffs-verb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoffs-verb.d.ts","sourceRoot":"","sources":["../../src/handoff/handoffs-verb.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAgB,KAAK,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,SAAS,eAAe,EAAE,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0D7B"}
@@ -0,0 +1,84 @@
1
+ // packages/cli/src/handoff/handoffs-verb.ts
2
+ //
3
+ // /handoffs — list the handoff stack. PORT FROM dark-factory-platform
4
+ // .claude/skills/handoff/scripts/handoffs.sh@a6f711b (58 LOC bash).
5
+ //
6
+ // Returns BOTH structured rows AND rendered text. The CLI prints text;
7
+ // the MCP tool returns the rows as structuredContent.
8
+ //
9
+ // Per-row link count is computed via extractLinkedItems (in-marker scoped),
10
+ // so stale outside-marker Linked-items sections don't inflate counts —
11
+ // matches bash's fix (lib.sh@a6f711b note in handoffs.sh:39-45).
12
+ //
13
+ // Sort order is ASCENDING by updatedAt — oldest first — matching bash's
14
+ // `sort_by(.updatedAt)`. localeCompare on ISO-8601 strings is byte-equal
15
+ // to lexical comparison (the canonical Z-suffixed shape sorts correctly).
16
+ //
17
+ // Per-row age uses an explicit `epoch !== undefined` guard (not `?? 0`),
18
+ // so a malformed updatedAt renders `"?"` rather than a 1970 "Nd ago" — a
19
+ // legitimate epoch of 0 still formats normally.
20
+ //
21
+ // Empty stack returns a distinct message (NOT a row), and gh failure
22
+ // throws HandoffError (fail-closed — a transient gh error should be
23
+ // diagnosed, not silently rendered as "stack is empty").
24
+ //
25
+ // /handoffs is per-repo: gh issue list is repo-scoped, and a cross-repo
26
+ // aggregator is deferred to OQ-12.3.
27
+ import { formatAge, isoToEpoch } from "./iso.js";
28
+ import { extractLinkedItems } from "./links.js";
29
+ import { HandoffError } from "./ports.js";
30
+ import { stripControlChars } from "./strip-control.js";
31
+ /**
32
+ * List the handoff stack (open + handoff-labeled + unassigned, sorted
33
+ * oldest → newest by updatedAt). Returns both structured rows and the
34
+ * rendered text so the CLI prints text and the MCP tool returns rows as
35
+ * structuredContent.
36
+ */
37
+ export async function runHandoffs(opts) {
38
+ const { gh, clock } = opts;
39
+ const logs = [];
40
+ let list;
41
+ try {
42
+ list = await gh.issueList({ state: "open", search: "no:assignee" });
43
+ }
44
+ catch {
45
+ throw new HandoffError("gh issue list failed — cannot render the handoff stack.");
46
+ }
47
+ if (list.length === 0) {
48
+ return {
49
+ rows: [],
50
+ text: "handoff stack is empty (no open, unassigned issues labeled 'handoff').",
51
+ logs,
52
+ };
53
+ }
54
+ // Sort ascending by updatedAt (matches bash sort_by(.updatedAt)).
55
+ // localeCompare on canonical ISO-8601 (Z-suffixed) sorts byte-equal.
56
+ const sorted = [...list].sort((a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? ""));
57
+ const now = clock.nowEpoch();
58
+ const rows = sorted.map((item) => {
59
+ const title = stripControlChars(item.title);
60
+ const linkedItems = extractLinkedItems(item.body ?? "");
61
+ const linkedCount = linkedItems.length;
62
+ const linkedDisplay = linkedCount === 0 ? "none" : `${linkedCount} items`;
63
+ const epoch = item.updatedAt ? isoToEpoch(item.updatedAt) : undefined;
64
+ // Explicit undefined-check (NOT `?? 0`) — a parse failure renders "?"
65
+ // rather than a 1970-relative "Nd ago".
66
+ const age = epoch !== undefined ? formatAge(epoch, now) : "?";
67
+ return {
68
+ issueNumber: item.number,
69
+ title,
70
+ age,
71
+ linkedCount,
72
+ linkedDisplay,
73
+ };
74
+ });
75
+ const lines = [];
76
+ lines.push("Handoff stack (oldest → newest):");
77
+ for (const row of rows) {
78
+ lines.push(`#${row.issueNumber} · ${row.title} · ${row.age} · linked: ${row.linkedDisplay}`);
79
+ }
80
+ lines.push("");
81
+ lines.push("Pick one: /accept <issue>");
82
+ return { rows, text: lines.join("\n"), logs };
83
+ }
84
+ //# sourceMappingURL=handoffs-verb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoffs-verb.js","sourceRoot":"","sources":["../../src/handoff/handoffs-verb.ts"],"names":[],"mappings":"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"}