@indigoai-us/hq-cli 5.345.57 → 5.345.59

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/command-catalog.generated.d.ts +12 -0
  3. package/dist/command-catalog.generated.js +15 -0
  4. package/dist/commands/access.d.ts +14 -0
  5. package/dist/commands/access.js +69 -1
  6. package/dist/commands/agent-kit.js +20 -10
  7. package/dist/commands/billing.d.ts +13 -0
  8. package/dist/commands/billing.js +31 -0
  9. package/dist/commands/db-dump.js +2 -2
  10. package/dist/commands/db-remote-deps.js +6 -0
  11. package/dist/commands/lanes-loop.js +3 -1
  12. package/dist/commands/lanes-questions.js +16 -0
  13. package/dist/commands/lanes-session-start.js +19 -3
  14. package/dist/commands/lanes-shared.d.ts +1 -0
  15. package/dist/commands/lanes.d.ts +3 -1
  16. package/dist/commands/lanes.js +52 -2
  17. package/dist/lib/agent-kit/inbox-poll-backoff-flag.d.ts +19 -0
  18. package/dist/lib/agent-kit/inbox-poll-backoff-flag.js +70 -0
  19. package/dist/lib/agent-kit/run/inbox.d.ts +7 -3
  20. package/dist/lib/agent-kit/run/inbox.js +45 -6
  21. package/dist/lib/agent-kit/run/stop-signals.d.ts +14 -0
  22. package/dist/lib/agent-kit/run/stop-signals.js +26 -0
  23. package/dist/lib/agent-kit/services.d.ts +2 -2
  24. package/dist/lib/agent-kit/services.js +5 -3
  25. package/dist/lib/anywhere-runtime-flag-key.d.ts +1 -0
  26. package/dist/lib/anywhere-runtime-flag-key.js +1 -0
  27. package/dist/lib/anywhere-runtime-revocation-marker.d.ts +4 -0
  28. package/dist/lib/anywhere-runtime-revocation-marker.js +26 -0
  29. package/dist/lib/daemon/run.d.ts +2 -0
  30. package/dist/lib/daemon/run.js +17 -4
  31. package/dist/lib/daemon/server.js +7 -0
  32. package/dist/lib/db/control-plane.d.ts +12 -1
  33. package/dist/lib/db/usage-format.d.ts +2 -1
  34. package/dist/lib/db/usage-format.js +19 -0
  35. package/dist/lib/install/provider-wrappers.js +13 -0
  36. package/dist/lib/lanes/loop.d.ts +23 -1
  37. package/dist/lib/lanes/loop.js +134 -19
  38. package/dist/lib/lanes/questions.d.ts +5 -0
  39. package/dist/lib/lanes/reconcile.js +3 -0
  40. package/dist/lib/lanes/story-ledger.js +30 -20
  41. package/dist/lib/lanes/types.js +1 -1
  42. package/package.json +1 -1
@@ -14,6 +14,16 @@ export function loopAdmissionPath(state, hasPendingResume, initialAdmission) {
14
14
  return "deferred";
15
15
  return "resume";
16
16
  }
17
+ function asidePath(file, marker) {
18
+ const parsed = path.parse(file);
19
+ let suffix = 1;
20
+ let candidate;
21
+ do {
22
+ candidate = path.join(parsed.dir, `${parsed.name}.${marker}.${suffix}${parsed.ext}`);
23
+ suffix += 1;
24
+ } while (fs.existsSync(candidate));
25
+ return candidate;
26
+ }
17
27
  function isRecord(value) {
18
28
  return typeof value === "object" && value !== null && !Array.isArray(value);
19
29
  }
@@ -331,11 +341,49 @@ export function enqueueLoopEnvelope(input) {
331
341
  }
332
342
  })
333
343
  .filter((record) => record !== undefined);
