jj-octo 0.7.2 → 0.8.1

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 +177 -42
  2. package/package.json +3 -3
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.2",
18267
+ version: "0.8.1",
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: {
@@ -18276,8 +18301,8 @@ var package_default = {
18276
18301
  "@tsconfig/strictest": "2.0.8",
18277
18302
  "@types/bun": "1.4.3",
18278
18303
  effect: "4.0.5",
18279
- oxfmt: "0.73.0",
18280
- oxlint: "1.88.0",
18304
+ oxfmt: "0.74.0",
18305
+ oxlint: "1.89.0",
18281
18306
  "oxlint-tsgolint": "7.0.2004",
18282
18307
  typescript: "7.0.2"
18283
18308
  },
@@ -18919,6 +18944,64 @@ var log2 = (revision) => gen2(function* () {
18919
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(`
18920
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."));
18921
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
+
18922
19005
  // src/cli/gh/stack/plan.ts
18923
19006
  var basesFor = (branches, trunk) => [
18924
19007
  trunk,
@@ -18948,7 +19031,7 @@ var titleAndBody = (description) => {
18948
19031
  const [title = "", ...rest] = description.trimEnd().split(`
18949
19032
  `);
18950
19033
  return { body: rest.join(`
18951
- `).trim(), title };
19034
+ `).replace(/^\s*\n/u, ""), title };
18952
19035
  };
18953
19036
  var descriptionFrom = ({
18954
19037
  body,
@@ -18963,30 +19046,38 @@ var normalizedDescription = (description, { ignoreClaudeCodeFooter }) => {
18963
19046
  `);
18964
19047
  return descriptionFrom(titleAndBody(ignoreClaudeCodeFooter ? text.replace(claudeCodeFooter, "") : text));
18965
19048
  };
19049
+ var agreedDescription = ({
19050
+ isOwnEarlier,
19051
+ onGitHub,
19052
+ pushed,
19053
+ recorded
19054
+ }) => getOrElse(recorded, () => isOwnEarlier ? onGitHub : pushed);
18966
19055
  var descriptionSyncFor = ({
19056
+ agreed,
18967
19057
  local,
18968
- onGitHub,
18969
- pushed
19058
+ onGitHub
18970
19059
  }) => {
18971
19060
  if (local === onGitHub) {
18972
19061
  return { _tag: "InSync" };
18973
19062
  }
18974
- if (local === pushed) {
19063
+ if (local === agreed) {
18975
19064
  return { _tag: "TakeGitHub" };
18976
19065
  }
18977
- return onGitHub === pushed ? { _tag: "SendLocal" } : { _tag: "Merge" };
19066
+ return onGitHub === agreed ? { _tag: "SendLocal" } : { _tag: "Merge" };
18978
19067
  };
18979
19068
 
18980
19069
  // src/cli/gh/stack/descriptions.ts
18981
19070
  class DescriptionConflict extends TaggedError2("DescriptionConflict") {
18982
19071
  }
19072
+ var kept = (text) => ({ send: false, text });
19073
+ var sent = (text) => ({ send: true, text });
18983
19074
  var ignoreClaudeCodeFooterKey = "jj-octo.ignore-claude-code-footer";
18984
19075
  var ignoresClaudeCodeFooter = jjFlag(ignoreClaudeCodeFooterKey);
18985
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) => {
18986
19077
  const [commit = "", json = '""'] = line.split("\t");
18987
19078
  return [commit, parseDescription(json)];
18988
19079
  }))));
18989
- var ownDescriptions = (bookmark, changeId) => gen2(function* () {
19080
+ var ownHistory = (bookmark, changeId) => gen2(function* () {
18990
19081
  const jj = yield* Jj;
18991
19082
  const output = yield* jj.read([
18992
19083
  "evolog",
@@ -18994,31 +19085,54 @@ var ownDescriptions = (bookmark, changeId) => gen2(function* () {
18994
19085
  "-r",
18995
19086
  bookmarkRevset(bookmark),
18996
19087
  "-T",
18997
- String.raw`commit.change_id() ++ "\t" ++ commit.description().escape_json() ++ "\n"`
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"`
18998
19089
  ]);
18999
19090
  return lines2(output).flatMap((line) => {
19000
- const [change = "", json = '""'] = line.split("\t");
19001
- return change === changeId ? [parseDescription(json)] : [];
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
+ ] : [];
19002
19101
  });
19003
19102
  });
