hyper-pm 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
@@ -12679,6 +12835,7 @@ var listActiveTicketSummaries = (projection, options) => {
12679
12835
  ...t.estimate !== void 0 ? { estimate: t.estimate } : {},
12680
12836
  ...t.startWorkAt !== void 0 ? { startWorkAt: t.startWorkAt } : {},
12681
12837
  ...t.targetFinishAt !== void 0 ? { targetFinishAt: t.targetFinishAt } : {},
12838
+ ...t.dependsOn !== void 0 && t.dependsOn.length > 0 ? { dependsOn: t.dependsOn } : {},
12682
12839
  ...t.linkedBranches.length > 0 ? { linkedBranches: t.linkedBranches } : {},
12683
12840
  ...last !== void 0 ? {
12684
12841
  lastPrActivity: {
@@ -12775,6 +12932,17 @@ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12775
12932
  }
12776
12933
  }
12777
12934
  }
12935
+ if (Object.prototype.hasOwnProperty.call(meta, "depends_on")) {
12936
+ const v = meta["depends_on"];
12937
+ if (v === null) {
12938
+ out["dependsOn"] = null;
12939
+ } else {
12940
+ const parsed = parseTicketDependsOnFromFenceValue(v);
12941
+ if (parsed !== void 0) {
12942
+ out["dependsOn"] = parsed;
12943
+ }
12944
+ }
12945
+ }
12778
12946
  return out;
12779
12947
  };
