hyper-pm 0.1.4 → 0.1.6

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 (4) hide show
  1. package/README.md +23 -11
  2. package/dist/index.cjs +1530 -886
  3. package/dist/main.cjs +1530 -886
  4. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -11540,6 +11540,103 @@ var normalizeTicketBranchListFromPayloadValue = (value) => {
11540
11540
  return normalizeTicketBranchListFromStrings(strings);
11541
11541
  };
11542
11542
 
11543
+ // src/lib/ticket-depends-on.ts
11544
+ var normalizeTicketDependsOnIds = (ids) => {
11545
+ const out = [];
11546
+ const seen = /* @__PURE__ */ new Set();
11547
+ for (const raw of ids) {
11548
+ const t = raw.trim();
11549
+ if (t === "") continue;
11550
+ if (seen.has(t)) continue;
11551
+ seen.add(t);
11552
+ out.push(t);
11553
+ }
11554
+ return out;
11555
+ };
11556
+ var ticketDependsOnListsEqual = (a, b) => {
11557
+ const an = normalizeTicketDependsOnIds(a ?? []);
11558
+ const bn = normalizeTicketDependsOnIds(b ?? []);
11559
+ if (an.length !== bn.length) return false;
11560
+ return an.every((x, i) => x === bn[i]);
11561
+ };
11562
+ var parseTicketDependsOnFromPayloadValue = (value) => {
11563
+ if (!Array.isArray(value)) return void 0;
11564
+ const strings = [];
11565
+ for (const x of value) {
11566
+ if (typeof x !== "string") return void 0;
11567
+ strings.push(x);
11568
+ }
11569
+ return normalizeTicketDependsOnIds(strings);
11570
+ };
11571
+ var parseTicketDependsOnFromFenceValue = (value) => {
11572
+ if (!Array.isArray(value)) return void 0;
11573
+ const strings = [];
11574
+ for (const x of value) {
11575
+ if (typeof x === "string") strings.push(x);
11576
+ }
11577
+ return normalizeTicketDependsOnIds(strings);
11578
+ };
11579
+ var wouldTicketDependsOnCreateCycle = (params) => {
11580
+ const { fromTicketId, nextDependsOn, successorsFor } = params;
11581
+ const dfsFromPrerequisite = (start) => {
11582
+ const stack = [start];
11583
+ const visited = /* @__PURE__ */ new Set();
11584
+ while (stack.length > 0) {
11585
+ const node = stack.pop();
11586
+ if (node === fromTicketId) return true;
11587
+ if (visited.has(node)) continue;
11588
+ visited.add(node);
11589
+ const next = successorsFor(node) ?? [];
11590
+ for (let i = next.length - 1; i >= 0; i -= 1) {
11591
+ stack.push(next[i]);
11592
+ }
11593
+ }
11594
+ return false;
11595
+ };
11596
+ for (const p of nextDependsOn) {
11597
+ if (dfsFromPrerequisite(p)) return true;
11598
+ }
11599
+ return false;
11600
+ };
11601
+ var ticketDependsOnSuccessorsForProjection = (projection, fromTicketId, nextDependsOn) => {
11602
+ return (ticketId) => {
11603
+ if (ticketId === fromTicketId) {
11604
+ return nextDependsOn.length > 0 ? nextDependsOn : void 0;
11605
+ }
11606
+ const row = projection.tickets.get(ticketId);
11607
+ if (!row || row.deleted) return void 0;
11608
+ return row.dependsOn;
11609
+ };
11610
+ };
11611
+ var validateTicketDependsOnForWrite = (params) => {
11612
+ const { projection, fromTicketId, nextDependsOn } = params;
11613
+ for (const id of nextDependsOn) {
11614
+ if (id === fromTicketId) {
11615
+ return `Ticket cannot depend on itself (${id})`;
11616
+ }
11617
+ const row = projection.tickets.get(id);
11618
+ if (row === void 0) {
11619
+ return `Dependency ticket not found: ${id}`;
11620
+ }
11621
+ if (row.deleted) {
11622
+ return `Dependency ticket deleted: ${id}`;
11623
+ }
11624
+ }
11625
+ const successorsFor = ticketDependsOnSuccessorsForProjection(
11626
+ projection,
11627
+ fromTicketId,
11628
+ nextDependsOn
11629
+ );
11630
+ if (wouldTicketDependsOnCreateCycle({
11631
+ fromTicketId,
11632
+ nextDependsOn,
11633
+ successorsFor
11634
+ })) {
11635
+ return "Ticket dependencies would create a cycle";
11636
+ }
11637
+ return void 0;
11638
+ };
11639
+
11543
11640
  // src/lib/ticket-planning-fields.ts
11544
11641
  var PRIORITY_SET = /* @__PURE__ */ new Set([
11545
11642
  "low",
@@ -11708,6 +11805,9 @@ var formatOutput = (format, value) => {
11708
11805
  return JSON.stringify(value);
11709
11806
  };
11710
11807
 
11808
+ // src/cli/normalize-raw-cli-argv.ts
11809
+ var normalizeRawCliArgv = (argv) => argv.map((token) => token === "--no-github" ? "--skip-network" : token);
11810
+
11711
11811
  // src/cli/prune-unchanged-work-item-update-payload.ts
11712
11812
  var branchListsEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
11713
11813
  var pruneEpicOrStoryUpdatePayloadAgainstRow = (cur, draft) => {
@@ -11763,6 +11863,20 @@ var pruneTicketUpdatePayloadAgainstRow = (cur, draft) => {
11763
11863
  out["labels"] = draft["labels"];
11764
11864
  }
11765
11865
  }
11866
+ if (draft["dependsOn"] !== void 0) {
11867
+ if (draft["dependsOn"] === null) {
11868
+ if (!ticketDependsOnListsEqual(cur.dependsOn, [])) {
11869
+ out["dependsOn"] = null;
11870
+ }
11871
+ } else {
11872
+ const parsed = parseTicketDependsOnFromPayloadValue(draft["dependsOn"]);
11873
+ if (parsed !== void 0) {
11874
+ if (!ticketDependsOnListsEqual(cur.dependsOn, parsed)) {
11875
+ out["dependsOn"] = parsed;
11876
+ }
11877
+ }
11878
+ }
11879
+ }
11766
11880
  if (draft["priority"] !== void 0) {
11767
11881
  const curP = cur.priority ?? null;
11768
11882
  const nextP = draft["priority"] === null ? null : draft["priority"];
@@ -11929,6 +12043,20 @@ var workItemUpdateAspects = (kind, payload) => {
11929
12043
  }
11930
12044
  }
11931
12045
  }
12046
+ if (kind === "ticket" && payload["dependsOn"] !== void 0) {
12047
+ if (payload["dependsOn"] === null) {
12048
+ aspects.push("cleared ticket dependencies");
12049
+ } else {
12050
+ const d = normalizeBranchList(payload["dependsOn"]);
12051
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12052
+ aspects.push(`set ticket dependencies to (${d.join(", ")})`);
12053
+ } else if (d.length > MAX_BRANCH_NAMES_TO_LIST) {
12054
+ aspects.push("updated ticket dependencies");
12055
+ } else {
12056
+ aspects.push("cleared ticket dependencies");
12057
+ }
12058
+ }
12059
+ }
11932
12060
  if (kind === "ticket" && payload["priority"] !== void 0) {
11933
12061
  if (payload["priority"] === null) {
11934
12062
  aspects.push("cleared priority");
@@ -12035,6 +12163,7 @@ var buildAuditLinkMetadata = (evt, githubRepo) => {
12035
12163
  if (p["status"] !== void 0) meta["status"] = String(p["status"]);
12036
12164
  if (p["assignee"] !== void 0) meta["assignee"] = p["assignee"];
12037
12165
  if (p["labels"] !== void 0) meta["labels"] = p["labels"];
12166
+ if (p["dependsOn"] !== void 0) meta["dependsOn"] = p["dependsOn"];
12038
12167
  if (p["priority"] !== void 0) meta["priority"] = p["priority"];
12039
12168
  if (p["size"] !== void 0) meta["size"] = p["size"];
12040
12169
  if (p["estimate"] !== void 0) meta["estimate"] = p["estimate"];
@@ -12102,6 +12231,14 @@ var formatEpicStoryTicketCreated = (evt, entity) => {
12102
12231
  parts.push("with labels");
12103
12232
  }
12104
12233
  }
12234
+ if (p["dependsOn"] !== void 0) {
12235
+ const d = normalizeBranchList(p["dependsOn"]);
12236
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12237
+ parts.push(`depending on (${d.join(", ")})`);
12238
+ } else if (d.length > MAX_BRANCH_NAMES_TO_LIST) {
12239
+ parts.push("with ticket dependencies");
12240
+ }
12241
+ }
12105
12242
  if (p["priority"] !== void 0) {
12106
12243
  parts.push(`with priority ${quoteStatus(String(p["priority"]))}`);
12107
12244
  }
@@ -12195,6 +12332,18 @@ var formatAuditHumanSentence = (evt) => {
12195
12332
  bits.push("updated labels");
12196
12333
  }
12197
12334
  }
12335
+ if (p["dependsOn"] !== void 0) {
12336
+ if (p["dependsOn"] === null) {
12337
+ bits.push("cleared ticket dependencies");
12338
+ } else {
12339
+ const d = normalizeBranchList(p["dependsOn"]);
12340
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12341
+ bits.push(`dependencies: ${d.join(", ")}`);
12342
+ } else {
12343
+ bits.push("updated ticket dependencies");
12344
+ }
12345
+ }
12346
+ }
12198
12347
  if (p["priority"] !== void 0) {
12199
12348
  bits.push("updated priority");
12200
12349
  }
@@ -12474,6 +12623,13 @@ var ticketMatchesTicketListQuery = (ticket, projection, query) => {
12474
12623
  return false;
12475
12624
  }
12476
12625
  }
12626
+ const dependsOnIncludesId = query.dependsOnIncludesId;
12627
+ if (dependsOnIncludesId !== void 0) {
12628
+ const deps = ticket.dependsOn ?? [];
12629
+ if (!deps.includes(dependsOnIncludesId)) {
12630
+ return false;
12631
+ }
12632
+ }
12477
12633
  return true;
12478
12634
  };
12479
12635
 
