@staff0rd/assist 0.416.0 → 0.417.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.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.416.0",
9
+ version: "0.417.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -6431,6 +6431,20 @@ function sendToDaemon(message) {
6431
6431
  });
6432
6432
  }
6433
6433
 
6434
+ // src/commands/sessions/daemon/readSocketLines.ts
6435
+ function readSocketLines(socket, onLine) {
6436
+ let buffer = "";
6437
+ socket.on("data", (chunk) => {
6438
+ buffer += chunk.toString("utf8");
6439
+ let newline = buffer.indexOf("\n");
6440
+ while (newline !== -1) {
6441
+ onLine(buffer.slice(0, newline));
6442
+ buffer = buffer.slice(newline + 1);
6443
+ newline = buffer.indexOf("\n");
6444
+ }
6445
+ });
6446
+ }
6447
+
6434
6448
  // src/commands/sessions/daemon/sendToDaemonAwaitAck.ts
6435
6449
  var ACK_TIMEOUT_MS = 1e3;
6436
6450
  function sendToDaemonAwaitAck(message) {
@@ -6449,16 +6463,8 @@ function sendToDaemonAwaitAck(message) {
6449
6463
  () => finish(new Error("timed out awaiting daemon ack")),
6450
6464
  ACK_TIMEOUT_MS
6451
6465
  );
6452
- let buffer = "";
6453
- socket.on("data", (chunk) => {
6454
- buffer += chunk.toString("utf8");
6455
- let newline = buffer.indexOf("\n");
6456
- while (newline !== -1) {
6457
- const line = buffer.slice(0, newline);
6458
- buffer = buffer.slice(newline + 1);
6459
- if (isAck(line)) return finish();
6460
- newline = buffer.indexOf("\n");
6461
- }
6466
+ readSocketLines(socket, (line) => {
6467
+ if (isAck(line)) finish();
6462
6468
  });
6463
6469
  socket.on("error", (error) => finish(error));
6464
6470
  socket.on(
@@ -18123,7 +18129,23 @@ async function prs(options2) {
18123
18129
  }
18124
18130
  }
18125
18131
 
18126
- // src/commands/prs/raise.ts
18132
+ // src/commands/prs/buildValidatedBody.ts
18133
+ function buildValidatedBody(options2, usage) {
18134
+ if (!options2.title || !options2.what || !options2.why) {
18135
+ console.error(usage);
18136
+ process.exit(1);
18137
+ }
18138
+ const body = buildPrBody({
18139
+ what: options2.what,
18140
+ why: options2.why,
18141
+ how: options2.how,
18142
+ resolves: options2.resolves
18143
+ });
18144
+ validatePrContent(options2.title, body);
18145
+ return { title: options2.title, body };
18146
+ }
18147
+
18148
+ // src/commands/prs/placePr.ts
18127
18149
  import { execFileSync as execFileSync7 } from "child_process";
18128
18150
 
18129
18151
  // src/commands/prs/buildCreateArgs.ts
@@ -18181,40 +18203,138 @@ function readSessionPrRef() {
18181
18203
  }
18182
18204
  }
18183
18205
 
18184
- // src/commands/prs/raise.ts
18185
- async function raise(options2) {
18186
- if (!options2.title || !options2.what || !options2.why) {
18187
- console.error(
18188
- "Usage: assist prs raise --title <title> --what <what> --why <why> [--how <how>] [--resolves <key>] [--force]"
18206
+ // src/commands/prs/recordPrActivity.ts
18207
+ async function recordPrActivity() {
18208
+ if (resolveSessionItemId() === null) return;
18209
+ const pr = readSessionPrRef();
18210
+ if (pr) await recordSessionRefs([pr]);
18211
+ }
18212
+
18213
+ // src/commands/prs/placePr.ts
18214
+ function hasUpstream() {
18215
+ try {
18216
+ execFileSync7(
18217
+ "git",
18218
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
18219
+ { stdio: "pipe" }
18189
18220
  );
18221
+ return true;
18222
+ } catch {
18223
+ return false;
18224
+ }
18225
+ }
18226
+ function ensureBranchPushed() {
18227
+ const args = hasUpstream() ? ["push"] : ["push", "--set-upstream", "origin", "HEAD"];
18228
+ execFileSync7("git", args, { stdio: "inherit" });
18229
+ }
18230
+ async function placePr(prNumber, title, body, options2) {
18231
+ const args = prNumber !== null ? buildEditArgs(prNumber, title, body) : buildCreateArgs(title, body, options2);
18232
+ try {
18233
+ if (prNumber === null && !options2.head) ensureBranchPushed();
18234
+ execFileSync7("gh", args, { stdio: "inherit" });
18235
+ } catch {
18190
18236
  process.exit(1);
18191
18237
  }
18192
- const body = buildPrBody({
18193
- what: options2.what,
18194
- why: options2.why,
18195
- how: options2.how,
18196
- resolves: options2.resolves
18238
+ await recordPrActivity();
18239
+ }
18240
+
18241
+ // src/commands/prs/previewAndPlace.ts
18242
+ import { randomUUID as randomUUID4 } from "crypto";
18243
+
18244
+ // src/commands/prs/requestPrDecision.ts
18245
+ function parseIncoming(line, requestId) {
18246
+ try {
18247
+ const msg = JSON.parse(line);
18248
+ if (msg.type === "error" && (msg.message ?? "").includes("pr-preview"))
18249
+ return { kind: "error", message: msg.message ?? "pr-preview failed" };
18250
+ if (msg.type !== "pr-decision" || msg.requestId !== requestId) return null;
18251
+ if (msg.decision !== "approve" && msg.decision !== "reject") return null;
18252
+ return {
18253
+ kind: "decision",
18254
+ decision: { decision: msg.decision, reason: msg.reason }
18255
+ };
18256
+ } catch {
18257
+ return null;
18258
+ }
18259
+ }
18260
+ function requestPrDecision(request) {
18261
+ return new Promise((resolve19, reject) => {
18262
+ connectToDaemon().then((socket) => {
18263
+ let settled = false;
18264
+ const finish = (error, decision) => {
18265
+ if (settled) return;
18266
+ settled = true;
18267
+ socket.destroy();
18268
+ if (error) reject(error);
18269
+ else resolve19(decision);
18270
+ };
18271
+ readSocketLines(socket, (line) => {
18272
+ const incoming = parseIncoming(line, request.requestId);
18273
+ if (!incoming) return;
18274
+ if (incoming.kind === "error") finish(new Error(incoming.message));
18275
+ else finish(void 0, incoming.decision);
18276
+ });
18277
+ socket.on("error", (error) => finish(error));
18278
+ socket.on(
18279
+ "close",
18280
+ () => finish(new Error("daemon closed before a decision was made"))
18281
+ );
18282
+ socket.write(`${JSON.stringify({ type: "pr-preview", ...request })}
18283
+ `);
18284
+ }, reject);
18197
18285
  });
18198
- validatePrContent(options2.title, body);
18199
- const existing = findCurrentPrNumber();
18200
- if (existing !== null && !options2.force) {
18286
+ }
18287
+
18288
+ // src/commands/prs/previewAndPlace.ts
18289
+ async function previewAndPlace(args) {
18290
+ console.log("Awaiting your approval in the assist web UI preview pane\u2026");
18291
+ let decision;
18292
+ try {
18293
+ decision = await requestPrDecision({
18294
+ sessionId: args.sessionId,
18295
+ requestId: randomUUID4(),
18296
+ title: args.title,
18297
+ body: args.body,
18298
+ prNumber: args.prNumber
18299
+ });
18300
+ } catch (error) {
18201
18301
  console.error(
18202
- `Error: A pull request already exists for this branch (#${existing}). Pass --force to overwrite it, or use 'assist prs edit' to update individual sections.`
18302
+ `Error: ${error instanceof Error ? error.message : String(error)}`
18203
18303
  );
18204
18304
  process.exit(1);
18205
18305
  }
18206
- const args = existing !== null ? buildEditArgs(existing, options2.title, body) : buildCreateArgs(options2.title, body, options2);
18207
- try {
18208
- execFileSync7("gh", args, { stdio: "inherit" });
18209
- } catch {
18306
+ if (decision.decision === "reject") {
18307
+ console.error(
18308
+ `PR preview rejected${decision.reason ? `: ${decision.reason}` : "."}`
18309
+ );
18210
18310
  process.exit(1);
18211
18311
  }
18212
- await recordPrActivity();
18312
+ await placePr(args.prNumber, args.title, args.body, args.options);
18213
18313
  }
18214
- async function recordPrActivity() {
18215
- if (resolveSessionItemId() === null) return;
18216
- const pr = readSessionPrRef();
18217
- if (pr) await recordSessionRefs([pr]);
18314
+
18315
+ // src/commands/prs/raise.ts
18316
+ var USAGE = "Usage: assist prs raise --title <title> --what <what> --why <why> [--how <how>] [--resolves <key>] [--force]";
18317
+ async function raise(options2) {
18318
+ const { title, body } = buildValidatedBody(options2, USAGE);
18319
+ const existing = findCurrentPrNumber();
18320
+ const sessionId = process.env.ASSIST_SESSION_ID;
18321
+ if (process.env.ASSIST_SESSION === "1" && sessionId) {
18322
+ await previewAndPlace({
18323
+ sessionId,
18324
+ title,
18325
+ body,
18326
+ prNumber: existing,
18327
+ options: options2
18328
+ });
18329
+ return;
18330
+ }
18331
+ if (existing !== null && !options2.force) {
18332
+ console.error(
18333
+ `Error: A pull request already exists for this branch (#${existing}). Pass --force to overwrite it, or use 'assist prs edit' to update individual sections.`
18334
+ );
18335
+ process.exit(1);
18336
+ }
18337
+ await placePr(existing, title, body, options2);
18218
18338
  }
18219
18339
 
18220
18340
  // src/commands/prs/reply.ts
@@ -18316,6 +18436,86 @@ function registerPrsEdit(prsCommand) {
18316
18436
  ).action(edit);
18317
18437
  }
18318
18438
 
18439
+ // src/commands/prs/raiseHelpText.ts
18440
+ var GUIDANCE = `Raise a pull request for the current branch. Use a concise description with no
18441
+ headers, and do not reference Claude or any AI assistance in the title or body.
18442
+
18443
+ The body is assembled from discrete section options; supply at minimum --title,
18444
+ --what, and --why:
18445
+
18446
+ --title <title> short PR title.
18447
+ --what <what> what the change does (rendered as ## What).
18448
+ --why <why> why the change is needed (rendered as ## Why).
18449
+ --how <how> optional; how the change works (rendered as ## How). Omit it
18450
+ unless the approach genuinely needs explaining.
18451
+ --resolves <key> Jira issue key resolved by this PR; repeatable. Each key's
18452
+ browse URL is appended inline to ## Why. Unless a Jira key is
18453
+ already known from the session, ask the user whether this PR
18454
+ resolves a Jira issue and for the key before raising; omit
18455
+ --resolves only if they say there isn't one.
18456
+
18457
+ Wrap symbols, file paths, function names, class names, variable names, config
18458
+ keys, CLI commands, and flag names in backticks.
18459
+
18460
+ One section, one question. Each section answers exactly one thing, and
18461
+ implementation detail lives only in ## How:
18462
+
18463
+ ## What what is observably different for someone using or calling this, and
18464
+ nothing else. Not how it's built, not which functions/files changed.
18465
+ ## Why the problem or motivation that made the change worth doing. Not how
18466
+ the solution works.
18467
+ ## How only the non-obvious decisions the diff alone won't explain: a
18468
+ deliberate trade-off, a workaround, a reason the obvious approach was
18469
+ rejected. Omit it entirely by default. Never restate ## What as
18470
+ mechanism, and never walk through the diff.
18471
+
18472
+ The most common failure is altitude bleed: an implementation detail gets sprayed
18473
+ into whichever section is being written \u2014 ## What narrates the diff, ## How
18474
+ restates ## What, ## Why smuggles in mechanism. If a sentence describes a
18475
+ mechanism, it belongs in ## How, and probably shouldn't exist at all unless it's
18476
+ a genuine non-obvious decision. Litmus \u2014 a sentence is almost certainly mechanism
18477
+ (so ## How, or cut it) if it contains "by \u2026ing", "so that it can", "because the
18478
+ X filters/needs/uses", or names an internal component, property, function, or
18479
+ file.
18480
+
18481
+ Brevity budget \u2014 keep each section within these limits:
18482
+
18483
+ ## What a few sentences (2\u20133).
18484
+ ## Why 1\u20132 sentences.
18485
+ ## How omit by default; a sentence or two, only for non-obvious decisions.
18486
+
18487
+ Write prose. A short paragraph is the natural form for ## What and ## Why \u2014
18488
+ bullets in ## What are a smell. Use bullets only when there are genuinely several
18489
+ independent, parallel items (3+); a single bullet is never right. A single
18490
+ non-list paragraph over ~600 characters or ~4 sentences is a wall of text and
18491
+ will be rejected.
18492
+
18493
+ If a pull request already exists for the branch, this command errors \u2014 pass
18494
+ --force to fully overwrite its title and body, or use 'assist prs edit' to update
18495
+ only individual sections (every other section of the existing body is preserved).`;
18496
+ var TERMINAL_CONFIRM = `Before running this command, the user must see the full proposed title and body \u2014
18497
+ do not assume they can see your reasoning or earlier tool output. Write the
18498
+ complete title and body verbatim in your visible reply, then use the
18499
+ AskUserQuestion tool to ask whether to create the PR, putting the full title and
18500
+ body in the approve option's preview field. This confirmation is mandatory in
18501
+ every permission mode, including auto-accept and bypass-permissions. If the user
18502
+ requests changes, revise and confirm again before running.`;
18503
+ var WEB_CONFIRM = `You are running inside an assist web session. When you run this command the
18504
+ drafted title and body are shown automatically in a preview pane beside the
18505
+ terminal in the web UI, for the user to approve or reject there \u2014 do NOT display
18506
+ the title and body or ask for confirmation in the terminal. The command blocks
18507
+ until the user decides: on approval it creates or updates the PR and reports it;
18508
+ on rejection it exits non-zero with the reason, which you must address before
18509
+ running again. Just compose the sections and run the command.`;
18510
+ function raiseHelpText() {
18511
+ const confirm = process.env.ASSIST_SESSION === "1" ? WEB_CONFIRM : TERMINAL_CONFIRM;
18512
+ return `
18513
+ ${GUIDANCE}
18514
+
18515
+ ${confirm}
18516
+ `;
18517
+ }
18518
+
18319
18519
  // src/commands/registerPrsRaise.ts
18320
18520
  function collect5(value, previous) {
18321
18521
  return previous.concat([value]);
@@ -18341,7 +18541,7 @@ function registerPrsRaise(prsCommand) {
18341
18541
  "Request a review (repeatable)",
18342
18542
  collect5,
18343
18543
  []
18344
- ).option("-m, --milestone <name>", "Add the pull request to a milestone").action(raise);
18544
+ ).option("-m, --milestone <name>", "Add the pull request to a milestone").addHelpText("after", () => raiseHelpText()).action(raise);
18345
18545
  }
18346
18546
 
18347
18547
  // src/commands/registerPrs.ts
@@ -24838,7 +25038,8 @@ function toSessionInfo({
24838
25038
  autoAdvance,
24839
25039
  starred,
24840
25040
  usedPct,
24841
- design
25041
+ design,
25042
+ pendingPrPreview
24842
25043
  }) {
24843
25044
  return {
24844
25045
  id,
@@ -24865,7 +25066,8 @@ function toSessionInfo({
24865
25066
  autoAdvance,
24866
25067
  starred,
24867
25068
  usedPct,
24868
- design
25069
+ design,
25070
+ pendingPrPreview
24869
25071
  };
24870
25072
  }
24871
25073
 
@@ -25026,7 +25228,7 @@ function createAssistSession(id, assistArgs, cwd, meta) {
25026
25228
  }
25027
25229
 
25028
25230
  // src/commands/sessions/daemon/createSession.ts
25029
- import { randomUUID as randomUUID4 } from "crypto";
25231
+ import { randomUUID as randomUUID5 } from "crypto";
25030
25232
 
25031
25233
  // src/commands/sessions/daemon/resolveRunConfig.ts
25032
25234
  function resolveRunConfig(name, cwd) {
@@ -25107,7 +25309,7 @@ function sessionBase(id, status3) {
25107
25309
  }
25108
25310
  function createSession(id, prompt, cwd, design, harness) {
25109
25311
  if (harness === "pi") return createPiSession(id, prompt, cwd);
25110
- const claudeSessionId = randomUUID4();
25312
+ const claudeSessionId = randomUUID5();
25111
25313
  return {
25112
25314
  ...sessionBase(id, prompt ? "running" : "waiting"),
25113
25315
  name: prompt?.slice(0, 40) || `Session ${id}`,
@@ -25258,6 +25460,74 @@ function logSpawnedSession(session) {
25258
25460
  );
25259
25461
  }
25260
25462
 
25463
+ // src/commands/sessions/daemon/PrPreviewCoordinator.ts
25464
+ var PrPreviewCoordinator = class {
25465
+ constructor(sessions, notify2) {
25466
+ this.sessions = sessions;
25467
+ this.notify = notify2;
25468
+ }
25469
+ waiters = /* @__PURE__ */ new Map();
25470
+ set(client, d) {
25471
+ const id = d.sessionId;
25472
+ const session = this.sessions.get(id);
25473
+ if (!session) {
25474
+ daemonLog(`pr-preview for unknown session id=${id} (ignoring)`);
25475
+ sendTo(client, {
25476
+ type: "error",
25477
+ message: `No live session ${id} for pr-preview`
25478
+ });
25479
+ return;
25480
+ }
25481
+ const prNumber = typeof d.prNumber === "number" ? d.prNumber : null;
25482
+ session.pendingPrPreview = {
25483
+ requestId: d.requestId,
25484
+ title: d.title,
25485
+ body: d.body,
25486
+ prNumber
25487
+ };
25488
+ this.waiters.set(id, client);
25489
+ daemonLog(
25490
+ `pr-preview set: id=${id} requestId=${d.requestId} target=${prNumber === null ? "create" : `edit #${prNumber}`}`
25491
+ );
25492
+ this.notify();
25493
+ }
25494
+ decide(d) {
25495
+ const id = d.sessionId;
25496
+ const requestId = d.requestId;
25497
+ const session = this.sessions.get(id);
25498
+ if (!session || session.pendingPrPreview?.requestId !== requestId) {
25499
+ daemonLog(
25500
+ `pr-decision for stale/unknown preview id=${id} requestId=${requestId} (ignoring)`
25501
+ );
25502
+ return;
25503
+ }
25504
+ daemonLog(
25505
+ `pr-decision received: id=${id} requestId=${requestId} decision=${d.decision}`
25506
+ );
25507
+ const waiter = this.waiters.get(id);
25508
+ if (waiter)
25509
+ sendTo(waiter, {
25510
+ type: "pr-decision",
25511
+ requestId,
25512
+ decision: d.decision,
25513
+ reason: d.reason
25514
+ });
25515
+ this.waiters.delete(id);
25516
+ session.pendingPrPreview = void 0;
25517
+ this.notify();
25518
+ }
25519
+ clearWaiter(client) {
25520
+ for (const [id, waiter] of this.waiters) {
25521
+ if (waiter !== client) continue;
25522
+ this.waiters.delete(id);
25523
+ const session = this.sessions.get(id);
25524
+ if (session) session.pendingPrPreview = void 0;
25525
+ daemonLog(`pr-preview cleared: id=${id} (waiting client disconnected)`);
25526
+ this.notify();
25527
+ }
25528
+ }
25529
+ };
25530
+
25261
25531
  // src/commands/sessions/daemon/applySetStatus.ts
25262
25532
  function applySetStatus(sessions, id, status3, source, onStatusChange) {
25263
25533
  const session = sessions.get(id);
@@ -25698,7 +25968,7 @@ function respawnSession(session, respawn, status3, clients, onStatusChange) {
25698
25968
  }
25699
25969
 
25700
25970
  // src/commands/sessions/daemon/respawnPlan.ts
25701
- import { randomUUID as randomUUID5 } from "crypto";
25971
+ import { randomUUID as randomUUID6 } from "crypto";
25702
25972
 
25703
25973
  // src/commands/sessions/daemon/assistResumeArgs.ts
25704
25974
  function assistResumeArgs(session) {
@@ -25737,7 +26007,7 @@ function respawnPlan(session) {
25737
26007
  return null;
25738
26008
  }
25739
26009
  function freshClaudePlan(session, prompt, cwd) {
25740
- const claudeSessionId = randomUUID5();
26010
+ const claudeSessionId = randomUUID6();
25741
26011
  return {
25742
26012
  spawn: () => {
25743
26013
  session.claudeSessionId = claudeSessionId;
@@ -27144,6 +27414,10 @@ var SessionManager = class {
27144
27414
  this.onIdleChange = onIdleChange;
27145
27415
  }
27146
27416
  sessions = /* @__PURE__ */ new Map();
27417
+ prPreview = new PrPreviewCoordinator(
27418
+ this.sessions,
27419
+ () => this.notify()
27420
+ );
27147
27421
  // why: dispatch calls active.set() on card click; broadcasts include active.toJSON()
27148
27422
  active = new ActiveSelection(() => this.notify());
27149
27423
  clients = new ClientHub(persistUsagePeak);
@@ -27159,6 +27433,7 @@ var SessionManager = class {
27159
27433
  removeClient(client) {
27160
27434
  this.clients.delete(client);
27161
27435
  this.clients.unsubscribeLogs(client);
27436
+ this.prPreview.clearWaiter(client);
27162
27437
  this.onIdleChange?.(this.isIdle());
27163
27438
  }
27164
27439
  isIdle = () => this.sessions.size === 0 && this.clients.size === 0;
@@ -27531,6 +27806,8 @@ var messageHandlers = {
27531
27806
  ),
27532
27807
  "set-active": (_client, m, d) => m.active.set(d.cwd, d.sessionId),
27533
27808
  "set-status": handleSetStatus,
27809
+ "pr-preview": (client, m, d) => m.prPreview.set(client, d),
27810
+ "pr-decision": (_client, m, d) => m.prPreview.decide(d),
27534
27811
  "ui-status": (_client, _m, d) => daemonLog(`ui rendered: id=${d.sessionId} status=${d.status}`)
27535
27812
  };
27536
27813
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.416.0",
3
+ "version": "0.417.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {