@indigoai-us/hq-cli 5.345.147 → 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 +30 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +297 -3
- package/dist/command-catalog.generated.js +368 -3
- package/dist/commands/agent-inbox.js +2 -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/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/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 +2783 -2354
- 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/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/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 +10 -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/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/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/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -2,16 +2,24 @@
|
|
|
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 * as fs from "node:fs";
|
|
12
19
|
import * as path from "node:path";
|
|
13
20
|
import { projectIdFromCwd } from "./company.js";
|
|
14
21
|
import { normalizeTaskId } from "./contract.js";
|
|
22
|
+
import { foreignSessionOwnsLane } from "./lane-bind.js";
|
|
15
23
|
import { findEnclosingGitDir, normalizeRemoteOwnerName, readOriginRemoteUrl, } from "./repo-remote.js";
|
|
16
24
|
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
17
25
|
function normalizeSlug(value) {
|
|
@@ -120,6 +128,55 @@ export function projectIdFromRepoRegistry(input) {
|
|
|
120
128
|
return [...matches][0];
|
|
121
129
|
return null;
|
|
122
130
|
}
|
|
131
|
+
function readResolveMeta(input) {
|
|
132
|
+
if (input.metaProjectId !== undefined || input.metaTaskId !== undefined) {
|
|
133
|
+
return {
|
|
134
|
+
projectId: normalizeSlug(input.metaProjectId) ?? undefined,
|
|
135
|
+
taskId: normalizeSlug(input.metaTaskId) ?? undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return readMetaProjectTask(input.sessionId, input.hqRoot);
|
|
139
|
+
}
|
|
140
|
+
function trustedTaskFrom(trusted, spawnTask) {
|
|
141
|
+
if (trusted?.taskId || trusted?.storyId) {
|
|
142
|
+
const norm = normalizeTaskId({
|
|
143
|
+
taskId: trusted.taskId,
|
|
144
|
+
storyId: trusted.storyId,
|
|
145
|
+
});
|
|
146
|
+
if (norm.ok)
|
|
147
|
+
return norm.taskId;
|
|
148
|
+
}
|
|
149
|
+
// Spawn wins over the bare `task` alias, matching the pre-authority order.
|
|
150
|
+
return spawnTask ?? normalizeSlug(trusted?.task);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A trusted project belongs to the resolved company only when that company's
|
|
154
|
+
* project directory exists. Missing company or root fails closed.
|
|
155
|
+
*/
|
|
156
|
+
function trustedProjectInResolvedCompany(input, projectId) {
|
|
157
|
+
const companySlug = input.resolvedCompanySlug?.trim();
|
|
158
|
+
const hqRoot = input.hqRoot?.trim();
|
|
159
|
+
if (!companySlug || !hqRoot)
|
|
160
|
+
return false;
|
|
161
|
+
try {
|
|
162
|
+
return fs.statSync(path.join(hqRoot, "companies", companySlug, "projects", projectId)).isDirectory();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Meta, cwd, and registry must not attach a project another session owns. */
|
|
169
|
+
function automaticProjectBlocked(input, projectId) {
|
|
170
|
+
const companySlug = input.resolvedCompanySlug?.trim()
|
|
171
|
+
|| input.existingState?.companySlug?.trim();
|
|
172
|
+
return foreignSessionOwnsLane({
|
|
173
|
+
hqRoot: input.hqRoot,
|
|
174
|
+
projectId,
|
|
175
|
+
sessionId: input.sessionId,
|
|
176
|
+
companySlug,
|
|
177
|
+
laneCache: input.laneCache,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
123
180
|
/**
|
|
124
181
|
* Resolve project and task. Never invents an adhoc target. Soft evidence is
|
|
125
182
|
* out of scope here (server candidates only).
|
|
@@ -127,7 +184,30 @@ export function projectIdFromRepoRegistry(input) {
|
|
|
127
184
|
export function resolveProjectTask(input) {
|
|
128
185
|
const env = input.env ?? process.env;
|
|
129
186
|
const state = input.existingState ?? null;
|
|
130
|
-
|
|
187
|
+
const trusted = input.trusted;
|
|
188
|
+
const spawnTask = normalizeSlug(env.HQ_SPAWN_TASK);
|
|
189
|
+
// 1. Explicit trusted bind. The shell writes `project`/`task` plus
|
|
190
|
+
// authority "trusted". That beats an existing automatic binding. Do not
|
|
191
|
+
// keep the previous project's task when the project changes.
|
|
192
|
+
const explicitTrusted = trusted?.authority === "trusted"
|
|
193
|
+
? normalizeSlug(trusted.projectId ?? trusted.project)
|
|
194
|
+
: undefined;
|
|
195
|
+
if (explicitTrusted && trustedProjectInResolvedCompany(input, explicitTrusted)) {
|
|
196
|
+
const meta = readResolveMeta(input);
|
|
197
|
+
let taskId = trustedTaskFrom(trusted, undefined);
|
|
198
|
+
if (!taskId && state?.projectId === explicitTrusted)
|
|
199
|
+
taskId = state.taskId;
|
|
200
|
+
if (!taskId && meta.projectId === explicitTrusted)
|
|
201
|
+
taskId = meta.taskId;
|
|
202
|
+
return {
|
|
203
|
+
projectId: explicitTrusted,
|
|
204
|
+
taskId,
|
|
205
|
+
source: "trusted_explicit",
|
|
206
|
+
complete: Boolean(explicitTrusted && taskId),
|
|
207
|
+
authority: "trusted",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// 2. Existing active binding. Ambient spawn and meta do not override it.
|
|
131
211
|
if (state) {
|
|
132
212
|
const boundStatuses = new Set(["bound", "needs_task", "needs_project"]);
|
|
133
213
|
if (boundStatuses.has(state.contextStatus) &&
|
|
@@ -154,40 +234,49 @@ export function resolveProjectTask(input) {
|
|
|
154
234
|
};
|
|
155
235
|
}
|
|
156
236
|
}
|
|
157
|
-
//
|
|
237
|
+
// 3. Trusted explicit: spawn + canonical observation projectId + meta.
|
|
238
|
+
// The bare `project` alias without authority is meta evidence.
|
|
158
239
|
const spawnProject = normalizeSlug(env.HQ_SPAWN_PROJECT);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
240
|
+
// A refused trusted project must not come back through the same observation's
|
|
241
|
+
// project alias or canonical projectId at a lower tier.
|
|
242
|
+
const canonicalTrustedProject = explicitTrusted
|
|
243
|
+
? undefined
|
|
244
|
+
: normalizeSlug(trusted?.projectId);
|
|
245
|
+
const aliasProject = explicitTrusted
|
|
246
|
+
? undefined
|
|
247
|
+
: normalizeSlug(trusted?.project);
|
|
248
|
+
// The refused observation's task is dropped with its project.
|
|
249
|
+
const trustedTask = explicitTrusted
|
|
250
|
+
? spawnTask
|
|
251
|
+
: trustedTaskFrom(trusted, spawnTask);
|
|
252
|
+
const resolvedMeta = readResolveMeta(input);
|
|
253
|
+
// The bind script writes meta before reconcile, so a refused trusted bind
|
|
254
|
+
// also left its project in meta. When a trusted bind was refused, meta must
|
|
255
|
+
// pass the same company check before it can attach.
|
|
256
|
+
const meta = explicitTrusted
|
|
257
|
+
&& resolvedMeta.projectId
|
|
258
|
+
&& !trustedProjectInResolvedCompany(input, resolvedMeta.projectId)
|
|
259
|
+
? {}
|
|
260
|
+
: resolvedMeta;
|
|
261
|
+
const explicitProject = canonicalTrustedProject ?? spawnProject;
|
|
262
|
+
if (explicitProject) {
|
|
263
|
+
const taskId = trustedTask ?? meta.taskId;
|
|
264
|
+
return {
|
|
265
|
+
projectId: explicitProject,
|
|
266
|
+
taskId,
|
|
267
|
+
source: "trusted_explicit",
|
|
268
|
+
complete: Boolean(explicitProject && taskId),
|
|
269
|
+
};
|
|
172
270
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Prefer spawn/trusted over meta; already merged above.
|
|
183
|
-
if (explicitProject) {
|
|
184
|
-
return {
|
|
185
|
-
projectId: explicitProject,
|
|
186
|
-
taskId: explicitTask,
|
|
187
|
-
source: "trusted_explicit",
|
|
188
|
-
complete: Boolean(explicitProject && explicitTask),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
271
|
+
const metaProject = meta.projectId ?? aliasProject;
|
|
272
|
+
if (metaProject && !automaticProjectBlocked(input, metaProject)) {
|
|
273
|
+
const taskId = trustedTask ?? meta.taskId;
|
|
274
|
+
return {
|
|
275
|
+
projectId: metaProject,
|
|
276
|
+
taskId,
|
|
277
|
+
source: "trusted_explicit",
|
|
278
|
+
complete: Boolean(metaProject && taskId),
|
|
279
|
+
};
|
|
191
280
|
}
|
|
192
281
|
// Preserve existing project from needs_task without task (not a full bind).
|
|
193
282
|
if (state?.projectId && state.contextStatus === "needs_task") {
|
|
@@ -198,9 +287,9 @@ export function resolveProjectTask(input) {
|
|
|
198
287
|
complete: Boolean(state.taskId),
|
|
199
288
|
};
|
|
200
289
|
}
|
|
201
|
-
//
|
|
290
|
+
// 4. Exactly one deterministic mapping, unless another session owns it.
|
|
202
291
|
const fromCwd = projectIdFromCwd(input.cwd, input.hqRoot);
|
|
203
|
-
if (fromCwd) {
|
|
292
|
+
if (fromCwd && !automaticProjectBlocked(input, fromCwd)) {
|
|
204
293
|
return {
|
|
205
294
|
projectId: fromCwd,
|
|
206
295
|
source: "deterministic_cwd",
|
|
@@ -213,10 +302,10 @@ export function resolveProjectTask(input) {
|
|
|
213
302
|
cwd: input.cwd,
|
|
214
303
|
hqRoot: input.hqRoot,
|
|
215
304
|
companySlug: state?.companySlug ??
|
|
216
|
-
|
|
305
|
+
trusted?.companySlug ??
|
|
217
306
|
normalizeSlug(env.HQ_SPAWN_COMPANY),
|
|
218
307
|
});
|
|
219
|
-
if (fromRegistry) {
|
|
308
|
+
if (fromRegistry && !automaticProjectBlocked(input, fromRegistry)) {
|
|
220
309
|
return {
|
|
221
310
|
projectId: fromRegistry,
|
|
222
311
|
source: "deterministic_registry",
|
|
@@ -18,10 +18,16 @@ export interface ReconcileObservation {
|
|
|
18
18
|
clientOperationId: string;
|
|
19
19
|
explicit?: TrustedExplicitContext & {
|
|
20
20
|
companySlug?: string;
|
|
21
|
+
project?: string;
|
|
22
|
+
task?: string;
|
|
23
|
+
authority?: string;
|
|
21
24
|
};
|
|
22
25
|
/** Alias for explicit (dispatch envelope). */
|
|
23
26
|
trustedContext?: TrustedExplicitContext & {
|
|
24
27
|
companySlug?: string;
|
|
28
|
+
project?: string;
|
|
29
|
+
task?: string;
|
|
30
|
+
authority?: string;
|
|
25
31
|
};
|
|
26
32
|
untrackedOrigin?: "user";
|
|
27
33
|
/** Structured untracked choice; only origin "user" is accepted. */
|
|
@@ -11,6 +11,7 @@ import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
|
|
|
11
11
|
import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
|
|
12
12
|
import { decisionFromCandidates, } from "./organize.js";
|
|
13
13
|
import { refineProjectBinding } from "./bind-best.js";
|
|
14
|
+
import { foreignSessionOwnsLane } from "./lane-bind.js";
|
|
14
15
|
import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
|
|
15
16
|
import { resolveProjectTask, shouldAskAfter } from "./project.js";
|
|
16
17
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
|
|
@@ -502,6 +503,8 @@ export async function reconcileObservation(obs, deps) {
|
|
|
502
503
|
contextStatus: "needs_project",
|
|
503
504
|
}
|
|
504
505
|
: prior;
|
|
506
|
+
// Every lane check in this reconcile reads the lane files once.
|
|
507
|
+
const laneCache = new Map();
|
|
505
508
|
let projectResolved = resolveProjectTask({
|
|
506
509
|
sessionId,
|
|
507
510
|
env,
|
|
@@ -509,6 +512,8 @@ export async function reconcileObservation(obs, deps) {
|
|
|
509
512
|
cwd,
|
|
510
513
|
hqRoot,
|
|
511
514
|
existingState: projectPrior,
|
|
515
|
+
resolvedCompanySlug: resolvedCompany.slug,
|
|
516
|
+
laneCache,
|
|
512
517
|
});
|
|
513
518
|
// Deterministic cwd projects belong to the cwd company. A higher-precedence
|
|
514
519
|
// identity UID must not adopt a project from an unrelated/unverified slug.
|
|
@@ -525,6 +530,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
525
530
|
let projectGate;
|
|
526
531
|
if (projectResolved &&
|
|
527
532
|
projectId &&
|
|
533
|
+
projectResolved.authority !== "trusted" &&
|
|
528
534
|
projectResolved.source !== "existing_binding" &&
|
|
529
535
|
obs.source !== "transcript" &&
|
|
530
536
|
hqRoot &&
|
|
@@ -539,11 +545,23 @@ export async function reconcileObservation(obs, deps) {
|
|
|
539
545
|
nowMs: nowFn().getTime(),
|
|
540
546
|
});
|
|
541
547
|
if (refined.action === "bind") {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
548
|
+
const switching = refined.projectId !== projectId;
|
|
549
|
+
const foreignSwitch = switching && foreignSessionOwnsLane({
|
|
550
|
+
hqRoot,
|
|
551
|
+
projectId: refined.projectId,
|
|
552
|
+
sessionId,
|
|
553
|
+
companySlug: resolvedCompany.slug,
|
|
554
|
+
laneCache,
|
|
555
|
+
});
|
|
556
|
+
// Confirming the already-allowed hint is fine. Do not follow a refine
|
|
557
|
+
// switch onto a lane another session owns.
|
|
558
|
+
if (!foreignSwitch) {
|
|
559
|
+
if (switching)
|
|
560
|
+
taskId = undefined;
|
|
561
|
+
projectId = refined.projectId;
|
|
562
|
+
projectBindReason = refined.reason;
|
|
563
|
+
diagnostics.push(`project bind: ${refined.reason}`);
|
|
564
|
+
}
|
|
547
565
|
}
|
|
548
566
|
else if (refined.action === "create") {
|
|
549
567
|
projectBindReason = refined.reason;
|
|
@@ -850,6 +868,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
850
868
|
...(decision ? { decision } : {}),
|
|
851
869
|
...(companyConfidence ? { companyConfidence } : {}),
|
|
852
870
|
...(presenceLeave ? { presenceLeave } : {}),
|
|
871
|
+
...(projectResolved?.authority === "trusted" ? { projectBindAuthority: "trusted" } : {}),
|
|
853
872
|
...bindStateFields,
|
|
854
873
|
...transcriptFields,
|
|
855
874
|
}, prior, companyRebind
|
|
@@ -936,6 +955,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
936
955
|
startedAt,
|
|
937
956
|
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
938
957
|
...(hasTurn ? { hasTurn: true } : {}),
|
|
958
|
+
...(projectResolved?.authority === "trusted" ? { projectBindAuthority: "trusted" } : {}),
|
|
939
959
|
...bindStateFields,
|
|
940
960
|
...transcriptFields,
|
|
941
961
|
},
|
|
@@ -93,6 +93,12 @@ export interface SessionStateFile {
|
|
|
93
93
|
companyConfidence?: "device_default";
|
|
94
94
|
/** Why the current project was chosen, or why the session is asking. */
|
|
95
95
|
projectBindReason?: string;
|
|
96
|
+
/**
|
|
97
|
+
* LOCAL-ONLY. "trusted" means an explicit trusted bind, operator
|
|
98
|
+
* bind-project, or organize receipt chose this project. Automatic rebind
|
|
99
|
+
* must not replace it until the next explicit choice.
|
|
100
|
+
*/
|
|
101
|
+
projectBindAuthority?: "trusted";
|
|
96
102
|
/** Ranked alternatives when the session is waiting for a project choice. */
|
|
97
103
|
projectCandidates?: Array<{
|
|
98
104
|
projectId: string;
|
|
@@ -207,6 +207,8 @@ function projectSessionState(state) {
|
|
|
207
207
|
out.companyConfidence = state.companyConfidence;
|
|
208
208
|
if (state.projectBindReason)
|
|
209
209
|
out.projectBindReason = state.projectBindReason;
|
|
210
|
+
if (state.projectBindAuthority === "trusted")
|
|
211
|
+
out.projectBindAuthority = "trusted";
|
|
210
212
|
if (state.projectCandidates?.length)
|
|
211
213
|
out.projectCandidates = state.projectCandidates;
|
|
212
214
|
if (state.projectGate)
|
|
@@ -299,6 +301,18 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
|
|
|
299
301
|
sameBinding) {
|
|
300
302
|
out.bindingEpisodeId = prior.bindingEpisodeId;
|
|
301
303
|
}
|
|
304
|
+
// A later hook that rewrites the same project in the same company must not
|
|
305
|
+
// drop an explicit trusted lock. A company change clears it even when the
|
|
306
|
+
// project slug matches. A different project keeps the lock only when the
|
|
307
|
+
// caller sets it again. A hook that omits company still counts as the same
|
|
308
|
+
// company, because preserveCompanyBinding copied the prior company above.
|
|
309
|
+
if (sameCompanyBinding &&
|
|
310
|
+
out.projectBindAuthority !== "trusted" &&
|
|
311
|
+
prior.projectBindAuthority === "trusted" &&
|
|
312
|
+
prior.projectId &&
|
|
313
|
+
out.projectId === prior.projectId) {
|
|
314
|
+
out.projectBindAuthority = "trusted";
|
|
315
|
+
}
|
|
302
316
|
return out;
|
|
303
317
|
}
|
|
304
318
|
function acknowledgedStatus(binding) {
|
|
@@ -397,7 +411,20 @@ export function writeAcknowledgedRegisterBinding(binding, root, now = () => new
|
|
|
397
411
|
}
|
|
398
412
|
const acknowledgementStrength = acknowledged === "bound" ? 3 : acknowledged === "needs_task" ? 2 : 1;
|
|
399
413
|
const priorStrength = bindingStrength(prior);
|
|
400
|
-
|
|
414
|
+
// Acked register ops are never pruned, and flush replays them on every
|
|
415
|
+
// cycle. An older ack for another project in the same company must not
|
|
416
|
+
// undo an explicit trusted bind, whatever its strength.
|
|
417
|
+
const sameCompanyAsPrior = prior.companyUid === binding.companyUid ||
|
|
418
|
+
Boolean(!prior.companyUid &&
|
|
419
|
+
prior.companySlug &&
|
|
420
|
+
binding.companySlug &&
|
|
421
|
+
prior.companySlug.toLowerCase() === binding.companySlug.toLowerCase());
|
|
422
|
+
const keepTrustedPrior = Boolean(sameCompanyAsPrior &&
|
|
423
|
+
prior.projectBindAuthority === "trusted" &&
|
|
424
|
+
prior.projectId &&
|
|
425
|
+
binding.projectId !== prior.projectId);
|
|
426
|
+
const preservePriorBinding = Boolean(keepTrustedPrior ||
|
|
427
|
+
(prior.companyUid === binding.companyUid && priorStrength > acknowledgementStrength));
|
|
401
428
|
const sameBindingAsPrior = prior.companyUid === binding.companyUid &&
|
|
402
429
|
prior.projectId === binding.projectId &&
|
|
403
430
|
prior.taskId === binding.taskId;
|
|
@@ -474,6 +501,7 @@ export function applyOperatorProjectBinding(state, projectId, now = () => new Da
|
|
|
474
501
|
projectGate: undefined,
|
|
475
502
|
projectCandidates: undefined,
|
|
476
503
|
projectBindReason: `operator selected ${projectId}`,
|
|
504
|
+
projectBindAuthority: "trusted",
|
|
477
505
|
updatedAt: now().toISOString(),
|
|
478
506
|
};
|
|
479
507
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copy-tree: portable replacement for the `rsync -a` copies on the pack
|
|
3
|
+
* install path.
|
|
4
|
+
*
|
|
5
|
+
* Pack installs used to shell out to `rsync`, which Windows does not ship, so
|
|
6
|
+
* every git-subpath or local-directory install failed there with
|
|
7
|
+
* `spawnSync rsync ENOENT`. This module does the same copy in Node on every
|
|
8
|
+
* platform:
|
|
9
|
+
*
|
|
10
|
+
* - `copyTree(src, dest)` copies the CONTENTS of `src` into `dest` (the
|
|
11
|
+
* `rsync -a src/ dest/` trailing-slash form). Directories recurse, regular
|
|
12
|
+
* files keep their permission bits and mtimes, and symlinks are recreated
|
|
13
|
+
* as symlinks with the identical target string. Links are never followed.
|
|
14
|
+
* Optional `exclude` names match an entry's basename at any depth, like
|
|
15
|
+
* `rsync --exclude=<name>`. Sockets, FIFOs and devices are skipped with a
|
|
16
|
+
* warning, as a non-root `rsync -a` does.
|
|
17
|
+
* - `removeTreeNoFollow(target)` deletes a tree without ever following a
|
|
18
|
+
* symlink or junction inside it: each entry is lstat'd, links are removed
|
|
19
|
+
* as links (unlink, or a non-recursive rmdir for a Windows junction), and
|
|
20
|
+
* directories are emptied then removed with a non-recursive rmdir. A
|
|
21
|
+
* Windows directory that is a reparse point but not a link (a volume mount
|
|
22
|
+
* point, a cloud-synced placeholder folder) stops the removal with
|
|
23
|
+
* `ReparsePointError`; its contents are never deleted.
|
|
24
|
+
*
|
|
25
|
+
* On Windows, creating a symlink needs Developer Mode or an elevated shell.
|
|
26
|
+
* When that fails with EPERM/EACCES and the link points at a directory inside
|
|
27
|
+
* the tree being copied, the copy falls back to a junction (no privilege
|
|
28
|
+
* needed). A junction needs an absolute target, so it points at the matching
|
|
29
|
+
* entry under `finalRoot`: the path the copy will live at once the caller has
|
|
30
|
+
* renamed it into place. Any other link target throws `CopyTreeLinkError` with
|
|
31
|
+
* a Developer Mode hint instead of silently dropping the link.
|
|
32
|
+
*
|
|
33
|
+
* TODO(windows-links): src/utils/safe-link.ts (PR 1967, branch
|
|
34
|
+
* fix/windows-install-path-daemon) adds its own junction fallback and
|
|
35
|
+
* link-only removal. Whichever of the two PRs merges second should reconcile
|
|
36
|
+
* these helpers into one module.
|
|
37
|
+
*/
|
|
38
|
+
export interface CopyTreeOptions {
|
|
39
|
+
/** Entry basenames to skip at any depth (`rsync --exclude=<name>`). */
|
|
40
|
+
exclude?: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Where `dest` will end up after the caller renames it (for example a
|
|
43
|
+
* staging dir that is renamed into `core/packages/<name>`). Junction
|
|
44
|
+
* fallbacks point under this path so they still resolve after the rename.
|
|
45
|
+
* Defaults to `dest`.
|
|
46
|
+
*/
|
|
47
|
+
finalRoot?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class CopyTreeLinkError extends Error {
|
|
50
|
+
constructor(linkPath: string, target: string, cause: NodeJS.ErrnoException);
|
|
51
|
+
}
|
|
52
|
+
export declare class ReparsePointError extends Error {
|
|
53
|
+
constructor(dir: string);
|
|
54
|
+
}
|
|
55
|
+
export declare function copyTree(src: string, dest: string, opts?: CopyTreeOptions): void;
|
|
56
|
+
/**
|
|
57
|
+
* Remove `target` and everything under it without following links. A missing
|
|
58
|
+
* target is not an error. If `target` itself is a link, only the link goes.
|
|
59
|
+
*/
|
|
60
|
+
export declare function removeTreeNoFollow(target: string): void;
|
|
61
|
+
//# sourceMappingURL=copy-tree.d.ts.map
|