@indigoai-us/hq-cli 5.293.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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
  3. package/dist/command-catalog.generated.d.ts +42 -24
  4. package/dist/command-catalog.generated.js +53 -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-questions.js +8 -5
  30. package/dist/commands/lanes-shared.d.ts +17 -0
  31. package/dist/commands/lanes-stories.d.ts +14 -0
  32. package/dist/commands/lanes-stories.js +308 -0
  33. package/dist/commands/lanes.js +39 -309
  34. package/dist/fast-core.js +53 -25
  35. package/dist/fast-scaffold.d.ts +15 -2
  36. package/dist/fast-scaffold.js +167 -7
  37. package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
  38. package/dist/lib/core-utils/worktree-resume.js +16 -0
  39. package/dist/lib/daemon/migrate.d.ts +8 -1
  40. package/dist/lib/daemon/migrate.js +19 -7
  41. package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
  42. package/dist/lib/lanes/autopilot-metrics.js +301 -0
  43. package/dist/lib/lanes/store.d.ts +2 -0
  44. package/dist/lib/lanes/store.js +32 -3
  45. package/dist/native-fast-route.d.ts +3 -1
  46. package/dist/native-fast-route.js +10 -8
  47. package/dist/utils/contribution-table.d.ts +1 -1
  48. package/dist/utils/fast-stream-errors.d.ts +12 -0
  49. package/dist/utils/fast-stream-errors.js +18 -0
  50. package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
  51. package/dist/utils/scaffold-fast-import-guard.js +6 -3
  52. package/package.json +1 -1