19004
- var syncDescription = ({ bookmark, changeId, description }, pullRequest, { ignoreClaudeCodeFooter, overwriteDescriptions, pushedDescription }) => gen2(function* () {
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* () {
19005
19114
  const git = yield* Git;
19006
19115
  const jj = yield* Jj;
19116
+ const records = yield* PrDescriptions;
19007
19117
  const describe = (text) => jj.run(["describe", "-r", bookmarkRevset(bookmark), "-m", text]);
19008
- const local = normalizedDescription(description, { ignoreClaudeCodeFooter });
19009
- const onGitHub = normalizedDescription(descriptionFrom(pullRequest), {
19010
- ignoreClaudeCodeFooter
19011
- });
19012
- const pushed = normalizedDescription(pushedDescription, { ignoreClaudeCodeFooter });
19013
- const isOwnEarlier = local !== onGitHub && onGitHub !== pushed && (yield* ownDescriptions(bookmark, changeId)).some((text) => normalizedDescription(text, { ignoreClaudeCodeFooter }) === onGitHub);
19014
- if (isOwnEarlier) {
19015
- return some2(local);
19016
- }
19017
- 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* () {
19018
19132
  if (overwriteDescriptions) {
19019
- return some2(local);
19133
+ return sent(local);
19020
19134
  }
19021
- const merged = yield* git.mergeText({ base: pushed, local, remote: onGitHub });
19135
+ const merged = yield* git.mergeText({ base: agreed, local, remote: onGitHub });
19022
19136
  if (merged.conflicted) {
19023
19137
  return yield* new DescriptionConflict({
19024
19138
  message: [
@@ -19032,7 +19146,7 @@ var syncDescription = ({ bookmark, changeId, description }, pullRequest, { ignor
19032
19146
  });
19033
19147
  }
19034
19148
  yield* describe(merged.text);
19035
- return some2(merged.text);
19149
+ return sent(merged.text);
19036
19150
  })), exhaustive2);
19037
19151
  });
19038
19152
 
@@ -19220,6 +19334,8 @@ var reconcile = ({
19220
19334
  }
19221
19335
  const onStacks = new Set(scope === "every stack" ? before.map(({ name }) => name) : scope.bookmarks);
19222
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));
19223
19339
  if (notCombined.length > 0) {
19224
19340
  yield* new NotCombined({
19225
19341
  message: [
@@ -19325,6 +19441,7 @@ var register = (remaining, pullRequests) => gen2(function* () {
19325
19441
  var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function* () {
19326
19442
  const github = yield* GitHub;
19327
19443
  const jj = yield* Jj;
19444
+ const records = yield* PrDescriptions;
19328
19445
  const bookmarks = stack.map(({ bookmark }) => bookmark);
19329
19446
  const bases = basesFor(bookmarks, trunk);
19330
19447
  const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
@@ -19332,19 +19449,25 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19332
19449
  const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
19333
19450
  const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
19334
19451
  const changes = [];
19335
- for (const entry of found) {
19336
- 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
+ };
19337
19460
  changes.push({
19338
- ...entry,
19339
- description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
19340
- ignoreClaudeCodeFooter,
19341
- overwriteDescriptions,
19342
- 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)
19343
19466
  })
19344
19467
  });
19345
19468
  }
19346
- const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
19347
- 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));
19348
19471
  const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
19349
19472
  const dissolve = mustDissolve(existing, wanted, moving.length > 0);
19350
19473
  if (dissolve && isSome2(existing)) {
@@ -19363,9 +19486,16 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19363
19486
  "push",
19364
19487
  ...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
19365
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
+ });
19366
19496
  const numbers = [];
19367
- for (const { base, change, description, pullRequest } of changes) {
19368
- if (isNone2(pullRequest)) {
19497
+ for (const { base, change, open } of changes) {
19498
+ if (isNone2(open)) {
19369
19499
  const created = yield* github.createPullRequest({
19370
19500
  ...titleAndBody(change.description),
19371
19501
  base,
@@ -19373,16 +19503,21 @@ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function*
19373
19503
  head: change.bookmark
19374
19504
  });
19375
19505
  yield* error(`opened #${created} for ${change.bookmark}`);
19506
+ yield* record(change.bookmark, change.description, created);
19376
19507
  numbers.push(created);
19377
19508
  } else {
19378
- const { number } = pullRequest.value;
19509
+ const {
19510
+ pullRequest: { number },
19511
+ synced: { send, text }
19512
+ } = open.value;
19379
19513
  if (moved.has(number) && base !== trunk) {
19380
19514
  yield* github.setBase(number, base);
19381
19515
  }
19382
- if (isSome2(description)) {
19383
- yield* github.setDescription(number, titleAndBody(description.value));
19516
+ if (send) {
19517
+ yield* github.setDescription(number, titleAndBody(text));
19384
19518
  yield* error(`updated the description of #${number}`);
19385
19519
  }
19520
+ yield* record(change.bookmark, text, number);
19386
19521
  numbers.push(number);
19387
19522
  }
19388
19523
  }
@@ -19493,7 +19628,7 @@ var submitCommand = make31("submit", {
19493
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")),
19494
19629
  publish: Boolean6("publish").pipe(withDefault5(false), withDescription2("Open new pull requests ready for review, instead of as drafts")),
19495
19630
  revision: revisionFlag("The top change to submit")
19496
- }, ({ 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. A local edit pushed with `jj git push` from this repository is sent too, unless the pull request was also edited on GitHub since the last submit. 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."));
19497
19632
 
19498
19633
  // src/cli/gh/command.ts
19499
19634
  var jjGhCommand = make31("jj gh").pipe(withSubcommands([
@@ -19506,6 +19641,6 @@ var jjGhCommand = make31("jj gh").pipe(withSubcommands([
19506
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`."));
19507
19642
 
19508
19643
  // src/main.ts
19509
- 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({
19510
19645
  builtIns: [Help, Version, Wizard, LogLevel]
19511
19646
  })), provide3([layer12, layer14])));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj-octo",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
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": {
@@ -37,8 +37,8 @@
37
37
  "@tsconfig/strictest": "2.0.8",
38
38
  "@types/bun": "1.4.3",
39
39
  "effect": "4.0.5",
40
- "oxfmt": "0.73.0",
41
- "oxlint": "1.88.0",
40
+ "oxfmt": "0.74.0",
41
+ "oxlint": "1.89.0",
42
42
  "oxlint-tsgolint": "7.0.2004",
43
43
  "typescript": "7.0.2"
44
44
  },