@indigoai-us/hq-cli 5.292.0 → 5.294.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 (75) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
  3. package/dist/command-catalog.generated.d.ts +45 -24
  4. package/dist/command-catalog.generated.js +57 -30
  5. package/dist/commands/core-command-catalog.js +3 -2
  6. package/dist/commands/core-delegate-w1b.d.ts +2 -0
  7. package/dist/commands/core-delegate-w1b.js +28 -2
  8. package/dist/commands/core-delegate.d.ts +3 -1
  9. package/dist/commands/core-delegate.js +35 -5
  10. package/dist/commands/core-handoff-fast.d.ts +18 -0
  11. package/dist/commands/core-handoff-fast.js +103 -0
  12. package/dist/commands/core-handoff-open-steps.d.ts +21 -0
  13. package/dist/commands/core-handoff-open-steps.js +264 -0
  14. package/dist/commands/core-handoff.d.ts +4 -2
  15. package/dist/commands/core-handoff.js +22 -249
  16. package/dist/commands/core-jobs-native.d.ts +2 -0
  17. package/dist/commands/core-jobs-native.js +22 -11
  18. package/dist/commands/core-knowledge-prefs.d.ts +2 -0
  19. package/dist/commands/core-knowledge-prefs.js +13 -1
  20. package/dist/commands/core-ontology-native.d.ts +2 -0
  21. package/dist/commands/core-ontology-native.js +22 -2
  22. package/dist/commands/core-policy.d.ts +2 -0
  23. package/dist/commands/core-policy.js +18 -1
  24. package/dist/commands/core.js +2 -1
  25. package/dist/commands/daemon.d.ts +2 -1
  26. package/dist/commands/daemon.js +22 -7
  27. package/dist/commands/lanes-autopilot.d.ts +11 -0
  28. package/dist/commands/lanes-autopilot.js +336 -0
  29. package/dist/commands/lanes-move.js +54 -1
  30. package/dist/commands/lanes-questions.js +12 -5
  31. package/dist/commands/lanes-shared.d.ts +17 -0
  32. package/dist/commands/lanes-stories.d.ts +14 -0
  33. package/dist/commands/lanes-stories.js +308 -0
  34. package/dist/commands/lanes.d.ts +2 -0
  35. package/dist/commands/lanes.js +158 -314
  36. package/dist/fast-core.js +53 -25
  37. package/dist/fast-scaffold.d.ts +15 -2
  38. package/dist/fast-scaffold.js +167 -7
  39. package/dist/lib/core-utils/sentry-report.js +2 -2
  40. package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
  41. package/dist/lib/core-utils/worktree-resume.js +16 -0
  42. package/dist/lib/daemon/migrate.d.ts +8 -1
  43. package/dist/lib/daemon/migrate.js +19 -7
  44. package/dist/lib/daemon/run.d.ts +11 -0
  45. package/dist/lib/daemon/run.js +17 -1
  46. package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
  47. package/dist/lib/lanes/autopilot-metrics.js +301 -0
  48. package/dist/lib/lanes/autopilot.d.ts +13 -1
  49. package/dist/lib/lanes/autopilot.js +271 -28
  50. package/dist/lib/lanes/grants.d.ts +1 -1
  51. package/dist/lib/lanes/grants.js +7 -5
  52. package/dist/lib/lanes/index.d.ts +1 -1
  53. package/dist/lib/lanes/keep-alive.d.ts +5 -0
  54. package/dist/lib/lanes/keep-alive.js +9 -4
  55. package/dist/lib/lanes/questions.d.ts +2 -1
  56. package/dist/lib/lanes/questions.js +2 -0
  57. package/dist/lib/lanes/reconcile.js +186 -9
  58. package/dist/lib/lanes/spawn.js +4 -0
  59. package/dist/lib/lanes/store.d.ts +2 -0
  60. package/dist/lib/lanes/store.js +53 -4
  61. package/dist/lib/lanes/types.d.ts +13 -1
  62. package/dist/lib/lanes/types.js +2 -2
  63. package/dist/lib/lanes/watcher.js +3 -1
  64. package/dist/native-fast-route.d.ts +3 -1
  65. package/dist/native-fast-route.js +10 -8
  66. package/dist/utils/contribution-table.d.ts +1 -1
  67. package/dist/utils/fast-stream-errors.d.ts +12 -0
  68. package/dist/utils/fast-stream-errors.js +18 -0
  69. package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
  70. package/dist/utils/scaffold-fast-import-guard.js +6 -3
  71. package/dist/utils/self-update.d.ts +7 -0
  72. package/dist/utils/self-update.js +5 -1
  73. package/dist/utils/version-gate.d.ts +15 -0
  74. package/dist/utils/version-gate.js +46 -2
  75. package/package.json +1 -1
