@indigoai-us/hq-cli 5.345.54 → 5.345.55
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 +19 -0
- package/dist/command-catalog.generated.d.ts +103 -2
- package/dist/command-catalog.generated.js +124 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-session-start.d.ts +22 -0
- package/dist/commands/lanes-session-start.js +215 -0
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.js +4 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq import` command group.
|
|
3
|
+
*
|
|
4
|
+
* hq import scan human-readable scan summary
|
|
5
|
+
* hq import scan --json only the final `done` event (one JSON line)
|
|
6
|
+
* hq import scan --json --stream every scan event as JSON Lines
|
|
7
|
+
*
|
|
8
|
+
* Runs the HQ tree's import-context scanner with `--progress-json` (local
|
|
9
|
+
* only, read-only apart from report.json). The HQ tree is `--hq-root`, else
|
|
10
|
+
* the one the HQ installer recorded in ~/.hq/menubar.json; this command does
|
|
11
|
+
* not guess from the current folder or $HQ_ROOT. Event contract and types:
|
|
12
|
+
* src/lib/import-scan/events.ts and docs/import-scan-stream.md.
|
|
13
|
+
*/
|
|
14
|
+
import { Command, type CommanderError } from "commander";
|
|
15
|
+
/** Exit code for every usage error of `hq import scan` (contract). */
|
|
16
|
+
export declare const IMPORT_SCAN_USAGE_EXIT_CODE = 2;
|
|
17
|
+
/** Exit code for a Commander error raised while parsing `hq import scan`. */
|
|
18
|
+
export declare function importScanExitCode(err: CommanderError): number;
|
|
19
|
+
export interface RegisterImportCommandDeps {
|
|
20
|
+
/** Ends the process (default process.exit). Tests pass one that throws. */
|
|
21
|
+
exit?: (code: number, err: CommanderError) => never;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerImportCommand(program: Command, deps?: RegisterImportCommandDeps): Command;
|
|
24
|
+
//# sourceMappingURL=import-scan.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq import` command group.
|
|
3
|
+
*
|
|
4
|
+
* hq import scan human-readable scan summary
|
|
5
|
+
* hq import scan --json only the final `done` event (one JSON line)
|
|
6
|
+
* hq import scan --json --stream every scan event as JSON Lines
|
|
7
|
+
*
|
|
8
|
+
* Runs the HQ tree's import-context scanner with `--progress-json` (local
|
|
9
|
+
* only, read-only apart from report.json). The HQ tree is `--hq-root`, else
|
|
10
|
+
* the one the HQ installer recorded in ~/.hq/menubar.json; this command does
|
|
11
|
+
* not guess from the current folder or $HQ_ROOT. Event contract and types:
|
|
12
|
+
* src/lib/import-scan/events.ts and docs/import-scan-stream.md.
|
|
13
|
+
*/
|
|
14
|
+
import { runImportScan } from "../lib/import-scan/run.js";
|
|
15
|
+
function collect(value, previous = []) {
|
|
16
|
+
return [...previous, value];
|
|
17
|
+
}
|
|
18
|
+
/** Exit code for every usage error of `hq import scan` (contract). */
|
|
19
|
+
export const IMPORT_SCAN_USAGE_EXIT_CODE = 2;
|
|
20
|
+
/** Commander error codes that mean "the command line was wrong". */
|
|
21
|
+
const COMMANDER_USAGE_ERROR_CODES = new Set([
|
|
22
|
+
"commander.unknownOption",
|
|
23
|
+
"commander.unknownCommand",
|
|
24
|
+
"commander.missingArgument",
|
|
25
|
+
"commander.optionMissingArgument",
|
|
26
|
+
"commander.missingMandatoryOptionValue",
|
|
27
|
+
"commander.conflictingOption",
|
|
28
|
+
"commander.excessArguments",
|
|
29
|
+
"commander.invalidArgument",
|
|
30
|
+
]);
|
|
31
|
+
/** Exit code for a Commander error raised while parsing `hq import scan`. */
|
|
32
|
+
export function importScanExitCode(err) {
|
|
33
|
+
if (err.exitCode !== 0 && COMMANDER_USAGE_ERROR_CODES.has(err.code))
|
|
34
|
+
return IMPORT_SCAN_USAGE_EXIT_CODE;
|
|
35
|
+
return err.exitCode;
|
|
36
|
+
}
|
|
37
|
+
export function registerImportCommand(program, deps = {}) {
|
|
38
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
39
|
+
const importCmd = program
|
|
40
|
+
.command("import")
|
|
41
|
+
.description("Import existing AI context (Claude Code, Codex, repos) into HQ");
|
|
42
|
+
importCmd
|
|
43
|
+
.command("scan")
|
|
44
|
+
.description("Scan this machine for AI context (sessions, repos, companies, projects); local only")
|
|
45
|
+
.option("--json", "Print only the final done event as JSON")
|
|
46
|
+
.option("--stream", "With --json, print every scan event as JSON Lines while scanning")
|
|
47
|
+
.option("--hq-root <path>", "HQ folder (default: the folder the HQ installer recorded)")
|
|
48
|
+
.option("--output <path>", "Where to write report.json (default: <hq>/workspace/imports/<scan id>/report.json)")
|
|
49
|
+
.option("--scope <dir>", "Also scan this folder (repeatable)", collect, [])
|
|
50
|
+
.option("--claude-export <path>", "Include a claude.ai data export (unzipped folder or conversations.json)")
|
|
51
|
+
.option("--no-default-scopes", "Scan only --scope folders, not the default code folders")
|
|
52
|
+
.addHelpText("after", "\nThe desktop app runs: hq import scan --json --stream\n" +
|
|
53
|
+
"Events (v1): start, source, count, company, project, error, done. See docs/import-scan-stream.md.")
|
|
54
|
+
// Usage errors (unknown option, missing value, --stream without --json)
|
|
55
|
+
// exit 2 per the scan contract. Scoped to this command only; Commander
|
|
56
|
+
// would otherwise exit 1 for most of them.
|
|
57
|
+
.exitOverride((err) => exit(importScanExitCode(err), err))
|
|
58
|
+
.action(async (opts, cmd) => {
|
|
59
|
+
if (opts.stream && !opts.json) {
|
|
60
|
+
cmd.error("error: --stream requires --json", { exitCode: 2 });
|
|
61
|
+
}
|
|
62
|
+
const mode = opts.json ? (opts.stream ? "stream" : "final") : "human";
|
|
63
|
+
const code = await runImportScan({
|
|
64
|
+
mode,
|
|
65
|
+
hqRoot: opts.hqRoot,
|
|
66
|
+
output: opts.output,
|
|
67
|
+
scopes: opts.scope,
|
|
68
|
+
claudeExport: opts.claudeExport,
|
|
69
|
+
noDefaultScopes: opts.defaultScopes === false,
|
|
70
|
+
}, { stdout: process.stdout, stderr: process.stderr });
|
|
71
|
+
process.exitCode = code;
|
|
72
|
+
});
|
|
73
|
+
return importCmd;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=import-scan.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
export interface SessionStartOptions {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
stdin?: NodeJS.ReadableStream;
|
|
7
|
+
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
8
|
+
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
9
|
+
resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
|
|
10
|
+
startSweep?: (root: string, env: NodeJS.ProcessEnv, context: SessionStartContext) => void;
|
|
11
|
+
}
|
|
12
|
+
interface SessionStartContext {
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
companySlug?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
17
|
+
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, context?: SessionStartContext): void;
|
|
18
|
+
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
19
|
+
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
20
|
+
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=lanes-session-start.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
7
|
+
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
8
|
+
import { LANE_CONDUCT_SESSION_START_FLAG } from "../lib/lanes/flag-keys.js";
|
|
9
|
+
function enabledFromSettings(root) {
|
|
10
|
+
const personal = path.join(root, "personal", "settings", "orchestrator.yaml");
|
|
11
|
+
const settings = fs.existsSync(personal) ? personal : path.join(root, "core", "settings", "orchestrator.yaml");
|
|
12
|
+
if (!fs.existsSync(settings))
|
|
13
|
+
return false;
|
|
14
|
+
const text = fs.readFileSync(settings, "utf8");
|
|
15
|
+
const block = /^conduct:\s*(?:#.*)?\r?\n((?:[ \t]+.*\r?\n?)*)/m.exec(text)?.[1] ?? "";
|
|
16
|
+
const value = /^\s+default_enabled:\s*["']?(true|false|yes|no|on|off|1|0)["']?\s*(?:#.*)?$/mi.exec(block)?.[1]?.toLowerCase();
|
|
17
|
+
return value === "true" || value === "yes" || value === "on" || value === "1";
|
|
18
|
+
}
|
|
19
|
+
function sessionWasDisabled(root, env, payloadSessionId) {
|
|
20
|
+
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
21
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId))
|
|
22
|
+
return false;
|
|
23
|
+
const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
24
|
+
if (!fs.existsSync(meta))
|
|
25
|
+
return false;
|
|
26
|
+
const text = fs.readFileSync(meta, "utf8");
|
|
27
|
+
return /^\s*(?:conduct_mode|conduct_enabled|conduct_default):\s*(?:off|false|no|0)\s*(?:#.*)?$/mi.test(text) ||
|
|
28
|
+
/^\s*conduct_engine:\s*(?:""|''|#.*)?\s*$/mi.test(text);
|
|
29
|
+
}
|
|
30
|
+
function sessionStartContext(root, env, payloadSessionId) {
|
|
31
|
+
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
32
|
+
let companySlug = "";
|
|
33
|
+
if (sessionId && /^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
34
|
+
const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
35
|
+
if (fs.existsSync(meta)) {
|
|
36
|
+
const text = fs.readFileSync(meta, "utf8");
|
|
37
|
+
companySlug = /^\s*company_slug:\s*["']?([A-Za-z0-9][A-Za-z0-9-]*)["']?\s*(?:#.*)?$/mi.exec(text)?.[1] ?? "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!companySlug)
|
|
41
|
+
companySlug = env.HQ_COMPANY_SLUG?.trim() ?? "";
|
|
42
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(companySlug))
|
|
43
|
+
companySlug = "";
|
|
44
|
+
return { ...(sessionId ? { sessionId } : {}), ...(companySlug ? { companySlug } : {}) };
|
|
45
|
+
}
|
|
46
|
+
function extractConductorBlock(core) {
|
|
47
|
+
const start = core.indexOf("<!-- inject:start -->");
|
|
48
|
+
const end = core.indexOf("<!-- inject:end -->");
|
|
49
|
+
if (start < 0 || end < 0 || end <= start)
|
|
50
|
+
return null;
|
|
51
|
+
return core.slice(start + "<!-- inject:start -->".length, end).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
|
|
52
|
+
}
|
|
53
|
+
const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
|
|
54
|
+
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
55
|
+
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn, context = {}) {
|
|
56
|
+
// The orphan command resolves company membership when --company is absent;
|
|
57
|
+
// skip here if this session has no tenant context rather than risk ambiguity.
|
|
58
|
+
if (!context.companySlug)
|
|
59
|
+
return;
|
|
60
|
+
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
61
|
+
const stateDir = path.join(stateRoot, "hq-cli");
|
|
62
|
+
const lockPath = path.join(stateDir, "lanes-session-start-orphan-sweep.json");
|
|
63
|
+
const reportDir = path.join(stateDir, "lanes-orphan-reports", context.companySlug);
|
|
64
|
+
const ownerToken = randomUUID();
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
67
|
+
fs.mkdirSync(reportDir, { recursive: true, mode: 0o700 });
|
|
68
|
+
let fd;
|
|
69
|
+
try {
|
|
70
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code !== "EEXIST")
|
|
74
|
+
return;
|
|
75
|
+
let previous = {};
|
|
76
|
+
try {
|
|
77
|
+
previous = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch { /* malformed lease can be reclaimed */ }
|
|
80
|
+
const age = Date.now() - Number(previous.started_at || 0);
|
|
81
|
+
let running = false;
|
|
82
|
+
if (Number.isInteger(previous.pid) && Number(previous.pid) > 0) {
|
|
83
|
+
try {
|
|
84
|
+
process.kill(Number(previous.pid), 0);
|
|
85
|
+
running = true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
running = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (age < ORPHAN_SWEEP_LOCK_TTL_MS && running)
|
|
92
|
+
return;
|
|
93
|
+
fs.rmSync(lockPath, { force: true });
|
|
94
|
+
try {
|
|
95
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: startedAt, owner_token: ownerToken }));
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
const cli = process.argv[1];
|
|
105
|
+
if (!cli) {
|
|
106
|
+
fs.rmSync(lockPath, { force: true });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const wrapper = [
|
|
110
|
+
"const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');",
|
|
111
|
+
"const [cli,lock,root,token,report,company]=process.argv.slice(1);",
|
|
112
|
+
"const cleanup=()=>{try{const lease=JSON.parse(fs.readFileSync(lock,'utf8'));if(lease.owner_token===token)fs.rmSync(lock,{force:true})}catch{}};",
|
|
113
|
+
"const start=()=>{let lease;try{lease=JSON.parse(fs.readFileSync(lock,'utf8'))}catch{}if(lease?.owner_token!==token){if(Date.now()<Number(lease?.started_at||0)+1000)return setTimeout(start,10);process.exit(0)}",
|
|
114
|
+
"try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
|
|
115
|
+
"const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
|
|
116
|
+
"const child=cp.spawn(process.execPath,[cli,'lanes','orphans','--company',company,'--json'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
|
|
117
|
+
"child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
|
|
118
|
+
"child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
|
|
119
|
+
].join("");
|
|
120
|
+
const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
|
|
121
|
+
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath, context.companySlug], {
|
|
122
|
+
detached: true,
|
|
123
|
+
stdio: "ignore",
|
|
124
|
+
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1" },
|
|
125
|
+
});
|
|
126
|
+
if (launcher.pid) {
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: launcher.pid, started_at: startedAt, owner_token: ownerToken }));
|
|
129
|
+
}
|
|
130
|
+
catch { /* TTL lease still bounds an orphaned lock */ }
|
|
131
|
+
}
|
|
132
|
+
launcher.on("error", () => {
|
|
133
|
+
try {
|
|
134
|
+
const lease = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
135
|
+
if (lease.owner_token === ownerToken)
|
|
136
|
+
fs.rmSync(lockPath, { force: true });
|
|
137
|
+
}
|
|
138
|
+
catch { /* The lease may already have been reclaimed. */ }
|
|
139
|
+
});
|
|
140
|
+
launcher.unref();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// SessionStart's orphan report is advisory and must never block startup.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
147
|
+
export async function runLaneSessionStart(options = {}) {
|
|
148
|
+
const env = options.env ?? process.env;
|
|
149
|
+
const stdin = options.stdin ?? process.stdin;
|
|
150
|
+
const stdout = options.stdout ?? process.stdout;
|
|
151
|
+
const stderr = options.stderr ?? process.stderr;
|
|
152
|
+
let debugLineWritten = false;
|
|
153
|
+
const debug = (message) => {
|
|
154
|
+
if (env.HQ_LANES_DEBUG === "1" && !debugLineWritten) {
|
|
155
|
+
debugLineWritten = true;
|
|
156
|
+
stderr.write(`${message}\n`);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
const chunks = [];
|
|
161
|
+
for await (const chunk of stdin)
|
|
162
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
163
|
+
const input = Buffer.concat(chunks).toString("utf8");
|
|
164
|
+
let source = "startup";
|
|
165
|
+
let payloadSessionId;
|
|
166
|
+
try {
|
|
167
|
+
const payload = JSON.parse(input);
|
|
168
|
+
source = payload.source || "startup";
|
|
169
|
+
payloadSessionId = payload.session_id;
|
|
170
|
+
}
|
|
171
|
+
catch { /* malformed hook input uses startup default */ }
|
|
172
|
+
if (source !== "startup")
|
|
173
|
+
return;
|
|
174
|
+
const disabledHooks = `,${env.HQ_DISABLED_HOOKS ?? ""},`.replace(/\s/g, "");
|
|
175
|
+
if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
|
|
176
|
+
return;
|
|
177
|
+
const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
|
|
178
|
+
const flagEnabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_SESSION_START_FLAG, {
|
|
179
|
+
env,
|
|
180
|
+
defaultValue: true,
|
|
181
|
+
reportFailure: (error) => {
|
|
182
|
+
const kind = error instanceof Error ? error.name : "Error";
|
|
183
|
+
debug(`hq lanes session-start: flag read failed (${kind})`);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (!flagEnabled)
|
|
187
|
+
return;
|
|
188
|
+
const context = sessionStartContext(root, env, payloadSessionId);
|
|
189
|
+
(options.startSweep ?? ((r, e, c) => startSessionStartOrphanSweep(r, e, spawn, c)))(root, env, context);
|
|
190
|
+
if (sessionWasDisabled(root, env, payloadSessionId))
|
|
191
|
+
return;
|
|
192
|
+
let enabled = enabledFromSettings(root);
|
|
193
|
+
if (/^(1|true|on|yes)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
|
|
194
|
+
enabled = true;
|
|
195
|
+
else if (/^(0|false|off|no)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
|
|
196
|
+
enabled = false;
|
|
197
|
+
if (!enabled)
|
|
198
|
+
return;
|
|
199
|
+
const core = path.join(root, ".claude", "skills", "conduct", "conductor-core.md");
|
|
200
|
+
if (!fs.existsSync(core))
|
|
201
|
+
return;
|
|
202
|
+
const block = extractConductorBlock(fs.readFileSync(core, "utf8"));
|
|
203
|
+
if (block)
|
|
204
|
+
stdout.write(`${block}\n`);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
debug("hq lanes session-start: bootstrap failed");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function registerLaneSessionStartCommand(lanes) {
|
|
211
|
+
lanes.command("session-start", { hidden: true })
|
|
212
|
+
.description("Internal SessionStart bootstrap")
|
|
213
|
+
.action(async () => runLaneSessionStart());
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=lanes-session-start.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type MonitorEngine } from "../lib/lanes/index.js";
|
|
1
2
|
import type { Ctx, MonitorCheckOptions, LaneWaitCommandOptions, LaneWatchHandlerDeps, LaneWatchOptions } from "./lanes-shared.js";
|
|
3
|
+
export declare function resolveMonitorEngine(requested: string | undefined, env: NodeJS.ProcessEnv): MonitorEngine;
|
|
2
4
|
export declare function createLaneWatchHandlers(deps: LaneWatchHandlerDeps): {
|
|
3
5
|
runWatch: (ctx: Ctx, laneIds: string[], opts: LaneWatchOptions) => Promise<number>;
|
|
4
6
|
runMonitorCheck: (ctx: Ctx, opts: MonitorCheckOptions) => Promise<number>;
|
|
@@ -10,8 +10,9 @@ const MONITOR_ACTIVE_LANE_STATES = new Set([
|
|
|
10
10
|
"awaiting_input",
|
|
11
11
|
"blocked",
|
|
12
12
|
]);
|
|
13
|
-
function resolveMonitorEngine(requested, env) {
|
|
13
|
+
export function resolveMonitorEngine(requested, env) {
|
|
14
14
|
const value = requested?.trim().toLowerCase() ||
|
|
15
|
+
env.HQ_LANE_ENGINE?.trim().toLowerCase() ||
|
|
15
16
|
env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
|
|
16
17
|
"claude";
|
|
17
18
|
if (!MONITOR_ENGINES.includes(value)) {
|
package/dist/commands/lanes.js
CHANGED
|
@@ -33,6 +33,7 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
|
33
33
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
34
34
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
|
+
import { registerLaneSessionStartCommand } from "./lanes-session-start.js";
|
|
36
37
|
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
37
38
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
38
39
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
@@ -9234,8 +9235,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9234
9235
|
timeoutPath,
|
|
9235
9236
|
});
|
|
9236
9237
|
const lanes = parent
|
|
9237
|
-
.command("lanes"
|
|
9238
|
+
.command("lanes")
|
|
9238
9239
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
9240
|
+
registerLaneSessionStartCommand(lanes);
|
|
9239
9241
|
registerLaneChecksCommand(lanes, {
|
|
9240
9242
|
hqRoot: ctx.hqRoot,
|
|
9241
9243
|
resolveCompany: ctx.resolveCompany,
|
|
@@ -9506,7 +9508,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9506
9508
|
.command("monitor-check")
|
|
9507
9509
|
.description("Check that active Claude senior lanes have live Monitor watch tasks")
|
|
9508
9510
|
.option("--session <id>", "Senior session id")
|
|
9509
|
-
.option("--engine <engine>", "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude")
|
|
9511
|
+
.option("--engine <engine>", "claude | codex | grok; defaults to HQ_LANE_ENGINE (HQ_CONDUCT_ENGINE is deprecated) or claude")
|
|
9510
9512
|
.option("--reminder", "Print the senior Monitor reminder text")
|
|
9511
9513
|
.option("--json", "JSON on stdout")
|
|
9512
9514
|
.action(wrapText(async (opts) => watchHandlers.runMonitorCheck(ctx, opts)));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function conflictNoticeStateDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
2
|
+
/** Acknowledge by appending a tombstone; runner-owned notice data is read-only here. */
|
|
3
|
+
export declare function acknowledgeConflictNotice(id: string, stateDir?: string, acknowledgedAt?: string): void;
|
|
4
|
+
//# sourceMappingURL=conflict-notices.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
function writeAllSync(fd, text) {
|
|
5
|
+
const bytes = Buffer.from(text);
|
|
6
|
+
let offset = 0;
|
|
7
|
+
while (offset < bytes.length) {
|
|
8
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
9
|
+
if (written <= 0)
|
|
10
|
+
throw new Error("Conflict acknowledgement append made no progress.");
|
|
11
|
+
offset += written;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function conflictNoticeStateDir(env = process.env, home = os.homedir()) {
|
|
15
|
+
return env.HQ_STATE_DIR || path.join(home, ".hq");
|
|
16
|
+
}
|
|
17
|
+
/** Acknowledge by appending a tombstone; runner-owned notice data is read-only here. */
|
|
18
|
+
export function acknowledgeConflictNotice(id, stateDir = conflictNoticeStateDir(), acknowledgedAt = new Date().toISOString()) {
|
|
19
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
20
|
+
throw new Error("Invalid conflict notice id.");
|
|
21
|
+
const noticesPath = path.join(stateDir, "conflict-notices.json");
|
|
22
|
+
let notices = [];
|
|
23
|
+
try {
|
|
24
|
+
const file = JSON.parse(fs.readFileSync(noticesPath, "utf8"));
|
|
25
|
+
if (file?.schema === 1 && Array.isArray(file.notices))
|
|
26
|
+
notices = file.notices;
|
|
27
|
+
else
|
|
28
|
+
process.stderr.write("Conflict notice schema is unsupported; treating notices as empty.\n");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== "ENOENT") {
|
|
32
|
+
process.stderr.write(`Conflict notice file could not be read; treating notices as empty (${error instanceof Error ? error.name : "UnknownError"}).\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!notices.some((notice) => notice.id === id)) {
|
|
36
|
+
throw new Error("Unknown conflict notice.");
|
|
37
|
+
}
|
|
38
|
+
const ackPath = path.join(stateDir, "conflict-notice-acks.jsonl");
|
|
39
|
+
let existing = "";
|
|
40
|
+
try {
|
|
41
|
+
existing = fs.readFileSync(ackPath, "utf8");
|
|
42
|
+
let malformedLineCount = 0;
|
|
43
|
+
let alreadyAcknowledged = false;
|
|
44
|
+
for (const line of existing.split("\n")) {
|
|
45
|
+
if (!line)
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const record = JSON.parse(line);
|
|
49
|
+
if (!/^[a-f0-9]{64}$/.test(record.id) || typeof record.acknowledgedAt !== "string") {
|
|
50
|
+
throw new Error("Invalid acknowledgement record");
|
|
51
|
+
}
|
|
52
|
+
if (record.id === id)
|
|
53
|
+
alreadyAcknowledged = true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
malformedLineCount += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (malformedLineCount > 0) {
|
|
60
|
+
process.stderr.write(`Skipped ${malformedLineCount} malformed conflict acknowledgement line(s).\n`);
|
|
61
|
+
}
|
|
62
|
+
if (alreadyAcknowledged)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code !== "ENOENT")
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
70
|
+
const fd = fs.openSync(ackPath, "a", 0o600);
|
|
71
|
+
try {
|
|
72
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
73
|
+
writeAllSync(fd, `${separator}${JSON.stringify({ id, acknowledgedAt })}\n`);
|
|
74
|
+
fs.fsyncSync(fd);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
fs.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=conflict-notices.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming context-scan events (contract v1).
|
|
3
|
+
*
|
|
4
|
+
* `hq import scan --json --stream` relays these as JSON Lines, one object per
|
|
5
|
+
* line. They are produced by the HQ import-context scanner
|
|
6
|
+
* (`.claude/skills/import-context/scan.sh --progress-json` in an HQ tree) and
|
|
7
|
+
* validated here before they reach stdout. The HQ desktop first-run setup
|
|
8
|
+
* renders them as a knowledge tree that grows while the scan runs.
|
|
9
|
+
*
|
|
10
|
+
* The canonical contract lives next to the scanner
|
|
11
|
+
* (`.claude/skills/import-context/progress-json.md` in hq-core) and in
|
|
12
|
+
* `docs/import-scan-stream.md` in this repo. The JSON Schema is
|
|
13
|
+
* `scan-event.schema.json` in this directory (shipped next to the compiled
|
|
14
|
+
* module as `dist/lib/import-scan/scan-event.schema.json`).
|
|
15
|
+
*
|
|
16
|
+
* Desktop consumers can import these types from
|
|
17
|
+
* `@indigoai-us/hq-cli/dist/lib/import-scan/events.js` or mirror them.
|
|
18
|
+
*
|
|
19
|
+
* Versioning: within v1, fields, source ids, count keys and messages may be
|
|
20
|
+
* added. Consumers ignore unknown fields and must not parse message text.
|
|
21
|
+
* Removing or retyping a field, or adding a new `type`, `status` or `basis`
|
|
22
|
+
* value, bumps `v`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SCAN_EVENT_VERSION: 1;
|
|
25
|
+
/** Event types, in the order a stream introduces them. */
|
|
26
|
+
export declare const SCAN_EVENT_TYPES: readonly ["start", "source", "count", "company", "project", "error", "done"];
|
|
27
|
+
export type ScanEventType = (typeof SCAN_EVENT_TYPES)[number];
|
|
28
|
+
export declare const SCAN_SOURCE_STATUSES: readonly ["scanning", "done", "skipped", "error"];
|
|
29
|
+
export type ScanSourceStatus = (typeof SCAN_SOURCE_STATUSES)[number];
|
|
30
|
+
export declare const SCAN_COMPANY_BASES: readonly ["hq-company", "repo-org", "folder"];
|
|
31
|
+
export type ScanCompanyBasis = (typeof SCAN_COMPANY_BASES)[number];
|
|
32
|
+
export declare const SCAN_PROJECT_BASES: readonly ["repo", "claude-code-cwd", "codex-cwd"];
|
|
33
|
+
export type ScanProjectBasis = (typeof SCAN_PROJECT_BASES)[number];
|
|
34
|
+
/**
|
|
35
|
+
* Source ids the v1 scanner emits, in scan order. Other ids may appear in
|
|
36
|
+
* later v1 scanners; show them by their `label`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const KNOWN_SCAN_SOURCE_IDS: readonly ["hq", "repos", "claude-code", "codex", "grok", "claude-ai", "artifacts"];
|
|
39
|
+
export type KnownScanSourceId = (typeof KNOWN_SCAN_SOURCE_IDS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* Source id used on `error` events that hq-cli itself emits (HQ not found,
|
|
42
|
+
* scanner missing or too old, scanner crashed). Never listed in `start`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SCAN_CLI_ERROR_SOURCE: "scanner";
|
|
45
|
+
/**
|
|
46
|
+
* Stable machine-readable codes on `error` events. hq-cli sets one on every
|
|
47
|
+
* error it emits itself (source "scanner"); scanner-emitted errors may omit it.
|
|
48
|
+
*
|
|
49
|
+
* scanner_outdated the HQ tree's scanner is missing or predates
|
|
50
|
+
* --progress-json; updating HQ fixes it
|
|
51
|
+
* scan_failed any other reason the scan could not run or finish
|
|
52
|
+
* (HQ not found, scanner crashed, cancelled, non-zero exit)
|
|
53
|
+
*
|
|
54
|
+
* Consumers branch on `code`, never on `message`. Treat an error without a
|
|
55
|
+
* code as non-fatal unless the stream ends with `report: null`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SCAN_ERROR_CODES: readonly ["scanner_outdated", "scan_failed"];
|
|
58
|
+
export type ScanErrorCode = (typeof SCAN_ERROR_CODES)[number];
|
|
59
|
+
/** Project ids: "p_" + 12 lowercase hex (pid() in scan-progress.sh). */
|
|
60
|
+
export declare const SCAN_PROJECT_ID_PATTERN: RegExp;
|
|
61
|
+
export interface ScanSourceInfo {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}
|
|
65
|
+
interface ScanEventBase {
|
|
66
|
+
v: typeof SCAN_EVENT_VERSION;
|
|
67
|
+
}
|
|
68
|
+
/** First line: the sources present on this machine, in scan order. */
|
|
69
|
+
export interface ScanStartEvent extends ScanEventBase {
|
|
70
|
+
type: "start";
|
|
71
|
+
sources: ScanSourceInfo[];
|
|
72
|
+
}
|
|
73
|
+
/** Source lifecycle: `scanning` once, then one of done/skipped/error. */
|
|
74
|
+
export interface ScanSourceEvent extends ScanEventBase {
|
|
75
|
+
type: "source";
|
|
76
|
+
id: string;
|
|
77
|
+
status: ScanSourceStatus;
|
|
78
|
+
/** Final counts, on `done`. Each value repeats the last `count` event for that key. */
|
|
79
|
+
counts?: Record<string, number>;
|
|
80
|
+
/** Plain-words reason, on `skipped` and `error`. */
|
|
81
|
+
message?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Running count; fires at 1-2-5 milestones and once with the final value. */
|
|
84
|
+
export interface ScanCountEvent extends ScanEventBase {
|
|
85
|
+
type: "count";
|
|
86
|
+
source: string;
|
|
87
|
+
key: string;
|
|
88
|
+
value: number;
|
|
89
|
+
}
|
|
90
|
+
/** A company from the deterministic rule pass. Emitted once per id. */
|
|
91
|
+
export interface ScanCompanyEvent extends ScanEventBase {
|
|
92
|
+
type: "company";
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
basis: ScanCompanyBasis;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A project. Upsert by `id`: an id repeats only when a later rule pass
|
|
99
|
+
* attaches a company to a project that had `company: null`.
|
|
100
|
+
*/
|
|
101
|
+
export interface ScanProjectEvent extends ScanEventBase {
|
|
102
|
+
type: "project";
|
|
103
|
+
/** "p_" + 12 lowercase hex, the same for the same project on any machine. */
|
|
104
|
+
id: string;
|
|
105
|
+
/** Folder or repository basename; never contains "/". */
|
|
106
|
+
name: string;
|
|
107
|
+
company: string | null;
|
|
108
|
+
basis: ScanProjectBasis;
|
|
109
|
+
}
|
|
110
|
+
/** Non-fatal problem, in plain words. */
|
|
111
|
+
export interface ScanErrorEvent extends ScanEventBase {
|
|
112
|
+
type: "error";
|
|
113
|
+
source: string;
|
|
114
|
+
message: string;
|
|
115
|
+
/** Stable code (SCAN_ERROR_CODES). Always set on hq-cli's own errors. */
|
|
116
|
+
code?: ScanErrorCode;
|
|
117
|
+
}
|
|
118
|
+
export interface ScanSummary {
|
|
119
|
+
companies: number;
|
|
120
|
+
projects: number;
|
|
121
|
+
sessions: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Last line. `report` is the absolute path of report.json, or `null` when
|
|
125
|
+
* hq-cli could not run the scan (then `summary` is all zeros and the command
|
|
126
|
+
* exits non-zero).
|
|
127
|
+
*/
|
|
128
|
+
export interface ScanDoneEvent extends ScanEventBase {
|
|
129
|
+
type: "done";
|
|
130
|
+
report: string | null;
|
|
131
|
+
summary: ScanSummary;
|
|
132
|
+
}
|
|
133
|
+
export type ScanEvent = ScanStartEvent | ScanSourceEvent | ScanCountEvent | ScanCompanyEvent | ScanProjectEvent | ScanErrorEvent | ScanDoneEvent;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=events.d.ts.map
|