@indigoai-us/hq-cli 5.293.0 → 5.295.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.
- package/CHANGELOG.md +39 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +54 -33
- package/dist/command-catalog.generated.js +68 -42
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +76 -1
- package/dist/commands/core-command-catalog.js +3 -2
- package/dist/commands/core-delegate-w1b.d.ts +2 -0
- package/dist/commands/core-delegate-w1b.js +28 -2
- package/dist/commands/core-delegate.d.ts +3 -1
- package/dist/commands/core-delegate.js +35 -5
- package/dist/commands/core-handoff-fast.d.ts +18 -0
- package/dist/commands/core-handoff-fast.js +103 -0
- package/dist/commands/core-handoff-open-steps.d.ts +21 -0
- package/dist/commands/core-handoff-open-steps.js +264 -0
- package/dist/commands/core-handoff.d.ts +4 -2
- package/dist/commands/core-handoff.js +22 -249
- package/dist/commands/core-jobs-native.d.ts +2 -0
- package/dist/commands/core-jobs-native.js +22 -11
- package/dist/commands/core-knowledge-prefs.d.ts +2 -0
- package/dist/commands/core-knowledge-prefs.js +13 -1
- package/dist/commands/core-ontology-native.d.ts +2 -0
- package/dist/commands/core-ontology-native.js +22 -2
- package/dist/commands/core-policy.d.ts +2 -0
- package/dist/commands/core-policy.js +18 -1
- package/dist/commands/core.js +2 -1
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +22 -7
- package/dist/commands/doctor.js +0 -74
- package/dist/commands/lanes-autopilot.d.ts +11 -0
- package/dist/commands/lanes-autopilot.js +336 -0
- package/dist/commands/lanes-message.js +23 -5
- package/dist/commands/lanes-questions.js +8 -5
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes-shared.d.ts +18 -2
- package/dist/commands/lanes-stories.d.ts +14 -0
- package/dist/commands/lanes-stories.js +308 -0
- package/dist/commands/lanes.d.ts +1 -9
- package/dist/commands/lanes.js +216 -481
- package/dist/fast-core.js +53 -25
- package/dist/fast-scaffold.d.ts +15 -7
- package/dist/fast-scaffold.js +167 -17
- package/dist/index.js +1 -10
- package/dist/lib/core-utils/sentry-report.d.ts +0 -19
- package/dist/lib/core-utils/sentry-report.js +8 -61
- package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
- package/dist/lib/core-utils/worktree-resume.js +16 -0
- package/dist/lib/daemon/migrate.d.ts +8 -1
- package/dist/lib/daemon/migrate.js +19 -7
- package/dist/lib/doctor/checks/claude-wiring.js +9 -0
- package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
- package/dist/lib/lanes/manager-status-gate.js +2 -43
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
- package/dist/lib/lanes/outpost-hire-cache.js +0 -1
- package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
- package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -3
- package/dist/lib/lanes/reconcile.js +44 -38
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +18 -5
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +54 -3
- package/dist/lib/lanes/types.d.ts +18 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +4 -3
- package/dist/lib/lanes/watcher.js +102 -34
- package/dist/lib/mesh/live/daemon/run.js +18 -1
- package/dist/native-fast-route.d.ts +3 -1
- package/dist/native-fast-route.js +10 -8
- package/dist/utils/cognito-session.d.ts +6 -0
- package/dist/utils/cognito-session.js +9 -0
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/fast-stream-errors.d.ts +12 -0
- package/dist/utils/fast-stream-errors.js +18 -0
- package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
- package/dist/utils/scaffold-fast-import-guard.js +6 -3
- package/dist/utils/self-update.js +31 -0
- package/dist/utils/version-gate.js +22 -3
- package/package.json +1 -1
- package/dist/fast-lanes-list-route.d.ts +0 -8
- package/dist/fast-lanes-list-route.js +0 -32
- package/dist/fast-lanes-list.d.ts +0 -3
- package/dist/fast-lanes-list.js +0 -224
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
- package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
- package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
- package/dist/lib/doctor/client-health-sentry.js +0 -286
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
- package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
- package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Lightweight entry adapters for the fast handoff routes. */
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* `handoff finalize --help` is historically an unknown-argument error. Keep
|
|
7
|
+
* that fast error path out of the large handoff implementation module.
|
|
8
|
+
*/
|
|
9
|
+
export function handoffFinalizeHelpResult(args, root, tmpRoot = "/tmp") {
|
|
10
|
+
if (args[0] !== "--help")
|
|
11
|
+
return undefined;
|
|
12
|
+
const absoluteRoot = fs.realpathSync(root);
|
|
13
|
+
const rootEnvironment = {
|
|
14
|
+
...process.env,
|
|
15
|
+
HQ_ROOT: absoluteRoot,
|
|
16
|
+
CLAUDE_PROJECT_DIR: absoluteRoot,
|
|
17
|
+
PWD: absoluteRoot,
|
|
18
|
+
};
|
|
19
|
+
const probe = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
|
20
|
+
cwd: absoluteRoot,
|
|
21
|
+
env: { ...rootEnvironment, LC_ALL: "C" },
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
if (!probe.error && probe.status === 0) {
|
|
26
|
+
// The result only determines whether later finalize work stages files. The
|
|
27
|
+
// unknown --help argument returns before that work starts.
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const output = `${String(probe.stdout ?? "")}${String(probe.stderr ?? "")}`.trimEnd();
|
|
31
|
+
const checkoutMarker = path.join(absoluteRoot, ".git");
|
|
32
|
+
if (fs.existsSync(checkoutMarker) || fs.lstatSync(checkoutMarker, { throwIfNoEntry: false })
|
|
33
|
+
|| !output.includes("not a git repository")) {
|
|
34
|
+
return {
|
|
35
|
+
code: 4,
|
|
36
|
+
stdout: "",
|
|
37
|
+
stderr: `handoff-finalize: Git checkout probe failed (exit ${probe.status ?? 127}): ${output}\n`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const gitPidFile = path.join(tmpRoot, "handoff-git-bg.pid");
|
|
42
|
+
if (fs.existsSync(gitPidFile)) {
|
|
43
|
+
try {
|
|
44
|
+
fs.rmSync(gitPidFile, { force: true });
|
|
45
|
+
}
|
|
46
|
+
catch { /* legacy cleanup is best-effort */ }
|
|
47
|
+
}
|
|
48
|
+
return { code: 2, stdout: "", stderr: "Unknown arg: --help\n" };
|
|
49
|
+
}
|
|
50
|
+
async function launchHandoffPost(args, root) {
|
|
51
|
+
return await new Promise((resolve) => {
|
|
52
|
+
try {
|
|
53
|
+
const child = spawn("hq", ["core", "--hq-root", root, "handoff", "post", ...args], {
|
|
54
|
+
cwd: root,
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
HQ_ROOT: root,
|
|
58
|
+
CLAUDE_PROJECT_DIR: root,
|
|
59
|
+
PWD: root,
|
|
60
|
+
HQ_NATIVE_HANDOFF_POST_CHILD: "1",
|
|
61
|
+
},
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: "ignore",
|
|
64
|
+
});
|
|
65
|
+
child.once("error", (error) => {
|
|
66
|
+
process.stderr.write(`${error.message}\n`);
|
|
67
|
+
resolve(127);
|
|
68
|
+
});
|
|
69
|
+
child.once("spawn", () => {
|
|
70
|
+
child.unref();
|
|
71
|
+
resolve(0);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
process.stderr.write(`${message}\n`);
|
|
77
|
+
resolve(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Keep cheap help/launch paths in this small module. Commands that do the
|
|
83
|
+
* actual handoff work load the existing implementation only when invoked.
|
|
84
|
+
*/
|
|
85
|
+
export async function runHandoffFastEntry(args, stdin, cwd, hqRoot, commandName) {
|
|
86
|
+
const root = hqRoot ?? cwd;
|
|
87
|
+
if (commandName === "post" && process.env.HQ_NATIVE_HANDOFF_POST_CHILD !== "1") {
|
|
88
|
+
return await launchHandoffPost(args, root);
|
|
89
|
+
}
|
|
90
|
+
if (commandName === "finalize") {
|
|
91
|
+
const helpResult = handoffFinalizeHelpResult(args, root);
|
|
92
|
+
if (helpResult) {
|
|
93
|
+
if (helpResult.stdout)
|
|
94
|
+
process.stdout.write(helpResult.stdout);
|
|
95
|
+
if (helpResult.stderr)
|
|
96
|
+
process.stderr.write(helpResult.stderr);
|
|
97
|
+
return helpResult.code;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const handoff = await import("./core-handoff.js");
|
|
101
|
+
return await handoff.runHandoffFast(args, stdin, cwd, root, commandName);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=core-handoff-fast.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Lightweight native handler for the handoff open-steps helper. */
|
|
2
|
+
type RunResult = {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
type HandoffContext = {
|
|
8
|
+
root: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
now?: () => Date;
|
|
11
|
+
};
|
|
12
|
+
export declare function prettyJson(value: unknown): string;
|
|
13
|
+
export declare function compactJson(value: unknown): string;
|
|
14
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
15
|
+
export declare function jqTruthy(value: unknown): boolean;
|
|
16
|
+
export declare function shellDate(date: Date): string;
|
|
17
|
+
export declare function runHandoffOpenSteps(args: string[], context: HandoffContext): RunResult;
|
|
18
|
+
/** Fast-entry adapter for the lightweight open-steps implementation. */
|
|
19
|
+
export declare function runHandoffOpenStepsFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined): number;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=core-handoff-open-steps.d.ts.map
|
|
@@ -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
|