jj-octo 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/jj-octo.js +191 -35
  2. package/package.json +1 -1
package/dist/jj-octo.js CHANGED
@@ -1410,6 +1410,7 @@ var getOrUndefined = /* @__PURE__ */ getOrElse(constUndefined);
1410
1410
  var map = /* @__PURE__ */ dual(2, (self, f) => isNone2(self) ? none2() : some2(f(self.value)));
1411
1411
  var flatMap = /* @__PURE__ */ dual(2, (self, f) => isNone2(self) ? none2() : f(self.value));
1412
1412
  var toArray = (self) => isNone2(self) ? [] : [self.value];
1413
+ var filter = /* @__PURE__ */ dual(2, (self, predicate) => isNone2(self) ? none2() : predicate(self.value) ? some2(self.value) : none2());
1413
1414
  var exists = /* @__PURE__ */ dual(2, (self, refinement) => isNone2(self) ? false : refinement(self.value));
1414
1415
 
1415
1416
  // node_modules/effect/dist/Context.js
@@ -3380,6 +3381,14 @@ var catchCauseFilter = /* @__PURE__ */ dual(3, (self, filter, f) => catchCause(s
3380
3381
  }));
3381
3382
  var catch_ = /* @__PURE__ */ dual(2, (self, f) => catchCauseFilter(self, findError, (e) => f(e)));
3382
3383
  var tapCause = /* @__PURE__ */ dual(2, (self, f) => catchCause(self, (cause) => andThen(f(cause), failCause(cause))));
