hyper-pm 0.1.3 → 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
@@ -7622,6 +7622,20 @@ var envSchema = external_exports.object({
7622
7622
  HYPER_PM_AI_API_KEY: external_exports.string().optional(),
7623
7623
  /** Optional override for hyper-pm JSONL event `actor` on CLI mutations. */
7624
7624
  HYPER_PM_ACTOR: external_exports.string().optional(),
7625
+ /**
7626
+ * Optional `git user.name` for hyper-pm data-branch commits when repo git
7627
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7628
+ */
7629
+ HYPER_PM_GIT_USER_NAME: external_exports.string().optional(),
7630
+ /**
7631
+ * Optional `git user.email` for hyper-pm data-branch commits when repo git
7632
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7633
+ */
7634
+ HYPER_PM_GIT_USER_EMAIL: external_exports.string().optional(),
7635
+ /** Standard Git override for commit author name (optional). */
7636
+ GIT_AUTHOR_NAME: external_exports.string().optional(),
7637
+ /** Standard Git override for commit author email (optional). */
7638
+ GIT_AUTHOR_EMAIL: external_exports.string().optional(),
7625
7639
  /**
7626
7640
  * Absolute path to the hyper-pm CLI bundle (`main.cjs`) for hyper-pm-mcp and
7627
7641
  * other integrations when auto-resolution from the `hyper-pm` package is insufficient.
@@ -11526,6 +11540,103 @@ var normalizeTicketBranchListFromPayloadValue = (value) => {
11526
11540
  return normalizeTicketBranchListFromStrings(strings);
11527
11541
  };
11528
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
+
11529
11640
  // src/lib/ticket-planning-fields.ts
11530
11641
  var PRIORITY_SET = /* @__PURE__ */ new Set([
11531
11642
  "low",
@@ -11694,6 +11805,9 @@ var formatOutput = (format, value) => {
11694
11805
  return JSON.stringify(value);
11695
11806
  };
11696
11807
 
11808
+ // src/cli/normalize-raw-cli-argv.ts
11809
+ var normalizeRawCliArgv = (argv) => argv.map((token) => token === "--no-github" ? "--skip-network" : token);
11810
+
11697
11811
  // src/cli/prune-unchanged-work-item-update-payload.ts
11698
11812
  var branchListsEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
11699
11813
  var pruneEpicOrStoryUpdatePayloadAgainstRow = (cur, draft) => {
@@ -11749,6 +11863,20 @@ var pruneTicketUpdatePayloadAgainstRow = (cur, draft) => {
11749
11863
  out["labels"] = draft["labels"];
11750
11864
  }
11751
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
+ }
11752
11880
  if (draft["priority"] !== void 0) {
11753
11881
  const curP = cur.priority ?? null;
11754
11882
  const nextP = draft["priority"] === null ? null : draft["priority"];
@@ -11915,6 +12043,20 @@ var workItemUpdateAspects = (kind, payload) => {
11915
12043
  }
11916
12044
  }
11917
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
+ }
11918
12060
  if (kind === "ticket" && payload["priority"] !== void 0) {
11919
12061
  if (payload["priority"] === null) {
11920
12062
  aspects.push("cleared priority");
@@ -12021,6 +12163,7 @@ var buildAuditLinkMetadata = (evt, githubRepo) => {
12021
12163
  if (p["status"] !== void 0) meta["status"] = String(p["status"]);
12022
12164
  if (p["assignee"] !== void 0) meta["assignee"] = p["assignee"];
12023
12165
  if (p["labels"] !== void 0) meta["labels"] = p["labels"];
12166
+ if (p["dependsOn"] !== void 0) meta["dependsOn"] = p["dependsOn"];
12024
12167
  if (p["priority"] !== void 0) meta["priority"] = p["priority"];
12025
12168
  if (p["size"] !== void 0) meta["size"] = p["size"];
12026
12169
  if (p["estimate"] !== void 0) meta["estimate"] = p["estimate"];
@@ -12088,6 +12231,14 @@ var formatEpicStoryTicketCreated = (evt, entity) => {
12088
12231
  parts.push("with labels");
12089
12232
  }
12090
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
+ }
12091
12242
  if (p["priority"] !== void 0) {
12092
12243
  parts.push(`with priority ${quoteStatus(String(p["priority"]))}`);
12093
12244
  }
@@ -12181,6 +12332,18 @@ var formatAuditHumanSentence = (evt) => {
12181
12332
  bits.push("updated labels");
12182
12333
  }
12183
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
+ }
12184
12347
  if (p["priority"] !== void 0) {
12185
12348
  bits.push("updated priority");
12186
12349
  }
@@ -12460,6 +12623,13 @@ var ticketMatchesTicketListQuery = (ticket, projection, query) => {
12460
12623
  return false;
12461
12624
  }
12462
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
+ }
12463
12633
  return true;
12464
12634
  };
12465
12635
 
@@ -12665,6 +12835,7 @@ var listActiveTicketSummaries = (projection, options) => {
12665
12835
  ...t.estimate !== void 0 ? { estimate: t.estimate } : {},
12666
12836
  ...t.startWorkAt !== void 0 ? { startWorkAt: t.startWorkAt } : {},
12667
12837
  ...t.targetFinishAt !== void 0 ? { targetFinishAt: t.targetFinishAt } : {},
12838
+ ...t.dependsOn !== void 0 && t.dependsOn.length > 0 ? { dependsOn: t.dependsOn } : {},
12668
12839
  ...t.linkedBranches.length > 0 ? { linkedBranches: t.linkedBranches } : {},
12669
12840
  ...last !== void 0 ? {
12670
12841
  lastPrActivity: {
@@ -12761,6 +12932,17 @@ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12761
12932
  }
12762
12933
  }
12763
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
+ }
12764
12946
  return out;
12765
12947
  };
12766
12948
  var buildGithubIssueBody = (params) => {
@@ -12786,6 +12968,9 @@ var buildGithubIssueBody = (params) => {
12786
12968
  if (p.targetFinishAt !== void 0) {
12787
12969
  meta.target_finish_at = p.targetFinishAt;
12788
12970
  }
12971
+ if (p.dependsOn !== void 0 && p.dependsOn.length > 0) {
12972
+ meta.depends_on = p.dependsOn;
12973
+ }
12789
12974
  }
12790
12975
  return `${params.description.trim()}
12791
12976
 
@@ -12811,6 +12996,9 @@ var ticketPlanningForGithubIssueBody = (ticket) => {
12811
12996
  if (ticket.targetFinishAt !== void 0) {
12812
12997
  out.targetFinishAt = ticket.targetFinishAt;
12813
12998
  }
12999
+ if (ticket.dependsOn !== void 0 && ticket.dependsOn.length > 0) {
13000
+ out.dependsOn = ticket.dependsOn;
13001
+ }
12814
13002
  return Object.keys(out).length > 0 ? out : void 0;
12815
13003
  };
12816
13004
 
@@ -13363,6 +13551,33 @@ var tryReadGithubOwnerRepoSlugFromGit = async (params) => {
13363
13551
  }
13364
13552
  };
13365
13553
 
13554
+ // src/git/resolve-effective-git-author-for-data-commit.ts
13555
+ var defaultDataCommitName = "hyper-pm";
13556
+ var defaultDataCommitEmail = "hyper-pm@users.noreply.github.com";
13557
+ var tryReadGitConfigUserName = async (cwd, runGit2) => {
13558
+ try {
13559
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.name"]);
13560
+ return stdout.trim();
13561
+ } catch {
13562
+ return "";
13563
+ }
13564
+ };
13565
+ var tryReadGitConfigUserEmail = async (cwd, runGit2) => {
13566
+ try {
13567
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.email"]);
13568
+ return stdout.trim();
13569
+ } catch {
13570
+ return "";
13571
+ }
13572
+ };
13573
+ var resolveEffectiveGitAuthorForDataCommit = async (cwd, runGit2, authorEnv) => {
13574
+ const fromGitName = await tryReadGitConfigUserName(cwd, runGit2);
13575
+ const fromGitEmail = await tryReadGitConfigUserEmail(cwd, runGit2);
13576
+ const name = fromGitName || authorEnv.HYPER_PM_GIT_USER_NAME?.trim() || authorEnv.GIT_AUTHOR_NAME?.trim() || defaultDataCommitName;
13577
+ const email = fromGitEmail || authorEnv.HYPER_PM_GIT_USER_EMAIL?.trim() || authorEnv.GIT_AUTHOR_EMAIL?.trim() || defaultDataCommitEmail;
13578
+ return { name, email };
13579
+ };
13580
+
13366
13581
  // src/run/commit-data.ts
13367
13582
  var maxActorSuffixLen = 60;
13368
13583
  var formatDataBranchCommitMessage = (base, actorSuffix) => {
@@ -13374,11 +13589,205 @@ var formatDataBranchCommitMessage = (base, actorSuffix) => {
13374
13589
  const suffix = collapsed.length > maxActorSuffixLen ? `${collapsed.slice(0, maxActorSuffixLen - 1)}\u2026` : collapsed;
13375
13590
  return `${base} (${suffix})`;
13376
13591
  };
13377
- var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2) => {
13592
+ var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2, opts) => {
13378
13593
  const { stdout } = await runGit2(worktreePath, ["status", "--porcelain"]);
13379
13594
  if (!stdout.trim()) return;
13380
13595
  await runGit2(worktreePath, ["add", "."]);
13381
- await runGit2(worktreePath, ["commit", "-m", message]);
13596
+ const authorEnv = opts?.authorEnv ?? env;
13597
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13598
+ worktreePath,
13599
+ runGit2,
13600
+ authorEnv
13601
+ );
13602
+ await runGit2(worktreePath, [
13603
+ "-c",
13604
+ `user.name=${name}`,
13605
+ "-c",
13606
+ `user.email=${email}`,
13607
+ "commit",
13608
+ "-m",
13609
+ message
13610
+ ]);
13611
+ };
13612
+
13613
+ // src/run/push-data-branch.ts
13614
+ var firstLineFromUnknown = (err) => {
13615
+ const raw = err instanceof Error ? err.message : String(err);
13616
+ const line = raw.trim().split("\n")[0]?.trim() ?? raw.trim();
13617
+ return line.length > 0 ? line : "git error";
13618
+ };
13619
+ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) => {
13620
+ try {
13621
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
13622
+ } catch (e) {
13623
+ return {
13624
+ status: "skipped_no_remote",
13625
+ detail: firstLineFromUnknown(e)
13626
+ };
13627
+ }
13628
+ try {
13629
+ await runGit2(worktreePath, ["push", "-u", remote, branch]);
13630
+ return { status: "pushed" };
13631
+ } catch (e) {
13632
+ return {
13633
+ status: "failed",
13634
+ detail: firstLineFromUnknown(e)
13635
+ };
13636
+ }
13637
+ };
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
+ };
13382
13791
  };
13383
13792
 
13384
13793
  // src/storage/append-event.ts
@@ -13576,6 +13985,12 @@ var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13576
13985
  if (typeof tf === "string") {
13577
13986
  row.targetFinishAt = tf;
13578
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
+ }
13579
13994
  };
13580
13995
  var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13581
13996
  if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
@@ -13622,6 +14037,20 @@ var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13622
14037
  } else if (typeof tf === "string") {
13623
14038
  row.targetFinishAt = tf;
13624
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
+ }
13625
14054
  };
13626
14055
  var applyCreatedAudit = (row, evt) => {
13627
14056
  row.createdAt = evt.ts;
@@ -14248,6 +14677,19 @@ var runGithubInboundSync = async (params) => {
14248
14677
  const planningSource = inboundTicketPlanningPayloadFromFenceMeta(meta);
14249
14678
  const planningPayload = {};
14250
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
+ }
14251
14693
  const tk = k;
14252
14694
  const cur = ticket[tk];
14253
14695
  if (v === null) {
@@ -14585,6 +15027,9 @@ var buildTicketListQueryFromReadListOpts = (o, deps) => {
14585
15027
  if (targetFinishBeforeMs !== void 0) {
14586
15028
  query.targetFinishBeforeMs = targetFinishBeforeMs;
14587
15029
  }
15030
+ if (o.dependsOn !== void 0 && o.dependsOn.trim() !== "") {
15031
+ query.dependsOnIncludesId = o.dependsOn.trim();
15032
+ }
14588
15033
  return Object.keys(query).length > 0 ? query : void 0;
14589
15034
  };
14590
15035
  var runCli = async (argv, deps = {
@@ -14808,6 +15253,11 @@ var runCli = async (argv, deps = {
14808
15253
  "planning label (repeatable)",
14809
15254
  (value, previous) => [...previous, value],
14810
15255
  []
15256
+ ).option(
15257
+ "--depends-on <id>",
15258
+ "prerequisite ticket id (repeatable)",
15259
+ (value, previous) => [...previous, value],
15260
+ []
14811
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() {
14812
15262
  const g = readGlobals(this);
14813
15263
  const o = this.opts();
@@ -14870,6 +15320,10 @@ var runCli = async (argv, deps = {
14870
15320
  "--target-finish-at",
14871
15321
  deps
14872
15322
  );
15323
+ const dependsOnTokensCreate = normalizeCliStringList(o.dependsOn);
15324
+ const dependsOnNormCreate = normalizeTicketDependsOnIds(
15325
+ dependsOnTokensCreate
15326
+ );
14873
15327
  const planningPayload = {
14874
15328
  ...labelsPayloadCreate,
14875
15329
  ...priorityParsed !== void 0 ? { priority: priorityParsed } : {},
@@ -14896,6 +15350,15 @@ var runCli = async (argv, deps = {
14896
15350
  const branchTokens = normalizeCliStringList(o.branch);
14897
15351
  const branchesNorm = normalizeTicketBranchListFromStrings(branchTokens);
14898
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 } : {};
14899
15362
  const evt = makeEvent(
14900
15363
  "TicketCreated",
14901
15364
  {
@@ -14906,6 +15369,7 @@ var runCli = async (argv, deps = {
14906
15369
  ...status !== void 0 ? { status } : {},
14907
15370
  ...assigneeCreate,
14908
15371
  ...branchesPayload,
15372
+ ...dependsOnPayloadCreate,
14909
15373
  ...planningPayload
14910
15374
  },
14911
15375
  deps.clock,
@@ -14963,6 +15427,9 @@ var runCli = async (argv, deps = {
14963
15427
  ).option(
14964
15428
  "--branch <name>",
14965
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"
14966
15433
  ).option(
14967
15434
  "--priority <p>",
14968
15435
  "when listing (no --id): OR-set of priorities (repeat flag); low|medium|high|urgent",
@@ -15042,7 +15509,17 @@ var runCli = async (argv, deps = {
15042
15509
  "--clear-labels",
15043
15510
  "remove all planning labels from the ticket",
15044
15511
  false
15045
- ).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() {
15046
15523
  const g = readGlobals(this);
15047
15524
  const o = this.opts();
15048
15525
  let body = o.body;
@@ -15094,6 +15571,14 @@ ${body}`
15094
15571
  );
15095
15572
  deps.exit(ExitCode.UserError);
15096
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
+ }
15097
15582
  const mutual = (clear, set, clearName, setName) => {
15098
15583
  if (clear === true && set !== void 0 && set !== "") {
15099
15584
  deps.error(`Cannot use ${clearName} and ${setName} together`);
@@ -15226,6 +15711,35 @@ ${body}`
15226
15711
  payload["labels"] = nextLabels;
15227
15712
  }
15228
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
+ }
15229
15743
  if (priorityUpdate !== void 0) {
15230
15744
  payload["priority"] = priorityUpdate;
15231
15745
  }
@@ -15539,94 +16053,192 @@ ${body}`
15539
16053
  }
15540
16054
  deps.exit(ExitCode.Success);
15541
16055
  });
