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/main.cjs CHANGED
@@ -11530,6 +11530,103 @@ var normalizeTicketBranchListFromPayloadValue = (value) => {
11530
11530
  return normalizeTicketBranchListFromStrings(strings);
11531
11531
  };
11532
11532
 
11533
+ // src/lib/ticket-depends-on.ts
11534
+ var normalizeTicketDependsOnIds = (ids) => {
11535
+ const out = [];
11536
+ const seen = /* @__PURE__ */ new Set();
11537
+ for (const raw of ids) {
11538
+ const t = raw.trim();
11539
+ if (t === "") continue;
11540
+ if (seen.has(t)) continue;
11541
+ seen.add(t);
11542
+ out.push(t);
11543
+ }
11544
+ return out;
11545
+ };
11546
+ var ticketDependsOnListsEqual = (a, b) => {
11547
+ const an = normalizeTicketDependsOnIds(a ?? []);
11548
+ const bn = normalizeTicketDependsOnIds(b ?? []);
11549
+ if (an.length !== bn.length) return false;
11550
+ return an.every((x, i) => x === bn[i]);
11551
+ };
11552
+ var parseTicketDependsOnFromPayloadValue = (value) => {
11553
+ if (!Array.isArray(value)) return void 0;
11554
+ const strings = [];
11555
+ for (const x of value) {
11556
+ if (typeof x !== "string") return void 0;
11557
+ strings.push(x);
11558
+ }
11559
+ return normalizeTicketDependsOnIds(strings);
11560
+ };
11561
+ var parseTicketDependsOnFromFenceValue = (value) => {
11562
+ if (!Array.isArray(value)) return void 0;
11563
+ const strings = [];
11564
+ for (const x of value) {
11565
+ if (typeof x === "string") strings.push(x);
11566
+ }
11567
+ return normalizeTicketDependsOnIds(strings);
11568
+ };
11569
+ var wouldTicketDependsOnCreateCycle = (params) => {
11570
+ const { fromTicketId, nextDependsOn, successorsFor } = params;
11571
+ const dfsFromPrerequisite = (start) => {
11572
+ const stack = [start];
11573
+ const visited = /* @__PURE__ */ new Set();
11574
+ while (stack.length > 0) {
11575
+ const node = stack.pop();
11576
+ if (node === fromTicketId) return true;
11577
+ if (visited.has(node)) continue;
11578
+ visited.add(node);
11579
+ const next = successorsFor(node) ?? [];
11580
+ for (let i = next.length - 1; i >= 0; i -= 1) {
11581
+ stack.push(next[i]);
11582
+ }
11583
+ }
11584
+ return false;
11585
+ };
11586
+ for (const p of nextDependsOn) {
11587
+ if (dfsFromPrerequisite(p)) return true;
11588
+ }
11589
+ return false;
11590
+ };
11591
+ var ticketDependsOnSuccessorsForProjection = (projection, fromTicketId, nextDependsOn) => {
11592
+ return (ticketId) => {
11593
+ if (ticketId === fromTicketId) {
11594
+ return nextDependsOn.length > 0 ? nextDependsOn : void 0;
11595
+ }
11596
+ const row = projection.tickets.get(ticketId);
11597
+ if (!row || row.deleted) return void 0;
11598
+ return row.dependsOn;
11599
+ };
11600
+ };
11601
+ var validateTicketDependsOnForWrite = (params) => {
11602
+ const { projection, fromTicketId, nextDependsOn } = params;
11603
+ for (const id of nextDependsOn) {
11604
+ if (id === fromTicketId) {
11605
+ return `Ticket cannot depend on itself (${id})`;
11606
+ }
11607
+ const row = projection.tickets.get(id);
11608
+ if (row === void 0) {
11609
+ return `Dependency ticket not found: ${id}`;
11610
+ }
11611
+ if (row.deleted) {
11612
+ return `Dependency ticket deleted: ${id}`;
11613
+ }
11614
+ }
11615
+ const successorsFor = ticketDependsOnSuccessorsForProjection(
11616
+ projection,
11617
+ fromTicketId,
11618
+ nextDependsOn
11619
+ );
11620
+ if (wouldTicketDependsOnCreateCycle({
11621
+ fromTicketId,
11622
+ nextDependsOn,
11623
+ successorsFor
11624
+ })) {
11625
+ return "Ticket dependencies would create a cycle";
11626
+ }
11627
+ return void 0;
11628
+ };
11629
+
11533
11630
  // src/lib/ticket-planning-fields.ts
