@indigoai-us/hq-cli 5.345.146 → 5.345.148
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 +45 -0
- package/README.md +14 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +326 -4
- package/dist/command-catalog.generated.js +403 -4
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/auth.js +4 -2
- package/dist/commands/bot.js +1 -1
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
- package/dist/commands/core-refresh-vault-access-native.js +214 -19
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +9 -2
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/files.js +10 -6
- package/dist/commands/install-global.d.ts +7 -0
- package/dist/commands/install-global.js +129 -35
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/login.js +4 -3
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +36 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/bot/daemon.js +3 -0
- package/dist/lib/bot/runtime/index.js +3 -1
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
- package/dist/lib/doctor/checks/anywhere.js +23 -8
- package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
- package/dist/lib/doctor/checks/cli-path.js +45 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +16 -0
- package/dist/lib/doctor/checks/release-links.d.ts +23 -0
- package/dist/lib/doctor/checks/release-links.js +152 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +13 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/user-path.d.ts +48 -0
- package/dist/lib/windows/user-path.js +114 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/lib/workers-registry/index.d.ts +6 -0
- package/dist/lib/workers-registry/index.js +8 -0
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/headless.d.ts +2 -0
- package/dist/utils/headless.js +8 -0
- package/dist/utils/login-browser.d.ts +4 -0
- package/dist/utils/login-browser.js +122 -0
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/dist/utils/team-upgrade.js +1 -7
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type RegRun = (cmd: string, args: string[]) => {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout: string;
|
|
4
|
+
};
|
|
5
|
+
export interface ShimPathCheck {
|
|
6
|
+
/** The folder holding `hq.cmd`. */
|
|
7
|
+
shimDir: string;
|
|
8
|
+
onPath: boolean;
|
|
9
|
+
/** Where the PATH came from: the persisted registry values, or this process's PATH when those could not be read. */
|
|
10
|
+
source: "registry" | "process";
|
|
11
|
+
}
|
|
12
|
+
export interface ShimPathDeps {
|
|
13
|
+
platform?: NodeJS.Platform;
|
|
14
|
+
/** The running CLI entry script (process.argv[1]). */
|
|
15
|
+
entry?: string;
|
|
16
|
+
exists?: (p: string) => boolean;
|
|
17
|
+
run?: RegRun;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
}
|
|
20
|
+
/** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
|
|
21
|
+
export declare function npmShimDir(entry: string | undefined, exists: (p: string) => boolean): string | null;
|
|
22
|
+
/** Expand `%NAME%` references the way Windows does, matching names case-insensitively. */
|
|
23
|
+
export declare function expandWindowsEnv(value: string, env: NodeJS.ProcessEnv): string;
|
|
24
|
+
/**
|
|
25
|
+
* The PATH a new terminal will get: the user Path then the machine Path, as
|
|
26
|
+
* stored in the registry. Falls back to this process's PATH when neither can
|
|
27
|
+
* be read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readPersistedPath(run: RegRun, env: NodeJS.ProcessEnv): {
|
|
30
|
+
value: string;
|
|
31
|
+
source: "registry" | "process";
|
|
32
|
+
};
|
|
33
|
+
export declare function isDirOnPath(dir: string, pathValue: string): boolean;
|
|
34
|
+
/** Null when not on Windows or when hq was not installed as an npm global shim. */
|
|
35
|
+
export declare function checkShimOnPath(deps?: ShimPathDeps): ShimPathCheck | null;
|
|
36
|
+
/** Dummy user variable set and cleared so Windows tells running programs that the environment changed. */
|
|
37
|
+
export declare const PATH_REFRESH_VAR = "HQ_PATH_REFRESH";
|
|
38
|
+
/**
|
|
39
|
+
* A PowerShell command that appends `dir` to the user Path, keeping the value
|
|
40
|
+
* as REG_EXPAND_SZ so existing `%VAR%` entries still expand. Writing the
|
|
41
|
+
* registry alone does not notify Explorer, so terminals it opens would keep the
|
|
42
|
+
* old PATH until the next sign-in. Setting and clearing a dummy user variable
|
|
43
|
+
* through [Environment]::SetEnvironmentVariable broadcasts WM_SETTINGCHANGE.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pathFixCommand(dir: string): string;
|
|
46
|
+
/** Lines to print when the shim folder is missing from PATH; empty when it is present. */
|
|
47
|
+
export declare function shimPathWarning(check: ShimPathCheck): string[];
|
|
48
|
+
//# sourceMappingURL=user-path.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the npm global shim folder (where `hq.cmd` lives) on the Windows PATH?
|
|
3
|
+
*
|
|
4
|
+
* npm writes `hq.cmd` directly under its prefix (never `<prefix>\bin`). When
|
|
5
|
+
* that folder is not on the user or machine Path, a new terminal cannot find
|
|
6
|
+
* `hq` even though the install succeeded. HQ detects this and prints the exact
|
|
7
|
+
* fix; it never edits PATH itself.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
const USER_ENV_KEY = "HKCU\\Environment";
|
|
13
|
+
const MACHINE_ENV_KEY = "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
|
|
14
|
+
/** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
|
|
15
|
+
export function npmShimDir(entry, exists) {
|
|
16
|
+
if (!entry)
|
|
17
|
+
return null;
|
|
18
|
+
const parts = path.win32.normalize(entry).split("\\");
|
|
19
|
+
const nm = parts.map((p) => p.toLowerCase()).lastIndexOf("node_modules");
|
|
20
|
+
if (nm <= 0)
|
|
21
|
+
return null;
|
|
22
|
+
const dir = parts.slice(0, nm).join("\\");
|
|
23
|
+
return exists(path.win32.join(dir, "hq.cmd")) ? dir : null;
|
|
24
|
+
}
|
|
25
|
+
/** Expand `%NAME%` references the way Windows does, matching names case-insensitively. */
|
|
26
|
+
export function expandWindowsEnv(value, env) {
|
|
27
|
+
return value.replace(/%([^%;]+)%/g, (whole, name) => {
|
|
28
|
+
const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
29
|
+
const found = key === undefined ? undefined : env[key];
|
|
30
|
+
return found === undefined ? whole : found;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function regPath(run, key) {
|
|
34
|
+
try {
|
|
35
|
+
const result = run("reg.exe", ["query", key, "/v", "Path"]);
|
|
36
|
+
if (result.status !== 0)
|
|
37
|
+
return null;
|
|
38
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
39
|
+
// An empty value prints no text after the type, which is an empty Path,
|
|
40
|
+
// not an unreadable one.
|
|
41
|
+
const match = /^\s+Path\s+REG_(?:EXPAND_)?SZ(?:\s+(.*))?$/i.exec(line);
|
|
42
|
+
if (match)
|
|
43
|
+
return (match[1] ?? "").trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// reg.exe missing or refused: treat as unreadable.
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The PATH a new terminal will get: the user Path then the machine Path, as
|
|
53
|
+
* stored in the registry. Falls back to this process's PATH when neither can
|
|
54
|
+
* be read.
|
|
55
|
+
*/
|
|
56
|
+
export function readPersistedPath(run, env) {
|
|
57
|
+
const values = [regPath(run, USER_ENV_KEY), regPath(run, MACHINE_ENV_KEY)].filter((v) => v !== null);
|
|
58
|
+
if (values.length === 0)
|
|
59
|
+
return { value: env.PATH ?? env.Path ?? "", source: "process" };
|
|
60
|
+
return { value: values.map((v) => expandWindowsEnv(v, env)).join(";"), source: "registry" };
|
|
61
|
+
}
|
|
62
|
+
function normalizeDir(dir) {
|
|
63
|
+
const unquoted = dir.trim().replace(/^"(.*)"$/, "$1").trim();
|
|
64
|
+
if (!unquoted)
|
|
65
|
+
return "";
|
|
66
|
+
return path.win32.normalize(unquoted).replace(/[\\/]+$/, "").toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
export function isDirOnPath(dir, pathValue) {
|
|
69
|
+
const want = normalizeDir(dir);
|
|
70
|
+
return pathValue.split(";").some((entry) => normalizeDir(entry) === want);
|
|
71
|
+
}
|
|
72
|
+
function defaultRun(cmd, args) {
|
|
73
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true, timeout: 5000 });
|
|
74
|
+
return { status: result.status, stdout: result.stdout ?? "" };
|
|
75
|
+
}
|
|
76
|
+
/** Null when not on Windows or when hq was not installed as an npm global shim. */
|
|
77
|
+
export function checkShimOnPath(deps = {}) {
|
|
78
|
+
if ((deps.platform ?? process.platform) !== "win32")
|
|
79
|
+
return null;
|
|
80
|
+
const shimDir = npmShimDir(deps.entry ?? process.argv[1], deps.exists ?? fs.existsSync);
|
|
81
|
+
if (!shimDir)
|
|
82
|
+
return null;
|
|
83
|
+
const persisted = readPersistedPath(deps.run ?? defaultRun, deps.env ?? process.env);
|
|
84
|
+
return { shimDir, onPath: isDirOnPath(shimDir, persisted.value), source: persisted.source };
|
|
85
|
+
}
|
|
86
|
+
/** Dummy user variable set and cleared so Windows tells running programs that the environment changed. */
|
|
87
|
+
export const PATH_REFRESH_VAR = "HQ_PATH_REFRESH";
|
|
88
|
+
/**
|
|
89
|
+
* A PowerShell command that appends `dir` to the user Path, keeping the value
|
|
90
|
+
* as REG_EXPAND_SZ so existing `%VAR%` entries still expand. Writing the
|
|
91
|
+
* registry alone does not notify Explorer, so terminals it opens would keep the
|
|
92
|
+
* old PATH until the next sign-in. Setting and clearing a dummy user variable
|
|
93
|
+
* through [Environment]::SetEnvironmentVariable broadcasts WM_SETTINGCHANGE.
|
|
94
|
+
*/
|
|
95
|
+
export function pathFixCommand(dir) {
|
|
96
|
+
const quoted = dir.replace(/'/g, "''");
|
|
97
|
+
return ("$p = (Get-Item HKCU:\\Environment).GetValue('Path','','DoNotExpandEnvironmentNames'); " +
|
|
98
|
+
"New-ItemProperty -Path HKCU:\\Environment -Name Path -PropertyType ExpandString -Force " +
|
|
99
|
+
`-Value (($p.TrimEnd(';') + ';${quoted}').TrimStart(';')); ` +
|
|
100
|
+
`[Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}','1','User'); ` +
|
|
101
|
+
`[Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}',$null,'User')`);
|
|
102
|
+
}
|
|
103
|
+
/** Lines to print when the shim folder is missing from PATH; empty when it is present. */
|
|
104
|
+
export function shimPathWarning(check) {
|
|
105
|
+
if (check.onPath)
|
|
106
|
+
return [];
|
|
107
|
+
return [
|
|
108
|
+
`The folder that holds the hq command (${check.shimDir}) is not on your Windows PATH, so a new terminal will not find hq.`,
|
|
109
|
+
"To add it for your user, run this in PowerShell, then open a new terminal:",
|
|
110
|
+
` ${pathFixCommand(check.shimDir)}`,
|
|
111
|
+
"HQ does not change PATH for you.",
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=user-path.js.map
|
|
@@ -32,7 +32,7 @@ export type AutoBindDecision = {
|
|
|
32
32
|
candidates: AutoBindCandidate[];
|
|
33
33
|
} | {
|
|
34
34
|
action: "skip";
|
|
35
|
-
reason: "no_git" | "no_company" | "no_match" | "ambiguous";
|
|
35
|
+
reason: "no_git" | "no_company" | "no_match" | "ambiguous" | "foreign_lane";
|
|
36
36
|
candidates: AutoBindCandidate[];
|
|
37
37
|
};
|
|
38
38
|
/** Find the git checkout for cwd and its current branch without spawning git. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { foreignSessionOwnsLane } from "./lane-bind.js";
|
|
13
14
|
function readText(file) {
|
|
14
15
|
try {
|
|
15
16
|
return fs.readFileSync(file, "utf8");
|
|
@@ -223,17 +224,35 @@ export function decideAutoBind(input) {
|
|
|
223
224
|
identities,
|
|
224
225
|
branch: git.branch,
|
|
225
226
|
});
|
|
226
|
-
|
|
227
|
+
const ownedByOtherSession = (projectId) => foreignSessionOwnsLane({
|
|
228
|
+
hqRoot: input.hqRoot,
|
|
229
|
+
projectId,
|
|
230
|
+
sessionId: state.sessionId,
|
|
231
|
+
companySlug,
|
|
232
|
+
});
|
|
233
|
+
if (prior) {
|
|
234
|
+
if (ownedByOtherSession(prior)) {
|
|
235
|
+
return { action: "skip", reason: "foreign_lane", candidates, ...extra };
|
|
236
|
+
}
|
|
227
237
|
return { action: "bind", projectId: prior, source: "history", candidates, ...extra };
|
|
238
|
+
}
|
|
228
239
|
const branchMatches = candidates.filter((c) => c.matchedBranch);
|
|
229
240
|
if (branchMatches.length === 1) {
|
|
230
|
-
|
|
241
|
+
const projectId = branchMatches[0].projectId;
|
|
242
|
+
if (ownedByOtherSession(projectId)) {
|
|
243
|
+
return { action: "skip", reason: "foreign_lane", candidates, ...extra };
|
|
244
|
+
}
|
|
245
|
+
return { action: "bind", projectId, source: "repo_branch", candidates, ...extra };
|
|
231
246
|
}
|
|
232
247
|
if (branchMatches.length > 1) {
|
|
233
248
|
return { action: "skip", reason: "ambiguous", candidates: branchMatches, ...extra };
|
|
234
249
|
}
|
|
235
250
|
if (candidates.length === 1) {
|
|
236
|
-
|
|
251
|
+
const projectId = candidates[0].projectId;
|
|
252
|
+
if (ownedByOtherSession(projectId)) {
|
|
253
|
+
return { action: "skip", reason: "foreign_lane", candidates, ...extra };
|
|
254
|
+
}
|
|
255
|
+
return { action: "bind", projectId, source: "repo_only", candidates, ...extra };
|
|
237
256
|
}
|
|
238
257
|
return {
|
|
239
258
|
action: "skip",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane ownership for automatic project attachment.
|
|
3
|
+
*
|
|
4
|
+
* A session may attach to a project when it is the senior, the provider
|
|
5
|
+
* session, or the creator of an active lane for that project. It must not
|
|
6
|
+
* attach just because a lane exists, the cwd matches, or a binding is recent.
|
|
7
|
+
* Terminal lanes do not count. A missing lane directory does not block.
|
|
8
|
+
*/
|
|
9
|
+
export interface ForeignLaneInput {
|
|
10
|
+
hqRoot?: string;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Company the bind is for. A lane matches only when its company slug
|
|
15
|
+
* matches too. Missing company, or a lane with no company, does not match.
|
|
16
|
+
*/
|
|
17
|
+
companySlug?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Lane records already read during this reconcile, keyed by lane directory.
|
|
20
|
+
* Pass the same map to every check in one reconcile so the lane files are
|
|
21
|
+
* read once.
|
|
22
|
+
*/
|
|
23
|
+
laneCache?: LaneRecordCache;
|
|
24
|
+
}
|
|
25
|
+
export type LaneRecordCache = Map<string, Record<string, unknown>[] | null>;
|
|
26
|
+
/**
|
|
27
|
+
* True when an active lane for this company and project is owned by a
|
|
28
|
+
* different session and this session does not own or create one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function foreignSessionOwnsLane(input: ForeignLaneInput): boolean;
|
|
31
|
+
//# sourceMappingURL=lane-bind.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane ownership for automatic project attachment.
|
|
3
|
+
*
|
|
4
|
+
* A session may attach to a project when it is the senior, the provider
|
|
5
|
+
* session, or the creator of an active lane for that project. It must not
|
|
6
|
+
* attach just because a lane exists, the cwd matches, or a binding is recent.
|
|
7
|
+
* Terminal lanes do not count. A missing lane directory does not block.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { TERMINAL_LANE_STATES } from "../lanes/types.js";
|
|
12
|
+
const TERMINAL = new Set(TERMINAL_LANE_STATES);
|
|
13
|
+
function asRecord(value) {
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
function sessionOwnsLane(lane, sessionId) {
|
|
19
|
+
const senior = asRecord(lane.senior);
|
|
20
|
+
if (senior?.kind === "session" && senior.id === sessionId)
|
|
21
|
+
return true;
|
|
22
|
+
if (lane.provider_session_id === sessionId)
|
|
23
|
+
return true;
|
|
24
|
+
if (lane.created_by_session === sessionId)
|
|
25
|
+
return true;
|
|
26
|
+
if (lane.created_by === sessionId)
|
|
27
|
+
return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function foreignSessionSenior(lane, sessionId) {
|
|
31
|
+
const senior = asRecord(lane.senior);
|
|
32
|
+
return senior?.kind === "session"
|
|
33
|
+
&& typeof senior.id === "string"
|
|
34
|
+
&& senior.id.length > 0
|
|
35
|
+
&& senior.id !== sessionId;
|
|
36
|
+
}
|
|
37
|
+
/** Personal lanes and lanes with no company do not match a company-scoped bind. */
|
|
38
|
+
function laneCompanyMatches(lane, companySlug) {
|
|
39
|
+
const company = asRecord(lane.company);
|
|
40
|
+
const slug = company && typeof company.slug === "string" ? company.slug.trim() : "";
|
|
41
|
+
if (!slug)
|
|
42
|
+
return false;
|
|
43
|
+
return slug.toLowerCase() === companySlug.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True when an active lane for this company and project is owned by a
|
|
47
|
+
* different session and this session does not own or create one.
|
|
48
|
+
*/
|
|
49
|
+
export function foreignSessionOwnsLane(input) {
|
|
50
|
+
const hqRoot = input.hqRoot?.trim();
|
|
51
|
+
const projectId = input.projectId?.trim();
|
|
52
|
+
const sessionId = input.sessionId?.trim();
|
|
53
|
+
const companySlug = input.companySlug?.trim();
|
|
54
|
+
if (!hqRoot || !projectId || !sessionId || !companySlug)
|
|
55
|
+
return false;
|
|
56
|
+
const dir = path.join(hqRoot, "workspace", "lanes", "lanes");
|
|
57
|
+
const lanes = readLaneRecords(dir, input.laneCache);
|
|
58
|
+
if (!lanes)
|
|
59
|
+
return false;
|
|
60
|
+
let foreign = false;
|
|
61
|
+
let mine = false;
|
|
62
|
+
for (const parsed of lanes) {
|
|
63
|
+
if (parsed.project_id !== projectId)
|
|
64
|
+
continue;
|
|
65
|
+
if (!laneCompanyMatches(parsed, companySlug))
|
|
66
|
+
continue;
|
|
67
|
+
if (typeof parsed.state === "string" && TERMINAL.has(parsed.state))
|
|
68
|
+
continue;
|
|
69
|
+
if (sessionOwnsLane(parsed, sessionId))
|
|
70
|
+
mine = true;
|
|
71
|
+
else if (foreignSessionSenior(parsed, sessionId))
|
|
72
|
+
foreign = true;
|
|
73
|
+
}
|
|
74
|
+
return foreign && !mine;
|
|
75
|
+
}
|
|
76
|
+
function readLaneRecords(dir, cache) {
|
|
77
|
+
if (cache?.has(dir))
|
|
78
|
+
return cache.get(dir) ?? null;
|
|
79
|
+
let records;
|
|
80
|
+
try {
|
|
81
|
+
records = [];
|
|
82
|
+
for (const name of fs.readdirSync(dir)) {
|
|
83
|
+
if (!name.endsWith(".json"))
|
|
84
|
+
continue;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = asRecord(JSON.parse(fs.readFileSync(path.join(dir, name), "utf8")));
|
|
87
|
+
if (parsed)
|
|
88
|
+
records.push(parsed);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// An unreadable lane file does not block.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
records = null;
|
|
97
|
+
}
|
|
98
|
+
cache?.set(dir, records);
|
|
99
|
+
return records;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=lane-bind.js.map
|
|
@@ -451,6 +451,7 @@ export async function submitOrganizeDecision(input) {
|
|
|
451
451
|
projectId: receipt.projectId,
|
|
452
452
|
...(receipt.taskId ? { taskId: receipt.taskId } : {}),
|
|
453
453
|
bindingEpisodeId: receipt.bindingEpisodeId,
|
|
454
|
+
projectBindAuthority: "trusted",
|
|
454
455
|
updatedAt: now,
|
|
455
456
|
// Clear ask — decision resolved.
|
|
456
457
|
decision: undefined,
|
|
@@ -56,6 +56,9 @@ export function rebindSessionToProject(input) {
|
|
|
56
56
|
return null;
|
|
57
57
|
if (current.projectId === input.projectId)
|
|
58
58
|
return current;
|
|
59
|
+
// An explicit trusted bind stays until the session rebinds explicitly.
|
|
60
|
+
if (current.projectBindAuthority === "trusted")
|
|
61
|
+
return current;
|
|
59
62
|
return {
|
|
60
63
|
...current,
|
|
61
64
|
projectId: input.projectId,
|
|
@@ -64,6 +67,7 @@ export function rebindSessionToProject(input) {
|
|
|
64
67
|
? "needs_task"
|
|
65
68
|
: current.contextStatus,
|
|
66
69
|
projectBindReason: `prd-sync project switch to ${input.projectId}`,
|
|
70
|
+
projectBindAuthority: undefined,
|
|
67
71
|
updatedAt: nowIso,
|
|
68
72
|
};
|
|
69
73
|
});
|
|
@@ -117,6 +121,9 @@ export function planPrdSync(input) {
|
|
|
117
121
|
return empty("different_project");
|
|
118
122
|
}
|
|
119
123
|
const targetProjectId = identity.projectId;
|
|
124
|
+
if (state.projectBindAuthority === "trusted" && state.projectId !== targetProjectId) {
|
|
125
|
+
return empty("different_project");
|
|
126
|
+
}
|
|
120
127
|
const rebindTo = state.projectId !== targetProjectId ? targetProjectId : undefined;
|
|
121
128
|
let stories;
|
|
122
129
|
try {
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
* Project/task resolution for work-context (US-007B).
|
|
3
3
|
*
|
|
4
4
|
* Order:
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 1. explicit trusted bind (observation authority "trusted"; shell alias
|
|
6
|
+
* `project` / `task`). Beats an existing automatic binding and is not
|
|
7
|
+
* refused because another session owns a lane. Refused when the project
|
|
8
|
+
* directory is not under the resolved company.
|
|
9
|
+
* 2. existing active binding (state bound / needs_task with projectId).
|
|
10
|
+
* Ambient HQ_SPAWN_PROJECT and meta.yaml do not override it.
|
|
11
|
+
* 3. trusted explicit (HQ_SPAWN_PROJECT/HQ_SPAWN_TASK, observation projectId,
|
|
12
|
+
* meta.yaml). Meta and the bare `project` alias are refused when another
|
|
13
|
+
* session owns an active lane for that project. Spawn is not.
|
|
14
|
+
* 4. exactly one deterministic mapping (cwd or registry), refused when
|
|
15
|
+
* another session owns an active lane for that project.
|
|
9
16
|
* Soft evidence never auto-binds — candidates are server-owned (GET .../candidates).
|
|
10
17
|
*/
|
|
11
18
|
import { type TrustedExplicitContext } from "./contract.js";
|
|
19
|
+
import { type LaneRecordCache } from "./lane-bind.js";
|
|
12
20
|
import type { SessionStateFile } from "./state.js";
|
|
13
21
|
export type ProjectResolutionSource = "existing_binding" | "trusted_explicit" | "deterministic_cwd" | "deterministic_registry";
|
|
14
22
|
export interface ResolvedProjectTask {
|
|
@@ -17,16 +25,25 @@ export interface ResolvedProjectTask {
|
|
|
17
25
|
source: ProjectResolutionSource;
|
|
18
26
|
/** True when project+task are both present (trusted or existing bound). */
|
|
19
27
|
complete: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Set when the operator or trusted-bind command explicitly chose this
|
|
30
|
+
* project. Later automatic sources must not replace it.
|
|
31
|
+
*/
|
|
32
|
+
authority?: "trusted";
|
|
20
33
|
}
|
|
34
|
+
type TrustedProjectInput = TrustedExplicitContext & {
|
|
35
|
+
companySlug?: string;
|
|
36
|
+
/** Shell and native trusted bind write `project`, not `projectId`. */
|
|
37
|
+
project?: string;
|
|
38
|
+
/** Shell and native trusted bind write `task`, not `taskId`. */
|
|
39
|
+
task?: string;
|
|
40
|
+
/** "trusted" is an explicit bind and beats an existing automatic binding. */
|
|
41
|
+
authority?: string;
|
|
42
|
+
};
|
|
21
43
|
export interface ProjectResolveInput {
|
|
22
44
|
sessionId: string;
|
|
23
45
|
env?: NodeJS.ProcessEnv;
|
|
24
|
-
trusted?:
|
|
25
|
-
companySlug?: string;
|
|
26
|
-
projectId?: string;
|
|
27
|
-
taskId?: string;
|
|
28
|
-
storyId?: string;
|
|
29
|
-
};
|
|
46
|
+
trusted?: TrustedProjectInput;
|
|
30
47
|
cwd?: string;
|
|
31
48
|
hqRoot?: string;
|
|
32
49
|
existingState?: SessionStateFile | null;
|
|
@@ -35,6 +52,14 @@ export interface ProjectResolveInput {
|
|
|
35
52
|
metaTaskId?: string | null;
|
|
36
53
|
/** Injectable registry project id (tests). */
|
|
37
54
|
registryProjectId?: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Company reconcile already resolved. A trusted project is pinned only when
|
|
57
|
+
* that company's project directory exists. Missing company or root refuses
|
|
58
|
+
* the pin.
|
|
59
|
+
*/
|
|
60
|
+
resolvedCompanySlug?: string;
|
|
61
|
+
/** Lane records shared across the lane checks of one reconcile. */
|
|
62
|
+
laneCache?: LaneRecordCache;
|
|
38
63
|
}
|
|
39
64
|
/**
|
|
40
65
|
* Read project / task from workspace/sessions/<sid>/meta.yaml (best-effort).
|
|
@@ -75,4 +100,5 @@ export declare function shouldAskAfter(input: {
|
|
|
75
100
|
hasTurn?: boolean;
|
|
76
101
|
now?: Date;
|
|
77
102
|
}): boolean;
|
|
103
|
+
export {};
|
|
78
104
|
//# sourceMappingURL=project.d.ts.map
|