@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,31 @@
1
+ import { type GhClient } from "./ports.js";
2
+ import { type RehydrateData } from "./rehydrate-core.js";
3
+ export interface RunAcceptOptions {
4
+ readonly issue: number;
5
+ readonly gh: GhClient;
6
+ }
7
+ export interface RunAcceptResult {
8
+ readonly issueNumber: number;
9
+ /**
10
+ * Rehydrate data — CLI prints via renderRehydrateText (Task 10), MCP
11
+ * returns as structuredContent.
12
+ */
13
+ readonly rehydrate: RehydrateData;
14
+ /**
15
+ * Success-path operator info lines (each printed to stderr by the CLI).
16
+ * Contains exactly the two `log "…"` lines from accept.sh that fire on
17
+ * the success path: the assign confirmation (step 5) and the close
18
+ * confirmation (step 7). Fatal paths put their guidance in the thrown
19
+ * HandoffError message, not here.
20
+ */
21
+ readonly logs: readonly string[];
22
+ }
23
+ /**
24
+ * Take the baton on a handoff issue. Validates → refuses-on-other →
25
+ * strict-rehydrates → drift-checks → assigns → verifies → closes. Any failure
26
+ * before step 5 leaves the issue untouched on the stack; a failure between
27
+ * step 5 and step 7 leaves the issue open + assigned-to-@me (re-run is
28
+ * idempotent via the updatedAt close-failure retry path in step 4).
29
+ */
30
+ export declare function runAccept(opts: RunAcceptOptions): Promise<RunAcceptResult>;
31
+ //# sourceMappingURL=accept-verb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accept-verb.d.ts","sourceRoot":"","sources":["../../src/handoff/accept-verb.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAI9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,eAAe,CAAC,CAiJ1B"}
@@ -0,0 +1,165 @@
1
+ // packages/cli/src/handoff/accept-verb.ts
2
+ //
3
+ // /accept atomic chain. PORT FROM dark-factory-platform .claude/skills/handoff/
4
+ // scripts/accept.sh@a6f711b (147 LOC). All read-only work precedes mutations;
5
+ // a failed step leaves the issue unassigned + open on the stack.
6
+ //
7
+ // gh call sequence (the load-bearing race-safety contract — Task 18 asserts
8
+ // this order via FakeGhClient.calls()):
9
+ // step 1: gh.issueView(issue) — slot 1 (body-bearing)
10
+ // ↓
11
+ // gh.apiUserLogin() — lazy; first assigneesStatus(…, me)
12
+ // triggers it, matching bash which
13
+ // calls `me_login` inside the FIRST
14
+ // assignees_status (lib.sh:88) → not
15
+ // before step 1. Eager resolution would
16
+ // reorder apiUserLogin ahead of
17
+ // issueView and break step-1-failure
18
+ // tests + the call-sequence assertion.
19
+ // step 3: gh.issueView(issue) — slot 2 (body-bearing; inside
20
+ // deriveRehydrateData)
21
+ // + per-linked-item gh.prView or gh.issueView (linked-item counter)
22
+ // step 4: gh.issueViewSlim(issue) — slot 1 (body-less)
23
+ // step 5: gh.issueAssignMe(issue)
24
+ // step 6: gh.issueViewSlim(issue) — slot 2 (body-less)
25
+ // step 7: gh.issueClose(issue)
26
+ //
27
+ // Both step 4 and step 6 use issueViewSlim — that's a separate counter from
28
+ // the body-bearing slots (validates step 1 vs the strict-rehydrate step 3).
29
+ //
30
+ // Error-path discipline (advisor catch #1): bash prints `warn "…"` to stderr
31
+ // BEFORE `exit 1`, so the operator sees the detailed guidance ("re-run
32
+ // /accept", "coordinate with @other", etc.). The HandoffError shape is
33
+ // frozen to the v1 surface (message + savedNotePath) for the Tasks 11-22
34
+ // window — there is no field to carry an extra "log line" past a throw. So
35
+ // on every fatal path we throw the DETAILED message itself, and `logs` only
36
+ // carries the two SUCCESS-path info lines that actually reach the return
37
+ // (matches handoff-verb.ts's discipline).
38
+ import { assigneesOtherCsv, assigneesStatus, MeLoginCache, } from "./assignees.js";
39
+ import { validateLatestBlock } from "./markers.js";
40
+ import { HandoffError } from "./ports.js";
41
+ import { deriveRehydrateData } from "./rehydrate-core.js";
42
+ const HANDOFF_LABEL = "handoff";
43
+ /**
44
+ * Take the baton on a handoff issue. Validates → refuses-on-other →
45
+ * strict-rehydrates → drift-checks → assigns → verifies → closes. Any failure
46
+ * before step 5 leaves the issue untouched on the stack; a failure between
47
+ * step 5 and step 7 leaves the issue open + assigned-to-@me (re-run is
48
+ * idempotent via the updatedAt close-failure retry path in step 4).
49
+ */
50
+ export async function runAccept(opts) {
51
+ const { issue, gh } = opts;
52
+ const logs = [];
53
+ const meCache = new MeLoginCache();
54
+ // --- step 1: validate (read-only) ----------------------------------------
55
+ let view;
56
+ try {
57
+ view = await gh.issueView(issue);
58
+ }
59
+ catch {
60
+ throw new HandoffError(`could not fetch issue #${issue} (gh error) — not mutating.`);
61
+ }
62
+ if (view.state === "CLOSED") {
63
+ throw new HandoffError(`issue #${issue} is closed — the handoff was already accepted.`);
64
+ }
65
+ // (view.state is typed `"OPEN" | "CLOSED"` so the bash's catch-all
66
+ // "unexpected state" branch is unreachable here; left out.)
67
+ const hasHandoffLabel = view.labels.some((l) => l.name === HANDOFF_LABEL);
68
+ if (!hasHandoffLabel) {
69
+ throw new HandoffError(`issue #${issue} is not a handoff issue (no \`${HANDOFF_LABEL}\` label).`);
70
+ }
71
+ // The body MUST carry a parseable LATEST agent-context block (matches the
72
+ // extractor `do_rehydrate` uses — see bash:64-70). A stale-valid + new-
73
+ // malformed body would pass a first-block check yet render no reasoning,
74
+ // and accept would close the handoff while losing the artifact it's
75
+ // supposed to preserve.
76
+ if (!validateLatestBlock(view.body)) {
77
+ throw new HandoffError(`issue #${issue} has no parseable agent-context block in the body (missing, malformed, or reversed markers — or the latest block is malformed) — refusing to accept (and close) a handoff with no reasoning artifact. Use \`/handoff ${issue}\` to add a well-formed note first.`);
78
+ }
79
+ // Lazy me-login (advisor catch #2): bash calls `me_login` inside the FIRST
80
+ // `assignees_status` call (lib.sh:88), which happens AFTER the step-1
81
+ // issue view. Resolving here — after the view + body validation, just
82
+ // before we need it — matches bash's gh call order. Eager resolution at
83
+ // the top of runAccept would reorder apiUserLogin ahead of issueView in
84
+ // FakeGhClient.calls() AND fire on step-1-failure paths where bash never
85
+ // hits `gh api user`.
86
+ const me = await meCache.resolve(gh);
87
+ const initialAssignees = view.assignees;
88
+ const initialUpdatedAt = view.updatedAt;
89
+ const initialStatus = assigneesStatus(initialAssignees, me);
90
+ // --- step 2: refuse if assigned to @other --------------------------------
91
+ if (initialStatus === "other") {
92
+ const others = assigneesOtherCsv(initialAssignees, me);
93
+ throw new HandoffError(`issue #${issue} is currently assigned to @${others} — coordinate with them or ask them to re-handoff (which un-assigns).`);
94
+ }
95
+ // --- step 3: strict rehydrate (read-only — abort the chain on failure) ---
96
+ // deriveRehydrateData internally calls gh.issueView (slot 2, body-bearing)
97
+ // then sequentially per linked item. It throws HandoffError on the
98
+ // wholesale issueView failure path; we add the strict linkFailures>0 gate.
99
+ const rehydrate = await deriveRehydrateData(issue, gh);
100
+ if (rehydrate.linkFailures > 0) {
101
+ throw new HandoffError(`rehydrate failed for #${issue} — leaving on the stack; no mutation.`);
102
+ }
103
+ // --- step 4: pre-assign drift check --------------------------------------
104
+ // issueViewSlim — separate seam from the body-bearing issueView slots
105
+ // above. Slot 1 of the SLIM counter.
106
+ let driftView;
107
+ try {
108
+ driftView = await gh.issueViewSlim(issue);
109
+ }
110
+ catch {
111
+ throw new HandoffError(`could not re-fetch issue #${issue} for drift check — not mutating.`);
112
+ }
113
+ if (driftView.state !== "OPEN") {
114
+ throw new HandoffError(`issue #${issue} changed between validation and assign — state is now ${driftView.state}. Another receiver may have claimed it; re-run \`/accept\` to retry against the new state, or check \`/handoffs\` for the current stack.`);
115
+ }
116
+ const driftStatus = assigneesStatus(driftView.assignees, me);
117
+ if (driftStatus === "other") {
118
+ throw new HandoffError(`issue #${issue} changed between validation and assign — another receiver may have claimed it; re-run \`/accept\` to retry against the new state, or check \`/handoffs\` for the current stack.`);
119
+ }
120
+ // updatedAt drift handling. The close-failure retry path is the ONLY one
121
+ // allowed to pass with updatedAt drift, and it requires INITIAL was
122
+ // already @me (assign happened on a previous attempt, close failed). If
123
+ // INITIAL was empty and updatedAt drifted to @me, that's a concurrent
124
+ // assignment by another tool/automation/same-account session — abort, do
125
+ // not proceed on the stale validate snapshot.
126
+ if (driftView.updatedAt !== initialUpdatedAt) {
127
+ if (initialStatus === "me" && driftStatus === "me") {
128
+ // close-failure retry path — pass through (do nothing)
129
+ }
130
+ else {
131
+ throw new HandoffError(`issue #${issue} was edited between validation and assign (initial=${initialStatus}, drift=${driftStatus}) — re-run \`/accept\` to retry against the new state.`);
132
+ }
133
+ }
134
+ // --- step 5: assign @me (mutation 1) -------------------------------------
135
+ try {
136
+ await gh.issueAssignMe(issue);
137
+ }
138
+ catch {
139
+ throw new HandoffError(`couldn't assign @me on #${issue} (gh error) — leaving on the stack.`);
140
+ }
141
+ logs.push(`accepted #${issue} — assigned to you.`);
142
+ // --- step 6: post-assign verify (multi-assignee abort path) --------------
143
+ // Slot 2 of the SLIM counter.
144
+ let postView;
145
+ try {
146
+ postView = await gh.issueViewSlim(issue);
147
+ }
148
+ catch {
149
+ throw new HandoffError(`couldn't verify post-assign state on #${issue} — leaving open + assigned; don't close.`);
150
+ }
151
+ if (assigneesStatus(postView.assignees, me) !== "me") {
152
+ const others = assigneesOtherCsv(postView.assignees, me);
153
+ throw new HandoffError(`collision detected after assign — issue #${issue} now has assignees [${others}]. Do not close. Coordinate with the other receiver(s); the operator may \`gh issue edit ${issue} --remove-assignee <other>\` or hand the baton off explicitly.`);
154
+ }
155
+ // --- step 7: close (Commitment 10 — handoff event complete) --------------
156
+ try {
157
+ await gh.issueClose(issue);
158
+ }
159
+ catch {
160
+ throw new HandoffError(`assigned but not closed — re-run \`/accept ${issue}\` to complete the close, or close manually as an operator-acknowledged exception.`);
161
+ }
162
+ logs.push(`closed #${issue} — handoff event complete.`);
163
+ return { issueNumber: issue, rehydrate, logs };
164
+ }
165
+ //# sourceMappingURL=accept-verb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accept-verb.js","sourceRoot":"","sources":["../../src/handoff/accept-verb.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Validate a positive-integer issue number string → number. Empty/undefined
3
+ * returns undefined (the "no issue arg" path several verbs allow).
4
+ *
5
+ * Mirrors bash require_issue_number (lib.sh:46-53):
6
+ * case "${1:-}" in
7
+ * "") return 0 ;; # empty allowed
8
+ * 0|0*) die "issue must be a positive integer (got: '$1')." ;;
9
+ * *[!0-9]*) die "issue must be a positive integer (got: '$1')." ;;
10
+ * *) return 0 ;;
11
+ * esac
12
+ *
13
+ * Note: the bash impl also rejects leading-whitespace ("42 " or " 42") because
14
+ * `case` does literal matching. We match that by rejecting any string that is
15
+ * not solely digits.
16
+ */
17
+ export declare function requireIssueNumber(input: string | undefined): number | undefined;
18
+ export declare function requireSafeArgs(args: readonly string[]): void;
19
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/handoff/args.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAWhF;AAkBD,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAS7D"}
@@ -0,0 +1,68 @@
1
+ // packages/cli/src/handoff/args.ts
2
+ //
3
+ // Argument validators. PORT FROM dark-factory-platform lib.sh@a6f711b:
4
+ // require_issue_number (lines 46-53)
5
+ // require_safe_args (lines 61-75)
6
+ //
7
+ // requireSafeArgs is defense-in-depth: in the TS CLI, real argv kills the
8
+ // shell-metacharacter injection vector that the bash .md/$ARGUMENTS surface
9
+ // was defending against (the operator's payload reaches process.argv as a
10
+ // string and cannot re-enter a shell). We keep the allow-list anyway because:
11
+ // (a) MCP tool callers might pass any string, and rejecting metacharacters
12
+ // early is cheap defense-in-depth, and
13
+ // (b) it preserves byte-equivalent error messages with the bash impl for
14
+ // the ~6 ported payload-rejection tests, simplifying the case-map.
15
+ import { HandoffError } from "./ports.js";
16
+ /**
17
+ * Validate a positive-integer issue number string → number. Empty/undefined
18
+ * returns undefined (the "no issue arg" path several verbs allow).
19
+ *
20
+ * Mirrors bash require_issue_number (lib.sh:46-53):
21
+ * case "${1:-}" in
22
+ * "") return 0 ;; # empty allowed
23
+ * 0|0*) die "issue must be a positive integer (got: '$1')." ;;
24
+ * *[!0-9]*) die "issue must be a positive integer (got: '$1')." ;;
25
+ * *) return 0 ;;
26
+ * esac
27
+ *
28
+ * Note: the bash impl also rejects leading-whitespace ("42 " or " 42") because
29
+ * `case` does literal matching. We match that by rejecting any string that is
30
+ * not solely digits.
31
+ */
32
+ export function requireIssueNumber(input) {
33
+ if (input === undefined || input === "")
34
+ return undefined;
35
+ // Reject 0 (case: "0") and leading-zero (case: "0*").
36
+ if (input === "0" || /^0/.test(input)) {
37
+ throw new HandoffError(`issue must be a positive integer (got: '${input}').`);
38
+ }
39
+ // Reject anything other than bare ASCII digits.
40
+ if (!/^[0-9]+$/.test(input)) {
41
+ throw new HandoffError(`issue must be a positive integer (got: '${input}').`);
42
+ }
43
+ return Number(input);
44
+ }
45
+ /**
46
+ * Allow-list per bash require_safe_args (lib.sh:61-75). Excludes shell
47
+ * metacharacters (`;$()'`|&><!*[]`) while permitting URL-safe chars (?=%+~)
48
+ * for --link URLs like https://github.com/o/r/pull/N?tab=files.
49
+ *
50
+ * Per the bash pattern `*[!a-zA-Z0-9_/#:.,@?=%+~\ -]*`, the allowed set is:
51
+ * alphanumeric (a-zA-Z0-9)
52
+ * _ / # : . , @ ? = % + ~
53
+ * space
54
+ * - (placed at end to avoid range interpretation)
55
+ *
56
+ * Throws HandoffError with the same message wording as bash on a disallowed
57
+ * character.
58
+ */
59
+ const SAFE_ARG_PATTERN = /^[a-zA-Z0-9_/#:.,@?=%+~ \-]*$/;
60
+ export function requireSafeArgs(args) {
61
+ for (const arg of args) {
62
+ if (!SAFE_ARG_PATTERN.test(arg)) {
63
+ throw new HandoffError("argument contains disallowed characters: refusing for safety " +
64
+ "(allowed: alphanumeric / # : . , @ - _ ? = % + ~ space).");
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/handoff/args.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,EAAE;AACF,uEAAuE;AACvE,wCAAwC;AACxC,wCAAwC;AACxC,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,0EAA0E;AAC1E,8EAA8E;AAC9E,6EAA6E;AAC7E,6CAA6C;AAC7C,2EAA2E;AAC3E,yEAAyE;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAyB;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1D,sDAAsD;IACtD,IAAI,KAAK,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,YAAY,CAAC,2CAA2C,KAAK,KAAK,CAAC,CAAC;IAChF,CAAC;IACD,gDAAgD;IAChD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CAAC,2CAA2C,KAAK,KAAK,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;AAEzD,MAAM,UAAU,eAAe,CAAC,IAAuB;IACrD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,YAAY,CACpB,+DAA+D;gBAC7D,0DAA0D,CAC7D,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { GhClient } from "./ports.js";
2
+ /**
3
+ * Classification of an issue's assignees set against @me:
4
+ * - "empty" → no assignees (available on the stack)
5
+ * - "me" → exactly [@me] (same-actor update / close-failure retry)
6
+ * - "other" → any non-empty set ≠ [@me] (refuse/abort per §4.1, §4.3 step 4)
7
+ *
8
+ * Note: "[@me, @me]" (a duplicated @me — defensive against GitHub returning
9
+ * duplicates) classifies as "other" because cardinality must be 1 for "me".
10
+ */
11
+ export type ClaimStatus = "empty" | "me" | "other";
12
+ /**
13
+ * Classify an assignees array. PORT FROM bash assignees_status (lib.sh:88-96):
14
+ * count == 0 → "empty"
15
+ * count == 1 && [@me] → "me"
16
+ * otherwise → "other"
17
+ */
18
+ export declare function assigneesStatus(assignees: ReadonlyArray<{
19
+ login: string;
20
+ }>, meLogin: string): ClaimStatus;
21
+ /**
22
+ * Comma-joined list of assignees with @me filtered out. Used by refuse-
23
+ * messages ("currently assigned to @<csv>"). Preserves input order.
24
+ * PORT FROM bash assignees_other_csv (lib.sh:99-103).
25
+ */
26
+ export declare function assigneesOtherCsv(assignees: ReadonlyArray<{
27
+ login: string;
28
+ }>, meLogin: string): string;
29
+ /**
30
+ * Cache for the @me login lookup. PORT FROM bash me_login + ME_LOGIN_CACHE
31
+ * (lib.sh:34-41). One API hit per process; cached across repeated callers.
32
+ */
33
+ export declare class MeLoginCache {
34
+ private cache;
35
+ resolve(gh: GhClient): Promise<string>;
36
+ }
37
+ //# sourceMappingURL=assignees.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignees.d.ts","sourceRoot":"","sources":["../../src/handoff/assignees.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3C,OAAO,EAAE,MAAM,GACd,WAAW,CAOb;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3C,OAAO,EAAE,MAAM,GACd,MAAM,CAKR;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAqB;IAC5B,OAAO,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;CAM7C"}
@@ -0,0 +1,46 @@
1
+ // packages/cli/src/handoff/assignees.ts
2
+ //
3
+ // Assignees status + me-login cache. PORT FROM dark-factory-platform
4
+ // .claude/skills/handoff/scripts/lib.sh@a6f711b lines 32-41 (me_login + cache),
5
+ // 88-103 (assignees_status + assignees_other_csv).
6
+ /**
7
+ * Classify an assignees array. PORT FROM bash assignees_status (lib.sh:88-96):
8
+ * count == 0 → "empty"
9
+ * count == 1 && [@me] → "me"
10
+ * otherwise → "other"
11
+ */
12
+ export function assigneesStatus(assignees, meLogin) {
13
+ if (assignees.length === 0)
14
+ return "empty";
15
+ // Length-checked, but TS with noUncheckedIndexedAccess needs the explicit
16
+ // binding to narrow `assignees[0]` from `T | undefined` to `T`.
17
+ const first = assignees[0];
18
+ if (assignees.length === 1 && first !== undefined && first.login === meLogin)
19
+ return "me";
20
+ return "other";
21
+ }
22
+ /**
23
+ * Comma-joined list of assignees with @me filtered out. Used by refuse-
24
+ * messages ("currently assigned to @<csv>"). Preserves input order.
25
+ * PORT FROM bash assignees_other_csv (lib.sh:99-103).
26
+ */
27
+ export function assigneesOtherCsv(assignees, meLogin) {
28
+ return assignees
29
+ .map((a) => a.login)
30
+ .filter((l) => l !== meLogin)
31
+ .join(",");
32
+ }
33
+ /**
34
+ * Cache for the @me login lookup. PORT FROM bash me_login + ME_LOGIN_CACHE
35
+ * (lib.sh:34-41). One API hit per process; cached across repeated callers.
36
+ */
37
+ export class MeLoginCache {
38
+ cache;
39
+ async resolve(gh) {
40
+ if (this.cache === undefined) {
41
+ this.cache = await gh.apiUserLogin();
42
+ }
43
+ return this.cache;
44
+ }
45
+ }
46
+ //# sourceMappingURL=assignees.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignees.js","sourceRoot":"","sources":["../../src/handoff/assignees.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,gFAAgF;AAChF,mDAAmD;AAenD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,SAA2C,EAC3C,OAAe;IAEf,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAC3C,0EAA0E;IAC1E,gEAAgE;IAChE,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1F,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,SAA2C,EAC3C,OAAe;IAEf,OAAO,SAAS;SACb,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;SACnB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC;SAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,YAAY;IACf,KAAK,CAAqB;IAClC,KAAK,CAAC,OAAO,CAAC,EAAY;QACxB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC,YAAY,EAAE,CAAC;QACvC,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF"}
@@ -0,0 +1,34 @@
1
+ import { type Clock, type GhClient, type GitClient } from "./ports.js";
2
+ export interface RunHandoffOptions {
3
+ readonly noteStdin: string;
4
+ readonly issue?: number;
5
+ readonly link?: readonly string[];
6
+ readonly unlink?: readonly string[];
7
+ readonly forceNew?: boolean;
8
+ readonly gh: GhClient;
9
+ readonly git: GitClient;
10
+ readonly clock: Clock;
11
+ }
12
+ export interface RunHandoffResult {
13
+ readonly issueNumber: number;
14
+ readonly noteUrl: string;
15
+ readonly created: boolean;
16
+ /** Operator-facing warn/log lines (each line printed to stderr by the CLI). */
17
+ readonly logs: readonly string[];
18
+ }
19
+ /**
20
+ * Put a work-stream on the handoff stack. Upserts the operator's
21
+ * marker-bounded rehydration note as a dedicated handoff GitHub Issue body,
22
+ * maintains its Linked-items section + label + leaves it unassigned. NO
23
+ * `git push` (Decision D4).
24
+ *
25
+ * Phases mirror the bash:
26
+ * A: validate stdin — markers + secret-scrub, no gh
27
+ * B: dirty worktree — warn, not refuse
28
+ * C: resolve target — explicit issue OR no-arg eligibility partition
29
+ * D: linked-items — extract → --link (with title scrub) → --unlink → auto-link
30
+ * E: compose new note — strip operator's stale section, inject script section
31
+ * F: PATCH or CREATE — with pre-PATCH race-safety drift check
32
+ */
33
+ export declare function runHandoff(opts: RunHandoffOptions): Promise<RunHandoffResult>;
34
+ //# sourceMappingURL=handoff-verb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-verb.d.ts","sourceRoot":"","sources":["../../src/handoff/handoff-verb.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAgB,KAAK,KAAK,EAAE,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AASrF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAyGD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAsW3B"}