package/dist/fast-core.js CHANGED
@@ -18,43 +18,71 @@ export async function runFastCore(argv) {
18
18
  const parsed = parseFastCore(argv);
19
19
  if (!parsed)
20
20
  return;
21
- // `root: "live"` — resolve the live HQ root and run the script in it, exactly
22
- // as core.ts's runEntry does. Fail soft: if no root can be resolved, run the
23
- // script anyway with the inherited environment (the hook already carries
24
- // CLAUDE_PROJECT_DIR, and each script has its own root fallback) rather than
25
- // crashing a Stop hook on a resolution error.
21
+ const entry = parsed.entry;
22
+ // The full graph's process gate accepts textual true/false overrides. It ignores
23
+ // numeric env values, so preserve that behavior for family flags here too.
24
+ const familyEnabled = entry.kind !== "native" || !entry.familyFlagKey ||
25
+ await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
26
+ const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
27
+ const shouldInjectRoot = useShell
28
+ ? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
29
+ !parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
30
+ : entry.kind === "shell"
31
+ ? true
32
+ : entry.root === "live";
33
+ // `root: "live"` — resolve the live HQ root using the same resolver as the
34
+ // full command graph. Existing shell fast entries keep their historical
35
+ // fail-soft behavior; newly routed commands fall back to the full graph so
36
+ // its expected-user-error formatting remains authoritative.
26
37
  let hqRoot;
27
- try {
28
- hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
29
- }
30
- catch {
31
- hqRoot = undefined;
38
+ if (shouldInjectRoot) {
39
+ try {
40
+ hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
41
+ }
42
+ catch {
43
+ if (entry.kind === "native") {
44
+ const { runCli } = await import("./main.js");
45
+ await runCli();
46
+ return;
47
+ }
48
+ hqRoot = undefined;
49
+ }
32
50
  }
33
- if (parsed.entry.kind === "native") {
34
- const handlerModule = await import(parsed.entry.modulePath);
35
- const handler = handlerModule[parsed.entry.exportName];
51
+ if (entry.kind === "native" && !useShell) {
52
+ const { installFastPathStreamErrorHandlers } = await import("./utils/fast-stream-errors.js");
53
+ installFastPathStreamErrorHandlers();
54
+ const handlerModule = await import(entry.modulePath);
55
+ const handler = handlerModule[entry.exportName];
36
56
  if (typeof handler !== "function") {
37
- throw new Error(`Native fast handler ${parsed.entry.exportName} was not exported`);
57
+ throw new Error(`Native fast handler ${entry.exportName} was not exported`);
38
58
  }
39
59
  const chunks = [];
40
- for await (const chunk of process.stdin)
41
- chunks.push(chunk);
42
- const code = await handler(argv, Buffer.concat(chunks).toString("utf8"), process.cwd(), hqRoot);
60
+ if (entry.readsStdin) {
61
+ for await (const chunk of process.stdin)
62
+ chunks.push(chunk);
63
+ }
64
+ const code = await handler(parsed.args, entry.readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
43
65
  // Let Node drain handler output before terminating. The timeout guard's
44
66
  // stderr message must survive the native route's process boundary.
45
67
  process.exitCode = code;
46
68
  return;
47
69
  }
48
- // Keep the CHILD in the caller's cwd, unlike the generic "live" dispatch. The
49
- // relocated scripts read their data tree from the injected root, not cwd, and
50
- // hq-session's Work Mesh registration serializes $PWD — so changing directory
51
- // to the HQ root would mislabel a bind made from inside a project/worktree.
70
+ const asset = entry.asset;
71
+ if (!asset)
72
+ throw new Error(`Fast route ${entry.name} has no shell companion`);
73
+ // Some historical registrations run from the live root, others preserve the
74
+ // caller cwd and only inject HQ_ROOT. Keep those choices explicit per entry.
75
+ const cwd = entry.kind === "native" && entry.shellCwd === "live"
76
+ ? hqRoot ?? process.cwd()
77
+ : process.cwd();
78
+ const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
79
+ (entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
52
80
  const { code } = runBundledScript({
53
- asset: parsed.entry.asset,
81
+ asset,
54
82
  args: parsed.args,
55
- cwd: process.cwd(),
56
- hqRoot,
57
- env: parsed.entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
83
+ cwd,
84
+ ...(injectRoot ? { hqRoot } : {}),
85
+ env: entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
58
86
  });
59
87
  // Propagate verbatim: these scripts use their exit code as their interface.
60
88
  process.exit(code);
@@ -15,16 +15,29 @@ export type FastScaffoldScript = {
15
15
  kind: "shell";
16
16
  /** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
17
17
  asset: string;
18
+ root?: "live";
18
19
  } | {
19
- /** Direct subcommand name — `hq core <name>`. */
20
+ /** Command path, joined with spaces, e.g. `policy frontmatter`. */
20
21
  name: string;
21
22
  kind: "native";
22
23
  /** Relative ESM module specifier, resolved from fast-core.js. */
23
24
  modulePath: string;
24
25
  /** Named export which accepts the native fast-handler contract. */
25
26
  exportName: string;
26
- /** Default-off registry flag for this native route. */
27
+ /** Existing default-off gate for the whole fast route. */
27
28
  flagKey: string;
29
+ /** Existing family gate that chooses this native handler or its shell companion. */
30
+ familyFlagKey?: string;
31
+ /** Shell rollback companion used while the family gate is off. */
32
+ asset?: string;
33
+ /** Root context used by the native handler. */
34
+ root?: "live" | "cwd" | "passthrough";
35
+ /** Cwd used for a shell rollback companion. Defaults to the caller cwd. */
36
+ shellCwd?: "caller" | "live";
37
+ /** Root injection behavior for a shell rollback companion. */
38
+ shellHqRoot?: "none" | "live" | "reconcile-default";
39
+ /** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
40
+ readsStdin?: boolean;
28
41
  };
29
42
  /** The relocated scripts that qualify for the entrypoint fast path. */
30
43
  export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
@@ -11,15 +11,175 @@
11
11
  */
12
12
  /** The relocated scripts that qualify for the entrypoint fast path. */
13
13
  export const FAST_SCAFFOLD_SCRIPTS = [
14
- { name: "checkpoint-stop-gate", kind: "shell", asset: "core/scripts/checkpoint-stop-gate.sh" },
15
- { name: "hq-session", kind: "shell", asset: "core/scripts/hq-session.sh" },
14
+ { name: "checkpoint-stop-gate", kind: "shell", asset: "core/scripts/checkpoint-stop-gate.sh", root: "live" },
15
+ { name: "hq-session", kind: "shell", asset: "core/scripts/hq-session.sh", root: "live" },
16
16
  {
17
17
  name: "timeout-guard",
18
18
  kind: "native",
19
19
  modulePath: "./lib/core-utils/timeout-guard-native.js",
20
20
  exportName: "runTimeoutGuardFast",
21
21
  flagKey: "hq-core-native-fast-route",
22
+ root: "cwd",
23
+ readsStdin: true,
22
24
  },
25
+ {
26
+ name: "policy frontmatter",
27
+ kind: "native",
28
+ modulePath: "./commands/core-policy.js",
29
+ exportName: "runPolicyFast",
30
+ flagKey: "hq-core-native-fast-route",
31
+ familyFlagKey: "core-native-policy",
32
+ asset: "core/scripts/read-policy-frontmatter.sh",
33
+ root: "cwd",
34
+ shellCwd: "caller",
35
+ shellHqRoot: "none",
36
+ },
37
+ {
38
+ name: "policy age-report",
39
+ kind: "native",
40
+ modulePath: "./commands/core-policy.js",
41
+ exportName: "runPolicyFast",
42
+ flagKey: "hq-core-native-fast-route",
43
+ familyFlagKey: "core-native-policy",
44
+ asset: "core/scripts/policy-age-report.sh",
45
+ root: "live",
46
+ shellCwd: "caller",
47
+ shellHqRoot: "live",
48
+ },
49
+ {
50
+ name: "policy retire",
51
+ kind: "native",
52
+ modulePath: "./commands/core-policy.js",
53
+ exportName: "runPolicyFast",
54
+ flagKey: "hq-core-native-fast-route",
55
+ familyFlagKey: "core-native-policy",
56
+ asset: "core/scripts/policy-retire.sh",
57
+ root: "live",
58
+ shellCwd: "caller",
59
+ shellHqRoot: "live",
60
+ },
61
+ {
62
+ name: "knowledge-prefs",
63
+ kind: "native",
64
+ modulePath: "./commands/core-knowledge-prefs.js",
65
+ exportName: "runKnowledgePrefsFast",
66
+ flagKey: "hq-core-native-fast-route",
67
+ familyFlagKey: "core-native-policy",
68
+ asset: "core/scripts/knowledge-prefs.sh",
69
+ root: "live",
70
+ shellCwd: "live",
71
+ shellHqRoot: "live",
72
+ },
73
+ ...["finalize", "post", "open-steps"].map((name) => ({
74
+ name: `handoff ${name}`,
75
+ kind: "native",
76
+ modulePath: name === "open-steps"
77
+ ? "./commands/core-handoff-open-steps.js"
78
+ : "./commands/core-handoff-fast.js",
79
+ exportName: name === "open-steps" ? "runHandoffOpenStepsFast" : "runHandoffFastEntry",
80
+ flagKey: "hq-core-native-fast-route",
81
+ familyFlagKey: "core-native-handoff",
82
+ asset: `core/scripts/handoff-${name === "open-steps" ? "open-steps" : name}.sh`,
83
+ root: "live",
84
+ shellCwd: "live",
85
+ shellHqRoot: "live",
86
+ })),
87
+ {
88
+ name: "worktree-gc",
89
+ kind: "native",
90
+ modulePath: "./lib/core-utils/worktree-resume.js",
91
+ exportName: "runWorktreeResumeFast",
92
+ flagKey: "hq-core-native-fast-route",
93
+ familyFlagKey: "core-native-handoff",
94
+ asset: "core/scripts/worktree-gc.sh",
95
+ root: "live",
96
+ shellCwd: "live",
97
+ shellHqRoot: "live",
98
+ },
99
+ {
100
+ name: "clean-worktree-reconcile-handoff",
101
+ kind: "native",
102
+ modulePath: "./lib/core-utils/worktree-resume.js",
103
+ exportName: "runWorktreeResumeFast",
104
+ flagKey: "hq-core-native-fast-route",
105
+ familyFlagKey: "core-native-handoff",
106
+ asset: "core/scripts/clean-worktree-reconcile-handoff.sh",
107
+ root: "cwd",
108
+ shellCwd: "caller",
109
+ shellHqRoot: "none",
110
+ },
111
+ {
112
+ name: "resume-thread-lock",
113
+ kind: "native",
114
+ modulePath: "./lib/core-utils/worktree-resume.js",
115
+ exportName: "runWorktreeResumeFast",
116
+ flagKey: "hq-core-native-fast-route",
117
+ familyFlagKey: "core-native-handoff",
118
+ asset: "core/scripts/resume-thread-lock.sh",
119
+ root: "live",
120
+ shellCwd: "live",
121
+ shellHqRoot: "live",
122
+ },
123
+ {
124
+ name: "verify-story-deliverables",
125
+ kind: "native",
126
+ modulePath: "./lib/core-utils/worktree-resume.js",
127
+ exportName: "runWorktreeResumeFast",
128
+ flagKey: "hq-core-native-fast-route",
129
+ familyFlagKey: "core-native-handoff",
130
+ asset: "core/scripts/verify-story-deliverables.sh",
131
+ root: "live",
132
+ shellCwd: "live",
133
+ shellHqRoot: "live",
134
+ },
135
+ ...["schedule-parse", "validate", "reconcile"].map((name) => ({
136
+ name: `jobs ${name}`,
137
+ kind: "native",
138
+ modulePath: "./commands/core-jobs-native.js",
139
+ exportName: "runJobsFast",
140
+ flagKey: "hq-core-native-fast-route",
141
+ familyFlagKey: "core-native-jobs",
142
+ asset: `core/scripts/${name === "reconcile" ? "outpost-jobs-reconcile" : `jobs-${name}`}.sh`,
143
+ root: name === "reconcile" ? "passthrough" : "cwd",
144
+ shellCwd: "caller",
145
+ shellHqRoot: name === "reconcile" ? "reconcile-default" : "none",
146
+ })),
147
+ ...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
148
+ name: `delegate ${name}`,
149
+ kind: "native",
150
+ modulePath: "./commands/core-delegate.js",
151
+ exportName: "runDelegateFast",
152
+ flagKey: "hq-core-native-fast-route",
153
+ familyFlagKey: "core-native-delegate",
154
+ asset: `core/scripts/hq-delegate-${name}.sh`,
155
+ root: name === "pickup" ? "cwd" : "live",
156
+ shellCwd: name === "pickup" ? "caller" : "live",
157
+ shellHqRoot: name === "pickup" ? "none" : "live",
158
+ })),
159
+ ...["bundle", "grant", "publish", "send", "verify"].map((name) => ({
160
+ name: `delegate ${name}`,
161
+ kind: "native",
162
+ modulePath: "./commands/core-delegate-w1b.js",
163
+ exportName: "runDelegateW1bFast",
164
+ flagKey: "hq-core-native-fast-route",
165
+ familyFlagKey: "core-native-delegate",
166
+ asset: `core/scripts/hq-delegate-${name}.sh`,
167
+ root: "live",
168
+ shellCwd: "live",
169
+ shellHqRoot: "live",
170
+ })),
171
+ ...["audience-grant", "brief", "candidate", "garden", "source-worklist", "source-yaml-validate"].map((name) => ({
172
+ name: `ontology ${name}`,
173
+ kind: "native",
174
+ modulePath: "./commands/core-ontology-native.js",
175
+ exportName: "runOntologyNativeFast",
176
+ flagKey: "hq-core-native-fast-route",
177
+ familyFlagKey: "core-native-ontology",
178
+ asset: `core/scripts/${name === "audience-grant" ? "ontology-audience-grant" : name === "source-worklist" ? "ontology-source-worklist" : name === "source-yaml-validate" ? "source-yaml-validate" : `ontology-${name}`}.${name === "brief" || name === "garden" ? "mjs" : "sh"}`,
179
+ root: name === "source-yaml-validate" ? "cwd" : "live",
180
+ shellCwd: "caller",
181
+ shellHqRoot: name === "source-yaml-validate" ? "none" : "live",
182
+ })),
23
183
  ];
24
184
  /**
25
185
  * Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
@@ -148,13 +308,13 @@ export function parseFastCore(argv) {
148
308
  hqRootArg = head.slice("--hq-root=".length);
149
309
  i += 1;
150
310
  }
151
- const name = argv[i];
152
- if (typeof name !== "string")
153
- return null;
154
- const entry = FAST_SCAFFOLD_SCRIPTS.find((candidate) => candidate.name === name);
311
+ const commandArgs = argv.slice(i);
312
+ const entry = [...FAST_SCAFFOLD_SCRIPTS]
313
+ .sort((left, right) => right.name.split(" ").length - left.name.split(" ").length)
314
+ .find((candidate) => candidate.name.split(" ").every((part, index) => commandArgs[index] === part));
155
315
  if (!entry)
156
316
  return null;
157
- return { entry, hqRootArg, args: argv.slice(i + 1) };
317
+ return { entry, hqRootArg, args: argv.slice(i + entry.name.split(" ").length) };
158
318
  }
159
319
  /** Whether this invocation is a fast-path scaffold dispatch. */
160
320
  export function isFastCoreRequest(argv) {
@@ -677,13 +677,13 @@ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
677
677
  ...sentryEnvelope(event, overrides, additionalTags),
678
678
  }, {});
679
679
  }
680
- /** Keep all hook-timeout conditions together by script while retaining tags for triage. */
680
+ /** Group hook-timeout conditions by normalized kind while retaining triage tags. */
681
681
  function hookTimeoutExceededOverrides(event) {
682
682
  if (!isHookTimeoutEvent(event))
683
683
  return undefined;
684
684
  return {
685
685
  level: "warning",
686
- fingerprint: ["hook-timeout", hookScript(event.metadata)],
686
+ fingerprint: ["hook-timeout", hookTimeoutKind(event)],
687
687
  };
688
688
  }
689
689
  /**
@@ -15,4 +15,6 @@ export declare function runWorktreeGc(args: readonly string[], hqRoot: string, i
15
15
  export declare function runCleanWorktreeReconcileHandoff(args: readonly string[], io?: CoreIo): number;
16
16
  export declare function runResumeThreadLock(args: readonly string[], hqRoot: string, io?: CoreIo): number;
17
17
  export declare function runVerifyStoryDeliverables(args: readonly string[], hqRoot: string, io?: CoreIo): Promise<number>;
18
+ /** Fast-entry adapter; family gating and shell fallback happen before import. */
19
+ export declare function runWorktreeResumeFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, commandName: string): Promise<number>;
18
20
  //# sourceMappingURL=worktree-resume.d.ts.map