@@ -0,0 +1,264 @@
1
+ /** Lightweight native handler for the handoff open-steps helper. */
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
5
+ const OPEN_STEPS_UNKNOWN_USAGE = `# hq-core: public
6
+ # handoff-open-steps.sh — list and close next steps across handoff threads
7
+ # (2026-09-07).
8
+ #
9
+ # Handoff threads record next_steps; since 2026-09-07 each carries an \x60id\x60
10
+ # (\x60<thread_id>#<n>\x60) and a \x60status\x60 (open|done|dropped). This script is the
11
+ # closure side: /startwork and /resumework list what is still open instead of
12
+ # re-copying it forward, and a session closes a step when it is actually done.
13
+ #
14
+ # Usage:
15
+ # handoff-open-steps.sh list [--limit N] [--company <slug>] [--json]
16
+ # handoff-open-steps.sh close <step-id> [--as done|dropped] [--note "<why>"]
17
+ # handoff-open-steps.sh reopen <step-id>
18
+ #
19
+ # Threads live in workspace/threads/T-*.json (and archive/**). Closing edits
20
+ # the step in place inside its thread file (only \x60status\x60, \x60closed_at\x60,
21
+ # \x60closed_by\x60, \x60note\x60) and bumps \x60updated_at\x60; nothing else in the thread is
22
+ # touched. Legacy steps without an id are listed with a synthetic id and can be
23
+ # closed the same way (the id is written on first close).
24
+ set -uo pipefail
25
+ HQ_ROOT="\${HQ_ROOT:-\${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)}}"
26
+ `;
27
+ export function prettyJson(value) {
28
+ return serializeJqJson(value, "pretty");
29
+ }
30
+ export function compactJson(value) {
31
+ return serializeJqJson(value, "compact");
32
+ }
33
+ export function isRecord(value) {
34
+ return value !== null && typeof value === "object" && !Array.isArray(value);
35
+ }
36
+ export function jqTruthy(value) {
37
+ return value !== undefined && value !== null && value !== false;
38
+ }
39
+ function listFiles(root, predicate) {
40
+ const found = [];
41
+ const visit = (directory) => {
42
+ let entries;
43
+ try {
44
+ entries = fs.readdirSync(directory, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ for (const entry of entries) {
50
+ const file = path.join(directory, entry.name);
51
+ if (entry.isDirectory())
52
+ visit(file);
53
+ else if (entry.isFile() && predicate(file, entry))
54
+ found.push(file);
55
+ }
56
+ };
57
+ visit(root);
58
+ return found;
59
+ }
60
+ function sortThreadFilesNewest(files) {
61
+ return files.sort((left, right) => {
62
+ const leftMtime = Math.floor(fs.statSync(left).mtimeMs / 1000);
63
+ const rightMtime = Math.floor(fs.statSync(right).mtimeMs / 1000);
64
+ if (leftMtime !== rightMtime)
65
+ return rightMtime - leftMtime;
66
+ // GNU sort -rn falls back to reverse lexical order for equal numeric keys.
67
+ return right.localeCompare(left);
68
+ });
69
+ }
70
+ function normalizedSteps(file, value) {
71
+ if (!isRecord(value))
72
+ return undefined;
73
+ const steps = value.next_steps ?? [];
74
+ if (!Array.isArray(steps))
75
+ return undefined;
76
+ const basename = path.basename(file).replace(/\.json$/, "");
77
+ const normalized = [];
78
+ for (let index = 0; index < steps.length; index += 1) {
79
+ const raw = steps[index];
80
+ if (typeof raw !== "string" && !isRecord(raw))
81
+ return undefined;
82
+ const step = typeof raw === "string" ? { step: raw } : { ...raw };
83
+ if (!jqTruthy(step.id))
84
+ step.id = `${basename}#${index + 1}`;
85
+ if (!jqTruthy(step.status))
86
+ step.status = "open";
87
+ delete step.key;
88
+ normalized.push(step);
89
+ }
90
+ return normalized;
91
+ }
92
+ function parseJson(file) {
93
+ return JSON.parse(fs.readFileSync(file, "utf8"));
94
+ }
95
+ export function shellDate(date) {
96
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
97
+ }
98
+ export function runHandoffOpenSteps(args, context) {
99
+ const root = context.root;
100
+ const threads = path.join(root, "workspace/threads");
101
+ const argv = [...args];
102
+ const command = argv[0] || "list";
103
+ if (argv.length > 0)
104
+ argv.shift();
105
+ if (command === "list") {
106
+ let limitText = "20";
107
+ let company = "";
108
+ let json = false;
109
+ for (let index = 0; index < argv.length;) {
110
+ const token = argv[index];
111
+ if (token === "--limit") {
112
+ limitText = argv[index + 1] ?? "";
113
+ index += 2;
114
+ }
115
+ else if (token === "--company") {
116
+ company = argv[index + 1] ?? "";
117
+ index += 2;
118
+ }
119
+ else if (token === "--json") {
120
+ json = true;
121
+ index += 1;
122
+ }
123
+ else
124
+ index += 1;
125
+ }
126
+ const numericLimit = /^-?\d+$/.test(limitText) ? Number(limitText) : Number.NaN;
127
+ const candidates = sortThreadFilesNewest(listFiles(threads, (file) => {
128
+ const name = path.basename(file);
129
+ return /^T-.*\.json$/.test(name) && !name.endsWith(".changeset.json") && !file.split(path.sep).includes("resume-locks");
130
+ }));
131
+ let count = 0;
132
+ const rows = [];
133
+ for (const file of candidates) {
134
+ if (!Number.isNaN(numericLimit) && count >= numericLimit)
135
+ break;
136
+ count += 1;
137
+ let thread;
138
+ let nextSteps;
139
+ try {
140
+ thread = parseJson(file);
141
+ nextSteps = normalizedSteps(file, thread);
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ if (!nextSteps || !isRecord(thread))
147
+ continue;
148
+ if (company) {
149
+ const companies = (isRecord(thread.metadata) ? thread.metadata.company : undefined) ?? [];
150
+ const contains = Array.isArray(companies)
151
+ ? companies.includes(company)
152
+ : typeof companies === "string" && companies.includes(company);
153
+ if (!contains)
154
+ continue;
155
+ }
156
+ for (const step of nextSteps) {
157
+ if (step.status !== "open")
158
+ continue;
159
+ rows.push({
160
+ thread: thread.thread_id,
161
+ title: (isRecord(thread.metadata) ? thread.metadata.title : undefined) ?? "",
162
+ created: thread.created_at,
163
+ id: step.id,
164
+ step: step.step ?? null,
165
+ });
166
+ }
167
+ }
168
+ if (json)
169
+ return { code: 0, stdout: prettyJson(rows), stderr: "" };
170
+ if (rows.length === 0)
171
+ return { code: 0, stdout: `No open handoff steps in the last ${count} thread(s).\n`, stderr: "" };
172
+ const jqString = (value) => value === null || value === undefined
173
+ ? "null"
174
+ : typeof value === "string" ? value : compactJson(value).trimEnd();
175
+ const output = rows.map((row) => `${jqString(row.id)} ${jqString(row.step)} [${jqString(row.title)}]\n`).join("");
176
+ return { code: 0, stdout: output, stderr: "" };
177
+ }
178
+ if (command === "close" || command === "reopen") {
179
+ const id = argv[0] ?? "";
180
+ const rest = argv.slice(1);
181
+ if (!id)
182
+ return { code: 2, stdout: "", stderr: `usage: ${command} <step-id>\n` };
183
+ let as = "done";
184
+ let note = "";
185
+ for (let index = 0; index < rest.length;) {
186
+ if (rest[index] === "--as") {
187
+ as = rest[index + 1] ?? "";
188
+ index += 2;
189
+ }
190
+ else if (rest[index] === "--note") {
191
+ note = rest[index + 1] ?? "";
192
+ index += 2;
193
+ }
194
+ else
195
+ index += 1;
196
+ }
197
+ if (command === "reopen")
198
+ as = "open";
199
+ if (!new Set(["done", "dropped", "open"]).has(as))
200
+ return { code: 2, stdout: "", stderr: "--as must be done|dropped\n" };
201
+ const threadId = id.split("#", 1)[0] ?? "";
202
+ const candidates = listFiles(threads, (file) => path.basename(file) === `${threadId}.json` && !file.split(path.sep).includes("resume-locks"));
203
+ const file = candidates[0];
204
+ if (!file)
205
+ return { code: 3, stdout: "", stderr: `handoff-open-steps: thread ${threadId} not found under ${threads}\n` };
206
+ const now = shellDate(context.now?.() ?? new Date());
207
+ const by = process.env.HQ_SESSION_ID || process.env.CLAUDE_SESSION_ID || "unknown-session";
208
+ let text;
209
+ try {
210
+ const thread = parseJson(file);
211
+ const steps = normalizedSteps(file, thread);
212
+ if (!steps || !isRecord(thread))
213
+ throw new Error("Cannot iterate over null (null)");
214
+ if (!steps.some((step) => step.id === id))
215
+ throw new Error(`step ${id} not in thread`);
216
+ const updated = steps.map((step) => {
217
+ if (step.id !== id)
218
+ return step;
219
+ if (as === "open") {
220
+ delete step.closed_at;
221
+ delete step.closed_by;
222
+ step.status = "open";
223
+ }
224
+ else {
225
+ step.status = as;
226
+ step.closed_at = now;
227
+ step.closed_by = by;
228
+ if (note !== "")
229
+ step.note = note;
230
+ }
231
+ return step;
232
+ });
233
+ thread.next_steps = updated;
234
+ thread.updated_at = now;
235
+ text = prettyJson(thread);
236
+ }
237
+ catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ const jqMessage = message.startsWith("step ")
240
+ ? `jq: error (at <stdin>:1): ${message}`
241
+ : `jq: error: ${message}`;
242
+ return { code: 3, stdout: "", stderr: `${jqMessage}\n` };
243
+ }
244
+ // jq > file.tmp followed by mv uses the caller's umask for this new inode.
245
+ const temporary = `${file}.tmp`;
246
+ fs.writeFileSync(temporary, text);
247
+ fs.renameSync(temporary, file);
248
+ return { code: 0, stdout: `handoff-open-steps: ${id} -> ${as}\n`, stderr: "" };
249
+ }
250
+ return { code: 2, stdout: OPEN_STEPS_UNKNOWN_USAGE, stderr: "" };
251
+ }
252
+ /** Fast-entry adapter for the lightweight open-steps implementation. */
253
+ export function runHandoffOpenStepsFast(args, _stdin, cwd, hqRoot) {
254
+ const root = hqRoot ?? cwd;
255
+ const result = runHandoffOpenSteps([...args], { root, cwd });
256
+ if (result.stdout)
257
+ process.stdout.write(result.stdout);
258
+ if (result.stderr)
259
+ process.stderr.write(result.stderr);
260
+ if (result.code !== 0)
261
+ process.exitCode = result.code;
262
+ return result.code;
263
+ }
264
+ //# sourceMappingURL=core-handoff-open-steps.js.map
@@ -1,5 +1,7 @@
1
1
  /** Native implementations of the handoff scaffold command family. */
2
2
  import { Command } from "commander";
3
+ import { runHandoffOpenSteps } from "./core-handoff-open-steps.js";
4
+ export { runHandoffOpenSteps };
3
5
  type RunResult = {
4
6
  code: number;
5
7
  stdout: string;
@@ -12,11 +14,11 @@ type HandoffContext = {
12
14
  /** Test-only override for the finalizer's historical fixed /tmp paths. */
13
15
  tmpDir?: string;
14
16
  };
15
- export declare function runHandoffOpenSteps(args: string[], context: HandoffContext): RunResult;
16
17
  export declare function runHandoffFinalize(args: string[], context: HandoffContext): RunResult;
17
18
  export declare function runHandoffPost(args: string[], context: HandoffContext): RunResult | Promise<RunResult>;
18
19
  export declare function runHandoffPostWorker(args: string[], context: HandoffContext): RunResult;
20
+ /** Fast-entry adapter; the shared family flag is resolved before this module loads. */
21
+ export declare function runHandoffFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined, commandName: string): Promise<number>;
19
22
  /** Register native handoff commands behind the shared default-off flag. */
20
23
  export declare function registerCoreHandoffCommand(core: Command): void;
21
- export {};
22
24
  //# sourceMappingURL=core-handoff.d.ts.map
@@ -5,7 +5,6 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { archiveOldThreads } from "../lib/core-utils/archive-old-threads.js";
8
- import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
9
8
  import { summarizeHqStatus } from "../lib/core-utils/hq-status-summary.js";
10
9
  import { enqueueSessionEvent } from "../lib/mesh/live/enqueue.js";
11
10
  import { renderIndexTarget } from "../lib/index-render/index.js";
@@ -13,7 +12,8 @@ import { defaultBackgroundDependencies, runBackgroundLauncher, } from "../lib/se
13
12
  import { resolveLiveRoot } from "../utils/hq-roots.js";
14
13
  import { runBundledScript } from "../utils/run-bundled-script.js";
15
14
  import { describeCoreCommand } from "./core-command-catalog.js";
16
- import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
15
+ import { compactJson, isRecord, jqTruthy, prettyJson, runHandoffOpenSteps, shellDate, } from "./core-handoff-open-steps.js";
16
+ export { runHandoffOpenSteps };
17
17
  const ASSETS = {
18
18
  finalize: "core/scripts/handoff-finalize.sh",
19
19
  post: "core/scripts/handoff-post.sh",
@@ -24,28 +24,6 @@ const DESCRIPTIONS = {
24
24
  post: "Run best-effort post-handoff maintenance",
25
25
  openSteps: "List and update open handoff steps",
26
26
  };
27
- const OPEN_STEPS_UNKNOWN_USAGE = `# hq-core: public
28
- # handoff-open-steps.sh — list and close next steps across handoff threads
29
- # (2026-09-07).
30
- #
31
- # Handoff threads record next_steps; since 2026-09-07 each carries an \x60id\x60
32
- # (\x60<thread_id>#<n>\x60) and a \x60status\x60 (open|done|dropped). This script is the
33
- # closure side: /startwork and /resumework list what is still open instead of
34
- # re-copying it forward, and a session closes a step when it is actually done.
35
- #
36
- # Usage:
37
- # handoff-open-steps.sh list [--limit N] [--company <slug>] [--json]
38
- # handoff-open-steps.sh close <step-id> [--as done|dropped] [--note "<why>"]
39
- # handoff-open-steps.sh reopen <step-id>
40
- #
41
- # Threads live in workspace/threads/T-*.json (and archive/**). Closing edits
42
- # the step in place inside its thread file (only \x60status\x60, \x60closed_at\x60,
43
- # \x60closed_by\x60, \x60note\x60) and bumps \x60updated_at\x60; nothing else in the thread is
44
- # touched. Legacy steps without an id are listed with a synthetic id and can be
45
- # closed the same way (the id is written on first close).
46
- set -uo pipefail
47
- HQ_ROOT="\${HQ_ROOT:-\${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)}}"
48
- `;
49
27
  const SESSION_ID_ENV_KEYS = [
50
28
  "HQ_SESSION_ID",
51
29
  "CLAUDE_CODE_SESSION_ID",
@@ -173,18 +151,6 @@ function copyClipboardText(value, cwd) {
173
151
  return false;
174
152
  }
175
153
  }
176
- function prettyJson(value) {
177
- return serializeJqJson(value, "pretty");
178
- }
179
- function compactJson(value) {
180
- return serializeJqJson(value, "compact");
181
- }
182
- function isRecord(value) {
183
- return value !== null && typeof value === "object" && !Array.isArray(value);
184
- }
185
- function jqTruthy(value) {
186
- return value !== undefined && value !== null && value !== false;
187
- }
188
154
  function mkdirParent(file) {
189
155
  fs.mkdirSync(path.dirname(file), { recursive: true });
190
156
  }
@@ -194,219 +160,6 @@ function writeAtomic(file, text, mode) {
194
160
  fs.writeFileSync(temporary, text, mode === undefined ? undefined : { mode });
195
161
  fs.renameSync(temporary, file);
196
162
  }
197
- function listFiles(root, predicate) {
198
- const found = [];
199
- const visit = (directory) => {
200
- let entries;
201
- try {
202
- entries = fs.readdirSync(directory, { withFileTypes: true });
203
- }
204
- catch {
205
- return;
206
- }
207
- for (const entry of entries) {
208
- const file = path.join(directory, entry.name);
209
- if (entry.isDirectory())
210
- visit(file);
211
- else if (entry.isFile() && predicate(file, entry))
212
- found.push(file);
213
- }
214
- };
215
- visit(root);
216
- return found;
217
- }
218
- function sortThreadFilesNewest(files) {
219
- return files.sort((left, right) => {
220
- const leftMtime = Math.floor(fs.statSync(left).mtimeMs / 1000);
221
- const rightMtime = Math.floor(fs.statSync(right).mtimeMs / 1000);
222
- if (leftMtime !== rightMtime)
223
- return rightMtime - leftMtime;
224
- // GNU sort -rn falls back to reverse lexical order for equal numeric keys.
225
- return right.localeCompare(left);
226
- });
227
- }
228
- function normalizedSteps(file, value) {
229
- if (!isRecord(value))
230
- return undefined;
231
- const steps = value.next_steps ?? [];
232
- if (!Array.isArray(steps))
233
- return undefined;
234
- const basename = path.basename(file).replace(/\.json$/, "");
235
- const normalized = [];
236
- for (let index = 0; index < steps.length; index += 1) {
237
- const raw = steps[index];
238
- if (typeof raw !== "string" && !isRecord(raw))
239
- return undefined;
240
- const step = typeof raw === "string" ? { step: raw } : { ...raw };
241
- if (!jqTruthy(step.id))
242
- step.id = `${basename}#${index + 1}`;
243
- if (!jqTruthy(step.status))
244
- step.status = "open";
245
- delete step.key;
246
- normalized.push(step);
247
- }
248
- return normalized;
249
- }
250
- function parseJson(file) {
251
- return JSON.parse(fs.readFileSync(file, "utf8"));
252
- }
253
- function shellDate(date) {
254
- return date.toISOString().replace(/\.\d{3}Z$/, "Z");
255
- }
256
- export function runHandoffOpenSteps(args, context) {
257
- const root = context.root;
258
- const threads = path.join(root, "workspace/threads");
259
- const argv = [...args];
260
- const command = argv[0] || "list";
261
- if (argv.length > 0)
262
- argv.shift();
263
- if (command === "list") {
264
- let limitText = "20";
265
- let company = "";
266
- let json = false;
267
- for (let index = 0; index < argv.length;) {
268
- const token = argv[index];
269
- if (token === "--limit") {
270
- limitText = argv[index + 1] ?? "";
271
- index += 2;
272
- }
273
- else if (token === "--company") {
274
- company = argv[index + 1] ?? "";
275
- index += 2;
276
- }
277
- else if (token === "--json") {
278
- json = true;
279
- index += 1;
280
- }
281
- else
282
- index += 1;
283
- }
284
- const numericLimit = /^-?\d+$/.test(limitText) ? Number(limitText) : Number.NaN;
285
- const candidates = sortThreadFilesNewest(listFiles(threads, (file) => {
286
- const name = path.basename(file);
287
- return /^T-.*\.json$/.test(name) && !name.endsWith(".changeset.json") && !file.split(path.sep).includes("resume-locks");
288
- }));
289
- let count = 0;
290
- const rows = [];
291
- for (const file of candidates) {
292
- if (!Number.isNaN(numericLimit) && count >= numericLimit)
293
- break;
294
- count += 1;
295
- let thread;
296
- let nextSteps;
297
- try {
298
- thread = parseJson(file);
299
- nextSteps = normalizedSteps(file, thread);
300
- }
301
- catch {
302
- continue;
303
- }
304
- if (!nextSteps || !isRecord(thread))
305
- continue;
306
- if (company) {
307
- const companies = (isRecord(thread.metadata) ? thread.metadata.company : undefined) ?? [];
308
- const contains = Array.isArray(companies)
309
- ? companies.includes(company)
310
- : typeof companies === "string" && companies.includes(company);
311
- if (!contains)
312
- continue;
313
- }
314
- for (const step of nextSteps) {
315
- if (step.status !== "open")
316
- continue;
317
- rows.push({
318
- thread: thread.thread_id,
319
- title: (isRecord(thread.metadata) ? thread.metadata.title : undefined) ?? "",
320
- created: thread.created_at,
321
- id: step.id,
322
- step: step.step ?? null,
323
- });
324
- }
325
- }
326
- if (json)
327
- return { code: 0, stdout: prettyJson(rows), stderr: "" };
328
- if (rows.length === 0)
329
- return { code: 0, stdout: `No open handoff steps in the last ${count} thread(s).\n`, stderr: "" };
330
- const jqString = (value) => value === null || value === undefined
331
- ? "null"
332
- : typeof value === "string" ? value : compactJson(value).trimEnd();
333
- const output = rows.map((row) => `${jqString(row.id)} ${jqString(row.step)} [${jqString(row.title)}]\n`).join("");
334
- return { code: 0, stdout: output, stderr: "" };
335
- }
336
- if (command === "close" || command === "reopen") {
337
- const id = argv[0] ?? "";
338
- const rest = argv.slice(1);
339
- if (!id)
340
- return { code: 2, stdout: "", stderr: `usage: ${command} <step-id>\n` };
341
- let as = "done";
342
- let note = "";
343
- for (let index = 0; index < rest.length;) {
344
- if (rest[index] === "--as") {
345
- as = rest[index + 1] ?? "";
346
- index += 2;
347
- }
348
- else if (rest[index] === "--note") {
349
- note = rest[index + 1] ?? "";
350
- index += 2;
351
- }
352
- else
353
- index += 1;
354
- }
355
- if (command === "reopen")
356
- as = "open";
357
- if (!new Set(["done", "dropped", "open"]).has(as))
358
- return { code: 2, stdout: "", stderr: "--as must be done|dropped\n" };
359
- const threadId = id.split("#", 1)[0] ?? "";
360
- const candidates = listFiles(threads, (file) => path.basename(file) === `${threadId}.json` && !file.split(path.sep).includes("resume-locks"));
361
- const file = candidates[0];
362
- if (!file)
363
- return { code: 3, stdout: "", stderr: `handoff-open-steps: thread ${threadId} not found under ${threads}\n` };
364
- const now = shellDate(nowFor(context));
365
- const by = process.env.HQ_SESSION_ID || process.env.CLAUDE_SESSION_ID || "unknown-session";
366
- let text;
367
- try {
368
- const thread = parseJson(file);
369
- const steps = normalizedSteps(file, thread);
370
- if (!steps || !isRecord(thread))
371
- throw new Error("Cannot iterate over null (null)");
372
- if (!steps.some((step) => step.id === id))
373
- throw new Error(`step ${id} not in thread`);
374
- const updated = steps.map((step) => {
375
- if (step.id !== id)
376
- return step;
377
- if (as === "open") {
378
- delete step.closed_at;
379
- delete step.closed_by;
380
- step.status = "open";
381
- }
382
- else {
383
- step.status = as;
384
- step.closed_at = now;
385
- step.closed_by = by;
386
- if (note !== "")
387
- step.note = note;
388
- }
389
- return step;
390
- });
391
- thread.next_steps = updated;
392
- thread.updated_at = now;
393
- text = prettyJson(thread);
394
- }
395
- catch (error) {
396
- const message = error instanceof Error ? error.message : String(error);
397
- const jqMessage = message.startsWith("step ")
398
- ? `jq: error (at <stdin>:1): ${message}`
399
- : `jq: error: ${message}`;
400
- return { code: 3, stdout: "", stderr: `${jqMessage}\n` };
401
- }
402
- // jq > file.tmp followed by mv uses the caller's umask for this new inode.
403
- const temporary = `${file}.tmp`;
404
- fs.writeFileSync(temporary, text);
405
- fs.renameSync(temporary, file);
406
- return { code: 0, stdout: `handoff-open-steps: ${id} -> ${as}\n`, stderr: "" };
407
- }
408
- return { code: 2, stdout: OPEN_STEPS_UNKNOWN_USAGE, stderr: "" };
409
- }
410
163
  function isSensitivePath(value) {
411
164
  const exceptions = new Set(["core/settings/.gitkeep", "core/settings/pure-ralph.json"]);
412
165
  if (exceptions.has(value))
@@ -1331,6 +1084,25 @@ function emitResult(result) {
1331
1084
  if (result.code !== 0)
1332
1085
  process.exit(result.code);
1333
1086
  }
1087
+ /** Fast-entry adapter; the shared family flag is resolved before this module loads. */
1088
+ export async function runHandoffFast(args, _stdin, _cwd, hqRoot, commandName) {
1089
+ const liveRoot = hqRoot ?? resolveLiveRoot();
1090
+ const context = { root: liveRoot, cwd: liveRoot };
1091
+ const result = await withRootEnvironment(liveRoot, () => {
1092
+ if (commandName === "finalize")
1093
+ return runHandoffFinalize([...args], context);
1094
+ if (commandName === "post")
1095
+ return runHandoffPost([...args], context);
1096
+ return runHandoffOpenSteps([...args], context);
1097
+ });
1098
+ if (result.stdout)
1099
+ process.stdout.write(result.stdout);
1100
+ if (result.stderr)
1101
+ process.stderr.write(result.stderr);
1102
+ if (result.code !== 0)
1103
+ process.exitCode = result.code;
1104
+ return result.code;
1105
+ }
1334
1106
  /** Register native handoff commands behind the shared default-off flag. */
1335
1107
  export function registerCoreHandoffCommand(core) {
1336
1108
  const handoff = core.command("handoff").description("Handoff session helpers");
@@ -1346,6 +1118,7 @@ export function registerCoreHandoffCommand(core) {
1346
1118
  const scope = core.opts();
1347
1119
  const root = resolveLiveRoot({ hqRoot: scope.hqRoot });
1348
1120
  const operands = cmd.args.length > 0 ? cmd.args : args;
1121
+ const { coreNativeHandoffEnabled } = await import("./core-native-handoff-flag.js");
1349
1122
  if (!await coreNativeHandoffEnabled()) {
1350
1123
  const bundled = runBundledScript({ asset: ASSETS[name === "open-steps" ? "openSteps" : name], args: operands, cwd: root, hqRoot: root });
1351
1124
  if (bundled.code !== 0)
@@ -19,5 +19,7 @@ export type ReconcileOptions = {
19
19
  export declare function runReconcile(argv: string[], rootArg?: string, options?: ReconcileOptions): Promise<number>;
20
20
  export declare function registerCoreJobsNativeCommand(core: Command): void;
21
21
  export declare function rawArgumentsForCommand(command: Command, fallback: string[], raw?: string[]): string[];
22
+ /** Fast-entry adapter; the shared family flag is resolved before this module loads. */
23
+ export declare function runJobsFast(args: readonly string[], _stdin: string, _cwd: string, _liveRoot: string | undefined, commandName: string, hqRootArg: string | undefined): Promise<number>;
22
24
  export {};
23
25
  //# sourceMappingURL=core-jobs-native.d.ts.map