@staff0rd/assist 0.416.0 → 0.418.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.418.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,159 @@ 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/parseIncoming.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: {
18255
+ decision: msg.decision,
18256
+ reason: msg.reason,
18257
+ comments: Array.isArray(msg.comments) ? msg.comments : void 0
18258
+ }
18259
+ };
18260
+ } catch {
18261
+ return null;
18262
+ }
18263
+ }
18264
+
18265
+ // src/commands/prs/requestPrDecision.ts
18266
+ function requestPrDecision(request) {
18267
+ return new Promise((resolve19, reject) => {
18268
+ connectToDaemon().then((socket) => {
18269
+ let settled = false;
18270
+ const finish = (error, decision) => {
18271
+ if (settled) return;
18272
+ settled = true;
18273
+ socket.destroy();
18274
+ if (error) reject(error);
18275
+ else resolve19(decision);
18276
+ };
18277
+ readSocketLines(socket, (line) => {
18278
+ const incoming = parseIncoming(line, request.requestId);
18279
+ if (!incoming) return;
18280
+ if (incoming.kind === "error") finish(new Error(incoming.message));
18281
+ else finish(void 0, incoming.decision);
18282
+ });
18283
+ socket.on("error", (error) => finish(error));
18284
+ socket.on(
18285
+ "close",
18286
+ () => finish(new Error("daemon closed before a decision was made"))
18287
+ );
18288
+ socket.write(`${JSON.stringify({ type: "pr-preview", ...request })}
18289
+ `);
18290
+ }, reject);
18197
18291
  });
18198
- validatePrContent(options2.title, body);
18199
- const existing = findCurrentPrNumber();
18200
- if (existing !== null && !options2.force) {
18292
+ }
18293
+
18294
+ // src/commands/prs/previewAndPlace.ts
18295
+ async function previewAndPlace(args) {
18296
+ console.log("Awaiting your approval in the assist web UI preview pane\u2026");
18297
+ let decision;
18298
+ try {
18299
+ decision = await requestPrDecision({
18300
+ sessionId: args.sessionId,
18301
+ requestId: randomUUID4(),
18302
+ title: args.title,
18303
+ body: args.body,
18304
+ prNumber: args.prNumber
18305
+ });
18306
+ } catch (error) {
18201
18307
  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.`
18308
+ `Error: ${error instanceof Error ? error.message : String(error)}`
18203
18309
  );
18204
18310
  process.exit(1);
18205
18311
  }
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 {
18312
+ if (decision.decision === "reject") {
18313
+ console.error(
18314
+ `PR preview rejected${decision.reason ? `: ${decision.reason}` : "."}`
18315
+ );
18316
+ const comments3 = decision.comments ?? [];
18317
+ if (comments3.length > 0) {
18318
+ console.error(
18319
+ `
18320
+ The reviewer left ${comments3.length} comment${comments3.length === 1 ? "" : "s"} on the preview. Address each one, then re-run this command:
18321
+ `
18322
+ );
18323
+ for (const [i, c] of comments3.entries()) {
18324
+ const quoted = c.quote.split("\n").map((line) => ` > ${line}`).join("\n");
18325
+ console.error(`${i + 1}. On:
18326
+ ${quoted}
18327
+ Comment: ${c.note}
18328
+ `);
18329
+ }
18330
+ }
18210
18331
  process.exit(1);
18211
18332
  }
18212
- await recordPrActivity();
18333
+ await placePr(args.prNumber, args.title, args.body, args.options);
18213
18334
  }
18214
- async function recordPrActivity() {
18215
- if (resolveSessionItemId() === null) return;
18216
- const pr = readSessionPrRef();
18217
- if (pr) await recordSessionRefs([pr]);
18335
+
18336
+ // src/commands/prs/raise.ts
18337
+ var USAGE = "Usage: assist prs raise --title <title> --what <what> --why <why> [--how <how>] [--resolves <key>] [--force]";
18338
+ async function raise(options2) {
18339
+ const { title, body } = buildValidatedBody(options2, USAGE);
18340
+ const existing = findCurrentPrNumber();
18341
+ const sessionId = process.env.ASSIST_SESSION_ID;
18342
+ if (process.env.ASSIST_SESSION === "1" && sessionId) {
18343
+ await previewAndPlace({
18344
+ sessionId,
18345
+ title,
18346
+ body,
18347
+ prNumber: existing,
18348
+ options: options2
18349
+ });
18350
+ return;
18351
+ }
18352
+ if (existing !== null && !options2.force) {
18353
+ console.error(
18354
+ `Error: A pull request already exists for this branch (#${existing}). Pass --force to overwrite it, or use 'assist prs edit' to update individual sections.`
18355
+ );
18356
+ process.exit(1);
18357
+ }
18358
+ await placePr(existing, title, body, options2);
18218
18359
  }
18219
18360
 
18220
18361
  // src/commands/prs/reply.ts
@@ -18316,6 +18457,86 @@ function registerPrsEdit(prsCommand) {
18316
18457
  ).action(edit);
18317
18458
  }