334
- if (records.some((record) => record.envelope_id === envelopeId) || fs.existsSync(path.join(resultsDir(input.runDir), resultName(envelopeId)))) {
344
+ const withdrawnDir = path.join(input.runDir, "loop", "withdrawn");
345
+ const stalledDir = path.join(input.runDir, "loop", "stalled");
346
+ const reusableRecords = records.filter((record) => record.envelope_id === envelopeId);
347
+ const terminalResult = fs.existsSync(path.join(resultsDir(input.runDir), resultName(envelopeId)))
348
+ ? readJson(path.join(resultsDir(input.runDir), resultName(envelopeId))) : undefined;
349
+ const reusable = reusableRecords.length > 0 && reusableRecords.every((record) => [stalledDir, withdrawnDir].some((dir) => envelopeFiles(dir).some((file) => {
350
+ try {
351
+ return fileEnvelope(file).envelope_id === record.envelope_id;
352
+ }
353
+ catch {
354
+ return false;
355
+ }
356
+ }))) && (!isRecord(terminalResult) || terminalResult.status === "stalled");
357
+ if ((records.some((record) => record.envelope_id === envelopeId) && !reusable) ||
358
+ (terminalResult !== undefined && (!reusable || !isRecord(terminalResult) || terminalResult.status !== "stalled"))) {
335
359
  throw new Error(`envelope id ${envelopeId} was already used in this loop lane`);
336
360
  }
361
+ if (reusable) {
362
+ for (const file of envelopeFiles(withdrawnDir).concat(envelopeFiles(stalledDir))) {
363
+ try {
364
+ if (fileEnvelope(file).envelope_id === envelopeId)
365
+ fs.renameSync(file, asidePath(file, path.basename(path.dirname(file))));
366
+ }
367
+ catch (error) {
368
+ if (error.code !== "ENOENT")
369
+ throw error;
370
+ }
371
+ }
372
+ if (terminalResult !== undefined) {
373
+ const resultFile = path.join(resultsDir(input.runDir), resultName(envelopeId));
374
+ fs.renameSync(resultFile, asidePath(resultFile, "stalled"));
375
+ }
376
+ }
337
377
  const incomingStop = envelope.kind === "stop";
338
- if (records.some((record) => record.envelope.kind === "stop")) {
378
+ const blockingStop = [pendingDir(input.runDir), activeDir(input.runDir)].some((scanDir) => envelopeFiles(scanDir).some((file) => {
379
+ try {
380
+ return fileEnvelope(file).envelope.kind === "stop";
381
+ }
382
+ catch {
383
+ return false;
384
+ }
385
+ }));
386
+ if (blockingStop) {
339
387
  throw new Error(incomingStop ? "a stop envelope is already queued or active" : "cannot enqueue a phase after a stop envelope");
340
388
  }
341
389
  const sequence = nextQueueSequence(input.runDir);
@@ -405,6 +453,77 @@ export function interruptLoopQueue(runDir) {
405
453
  }
406
454
  return { withdrawn, already_picked_up: alreadyPickedUp };
407
455
  }
456
+ export function interruptLoopQueueForStory(runDir, storyId, phase) {
457
+ const withdrawn = [];
458
+ const alreadyPickedUp = [];
459
+ const matches = (record) => {
460
+ const envelope = record.envelope;
461
+ return envelope.story_id === storyId && (phase === undefined || envelope.phase === phase);
462
+ };
463
+ for (const file of envelopeFiles(pendingDir(runDir))) {
464
+ const record = fileEnvelope(file);
465
+ if (!matches(record))
466
+ continue;
467
+ const destination = path.join(runDir, "loop", "withdrawn", path.basename(file));
468
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
469
+ fs.renameSync(file, destination);
470
+ withdrawn.push(record.envelope_id);
471
+ }
472
+ for (const file of envelopeFiles(activeDir(runDir))) {
473
+ const record = fileEnvelope(file);
474
+ if (matches(record))
475
+ alreadyPickedUp.push(record.envelope_id);
476
+ }
477
+ return { withdrawn, already_picked_up: alreadyPickedUp };
478
+ }
479
+ export function applyLoopStallAnswer(input) {
480
+ const results = fs.existsSync(resultsDir(input.runDir))
481
+ ? fs.readdirSync(resultsDir(input.runDir)).filter((name) => name.endsWith(".json")) : [];
482
+ const resultPath = results.map((name) => path.join(resultsDir(input.runDir), name)).find((file) => {
483
+ try {
484
+ const result = readJson(file);
485
+ return isRecord(result) && result.status === "stalled" && result.question_id === input.questionId;
486
+ }
487
+ catch (error) {
488
+ if (error instanceof SyntaxError)
489
+ return false;
490
+ throw error;
491
+ }
492
+ });
493
+ if (!resultPath)
494
+ return undefined;
495
+ const result = readJson(resultPath);
496
+ const envelopeFile = envelopeFiles(path.join(input.runDir, "loop", "stalled")).find((file) => {
497
+ try {
498
+ return fileEnvelope(file).envelope_id === result.envelope_id;
499
+ }
500
+ catch {
501
+ return false;
502
+ }
503
+ });
504
+ if (!envelopeFile)
505
+ throw new Error(`stalled loop envelope ${result.envelope_id} is missing`);
506
+ const record = fileEnvelope(envelopeFile);
507
+ const action = input.choice === "Retry with a longer deadline" ? "requeued" : "parked";
508
+ if (action === "requeued") {
509
+ record.deadline_budget_seconds = Math.max(1, record.deadline_budget_seconds ?? phaseDeadlineBudget(record.envelope, 3600)) * 2;
510
+ const head = path.join(pendingDir(input.runDir), `0000000000000-answer-${path.basename(envelopeFile)}`);
511
+ fs.mkdirSync(pendingDir(input.runDir), { recursive: true });
512
+ fs.renameSync(envelopeFile, head);
513
+ fs.renameSync(resultPath, asidePath(resultPath, "stalled"));
514
+ const pending = fileEnvelope(head);
515
+ pending.deadline_budget_seconds = record.deadline_budget_seconds;
516
+ atomicJson(head, pending);
517
+ persistStatus(input.runDir, path.basename(input.runDir), { stall_counts: { [record.envelope_id]: 0 } });
518
+ }
519
+ else {
520
+ atomicJson(resultPath, {
521
+ ...result,
522
+ decision: { question_id: input.questionId, choice: input.choice, ...(input.notes ? { notes: input.notes } : {}), at: input.at },
523
+ });
524
+ }
525
+ return { lane_id: path.basename(input.runDir), envelope_id: result.envelope_id, action };
526
+ }
408
527
  export function loopStatus(runDir, laneId) {
409
528
  const loop = fs.existsSync(loopPath(runDir)) && isRecord(readJson(loopPath(runDir))) ? readJson(loopPath(runDir)) : {};
410
529
  const active = envelopeFiles(activeDir(runDir)).map(fileEnvelope)[0];
@@ -578,22 +697,16 @@ export async function runLoopLane(options) {
578
697
  const previousSession = sessions.get(sessionKey);
579
698
  const startedAt = now().toISOString();
580
699
  const startedAtMs = Date.parse(startedAt);
581
- let deadlineAtMs = record.deadline_at_ms;
582
- if (deadlineAtMs === undefined) {
583
- const absoluteDeadline = envelope.deadline;
584
- if (typeof absoluteDeadline === "string")
585
- deadlineAtMs = Date.parse(absoluteDeadline);
586
- else {
587
- const budget = record.deadline_budget_seconds ?? firstDeadline.get(record.envelope_id) ?? phaseDeadlineBudget(envelope, defaultDeadlineSeconds);
588
- firstDeadline.set(record.envelope_id, budget);
589
- record.deadline_budget_seconds = budget;
590
- deadlineAtMs = startedAtMs + budget * 1000;
591
- }
592
- record.deadline_at_ms = deadlineAtMs;
593
- }
700
+ const absoluteDeadline = envelope.deadline;
701
+ const budget = record.deadline_budget_seconds ?? firstDeadline.get(record.envelope_id) ??
702
+ (typeof absoluteDeadline === "string"
703
+ ? Math.max(1, Math.round((Date.parse(absoluteDeadline) - startedAtMs) / 1000))
704
+ : phaseDeadlineBudget(envelope, defaultDeadlineSeconds));
705
+ firstDeadline.set(record.envelope_id, budget);
706
+ record.deadline_budget_seconds = budget;
594
707
  atomicJson(file, record);
595
708
  persistStatus(options.runDir, options.lane.lane_id, { state: "running", queue_depth: listLoopQueue(options.runDir).length, active_envelope_id: record.envelope_id, active_phase: phaseName(envelope, record.envelope_id), stall_counts: Object.fromEntries(stalls) });
596
- const timer = abortAfter(deadlineAtMs - startedAtMs, options.signal);
709
+ const timer = abortAfter(budget * 1000, options.signal);
597
710
  try {
598
711
  let result;
599
712
  let fallback = false;
@@ -673,9 +786,11 @@ export async function runLoopLane(options) {
673
786
  continue;
674
787
  }
675
788
  const message = error instanceof Error ? error.message : String(error);
676
- const result = { schema_version: LOOP_SCHEMA, envelope_id: record.envelope_id, status: "stalled", started_at: startedAt, finished_at: now().toISOString(), error: message };
789
+ const questionId = options.raiseQuestion ? randomUUID() : undefined;
790
+ const result = { schema_version: LOOP_SCHEMA, envelope_id: record.envelope_id, status: "stalled", started_at: startedAt, finished_at: now().toISOString(), error: message, ...(questionId ? { question_id: questionId } : {}) };
677
791
  writeResultAt(options.runDir, file, record, result);
678
- await options.raiseQuestion?.({ envelope, envelopeId: record.envelope_id, attempt, error: message });
792
+ if (questionId)
793
+ await options.raiseQuestion?.({ envelope, envelopeId: record.envelope_id, attempt, error: message, questionId });
679
794
  completed += 1;
680
795
  persistStatus(options.runDir, options.lane.lane_id, { state: "parked", queue_depth: listLoopQueue(options.runDir).length, active_envelope_id: null, active_phase: null, stall_counts: Object.fromEntries(stalls) });
681
796
  releaseRunner();
@@ -715,7 +830,7 @@ function writeResultAt(runDir, activeFile, record, result) {
715
830
  summary: stalled ? "Phase stalled twice and needs operator direction." : "Phase execution failed.",
716
831
  files_changed: [], commits: [],
717
832
  back_pressure: { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" },
718
- context_for_next: result.error ?? result.status,
833
+ context_for_next: [result.error ?? result.status, ...(result.question_id ? [`Decision question: ${result.question_id}`] : [])].join("\n"),
719
834
  };
720
835
  atomicJson(path.resolve(externalPath), failure);
721
836
  }
@@ -10,6 +10,11 @@ export interface QuestionBody {
10
10
  options: string[];
11
11
  recommended?: string;
12
12
  notes?: string;
13
+ loop_stall?: {
14
+ lane_id: string;
15
+ envelope_id: string;
16
+ attempt: number;
17
+ };
13
18
  }
14
19
  export declare function assertQuestionBodySecretSafe(body: QuestionBody): void;
15
20
  /** Errors that make an envelope ask unsafe or impossible to persist. */
@@ -12,6 +12,7 @@ import { compareLaneQueueOrder, compareLaneQueueOrderWithLeads, freeClaudeAccoun
12
12
  import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
13
13
  import { effectiveAccountHome } from "./account-home.js";
14
14
  import { enforceRegistryApprovalGate } from "./approval-gate.js";
15
+ import { loopStatus } from "./loop.js";
15
16
  import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
16
17
  import { MAX_LANE_DEPTH } from "./constants.js";
17
18
  import { laneDepthCap } from "./depth-cap.js";
@@ -3600,6 +3601,8 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
3600
3601
  const current = readLane(hqRoot, lane.lane_id);
3601
3602
  if (current.state !== "awaiting_input")
3602
3603
  continue;
3604
+ if (current.loop_mode === true && loopStatus(laneRunDir(hqRoot, current.lane_id), current.lane_id).state === "parked")
3605
+ continue;
3603
3606
  const pending = questions.filter((q) => q.origin.id === lane.lane_id && q.status === "pending");
3604
3607
  const answered = questions.filter((q) => q.origin.id === lane.lane_id && q.status === "answered");
3605
3608
  if (pending.length > 0 || answered.length === 0)
@@ -9,7 +9,8 @@ const LOCK_TIMEOUT_MS = 10_000;
9
9
  const LOCK_STALE_MS = 60_000;
10
10
  const LOCK_RETRY_MS = 10;
11
11
  const STORY_STATES = ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped"];
12
- const MANAGER_OWNED_STATES = new Set(["review", "done", "dropped"]);
12
+ const LEAD_CONTROLLED_STATES = new Set(["waiting", "blocked", "dropped"]);
13
+ const STORY_STATE_ORDER = ["todo", "in_progress", "review", "done"];
13
14
  const TERMINAL_STATES = new Set(["done", "failed", "interrupted", "cancelled", "killed"]);
14
15
  function projectFocusOverrides(value) {
15
16
  if (value === undefined)
@@ -561,6 +562,13 @@ function validPriorQuestions(value, storyId) {
561
562
  }
562
563
  return value;
563
564
  }
565
+ function canonicalStoryState(value) {
566
+ if (value === "in_review" || value === "verification")
567
+ return "review";
568
+ return typeof value === "string" && STORY_STATES.includes(value)
569
+ ? value
570
+ : undefined;
571
+ }
564
572
  function setLaneStatusFields(story, lane, laneStoryStatus, isCurrentAttribution) {
565
573
  const laneStatus = story.lane_status;
566
574
  if (laneStatus !== undefined && !isJsonObject(laneStatus)) {
@@ -615,29 +623,31 @@ function setLaneStatusFields(story, lane, laneStoryStatus, isCurrentAttribution)
615
623
  else {
616
624
  delete story.pending_questions;
617
625
  }
618
- if (story.state === "in_review" || story.state === "verification") {
619
- story.state = "review";
620
- }
621
- if (MANAGER_OWNED_STATES.has(String(story.state))) {
622
- // A manager decision always wins over lane-derived status.
623
- }
624
- else if (blockers.length > 0) {
625
- story.state = "blocked";
626
- }
627
- else if (laneStoryStatus === "review") {
628
- story.state = "review";
629
- }
630
- else if (laneStoryStatus === "done") {
626
+ const currentState = canonicalStoryState(story.state) ?? canonicalStoryState(story.status);
627
+ const currentStates = currentState === undefined ? [] : [currentState];
628
+ const laneProgressState = laneStoryStatus === "review"
629
+ ? "review"
630
+ : laneStoryStatus === "done"
631
+ ? "done"
632
+ : laneStoryStatus === "in_progress" || lane.state === "queued" || lane.state === "running" || lane.state === "awaiting_input"
633
+ ? "in_progress"
634
+ : undefined;
635
+ const currentProgressState = STORY_STATE_ORDER.reduce((highest, state) => currentStates.includes(state) || laneProgressState === state ? state : highest, undefined);
636
+ const laneManagedBlocked = isJsonObject(laneStatus) && isJsonObject(laneStatus.saved);
637
+ const leadControlledState = currentStates.find((state) => LEAD_CONTROLLED_STATES.has(state) && !(state === "blocked" && laneManagedBlocked));
638
+ if (currentStates.includes("done"))
631
639
  story.state = "done";
640
+ else if (leadControlledState !== undefined)
641
+ story.state = leadControlledState;
642
+ else if (blockers.length > 0 && currentProgressState !== "review" && currentProgressState !== "done") {
643
+ story.state = "blocked";
632
644
  }
633
- else if (laneStoryStatus === "in_progress" || lane.state === "queued" || lane.state === "running" || lane.state === "awaiting_input") {
634
- story.state = "in_progress";
645
+ else if (currentProgressState !== undefined) {
646
+ story.state = currentProgressState;
635
647
  }
636
648
  else if (saved !== undefined && story.state === "blocked") {
637
- const previous = saved.state;
638
- story.state = typeof previous === "string" && STORY_STATES.includes(previous)
639
- ? previous
640
- : "in_progress";
649
+ const previous = canonicalStoryState(saved.state);
650
+ story.state = previous ?? "in_progress";
641
651
  }
642
652
  if (typeof story.state === "string" && STORY_STATES.includes(story.state)) {
643
653
  story.status = story.state;
@@ -236,7 +236,7 @@ export const LANE_FIELD_WRITERS = {
236
236
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
237
237
  keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile", "spawn"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile", "spawn"], keep_alive_next_eligible_at: ["command", "reconcile", "spawn"], keep_alive_exhausted_at: ["command", "reconcile", "spawn"], loop_mode: ["creator"],
238
238
  provider: ["creator", "command"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
239
- version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["watcher", "reconcile"],
239
+ version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["command", "watcher", "reconcile"],
240
240
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
241
241
  last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
242
242
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.57",
3
+ "version": "5.345.59",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {