@indigoai-us/hq-cli 5.290.0 → 5.291.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 +44 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
- package/assets/scaffold/core/scripts/hq-session.sh +182 -21
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
- package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
- package/dist/command-catalog.generated.d.ts +59 -0
- package/dist/command-catalog.generated.js +73 -0
- package/dist/commands/cloud.d.ts +26 -14
- package/dist/commands/cloud.js +207 -56
- package/dist/commands/core-jobs-native.d.ts +8 -0
- package/dist/commands/core-jobs-native.js +624 -0
- package/dist/commands/core-native-jobs-flag.d.ts +4 -0
- package/dist/commands/core-native-jobs-flag.js +7 -0
- package/dist/commands/core-policy.js +24 -5
- package/dist/commands/core.js +7 -0
- package/dist/commands/integrations-core.js +7 -0
- package/dist/commands/lanes.js +111 -35
- package/dist/commands/members.js +5 -0
- package/dist/commands/mesh.js +46 -25
- package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
- package/dist/commands/personal-targeted-push-flag.js +7 -0
- package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
- package/dist/commands/personal-targeted-push-paths.js +67 -0
- package/dist/commands/secrets.js +9 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +28 -0
- package/dist/fast-core.js +1 -0
- package/dist/lib/billing/free-month.d.ts +18 -0
- package/dist/lib/billing/free-month.js +31 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
- package/dist/lib/billing/plan-limit-refusals.js +187 -0
- package/dist/lib/billing/plan-lock.d.ts +2 -6
- package/dist/lib/billing/plan-lock.js +5 -16
- package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
- package/dist/lib/core-utils/secret-patterns.js +3 -2
- 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/lanes/autopilot.d.ts +69 -0
- package/dist/lib/lanes/autopilot.js +418 -0
- package/dist/lib/lanes/mesh-events.js +2 -0
- package/dist/lib/lanes/store.js +11 -6
- package/dist/lib/mesh/api.js +4 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
- package/dist/lib/mesh/live/daemon/state.js +34 -2
- package/dist/main.js +23 -12
- package/dist/utils/plan-gate-error.d.ts +24 -0
- package/dist/utils/plan-gate-error.js +219 -9
- package/package.json +2 -2
package/dist/commands/core.js
CHANGED
|
@@ -35,6 +35,7 @@ import { registerCoreHandoffCommand } from "./core-handoff.js";
|
|
|
35
35
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
36
36
|
import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
37
37
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
38
|
+
import { registerCoreJobsNativeCommand } from "./core-jobs-native.js";
|
|
38
39
|
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
39
40
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
40
41
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
@@ -211,6 +212,8 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
211
212
|
* direct `hq core` command.
|
|
212
213
|
*/
|
|
213
214
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
215
|
+
{ asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
|
|
216
|
+
{ asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
|
|
214
217
|
// Shared shell libs sourced by the two relocated scripts (and hooks). Never
|
|
215
218
|
// dispatched on their own (so no `hq core` command), but they MUST ship in the
|
|
216
219
|
// tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
|
|
@@ -308,6 +311,9 @@ function runEntry(entry, scope, args) {
|
|
|
308
311
|
args,
|
|
309
312
|
cwd,
|
|
310
313
|
hqRoot,
|
|
314
|
+
env: entry.asset === "core/scripts/hq-session.sh"
|
|
315
|
+
? { HQ_HQ_SESSION_NO_CLI: "1" }
|
|
316
|
+
: undefined,
|
|
311
317
|
});
|
|
312
318
|
// Propagate verbatim. Several of these scripts use their exit code as their
|
|
313
319
|
// interface (drift detected, lint failed), so remapping it would break the
|
|
@@ -335,6 +341,7 @@ export function registerCoreCommands(program) {
|
|
|
335
341
|
registerCoreHandoffCommand(core);
|
|
336
342
|
registerCoreKnowledgePrefsCommand(core);
|
|
337
343
|
registerCorePolicyCommand(core);
|
|
344
|
+
registerCoreJobsNativeCommand(core);
|
|
338
345
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
339
346
|
// as a default-off rollback path for one release. Keep the gate helper small
|
|
340
347
|
// so sibling Wave 2 work can reuse it without editing this command table.
|
|
@@ -12,6 +12,7 @@ import { randomUUID } from "node:crypto";
|
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
14
14
|
import { AuthError } from "../utils/auth-error.js";
|
|
15
|
+
import { planGateErrorFromPayload } from "../utils/plan-gate-error.js";
|
|
15
16
|
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
16
17
|
import { redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
17
18
|
import { connectionHealthNotice } from "../lib/integrations/health.js";
|
|
@@ -195,6 +196,12 @@ export async function raiseForResponse(res, fallback) {
|
|
|
195
196
|
raiseIfUnauthorized(res);
|
|
196
197
|
raiseIfUpstreamUnavailable(res);
|
|
197
198
|
const body = (await res.json().catch(() => ({})));
|
|
199
|
+
// A plan-limit 402 is normally decoded by vaultApiFetch before it gets here.
|
|
200
|
+
// Decode it again so no integration route can print the wire token
|
|
201
|
+
// `plan_limit_reached` as its whole message; main.ts renders the typed gate.
|
|
202
|
+
const planGate = planGateErrorFromPayload(res.status, body);
|
|
203
|
+
if (planGate)
|
|
204
|
+
throw planGate;
|
|
198
205
|
// hq-pro mints this error text, so it is untrusted input to a message we
|
|
199
206
|
// print verbatim — scrub credentials out before it can reach a terminal or a
|
|
200
207
|
// Sentry crash report (same reasoning as the gateway path below).
|
package/dist/commands/lanes.js
CHANGED
|
@@ -32,6 +32,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
|
32
32
|
import { startOutpostLaneSnapshotRefresh } from "../lib/lanes/outpost-snapshot-refresh.js";
|
|
33
33
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
34
34
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
35
|
+
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
35
36
|
import { getOrFetchOutpostSnapshot, OUTPOST_SNAPSHOT_CACHE_TTL_MS, outpostLaneListSnapshotKey, OutpostSnapshotInvocationMemo, } from "../lib/lanes/outpost-snapshot-cache.js";
|
|
36
37
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
37
38
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
@@ -2369,6 +2370,8 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId, knownAbse
|
|
|
2369
2370
|
failures.push(err);
|
|
2370
2371
|
continue;
|
|
2371
2372
|
}
|
|
2373
|
+
if (!story.project_id?.trim() || !story.story_id?.trim())
|
|
2374
|
+
continue;
|
|
2372
2375
|
if (knownAbsentStories.has(storyBoardStoryKey(story.project_id, story.story_id)))
|
|
2373
2376
|
continue;
|
|
2374
2377
|
try {
|
|
@@ -3453,40 +3456,42 @@ async function runCreate(ctx, opts) {
|
|
|
3453
3456
|
: undefined,
|
|
3454
3457
|
});
|
|
3455
3458
|
}
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3459
|
+
if (!opts.quiet)
|
|
3460
|
+
printJson({
|
|
3461
|
+
ok: true,
|
|
3462
|
+
action: "create",
|
|
3463
|
+
lane_id: started.lane_id,
|
|
3464
|
+
state: started.state,
|
|
3465
|
+
queued_reason: started.queued_reason ?? null,
|
|
3466
|
+
priority: started.priority,
|
|
3467
|
+
provider: started.provider,
|
|
3468
|
+
account: started.account ?? null,
|
|
3469
|
+
model: started.model ?? null,
|
|
3470
|
+
effort: started.effort ?? null,
|
|
3471
|
+
effort_source: started.effort_source ?? null,
|
|
3472
|
+
host: started.host,
|
|
3473
|
+
...(autoPlacementSkippedHosts.length > 0
|
|
3474
|
+
? { skipped_hosts: autoPlacementSkippedHosts }
|
|
3475
|
+
: {}),
|
|
3476
|
+
company: started.company,
|
|
3477
|
+
project_id: started.project_id,
|
|
3478
|
+
story_id: started.story_id,
|
|
3479
|
+
stories: laneStories(started),
|
|
3480
|
+
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
3481
|
+
thread_id: started.thread_id ?? null,
|
|
3482
|
+
senior: started.senior,
|
|
3483
|
+
depth: started.depth,
|
|
3484
|
+
timeout_secs: timeoutSecs ?? null,
|
|
3485
|
+
...(validation ? { validation } : {}),
|
|
3486
|
+
...(started.outpost_command_id
|
|
3487
|
+
? {
|
|
3488
|
+
outpost_command_id: started
|
|
3489
|
+
.outpost_command_id,
|
|
3490
|
+
}
|
|
3491
|
+
: {}),
|
|
3492
|
+
lane: started,
|
|
3493
|
+
});
|
|
3494
|
+
return started;
|
|
3490
3495
|
}
|
|
3491
3496
|
async function dispatchCreatedOutpostLane(ctx, input) {
|
|
3492
3497
|
const { hqRoot, lane, outpostId, briefText, timeout, preserveQueuedOnFailure = false, } = input;
|
|
@@ -6464,7 +6469,7 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
6464
6469
|
...payload,
|
|
6465
6470
|
lane_status: lane.state,
|
|
6466
6471
|
});
|
|
6467
|
-
if (!skipStoryStatusPatch) {
|
|
6472
|
+
if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
|
|
6468
6473
|
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
6469
6474
|
}
|
|
6470
6475
|
}
|
|
@@ -7824,8 +7829,44 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7824
7829
|
summary.ok = false;
|
|
7825
7830
|
}
|
|
7826
7831
|
const terminalizedRemoteIds = new Set(remoteLost.map(({ id }) => id));
|
|
7832
|
+
let autopilot;
|
|
7833
|
+
try {
|
|
7834
|
+
const host = reconcileHostFromCtx(ctx);
|
|
7835
|
+
autopilot = await runLeadAutopilotPass({
|
|
7836
|
+
hqRoot,
|
|
7837
|
+
host: host.outpostId ?? "local",
|
|
7838
|
+
hostname: host.hostname,
|
|
7839
|
+
outpostId: host.outpostId,
|
|
7840
|
+
now: ctx.now(),
|
|
7841
|
+
dryRun: Boolean(opts.dryRun),
|
|
7842
|
+
readFlag: readLanesRegistryFlag,
|
|
7843
|
+
keepAliveResumedLeads: new Set(summary.resumed.filter((entry) => entry.reason === "keep_alive").map((entry) => entry.id)),
|
|
7844
|
+
startWorker: async ({ lead, story }) => {
|
|
7845
|
+
const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
|
|
7846
|
+
const started = await runCreate(workerCtx, {
|
|
7847
|
+
company: lead.company?.slug,
|
|
7848
|
+
project: lead.project_id,
|
|
7849
|
+
story: story.id,
|
|
7850
|
+
briefFile: story.briefPath,
|
|
7851
|
+
provider: "codex",
|
|
7852
|
+
model: "gpt-6-luna",
|
|
7853
|
+
effort: "high",
|
|
7854
|
+
senior: `lane:${lead.lane_id}`,
|
|
7855
|
+
quiet: true,
|
|
7856
|
+
});
|
|
7857
|
+
return { lane_id: started.lane_id };
|
|
7858
|
+
},
|
|
7859
|
+
});
|
|
7860
|
+
}
|
|
7861
|
+
catch (error) {
|
|
7862
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7863
|
+
summary.ok = false;
|
|
7864
|
+
summary.errors.push({ code: "lead_autopilot_failed", message });
|
|
7865
|
+
process.stderr.write(`hq lanes autopilot: ${message}\n`);
|
|
7866
|
+
}
|
|
7827
7867
|
printJson({
|
|
7828
7868
|
...summary,
|
|
7869
|
+
...(autopilot ? { autopilot } : {}),
|
|
7829
7870
|
...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
|
|
7830
7871
|
...(mirrorRefresh.unverifiedRemote.size > 0
|
|
7831
7872
|
? {
|
|
@@ -8590,6 +8631,41 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8590
8631
|
.action(wrap(async (opts) => {
|
|
8591
8632
|
await runPickup(ctx, opts);
|
|
8592
8633
|
}));
|
|
8634
|
+
lanes
|
|
8635
|
+
.command("autopilot")
|
|
8636
|
+
.description("Check registered team leads and wake them for ready work")
|
|
8637
|
+
.command("run")
|
|
8638
|
+
.description("Run one lead autopilot check pass")
|
|
8639
|
+
.option("--dry-run", "Report proposed wakes without writing them")
|
|
8640
|
+
.option("--json", "Emit JSON on stdout (default)")
|
|
8641
|
+
.action(wrap(async (opts) => {
|
|
8642
|
+
const host = reconcileHostFromCtx(ctx);
|
|
8643
|
+
const result = await runLeadAutopilotPass({
|
|
8644
|
+
hqRoot: ctx.hqRoot(),
|
|
8645
|
+
host: host.outpostId ?? "local",
|
|
8646
|
+
hostname: host.hostname,
|
|
8647
|
+
outpostId: host.outpostId,
|
|
8648
|
+
now: ctx.now(),
|
|
8649
|
+
dryRun: Boolean(opts.dryRun),
|
|
8650
|
+
readFlag: readLanesRegistryFlag,
|
|
8651
|
+
startWorker: async ({ lead, story }) => {
|
|
8652
|
+
const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
|
|
8653
|
+
const started = await runCreate(workerCtx, {
|
|
8654
|
+
company: lead.company?.slug,
|
|
8655
|
+
project: lead.project_id,
|
|
8656
|
+
story: story.id,
|
|
8657
|
+
briefFile: story.briefPath,
|
|
8658
|
+
provider: "codex",
|
|
8659
|
+
model: "gpt-6-luna",
|
|
8660
|
+
effort: "high",
|
|
8661
|
+
senior: `lane:${lead.lane_id}`,
|
|
8662
|
+
quiet: true,
|
|
8663
|
+
});
|
|
8664
|
+
return { lane_id: started.lane_id };
|
|
8665
|
+
},
|
|
8666
|
+
});
|
|
8667
|
+
printJson(result);
|
|
8668
|
+
}));
|
|
8593
8669
|
const reconcile = lanes
|
|
8594
8670
|
.command("reconcile", { hidden: true })
|
|
8595
8671
|
.description("Run one machine-owned reconcile pass and exit")
|
package/dist/commands/members.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
3
3
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
4
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
5
6
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
6
7
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
7
8
|
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
@@ -558,6 +559,10 @@ export function registerMembersCommand(program) {
|
|
|
558
559
|
}
|
|
559
560
|
}
|
|
560
561
|
catch (err) {
|
|
562
|
+
// A seat-limit refusal goes to main.ts's shared plan-gate renderer:
|
|
563
|
+
// reason, usage, fix options and upgrade link, then a non-zero exit.
|
|
564
|
+
if (isPlanGateError(err))
|
|
565
|
+
throw err;
|
|
561
566
|
// A 409 may refer to either an active membership or an existing
|
|
562
567
|
// pending invite. Probe both views so email targets get actionable
|
|
563
568
|
// guidance while preserving the terminal exit-0 behavior for bulk
|
package/dist/commands/mesh.js
CHANGED
|
@@ -11,7 +11,7 @@ import { readFileSync } from "node:fs";
|
|
|
11
11
|
import { findHqRoot } from "../utils/manifest.js";
|
|
12
12
|
import { CLI_VERSION } from "../cli-version.js";
|
|
13
13
|
import chalk from "chalk";
|
|
14
|
-
import {
|
|
14
|
+
import { ensureCognitoToken, isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
17
|
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
|
|
@@ -26,7 +26,9 @@ import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/i
|
|
|
26
26
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
27
27
|
import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
|
|
28
28
|
import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
|
|
29
|
-
const
|
|
29
|
+
const ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
|
|
30
|
+
// Leave 500 ms of headroom inside SessionStart's 2 s process-group limit.
|
|
31
|
+
const OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 1_500;
|
|
30
32
|
import { buildEmitEvidence, machineEmitDisposition } from "../lib/mesh/live/emit-evidence.js";
|
|
31
33
|
import { appendHeldLine } from "../lib/mesh/live/spool.js";
|
|
32
34
|
import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
|
|
@@ -860,29 +862,45 @@ async function runContextReconcile(opts) {
|
|
|
860
862
|
let offline = Boolean(opts.offline);
|
|
861
863
|
let token;
|
|
862
864
|
try {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
865
|
+
if (offline) {
|
|
866
|
+
if (isMachineIdentity()) {
|
|
867
|
+
token = await ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
const cached = loadCachedTokens();
|
|
871
|
+
if (cached && !isExpiring(cached, 120)) {
|
|
872
|
+
token = cached.accessToken;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
token = await requireToken();
|
|
878
|
+
}
|
|
879
|
+
if (token) {
|
|
880
|
+
const sessionToken = token;
|
|
881
|
+
const membershipValidationTimeoutMs = offline
|
|
882
|
+
? OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS
|
|
883
|
+
: ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS;
|
|
884
|
+
validateMembership = async (candidate) => {
|
|
885
|
+
const membership = await new Promise((resolve, reject) => {
|
|
886
|
+
const timer = setTimeout(() => reject(new Error("membership validation timed out")), membershipValidationTimeoutMs);
|
|
887
|
+
void resolveActiveMembershipCompany(sessionToken, candidate).then((value) => {
|
|
888
|
+
clearTimeout(timer);
|
|
889
|
+
resolve(value);
|
|
890
|
+
}, (error) => {
|
|
891
|
+
clearTimeout(timer);
|
|
892
|
+
reject(error);
|
|
893
|
+
});
|
|
876
894
|
});
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
895
|
+
if (!membership || typeof membership.companyUid !== "string" || !membership.companyUid) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return { uid: membership.companyUid };
|
|
899
|
+
};
|
|
900
|
+
if (!offline) {
|
|
901
|
+
deliver = createWorkSessionDeliverer({ token: sessionToken });
|
|
902
|
+
fetchCandidates = createCandidatesFetcher({ token: sessionToken });
|
|
880
903
|
}
|
|
881
|
-
return { uid: membership.companyUid };
|
|
882
|
-
};
|
|
883
|
-
if (!offline) {
|
|
884
|
-
deliver = createWorkSessionDeliverer({ token: sessionToken });
|
|
885
|
-
fetchCandidates = createCandidatesFetcher({ token: sessionToken });
|
|
886
904
|
}
|
|
887
905
|
}
|
|
888
906
|
catch {
|
|
@@ -900,7 +918,10 @@ async function runContextReconcile(opts) {
|
|
|
900
918
|
log: (message) => console.error(chalk.dim(message)),
|
|
901
919
|
});
|
|
902
920
|
if (activation.status === "needs_choice") {
|
|
903
|
-
const interactive = !
|
|
921
|
+
const interactive = !offline &&
|
|
922
|
+
!opts.machine &&
|
|
923
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
|
|
924
|
+
!isMachineIdentity();
|
|
904
925
|
if (interactive) {
|
|
905
926
|
const choices = activation.memberships
|
|
906
927
|
.map((membership, index) => `${index + 1}. ${membership.slug}`)
|
|
@@ -920,7 +941,7 @@ async function runContextReconcile(opts) {
|
|
|
920
941
|
rl.close();
|
|
921
942
|
}
|
|
922
943
|
}
|
|
923
|
-
else if (!opts.machine) {
|
|
944
|
+
else if (!offline && !opts.machine) {
|
|
924
945
|
console.error(chalk.yellow("Multiple active memberships. Run `hq mesh context default set <slug>` to choose this device's default company."));
|
|
925
946
|
}
|
|
926
947
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
2
|
+
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
3
|
+
export declare function personalTargetedPushPathsEnabled(): boolean;
|
|
4
|
+
//# sourceMappingURL=personal-targeted-push-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
+
export const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
3
|
+
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
4
|
+
export function personalTargetedPushPathsEnabled() {
|
|
5
|
+
return resolveProcessFlagGate(CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=personal-targeted-push-flag.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Resolve explicit --personal paths against the same roots as a full personal push. */
|
|
2
|
+
export declare function resolveExplicitPersonalPushPaths(hqRoot: string, requestedPaths: readonly string[], cwd?: string): string[];
|
|
3
|
+
//# sourceMappingURL=personal-targeted-push-paths.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { computePersonalVaultPaths, matchPersonalVaultExclusion, } from "@indigoai-us/hq-cloud";
|
|
4
|
+
function isWithin(parent, candidate) {
|
|
5
|
+
const relative = path.relative(parent, candidate);
|
|
6
|
+
return relative === "" ||
|
|
7
|
+
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
8
|
+
}
|
|
9
|
+
function rejectPath(input, reason) {
|
|
10
|
+
throw new Error(`--personal cannot push path "${input}": ${reason}`);
|
|
11
|
+
}
|
|
12
|
+
/** Resolve explicit --personal paths against the same roots as a full personal push. */
|
|
13
|
+
export function resolveExplicitPersonalPushPaths(hqRoot, requestedPaths, cwd = process.cwd()) {
|
|
14
|
+
let realRoot;
|
|
15
|
+
try {
|
|
16
|
+
realRoot = fs.realpathSync.native(hqRoot);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
20
|
+
throw new Error(`--personal cannot resolve HQ root "${hqRoot}" (${code})`);
|
|
21
|
+
}
|
|
22
|
+
const allowedRoots = computePersonalVaultPaths(realRoot)
|
|
23
|
+
.map((allowedPath) => {
|
|
24
|
+
try {
|
|
25
|
+
return fs.realpathSync.native(allowedPath);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
29
|
+
throw new Error(`--personal personal-vault scope changed at "${allowedPath}" (${code})`);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.filter((allowedPath) => isWithin(realRoot, allowedPath));
|
|
33
|
+
const resolved = [];
|
|
34
|
+
for (const input of requestedPaths) {
|
|
35
|
+
const normalizedInput = path.resolve(cwd, input);
|
|
36
|
+
let realPath;
|
|
37
|
+
try {
|
|
38
|
+
realPath = fs.realpathSync.native(normalizedInput);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
42
|
+
rejectPath(input, `path cannot be resolved (${code})`);
|
|
43
|
+
}
|
|
44
|
+
if (!isWithin(realRoot, realPath)) {
|
|
45
|
+
rejectPath(input, "resolved path escapes the HQ root");
|
|
46
|
+
}
|
|
47
|
+
let isDirectory;
|
|
48
|
+
try {
|
|
49
|
+
isDirectory = fs.statSync(realPath).isDirectory();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
53
|
+
rejectPath(input, `path cannot be inspected (${code})`);
|
|
54
|
+
}
|
|
55
|
+
const relativePath = path.relative(realRoot, realPath).split(path.sep).join("/");
|
|
56
|
+
const exclusion = matchPersonalVaultExclusion(relativePath, isDirectory);
|
|
57
|
+
if (exclusion) {
|
|
58
|
+
rejectPath(input, `excluded by personal-vault rule ${exclusion.id}`);
|
|
59
|
+
}
|
|
60
|
+
if (!allowedRoots.some((allowedRoot) => isWithin(allowedRoot, realPath))) {
|
|
61
|
+
rejectPath(input, "resolved path is outside the personal-vault paths");
|
|
62
|
+
}
|
|
63
|
+
resolved.push(realPath);
|
|
64
|
+
}
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=personal-targeted-push-paths.js.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -5,6 +5,7 @@ import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-s
|
|
|
5
5
|
import { isAuthError } from "../utils/auth-error.js";
|
|
6
6
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
7
7
|
import { computeSha256 } from "../utils/integrity.js";
|
|
8
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
9
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
9
10
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
10
11
|
import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
|
|
@@ -988,6 +989,11 @@ export function registerSecretsCommand(program) {
|
|
|
988
989
|
}
|
|
989
990
|
}
|
|
990
991
|
catch (err) {
|
|
992
|
+
// A plan-limit refusal goes to main.ts's shared renderer, which prints
|
|
993
|
+
// the reason, usage, fix options and upgrade link, offers the upgrade
|
|
994
|
+
// interactively, and exits non-zero.
|
|
995
|
+
if (isPlanGateError(err))
|
|
996
|
+
throw err;
|
|
991
997
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
992
998
|
process.exit(1);
|
|
993
999
|
}
|
|
@@ -1650,6 +1656,9 @@ export function registerSecretsCommand(program) {
|
|
|
1650
1656
|
console.log(chalk.dim(" One-time use — link is invalidated after first submission."));
|
|
1651
1657
|
}
|
|
1652
1658
|
catch (err) {
|
|
1659
|
+
// Same shared plan-limit renderer as `secrets set` (see main.ts).
|
|
1660
|
+
if (isPlanGateError(err))
|
|
1661
|
+
throw err;
|
|
1653
1662
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
1654
1663
|
process.exit(1);
|
|
1655
1664
|
}
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { Command } from "commander";
|
|
|
18
18
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
19
19
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
20
20
|
import { surfaceCompanySkill } from "../lib/company-skill-wrapper.js";
|
|
21
|
+
import { type PlanLimitRefusalSummary } from "../lib/billing/plan-limit-refusals.js";
|
|
21
22
|
export declare const SKILL_UID_PATTERN: RegExp;
|
|
22
23
|
export declare const SKILL_SLUG_PATTERN: RegExp;
|
|
23
24
|
interface SkillSyncInput {
|
|
@@ -30,6 +31,8 @@ interface SkillSyncResult {
|
|
|
30
31
|
filesUploaded: number;
|
|
31
32
|
filesSkipped: number;
|
|
32
33
|
aborted: boolean;
|
|
34
|
+
/** Set when a plan limit refused the upload; absent or null otherwise. */
|
|
35
|
+
planLimit?: PlanLimitRefusalSummary | null;
|
|
33
36
|
}
|
|
34
37
|
export declare function parseSkillUid(markdown: string): string | undefined;
|
|
35
38
|
export declare function readActiveCompanySlug(hqRoot: string): string | undefined;
|
package/dist/commands/skill.js
CHANGED
|
@@ -27,25 +27,47 @@ import { surfaceCompanySkill } from "../lib/company-skill-wrapper.js";
|
|
|
27
27
|
import { AuthError } from "../utils/auth-error.js";
|
|
28
28
|
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
29
29
|
import { stampVaultAccessDenied, vaultAccessDeniedDiagnostics, } from "../utils/vault-access-denied-error.js";
|
|
30
|
+
import { PlanGateError } from "../utils/plan-gate-error.js";
|
|
31
|
+
import { createPlanLimitRefusalTracker, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
30
32
|
export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
|
|
31
33
|
export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
32
34
|
const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
33
35
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
34
36
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
35
37
|
async function defaultSyncFile(input) {
|
|
38
|
+
// The runner reports a plan-limit refusal as an event, not a throw, and still
|
|
39
|
+
// returns normally. Track it so the command cannot claim "Sync: complete".
|
|
40
|
+
const tracker = createPlanLimitRefusalTracker({ companySlug: input.companySlug });
|
|
36
41
|
const result = await share({
|
|
37
42
|
paths: [input.filePath],
|
|
38
43
|
company: input.companySlug,
|
|
39
44
|
hqRoot: input.hqRoot,
|
|
40
45
|
vaultConfig: buildVaultConfig(input.token),
|
|
41
46
|
onConflict: "abort",
|
|
47
|
+
progressRecorder: trackingProgressRecorder(tracker, input.companySlug),
|
|
42
48
|
});
|
|
43
49
|
return {
|
|
44
50
|
filesUploaded: result.filesUploaded,
|
|
45
51
|
filesSkipped: result.filesSkipped,
|
|
46
52
|
aborted: result.aborted,
|
|
53
|
+
planLimit: tracker.summary(),
|
|
47
54
|
};
|
|
48
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The upload of a stamped skill file was refused by a plan limit. Registration
|
|
58
|
+
* and the local stamp already succeeded, so say that on stderr, then return a
|
|
59
|
+
* typed plan gate for main.ts to render (reason and upgrade link) and exit
|
|
60
|
+
* non-zero. Never "Skill ready … Sync: complete".
|
|
61
|
+
*/
|
|
62
|
+
function skillUploadRefused(skillUid, filePath, companySlug, refused) {
|
|
63
|
+
console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused by your plan limit.`));
|
|
64
|
+
return new PlanGateError("PLAN_LIMIT_EXCEEDED", {
|
|
65
|
+
reason: `${refused.message ?? "New files are paused by your plan limit."} ` +
|
|
66
|
+
"The skill file was not uploaded. Once the limit is sorted, upload it with: " +
|
|
67
|
+
`hq sync push --company ${companySlug} '${filePath}'`,
|
|
68
|
+
...(refused.upgradeUrl ? { upgradeUrl: refused.upgradeUrl } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
49
71
|
export function parseSkillUid(markdown) {
|
|
50
72
|
const match = markdown.match(FRONTMATTER_RE);
|
|
51
73
|
if (!match)
|
|
@@ -575,6 +597,9 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
575
597
|
// Sentry rather than filing a crash on its first occurrence.
|
|
576
598
|
throw localSkillError(`Skill ${registered.skillUid} is stamped locally at '${filePath}', but sync aborted because the remote file conflicts.`);
|
|
577
599
|
}
|
|
600
|
+
if (syncResult.planLimit) {
|
|
601
|
+
throw skillUploadRefused(registered.skillUid, filePath, companySlug, syncResult.planLimit);
|
|
602
|
+
}
|
|
578
603
|
}
|
|
579
604
|
console.log(chalk.green(`Skill ready: ${registered.skillUid}`));
|
|
580
605
|
console.log(` File: ${filePath}`);
|
|
@@ -739,6 +764,9 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
739
764
|
if (syncResult.aborted) {
|
|
740
765
|
throw localSkillError(`Skill ${skillUid} is stamped locally at '${canonicalPath}', but sync aborted because the remote file conflicts.`);
|
|
741
766
|
}
|
|
767
|
+
if (syncResult.planLimit) {
|
|
768
|
+
throw skillUploadRefused(skillUid, canonicalPath, companySlug, syncResult.planLimit);
|
|
769
|
+
}
|
|
742
770
|
}
|
|
743
771
|
console.log(chalk.green(`Skill promoted: ${skillUid}`));
|
|
744
772
|
console.log(` File: ${canonicalPath}`);
|
package/dist/fast-core.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function runFastCore(argv) {
|
|
|
54
54
|
args: parsed.args,
|
|
55
55
|
cwd: process.cwd(),
|
|
56
56
|
hqRoot,
|
|
57
|
+
env: parsed.entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
|
|
57
58
|
});
|
|
58
59
|
// Propagate verbatim: these scripts use their exit code as their interface.
|
|
59
60
|
process.exit(code);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free-month offer hq-pro attaches to Starter plan-limit payloads (the
|
|
3
|
+
* usage-limits nag and the hard-stop 402 body). Kept in a leaf module with no
|
|
4
|
+
* imports so the plan-gate error decoder can reuse the one parser without
|
|
5
|
+
* pulling in the HTTP layer that itself imports the decoder.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlanLimitFreeMonth {
|
|
8
|
+
eligible: boolean;
|
|
9
|
+
trialDays?: number;
|
|
10
|
+
url?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse `freeMonth` off a server payload. Anything but an explicit
|
|
14
|
+
* `eligible: true` is "no offer": an absent or malformed field never invents
|
|
15
|
+
* one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFreeMonth(value: unknown): PlanLimitFreeMonth | undefined;
|
|
18
|
+
//# sourceMappingURL=free-month.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free-month offer hq-pro attaches to Starter plan-limit payloads (the
|
|
3
|
+
* usage-limits nag and the hard-stop 402 body). Kept in a leaf module with no
|
|
4
|
+
* imports so the plan-gate error decoder can reuse the one parser without
|
|
5
|
+
* pulling in the HTTP layer that itself imports the decoder.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse `freeMonth` off a server payload. Anything but an explicit
|
|
9
|
+
* `eligible: true` is "no offer": an absent or malformed field never invents
|
|
10
|
+
* one.
|
|
11
|
+
*/
|
|
12
|
+
export function parseFreeMonth(value) {
|
|
13
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const rec = value;
|
|
17
|
+
if (rec.eligible !== true)
|
|
18
|
+
return undefined;
|
|
19
|
+
const url = typeof rec.url === "string" && rec.url.trim().length > 0
|
|
20
|
+
? rec.url.trim()
|
|
21
|
+
: undefined;
|
|
22
|
+
const trialDays = typeof rec.trialDays === "number" && Number.isFinite(rec.trialDays)
|
|
23
|
+
? rec.trialDays
|
|
24
|
+
: undefined;
|
|
25
|
+
return {
|
|
26
|
+
eligible: true,
|
|
27
|
+
...(trialDays !== undefined ? { trialDays } : {}),
|
|
28
|
+
...(url ? { url } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=free-month.js.map
|