12780
12948
  var buildGithubIssueBody = (params) => {
@@ -12800,6 +12968,9 @@ var buildGithubIssueBody = (params) => {
12800
12968
  if (p.targetFinishAt !== void 0) {
12801
12969
  meta.target_finish_at = p.targetFinishAt;
12802
12970
  }
12971
+ if (p.dependsOn !== void 0 && p.dependsOn.length > 0) {
12972
+ meta.depends_on = p.dependsOn;
12973
+ }
12803
12974
  }
12804
12975
  return `${params.description.trim()}
12805
12976
 
@@ -12825,6 +12996,9 @@ var ticketPlanningForGithubIssueBody = (ticket) => {
12825
12996
  if (ticket.targetFinishAt !== void 0) {
12826
12997
  out.targetFinishAt = ticket.targetFinishAt;
12827
12998
  }
12999
+ if (ticket.dependsOn !== void 0 && ticket.dependsOn.length > 0) {
13000
+ out.dependsOn = ticket.dependsOn;
13001
+ }
12828
13002
  return Object.keys(out).length > 0 ? out : void 0;
12829
13003
  };
12830
13004
 
@@ -13462,6 +13636,160 @@ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) =>
13462
13636
  }
13463
13637
  };
13464
13638
 
13639
+ // src/run/sync-remote-data-branch.ts
13640
+ var SyncRemoteDataBranchMergeError = class extends Error {
13641
+ /** @param message - Human-readable reason (stderr excerpt or generic text). */
13642
+ constructor(message) {
13643
+ super(message);
13644
+ this.name = "SyncRemoteDataBranchMergeError";
13645
+ }
13646
+ };
13647
+ var remoteTrackingRef = (remote, dataBranch) => `refs/remotes/${remote}/${dataBranch}`;
13648
+ var mergeRefSpecifier = (remote, dataBranch) => `${remote}/${dataBranch}`;
13649
+ var isLikelyNonFastForwardPushFailure = (detail) => {
13650
+ if (detail === void 0) return false;
13651
+ const d = detail.toLowerCase();
13652
+ return d.includes("non-fast-forward") || d.includes("failed to push");
13653
+ };
13654
+ var classifyMergeOutput = (combined) => {
13655
+ const c = combined.toLowerCase();
13656
+ if (c.includes("already up to date")) {
13657
+ return "up_to_date";
13658
+ }
13659
+ if (c.includes("fast-forward")) {
13660
+ return "fast_forward";
13661
+ }
13662
+ return "merge_commit";
13663
+ };
13664
+ var refExists = async (cwd, ref, runGit2) => {
13665
+ try {
13666
+ await runGit2(cwd, ["show-ref", "--verify", "--quiet", ref]);
13667
+ return true;
13668
+ } catch {
13669
+ return false;
13670
+ }
13671
+ };
13672
+ var fetchRemoteDataBranch = async (worktreePath, remote, dataBranch, runGit2) => {
13673
+ try {
13674
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
13675
+ } catch {
13676
+ return "skipped_no_remote";
13677
+ }
13678
+ try {
13679
+ await runGit2(worktreePath, ["fetch", remote, dataBranch]);
13680
+ return "ok";
13681
+ } catch (e) {
13682
+ const msg = e instanceof Error ? e.message : String(e);
13683
+ if (/couldn't find remote ref|could not find remote ref/i.test(msg)) {
13684
+ return "remote_branch_absent";
13685
+ }
13686
+ throw e;
13687
+ }
13688
+ };
13689
+ var mergeRemoteTrackingIntoHead = async (worktreePath, remote, dataBranch, runGit2, authorEnv = env) => {
13690
+ const spec = mergeRefSpecifier(remote, dataBranch);
13691
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13692
+ worktreePath,
13693
+ runGit2,
13694
+ authorEnv
13695
+ );
13696
+ try {
13697
+ const { stdout, stderr } = await runGit2(worktreePath, [
13698
+ "-c",
13699
+ `user.name=${name}`,
13700
+ "-c",
13701
+ `user.email=${email}`,
13702
+ "merge",
13703
+ "--no-edit",
13704
+ spec
13705
+ ]);
13706
+ return classifyMergeOutput(`${stdout}
13707
+ ${stderr}`);
13708
+ } catch (e) {
13709
+ await runGit2(worktreePath, ["merge", "--abort"]).catch(() => {
13710
+ });
13711
+ const msg = e instanceof Error ? e.message : String(e);
13712
+ throw new SyncRemoteDataBranchMergeError(
13713
+ 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}`
13714
+ );
13715
+ }
13716
+ };
13717
+ var runRemoteDataBranchGitSync = async (worktreePath, remote, dataBranch, runGit2, skipPush, deps = {}) => {
13718
+ const tryPushFn = deps.tryPush ?? tryPushDataBranchToRemote;
13719
+ const maxPushAttempts = deps.maxPushAttempts ?? 3;
13720
+ const authorEnv = deps.authorEnv ?? env;
13721
+ let dataBranchFetch = await fetchRemoteDataBranch(worktreePath, remote, dataBranch, runGit2);
13722
+ let dataBranchMerge = dataBranchFetch === "skipped_no_remote" ? "skipped_no_remote" : dataBranchFetch === "remote_branch_absent" ? "skipped_missing_remote_branch" : "up_to_date";
13723
+ if (dataBranchFetch === "ok") {
13724
+ const tracking = remoteTrackingRef(remote, dataBranch);
13725
+ const exists = await refExists(worktreePath, tracking, runGit2);
13726
+ if (!exists) {
13727
+ dataBranchMerge = "skipped_missing_remote_branch";
13728
+ } else {
13729
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
13730
+ worktreePath,
13731
+ remote,
13732
+ dataBranch,
13733
+ runGit2,
13734
+ authorEnv
13735
+ );
13736
+ }
13737
+ }
13738
+ if (skipPush) {
13739
+ return {
13740
+ dataBranchFetch,
13741
+ dataBranchMerge,
13742
+ dataBranchPush: "skipped_cli",
13743
+ dataBranchPushDetail: "skip-push",
13744
+ pushAttempts: 0
13745
+ };
13746
+ }
13747
+ let pushAttempts = 0;
13748
+ let lastPush = { status: "skipped_no_remote" };
13749
+ const refetchAndMerge = async () => {
13750
+ dataBranchFetch = await fetchRemoteDataBranch(
13751
+ worktreePath,
13752
+ remote,
13753
+ dataBranch,
13754
+ runGit2
13755
+ );
13756
+ if (dataBranchFetch !== "ok") {
13757
+ return;
13758
+ }
13759
+ const tracking = remoteTrackingRef(remote, dataBranch);
13760
+ const exists = await refExists(worktreePath, tracking, runGit2);
13761
+ if (!exists) {
13762
+ return;
13763
+ }
13764
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
13765
+ worktreePath,
13766
+ remote,
13767
+ dataBranch,
13768
+ runGit2,
13769
+ authorEnv
13770
+ );
13771
+ };
13772
+ for (let attempt = 1; attempt <= maxPushAttempts; attempt += 1) {
13773
+ pushAttempts = attempt;
13774
+ lastPush = await tryPushFn(worktreePath, remote, dataBranch, runGit2);
13775
+ if (lastPush.status === "pushed" || lastPush.status === "skipped_no_remote") {
13776
+ break;
13777
+ }
13778
+ if (lastPush.status === "failed" && isLikelyNonFastForwardPushFailure(lastPush.detail) && attempt < maxPushAttempts) {
13779
+ await refetchAndMerge();
13780
+ continue;
13781
+ }
13782
+ break;
13783
+ }
13784
+ return {
13785
+ dataBranchFetch,
13786
+ dataBranchMerge,
13787
+ dataBranchPush: lastPush.status,
13788
+ ...lastPush.detail !== void 0 ? { dataBranchPushDetail: lastPush.detail } : {},
13789
+ pushAttempts
13790
+ };
13791
+ };
13792
+
13465
13793
  // src/storage/append-event.ts
