@indigoai-us/hq-cli 5.345.152 → 5.345.154
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 +96 -0
- package/dist/bin/hq-auth-refresh.d.ts +4 -0
- package/dist/bin/hq-auth-refresh.js +9 -1
- package/dist/command-catalog.generated.d.ts +185 -0
- package/dist/command-catalog.generated.js +227 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +12 -0
- package/dist/commands/agent-enroll.js +37 -4
- package/dist/commands/cloud.d.ts +17 -0
- package/dist/commands/cloud.js +48 -1
- package/dist/commands/core-policy.js +4 -4
- package/dist/commands/core-session-project-native.js +19 -4
- package/dist/commands/decide.d.ts +13 -0
- package/dist/commands/decide.js +74 -0
- package/dist/commands/integrations-connect.js +24 -1
- package/dist/commands/integrations-core.d.ts +14 -0
- package/dist/commands/integrations-core.js +19 -1
- package/dist/commands/integrations-import.js +22 -1
- package/dist/commands/integrations.js +43 -2
- package/dist/commands/lanes-message.js +4 -11
- package/dist/commands/lanes-questions.js +3 -0
- package/dist/commands/lanes-resume.js +4 -11
- package/dist/commands/lanes-stories.js +3 -0
- package/dist/commands/lanes.js +7 -1
- package/dist/commands/run.js +14 -5
- package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
- package/dist/lib/agent-kit/adopt-identity.js +18 -3
- package/dist/lib/agent-kit/services.d.ts +3 -1
- package/dist/lib/agent-kit/services.js +5 -1
- package/dist/lib/daemon/ops/policy.d.ts +9 -1
- package/dist/lib/daemon/ops/policy.js +37 -2
- package/dist/lib/decisions/client.d.ts +67 -0
- package/dist/lib/decisions/client.js +120 -0
- package/dist/lib/decisions/expiry.d.ts +2 -0
- package/dist/lib/decisions/expiry.js +12 -0
- package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
- package/dist/lib/decisions/fixtures/us001.js +21 -0
- package/dist/lib/decisions/import-lanes.d.ts +16 -0
- package/dist/lib/decisions/import-lanes.js +152 -0
- package/dist/lib/decisions/principal.d.ts +11 -0
- package/dist/lib/decisions/principal.js +52 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
- package/dist/lib/doctor/checks/claude-wiring.js +21 -10
- package/dist/lib/doctor/checks/work-context.js +7 -2
- package/dist/lib/doctor/fix/apply.d.ts +3 -2
- package/dist/lib/doctor/fix/apply.js +19 -15
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
- package/dist/lib/doctor/fix/remediation.js +4 -2
- package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
- package/dist/lib/doctor/hook-gate-profiles.js +34 -0
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/install/provider-wrappers.js +5 -1
- package/dist/lib/lanes/autopilot.js +4 -1
- package/dist/lib/lanes/imported-question.d.ts +5 -0
- package/dist/lib/lanes/imported-question.js +17 -0
- package/dist/lib/lanes/questions.d.ts +0 -5
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +21 -10
- package/dist/lib/lanes/store.d.ts +0 -8
- package/dist/lib/lanes/store.js +2 -1
- package/dist/lib/lanes/stories.d.ts +15 -0
- package/dist/lib/lanes/stories.js +25 -0
- package/dist/lib/mesh/client.d.ts +8 -0
- package/dist/lib/mesh/client.js +44 -3
- package/dist/lib/mesh/denial-codes.js +1 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
- package/dist/lib/mesh/live/daemon/run.js +30 -1
- package/dist/lib/mesh/live/flush.d.ts +7 -0
- package/dist/lib/mesh/live/flush.js +53 -10
- package/dist/lib/registry/registry.js +6 -0
- package/dist/lib/scan-packages/index.js +7 -3
- package/dist/lib/sync/read-only-local-only.d.ts +45 -0
- package/dist/lib/sync/read-only-local-only.js +122 -0
- package/dist/lib/work-context/organize.d.ts +2 -0
- package/dist/lib/work-context/organize.js +22 -4
- package/dist/lib/workers-registry/index.d.ts +12 -1
- package/dist/lib/workers-registry/index.js +39 -1
- package/dist/utils/cognito-session.d.ts +4 -1
- package/dist/utils/cognito-session.js +11 -1
- package/package.json +7 -7
|
@@ -262,6 +262,7 @@ export async function flushSessionEvents(inputDeps) {
|
|
|
262
262
|
pendingRestore.delete(event);
|
|
263
263
|
};
|
|
264
264
|
const loggedProject404 = new Set();
|
|
265
|
+
const heldUidCache = new Map();
|
|
265
266
|
sessionLoop: for (const [sessionId, items] of bySession) {
|
|
266
267
|
if (deps.signal?.aborted)
|
|
267
268
|
break sessionLoop;
|
|
@@ -283,7 +284,8 @@ export async function flushSessionEvents(inputDeps) {
|
|
|
283
284
|
if (leaveResult.abort)
|
|
284
285
|
continue;
|
|
285
286
|
const remainingItems = leaveResult.items;
|
|
286
|
-
|
|
287
|
+
const heldBinding = bindHeldCompanyIfResolved(sessionId, remainingItems, state, deps, now, heldUidCache);
|
|
288
|
+
state = heldBinding instanceof Promise ? await heldBinding : heldBinding;
|
|
287
289
|
if (state?.companyConfidence === "device_default" &&
|
|
288
290
|
deps.authorizeDeviceDefaultTenant) {
|
|
289
291
|
const allowed = await deps.authorizeDeviceDefaultTenant(state);
|
|
@@ -646,12 +648,12 @@ const SESSION_EVENT_HARNESSES = new Set([
|
|
|
646
648
|
"agent-box",
|
|
647
649
|
"agents-v2",
|
|
648
650
|
]);
|
|
649
|
-
function
|
|
651
|
+
function resolveHeldCompany(sessionId, items, state, deps) {
|
|
650
652
|
if (state?.companyUid)
|
|
651
|
-
return
|
|
653
|
+
return null;
|
|
652
654
|
const sample = items[0]?.event;
|
|
653
655
|
if (!sample)
|
|
654
|
-
return
|
|
656
|
+
return null;
|
|
655
657
|
const cwd = eventText(sample, "cwd");
|
|
656
658
|
const hqRoot = eventText(sample, "hqRoot");
|
|
657
659
|
const remoteOwnerSlug = cwd
|
|
@@ -675,14 +677,16 @@ function bindHeldCompanyIfResolved(sessionId, items, state, deps, now) {
|
|
|
675
677
|
remoteOwnerSlug,
|
|
676
678
|
repoIdentityKey,
|
|
677
679
|
});
|
|
678
|
-
if (resolution.status !== "resolved"
|
|
679
|
-
return
|
|
680
|
+
if (resolution.status !== "resolved")
|
|
681
|
+
return null;
|
|
680
682
|
if (resolution.company.source === "trusted_explicit" &&
|
|
681
683
|
!eventText(sample, "companySlug")) {
|
|
682
|
-
return
|
|
684
|
+
return null;
|
|
683
685
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
+
return resolution.company;
|
|
687
|
+
}
|
|
688
|
+
function writeHeldCompanyBinding(sessionId, state, company, deps, now) {
|
|
689
|
+
const { uid, slug } = company;
|
|
686
690
|
const nowIso = now().toISOString();
|
|
687
691
|
const next = updateSessionState(sessionId, deps.workContextRoot, (current) => {
|
|
688
692
|
const base = current ?? state;
|
|
@@ -703,7 +707,7 @@ function bindHeldCompanyIfResolved(sessionId, items, state, deps, now) {
|
|
|
703
707
|
contractVersion: base.contractVersion ?? 1,
|
|
704
708
|
companyUid: uid,
|
|
705
709
|
...(slug ? { companySlug: slug } : {}),
|
|
706
|
-
...(
|
|
710
|
+
...(company.source === "device_default"
|
|
707
711
|
? { companyConfidence: "device_default" }
|
|
708
712
|
: {}),
|
|
709
713
|
contextStatus,
|
|
@@ -712,6 +716,45 @@ function bindHeldCompanyIfResolved(sessionId, items, state, deps, now) {
|
|
|
712
716
|
});
|
|
713
717
|
return next ?? readSessionState(sessionId, deps.workContextRoot) ?? state;
|
|
714
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* Bind a held session to a company. Local evidence with a uid binds directly.
|
|
721
|
+
* Local evidence that only names a slug (session meta, cwd, git remote) needs
|
|
722
|
+
* a membership-validated slug -> uid lookup; without it the session would stay
|
|
723
|
+
* NEEDS_COMPANY on every retry (Shepherd 2361).
|
|
724
|
+
*/
|
|
725
|
+
function bindHeldCompanyIfResolved(sessionId, items, state, deps, now, uidCache) {
|
|
726
|
+
// Stay synchronous unless a uid lookup is needed: the flush loop's
|
|
727
|
+
// microtask ordering is relied on by retry/back-pressure timing.
|
|
728
|
+
const company = resolveHeldCompany(sessionId, items, state, deps);
|
|
729
|
+
if (!company)
|
|
730
|
+
return state;
|
|
731
|
+
if (company.uid) {
|
|
732
|
+
return writeHeldCompanyBinding(sessionId, state, { uid: company.uid, slug: company.slug, source: company.source }, deps, now);
|
|
733
|
+
}
|
|
734
|
+
const slug = company.slug?.trim().toLowerCase();
|
|
735
|
+
if (!slug || !deps.resolveCompanyUid || company.source === "device_default")
|
|
736
|
+
return state;
|
|
737
|
+
let pending = uidCache.get(slug);
|
|
738
|
+
if (!pending) {
|
|
739
|
+
const resolver = deps.resolveCompanyUid;
|
|
740
|
+
pending = (async () => {
|
|
741
|
+
try {
|
|
742
|
+
const uid = await resolver(slug);
|
|
743
|
+
return typeof uid === "string" && uid.trim() ? uid.trim() : null;
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
deps.log?.(`held company uid lookup failed for slug=${slug}: ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
})();
|
|
750
|
+
uidCache.set(slug, pending);
|
|
751
|
+
}
|
|
752
|
+
return pending.then((uid) => {
|
|
753
|
+
if (!uid)
|
|
754
|
+
return state;
|
|
755
|
+
return writeHeldCompanyBinding(sessionId, state, { uid, slug: company.slug, source: company.source }, deps, now);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
715
758
|
function applySessionMetaCompanyRebind(sessionId, items, state, deps, now) {
|
|
716
759
|
if (!state)
|
|
717
760
|
return state;
|
|
@@ -90,6 +90,12 @@ export function pathKey(dir) {
|
|
|
90
90
|
export function canonicalDir(dir) {
|
|
91
91
|
const abs = path.resolve(dir);
|
|
92
92
|
try {
|
|
93
|
+
// macOS volumes are case-insensitive by default: /x/CaseRepo and
|
|
94
|
+
// /x/caserepo are one folder, but the JS realpath keeps the caller's
|
|
95
|
+
// spelling, so the same repo produced two registry keys. realpath(3)
|
|
96
|
+
// returns the on-disk spelling. Other platforms keep the JS realpath.
|
|
97
|
+
if (process.platform === "darwin")
|
|
98
|
+
return fs.realpathSync.native(abs);
|
|
93
99
|
return fs.realpathSync(abs);
|
|
94
100
|
}
|
|
95
101
|
catch {
|
|
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as yaml from 'js-yaml';
|
|
5
5
|
import { contributionLinks } from '../../utils/pack-contribution-links.js';
|
|
6
6
|
import { CONTRIBUTION_TABLE } from '../../utils/contribution-table.js';
|
|
7
|
-
import { isExcludedWorkerDirectory } from '../workers-registry/index.js';
|
|
7
|
+
import { isExcludedWorkerDirectory, isNestedWorkerCheckout } from '../workers-registry/index.js';
|
|
8
8
|
function isDirectory(target) { try {
|
|
9
9
|
return fs.statSync(target).isDirectory();
|
|
10
10
|
}
|
|
@@ -77,8 +77,9 @@ function allWorkerYamlFiles(root, relative) {
|
|
|
77
77
|
const output = [];
|
|
78
78
|
const walk = (next, seen) => {
|
|
79
79
|
// Prune exactly what the worker registry prunes (tenant repos/ checkouts,
|
|
80
|
-
// _template, _overrides). A worker.yaml the registry
|
|
81
|
-
// collide with a pack worker, and walking source
|
|
80
|
+
// nested git checkouts, _template, _overrides). A worker.yaml the registry
|
|
81
|
+
// never indexes cannot collide with a pack worker, and walking source
|
|
82
|
+
// checkouts is slow.
|
|
82
83
|
if (isExcludedWorkerDirectory(next))
|
|
83
84
|
return;
|
|
84
85
|
const absolute = path.join(root, next);
|
|
@@ -101,7 +102,10 @@ function allWorkerYamlFiles(root, relative) {
|
|
|
101
102
|
catch {
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
105
|
+
const checkout = isNestedWorkerCheckout(relative, next, names);
|
|
104
106
|
for (const name of names) {
|
|
107
|
+
if (checkout && name !== 'worker.yaml')
|
|
108
|
+
continue;
|
|
105
109
|
const child = path.join(next, name);
|
|
106
110
|
const childAbsolute = path.join(root, child);
|
|
107
111
|
if (name === 'worker.yaml' && (() => { try {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type VaultGrant = {
|
|
2
|
+
path: string;
|
|
3
|
+
permission: string;
|
|
4
|
+
};
|
|
5
|
+
export type VaultAccessCompany = {
|
|
6
|
+
role?: string;
|
|
7
|
+
enforced?: boolean;
|
|
8
|
+
grants?: VaultGrant[];
|
|
9
|
+
};
|
|
10
|
+
export type VaultAccessManifest = {
|
|
11
|
+
version?: number;
|
|
12
|
+
companies?: Record<string, VaultAccessCompany>;
|
|
13
|
+
};
|
|
14
|
+
/** bypass | unknown | admin | write | read | none */
|
|
15
|
+
export type EffectivePermission = "bypass" | "unknown" | "admin" | "write" | "read" | "none";
|
|
16
|
+
export declare function readVaultAccessManifest(hqRoot: string): VaultAccessManifest | null;
|
|
17
|
+
/** Whether the manifest entry is one the write hook enforces grants for. */
|
|
18
|
+
export declare function isGrantEnforced(company: VaultAccessCompany | undefined): boolean;
|
|
19
|
+
export declare function effectivePermission(company: VaultAccessCompany | undefined, rel: string): EffectivePermission;
|
|
20
|
+
export type ReadOnlyLocalOnlyFile = {
|
|
21
|
+
path: string;
|
|
22
|
+
permission: "read" | "none";
|
|
23
|
+
};
|
|
24
|
+
export type ReadOnlyLocalOnlyResult = {
|
|
25
|
+
files: ReadOnlyLocalOnlyFile[];
|
|
26
|
+
total: number;
|
|
27
|
+
};
|
|
28
|
+
export type FindReadOnlyLocalOnlyOptions = {
|
|
29
|
+
hqRoot: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
company: VaultAccessCompany;
|
|
32
|
+
/** Company-relative keys already in the sync journal (live or tombstoned). */
|
|
33
|
+
journalKeys: ReadonlySet<string>;
|
|
34
|
+
/** Engine ignore filter over absolute paths; true = would sync. */
|
|
35
|
+
shouldSync?: (absPath: string, isDir?: boolean) => boolean;
|
|
36
|
+
/** Max paths returned; `total` still counts every match. */
|
|
37
|
+
limit?: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Walk `companies/<slug>/` and return files that are not in the journal and
|
|
41
|
+
* whose effective permission is read or none. Returns an empty result for
|
|
42
|
+
* roles the hook does not enforce (owner/admin/unknown, enforced=false).
|
|
43
|
+
*/
|
|
44
|
+
export declare function findReadOnlyLocalOnly(opts: FindReadOnlyLocalOnlyOptions): ReadOnlyLocalOnlyResult;
|
|
45
|
+
//# sourceMappingURL=read-only-local-only.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-only files under read-only vault prefixes (feedback_510661bc, Shepherd #2813).
|
|
3
|
+
*
|
|
4
|
+
* A teammate with a read-only share can still hold local files under that
|
|
5
|
+
* prefix — for example files written before the enforce-vault-write-access
|
|
6
|
+
* hook existed. Push will never upload them, so `hq sync status` lists them.
|
|
7
|
+
*
|
|
8
|
+
* Read-only and network-free: the access data is the local manifest at
|
|
9
|
+
* `<hqRoot>/.hq/vault-access.json` (written by refresh-vault-access), and the
|
|
10
|
+
* "already synced" set is the company's sync journal the caller already has.
|
|
11
|
+
* Permission resolution mirrors `effective_permission` in
|
|
12
|
+
* `.claude/hooks/enforce-vault-write-access.sh`.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
export function readVaultAccessManifest(hqRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq", "vault-access.json"), "utf-8"));
|
|
19
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function rank(permission) {
|
|
26
|
+
if (permission === "admin")
|
|
27
|
+
return 3;
|
|
28
|
+
if (permission === "write")
|
|
29
|
+
return 2;
|
|
30
|
+
if (permission === "read")
|
|
31
|
+
return 1;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
function grantMatches(grant, rel) {
|
|
35
|
+
const p = grant.path;
|
|
36
|
+
if (typeof p !== "string")
|
|
37
|
+
return false;
|
|
38
|
+
if (p === "*" || p === rel)
|
|
39
|
+
return true;
|
|
40
|
+
if (p.endsWith("/*")) {
|
|
41
|
+
return rel.startsWith(p.slice(0, -1)) || rel === p.slice(0, -2);
|
|
42
|
+
}
|
|
43
|
+
if (p.endsWith("/") && (grant.permission === "write" || grant.permission === "admin") && rel.startsWith(p)) {
|
|
44
|
+
const child = rel.slice(p.length);
|
|
45
|
+
return child.length > 0 && !child.includes("/");
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Whether the manifest entry is one the write hook enforces grants for. */
|
|
50
|
+
export function isGrantEnforced(company) {
|
|
51
|
+
if (!company)
|
|
52
|
+
return false;
|
|
53
|
+
const role = company.role ?? "unknown";
|
|
54
|
+
if (role !== "member" && role !== "guest")
|
|
55
|
+
return false;
|
|
56
|
+
return company.enforced !== false;
|
|
57
|
+
}
|
|
58
|
+
export function effectivePermission(company, rel) {
|
|
59
|
+
if (!company)
|
|
60
|
+
return "unknown";
|
|
61
|
+
const role = company.role ?? "unknown";
|
|
62
|
+
if (role === "owner" || role === "admin")
|
|
63
|
+
return "bypass";
|
|
64
|
+
if (company.enforced === false)
|
|
65
|
+
return "bypass";
|
|
66
|
+
if (role !== "member" && role !== "guest")
|
|
67
|
+
return "unknown";
|
|
68
|
+
const matches = (company.grants ?? []).filter((g) => g && grantMatches(g, rel));
|
|
69
|
+
if (matches.length === 0)
|
|
70
|
+
return "none";
|
|
71
|
+
matches.sort((a, b) => a.path.length - b.path.length || rank(a.permission) - rank(b.permission));
|
|
72
|
+
const best = matches[matches.length - 1].permission;
|
|
73
|
+
return best === "admin" || best === "write" || best === "read" ? best : "none";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Walk `companies/<slug>/` and return files that are not in the journal and
|
|
77
|
+
* whose effective permission is read or none. Returns an empty result for
|
|
78
|
+
* roles the hook does not enforce (owner/admin/unknown, enforced=false).
|
|
79
|
+
*/
|
|
80
|
+
export function findReadOnlyLocalOnly(opts) {
|
|
81
|
+
if (!isGrantEnforced(opts.company))
|
|
82
|
+
return { files: [], total: 0 };
|
|
83
|
+
const found = [];
|
|
84
|
+
const root = path.join(opts.hqRoot, "companies", opts.slug);
|
|
85
|
+
const limit = opts.limit ?? 20;
|
|
86
|
+
const stack = [root];
|
|
87
|
+
while (stack.length > 0) {
|
|
88
|
+
const dir = stack.pop();
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const abs = path.join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
100
|
+
continue;
|
|
101
|
+
if (opts.shouldSync && !opts.shouldSync(abs, true))
|
|
102
|
+
continue;
|
|
103
|
+
stack.push(abs);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!entry.isFile())
|
|
107
|
+
continue;
|
|
108
|
+
if (opts.shouldSync && !opts.shouldSync(abs, false))
|
|
109
|
+
continue;
|
|
110
|
+
const rel = path.relative(root, abs).split(path.sep).join("/");
|
|
111
|
+
if (opts.journalKeys.has(rel))
|
|
112
|
+
continue;
|
|
113
|
+
const permission = effectivePermission(opts.company, rel);
|
|
114
|
+
if (permission !== "read" && permission !== "none")
|
|
115
|
+
continue;
|
|
116
|
+
found.push({ path: rel, permission });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
found.sort((a, b) => a.path.localeCompare(b.path));
|
|
120
|
+
return { files: found.slice(0, limit), total: found.length };
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=read-only-local-only.js.map
|
|
@@ -92,6 +92,8 @@ export declare function prepareOrganizeDecision(input: {
|
|
|
92
92
|
/** Skip network; leave unresolved without candidates. */
|
|
93
93
|
offline?: boolean;
|
|
94
94
|
}): Promise<OrganizeListResult>;
|
|
95
|
+
/** True when a local decision's server window has passed (or is unparsable). */
|
|
96
|
+
export declare function isLocalDecisionExpired(expiresAt: string | undefined, now?: Date): boolean;
|
|
95
97
|
export declare function formatOrganizeList(result: OrganizeListResult): string[];
|
|
96
98
|
/**
|
|
97
99
|
* Mark that the ask was cancelled / timed out / surface incapable.
|
|
@@ -114,8 +114,17 @@ export async function prepareOrganizeDecision(input) {
|
|
|
114
114
|
reason: state.contextStatus,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
// Ask-once: an existing decision is never replaced or re-armed
|
|
118
|
-
|
|
117
|
+
// Ask-once: an existing decision is never replaced or re-armed — unless its
|
|
118
|
+
// server decision window has expired. The server stamps a fresh window only
|
|
119
|
+
// when candidates are listed, so returning the stale decision would make
|
|
120
|
+
// every later organize fail with DECISION_EXPIRED / DECISION_WINDOW_MISSING.
|
|
121
|
+
const refreshExpired = Boolean(state.decision?.decisionId) &&
|
|
122
|
+
state.decision?.askAfter === true &&
|
|
123
|
+
isLocalDecisionExpired(state.decision?.expiresAt, nowFn()) &&
|
|
124
|
+
Boolean(state.companyUid) &&
|
|
125
|
+
!input.offline &&
|
|
126
|
+
Boolean(input.fetchCandidates);
|
|
127
|
+
if (state.decision?.decisionId && !refreshExpired) {
|
|
119
128
|
return {
|
|
120
129
|
sessionId: input.sessionId,
|
|
121
130
|
contextStatus: state.contextStatus,
|
|
@@ -124,7 +133,7 @@ export async function prepareOrganizeDecision(input) {
|
|
|
124
133
|
reason: state.decision.askAfter ? "pending_ask" : "ask_already_settled",
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
|
-
const ready = shouldAskAfter({
|
|
136
|
+
const ready = refreshExpired || shouldAskAfter({
|
|
128
137
|
toolWrites: state.toolWrites,
|
|
129
138
|
startedAt: state.startedAt,
|
|
130
139
|
hasTurn: state.hasTurn,
|
|
@@ -168,9 +177,18 @@ export async function prepareOrganizeDecision(input) {
|
|
|
168
177
|
contextStatus: next.contextStatus,
|
|
169
178
|
pending: true,
|
|
170
179
|
decision,
|
|
171
|
-
reason: "ask_prepared",
|
|
180
|
+
reason: refreshExpired ? "ask_refreshed" : "ask_prepared",
|
|
172
181
|
};
|
|
173
182
|
}
|
|
183
|
+
/** True when a local decision's server window has passed (or is unparsable). */
|
|
184
|
+
export function isLocalDecisionExpired(expiresAt, now = new Date()) {
|
|
185
|
+
if (!expiresAt)
|
|
186
|
+
return false;
|
|
187
|
+
const ms = Date.parse(expiresAt);
|
|
188
|
+
if (!Number.isFinite(ms))
|
|
189
|
+
return true;
|
|
190
|
+
return now.getTime() >= ms;
|
|
191
|
+
}
|
|
174
192
|
export function formatOrganizeList(result) {
|
|
175
193
|
const lines = [
|
|
176
194
|
`session ${result.sessionId} status=${result.contextStatus}` +
|
|
@@ -28,9 +28,20 @@ export interface GenerateWorkersRegistryResult {
|
|
|
28
28
|
* (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
|
|
29
29
|
* nested HQ root under a company project. Those copies land in the registry as
|
|
30
30
|
* `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
|
|
31
|
-
* `hq lanes create` then fails with `worker_tenancy_unresolved`.
|
|
31
|
+
* `hq lanes create` then fails with `worker_tenancy_unresolved`. HQ's own
|
|
32
|
+
* `workspace/worktrees/<name>/` is the same shape.
|
|
33
|
+
*
|
|
34
|
+
* `node_modules` and `.git` never hold workers and are large, so they are
|
|
35
|
+
* skipped wherever they appear. Any other checkout (a directory holding `.git`)
|
|
36
|
+
* is caught by the walk itself, because only the filesystem can tell.
|
|
32
37
|
*/
|
|
33
38
|
export declare function isExcludedWorkerDirectory(relativeDirectory: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* True when `relativeDirectory`, found while walking `scanRoot`, is a separate
|
|
41
|
+
* git checkout (its listing `names` holds `.git`). The caller may still take a
|
|
42
|
+
* `worker.yaml` directly inside it, but must not descend any further.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isNestedWorkerCheckout(scanRoot: string, relativeDirectory: string, names: readonly string[]): boolean;
|
|
34
45
|
/**
|
|
35
46
|
* Generate core/workers/registry.yaml from worker.yaml files. Invalid workers
|
|
36
47
|
* are quarantined but do not prevent all valid workers from being registered.
|
|
@@ -59,7 +59,13 @@ function workerYamlFiles(root, relativeRoot) {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
62
|
+
// A `.git` file (linked worktree) or folder (clone) below the scan root marks
|
|
63
|
+
// a separate checkout. Its worker.yaml files belong to that checkout, so the
|
|
64
|
+
// walk stops there. A checkout that is itself one worker keeps its own entry.
|
|
65
|
+
const isNestedCheckout = isNestedWorkerCheckout(relativeRoot, relative, entries.map((entry) => entry.name));
|
|
62
66
|
for (const entry of entries) {
|
|
67
|
+
if (isNestedCheckout && entry.name !== 'worker.yaml')
|
|
68
|
+
continue;
|
|
63
69
|
const childRelative = path.join(relative, entry.name);
|
|
64
70
|
const child = path.join(root, childRelative);
|
|
65
71
|
if (entry.name === 'worker.yaml' && (entry.isFile() || isFile(child))) {
|
|
@@ -162,12 +168,21 @@ function withoutTimestamp(content) {
|
|
|
162
168
|
* (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
|
|
163
169
|
* nested HQ root under a company project. Those copies land in the registry as
|
|
164
170
|
* `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
|
|
165
|
-
* `hq lanes create` then fails with `worker_tenancy_unresolved`.
|
|
171
|
+
* `hq lanes create` then fails with `worker_tenancy_unresolved`. HQ's own
|
|
172
|
+
* `workspace/worktrees/<name>/` is the same shape.
|
|
173
|
+
*
|
|
174
|
+
* `node_modules` and `.git` never hold workers and are large, so they are
|
|
175
|
+
* skipped wherever they appear. Any other checkout (a directory holding `.git`)
|
|
176
|
+
* is caught by the walk itself, because only the filesystem can tell.
|
|
166
177
|
*/
|
|
167
178
|
export function isExcludedWorkerDirectory(relativeDirectory) {
|
|
168
179
|
const normalized = relativeDirectory.replaceAll('\\', '/');
|
|
169
180
|
if (/(?:^|\/)\.(?:claude|codex)\/worktrees(?:\/|$)/.test(normalized))
|
|
170
181
|
return true;
|
|
182
|
+
if (/(?:^|\/)workspace\/worktrees(?:\/|$)/.test(normalized))
|
|
183
|
+
return true;
|
|
184
|
+
if (/(?:^|\/)(?:node_modules|\.git)(?:\/|$)/.test(normalized))
|
|
185
|
+
return true;
|
|
171
186
|
const [root, tenant, nested] = normalized.split('/');
|
|
172
187
|
if (root !== 'companies')
|
|
173
188
|
return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
|
|
@@ -177,6 +192,29 @@ export function isExcludedWorkerDirectory(relativeDirectory) {
|
|
|
177
192
|
return true;
|
|
178
193
|
return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
|
|
179
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Directories that may be git repositories without being a nested checkout.
|
|
197
|
+
*
|
|
198
|
+
* An operator can keep a whole tenant (`companies/<slug>`) or a tenant's
|
|
199
|
+
* `workers` folder in its own repository. Treating either as a checkout would
|
|
200
|
+
* drop every real worker in that tenant.
|
|
201
|
+
*/
|
|
202
|
+
function isCheckoutBoundaryExempt(relativeDirectory) {
|
|
203
|
+
const parts = relativeDirectory.replaceAll('\\', '/').split('/');
|
|
204
|
+
if (parts[0] !== 'companies')
|
|
205
|
+
return false;
|
|
206
|
+
return parts.length === 2 || (parts.length === 3 && parts[2] === 'workers');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* True when `relativeDirectory`, found while walking `scanRoot`, is a separate
|
|
210
|
+
* git checkout (its listing `names` holds `.git`). The caller may still take a
|
|
211
|
+
* `worker.yaml` directly inside it, but must not descend any further.
|
|
212
|
+
*/
|
|
213
|
+
export function isNestedWorkerCheckout(scanRoot, relativeDirectory, names) {
|
|
214
|
+
return relativeDirectory !== scanRoot
|
|
215
|
+
&& !isCheckoutBoundaryExempt(relativeDirectory)
|
|
216
|
+
&& names.includes('.git');
|
|
217
|
+
}
|
|
180
218
|
/**
|
|
181
219
|
* Every exclusion is a property of the directory holding the `worker.yaml`, so
|
|
182
220
|
* the file rule is the directory rule applied to its parent. Deriving it keeps
|
|
@@ -294,7 +294,10 @@ export declare function buildVaultConfig(authToken: string): VaultServiceConfig;
|
|
|
294
294
|
* means no valid session could be ensured non-interactively; the caller gets a
|
|
295
295
|
* reason string and can decide what to do next. Never opens a browser.
|
|
296
296
|
*/
|
|
297
|
-
type MachineRefreshOptions = Pick<EnsureCognitoTokenOptions, "tokenSource"> & MachineTokenValidityOptions
|
|
297
|
+
type MachineRefreshOptions = Pick<EnsureCognitoTokenOptions, "tokenSource"> & MachineTokenValidityOptions & {
|
|
298
|
+
/** Refresh the person session even when the cached token is still valid. */
|
|
299
|
+
force?: boolean;
|
|
300
|
+
};
|
|
298
301
|
export declare function refreshCachedSession(options?: MachineRefreshOptions): Promise<{
|
|
299
302
|
refreshed: boolean;
|
|
300
303
|
reason?: string;
|
|
@@ -1158,6 +1158,15 @@ export function buildVaultConfig(authToken) {
|
|
|
1158
1158
|
clientInfo: CLI_CLIENT_INFO,
|
|
1159
1159
|
};
|
|
1160
1160
|
}
|
|
1161
|
+
/** Person sessions renew below this many seconds of validity by default. */
|
|
1162
|
+
const PERSON_REFRESH_DEFAULT_SECONDS = 120;
|
|
1163
|
+
/** Person tokens last one hour; a caller minimum above this would always refresh. */
|
|
1164
|
+
const MAX_PERSON_MIN_VALIDITY_SECONDS = 1800;
|
|
1165
|
+
function personRefreshThresholdSeconds(minValiditySeconds) {
|
|
1166
|
+
if (minValiditySeconds === undefined)
|
|
1167
|
+
return PERSON_REFRESH_DEFAULT_SECONDS;
|
|
1168
|
+
return Math.max(PERSON_REFRESH_DEFAULT_SECONDS, Math.min(minValiditySeconds, MAX_PERSON_MIN_VALIDITY_SECONDS));
|
|
1169
|
+
}
|
|
1161
1170
|
export async function refreshCachedSession(options = {}) {
|
|
1162
1171
|
if (wantsMachineCognitoTokens(options))
|
|
1163
1172
|
return refreshCachedSessionUnserialized(options);
|
|
@@ -1198,7 +1207,8 @@ async function refreshCachedSessionUnserialized(options) {
|
|
|
1198
1207
|
}
|
|
1199
1208
|
return { refreshed: false, reason: "no cached session" };
|
|
1200
1209
|
}
|
|
1201
|
-
if (!
|
|
1210
|
+
if (!options.force &&
|
|
1211
|
+
!isExpiring(cached, personRefreshThresholdSeconds(options.minValiditySeconds))) {
|
|
1202
1212
|
return { refreshed: true };
|
|
1203
1213
|
}
|
|
1204
1214
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.154",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.154",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.154",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.154",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.154",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.154",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.154"
|
|
123
123
|
}
|
|
124
124
|
}
|