11534
11631
  var PRIORITY_SET = /* @__PURE__ */ new Set([
11535
11632
  "low",
@@ -11698,6 +11795,9 @@ var formatOutput = (format, value) => {
11698
11795
  return JSON.stringify(value);
11699
11796
  };
11700
11797
 
11798
+ // src/cli/normalize-raw-cli-argv.ts
11799
+ var normalizeRawCliArgv = (argv) => argv.map((token) => token === "--no-github" ? "--skip-network" : token);
11800
+
11701
11801
  // src/cli/prune-unchanged-work-item-update-payload.ts
11702
11802
  var branchListsEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
11703
11803
  var pruneEpicOrStoryUpdatePayloadAgainstRow = (cur, draft) => {
@@ -11753,6 +11853,20 @@ var pruneTicketUpdatePayloadAgainstRow = (cur, draft) => {
11753
11853
  out["labels"] = draft["labels"];
11754
11854
  }
11755
11855
  }
11856
+ if (draft["dependsOn"] !== void 0) {
11857
+ if (draft["dependsOn"] === null) {
11858
+ if (!ticketDependsOnListsEqual(cur.dependsOn, [])) {
11859
+ out["dependsOn"] = null;
11860
+ }
11861
+ } else {
11862
+ const parsed = parseTicketDependsOnFromPayloadValue(draft["dependsOn"]);
11863
+ if (parsed !== void 0) {
11864
+ if (!ticketDependsOnListsEqual(cur.dependsOn, parsed)) {
11865
+ out["dependsOn"] = parsed;
11866
+ }
11867
+ }
11868
+ }
11869
+ }
11756
11870
  if (draft["priority"] !== void 0) {
11757
11871
  const curP = cur.priority ?? null;
11758
11872
  const nextP = draft["priority"] === null ? null : draft["priority"];
@@ -11919,6 +12033,20 @@ var workItemUpdateAspects = (kind, payload) => {
11919
12033
  }
11920
12034
  }
11921
12035
  }
12036
+ if (kind === "ticket" && payload["dependsOn"] !== void 0) {
12037
+ if (payload["dependsOn"] === null) {
12038
+ aspects.push("cleared ticket dependencies");
12039
+ } else {
12040
+ const d = normalizeBranchList(payload["dependsOn"]);
12041
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12042
+ aspects.push(`set ticket dependencies to (${d.join(", ")})`);
12043
+ } else if (d.length > MAX_BRANCH_NAMES_TO_LIST) {
12044
+ aspects.push("updated ticket dependencies");
12045
+ } else {
12046
+ aspects.push("cleared ticket dependencies");
12047
+ }
12048
+ }
12049
+ }
11922
12050
  if (kind === "ticket" && payload["priority"] !== void 0) {
11923
12051
  if (payload["priority"] === null) {
11924
12052
  aspects.push("cleared priority");
@@ -12025,6 +12153,7 @@ var buildAuditLinkMetadata = (evt, githubRepo) => {
12025
12153
  if (p["status"] !== void 0) meta["status"] = String(p["status"]);
12026
12154
  if (p["assignee"] !== void 0) meta["assignee"] = p["assignee"];
12027
12155
  if (p["labels"] !== void 0) meta["labels"] = p["labels"];
12156
+ if (p["dependsOn"] !== void 0) meta["dependsOn"] = p["dependsOn"];
12028
12157
  if (p["priority"] !== void 0) meta["priority"] = p["priority"];
12029
12158
  if (p["size"] !== void 0) meta["size"] = p["size"];
12030
12159
  if (p["estimate"] !== void 0) meta["estimate"] = p["estimate"];
@@ -12092,6 +12221,14 @@ var formatEpicStoryTicketCreated = (evt, entity) => {
12092
12221
  parts.push("with labels");
12093
12222
  }
12094
12223
  }
12224
+ if (p["dependsOn"] !== void 0) {
12225
+ const d = normalizeBranchList(p["dependsOn"]);
12226
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12227
+ parts.push(`depending on (${d.join(", ")})`);
12228
+ } else if (d.length > MAX_BRANCH_NAMES_TO_LIST) {
12229
+ parts.push("with ticket dependencies");
12230
+ }
12231
+ }
12095
12232
  if (p["priority"] !== void 0) {
12096
12233
  parts.push(`with priority ${quoteStatus(String(p["priority"]))}`);
12097
12234
  }
@@ -12185,6 +12322,18 @@ var formatAuditHumanSentence = (evt) => {
12185
12322
  bits.push("updated labels");
12186
12323
  }
12187
12324
  }
12325
+ if (p["dependsOn"] !== void 0) {
12326
+ if (p["dependsOn"] === null) {
12327
+ bits.push("cleared ticket dependencies");
12328
+ } else {
12329
+ const d = normalizeBranchList(p["dependsOn"]);
12330
+ if (d.length > 0 && d.length <= MAX_BRANCH_NAMES_TO_LIST) {
12331
+ bits.push(`dependencies: ${d.join(", ")}`);
12332
+ } else {
12333
+ bits.push("updated ticket dependencies");
12334
+ }
12335
+ }
12336
+ }
12188
12337
  if (p["priority"] !== void 0) {
12189
12338
  bits.push("updated priority");
12190
12339
  }
@@ -12464,6 +12613,13 @@ var ticketMatchesTicketListQuery = (ticket, projection, query) => {
12464
12613
  return false;
12465
12614
  }
12466
12615
  }
12616
+ const dependsOnIncludesId = query.dependsOnIncludesId;
12617
+ if (dependsOnIncludesId !== void 0) {
12618
+ const deps = ticket.dependsOn ?? [];
12619
+ if (!deps.includes(dependsOnIncludesId)) {
12620
+ return false;
12621
+ }
12622
+ }
12467
12623
  return true;
