@raidiant/notifai 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -209,7 +209,7 @@ export declare function configSetCommand(deps: CommandDeps, key: string, rawValu
209
209
  * 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
210
210
  * Keep this immutable and public because the command is printed to users.
211
211
  */
212
- export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.2.0";
212
+ export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.2.1";
213
213
  /** Derive a contract-valid project slug from a directory name. */
214
214
  export declare function projectSlugFrom(name: string): string;
215
215
  export interface InitFlags {
package/dist/commands.js CHANGED
@@ -433,20 +433,25 @@ export async function repliesCommand(deps, requestedId, flags) {
433
433
  deps.io.err('Pass a request id or --pending, not both.');
434
434
  return EXIT.usage;
435
435
  }
436
- let requestId = requestedId;
436
+ let requestIds = requestedId === undefined ? [] : [requestedId];
437
437
  if (flags.pending === true) {
438
438
  const sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)());
439
439
  if (sessionId === null) {
440
440
  deps.io.err('No active session pointer is available in this directory.');
441
441
  return EXIT.noReply;
442
442
  }
443
- requestId = readSessionState(sessionId, deps.env).pending?.request_id;
444
- if (requestId === undefined) {
443
+ // Every delivered question in the session's queue, in registration order —
444
+ // an agent may have several outstanding at once.
445
+ const state = readSessionState(sessionId, deps.env);
446
+ requestIds = (Array.isArray(state.pending) ? state.pending : [])
447
+ .map((entry) => entry.request_id)
448
+ .filter((id) => id !== undefined);
449
+ if (requestIds.length === 0) {
445
450
  deps.io.err(`Session ${sessionId} has no pushed question pending.`);
446
451
  return EXIT.noReply;
447
452
  }
448
453
  }
449
- if (requestId === undefined) {
454
+ if (requestIds.length === 0) {
450
455
  deps.io.err('Pass a request id or --pending.');
451
456
  return EXIT.usage;
452
457
  }
@@ -455,27 +460,44 @@ export async function repliesCommand(deps, requestedId, flags) {
455
460
  if (!authed)
456
461
  return EXIT.auth;
457
462
  try {
458
- const result = await waitForReply(authed.client, requestId, {
459
- timeoutSeconds: waitSeconds,
460
- afterSeq,
461
- now: deps.now,
462
- sleep: deps.sleep,
463
- });
464
- if (flags.json) {
465
- deps.io.out(JSON.stringify({ ...result.response, degraded: result.degraded }, null, 2));
463
+ let anyReplies = false;
464
+ let anyDegraded = false;
465
+ let allTimedOut = true;
466
+ const jsonBodies = [];
467
+ for (const requestId of requestIds) {
468
+ const result = await waitForReply(authed.client, requestId, {
469
+ timeoutSeconds: waitSeconds,
470
+ afterSeq,
471
+ now: deps.now,
472
+ sleep: deps.sleep,
473
+ });
474
+ if (flags.json) {
475
+ jsonBodies.push({ ...result.response, degraded: result.degraded });
476
+ }
477
+ else if (result.response.replies.length > 0) {
478
+ if (flags.pending === true)
479
+ deps.io.out(`pending request ${requestId}`);
480
+ printReplies(deps, result.response.replies);
481
+ }
482
+ else {
483
+ printNoReply(deps, requestId, result.response.reply_expires_at);
484
+ }
485
+ anyReplies ||= result.response.replies.length > 0;
486
+ anyDegraded ||= result.degraded;
487
+ allTimedOut &&= result.timedOut;
466
488
  }
467
- else if (result.response.replies.length > 0) {
468
- if (flags.pending === true)
469
- deps.io.out(`pending request ${requestId}`);
470
- printReplies(deps, result.response.replies);
489
+ if (flags.json) {
490
+ // One request keeps the response shape agents already parse; several
491
+ // (only possible via --pending) arrive as an array in queue order.
492
+ deps.io.out(JSON.stringify(jsonBodies.length === 1 ? jsonBodies[0] : jsonBodies, null, 2));
471
493
  }
472
- else
473
- printNoReply(deps, requestId, result.response.reply_expires_at);
474
- if (result.degraded) {
494
+ if (anyDegraded) {
475
495
  deps.io.err(DEGRADED_WAIT_WARNING);
476
496
  return EXIT.network;
477
497
  }
478
- return result.timedOut ? EXIT.noReply : EXIT.ok;
498
+ if (anyReplies)
499
+ return EXIT.ok;
500
+ return allTimedOut ? EXIT.noReply : EXIT.ok;
479
501
  }
480
502
  catch (err) {
481
503
  return reportError(deps, err);
@@ -1462,7 +1484,7 @@ export async function configSetCommand(deps, key, rawValue, flags) {
1462
1484
  * 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
1463
1485
  * Keep this immutable and public because the command is printed to users.
1464
1486
  */
1465
- export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.2.0';
1487
+ export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.2.1';
1466
1488
  function skillSourceParts() {
1467
1489
  const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE);
1468
1490
  return match === null ? null : { source: match[1], ref: match[2] };
@@ -2474,12 +2496,16 @@ function remedyLine(state) {
2474
2496
  * way and is not worth re-deriving. The judgment added here is which failures
2475
2497
  * actually stand in the way.
2476
2498
  *
2477
- * Two do not. A hook that has never fired is the normal condition of an
2478
- * install thirty seconds old, and OpenCode's inability to resume an idle turn
2479
- * is a property of that harness rather than a fault in this setup. Treating
2480
- * either as blocking would mean `init` could never finish for an OpenCode
2481
- * user, or could only finish after a session had already run — so both report
2482
- * as things worth knowing rather than things to fix.
2499
+ * Not everything failed is in the way. A pointer that has never been
2500
+ * published is the normal condition of an install thirty seconds old — the
2501
+ * next prompt fixes it and no command can — and OpenCode's inability to
2502
+ * resume an idle turn is a property of that harness rather than a fault in
2503
+ * this setup. Treating those as blocking would mean `init` could never
2504
+ * finish for an OpenCode user, or could only finish after a session had
2505
+ * already run — so they report as things worth knowing rather than things to
2506
+ * fix, and `init` walks on to the states it can actually close, delivery
2507
+ * proof included. Which failures are informational, and what the true remedy
2508
+ * is, is each check's own call (`HookCheck`).
2483
2509
  */
2484
2510
  function hookStates(deps) {
2485
2511
  const installations = findInstallations(deps.cwd, deps.env);
@@ -2519,23 +2545,30 @@ function hookStates(deps) {
2519
2545
  }
2520
2546
  /** Real but not in the way; see the note above. */
2521
2547
  const informational = new Set(['hooks (opencode continuation)']);
2522
- if (active === null)
2523
- informational.add('hooks (fired)');
2524
2548
  return [
2525
2549
  ...hookChecks(deps).map((check) => ({
2526
2550
  id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
2527
2551
  title: check.name === 'hooks' || check.name.startsWith('hooks (active')
2528
2552
  ? 'Question routing'
2529
2553
  : check.name,
2530
- status: check.ok ? 'ready' : informational.has(check.name) ? 'optional-gap' : 'gap',
2554
+ status: check.ok
2555
+ ? 'ready'
2556
+ : check.informational === true || informational.has(check.name)
2557
+ ? 'optional-gap'
2558
+ : 'gap',
2531
2559
  detail: check.detail,
2532
2560
  ...(check.ok
2533
2561
  ? {}
2534
2562
  : {
2535
2563
  remedy: {
2536
2564
  by: 'user-here',
2537
- summary: 'the detail above names what to change',
2538
- command: 'notifai hooks install',
2565
+ // The check's own remedy when it has one; the generic reinstall
2566
+ // line was wrong exactly where it mattered (an unfired pointer
2567
+ // needs a prompt, not `hooks install`).
2568
+ ...(check.remedy ?? {
2569
+ summary: 'the detail above names what to change',
2570
+ command: 'notifai hooks install',
2571
+ }),
2539
2572
  },
2540
2573
  }),
2541
2574
  })),
@@ -2573,21 +2606,52 @@ function hookChecks(deps) {
2573
2606
  detail: activeInstallations.length > 0
2574
2607
  ? `active ${active.label} session has a matching hook installation`
2575
2608
  : `active ${active.label} session has no matching hook installation — run \`notifai hooks install --harness ${active.harness}\``,
2609
+ ...(activeInstallations.length > 0
2610
+ ? {}
2611
+ : {
2612
+ remedy: {
2613
+ summary: `install hooks for the active ${active.label} session`,
2614
+ command: `notifai hooks install --harness ${active.harness}`,
2615
+ },
2616
+ }),
2576
2617
  });
2577
2618
  if (activeInstallations.length > 0) {
2578
2619
  const pointer = readProjectSessionPointer(deps.cwd, deps.env, (deps.now ?? Date.now)());
2579
- const matches = pointer !== null &&
2580
- pointer.harness === active.harness &&
2581
- (active.sessionId === undefined || pointer.sessionId === active.sessionId);
2582
- checks.push({
2583
- name: 'hooks (active session)',
2584
- ok: matches,
2585
- detail: pointer === null
2586
- ? `active ${active.label} session has not published a live pointer — send one ${active.label} prompt, then check again`
2587
- : matches
2620
+ if (pointer === null) {
2621
+ // The normal condition of hooks installed moments ago: the pointer
2622
+ // appears when the harness next fires a hook, and no command can
2623
+ // force that. Informational, so `init` walks on to the states it can
2624
+ // actually prove instead of exiting over evidence only time produces.
2625
+ checks.push({
2626
+ name: 'hooks (active session)',
2627
+ ok: false,
2628
+ informational: true,
2629
+ detail: `active ${active.label} session has not published a live pointer — send one ${active.label} prompt, then check again`,
2630
+ remedy: {
2631
+ summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
2632
+ command: 'notifai doctor',
2633
+ },
2634
+ });
2635
+ }
2636
+ else {
2637
+ const matches = pointer.harness === active.harness &&
2638
+ (active.sessionId === undefined || pointer.sessionId === active.sessionId);
2639
+ checks.push({
2640
+ name: 'hooks (active session)',
2641
+ ok: matches,
2642
+ detail: matches
2588
2643
  ? `the project pointer belongs to the active ${active.label} session`
2589
2644
  : `the project pointer belongs to another ${active.label} session or harness; refusing cross-session routing`,
2590
- });
2645
+ ...(matches
2646
+ ? {}
2647
+ : {
2648
+ remedy: {
2649
+ summary: `send one ${active.label} prompt in this session so the pointer belongs to it`,
2650
+ command: 'notifai doctor',
2651
+ },
2652
+ }),
2653
+ });
2654
+ }
2591
2655
  }
2592
2656
  }
2593
2657
  // A handler naming an event this build dropped exits 2 every time the harness
@@ -2655,9 +2719,20 @@ function hookChecks(deps) {
2655
2719
  checks.push({
2656
2720
  name: 'hooks (fired)',
2657
2721
  ok: fired,
2722
+ // Never-fired is evidence about the past, not a fault in the setup, and
2723
+ // the cure is a prompt no command can send.
2724
+ informational: true,
2658
2725
  detail: fired
2659
2726
  ? 'a session in this directory has run them'
2660
2727
  : `no session pointer from the last 24 hours — ${hookActivationAdvice(activationInstallations)}`,
2728
+ ...(fired
2729
+ ? {}
2730
+ : {
2731
+ remedy: {
2732
+ summary: 'send one prompt in a session here, then re-check',
2733
+ command: 'notifai doctor',
2734
+ },
2735
+ }),
2661
2736
  });
2662
2737
  if (installations.some((installation) => installation.harness === 'opencode')) {
2663
2738
  checks.push({