@indigoai-us/hq-cli 5.345.148 → 5.345.150
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 +75 -0
- package/README.md +2 -0
- package/dist/command-catalog.generated.d.ts +62 -14
- package/dist/command-catalog.generated.js +74 -14
- package/dist/command-help.js +2 -1
- package/dist/commands/_patterns.d.ts +6 -0
- package/dist/commands/_patterns.js +6 -0
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/core-refresh-vault-access-native.d.ts +16 -0
- package/dist/commands/core-refresh-vault-access-native.js +142 -9
- package/dist/commands/core.js +4 -1
- package/dist/commands/dm.js +9 -1
- package/dist/commands/groups.js +9 -6
- package/dist/commands/install-global.d.ts +1 -8
- package/dist/commands/install-global.js +11 -27
- package/dist/commands/lanes-questions.js +7 -4
- package/dist/commands/lanes.js +13 -6
- package/dist/commands/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/members.d.ts +35 -0
- package/dist/commands/members.js +65 -14
- package/dist/commands/reindex.js +19 -0
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +11 -6
- package/dist/hook-engine-path.js +10 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +70 -5
- package/dist/lazy-commands.js +2 -0
- package/dist/lib/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -0
- package/dist/lib/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
- package/dist/lib/doctor/checks/anywhere.js +63 -12
- package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
- package/dist/lib/doctor/checks/cli-path.js +2 -1
- package/dist/lib/doctor/checks/hook-engine.js +2 -1
- 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/hook-engine/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/store.d.ts +11 -0
- package/dist/lib/lanes/store.js +20 -0
- package/dist/lib/lanes/watch.js +5 -3
- package/dist/lib/windows/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +80 -3
- package/dist/lib/windows/user-path.js +187 -26
- package/dist/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- package/dist/utils/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- package/package.json +9 -9
|
@@ -2,7 +2,6 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
3
3
|
import { type RuntimeHookTrustResult } from '../utils/hook-trust.js';
|
|
4
4
|
import { type ShimPathCheck } from '../lib/windows/user-path.js';
|
|
5
|
-
import { type HookTarget } from '../lib/install/claude-targets.js';
|
|
6
5
|
export interface GlobalInstallOptions {
|
|
7
6
|
/** HQ checkout to install from (absolute). */
|
|
8
7
|
hqRoot: string;
|
|
@@ -32,13 +31,7 @@ export interface GlobalInstallOptions {
|
|
|
32
31
|
/** Platform override for link creation (default process.platform). */
|
|
33
32
|
platform?: NodeJS.Platform;
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
|
|
37
|
-
/**
|
|
38
|
-
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
39
|
-
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
40
|
-
*/
|
|
41
|
-
export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
|
|
34
|
+
export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
|
|
42
35
|
export interface ClaudePluginSource {
|
|
43
36
|
sourceDir: string;
|
|
44
37
|
marketplaceName: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hookTargetFor } from '../lib/install/hook-target.js';
|
|
1
2
|
/**
|
|
2
3
|
* `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
|
|
3
4
|
* (hq-anywhere-runtime US-005).
|
|
@@ -25,34 +26,11 @@ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/r
|
|
|
25
26
|
import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
|
|
26
27
|
import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
|
|
27
28
|
import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
28
|
-
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
29
|
-
import { readDaemonConfig } from '../lib/daemon/config.js';
|
|
30
|
-
import { daemonPaths } from '../lib/daemon/paths.js';
|
|
31
29
|
import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
|
|
32
|
-
import { checkShimOnPath, shimPathWarning } from '../lib/windows/user-path.js';
|
|
30
|
+
import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
|
|
33
31
|
import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
|
|
34
32
|
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
35
|
-
|
|
36
|
-
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
|
|
37
|
-
if (platform === 'win32')
|
|
38
|
-
return queryTask(DAEMON_WINDOWS_TASK);
|
|
39
|
-
const candidates = platform === 'darwin'
|
|
40
|
-
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
41
|
-
: platform === 'linux'
|
|
42
|
-
? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
|
|
43
|
-
: [];
|
|
44
|
-
return candidates.some((p) => fs.existsSync(p));
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
48
|
-
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
49
|
-
*/
|
|
50
|
-
export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
|
|
51
|
-
const installed = daemonInstalled ?? isDaemonInstalled(home);
|
|
52
|
-
const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
|
|
53
|
-
const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
|
|
54
|
-
return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
|
|
55
|
-
}
|
|
33
|
+
export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
|
|
56
34
|
const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
|
|
57
35
|
/** Resolve local overrides first, then the published HQ marketplace. */
|
|
58
36
|
export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
|
|
@@ -615,7 +593,7 @@ export function installGlobalClaude(opts) {
|
|
|
615
593
|
if (pluginSource)
|
|
616
594
|
log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
|
|
617
595
|
else {
|
|
618
|
-
log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
|
|
596
|
+
log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
|
|
619
597
|
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
|
|
620
598
|
}
|
|
621
599
|
log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
|
|
@@ -994,7 +972,7 @@ export function installGlobalCodex(opts) {
|
|
|
994
972
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
995
973
|
reportSkillCopies(linkKinds, log);
|
|
996
974
|
reportSkippedSkills(skills, log, false);
|
|
997
|
-
log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
975
|
+
log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
998
976
|
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
|
|
999
977
|
return touched;
|
|
1000
978
|
}
|
|
@@ -1213,6 +1191,12 @@ export async function runInstallGlobal(opts) {
|
|
|
1213
1191
|
if (runtime === 'codex' && !opts.dryRun)
|
|
1214
1192
|
await trustInstalledCodexHooks(hqRoot, opts);
|
|
1215
1193
|
if (!opts.dryRun) {
|
|
1194
|
+
try {
|
|
1195
|
+
ensureShimOnUserPath({ env, entry: process.argv[1] });
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
// The warning below is the fallback when the user Path cannot be updated.
|
|
1199
|
+
}
|
|
1216
1200
|
const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
|
|
1217
1201
|
if (shimPath)
|
|
1218
1202
|
for (const line of shimPathWarning(shimPath))
|
|
@@ -2,7 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
3
3
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
4
4
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
5
|
-
import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
5
|
+
import { appendCappedHistory, createQuestion, expandLocalLaneIdPrefix, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
6
6
|
import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
|
|
7
7
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
8
8
|
import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
|
|
@@ -565,6 +565,9 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
565
565
|
const hqRoot = ctx.hqRoot();
|
|
566
566
|
const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
|
|
567
567
|
const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
|
|
568
|
+
// --from matches the full lane id; a short prefix expands only when it
|
|
569
|
+
// names exactly one local lane (ambiguous prefixes are refused).
|
|
570
|
+
const from = opts.from ? (expandLocalLaneIdPrefix(hqRoot, opts.from) ?? opts.from) : undefined;
|
|
568
571
|
const company = opts.company
|
|
569
572
|
? await ctx.resolveCompany(opts.company)
|
|
570
573
|
: await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
@@ -574,7 +577,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
574
577
|
.filter((q) => {
|
|
575
578
|
if (status && q.status !== status)
|
|
576
579
|
return false;
|
|
577
|
-
if (
|
|
580
|
+
if (from && q.lane_id !== from && q.origin.id !== from)
|
|
578
581
|
return false;
|
|
579
582
|
if (opts.project && q.project_id !== opts.project)
|
|
580
583
|
return false;
|
|
@@ -618,7 +621,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
618
621
|
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, undefined, {
|
|
619
622
|
sessionId: currentSessionId ?? undefined,
|
|
620
623
|
for: opts.for,
|
|
621
|
-
from
|
|
624
|
+
from,
|
|
622
625
|
project: opts.project,
|
|
623
626
|
story: opts.story,
|
|
624
627
|
status: opts.status,
|
|
@@ -651,7 +654,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
651
654
|
.filter(({ question: q, lane, audience }) => {
|
|
652
655
|
if (status && q.status !== status)
|
|
653
656
|
return false;
|
|
654
|
-
if (
|
|
657
|
+
if (from && q.lane_id !== from && q.origin.id !== from)
|
|
655
658
|
return false;
|
|
656
659
|
if (opts.project && q.project_id !== opts.project)
|
|
657
660
|
return false;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -78,7 +78,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
78
78
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
79
79
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
80
80
|
import { remoteControlSelectionConfig, remoteControlSessionHome } from "../lib/lanes/remote-control-lane.js";
|
|
81
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
81
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, expandLocalLaneIdPrefix, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
82
82
|
import { admitQueuedNow } from "../lib/lanes/reconcile.js";
|
|
83
83
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
84
84
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
@@ -1979,6 +1979,11 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1979
1979
|
if (localMatch)
|
|
1980
1980
|
return { lane: readLane(ctx.hqRoot(), localMatch), remoteOnly: false };
|
|
1981
1981
|
}
|
|
1982
|
+
// A short prefix expands only to exactly one local lane; more than one is
|
|
1983
|
+
// refused with the candidates listed (lane_id_ambiguous).
|
|
1984
|
+
const prefixMatch = expandLocalLaneIdPrefix(ctx.hqRoot(), laneId);
|
|
1985
|
+
if (prefixMatch)
|
|
1986
|
+
return { lane: readLane(ctx.hqRoot(), prefixMatch), remoteOnly: false };
|
|
1982
1987
|
const separator = laneId.lastIndexOf("_");
|
|
1983
1988
|
const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
|
|
1984
1989
|
const ownerLookupGate = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
|
|
@@ -4561,7 +4566,8 @@ async function runCreate(ctx, opts) {
|
|
|
4561
4566
|
printJson({
|
|
4562
4567
|
ok: true,
|
|
4563
4568
|
action: "create",
|
|
4564
|
-
admitted
|
|
4569
|
+
// lane_id stays ahead of `admitted`: callers that take the first lane id
|
|
4570
|
+
// in this output must get the lane they created, not one it admitted.
|
|
4565
4571
|
lane_id: started.lane_id,
|
|
4566
4572
|
state: started.state,
|
|
4567
4573
|
queued_reason: started.queued_reason ?? null,
|
|
@@ -4603,6 +4609,7 @@ async function runCreate(ctx, opts) {
|
|
|
4603
4609
|
.outpost_command_id,
|
|
4604
4610
|
}
|
|
4605
4611
|
: {}),
|
|
4612
|
+
admitted,
|
|
4606
4613
|
lane: started,
|
|
4607
4614
|
});
|
|
4608
4615
|
return started;
|
|
@@ -8942,10 +8949,10 @@ async function runCreateSubagent(ctx, opts) {
|
|
|
8942
8949
|
}
|
|
8943
8950
|
assertNoSecretText("focus override reason", opts.focusOverride.trim());
|
|
8944
8951
|
}
|
|
8952
|
+
// A subagent lane is a board record for work the senior session runs with
|
|
8953
|
+
// its own host's subagent tool (Claude's Agent, Codex's spawn_agent). No
|
|
8954
|
+
// process is spawned, so any provider is only attribution.
|
|
8945
8955
|
const provider = opts.provider ? parseLaneProvider(opts.provider) : "claude";
|
|
8946
|
-
if (provider !== "claude") {
|
|
8947
|
-
throw new LanesError("subagent_runner_requires_claude", "--runner subagent records a Claude subagent; use --provider claude or omit it", { provider });
|
|
8948
|
-
}
|
|
8949
8956
|
if (ctx.env.HQ_LANE_ID?.trim()) {
|
|
8950
8957
|
throw new LanesError("subagent_runner_requires_session", "A subagent lane belongs to the session that runs the subagent; a lane worker cannot register one", { caller_lane_id: ctx.env.HQ_LANE_ID.trim() });
|
|
8951
8958
|
}
|
|
@@ -10277,7 +10284,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
10277
10284
|
.option("--fresh-thread", "Skip worker continuity and start a cold provider session")
|
|
10278
10285
|
.option("--loop", "Keep one worker lane registered to process queued phase envelopes")
|
|
10279
10286
|
.option("--provider <name>", "claude | codex | grok")
|
|
10280
|
-
.option("--runner <kind>", "process (default) | subagent: register a lane an in-session Claude
|
|
10287
|
+
.option("--runner <kind>", "process (default) | subagent: register a lane an in-session subagent runs (Claude Agent, Codex spawn_agent); skips admission and starts no process")
|
|
10281
10288
|
.option("--remote-control", "Open a Claude lane in Remote Control using account a for login and the selected account for inference")
|
|
10282
10289
|
.option("--title <text>", "Human-readable Claude Remote Control session title; required with --remote-control")
|
|
10283
10290
|
.option("--account <id>", "Provider account id, or 'auto' to pick the account with the most room (all accounts are one pool across companies)")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
|
-
import { type CloudAssetMediaKind } from "@indigoai-us/hq-cloud";
|
|
2
|
+
import { type CloudAssetMediaKind, type PullScope, type PullScopeClient } from "@indigoai-us/hq-cloud";
|
|
3
3
|
import type { LibraryCaptionNote, LibraryCaptionRequest } from "./library-caption.js";
|
|
4
4
|
export interface LibraryCompany {
|
|
5
5
|
uid: string;
|
|
@@ -37,6 +37,13 @@ export interface LibraryAddDeps {
|
|
|
37
37
|
* a caption failure never fails the upload. Absent = no captioning.
|
|
38
38
|
*/
|
|
39
39
|
caption?: (request: LibraryCaptionRequest) => Promise<LibraryCaptionNote>;
|
|
40
|
+
/**
|
|
41
|
+
* The membership's sync scope for the company. Throws when it cannot be
|
|
42
|
+
* read. Absent = the scope is not checked and nothing is pinned.
|
|
43
|
+
*/
|
|
44
|
+
syncScope?: (company: LibraryCompany) => Promise<Pick<PullScope, "syncMode" | "prefixSet">>;
|
|
45
|
+
/** `hq files get`'s pin writer (`.hq/pins.json`). */
|
|
46
|
+
addPin?: (hqRoot: string, companySlug: string, prefix: string) => void;
|
|
40
47
|
}
|
|
41
48
|
export interface LibraryAddOptions {
|
|
42
49
|
company?: string;
|
|
@@ -60,9 +67,19 @@ export interface LibraryAddItem {
|
|
|
60
67
|
/** AI caption outcome, set when captioning ran for this item. */
|
|
61
68
|
caption?: LibraryCaptionNote;
|
|
62
69
|
}
|
|
70
|
+
export interface LibrarySyncScopeNote {
|
|
71
|
+
status: "in-scope" | "pinned" | "not-pinned";
|
|
72
|
+
/** Company-relative pins written (`pinned`) or still needed (`not-pinned`). */
|
|
73
|
+
pins: string[];
|
|
74
|
+
/** Set when not pinned and `hq files get` can pin by hand (the library folder). */
|
|
75
|
+
command?: string;
|
|
76
|
+
reason?: string;
|
|
77
|
+
}
|
|
63
78
|
export interface LibraryAddResult {
|
|
64
79
|
company: string;
|
|
65
80
|
items: LibraryAddItem[];
|
|
81
|
+
/** Set when at least one placeholder was written and the scope was checked. */
|
|
82
|
+
syncScope?: LibrarySyncScopeNote;
|
|
66
83
|
}
|
|
67
84
|
export declare function contentTypeFor(name: string, served?: string): string;
|
|
68
85
|
export declare function mediaKindFor(contentType: string): CloudAssetMediaKind;
|
|
@@ -89,6 +106,19 @@ export declare function offloadTimestamp(now: Date): string;
|
|
|
89
106
|
* only that source.
|
|
90
107
|
*/
|
|
91
108
|
export declare function runLibraryAdd(inputs: readonly string[], options: LibraryAddOptions, deps: LibraryAddDeps): Promise<LibraryAddResult>;
|
|
109
|
+
/**
|
|
110
|
+
* A `shared` or `custom` sync pushes only paths inside the membership's
|
|
111
|
+
* prefix set (explicit grants plus pins), so a placeholder written outside it
|
|
112
|
+
* never reaches the vault. Pin it with `hq files get`'s pin writer: the
|
|
113
|
+
* default library folder as a whole, and for any other `--to` folder only the
|
|
114
|
+
* written placeholders, so the rest of that folder stays out of sync.
|
|
115
|
+
*/
|
|
116
|
+
export declare function ensureLibraryInSyncScope(company: LibraryCompany, dirRel: string, placeholderPaths: readonly string[], deps: Pick<LibraryAddDeps, "hqRoot" | "syncScope" | "addPin">): Promise<LibrarySyncScopeNote | undefined>;
|
|
117
|
+
/**
|
|
118
|
+
* {@link resolvePullScope} answers `all` when any call fails. Report the
|
|
119
|
+
* failure instead, so a failed lookup is not mistaken for full sync.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resolveLibrarySyncScope(client: Required<PullScopeClient>, company: LibraryCompany, hqRoot: string): Promise<PullScope>;
|
|
92
122
|
export interface LibraryTagResult {
|
|
93
123
|
placeholderPath: string;
|
|
94
124
|
tags: string[];
|
|
@@ -28,7 +28,7 @@ import http from "node:http";
|
|
|
28
28
|
import https from "node:https";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { Readable } from "node:stream";
|
|
31
|
-
import { formatCloudAssetBlock, formatCloudPlaceholder, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
|
|
31
|
+
import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
|
|
32
32
|
import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
|
|
33
33
|
import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
|
|
34
34
|
/** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
|
|
@@ -321,7 +321,74 @@ export async function runLibraryAdd(inputs, options, deps) {
|
|
|
321
321
|
.catch((err) => ({ status: "failed", note: err.message }));
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
-
|
|
324
|
+
const written = items.flatMap((item) => (item.ok && item.placeholderPath ? [item.placeholderPath] : []));
|
|
325
|
+
if (written.length === 0)
|
|
326
|
+
return { company: slug, items };
|
|
327
|
+
const syncScope = await ensureLibraryInSyncScope(company, dirRel, written, deps);
|
|
328
|
+
return { company: slug, items, ...(syncScope ? { syncScope } : {}) };
|
|
329
|
+
}
|
|
330
|
+
const LIBRARY_FOLDER = "library/";
|
|
331
|
+
/**
|
|
332
|
+
* A `shared` or `custom` sync pushes only paths inside the membership's
|
|
333
|
+
* prefix set (explicit grants plus pins), so a placeholder written outside it
|
|
334
|
+
* never reaches the vault. Pin it with `hq files get`'s pin writer: the
|
|
335
|
+
* default library folder as a whole, and for any other `--to` folder only the
|
|
336
|
+
* written placeholders, so the rest of that folder stays out of sync.
|
|
337
|
+
*/
|
|
338
|
+
export async function ensureLibraryInSyncScope(company, dirRel, placeholderPaths, deps) {
|
|
339
|
+
if (!deps.syncScope || !deps.addPin)
|
|
340
|
+
return undefined;
|
|
341
|
+
const companyRel = (rel) => rel.split("/").slice(2).join("/");
|
|
342
|
+
const isLibrary = `${companyRel(dirRel)}/` === LIBRARY_FOLDER;
|
|
343
|
+
const targets = isLibrary ? [LIBRARY_FOLDER] : placeholderPaths.map(companyRel);
|
|
344
|
+
const command = isLibrary ? `hq files get ${LIBRARY_FOLDER} --company ${company.slug}` : undefined;
|
|
345
|
+
let scope;
|
|
346
|
+
try {
|
|
347
|
+
scope = await deps.syncScope(company);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return {
|
|
351
|
+
status: "not-pinned",
|
|
352
|
+
pins: targets,
|
|
353
|
+
...(command ? { command } : {}),
|
|
354
|
+
reason: `could not read your sync scope: ${err.message}`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const missing = scope.syncMode === "all" ? [] : targets.filter((t) => !isCoveredByAny(t, scope.prefixSet ?? []));
|
|
358
|
+
if (missing.length === 0)
|
|
359
|
+
return { status: "in-scope", pins: [] };
|
|
360
|
+
try {
|
|
361
|
+
for (const target of missing)
|
|
362
|
+
deps.addPin(deps.hqRoot, company.slug, target);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
return {
|
|
366
|
+
status: "not-pinned",
|
|
367
|
+
pins: missing,
|
|
368
|
+
...(command ? { command } : {}),
|
|
369
|
+
reason: `could not write the pin: ${err.message}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return { status: "pinned", pins: missing };
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* {@link resolvePullScope} answers `all` when any call fails. Report the
|
|
376
|
+
* failure instead, so a failed lookup is not mistaken for full sync.
|
|
377
|
+
*/
|
|
378
|
+
export async function resolveLibrarySyncScope(client, company, hqRoot) {
|
|
379
|
+
let failure;
|
|
380
|
+
const watch = (call) => call.catch((err) => {
|
|
381
|
+
failure ??= err;
|
|
382
|
+
throw err;
|
|
383
|
+
});
|
|
384
|
+
const scope = await resolvePullScope({
|
|
385
|
+
listMyMemberships: () => watch(client.listMyMemberships()),
|
|
386
|
+
getMembershipSyncConfig: (id) => watch(client.getMembershipSyncConfig(id)),
|
|
387
|
+
listMyExplicitGrants: (uid) => watch(client.listMyExplicitGrants(uid)),
|
|
388
|
+
}, company.uid, company.slug, hqRoot);
|
|
389
|
+
if (failure !== undefined)
|
|
390
|
+
throw failure instanceof Error ? failure : new Error(String(failure));
|
|
391
|
+
return scope;
|
|
325
392
|
}
|
|
326
393
|
async function writeAssetPlaceholder(p, options, deps, dirAbs) {
|
|
327
394
|
const ts = offloadTimestamp(deps.now());
|
package/dist/commands/library.js
CHANGED
|
@@ -35,7 +35,7 @@ import { findHqRoot } from "../utils/manifest.js";
|
|
|
35
35
|
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
36
36
|
import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
|
|
37
37
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
38
|
-
import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
|
|
38
|
+
import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
|
|
39
39
|
import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
|
|
40
40
|
import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
|
|
41
41
|
/** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
|
|
@@ -295,8 +295,39 @@ export function formatLibraryAddResult(result) {
|
|
|
295
295
|
return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
|
|
296
296
|
return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
|
|
297
297
|
})
|
|
298
|
+
.concat(formatLibrarySyncScope(result))
|
|
298
299
|
.join("\n");
|
|
299
300
|
}
|
|
301
|
+
function formatLibrarySyncScope({ company, syncScope }) {
|
|
302
|
+
if (!syncScope || syncScope.status === "in-scope")
|
|
303
|
+
return [];
|
|
304
|
+
const pins = syncScope.pins.map((pin) => `${company}:${pin}`).join(", ");
|
|
305
|
+
if (syncScope.status === "pinned") {
|
|
306
|
+
return [chalk.dim(`Pinned ${pins} so sync uploads these placeholders to the company vault.`)];
|
|
307
|
+
}
|
|
308
|
+
const fix = syncScope.command
|
|
309
|
+
? `Pin the folder with: ${syncScope.command}`
|
|
310
|
+
: `Add ${syncScope.pins.join(", ")} under "${company}" in .hq/pins.json at the HQ root.`;
|
|
311
|
+
return [
|
|
312
|
+
chalk.yellow(`Warning: sync may not upload these placeholders (${syncScope.reason}). ` +
|
|
313
|
+
`Until it does, teammates and cloud search will not see them. ${fix}`),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
/** Sync scope wiring for `hq library add`; an API key has no sync tree to check. */
|
|
317
|
+
async function librarySyncScopeDeps(credential, hqRoot) {
|
|
318
|
+
if (credential.kind !== "cognito")
|
|
319
|
+
return {};
|
|
320
|
+
const [{ VaultClient }, { buildVaultConfig }, { addPin }] = await Promise.all([
|
|
321
|
+
import("@indigoai-us/hq-cloud"),
|
|
322
|
+
import("../utils/cognito-session.js"),
|
|
323
|
+
import("./files-browse.js"),
|
|
324
|
+
]);
|
|
325
|
+
const client = new VaultClient(buildVaultConfig(credential.token));
|
|
326
|
+
return {
|
|
327
|
+
syncScope: (company) => resolveLibrarySyncScope(client, company, hqRoot),
|
|
328
|
+
addPin,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
300
331
|
/** GET a presigned URL. Errors never echo the URL, which carries a signature. */
|
|
301
332
|
async function downloadPresigned(url) {
|
|
302
333
|
const res = await fetch(url);
|
|
@@ -366,9 +397,11 @@ export function registerLibraryCommand(program) {
|
|
|
366
397
|
.option("--json", "Print machine-readable JSON")
|
|
367
398
|
.action(async (sources, options) => {
|
|
368
399
|
try {
|
|
369
|
-
const
|
|
400
|
+
const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
|
|
370
401
|
interactive: true,
|
|
371
402
|
});
|
|
403
|
+
const { token } = credential;
|
|
404
|
+
const hqRoot = findHqRoot();
|
|
372
405
|
const result = await runLibraryAdd(sources, {
|
|
373
406
|
company: options.company,
|
|
374
407
|
to: options.to,
|
|
@@ -376,11 +409,12 @@ export function registerLibraryCommand(program) {
|
|
|
376
409
|
title: options.title,
|
|
377
410
|
note: options.note,
|
|
378
411
|
}, {
|
|
379
|
-
hqRoot
|
|
412
|
+
hqRoot,
|
|
380
413
|
now: () => new Date(),
|
|
381
414
|
resolveCompany: (slug) => libraryCompany(token, slug),
|
|
382
415
|
presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
383
416
|
...defaultLibraryIo,
|
|
417
|
+
...(await librarySyncScopeDeps(credential, hqRoot)),
|
|
384
418
|
...(options.caption === false
|
|
385
419
|
? {}
|
|
386
420
|
: {
|
|
@@ -30,6 +30,23 @@ export interface ActiveMember {
|
|
|
30
30
|
personName?: string;
|
|
31
31
|
personSlug?: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The company's active Outpost, as listed beside the members by
|
|
35
|
+
* `GET /membership/company/{companyUid}`. It is read-only: it can be granted
|
|
36
|
+
* secrets (`hq secrets share --with out_…`, `hq groups add`), but it cannot be
|
|
37
|
+
* invited, promoted or removed.
|
|
38
|
+
*/
|
|
39
|
+
export interface CompanyOutpostMember {
|
|
40
|
+
uid: string;
|
|
41
|
+
kind: "outpost";
|
|
42
|
+
label: string;
|
|
43
|
+
outpostId: string;
|
|
44
|
+
readOnly: true;
|
|
45
|
+
}
|
|
46
|
+
export interface CompanyRoster {
|
|
47
|
+
members: ActiveMember[];
|
|
48
|
+
outposts: CompanyOutpostMember[];
|
|
49
|
+
}
|
|
33
50
|
export interface InviteOptions {
|
|
34
51
|
target: string;
|
|
35
52
|
role: string;
|
|
@@ -165,7 +182,25 @@ export declare function pendingInviteJson(inv: PendingInvite): {
|
|
|
165
182
|
invited_at: string;
|
|
166
183
|
membership_key: string;
|
|
167
184
|
};
|
|
185
|
+
/** JSON row for a company Outpost in `hq members list --json`. */
|
|
186
|
+
export declare function companyOutpostJson(o: CompanyOutpostMember): {
|
|
187
|
+
person_id: string;
|
|
188
|
+
kind: "outpost";
|
|
189
|
+
label: string;
|
|
190
|
+
outpost_id: string;
|
|
191
|
+
read_only: true;
|
|
192
|
+
};
|
|
168
193
|
export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
|
|
194
|
+
/**
|
|
195
|
+
* Active members plus the company's active Outpost. Servers that predate
|
|
196
|
+
* Outpost listing omit `outposts`; that reads as none.
|
|
197
|
+
*/
|
|
198
|
+
export declare function listCompanyRoster(token: string, companyUid: string): Promise<CompanyRoster>;
|
|
199
|
+
/**
|
|
200
|
+
* The company Outpost is listed read-only. hq-pro rejects promote, remove and
|
|
201
|
+
* revoke for it (OUTPOST_MEMBER_READ_ONLY); say so before any request goes out.
|
|
202
|
+
*/
|
|
203
|
+
export declare const OUTPOST_READ_ONLY_MESSAGE: string;
|
|
169
204
|
/**
|
|
170
205
|
* Resolve a role-change target to an active membership key.
|
|
171
206
|
*
|
package/dist/commands/members.js
CHANGED
|
@@ -4,6 +4,7 @@ import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
|
4
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
5
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
6
6
|
import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
7
|
+
import { OUTPOST_UID_PATTERN } from "./_patterns.js";
|
|
7
8
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
8
9
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
9
10
|
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
@@ -261,7 +262,24 @@ export function pendingInviteJson(inv) {
|
|
|
261
262
|
membership_key: inv.membershipKey,
|
|
262
263
|
};
|
|
263
264
|
}
|
|
265
|
+
/** JSON row for a company Outpost in `hq members list --json`. */
|
|
266
|
+
export function companyOutpostJson(o) {
|
|
267
|
+
return {
|
|
268
|
+
person_id: o.uid,
|
|
269
|
+
kind: "outpost",
|
|
270
|
+
label: o.label || "Outpost",
|
|
271
|
+
outpost_id: o.outpostId,
|
|
272
|
+
read_only: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
264
275
|
export async function listActiveMembers(token, companyUid) {
|
|
276
|
+
return (await listCompanyRoster(token, companyUid)).members;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Active members plus the company's active Outpost. Servers that predate
|
|
280
|
+
* Outpost listing omit `outposts`; that reads as none.
|
|
281
|
+
*/
|
|
282
|
+
export async function listCompanyRoster(token, companyUid) {
|
|
265
283
|
const res = await vaultApiFetch({
|
|
266
284
|
token,
|
|
267
285
|
path: `/membership/company/${encodeURIComponent(companyUid)}`,
|
|
@@ -270,10 +288,26 @@ export async function listActiveMembers(token, companyUid) {
|
|
|
270
288
|
const err = (await res.json().catch(() => ({})));
|
|
271
289
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
272
290
|
}
|
|
273
|
-
// Server schema: `{ members: [...] }` — active members
|
|
274
|
-
// resolved person metadata (personEmail / personName /
|
|
291
|
+
// Server schema: `{ members: [...], outposts: [...] }` — active members
|
|
292
|
+
// only, enriched with resolved person metadata (personEmail / personName /
|
|
293
|
+
// personSlug), plus the read-only company Outpost rows.
|
|
275
294
|
const data = (await res.json());
|
|
276
|
-
|
|
295
|
+
const outposts = (data?.outposts ?? []).filter((o) => o?.kind === "outpost" &&
|
|
296
|
+
typeof o.uid === "string" &&
|
|
297
|
+
OUTPOST_UID_PATTERN.test(o.uid));
|
|
298
|
+
return { members: data?.members ?? [], outposts };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The company Outpost is listed read-only. hq-pro rejects promote, remove and
|
|
302
|
+
* revoke for it (OUTPOST_MEMBER_READ_ONLY); say so before any request goes out.
|
|
303
|
+
*/
|
|
304
|
+
export const OUTPOST_READ_ONLY_MESSAGE = "An Outpost is shown read-only and cannot be invited, promoted or removed. " +
|
|
305
|
+
"To change what it can read, use `hq secrets share` / `hq secrets unshare` or `hq groups`.";
|
|
306
|
+
function assertNotOutpost(target) {
|
|
307
|
+
const uid = (target.split("#")[0] ?? "").trim();
|
|
308
|
+
if (OUTPOST_UID_PATTERN.test(uid)) {
|
|
309
|
+
throw new Error(OUTPOST_READ_ONLY_MESSAGE);
|
|
310
|
+
}
|
|
277
311
|
}
|
|
278
312
|
/**
|
|
279
313
|
* Resolve a role-change target to an active membership key.
|
|
@@ -283,6 +317,7 @@ export async function listActiveMembers(token, companyUid) {
|
|
|
283
317
|
* active member list instead of synthesizing an `email:...#company` key.
|
|
284
318
|
*/
|
|
285
319
|
export async function resolveRoleChangeTarget(token, companyUid, target) {
|
|
320
|
+
assertNotOutpost(target);
|
|
286
321
|
if (target.includes("#"))
|
|
287
322
|
return target;
|
|
288
323
|
const detected = detectTarget(target);
|
|
@@ -325,6 +360,7 @@ export async function resolveRoleChangeTarget(token, companyUid, target) {
|
|
|
325
360
|
* that exact email was just shown by `hq members list`.
|
|
326
361
|
*/
|
|
327
362
|
export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
|
|
363
|
+
assertNotOutpost(arg);
|
|
328
364
|
if (arg.includes("#"))
|
|
329
365
|
return arg;
|
|
330
366
|
const detected = detectTarget(arg);
|
|
@@ -820,33 +856,48 @@ export function registerMembersCommand(program) {
|
|
|
820
856
|
}
|
|
821
857
|
// Default: list ACTIVE members so their emails drop straight into
|
|
822
858
|
// `hq secrets share <path> --with <email>`.
|
|
823
|
-
|
|
859
|
+
// The company Outpost is listed after the people, read-only; its
|
|
860
|
+
// out_ id drops into `hq secrets share <path> --with out_…`.
|
|
861
|
+
const { members: activeMembers, outposts } = await listCompanyRoster(token, companyUid);
|
|
824
862
|
if (opts.json) {
|
|
825
863
|
console.log(JSON.stringify({
|
|
826
864
|
company_uid: companyUid,
|
|
827
865
|
members: activeMembers.map(activeMemberJson),
|
|
866
|
+
outposts: outposts.map(companyOutpostJson),
|
|
828
867
|
}, null, 2));
|
|
829
868
|
return;
|
|
830
869
|
}
|
|
831
|
-
if (activeMembers.length === 0) {
|
|
870
|
+
if (activeMembers.length === 0 && outposts.length === 0) {
|
|
832
871
|
console.log(chalk.gray("No active members found for this company."));
|
|
833
872
|
return;
|
|
834
873
|
}
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
874
|
+
const rows = [
|
|
875
|
+
...activeMembers.map((m) => ({
|
|
876
|
+
email: m.personEmail ?? m.personUid,
|
|
877
|
+
role: m.role,
|
|
878
|
+
id: m.personUid,
|
|
879
|
+
name: m.personName ?? m.personSlug ?? "",
|
|
880
|
+
})),
|
|
881
|
+
...outposts.map((o) => ({
|
|
882
|
+
email: o.uid,
|
|
883
|
+
role: "outpost",
|
|
884
|
+
id: o.uid,
|
|
885
|
+
name: `${o.label || "Outpost"} (read-only)`,
|
|
886
|
+
})),
|
|
887
|
+
];
|
|
888
|
+
const emailW = Math.max(5, ...rows.map((r) => r.email.length));
|
|
889
|
+
const roleW = Math.max(4, ...rows.map((r) => r.role.length));
|
|
890
|
+
const idW = Math.max(9, ...rows.map((r) => r.id.length));
|
|
838
891
|
const header = ["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW)];
|
|
839
892
|
if (opts.ids)
|
|
840
893
|
header.push("PERSON_ID".padEnd(idW));
|
|
841
894
|
header.push("NAME");
|
|
842
895
|
console.log(chalk.bold(header.join(" ")));
|
|
843
|
-
for (const
|
|
844
|
-
const
|
|
845
|
-
const name = m.personName ?? m.personSlug ?? "";
|
|
846
|
-
const row = [email.padEnd(emailW), m.role.padEnd(roleW)];
|
|
896
|
+
for (const r of rows) {
|
|
897
|
+
const row = [r.email.padEnd(emailW), r.role.padEnd(roleW)];
|
|
847
898
|
if (opts.ids)
|
|
848
|
-
row.push(
|
|
849
|
-
row.push(name);
|
|
899
|
+
row.push(r.id.padEnd(idW));
|
|
900
|
+
row.push(r.name);
|
|
850
901
|
console.log(row.join(" "));
|
|
851
902
|
}
|
|
852
903
|
}
|
package/dist/commands/reindex.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reconcileHookRegistrations } from '../lib/hook-engine/reconcile.js';
|
|
2
|
+
import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
1
3
|
/**
|
|
2
4
|
* hq reindex — surface namespaced skills and commands, clean up retired
|
|
3
5
|
* personal-overlay mirror links, capture coding-harness session logs, regenerate
|
|
@@ -775,6 +777,23 @@ export function registerReindexCommand(program) {
|
|
|
775
777
|
// means a sync/rescue holds the shared operation lock and may be
|
|
776
778
|
// rewriting .claude/settings.json right now, and this is an unlocked
|
|
777
779
|
// whole-file read-modify-write.
|
|
780
|
+
// Registration reconcile is best-effort: a failure leaves the current
|
|
781
|
+
// registrations in place and must not stop the rest of reindex.
|
|
782
|
+
try {
|
|
783
|
+
// A file that failed is left as it was; the keys of files that were
|
|
784
|
+
// rewritten are still trusted so Codex keeps running their hooks.
|
|
785
|
+
const { codexUserKeys, failures } = reconcileHookRegistrations(hqRoot);
|
|
786
|
+
for (const failure of failures)
|
|
787
|
+
console.warn(`reindex: hook registration reconcile skipped ${failure}`);
|
|
788
|
+
if (codexUserKeys.length) {
|
|
789
|
+
const trust = await trustCodexUserHooks(hqRoot, codexUserKeys);
|
|
790
|
+
if (trust.status === 'failed')
|
|
791
|
+
console.warn(`reindex: Codex hook trust needs repair: ${trust.reason}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.warn(`reindex: hook registration reconcile skipped: ${error.message}`);
|
|
796
|
+
}
|
|
778
797
|
dropProjectDefaultModeForBypassUser(hqRoot);
|
|
779
798
|
await trustHqRuntimeHooks(hqRoot, undefined, {
|
|
780
799
|
rootAliases: [literalHqRoot],
|