12468
12624
  };
12469
12625
 
@@ -12669,6 +12825,7 @@ var listActiveTicketSummaries = (projection, options) => {
12669
12825
  ...t.estimate !== void 0 ? { estimate: t.estimate } : {},
12670
12826
  ...t.startWorkAt !== void 0 ? { startWorkAt: t.startWorkAt } : {},
12671
12827
  ...t.targetFinishAt !== void 0 ? { targetFinishAt: t.targetFinishAt } : {},
12828
+ ...t.dependsOn !== void 0 && t.dependsOn.length > 0 ? { dependsOn: t.dependsOn } : {},
12672
12829
  ...t.linkedBranches.length > 0 ? { linkedBranches: t.linkedBranches } : {},
12673
12830
  ...last !== void 0 ? {
12674
12831
  lastPrActivity: {
@@ -12765,6 +12922,17 @@ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12765
12922
  }
12766
12923
  }
12767
12924
  }
12925
+ if (Object.prototype.hasOwnProperty.call(meta, "depends_on")) {
12926
+ const v = meta["depends_on"];
12927
+ if (v === null) {
12928
+ out["dependsOn"] = null;
12929
+ } else {
12930
+ const parsed = parseTicketDependsOnFromFenceValue(v);
12931
+ if (parsed !== void 0) {
12932
+ out["dependsOn"] = parsed;
12933
+ }
12934
+ }
12935
+ }
12768
12936
  return out;
12769
12937
  };
