@join-pantheon/cli 0.24.0 → 0.24.4

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/worker.js CHANGED
@@ -10,6 +10,7 @@ import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolic
10
10
  import { agentHookStates, repairHook } from "./notify.js";
11
11
  import { fence, assess } from "./untrusted.js";
12
12
  import { allowed, recordRun } from "./spend.js";
13
+ import { loadContinued, markContinued } from "./continued.js";
13
14
  import { loadProfile, reservedDecisionsNote } from "./profile.js";
14
15
  import { clip } from "./brief.js";
15
16
  import { composeWorker } from "./notice.js";
@@ -178,6 +179,7 @@ There is no human in this session. Do not ask for approval; you will not get an
178
179
  Act, reply if a reply is owed, then finish.`;
179
180
  }
180
181
  let fleetLock = null;
182
+ const CONTINUATION_HORIZON_MS = 24 * 60 * 60_000;
181
183
  export class Worker {
182
184
  cfg;
183
185
  opts;
@@ -218,6 +220,19 @@ export class Worker {
218
220
  this.cfg = cfg;
219
221
  this.opts = opts;
220
222
  }
223
+ spaceKey() { return this.cfg.spaceId ?? this.cfg.space; }
224
+ contLoaded = false;
225
+ loadContinued() {
226
+ if (this.contLoaded)
227
+ return;
228
+ this.contLoaded = true;
229
+ for (const id of loadContinued(this.spaceKey(), this.opts.home))
230
+ this.contDone.add(id);
231
+ }
232
+ markDone(id) {
233
+ this.contDone.add(id);
234
+ markContinued(this.spaceKey(), id, this.opts.home);
235
+ }
221
236
  async bringMachineAlong() {
222
237
  if (this.opts.selfUpgrade?.run)
223
238
  return;
@@ -459,7 +474,7 @@ export class Worker {
459
474
  this.requeueContinuation(continuation, 10_000);
460
475
  }
461
476
  else {
462
- this.contDone.add(continuation.id);
477
+ this.markDone(continuation.id);
463
478
  if (!this.skipLogged.has(`cont:${continuation.id}`)) {
464
479
  this.skipLogged.add(`cont:${continuation.id}`);
465
480
  this.opts.log(`- not continuing from ${continuation.id}: ${msg.split("\n")[0]}`);
@@ -510,7 +525,7 @@ export class Worker {
510
525
  return this.runJob(client, o, persona, policy, (branch) => buildBrief(o, trigger, branch, persona), true);
511
526
  }
512
527
  async handleContinuation(client, act, persona, policy) {
513
- this.contDone.add(act.id);
528
+ this.markDone(act.id);
514
529
  const o = {
515
530
  id: `cont:${act.id}`, kind: "CONTINUE", owner: this.cfg.member,
516
531
  ref: act.id, goal: act.goal, created_at: act.ts,
@@ -590,19 +605,23 @@ export class Worker {
590
605
  at: Date.now(), goal: o.goal, obligation: o.id, ok,
591
606
  triggeredBy, summary,
592
607
  }, this.opts.home);
593
- if (this.opts.notify && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
608
+ const sent = client.mineSince(startedAt, o.goal);
609
+ const quiet = ok && !requireReply && sent.length === 0;
610
+ if (this.opts.notify && !quiet && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
594
611
  try {
595
- const reply = client.mineSince(startedAt, o.goal).pop() ?? null;
612
+ const reply = sent[sent.length - 1] ?? null;
596
613
  this.opts.notify(composeWorker({ ok, triggeredBy, obligationBody: o.body, goal: o.goal ? client.goals.get(o.goal) : undefined, reply, obligationRef: o.ref }, (() => { const tools = installedTools(); return { tools, preferred: preferredTool(loadProfile(this.cfg), tools, this.opts.home) }; })()));
597
614
  }
598
615
  catch { }
599
616
  }
600
- this.opts.log(ok ? `< handled ${o.id} on ${branch}`
601
- : !exited ? `! run failed for ${o.id}: ${detail.split("\n")[0]} (see ${runLog})`
602
- : `! run exited cleanly but sent no act for ${o.id} — see ${runLog}`);
617
+ this.opts.log(quiet ? `< read ${o.id} on ${branch} — the thread was already settled, nothing sent`
618
+ : ok ? `< handled ${o.id} on ${branch}`
619
+ : !exited ? `! run failed for ${o.id}: ${detail.split("\n")[0]} (see ${runLog})`
620
+ : `! run exited cleanly but sent no act for ${o.id} — see ${runLog}`);
603
621
  return { obligation: o, branch, worktree, ok, detail };
604
622
  }
605
623
  enqueueContinuations(client, arrived) {
624
+ this.loadContinued();
606
625
  for (const r of arrived) {
607
626
  if (this.contDone.has(r.id) || this.contQueue.some((c) => c.act.id === r.id))
608
627
  continue;
@@ -613,6 +632,11 @@ export class Worker {
613
632
  ...(g ? { goalMembers: g.members, goalState: g.state } : {}),
614
633
  }))
615
634
  continue;
635
+ if (Date.now() - r.ts > CONTINUATION_HORIZON_MS) {
636
+ this.markDone(r.id);
637
+ this.opts.log(`- not continuing from ${r.act} ${r.id}: it landed ${Math.round((Date.now() - r.ts) / 3_600_000)}h ago — that thread is for a live session to read`);
638
+ continue;
639
+ }
616
640
  this.contQueue.push({ act: r, notBefore: Date.now() });
617
641
  if (this.contQueue.length > 20)
618
642
  this.contQueue.shift();
@@ -638,7 +662,7 @@ export class Worker {
638
662
  ref: r.id, goal: r.goal, created_at: r.ts, body: r.body.slice(0, 120),
639
663
  });
640
664
  if (sel.skip) {
641
- this.contDone.add(r.id);
665
+ this.markDone(r.id);
642
666
  if (!this.skipLogged.has(`cont:${r.id}`)) {
643
667
  this.skipLogged.add(`cont:${r.id}`);
644
668
  this.opts.log(`- not continuing from ${r.act} ${r.id}: ${sel.skip}`);
@@ -689,10 +713,10 @@ export class Worker {
689
713
  result = { ok: false, alreadyCurrent: false, from: VERSION, to: relayPkg ?? "?" };
690
714
  this.opts.log(`! self-upgrade failed: ${e.message}`);
691
715
  }
692
- if (result.ok && !result.alreadyCurrent && result.to === result.from) {
716
+ if (result.ok && result.to === result.from) {
693
717
  delete policy.attempts.self_upgrade;
694
718
  savePolicy(policy, home);
695
- this.opts.log(`* self-upgrade: the registry does not serve ${relayPkg} yet (installed ${result.to}); will check again`);
719
+ this.opts.log(`* self-upgrade: nothing newer than ${result.to} to install yet (the relay runs ${relayPkg}); will check again`);
696
720
  return;
697
721
  }
698
722
  if (result.ok && !result.alreadyCurrent) {
@@ -9,6 +9,7 @@
9
9
  * (the choice becomes the default), through a .command file
10
10
  * Terminal runs — no automation permission needed
11
11
  * Show details prints the act and the decision in Terminal
12
+ * Done (decisions) answers it as handled, silently — the reminders stop
12
13
  * Dismiss / × closes
13
14
  *
14
15
  * It owns no state. Everything it needs arrives as one JSON document:
@@ -25,7 +26,7 @@ import AppKit
25
26
  let args = CommandLine.arguments
26
27
 
27
28
  if args.count == 2, args[1] == "version" {
28
- print("2")
29
+ print("3")
29
30
  exit(0)
30
31
  }
31
32
 
@@ -45,6 +46,8 @@ struct Notice: Decodable {
45
46
  let details: Details?
46
47
  /** where to write the chosen action and which .command files to open — supplied by the CLI */
47
48
  let bin_dir: String
49
+ /** the pantheon CLI to run silently for one-click actions — supplied by the CLI, since this helper has no PATH worth trusting */
50
+ let pantheon: String?
48
51
  }
49
52
 
50
53
  var notice: Notice
@@ -138,6 +141,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
138
141
  act(["action": "details", "act": notice.details?.act as Any, "decision": notice.details?.decision as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-details.command")
139
142
  }
140
143
 
144
+ /// Done: write the action, run the CLI without a Terminal, close. The CLI answers the decision as handled.
145
+ @objc func markDone(_ sender: Any?) {
146
+ guard !notice.bin_dir.isEmpty, let decision = notice.details?.decision else { NSApp.terminate(nil); return }
147
+ let dir = URL(fileURLWithPath: notice.bin_dir)
148
+ let payload: [String: Any] = ["action": "done", "decision": decision, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }
149
+ if let data = try? JSONSerialization.data(withJSONObject: payload) {
150
+ try? data.write(to: dir.appendingPathComponent(".hud-action.json"), options: .atomic)
151
+ }
152
+ let bin = (notice.pantheon?.isEmpty == false) ? notice.pantheon! : "pantheon"
153
+ let quoted = "'" + bin.replacingOccurrences(of: "'", with: "'\\''") + "'"
154
+ let p = Process()
155
+ p.executableURL = URL(fileURLWithPath: "/bin/sh")
156
+ p.arguments = ["-lc", "\(quoted) explain --queued >/dev/null 2>&1"] // login shell: PATH with node, as the user's own terminal has
157
+ try? p.run()
158
+ NSApp.terminate(nil)
159
+ }
160
+
141
161
  @objc func legacyOpen(_ sender: Any?) {
142
162
  if !legacyPrompt.isEmpty { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(legacyPrompt, forType: .string) }
143
163
  if let b = legacyBundle, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: b) {
@@ -260,6 +280,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
260
280
  constraints += [d.leadingAnchor.constraint(equalTo: pull.trailingAnchor, constant: 14), d.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
261
281
  trailing = d
262
282
  }
283
+ if notice.details?.decision != nil {
284
+ // A decision can be settled from the banner itself: one click, no terminal, no more reminders.
285
+ let done = NSButton(title: "Done", target: self, action: #selector(markDone(_:)))
286
+ done.isBordered = false; done.font = .systemFont(ofSize: 12, weight: .medium); done.contentTintColor = .controlAccentColor
287
+ done.setAccessibilityLabel("Mark this decision handled")
288
+ done.translatesAutoresizingMaskIntoConstraints = false
289
+ root.addSubview(done)
290
+ constraints += [done.leadingAnchor.constraint(equalTo: trailing.trailingAnchor, constant: 14), done.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
291
+ trailing = done
292
+ }
263
293
  let dis = NSButton(title: "Dismiss", target: self, action: #selector(dismiss(_:)))
264
294
  dis.isBordered = false; dis.font = .systemFont(ofSize: 12); dis.contentTintColor = .secondaryLabelColor
265
295
  dis.translatesAutoresizingMaskIntoConstraints = false