@indigoai-us/hq-cli 5.345.53 → 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 +24 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +1 -1
- package/assets/bot-workers/setup/skills/agents-and-team.md +1 -1
- package/assets/bot-workers/setup/skills/pricing.md +1 -1
- 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/billing/plan-lock.d.ts +2 -2
- package/dist/lib/billing/plan-lock.js +2 -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/dist/utils/manifest.d.ts +7 -1
- package/dist/utils/manifest.js +14 -6
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -30,6 +30,7 @@ import { daemonPaths } from "../lib/daemon/paths.js";
|
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
32
|
import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
33
|
+
import { acknowledgeConflictNotice, conflictNoticeStateDir } from "../lib/daemon/conflict-notices.js";
|
|
33
34
|
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
34
35
|
import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
|
|
35
36
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
@@ -959,6 +960,21 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
959
960
|
const sync = daemon
|
|
960
961
|
.command("sync")
|
|
961
962
|
.description("Inspect and control the daemon's company sync runner");
|
|
963
|
+
sync
|
|
964
|
+
.command("conflicts")
|
|
965
|
+
.description("Manage parked conflict copy notices")
|
|
966
|
+
.command("acknowledge <noticeId>")
|
|
967
|
+
.description("Acknowledge a parked conflict copy notice")
|
|
968
|
+
.action(wrap(async (d, noticeId) => {
|
|
969
|
+
const host = currentHost(d);
|
|
970
|
+
const managedHome = host.runAsUser ? d.userHome(host.runAsUser) : d.home;
|
|
971
|
+
const stateDir = conflictNoticeStateDir(d.env, managedHome);
|
|
972
|
+
acknowledgeConflictNotice(noticeId, stateDir);
|
|
973
|
+
if (host.runAsUser && d.isRoot) {
|
|
974
|
+
d.run("chown", [`${host.runAsUser}:`, path.join(stateDir, "conflict-notice-acks.jsonl")]);
|
|
975
|
+
}
|
|
976
|
+
d.out("Conflict notice acknowledged.");
|
|
977
|
+
}));
|
|
962
978
|
sync
|
|
963
979
|
.command("status")
|
|
964
980
|
.description("Show sync ownership, heartbeat, last pass and log path")
|
|
@@ -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)));
|
|
@@ -56,9 +56,9 @@ export interface PlanLimitStatus {
|
|
|
56
56
|
export declare const STARTER_MEMBER_TARGET = 5;
|
|
57
57
|
/**
|
|
58
58
|
* Starter integration cap quoted when the server did not send a limit.
|
|
59
|
-
*
|
|
59
|
+
* Three: Starter includes three integrations. Matches the hq-pro server limit.
|
|
60
60
|
*/
|
|
61
|
-
export declare const STARTER_INTEGRATION_TARGET =
|
|
61
|
+
export declare const STARTER_INTEGRATION_TARGET = 3;
|
|
62
62
|
/** Starter secret cap quoted when the server did not send a secrets.limit. */
|
|
63
63
|
export declare const STARTER_SECRET_TARGET = 10;
|
|
64
64
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
@@ -44,9 +44,9 @@ export const PLAN_LIMIT_REASONS = [
|
|
|
44
44
|
export const STARTER_MEMBER_TARGET = 5;
|
|
45
45
|
/**
|
|
46
46
|
* Starter integration cap quoted when the server did not send a limit.
|
|
47
|
-
*
|
|
47
|
+
* Three: Starter includes three integrations. Matches the hq-pro server limit.
|
|
48
48
|
*/
|
|
49
|
-
export const STARTER_INTEGRATION_TARGET =
|
|
49
|
+
export const STARTER_INTEGRATION_TARGET = 3;
|
|
50
50
|
/** Starter secret cap quoted when the server did not send a secrets.limit. */
|
|
51
51
|
export const STARTER_SECRET_TARGET = 10;
|
|
52
52
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
@@ -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
|