12770
12938
  var buildGithubIssueBody = (params) => {
@@ -12790,6 +12958,9 @@ var buildGithubIssueBody = (params) => {
12790
12958
  if (p.targetFinishAt !== void 0) {
12791
12959
  meta.target_finish_at = p.targetFinishAt;
12792
12960
  }
12961
+ if (p.dependsOn !== void 0 && p.dependsOn.length > 0) {
12962
+ meta.depends_on = p.dependsOn;
12963
+ }
12793
12964
  }
12794
12965
  return `${params.description.trim()}
12795
12966
 
@@ -12815,6 +12986,9 @@ var ticketPlanningForGithubIssueBody = (ticket) => {
12815
12986
  if (ticket.targetFinishAt !== void 0) {
12816
12987
  out.targetFinishAt = ticket.targetFinishAt;
12817
12988
  }
12989
+ if (ticket.dependsOn !== void 0 && ticket.dependsOn.length > 0) {
12990
+ out.dependsOn = ticket.dependsOn;
12991
+ }
12818
12992
  return Object.keys(out).length > 0 ? out : void 0;
12819
12993
  };
12820
12994
 
@@ -13452,6 +13626,160 @@ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) =>
13452
13626
  }
13453
13627
  };
13454
13628
 
13629
+ // src/run/sync-remote-data-branch.ts
13630
+ var SyncRemoteDataBranchMergeError = class extends Error {
13631
+ /** @param message - Human-readable reason (stderr excerpt or generic text). */
13632
+ constructor(message) {
13633
+ super(message);
13634
+ this.name = "SyncRemoteDataBranchMergeError";
13635
+ }
13636
+ };
13637
+ var remoteTrackingRef = (remote, dataBranch) => `refs/remotes/${remote}/${dataBranch}`;
13638
+ var mergeRefSpecifier = (remote, dataBranch) => `${remote}/${dataBranch}`;
13639
+ var isLikelyNonFastForwardPushFailure = (detail) => {
13640
+ if (detail === void 0) return false;
13641
+ const d = detail.toLowerCase();
13642
+ return d.includes("non-fast-forward") || d.includes("failed to push");
13643
+ };
13644
+ var classifyMergeOutput = (combined) => {
13645
+ const c = combined.toLowerCase();
13646
+ if (c.includes("already up to date")) {
13647
+ return "up_to_date";
13648
+ }
13649
+ if (c.includes("fast-forward")) {
13650
+ return "fast_forward";
13651
+ }
13652
+ return "merge_commit";
13653
+ };
13654
+ var refExists = async (cwd, ref, runGit2) => {
13655
+ try {
13656
+ await runGit2(cwd, ["show-ref", "--verify", "--quiet", ref]);
13657
+ return true;
13658
+ } catch {
13659
+ return false;
13660
+ }
13661
+ };
13662
+ var fetchRemoteDataBranch = async (worktreePath, remote, dataBranch, runGit2) => {
13663
+ try {
13664
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
13665
+ } catch {
13666
+ return "skipped_no_remote";
13667
+ }
13668
+ try {
13669
+ await runGit2(worktreePath, ["fetch", remote, dataBranch]);
13670
+ return "ok";
13671
+ } catch (e) {
13672
+ const msg = e instanceof Error ? e.message : String(e);
13673
+ if (/couldn't find remote ref|could not find remote ref/i.test(msg)) {
13674
+ return "remote_branch_absent";
13675
+ }
13676
+ throw e;
13677
+ }
13678
+ };
13679
+ var mergeRemoteTrackingIntoHead = async (worktreePath, remote, dataBranch, runGit2, authorEnv = env) => {
13680
+ const spec = mergeRefSpecifier(remote, dataBranch);
13681
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13682
+ worktreePath,
13683
+ runGit2,
13684
+ authorEnv
13685
+ );
13686
+ try {
13687
+ const { stdout, stderr } = await runGit2(worktreePath, [
13688
+ "-c",
13689
+ `user.name=${name}`,
13690
+ "-c",
13691
+ `user.email=${email}`,
13692
+ "merge",
13693
+ "--no-edit",
13694
+ spec
13695
+ ]);
13696
+ return classifyMergeOutput(`${stdout}
13697
+ ${stderr}`);
13698
+ } catch (e) {
13699
+ await runGit2(worktreePath, ["merge", "--abort"]).catch(() => {
13700
+ });
13701
+ const msg = e instanceof Error ? e.message : String(e);
13702
+ throw new SyncRemoteDataBranchMergeError(
13703
+ 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}`
13704
+ );
13705
+ }
13706
+ };
13707
+ var runRemoteDataBranchGitSync = async (worktreePath, remote, dataBranch, runGit2, skipPush, deps = {}) => {
13708
+ const tryPushFn = deps.tryPush ?? tryPushDataBranchToRemote;
13709
+ const maxPushAttempts = deps.maxPushAttempts ?? 3;
13710
+ const authorEnv = deps.authorEnv ?? env;
13711
+ let dataBranchFetch = await fetchRemoteDataBranch(worktreePath, remote, dataBranch, runGit2);
13712
+ let dataBranchMerge = dataBranchFetch === "skipped_no_remote" ? "skipped_no_remote" : dataBranchFetch === "remote_branch_absent" ? "skipped_missing_remote_branch" : "up_to_date";
13713
+ if (dataBranchFetch === "ok") {
13714
+ const tracking = remoteTrackingRef(remote, dataBranch);
13715
+ const exists = await refExists(worktreePath, tracking, runGit2);
13716
+ if (!exists) {
13717
+ dataBranchMerge = "skipped_missing_remote_branch";
13718
+ } else {
13719
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
13720
+ worktreePath,
13721
+ remote,
13722
+ dataBranch,
13723
+ runGit2,
13724
+ authorEnv
13725
+ );
13726
+ }
13727
+ }
13728
+ if (skipPush) {
13729
+ return {
13730
+ dataBranchFetch,
13731
+ dataBranchMerge,
13732
+ dataBranchPush: "skipped_cli",
13733
+ dataBranchPushDetail: "skip-push",
13734
+ pushAttempts: 0
13735
+ };
13736
+ }
13737
+ let pushAttempts = 0;
13738
+ let lastPush = { status: "skipped_no_remote" };
13739
+ const refetchAndMerge = async () => {
13740
+ dataBranchFetch = await fetchRemoteDataBranch(
13741
+ worktreePath,
13742
+ remote,
13743
+ dataBranch,
13744
+ runGit2
13745
+ );
13746
+ if (dataBranchFetch !== "ok") {
13747
+ return;
13748
+ }
13749
+ const tracking = remoteTrackingRef(remote, dataBranch);
13750
+ const exists = await refExists(worktreePath, tracking, runGit2);
13751
+ if (!exists) {
13752
+ return;
13753
+ }
13754
+ dataBranchMerge = await mergeRemoteTrackingIntoHead(
13755
+ worktreePath,
13756
+ remote,
13757
+ dataBranch,
13758
+ runGit2,
13759
+ authorEnv
13760
+ );
13761
+ };
13762
+ for (let attempt = 1; attempt <= maxPushAttempts; attempt += 1) {
13763
+ pushAttempts = attempt;
13764
+ lastPush = await tryPushFn(worktreePath, remote, dataBranch, runGit2);
13765
+ if (lastPush.status === "pushed" || lastPush.status === "skipped_no_remote") {
13766
+ break;
13767
+ }
13768
+ if (lastPush.status === "failed" && isLikelyNonFastForwardPushFailure(lastPush.detail) && attempt < maxPushAttempts) {
13769
+ await refetchAndMerge();
13770
+ continue;
13771
+ }
13772
+ break;
13773
+ }
13774
+ return {
13775
+ dataBranchFetch,
13776
+ dataBranchMerge,
13777
+ dataBranchPush: lastPush.status,
13778
+ ...lastPush.detail !== void 0 ? { dataBranchPushDetail: lastPush.detail } : {},
13779
+ pushAttempts
13780
+ };
13781
+ };
13782
+
13455
13783
  // src/storage/append-event.ts
13456
13784
  var import_promises5 = require("node:fs/promises");
13457
13785
  var import_node_path5 = require("node:path");
@@ -13647,6 +13975,12 @@ var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13647
13975
  if (typeof tf === "string") {
13648
13976
  row.targetFinishAt = tf;
13649
13977
  }
13978
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
13979
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
13980
+ if (v !== void 0 && v.length > 0) {
13981
+ row.dependsOn = v;
13982
+ }
13983
+ }
13650
13984
  };
