@indigoai-us/hq-cli 5.300.0 → 5.302.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
  3. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  4. package/dist/command-catalog.generated.d.ts +32 -3
  5. package/dist/command-catalog.generated.js +39 -3
  6. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  7. package/dist/commands/__fixtures__/access-vault.js +1 -1
  8. package/dist/commands/access.d.ts +2 -1
  9. package/dist/commands/access.js +58 -49
  10. package/dist/commands/bot.js +1 -0
  11. package/dist/commands/cloud-provision.js +51 -2
  12. package/dist/commands/core-checkpoint.js +1 -0
  13. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  14. package/dist/commands/core-company-index-scope-flag.js +6 -0
  15. package/dist/commands/core-handoff-fast.js +1 -0
  16. package/dist/commands/core-handoff.js +2 -0
  17. package/dist/commands/core-job-native-utils.d.ts +6 -0
  18. package/dist/commands/core-job-native-utils.js +80 -0
  19. package/dist/commands/core-job-native.d.ts +2 -4
  20. package/dist/commands/core-job-native.js +6 -10
  21. package/dist/commands/core-job-notify-native.d.ts +36 -0
  22. package/dist/commands/core-job-notify-native.js +359 -0
  23. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  24. package/dist/commands/core-job-remediate-native.js +374 -0
  25. package/dist/commands/core.js +26 -8
  26. package/dist/commands/deploy-comments.js +5 -2
  27. package/dist/commands/files.js +31 -14
  28. package/dist/commands/integrations-api.d.ts +6 -6
  29. package/dist/commands/integrations-api.js +4 -13
  30. package/dist/commands/integrations-connect.d.ts +3 -0
  31. package/dist/commands/integrations-connect.js +21 -2
  32. package/dist/commands/integrations-manage.js +1 -4
  33. package/dist/commands/lanes-message.js +43 -5
  34. package/dist/commands/lanes-questions.d.ts +4 -2
  35. package/dist/commands/lanes-questions.js +123 -2
  36. package/dist/commands/lanes-resume.js +25 -4
  37. package/dist/commands/lanes-shared.d.ts +15 -1
  38. package/dist/commands/lanes-watch.js +5 -1
  39. package/dist/commands/lanes.d.ts +2 -0
  40. package/dist/commands/lanes.js +92 -9
  41. package/dist/commands/meetings.js +35 -16
  42. package/dist/commands/monitor.js +17 -2
  43. package/dist/commands/report.js +5 -2
  44. package/dist/commands/rescue.d.ts +4 -0
  45. package/dist/commands/rescue.js +27 -17
  46. package/dist/commands/safe-extract.d.ts +4 -4
  47. package/dist/commands/safe-extract.js +6 -7
  48. package/dist/lib/agent-kit/fallback.d.ts +1 -0
  49. package/dist/lib/agent-kit/fallback.js +1 -0
  50. package/dist/lib/bot/daemon.d.ts +5 -1
  51. package/dist/lib/bot/daemon.js +4 -4
  52. package/dist/lib/core-utils/soft-timeout.js +1 -1
  53. package/dist/lib/core-utils/token-usage-report.js +17 -3
  54. package/dist/lib/deploy-apps.d.ts +13 -0
  55. package/dist/lib/deploy-apps.js +24 -0
  56. package/dist/lib/index-render/companies.d.ts +4 -1
  57. package/dist/lib/index-render/companies.js +66 -3
  58. package/dist/lib/index-render/index.d.ts +1 -0
  59. package/dist/lib/index-render/index.js +2 -1
  60. package/dist/lib/index-render/projects.js +82 -5
  61. package/dist/lib/index-render/shared.d.ts +1 -1
  62. package/dist/lib/index-render/shared.js +4 -2
  63. package/dist/lib/lanes/delivery.d.ts +7 -3
  64. package/dist/lib/lanes/delivery.js +18 -8
  65. package/dist/lib/lanes/index.d.ts +1 -1
  66. package/dist/lib/lanes/keep-alive.d.ts +13 -3
  67. package/dist/lib/lanes/keep-alive.js +29 -5
  68. package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
  69. package/dist/lib/lanes/outpost-dispatch.js +12 -0
  70. package/dist/lib/lanes/reconcile.d.ts +1 -0
  71. package/dist/lib/lanes/reconcile.js +358 -38
  72. package/dist/lib/lanes/spawn.d.ts +1 -0
  73. package/dist/lib/lanes/spawn.js +20 -10
  74. package/dist/lib/lanes/store.js +24 -0
  75. package/dist/lib/lanes/story-ledger.d.ts +2 -0
  76. package/dist/lib/lanes/story-ledger.js +8 -0
  77. package/dist/lib/lanes/types.d.ts +26 -4
  78. package/dist/lib/lanes/types.js +33 -1
  79. package/dist/lib/lanes/watch.d.ts +1 -0
  80. package/dist/lib/lanes/watch.js +19 -6
  81. package/dist/lib/lanes/watcher.js +139 -9
  82. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  83. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  84. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  85. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  86. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  87. package/dist/lib/mesh/live/daemon/run.js +36 -2
  88. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  89. package/dist/lib/search-index/background.d.ts +1 -0
  90. package/dist/lib/search-index/background.js +1 -0
  91. package/dist/utils/self-update.js +51 -4
  92. package/dist/utils/staged-npm-install.d.ts +16 -0
  93. package/dist/utils/staged-npm-install.js +264 -0
  94. package/dist/utils/update-command-supervisor.cjs +54 -1
  95. package/dist/utils/version-gate.d.ts +8 -0
  96. package/dist/utils/version-gate.js +83 -16
  97. package/package.json +2 -2
  98. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  99. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -120,7 +120,7 @@ function comparison(projectDirs, before, after, minHours, json, stdout) {
120
120
  /** Print token usage totals from Claude session JSONL files. */
121
121
  export function tokenUsageReport(args = [], options = {}) {
122
122
  const { stdout, stderr } = ioFor(options);
123
- let lastDays = 7, since = "", json = false, before = "", after = "", minHours = 0;
123
+ let lastDays = 7, since = "", json = false, before = "", after = "", minHours = 0, projectDir = options.projectDir, projectDirSeen = false;
124
124
  for (let index = 0; index < args.length;) {
125
125
  const arg = args[index];
126
126
  if (arg === "--last") {
@@ -149,8 +149,22 @@ export function tokenUsageReport(args = [], options = {}) {
149
149
  minHours = Number.parseFloat(args[index + 1] ?? "");
150
150
  index += 2;
151
151
  }
152
+ else if (arg === "--project-dir") {
153
+ const value = args[index + 1];
154
+ if (!value || value.startsWith("--")) {
155
+ line(stderr, "--project-dir requires a path");
156
+ return 1;
157
+ }
158
+ if (projectDirSeen) {
159
+ line(stderr, "--project-dir may be specified only once");
160
+ return 1;
161
+ }
162
+ projectDir = value;
163
+ projectDirSeen = true;
164
+ index += 2;
165
+ }
152
166
  else if (arg === "-h" || arg === "--help") {
153
- stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
167
+ stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\n scripts/token-usage-report.sh --project-dir <path> # use one Claude activity folder\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
154
168
  return 0;
155
169
  }
156
170
  else {
@@ -158,7 +172,7 @@ export function tokenUsageReport(args = [], options = {}) {
158
172
  return 1;
159
173
  }
160
174
  }
161
- const projectDirs = resolveProjectDirs(options).filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
175
+ const projectDirs = resolveProjectDirs({ ...options, projectDir }).filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
162
176
  if (before && after) {
163
177
  comparison(projectDirs, before, after, minHours, json, stdout);
164
178
  return 0;
@@ -0,0 +1,13 @@
1
+ /** Pagination for org-scoped hq-deploy app listings. */
2
+ export declare const MAX_DEPLOY_APPS_PAGES = 100;
3
+ export interface DeployAppsPage<T> {
4
+ apps?: unknown;
5
+ nextCursor?: unknown;
6
+ }
7
+ /**
8
+ * Collect every page from GET /api/apps. Older hq-deploy versions omit
9
+ * nextCursor, which remains a valid single-page response. The cap prevents a
10
+ * bad cursor response from creating an unbounded CLI request loop.
11
+ */
12
+ export declare function listDeployApps<T>(fetchPage: (cursor?: string) => Promise<DeployAppsPage<T>>): Promise<T[]>;
13
+ //# sourceMappingURL=deploy-apps.d.ts.map
@@ -0,0 +1,24 @@
1
+ /** Pagination for org-scoped hq-deploy app listings. */
2
+ export const MAX_DEPLOY_APPS_PAGES = 100;
3
+ /**
4
+ * Collect every page from GET /api/apps. Older hq-deploy versions omit
5
+ * nextCursor, which remains a valid single-page response. The cap prevents a
6
+ * bad cursor response from creating an unbounded CLI request loop.
7
+ */
8
+ export async function listDeployApps(fetchPage) {
9
+ const apps = [];
10
+ let cursor;
11
+ for (let page = 0; page < MAX_DEPLOY_APPS_PAGES; page += 1) {
12
+ const result = await fetchPage(cursor);
13
+ if (Array.isArray(result.apps))
14
+ apps.push(...result.apps);
15
+ if (result.nextCursor === undefined || result.nextCursor === null)
16
+ return apps;
17
+ if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
18
+ throw new Error("hq-deploy returned an invalid apps-list cursor.");
19
+ }
20
+ cursor = result.nextCursor;
21
+ }
22
+ throw new Error(`hq-deploy apps list exceeded the ${MAX_DEPLOY_APPS_PAGES}-page safety cap.`);
23
+ }
24
+ //# sourceMappingURL=deploy-apps.js.map
@@ -1,3 +1,6 @@
1
1
  import { RenderContext, RenderResult } from "./shared.js";
2
- export declare function renderCompanies(context: RenderContext): RenderResult;
2
+ export declare class CompanyIndexUserError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function renderCompanies(context: RenderContext, args?: string[]): RenderResult;
3
6
  //# sourceMappingURL=companies.d.ts.map
@@ -1,6 +1,12 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { basename, countImmediate, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, titleize, truncate, write } from "./shared.js";
4
+ export class CompanyIndexUserError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "CompanyIndexUserError";
8
+ }
9
+ }
4
10
  function description(root, company, name) {
5
11
  const base = `companies/${company}/${name}`;
6
12
  const nonHidden = (child) => !isHidden(child);
@@ -17,9 +23,66 @@ function description(root, company, name) {
17
23
  default: return "—";
18
24
  }
19
25
  }
20
- export function renderCompanies(context) {
26
+ export function renderCompanies(context, args = []) {
27
+ if (args.some((arg) => arg === "--help" || arg === "-h")) {
28
+ (context.output ?? ((message) => process.stdout.write(message)))("Usage: hq core rebuild-index companies [--company <slug>]\n\n" +
29
+ "Regenerate company root INDEX.md files. Without --company, all companies are regenerated.\n" +
30
+ "--company <slug> regenerates only that company's index.\n" +
31
+ "Company-scoped writes require the cli.rebuild-index-company-scope flag.\n");
32
+ return { written: [] };
33
+ }
34
+ let selectedCompany;
35
+ for (let index = 0; index < args.length; index += 1) {
36
+ const arg = args[index];
37
+ if (arg === "--company") {
38
+ if (selectedCompany !== undefined)
39
+ throw new CompanyIndexUserError("rebuild-index companies: --company may be supplied once");
40
+ const value = args[index + 1];
41
+ if (!value || value.startsWith("--"))
42
+ throw new CompanyIndexUserError("rebuild-index companies: --company requires a company slug");
43
+ selectedCompany = value;
44
+ index += 1;
45
+ }
46
+ else if (arg.startsWith("--company=")) {
47
+ if (selectedCompany !== undefined)
48
+ throw new CompanyIndexUserError("rebuild-index companies: --company may be supplied once");
49
+ selectedCompany = arg.slice("--company=".length);
50
+ if (!selectedCompany)
51
+ throw new CompanyIndexUserError("rebuild-index companies: --company requires a company slug");
52
+ }
53
+ else {
54
+ throw new CompanyIndexUserError(`rebuild-index companies: unknown option ${arg}`);
55
+ }
56
+ }
21
57
  const written = [];
22
- for (const directory of immediateEntries(context.root, "companies", "dir")) {
58
+ let scopedDirectories;
59
+ if (selectedCompany === undefined) {
60
+ scopedDirectories = immediateEntries(context.root, "companies", "dir");
61
+ }
62
+ else {
63
+ if (selectedCompany === "." || selectedCompany === ".." ||
64
+ selectedCompany.includes("/") || selectedCompany.includes("\\") ||
65
+ path.basename(selectedCompany) !== selectedCompany ||
66
+ selectedCompany.startsWith("_") || isHidden(selectedCompany)) {
67
+ throw new CompanyIndexUserError(`rebuild-index companies: invalid company slug "${selectedCompany}"`);
68
+ }
69
+ const companiesRoot = path.resolve(context.root, "companies");
70
+ const selectedDirectory = path.resolve(companiesRoot, selectedCompany);
71
+ let selectedStat;
72
+ try {
73
+ selectedStat = fs.lstatSync(selectedDirectory);
74
+ }
75
+ catch (cause) {
76
+ if (cause.code !== "ENOENT")
77
+ throw cause;
78
+ throw new CompanyIndexUserError(`rebuild-index companies: no company directory named "${selectedCompany}"`);
79
+ }
80
+ if (!selectedStat.isDirectory()) {
81
+ throw new CompanyIndexUserError(`rebuild-index companies: no company directory named "${selectedCompany}"`);
82
+ }
83
+ scopedDirectories = [selectedDirectory];
84
+ }
85
+ for (const directory of scopedDirectories) {
23
86
  const company = basename(directory);
24
87
  if (company.startsWith("_") || isHidden(company))
25
88
  continue;
@@ -40,7 +103,7 @@ export function renderCompanies(context) {
40
103
  const parsed = readJson(prd);
41
104
  let text = typeof parsed?.description === "string" ? parsed.description : "";
42
105
  text = truncate(sanitize(text), 110) || "—";
43
- lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active")} | ${text} |`);
106
+ lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active", prd)} | ${text} |`);
44
107
  }
45
108
  }
46
109
  else
@@ -2,4 +2,5 @@ import { RenderContext, RenderResult } from "./shared.js";
2
2
  export type IndexTarget = "all" | "companies" | "company-knowledge" | "orchestrator" | "projects" | "public-knowledge" | "reports" | "social-drafts" | "threads" | "workers";
3
3
  export declare function renderIndexTarget(target: IndexTarget, context: RenderContext, args?: string[]): RenderResult;
4
4
  export { atomicWrite, comparePath, immediateEntries, type RenderContext, type RenderResult } from "./shared.js";
5
+ export { CompanyIndexUserError } from "./companies.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -10,7 +10,7 @@ import { renderWorkers } from "./workers.js";
10
10
  const allOrder = ["threads", "orchestrator", "companies", "projects", "company-knowledge", "public-knowledge", "workers", "reports", "social-drafts"];
11
11
  export function renderIndexTarget(target, context, args = []) {
12
12
  switch (target) {
13
- case "companies": return renderCompanies(context);
13
+ case "companies": return renderCompanies(context, args);
14
14
  case "company-knowledge": return renderCompanyKnowledge(context);
15
15
  case "orchestrator": return renderOrchestrator(context);
16
16
  case "projects": return renderProjects(context);
@@ -40,4 +40,5 @@ export function renderIndexTarget(target, context, args = []) {
40
40
  }
41
41
  }
42
42
  export { atomicWrite, comparePath, immediateEntries } from "./shared.js";
43
+ export { CompanyIndexUserError } from "./companies.js";
43
44
  //# sourceMappingURL=index.js.map
@@ -1,16 +1,92 @@
1
+ import * as fs from "fs";
1
2
  import * as path from "path";
2
- import { basename, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, truncate, write } from "./shared.js";
3
+ import { at, basename, date, exists, extractKeepFences, generatedFingerprint, generatedStamp, hasUnclosedKeepMarker, immediateEntries, isHidden, log, looksAutoGenerated, projectStatus, readJson, readText, sanitize, stampGenerated, titleize, truncate, write, } from "./shared.js";
4
+ function composeCompanyProjectsIndex(raw, existing) {
5
+ const stamped = stampGenerated(raw);
6
+ const fences = existing === undefined ? "" : extractKeepFences(existing);
7
+ if (!fences)
8
+ return stamped;
9
+ return `${stamped.replace(/\n+$/, "\n")}\n${fences}\n`;
10
+ }
11
+ function shouldOverwriteCompanyProjectsIndex(context, existing, next) {
12
+ if (existing === undefined)
13
+ return "write";
14
+ if (context.force)
15
+ return "write";
16
+ if (hasUnclosedKeepMarker(existing))
17
+ return "skip";
18
+ const stored = generatedStamp(existing);
19
+ if (stored && stored === generatedFingerprint(existing))
20
+ return "write";
21
+ if (stored)
22
+ return "refuse";
23
+ if (looksAutoGenerated(existing))
24
+ return "write";
25
+ if (generatedFingerprint(existing) === generatedFingerprint(next))
26
+ return "write";
27
+ return "refuse";
28
+ }
29
+ function renderCompanyProjects(context, company, projectRoot) {
30
+ const written = [];
31
+ const lines = [`# ${titleize(company)} Projects`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Project | Status | Description |", "|---------|--------|-------------|"];
32
+ for (const directory of immediateEntries(context.root, projectRoot, "dir")) {
33
+ const name = basename(directory);
34
+ if (name.startsWith("_") || isHidden(name))
35
+ continue;
36
+ const prd = path.join(directory, "prd.json");
37
+ const parsed = readJson(prd);
38
+ const description = truncate(sanitize(typeof parsed?.description === "string" ? parsed.description : ""), 110) || "—";
39
+ lines.push(`| \`${name}\` | ${projectStatus(context.root, name, prd, "active", prd)} | ${description} |`);
40
+ }
41
+ lines.push("");
42
+ const output = `${projectRoot}/INDEX.md`;
43
+ const existing = readText(at(context.root, output));
44
+ // An unreadable existing file is not an invitation to replace it.
45
+ if (existing === undefined && fs.existsSync(at(context.root, output))) {
46
+ context.refused?.push(output);
47
+ log(context, `rebuild-projects-index: refused to overwrite unreadable ${output}`);
48
+ return written;
49
+ }
50
+ const next = composeCompanyProjectsIndex(lines.join("\n"), existing);
51
+ const decision = shouldOverwriteCompanyProjectsIndex(context, existing, next);
52
+ if (decision === "skip") {
53
+ log(context, `rebuild-projects-index: skipped ${output} (<!-- hq:keep -->)`);
54
+ return written;
55
+ }
56
+ if (decision === "refuse") {
57
+ context.refused?.push(output);
58
+ log(context, `rebuild-projects-index: refused ${output} (hand-written or edited; differs from last generated output)`);
59
+ log(context, "Re-run with --accept to overwrite, or add <!-- hq:keep --> to opt out.");
60
+ return written;
61
+ }
62
+ write(context, output, next);
63
+ written.push(output);
64
+ log(context, `rebuild-projects-index: wrote ${output}`);
65
+ return written;
66
+ }
3
67
  export function renderProjects(context) {
68
+ const written = [];
69
+ const companyDirectories = immediateEntries(context.root, "companies", "dir");
70
+ for (const companyDirectory of companyDirectories) {
71
+ const company = basename(companyDirectory);
72
+ if (company.startsWith("_") || isHidden(company))
73
+ continue;
74
+ const projectRoot = ["companies", company, "projects"].join("/");
75
+ if (immediateEntries(context.root, projectRoot, "dir").length > 0 || fs.existsSync(at(context.root, projectRoot))) {
76
+ written.push(...renderCompanyProjects(context, company, projectRoot));
77
+ }
78
+ }
4
79
  const base = "personal/projects";
5
80
  if (!exists(context.root, base)) {
6
81
  log(context, "rebuild-projects-index: personal/projects/ missing, skipping");
7
- return { written: [] };
82
+ return { written };
8
83
  }
9
84
  const lines = ["# Projects", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ projects only. Company-scoped projects live in `companies/{co}/projects/`.", "", "## Company Project Directories", "", "| Company | Path |", "|---------|------|"];
10
- for (const companyDirectory of immediateEntries(context.root, "companies", "dir")) {
85
+ for (const companyDirectory of companyDirectories) {
11
86
  const company = basename(companyDirectory);
12
- if (!company.startsWith("_") && !isHidden(company) && exists(context.root, `companies/${company}/projects`))
87
+ if (!company.startsWith("_") && !isHidden(company) && fs.existsSync(at(context.root, ["companies", company, "projects"].join("/")))) {
13
88
  lines.push(`| ${company} | \`companies/${company}/projects/\` |`);
89
+ }
14
90
  }
15
91
  lines.push("", "## Personal/HQ Projects", "", "Projects here are HQ infrastructure or cross-company tools.", "", "| Project | Stories | Status | Description |", "|---------|---------|--------|-------------|");
16
92
  const projects = immediateEntries(context.root, base, "dir");
@@ -27,7 +103,8 @@ export function renderProjects(context) {
27
103
  lines.push("");
28
104
  const output = `${base}/INDEX.md`;
29
105
  write(context, output, lines.join("\n"));
106
+ written.push(output);
30
107
  log(context, `rebuild-projects-index: wrote ${output} (${projects.length} HQ project(s))`);
31
- return { written: [output] };
108
+ return { written };
32
109
  }
33
110
  //# sourceMappingURL=projects.js.map
@@ -39,7 +39,7 @@ export declare function hasUnclosedKeepMarker(content: string): boolean;
39
39
  export declare function looksAutoGenerated(content: string): boolean;
40
40
  export declare function generatedStamp(content: string): string | undefined;
41
41
  export declare function log(context: RenderContext, message: string): void;
42
- export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string): string;
42
+ export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string, expectedStatePrdPath?: string): string;
43
43
  export declare function basename(file: string): string;
44
44
  export declare function isHidden(name: string): boolean;
45
45
  export declare function mtime(file: string): number;
@@ -134,9 +134,11 @@ export function generatedStamp(content) {
134
134
  return content.match(/^<!-- hq-generated-sha256: ([a-f0-9]{64}) -->/m)?.[1];
135
135
  }
136
136
  export function log(context, message) { context.log?.(message); }
137
- export function projectStatus(root, project, prdPath, fallback) {
137
+ export function projectStatus(root, project, prdPath, fallback, expectedStatePrdPath) {
138
138
  const state = readJson(at(root, `workspace/orchestrator/${project}/state.json`));
139
- if (state) {
139
+ const stateMatchesPrd = expectedStatePrdPath === undefined || (typeof state?.prd_path === "string" &&
140
+ path.resolve(root, state.prd_path) === path.resolve(root, expectedStatePrdPath));
141
+ if (state && stateMatchesPrd) {
140
142
  const raw = typeof state.status === "string" ? state.status : "active";
141
143
  if (["completed", "complete"].includes(raw))
142
144
  return "complete";
@@ -41,8 +41,8 @@ export declare function sessionNotPortableMessage(provider: LaneProvider): strin
41
41
  * is not terminal (spec §6.7) and nothing stopped the lane deliberately.
42
42
  *
43
43
  * Refused by `hq lanes message`:
44
- * `awaiting_input` has its own path, `hq lanes questions answer`, and the
45
- * reconcile pass resumes it once every question is answered. `killed` and
44
+ * `awaiting_input` has its own path, `hq lanes questions answer`, while a
45
+ * senior may resume it when no answerable pending question remains. `killed` and
46
46
  * `cancelled` were stopped on purpose by the operator; a message must not
47
47
  * silently restart them. `failed` means the run ended without an envelope or
48
48
  * never started; only the lane's senior can explicitly recover it with
@@ -66,6 +66,8 @@ export type ResumeCommandState = (typeof RESUME_COMMAND_STATES)[number];
66
66
  export declare function isResumeCommandState(state: LaneState): state is ResumeCommandState;
67
67
  export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "none"];
68
68
  export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
69
+ /** Additional presentation-only result used by `hq lanes show`. */
70
+ export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
69
71
  export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair">;
70
72
  /** Only the terminal result of a failed automatic envelope repair is recoverable. */
71
73
  export declare function isFailedEnvelopeRepairLane(lane: DeliveryLane): boolean;
@@ -139,7 +141,8 @@ export type LaunchTrigger = {
139
141
  /** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
140
142
  kind: "resume";
141
143
  /** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
142
- from: ResumeCommandState | "failed";
144
+ from: ResumeCommandState | "failed" | "awaiting_input";
145
+ allowAwaitingInput?: true;
143
146
  fromFailed?: true;
144
147
  /** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
145
148
  expectedSenior?: SeniorRef;
@@ -158,6 +161,7 @@ export type LaunchPlan = {
158
161
  */
159
162
  export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
160
163
  allowFailed?: boolean;
164
+ allowAwaitingInput?: boolean;
161
165
  }): {
162
166
  error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
163
167
  message: string;
@@ -61,8 +61,8 @@ export function sessionNotPortableMessage(provider) {
61
61
  * is not terminal (spec §6.7) and nothing stopped the lane deliberately.
62
62
  *
63
63
  * Refused by `hq lanes message`:
64
- * `awaiting_input` has its own path, `hq lanes questions answer`, and the
65
- * reconcile pass resumes it once every question is answered. `killed` and
64
+ * `awaiting_input` has its own path, `hq lanes questions answer`, while a
65
+ * senior may resume it when no answerable pending question remains. `killed` and
66
66
  * `cancelled` were stopped on purpose by the operator; a message must not
67
67
  * silently restart them. `failed` means the run ended without an envelope or
68
68
  * never started; only the lane's senior can explicitly recover it with
@@ -104,7 +104,7 @@ export function laneDeliveryMechanism(lane) {
104
104
  if (lane.state === "queued" || lane.state === "running")
105
105
  return "dropbox";
106
106
  if (providerSupportsSessionResume(lane.provider) &&
107
- (isResumableLaneState(lane.state) || isFailedEnvelopeRepairLane(lane)) &&
107
+ (isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane)) &&
108
108
  hasRecordedSessionId(lane)) {
109
109
  return "resume";
110
110
  }
@@ -348,6 +348,7 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
348
348
  */
349
349
  export function resumeCommandRefusal(lane, options = {}) {
350
350
  if (!isResumeCommandState(lane.state) &&
351
+ !(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
351
352
  !isFailedEnvelopeRepairLane(lane) &&
352
353
  !(options.allowFailed === true && lane.state === "failed")) {
353
354
  return {
@@ -398,7 +399,10 @@ export function planLaneLaunch(lane, trigger) {
398
399
  return { mode: "resume", session_id: lane.provider_session_id };
399
400
  }
400
401
  case "resume": {
401
- const refusal = resumeCommandRefusal(lane, { allowFailed: trigger.fromFailed === true });
402
+ const refusal = resumeCommandRefusal(lane, {
403
+ allowFailed: trigger.fromFailed === true,
404
+ allowAwaitingInput: trigger.allowAwaitingInput === true,
405
+ });
402
406
  if (refusal !== null || !hasRecordedSessionId(lane)) {
403
407
  throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
404
408
  }
@@ -419,11 +423,17 @@ export function claimableStatesFor(trigger, lane) {
419
423
  case "answered_questions":
420
424
  return ["awaiting_input"];
421
425
  case "message":
422
- return envelopeRepairFailed ? [...RESUMABLE_LANE_STATES, "failed"] : RESUMABLE_LANE_STATES;
423
- case "resume":
424
- return trigger.fromFailed === true || envelopeRepairFailed
426
+ return envelopeRepairFailed
427
+ ? [...RESUMABLE_LANE_STATES, "failed"]
428
+ : lane?.state === "awaiting_input"
429
+ ? [...RESUMABLE_LANE_STATES, "awaiting_input"]
430
+ : RESUMABLE_LANE_STATES;
431
+ case "resume": {
432
+ const states = trigger.fromFailed === true || envelopeRepairFailed
425
433
  ? [...RESUME_COMMAND_STATES, "failed"]
426
- : RESUME_COMMAND_STATES;
434
+ : [...RESUME_COMMAND_STATES];
435
+ return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
436
+ }
427
437
  default: {
428
438
  const _exhaustive = trigger;
429
439
  return _exhaustive;
@@ -64,7 +64,7 @@ export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml
64
64
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
65
65
  export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
66
66
  export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
67
- export type { DeliveryLane, LaneDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
67
+ export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
68
68
  export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
69
69
  export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
70
70
  export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
@@ -1,6 +1,9 @@
1
- import type { LaneRecord, ManagerStatusGateHistoryEntry, QuestionRecord } from "./types.js";
1
+ import { type LaneRecord, type ManagerStatusGateHistoryEntry, type QuestionRecord } from "./types.js";
2
2
  export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
3
3
  export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
4
+ export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
5
+ export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
6
+ export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS: number;
4
7
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
5
8
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
6
9
  export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS: number;
@@ -35,10 +38,17 @@ export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_
35
38
  export interface KeepAliveDoneEnvelope {
36
39
  decision?: string;
37
40
  story_complete?: boolean;
41
+ next_check_at?: string;
38
42
  }
39
- export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete";
43
+ export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete" | "next_check_at";
44
+ export declare function keepAliveNextCheckAtMs(value: unknown, nowMs: number, anchorMs?: number): number | undefined;
40
45
  /** Shared done-state policy used by watch, reconcile, and the show command. */
41
- export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): DoneKeepAliveVerdict | null;
46
+ export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean, options?: {
47
+ nextCheckAtEnabled?: boolean;
48
+ hasPendingWork?: boolean;
49
+ nowMs?: number;
50
+ nextCheckAnchorMs?: number;
51
+ }): DoneKeepAliveVerdict | null;
42
52
  export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
43
53
  /** Work signals already persisted for a lane that ended its turn. */
44
54
  export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
@@ -3,8 +3,12 @@ import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
4
  import { dropboxInboxHasMessages } from "./dropbox.js";
5
5
  import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
6
+ import { isIsoDateTime } from "./types.js";
6
7
  export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
7
8
  export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
9
+ export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
10
+ export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
11
+ export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
8
12
  export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
9
13
  export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
10
14
  export const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS = 15 * 60 * 1000;
@@ -98,15 +102,35 @@ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText)
98
102
  export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
99
103
  export const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
100
104
  export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
105
+ export function keepAliveNextCheckAtMs(value, nowMs, anchorMs = nowMs) {
106
+ if (typeof value !== "string" || !isIsoDateTime(value))
107
+ return undefined;
108
+ const parsed = Date.parse(value);
109
+ if (parsed <= nowMs)
110
+ return undefined;
111
+ const stableAnchorMs = Number.isFinite(anchorMs) ? anchorMs : nowMs;
112
+ const capped = Math.min(parsed, stableAnchorMs + LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS);
113
+ return capped > nowMs ? capped : undefined;
114
+ }
101
115
  /** Shared done-state policy used by watch, reconcile, and the show command. */
102
- export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork) {
116
+ export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork, options = {}) {
103
117
  if (envelope.decision !== "done")
104
118
  return null;
119
+ let verdict;
105
120
  if (envelope.story_complete !== true)
106
- return "eligible";
107
- if (lane.keep_alive_standing !== true)
108
- return "story_complete";
109
- return hasQueuedWork ? "eligible" : "standing_idle";
121
+ verdict = "eligible";
122
+ else if (lane.keep_alive_standing !== true)
123
+ verdict = "story_complete";
124
+ else
125
+ verdict = hasQueuedWork ? "eligible" : "standing_idle";
126
+ if (verdict === "eligible" && options.nextCheckAtEnabled === true && options.hasPendingWork !== true) {
127
+ const nowMs = options.nowMs ?? Date.now();
128
+ const anchorMs = options.nextCheckAnchorMs ?? nowMs;
129
+ const nextCheckAtMs = keepAliveNextCheckAtMs(envelope.next_check_at, nowMs, anchorMs);
130
+ if (nextCheckAtMs !== undefined)
131
+ return "next_check_at";
132
+ }
133
+ return verdict;
110
134
  }
111
135
  export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
112
136
  return doneKeepAliveVerdict(lane, envelope, hasQueuedWork) === "eligible";
@@ -105,6 +105,11 @@ export declare function buildOutpostQuestionListCommand(input?: {
105
105
  }): string;
106
106
  /** Read one question's body and owner-computed audience from its Outpost. */
107
107
  export declare function buildOutpostQuestionShowCommand(questionId: string): string;
108
+ /** Cancel an owner-hosted question through the existing questions command. */
109
+ export declare function buildOutpostQuestionCancelCommand(input: {
110
+ questionId: string;
111
+ reason: string;
112
+ }): string;
108
113
  /** Commit a queued lane move on the host that currently owns its state. */
109
114
  export declare function buildOutpostMoveTargetCommand(input: {
110
115
  laneId: string;
@@ -113,6 +113,18 @@ export function buildOutpostQuestionShowCommand(questionId) {
113
113
  questionId,
114
114
  ]));
115
115
  }
116
+ /** Cancel an owner-hosted question through the existing questions command. */
117
+ export function buildOutpostQuestionCancelCommand(input) {
118
+ return withRemoteHqDir(joinCommandParts([
119
+ "hq",
120
+ "lanes",
121
+ "questions",
122
+ "cancel",
123
+ input.questionId,
124
+ "--reason",
125
+ input.reason,
126
+ ]));
127
+ }
116
128
  /** Commit a queued lane move on the host that currently owns its state. */
117
129
  export function buildOutpostMoveTargetCommand(input) {
118
130
  return withRemoteHqDir(joinCommandParts([
@@ -24,6 +24,7 @@ export declare const RECONCILE_LOCK_TIMEOUT_MS = 5000;
24
24
  /** A pickup probe that has not cleared its marker within this window is abandoned. */
25
25
  export declare const PICKUP_VALIDATION_MAX_AGE_MS: number;
26
26
  export declare const PICKUP_VALIDATION_TIMEOUT_REASON: "pickup_validation_timeout";
27
+ export declare const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
27
28
  export declare const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
28
29
  export declare const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
29
30
  export interface ReconcileSkip {