@@ -12626,6 +12782,7 @@ var sortTicketRecordsForList = (tickets, field, dir) => [...tickets].sort((x, y)
12626
12782
  // src/cli/list-projection-summaries.ts
12627
12783
  var listActiveEpicSummaries = (projection) => [...projection.epics.values()].filter((e) => !e.deleted).map((e) => ({
12628
12784
  id: e.id,
12785
+ number: e.number,
12629
12786
  title: e.title,
12630
12787
  status: e.status,
12631
12788
  createdAt: e.createdAt,
@@ -12637,6 +12794,7 @@ var listActiveStorySummaries = (projection, options) => [...projection.stories.v
12637
12794
  (s) => !s.deleted && (options?.epicId === void 0 || s.epicId === options.epicId)
12638
12795
  ).map((s) => ({
12639
12796
  id: s.id,
12797
+ number: s.number,
12640
12798
  title: s.title,
12641
12799
  epicId: s.epicId,
12642
12800
  status: s.status,
@@ -12669,6 +12827,7 @@ var listActiveTicketSummaries = (projection, options) => {
12669
12827
  const last = recent !== void 0 && recent.length > 0 ? recent[recent.length - 1] : void 0;
12670
12828
  return {
12671
12829
  id: t.id,
12830
+ number: t.number,
12672
12831
  title: t.title,
12673
12832
  status: t.status,
12674
12833
  storyId: t.storyId,
@@ -12679,6 +12838,7 @@ var listActiveTicketSummaries = (projection, options) => {
12679
12838
  ...t.estimate !== void 0 ? { estimate: t.estimate } : {},
12680
12839
  ...t.startWorkAt !== void 0 ? { startWorkAt: t.startWorkAt } : {},
12681
12840
  ...t.targetFinishAt !== void 0 ? { targetFinishAt: t.targetFinishAt } : {},
12841
+ ...t.dependsOn !== void 0 && t.dependsOn.length > 0 ? { dependsOn: t.dependsOn } : {},
12682
12842
  ...t.linkedBranches.length > 0 ? { linkedBranches: t.linkedBranches } : {},
12683
12843
  ...last !== void 0 ? {
12684
12844
  lastPrActivity: {
@@ -12775,6 +12935,17 @@ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12775
12935
  }
12776
12936
  }
12777
12937
  }
12938
+ if (Object.prototype.hasOwnProperty.call(meta, "depends_on")) {
12939
+ const v = meta["depends_on"];
12940
+ if (v === null) {
12941
+ out["dependsOn"] = null;
12942
+ } else {
12943
+ const parsed = parseTicketDependsOnFromFenceValue(v);
12944
+ if (parsed !== void 0) {
12945
+ out["dependsOn"] = parsed;
12946
+ }
12947
+ }
12948
+ }
12778
12949
  return out;
12779
12950
  };
12780
12951
  var buildGithubIssueBody = (params) => {
@@ -12800,6 +12971,9 @@ var buildGithubIssueBody = (params) => {
12800
12971
  if (p.targetFinishAt !== void 0) {
12801
12972
  meta.target_finish_at = p.targetFinishAt;
12802
12973
  }
12974
+ if (p.dependsOn !== void 0 && p.dependsOn.length > 0) {
12975
+ meta.depends_on = p.dependsOn;
12976
+ }
12803
12977
  }
12804
12978
  return `${params.description.trim()}
12805
12979
 
@@ -12825,6 +12999,9 @@ var ticketPlanningForGithubIssueBody = (ticket) => {
12825
12999
  if (ticket.targetFinishAt !== void 0) {
12826
13000
  out.targetFinishAt = ticket.targetFinishAt;
12827
13001
  }
13002
+ if (ticket.dependsOn !== void 0 && ticket.dependsOn.length > 0) {
13003
+ out.dependsOn = ticket.dependsOn;
13004
+ }
12828
13005
  return Object.keys(out).length > 0 ? out : void 0;
12829
13006
  };
12830
13007
 
@@ -13019,750 +13196,302 @@ var partitionGithubIssuesForImport = (params) => {
13019
13196
  }
13020
13197
  return { candidates, skipped };
13021
13198
  };
13022
- var mergeTicketImportCreatePayload = (ticketId, base, storyId) => {
13199
+ var mergeTicketImportCreatePayload = (ticketId, base, storyId, number) => {
13023
13200
  const storyTrimmed = storyId !== void 0 && storyId !== "" ? storyId.trim() : void 0;
13024
13201
  const storyPayload = storyTrimmed !== void 0 && storyTrimmed !== "" ? { storyId: storyTrimmed } : {};
13025
- return { id: ticketId, ...base, ...storyPayload };
13202
+ return { id: ticketId, number, ...base, ...storyPayload };
13026
13203
  };
13027
13204
 
13028
- // src/config/hyper-pm-config.ts
13029
- var hyperPmConfigSchema = external_exports.object({
13030
- schema: external_exports.literal(1),
13031
- dataBranch: external_exports.string().min(1),
13032
- remote: external_exports.string().min(1).default("origin"),
13033
- sync: external_exports.enum(["off", "outbound", "full"]).default("outbound"),
13034
- githubRepo: external_exports.string().optional(),
13035
- issueMapping: external_exports.enum(["ticket", "story", "epic"]).default("ticket")
13036
- });
13037
-
13038
- // src/config/load-config.ts
13039
- var import_promises = require("node:fs/promises");
13040
- var import_node_path = require("node:path");
13041
- var CONFIG_DIR = ".hyper-pm";
13042
- var CONFIG_FILE = "config.json";
13043
- var getHyperPmConfigPath = (repoRoot) => {
13044
- return (0, import_node_path.join)(repoRoot, CONFIG_DIR, CONFIG_FILE);
13045
- };
13046
- var loadHyperPmConfig = async (repoRoot, overrides = {}) => {
13047
- const raw = await (0, import_promises.readFile)(getHyperPmConfigPath(repoRoot), "utf8");
13048
- const parsed = JSON.parse(raw);
13049
- const base = hyperPmConfigSchema.parse(parsed);
13050
- const merged = { ...base, ...stripUndefined(overrides) };
13051
- return hyperPmConfigSchema.parse(merged);
13052
- };
13053
- var stripUndefined = (obj) => {
13054
- const out = {};
13055
- for (const [k, v] of Object.entries(obj)) {
13056
- if (v !== void 0) out[k] = v;
13205
+ // src/lib/github-pr-activity.ts
13206
+ var GITHUB_PR_ACTIVITY_RECENT_CAP = 20;
13207
+ var githubPrActivityKindSchema = external_exports.enum([
13208
+ "opened",
13209
+ "updated",
13210
+ "commented",
13211
+ "reviewed",
13212
+ "merged",
13213
+ "closed",
13214
+ "ready_for_review"
13215
+ ]);
13216
+ var githubPrReviewStateSchema = external_exports.enum([
13217
+ "approved",
13218
+ "changes_requested",
13219
+ "commented"
13220
+ ]);
13221
+ var buildPrOpenSourceId = (ticketId, prNumber) => `hyper-pm:pr-open:${ticketId}:${prNumber}`;
13222
+ var parseGithubPrActivityPayload = (payload) => {
13223
+ const ticketId = payload["ticketId"];
13224
+ const prRaw = payload["prNumber"];
13225
+ const kindRaw = payload["kind"];
13226
+ const occurredAt = payload["occurredAt"];
13227
+ const sourceId = payload["sourceId"];
13228
+ if (typeof ticketId !== "string" || typeof occurredAt !== "string" || typeof sourceId !== "string") {
13229
+ return void 0;
13230
+ }
13231
+ const prNumber = typeof prRaw === "number" ? prRaw : Number(prRaw);
13232
+ if (!Number.isFinite(prNumber)) return void 0;
13233
+ const kindParsed = githubPrActivityKindSchema.safeParse(kindRaw);
13234
+ if (!kindParsed.success) return void 0;
13235
+ const out = {
13236
+ prNumber,
13237
+ kind: kindParsed.data,
13238
+ occurredAt,
13239
+ sourceId
13240
+ };
13241
+ const url = payload["url"];
13242
+ if (typeof url === "string" && url.length > 0) {
13243
+ out.url = url;
13244
+ }
13245
+ const rs = githubPrReviewStateSchema.safeParse(payload["reviewState"]);
13246
+ if (rs.success) {
13247
+ out.reviewState = rs.data;
13057
13248
  }
13058
13249
  return out;
13059
13250
  };
13060
13251
 
13061
- // src/config/save-config.ts
13062
- var import_promises2 = require("node:fs/promises");
13063
- var import_node_path2 = require("node:path");
13064
- var saveHyperPmConfig = async (repoRoot, config) => {
13065
- const target = getHyperPmConfigPath(repoRoot);
13066
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(target), { recursive: true });
13067
- await (0, import_promises2.writeFile)(target, `${JSON.stringify(config, null, 2)}
13068
- `, "utf8");
13252
+ // src/storage/projection.ts
13253
+ var readOptionalPositiveIntegerFromPayload = (payload, key) => {
13254
+ if (!Object.prototype.hasOwnProperty.call(payload, key)) return void 0;
13255
+ const raw = payload[key];
13256
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
13257
+ if (!Number.isInteger(raw)) return void 0;
13258
+ if (raw < 1 || raw > Number.MAX_SAFE_INTEGER) return void 0;
13259
+ return raw;
13069
13260
  };
13070
-
13071
- // src/doctor/run-doctor.ts
13072
- var runDoctorOnLines = (lines) => {
13073
- const issues = [];
13074
- for (let i = 0; i < lines.length; i++) {
13075
- const line = lines[i]?.trim() ?? "";
13076
- if (!line) continue;
13077
- let json;
13078
- try {
13079
- json = JSON.parse(line);
13080
- } catch (e) {
13081
- issues.push({
13082
- kind: "invalid-json",
13083
- line: i + 1,
13084
- message: e instanceof Error ? e.message : "parse error"
13085
- });
13086
- return issues;
13087
- }
13088
- const parsed = eventLineSchema.safeParse(json);
13089
- if (!parsed.success) {
13090
- issues.push({
13091
- kind: "invalid-event",
13092
- line: i + 1,
13093
- message: parsed.error.message
13094
- });
13095
- return issues;
13096
- }
13261
+ var maxNumberAmongRows = (rows) => {
13262
+ let m = 0;
13263
+ for (const r of rows) {
13264
+ if (r.number > m) m = r.number;
13097
13265
  }
13098
- return issues;
13266
+ return m;
13099
13267
  };
13100
-
13101
- // src/git/create-and-checkout-branch.ts
13102
- var createAndCheckoutBranch = async (opts) => {
13103
- await opts.runGit(opts.repoRoot, [
13104
- "switch",
13105
- "-c",
13106
- opts.branchName,
13107
- opts.startPoint
13108
- ]);
13268
+ var maxEpicNumberInProjection = (projection) => maxNumberAmongRows(projection.epics.values());
13269
+ var maxStoryNumberInProjection = (projection) => maxNumberAmongRows(projection.stories.values());
13270
+ var maxTicketNumberInProjection = (projection) => maxNumberAmongRows(projection.tickets.values());
13271
+ var nextEpicNumberForCreate = (projection) => maxEpicNumberInProjection(projection) + 1;
13272
+ var nextStoryNumberForCreate = (projection) => maxStoryNumberInProjection(projection) + 1;
13273
+ var nextTicketNumberForCreate = (projection) => maxTicketNumberInProjection(projection) + 1;
13274
+ var resolveWorkItemCreateNumber = (projection, kind, payload) => {
13275
+ const explicit = readOptionalPositiveIntegerFromPayload(payload, "number");
13276
+ if (explicit !== void 0) {
13277
+ return explicit;
13278
+ }
13279
+ return (kind === "epic" ? maxEpicNumberInProjection(projection) : kind === "story" ? maxStoryNumberInProjection(projection) : maxTicketNumberInProjection(projection)) + 1;
13109
13280
  };
13110
-
13111
- // src/git/data-worktree-session.ts
13112
- var import_promises3 = require("node:fs/promises");
13113
- var import_node_path3 = require("node:path");
13114
- var ensureDir = async (path) => {
13115
- await (0, import_promises3.mkdir)(path, { recursive: true });
13281
+ var emptyProjection = () => ({
13282
+ epics: /* @__PURE__ */ new Map(),
13283
+ stories: /* @__PURE__ */ new Map(),
13284
+ tickets: /* @__PURE__ */ new Map()
13285
+ });
13286
+ var parsePrRefs = (body) => {
13287
+ const out = /* @__PURE__ */ new Set();
13288
+ const re = /\b(?:Closes|Refs|Fixes)\s+#(\d+)\b/gi;
13289
+ let m = re.exec(body);
13290
+ while (m !== null) {
13291
+ out.add(Number(m[1]));
13292
+ m = re.exec(body);
13293
+ }
13294
+ return [...out];
13116
13295
  };
13117
- var defaultPathExists = async (path) => {
13118
- try {
13119
- await (0, import_promises3.access)(path);
13120
- return true;
13121
- } catch {
13122
- return false;
13296
+ var applyTicketAssigneeFromPayload = (row, payload) => {
13297
+ if (!Object.prototype.hasOwnProperty.call(payload, "assignee")) return;
13298
+ const v = payload["assignee"];
13299
+ if (v === null) {
13300
+ delete row.assignee;
13301
+ return;
13123
13302
  }
13303
+ if (typeof v !== "string") return;
13304
+ const n = normalizeGithubLogin(v);
13305
+ if (n === "") delete row.assignee;
13306
+ else row.assignee = n;
13124
13307
  };
13125
- var parseDataBranchWorktreeFromPorcelain = (stdout, dataBranch) => {
13126
- const wantRef = `refs/heads/${dataBranch}`;
13127
- const blocks = stdout.split(/\n\n+/).map((b) => b.trim()).filter(Boolean);
13128
- for (const block of blocks) {
13129
- const lines = block.split("\n");
13130
- let worktreePath;
13131
- let branch;
13132
- for (const line of lines) {
13133
- if (line.startsWith("worktree ")) {
13134
- worktreePath = line.slice("worktree ".length);
13135
- } else if (line.startsWith("branch ")) {
13136
- branch = line.slice("branch ".length);
13137
- }
13138
- }
13139
- if (worktreePath !== void 0 && branch === wantRef) {
13140
- return worktreePath;
13141
- }
13308
+ var storyIdFromTicketCreatedPayload = (payload) => {
13309
+ if (!Object.prototype.hasOwnProperty.call(payload, "storyId")) {
13310
+ return null;
13142
13311
  }
13143
- return void 0;
13312
+ const v = payload["storyId"];
13313
+ if (v === null) return null;
13314
+ if (typeof v !== "string") return null;
13315
+ const t = v.trim();
13316
+ return t === "" ? null : t;
13144
13317
  };
13145
- var openDataBranchWorktree = async (opts) => {
13146
- const pathExists = opts.pathExists ?? defaultPathExists;
13147
- const { stdout: listOut } = await opts.runGit(opts.repoRoot, [
13148
- "worktree",
13149
- "list",
13150
- "--porcelain"
13151
- ]);
13152
- const listedPath = parseDataBranchWorktreeFromPorcelain(
13153
- listOut,
13154
- opts.dataBranch
13155
- );
13156
- if (listedPath !== void 0 && await pathExists(listedPath)) {
13157
- const dispose2 = async () => {
13158
- return;
13159
- };
13160
- return { worktreePath: listedPath, dispose: dispose2 };
13161
- }
13162
- const worktreePath = (0, import_node_path3.join)(
13163
- opts.tmpBase,
13164
- `hyper-pm-worktree-${ulid().toLowerCase()}`
13165
- );
13166
- await ensureDir(opts.tmpBase);
13167
- try {
13168
- await opts.runGit(opts.repoRoot, [
13169
- "worktree",
13170
- "add",
13171
- worktreePath,
13172
- opts.dataBranch
13173
- ]);
13174
- } catch (err) {
13175
- await (0, import_promises3.rm)(worktreePath, { recursive: true, force: true }).catch(() => {
13176
- });
13177
- throw err;
13178
- }
13179
- const dispose = async () => {
13180
- if (opts.keepWorktree) {
13181
- return;
13182
- }
13183
- await opts.runGit(opts.repoRoot, ["worktree", "remove", "--force", worktreePath]).catch(() => {
13184
- });
13185
- await (0, import_promises3.rm)(worktreePath, { recursive: true, force: true }).catch(() => {
13186
- });
13187
- };
13188
- return { worktreePath, dispose };
13189
- };
13190
-
13191
- // src/git/find-git-root.ts
13192
- var findGitRoot = async (cwd, deps) => {
13193
- const { stdout } = await deps.runGit(cwd, ["rev-parse", "--show-toplevel"]);
13194
- return stdout;
13195
- };
13196
-
13197
- // src/git/init-orphan-data-branch.ts
13198
- var import_promises4 = require("node:fs/promises");
13199
- var import_node_path4 = require("node:path");
13200
- var initOrphanDataBranchInWorktree = async (opts) => {
13201
- const { stdout: tip } = await opts.runGit(opts.repoRoot, [
13202
- "rev-parse",
13203
- "HEAD"
13204
- ]);
13205
- const tipCommit = tip.trim();
13206
- await opts.runGit(opts.repoRoot, [
13207
- "worktree",
13208
- "add",
13209
- opts.worktreePath,
13210
- tipCommit
13211
- ]);
13212
- await opts.runGit(opts.worktreePath, [
13213
- "checkout",
13214
- "--orphan",
13215
- opts.dataBranch
13216
- ]);
13217
- await opts.runGit(opts.worktreePath, ["rm", "-rf", "."]).catch(() => {
13218
- });
13219
- const marker = (0, import_node_path4.join)(opts.worktreePath, "README.hyper-pm.md");
13220
- await (0, import_promises4.writeFile)(
13221
- marker,
13222
- "# hyper-pm data branch\n\nAppend-only events live under `events/`.\n",
13223
- "utf8"
13224
- );
13225
- await opts.runGit(opts.worktreePath, ["add", "."]);
13226
- await opts.runGit(opts.worktreePath, [
13227
- "commit",
13228
- "-m",
13229
- "init hyper-pm data branch"
13230
- ]);
13231
- };
13232
-
13233
- // src/git/pick-unique-local-branch-name.ts
13234
- var DEFAULT_MAX_SUFFIX = 1e3;
13235
- var localBranchRefExists = async (repoRoot, name, git) => {
13236
- try {
13237
- await git(repoRoot, ["show-ref", "--verify", `refs/heads/${name}`]);
13238
- return true;
13239
- } catch {
13240
- return false;
13318
+ var applyTicketStoryIdFromPayload = (row, payload) => {
13319
+ if (!Object.prototype.hasOwnProperty.call(payload, "storyId")) return;
13320
+ const v = payload["storyId"];
13321
+ if (v === null) {
13322
+ row.storyId = null;
13323
+ return;
13241
13324
  }
13325
+ if (typeof v !== "string") return;
13326
+ const t = v.trim();
13327
+ row.storyId = t === "" ? null : t;
13242
13328
  };
13243
- var pickUniqueLocalBranchName = async (opts) => {
13244
- const max = opts.maxSuffix ?? DEFAULT_MAX_SUFFIX;
13245
- const { preferredBase, repoRoot, runGit: git } = opts;
13246
- for (let n = 1; n <= max; n += 1) {
13247
- const raw = n === 1 ? preferredBase : `${preferredBase}-${n}`;
13248
- const norm = normalizeTicketBranchName(raw);
13249
- if (norm === void 0) {
13250
- continue;
13251
- }
13252
- if (!await localBranchRefExists(repoRoot, norm, git)) {
13253
- return { branch: norm, preferred: preferredBase };
13254
- }
13329
+ var linkedBranchesFromTicketCreatedPayload = (payload) => {
13330
+ if (!Object.prototype.hasOwnProperty.call(payload, "branches")) {
13331
+ return [];
13255
13332
  }
13256
- throw new Error(
13257
- `Could not allocate a free local branch name from ${JSON.stringify(preferredBase)} (tried suffixes up to -${String(max)})`
13258
- );
13333
+ return normalizeTicketBranchListFromPayloadValue(payload["branches"]);
13259
13334
  };
13260
-
13261
- // src/git/resolve-integration-start-point.ts
13262
- var assertGitRefResolvable = async (repoRoot, ref, git) => {
13263
- try {
13264
- await git(repoRoot, ["rev-parse", "-q", "--verify", ref]);
13265
- } catch {
13266
- throw new Error(`Invalid or ambiguous --from ref: ${ref}`);
13267
- }
13335
+ var applyTicketBranchesFromUpdatePayload = (row, payload) => {
13336
+ if (!Object.prototype.hasOwnProperty.call(payload, "branches")) return;
13337
+ const v = payload["branches"];
13338
+ if (!Array.isArray(v)) return;
13339
+ row.linkedBranches = normalizeTicketBranchListFromPayloadValue(v);
13268
13340
  };
13269
- var resolveIntegrationStartPoint = async (repoRoot, remote, git) => {
13270
- const symRef = `refs/remotes/${remote}/HEAD`;
13271
- try {
13272
- const { stdout } = await git(repoRoot, ["symbolic-ref", "-q", symRef]);
13273
- const target = stdout.trim();
13274
- if (target !== "") {
13275
- await git(repoRoot, ["rev-parse", "-q", "--verify", target]);
13276
- return target;
13341
+ var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13342
+ if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
13343
+ const v = ticketLabelsFromPayloadValue(payload["labels"]);
13344
+ if (v !== void 0 && v.length > 0) {
13345
+ row.labels = v;
13277
13346
  }
13278
- } catch {
13279
13347
  }
13280
- for (const head of ["refs/heads/main", "refs/heads/master"]) {
13281
- try {
13282
- await git(repoRoot, ["rev-parse", "-q", "--verify", head]);
13283
- return head;
13284
- } catch {
13285
- }
13348
+ const pr = readTicketPriorityPatch(payload);
13349
+ if (typeof pr === "string") {
13350
+ row.priority = pr;
13286
13351
  }
13287
- try {
13288
- await git(repoRoot, ["rev-parse", "-q", "--verify", "HEAD"]);
13289
- return "HEAD";
13290
- } catch {
13352
+ const sz = readTicketSizePatch(payload);
13353
+ if (typeof sz === "string") {
13354
+ row.size = sz;
13291
13355
  }
13292
- throw new Error(
13293
- "Could not resolve a default branch to branch from; pass --from <ref> (e.g. main or origin/main)."
13294
- );
13295
- };
13296
-
13297
- // src/git/list-repo-commit-authors.ts
13298
- var listRepoCommitAuthors = async (repoRoot, git) => {
13299
- try {
13300
- const { stdout } = await git(repoRoot, [
13301
- "-c",
13302
- "log.showSignature=false",
13303
- "log",
13304
- "--all",
13305
- "--format=%an%x1f%ae"
13306
- ]);
13307
- if (stdout === "") return [];
13308
- const lines = stdout.split("\n").filter((l) => l.length > 0);
13309
- const seenEmail = /* @__PURE__ */ new Set();
13310
- const out = [];
13311
- for (const line of lines) {
13312
- const sep2 = line.indexOf("");
13313
- if (sep2 <= 0) continue;
13314
- const name = line.slice(0, sep2).trim();
13315
- const email = line.slice(sep2 + 1).trim();
13316
- if (email === "") continue;
13317
- const key = email.toLowerCase();
13318
- if (seenEmail.has(key)) continue;
13319
- seenEmail.add(key);
13320
- const loginGuess = guessGithubLoginFromContact(name, email);
13321
- out.push(
13322
- loginGuess !== void 0 ? { name, email, loginGuess } : { name, email }
13323
- );
13324
- }
13325
- return out;
13326
- } catch {
13327
- return [];
13356
+ const est = readTicketEstimatePatch(payload);
13357
+ if (typeof est === "number") {
13358
+ row.estimate = est;
13328
13359
  }
13329
- };
13330
-
13331
- // src/git/parse-github-owner-repo-from-remote-url.ts
13332
- var parseGithubOwnerRepoFromRemoteUrl = (rawUrl) => {
13333
- const trimmed = rawUrl.trim();
13334
- if (!trimmed) {
13335
- return void 0;
13360
+ const sw = readTicketIsoInstantPatch(payload, "startWorkAt");
13361
+ if (typeof sw === "string") {
13362
+ row.startWorkAt = sw;
13336
13363
  }
13337
- const scpMatch = /^git@github\.com:(?<path>.+)$/i.exec(trimmed);
13338
- if (scpMatch?.groups?.path) {
13339
- return slugFromGithubPath(scpMatch.groups.path);
13364
+ const tf = readTicketIsoInstantPatch(payload, "targetFinishAt");
13365
+ if (typeof tf === "string") {
13366
+ row.targetFinishAt = tf;
13340
13367
  }
13341
- try {
13342
- const withScheme = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
13343
- const u = new URL(withScheme);
13344
- const host = u.hostname.toLowerCase();
13345
- if (host !== "github.com" && host !== "www.github.com") {
13346
- return void 0;
13368
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
13369
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
13370
+ if (v !== void 0 && v.length > 0) {
13371
+ row.dependsOn = v;
13347
13372
  }
13348
- return slugFromGithubPath(u.pathname);
13349
- } catch {
13350
- return void 0;
13351
13373
  }
13352
13374
  };
13353
- var slugFromGithubPath = (path) => {
13354
- const segments = path.replace(/^\/+/, "").split("/").filter((s) => s.length > 0).map((s) => s.replace(/\.git$/i, ""));
13355
- if (segments.length < 2) {
13356
- return void 0;
13375
+ var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13376
+ if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
13377
+ if (payload["labels"] === null) {
13378
+ delete row.labels;
13379
+ } else {
13380
+ const v = ticketLabelsFromPayloadValue(payload["labels"]);
13381
+ if (v !== void 0) {
13382
+ if (v.length === 0) {
13383
+ delete row.labels;
13384
+ } else {
13385
+ row.labels = v;
13386
+ }
13387
+ }
13388
+ }
13357
13389
  }
13358
- const owner = segments[0];
13359
- const repo = segments[1];
13360
- if (!owner || !repo) {
13361
- return void 0;
13390
+ const pr = readTicketPriorityPatch(payload);
13391
+ if (pr === null) {
13392
+ delete row.priority;
13393
+ } else if (typeof pr === "string") {
13394
+ row.priority = pr;
13362
13395
  }
13363
- return `${owner}/${repo}`;
13364
- };
13365
-
13366
- // src/git/try-read-github-owner-repo-slug-from-git.ts
13367
- var tryReadGithubOwnerRepoSlugFromGit = async (params) => {
13368
- try {
13369
- const { stdout } = await params.runGit(params.repoRoot, [
13370
- "remote",
13371
- "get-url",
13372
- params.remote
13373
- ]);
13374
- return parseGithubOwnerRepoFromRemoteUrl(stdout);
13375
- } catch {
13376
- return void 0;
13396
+ const sz = readTicketSizePatch(payload);
13397
+ if (sz === null) {
13398
+ delete row.size;
13399
+ } else if (typeof sz === "string") {
13400
+ row.size = sz;
13377
13401
  }
13378
- };
13379
-
13380
- // src/git/resolve-effective-git-author-for-data-commit.ts
13381
- var defaultDataCommitName = "hyper-pm";
13382
- var defaultDataCommitEmail = "hyper-pm@users.noreply.github.com";
13383
- var tryReadGitConfigUserName = async (cwd, runGit2) => {
13384
- try {
13385
- const { stdout } = await runGit2(cwd, ["config", "--get", "user.name"]);
13386
- return stdout.trim();
13387
- } catch {
13388
- return "";
13402
+ const est = readTicketEstimatePatch(payload);
13403
+ if (est === null) {
13404
+ delete row.estimate;
13405
+ } else if (typeof est === "number") {
13406
+ row.estimate = est;
13389
13407
  }
13390
- };
13391
- var tryReadGitConfigUserEmail = async (cwd, runGit2) => {
13392
- try {
13393
- const { stdout } = await runGit2(cwd, ["config", "--get", "user.email"]);
13394
- return stdout.trim();
13395
- } catch {
13396
- return "";
13408
+ const sw = readTicketIsoInstantPatch(payload, "startWorkAt");
13409
+ if (sw === null) {
13410
+ delete row.startWorkAt;
13411
+ } else if (typeof sw === "string") {
13412
+ row.startWorkAt = sw;
13397
13413
  }
13398
- };
13399
- var resolveEffectiveGitAuthorForDataCommit = async (cwd, runGit2, authorEnv) => {
13400
- const fromGitName = await tryReadGitConfigUserName(cwd, runGit2);
13401
- const fromGitEmail = await tryReadGitConfigUserEmail(cwd, runGit2);
13402
- const name = fromGitName || authorEnv.HYPER_PM_GIT_USER_NAME?.trim() || authorEnv.GIT_AUTHOR_NAME?.trim() || defaultDataCommitName;
13403
- const email = fromGitEmail || authorEnv.HYPER_PM_GIT_USER_EMAIL?.trim() || authorEnv.GIT_AUTHOR_EMAIL?.trim() || defaultDataCommitEmail;
13404
- return { name, email };
13405
- };
13406
-
13407
- // src/run/commit-data.ts
13408
- var maxActorSuffixLen = 60;
13409
- var formatDataBranchCommitMessage = (base, actorSuffix) => {
13410
- const raw = actorSuffix?.trim();
13411
- if (!raw) {
13412
- return base;
13414
+ const tf = readTicketIsoInstantPatch(payload, "targetFinishAt");
13415
+ if (tf === null) {
13416
+ delete row.targetFinishAt;
13417
+ } else if (typeof tf === "string") {
13418
+ row.targetFinishAt = tf;
13419
+ }
13420
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
13421
+ if (payload["dependsOn"] === null) {
13422
+ delete row.dependsOn;
13423
+ } else {
13424
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
13425
+ if (v !== void 0) {
13426
+ if (v.length === 0) {
13427
+ delete row.dependsOn;
13428
+ } else {
13429
+ row.dependsOn = v;
13430
+ }
13431
+ }
13432
+ }
13413
13433
  }
13414
- const collapsed = raw.replace(/\s+/g, " ");
13415
- const suffix = collapsed.length > maxActorSuffixLen ? `${collapsed.slice(0, maxActorSuffixLen - 1)}\u2026` : collapsed;
13416
- return `${base} (${suffix})`;
13417
- };
13418
- var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2, opts) => {
13419
- const { stdout } = await runGit2(worktreePath, ["status", "--porcelain"]);
13420
- if (!stdout.trim()) return;
13421
- await runGit2(worktreePath, ["add", "."]);
13422
- const authorEnv = opts?.authorEnv ?? env;
13423
- const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13424
- worktreePath,
13425
- runGit2,
13426
- authorEnv
13427
- );
13428
- await runGit2(worktreePath, [
13429
- "-c",
13430
- `user.name=${name}`,
13431
- "-c",
13432
- `user.email=${email}`,
13433
- "commit",
13434
- "-m",
13435
- message
13436
- ]);
13437
13434
  };
13438
-
13439
- // src/run/push-data-branch.ts
13440
- var firstLineFromUnknown = (err) => {
13441
- const raw = err instanceof Error ? err.message : String(err);
13442
- const line = raw.trim().split("\n")[0]?.trim() ?? raw.trim();
13443
- return line.length > 0 ? line : "git error";
13435
+ var applyCreatedAudit = (row, evt) => {
13436
+ row.createdAt = evt.ts;
13437
+ row.createdBy = evt.actor;
13438
+ row.updatedAt = evt.ts;
13439
+ row.updatedBy = evt.actor;
13444
13440
  };
13445
- var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) => {
13446
- try {
13447
- await runGit2(worktreePath, ["remote", "get-url", remote]);
13448
- } catch (e) {
13449
- return {
13450
- status: "skipped_no_remote",
13451
- detail: firstLineFromUnknown(e)
13452
- };
13453
- }
13454
- try {
13455
- await runGit2(worktreePath, ["push", "-u", remote, branch]);
13456
- return { status: "pushed" };
13457
- } catch (e) {
13458
- return {
13459
- status: "failed",
13460
- detail: firstLineFromUnknown(e)
13461
- };
13462
- }
13441
+ var applyLastUpdate = (row, evt) => {
13442
+ row.updatedAt = evt.ts;
13443
+ row.updatedBy = evt.actor;
13463
13444
  };
13464
-
13465
- // src/storage/append-event.ts
13466
- var import_promises5 = require("node:fs/promises");
13467
- var import_node_path5 = require("node:path");
13468
-
13469
- // src/storage/event-path.ts
13470
- var nextEventRelPath = (now, deps) => {
13471
- const y = String(now.getUTCFullYear());
13472
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
13473
- const nextId = deps?.nextId ?? (() => ulid().toLowerCase());
13474
- const part = `part-${nextId()}`;
13475
- return `events/${y}/${m}/${part}.jsonl`;
13445
+ var applyStatusIfChanged = (row, evt, nextStatus) => {
13446
+ if (row.status === nextStatus) return false;
13447
+ row.status = nextStatus;
13448
+ row.statusChangedAt = evt.ts;
13449
+ row.statusChangedBy = evt.actor;
13450
+ return true;
13476
13451
  };
13477
-
13478
- // src/storage/append-event.ts
13479
- var appendEventLine = async (dataRoot, event, clock, opts) => {
13480
- const rel = nextEventRelPath(clock.now(), {
13481
- nextId: opts?.nextEventId
13452
+ var appendTicketCommentFromEvent = (ticket, evt) => {
13453
+ const list = ticket.comments ?? (ticket.comments = []);
13454
+ list.push({
13455
+ id: evt.id,
13456
+ body: String(evt.payload["body"] ?? ""),
13457
+ createdAt: evt.ts,
13458
+ createdBy: evt.actor
13482
13459
  });
13483
- const abs = `${dataRoot.replace(/\/$/, "")}/${rel}`;
13484
- await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(abs), { recursive: true });
13485
- await (0, import_promises5.appendFile)(abs, `${JSON.stringify(event)}
13486
- `, "utf8");
13487
- return rel;
13460
+ applyLastUpdate(ticket, evt);
13488
13461
  };
13489
-
13490
- // src/storage/read-event-lines.ts
13491
- var import_promises6 = require("node:fs/promises");
13492
- var import_node_path6 = require("node:path");
13493
- var listJsonlFiles = async (dir, root) => {
13494
- const out = [];
13495
- const entries = await (0, import_promises6.readdir)(dir, { withFileTypes: true });
13496
- for (const e of entries) {
13497
- const abs = (0, import_node_path6.join)(dir, e.name);
13498
- if (e.isDirectory()) {
13499
- out.push(...await listJsonlFiles(abs, root));
13500
- } else if (e.isFile() && e.name.endsWith(".jsonl")) {
13501
- out.push((0, import_node_path6.relative)(root, abs).split("\\").join("/"));
13502
- }
13462
+ var resolveInboundTicketStatusFromPayload = (ticket, payload) => {
13463
+ const explicit = parseWorkItemStatus(payload["status"]);
13464
+ if (explicit !== void 0) return explicit;
13465
+ const legacySt = payload["state"];
13466
+ if (legacySt === "open" || legacySt === "closed") {
13467
+ return resolveTicketInboundStatus({
13468
+ issueState: legacySt,
13469
+ currentStatus: ticket.status
13470
+ });
13503
13471
  }
13504
- return out;
13472
+ return void 0;
13505
13473
  };
13506
- var readAllEventLines = async (dataRoot) => {
13507
- const eventsRoot = (0, import_node_path6.join)(dataRoot, "events");
13508
- let files = [];
13509
- try {
13510
- files = await listJsonlFiles(eventsRoot, dataRoot);
13511
- } catch {
13512
- return [];
13513
- }
13514
- files.sort((a, b) => a.localeCompare(b));
13515
- const lines = [];
13516
- for (const rel of files) {
13517
- const content = await (0, import_promises6.readFile)((0, import_node_path6.join)(dataRoot, rel), "utf8");
13518
- for (const line of content.split("\n")) {
13519
- if (line.trim()) lines.push(line);
13520
- }
13521
- }
13522
- return lines;
13523
- };
13524
-
13525
- // src/lib/github-pr-activity.ts
13526
- var GITHUB_PR_ACTIVITY_RECENT_CAP = 20;
13527
- var githubPrActivityKindSchema = external_exports.enum([
13528
- "opened",
13529
- "updated",
13530
- "commented",
13531
- "reviewed",
13532
- "merged",
13533
- "closed",
13534
- "ready_for_review"
13535
- ]);
13536
- var githubPrReviewStateSchema = external_exports.enum([
13537
- "approved",
13538
- "changes_requested",
13539
- "commented"
13540
- ]);
13541
- var buildPrOpenSourceId = (ticketId, prNumber) => `hyper-pm:pr-open:${ticketId}:${prNumber}`;
13542
- var parseGithubPrActivityPayload = (payload) => {
13543
- const ticketId = payload["ticketId"];
13544
- const prRaw = payload["prNumber"];
13545
- const kindRaw = payload["kind"];
13546
- const occurredAt = payload["occurredAt"];
13547
- const sourceId = payload["sourceId"];
13548
- if (typeof ticketId !== "string" || typeof occurredAt !== "string" || typeof sourceId !== "string") {
13549
- return void 0;
13550
- }
13551
- const prNumber = typeof prRaw === "number" ? prRaw : Number(prRaw);
13552
- if (!Number.isFinite(prNumber)) return void 0;
13553
- const kindParsed = githubPrActivityKindSchema.safeParse(kindRaw);
13554
- if (!kindParsed.success) return void 0;
13555
- const out = {
13556
- prNumber,
13557
- kind: kindParsed.data,
13558
- occurredAt,
13559
- sourceId
13560
- };
13561
- const url = payload["url"];
13562
- if (typeof url === "string" && url.length > 0) {
13563
- out.url = url;
13564
- }
13565
- const rs = githubPrReviewStateSchema.safeParse(payload["reviewState"]);
13566
- if (rs.success) {
13567
- out.reviewState = rs.data;
13568
- }
13569
- return out;
13570
- };
13571
-
13572
- // src/storage/projection.ts
13573
- var emptyProjection = () => ({
13574
- epics: /* @__PURE__ */ new Map(),
13575
- stories: /* @__PURE__ */ new Map(),
13576
- tickets: /* @__PURE__ */ new Map()
13577
- });
13578
- var parsePrRefs = (body) => {
13579
- const out = /* @__PURE__ */ new Set();
13580
- const re = /\b(?:Closes|Refs|Fixes)\s+#(\d+)\b/gi;
13581
- let m = re.exec(body);
13582
- while (m !== null) {
13583
- out.add(Number(m[1]));
13584
- m = re.exec(body);
13585
- }
13586
- return [...out];
13587
- };
13588
- var applyTicketAssigneeFromPayload = (row, payload) => {
13589
- if (!Object.prototype.hasOwnProperty.call(payload, "assignee")) return;
13590
- const v = payload["assignee"];
13591
- if (v === null) {
13592
- delete row.assignee;
13593
- return;
13594
- }
13595
- if (typeof v !== "string") return;
13596
- const n = normalizeGithubLogin(v);
13597
- if (n === "") delete row.assignee;
13598
- else row.assignee = n;
13599
- };
13600
- var storyIdFromTicketCreatedPayload = (payload) => {
13601
- if (!Object.prototype.hasOwnProperty.call(payload, "storyId")) {
13602
- return null;
13603
- }
13604
- const v = payload["storyId"];
13605
- if (v === null) return null;
13606
- if (typeof v !== "string") return null;
13607
- const t = v.trim();
13608
- return t === "" ? null : t;
13609
- };
13610
- var applyTicketStoryIdFromPayload = (row, payload) => {
13611
- if (!Object.prototype.hasOwnProperty.call(payload, "storyId")) return;
13612
- const v = payload["storyId"];
13613
- if (v === null) {
13614
- row.storyId = null;
13615
- return;
13616
- }
13617
- if (typeof v !== "string") return;
13618
- const t = v.trim();
13619
- row.storyId = t === "" ? null : t;
13620
- };
13621
- var linkedBranchesFromTicketCreatedPayload = (payload) => {
13622
- if (!Object.prototype.hasOwnProperty.call(payload, "branches")) {
13623
- return [];
13624
- }
13625
- return normalizeTicketBranchListFromPayloadValue(payload["branches"]);
13626
- };
13627
- var applyTicketBranchesFromUpdatePayload = (row, payload) => {
13628
- if (!Object.prototype.hasOwnProperty.call(payload, "branches")) return;
13629
- const v = payload["branches"];
13630
- if (!Array.isArray(v)) return;
13631
- row.linkedBranches = normalizeTicketBranchListFromPayloadValue(v);
13632
- };
13633
- var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13634
- if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
13635
- const v = ticketLabelsFromPayloadValue(payload["labels"]);
13636
- if (v !== void 0 && v.length > 0) {
13637
- row.labels = v;
13638
- }
13639
- }
13640
- const pr = readTicketPriorityPatch(payload);
13641
- if (typeof pr === "string") {
13642
- row.priority = pr;
13643
- }
13644
- const sz = readTicketSizePatch(payload);
13645
- if (typeof sz === "string") {
13646
- row.size = sz;
13647
- }
13648
- const est = readTicketEstimatePatch(payload);
13649
- if (typeof est === "number") {
13650
- row.estimate = est;
13651
- }
13652
- const sw = readTicketIsoInstantPatch(payload, "startWorkAt");
13653
- if (typeof sw === "string") {
13654
- row.startWorkAt = sw;
13655
- }
13656
- const tf = readTicketIsoInstantPatch(payload, "targetFinishAt");
13657
- if (typeof tf === "string") {
13658
- row.targetFinishAt = tf;
13659
- }
13660
- };
13661
- var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13662
- if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
13663
- if (payload["labels"] === null) {
13664
- delete row.labels;
13665
- } else {
13666
- const v = ticketLabelsFromPayloadValue(payload["labels"]);
13667
- if (v !== void 0) {
13668
- if (v.length === 0) {
13669
- delete row.labels;
13670
- } else {
13671
- row.labels = v;
13672
- }
13673
- }
13674
- }
13675
- }
13676
- const pr = readTicketPriorityPatch(payload);
13677
- if (pr === null) {
13678
- delete row.priority;
13679
- } else if (typeof pr === "string") {
13680
- row.priority = pr;
13681
- }
13682
- const sz = readTicketSizePatch(payload);
13683
- if (sz === null) {
13684
- delete row.size;
13685
- } else if (typeof sz === "string") {
13686
- row.size = sz;
13687
- }
13688
- const est = readTicketEstimatePatch(payload);
13689
- if (est === null) {
13690
- delete row.estimate;
13691
- } else if (typeof est === "number") {
13692
- row.estimate = est;
13693
- }
13694
- const sw = readTicketIsoInstantPatch(payload, "startWorkAt");
13695
- if (sw === null) {
13696
- delete row.startWorkAt;
13697
- } else if (typeof sw === "string") {
13698
- row.startWorkAt = sw;
13699
- }
13700
- const tf = readTicketIsoInstantPatch(payload, "targetFinishAt");
13701
- if (tf === null) {
13702
- delete row.targetFinishAt;
13703
- } else if (typeof tf === "string") {
13704
- row.targetFinishAt = tf;
13705
- }
13706
- };
13707
- var applyCreatedAudit = (row, evt) => {
13708
- row.createdAt = evt.ts;
13709
- row.createdBy = evt.actor;
13710
- row.updatedAt = evt.ts;
13711
- row.updatedBy = evt.actor;
13712
- };
13713
- var applyLastUpdate = (row, evt) => {
13714
- row.updatedAt = evt.ts;
13715
- row.updatedBy = evt.actor;
13716
- };
13717
- var applyStatusIfChanged = (row, evt, nextStatus) => {
13718
- if (row.status === nextStatus) return false;
13719
- row.status = nextStatus;
13720
- row.statusChangedAt = evt.ts;
13721
- row.statusChangedBy = evt.actor;
13722
- return true;
13723
- };
13724
- var appendTicketCommentFromEvent = (ticket, evt) => {
13725
- const list = ticket.comments ?? (ticket.comments = []);
13726
- list.push({
13727
- id: evt.id,
13728
- body: String(evt.payload["body"] ?? ""),
13729
- createdAt: evt.ts,
13730
- createdBy: evt.actor
13731
- });
13732
- applyLastUpdate(ticket, evt);
13733
- };
13734
- var resolveInboundTicketStatusFromPayload = (ticket, payload) => {
13735
- const explicit = parseWorkItemStatus(payload["status"]);
13736
- if (explicit !== void 0) return explicit;
13737
- const legacySt = payload["state"];
13738
- if (legacySt === "open" || legacySt === "closed") {
13739
- return resolveTicketInboundStatus({
13740
- issueState: legacySt,
13741
- currentStatus: ticket.status
13742
- });
13743
- }
13744
- return void 0;
13745
- };
13746
- var applyEvent = (projection, evt) => {
13747
- switch (evt.type) {
13748
- case "EpicCreated": {
13749
- const id = String(evt.payload["id"]);
13750
- const status = resolveStatusForNewEpicStoryPayload(evt.payload);
13751
- const row = {
13752
- id,
13753
- title: String(evt.payload["title"] ?? ""),
13754
- body: String(evt.payload["body"] ?? ""),
13755
- status,
13756
- statusChangedAt: evt.ts,
13757
- statusChangedBy: evt.actor,
13758
- createdAt: "",
13759
- createdBy: "",
13760
- updatedAt: "",
13761
- updatedBy: ""
13762
- };
13763
- applyCreatedAudit(row, evt);
13764
- projection.epics.set(id, row);
13765
- break;
13474
+ var applyEvent = (projection, evt) => {
13475
+ switch (evt.type) {
13476
+ case "EpicCreated": {
13477
+ const id = String(evt.payload["id"]);
13478
+ const status = resolveStatusForNewEpicStoryPayload(evt.payload);
13479
+ const row = {
13480
+ id,
13481
+ number: resolveWorkItemCreateNumber(projection, "epic", evt.payload),
13482
+ title: String(evt.payload["title"] ?? ""),
13483
+ body: String(evt.payload["body"] ?? ""),
13484
+ status,
13485
+ statusChangedAt: evt.ts,
13486
+ statusChangedBy: evt.actor,
13487
+ createdAt: "",
13488
+ createdBy: "",
13489
+ updatedAt: "",
13490
+ updatedBy: ""
13491
+ };
13492
+ applyCreatedAudit(row, evt);
13493
+ projection.epics.set(id, row);
13494
+ break;
13766
13495
  }
13767
13496
  case "EpicUpdated": {
13768
13497
  const id = String(evt.payload["id"]);
@@ -13795,6 +13524,7 @@ var applyEvent = (projection, evt) => {
13795
13524
  const status = resolveStatusForNewEpicStoryPayload(evt.payload);
13796
13525
  const row = {
13797
13526
  id,
13527
+ number: resolveWorkItemCreateNumber(projection, "story", evt.payload),
13798
13528
  epicId: String(evt.payload["epicId"]),
13799
13529
  title: String(evt.payload["title"] ?? ""),
13800
13530
  body: String(evt.payload["body"] ?? ""),
@@ -13842,6 +13572,7 @@ var applyEvent = (projection, evt) => {
13842
13572
  const status = resolveStatusForNewTicketPayload(evt.payload);
13843
13573
  const row = {
13844
13574
  id,
13575
+ number: resolveWorkItemCreateNumber(projection, "ticket", evt.payload),
13845
13576
  storyId: storyIdFromTicketCreatedPayload(evt.payload),
13846
13577
  title: String(evt.payload["title"] ?? ""),
13847
13578
  body,
@@ -13954,28 +13685,733 @@ var applyEvent = (projection, evt) => {
13954
13685
  }
13955
13686
  break;
13956
13687
  }
13957
- default:
13958
- break;
13688
+ default:
13689
+ break;
13690
+ }
13691
+ };
13692
+ var replayEvents = (lines) => {
13693
+ const proj = emptyProjection();
13694
+ const events = [];
13695
+ for (const line of lines) {
13696
+ const trimmed = line.trim();
13697
+ if (!trimmed) continue;
13698
+ const json = JSON.parse(trimmed);
13699
+ events.push(eventLineSchema.parse(json));
13700
+ }
13701
+ events.sort((a, b) => {
13702
+ const t = a.ts.localeCompare(b.ts);
13703
+ if (t !== 0) return t;
13704
+ return a.id.localeCompare(b.id);
13705
+ });
13706
+ for (const e of events) {
13707
+ applyEvent(proj, e);
13708
+ }
13709
+ return proj;
13710
+ };
13711
+
13712
+ // src/cli/resolve-projection-work-item-id.ts
13713
+ var isDigitOnlyWorkItemRef = (raw) => /^\d+$/.test(raw.trim());
13714
+ var resolveEpicId = (projection, raw) => {
13715
+ const t = raw.trim();
13716
+ if (t === "") return void 0;
13717
+ if (projection.epics.has(t)) return t;
13718
+ if (!isDigitOnlyWorkItemRef(t)) return void 0;
13719
+ const n = Number(t);
13720
+ const hits = [...projection.epics.values()].filter((e) => e.number === n);
13721
+ if (hits.length !== 1) return void 0;
13722
+ return hits[0].id;
13723
+ };
13724
+ var resolveStoryId = (projection, raw) => {
13725
+ const t = raw.trim();
13726
+ if (t === "") return void 0;
13727
+ if (projection.stories.has(t)) return t;
13728
+ if (!isDigitOnlyWorkItemRef(t)) return void 0;
13729
+ const n = Number(t);
13730
+ const hits = [...projection.stories.values()].filter((s) => s.number === n);
13731
+ if (hits.length !== 1) return void 0;
13732
+ return hits[0].id;
13733
+ };
13734
+ var resolveTicketId = (projection, raw) => {
13735
+ const t = raw.trim();
13736
+ if (t === "") return void 0;
13737
+ if (projection.tickets.has(t)) return t;
13738
+ if (!isDigitOnlyWorkItemRef(t)) return void 0;
13739
+ const n = Number(t);
13740
+ const hits = [...projection.tickets.values()].filter((x) => x.number === n);
13741
+ if (hits.length !== 1) return void 0;
13742
+ return hits[0].id;
13743
+ };
13744
+ var resolveTicketDependsOnTokensToIds = (projection, tokens) => {
13745
+ const mapped = tokens.map((raw) => {
13746
+ const t = raw.trim();
13747
+ if (t === "") return "";
13748
+ if (projection.tickets.has(t)) return t;
13749
+ if (!isDigitOnlyWorkItemRef(t)) return t;
13750
+ const hit = resolveTicketId(projection, t);
13751
+ return hit ?? t;
13752
+ });
13753
+ return normalizeTicketDependsOnIds(mapped);
13754
+ };
13755
+ var assertCreatePayloadUsesExpectedHeadNumber = (projection, kind, payload) => {
13756
+ const n = readOptionalPositiveIntegerFromPayload(payload, "number");
13757
+ const expected = kind === "epic" ? nextEpicNumberForCreate(projection) : kind === "story" ? nextStoryNumberForCreate(projection) : nextTicketNumberForCreate(projection);
13758
+ if (n !== expected) {
13759
+ const got = n === void 0 ? "missing" : String(n);
13760
+ throw new Error(
13761
+ `Invalid ${kind} number for this data branch: expected ${String(expected)}, got ${got}.`
13762
+ );
13763
+ }
13764
+ };
13765
+
13766
+ // src/config/hyper-pm-config.ts
13767
+ var hyperPmConfigSchema = external_exports.object({
13768
+ schema: external_exports.literal(1),
13769
+ dataBranch: external_exports.string().min(1),
13770
+ remote: external_exports.string().min(1).default("origin"),
13771
+ sync: external_exports.enum(["off", "outbound", "full"]).default("outbound"),
13772
+ githubRepo: external_exports.string().optional(),
13773
+ issueMapping: external_exports.enum(["ticket", "story", "epic"]).default("ticket")
13774
+ });
13775
+
13776
+ // src/config/load-config.ts
13777
+ var import_promises = require("node:fs/promises");
13778
+ var import_node_path = require("node:path");
13779
+ var CONFIG_DIR = ".hyper-pm";
13780
+ var CONFIG_FILE = "config.json";
13781
+ var getHyperPmConfigPath = (repoRoot) => {
13782
+ return (0, import_node_path.join)(repoRoot, CONFIG_DIR, CONFIG_FILE);
13783
+ };
13784
+ var loadHyperPmConfig = async (repoRoot, overrides = {}) => {
13785
+ const raw = await (0, import_promises.readFile)(getHyperPmConfigPath(repoRoot), "utf8");
13786
+ const parsed = JSON.parse(raw);
13787
+ const base = hyperPmConfigSchema.parse(parsed);
13788
+ const merged = { ...base, ...stripUndefined(overrides) };
13789
+ return hyperPmConfigSchema.parse(merged);
13790
+ };
13791
+ var stripUndefined = (obj) => {
13792
+ const out = {};
13793
+ for (const [k, v] of Object.entries(obj)) {
13794
+ if (v !== void 0) out[k] = v;
13795
+ }
13796
+ return out;
13797
+ };
13798
+
13799
+ // src/config/save-config.ts
13800
+ var import_promises2 = require("node:fs/promises");
13801
+ var import_node_path2 = require("node:path");
13802
+ var saveHyperPmConfig = async (repoRoot, config) => {
13803
+ const target = getHyperPmConfigPath(repoRoot);
13804
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(target), { recursive: true });
13805
+ await (0, import_promises2.writeFile)(target, `${JSON.stringify(config, null, 2)}
13806
+ `, "utf8");
13807
+ };
13808
+
13809
+ // src/doctor/run-doctor.ts
13810
+ var runDoctorOnLines = (lines) => {
13811
+ const issues = [];
13812
+ for (let i = 0; i < lines.length; i++) {
13813
+ const line = lines[i]?.trim() ?? "";
13814
+ if (!line) continue;
13815
+ let json;
13816
+ try {
13817
+ json = JSON.parse(line);
13818
+ } catch (e) {
13819
+ issues.push({
13820
+ kind: "invalid-json",
13821
+ line: i + 1,
13822
+ message: e instanceof Error ? e.message : "parse error"
13823
+ });
13824
+ return issues;
13825
+ }
13826
+ const parsed = eventLineSchema.safeParse(json);
13827
+ if (!parsed.success) {
13828
+ issues.push({
13829
+ kind: "invalid-event",
13830
+ line: i + 1,
13831
+ message: parsed.error.message
13832
+ });
13833
+ return issues;
13834
+ }
13835
+ }
13836
+ return issues;
13837
+ };
13838
+
13839
+ // src/git/create-and-checkout-branch.ts
13840
+ var createAndCheckoutBranch = async (opts) => {
13841
+ await opts.runGit(opts.repoRoot, [
13842
+ "switch",
13843
+ "-c",
13844
+ opts.branchName,
13845
+ opts.startPoint
13846
+ ]);
13847
+ };
13848
+
13849
+ // src/git/data-worktree-session.ts
13850
+ var import_promises3 = require("node:fs/promises");
13851
+ var import_node_path3 = require("node:path");
13852
+ var ensureDir = async (path) => {
13853
+ await (0, import_promises3.mkdir)(path, { recursive: true });
13854
+ };
13855
+ var defaultPathExists = async (path) => {
13856
+ try {
13857
+ await (0, import_promises3.access)(path);
13858
+ return true;
13859
+ } catch {
13860
+ return false;
13861
+ }
13862
+ };
13863
+ var parseDataBranchWorktreeFromPorcelain = (stdout, dataBranch) => {
13864
+ const wantRef = `refs/heads/${dataBranch}`;
13865
+ const blocks = stdout.split(/\n\n+/).map((b) => b.trim()).filter(Boolean);
13866
+ for (const block of blocks) {
13867
+ const lines = block.split("\n");
13868
+ let worktreePath;
13869
+ let branch;
13870
+ for (const line of lines) {
13871
+ if (line.startsWith("worktree ")) {
13872
+ worktreePath = line.slice("worktree ".length);
13873
+ } else if (line.startsWith("branch ")) {
13874
+ branch = line.slice("branch ".length);
13875
+ }
13876
+ }
13877
+ if (worktreePath !== void 0 && branch === wantRef) {
13878
+ return worktreePath;
13879
+ }
13880
+ }
13881
+ return void 0;
13882
+ };
13883
+ var openDataBranchWorktree = async (opts) => {
13884
+ const pathExists = opts.pathExists ?? defaultPathExists;
13885
+ const { stdout: listOut } = await opts.runGit(opts.repoRoot, [
13886
+ "worktree",
13887
+ "list",
13888
+ "--porcelain"
13889
+ ]);
13890
+ const listedPath = parseDataBranchWorktreeFromPorcelain(
13891
+ listOut,
13892
+ opts.dataBranch
13893
+ );
13894
+ if (listedPath !== void 0 && await pathExists(listedPath)) {
13895
+ const dispose2 = async () => {
13896
+ return;
13897
+ };
13898
+ return { worktreePath: listedPath, dispose: dispose2 };
13899
+ }
13900
+ const worktreePath = (0, import_node_path3.join)(
13901
+ opts.tmpBase,
13902
+ `hyper-pm-worktree-${ulid().toLowerCase()}`
13903
+ );
13904
+ await ensureDir(opts.tmpBase);
13905
+ try {
13906
+ await opts.runGit(opts.repoRoot, [
13907
+ "worktree",
13908
+ "add",
13909
+ worktreePath,
13910
+ opts.dataBranch
13911
+ ]);
13912
+ } catch (err) {
13913
+ await (0, import_promises3.rm)(worktreePath, { recursive: true, force: true }).catch(() => {
13914
+ });
13915
+ throw err;
13916
+ }
13917
+ const dispose = async () => {
13918
+ if (opts.keepWorktree) {
13919
+ return;
13920
+ }
13921
+ await opts.runGit(opts.repoRoot, ["worktree", "remove", "--force", worktreePath]).catch(() => {
13922
+ });
13923
+ await (0, import_promises3.rm)(worktreePath, { recursive: true, force: true }).catch(() => {
13924
+ });
13925
+ };
13926
+ return { worktreePath, dispose };
13927
+ };
13928
+
13929
+ // src/git/find-git-root.ts
13930
+ var findGitRoot = async (cwd, deps) => {
13931
+ const { stdout } = await deps.runGit(cwd, ["rev-parse", "--show-toplevel"]);
13932
+ return stdout;
13933
+ };
13934
+
13935
+ // src/git/init-orphan-data-branch.ts
13936
+ var import_promises4 = require("node:fs/promises");
13937
+ var import_node_path4 = require("node:path");
13938
+ var initOrphanDataBranchInWorktree = async (opts) => {
13939
+ const { stdout: tip } = await opts.runGit(opts.repoRoot, [
13940
+ "rev-parse",
13941
+ "HEAD"
13942
+ ]);
13943
+ const tipCommit = tip.trim();
13944
+ await opts.runGit(opts.repoRoot, [
13945
+ "worktree",
13946
+ "add",
13947
+ opts.worktreePath,
13948
+ tipCommit
13949
+ ]);
13950
+ await opts.runGit(opts.worktreePath, [
13951
+ "checkout",
13952
+ "--orphan",
13953
+ opts.dataBranch
13954
+ ]);
13955
+ await opts.runGit(opts.worktreePath, ["rm", "-rf", "."]).catch(() => {
13956
+ });
13957
+ const marker = (0, import_node_path4.join)(opts.worktreePath, "README.hyper-pm.md");
13958
+ await (0, import_promises4.writeFile)(
13959
+ marker,
13960
+ "# hyper-pm data branch\n\nAppend-only events live under `events/`.\n",
13961
+ "utf8"
13962
+ );
13963
+ await opts.runGit(opts.worktreePath, ["add", "."]);
13964
+ await opts.runGit(opts.worktreePath, [
13965
+ "commit",
13966
+ "-m",
13967
+ "init hyper-pm data branch"
13968
+ ]);
13969
+ };
13970
+
13971
+ // src/git/pick-unique-local-branch-name.ts
13972
+ var DEFAULT_MAX_SUFFIX = 1e3;
13973
+ var localBranchRefExists = async (repoRoot, name, git) => {
13974
+ try {
13975
+ await git(repoRoot, ["show-ref", "--verify", `refs/heads/${name}`]);
13976
+ return true;
13977
+ } catch {
13978
+ return false;
13979
+ }
13980
+ };
13981
+ var pickUniqueLocalBranchName = async (opts) => {
13982
+ const max = opts.maxSuffix ?? DEFAULT_MAX_SUFFIX;
13983
+ const { preferredBase, repoRoot, runGit: git } = opts;
13984
+ for (let n = 1; n <= max; n += 1) {
13985
+ const raw = n === 1 ? preferredBase : `${preferredBase}-${n}`;
13986
+ const norm = normalizeTicketBranchName(raw);
13987
+ if (norm === void 0) {
13988
+ continue;
13989
+ }
13990
+ if (!await localBranchRefExists(repoRoot, norm, git)) {
13991
+ return { branch: norm, preferred: preferredBase };
13992
+ }
13993
+ }
13994
+ throw new Error(
13995
+ `Could not allocate a free local branch name from ${JSON.stringify(preferredBase)} (tried suffixes up to -${String(max)})`
13996
+ );
13997
+ };
13998
+
13999
+ // src/git/resolve-integration-start-point.ts
14000
+ var assertGitRefResolvable = async (repoRoot, ref, git) => {
14001
+ try {
14002
+ await git(repoRoot, ["rev-parse", "-q", "--verify", ref]);
14003
+ } catch {
14004
+ throw new Error(`Invalid or ambiguous --from ref: ${ref}`);
14005
+ }
14006
+ };
14007
+ var resolveIntegrationStartPoint = async (repoRoot, remote, git) => {
14008
+ const symRef = `refs/remotes/${remote}/HEAD`;
14009
+ try {
14010
+ const { stdout } = await git(repoRoot, ["symbolic-ref", "-q", symRef]);
14011
+ const target = stdout.trim();
14012
+ if (target !== "") {
14013
+ await git(repoRoot, ["rev-parse", "-q", "--verify", target]);
14014
+ return target;
14015
+ }
14016
+ } catch {
14017
+ }
14018
+ for (const head of ["refs/heads/main", "refs/heads/master"]) {
14019
+ try {
14020
+ await git(repoRoot, ["rev-parse", "-q", "--verify", head]);
14021
+ return head;
14022
+ } catch {
14023
+ }
14024
+ }
14025
+ try {
14026
+ await git(repoRoot, ["rev-parse", "-q", "--verify", "HEAD"]);
14027
+ return "HEAD";
14028
+ } catch {
14029
+ }
14030
+ throw new Error(
14031
+ "Could not resolve a default branch to branch from; pass --from <ref> (e.g. main or origin/main)."
14032
+ );
14033
+ };
14034
+
14035
+ // src/git/list-repo-commit-authors.ts
14036
+ var listRepoCommitAuthors = async (repoRoot, git) => {
14037
+ try {
14038
+ const { stdout } = await git(repoRoot, [
14039
+ "-c",
14040
+ "log.showSignature=false",
14041
+ "log",
14042
+ "--all",
14043
+ "--format=%an%x1f%ae"
14044
+ ]);
14045
+ if (stdout === "") return [];
14046
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
14047
+ const seenEmail = /* @__PURE__ */ new Set();
14048
+ const out = [];
14049
+ for (const line of lines) {
14050
+ const sep2 = line.indexOf("");
14051
+ if (sep2 <= 0) continue;
14052
+ const name = line.slice(0, sep2).trim();
14053
+ const email = line.slice(sep2 + 1).trim();
14054
+ if (email === "") continue;
14055
+ const key = email.toLowerCase();
14056
+ if (seenEmail.has(key)) continue;
14057
+ seenEmail.add(key);
14058
+ const loginGuess = guessGithubLoginFromContact(name, email);
14059
+ out.push(
14060
+ loginGuess !== void 0 ? { name, email, loginGuess } : { name, email }
14061
+ );
14062
+ }
14063
+ return out;
14064
+ } catch {
14065
+ return [];
14066
+ }
14067
+ };
14068
+
14069
+ // src/git/parse-github-owner-repo-from-remote-url.ts
14070
+ var parseGithubOwnerRepoFromRemoteUrl = (rawUrl) => {
14071
+ const trimmed = rawUrl.trim();
14072
+ if (!trimmed) {
14073
+ return void 0;
14074
+ }
14075
+ const scpMatch = /^git@github\.com:(?<path>.+)$/i.exec(trimmed);
14076
+ if (scpMatch?.groups?.path) {
14077
+ return slugFromGithubPath(scpMatch.groups.path);
14078
+ }
14079
+ try {
14080
+ const withScheme = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
14081
+ const u = new URL(withScheme);
14082
+ const host = u.hostname.toLowerCase();
14083
+ if (host !== "github.com" && host !== "www.github.com") {
14084
+ return void 0;
14085
+ }
14086
+ return slugFromGithubPath(u.pathname);
14087
+ } catch {
14088
+ return void 0;
14089
+ }
14090
+ };
14091
+ var slugFromGithubPath = (path) => {
14092
+ const segments = path.replace(/^\/+/, "").split("/").filter((s) => s.length > 0).map((s) => s.replace(/\.git$/i, ""));
14093
+ if (segments.length < 2) {
14094
+ return void 0;
14095
+ }
14096
+ const owner = segments[0];
14097
+ const repo = segments[1];
14098
+ if (!owner || !repo) {
14099
+ return void 0;
14100
+ }
14101
+ return `${owner}/${repo}`;
14102
+ };
14103
+
14104
+ // src/git/try-read-github-owner-repo-slug-from-git.ts
14105
+ var tryReadGithubOwnerRepoSlugFromGit = async (params) => {
14106
+ try {
14107
+ const { stdout } = await params.runGit(params.repoRoot, [
14108
+ "remote",
14109
+ "get-url",
14110
+ params.remote
14111
+ ]);
14112
+ return parseGithubOwnerRepoFromRemoteUrl(stdout);
14113
+ } catch {
14114
+ return void 0;
14115
+ }
14116
+ };
14117
+
14118
+ // src/git/resolve-effective-git-author-for-data-commit.ts
14119
+ var defaultDataCommitName = "hyper-pm";
14120
+ var defaultDataCommitEmail = "hyper-pm@users.noreply.github.com";
14121
+ var tryReadGitConfigUserName = async (cwd, runGit2) => {
14122
+ try {
14123
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.name"]);
14124
+ return stdout.trim();
14125
+ } catch {
14126
+ return "";
14127
+ }
14128
+ };
14129
+ var tryReadGitConfigUserEmail = async (cwd, runGit2) => {
14130
+ try {
14131
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.email"]);
14132
+ return stdout.trim();
14133
+ } catch {
14134
+ return "";
14135
+ }
14136
+ };
14137
+ var resolveEffectiveGitAuthorForDataCommit = async (cwd, runGit2, authorEnv) => {
14138
+ const fromGitName = await tryReadGitConfigUserName(cwd, runGit2);
14139
+ const fromGitEmail = await tryReadGitConfigUserEmail(cwd, runGit2);
14140
+ const name = fromGitName || authorEnv.HYPER_PM_GIT_USER_NAME?.trim() || authorEnv.GIT_AUTHOR_NAME?.trim() || defaultDataCommitName;
14141
+ const email = fromGitEmail || authorEnv.HYPER_PM_GIT_USER_EMAIL?.trim() || authorEnv.GIT_AUTHOR_EMAIL?.trim() || defaultDataCommitEmail;
14142
+ return { name, email };
14143
+ };
14144
+
14145
+ // src/run/commit-data.ts
14146
+ var maxActorSuffixLen = 60;
14147
+ var formatDataBranchCommitMessage = (base, actorSuffix) => {
14148
+ const raw = actorSuffix?.trim();
14149
+ if (!raw) {
14150
+ return base;
14151
+ }
14152
+ const collapsed = raw.replace(/\s+/g, " ");
14153
+ const suffix = collapsed.length > maxActorSuffixLen ? `${collapsed.slice(0, maxActorSuffixLen - 1)}\u2026` : collapsed;
14154
+ return `${base} (${suffix})`;
14155
+ };
14156
+ var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2, opts) => {
14157
+ const { stdout } = await runGit2(worktreePath, ["status", "--porcelain"]);
14158
+ if (!stdout.trim()) return;
14159
+ await runGit2(worktreePath, ["add", "."]);
14160
+ const authorEnv = opts?.authorEnv ?? env;
14161
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
14162
+ worktreePath,
14163
+ runGit2,
14164
+ authorEnv
14165
+ );
14166
+ await runGit2(worktreePath, [
14167
+ "-c",
14168
+ `user.name=${name}`,
14169
+ "-c",
14170
+ `user.email=${email}`,
14171
+ "commit",
14172
+ "-m",
14173
+ message
14174
+ ]);
14175
+ };
14176
+
14177
+ // src/run/push-data-branch.ts
14178
+ var firstLineFromUnknown = (err) => {
14179
+ const raw = err instanceof Error ? err.message : String(err);
14180
+ const line = raw.trim().split("\n")[0]?.trim() ?? raw.trim();
14181
+ return line.length > 0 ? line : "git error";
14182
+ };
14183
+ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) => {
14184
+ try {
14185
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
14186
+ } catch (e) {
14187
+ return {
14188
+ status: "skipped_no_remote",
14189
+ detail: firstLineFromUnknown(e)
14190
+ };
14191
+ }
14192
+ try {
14193
+ await runGit2(worktreePath, ["push", "-u", remote, branch]);
14194
+ return { status: "pushed" };
14195
+ } catch (e) {
14196
+ return {
14197
+ status: "failed",
14198
+ detail: firstLineFromUnknown(e)
14199
+ };
14200
+ }
14201
+ };
14202
+
14203
+ // src/run/sync-remote-data-branch.ts
14204
+ var SyncRemoteDataBranchMergeError = class extends Error {
14205
+ /** @param message - Human-readable reason (stderr excerpt or generic text). */
14206
+ constructor(message) {
14207
+ super(message);
14208
+ this.name = "SyncRemoteDataBranchMergeError";
14209
+ }
14210
+ };
14211
+ var remoteTrackingRef = (remote, dataBranch) => `refs/remotes/${remote}/${dataBranch}`;
14212
+ var mergeRefSpecifier = (remote, dataBranch) => `${remote}/${dataBranch}`;
14213
+ var isLikelyNonFastForwardPushFailure = (detail) => {
14214
+ if (detail === void 0) return false;
14215
+ const d = detail.toLowerCase();
14216
+ return d.includes("non-fast-forward") || d.includes("failed to push");
14217
+ };
14218
+ var classifyMergeOutput = (combined) => {
14219
+ const c = combined.toLowerCase();
14220
+ if (c.includes("already up to date")) {
14221
+ return "up_to_date";
14222
+ }
14223
+ if (c.includes("fast-forward")) {
14224
+ return "fast_forward";
14225
+ }
14226
+ return "merge_commit";
14227
+ };
14228
+ var refExists = async (cwd, ref, runGit2) => {
14229
+ try {
14230
+ await runGit2(cwd, ["show-ref", "--verify", "--quiet", ref]);
14231
+ return true;
14232
+ } catch {
14233
+ return false;
14234
+ }
14235
+ };
14236
+ var fetchRemoteDataBranch = async (worktreePath, remote, dataBranch, runGit2) => {
14237
+ try {
14238
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
14239
+ } catch {
14240
+ return "skipped_no_remote";
14241
+ }
14242
+ try {
14243
+ await runGit2(worktreePath, ["fetch", remote, dataBranch]);
14244
+ return "ok";
14245
+ } catch (e) {
14246
+ const msg = e instanceof Error ? e.message : String(e);
14247
+ if (/couldn't find remote ref|could not find remote ref/i.test(msg)) {
14248
+ return "remote_branch_absent";
14249
+ }
14250
+ throw e;
14251
+ }
14252
+ };
14253
+ var mergeRemoteTrackingIntoHead = async (worktreePath, remote, dataBranch, runGit2, authorEnv = env) => {
14254
+ const spec = mergeRefSpecifier(remote, dataBranch);
14255
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
14256
+ worktreePath,
14257
+ runGit2,
14258
+ authorEnv
14259
+ );
14260
+ try {
14261
+ const { stdout, stderr } = await runGit2(worktreePath, [
14262
+ "-c",
14263
+ `user.name=${name}`,
14264
+ "-c",
14265
+ `user.email=${email}`,
14266
+ "merge",
14267
+ "--no-edit",
14268
+ spec
14269
+ ]);
14270
+ return classifyMergeOutput(`${stdout}
14271
+ ${stderr}`);
14272
+ } catch (e) {
14273
+ await runGit2(worktreePath, ["merge", "--abort"]).catch(() => {
14274
+ });
14275
+ const msg = e instanceof Error ? e.message : String(e);
14276
+ throw new SyncRemoteDataBranchMergeError(
14277
+ msg.toLowerCase().includes("conflict") ? "Merge conflict while syncing hyper-pm data branch; merge aborted. Resolve manually on the data branch if needed." : `git merge failed: ${msg.trim().split("\n")[0] ?? msg}`
14278
+ );
14279
+ }
14280
+ };
14281
+ var runRemoteDataBranchGitSync = async (worktreePath, remote, dataBranch, runGit2, skipPush, deps = {}) => {
14282
+ const tryPushFn = deps.tryPush ?? tryPushDataBranchToRemote;
14283
+ const maxPushAttempts = deps.maxPushAttempts ?? 3;
14284
+ const authorEnv = deps.authorEnv ?? env;
14285
+ let dataBranchFetch = await fetchRemoteDataBranch(worktreePath, remote, dataBranch, runGit2);
14286
+ let dataBranchMerge = dataBranchFetch === "skipped_no_remote" ? "skipped_no_remote" : dataBranchFetch === "remote_branch_absent" ? "skipped_missing_remote_branch" : "up_to_date";
14287
+ if (dataBranchFetch === "ok") {
14288
+ const tracking = remoteTrackingRef(remote, dataBranch);
14289
+ const exists = await refExists(worktreePath, tracking, runGit2);
14290
+ if (!exists) {
14291
+ dataBranchMerge = "skipped_missing_remote_branch";
14292
+ } else {
14293
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
14294
+ worktreePath,
14295
+ remote,
14296
+ dataBranch,
14297
+ runGit2,
14298
+ authorEnv
14299
+ );
14300
+ }
14301
+ }
14302
+ if (skipPush) {
14303
+ return {
14304
+ dataBranchFetch,
14305
+ dataBranchMerge,
14306
+ dataBranchPush: "skipped_cli",
14307
+ dataBranchPushDetail: "skip-push",
14308
+ pushAttempts: 0
14309
+ };
14310
+ }
14311
+ let pushAttempts = 0;
14312
+ let lastPush = { status: "skipped_no_remote" };
14313
+ const refetchAndMerge = async () => {
14314
+ dataBranchFetch = await fetchRemoteDataBranch(
14315
+ worktreePath,
14316
+ remote,
14317
+ dataBranch,
14318
+ runGit2
14319
+ );
14320
+ if (dataBranchFetch !== "ok") {
14321
+ return;
14322
+ }
14323
+ const tracking = remoteTrackingRef(remote, dataBranch);
14324
+ const exists = await refExists(worktreePath, tracking, runGit2);
14325
+ if (!exists) {
14326
+ return;
14327
+ }
14328
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
14329
+ worktreePath,
14330
+ remote,
14331
+ dataBranch,
14332
+ runGit2,
14333
+ authorEnv
14334
+ );
14335
+ };
14336
+ for (let attempt = 1; attempt <= maxPushAttempts; attempt += 1) {
14337
+ pushAttempts = attempt;
14338
+ lastPush = await tryPushFn(worktreePath, remote, dataBranch, runGit2);
14339
+ if (lastPush.status === "pushed" || lastPush.status === "skipped_no_remote") {
14340
+ break;
14341
+ }
14342
+ if (lastPush.status === "failed" && isLikelyNonFastForwardPushFailure(lastPush.detail) && attempt < maxPushAttempts) {
14343
+ await refetchAndMerge();
14344
+ continue;
14345
+ }
14346
+ break;
14347
+ }
14348
+ return {
14349
+ dataBranchFetch,
14350
+ dataBranchMerge,
14351
+ dataBranchPush: lastPush.status,
14352
+ ...lastPush.detail !== void 0 ? { dataBranchPushDetail: lastPush.detail } : {},
14353
+ pushAttempts
14354
+ };
14355
+ };
14356
+
14357
+ // src/storage/append-event.ts
14358
+ var import_promises5 = require("node:fs/promises");
14359
+ var import_node_path5 = require("node:path");
14360
+
14361
+ // src/storage/event-path.ts
14362
+ var nextEventRelPath = (now, deps) => {
14363
+ const y = String(now.getUTCFullYear());
14364
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
14365
+ const nextId = deps?.nextId ?? (() => ulid().toLowerCase());
14366
+ const part = `part-${nextId()}`;
14367
+ return `events/${y}/${m}/${part}.jsonl`;
14368
+ };
14369
+
14370
+ // src/storage/append-event.ts
14371
+ var appendEventLine = async (dataRoot, event, clock, opts) => {
14372
+ const rel = nextEventRelPath(clock.now(), {
14373
+ nextId: opts?.nextEventId
14374
+ });
14375
+ const abs = `${dataRoot.replace(/\/$/, "")}/${rel}`;
14376
+ await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(abs), { recursive: true });
14377
+ await (0, import_promises5.appendFile)(abs, `${JSON.stringify(event)}
14378
+ `, "utf8");
14379
+ return rel;
14380
+ };
14381
+
14382
+ // src/storage/read-event-lines.ts
14383
+ var import_promises6 = require("node:fs/promises");
14384
+ var import_node_path6 = require("node:path");
14385
+ var listJsonlFiles = async (dir, root) => {
14386
+ const out = [];
14387
+ const entries = await (0, import_promises6.readdir)(dir, { withFileTypes: true });
14388
+ for (const e of entries) {
14389
+ const abs = (0, import_node_path6.join)(dir, e.name);
14390
+ if (e.isDirectory()) {
14391
+ out.push(...await listJsonlFiles(abs, root));
14392
+ } else if (e.isFile() && e.name.endsWith(".jsonl")) {
14393
+ out.push((0, import_node_path6.relative)(root, abs).split("\\").join("/"));
14394
+ }
13959
14395
  }
14396
+ return out;
13960
14397
  };
13961
- var replayEvents = (lines) => {
13962
- const proj = emptyProjection();
13963
- const events = [];
13964
- for (const line of lines) {
13965
- const trimmed = line.trim();
13966
- if (!trimmed) continue;
13967
- const json = JSON.parse(trimmed);
13968
- events.push(eventLineSchema.parse(json));
14398
+ var readAllEventLines = async (dataRoot) => {
14399
+ const eventsRoot = (0, import_node_path6.join)(dataRoot, "events");
14400
+ let files = [];
14401
+ try {
14402
+ files = await listJsonlFiles(eventsRoot, dataRoot);
14403
+ } catch {
14404
+ return [];
13969
14405
  }
13970
- events.sort((a, b) => {
13971
- const t = a.ts.localeCompare(b.ts);
13972
- if (t !== 0) return t;
13973
- return a.id.localeCompare(b.id);
13974
- });
13975
- for (const e of events) {
13976
- applyEvent(proj, e);
14406
+ files.sort((a, b) => a.localeCompare(b));
14407
+ const lines = [];
14408
+ for (const rel of files) {
14409
+ const content = await (0, import_promises6.readFile)((0, import_node_path6.join)(dataRoot, rel), "utf8");
14410
+ for (const line of content.split("\n")) {
14411
+ if (line.trim()) lines.push(line);
14412
+ }
13977
14413
  }
13978
- return proj;
14414
+ return lines;
13979
14415
  };
13980
14416
 
13981
14417
  // src/sync/collect-github-pr-activity-source-ids.ts
@@ -14329,6 +14765,19 @@ var runGithubInboundSync = async (params) => {
14329
14765
  const planningSource = inboundTicketPlanningPayloadFromFenceMeta(meta);
14330
14766
  const planningPayload = {};
14331
14767
  for (const [k, v] of Object.entries(planningSource)) {
14768
+ if (k === "dependsOn") {
14769
+ if (v === null) {
14770
+ if (!ticketDependsOnListsEqual(ticket.dependsOn, [])) {
14771
+ planningPayload["dependsOn"] = null;
14772
+ }
14773
+ } else if (Array.isArray(v)) {
14774
+ const parsed = parseTicketDependsOnFromPayloadValue(v);
14775
+ if (parsed !== void 0 && !ticketDependsOnListsEqual(ticket.dependsOn, parsed)) {
14776
+ planningPayload["dependsOn"] = parsed;
14777
+ }
14778
+ }
14779
+ continue;
14780
+ }
14332
14781
  const tk = k;
14333
14782
  const cur = ticket[tk];
14334
14783
  if (v === null) {
@@ -14522,14 +14971,15 @@ var parseCliWorkItemStatusList = (raws, deps) => {
14522
14971
  }
14523
14972
  return out;
14524
14973
  };
14525
- var buildTicketListQueryFromReadListOpts = (o, deps) => {
14974
+ var buildTicketListQueryFromReadListOpts = (projection, o, deps) => {
14526
14975
  const query = {};
14527
14976
  const statusTokens = normalizeCliStringList(o.status);
14528
14977
  if (statusTokens.length > 0) {
14529
14978
  query.statuses = parseCliWorkItemStatusList(statusTokens, deps);
14530
14979
  }
14531
14980
  if (o.epic !== void 0 && o.epic !== "") {
14532
- query.epicId = o.epic;
14981
+ const trimmed = o.epic.trim();
14982
+ query.epicId = resolveEpicId(projection, trimmed) ?? trimmed;
14533
14983
  }
14534
14984
  const createdAfterMs = parseCliOptionalIsoMillis(
14535
14985
  o.createdAfter,
@@ -14666,6 +15116,10 @@ var buildTicketListQueryFromReadListOpts = (o, deps) => {
14666
15116
  if (targetFinishBeforeMs !== void 0) {
14667
15117
  query.targetFinishBeforeMs = targetFinishBeforeMs;
14668
15118
  }
15119
+ if (o.dependsOn !== void 0 && o.dependsOn.trim() !== "") {
15120
+ const trimmed = o.dependsOn.trim();
15121
+ query.dependsOnIncludesId = resolveTicketId(projection, trimmed) ?? trimmed;
15122
+ }
14669
15123
  return Object.keys(query).length > 0 ? query : void 0;
14670
15124
  };
14671
15125
  var runCli = async (argv, deps = {
@@ -14741,19 +15195,19 @@ var runCli = async (argv, deps = {
14741
15195
  const g = readGlobals(this);
14742
15196
  const o = this.opts();
14743
15197
  await mutateDataBranch(g, deps, async (root, { actor }) => {
15198
+ const lines = await readAllEventLines(root);
15199
+ const proj = replayEvents(lines);
14744
15200
  const id = o.id ?? ulid();
14745
15201
  const status = parseCliWorkItemStatus(o.status, deps);
14746
- const evt = makeEvent(
14747
- "EpicCreated",
14748
- {
14749
- id,
14750
- title: o.title,
14751
- body: o.body,
14752
- ...status !== void 0 ? { status } : {}
14753
- },
14754
- deps.clock,
14755
- actor
14756
- );
15202
+ const payload = {
15203
+ id,
15204
+ number: nextEpicNumberForCreate(proj),
15205
+ title: o.title,
15206
+ body: o.body,
15207
+ ...status !== void 0 ? { status } : {}
15208
+ };
15209
+ assertCreatePayloadUsesExpectedHeadNumber(proj, "epic", payload);
15210
+ const evt = makeEvent("EpicCreated", payload, deps.clock, actor);
14757
15211
  await appendEventLine(root, evt, deps.clock);
14758
15212
  return evt.payload;
14759
15213
  });
@@ -14769,18 +15223,19 @@ var runCli = async (argv, deps = {
14769
15223
  await mutateDataBranch(g, deps, async (root, { actor }) => {
14770
15224
  const lines = await readAllEventLines(root);
14771
15225
  const proj = replayEvents(lines);
14772
- const cur = proj.epics.get(o.id);
15226
+ const epicId = resolveEpicId(proj, o.id) ?? o.id.trim();
15227
+ const cur = proj.epics.get(epicId);
14773
15228
  if (!cur || cur.deleted) {
14774
15229
  throw new Error(`Epic not found: ${o.id}`);
14775
15230
  }
14776
15231
  const status = parseCliWorkItemStatus(o.status, deps);
14777
- const draft = { id: o.id };
15232
+ const draft = { id: epicId };
14778
15233
  if (o.title !== void 0) draft["title"] = o.title;
14779
15234
  if (o.body !== void 0) draft["body"] = o.body;
14780
15235
  if (status !== void 0) draft["status"] = status;
14781
15236
  const payload = pruneEpicOrStoryUpdatePayloadAgainstRow(cur, draft);
14782
15237
  if (isNoOpUpdatePayload(payload)) {
14783
- return { id: o.id, noChanges: true };
15238
+ return { id: epicId, noChanges: true };
14784
15239
  }
14785
15240
  const evt = makeEvent("EpicUpdated", payload, deps.clock, actor);
14786
15241
  await appendEventLine(root, evt, deps.clock);
@@ -14791,9 +15246,12 @@ var runCli = async (argv, deps = {
14791
15246
  const g = readGlobals(this);
14792
15247
  const o = this.opts();
14793
15248
  await mutateDataBranch(g, deps, async (root, { actor }) => {
14794
- const evt = makeEvent("EpicDeleted", { id: o.id }, deps.clock, actor);
15249
+ const lines = await readAllEventLines(root);
15250
+ const proj = replayEvents(lines);
15251
+ const epicId = resolveEpicId(proj, o.id) ?? o.id.trim();
15252
+ const evt = makeEvent("EpicDeleted", { id: epicId }, deps.clock, actor);
14795
15253
  await appendEventLine(root, evt, deps.clock);
14796
- return { id: o.id, deleted: true };
15254
+ return { id: epicId, deleted: true };
14797
15255
  });
14798
15256
  });
14799
15257
  const story = program2.command("story");
@@ -14806,24 +15264,23 @@ var runCli = async (argv, deps = {
14806
15264
  await mutateDataBranch(g, deps, async (root, { actor }) => {
14807
15265
  const lines = await readAllEventLines(root);
14808
15266
  const proj = replayEvents(lines);
14809
- const epic2 = proj.epics.get(o.epic);
15267
+ const epicId = resolveEpicId(proj, o.epic) ?? o.epic.trim();
15268
+ const epic2 = proj.epics.get(epicId);
14810
15269
  if (!epic2 || epic2.deleted) {
14811
15270
  throw new Error(`Epic not found: ${o.epic}`);
14812
15271
  }
14813
15272
  const id = o.id ?? ulid();
14814
15273
  const status = parseCliWorkItemStatus(o.status, deps);
14815
- const evt = makeEvent(
14816
- "StoryCreated",
14817
- {
14818
- id,
14819
- epicId: o.epic,
14820
- title: o.title,
14821
- body: o.body,
14822
- ...status !== void 0 ? { status } : {}
14823
- },
14824
- deps.clock,
14825
- actor
14826
- );
15274
+ const payload = {
15275
+ id,
15276
+ number: nextStoryNumberForCreate(proj),
15277
+ epicId,
15278
+ title: o.title,
15279
+ body: o.body,
15280
+ ...status !== void 0 ? { status } : {}
15281
+ };
15282
+ assertCreatePayloadUsesExpectedHeadNumber(proj, "story", payload);
15283
+ const evt = makeEvent("StoryCreated", payload, deps.clock, actor);
14827
15284
  await appendEventLine(root, evt, deps.clock);
14828
15285
  return evt.payload;
14829
15286
  });
@@ -14842,18 +15299,19 @@ var runCli = async (argv, deps = {
14842
15299
  await mutateDataBranch(g, deps, async (root, { actor }) => {
14843
15300
  const lines = await readAllEventLines(root);
14844
15301
  const proj = replayEvents(lines);
14845
- const cur = proj.stories.get(o.id);
15302
+ const storyId = resolveStoryId(proj, o.id) ?? o.id.trim();
15303
+ const cur = proj.stories.get(storyId);
14846
15304
  if (!cur || cur.deleted) {
14847
15305
  throw new Error(`Story not found: ${o.id}`);
14848
15306
  }
14849
15307
  const status = parseCliWorkItemStatus(o.status, deps);
14850
- const draft = { id: o.id };
15308
+ const draft = { id: storyId };
14851
15309
  if (o.title !== void 0) draft["title"] = o.title;
14852
15310
  if (o.body !== void 0) draft["body"] = o.body;
14853
15311
  if (status !== void 0) draft["status"] = status;
14854
15312
  const payload = pruneEpicOrStoryUpdatePayloadAgainstRow(cur, draft);
14855
15313
  if (isNoOpUpdatePayload(payload)) {
14856
- return { id: o.id, noChanges: true };
15314
+ return { id: storyId, noChanges: true };
14857
15315
  }
14858
15316
  const evt = makeEvent("StoryUpdated", payload, deps.clock, actor);
14859
15317
  await appendEventLine(root, evt, deps.clock);
@@ -14864,9 +15322,17 @@ var runCli = async (argv, deps = {
14864
15322
  const g = readGlobals(this);
14865
15323
  const o = this.opts();
14866
15324
  await mutateDataBranch(g, deps, async (root, { actor }) => {
14867
- const evt = makeEvent("StoryDeleted", { id: o.id }, deps.clock, actor);
15325
+ const lines = await readAllEventLines(root);
15326
+ const proj = replayEvents(lines);
15327
+ const storyId = resolveStoryId(proj, o.id) ?? o.id.trim();
15328
+ const evt = makeEvent(
15329
+ "StoryDeleted",
15330
+ { id: storyId },
15331
+ deps.clock,
15332
+ actor
15333
+ );
14868
15334
  await appendEventLine(root, evt, deps.clock);
14869
- return { id: o.id, deleted: true };
15335
+ return { id: storyId, deleted: true };
14870
15336
  });
14871
15337
  });
14872
15338
  const ticket = program2.command("ticket");
@@ -14889,6 +15355,11 @@ var runCli = async (argv, deps = {
14889
15355
  "planning label (repeatable)",
14890
15356
  (value, previous) => [...previous, value],
14891
15357
  []
15358
+ ).option(
15359
+ "--depends-on <id>",
15360
+ "prerequisite ticket id (repeatable)",
15361
+ (value, previous) => [...previous, value],
15362
+ []
14892
15363
  ).option("--priority <p>", "low|medium|high|urgent").option("--size <s>", "xs|s|m|l|xl").option("--estimate <n>", "non-negative estimate (e.g. story points)").option("--start-at <iso>", "planned start work at (ISO-8601)").option("--target-finish-at <iso>", "planned target finish at (ISO-8601)").option("--ai-draft", "draft body via AI (explicit)", false).action(async function() {
14893
15364
  const g = readGlobals(this);
14894
15365
  const o = this.opts();
@@ -14951,6 +15422,7 @@ var runCli = async (argv, deps = {
14951
15422
  "--target-finish-at",
14952
15423
  deps
14953
15424
  );
15425
+ const dependsOnTokensCreate = normalizeCliStringList(o.dependsOn);
14954
15426
  const planningPayload = {
14955
15427
  ...labelsPayloadCreate,
14956
15428
  ...priorityParsed !== void 0 ? { priority: priorityParsed } : {},
@@ -14964,31 +15436,53 @@ var runCli = async (argv, deps = {
14964
15436
  const proj = replayEvents(lines);
14965
15437
  const storyRaw = o.story;
14966
15438
  const storyTrimmed = storyRaw !== void 0 && storyRaw !== "" ? storyRaw.trim() : void 0;
15439
+ const storyIdResolved = storyTrimmed !== void 0 ? resolveStoryId(proj, storyTrimmed) ?? storyTrimmed : void 0;
14967
15440
  if (storyTrimmed !== void 0) {
14968
- const storyRow = proj.stories.get(storyTrimmed);
15441
+ const storyRow = proj.stories.get(storyIdResolved ?? "");
14969
15442
  if (!storyRow || storyRow.deleted) {
14970
15443
  throw new Error(`Story not found: ${storyTrimmed}`);
14971
15444
  }
14972
15445
  }
14973
15446
  const id = o.id ?? ulid();
15447
+ const dependsOnNormCreate = resolveTicketDependsOnTokensToIds(
15448
+ proj,
15449
+ dependsOnTokensCreate
15450
+ );
14974
15451
  const status = parseCliWorkItemStatus(o.status, deps);
14975
15452
  const assigneeCreate = o.assignee !== void 0 ? { assignee: normalizeGithubLogin(o.assignee) } : {};
14976
- const storyPayload = storyTrimmed !== void 0 ? { storyId: storyTrimmed } : {};
15453
+ const storyPayload = storyIdResolved !== void 0 ? { storyId: storyIdResolved } : {};
14977
15454
  const branchTokens = normalizeCliStringList(o.branch);
14978
15455
  const branchesNorm = normalizeTicketBranchListFromStrings(branchTokens);
14979
15456
  const branchesPayload = branchesNorm.length > 0 ? { branches: branchesNorm } : {};
15457
+ const dependsOnErr = validateTicketDependsOnForWrite({
15458
+ projection: proj,
15459
+ fromTicketId: id,
15460
+ nextDependsOn: dependsOnNormCreate
15461
+ });
15462
+ if (dependsOnErr !== void 0) {
15463
+ throw new Error(dependsOnErr);
15464
+ }
15465
+ const dependsOnPayloadCreate = dependsOnNormCreate.length > 0 ? { dependsOn: dependsOnNormCreate } : {};
15466
+ const createPayload = {
15467
+ id,
15468
+ number: nextTicketNumberForCreate(proj),
15469
+ ...storyPayload,
15470
+ title: o.title,
15471
+ body,
15472
+ ...status !== void 0 ? { status } : {},
15473
+ ...assigneeCreate,
15474
+ ...branchesPayload,
15475
+ ...dependsOnPayloadCreate,
15476
+ ...planningPayload
15477
+ };
15478
+ assertCreatePayloadUsesExpectedHeadNumber(
15479
+ proj,
15480
+ "ticket",
15481
+ createPayload
15482
+ );
14980
15483
  const evt = makeEvent(
14981
15484
  "TicketCreated",
14982
- {
14983
- id,
14984
- ...storyPayload,
14985
- title: o.title,
14986
- body,
14987
- ...status !== void 0 ? { status } : {},
14988
- ...assigneeCreate,
14989
- ...branchesPayload,
14990
- ...planningPayload
14991
- },
15485
+ createPayload,
14992
15486
  deps.clock,
14993
15487
  actor
14994
15488
  );
@@ -15044,6 +15538,9 @@ var runCli = async (argv, deps = {
15044
15538
  ).option(
15045
15539
  "--branch <name>",
15046
15540
  "when listing (no --id): only tickets linked to this branch (normalized exact match)"
15541
+ ).option(
15542
+ "--depends-on <id>",
15543
+ "when listing (no --id): only tickets that list this ticket id in dependsOn"
15047
15544
  ).option(
15048
15545
  "--priority <p>",
15049
15546
  "when listing (no --id): OR-set of priorities (repeat flag); low|medium|high|urgent",
@@ -15123,7 +15620,17 @@ var runCli = async (argv, deps = {
15123
15620
  "--clear-labels",
15124
15621
  "remove all planning labels from the ticket",
15125
15622
  false
15126
- ).option("--priority <p>", "low|medium|high|urgent").option("--clear-priority", "remove priority", false).option("--size <s>", "xs|s|m|l|xl").option("--clear-size", "remove size", false).option("--estimate <n>", "non-negative estimate (e.g. story points)").option("--clear-estimate", "remove estimate", false).option("--start-at <iso>", "planned start work at (ISO-8601)").option("--clear-start-at", "remove start work date", false).option("--target-finish-at <iso>", "planned target finish at (ISO-8601)").option("--clear-target-finish-at", "remove target finish date", false).option("--ai-improve", "expand description via AI (explicit)", false).action(async function() {
15623
+ ).option(
15624
+ "--add-depends-on <id>",
15625
+ "add a prerequisite ticket id (repeatable)",
15626
+ (value, previous) => [...previous, value],
15627
+ []
15628
+ ).option(
15629
+ "--remove-depends-on <id>",
15630
+ "remove a prerequisite ticket id (repeatable)",
15631
+ (value, previous) => [...previous, value],
15632
+ []
15633
+ ).option("--clear-depends-on", "remove all ticket dependencies", false).option("--priority <p>", "low|medium|high|urgent").option("--clear-priority", "remove priority", false).option("--size <s>", "xs|s|m|l|xl").option("--clear-size", "remove size", false).option("--estimate <n>", "non-negative estimate (e.g. story points)").option("--clear-estimate", "remove estimate", false).option("--start-at <iso>", "planned start work at (ISO-8601)").option("--clear-start-at", "remove start work date", false).option("--target-finish-at <iso>", "planned target finish at (ISO-8601)").option("--clear-target-finish-at", "remove target finish date", false).option("--ai-improve", "expand description via AI (explicit)", false).action(async function() {
15127
15634
  const g = readGlobals(this);
15128
15635
  const o = this.opts();
15129
15636
  let body = o.body;
@@ -15175,6 +15682,14 @@ ${body}`
15175
15682
  );
15176
15683
  deps.exit(ExitCode.UserError);
15177
15684
  }
15685
+ const addDependsOnTokens = normalizeCliStringList(o.addDependsOn);
15686
+ const removeDependsOnTokens = normalizeCliStringList(o.removeDependsOn);
15687
+ if (o.clearDependsOn === true && (addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0)) {
15688
+ deps.error(
15689
+ "Cannot use --clear-depends-on with --add-depends-on or --remove-depends-on"
15690
+ );
15691
+ deps.exit(ExitCode.UserError);
15692
+ }
15178
15693
  const mutual = (clear, set, clearName, setName) => {
15179
15694
  if (clear === true && set !== void 0 && set !== "") {
15180
15695
  deps.error(`Cannot use ${clearName} and ${setName} together`);
@@ -15239,25 +15754,27 @@ ${body}`
15239
15754
  await mutateDataBranch(g, deps, async (root, { actor }) => {
15240
15755
  const lines = await readAllEventLines(root);
15241
15756
  const proj = replayEvents(lines);
15757
+ const ticketId = resolveTicketId(proj, o.id) ?? o.id.trim();
15758
+ const storyIdResolved = storyTrimmed !== void 0 ? resolveStoryId(proj, storyTrimmed) ?? storyTrimmed : void 0;
15242
15759
  if (storyTrimmed !== void 0) {
15243
- const storyRow = proj.stories.get(storyTrimmed);
15760
+ const storyRow = proj.stories.get(storyIdResolved ?? "");
15244
15761
  if (!storyRow || storyRow.deleted) {
15245
15762
  throw new Error(`Story not found: ${storyTrimmed}`);
15246
15763
  }
15247
15764
  }
15248
- const curTicket = proj.tickets.get(o.id);
15765
+ const curTicket = proj.tickets.get(ticketId);
15249
15766
  if (curTicket === void 0 || curTicket.deleted) {
15250
15767
  throw new Error(`Ticket not found: ${o.id}`);
15251
15768
  }
15252
15769
  const status = parseCliWorkItemStatus(o.status, deps);
15253
- const payload = { id: o.id };
15770
+ const payload = { id: ticketId };
15254
15771
  if (o.title !== void 0) payload["title"] = o.title;
15255
15772
  if (body !== void 0) payload["body"] = body;
15256
15773
  if (status !== void 0) payload["status"] = status;
15257
15774
  if (o.unlinkStory) {
15258
15775
  payload["storyId"] = null;
15259
15776
  } else if (storyTrimmed !== void 0) {
15260
- payload["storyId"] = storyTrimmed;
15777
+ payload["storyId"] = storyIdResolved;
15261
15778
  }
15262
15779
  if (o.unassign) {
15263
15780
  payload["assignee"] = null;
@@ -15307,6 +15824,35 @@ ${body}`
15307
15824
  payload["labels"] = nextLabels;
15308
15825
  }
15309
15826
  }
15827
+ const wantsDependsOnChange = o.clearDependsOn === true || addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0;
15828
+ if (wantsDependsOnChange) {
15829
+ let nextDepends;
15830
+ if (o.clearDependsOn === true) {
15831
+ nextDepends = [];
15832
+ } else {
15833
+ const removeDepSet = new Set(
15834
+ resolveTicketDependsOnTokensToIds(proj, removeDependsOnTokens)
15835
+ );
15836
+ nextDepends = normalizeTicketDependsOnIds(
15837
+ (curTicket.dependsOn ?? []).filter((d) => !removeDepSet.has(d))
15838
+ );
15839
+ nextDepends = normalizeTicketDependsOnIds([
15840
+ ...nextDepends,
15841
+ ...resolveTicketDependsOnTokensToIds(proj, addDependsOnTokens)
15842
+ ]);
15843
+ }
15844
+ const depErr = validateTicketDependsOnForWrite({
15845
+ projection: proj,
15846
+ fromTicketId: ticketId,
15847
+ nextDependsOn: nextDepends
15848
+ });
15849
+ if (depErr !== void 0) {
15850
+ throw new Error(depErr);
15851
+ }
15852
+ if (!ticketDependsOnListsEqual(curTicket.dependsOn, nextDepends)) {
15853
+ payload["dependsOn"] = nextDepends;
15854
+ }
15855
+ }
15310
15856
  if (priorityUpdate !== void 0) {
15311
15857
  payload["priority"] = priorityUpdate;
15312
15858
  }
@@ -15327,7 +15873,7 @@ ${body}`
15327
15873
  payload
15328
15874
  );
15329
15875
  if (isNoOpUpdatePayload(prunedPayload)) {
15330
- return { id: o.id, noChanges: true };
15876
+ return { id: ticketId, noChanges: true };
15331
15877
  }
15332
15878
  const evt = makeEvent(
15333
15879
  "TicketUpdated",
@@ -15364,25 +15910,25 @@ ${body}`
15364
15910
  runGit
15365
15911
  );
15366
15912
  }
15367
- const preferred = normalizeTicketBranchName(
15368
- o.branch ?? `hyper-pm/${o.id}`
15369
- );
15370
- if (preferred === void 0) {
15371
- deps.error(
15372
- "Invalid --branch or default branch name for this ticket id"
15373
- );
15374
- deps.exit(ExitCode.UserError);
15375
- }
15376
15913
  await mutateDataBranch(g, deps, async (root, { actor }) => {
15377
15914
  const lines = await readAllEventLines(root);
15378
15915
  const proj = replayEvents(lines);
15379
- const curRow = proj.tickets.get(o.id);
15916
+ const ticketId = resolveTicketId(proj, o.id) ?? o.id.trim();
15917
+ const preferred = normalizeTicketBranchName(
15918
+ o.branch ?? `hyper-pm/${ticketId}`
15919
+ );
15920
+ if (preferred === void 0) {
15921
+ throw new Error(
15922
+ "Invalid --branch or default branch name for this ticket id"
15923
+ );
15924
+ }
15925
+ const curRow = proj.tickets.get(ticketId);
15380
15926
  if (curRow === void 0 || curRow.deleted) {
15381
15927
  throw new Error(`Ticket not found: ${o.id}`);
15382
15928
  }
15383
15929
  if (curRow.status === "done" || curRow.status === "cancelled") {
15384
15930
  throw new Error(
15385
- `Ticket ${o.id} is ${curRow.status}; change status before starting work`
15931
+ `Ticket ${ticketId} is ${curRow.status}; change status before starting work`
15386
15932
  );
15387
15933
  }
15388
15934
  const { branch: chosenBranch } = await pickUniqueLocalBranchName({
@@ -15403,7 +15949,7 @@ ${body}`
15403
15949
  }
15404
15950
  next = normalizeTicketBranchListFromStrings(next);
15405
15951
  const payload = {
15406
- id: o.id,
15952
+ id: ticketId,
15407
15953
  status: "in_progress"
15408
15954
  };
15409
15955
  if (!ticketBranchListsEqual(next, curRow.linkedBranches)) {
@@ -15412,7 +15958,7 @@ ${body}`
15412
15958
  const evt = makeEvent("TicketUpdated", payload, deps.clock, actor);
15413
15959
  await appendEventLine(root, evt, deps.clock);
15414
15960
  const result = {
15415
- id: o.id,
15961
+ id: ticketId,
15416
15962
  status: "in_progress",
15417
15963
  branch: chosenBranch,
15418
15964
  branches: next
@@ -15434,27 +15980,36 @@ ${body}`
15434
15980
  await mutateDataBranch(g, deps, async (root, { actor }) => {
15435
15981
  const lines = await readAllEventLines(root);
15436
15982
  const proj = replayEvents(lines);
15437
- const row = proj.tickets.get(o.id);
15983
+ const ticketId = resolveTicketId(proj, o.id) ?? o.id.trim();
15984
+ const row = proj.tickets.get(ticketId);
15438
15985
  if (!row || row.deleted) {
15439
15986
  throw new Error(`Ticket not found: ${o.id}`);
15440
15987
  }
15441
15988
  const evt = makeEvent(
15442
15989
  "TicketCommentAdded",
15443
- { ticketId: o.id, body: trimmed },
15990
+ { ticketId, body: trimmed },
15444
15991
  deps.clock,
15445
15992
  actor
15446
15993
  );
15447
15994
  await appendEventLine(root, evt, deps.clock);
15448
- return { commentId: evt.id, ticketId: o.id, body: trimmed };
15995
+ return { commentId: evt.id, ticketId, body: trimmed };
15449
15996
  });
15450
15997
  });
15451
15998
  ticket.command("delete").requiredOption("--id <id>").action(async function() {
15452
15999
  const g = readGlobals(this);
15453
16000
  const o = this.opts();
15454
16001
  await mutateDataBranch(g, deps, async (root, { actor }) => {
15455
- const evt = makeEvent("TicketDeleted", { id: o.id }, deps.clock, actor);
16002
+ const lines = await readAllEventLines(root);
16003
+ const proj = replayEvents(lines);
16004
+ const ticketId = resolveTicketId(proj, o.id) ?? o.id.trim();
16005
+ const evt = makeEvent(
16006
+ "TicketDeleted",
16007
+ { id: ticketId },
16008
+ deps.clock,
16009
+ actor
16010
+ );
15456
16011
  await appendEventLine(root, evt, deps.clock);
15457
- return { id: o.id, deleted: true };
16012
+ return { id: ticketId, deleted: true };
15458
16013
  });
15459
16014
  });
15460
16015
  ticket.command("import-github").description(
@@ -15525,7 +16080,8 @@ ${body}`
15525
16080
  const storyRaw = o.story;
15526
16081
  const storyTrimmed = storyRaw !== void 0 && storyRaw !== "" ? storyRaw.trim() : void 0;
15527
16082
  if (storyTrimmed !== void 0 && storyTrimmed !== "") {
15528
- const storyRow = proj.stories.get(storyTrimmed);
16083
+ const storyIdForImport = resolveStoryId(proj, storyTrimmed) ?? storyTrimmed;
16084
+ const storyRow = proj.stories.get(storyIdForImport);
15529
16085
  if (!storyRow || storyRow.deleted) {
15530
16086
  deps.error(`Story not found: ${storyTrimmed}`);
15531
16087
  deps.exit(ExitCode.UserError);
@@ -15568,12 +16124,22 @@ ${body}`
15568
16124
  );
15569
16125
  } else {
15570
16126
  const imported = [];
16127
+ let importProj = proj;
16128
+ let importLines = lines;
16129
+ const storyIdForPayload = storyTrimmed !== void 0 && storyTrimmed !== "" ? resolveStoryId(importProj, storyTrimmed) ?? storyTrimmed : void 0;
15571
16130
  for (const c of candidates) {
15572
16131
  const ticketId = ulid();
16132
+ const nextNum = nextTicketNumberForCreate(importProj);
15573
16133
  const createPayload = mergeTicketImportCreatePayload(
15574
16134
  ticketId,
15575
16135
  c.ticketCreatedPayloadBase,
15576
- storyTrimmed
16136
+ storyIdForPayload,
16137
+ nextNum
16138
+ );
16139
+ assertCreatePayloadUsesExpectedHeadNumber(
16140
+ importProj,
16141
+ "ticket",
16142
+ createPayload
15577
16143
  );
15578
16144
  const createdEvt = makeEvent(
15579
16145
  "TicketCreated",
@@ -15594,6 +16160,8 @@ ${body}`
15594
16160
  );
15595
16161
  await appendEventLine(session.worktreePath, linkEvt, deps.clock);
15596
16162
  imported.push({ ticketId, issueNumber: c.issueNumber });
16163
+ importLines = await readAllEventLines(session.worktreePath);
16164
+ importProj = replayEvents(importLines);
15597
16165
  }
15598
16166
  await commitDataWorktreeIfNeeded(
15599
16167
  session.worktreePath,
@@ -15620,7 +16188,19 @@ ${body}`
15620
16188
  }
15621
16189
  deps.exit(ExitCode.Success);
15622
16190
  });
15623
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).option(
16191
+ program2.command("sync").description("Sync data branch over git; optional GitHub Issues sync").option(
16192
+ "--skip-network",
16193
+ "skip all sync network operations (git fetch/merge/push and GitHub); legacy: --no-github",
16194
+ false
16195
+ ).option(
16196
+ "--with-github",
16197
+ "also run GitHub Issues sync (requires GITHUB_TOKEN or gh; needs sync not off in config)",
16198
+ false
16199
+ ).option(
16200
+ "--git-data",
16201
+ "legacy no-op: default sync already updates the data branch via git only",
16202
+ false
16203
+ ).option(
15624
16204
  "--skip-push",
15625
16205
  "after a successful sync, do not push the data branch to the remote",
15626
16206
  false
@@ -15629,115 +16209,171 @@ ${body}`
15629
16209
  const o = this.opts();
15630
16210
  const repoRoot = await resolveRepoRoot(g.repo);
15631
16211
  const cfg = await loadMergedConfig(repoRoot, g);
15632
- if (cfg.sync === "off" || o.noGithub) {
16212
+ if (o.withGithub && o.skipNetwork) {
16213
+ deps.error("Cannot use --with-github together with --skip-network");
16214
+ deps.exit(ExitCode.UserError);
16215
+ }
16216
+ if (o.skipNetwork) {
15633
16217
  deps.log(formatOutput(g.format, { ok: true, skipped: true }));
15634
16218
  deps.exit(ExitCode.Success);
15635
16219
  }
15636
- const githubToken = await resolveGithubTokenForSync({
15637
- envToken: env.GITHUB_TOKEN,
15638
- cwd: repoRoot
15639
- });
15640
- if (!githubToken) {
15641
- deps.error(
15642
- "GitHub auth required for sync: set GITHUB_TOKEN or run `gh auth login`"
15643
- );
15644
- deps.exit(ExitCode.EnvironmentAuth);
15645
- }
15646
- const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
15647
- const session = await openDataBranchWorktree({
15648
- repoRoot,
15649
- dataBranch: cfg.dataBranch,
15650
- tmpBase,
15651
- keepWorktree: g.keepWorktree,
15652
- runGit
15653
- });
15654
- try {
15655
- const projection = await loadProjectionFromDataRoot(
15656
- session.worktreePath
15657
- );
15658
- const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
16220
+ if (o.withGithub) {
16221
+ if (cfg.sync === "off") {
16222
+ deps.error(
16223
+ "GitHub Issues sync is off in config (sync: off). Omit --with-github to sync the data branch via git only, or set sync to outbound/full."
16224
+ );
16225
+ deps.exit(ExitCode.UserError);
16226
+ }
16227
+ const githubToken = await resolveGithubTokenForSync({
16228
+ envToken: env.GITHUB_TOKEN,
16229
+ cwd: repoRoot
16230
+ });
16231
+ if (!githubToken) {
16232
+ deps.error(
16233
+ "GitHub auth required for sync --with-github: set GITHUB_TOKEN or run `gh auth login`"
16234
+ );
16235
+ deps.exit(ExitCode.EnvironmentAuth);
16236
+ }
16237
+ const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16238
+ const session = await openDataBranchWorktree({
15659
16239
  repoRoot,
15660
- remote: cfg.remote,
16240
+ dataBranch: cfg.dataBranch,
16241
+ tmpBase,
16242
+ keepWorktree: g.keepWorktree,
15661
16243
  runGit
15662
16244
  });
15663
- const { owner, repo: repo2 } = resolveGithubRepo(
15664
- cfg,
15665
- env.GITHUB_REPO,
15666
- gitDerivedSlug
15667
- );
15668
- const octokit = new Octokit2({ auth: githubToken });
15669
- const outboundActor = await resolveGithubTokenActor(octokit);
15670
- const depsGh = {
15671
- octokit,
15672
- owner,
15673
- repo: repo2,
15674
- clock: deps.clock,
15675
- outboundActor
15676
- };
15677
- await runGithubOutboundSync({
15678
- dataRoot: session.worktreePath,
15679
- projection,
15680
- config: cfg,
15681
- deps: depsGh
15682
- });
15683
- await runGithubInboundSync({
15684
- dataRoot: session.worktreePath,
15685
- projection,
15686
- config: cfg,
15687
- deps: depsGh
15688
- });
15689
- const projectionAfterInbound = await loadProjectionFromDataRoot(
15690
- session.worktreePath
15691
- );
15692
- await runGithubPrActivitySync({
15693
- projection: projectionAfterInbound,
15694
- config: cfg,
15695
- deps: defaultGithubPrActivitySyncDeps({
15696
- dataRoot: session.worktreePath,
15697
- clock: deps.clock,
16245
+ try {
16246
+ const projection = await loadProjectionFromDataRoot(
16247
+ session.worktreePath
16248
+ );
16249
+ const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
16250
+ repoRoot,
16251
+ remote: cfg.remote,
16252
+ runGit
16253
+ });
16254
+ const { owner, repo: repo2 } = resolveGithubRepo(
16255
+ cfg,
16256
+ env.GITHUB_REPO,
16257
+ gitDerivedSlug
16258
+ );
16259
+ const octokit = new Octokit2({ auth: githubToken });
16260
+ const outboundActor = await resolveGithubTokenActor(octokit);
16261
+ const depsGh = {
15698
16262
  octokit,
15699
16263
  owner,
15700
16264
  repo: repo2,
15701
- actor: outboundActor
15702
- })
15703
- });
15704
- await commitDataWorktreeIfNeeded(
15705
- session.worktreePath,
15706
- formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15707
- runGit
15708
- );
15709
- let dataBranchPush;
15710
- let dataBranchPushDetail;
15711
- if (o.skipPush) {
15712
- dataBranchPush = "skipped_cli";
15713
- dataBranchPushDetail = "skip-push";
15714
- } else {
15715
- const pushResult = await tryPushDataBranchToRemote(
16265
+ clock: deps.clock,
16266
+ outboundActor
16267
+ };
16268
+ await runGithubOutboundSync({
16269
+ dataRoot: session.worktreePath,
16270
+ projection,
16271
+ config: cfg,
16272
+ deps: depsGh
16273
+ });
16274
+ await runGithubInboundSync({
16275
+ dataRoot: session.worktreePath,
16276
+ projection,
16277
+ config: cfg,
16278
+ deps: depsGh
16279
+ });
16280
+ const projectionAfterInbound = await loadProjectionFromDataRoot(
16281
+ session.worktreePath
16282
+ );
16283
+ await runGithubPrActivitySync({
16284
+ projection: projectionAfterInbound,
16285
+ config: cfg,
16286
+ deps: defaultGithubPrActivitySyncDeps({
16287
+ dataRoot: session.worktreePath,
16288
+ clock: deps.clock,
16289
+ octokit,
16290
+ owner,
16291
+ repo: repo2,
16292
+ actor: outboundActor
16293
+ })
16294
+ });
16295
+ await commitDataWorktreeIfNeeded(
15716
16296
  session.worktreePath,
15717
- cfg.remote,
15718
- cfg.dataBranch,
16297
+ formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15719
16298
  runGit
15720
16299
  );
15721
- dataBranchPush = pushResult.status;
15722
- dataBranchPushDetail = pushResult.detail;
15723
- if (pushResult.status === "failed" && pushResult.detail) {
15724
- deps.error(
15725
- `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
16300
+ let dataBranchPush;
16301
+ let dataBranchPushDetail;
16302
+ if (o.skipPush) {
16303
+ dataBranchPush = "skipped_cli";
16304
+ dataBranchPushDetail = "skip-push";
16305
+ } else {
16306
+ const pushResult = await tryPushDataBranchToRemote(
16307
+ session.worktreePath,
16308
+ cfg.remote,
16309
+ cfg.dataBranch,
16310
+ runGit
15726
16311
  );
16312
+ dataBranchPush = pushResult.status;
16313
+ dataBranchPushDetail = pushResult.detail;
16314
+ if (pushResult.status === "failed" && pushResult.detail) {
16315
+ deps.error(
16316
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
16317
+ );
16318
+ }
15727
16319
  }
16320
+ deps.log(
16321
+ formatOutput(g.format, {
16322
+ ok: true,
16323
+ githubSync: true,
16324
+ dataBranchPush,
16325
+ ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16326
+ })
16327
+ );
16328
+ } catch (e) {
16329
+ deps.error(e instanceof Error ? e.message : String(e));
16330
+ deps.exit(ExitCode.ExternalApi);
16331
+ } finally {
16332
+ await session.dispose();
16333
+ }
16334
+ deps.exit(ExitCode.Success);
16335
+ }
16336
+ const tmpBaseGit = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16337
+ const sessionGit = await openDataBranchWorktree({
16338
+ repoRoot,
16339
+ dataBranch: cfg.dataBranch,
16340
+ tmpBase: tmpBaseGit,
16341
+ keepWorktree: g.keepWorktree,
16342
+ runGit
16343
+ });
16344
+ try {
16345
+ let syncResult;
16346
+ try {
16347
+ syncResult = await runRemoteDataBranchGitSync(
16348
+ sessionGit.worktreePath,
16349
+ cfg.remote,
16350
+ cfg.dataBranch,
16351
+ runGit,
16352
+ Boolean(o.skipPush)
16353
+ );
16354
+ } catch (e) {
16355
+ if (e instanceof SyncRemoteDataBranchMergeError) {
16356
+ deps.error(e.message);
16357
+ deps.exit(ExitCode.UserError);
16358
+ }
16359
+ deps.error(e instanceof Error ? e.message : String(e));
16360
+ deps.exit(ExitCode.UserError);
16361
+ }
16362
+ if (syncResult.dataBranchPush === "failed" && syncResult.dataBranchPushDetail !== void 0) {
16363
+ deps.error(
16364
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${syncResult.dataBranchPushDetail}`
16365
+ );
15728
16366
  }
15729
16367
  deps.log(
15730
16368
  formatOutput(g.format, {
15731
16369
  ok: true,
15732
- dataBranchPush,
15733
- ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16370
+ gitDataOnly: true,
16371
+ ...o.gitData ? { legacyGitDataFlag: true } : {},
16372
+ ...syncResult
15734
16373
  })
15735
16374
  );
15736
- } catch (e) {
15737
- deps.error(e instanceof Error ? e.message : String(e));
15738
- deps.exit(ExitCode.ExternalApi);
15739
16375
  } finally {
15740
- await session.dispose();
16376
+ await sessionGit.dispose();
15741
16377
  }
15742
16378
  deps.exit(ExitCode.Success);
15743
16379
  });
@@ -15903,7 +16539,7 @@ ${body}`
15903
16539
  await session.dispose();
15904
16540
  }
15905
16541
  });
15906
- await program2.parseAsync(argv, { from: "node" });
16542
+ await program2.parseAsync(normalizeRawCliArgv(argv), { from: "node" });
15907
16543
  };
15908
16544
  var makeEvent = (type, payload, clock, actor) => ({
15909
16545
  schema: 1,
@@ -15993,7 +16629,8 @@ var readEpic = async (g, id, deps) => {
15993
16629
  formatOutput(g.format, { items: listActiveEpicSummaries(proj) })
15994
16630
  );
15995
16631
  } else {
15996
- const row = proj.epics.get(id);
16632
+ const epicId = resolveEpicId(proj, id) ?? id.trim();
16633
+ const row = proj.epics.get(epicId);
15997
16634
  if (!row || row.deleted) {
15998
16635
  deps.error("Epic not found");
15999
16636
  exitCode = ExitCode.UserError;
@@ -16028,7 +16665,8 @@ var readStory = async (g, opts, deps) => {
16028
16665
  const proj = replayEvents(lines);
16029
16666
  const { id, epicId } = opts;
16030
16667
  if (id === void 0 || id === "") {
16031
- const epicFilter = epicId !== void 0 && epicId !== "" ? epicId : void 0;
16668
+ const epicFilterRaw = epicId !== void 0 && epicId !== "" ? epicId : void 0;
16669
+ const epicFilter = epicFilterRaw !== void 0 ? resolveEpicId(proj, epicFilterRaw) ?? epicFilterRaw.trim() : void 0;
16032
16670
  if (epicFilter !== void 0) {
16033
16671
  const epicRow = proj.epics.get(epicFilter);
16034
16672
  if (!epicRow || epicRow.deleted) {
@@ -16049,7 +16687,8 @@ var readStory = async (g, opts, deps) => {
16049
16687
  );
16050
16688
  }
16051
16689
  } else {
16052
- const row = proj.stories.get(id);
16690
+ const storyId = resolveStoryId(proj, id) ?? id.trim();
16691
+ const row = proj.stories.get(storyId);
16053
16692
  if (!row || row.deleted) {
16054
16693
  deps.error("Story not found");
16055
16694
  exitCode = ExitCode.UserError;
@@ -16093,8 +16732,8 @@ var readTicket = async (g, opts, deps) => {
16093
16732
  } = opts;
16094
16733
  if (id === void 0 || id === "") {
16095
16734
  const listWithoutStory = withoutStoryRaw === true;
16096
- const storyFilter = storyIdRaw !== void 0 && storyIdRaw !== "" ? storyIdRaw : void 0;
16097
- const epicFilter = epicIdRaw !== void 0 && epicIdRaw !== "" ? epicIdRaw : void 0;
16735
+ const storyFilter = storyIdRaw !== void 0 && storyIdRaw !== "" ? resolveStoryId(proj, storyIdRaw) ?? storyIdRaw.trim() : void 0;
16736
+ const epicFilter = epicIdRaw !== void 0 && epicIdRaw !== "" ? resolveEpicId(proj, epicIdRaw) ?? epicIdRaw.trim() : void 0;
16098
16737
  const sortBy = tryParseTicketListSortField(sortByOpt);
16099
16738
  const sortDir = tryParseTicketListSortDir(sortDirOpt);
16100
16739
  if (sortBy === void 0) {
@@ -16133,6 +16772,7 @@ var readTicket = async (g, opts, deps) => {
16133
16772
  exitCode = ExitCode.UserError;
16134
16773
  } else {
16135
16774
  const listQuery = buildTicketListQueryFromReadListOpts(
16775
+ proj,
16136
16776
  { epic: epicFilter, ...listFlagRest },
16137
16777
  deps
16138
16778
  );
@@ -16153,6 +16793,7 @@ var readTicket = async (g, opts, deps) => {
16153
16793
  exitCode = ExitCode.UserError;
16154
16794
  } else {
16155
16795
  const listQuery = buildTicketListQueryFromReadListOpts(
16796
+ proj,
16156
16797
  listFlagRest,
16157
16798
  deps
16158
16799
  );
@@ -16169,6 +16810,7 @@ var readTicket = async (g, opts, deps) => {
16169
16810
  }
16170
16811
  } else if (listWithoutStory) {
16171
16812
  const listQuery = buildTicketListQueryFromReadListOpts(
16813
+ proj,
16172
16814
  { withoutStory: true, ...listFlagRest },
16173
16815
  deps
16174
16816
  );
@@ -16183,6 +16825,7 @@ var readTicket = async (g, opts, deps) => {
16183
16825
  );
16184
16826
  } else {
16185
16827
  const listQuery = buildTicketListQueryFromReadListOpts(
16828
+ proj,
16186
16829
  { epic: epicFilter, ...listFlagRest },
16187
16830
  deps
16188
16831
  );
@@ -16197,7 +16840,8 @@ var readTicket = async (g, opts, deps) => {
16197
16840
  );
16198
16841
  }
16199
16842
  } else {
16200
- const row = proj.tickets.get(id);
16843
+ const ticketId = resolveTicketId(proj, id) ?? id.trim();
16844
+ const row = proj.tickets.get(ticketId);
16201
16845
  if (!row || row.deleted) {
16202
16846
  deps.error("Ticket not found");
16203
16847
  exitCode = ExitCode.UserError;