3384
+ var tapCauseFilter = /* @__PURE__ */ dual(3, (self, filter, f) => catchCause(self, (cause) => {
3385
+ const result = filter(cause);
3386
+ if (isFailure2(result)) {
3387
+ return failCause(cause);
3388
+ }
3389
+ return andThen(f(result.success, cause), failCause(cause));
3390
+ }));
3391
+ var tapError = /* @__PURE__ */ dual(2, (self, f) => tapCauseFilter(self, findError, (e) => f(e)));
3383
3392
  var catchIf = /* @__PURE__ */ dual((args) => isEffect(args[0]), (self, predicate, f, orElse) => catchCause(self, (cause) => {
3384
3393
  const error = findError(cause);
3385
3394
  if (isFailure2(error))
@@ -4998,6 +5007,7 @@ var catchCause2 = catchCause;
4998
5007
  var catchFilter2 = catchFilter;
4999
5008
  var mapError3 = mapError2;
5000
5009
  var orDie2 = orDie;
5010
+ var tapError2 = tapError;
5001
5011
  var tapCause2 = tapCause;
5002
5012
  var ignore2 = ignore;
5003
5013
  var orElseSucceed2 = orElseSucceed;
@@ -10937,6 +10947,9 @@ function decodeUnknownEffect(schema, options) {
10937
10947
  const parser = run2(schema.ast);
10938
10948
  return options === undefined ? parser : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions));
10939
10949
  }
10950
+ function decodeUnknownOption(schema, options) {
10951
+ return asOption(decodeUnknownEffect(schema, options));
10952
+ }
10940
10953
  var mergeParseOptions = (options, overrideOptions) => overrideOptions ? {
10941
10954
  ...options,
10942
10955
  ...overrideOptions
@@ -10975,6 +10988,17 @@ function runWithCompiler(compiler, ast) {
10975
10988
  function asExit(parser) {
10976
10989
  return (input, options) => runSyncExit2(parser(input, options));
10977
10990
  }
10991
+ function asOption(parser) {
10992
+ const parserExit = asExit(parser);
10993
+ return (input, options) => {
10994
+ const exit = parserExit(input, options);
10995
+ if (isSuccess3(exit)) {
10996
+ return some2(exit.value);
10997
+ }
10998
+ getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues");
10999
+ return none2();
11000
+ };
11001
+ }
10978
11002
  function runSync3(effect, message) {
10979
11003
  const exit = runSyncExit2(effect);
10980
11004
  if (isSuccess3(exit)) {
@@ -11304,6 +11328,7 @@ function decodeUnknownEffect2(schema, options) {
11304
11328
  };
11305
11329
  }
11306
11330
  var decodeEffect2 = decodeUnknownEffect2;
11331
+ var decodeUnknownOption2 = decodeUnknownOption;
11307
11332
  var make25 = make24;
11308
11333
  function isSchema(u) {
11309
11334
  return hasProperty(u, TypeId29) && u[TypeId29] === TypeId29;
@@ -18239,7 +18264,7 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
18239
18264
  // package.json
18240
18265
  var package_default = {
18241
18266
  name: "jj-octo",
18242
- version: "0.7.1",
18267
+ version: "0.8.0",
18243
18268
  description: "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
18244
18269
  license: "MIT",
18245
18270
  repository: {
@@ -18449,6 +18474,10 @@ var anyOf = (revsets) => ["none()", ...revsets].join(" | ");
18449
18474
  var bookmarkRevset = (bookmark) => `bookmarks(exact:"${bookmark}")`;
18450
18475
  var lines2 = (output) => output.split(`
18451
18476
  `).filter((line) => line !== "");
18477
+ var parseDescription = (json) => {
18478
+ const parsed = JSON.parse(json);
18479
+ return typeof parsed === "string" ? parsed : "";
18480
+ };
18452
18481
  var readLog = (revset, template) => gen2(function* () {
18453
18482
  const jj = yield* Jj;
18454
18483
  return yield* jj.read(["log", "--no-graph", "--reversed", "-r", revset, "-T", template]);
@@ -18496,8 +18525,7 @@ var loggedChanges = (revset) => logLines(revset, CHANGE).pipe(map7((logged) => l
18496
18525
  bookmarks = "",
18497
18526
  json = '""'
18498
18527
  ] = line.split("\t");
18499
- const parsed = JSON.parse(json);
18500
- const description = typeof parsed === "string" ? parsed : "";
18528
+ const description = parseDescription(json);
18501
18529
  return {
18502
18530
  bookmarks: bookmarks === "" ? [] : bookmarks.split(" "),
18503
18531
  changeId,
@@ -18916,6 +18944,64 @@ var log2 = (revision) => gen2(function* () {
18916
18944
  var logCommand = make31("log", { revision: revisionFlag("A change of the stack to show") }, ({ revision }) => asUserError(log2(revision).pipe(provide3(GitHubFromOrigin))).pipe(flatMap4((printed) => log(printed.join(`
18917
18945
  `))))).pipe(withHelp("Show changes and their pull requests", "Prints one line per change, top first: its bookmark, its pull request with whether that is a draft or ready for review, and whether the change is pushed, changed since the push, or not pushed.", "Fails if the changes aren't a chain, if one has conflicts, or if one doesn't have exactly one bookmark. `submit` makes the same check before it changes anything.", "Changes nothing, locally or on GitHub."));
18918
18946
 
18947
+ // src/clients/pr-descriptions.ts
18948
+ class PrDescriptions extends Service()("jj-octo/PrDescriptions") {
18949
+ }
18950
+ var StoredRecord = Struct({
18951
+ bookmark: String4,
18952
+ commit: String4,
18953
+ description: String4
18954
+ });
18955
+ var decode2 = decodeUnknownOption2(fromJsonString2(StoredRecord));
18956
+ var RECORD = /^\d+$/u;
18957
+ var PARTIAL = /^\d+\.[\da-f-]+\.partial$/u;
18958
+ var STOPPED_AFTER_MS = 60 * 60 * 1000;
18959
+ var warn = (action) => (error2) => error(`couldn't ${action} the agreed pull request descriptions: ${error2.message}`);
18960
+ var prDescriptionsLayer = effect(PrDescriptions, gen2(function* () {
18961
+ const fs = yield* FileSystem;
18962
+ const jj = yield* Jj;
18963
+ const path = yield* Path;
18964
+ const directory = yield* cached2(gen2(function* () {
18965
+ const dotJj = path.join((yield* jj.read(["workspace", "root"])).trim(), ".jj");
18966
+ const repository = path.join(dotJj, "repo");
18967
+ const shared = (yield* fs.stat(repository)).type === "File" ? path.resolve(dotJj, (yield* fs.readFileString(repository)).trim()) : repository;
18968
+ return path.join(shared, "jj-octo", "pr-descriptions");
18969
+ }));
18970
+ const recordOf = (file) => fs.readFileString(file).pipe(map7(decode2), orElseSucceed2(() => none2()));
18971
+ const remove = (file, name) => fs.remove(file, { force: true }).pipe(catch_2(warn(`prune ${name} in`)));
18972
+ return {
18973
+ prune: (bookmarks) => gen2(function* () {
18974
+ const records = yield* directory;
18975
+ const names = (yield* fs.exists(records)) ? yield* fs.readDirectory(records) : [];
18976
+ const recorded = names.filter((name) => RECORD.test(name));
18977
+ const left = recorded.length === 0 ? [] : yield* bookmarks;
18978
+ for (const name of recorded) {
18979
+ const file = path.join(records, name);
18980
+ const record = yield* recordOf(file);
18981
+ if (isNone2(record) || !left.includes(record.value.bookmark)) {
18982
+ yield* remove(file, name);
18983
+ }
18984
+ }
18985
+ const now = Date.now();
18986
+ for (const name of names.filter((file) => PARTIAL.test(file))) {
18987
+ const file = path.join(records, name);
18988
+ const modified = yield* fs.stat(file).pipe(map7(({ mtime }) => getOrElse(mtime, () => new Date(0)).getTime()), orElseSucceed2(() => now));
18989
+ if (now - modified > STOPPED_AFTER_MS) {
18990
+ yield* remove(file, name);
18991
+ }
18992
+ }
18993
+ }).pipe(catch_2(warn("prune"))),
18994
+ read: (pullRequest, bookmark) => directory.pipe(flatMap4((records) => recordOf(path.join(records, String(pullRequest)))), map7(filter((record) => record.bookmark === bookmark)), orElseSucceed2(() => none2())),
18995
+ write: ({ bookmark, commit, description, pullRequest }) => gen2(function* () {
18996
+ const records = yield* directory;
18997
+ yield* fs.makeDirectory(records, { recursive: true });
18998
+ const record = path.join(records, String(pullRequest));
18999
+ const partial = `${record}.${crypto.randomUUID()}.partial`;
19000
+ yield* fs.writeFileString(partial, JSON.stringify({ bookmark, commit, description })).pipe(andThen2(fs.rename(partial, record)), tapError2(() => fs.remove(partial, { force: true }).pipe(ignore2)));
19001
+ }).pipe(catch_2(warn("record")))
19002
+ };
19003
+ }));
19004
+
18919
19005
  // src/cli/gh/stack/plan.ts
18920
19006
  var basesFor = (branches, trunk) => [
18921
19007
  trunk,
@@ -18960,44 +19046,93 @@ var normalizedDescription = (description, { ignoreClaudeCodeFooter }) => {
18960
19046
  `);
18961
19047
  return descriptionFrom(titleAndBody(ignoreClaudeCodeFooter ? text.replace(claudeCodeFooter, "") : text));
18962
19048
  };
19049
+ var agreedDescription = ({
19050
+ isOwnEarlier,
19051
+ onGitHub,
19052
+ pushed,
19053
+ recorded
19054
+ }) => getOrElse(recorded, () => isOwnEarlier ? onGitHub : pushed);
18963
19055
  var descriptionSyncFor = ({
19056
+ agreed,
18964
19057
  local,
18965
- onGitHub,
18966
- pushed
19058
+ onGitHub
18967
19059
  }) => {
18968
19060
  if (local === onGitHub) {
18969
19061
  return { _tag: "InSync" };
18970
19062
  }
18971
- if (local === pushed) {
19063
+ if (local === agreed) {
18972
19064
  return { _tag: "TakeGitHub" };
18973
19065
  }
18974
- return onGitHub === pushed ? { _tag: "SendLocal" } : { _tag: "Merge" };
19066
+ return onGitHub === agreed ? { _tag: "SendLocal" } : { _tag: "Merge" };
18975
19067
  };
18976
19068
 
18977
19069
  // src/cli/gh/stack/descriptions.ts
18978
19070
  class DescriptionConflict extends TaggedError2("DescriptionConflict") {
18979
19071
  }
19072
+ var kept = (text) => ({ send: false, text });
19073
+ var sent = (text) => ({ send: true, text });
18980
19074
  var ignoreClaudeCodeFooterKey = "jj-octo.ignore-claude-code-footer";
18981
19075
  var ignoresClaudeCodeFooter = jjFlag(ignoreClaudeCodeFooterKey);
18982
19076
  var descriptionsOf = (commits) => commits.length === 0 ? succeed6(new Map) : readLog(commits.join(" | "), String.raw`commit_id ++ "\t" ++ description.escape_json() ++ "\n"`).pipe(map7((output) => new Map(lines2(output).map((line) => {
18983
19077
  const [commit = "", json = '""'] = line.split("\t");
18984
- const parsed = JSON.parse(json);
18985
- return [commit, typeof parsed === "string" ? parsed : ""];
19078
+ return [commit, parseDescription(json)];
18986
19079
  }))));
18987
- var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCodeFooter, overwriteDescriptions, pushedDescription }) => gen2(function* () {
19080
+ var ownHistory = (bookmark, changeId) => gen2(function* () {
19081
+ const jj = yield* Jj;
19082
+ const output = yield* jj.read([
19083
+ "evolog",
19084
+ "--no-graph",
19085
+ "-r",
19086
+ bookmarkRevset(bookmark),
19087
+ "-T",
19088
+ String.raw`commit.change_id() ++ "\t" ++ commit.commit_id() ++ "\t" ++ predecessors.map(|p| p.commit_id()).join(",") ++ "\t" ++ if(operation, operation.description()) ++ "\t" ++ commit.description().escape_json() ++ "\n"`
19089
+ ]);
19090
+ return lines2(output).flatMap((line) => {
19091
+ const [change = "", commit = "", predecessors = "", operation = "", json = '""'] = line.split("\t");
19092
+ const isImported = operation.startsWith("fetch from git") || operation.startsWith("import git");
19093
+ return change === changeId ? [
19094
+ {
19095
+ commit,
19096
+ description: parseDescription(json),
19097
+ isImported,
19098
+ predecessors: predecessors === "" ? [] : predecessors.split(",")
19099
+ }
19100
+ ] : [];
19101
+ });
19102
+ });
19103
+ var isDescribedHereFrom = (history, commit) => {
19104
+ const since = history.findIndex((entry) => entry.commit === commit);
19105
+ const descriptions = new Map(history.map((entry) => [entry.commit, entry.description]));
19106
+ return since !== -1 && history.slice(0, since).every(({ description, isImported, predecessors }) => !isImported || predecessors.some((each) => descriptions.get(each) === description));
19107
+ };
19108
+ var syncDescription = ({ bookmark, changeId, description }, pullRequest, {
19109
+ ignoreClaudeCodeFooter,
19110
+ overwriteDescriptions,
19111
+ pushedCommit,
19112
+ pushedDescription
19113
+ }) => gen2(function* () {
18988
19114
  const git = yield* Git;
18989
19115
  const jj = yield* Jj;
19116
+ const records = yield* PrDescriptions;
18990
19117
  const describe = (text) => jj.run(["describe", "-r", bookmarkRevset(bookmark), "-m", text]);
18991
- const local = normalizedDescription(description, { ignoreClaudeCodeFooter });
18992
- const onGitHub = normalizedDescription(descriptionFrom(pullRequest), {
18993
- ignoreClaudeCodeFooter
18994
- });
18995
- const pushed = normalizedDescription(pushedDescription, { ignoreClaudeCodeFooter });
18996
- return yield* value4(descriptionSyncFor({ local, onGitHub, pushed })).pipe(tag3("InSync", () => succeed6(none2())), tag3("TakeGitHub", () => describe(onGitHub).pipe(andThen2(error(`took the description of #${pullRequest.number} from GitHub`)), as2(none2()))), tag3("SendLocal", () => succeed6(some2(local))), tag3("Merge", () => gen2(function* () {
19118
+ const footer = { ignoreClaudeCodeFooter };
19119
+ const normalized = (text) => normalizedDescription(text, footer);
19120
+ const local = normalized(description);
19121
+ const onGitHub = normalized(descriptionFrom(pullRequest));
19122
+ const pushed = normalized(pushedDescription);
19123
+ const record = yield* records.read(pullRequest.number, bookmark);
19124
+ const history = local === onGitHub || isNone2(record) && onGitHub === pushed ? [] : yield* ownHistory(bookmark, changeId);
19125
+ const age = (commit) => history.findIndex((entry) => entry.commit === commit);
19126
+ const pushedAge = age(pushedCommit);
19127
+ const isPushedAgreedSince = (commit) => onGitHub === pushed && pushedAge !== -1 && pushedAge < age(commit);
19128
+ const recorded = record.pipe(filter(({ commit }) => isDescribedHereFrom(history, commit) && !isPushedAgreedSince(commit)), map(({ description: text }) => normalized(text)));
19129
+ const isOwnEarlier = onGitHub !== pushed && history.some((entry) => normalized(entry.description) === onGitHub);
19130
+ const agreed = agreedDescription({ isOwnEarlier, onGitHub, pushed, recorded });
19131
+ return yield* value4(descriptionSyncFor({ agreed, local, onGitHub })).pipe(tag3("InSync", () => succeed6(kept(local))), tag3("TakeGitHub", () => describe(onGitHub).pipe(andThen2(error(`took the description of #${pullRequest.number} from GitHub`)), as2(kept(onGitHub)))), tag3("SendLocal", () => succeed6(sent(local))), tag3("Merge", () => gen2(function* () {
18997
19132
  if (overwriteDescriptions) {
18998
- return some2(local);
19133
+ return sent(local);
18999
19134
  }
19000
- const merged = yield* git.mergeText({ base: pushed, local, remote: onGitHub });
19135
+ const merged = yield* git.mergeText({ base: agreed, local, remote: onGitHub });
19001
19136
  if (merged.conflicted) {
19002
19137
  return yield* new DescriptionConflict({
19003
19138
  message: [
@@ -19011,7 +19146,7 @@ var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCod
19011
19146
  });
19012
19147
  }
19013
19148
  yield* describe(merged.text);
19014
- return some2(merged.text);
19149
+ return sent(merged.text);
19015
19150
  })), exhaustive2);
19016
19151
  });
19017
19152
 
@@ -19199,6 +19334,8 @@ var reconcile = ({
19199
19334
  }
19200
19335
  const onStacks = new Set(scope === "every stack" ? before.map(({ name }) => name) : scope.bookmarks);
19201
19336
  yield* forget((yield* bookmarksAt("::trunk()")).filter((name) => onStacks.has(name)));
19337
+ const records = yield* PrDescriptions;
19338
+ yield* records.prune(provideService2(bookmarksAt("all()"), Jj, jj));
19202
19339
  if (notCombined.length > 0) {
19203
19340
  yield* new NotCombined({
19204
19341
  message: [
@@ -19304,6 +19441,7 @@ var register = (remaining, pullRequests) => gen2(function* () {
19304
19441
  var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function* () {
19305
19442
  const github = yield* GitHub;
19306
19443
  const jj = yield* Jj;
19444
+ const records = yield* PrDescriptions;
19307
19445
  const bookmarks = stack.map(({ bookmark }) => bookmark);
19308
19446
  const bases = basesFor(bookmarks, trunk);
19309
19447
  const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
@@ -19311,19 +19449,25 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19311
19449
  const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
19312
19450
  const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
19313
19451
  const changes = [];
19314
- for (const entry of found) {
19315
- const pushed = targets.get(entry.change.bookmark)?.origin;
19452
+ for (const { base, change, pullRequest } of found) {
19453
+ const pushed = targets.get(change.bookmark)?.origin;
19454
+ const options = {
19455
+ ignoreClaudeCodeFooter,
19456
+ overwriteDescriptions,
19457
+ pushedCommit: pushed,
19458
+ pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
19459
+ };
19316
19460
  changes.push({
19317
- ...entry,
19318
- description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
19319
- ignoreClaudeCodeFooter,
19320
- overwriteDescriptions,
19321
- pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
19461
+ base,
19462
+ change,
19463
+ open: isNone2(pullRequest) ? none2() : some2({
19464
+ pullRequest: pullRequest.value,
19465
+ synced: yield* syncDescription(change, pullRequest.value, options)
19322
19466
  })
19323
19467
  });
19324
19468
  }
19325
- const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
19326
- const wanted = changes.map(({ pullRequest }) => isSome2(pullRequest) ? some2(pullRequest.value.number) : none2());
19469
+ const moving = changes.flatMap(({ base, open }) => isSome2(open) && open.value.pullRequest.base !== base ? [open.value.pullRequest] : []);
19470
+ const wanted = changes.map(({ open }) => map(open, ({ pullRequest }) => pullRequest.number));
19327
19471
  const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
19328
19472
  const dissolve = mustDissolve(existing, wanted, moving.length > 0);
19329
19473
  if (dissolve && isSome2(existing)) {
@@ -19342,9 +19486,16 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19342
19486
  "push",
19343
19487
  ...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
19344
19488
  ]);
19489
+ const pushedTargets = yield* bookmarkTargets;
19490
+ const record = (bookmark, description, pullRequest) => records.write({
19491
+ bookmark,
19492
+ commit: pushedTargets.get(bookmark)?.local ?? "",
19493
+ description,
19494
+ pullRequest
19495
+ });
19345
19496
  const numbers = [];
19346
- for (const { base, change, description, pullRequest } of changes) {
19347
- if (isNone2(pullRequest)) {
19497
+ for (const { base, change, open } of changes) {
19498
+ if (isNone2(open)) {
19348
19499
  const created = yield* github.createPullRequest({
19349
19500
  ...titleAndBody(change.description),
19350
19501
  base,
@@ -19352,16 +19503,21 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19352
19503
  head: change.bookmark
19353
19504
  });
19354
19505
  yield* error(`opened #${created} for ${change.bookmark}`);
19506
+ yield* record(change.bookmark, change.description, created);
19355
19507
  numbers.push(created);
19356
19508
  } else {
19357
- const { number } = pullRequest.value;
19509
+ const {
19510
+ pullRequest: { number },
19511
+ synced: { send, text }
19512
+ } = open.value;
19358
19513
  if (moved.has(number) && base !== trunk) {
19359
19514
  yield* github.setBase(number, base);
19360
19515
  }
19361
- if (isSome2(description)) {
19362
- yield* github.setDescription(number, titleAndBody(description.value));
19516
+ if (send) {
19517
+ yield* github.setDescription(number, titleAndBody(text));
19363
19518
  yield* error(`updated the description of #${number}`);
19364
19519
  }
19520
+ yield* record(change.bookmark, text, number);
19365
19521
  numbers.push(number);
19366
19522
  }
19367
19523
  }
@@ -19472,7 +19628,7 @@ var submitCommand = make31("submit", {
19472
19628
  overwriteDescriptions: Boolean6("overwrite-descriptions").pipe(withDefault5(false), withDescription2("Where a description changed both locally and on GitHub, send the local one instead of merging the two")),
19473
19629
  publish: Boolean6("publish").pipe(withDefault5(false), withDescription2("Open new pull requests ready for review, instead of as drafts")),
19474
19630
  revision: revisionFlag("The top change to submit")
19475
- }, ({ overwriteDescriptions, publish, revision }) => asUserError(submit(revision, { draft: !publish, overwriteDescriptions }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Push a change and the ones below it, and open their pull requests", "Pushes the bookmark of each of those changes. An empty working copy means the change below it, so from the top of a stack this submits all of it. Opens a pull request for each branch that has none, with the title and body from the change's description. Points each pull request at the branch below it, and the bottom one at the default branch.", "New pull requests are opened as drafts, or ready for review with `--publish`. The flag doesn't change a pull request that already exists.", "Keeps each pull request's title and body the same as its change's description. A description edited on GitHub is written into the change, one edited locally is sent to the pull request, and edits on both sides are merged line by line. Edits to the same lines stop the submit before anything is pushed.", "First, where any of those changes' pull requests merged or any of their branches changed on GitHub, syncs their stack as `jj gh sync` does, and leaves other stacks alone. That drops the merged changes, takes what GitHub added to the branches, and rebases the rest onto the trunk, so the push doesn't undo GitHub's work.", "Stops before changing anything where one of their branches was deleted on GitHub and their change doesn't count as merged: no pull request from it merged, another commit merged, or the change was edited since. It says how to push the change again, or drop it.", "Registers the pull requests as one GitHub stack. A stack of one change is a single pull request, which GitHub has no stack for.", "If changes were reordered, inserted or dropped since the last submit, the GitHub stack is dissolved and registered again under a new number. The pull requests stay open, including the one of a dropped change."));
19631
+ }, ({ overwriteDescriptions, publish, revision }) => asUserError(submit(revision, { draft: !publish, overwriteDescriptions }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Push a change and the ones below it, and open their pull requests", "Pushes the bookmark of each of those changes. An empty working copy means the change below it, so from the top of a stack this submits all of it. Opens a pull request for each branch that has none, with the title and body from the change's description. Points each pull request at the branch below it, and the bottom one at the default branch.", "New pull requests are opened as drafts, or ready for review with `--publish`. The flag doesn't change a pull request that already exists.", "Keeps each pull request's title and body the same as its change's description. A description edited on GitHub is written into the change, one edited locally is sent to the pull request, and edits on both sides are merged line by line. That holds for a local edit pushed with `jj git push` too, since submit records the description that both sides last agreed on in `jj-octo/pr-descriptions` under the repository's `.jj/repo`, in the main workspace. Edits to the same lines stop the submit before anything is pushed.", "First, where any of those changes' pull requests merged or any of their branches changed on GitHub, syncs their stack as `jj gh sync` does, and leaves other stacks alone. That drops the merged changes, takes what GitHub added to the branches, and rebases the rest onto the trunk, so the push doesn't undo GitHub's work.", "Stops before changing anything where one of their branches was deleted on GitHub and their change doesn't count as merged: no pull request from it merged, another commit merged, or the change was edited since. It says how to push the change again, or drop it.", "Registers the pull requests as one GitHub stack. A stack of one change is a single pull request, which GitHub has no stack for.", "If changes were reordered, inserted or dropped since the last submit, the GitHub stack is dissolved and registered again under a new number. The pull requests stay open, including the one of a dropped change."));
19476
19632
 
19477
19633
  // src/cli/gh/command.ts
19478
19634
  var jjGhCommand = make31("jj gh").pipe(withSubcommands([
@@ -19485,6 +19641,6 @@ var jjGhCommand = make31("jj gh").pipe(withSubcommands([
19485
19641
  ]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
19486
19642
 
19487
19643
  // src/main.ts
19488
- runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, JjLive]), provide3(layer13({
19644
+ runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, provideMerge(prDescriptionsLayer, JjLive)]), provide3(layer13({
19489
19645
  builtIns: [Help, Version, Wizard, LogLevel]
19490
19646
  })), provide3([layer12, layer14])));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj-octo",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
5
5
  "license": "MIT",
6
6
  "repository": {