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/main.cjs CHANGED
@@ -7612,6 +7612,20 @@ var envSchema = external_exports.object({
7612
7612
  HYPER_PM_AI_API_KEY: external_exports.string().optional(),
7613
7613
  /** Optional override for hyper-pm JSONL event `actor` on CLI mutations. */
7614
7614
  HYPER_PM_ACTOR: external_exports.string().optional(),
7615
+ /**
7616
+ * Optional `git user.name` for hyper-pm data-branch commits when repo git
7617
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7618
+ */
7619
+ HYPER_PM_GIT_USER_NAME: external_exports.string().optional(),
7620
+ /**
7621
+ * Optional `git user.email` for hyper-pm data-branch commits when repo git
7622
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7623
+ */
7624
+ HYPER_PM_GIT_USER_EMAIL: external_exports.string().optional(),
7625
+ /** Standard Git override for commit author name (optional). */
7626
+ GIT_AUTHOR_NAME: external_exports.string().optional(),
7627
+ /** Standard Git override for commit author email (optional). */
7628
+ GIT_AUTHOR_EMAIL: external_exports.string().optional(),
7615
7629
  /**
7616
7630
  * Absolute path to the hyper-pm CLI bundle (`main.cjs`) for hyper-pm-mcp and
7617
7631
  * other integrations when auto-resolution from the `hyper-pm` package is insufficient.
@@ -11516,6 +11530,103 @@ var normalizeTicketBranchListFromPayloadValue = (value) => {
11516
11530
  return normalizeTicketBranchListFromStrings(strings);
11517
11531
  };
11518
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
+
11519
11630
  // src/lib/ticket-planning-fields.ts
11520
11631
  var PRIORITY_SET = /* @__PURE__ */ new Set([
11521
11632
  "low",
@@ -11684,6 +11795,9 @@ var formatOutput = (format, value) => {
11684
11795
  return JSON.stringify(value);
11685
11796
  };
11686
11797
 
11798
+ // src/cli/normalize-raw-cli-argv.ts
11799
+ var normalizeRawCliArgv = (argv) => argv.map((token) => token === "--no-github" ? "--skip-network" : token);
11800
+
11687
11801
  // src/cli/prune-unchanged-work-item-update-payload.ts
11688
11802
  var branchListsEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
11689
11803
  var pruneEpicOrStoryUpdatePayloadAgainstRow = (cur, draft) => {
@@ -11739,6 +11853,20 @@ var pruneTicketUpdatePayloadAgainstRow = (cur, draft) => {
11739
11853
  out["labels"] = draft["labels"];
11740
11854
  }
11741
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
+ }
11742
11870
  if (draft["priority"] !== void 0) {
11743
11871
  const curP = cur.priority ?? null;
11744
11872
  const nextP = draft["priority"] === null ? null : draft["priority"];
@@ -11905,6 +12033,20 @@ var workItemUpdateAspects = (kind, payload) => {
11905
12033
  }
11906
12034
  }
11907
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
+ }
11908
12050
  if (kind === "ticket" && payload["priority"] !== void 0) {
11909
12051
  if (payload["priority"] === null) {
11910
12052
  aspects.push("cleared priority");
@@ -12011,6 +12153,7 @@ var buildAuditLinkMetadata = (evt, githubRepo) => {
12011
12153
  if (p["status"] !== void 0) meta["status"] = String(p["status"]);
12012
12154
  if (p["assignee"] !== void 0) meta["assignee"] = p["assignee"];
12013
12155
  if (p["labels"] !== void 0) meta["labels"] = p["labels"];
12156
+ if (p["dependsOn"] !== void 0) meta["dependsOn"] = p["dependsOn"];
12014
12157
  if (p["priority"] !== void 0) meta["priority"] = p["priority"];
12015
12158
  if (p["size"] !== void 0) meta["size"] = p["size"];
12016
12159
  if (p["estimate"] !== void 0) meta["estimate"] = p["estimate"];
@@ -12078,6 +12221,14 @@ var formatEpicStoryTicketCreated = (evt, entity) => {
12078
12221
  parts.push("with labels");
12079
12222
  }
12080
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
+ }
12081
12232
  if (p["priority"] !== void 0) {
12082
12233
  parts.push(`with priority ${quoteStatus(String(p["priority"]))}`);
12083
12234
  }
@@ -12171,6 +12322,18 @@ var formatAuditHumanSentence = (evt) => {
12171
12322
  bits.push("updated labels");
12172
12323
  }
12173
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
+ }
12174
12337
  if (p["priority"] !== void 0) {
12175
12338
  bits.push("updated priority");
12176
12339
  }
@@ -12450,6 +12613,13 @@ var ticketMatchesTicketListQuery = (ticket, projection, query) => {
12450
12613
  return false;
12451
12614
  }
12452
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
+ }
12453
12623
  return true;
12454
12624
  };
12455
12625
 
@@ -12655,6 +12825,7 @@ var listActiveTicketSummaries = (projection, options) => {
12655
12825
  ...t.estimate !== void 0 ? { estimate: t.estimate } : {},
12656
12826
  ...t.startWorkAt !== void 0 ? { startWorkAt: t.startWorkAt } : {},
12657
12827
  ...t.targetFinishAt !== void 0 ? { targetFinishAt: t.targetFinishAt } : {},
12828
+ ...t.dependsOn !== void 0 && t.dependsOn.length > 0 ? { dependsOn: t.dependsOn } : {},
12658
12829
  ...t.linkedBranches.length > 0 ? { linkedBranches: t.linkedBranches } : {},
12659
12830
  ...last !== void 0 ? {
12660
12831
  lastPrActivity: {
@@ -12751,6 +12922,17 @@ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12751
12922
  }
12752
12923
  }
12753
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
+ }
12754
12936
  return out;
12755
12937
  };
12756
12938
  var buildGithubIssueBody = (params) => {
@@ -12776,6 +12958,9 @@ var buildGithubIssueBody = (params) => {
12776
12958
  if (p.targetFinishAt !== void 0) {
12777
12959
  meta.target_finish_at = p.targetFinishAt;
12778
12960
  }
12961
+ if (p.dependsOn !== void 0 && p.dependsOn.length > 0) {
12962
+ meta.depends_on = p.dependsOn;
12963
+ }
12779
12964
  }
12780
12965
  return `${params.description.trim()}
12781
12966
 
@@ -12801,6 +12986,9 @@ var ticketPlanningForGithubIssueBody = (ticket) => {
12801
12986
  if (ticket.targetFinishAt !== void 0) {
12802
12987
  out.targetFinishAt = ticket.targetFinishAt;
12803
12988
  }
12989
+ if (ticket.dependsOn !== void 0 && ticket.dependsOn.length > 0) {
12990
+ out.dependsOn = ticket.dependsOn;
12991
+ }
12804
12992
  return Object.keys(out).length > 0 ? out : void 0;
12805
12993
  };
12806
12994
 
@@ -13353,6 +13541,33 @@ var tryReadGithubOwnerRepoSlugFromGit = async (params) => {
13353
13541
  }
13354
13542
  };
13355
13543
 
13544
+ // src/git/resolve-effective-git-author-for-data-commit.ts
13545
+ var defaultDataCommitName = "hyper-pm";
13546
+ var defaultDataCommitEmail = "hyper-pm@users.noreply.github.com";
13547
+ var tryReadGitConfigUserName = async (cwd, runGit2) => {
13548
+ try {
13549
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.name"]);
13550
+ return stdout.trim();
13551
+ } catch {
13552
+ return "";
13553
+ }
13554
+ };
13555
+ var tryReadGitConfigUserEmail = async (cwd, runGit2) => {
13556
+ try {
13557
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.email"]);
13558
+ return stdout.trim();
13559
+ } catch {
13560
+ return "";
13561
+ }
13562
+ };
13563
+ var resolveEffectiveGitAuthorForDataCommit = async (cwd, runGit2, authorEnv) => {
13564
+ const fromGitName = await tryReadGitConfigUserName(cwd, runGit2);
13565
+ const fromGitEmail = await tryReadGitConfigUserEmail(cwd, runGit2);
13566
+ const name = fromGitName || authorEnv.HYPER_PM_GIT_USER_NAME?.trim() || authorEnv.GIT_AUTHOR_NAME?.trim() || defaultDataCommitName;
13567
+ const email = fromGitEmail || authorEnv.HYPER_PM_GIT_USER_EMAIL?.trim() || authorEnv.GIT_AUTHOR_EMAIL?.trim() || defaultDataCommitEmail;
13568
+ return { name, email };
13569
+ };
13570
+
13356
13571
  // src/run/commit-data.ts
13357
13572
  var maxActorSuffixLen = 60;
13358
13573
  var formatDataBranchCommitMessage = (base, actorSuffix) => {
@@ -13364,11 +13579,205 @@ var formatDataBranchCommitMessage = (base, actorSuffix) => {
13364
13579
  const suffix = collapsed.length > maxActorSuffixLen ? `${collapsed.slice(0, maxActorSuffixLen - 1)}\u2026` : collapsed;
13365
13580
  return `${base} (${suffix})`;
13366
13581
  };
13367
- var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2) => {
13582
+ var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2, opts) => {
13368
13583
  const { stdout } = await runGit2(worktreePath, ["status", "--porcelain"]);
13369
13584
  if (!stdout.trim()) return;
13370
13585
  await runGit2(worktreePath, ["add", "."]);
13371
- await runGit2(worktreePath, ["commit", "-m", message]);
13586
+ const authorEnv = opts?.authorEnv ?? env;
13587
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13588
+ worktreePath,
13589
+ runGit2,
13590
+ authorEnv
13591
+ );
13592
+ await runGit2(worktreePath, [
13593
+ "-c",
13594
+ `user.name=${name}`,
13595
+ "-c",
13596
+ `user.email=${email}`,
13597
+ "commit",
13598
+ "-m",
13599
+ message
13600
+ ]);
13601
+ };
13602
+
13603
+ // src/run/push-data-branch.ts
13604
+ var firstLineFromUnknown = (err) => {
13605
+ const raw = err instanceof Error ? err.message : String(err);
13606
+ const line = raw.trim().split("\n")[0]?.trim() ?? raw.trim();
13607
+ return line.length > 0 ? line : "git error";
13608
+ };
13609
+ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) => {
13610
+ try {
13611
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
13612
+ } catch (e) {
13613
+ return {
13614
+ status: "skipped_no_remote",
13615
+ detail: firstLineFromUnknown(e)
13616
+ };
13617
+ }
13618
+ try {
13619
+ await runGit2(worktreePath, ["push", "-u", remote, branch]);
13620
+ return { status: "pushed" };
13621
+ } catch (e) {
13622
+ return {
13623
+ status: "failed",
13624
+ detail: firstLineFromUnknown(e)
13625
+ };
13626
+ }
13627
+ };
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
+ };
13372
13781
  };
13373
13782
 
13374
13783
  // src/storage/append-event.ts
@@ -13566,6 +13975,12 @@ var applyTicketPlanningFieldsFromCreatePayload = (row, payload) => {
13566
13975
  if (typeof tf === "string") {
13567
13976
  row.targetFinishAt = tf;
13568
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
+ }
13569
13984
  };
13570
13985
  var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13571
13986
  if (Object.prototype.hasOwnProperty.call(payload, "labels")) {
@@ -13612,6 +14027,20 @@ var applyTicketPlanningFieldsFromUpdatePayload = (row, payload) => {
13612
14027
  } else if (typeof tf === "string") {
13613
14028
  row.targetFinishAt = tf;
13614
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
+ }
13615
14044
  };
13616
14045
  var applyCreatedAudit = (row, evt) => {
13617
14046
  row.createdAt = evt.ts;
@@ -14238,6 +14667,19 @@ var runGithubInboundSync = async (params) => {
14238
14667
  const planningSource = inboundTicketPlanningPayloadFromFenceMeta(meta);
14239
14668
  const planningPayload = {};
14240
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
+ }
14241
14683
  const tk = k;
14242
14684
  const cur = ticket[tk];
14243
14685
  if (v === null) {
@@ -14575,6 +15017,9 @@ var buildTicketListQueryFromReadListOpts = (o, deps) => {
14575
15017
  if (targetFinishBeforeMs !== void 0) {
14576
15018
  query.targetFinishBeforeMs = targetFinishBeforeMs;
14577
15019
  }
15020
+ if (o.dependsOn !== void 0 && o.dependsOn.trim() !== "") {
15021
+ query.dependsOnIncludesId = o.dependsOn.trim();
15022
+ }
14578
15023
  return Object.keys(query).length > 0 ? query : void 0;
14579
15024
  };
14580
15025
  var runCli = async (argv, deps = {
@@ -14798,6 +15243,11 @@ var runCli = async (argv, deps = {
14798
15243
  "planning label (repeatable)",
14799
15244
  (value, previous) => [...previous, value],
14800
15245
  []
15246
+ ).option(
15247
+ "--depends-on <id>",
15248
+ "prerequisite ticket id (repeatable)",
15249
+ (value, previous) => [...previous, value],
15250
+ []
14801
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() {
14802
15252
  const g = readGlobals(this);
14803
15253
  const o = this.opts();
@@ -14860,6 +15310,10 @@ var runCli = async (argv, deps = {
14860
15310
  "--target-finish-at",
14861
15311
  deps
14862
15312
  );
15313
+ const dependsOnTokensCreate = normalizeCliStringList(o.dependsOn);
15314
+ const dependsOnNormCreate = normalizeTicketDependsOnIds(
15315
+ dependsOnTokensCreate
15316
+ );
14863
15317
  const planningPayload = {
14864
15318
  ...labelsPayloadCreate,
14865
15319
  ...priorityParsed !== void 0 ? { priority: priorityParsed } : {},
@@ -14886,6 +15340,15 @@ var runCli = async (argv, deps = {
14886
15340
  const branchTokens = normalizeCliStringList(o.branch);
14887
15341
  const branchesNorm = normalizeTicketBranchListFromStrings(branchTokens);
14888
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 } : {};
14889
15352
  const evt = makeEvent(
14890
15353
  "TicketCreated",
14891
15354
  {
@@ -14896,6 +15359,7 @@ var runCli = async (argv, deps = {
14896
15359
  ...status !== void 0 ? { status } : {},
14897
15360
  ...assigneeCreate,
14898
15361
  ...branchesPayload,
15362
+ ...dependsOnPayloadCreate,
14899
15363
  ...planningPayload
14900
15364
  },
14901
15365
  deps.clock,
@@ -14953,6 +15417,9 @@ var runCli = async (argv, deps = {
14953
15417
  ).option(
14954
15418
  "--branch <name>",
14955
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"
14956
15423
  ).option(
14957
15424
  "--priority <p>",
14958
15425
  "when listing (no --id): OR-set of priorities (repeat flag); low|medium|high|urgent",
@@ -15032,7 +15499,17 @@ var runCli = async (argv, deps = {
15032
15499
  "--clear-labels",
15033
15500
  "remove all planning labels from the ticket",
15034
15501
  false
15035
- ).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() {
15036
15513
  const g = readGlobals(this);
15037
15514
  const o = this.opts();
15038
15515
  let body = o.body;
@@ -15084,6 +15561,14 @@ ${body}`
15084
15561
  );
15085
15562
  deps.exit(ExitCode.UserError);
15086
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
+ }
15087
15572
  const mutual = (clear, set, clearName, setName) => {
15088
15573
  if (clear === true && set !== void 0 && set !== "") {
15089
15574
  deps.error(`Cannot use ${clearName} and ${setName} together`);
@@ -15216,6 +15701,35 @@ ${body}`
15216
15701
  payload["labels"] = nextLabels;
15217
15702
  }
15218
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
+ }
15219
15733
  if (priorityUpdate !== void 0) {
15220
15734
  payload["priority"] = priorityUpdate;
15221
15735
  }
@@ -15529,94 +16043,192 @@ ${body}`
15529
16043
  }
15530
16044
  deps.exit(ExitCode.Success);
15531
16045
  });