15542
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).action(async function() {
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(
16069
+ "--skip-push",
16070
+ "after a successful sync, do not push the data branch to the remote",
16071
+ false
16072
+ ).action(async function() {
15543
16073
  const g = readGlobals(this);
15544
16074
  const o = this.opts();
15545
16075
  const repoRoot = await resolveRepoRoot(g.repo);
15546
16076
  const cfg = await loadMergedConfig(repoRoot, g);
15547
- 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) {
15548
16082
  deps.log(formatOutput(g.format, { ok: true, skipped: true }));
15549
16083
  deps.exit(ExitCode.Success);
15550
16084
  }
15551
- const githubToken = await resolveGithubTokenForSync({
15552
- envToken: env.GITHUB_TOKEN,
15553
- cwd: repoRoot
15554
- });
15555
- if (!githubToken) {
15556
- deps.error(
15557
- "GitHub auth required for sync: set GITHUB_TOKEN or run `gh auth login`"
15558
- );
15559
- deps.exit(ExitCode.EnvironmentAuth);
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({
16104
+ repoRoot,
16105
+ dataBranch: cfg.dataBranch,
16106
+ tmpBase,
16107
+ keepWorktree: g.keepWorktree,
16108
+ runGit
16109
+ });
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 = {
16127
+ octokit,
16128
+ owner,
16129
+ repo: repo2,
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(
16161
+ session.worktreePath,
16162
+ formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
16163
+ runGit
16164
+ );
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
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
+ }
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);
15560
16200
  }
15561
- const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
15562
- const session = await openDataBranchWorktree({
16201
+ const tmpBaseGit = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16202
+ const sessionGit = await openDataBranchWorktree({
15563
16203
  repoRoot,
15564
16204
  dataBranch: cfg.dataBranch,
15565
- tmpBase,
16205
+ tmpBase: tmpBaseGit,
15566
16206
  keepWorktree: g.keepWorktree,
15567
16207
  runGit
15568
16208
  });
15569
16209
  try {
15570
- const projection = await loadProjectionFromDataRoot(
15571
- session.worktreePath
15572
- );
15573
- const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
15574
- repoRoot,
15575
- remote: cfg.remote,
15576
- runGit
15577
- });
15578
- const { owner, repo: repo2 } = resolveGithubRepo(
15579
- cfg,
15580
- env.GITHUB_REPO,
15581
- gitDerivedSlug
15582
- );
15583
- const octokit = new Octokit2({ auth: githubToken });
15584
- const outboundActor = await resolveGithubTokenActor(octokit);
15585
- const depsGh = {
15586
- octokit,
15587
- owner,
15588
- repo: repo2,
15589
- clock: deps.clock,
15590
- outboundActor
15591
- };
15592
- await runGithubOutboundSync({
15593
- dataRoot: session.worktreePath,
15594
- projection,
15595
- config: cfg,
15596
- deps: depsGh
15597
- });
15598
- await runGithubInboundSync({
15599
- dataRoot: session.worktreePath,
15600
- projection,
15601
- config: cfg,
15602
- deps: depsGh
15603
- });
15604
- const projectionAfterInbound = await loadProjectionFromDataRoot(
15605
- session.worktreePath
15606
- );
15607
- await runGithubPrActivitySync({
15608
- projection: projectionAfterInbound,
15609
- config: cfg,
15610
- deps: defaultGithubPrActivitySyncDeps({
15611
- dataRoot: session.worktreePath,
15612
- clock: deps.clock,
15613
- octokit,
15614
- owner,
15615
- repo: repo2,
15616
- actor: outboundActor
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
+ );
16231
+ }
16232
+ deps.log(
16233
+ formatOutput(g.format, {
16234
+ ok: true,
16235
+ gitDataOnly: true,
16236
+ ...o.gitData ? { legacyGitDataFlag: true } : {},
16237
+ ...syncResult
15617
16238
  })
15618
- });
15619
- await commitDataWorktreeIfNeeded(
15620
- session.worktreePath,
15621
- formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15622
- runGit
15623
16239
  );
15624
- deps.log(formatOutput(g.format, { ok: true }));
15625
- } catch (e) {
15626
- deps.error(e instanceof Error ? e.message : String(e));
15627
- deps.exit(ExitCode.ExternalApi);
15628
16240
  } finally {
15629
- await session.dispose();
16241
+ await sessionGit.dispose();
15630
16242
  }
15631
16243
  deps.exit(ExitCode.Success);
15632
16244
  });
@@ -15792,7 +16404,7 @@ ${body}`
15792
16404
  await session.dispose();
15793
16405
  }
15794
16406
  });
15795
- await program2.parseAsync(argv, { from: "node" });
16407
+ await program2.parseAsync(normalizeRawCliArgv(argv), { from: "node" });
15796
16408
  };
15797
16409
  var makeEvent = (type, payload, clock, actor) => ({
15798
16410
  schema: 1,