@indigoai-us/hq-cli 5.274.1 → 5.276.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 +88 -4
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +74 -18
- package/dist/command-catalog.generated.d.ts +34 -1
- package/dist/command-catalog.generated.js +43 -1
- package/dist/commands/cloud.js +22 -9
- package/dist/commands/company.js +66 -3
- package/dist/commands/core-checkpoint-gate-setting.d.ts +69 -0
- package/dist/commands/core-checkpoint-gate-setting.js +163 -0
- 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-ontology.js +3 -3
- package/dist/commands/core.js +3 -1
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +9 -3
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes.js +33 -11
- 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.js +7 -1
- 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 +16 -7
- package/dist/fast-scaffold.js +13 -8
- package/dist/index.js +16 -1
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- 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 +23 -0
- package/dist/lib/checkpoint-ontology/plan.js +53 -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/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 +7 -0
- package/dist/lib/daemon/catalog.js +6 -0
- package/dist/lib/daemon/flag.d.ts +23 -0
- package/dist/lib/daemon/flag.js +57 -0
- package/dist/lib/daemon/install.js +6 -1
- package/dist/lib/daemon/remote-control.d.ts +6 -0
- package/dist/lib/daemon/remote-control.js +12 -0
- package/dist/lib/daemon/run.js +115 -2
- package/dist/lib/daemon/succession.d.ts +116 -0
- package/dist/lib/daemon/succession.js +262 -0
- package/dist/lib/daemon/supervisor.d.ts +34 -1
- package/dist/lib/daemon/supervisor.js +190 -23
- 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/host-capacity.js +7 -1
- package/dist/lib/lanes/models.d.ts +2 -0
- package/dist/lib/lanes/models.js +6 -1
- package/dist/lib/lanes/reconcile.js +20 -4
- 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/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 +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -27,6 +27,7 @@ import { registerLaneReportCommands } from "./lanes-report.js";
|
|
|
27
27
|
import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
|
|
28
28
|
import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
29
29
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
|
+
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
30
31
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
31
32
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
32
33
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
@@ -1158,7 +1159,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1158
1159
|
const actualLocalLanes = hostname
|
|
1159
1160
|
? localLanes.filter((lane) => lane.host.kind === "local" && lane.host.id === hostname)
|
|
1160
1161
|
: [];
|
|
1161
|
-
if (
|
|
1162
|
+
if (hostname) {
|
|
1162
1163
|
snapshots.push({
|
|
1163
1164
|
capacity: buildHostCapacitySnapshot({
|
|
1164
1165
|
host: actualLocalHost,
|
|
@@ -2402,6 +2403,11 @@ async function runCreate(ctx, opts) {
|
|
|
2402
2403
|
let placementHostOverride;
|
|
2403
2404
|
let autoPlacementChoice;
|
|
2404
2405
|
let autoPlacementSkippedHosts = [];
|
|
2406
|
+
let placementMachineIdentity;
|
|
2407
|
+
const resolvePlacementMachineIdentity = () => {
|
|
2408
|
+
placementMachineIdentity ??= ctx.resolveMachineIdentity();
|
|
2409
|
+
return placementMachineIdentity;
|
|
2410
|
+
};
|
|
2405
2411
|
let balanceHostLoad;
|
|
2406
2412
|
let balanceTargetHost;
|
|
2407
2413
|
let inheritedPlacement;
|
|
@@ -2485,23 +2491,39 @@ async function runCreate(ctx, opts) {
|
|
|
2485
2491
|
}
|
|
2486
2492
|
throw err;
|
|
2487
2493
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2494
|
+
const chosenHost = autoPlacementChoice.host;
|
|
2495
|
+
const localHostname = ctx.hostname().trim();
|
|
2496
|
+
if (!localHostname) {
|
|
2497
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host for automatic lane placement", {});
|
|
2498
|
+
}
|
|
2499
|
+
const localHost = { kind: "local", id: localHostname };
|
|
2500
|
+
let chosenHostIsCaller = chosenHost.id === currentHost.id ||
|
|
2501
|
+
sanitizeHostComponent(chosenHost.id) === sanitizeHostComponent(localHostname);
|
|
2502
|
+
if (!chosenHostIsCaller && chosenHost.kind === "outpost") {
|
|
2503
|
+
const identity = resolvePlacementMachineIdentity();
|
|
2504
|
+
if (identity.status === "ok") {
|
|
2505
|
+
const machineIds = [identity.id, identity.imds, identity.hostname]
|
|
2506
|
+
.filter((id) => typeof id === "string" && id.trim() !== "");
|
|
2507
|
+
chosenHostIsCaller = machineIds.some((id) => sanitizeHostComponent(chosenHost.id) === sanitizeHostComponent(id.trim()));
|
|
2508
|
+
}
|
|
2492
2509
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2510
|
+
if (chosenHostIsCaller) {
|
|
2511
|
+
// A capacity snapshot can identify this process as an Outpost (or an
|
|
2512
|
+
// alias of its machine). Auto placement must spawn locally so the
|
|
2513
|
+
// Outpost self-target guard is never reached.
|
|
2514
|
+
placementHostOverride = localHost;
|
|
2515
|
+
outpostId = undefined;
|
|
2495
2516
|
}
|
|
2496
2517
|
else {
|
|
2497
|
-
|
|
2518
|
+
placementHostOverride = chosenHost;
|
|
2519
|
+
outpostId = chosenHost.kind === "outpost" ? chosenHost.id : undefined;
|
|
2498
2520
|
}
|
|
2499
2521
|
}
|
|
2500
2522
|
}
|
|
2501
2523
|
let originHost;
|
|
2502
2524
|
let originHostSource;
|
|
2503
2525
|
if (outpostId) {
|
|
2504
|
-
const identity =
|
|
2526
|
+
const identity = resolvePlacementMachineIdentity();
|
|
2505
2527
|
if (identity.status !== "ok") {
|
|
2506
2528
|
throw new LanesError("origin_host_unresolvable", "Cannot record origin_host; this machine's identity is unresolvable", { identity });
|
|
2507
2529
|
}
|
|
@@ -6890,7 +6912,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6890
6912
|
.option("--placement <mode>", "local | auto | outpost:<id>")
|
|
6891
6913
|
.option("--default-placement <mode>", "Default placement for lanes launched by this lane: local | auto | outpost:<id>")
|
|
6892
6914
|
.action(wrap(async (opts) => {
|
|
6893
|
-
await runCreate(ctx, opts);
|
|
6915
|
+
await withCliFeatureUsed("lane_create", () => runCreate(ctx, opts));
|
|
6894
6916
|
}));
|
|
6895
6917
|
lanes
|
|
6896
6918
|
.command("approve-target", { hidden: true })
|
|
@@ -7194,7 +7216,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7194
7216
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
7195
7217
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
7196
7218
|
.action(wrap(async (laneId, opts) => {
|
|
7197
|
-
await resumeHandlers.runResume(ctx, laneId, opts);
|
|
7219
|
+
await withCliFeatureUsed("lane_resume", () => resumeHandlers.runResume(ctx, laneId, opts));
|
|
7198
7220
|
}));
|
|
7199
7221
|
lanes
|
|
7200
7222
|
.command("reparent")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { withCliFeatureUsed } from '../utils/cli-telemetry.js';
|
|
2
|
+
/** Attribute hq run's batch secret read to its resolved company UID. */
|
|
3
|
+
export function withRunSecretReadTelemetry(companyUid, action) {
|
|
4
|
+
return withCliFeatureUsed('secret_read', action, companyUid);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=run-feature-telemetry.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -11,6 +11,7 @@ import { discoverSchemas } from '../run/discover-schemas.js';
|
|
|
11
11
|
import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
|
|
12
12
|
import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
|
|
13
13
|
import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
|
|
14
|
+
import { withRunSecretReadTelemetry } from './run-feature-telemetry.js';
|
|
14
15
|
const SECRET_LOAD_TIMEOUT_MS = 30_000;
|
|
15
16
|
export async function buildRunUsage(scriptPath, scriptId) {
|
|
16
17
|
if (scriptId && !scriptPath) {
|
|
@@ -87,7 +88,7 @@ export function registerRunCommand(program) {
|
|
|
87
88
|
allowOfflineScopeFallback: true,
|
|
88
89
|
});
|
|
89
90
|
const usage = await buildRunUsage(opts.script, opts.scriptId);
|
|
90
|
-
const fetchBatch = async (companyUid, names, requestUsage) => {
|
|
91
|
+
const fetchBatch = async (companyUid, names, requestUsage) => withRunSecretReadTelemetry(companyUid, async () => {
|
|
91
92
|
const res = await vaultApiFetch({
|
|
92
93
|
token,
|
|
93
94
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
@@ -113,7 +114,7 @@ export function registerRunCommand(program) {
|
|
|
113
114
|
throw new Error(`Failed to batch-load secrets: ${message}`);
|
|
114
115
|
}
|
|
115
116
|
return res.json();
|
|
116
|
-
};
|
|
117
|
+
});
|
|
117
118
|
const pluginOpts = {
|
|
118
119
|
companyOverride: opts.company,
|
|
119
120
|
principalUid: cognitoCachePrincipalUid(token) ?? undefined,
|
package/dist/commands/secrets.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { writeSync } from "node:fs";
|
|
5
5
|
import * as nodePath from "node:path";
|
|
6
6
|
import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-session.js";
|
|
7
|
+
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
7
8
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
8
9
|
import { computeSha256 } from "../utils/integrity.js";
|
|
9
10
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
@@ -1498,9 +1499,14 @@ export function registerSecretsCommand(program) {
|
|
|
1498
1499
|
const companyUid = cred.kind === "api-key"
|
|
1499
1500
|
? "__api_key__"
|
|
1500
1501
|
: await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
|
|
1501
|
-
const
|
|
1502
|
+
const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
|
|
1502
1503
|
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1503
1504
|
: undefined, { allowOfflineFallback: true });
|
|
1505
|
+
// API-key auth has no signed-in person identity for the CLI telemetry
|
|
1506
|
+
// endpoint. Do not attribute that read to an unrelated cached session.
|
|
1507
|
+
const revealed = cred.kind === "cognito"
|
|
1508
|
+
? await withCliFeatureUsed("secret_read", readSecrets, companyUid)
|
|
1509
|
+
: await readSecrets();
|
|
1504
1510
|
const secretEnv = {};
|
|
1505
1511
|
for (const key of keys) {
|
|
1506
1512
|
const value = revealed.get(key);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { type PlanLimitStatus } from "../lib/billing/plan-lock.js";
|
|
6
|
+
export declare const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
|
|
6
7
|
export interface WhoamiTokenIdentity {
|
|
7
8
|
email?: string;
|
|
8
9
|
sub?: string;
|
|
@@ -20,11 +21,8 @@ export interface WhoamiDisplayIdentity {
|
|
|
20
21
|
* email with the token subject of the delegating machine.
|
|
21
22
|
*/
|
|
22
23
|
export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* orientation when it is Starter and healthy, and nothing at all otherwise.
|
|
27
|
-
*/
|
|
28
|
-
export declare function renderWhoamiPlanBlock(status: PlanLimitStatus): string | null;
|
|
24
|
+
export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
|
|
25
|
+
inviteTeammateEnabled?: boolean;
|
|
26
|
+
}): string | null;
|
|
29
27
|
export declare function registerWhoamiCommand(program: Command): void;
|
|
30
28
|
//# sourceMappingURL=whoami.d.ts.map
|
package/dist/commands/whoami.js
CHANGED
|
@@ -6,6 +6,9 @@ import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq
|
|
|
6
6
|
import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
|
|
8
8
|
import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
|
|
9
|
+
import { resolveCompanyFlagGate, } from "../lib/flag-registry.js";
|
|
10
|
+
import { queuePlanLimitInviteTeammateExposure } from "../utils/cli-telemetry.js";
|
|
11
|
+
export const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
|
|
9
12
|
/**
|
|
10
13
|
* Resolves the person represented by an ID token without pairing a delegated
|
|
11
14
|
* email with the token subject of the delegating machine.
|
|
@@ -62,7 +65,13 @@ async function readPlanLimits(company) {
|
|
|
62
65
|
// default `interactive: true`, a box with no cached session would launch a
|
|
63
66
|
// browser sign-in and block the caller instead of resolving to UNKNOWN.
|
|
64
67
|
const token = await ensureCognitoToken({ interactive: false });
|
|
65
|
-
|
|
68
|
+
const status = await fetchPlanLimitStatus(token, slug);
|
|
69
|
+
if (!status)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
status,
|
|
73
|
+
idToken: loadCachedTokens()?.idToken,
|
|
74
|
+
};
|
|
66
75
|
}
|
|
67
76
|
catch {
|
|
68
77
|
return null;
|
|
@@ -73,19 +82,41 @@ async function readPlanLimits(company) {
|
|
|
73
82
|
* workspace is over its Starter limits, the one-line `Plan: Starter — …`
|
|
74
83
|
* orientation when it is Starter and healthy, and nothing at all otherwise.
|
|
75
84
|
*/
|
|
76
|
-
|
|
85
|
+
function shouldOfferInviteTeammateSuggestion(status) {
|
|
86
|
+
if (!status.over ||
|
|
87
|
+
status.plan !== "free" ||
|
|
88
|
+
status.members?.used !== 1 ||
|
|
89
|
+
!status.companyUid) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
async function inviteTeammateSuggestionEnabled(status, idToken) {
|
|
95
|
+
if (!shouldOfferInviteTeammateSuggestion(status) || !status.companyUid) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return resolveCompanyFlagGate(status.companyUid, idToken ?? "", CLI_LIMIT_INVITE_TEAMMATE_FLAG, { fallback: false }, () => false);
|
|
99
|
+
}
|
|
100
|
+
export function renderWhoamiPlanBlock(status, opts = {}) {
|
|
77
101
|
if (status.over) {
|
|
78
|
-
|
|
102
|
+
const inviteTeammateEnabled = opts.inviteTeammateEnabled === true;
|
|
103
|
+
return colorizePlanLimitNotice(renderPlanLimitNotice(status, { inviteTeammateEnabled }));
|
|
79
104
|
}
|
|
80
105
|
return renderPlanLine(status);
|
|
81
106
|
}
|
|
82
107
|
async function printPlanBlock(company) {
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
108
|
+
const planLimits = await readPlanLimits(company);
|
|
109
|
+
if (!planLimits)
|
|
110
|
+
return;
|
|
111
|
+
const { status, idToken } = planLimits;
|
|
112
|
+
const inviteTeammateEnabled = await inviteTeammateSuggestionEnabled(status, idToken);
|
|
113
|
+
const block = renderWhoamiPlanBlock(status, { inviteTeammateEnabled });
|
|
114
|
+
if (!block)
|
|
85
115
|
return;
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
116
|
+
console.log(block);
|
|
117
|
+
if (inviteTeammateEnabled && status.companyUid) {
|
|
118
|
+
queuePlanLimitInviteTeammateExposure(status.companyUid);
|
|
119
|
+
}
|
|
89
120
|
}
|
|
90
121
|
export function registerWhoamiCommand(program) {
|
|
91
122
|
program
|
package/dist/fast-core.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fast-path dispatcher for
|
|
2
|
+
* Fast-path dispatcher for selected core commands.
|
|
3
3
|
*
|
|
4
4
|
* Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
|
|
5
|
-
* ever imported, so
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `core.ts` remains the complete path for any invocation this does not match.
|
|
5
|
+
* ever imported, so hot hook commands avoid the full commander graph. Shell
|
|
6
|
+
* entries use the bundled-script runner; native entries import their one handler
|
|
7
|
+
* module. The registration in `core.ts` remains the complete fallback path.
|
|
9
8
|
*/
|
|
10
9
|
/**
|
|
11
|
-
* Run the fast-path
|
|
10
|
+
* Run the fast-path core command named in `argv` and exit with its code.
|
|
12
11
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
13
12
|
* doing anything.
|
|
14
13
|
*/
|
|
15
|
-
export declare function runFastCore(argv: readonly string[]): void
|
|
14
|
+
export declare function runFastCore(argv: readonly string[]): Promise<void>;
|
|
16
15
|
//# sourceMappingURL=fast-core.d.ts.map
|
package/dist/fast-core.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fast-path dispatcher for
|
|
2
|
+
* Fast-path dispatcher for selected core commands.
|
|
3
3
|
*
|
|
4
4
|
* Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
|
|
5
|
-
* ever imported, so
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `core.ts` remains the complete path for any invocation this does not match.
|
|
5
|
+
* ever imported, so hot hook commands avoid the full commander graph. Shell
|
|
6
|
+
* entries use the bundled-script runner; native entries import their one handler
|
|
7
|
+
* module. The registration in `core.ts` remains the complete fallback path.
|
|
9
8
|
*/
|
|
10
9
|
import { resolveLiveRoot } from "./utils/hq-roots.js";
|
|
11
10
|
import { runBundledScript } from "./utils/run-bundled-script.js";
|
|
12
11
|
import { parseFastCore } from "./fast-scaffold.js";
|
|
13
12
|
/**
|
|
14
|
-
* Run the fast-path
|
|
13
|
+
* Run the fast-path core command named in `argv` and exit with its code.
|
|
15
14
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
16
15
|
* doing anything.
|
|
17
16
|
*/
|
|
18
|
-
export function runFastCore(argv) {
|
|
17
|
+
export async function runFastCore(argv) {
|
|
19
18
|
const parsed = parseFastCore(argv);
|
|
20
19
|
if (!parsed)
|
|
21
20
|
return;
|
|
@@ -31,12 +30,27 @@ export function runFastCore(argv) {
|
|
|
31
30
|
catch {
|
|
32
31
|
hqRoot = undefined;
|
|
33
32
|
}
|
|
33
|
+
if (parsed.entry.kind === "native") {
|
|
34
|
+
const handlerModule = await import(parsed.entry.modulePath);
|
|
35
|
+
const handler = handlerModule[parsed.entry.exportName];
|
|
36
|
+
if (typeof handler !== "function") {
|
|
37
|
+
throw new Error(`Native fast handler ${parsed.entry.exportName} was not exported`);
|
|
38
|
+
}
|
|
39
|
+
const chunks = [];
|
|
40
|
+
for await (const chunk of process.stdin)
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
const code = await handler(argv, Buffer.concat(chunks).toString("utf8"), process.cwd(), hqRoot);
|
|
43
|
+
// Let Node drain handler output before terminating. The timeout guard's
|
|
44
|
+
// stderr message must survive the native route's process boundary.
|
|
45
|
+
process.exitCode = code;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
34
48
|
// Keep the CHILD in the caller's cwd, unlike the generic "live" dispatch. The
|
|
35
49
|
// relocated scripts read their data tree from the injected root, not cwd, and
|
|
36
50
|
// hq-session's Work Mesh registration serializes $PWD — so changing directory
|
|
37
51
|
// to the HQ root would mislabel a bind made from inside a project/worktree.
|
|
38
52
|
const { code } = runBundledScript({
|
|
39
|
-
asset: parsed.asset,
|
|
53
|
+
asset: parsed.entry.asset,
|
|
40
54
|
args: parsed.args,
|
|
41
55
|
cwd: process.cwd(),
|
|
42
56
|
hqRoot,
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
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
|
export type FastScaffoldScript = {
|
|
15
13
|
/** Direct subcommand name — `hq core <name>`. */
|
|
16
14
|
name: string;
|
|
15
|
+
kind: "shell";
|
|
17
16
|
/** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
|
|
18
17
|
asset: string;
|
|
18
|
+
} | {
|
|
19
|
+
/** Direct subcommand name — `hq core <name>`. */
|
|
20
|
+
name: string;
|
|
21
|
+
kind: "native";
|
|
22
|
+
/** Relative ESM module specifier, resolved from fast-core.js. */
|
|
23
|
+
modulePath: string;
|
|
24
|
+
/** Named export which accepts the native fast-handler contract. */
|
|
25
|
+
exportName: string;
|
|
26
|
+
/** Default-off registry flag for this native route. */
|
|
27
|
+
flagKey: string;
|
|
19
28
|
};
|
|
20
29
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
21
30
|
export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
|
|
22
31
|
export type ParsedFastCore = {
|
|
23
|
-
/** The
|
|
24
|
-
|
|
32
|
+
/** The manifest entry to run. */
|
|
33
|
+
entry: FastScaffoldScript;
|
|
25
34
|
/** `--hq-root <path>` value, if the caller passed the group option. */
|
|
26
35
|
hqRootArg?: string;
|
|
27
36
|
/** Everything after the command name, passed to the script verbatim. */
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
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
|
];
|
|
19
24
|
/**
|
|
20
25
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
@@ -48,7 +53,7 @@ export function parseFastCore(argv) {
|
|
|
48
53
|
const entry = FAST_SCAFFOLD_SCRIPTS.find((candidate) => candidate.name === name);
|
|
49
54
|
if (!entry)
|
|
50
55
|
return null;
|
|
51
|
-
return {
|
|
56
|
+
return { entry, hqRootArg, args: argv.slice(i + 1) };
|
|
52
57
|
}
|
|
53
58
|
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
54
59
|
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 } 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,21 @@ 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
|
+
}
|
|
33
48
|
else if (isFastCoreRequest(process.argv)) {
|
|
34
49
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
35
50
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
@@ -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
|
/**
|
|
@@ -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);
|
|
@@ -94,6 +94,15 @@ export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-pr
|
|
|
94
94
|
* and lost no gold link.
|
|
95
95
|
*/
|
|
96
96
|
export declare const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
|
|
97
|
+
/**
|
|
98
|
+
* CO-59: consume the files touched by company checkpoints that were queued
|
|
99
|
+
* behind a busy sibling (see queued-touches.ts). On, the plan adds the
|
|
100
|
+
* company's queued touches to this run's session-touched set, and a run whose
|
|
101
|
+
* own payload names no company plans the company the parent picked for it
|
|
102
|
+
* (`ontology_company`), without that run's session text. Off, queued touches
|
|
103
|
+
* wait on disk and a company-less run skips the ontology step as before.
|
|
104
|
+
*/
|
|
105
|
+
export declare const QUEUED_TOUCHES_FLAG = "checkpoint.ontology.queued-touches";
|
|
97
106
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
98
107
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
99
108
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -183,6 +192,11 @@ export interface OntologyPlan {
|
|
|
183
192
|
}
|
|
184
193
|
export interface CheckpointPayloadLike {
|
|
185
194
|
company?: unknown;
|
|
195
|
+
/**
|
|
196
|
+
* Set by `hq core checkpoint` only when the payload names no company and
|
|
197
|
+
* another company has queued touches (CO-59). Never scopes anything else.
|
|
198
|
+
*/
|
|
199
|
+
ontology_company?: unknown;
|
|
186
200
|
/** The sibling backend `hq core checkpoint` spawned for this payload. */
|
|
187
201
|
backend?: unknown;
|
|
188
202
|
files?: unknown;
|
|
@@ -234,6 +248,15 @@ export declare function refreshLedger(company: OntologyCompany, existing: Ledger
|
|
|
234
248
|
ledger: Ledger;
|
|
235
249
|
firstRun: boolean;
|
|
236
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* The company this run's ontology step works on: the payload's own company,
|
|
253
|
+
* or else the `ontology_company` the checkpoint parent picked for queued
|
|
254
|
+
* touches (`borrowed`).
|
|
255
|
+
*/
|
|
256
|
+
export declare function payloadOntologyCompany(payload: CheckpointPayloadLike): {
|
|
257
|
+
slug: string | null;
|
|
258
|
+
borrowed: boolean;
|
|
259
|
+
};
|
|
237
260
|
/** Plan this run. Never throws for a skip; returns the reason instead. */
|
|
238
261
|
export declare function planOntologyRun(options: PlanOptions): Promise<PlanOutcome>;
|
|
239
262
|
/**
|