13651
13985
  var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13652
13986
  if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
@@ -13693,6 +14027,20 @@ var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13693
14027
  } else if (typeof tf === "string") {
13694
14028
  row.targetFinishAt = tf;
13695
14029
  }
14030
+ if (Object.prototype.hasOwnProperty.call(payload, "dependsOn")) {
14031
+ if (payload["dependsOn"] === null) {
14032
+ delete row.dependsOn;
14033
+ } else {
14034
+ const v = parseTicketDependsOnFromPayloadValue(payload["dependsOn"]);
14035
+ if (v !== void 0) {
14036
+ if (v.length === 0) {
14037
+ delete row.dependsOn;
14038
+ } else {
14039
+ row.dependsOn = v;
14040
+ }
14041
+ }
14042
+ }
14043
+ }
13696
14044
  };
13697
14045
  var applyCreatedAudit = (row, evt) => {
13698
14046
  row.createdAt = evt.ts;
@@ -14319,6 +14667,19 @@ var runGithubInboundSync = async (params) => {
14319
14667
  const planningSource = inboundTicketPlanningPayloadFromFenceMeta(meta);
14320
14668
  const planningPayload = {};
14321
14669
  for (const [k, v] of Object.entries(planningSource)) {
14670
+ if (k === "dependsOn") {
14671
+ if (v === null) {
14672
+ if (!ticketDependsOnListsEqual(ticket.dependsOn, [])) {
14673
+ planningPayload["dependsOn"] = null;
14674
+ }
14675
+ } else if (Array.isArray(v)) {
14676
+ const parsed = parseTicketDependsOnFromPayloadValue(v);
14677
+ if (parsed !== void 0 && !ticketDependsOnListsEqual(ticket.dependsOn, parsed)) {
14678
+ planningPayload["dependsOn"] = parsed;
14679
+ }
14680
+ }
14681
+ continue;
14682
+ }
14322
14683
  const tk = k;
14323
14684
  const cur = ticket[tk];
14324
14685
  if (v === null) {
@@ -14656,6 +15017,9 @@ var buildTicketListQueryFromReadListOpts = (o, deps) => {
14656
15017
  if (targetFinishBeforeMs !== void 0) {
14657
15018
  query.targetFinishBeforeMs = targetFinishBeforeMs;
14658
15019
  }
15020
+ if (o.dependsOn !== void 0 && o.dependsOn.trim() !== "") {
15021
+ query.dependsOnIncludesId = o.dependsOn.trim();
15022
+ }
14659
15023
  return Object.keys(query).length > 0 ? query : void 0;
14660
15024
  };
14661
15025
  var runCli = async (argv, deps = {
@@ -14879,6 +15243,11 @@ var runCli = async (argv, deps = {
14879
15243
  "planning label (repeatable)",
14880
15244
  (value, previous) => [...previous, value],
14881
15245
  []
15246
+ ).option(
15247
+ "--depends-on <id>",
15248
+ "prerequisite ticket id (repeatable)",
15249
+ (value, previous) => [...previous, value],
15250
+ []
14882
15251
  ).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() {
14883
15252
  const g = readGlobals(this);
14884
15253
  const o = this.opts();
@@ -14941,6 +15310,10 @@ var runCli = async (argv, deps = {
14941
15310
  "--target-finish-at",
14942
15311
  deps
14943
15312
  );
15313
+ const dependsOnTokensCreate = normalizeCliStringList(o.dependsOn);
15314
+ const dependsOnNormCreate = normalizeTicketDependsOnIds(
15315
+ dependsOnTokensCreate
15316
+ );
14944
15317
  const planningPayload = {
14945
15318
  ...labelsPayloadCreate,
14946
15319
  ...priorityParsed !== void 0 ? { priority: priorityParsed } : {},
@@ -14967,6 +15340,15 @@ var runCli = async (argv, deps = {
14967
15340
  const branchTokens = normalizeCliStringList(o.branch);
14968
15341
  const branchesNorm = normalizeTicketBranchListFromStrings(branchTokens);
14969
15342
  const branchesPayload = branchesNorm.length > 0 ? { branches: branchesNorm } : {};
15343
+ const dependsOnErr = validateTicketDependsOnForWrite({
15344
+ projection: proj,
15345
+ fromTicketId: id,
15346
+ nextDependsOn: dependsOnNormCreate
15347
+ });
15348
+ if (dependsOnErr !== void 0) {
15349
+ throw new Error(dependsOnErr);
15350
+ }
15351
+ const dependsOnPayloadCreate = dependsOnNormCreate.length > 0 ? { dependsOn: dependsOnNormCreate } : {};
14970
15352
  const evt = makeEvent(
14971
15353
  "TicketCreated",
14972
15354
  {
@@ -14977,6 +15359,7 @@ var runCli = async (argv, deps = {
14977
15359
  ...status !== void 0 ? { status } : {},
14978
15360
  ...assigneeCreate,
14979
15361
  ...branchesPayload,
15362
+ ...dependsOnPayloadCreate,
14980
15363
  ...planningPayload
14981
15364
  },
14982
15365
  deps.clock,
@@ -15034,6 +15417,9 @@ var runCli = async (argv, deps = {
15034
15417
  ).option(
15035
15418
  "--branch <name>",
15036
15419
  "when listing (no --id): only tickets linked to this branch (normalized exact match)"
15420
+ ).option(
15421
+ "--depends-on <id>",
15422
+ "when listing (no --id): only tickets that list this ticket id in dependsOn"
15037
15423
  ).option(
15038
15424
  "--priority <p>",
15039
15425
  "when listing (no --id): OR-set of priorities (repeat flag); low|medium|high|urgent",
@@ -15113,7 +15499,17 @@ var runCli = async (argv, deps = {
15113
15499
  "--clear-labels",
15114
15500
  "remove all planning labels from the ticket",
15115
15501
  false
15116
- ).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() {
15502
+ ).option(
15503
+ "--add-depends-on <id>",
15504
+ "add a prerequisite ticket id (repeatable)",
15505
+ (value, previous) => [...previous, value],
15506
+ []
15507
+ ).option(
15508
+ "--remove-depends-on <id>",
15509
+ "remove a prerequisite ticket id (repeatable)",
15510
+ (value, previous) => [...previous, value],
15511
+ []
15512
+ ).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() {
15117
15513
  const g = readGlobals(this);
15118
15514
  const o = this.opts();
15119
15515
  let body = o.body;
@@ -15165,6 +15561,14 @@ ${body}`
15165
15561
  );
15166
15562
  deps.exit(ExitCode.UserError);
15167
15563
  }
15564
+ const addDependsOnTokens = normalizeCliStringList(o.addDependsOn);
15565
+ const removeDependsOnTokens = normalizeCliStringList(o.removeDependsOn);
15566
+ if (o.clearDependsOn === true && (addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0)) {
15567
+ deps.error(
15568
+ "Cannot use --clear-depends-on with --add-depends-on or --remove-depends-on"
15569
+ );
15570
+ deps.exit(ExitCode.UserError);
15571
+ }
15168
15572
  const mutual = (clear, set, clearName, setName) => {
15169
15573
  if (clear === true && set !== void 0 && set !== "") {
15170
15574
  deps.error(`Cannot use ${clearName} and ${setName} together`);
@@ -15297,6 +15701,35 @@ ${body}`
15297
15701
  payload["labels"] = nextLabels;
15298
15702
  }
15299
15703
  }
15704
+ const wantsDependsOnChange = o.clearDependsOn === true || addDependsOnTokens.length > 0 || removeDependsOnTokens.length > 0;
15705
+ if (wantsDependsOnChange) {
15706
+ let nextDepends;
15707
+ if (o.clearDependsOn === true) {
15708
+ nextDepends = [];
15709
+ } else {
15710
+ const removeDepSet = new Set(
15711
+ normalizeTicketDependsOnIds(removeDependsOnTokens)
15712
+ );
15713
+ nextDepends = normalizeTicketDependsOnIds(
15714
+ (curTicket.dependsOn ?? []).filter((d) => !removeDepSet.has(d))
15715
+ );
15716
+ nextDepends = normalizeTicketDependsOnIds([
15717
+ ...nextDepends,
15718
+ ...addDependsOnTokens
15719
+ ]);
15720
+ }
15721
+ const depErr = validateTicketDependsOnForWrite({
15722
+ projection: proj,
15723
+ fromTicketId: o.id,
15724
+ nextDependsOn: nextDepends
15725
+ });
15726
+ if (depErr !== void 0) {
15727
+ throw new Error(depErr);
15728
+ }
15729
+ if (!ticketDependsOnListsEqual(curTicket.dependsOn, nextDepends)) {
15730
+ payload["dependsOn"] = nextDepends;
15731
+ }
15732
+ }
15300
15733
  if (priorityUpdate !== void 0) {
15301
15734
  payload["priority"] = priorityUpdate;
15302
15735
  }
@@ -15610,7 +16043,19 @@ ${body}`
15610
16043
  }
15611
16044
  deps.exit(ExitCode.Success);
15612
16045
  });
15613
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).option(
16046
+ program2.command("sync").description("Sync data branch over git; optional GitHub Issues sync").option(
16047
+ "--skip-network",
16048
+ "skip all sync network operations (git fetch/merge/push and GitHub); legacy: --no-github",
16049
+ false
16050
+ ).option(
16051
+ "--with-github",
16052
+ "also run GitHub Issues sync (requires GITHUB_TOKEN or gh; needs sync not off in config)",
16053
+ false
16054
+ ).option(
16055
+ "--git-data",
16056
+ "legacy no-op: default sync already updates the data branch via git only",
16057
+ false
16058
+ ).option(
15614
16059
  "--skip-push",
15615
16060
  "after a successful sync, do not push the data branch to the remote",
15616
16061
  false
@@ -15619,115 +16064,171 @@ ${body}`
15619
16064
  const o = this.opts();
15620
16065
  const repoRoot = await resolveRepoRoot(g.repo);
15621
16066
  const cfg = await loadMergedConfig(repoRoot, g);
15622
- if (cfg.sync === "off" || o.noGithub) {
16067
+ if (o.withGithub && o.skipNetwork) {
16068
+ deps.error("Cannot use --with-github together with --skip-network");
16069
+ deps.exit(ExitCode.UserError);
16070
+ }
16071
+ if (o.skipNetwork) {
15623
16072
  deps.log(formatOutput(g.format, { ok: true, skipped: true }));
15624
16073
  deps.exit(ExitCode.Success);
15625
16074
  }
15626
- const githubToken = await resolveGithubTokenForSync({
15627
- envToken: env.GITHUB_TOKEN,
15628
- cwd: repoRoot
15629
- });
15630
- if (!githubToken) {
15631
- deps.error(
15632
- "GitHub auth required for sync: set GITHUB_TOKEN or run `gh auth login`"
15633
- );
15634
- deps.exit(ExitCode.EnvironmentAuth);
15635
- }
15636
- const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
15637
- const session = await openDataBranchWorktree({
15638
- repoRoot,
15639
- dataBranch: cfg.dataBranch,
15640
- tmpBase,
15641
- keepWorktree: g.keepWorktree,
15642
- runGit
15643
- });
15644
- try {
15645
- const projection = await loadProjectionFromDataRoot(
15646
- session.worktreePath
15647
- );
15648
- const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
16075
+ if (o.withGithub) {
16076
+ if (cfg.sync === "off") {
16077
+ deps.error(
16078
+ "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."
16079
+ );
16080
+ deps.exit(ExitCode.UserError);
16081
+ }
16082
+ const githubToken = await resolveGithubTokenForSync({
16083
+ envToken: env.GITHUB_TOKEN,
16084
+ cwd: repoRoot
16085
+ });
16086
+ if (!githubToken) {
16087
+ deps.error(
16088
+ "GitHub auth required for sync --with-github: set GITHUB_TOKEN or run `gh auth login`"
16089
+ );
16090
+ deps.exit(ExitCode.EnvironmentAuth);
16091
+ }
16092
+ const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16093
+ const session = await openDataBranchWorktree({
15649
16094
  repoRoot,
15650
- remote: cfg.remote,
16095
+ dataBranch: cfg.dataBranch,
16096
+ tmpBase,
16097
+ keepWorktree: g.keepWorktree,
15651
16098
  runGit
15652
16099
  });
15653
- const { owner, repo: repo2 } = resolveGithubRepo(
15654
- cfg,
15655
- env.GITHUB_REPO,
15656
- gitDerivedSlug
15657
- );
15658
- const octokit = new Octokit2({ auth: githubToken });
15659
- const outboundActor = await resolveGithubTokenActor(octokit);
15660
- const depsGh = {
15661
- octokit,
15662
- owner,
15663
- repo: repo2,
15664
- clock: deps.clock,
15665
- outboundActor
15666
- };
15667
- await runGithubOutboundSync({
15668
- dataRoot: session.worktreePath,
15669
- projection,
15670
- config: cfg,
15671
- deps: depsGh
15672
- });
15673
- await runGithubInboundSync({
15674
- dataRoot: session.worktreePath,
15675
- projection,
15676
- config: cfg,
15677
- deps: depsGh
15678
- });
15679
- const projectionAfterInbound = await loadProjectionFromDataRoot(
15680
- session.worktreePath
15681
- );
15682
- await runGithubPrActivitySync({
15683
- projection: projectionAfterInbound,
15684
- config: cfg,
15685
- deps: defaultGithubPrActivitySyncDeps({
15686
- dataRoot: session.worktreePath,
15687
- clock: deps.clock,
16100
+ try {
16101
+ const projection = await loadProjectionFromDataRoot(
16102
+ session.worktreePath
16103
+ );
16104
+ const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
16105
+ repoRoot,
16106
+ remote: cfg.remote,
16107
+ runGit
16108
+ });
16109
+ const { owner, repo: repo2 } = resolveGithubRepo(
16110
+ cfg,
16111
+ env.GITHUB_REPO,
16112
+ gitDerivedSlug
16113
+ );
16114
+ const octokit = new Octokit2({ auth: githubToken });
16115
+ const outboundActor = await resolveGithubTokenActor(octokit);
16116
+ const depsGh = {
15688
16117
  octokit,
15689
16118
  owner,
15690
16119
  repo: repo2,
15691
- actor: outboundActor
15692
- })
15693
- });
15694
- await commitDataWorktreeIfNeeded(
15695
- session.worktreePath,
15696
- formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15697
- runGit
15698
- );
15699
- let dataBranchPush;
15700
- let dataBranchPushDetail;
15701
- if (o.skipPush) {
15702
- dataBranchPush = "skipped_cli";
15703
- dataBranchPushDetail = "skip-push";
15704
- } else {
15705
- const pushResult = await tryPushDataBranchToRemote(
16120
+ clock: deps.clock,
16121
+ outboundActor
16122
+ };
16123
+ await runGithubOutboundSync({
16124
+ dataRoot: session.worktreePath,
16125
+ projection,
16126
+ config: cfg,
16127
+ deps: depsGh
16128
+ });
16129
+ await runGithubInboundSync({
16130
+ dataRoot: session.worktreePath,
16131
+ projection,
16132
+ config: cfg,
16133
+ deps: depsGh
16134
+ });
16135
+ const projectionAfterInbound = await loadProjectionFromDataRoot(
16136
+ session.worktreePath
16137
+ );
16138
+ await runGithubPrActivitySync({
16139
+ projection: projectionAfterInbound,
16140
+ config: cfg,
16141
+ deps: defaultGithubPrActivitySyncDeps({
16142
+ dataRoot: session.worktreePath,
16143
+ clock: deps.clock,
16144
+ octokit,
16145
+ owner,
16146
+ repo: repo2,
16147
+ actor: outboundActor
16148
+ })
16149
+ });
16150
+ await commitDataWorktreeIfNeeded(
15706
16151
  session.worktreePath,
15707
- cfg.remote,
15708
- cfg.dataBranch,
16152
+ formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15709
16153
  runGit
15710
16154
  );
15711
- dataBranchPush = pushResult.status;
15712
- dataBranchPushDetail = pushResult.detail;
15713
- if (pushResult.status === "failed" && pushResult.detail) {
15714
- deps.error(
15715
- `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
16155
+ let dataBranchPush;
16156
+ let dataBranchPushDetail;
16157
+ if (o.skipPush) {
16158
+ dataBranchPush = "skipped_cli";
16159
+ dataBranchPushDetail = "skip-push";
16160
+ } else {
16161
+ const pushResult = await tryPushDataBranchToRemote(
16162
+ session.worktreePath,
16163
+ cfg.remote,
16164
+ cfg.dataBranch,
16165
+ runGit
15716
16166
  );
16167
+ dataBranchPush = pushResult.status;
16168
+ dataBranchPushDetail = pushResult.detail;
16169
+ if (pushResult.status === "failed" && pushResult.detail) {
16170
+ deps.error(
16171
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
16172
+ );
16173
+ }
15717
16174
  }
16175
+ deps.log(
16176
+ formatOutput(g.format, {
16177
+ ok: true,
16178
+ githubSync: true,
16179
+ dataBranchPush,
16180
+ ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16181
+ })
16182
+ );
16183
+ } catch (e) {
16184
+ deps.error(e instanceof Error ? e.message : String(e));
16185
+ deps.exit(ExitCode.ExternalApi);
16186
+ } finally {
16187
+ await session.dispose();
16188
+ }
16189
+ deps.exit(ExitCode.Success);
16190
+ }
16191
+ const tmpBaseGit = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16192
+ const sessionGit = await openDataBranchWorktree({
16193
+ repoRoot,
16194
+ dataBranch: cfg.dataBranch,
16195
+ tmpBase: tmpBaseGit,
16196
+ keepWorktree: g.keepWorktree,
16197
+ runGit
16198
+ });
16199
+ try {
16200
+ let syncResult;
16201
+ try {
16202
+ syncResult = await runRemoteDataBranchGitSync(
16203
+ sessionGit.worktreePath,
16204
+ cfg.remote,
16205
+ cfg.dataBranch,
16206
+ runGit,
16207
+ Boolean(o.skipPush)
16208
+ );
16209
+ } catch (e) {
16210
+ if (e instanceof SyncRemoteDataBranchMergeError) {
16211
+ deps.error(e.message);
16212
+ deps.exit(ExitCode.UserError);
16213
+ }
16214
+ deps.error(e instanceof Error ? e.message : String(e));
16215
+ deps.exit(ExitCode.UserError);
16216
+ }
16217
+ if (syncResult.dataBranchPush === "failed" && syncResult.dataBranchPushDetail !== void 0) {
16218
+ deps.error(
16219
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${syncResult.dataBranchPushDetail}`
16220
+ );
15718
16221
  }
15719
16222
  deps.log(
15720
16223
  formatOutput(g.format, {
15721
16224
  ok: true,
15722
- dataBranchPush,
15723
- ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
16225
+ gitDataOnly: true,
16226
+ ...o.gitData ? { legacyGitDataFlag: true } : {},
16227
+ ...syncResult
15724
16228
  })
15725
16229
  );
15726
- } catch (e) {
15727
- deps.error(e instanceof Error ? e.message : String(e));
15728
- deps.exit(ExitCode.ExternalApi);
15729
16230
  } finally {
15730
- await session.dispose();
16231
+ await sessionGit.dispose();
15731
16232
  }
15732
16233
  deps.exit(ExitCode.Success);
15733
16234
  });
@@ -15893,7 +16394,7 @@ ${body}`
15893
16394
  await session.dispose();
15894
16395
  }
15895
16396
  });
15896
- await program2.parseAsync(argv, { from: "node" });
16397
+ await program2.parseAsync(normalizeRawCliArgv(argv), { from: "node" });
15897
16398
  };
15898
16399
  var makeEvent = (type, payload, clock, actor) => ({
15899
16400
  schema: 1,