18318
18459
 
18460
+ // src/commands/prs/raiseHelpText.ts
18461
+ var GUIDANCE = `Raise a pull request for the current branch. Use a concise description with no
18462
+ headers, and do not reference Claude or any AI assistance in the title or body.
18463
+
18464
+ The body is assembled from discrete section options; supply at minimum --title,
18465
+ --what, and --why:
18466
+
18467
+ --title <title> short PR title.
18468
+ --what <what> what the change does (rendered as ## What).
18469
+ --why <why> why the change is needed (rendered as ## Why).
18470
+ --how <how> optional; how the change works (rendered as ## How). Omit it
18471
+ unless the approach genuinely needs explaining.
18472
+ --resolves <key> Jira issue key resolved by this PR; repeatable. Each key's
18473
+ browse URL is appended inline to ## Why. Unless a Jira key is
18474
+ already known from the session, ask the user whether this PR
18475
+ resolves a Jira issue and for the key before raising; omit
18476
+ --resolves only if they say there isn't one.
18477
+
18478
+ Wrap symbols, file paths, function names, class names, variable names, config
18479
+ keys, CLI commands, and flag names in backticks.
18480
+
18481
+ One section, one question. Each section answers exactly one thing, and
18482
+ implementation detail lives only in ## How:
18483
+
18484
+ ## What what is observably different for someone using or calling this, and
18485
+ nothing else. Not how it's built, not which functions/files changed.
18486
+ ## Why the problem or motivation that made the change worth doing. Not how
18487
+ the solution works.
18488
+ ## How only the non-obvious decisions the diff alone won't explain: a
18489
+ deliberate trade-off, a workaround, a reason the obvious approach was
18490
+ rejected. Omit it entirely by default. Never restate ## What as
18491
+ mechanism, and never walk through the diff.
18492
+
18493
+ The most common failure is altitude bleed: an implementation detail gets sprayed
18494
+ into whichever section is being written \u2014 ## What narrates the diff, ## How
18495
+ restates ## What, ## Why smuggles in mechanism. If a sentence describes a
18496
+ mechanism, it belongs in ## How, and probably shouldn't exist at all unless it's
18497
+ a genuine non-obvious decision. Litmus \u2014 a sentence is almost certainly mechanism
18498
+ (so ## How, or cut it) if it contains "by \u2026ing", "so that it can", "because the
18499
+ X filters/needs/uses", or names an internal component, property, function, or
18500
+ file.
18501
+
18502
+ Brevity budget \u2014 keep each section within these limits:
18503
+
18504
+ ## What a few sentences (2\u20133).
18505
+ ## Why 1\u20132 sentences.
18506
+ ## How omit by default; a sentence or two, only for non-obvious decisions.
18507
+
18508
+ Write prose. A short paragraph is the natural form for ## What and ## Why \u2014
18509
+ bullets in ## What are a smell. Use bullets only when there are genuinely several
18510
+ independent, parallel items (3+); a single bullet is never right. A single
18511
+ non-list paragraph over ~600 characters or ~4 sentences is a wall of text and
18512
+ will be rejected.
18513
+
18514
+ If a pull request already exists for the branch, this command errors \u2014 pass
18515
+ --force to fully overwrite its title and body, or use 'assist prs edit' to update
18516
+ only individual sections (every other section of the existing body is preserved).`;
18517
+ var TERMINAL_CONFIRM = `Before running this command, the user must see the full proposed title and body \u2014
18518
+ do not assume they can see your reasoning or earlier tool output. Write the
18519
+ complete title and body verbatim in your visible reply, then use the
18520
+ AskUserQuestion tool to ask whether to create the PR, putting the full title and
18521
+ body in the approve option's preview field. This confirmation is mandatory in
18522
+ every permission mode, including auto-accept and bypass-permissions. If the user
18523
+ requests changes, revise and confirm again before running.`;
18524
+ var WEB_CONFIRM = `You are running inside an assist web session. When you run this command the
18525
+ drafted title and body are shown automatically in a preview pane beside the
18526
+ terminal in the web UI, for the user to approve or reject there \u2014 do NOT display
18527
+ the title and body or ask for confirmation in the terminal. The command blocks
18528
+ until the user decides: on approval it creates or updates the PR and reports it;
18529
+ on rejection it exits non-zero with the reason, which you must address before
18530
+ running again. Just compose the sections and run the command.`;
18531
+ function raiseHelpText() {
18532
+ const confirm = process.env.ASSIST_SESSION === "1" ? WEB_CONFIRM : TERMINAL_CONFIRM;
18533
+ return `
18534
+ ${GUIDANCE}
18535
+
18536
+ ${confirm}
18537
+ `;
18538
+ }
18539
+
18319
18540
  // src/commands/registerPrsRaise.ts