13466
13794
  var import_promises5 = require("node:fs/promises");
13467
13795
  var import_node_path5 = require("node:path");
@@ -13657,6 +13985,12 @@ var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13657
13985
  if (typeof tf === "string") {
13658
13986
  row.targetFinishAt = tf;
13659
13987
  }
13988
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
13989
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
13990
+ if (v !== void 0 && v.length > 0) {
13991
+ row.dependsOn = v;
13992
+ }
13993
+ }
13660
13994
  };
13661
13995
  var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13662
13996
  if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
@@ -13703,6 +14037,20 @@ var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13703
14037
  } else if (typeof tf === "string") {
13704
14038
  row.targetFinishAt = tf;
13705
14039
  }
14040
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
14041
+ if (payload["dependsOn"] === null) {
14042
+ delete row.dependsOn;
14043
+ } else {
14044
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
14045
+ if (v !== void 0) {
14046
+ if (v.length === 0) {
14047
+ delete row.dependsOn;
14048
+ } else {
14049
+ row.dependsOn = v;
14050
+ }
14051
+ }
14052
+ }
14053
+ }
13706
14054
  };
13707
14055
  var applyCreatedAudit = (row, evt) => {
13708
14056
  row.createdAt = evt.ts;
@@ -14329,6 +14677,19 @@ var runGithubInboundSync = async (params) => {
14329
14677
  const planningSource = inboundTicketPlanningPayloadFromFenceMeta(meta);
14330
14678
  const planningPayload = {};
14331
14679
  for (const [k, v] of Object.entries(planningSource)) {
14680
+ if (k === "dependsOn") {
14681
+ if (v === null) {
14682
+ if (!ticketDependsOnListsEqual(ticket.dependsOn, [])) {
14683
+ planningPayload["dependsOn"] = null;
14684
+ }
14685
+ } else if (Array.isArray(v)) {
14686
+ const parsed = parseTicketDependsOnFromPayloadValue(v);
14687
+ if (parsed !== void 0 && !ticketDependsOnListsEqual(ticket.dependsOn, parsed)) {
14688
+ planningPayload["dependsOn"] = parsed;
14689
+ }
14690
+ }
14691
+ continue;
14692
+ }
14332
14693
  const tk = k;
14333
14694
  const cur = ticket[tk];
14334
14695
  if (v === null) {
@@ -14666,6 +15027,9 @@ var buildTicketListQueryFromReadListOpts = (o, deps) => {
14666
15027
  if (targetFinishBeforeMs !== void 0) {
14667
15028
  query.targetFinishBeforeMs = targetFinishBeforeMs;
14668
15029
  }
15030
+ if (o.dependsOn !== void 0 && o.dependsOn.trim() !== "") {
15031
+ query.dependsOnIncludesId = o.dependsOn.trim();
15032
+ }
14669
15033
  return Object.keys(query).length > 0 ? query : void 0;
14670
15034
  };
14671
15035
  var runCli = async (argv, deps = {
@@ -14889,6 +15253,11 @@ var runCli = async (argv, deps = {
14889
15253
  "planning label (repeatable)",
14890
15254
  (value, previous) => [...previous, value],
14891
15255
  []
15256
+ ).option(
15257
+ "--depends-on <id>",
15258
+ "prerequisite ticket id (repeatable)",
15259
+ (value, previous) => [...previous, value],
15260
+ []
14892
15261
  ).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
15262
  const g = readGlobals(this);
14894
15263
  const o = this.opts();
@@ -14951,6 +15320,10 @@ var runCli = async (argv, deps = {
14951
15320
  "--target-finish-at",
14952
15321
  deps
14953
15322
  );
15323
+ const dependsOnTokensCreate = normalizeCliStringList(o.dependsOn);
15324
+ const dependsOnNormCreate = normalizeTicketDependsOnIds(
15325
+ dependsOnTokensCreate
15326
+ );
14954
15327
  const planningPayload = {
14955
15328
  ...labelsPayloadCreate,
14956
15329
  ...priorityParsed !== void 0 ? { priority: priorityParsed } : {},
@@ -14977,6 +15350,15 @@ var runCli = async (argv, deps = {
14977
15350
  const branchTokens = normalizeCliStringList(o.branch);
14978
15351
  const branchesNorm = normalizeTicketBranchListFromStrings(branchTokens);
14979
15352
  const branchesPayload = branchesNorm.length > 0 ? { branches: branchesNorm } : {};
15353
+ const dependsOnErr = validateTicketDependsOnForWrite({
15354
+ projection: proj,
15355
+ fromTicketId: id,
15356
+ nextDependsOn: dependsOnNormCreate
15357
+ });
15358
+ if (dependsOnErr !== void 0) {
15359
+ throw new Error(dependsOnErr);
15360
+ }
15361
+ const dependsOnPayloadCreate = dependsOnNormCreate.length > 0 ? { dependsOn: dependsOnNormCreate } : {};
14980
15362
  const evt = makeEvent(
14981
15363
  "TicketCreated",
14982
15364
  {
@@ -14987,6 +15369,7 @@ var runCli = async (argv, deps = {
14987
15369
  ...status !== void 0 ? { status } : {},
14988
15370
  ...assigneeCreate,
14989
15371
  ...branchesPayload,
15372
+ ...dependsOnPayloadCreate,
14990
15373
  ...planningPayload
14991
15374
  },
14992
15375
  deps.clock,
@@ -15044,6 +15427,9 @@ var runCli = async (argv, deps = {
15044
15427
  ).option(
15045
15428
  "--branch <name>",
15046
15429
  "when listing (no --id): only tickets linked to this branch (normalized exact match)"
15430
+ ).option(
15431
+ "--depends-on <id>",
15432
+ "when listing (no --id): only tickets that list this ticket id in dependsOn"
15047
15433
  ).option(
15048
15434
  "--priority <p>",
15049
15435
  "when listing (no --id): OR-set of priorities (repeat flag); low|medium|high|urgent",
@@ -15123,7 +15509,17 @@ var runCli = async (argv, deps = {
15123
15509
  "--clear-labels",
15124
15510
  "remove all planning labels from the ticket",
15125
15511
  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() {
15512
+ ).option(
15513
+ "--add-depends-on <id>",
15514
+ "add a prerequisite ticket id (repeatable)",
15515
+ (value, previous) => [...previous, value],
15516
+ []
15517
+ ).option(
15518
+ "--remove-depends-on <id>",
15519
+ "remove a prerequisite ticket id (repeatable)",
15520
+ (value, previous) => [...previous, value],
15521
+ []
15522
+ ).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
15523
  const g = readGlobals(this);
15128
15524
  const o = this.opts();
15129
15525
  let body = o.body;
@@ -15175,6 +15571,14 @@ ${body}`
15175
15571
  );
15176
15572
  deps.exit(ExitCode.UserError);
15177
15573
  }
15574
+ const addDependsOnTokens = normalizeCliStringList(o.addDependsOn);
15575
+ const removeDependsOnTokens = normalizeCliStringList(o.removeDependsOn);
15576
+ if (o.clearDependsOn === true && (addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0)) {
15577
+ deps.error(
15578
+ "Cannot use --clear-depends-on with --add-depends-on or --remove-depends-on"
15579
+ );
15580
+ deps.exit(ExitCode.UserError);
15581
+ }
15178
15582
  const mutual = (clear, set, clearName, setName) => {
15179
15583
  if (clear === true && set !== void 0 && set !== "") {
15180
15584
  deps.error(`Cannot use ${clearName} and ${setName} together`);
@@ -15307,6 +15711,35 @@ ${body}`
15307
15711
  payload["labels"] = nextLabels;
15308
15712
  }
15309
15713
  }
15714
+ const wantsDependsOnChange = o.clearDependsOn === true || addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0;
15715
+ if (wantsDependsOnChange) {
15716
+ let nextDepends;
15717
+ if (o.clearDependsOn === true) {
15718
+ nextDepends = [];
15719
+ } else {
15720
+ const removeDepSet = new Set(
15721
+ normalizeTicketDependsOnIds(removeDependsOnTokens)
15722
+ );
15723
+ nextDepends = normalizeTicketDependsOnIds(
15724
+ (curTicket.dependsOn ?? []).filter((d) => !removeDepSet.has(d))
15725
+ );
15726
+ nextDepends = normalizeTicketDependsOnIds([
15727
+ ...nextDepends,
15728
+ ...addDependsOnTokens
15729
+ ]);
15730
+ }
15731
+ const depErr = validateTicketDependsOnForWrite({
15732
+ projection: proj,
15733
+ fromTicketId: o.id,
15734
+ nextDependsOn: nextDepends
15735
+ });
15736
+ if (depErr !== void 0) {
15737
+ throw new Error(depErr);
15738
+ }
15739
+ if (!ticketDependsOnListsEqual(curTicket.dependsOn, nextDepends)) {
15740
+ payload["dependsOn"] = nextDepends;
15741
+ }
15742
+ }
15310
15743
  if (priorityUpdate !== void 0) {
15311
15744
  payload["priority"] = priorityUpdate;
15312
15745
  }
@@ -15620,7 +16053,19 @@ ${body}`
15620
16053
  }
15621
16054
  deps.exit(ExitCode.Success);
15622
16055
  });
15623
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).option(
16056
+ program2.command("sync").description("Sync data branch over git; optional GitHub Issues sync").option(
16057
+ "--skip-network",
16058
+ "skip all sync network operations (git fetch/merge/push and GitHub); legacy: --no-github",
16059
+ false
16060
+ ).option(
16061
+ "--with-github",
16062
+ "also run GitHub Issues sync (requires GITHUB_TOKEN or gh; needs sync not off in config)",
16063
+ false
16064
+ ).option(
16065
+ "--git-data",
16066
+ "legacy no-op: default sync already updates the data branch via git only",
16067
+ false
16068
+ ).option(
15624
16069
  "--skip-push",
15625
16070
  "after a successful sync, do not push the data branch to the remote",
15626
16071
  false
@@ -15629,115 +16074,171 @@ ${body}`
15629
16074
  const o = this.opts();
15630
16075
  const repoRoot = await resolveRepoRoot(g.repo);
15631
16076
  const cfg = await loadMergedConfig(repoRoot, g);
15632
- if (cfg.sync === "off" || o.noGithub) {
16077
+ if (o.withGithub && o.skipNetwork) {
16078
+ deps.error("Cannot use --with-github together with --skip-network");
16079
+ deps.exit(ExitCode.UserError);
16080
+ }
16081
+ if (o.skipNetwork) {
15633
16082
  deps.log(formatOutput(g.format, { ok: true, skipped: true }));
15634
16083
  deps.exit(ExitCode.Success);
15635
16084
  }
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({
16085
+ if (o.withGithub) {
16086
+ if (cfg.sync === "off") {
16087
+ deps.error(
16088
+ "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."
16089
+ );
16090
+ deps.exit(ExitCode.UserError);
16091
+ }
16092
+ const githubToken = await resolveGithubTokenForSync({
16093
+ envToken: env.GITHUB_TOKEN,
16094
+ cwd: repoRoot
16095
+ });
16096
+ if (!githubToken) {
16097
+ deps.error(
16098
+ "GitHub auth required for sync --with-github: set GITHUB_TOKEN or run `gh auth login`"
16099
+ );
16100
+ deps.exit(ExitCode.EnvironmentAuth);
16101
+ }
16102
+ const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16103
+ const session = await openDataBranchWorktree({
15659
16104
  repoRoot,
15660
- remote: cfg.remote,
16105
+ dataBranch: cfg.dataBranch,
16106
+ tmpBase,
16107
+ keepWorktree: g.keepWorktree,
15661
16108
  runGit
15662
16109
  });
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,
16110
+ try {
16111
+ const projection = await loadProjectionFromDataRoot(
16112
+ session.worktreePath
16113
+ );
16114
+ const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
16115
+ repoRoot,
16116
+ remote: cfg.remote,
16117
+ runGit
16118
+ });
16119
+ const { owner, repo: repo2 } = resolveGithubRepo(
16120
+ cfg,
16121
+ env.GITHUB_REPO,
16122
+ gitDerivedSlug
16123
+ );
16124
+ const octokit = new Octokit2({ auth: githubToken });
16125
+ const outboundActor = await resolveGithubTokenActor(octokit);
16126
+ const depsGh = {
15698
16127
  octokit,
15699
16128
  owner,
15700
16129
  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(
16130
+ clock: deps.clock,
16131
+ outboundActor
16132
+ };
16133
+ await runGithubOutboundSync({
16134
+ dataRoot: session.worktreePath,
16135
+ projection,
16136
+ config: cfg,
16137
+ deps: depsGh
16138
+ });
16139
+ await runGithubInboundSync({
16140
+ dataRoot: session.worktreePath,
16141
+ projection,
16142
+ config: cfg,
16143
+ deps: depsGh
16144
+ });
16145
+ const projectionAfterInbound = await loadProjectionFromDataRoot(
16146
+ session.worktreePath
16147
+ );
16148
+ await runGithubPrActivitySync({
16149
+ projection: projectionAfterInbound,
16150
+ config: cfg,
16151
+ deps: defaultGithubPrActivitySyncDeps({
16152
+ dataRoot: session.worktreePath,
16153
+ clock: deps.clock,
16154
+ octokit,
16155
+ owner,
16156
+ repo: repo2,
16157
+ actor: outboundActor
16158
+ })
16159
+ });
16160
+ await commitDataWorktreeIfNeeded(
15716
16161
  session.worktreePath,
15717
- cfg.remote,
15718
- cfg.dataBranch,
16162
+ formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15719
16163
  runGit
15720
16164
  );
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}`
16165
+ let dataBranchPush;
16166
+ let dataBranchPushDetail;
16167
+ if (o.skipPush) {
16168
+ dataBranchPush = "skipped_cli";
16169
+ dataBranchPushDetail = "skip-push";
16170
+ } else {
16171
+ const pushResult = await tryPushDataBranchToRemote(
16172
+ session.worktreePath,
16173
+ cfg.remote,
16174
+ cfg.dataBranch,
16175
+ runGit
15726
16176
  );
16177
+ dataBranchPush = pushResult.status;
16178
+ dataBranchPushDetail = pushResult.detail;
16179
+ if (pushResult.status === "failed" && pushResult.detail) {
16180
+ deps.error(
16181
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
16182
+ );
16183
+ }
15727
16184
  }
16185
+ deps.log(
16186
+ formatOutput(g.format, {
16187
+ ok: true,
16188
+ githubSync: true,
16189
+ dataBranchPush,
16190
+ ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16191
+ })
16192
+ );
16193
+ } catch (e) {
16194
+ deps.error(e instanceof Error ? e.message : String(e));
16195
+ deps.exit(ExitCode.ExternalApi);
16196
+ } finally {
16197
+ await session.dispose();
16198
+ }
16199
+ deps.exit(ExitCode.Success);
16200
+ }
16201
+ const tmpBaseGit = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16202
+ const sessionGit = await openDataBranchWorktree({
16203
+ repoRoot,
16204
+ dataBranch: cfg.dataBranch,
16205
+ tmpBase: tmpBaseGit,
16206
+ keepWorktree: g.keepWorktree,
16207
+ runGit
16208
+ });
16209
+ try {
16210
+ let syncResult;
16211
+ try {
16212
+ syncResult = await runRemoteDataBranchGitSync(
16213
+ sessionGit.worktreePath,
16214
+ cfg.remote,
16215
+ cfg.dataBranch,
16216
+ runGit,
16217
+ Boolean(o.skipPush)
16218
+ );
16219
+ } catch (e) {
16220
+ if (e instanceof SyncRemoteDataBranchMergeError) {
16221
+ deps.error(e.message);
16222
+ deps.exit(ExitCode.UserError);
16223
+ }
16224
+ deps.error(e instanceof Error ? e.message : String(e));
16225
+ deps.exit(ExitCode.UserError);
16226
+ }
16227
+ if (syncResult.dataBranchPush === "failed" && syncResult.dataBranchPushDetail !== void 0) {
16228
+ deps.error(
16229
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${syncResult.dataBranchPushDetail}`
16230
+ );
15728
16231
  }
15729
16232
  deps.log(
15730
16233
  formatOutput(g.format, {
15731
16234
  ok: true,
15732
- dataBranchPush,
15733
- ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16235
+ gitDataOnly: true,
16236
+ ...o.gitData ? { legacyGitDataFlag: true } : {},
16237
+ ...syncResult
15734
16238
  })
15735
16239
  );
15736
- } catch (e) {
15737
- deps.error(e instanceof Error ? e.message : String(e));
15738
- deps.exit(ExitCode.ExternalApi);
15739
16240
  } finally {
15740
- await session.dispose();
16241
+ await sessionGit.dispose();
15741
16242
  }
15742
16243
  deps.exit(ExitCode.Success);
15743
16244
  });
@@ -15903,7 +16404,7 @@ ${body}`
15903
16404
  await session.dispose();
15904
16405
  }
15905
16406
  });
15906
- await program2.parseAsync(argv, { from: "node" });
16407
+ await program2.parseAsync(normalizeRawCliArgv(argv), { from: "node" });
15907
16408
  };
15908
16409
  var makeEvent = (type, payload, clock, actor) => ({
15909
16410
  schema: 1,