@@ -1179,4 +1179,20 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
1179
1179
  stdout(io, `verify-story-deliverables: ${storyId} — all ${refs.length} reference(s) present (${declaredCount} declared, ${refs.length - declaredCount} from worker evidence)${recordedSuffix}\n`);
1180
1180
  return 0;
1181
1181
  }
1182
+ /** Fast-entry adapter; family gating and shell fallback happen before import. */
1183
+ export async function runWorktreeResumeFast(args, _stdin, cwd, hqRoot, commandName) {
1184
+ const root = hqRoot ?? cwd;
1185
+ let code;
1186
+ if (commandName === "worktree-gc")
1187
+ code = runWorktreeGc(args, root);
1188
+ else if (commandName === "clean-worktree-reconcile-handoff")
1189
+ code = runCleanWorktreeReconcileHandoff(args);
1190
+ else if (commandName === "resume-thread-lock")
1191
+ code = runResumeThreadLock(args, root);
1192
+ else
1193
+ code = await runVerifyStoryDeliverables(args, root);
1194
+ if (code !== 0)
1195
+ process.exitCode = code;
1196
+ return code;
1197
+ }
1182
1198
  //# sourceMappingURL=worktree-resume.js.map
@@ -9,7 +9,9 @@
9
9
  * back. A unit whose stop fails is left where it was and not recorded.