15532
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).action(async function() {
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(
16059
+ "--skip-push",
16060
+ "after a successful sync, do not push the data branch to the remote",
16061
+ false
16062
+ ).action(async function() {
15533
16063
  const g = readGlobals(this);
15534
16064
  const o = this.opts();
15535
16065
  const repoRoot = await resolveRepoRoot(g.repo);
15536
16066
  const cfg = await loadMergedConfig(repoRoot, g);
15537
- 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) {
15538
16072
  deps.log(formatOutput(g.format, { ok: true, skipped: true }));
15539
16073
  deps.exit(ExitCode.Success);
15540
16074
  }
15541
- const githubToken = await resolveGithubTokenForSync({
15542
- envToken: env.GITHUB_TOKEN,
15543
- cwd: repoRoot
15544
- });
15545
- if (!githubToken) {
15546
- deps.error(
15547
- "GitHub auth required for sync: set GITHUB_TOKEN or run `gh auth login`"
15548
- );
15549
- deps.exit(ExitCode.EnvironmentAuth);
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({
16094
+ repoRoot,
16095
+ dataBranch: cfg.dataBranch,
16096
+ tmpBase,
16097
+ keepWorktree: g.keepWorktree,
16098
+ runGit
16099
+ });
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 = {
16117
+ octokit,
16118
+ owner,
16119
+ repo: repo2,
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(
16151
+ session.worktreePath,
16152
+ formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
16153
+ runGit
16154
+ );
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
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
+ }
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);
15550
16190
  }
15551
- const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
15552
- const session = await openDataBranchWorktree({
16191
+ const tmpBaseGit = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
16192
+ const sessionGit = await openDataBranchWorktree({
15553
16193
  repoRoot,
15554
16194
  dataBranch: cfg.dataBranch,
15555
- tmpBase,
16195
+ tmpBase: tmpBaseGit,
15556
16196
  keepWorktree: g.keepWorktree,
15557
16197
  runGit
15558
16198
  });
15559
16199
  try {
15560
- const projection = await loadProjectionFromDataRoot(
15561
- session.worktreePath
15562
- );
15563
- const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
15564
- repoRoot,
15565
- remote: cfg.remote,
15566
- runGit
15567
- });
15568
- const { owner, repo: repo2 } = resolveGithubRepo(
15569
- cfg,
15570
- env.GITHUB_REPO,
15571
- gitDerivedSlug
15572
- );
15573
- const octokit = new Octokit2({ auth: githubToken });
15574
- const outboundActor = await resolveGithubTokenActor(octokit);
15575
- const depsGh = {
15576
- octokit,
15577
- owner,
15578
- repo: repo2,
15579
- clock: deps.clock,
15580
- outboundActor
15581
- };
15582
- await runGithubOutboundSync({
15583
- dataRoot: session.worktreePath,
15584
- projection,
15585
- config: cfg,
15586
- deps: depsGh
15587
- });
15588
- await runGithubInboundSync({
15589
- dataRoot: session.worktreePath,
15590
- projection,
15591
- config: cfg,
15592
- deps: depsGh
15593
- });
15594
- const projectionAfterInbound = await loadProjectionFromDataRoot(
15595
- session.worktreePath
15596
- );
15597
- await runGithubPrActivitySync({
15598
- projection: projectionAfterInbound,
15599
- config: cfg,
15600
- deps: defaultGithubPrActivitySyncDeps({
15601
- dataRoot: session.worktreePath,
15602
- clock: deps.clock,
15603
- octokit,
15604
- owner,
15605
- repo: repo2,
15606
- actor: outboundActor
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
+ );
16221
+ }
16222
+ deps.log(
16223
+ formatOutput(g.format, {
16224
+ ok: true,
16225
+ gitDataOnly: true,
16226
+ ...o.gitData ? { legacyGitDataFlag: true } : {},
16227
+ ...syncResult
15607
16228
  })
15608
- });
15609
- await commitDataWorktreeIfNeeded(
15610
- session.worktreePath,
15611
- formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15612
- runGit
15613
16229
  );
15614
- deps.log(formatOutput(g.format, { ok: true }));
15615
- } catch (e) {
15616
- deps.error(e instanceof Error ? e.message : String(e));
15617
- deps.exit(ExitCode.ExternalApi);
15618
16230
  } finally {
15619
- await session.dispose();
16231
+ await sessionGit.dispose();
15620
16232
  }
15621
16233
  deps.exit(ExitCode.Success);
15622
16234
  });
@@ -15782,7 +16394,7 @@ ${body}`
15782
16394
  await session.dispose();
15783
16395
  }
15784
16396
  });
15785
- await program2.parseAsync(argv, { from: "node" });
16397
+ await program2.parseAsync(normalizeRawCliArgv(argv), { from: "node" });
15786
16398
  };
15787
16399
  var makeEvent = (type, payload, clock, actor) => ({
15788
16400
  schema: 1,