@indigoai-us/hq-cli 5.275.0 → 5.277.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 +112 -0
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-checkpoint.d.ts +4 -2
- package/dist/commands/core-checkpoint.js +34 -3
- package/dist/commands/core-command-catalog.d.ts +4 -2
- package/dist/commands/core-command-catalog.js +5 -2
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core-ontology.js +3 -3
- package/dist/commands/core.js +11 -1
- package/dist/commands/daemon.d.ts +4 -0
- package/dist/commands/daemon.js +175 -2
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +402 -49
- package/dist/commands/run-feature-telemetry.d.ts +3 -0
- package/dist/commands/run-feature-telemetry.js +6 -0
- package/dist/commands/run.js +3 -2
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +80 -45
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/commands/whoami.d.ts +4 -6
- package/dist/commands/whoami.js +39 -8
- package/dist/fast-core.d.ts +6 -7
- package/dist/fast-core.js +22 -8
- package/dist/fast-scaffold.d.ts +33 -7
- package/dist/fast-scaffold.js +104 -8
- package/dist/index.js +32 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +60 -3
- package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
- package/dist/lib/checkpoint-ontology/company.js +15 -7
- package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
- package/dist/lib/checkpoint-ontology/plan.js +66 -4
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
- package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
- package/dist/lib/core-utils/timeout-guard-native.js +40 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
- package/dist/lib/core-utils/timeout-guard.js +28 -13
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.d.ts +12 -7
- package/dist/lib/flag-registry.js +66 -75
- package/dist/lib/flag-snapshot-file.d.ts +20 -0
- package/dist/lib/flag-snapshot-file.js +91 -0
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.js +15 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.js +20 -4
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +30 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/lib/process-flag-company-context.d.ts +8 -0
- package/dist/lib/process-flag-company-context.js +67 -0
- package/dist/main.js +3 -1
- package/dist/native-fast-route.d.ts +3 -0
- package/dist/native-fast-route.js +39 -0
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- package/dist/utils/cli-telemetry.d.ts +26 -2
- package/dist/utils/cli-telemetry.js +178 -3
- package/dist/utils/hq-root-resolver.d.ts +13 -0
- package/dist/utils/hq-root-resolver.js +103 -0
- package/dist/utils/scaffold-fast-import-guard.js +6 -1
- package/dist/utils/vault-api.d.ts +8 -0
- package/dist/utils/vault-api.js +13 -1
- package/package.json +2 -2
package/dist/fast-scaffold.js
CHANGED
|
@@ -1,21 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fast-path manifest for
|
|
2
|
+
* Fast-path manifest for selected core commands, kept deliberately
|
|
3
3
|
* dependency-light.
|
|
4
4
|
*
|
|
5
5
|
* `main.ts` eagerly imports the CLI's entire (~60-module) command graph, so any
|
|
6
6
|
* command routed through it pays seconds of startup — fine for a human typing a
|
|
7
7
|
* command, ruinous for the hot plumbing this hosts: `hq core hq-session` is
|
|
8
8
|
* called several times per skill/hook, and `hq core checkpoint-stop-gate` runs
|
|
9
|
-
* on every turn's Stop. The entrypoint routes
|
|
10
|
-
*
|
|
11
|
-
* routing needs WITHOUT importing anything heavy. `core.ts` names the same
|
|
12
|
-
* assets, and a test pins the two lists together so they cannot drift.
|
|
9
|
+
* on every turn's Stop. The entrypoint routes eligible commands before loading
|
|
10
|
+
* the graph, and this module holds the parser + table that routing needs.
|
|
13
11
|
*/
|
|
14
12
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
15
13
|
export const FAST_SCAFFOLD_SCRIPTS = [
|
|
16
|
-
{ name: "checkpoint-stop-gate", asset: "core/scripts/checkpoint-stop-gate.sh" },
|
|
17
|
-
{ name: "hq-session", asset: "core/scripts/hq-session.sh" },
|
|
14
|
+
{ name: "checkpoint-stop-gate", kind: "shell", asset: "core/scripts/checkpoint-stop-gate.sh" },
|
|
15
|
+
{ name: "hq-session", kind: "shell", asset: "core/scripts/hq-session.sh" },
|
|
16
|
+
{
|
|
17
|
+
name: "timeout-guard",
|
|
18
|
+
kind: "native",
|
|
19
|
+
modulePath: "./lib/core-utils/timeout-guard-native.js",
|
|
20
|
+
exportName: "runTimeoutGuardFast",
|
|
21
|
+
flagKey: "hq-core-native-fast-route",
|
|
22
|
+
},
|
|
18
23
|
];
|
|
24
|
+
/**
|
|
25
|
+
* Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
|
|
26
|
+
* fast path. Anything this parser does not understand stays with Commander in
|
|
27
|
+
* the full CLI, which remains the source of truth for help and errors.
|
|
28
|
+
*/
|
|
29
|
+
export function parseSecretsEnvFastRequest(argv) {
|
|
30
|
+
if (argv[2] !== "secrets")
|
|
31
|
+
return null;
|
|
32
|
+
let index = 3;
|
|
33
|
+
const scope = {};
|
|
34
|
+
let hasCompanyScope = false;
|
|
35
|
+
let hasPersonalScope = false;
|
|
36
|
+
while (index < argv.length && argv[index] !== "env") {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
if (arg === "--company") {
|
|
39
|
+
const value = argv[index + 1];
|
|
40
|
+
if (!value || value.startsWith("-") || hasPersonalScope)
|
|
41
|
+
return null;
|
|
42
|
+
hasCompanyScope = true;
|
|
43
|
+
scope.company = value;
|
|
44
|
+
index += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg?.startsWith("--company=")) {
|
|
48
|
+
const value = arg.slice("--company=".length);
|
|
49
|
+
if (!value || hasPersonalScope)
|
|
50
|
+
return null;
|
|
51
|
+
hasCompanyScope = true;
|
|
52
|
+
scope.company = value;
|
|
53
|
+
index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === "--personal") {
|
|
57
|
+
if (hasCompanyScope)
|
|
58
|
+
return null;
|
|
59
|
+
hasPersonalScope = true;
|
|
60
|
+
scope.personal = true;
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (argv[index] !== "env")
|
|
67
|
+
return null;
|
|
68
|
+
index += 1;
|
|
69
|
+
const options = { only: [] };
|
|
70
|
+
let hasOnly = false;
|
|
71
|
+
while (index < argv.length) {
|
|
72
|
+
const arg = argv[index];
|
|
73
|
+
if (arg === "-h" || arg === "--help" || arg === "--")
|
|
74
|
+
return null;
|
|
75
|
+
let option;
|
|
76
|
+
let value;
|
|
77
|
+
if (arg === "--only" || arg === "--script" || arg === "--script-id") {
|
|
78
|
+
option = arg;
|
|
79
|
+
value = argv[index + 1];
|
|
80
|
+
if (value === undefined || value.startsWith("-"))
|
|
81
|
+
return null;
|
|
82
|
+
index += 2;
|
|
83
|
+
}
|
|
84
|
+
else if (arg?.startsWith("--only=")) {
|
|
85
|
+
option = "--only";
|
|
86
|
+
value = arg.slice("--only=".length);
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
else if (arg?.startsWith("--script=")) {
|
|
90
|
+
option = "--script";
|
|
91
|
+
value = arg.slice("--script=".length);
|
|
92
|
+
index += 1;
|
|
93
|
+
}
|
|
94
|
+
else if (arg?.startsWith("--script-id=")) {
|
|
95
|
+
option = "--script-id";
|
|
96
|
+
value = arg.slice("--script-id=".length);
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (option === "--only") {
|
|
103
|
+
hasOnly = true;
|
|
104
|
+
options.only.push(...(value ?? "").split(",").map((key) => key.trim()).filter(Boolean));
|
|
105
|
+
}
|
|
106
|
+
else if (option === "--script") {
|
|
107
|
+
options.script = value;
|
|
108
|
+
}
|
|
109
|
+
else if (option === "--script-id") {
|
|
110
|
+
options.scriptId = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return hasOnly ? { options, scope } : null;
|
|
114
|
+
}
|
|
19
115
|
/**
|
|
20
116
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
21
117
|
* fast-path script. Returns null for anything else so the caller defers to the
|
|
@@ -48,7 +144,7 @@ export function parseFastCore(argv) {
|
|
|
48
144
|
const entry = FAST_SCAFFOLD_SCRIPTS.find((candidate) => candidate.name === name);
|
|
49
145
|
if (!entry)
|
|
50
146
|
return null;
|
|
51
|
-
return {
|
|
147
|
+
return { entry, hqRootArg, args: argv.slice(i + 1) };
|
|
52
148
|
}
|
|
53
149
|
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
54
150
|
export function isFastCoreRequest(argv) {
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import "./node-network-compat.js";
|
|
|
6
6
|
import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
|
|
7
7
|
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
8
|
// every path (including --version) without reintroducing the heavy startup.
|
|
9
|
-
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
9
|
+
import { isFastCoreRequest, parseFastCore, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
|
|
10
10
|
// Also dependency-light: registers a process-level rejection boundary; its only
|
|
11
11
|
// heavy import (main.js) is lazy and reached only when a rejection fires.
|
|
12
12
|
import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
|
|
@@ -30,6 +30,37 @@ if (isVersionRequest(process.argv)) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
else if (parseFastCore(process.argv)?.entry.kind === "native") {
|
|
34
|
+
// Native routes are default-off. Resolve the company-scoped persisted flag
|
|
35
|
+
// without loading the command graph; a miss or stale snapshot takes the
|
|
36
|
+
// existing full-graph route.
|
|
37
|
+
void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
|
|
38
|
+
if (await nativeFastRouteEnabled(process.argv)) {
|
|
39
|
+
const { runFastCore } = await import("./fast-core.js");
|
|
40
|
+
await runFastCore(process.argv);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const { runCli } = await import("./main.js");
|
|
44
|
+
await runCli();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (parseSecretsEnvFastRequest(process.argv)) {
|
|
49
|
+
const request = parseSecretsEnvFastRequest(process.argv);
|
|
50
|
+
void import("./secrets-env-fast-route.js").then(async (route) => {
|
|
51
|
+
await route.dispatchSecretsEnvFastRequest(request, {
|
|
52
|
+
isEnabled: () => route.secretsEnvFastRouteEnabled(request),
|
|
53
|
+
runFast: async () => {
|
|
54
|
+
const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
|
|
55
|
+
return runSecretsEnvFastPath(request);
|
|
56
|
+
},
|
|
57
|
+
runCurrent: async () => {
|
|
58
|
+
const { runCli } = await import("./main.js");
|
|
59
|
+
await runCli();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
33
64
|
else if (isFastCoreRequest(process.argv)) {
|
|
34
65
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
35
66
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
package/dist/lanes-drain-hook.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
function parseHookInput(text) {
|
|
3
5
|
if (text.trim() === "")
|
|
4
6
|
return {};
|
|
@@ -24,6 +26,116 @@ function parseHookInput(text) {
|
|
|
24
26
|
...(typeof value.hook_event_name === "string" ? { hook_event_name: value.hook_event_name } : {}),
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
function laneIdIsSafe(laneId) {
|
|
30
|
+
return laneId !== "" && laneId.trim() === laneId &&
|
|
31
|
+
!laneId.includes("/") && !laneId.includes("\\") &&
|
|
32
|
+
!laneId.includes("\0") && laneId !== "." && laneId !== ".." &&
|
|
33
|
+
!laneId.includes("..");
|
|
34
|
+
}
|
|
35
|
+
function fileExists(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
fs.statSync(filePath);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code === "ENOENT")
|
|
42
|
+
return false;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function listVisibleNames(dir) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readdirSync(dir).filter((name) => !name.startsWith("."));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code === "ENOENT")
|
|
52
|
+
return [];
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Return true whenever the full drainer may have work or a condition to report. */
|
|
57
|
+
function dropboxMayNeedDrain(box) {
|
|
58
|
+
const inbox = path.join(box, "inbox.jsonl");
|
|
59
|
+
if (fileExists(inbox))
|
|
60
|
+
return true;
|
|
61
|
+
const draining = path.join(box, "draining");
|
|
62
|
+
if (listVisibleNames(draining).length > 0)
|
|
63
|
+
return true;
|
|
64
|
+
const inflightRoot = path.join(box, "inflight");
|
|
65
|
+
for (const name of listVisibleNames(inflightRoot)) {
|
|
66
|
+
const inflight = path.join(inflightRoot, name);
|
|
67
|
+
const stat = fs.statSync(inflight);
|
|
68
|
+
if (!stat.isDirectory())
|
|
69
|
+
return true;
|
|
70
|
+
if (listVisibleNames(inflight).length > 0)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function replayMayNeedWork(box, turnId, input) {
|
|
76
|
+
if (!/^[A-Za-z0-9-]{1,80}$/.test(turnId))
|
|
77
|
+
return false;
|
|
78
|
+
if (input.transcript_path)
|
|
79
|
+
return true;
|
|
80
|
+
let text;
|
|
81
|
+
const statePath = path.join(box, "message-compaction-replay.json");
|
|
82
|
+
try {
|
|
83
|
+
text = fs.readFileSync(statePath, "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Keep the existing named replay error behavior for missing or unreadable state.
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(text);
|
|
91
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
92
|
+
return true;
|
|
93
|
+
const state = parsed;
|
|
94
|
+
if (state.schema_version !== 1 ||
|
|
95
|
+
state.turn_id !== turnId ||
|
|
96
|
+
(state.last_compaction_marker !== null &&
|
|
97
|
+
(!Number.isSafeInteger(state.last_compaction_marker) || Number(state.last_compaction_marker) < 0)) ||
|
|
98
|
+
(state.transcript_offset !== undefined &&
|
|
99
|
+
(!Number.isSafeInteger(state.transcript_offset) || Number(state.transcript_offset) < 0)) ||
|
|
100
|
+
(state.transcript_path !== undefined && typeof state.transcript_path !== "string") ||
|
|
101
|
+
state.flag_enabled !== undefined) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function recordToolActivity(hqRoot, laneId) {
|
|
111
|
+
try {
|
|
112
|
+
const activityPath = path.join(hqRoot, "workspace", "lanes-runs", laneId, "logs", "tool-activity.json");
|
|
113
|
+
fs.mkdirSync(path.dirname(activityPath), { recursive: true });
|
|
114
|
+
const record = {
|
|
115
|
+
at: new Date().toISOString(),
|
|
116
|
+
hook_pid: process.pid,
|
|
117
|
+
hook_event: "PreToolUse",
|
|
118
|
+
};
|
|
119
|
+
const tempPath = `${activityPath}.${process.pid}.${process.hrtime.bigint()}.${randomBytes(4).toString("hex")}.tmp`;
|
|
120
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
121
|
+
fs.renameSync(tempPath, activityPath);
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function finish(result, inputError) {
|
|
129
|
+
if (result.stdout)
|
|
130
|
+
process.stdout.write(result.stdout);
|
|
131
|
+
if (inputError || result.stderr)
|
|
132
|
+
process.stderr.write(`${inputError}${result.stderr}`);
|
|
133
|
+
process.exitCode = result.exitCode;
|
|
134
|
+
}
|
|
135
|
+
async function runFullPath(input, inputError) {
|
|
136
|
+
const { runDrainHook } = await import("./lib/lanes/dropbox.js");
|
|
137
|
+
finish(await runDrainHook(process.env, input), inputError);
|
|
138
|
+
}
|
|
27
139
|
async function main() {
|
|
28
140
|
const chunks = [];
|
|
29
141
|
for await (const chunk of process.stdin)
|
|
@@ -37,12 +149,47 @@ async function main() {
|
|
|
37
149
|
catch (err) {
|
|
38
150
|
inputError = `hq-lanes-drain: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
39
151
|
}
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
process.
|
|
152
|
+
const laneId = process.env.HQ_LANE_ID ?? "";
|
|
153
|
+
if (laneId === "") {
|
|
154
|
+
finish({ exitCode: 0, stdout: "", stderr: "" }, inputError);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const hqRoot = process.env.HQ_ROOT ?? "";
|
|
158
|
+
if (hqRoot === "" || !laneIdIsSafe(laneId)) {
|
|
159
|
+
await runFullPath(input, inputError);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const box = path.join(hqRoot, "workspace", "lanes-runs", laneId, "dropbox");
|
|
163
|
+
let noWork = false;
|
|
164
|
+
try {
|
|
165
|
+
noWork = !dropboxMayNeedDrain(box) &&
|
|
166
|
+
!replayMayNeedWork(box, process.env.HQ_LANE_TURN_ID ?? "", input);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Filesystem uncertainty must use the full path so it retains its named errors.
|
|
170
|
+
}
|
|
171
|
+
if (!noWork) {
|
|
172
|
+
await runFullPath(input, inputError);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let stderr = recordToolActivity(hqRoot, laneId);
|
|
176
|
+
const directories = [
|
|
177
|
+
box,
|
|
178
|
+
path.join(box, "draining"),
|
|
179
|
+
path.join(box, "delivered"),
|
|
180
|
+
path.join(box, "inflight"),
|
|
181
|
+
];
|
|
182
|
+
try {
|
|
183
|
+
for (const directory of directories)
|
|
184
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const message = `Failed to open drop box ${box}: ${err instanceof Error ? err.message : String(err)}`;
|
|
188
|
+
stderr += `hq-lanes-drain: ${message}\n`;
|
|
189
|
+
finish({ exitCode: 1, stdout: "", stderr }, inputError);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
finish({ exitCode: 0, stdout: "", stderr }, inputError);
|
|
46
193
|
}
|
|
47
194
|
main().catch((err) => {
|
|
48
195
|
process.stderr.write(`hq-lanes-drain: hook_runner_failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -138,8 +138,9 @@ export async function pollInboxOnce(deps) {
|
|
|
138
138
|
if (res.status !== 200) {
|
|
139
139
|
writeComponentStatus(deps.paths, "inbox", "error");
|
|
140
140
|
const url = res.url ?? agentInboxUrl(deps.apiBaseUrl, deps.agentUid);
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// Non-OK bodies are not constrained to an error envelope and could carry
|
|
142
|
+
// inbox items. Keep the status and route, but do not copy the body to logs.
|
|
143
|
+
deps.log("warn", `inbox poll GET ${url} → ${res.status}`);
|
|
143
144
|
return { ok: false, mirrored: 0 };
|
|
144
145
|
}
|
|
145
146
|
writeComponentStatus(deps.paths, "inbox", "ok");
|
|
@@ -125,7 +125,9 @@ export declare const HARD_STOP_SENTENCE: string;
|
|
|
125
125
|
* dimension this CLI does not know) renders a generic line rather than a claim
|
|
126
126
|
* about a dimension nobody measured.
|
|
127
127
|
*/
|
|
128
|
-
export declare function renderPlanLimitNotice(status: PlanLimitStatus
|
|
128
|
+
export declare function renderPlanLimitNotice(status: PlanLimitStatus, opts?: {
|
|
129
|
+
inviteTeammateEnabled?: boolean;
|
|
130
|
+
}): string;
|
|
129
131
|
/**
|
|
130
132
|
* The one-line form injected on every turn while the workspace stays over.
|
|
131
133
|
* Kept to a single line on purpose — it repeats each turn.
|
|
@@ -375,7 +375,7 @@ function capitalize(text) {
|
|
|
375
375
|
* dimension this CLI does not know) renders a generic line rather than a claim
|
|
376
376
|
* about a dimension nobody measured.
|
|
377
377
|
*/
|
|
378
|
-
export function renderPlanLimitNotice(status) {
|
|
378
|
+
export function renderPlanLimitNotice(status, opts = {}) {
|
|
379
379
|
const { companySlug, freeMonth } = status;
|
|
380
380
|
const lines = [];
|
|
381
381
|
lines.push(`HQ Starter — ${companySlug} is over its plan limits.`);
|
|
@@ -405,6 +405,12 @@ export function renderPlanLimitNotice(status) {
|
|
|
405
405
|
lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
|
|
406
406
|
}
|
|
407
407
|
lines.push(` Upgrade: ${status.upgradeUrl}`);
|
|
408
|
+
if (opts.inviteTeammateEnabled === true &&
|
|
409
|
+
status.plan === "free" &&
|
|
410
|
+
status.over &&
|
|
411
|
+
status.members?.used === 1) {
|
|
412
|
+
lines.push(` Invite a teammate with: hq members invite <email> --company ${companySlug} --role member`);
|
|
413
|
+
}
|
|
408
414
|
return lines.join("\n");
|
|
409
415
|
}
|
|
410
416
|
/**
|
|
@@ -53,6 +53,12 @@ export interface ApplyReport {
|
|
|
53
53
|
canonical_name: string;
|
|
54
54
|
path: string;
|
|
55
55
|
}>;
|
|
56
|
+
/** Present only when `checkpoint.ontology.dominant-sibling-link` was on: knowledge links moved from a weak sibling to the dominant one. */
|
|
57
|
+
dominant_sibling_redirects?: Array<{
|
|
58
|
+
input: string;
|
|
59
|
+
from: string;
|
|
60
|
+
to: string;
|
|
61
|
+
}>;
|
|
56
62
|
rejected: Array<{
|
|
57
63
|
input: string;
|
|
58
64
|
type: string;
|
|
@@ -16,7 +16,7 @@ import { checkSyncClient, readSyncClientVersions } from "./sync-client.js";
|
|
|
16
16
|
import { appendBackRefs, parseEntityFile, patchEntityFile, previewSignalContent, renderNewEntity, validateEntityFrontmatter, } from "./entity-file.js";
|
|
17
17
|
import { canonicalizeName, entityId, isRejectedName, isWritableEntityType, normalizeEntityName, squash, toSlug, } from "./identity.js";
|
|
18
18
|
import { CANDIDATES_FILE, CANDIDATES_SCHEMA, PLAN_FILE, PLAN_SCHEMA, REPORT_FILE, readLedger, writeLedger, } from "./plan.js";
|
|
19
|
-
import { buildEntityIndex, indexAlias, indexEntity, listSignalFiles, readSignal } from "./tree.js";
|
|
19
|
+
import { buildEntityIndex, countInboundLinks, indexAlias, indexEntity, listSignalFiles, readSignal } from "./tree.js";
|
|
20
20
|
export const CAPS = {
|
|
21
21
|
creates: 25,
|
|
22
22
|
updates: 100,
|
|
@@ -26,6 +26,10 @@ export const CAPS = {
|
|
|
26
26
|
nameLength: MAX_NAME_LENGTH,
|
|
27
27
|
};
|
|
28
28
|
export const MIN_CREATE_CONFIDENCE = 0.7;
|
|
29
|
+
/** Inbound links the dominant sibling must have for each one the named entity has. */
|
|
30
|
+
const DOMINANT_SIBLING_RATIO = 3;
|
|
31
|
+
/** Inbound links the dominant sibling must have before a redirect. */
|
|
32
|
+
const DOMINANT_SIBLING_MIN_INBOUND = 5;
|
|
29
33
|
export class ApplyError extends Error {
|
|
30
34
|
constructor(message) {
|
|
31
35
|
super(message);
|
|
@@ -211,6 +215,7 @@ export function applyOntologyRun(options) {
|
|
|
211
215
|
matched: 0,
|
|
212
216
|
session_links: [],
|
|
213
217
|
links: [],
|
|
218
|
+
...(plan.dominant_sibling_link === true ? { dominant_sibling_redirects: [] } : {}),
|
|
214
219
|
rejected: [],
|
|
215
220
|
deferred: [],
|
|
216
221
|
suspect_duplicates: [],
|
|
@@ -300,6 +305,44 @@ export function applyOntologyRun(options) {
|
|
|
300
305
|
if (types.size > 1)
|
|
301
306
|
crossTypeNames.add(name);
|
|
302
307
|
}
|
|
308
|
+
// checkpoint.ontology.dominant-sibling-link: cross-type names, and how many other entity files link to each.
|
|
309
|
+
const inbound = plan.dominant_sibling_link === true ? countInboundLinks(company.dir, index) : new Map();
|
|
310
|
+
const siblings = new Map();
|
|
311
|
+
if (plan.dominant_sibling_link === true) {
|
|
312
|
+
const grouped = new Map();
|
|
313
|
+
for (const entity of index.byRel.values()) {
|
|
314
|
+
const key = crossTypeKey(entity.canonical_name);
|
|
315
|
+
if (key === "")
|
|
316
|
+
continue;
|
|
317
|
+
const members = grouped.get(key) ?? [];
|
|
318
|
+
members.push({ rel: entity.rel, type: entity.type });
|
|
319
|
+
grouped.set(key, members);
|
|
320
|
+
}
|
|
321
|
+
for (const [key, members] of grouped) {
|
|
322
|
+
if (new Set(members.map((member) => member.type)).size < 2)
|
|
323
|
+
continue;
|
|
324
|
+
siblings.set(key, members.map((member) => member.rel));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const dominantSibling = (rel) => {
|
|
328
|
+
const group = siblings.get(crossTypeKey(index.byRel.get(rel).canonical_name));
|
|
329
|
+
if (!group)
|
|
330
|
+
return null;
|
|
331
|
+
let best = group[0];
|
|
332
|
+
for (const member of group) {
|
|
333
|
+
const count = inbound.get(member) ?? 0;
|
|
334
|
+
const bestCount = inbound.get(best) ?? 0;
|
|
335
|
+
if (count > bestCount || (count === bestCount && member.localeCompare(best) < 0))
|
|
336
|
+
best = member;
|
|
337
|
+
}
|
|
338
|
+
const relCount = inbound.get(rel) ?? 0;
|
|
339
|
+
const bestCount = inbound.get(best) ?? 0;
|
|
340
|
+
if (relCount >= bestCount)
|
|
341
|
+
return null;
|
|
342
|
+
if (bestCount >= DOMINANT_SIBLING_RATIO * relCount && bestCount >= DOMINANT_SIBLING_MIN_INBOUND)
|
|
343
|
+
return best;
|
|
344
|
+
return null;
|
|
345
|
+
};
|
|
303
346
|
// checkpoint.ontology.named-evidence: the company's own company entity.
|
|
304
347
|
const ownCompanyRel = `ontology/entities/company/${company.slug}.md`;
|
|
305
348
|
const isOwnCompany = (entity, rel) => entity.type === "company" && (rel === ownCompanyRel || crossTypeKey(entity.canonical_name) === crossTypeKey(company.slug));
|
|
@@ -361,19 +404,33 @@ export function applyOntologyRun(options) {
|
|
|
361
404
|
note(report.rejected, "ambiguous_type");
|
|
362
405
|
continue;
|
|
363
406
|
}
|
|
407
|
+
const redirect = input.kind === "knowledge" && plan.dominant_sibling_link === true ? dominantSibling(resolved.target) : null;
|
|
408
|
+
const targetRel = redirect ?? resolved.target;
|
|
409
|
+
const targetEntity = redirect ? index.byRel.get(targetRel) : linked;
|
|
364
410
|
if (plan.named_evidence === true &&
|
|
365
411
|
input.kind === "knowledge" &&
|
|
366
|
-
isOwnCompany(
|
|
412
|
+
isOwnCompany(targetEntity, targetRel) &&
|
|
367
413
|
!evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
|
|
368
414
|
note(report.rejected, "unnamed_in_evidence");
|
|
369
415
|
continue;
|
|
370
416
|
}
|
|
371
417
|
report.matched += 1;
|
|
372
|
-
report.links.push({
|
|
418
|
+
report.links.push({
|
|
419
|
+
input: candidate.input,
|
|
420
|
+
action: "link",
|
|
421
|
+
type: targetEntity.type,
|
|
422
|
+
canonical_name: targetEntity.canonical_name,
|
|
423
|
+
path: targetRel,
|
|
424
|
+
});
|
|
373
425
|
if (resolved.tier === 1 && resolved.duplicates.length > 0) {
|
|
374
426
|
report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: [resolved.target, ...resolved.duplicates] });
|
|
427
|
+
}
|
|
428
|
+
if (redirect) {
|
|
429
|
+
report.dominant_sibling_redirects?.push({ input: candidate.input, from: resolved.target, to: targetRel });
|
|
375
430
|
continue;
|
|
376
431
|
}
|
|
432
|
+
if (resolved.tier === 1 && resolved.duplicates.length > 0)
|
|
433
|
+
continue;
|
|
377
434
|
// checkpoint.ontology.no-link-aliases: the link is reported and the entity file is left as it is.
|
|
378
435
|
if (plan.link_aliases === false)
|
|
379
436
|
continue;
|
|
@@ -29,10 +29,14 @@ export declare class WriteBoundsError extends Error {
|
|
|
29
29
|
constructor(message: string);
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* Resolve `target` only when it stays inside one of `roots` with no symlink
|
|
33
|
+
* anywhere between `anchor` (the HQ root) and the file. A root or a directory
|
|
34
|
+
* under it that is a symlink fails closed, and so does a target that is itself
|
|
35
|
+
* a symlink. Missing directories are created only after that check.
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertSafeTarget(anchor: string, roots: string[], target: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Write `content` to `target` only when {@link assertSafeTarget} accepts it.
|
|
36
40
|
* The file is written to a temporary sibling and renamed, so a reader never
|
|
37
41
|
* sees half a file.
|
|
38
42
|
*/
|
|
@@ -71,14 +71,12 @@ function nearestExisting(target) {
|
|
|
71
71
|
return current;
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* The file is written to a temporary sibling and renamed, so a reader never
|
|
79
|
-
* sees half a file.
|
|
74
|
+
* Resolve `target` only when it stays inside one of `roots` with no symlink
|
|
75
|
+
* anywhere between `anchor` (the HQ root) and the file. A root or a directory
|
|
76
|
+
* under it that is a symlink fails closed, and so does a target that is itself
|
|
77
|
+
* a symlink. Missing directories are created only after that check.
|
|
80
78
|
*/
|
|
81
|
-
export function
|
|
79
|
+
export function assertSafeTarget(anchor, roots, target) {
|
|
82
80
|
const resolved = path.resolve(target);
|
|
83
81
|
const root = roots.map((candidate) => path.resolve(candidate)).find((candidate) => isInside(candidate, resolved));
|
|
84
82
|
if (!root)
|
|
@@ -103,6 +101,16 @@ export function safeWriteFile(anchor, roots, target, content) {
|
|
|
103
101
|
if (error.code !== "ENOENT")
|
|
104
102
|
throw error;
|
|
105
103
|
}
|
|
104
|
+
return path.resolve(target);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Write `content` to `target` only when {@link assertSafeTarget} accepts it.
|
|
108
|
+
* The file is written to a temporary sibling and renamed, so a reader never
|
|
109
|
+
* sees half a file.
|
|
110
|
+
*/
|
|
111
|
+
export function safeWriteFile(anchor, roots, target, content) {
|
|
112
|
+
const resolved = assertSafeTarget(anchor, roots, target);
|
|
113
|
+
const parent = path.dirname(resolved);
|
|
106
114
|
const tmp = path.join(parent, `.${path.basename(resolved)}.${process.pid}.tmp`);
|
|
107
115
|
fs.writeFileSync(tmp, content);
|
|
108
116
|
fs.renameSync(tmp, resolved);
|
|
@@ -70,6 +70,17 @@ export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-pe
|
|
|
70
70
|
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
71
71
|
*/
|
|
72
72
|
export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
73
|
+
/**
|
|
74
|
+
* A knowledge link to the weaker of two entities that share a canonical name
|
|
75
|
+
* across types is redirected to the sibling other entity files link to
|
|
76
|
+
* (`project/build-your-own-agi` beside `concept/build-your-own-agi`, or
|
|
77
|
+
* `project/hq-core 2` beside `project/hq-core` when a concept also holds the
|
|
78
|
+
* name). The plan carries `dominant_sibling_link: true`, and apply reports
|
|
79
|
+
* the move in `dominant_sibling_redirects` without writing the entity files.
|
|
80
|
+
* CO-60: a replay of six runs raised knowledge precision from 0.725 to 0.754
|
|
81
|
+
* and recall from 0.784 to 0.816, and never changed a create.
|
|
82
|
+
*/
|
|
83
|
+
export declare const DOMINANT_SIBLING_LINK_FLAG = "checkpoint.ontology.dominant-sibling-link";
|
|
73
84
|
/**
|
|
74
85
|
* A knowledge concept candidate whose name is an alias of more than one
|
|
75
86
|
* entity, and which apply would defer as `ambiguous_alias`, links instead to
|
|
@@ -94,6 +105,15 @@ export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-pr
|
|
|
94
105
|
* and lost no gold link.
|
|
95
106
|
*/
|
|
96
107
|
export declare const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
|
|
108
|
+
/**
|
|
109
|
+
* CO-59: consume the files touched by company checkpoints that were queued
|
|
110
|
+
* behind a busy sibling (see queued-touches.ts). On, the plan adds the
|
|
111
|
+
* company's queued touches to this run's session-touched set, and a run whose
|
|
112
|
+
* own payload names no company plans the company the parent picked for it
|
|
113
|
+
* (`ontology_company`), without that run's session text. Off, queued touches
|
|
114
|
+
* wait on disk and a company-less run skips the ontology step as before.
|
|
115
|
+
*/
|
|
116
|
+
export declare const QUEUED_TOUCHES_FLAG = "checkpoint.ontology.queued-touches";
|
|
97
117
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
98
118
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
99
119
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -164,6 +184,8 @@ export interface OntologyPlan {
|
|
|
164
184
|
first_name_people?: true;
|
|
165
185
|
/** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
|
|
166
186
|
skip_ambiguous_concept_links?: true;
|
|
187
|
+
/** Set by checkpoint.ontology.dominant-sibling-link: apply redirects a knowledge link from a weak cross-type sibling to the one other files link to. */
|
|
188
|
+
dominant_sibling_link?: true;
|
|
167
189
|
/** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
|
|
168
190
|
concept_project_link?: true;
|
|
169
191
|
/** Set by checkpoint.ontology.named-evidence: apply rejects a knowledge link to the company's own entity, or a concept-project-link redirect, whose evidence does not name it as a word. */
|
|
@@ -183,6 +205,11 @@ export interface OntologyPlan {
|
|
|
183
205
|
}
|
|
184
206
|
export interface CheckpointPayloadLike {
|
|
185
207
|
company?: unknown;
|
|
208
|
+
/**
|
|
209
|
+
* Set by `hq core checkpoint` only when the payload names no company and
|
|
210
|
+
* another company has queued touches (CO-59). Never scopes anything else.
|
|
211
|
+
*/
|
|
212
|
+
ontology_company?: unknown;
|
|
186
213
|
/** The sibling backend `hq core checkpoint` spawned for this payload. */
|
|
187
214
|
backend?: unknown;
|
|
188
215
|
files?: unknown;
|
|
@@ -234,6 +261,15 @@ export declare function refreshLedger(company: OntologyCompany, existing: Ledger
|
|
|
234
261
|
ledger: Ledger;
|
|
235
262
|
firstRun: boolean;
|
|
236
263
|
};
|
|
264
|
+
/**
|
|
265
|
+
* The company this run's ontology step works on: the payload's own company,
|
|
266
|
+
* or else the `ontology_company` the checkpoint parent picked for queued
|
|
267
|
+
* touches (`borrowed`).
|
|
268
|
+
*/
|
|
269
|
+
export declare function payloadOntologyCompany(payload: CheckpointPayloadLike): {
|
|
270
|
+
slug: string | null;
|
|
271
|
+
borrowed: boolean;
|
|
272
|
+
};
|
|
237
273
|
/** Plan this run. Never throws for a skip; returns the reason instead. */
|
|
238
274
|
export declare function planOntologyRun(options: PlanOptions): Promise<PlanOutcome>;
|
|
239
275
|
/**
|