jjhub 0.1.14 → 0.1.15

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  GitHub CLI with Jujutsu-native superpowers: stable change IDs, stacks,
4
4
  platform-wide undo, and conflicts that wait — layered over plain GitHub
5
- repos by a [JJHub](https://jjhub.johnhenry.me) server.
5
+ repos by a [JJHub](https://jjhub.erisera.com) server.
6
6
 
7
7
  ## Install
8
8
 
@@ -27,8 +27,8 @@ jjhub help # everything else
27
27
  ```
28
28
 
29
29
  The CLI talks to the JJHub server at `JJHUB_URL` (default
30
- `http://localhost:3000`) — e.g. `JJHUB_URL=https://jjhub.johnhenry.me jjhub repo list`,
31
- or use `jjhub auth login --url https://jjhub.johnhenry.me` once and the saved
30
+ `http://localhost:3000`) — e.g. `JJHUB_URL=https://jjhub.erisera.com jjhub repo list`,
31
+ or use `jjhub auth login --url https://jjhub.erisera.com` once and the saved
32
32
  config takes over.
33
33
 
34
34
  This package is the standalone CLI build (single bundled file, no runtime
@@ -198,6 +198,12 @@ var init_api_client = __esm({
198
198
  input
199
199
  );
200
200
  }
201
+ listReviewerRoutingAudit(id, changeId2) {
202
+ return this.request(
203
+ "GET",
204
+ `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/reviewers/audit`
205
+ );
206
+ }
201
207
  requestReview(id, changeId2) {
202
208
  return this.request(
203
209
  "POST",
@@ -288,6 +294,12 @@ var init_api_client = __esm({
288
294
  `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/workflow-evidence`
289
295
  );
290
296
  }
297
+ rerunWorkflowRun(id, changeId2, runId) {
298
+ return this.request(
299
+ "POST",
300
+ `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/workflow-runs/${encodeURIComponent(runId)}/rerun`
301
+ );
302
+ }
291
303
  getDeploymentEvidence(id, changeId2) {
292
304
  return this.request(
293
305
  "GET",
@@ -306,6 +318,12 @@ var init_api_client = __esm({
306
318
  `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/issue-context`
307
319
  );
308
320
  }
321
+ getProjectV2Evidence(id, changeId2) {
322
+ return this.request(
323
+ "GET",
324
+ `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/project-v2-evidence`
325
+ );
326
+ }
309
327
  listChangeFiles(id, changeId2) {
310
328
  return this.request(
311
329
  "GET",
@@ -319,6 +337,12 @@ var init_api_client = __esm({
319
337
  input
320
338
  );
321
339
  }
340
+ recordContentOpReceipt(id, input) {
341
+ return this.request("POST", `/repositories/${encodeURIComponent(id)}/content-op-receipts`, input);
342
+ }
343
+ listContentOpReceipts(id) {
344
+ return this.request("GET", `/repositories/${encodeURIComponent(id)}/content-op-receipts`);
345
+ }
322
346
  listStacks(id) {
323
347
  return this.request("GET", `/repositories/${encodeURIComponent(id)}/stacks`);
324
348
  }
@@ -375,6 +399,18 @@ var init_api_client = __esm({
375
399
  setBookmark(id, input) {
376
400
  return this.request("POST", `/repositories/${encodeURIComponent(id)}/bookmarks`, input);
377
401
  }
402
+ listSavedViews(id) {
403
+ return this.request("GET", `/repositories/${encodeURIComponent(id)}/saved-views`);
404
+ }
405
+ createSavedView(id, input) {
406
+ return this.request("POST", `/repositories/${encodeURIComponent(id)}/saved-views`, input);
407
+ }
408
+ deleteSavedView(id, viewName) {
409
+ return this.request(
410
+ "DELETE",
411
+ `/repositories/${encodeURIComponent(id)}/saved-views/${encodeURIComponent(viewName)}`
412
+ );
413
+ }
378
414
  listConflicts(id) {
379
415
  return this.request("GET", `/repositories/${encodeURIComponent(id)}/conflicts`);
380
416
  }
@@ -419,6 +455,12 @@ var init_api_client = __esm({
419
455
  headOperation(id) {
420
456
  return this.request("GET", `/repositories/${encodeURIComponent(id)}/operations/head`);
421
457
  }
458
+ getOperationDiff(id, fromOpId, toOpId) {
459
+ return this.request(
460
+ "GET",
461
+ `/repositories/${encodeURIComponent(id)}/operations/diff/${encodeURIComponent(fromOpId)}/${encodeURIComponent(toOpId)}`
462
+ );
463
+ }
422
464
  undo(id, input) {
423
465
  return this.request("POST", `/repositories/${encodeURIComponent(id)}/undo`, input);
424
466
  }
@@ -60616,6 +60658,15 @@ var init_operations = __esm({
60616
60658
  description: "Remove explicitly requested GitHub users or teams from an in-review change.",
60617
60659
  returns: "ReviewHealth"
60618
60660
  }),
60661
+ op({
60662
+ name: "listReviewerRoutingAudit",
60663
+ method: "GET",
60664
+ pathParams: ["id", "changeId"],
60665
+ path: ([id, changeId2]) => `/repositories/${id}/changes/${changeId2}/reviewers/audit`,
60666
+ body: null,
60667
+ description: "List this change's reviewer-routing audit trail: every explicit GitHub reviewer request/remove that actually succeeded, who did it, and when. A failed GitHub API call never creates an entry.",
60668
+ returns: "ReviewerRoutingAuditEntry[]"
60669
+ }),
60619
60670
  op({
60620
60671
  name: "requestReview",
60621
60672
  method: "POST",
@@ -60722,8 +60773,13 @@ var init_operations = __esm({
60722
60773
  files: external_exports.array(
60723
60774
  external_exports.object({
60724
60775
  path: external_exports.string().describe("Repo-relative file path."),
60725
- content: external_exports.string().describe("File content \u2014 UTF-8 text, or base64 if encoding='base64'."),
60726
- encoding: external_exports.enum(["utf-8", "base64"]).optional().describe("Defaults to 'utf-8'.")
60776
+ content: external_exports.string().optional().describe(
60777
+ "Full file content \u2014 UTF-8 text, or base64 if encoding='base64'. Exactly one of content/diff must be set."
60778
+ ),
60779
+ diff: external_exports.string().optional().describe(
60780
+ "Unified diff to apply against the file's CURRENT content on this change's branch, as a smaller alternative to sending full content. Exactly one of content/diff must be set. Rejected with a clear error if the diff doesn't apply cleanly (stale base) or the path doesn't exist yet on this branch."
60781
+ ),
60782
+ encoding: external_exports.enum(["utf-8", "base64"]).optional().describe("Defaults to 'utf-8'. Only meaningful with content, not diff.")
60727
60783
  })
60728
60784
  ).optional().describe("Files to write. May be omitted/empty for a delete-only commit."),
60729
60785
  deletePaths: external_exports.array(external_exports.string()).optional().describe(
@@ -60760,6 +60816,15 @@ var init_operations = __esm({
60760
60816
  description: "Show bounded live GitHub Actions workflow-run and job evidence behind a change's projected head, with explicit merge-queue context.",
60761
60817
  returns: "WorkflowEvidence"
60762
60818
  }),
60819
+ op({
60820
+ name: "rerunWorkflowRun",
60821
+ method: "POST",
60822
+ pathParams: ["id", "changeId", "runId"],
60823
+ path: ([id, changeId2, runId]) => `/repositories/${id}/changes/${changeId2}/workflow-runs/${runId}/rerun`,
60824
+ body: null,
60825
+ description: "Ask GitHub to re-run a workflow run behind a change's projected head. The run id must already appear in that change's bounded workflow evidence \u2014 an unrelated or unknown run id is rejected before any GitHub call.",
60826
+ returns: "WorkflowRerunResult"
60827
+ }),
60763
60828
  op({
60764
60829
  name: "getDeploymentEvidence",
60765
60830
  method: "GET",
@@ -60787,6 +60852,15 @@ var init_operations = __esm({
60787
60852
  description: "Show bounded live GitHub issue context referenced by a change's title and description, including closing and blocking intent.",
60788
60853
  returns: "ChangeIssueContext"
60789
60854
  }),
60855
+ op({
60856
+ name: "getProjectV2Evidence",
60857
+ method: "GET",
60858
+ pathParams: ["id", "changeId"],
60859
+ path: ([id, changeId2]) => `/repositories/${id}/changes/${changeId2}/project-v2-evidence`,
60860
+ body: null,
60861
+ description: "Show bounded, read-only GitHub Projects v2 membership and field values for a change's linked issues \u2014 which project(s) each is on and its status/iteration/other field values there. Never mutates a project item.",
60862
+ returns: "ProjectV2Evidence"
60863
+ }),
60790
60864
  op({
60791
60865
  name: "listChangeFiles",
60792
60866
  method: "GET",
@@ -60810,6 +60884,34 @@ var init_operations = __esm({
60810
60884
  description: "Permanently redact a change's title and description everywhere JJHub stores them. Requires repo owner access, an erasure allowlist entry, and a real user session; the legacy API key is rejected. Records audit metadata, never the erased value.",
60811
60885
  returns: "{ erasures: Erasure[]; rowsTouched: number }"
60812
60886
  }),
60887
+ op({
60888
+ name: "recordContentOpReceipt",
60889
+ method: "POST",
60890
+ pathParams: ["id"],
60891
+ path: ([id]) => `/repositories/${id}/content-op-receipts`,
60892
+ body: external_exports.object({
60893
+ kind: external_exports.enum(["split", "absorb", "squash", "duplicate", "restore"]).describe("Which local jj content-curation operation this receipt covers."),
60894
+ sourceRevision: external_exports.string().min(1).describe("The jj change id the operation was run against."),
60895
+ destinationRevision: external_exports.string().optional().describe("The jj change id passed as --destination, if any."),
60896
+ paths: external_exports.array(external_exports.string()).optional().describe("Path selection from the invocation; omitted = entire revision."),
60897
+ expectedOperationId: external_exports.string().min(1).describe("The --expected-operation id supplied for this apply, from a prior preview."),
60898
+ resultOperationId: external_exports.string().optional().describe("The jj operation id produced by a successful apply; omitted when the apply was rejected."),
60899
+ changedChangeIds: external_exports.array(external_exports.string()).optional().describe("jj change ids the apply actually touched; omitted/empty when the apply was rejected."),
60900
+ succeeded: external_exports.boolean().describe("Whether the local jj mutation actually applied."),
60901
+ errorMessage: external_exports.string().optional().describe("Failure detail (e.g. a stale --expected-operation) when succeeded is false.")
60902
+ }),
60903
+ description: "Record one `jjhub content apply` invocation's outcome for the durable content-op receipt trail (issue #276) \u2014 called by the CLI right after its local jj mutation succeeds or is rejected. The local jj mutation itself already happened by this point; this only records what happened.",
60904
+ returns: "ContentOpReceipt"
60905
+ }),
60906
+ op({
60907
+ name: "listContentOpReceipts",
60908
+ method: "GET",
60909
+ pathParams: ["id"],
60910
+ path: ([id]) => `/repositories/${id}/content-op-receipts`,
60911
+ body: null,
60912
+ description: "List this repository's content-op receipt trail: every `jjhub content apply` invocation, successful or rejected.",
60913
+ returns: "ContentOpReceipt[]"
60914
+ }),
60813
60915
  // --- stacks ---
60814
60916
  op({
60815
60917
  name: "listStacks",
@@ -60923,6 +61025,37 @@ var init_operations = __esm({
60923
61025
  returns: "Bookmark"
60924
61026
  }),
60925
61027
  // deleteBookmark: excluded — see the module doc comment ("force" is a query param).
61028
+ // --- saved views (issue #274) ---
61029
+ op({
61030
+ name: "listSavedViews",
61031
+ method: "GET",
61032
+ pathParams: ["id"],
61033
+ path: ([id]) => `/repositories/${id}/saved-views`,
61034
+ body: null,
61035
+ description: "List saved revset views: named, reusable jj revset expressions stored per repository. Storage only \u2014 evaluate one locally with `jjhub revset <localRepoPath> <expression>`.",
61036
+ returns: "SavedView[]"
61037
+ }),
61038
+ op({
61039
+ name: "createSavedView",
61040
+ method: "POST",
61041
+ pathParams: ["id"],
61042
+ path: ([id]) => `/repositories/${id}/saved-views`,
61043
+ body: external_exports.object({
61044
+ name: external_exports.string().min(1).describe("Saved view name, e.g. 'my-open-changes'."),
61045
+ revsetExpression: external_exports.string().min(1).describe("The jj revset expression this view saves, e.g. 'mine() & open()'.")
61046
+ }),
61047
+ description: "Save a named jj revset expression for later reuse. Storage only \u2014 JJHub has no local jj workspace to evaluate it against; run `jjhub revset <localRepoPath> <expression>` to actually evaluate the expression.",
61048
+ returns: "SavedView"
61049
+ }),
61050
+ op({
61051
+ name: "deleteSavedView",
61052
+ method: "DELETE",
61053
+ pathParams: ["id", "viewName"],
61054
+ path: ([id, viewName]) => `/repositories/${id}/saved-views/${viewName}`,
61055
+ body: null,
61056
+ description: "Delete a saved revset view by name. The dual of createSavedView.",
61057
+ returns: "void"
61058
+ }),
60926
61059
  // --- conflicts ---
60927
61060
  op({
60928
61061
  name: "listConflicts",
@@ -61006,6 +61139,15 @@ var init_operations = __esm({
61006
61139
  returns: "Operation | null",
61007
61140
  returnsCli: "OperationSummary | null"
61008
61141
  }),
61142
+ op({
61143
+ name: "getOperationDiff",
61144
+ method: "GET",
61145
+ pathParams: ["id", "fromOpId", "toOpId"],
61146
+ path: ([id, fromOpId, toOpId]) => `/repositories/${id}/operations/diff/${fromOpId}/${toOpId}`,
61147
+ body: null,
61148
+ description: "Structural diff between two operations' immutable snapshots: which changes/stacks/bookmarks/conflicts were created, modified, or removed going from fromOpId to toOpId. Works for adjacent or far-apart operations; never replays the operations in between.",
61149
+ returns: "OperationDiff"
61150
+ }),
61009
61151
  op({
61010
61152
  name: "undo",
61011
61153
  method: "POST",
@@ -61152,7 +61294,7 @@ var init_package = __esm({
61152
61294
  "package.json"() {
61153
61295
  package_default = {
61154
61296
  name: "jjhub",
61155
- version: "0.1.14",
61297
+ version: "0.1.15",
61156
61298
  private: true,
61157
61299
  description: "JJHub \u2014 a Jujutsu-native overlay atop GitHub: stable change IDs, stacked PRs, platform-wide undo, and an MCP server for coding agents.",
61158
61300
  type: "module",
@@ -61185,6 +61327,7 @@ var init_package = __esm({
61185
61327
  "ai.matey.backend": "^0.8.2",
61186
61328
  "ai.matey.core": "^0.3.4",
61187
61329
  "ai.matey.types": "^0.5.1",
61330
+ diff: "^7.0.0",
61188
61331
  fastify: "^5.2.0",
61189
61332
  "isomorphic-git": "^1.40.0",
61190
61333
  "isomorphic-jj": "^1.7.0",
@@ -61195,6 +61338,7 @@ var init_package = __esm({
61195
61338
  },
61196
61339
  devDependencies: {
61197
61340
  "@biomejs/biome": "^2.5.6",
61341
+ "@types/diff": "^7.0.2",
61198
61342
  "@types/node": "^26.1.2",
61199
61343
  "@types/nodemailer": "^8.0.1",
61200
61344
  "@types/web-push": "^3.6.4",
@@ -61208,7 +61352,7 @@ var init_package = __esm({
61208
61352
  },
61209
61353
  repository: {
61210
61354
  type: "git",
61211
- url: "git+https://github.com/johnhenry/jjhub.git"
61355
+ url: "git+https://github.com/erisera-code/jjhub.git"
61212
61356
  },
61213
61357
  keywords: [
61214
61358
  "jujutsu",
@@ -61810,7 +61954,11 @@ var init_server3 = __esm({
61810
61954
  stackId,
61811
61955
  conflictId: external_exports.string().describe("Conflict id, e.g. from list_conflicts."),
61812
61956
  name: external_exports.string().describe("Bookmark name."),
61813
- confirmationId: external_exports.string().describe("Pending confirmation id, e.g. from list_pending_confirmations.")
61957
+ viewName: external_exports.string().describe("Saved view name, e.g. from list_saved_views."),
61958
+ confirmationId: external_exports.string().describe("Pending confirmation id, e.g. from list_pending_confirmations."),
61959
+ fromOpId: external_exports.string().describe("Earlier operation id to diff from, e.g. 'OP-0050' \u2014 see list_operations."),
61960
+ toOpId: external_exports.string().describe("Later operation id to diff to, e.g. 'OP-0093' \u2014 see list_operations."),
61961
+ runId: external_exports.string().describe("GitHub Actions workflow run id, e.g. from get_workflow_evidence.")
61814
61962
  };
61815
61963
  HANDWRITTEN_TOOL_NAMES = [
61816
61964
  "whoami",
@@ -119258,6 +119406,51 @@ function sshKeyFingerprint(authorizedKeysLine) {
119258
119406
  return `SHA256:${digest.replace(/=+$/, "")}`;
119259
119407
  }
119260
119408
 
119409
+ // src/jj/route-selection.ts
119410
+ var RemoteRouteError = class extends Error {
119411
+ candidates;
119412
+ constructor(message, candidates) {
119413
+ super(message);
119414
+ this.name = "RemoteRouteError";
119415
+ this.candidates = candidates;
119416
+ }
119417
+ };
119418
+ function selectPushRoute(topology, overrideRemoteName) {
119419
+ const candidateNames = topology.remotes.map((remote) => remote.name);
119420
+ if (overrideRemoteName) {
119421
+ const match = topology.remotes.find((remote) => remote.name === overrideRemoteName);
119422
+ if (!match) {
119423
+ throw new RemoteRouteError(
119424
+ candidateNames.length > 0 ? `--remote "${overrideRemoteName}" is not among this workspace's remotes (${candidateNames.join(", ")})` : `--remote "${overrideRemoteName}" was given, but this workspace has no Git remotes at all`,
119425
+ candidateNames
119426
+ );
119427
+ }
119428
+ return { remote: match, reason: "override" };
119429
+ }
119430
+ if (topology.remotes.length === 0) {
119431
+ throw new RemoteRouteError(
119432
+ "no Git remotes were observed in this workspace; configure one or pass --remote",
119433
+ []
119434
+ );
119435
+ }
119436
+ if (topology.remotes.length === 1) {
119437
+ return { remote: topology.remotes[0], reason: "single" };
119438
+ }
119439
+ if (topology.trunkChangeId) {
119440
+ const trunkTrackingBookmark = topology.bookmarks.find(
119441
+ (bookmark) => bookmark.remote !== null && bookmark.tracked && bookmark.targetChangeId === topology.trunkChangeId
119442
+ );
119443
+ const trunkRemote = trunkTrackingBookmark && topology.remotes.find((remote) => remote.name === trunkTrackingBookmark.remote);
119444
+ if (trunkRemote) return { remote: trunkRemote, reason: "trunk-tracking" };
119445
+ }
119446
+ const origin = topology.remotes.find((remote) => remote.name === "origin");
119447
+ if (origin) return { remote: origin, reason: "origin-default" };
119448
+ throw new RemoteRouteError(
119449
+ `multiple remotes are configured (${candidateNames.join(", ")}) and none is unambiguous (no remote tracks trunk(), and none is named "origin"); pass --remote <name> to choose one`,
119450
+ candidateNames
119451
+ );
119452
+ }
119453
+
119261
119454
  // src/service/confirmation-signature.ts
119262
119455
  function confirmationSignatureMessage(payload) {
119263
119456
  return Buffer.from(
@@ -119369,7 +119562,7 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
119369
119562
  };
119370
119563
  const log = opts.log ?? (() => {
119371
119564
  });
119372
- await reportWorkspaceTopology(jj, api, repoId, opts, log);
119565
+ const topologyObservation = await reportWorkspaceTopology(jj, api, repoId, opts, log);
119373
119566
  let entries = await jj.snapshotLog();
119374
119567
  const serverChanges = await api.listChanges(repoId);
119375
119568
  let defaultBookmark = "main";
@@ -119395,6 +119588,17 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
119395
119588
  log("[daemon] commit push skipped: no local repo path");
119396
119589
  return pusher;
119397
119590
  }
119591
+ if (topologyObservation) {
119592
+ try {
119593
+ selectPushRoute(topologyObservation.topology, opts.remote);
119594
+ } catch (err2) {
119595
+ if (err2 instanceof RemoteRouteError) {
119596
+ log(`[daemon] commit push skipped: ${err2.message}`);
119597
+ return pusher;
119598
+ }
119599
+ throw err2;
119600
+ }
119601
+ }
119398
119602
  if (!GITHUB_FULLNAME_RE.test(repo.fullName)) {
119399
119603
  log(
119400
119604
  `[daemon] commit push skipped: repo fullName "${repo.fullName}" does not look like a GitHub repo`
@@ -119701,9 +119905,17 @@ async function signPendingConfirmationsGpg(api, repoId, signer, githubKeyFetcher
119701
119905
  }
119702
119906
  }
119703
119907
  async function reportWorkspaceTopology(jj, api, repoId, opts, log) {
119704
- if (!jj.observeWorkspaceTopology) return;
119908
+ if (!jj.observeWorkspaceTopology) return null;
119909
+ let observation;
119910
+ try {
119911
+ observation = await jj.observeWorkspaceTopology();
119912
+ } catch (err2) {
119913
+ log(
119914
+ `[daemon] workspace topology observation failed (continuing): ${err2 instanceof Error ? err2.message : String(err2)}`
119915
+ );
119916
+ return null;
119917
+ }
119705
119918
  try {
119706
- const observation = await jj.observeWorkspaceTopology();
119707
119919
  const capabilities = ["snapshot", "topology"];
119708
119920
  if (jj.queryRevset) capabilities.push("revset");
119709
119921
  if (jj.previewContentOperation && jj.executeContentOperation) capabilities.push("content-operations");
@@ -119727,6 +119939,7 @@ async function reportWorkspaceTopology(jj, api, repoId, opts, log) {
119727
119939
  );
119728
119940
  }
119729
119941
  }
119942
+ return observation;
119730
119943
  }
119731
119944
  async function sendHeartbeat(api, repoId, opts, ok, message, log) {
119732
119945
  if (opts.reportHeartbeat === false) return;
@@ -119759,6 +119972,7 @@ async function runDaemonLoop(jj, api, repoId, opts = {}) {
119759
119972
  pushCommit: opts.pushCommit,
119760
119973
  repoPath: opts.repoPath,
119761
119974
  twoWay: opts.twoWay,
119975
+ remote: opts.remote,
119762
119976
  knownMapping,
119763
119977
  log
119764
119978
  });
@@ -119888,7 +120102,8 @@ function spawnDaemon(opts) {
119888
120102
  String(opts.intervalSeconds ?? 5),
119889
120103
  ...opts.engine ? ["--engine", opts.engine] : [],
119890
120104
  ...opts.baseUrl ? ["--url", opts.baseUrl] : [],
119891
- ...opts.twoWay ? ["--two-way"] : []
120105
+ ...opts.twoWay ? ["--two-way"] : [],
120106
+ ...opts.remote ? ["--remote", opts.remote] : []
119892
120107
  ];
119893
120108
  const spawnFn = opts.spawnFn ?? defaultSpawnFn;
119894
120109
  const child = spawnFn(process.execPath, args, { detached: true, stdio: "ignore", env: process.env });
@@ -119927,6 +120142,8 @@ var JJH_NOUNS = /* @__PURE__ */ new Set([
119927
120142
  "change",
119928
120143
  "stack",
119929
120144
  "bookmark",
120145
+ "workspace",
120146
+ "view",
119930
120147
  "op",
119931
120148
  "undo",
119932
120149
  "redo",
@@ -119990,6 +120207,8 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
119990
120207
  change squash <changeId>
119991
120208
  change abandon <changeId>
119992
120209
  change restack <changeId> rebase a needs-restack change onto its rewritten ancestor
120210
+ change request-review <changeId> move a draft change into review and mark its PR ready for review
120211
+ change return-to-draft <changeId> return an in-review change and its PR to draft
119993
120212
  change land <changeId> [--force] --force bypasses a failing/pending check-run gate
119994
120213
  change commit <changeId> [<file...>] [-m msg] [--delete <path>]...
119995
120214
  commit working-tree files onto the change's branch;
@@ -119998,10 +120217,17 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
119998
120217
  change split <changeId> <newTitle>
119999
120218
  change checks <changeId> list GitHub check runs for a change
120000
120219
  change actions <changeId> show bounded GitHub Actions workflow and job evidence
120220
+ change actions rerun <changeId> <runId>
120221
+ ask GitHub to re-run a workflow run shown by
120222
+ "change actions <changeId>" (issue #279); a run id not
120223
+ associated with the change is rejected before any GitHub call
120001
120224
  change deployments <changeId> show bounded GitHub deployment evidence
120002
120225
  change security <changeId> show bounded GitHub code-scanning evidence
120003
120226
  change issues <changeId> show linked GitHub issue context for a change
120227
+ change projects <changeId> show bounded GitHub Projects v2 evidence for linked issues
120004
120228
  change files <changeId> list locally-stored files (contentMode "local" repos only)
120229
+ change reviewers <changeId> reviewer-routing audit trail: every request/remove that
120230
+ actually succeeded against GitHub, who did it, and when
120005
120231
 
120006
120232
  stack create <title> [--id x] [--changes CHG-1,CHG-2]
120007
120233
  stack list
@@ -120015,12 +120241,24 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
120015
120241
  bookmark set <name> <tracked|view> [--target CHG-x]
120016
120242
  bookmark delete <name> [--force] --force required for the default (trunk) bookmark
120017
120243
 
120244
+ workspace list live local jj workspace agents reporting into this
120245
+ repo (see "jjhub daemon"), with their last-seen
120246
+ topology observation
120247
+
120248
+ view save <name> <revsetExpression> save a named jj revset expression for later reuse (storage
120249
+ only \u2014 evaluate it with "jjhub revset")
120250
+ view list list saved views in this repo
120251
+ view delete <name>
120252
+
120018
120253
  op log
120254
+ op diff <fromOpId> <toOpId> structural diff between two operations' immutable snapshots
120255
+ (which changes/stacks/bookmarks/conflicts were created,
120256
+ modified, or removed); works for adjacent or far-apart ops
120019
120257
  undo [--force] --force allows undoing a "land" op (can't unmerge the real PR)
120020
120258
  redo
120021
120259
 
120022
120260
  mcp serve MCP server over stdio for agent clients (Claude Code, Cursor, ...)
120023
- mcp serve --http <port> same 77 tools over Streamable HTTP (POST /); gated by the
120261
+ mcp serve --http <port> same 86 tools over Streamable HTTP (POST /); gated by the
120024
120262
  configured credential as a bearer token when one is set.
120025
120263
  For public exposure prefer the API server's own /mcp
120026
120264
  endpoint, which speaks real OAuth (issue #48)
@@ -120048,11 +120286,16 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
120048
120286
  auth logout forget the saved connection/token
120049
120287
  auth status who the saved credential authenticates as
120050
120288
 
120051
- daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--two-way]
120289
+ daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--two-way] [--remote name]
120052
120290
  sync a local jj working copy into JJHub (inbound direction), foreground
120053
120291
  --two-way also reflects JJHub-side edits back into the local jj repo
120054
120292
  (opt-in, subprocess engine only \u2014 see docs/ROADMAP-JJH-PARITY.md Phase 3)
120055
- daemon start [alias|localRepoPath] [--interval 5] [--engine ...] [--two-way]
120293
+ --remote picks which local jj remote real-commit push targets when the
120294
+ workspace has more than one configured (subprocess engine only); without
120295
+ it, one remote is used as-is, a trunk-tracking remote or "origin" is
120296
+ preferred among several, and a still-ambiguous set of remotes skips the
120297
+ push with a clear error rather than guessing
120298
+ daemon start [alias|localRepoPath] [--interval 5] [--engine ...] [--two-way] [--remote name]
120056
120299
  spawn the same sync as a detached background process
120057
120300
  daemon stop [alias]
120058
120301
  daemon status [alias]
@@ -120071,6 +120314,10 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
120071
120314
  content apply <localRepoPath> <split|absorb|squash|duplicate|restore>
120072
120315
  same selection plus --expected-operation <jj-operation-id> from preview
120073
120316
  local jj content curation with immutable-boundary enforcement; file selections only
120317
+ records a durable receipt on the JJHub server (best-effort) \u2014 see content receipts
120318
+ content receipts [--repo owner/name|repoId]
120319
+ list this repo's content-op receipt trail: every content apply invocation,
120320
+ successful or rejected
120074
120321
 
120075
120322
  Anything else (pr, issue, ...) is passed through to gh. (jjhub's own "auth" command
120076
120323
  signs into the JJHub server \u2014 it uses your GitHub identity via the server's
@@ -120132,6 +120379,7 @@ async function runCli(argv, opts = {}) {
120132
120379
  repos: { type: "string" },
120133
120380
  message: { type: "string", short: "m" },
120134
120381
  "two-way": { type: "boolean", default: false },
120382
+ remote: { type: "string" },
120135
120383
  members: { type: "string" },
120136
120384
  "github-token": { type: "string" },
120137
120385
  force: { type: "boolean", default: false },
@@ -120190,6 +120438,10 @@ async function dispatch(ctx, pos, values) {
120190
120438
  return undoRedoCmd(ctx, group, values);
120191
120439
  case "bookmark":
120192
120440
  return bookmarkCmd(ctx, pos.slice(1), values);
120441
+ case "workspace":
120442
+ return workspaceCmd(ctx, pos.slice(1));
120443
+ case "view":
120444
+ return viewCmd(ctx, pos.slice(1));
120193
120445
  case "conflict":
120194
120446
  return conflictCmd(ctx, pos.slice(1));
120195
120447
  case "sync":
@@ -120351,7 +120603,7 @@ async function changeCmd(ctx, pos, values) {
120351
120603
  if (!sub) {
120352
120604
  return usageError(
120353
120605
  ctx,
120354
- "change <new|list|show|describe|amend|squash|abandon|restack|land|commit|diff|split|checks|files> ..."
120606
+ "change <new|list|show|describe|amend|squash|abandon|restack|request-review|return-to-draft|land|commit|diff|split|checks|files|reviewers> ..."
120355
120607
  );
120356
120608
  }
120357
120609
  switch (sub) {
@@ -120434,6 +120686,22 @@ async function changeCmd(ctx, pos, values) {
120434
120686
  ctx.out(`restacked ${change.changeId} (commit ${change.currentCommitSha})`);
120435
120687
  return 0;
120436
120688
  }
120689
+ case "request-review": {
120690
+ const changeId2 = rest[0];
120691
+ if (!changeId2) return usageError(ctx, "change request-review <changeId>");
120692
+ const change = await ctx.api.requestReview(await ctx.repoId(), changeId2);
120693
+ if (ctx.json) return emitJson(ctx, change);
120694
+ ctx.out(`${change.changeId} now in review`);
120695
+ return 0;
120696
+ }
120697
+ case "return-to-draft": {
120698
+ const changeId2 = rest[0];
120699
+ if (!changeId2) return usageError(ctx, "change return-to-draft <changeId>");
120700
+ const change = await ctx.api.returnToDraft(await ctx.repoId(), changeId2);
120701
+ if (ctx.json) return emitJson(ctx, change);
120702
+ ctx.out(`${change.changeId} returned to draft`);
120703
+ return 0;
120704
+ }
120437
120705
  case "land": {
120438
120706
  const changeId2 = rest[0];
120439
120707
  if (!changeId2) return usageError(ctx, "change land <changeId> [--force]");
@@ -120515,6 +120783,14 @@ async function changeCmd(ctx, pos, values) {
120515
120783
  return 0;
120516
120784
  }
120517
120785
  case "actions": {
120786
+ if (rest[0] === "rerun") {
120787
+ const [, changeId3, runId] = rest;
120788
+ if (!changeId3 || !runId) return usageError(ctx, "change actions rerun <changeId> <runId>");
120789
+ const result = await ctx.api.rerunWorkflowRun(await ctx.repoId(), changeId3, runId);
120790
+ if (ctx.json) return emitJson(ctx, result);
120791
+ ctx.out(`requested rerun of workflow run ${result.runId} (${result.runName}) for ${changeId3}`);
120792
+ return 0;
120793
+ }
120518
120794
  const changeId2 = rest[0];
120519
120795
  if (!changeId2) return usageError(ctx, "change actions <changeId>");
120520
120796
  const evidence = await ctx.api.getWorkflowEvidence(await ctx.repoId(), changeId2);
@@ -120606,6 +120882,24 @@ async function changeCmd(ctx, pos, values) {
120606
120882
  );
120607
120883
  return 0;
120608
120884
  }
120885
+ case "projects": {
120886
+ const changeId2 = rest[0];
120887
+ if (!changeId2) return usageError(ctx, "change projects <changeId>");
120888
+ const evidence = await ctx.api.getProjectV2Evidence(await ctx.repoId(), changeId2);
120889
+ if (ctx.json) return emitJson(ctx, evidence);
120890
+ ctx.out(evidence.detail);
120891
+ const rows = evidence.issues.flatMap(
120892
+ (issue2) => issue2.items.length > 0 ? issue2.items.map((item) => [
120893
+ `${issue2.repositoryFullName}#${issue2.number}`,
120894
+ `${item.projectTitle} (#${item.projectNumber})`,
120895
+ item.fieldValues.map((f) => `${f.fieldName}: ${f.value}`).join(", ") || "-",
120896
+ item.isArchived ? "yes" : "no"
120897
+ ]) : []
120898
+ );
120899
+ if (rows.length === 0) return 0;
120900
+ ctx.out(table([["ISSUE", "PROJECT", "FIELDS", "ARCHIVED"], ...rows]));
120901
+ return 0;
120902
+ }
120609
120903
  case "files": {
120610
120904
  const changeId2 = rest[0];
120611
120905
  if (!changeId2) return usageError(ctx, "change files <changeId>");
@@ -120623,6 +120917,29 @@ async function changeCmd(ctx, pos, values) {
120623
120917
  );
120624
120918
  return 0;
120625
120919
  }
120920
+ case "reviewers": {
120921
+ const changeId2 = rest[0];
120922
+ if (!changeId2) return usageError(ctx, "change reviewers <changeId>");
120923
+ const entries = await ctx.api.listReviewerRoutingAudit(await ctx.repoId(), changeId2);
120924
+ if (ctx.json) return emitJson(ctx, entries);
120925
+ if (entries.length === 0) {
120926
+ ctx.out("no reviewer-routing audit entries");
120927
+ return 0;
120928
+ }
120929
+ ctx.out(
120930
+ table([
120931
+ ["ACTION", "REVIEWERS", "TEAMS", "BY", "AT"],
120932
+ ...entries.map((e) => [
120933
+ e.action,
120934
+ e.reviewers.join(",") || "-",
120935
+ e.teamReviewers.join(",") || "-",
120936
+ e.performedBy ?? "-",
120937
+ e.performedAt
120938
+ ])
120939
+ ])
120940
+ );
120941
+ return 0;
120942
+ }
120626
120943
  default:
120627
120944
  return usageError(ctx, `unknown change subcommand "${sub}"`);
120628
120945
  }
@@ -120793,9 +121110,77 @@ async function bookmarkCmd(ctx, pos, values) {
120793
121110
  }
120794
121111
  return usageError(ctx, `unknown bookmark subcommand "${sub}"`);
120795
121112
  }
120796
- async function opCmd(ctx, pos) {
121113
+ async function workspaceCmd(ctx, pos) {
120797
121114
  const [sub] = pos;
120798
- if (sub !== "log") return usageError(ctx, "op log");
121115
+ if (sub === "list") {
121116
+ const agents = await ctx.api.listWorkspaceAgents(await ctx.repoId());
121117
+ if (ctx.json) return emitJson(ctx, agents);
121118
+ if (agents.length === 0) {
121119
+ ctx.out("no workspace agents (they appear once a local `jjhub daemon` reports in)");
121120
+ return 0;
121121
+ }
121122
+ ctx.out(
121123
+ table([
121124
+ ["WORKSPACE", "LOCAL REPO", "PATH", "WORKING COPY", "JJ VERSION", "OK", "LAST SEEN"],
121125
+ ...agents.map(workspaceAgentRow)
121126
+ ])
121127
+ );
121128
+ return 0;
121129
+ }
121130
+ return usageError(ctx, "workspace list");
121131
+ }
121132
+ function workspaceAgentRow(a) {
121133
+ return [
121134
+ a.workspaceName,
121135
+ a.localRepositoryId,
121136
+ a.localPath ?? "-",
121137
+ a.workingCopyChangeId ?? "-",
121138
+ a.jjVersion ?? "-",
121139
+ a.ok ? "yes" : "no",
121140
+ a.lastSeenAt
121141
+ ];
121142
+ }
121143
+ async function viewCmd(ctx, pos) {
121144
+ const [sub, ...rest] = pos;
121145
+ if (!sub) return usageError(ctx, "view <save|list|delete> ...");
121146
+ if (sub === "list") {
121147
+ const views = await ctx.api.listSavedViews(await ctx.repoId());
121148
+ if (ctx.json) return emitJson(ctx, views);
121149
+ if (views.length === 0) {
121150
+ ctx.out("no saved views");
121151
+ return 0;
121152
+ }
121153
+ ctx.out(
121154
+ table([
121155
+ ["NAME", "REVSET", "CREATED BY"],
121156
+ ...views.map((v) => [v.name, v.revsetExpression, v.createdBy ?? "-"])
121157
+ ])
121158
+ );
121159
+ return 0;
121160
+ }
121161
+ if (sub === "save") {
121162
+ const [name, revsetExpression, ...extra] = rest;
121163
+ if (!name || !revsetExpression || extra.length > 0) {
121164
+ return usageError(ctx, "view save <name> <revsetExpression>");
121165
+ }
121166
+ const view = await ctx.api.createSavedView(await ctx.repoId(), { name, revsetExpression });
121167
+ if (ctx.json) return emitJson(ctx, view);
121168
+ ctx.out(`saved view ${view.name}: ${view.revsetExpression}`);
121169
+ return 0;
121170
+ }
121171
+ if (sub === "delete") {
121172
+ const [name] = rest;
121173
+ if (!name) return usageError(ctx, "view delete <name>");
121174
+ await ctx.api.deleteSavedView(await ctx.repoId(), name);
121175
+ ctx.out(`deleted view ${name}`);
121176
+ return 0;
121177
+ }
121178
+ return usageError(ctx, `unknown view subcommand "${sub}"`);
121179
+ }
121180
+ async function opCmd(ctx, pos) {
121181
+ const [sub, ...rest] = pos;
121182
+ if (sub === "diff") return opDiffCmd(ctx, rest);
121183
+ if (sub !== "log") return usageError(ctx, "op log | op diff <fromOpId> <toOpId>");
120799
121184
  const repoId = await ctx.repoId();
120800
121185
  const [ops, head] = await Promise.all([ctx.api.listOperations(repoId), ctx.api.headOperation(repoId)]);
120801
121186
  if (ctx.json) return emitJson(ctx, ops);
@@ -120807,6 +121192,28 @@ async function opCmd(ctx, pos) {
120807
121192
  ctx.out(table([[" ", "OP", "TYPE", "SUMMARY", "AT"], ...rows]));
120808
121193
  return 0;
120809
121194
  }
121195
+ function formatDiffBucket(label, bucket) {
121196
+ const parts = [];
121197
+ if (bucket.created.length > 0) parts.push(`created: ${bucket.created.join(", ")}`);
121198
+ if (bucket.modified.length > 0) parts.push(`modified: ${bucket.modified.join(", ")}`);
121199
+ if (bucket.removed.length > 0) parts.push(`removed: ${bucket.removed.join(", ")}`);
121200
+ return parts.length === 0 ? `${label}: no changes` : `${label}:
121201
+ ${parts.join("\n ")}`;
121202
+ }
121203
+ async function opDiffCmd(ctx, pos) {
121204
+ const [fromOpId, toOpId] = pos;
121205
+ if (!fromOpId || !toOpId) return usageError(ctx, "op diff <fromOpId> <toOpId>");
121206
+ const repoId = await ctx.repoId();
121207
+ const diff = await ctx.api.getOperationDiff(repoId, fromOpId, toOpId);
121208
+ if (ctx.json) return emitJson(ctx, diff);
121209
+ ctx.out(`${diff.fromOpId} (seq ${diff.fromSequence}) -> ${diff.toOpId} (seq ${diff.toSequence})`);
121210
+ ctx.out("");
121211
+ ctx.out(formatDiffBucket("changes", diff.changes));
121212
+ ctx.out(formatDiffBucket("stacks", diff.stacks));
121213
+ ctx.out(formatDiffBucket("bookmarks", diff.bookmarks));
121214
+ ctx.out(formatDiffBucket("conflicts", diff.conflicts));
121215
+ return 0;
121216
+ }
120810
121217
  async function undoRedoCmd(ctx, verb, values) {
120811
121218
  const repoId = await ctx.repoId();
120812
121219
  const op2 = verb === "undo" ? await ctx.api.undo(repoId, { force: values.force }) : await ctx.api.redo(repoId);
@@ -121095,12 +121502,46 @@ var CONTENT_OPERATION_KINDS = [
121095
121502
  "duplicate",
121096
121503
  "restore"
121097
121504
  ];
121505
+ async function recordContentOpReceiptBestEffort(ctx, input) {
121506
+ try {
121507
+ const repoId = await ctx.repoId();
121508
+ await ctx.api.recordContentOpReceipt(repoId, input);
121509
+ } catch (e) {
121510
+ ctx.err(
121511
+ `warning: could not record a JJHub content-op receipt for this apply (continuing): ${e instanceof Error ? e.message : String(e)}`
121512
+ );
121513
+ }
121514
+ }
121515
+ async function contentReceiptsCmd(ctx, pos) {
121516
+ if (pos.length > 0) return usageError(ctx, "content receipts [--repo owner/name|repoId]");
121517
+ const repoId = await ctx.repoId();
121518
+ const receipts = await ctx.api.listContentOpReceipts(repoId);
121519
+ if (ctx.json) return emitJson(ctx, receipts);
121520
+ ctx.out(`${receipts.length} content-op receipt${receipts.length === 1 ? "" : "s"}`);
121521
+ if (receipts.length > 0) {
121522
+ ctx.out(
121523
+ table([
121524
+ ["WHEN", "KIND", "SOURCE", "OK", "RESULT / ERROR", "EXPECTED-OP"],
121525
+ ...receipts.map((r) => [
121526
+ r.requestedAt,
121527
+ r.kind,
121528
+ r.sourceRevision,
121529
+ r.succeeded ? "yes" : "no",
121530
+ r.succeeded ? r.resultOperationId ?? "" : r.errorMessage ?? "failed",
121531
+ r.expectedOperationId
121532
+ ])
121533
+ ])
121534
+ );
121535
+ }
121536
+ return 0;
121537
+ }
121098
121538
  async function contentCmd(ctx, pos, values) {
121539
+ if (pos[0] === "receipts") return contentReceiptsCmd(ctx, pos.slice(1));
121099
121540
  const [mode, localRepoPath, kindValue, ...extra] = pos;
121100
121541
  if (mode !== "preview" && mode !== "apply" || !localRepoPath || !kindValue || extra.length > 0 || !CONTENT_OPERATION_KINDS.includes(kindValue)) {
121101
121542
  return usageError(
121102
121543
  ctx,
121103
- "content <preview|apply> <localRepoPath> <split|absorb|squash|duplicate|restore> --source <jj-change-id> [--destination <jj-change-id>] [--path <relative-path>]..."
121544
+ "content <preview|apply> <localRepoPath> <split|absorb|squash|duplicate|restore> --source <jj-change-id> [--destination <jj-change-id>] [--path <relative-path>]...\ncontent receipts [--repo owner/name|repoId]"
121104
121545
  );
121105
121546
  }
121106
121547
  const engine = values.engine;
@@ -121161,7 +121602,26 @@ async function contentCmd(ctx, pos, values) {
121161
121602
  if (!input.expectedOperationId) {
121162
121603
  return usageError(ctx, "content apply ... --expected-operation <jj-operation-id-from-preview>");
121163
121604
  }
121164
- const receipt = await jj.executeContentOperation(input);
121605
+ let receipt;
121606
+ let applyError;
121607
+ try {
121608
+ receipt = await jj.executeContentOperation(input);
121609
+ } catch (e) {
121610
+ applyError = e instanceof Error ? e : new Error(String(e));
121611
+ }
121612
+ await recordContentOpReceiptBestEffort(ctx, {
121613
+ kind: input.kind,
121614
+ sourceRevision: input.sourceRevision,
121615
+ destinationRevision: input.destinationRevision,
121616
+ paths: input.paths,
121617
+ expectedOperationId: input.expectedOperationId,
121618
+ resultOperationId: receipt?.operationId,
121619
+ changedChangeIds: receipt?.changedChangeIds ?? [],
121620
+ succeeded: applyError === void 0,
121621
+ errorMessage: applyError?.message
121622
+ });
121623
+ if (applyError) throw applyError;
121624
+ if (!receipt) throw new Error("unreachable: content apply produced neither a receipt nor an error");
121165
121625
  if (ctx.json) return emitJson(ctx, receipt);
121166
121626
  ctx.out(`${receipt.input.kind} completed in ${localRepoPath}`);
121167
121627
  ctx.out(` jj operation: ${receipt.operationId}`);
@@ -121178,7 +121638,7 @@ async function daemonForegroundCmd(ctx, pos, values) {
121178
121638
  if (!localRepoPath) {
121179
121639
  return usageError(
121180
121640
  ctx,
121181
- "daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic]"
121641
+ "daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--remote name]"
121182
121642
  );
121183
121643
  }
121184
121644
  const engine = values.engine;
@@ -121196,10 +121656,12 @@ async function daemonForegroundCmd(ctx, pos, values) {
121196
121656
  return 1;
121197
121657
  }
121198
121658
  const twoWay = values["two-way"] === true;
121659
+ const remote = values.remote;
121199
121660
  if (values.once === true) {
121200
121661
  const summary = await runDaemonOnce(jj, ctx.api, repoId, {
121201
121662
  repoPath: localRepoPath,
121202
121663
  twoWay,
121664
+ remote,
121203
121665
  log: ctx.json ? ctx.err : ctx.out
121204
121666
  });
121205
121667
  const exitCode = summary.pushFailures > 0 || summary.syncErrors > 0 ? 1 : 0;
@@ -121222,7 +121684,8 @@ async function daemonForegroundCmd(ctx, pos, values) {
121222
121684
  log: ctx.out,
121223
121685
  repoPath: localRepoPath,
121224
121686
  engineLabel: engine ?? "auto",
121225
- twoWay
121687
+ twoWay,
121688
+ remote
121226
121689
  });
121227
121690
  return 0;
121228
121691
  }
@@ -121307,6 +121770,7 @@ async function daemonStartCmd(ctx, rest, values) {
121307
121770
  engine,
121308
121771
  baseUrl: ctx.opts.baseUrl,
121309
121772
  twoWay: values["two-way"] === true,
121773
+ remote: values.remote,
121310
121774
  spawnFn: ctx.opts.spawnDaemonProcess
121311
121775
  });
121312
121776
  setRepoAlias(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jjhub",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "jjhub — GitHub CLI with Jujutsu-native superpowers: stable change IDs, stacks, platform-wide undo, and conflicts that wait, layered over plain GitHub repos by a JJHub server.",
5
5
  "bin": {
6
6
  "jjhub": "./dist/cli/jjhub.mjs"
@@ -10,7 +10,7 @@
10
10
  "node": ">=22.6.0"
11
11
  },
12
12
  "license": "UNLICENSED",
13
- "homepage": "https://jjhub.johnhenry.me",
13
+ "homepage": "https://jjhub.erisera.com",
14
14
  "keywords": [
15
15
  "jujutsu",
16
16
  "jj",
@@ -69,9 +69,14 @@ change land <changeId> [--force] waits for real CI triggered by its own re
69
69
  change split <changeId> <newTitle>
70
70
  change checks <changeId> list GitHub check runs for a change
71
71
  change actions <changeId> show bounded GitHub Actions workflow and job evidence
72
+ change actions rerun <changeId> <runId>
73
+ ask GitHub to re-run a workflow run shown by "change actions"
72
74
  change deployments <changeId> show bounded GitHub deployment evidence
73
75
  change security <changeId> show bounded GitHub code-scanning evidence
74
76
  change issues <changeId> show linked GitHub issue context for a change
77
+ change reviewers <changeId> reviewer-routing audit trail: every request/remove that
78
+ actually succeeded against GitHub, who did it, and when
79
+ change projects <changeId> show bounded GitHub Projects v2 evidence for linked issues
75
80
 
76
81
  stack create <title> [--id x] [--changes CHG-1,CHG-2]
77
82
  stack list
@@ -85,6 +90,11 @@ bookmark list
85
90
  bookmark delete <name> [--force] --force required for the default (trunk) bookmark
86
91
  bookmark set <name> <tracked|view> [--target CHG-x]
87
92
 
93
+ view save <name> <revsetExpression> save a named jj revset expression for later reuse (storage
94
+ only — evaluate it with "jjhub revset")
95
+ view list list saved views in this repo
96
+ view delete <name>
97
+
88
98
  op log "@" marks the current head operation
89
99
  undo [--force] refused when the head op is a "land" (real merge can't be unwound); --force reverts local state anyway
90
100
  redo
@@ -105,7 +115,7 @@ auth sessions list your live sessions; auth revoke <id>
105
115
  auth logout forget the saved connection/token
106
116
  auth status who the saved credential authenticates as
107
117
 
108
- mcp serve MCP server over stdio (77 tools) for agent clients
118
+ mcp serve MCP server over stdio (86 tools) for agent clients
109
119
  mcp serve --http <port> the same tools over Streamable HTTP (POST /)
110
120
  acp serve Agent Client Protocol agent over stdio (Zed's agent
111
121
  panel, ...) — every operation as a slash command
@@ -130,7 +140,11 @@ content preview <localRepoPath> <split|absorb|squash|duplicate|restore>
130
140
  content apply <localRepoPath> <split|absorb|squash|duplicate|restore>
131
141
  same selection plus --expected-operation <jj-operation-id> returned by preview
132
142
  local-only jj content curation; checked against jj's immutable policy and
133
- never implemented as a browser patch transform
143
+ never implemented as a browser patch transform; records a durable receipt
144
+ on the JJHub server (best-effort — see "content receipts" below)
145
+ content receipts [--repo owner/name|repoId]
146
+ list this repo's content-op receipt trail: every content apply invocation,
147
+ successful or rejected, with its kind, revisions, and outcome
134
148
  ```
135
149
 
136
150
  Anything else (`pr`, `issue`, ...) is passed through to `gh` — e.g. `jjhub pr view 7`. `jjhub auth`
@@ -243,6 +257,15 @@ targets are refused without `--ignore-immutable`. `--path` selects complete
243
257
  relative paths; interactive hunk selection remains jj's configured diff editor.
244
258
  Run `jjhub daemon . --once` afterwards to reconcile the local graph.
245
259
 
260
+ Each `content apply` invocation — successful or rejected (e.g. a stale
261
+ `--expected-operation`) — records a durable receipt on the JJHub server
262
+ (best-effort: a missing repo context or unreachable server only produces a
263
+ warning, never blocks the local jj mutation). Review the trail with:
264
+
265
+ ```bash
266
+ npm run jjhub -- content receipts
267
+ ```
268
+
246
269
  ### Resolve a conflict
247
270
 
248
271
  ```bash