10
10
  *
11
11
  * Units owned by another product (the desktop app's sync agent) are never
12
- * touched, and box system units need root. System units are recorded and
12
+ * touched, and box system units need root. Root on a box stops and restarts
13
+ * the login user's own units through that user's systemd manager, since
14
+ * root's `systemctl --user` reaches only root's. System units are recorded and
13
15
  * saved in root-owned storage (not the login user's daemon folder, which the
14
16
  * user can edit), and a restore checks each one lands in the system unit
15
17
  * folder, so nobody but root decides what gets installed there.
@@ -33,6 +35,11 @@ export interface MigrateDeps {
33
35
  systemRecordPath: string;
34
36
  /** Where box system units live (/etc/systemd/system); restores must land here. */
35
37
  systemUnitDir: string;
38
+ /** Root on a box: the login user whose systemd user units these are. */
39
+ userUnitsOwner?: {
40
+ user: string;
41
+ uid: number;
42
+ };
36
43
  now?: () => number;
37
44
  /** Tests inject a failing writer. */
38
45
  writeRecord?: (recordPath: string, units: AdoptedUnit[]) => void;
@@ -9,7 +9,9 @@
9
9
  * back. A unit whose stop fails is left where it was and not recorded.
10
10
  *
11
11
  * Units owned by another product (the desktop app's sync agent) are never
12
- * touched, and box system units need root. System units are recorded and
12
+ * touched, and box system units need root. Root on a box stops and restarts
13
+ * the login user's own units through that user's systemd manager, since
14
+ * root's `systemctl --user` reaches only root's. System units are recorded and
13
15
  * saved in root-owned storage (not the login user's daemon folder, which the
14
16
  * user can edit), and a restore checks each one lands in the system unit
15
17
  * folder, so nobody but root decides what gets installed there.
@@ -56,6 +58,18 @@ function must(run, cmd, args) {
56
58
  }
57
59
  return r.stdout;
58
60
  }
61
+ /** `systemctl` for a unit: `--user` units go to their owner's manager, as that user when root acts for them. */
62
+ function systemctl(unit, deps, args) {
63
+ if (unit.kind !== "systemd-user")
64
+ return ["systemctl", args];
65
+ const owner = deps.isRoot ? deps.userUnitsOwner : undefined;
66
+ if (!owner)
67
+ return ["systemctl", ["--user", ...args]];
68
+ return [
69
+ "runuser",
70
+ ["-u", owner.user, "--", "env", `XDG_RUNTIME_DIR=/run/user/${owner.uid}`, "systemctl", "--user", ...args],
71
+ ];
72
+ }
59
73
  function moveFile(from, to) {
60
74
  ensurePrivateDir(path.dirname(to));
61
75
  try {
@@ -92,10 +106,9 @@ function adoptOne(unit, deps) {
92
106
  case "systemd-system": {
93
107
  if (!unit.path)
94
108
  throw new Error(`no unit file path for ${unit.name}`);
95
- const scope = unit.kind === "systemd-user" ? ["--user"] : [];
96
- must(deps.run, "systemctl", [...scope, "disable", "--now", unit.name]);
109
+ must(deps.run, ...systemctl(unit, deps, ["disable", "--now", unit.name]));
97
110
  moveFile(unit.path, to);
98
- deps.run("systemctl", [...scope, "daemon-reload"]);
111
+ deps.run(...systemctl(unit, deps, ["daemon-reload"]));
99
112
  return to;
100
113
  }
101
114
  case "schtasks": {
@@ -190,10 +203,9 @@ function restoreOne(unit, deps) {
190
203
  if (unit.kind === "systemd-system" && !deps.isRoot) {
191
204
  throw new Error("a system unit; run `sudo hq daemon uninstall --restore-legacy`");
192
205
  }
193
- const scope = unit.kind === "systemd-user" ? ["--user"] : [];
194
206
  moveBack(unit, unit.path);
195
- deps.run("systemctl", [...scope, "daemon-reload"]);
196
- must(deps.run, "systemctl", [...scope, "enable", "--now", unit.name]);
207
+ deps.run(...systemctl(unit, deps, ["daemon-reload"]));
208
+ must(deps.run, ...systemctl(unit, deps, ["enable", "--now", unit.name]));
197
209
  return;
198
210
  }
199
211
  case "schtasks":
@@ -62,5 +62,16 @@ export declare function rotateLogs(logsDir: string, maxBytes?: number): number;
62
62
  * an on-failure unit down. The daemon's updater handles CLI updates.
63
63
  */
64
64
  export declare function unitProcessEnv(base: NodeJS.ProcessEnv, unit: UnitPlan): NodeJS.ProcessEnv;
65
+ /**
66
+ * A systemd system unit starts the daemon as the login user without the
67
+ * variables a login session sets, so `systemctl --user` from its services
68
+ * cannot find the user bus ("Failed to connect to bus: No medium found").
69
+ * Point them at the user's runtime dir when it exists and nothing set one.
70
+ */
71
+ export declare function withUserRuntimeDir(env: NodeJS.ProcessEnv, host: {
72
+ platform: NodeJS.Platform;
73
+ uid: number;
74
+ exists: (dir: string) => boolean;
75
+ }): NodeJS.ProcessEnv;
65
76
  export declare function runDaemon(opts: RunDaemonOptions): Promise<number>;
66
77
  //# sourceMappingURL=run.d.ts.map
@@ -233,8 +233,24 @@ function isFifo(file) {
233
233
  return false;
234
234
  }
235
235
  }
236
+ /**
237
+ * A systemd system unit starts the daemon as the login user without the
238
+ * variables a login session sets, so `systemctl --user` from its services
239
+ * cannot find the user bus ("Failed to connect to bus: No medium found").
240
+ * Point them at the user's runtime dir when it exists and nothing set one.
241
+ */
242
+ export function withUserRuntimeDir(env, host) {
243
+ if (host.platform !== "linux" || host.uid === 0 || env.XDG_RUNTIME_DIR)
244
+ return env;
245
+ const dir = `/run/user/${host.uid}`;
246
+ return host.exists(dir) ? { ...env, XDG_RUNTIME_DIR: dir } : env;
247
+ }
236
248
  export async function runDaemon(opts) {
237
- const env = opts.env ?? process.env;
249
+ const env = withUserRuntimeDir(opts.env ?? process.env, {
250
+ platform: process.platform,
251
+ uid: process.getuid?.() ?? -1,
252
+ exists: fs.existsSync,
253
+ });
238
254
  const home = opts.home ?? os.homedir();
239
255
  const platform = process.platform;
240
256
  const paths = daemonPaths(home, env);
@@ -0,0 +1,99 @@
1
+ export interface AutopilotMetricsLead {
2
+ team: string;
3
+ lane_id: string;
4
+ project_id: string;
5
+ }
6
+ export interface AutopilotMetricsLane {
7
+ lane_id: string;
8
+ project_id: string;
9
+ story_id: string;
10
+ senior: {
11
+ kind: string;
12
+ id?: string;
13
+ };
14
+ state: string;
15
+ created_at: string;
16
+ updated_at?: string;
17
+ ended_at?: string;
18
+ hire_grant?: number;
19
+ last_resume?: {
20
+ at?: string;
21
+ };
22
+ last_envelope?: {
23
+ at?: string;
24
+ artifacts?: unknown[];
25
+ };
26
+ }
27
+ export interface AutopilotMetricsStory {
28
+ id: string;
29
+ state?: string;
30
+ status?: string;
31
+ created_at?: string;
32
+ updated_at?: string;
33
+ prs?: unknown[];
34
+ lanes?: unknown[];
35
+ }
36
+ export interface AutopilotMetricsProject {
37
+ project_id: string;
38
+ stories: readonly AutopilotMetricsStory[];
39
+ }
40
+ export interface AutopilotMetricsNudge {
41
+ team: string;
42
+ at: string;
43
+ }
44
+ export interface AutopilotMetricsReview {
45
+ repo: string;
46
+ pr: number;
47
+ recorded_at: string;
48
+ worker_lane?: string | null;
49
+ }
50
+ export interface AutopilotMetricsResume {
51
+ lane_id: string;
52
+ at: string;
53
+ }
54
+ export interface AutopilotMetricsDefect {
55
+ at: string;
56
+ team: string;
57
+ defect: string;
58
+ }
59
+ export interface BuildAutopilotMetricsReportInput {
60
+ since: string;
61
+ until: string;
62
+ leads: readonly AutopilotMetricsLead[];
63
+ lanes: readonly AutopilotMetricsLane[];
64
+ projects: readonly AutopilotMetricsProject[];
65
+ reviews: readonly AutopilotMetricsReview[];
66
+ nudges: readonly AutopilotMetricsNudge[];
67
+ nudgeDefects: readonly AutopilotMetricsDefect[];
68
+ seniorResumes: readonly AutopilotMetricsResume[];
69
+ }
70
+ export interface AutopilotMetricsDay {
71
+ team: string;
72
+ day: string;
73
+ operator_nudges: number;
74
+ reviewed_lanes: number;
75
+ review_latency_minutes_total: number;
76
+ review_latency_minutes_average: number | null;
77
+ idle_slot_minutes_ready_work: number;
78
+ }
79
+ export interface AutopilotReviewLatency {
80
+ team: string;
81
+ lane_id: string;
82
+ story_id: string;
83
+ finished_at: string;
84
+ reviewed_at: string;
85
+ minutes: number;
86
+ end_event: "review_record" | "senior_resume" | "story_closed";
87
+ }
88
+ export interface AutopilotNudgeDefect extends AutopilotMetricsDefect {
89
+ wake: null;
90
+ }
91
+ export interface AutopilotMetricsReport {
92
+ since: string;
93
+ until: string;
94
+ days: AutopilotMetricsDay[];
95
+ review_latencies: AutopilotReviewLatency[];
96
+ nudge_defects: AutopilotNudgeDefect[];
97
+ }
98
+ export declare function buildAutopilotMetricsReport(input: BuildAutopilotMetricsReportInput): AutopilotMetricsReport;
99
+ //# sourceMappingURL=autopilot-metrics.d.ts.map