18320
18541
  function collect5(value, previous) {
18321
18542
  return previous.concat([value]);
@@ -18341,7 +18562,7 @@ function registerPrsRaise(prsCommand) {
18341
18562
  "Request a review (repeatable)",
18342
18563
  collect5,
18343
18564
  []
18344
- ).option("-m, --milestone <name>", "Add the pull request to a milestone").action(raise);
18565
+ ).option("-m, --milestone <name>", "Add the pull request to a milestone").addHelpText("after", () => raiseHelpText()).action(raise);
18345
18566
  }
18346
18567
 
18347
18568
  // src/commands/registerPrs.ts
@@ -24838,7 +25059,8 @@ function toSessionInfo({
24838
25059
  autoAdvance,
24839
25060
  starred,
24840
25061
  usedPct,
24841
- design
25062
+ design,
25063
+ pendingPrPreview
24842
25064
  }) {
24843
25065
  return {
24844
25066
  id,
@@ -24865,7 +25087,8 @@ function toSessionInfo({
24865
25087
  autoAdvance,
24866
25088
  starred,
24867
25089
  usedPct,
24868
- design
25090
+ design,
25091
+ pendingPrPreview
24869
25092
  };
24870
25093
  }
24871
25094
 
@@ -25026,7 +25249,7 @@ function createAssistSession(id, assistArgs, cwd, meta) {
25026
25249
  }
25027
25250
 
25028
25251
  // src/commands/sessions/daemon/createSession.ts
25029
- import { randomUUID as randomUUID4 } from "crypto";
25252
+ import { randomUUID as randomUUID5 } from "crypto";
25030
25253
 
25031
25254
  // src/commands/sessions/daemon/resolveRunConfig.ts
25032
25255
  function resolveRunConfig(name, cwd) {
@@ -25107,7 +25330,7 @@ function sessionBase(id, status3) {
25107
25330
  }
25108
25331
  function createSession(id, prompt, cwd, design, harness) {
25109
25332
  if (harness === "pi") return createPiSession(id, prompt, cwd);
25110
- const claudeSessionId = randomUUID4();
25333
+ const claudeSessionId = randomUUID5();
25111
25334
  return {
25112
25335
  ...sessionBase(id, prompt ? "running" : "waiting"),
25113
25336
  name: prompt?.slice(0, 40) || `Session ${id}`,
@@ -25258,6 +25481,86 @@ function logSpawnedSession(session) {
25258
25481
  );
25259
25482
  }
25260
25483
 
25484
+ // src/commands/sessions/daemon/decidePrPreview.ts
25485
+ function decidePrPreview(sessions, waiters, notify2, d) {
25486
+ const id = d.sessionId;
25487
+ const requestId = d.requestId;
25488
+ const session = sessions.get(id);
25489
+ if (!session || session.pendingPrPreview?.requestId !== requestId) {
25490
+ daemonLog(
25491
+ `pr-decision for stale/unknown preview id=${id} requestId=${requestId} (ignoring)`
25492
+ );
25493
+ return;
25494
+ }
25495
+ const commentCount = Array.isArray(d.comments) ? d.comments.length : 0;
25496
+ daemonLog(
25497
+ `pr-decision received: id=${id} requestId=${requestId} decision=${d.decision} comments=${commentCount}`
25498
+ );
25499
+ const waiter = waiters.get(id);
25500
+ if (waiter)
25501
+ sendTo(waiter, {
25502
+ type: "pr-decision",
25503
+ requestId,
25504
+ decision: d.decision,
25505
+ reason: d.reason,
25506
+ comments: d.comments
25507
+ });
25508
+ waiters.delete(id);
25509
+ session.pendingPrPreview = void 0;
25510
+ notify2();
25511
+ }
25512
+
25513
+ // src/commands/sessions/daemon/setPrPreview.ts
25514
+ function setPrPreview(sessions, waiters, notify2, client, d) {
25515
+ const id = d.sessionId;
25516
+ const session = sessions.get(id);
25517
+ if (!session) {
25518
+ daemonLog(`pr-preview for unknown session id=${id} (ignoring)`);
25519
+ sendTo(client, {
25520
+ type: "error",
25521
+ message: `No live session ${id} for pr-preview`
25522
+ });
25523
+ return;
25524
+ }
25525
+ const prNumber = typeof d.prNumber === "number" ? d.prNumber : null;
25526
+ session.pendingPrPreview = {
25527
+ requestId: d.requestId,
25528
+ title: d.title,
25529
+ body: d.body,
25530
+ prNumber
25531
+ };
25532
+ waiters.set(id, client);
25533
+ daemonLog(
25534
+ `pr-preview set: id=${id} requestId=${d.requestId} target=${prNumber === null ? "create" : `edit #${prNumber}`}`
25535
+ );
25536
+ notify2();
25537
+ }
25538
+
25539
+ // src/commands/sessions/daemon/PrPreviewCoordinator.ts
25540
+ var PrPreviewCoordinator = class {
25541
+ constructor(sessions, notify2) {
25542
+ this.sessions = sessions;
25543
+ this.notify = notify2;
25544
+ }
25545
+ waiters = /* @__PURE__ */ new Map();
25546
+ set(client, d) {
25547
+ setPrPreview(this.sessions, this.waiters, this.notify, client, d);
25548
+ }
25549
+ decide(d) {
25550
+ decidePrPreview(this.sessions, this.waiters, this.notify, d);
25551
+ }
25552
+ clearWaiter(client) {
25553
+ for (const [id, waiter] of this.waiters) {
25554
+ if (waiter !== client) continue;
25555
+ this.waiters.delete(id);
25556
+ const session = this.sessions.get(id);
25557
+ if (session) session.pendingPrPreview = void 0;
25558
+ daemonLog(`pr-preview cleared: id=${id} (waiting client disconnected)`);
25559
+ this.notify();
25560
+ }
25561
+ }
25562
+ };
25563
+
25261
25564
  // src/commands/sessions/daemon/applySetStatus.ts
25262
25565
  function applySetStatus(sessions, id, status3, source, onStatusChange) {
25263
25566
  const session = sessions.get(id);
@@ -25698,7 +26001,7 @@ function respawnSession(session, respawn, status3, clients, onStatusChange) {
25698
26001
  }
25699
26002
 
25700
26003
  // src/commands/sessions/daemon/respawnPlan.ts
25701
- import { randomUUID as randomUUID5 } from "crypto";
26004
+ import { randomUUID as randomUUID6 } from "crypto";
25702
26005
 
25703
26006
  // src/commands/sessions/daemon/assistResumeArgs.ts
25704
26007
  function assistResumeArgs(session) {
@@ -25737,7 +26040,7 @@ function respawnPlan(session) {
25737
26040
  return null;
25738
26041
  }
25739
26042
  function freshClaudePlan(session, prompt, cwd) {
25740
- const claudeSessionId = randomUUID5();
26043
+ const claudeSessionId = randomUUID6();
25741
26044
  return {
25742
26045
  spawn: () => {
25743
26046
  session.claudeSessionId = claudeSessionId;
@@ -27144,6 +27447,10 @@ var SessionManager = class {
27144
27447
  this.onIdleChange = onIdleChange;
27145
27448
  }
27146
27449
  sessions = /* @__PURE__ */ new Map();
27450
+ prPreview = new PrPreviewCoordinator(
27451
+ this.sessions,
27452
+ () => this.notify()
27453
+ );
27147
27454
  // why: dispatch calls active.set() on card click; broadcasts include active.toJSON()
27148
27455
  active = new ActiveSelection(() => this.notify());
27149
27456
  clients = new ClientHub(persistUsagePeak);
@@ -27159,6 +27466,7 @@ var SessionManager = class {
27159
27466
  removeClient(client) {
27160
27467
  this.clients.delete(client);
27161
27468
  this.clients.unsubscribeLogs(client);
27469
+ this.prPreview.clearWaiter(client);
27162
27470
  this.onIdleChange?.(this.isIdle());
27163
27471
  }
27164
27472
  isIdle = () => this.sessions.size === 0 && this.clients.size === 0;
@@ -27531,6 +27839,8 @@ var messageHandlers = {
27531
27839
  ),
27532
27840
  "set-active": (_client, m, d) => m.active.set(d.cwd, d.sessionId),
27533
27841
  "set-status": handleSetStatus,
27842
+ "pr-preview": (client, m, d) => m.prPreview.set(client, d),
27843
+ "pr-decision": (_client, m, d) => m.prPreview.decide(d),
27534
27844
  "ui-status": (_client, _m, d) => daemonLog(`ui rendered: id=${d.sessionId} status=${d.status}`)
27535
27845
  };
27536
27846
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.416.0",
3
+ "version": "0.418.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {