@indigoai-us/hq-cli 5.345.123 → 5.345.125
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 +39 -0
- package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
- package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
- package/dist/command-catalog.generated.d.ts +239 -0
- package/dist/command-catalog.generated.js +291 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
- package/dist/commands/core.js +12 -0
- package/dist/commands/lanes-accounts.js +22 -3
- package/dist/commands/lanes.js +25 -32
- package/dist/commands/mesh.js +457 -16
- package/dist/fast-scaffold.js +26 -0
- package/dist/lib/lanes/accounts.d.ts +48 -2
- package/dist/lib/lanes/accounts.js +160 -9
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/message-replay.js +6 -1
- package/dist/lib/lanes/provider-rejection.d.ts +2 -6
- package/dist/lib/lanes/provider-rejection.js +19 -0
- package/dist/lib/lanes/quota-notice.d.ts +24 -0
- package/dist/lib/lanes/quota-notice.js +61 -0
- package/dist/lib/lanes/quota.d.ts +55 -15
- package/dist/lib/lanes/quota.js +231 -49
- package/dist/lib/lanes/reconcile.js +17 -1
- package/dist/lib/lanes/session-relink.d.ts +2 -0
- package/dist/lib/lanes/session-relink.js +98 -4
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +58 -12
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +13 -1
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +3 -0
- package/dist/lib/mesh/client.d.ts +6 -1
- package/dist/lib/mesh/client.js +18 -5
- package/dist/lib/mesh/team-coordination.d.ts +106 -0
- package/dist/lib/mesh/team-coordination.js +297 -0
- package/dist/lib/work-context/auto-bind.d.ts +75 -0
- package/dist/lib/work-context/auto-bind.js +245 -0
- package/package.json +1 -1
|
@@ -117,8 +117,102 @@ function uniqueHomes(homes) {
|
|
|
117
117
|
}
|
|
118
118
|
return [...unique];
|
|
119
119
|
}
|
|
120
|
-
function missingTranscript(input, homes) {
|
|
121
|
-
return new LanesError("provider_transcript_missing", `Provider transcript for session ${input.sessionId} was not found. Homes searched: ${homes.join(", ")}
|
|
120
|
+
function missingTranscript(input, homes, dirsScanned) {
|
|
121
|
+
return new LanesError("provider_transcript_missing", `Provider transcript for session ${input.sessionId} was not found. Homes searched: ${homes.join(", ")}. Directories scanned: ${dirsScanned.length > 0 ? dirsScanned.join(", ") : "none"}`, {
|
|
122
|
+
provider: input.provider,
|
|
123
|
+
session_id: input.sessionId,
|
|
124
|
+
homes_searched: homes,
|
|
125
|
+
directories_scanned: dirsScanned,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Every `<home>/projects/<dir>/` that could hold a claude transcript. A lane
|
|
130
|
+
* can record one cwd and have run in another (a worktree), so a resume looks
|
|
131
|
+
* in every project directory before refusing.
|
|
132
|
+
*/
|
|
133
|
+
function claudeProjectDirs(home) {
|
|
134
|
+
const root = path.join(home, "projects");
|
|
135
|
+
let entries;
|
|
136
|
+
try {
|
|
137
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const code = err.code;
|
|
141
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
142
|
+
return [];
|
|
143
|
+
throw new LanesError("provider_home_search_failed", `Could not search provider home ${home}: ${err instanceof Error ? err.message : String(err)}`, { provider: "claude", home });
|
|
144
|
+
}
|
|
145
|
+
return entries
|
|
146
|
+
.filter((entry) => entry.isDirectory())
|
|
147
|
+
.map((entry) => path.join(root, entry.name))
|
|
148
|
+
.sort();
|
|
149
|
+
}
|
|
150
|
+
function isFile(target) {
|
|
151
|
+
try {
|
|
152
|
+
return fs.statSync(target).isFile();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const code = err.code;
|
|
156
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
157
|
+
return false;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Last resort for a claude session that is not under the expected project
|
|
163
|
+
* directory in any home: scan every project directory of every searched home,
|
|
164
|
+
* then copy the transcript (and its sibling subagent directory) into the
|
|
165
|
+
* target home under the project directory for `input.cwd`, which is where
|
|
166
|
+
* `claude --resume` started in that cwd looks for it.
|
|
167
|
+
*/
|
|
168
|
+
function relinkFromAnyProject(input, targetHome, searched) {
|
|
169
|
+
const scanned = [];
|
|
170
|
+
if (input.provider !== "claude") {
|
|
171
|
+
for (const home of searched)
|
|
172
|
+
scanned.push(path.join(home, "sessions"));
|
|
173
|
+
throw missingTranscript(input, searched, scanned);
|
|
174
|
+
}
|
|
175
|
+
for (const home of searched) {
|
|
176
|
+
const found = [];
|
|
177
|
+
const dirs = claudeProjectDirs(home);
|
|
178
|
+
if (dirs.length === 0)
|
|
179
|
+
scanned.push(path.join(home, "projects"));
|
|
180
|
+
for (const dir of dirs) {
|
|
181
|
+
scanned.push(dir);
|
|
182
|
+
const candidate = path.join(dir, `${input.sessionId}.jsonl`);
|
|
183
|
+
if (isFile(candidate))
|
|
184
|
+
found.push(candidate);
|
|
185
|
+
}
|
|
186
|
+
if (found.length === 0)
|
|
187
|
+
continue;
|
|
188
|
+
if (found.length > 1) {
|
|
189
|
+
throw new LanesError("provider_transcript_ambiguous", `Multiple claude transcripts for session ${input.sessionId} exist in ${home}`, { provider: input.provider, session_id: input.sessionId, homes_searched: searched, matches: found });
|
|
190
|
+
}
|
|
191
|
+
const source = found[0];
|
|
192
|
+
const targetDir = path.join(targetHome, "projects", encodeClaudeProjectDir(input.cwd));
|
|
193
|
+
const target = path.join(targetDir, `${input.sessionId}.jsonl`);
|
|
194
|
+
const subagents = path.join(path.dirname(source), input.sessionId);
|
|
195
|
+
const hasSubagents = statDirectory(subagents);
|
|
196
|
+
const plan = {
|
|
197
|
+
provider: "claude",
|
|
198
|
+
sources: [source],
|
|
199
|
+
targets: [target],
|
|
200
|
+
source_dir: hasSubagents ? subagents : null,
|
|
201
|
+
target_dir: hasSubagents ? path.join(targetDir, input.sessionId) : null,
|
|
202
|
+
};
|
|
203
|
+
const copied = copySessionFiles(plan);
|
|
204
|
+
if (!isFile(target)) {
|
|
205
|
+
throw new LanesError("provider_transcript_relink_failed", `Could not verify session ${input.sessionId} in target home ${targetHome}`, { provider: input.provider, session_id: input.sessionId, target_home: targetHome });
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...copied,
|
|
209
|
+
source_home: home,
|
|
210
|
+
transcript_path: target,
|
|
211
|
+
homes_searched: searched,
|
|
212
|
+
found_outside_cwd: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
throw missingTranscript(input, searched, scanned);
|
|
122
216
|
}
|
|
123
217
|
function copyVerifiedTranscript(input, sourceHome, targetHome, searched) {
|
|
124
218
|
let copied;
|
|
@@ -132,7 +226,7 @@ function copyVerifiedTranscript(input, sourceHome, targetHome, searched) {
|
|
|
132
226
|
}
|
|
133
227
|
catch (err) {
|
|
134
228
|
if (err instanceof LanesError && err.error === "provider_session_missing") {
|
|
135
|
-
|
|
229
|
+
return relinkFromAnyProject(input, targetHome, searched);
|
|
136
230
|
}
|
|
137
231
|
throw err;
|
|
138
232
|
}
|
|
@@ -202,7 +296,7 @@ export function ensureSessionHome(input) {
|
|
|
202
296
|
matches.push({ home, transcript: found[0] });
|
|
203
297
|
}
|
|
204
298
|
if (matches.length === 0)
|
|
205
|
-
|
|
299
|
+
return relinkFromAnyProject(input, targetHome, searched);
|
|
206
300
|
if (matches.length > 1) {
|
|
207
301
|
throw new LanesError("provider_transcript_ambiguous", `Provider transcript for session ${input.sessionId} exists in multiple homes: ${matches.map((match) => match.home).join(", ")}`, {
|
|
208
302
|
provider: input.provider,
|
|
@@ -255,7 +255,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
|
|
|
255
255
|
export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
|
|
256
256
|
envVar: string;
|
|
257
257
|
home: string;
|
|
258
|
-
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"]): NodeJS.ProcessEnv;
|
|
258
|
+
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"], homedir?: () => string): NodeJS.ProcessEnv;
|
|
259
259
|
export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
|
|
260
260
|
/** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
|
|
261
261
|
export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { configureRemoteControlChildEnv, prepareRemoteControlLane, remoteControlSelectionConfig } from "./remote-control-lane.js";
|
|
16
|
-
import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
|
|
16
|
+
import { PROVIDER_HOME_ENV, accountStatus, isDefaultProviderHome, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
|
|
17
17
|
import { effectiveAccountHome } from "./account-home.js";
|
|
18
18
|
import { buildHostCapacitySnapshot } from "./host-capacity.js";
|
|
19
19
|
import { selectClaudeAccount } from "./claude-balance.js";
|
|
@@ -1661,7 +1661,7 @@ export function buildProviderCommand(provider, input) {
|
|
|
1661
1661
|
args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendCodexWorkerPolicyDirFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.workerPolicyDir), input.model), input.effort), input.fastMode),
|
|
1662
1662
|
};
|
|
1663
1663
|
}
|
|
1664
|
-
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker) {
|
|
1664
|
+
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker, homedir) {
|
|
1665
1665
|
const accepted = Object.values(PROVIDER_HOME_ENV);
|
|
1666
1666
|
if (!accepted.includes(resolved.envVar)) {
|
|
1667
1667
|
throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
|
|
@@ -1672,7 +1672,16 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
|
|
|
1672
1672
|
delete env.HQ_POLICY_WORKER_DIR;
|
|
1673
1673
|
delete env.HQ_LANE_APPROVAL_REQUIRED;
|
|
1674
1674
|
delete env.HQ_LANE_HUMAN_CHECKPOINTS;
|
|
1675
|
-
|
|
1675
|
+
if (resolved.envVar === PROVIDER_HOME_ENV.claude &&
|
|
1676
|
+
isDefaultProviderHome("claude", resolved.home, homedir)) {
|
|
1677
|
+
// The default ~/.claude home runs with CLAUDE_CONFIG_DIR unset so the
|
|
1678
|
+
// worker reads the same keychain credential as the interactive session
|
|
1679
|
+
// (see isDefaultProviderHome). An inherited value is dropped too.
|
|
1680
|
+
delete env[resolved.envVar];
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
env[resolved.envVar] = resolved.home;
|
|
1684
|
+
}
|
|
1676
1685
|
delete env[CLAUDE_AUTO_COMPACT_ENV];
|
|
1677
1686
|
if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
|
|
1678
1687
|
env[CLAUDE_AUTO_COMPACT_ENV] = String(autoCompactTokens);
|
|
@@ -2075,6 +2084,14 @@ export function readSpawnManifest(runDir) {
|
|
|
2075
2084
|
}
|
|
2076
2085
|
return out;
|
|
2077
2086
|
}
|
|
2087
|
+
function isExistingDirectory(target) {
|
|
2088
|
+
try {
|
|
2089
|
+
return fs.statSync(target).isDirectory();
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2078
2095
|
function laneLaunchResult(lane) {
|
|
2079
2096
|
const { provider_home_at_spawn: _providerHomeAtSpawn, ...safe } = lane;
|
|
2080
2097
|
return safe;
|
|
@@ -2327,6 +2344,17 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2327
2344
|
: existing;
|
|
2328
2345
|
throw new LanesError("account_not_logged_in", `Account ${resolved.id} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: resolved.id, lane_id: lane.lane_id, lane: failed });
|
|
2329
2346
|
}
|
|
2347
|
+
// The worker starts here, and claude keys its transcript by this directory,
|
|
2348
|
+
// so the transcript checks below use the same value. A resume reuses the
|
|
2349
|
+
// directory the last run started in (a worktree for pipeline lanes) when it
|
|
2350
|
+
// still exists.
|
|
2351
|
+
const recordedWorkingDirectory = plan.mode === "resume" &&
|
|
2352
|
+
typeof existing.working_directory_at_spawn === "string" &&
|
|
2353
|
+
existing.working_directory_at_spawn.trim() !== "" &&
|
|
2354
|
+
isExistingDirectory(existing.working_directory_at_spawn)
|
|
2355
|
+
? existing.working_directory_at_spawn
|
|
2356
|
+
: undefined;
|
|
2357
|
+
const workerCwd = deps.workingDirectory ?? pendingResume?.working_directory ?? recordedWorkingDirectory ?? hqRoot;
|
|
2330
2358
|
if (existing.account !== resolved.id) {
|
|
2331
2359
|
const source = existing.account
|
|
2332
2360
|
? config.providers[existing.provider].accounts[existing.account]
|
|
@@ -2335,12 +2363,22 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2335
2363
|
source &&
|
|
2336
2364
|
existing.provider_session_id &&
|
|
2337
2365
|
providerSupportsSessionResume(existing.provider)) {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2366
|
+
try {
|
|
2367
|
+
copySessionFiles(planSessionCopy(existing.provider, {
|
|
2368
|
+
sourceHome: existing.provider_home_at_spawn ?? effectiveAccountHome(source, hostname).home,
|
|
2369
|
+
targetHome: resolved.home,
|
|
2370
|
+
cwd: workerCwd,
|
|
2371
|
+
sessionId: existing.provider_session_id,
|
|
2372
|
+
}));
|
|
2373
|
+
}
|
|
2374
|
+
catch (err) {
|
|
2375
|
+
// A resume below searches every home and project directory for the
|
|
2376
|
+
// session, so a transcript under another cwd is handled there.
|
|
2377
|
+
const resumeFollows = plan.mode === "resume" && existing.provider !== "grok";
|
|
2378
|
+
if (!(resumeFollows && err instanceof LanesError && err.error === "provider_session_missing")) {
|
|
2379
|
+
throw err;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2344
2382
|
}
|
|
2345
2383
|
}
|
|
2346
2384
|
if (plan.mode === "resume" && existing.provider !== "grok") {
|
|
@@ -2350,7 +2388,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2350
2388
|
ensureSessionHome({
|
|
2351
2389
|
provider: existing.provider,
|
|
2352
2390
|
sessionId: plan.session_id,
|
|
2353
|
-
cwd:
|
|
2391
|
+
cwd: workerCwd,
|
|
2354
2392
|
targetHome,
|
|
2355
2393
|
registeredHomes: Object.values(config.providers[existing.provider].accounts)
|
|
2356
2394
|
.map((account) => effectiveAccountHome(account, hostname).home),
|
|
@@ -2644,6 +2682,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2644
2682
|
pending_settings: undefined,
|
|
2645
2683
|
account: resolved.id,
|
|
2646
2684
|
provider_home_at_spawn: remoteControlHome ?? resolved.home,
|
|
2685
|
+
working_directory_at_spawn: workerCwd,
|
|
2647
2686
|
...(model !== undefined ? { model } : {}),
|
|
2648
2687
|
...(model !== undefined
|
|
2649
2688
|
? {
|
|
@@ -2789,7 +2828,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2789
2828
|
let watcherPid;
|
|
2790
2829
|
try {
|
|
2791
2830
|
noteCapacityAdvisory(lane, env);
|
|
2792
|
-
workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv,
|
|
2831
|
+
workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, workerCwd, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
|
|
2793
2832
|
writePidRecord(path.join(runDir, "worker.pid"), {
|
|
2794
2833
|
pid: workerPid,
|
|
2795
2834
|
starttime: readProcStarttime(workerPid) ?? undefined,
|
|
@@ -3095,12 +3134,19 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
3095
3134
|
? { resume: { session_id: sessionId, prompt: repairPrompt } }
|
|
3096
3135
|
: { freshTurnPrompt: repairPrompt }),
|
|
3097
3136
|
});
|
|
3137
|
+
// The repair turn resumes the run's session, which claude keys by the
|
|
3138
|
+
// directory that run started in.
|
|
3139
|
+
const repairCwd = mode === "resume" &&
|
|
3140
|
+
typeof lane.working_directory_at_spawn === "string" &&
|
|
3141
|
+
isExistingDirectory(lane.working_directory_at_spawn)
|
|
3142
|
+
? lane.working_directory_at_spawn
|
|
3143
|
+
: hqRoot;
|
|
3098
3144
|
const workerOutOffset = fileSizeOrZero(path.join(logsDir, "worker.out"));
|
|
3099
3145
|
const previous = readSpawnManifest(runDir);
|
|
3100
3146
|
let workerPid;
|
|
3101
3147
|
try {
|
|
3102
3148
|
noteCapacityAdvisory(lane, env);
|
|
3103
|
-
workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv,
|
|
3149
|
+
workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, repairCwd, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
|
|
3104
3150
|
writePidRecord(path.join(runDir, "worker.pid"), {
|
|
3105
3151
|
pid: workerPid,
|
|
3106
3152
|
starttime: readProcStarttime(workerPid) ?? undefined,
|
|
@@ -772,6 +772,11 @@ export interface LaneRecord {
|
|
|
772
772
|
provider_session_id_absent?: string;
|
|
773
773
|
/** Provider home used by the most recently spawned run, for transcript recovery. */
|
|
774
774
|
provider_home_at_spawn?: string;
|
|
775
|
+
/**
|
|
776
|
+
* Working directory the most recently spawned run started in. Claude keys
|
|
777
|
+
* its transcript by this directory, so a later resume starts there too.
|
|
778
|
+
*/
|
|
779
|
+
working_directory_at_spawn?: string;
|
|
775
780
|
/** Set at spawn when this run continued a recorded session instead of starting fresh. */
|
|
776
781
|
last_resume?: LaneResumeRecord;
|
|
777
782
|
/** Last 50 lane worker starts after the initial start, including actor provenance. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -242,7 +242,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
242
242
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
243
243
|
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile", "spawn"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
244
244
|
quota_reset_at: ["watcher", "reconcile", "spawn", "command"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
245
|
-
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], resume_history: ["spawn", "watcher"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
|
|
245
|
+
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], working_directory_at_spawn: ["spawn"], last_resume: ["spawn"], resume_history: ["spawn", "watcher"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
|
|
246
246
|
};
|
|
247
247
|
function isPlainObject(value) {
|
|
248
248
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -43,6 +43,7 @@ import { appendCappedHistory, listQuestionIds, readLane, readQuestion, retryUpda
|
|
|
43
43
|
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
44
44
|
import { readWorkerRunResult } from "./delivery.js";
|
|
45
45
|
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
46
|
+
import { deliverQuotaHoldNotice, quotaHoldNotice } from "./quota-notice.js";
|
|
46
47
|
import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
47
48
|
import { appendDropboxMessageAtPath, dropboxInboxHasMessages, readToolActivity, toolActivityPath } from "./dropbox.js";
|
|
48
49
|
import { loadLanesConfig, providerProgressEventMinIntervalMs, providerQuotaDefaultHoldMs } from "./config.js";
|
|
@@ -1764,7 +1765,18 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1764
1765
|
(classification.reason === "account_quota_exhausted" ||
|
|
1765
1766
|
classification.reason === "account_model_unavailable")) {
|
|
1766
1767
|
try {
|
|
1767
|
-
|
|
1768
|
+
const observedAt = new Date(now());
|
|
1769
|
+
const quotaState = recordAccountQuota(hqRoot, quotaLane.provider, quotaLane.account, quota, {
|
|
1770
|
+
now: observedAt,
|
|
1771
|
+
unconfirmedHoldMs: providerQuotaDefaultHoldMs(loadLanesConfig(hqRoot).providers[quotaLane.provider]),
|
|
1772
|
+
});
|
|
1773
|
+
if (classification.reason === "account_quota_exhausted") {
|
|
1774
|
+
const notice = quotaHoldNotice(quotaState, quotaLane.provider, quotaLane.account, quotaLane.lane_id);
|
|
1775
|
+
if (notice) {
|
|
1776
|
+
const deliveredTo = deliverQuotaHoldNotice(hqRoot, quotaLane, notice, observedAt.toISOString());
|
|
1777
|
+
logDecision({ event: "quota_hold_notice", at: tick.emit.at, account: quotaLane.account, delivered_to: deliveredTo });
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1768
1780
|
}
|
|
1769
1781
|
catch (err) {
|
|
1770
1782
|
process.stderr.write(`hq lanes watcher: quota record failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface StoryPatch {
|
|
|
137
137
|
status?: string;
|
|
138
138
|
assigneeUid?: string;
|
|
139
139
|
clearAssignee?: boolean;
|
|
140
|
+
/** Override another holder's assignment (server answers 409 otherwise). */
|
|
141
|
+
force?: boolean;
|
|
140
142
|
}
|
|
141
143
|
export interface StoryPatchResult {
|
|
142
144
|
projectId: string;
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -56,6 +56,7 @@ export async function meshJson(token, path, init = {}) {
|
|
|
56
56
|
].filter(Boolean);
|
|
57
57
|
const failure = new Error(`${res.status} ${code}${detail}${context.length ? ` (${context.join("; ")})` : ""}`);
|
|
58
58
|
failure.status = res.status;
|
|
59
|
+
failure.body = data;
|
|
59
60
|
if (typeof err.code === "string" && err.code)
|
|
60
61
|
failure.code = err.code;
|
|
61
62
|
throw failure;
|
|
@@ -398,6 +399,8 @@ export async function patchStory(token, companyUid, projectId, storyId, patch) {
|
|
|
398
399
|
body.assigneeUid = patch.assigneeUid;
|
|
399
400
|
if (patch.clearAssignee)
|
|
400
401
|
body.clearAssignee = true;
|
|
402
|
+
if (patch.force)
|
|
403
|
+
body.force = true;
|
|
401
404
|
const view = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/stories/${encodeURIComponent(storyId)}`, {
|
|
402
405
|
method: "PATCH",
|
|
403
406
|
body,
|
|
@@ -114,6 +114,11 @@ export declare function fetchCompanyLive(token: string, companyUid: string, opts
|
|
|
114
114
|
cursor?: string;
|
|
115
115
|
get?: (path: string, query: Record<string, string>) => Promise<unknown>;
|
|
116
116
|
}): Promise<CompanyLiveReadResponse>;
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Fill participants[].displayName from the local directory cache when the
|
|
119
|
+
* server omitted it (older hq-pro builds do not send it).
|
|
120
|
+
*/
|
|
121
|
+
export declare function withParticipantDisplayNames(live: CompanyLiveReadResponse, names: ReadonlyMap<string, string>): CompanyLiveReadResponse;
|
|
122
|
+
export declare function formatCompanyLiveTable(live: CompanyLiveReadResponse, companyLabel: string, names?: ReadonlyMap<string, string>): string[];
|
|
118
123
|
export type { OrganizeSubmitDecision, OrganizeReceipt, CandidatesResponse, MigrateReceipt, PostMigrateFn, };
|
|
119
124
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/lib/mesh/client.js
CHANGED
|
@@ -290,22 +290,35 @@ export async function fetchCompanyLive(token, companyUid, opts) {
|
|
|
290
290
|
: await meshJson(token, "/v1/work-mesh/live", { query });
|
|
291
291
|
return data;
|
|
292
292
|
}
|
|
293
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Fill participants[].displayName from the local directory cache when the
|
|
295
|
+
* server omitted it (older hq-pro builds do not send it).
|
|
296
|
+
*/
|
|
297
|
+
export function withParticipantDisplayNames(live, names) {
|
|
298
|
+
return {
|
|
299
|
+
...live,
|
|
300
|
+
participants: (live.participants ?? []).map((p) => {
|
|
301
|
+
const name = p.displayName?.trim() || names.get(p.actorUid);
|
|
302
|
+
return name ? { ...p, displayName: name } : p;
|
|
303
|
+
}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export function formatCompanyLiveTable(live, companyLabel, names = new Map()) {
|
|
294
307
|
const lines = [
|
|
295
308
|
`Work mesh live: ${companyLabel} generatedAt=${live.generatedAt}`,
|
|
296
|
-
"
|
|
309
|
+
"name presence sessionId status context project task turns",
|
|
297
310
|
];
|
|
298
311
|
for (const p of live.participants ?? []) {
|
|
312
|
+
const who = p.displayName?.trim() || names.get(p.actorUid) || p.actorUid;
|
|
299
313
|
const sessions = p.sessions ?? [];
|
|
300
314
|
if (sessions.length === 0) {
|
|
301
|
-
lines.push(`${
|
|
315
|
+
lines.push(`${who}\t${p.presence ?? "?"}\t-\t-\t-\t-\t-\t-`);
|
|
302
316
|
continue;
|
|
303
317
|
}
|
|
304
318
|
for (const s of sessions) {
|
|
305
319
|
lines.push([
|
|
306
|
-
|
|
320
|
+
who,
|
|
307
321
|
p.presence ?? "?",
|
|
308
|
-
p.displayName ?? "",
|
|
309
322
|
s.sessionId,
|
|
310
323
|
s.status ?? "?",
|
|
311
324
|
s.contextStatus ?? "?",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Mesh team coordination (WMTC-7 / WMTC-8): story claims, overlap,
|
|
3
|
+
* project reassignment, assignee suggestions, and the presence roster.
|
|
4
|
+
*
|
|
5
|
+
* Every route here except the story PATCH is new on hq-pro. Callers treat a
|
|
6
|
+
* 404/405 without an error code as "this server does not have the route yet"
|
|
7
|
+
* and degrade to one plain line instead of failing.
|
|
8
|
+
*/
|
|
9
|
+
export declare const STORY_ASSIGNEE_CONFLICT = "STORY_ASSIGNEE_CONFLICT";
|
|
10
|
+
/**
|
|
11
|
+
* True when the server does not know the route: 405, or a 404 that carries no
|
|
12
|
+
* error code. A coded 404 (for example PROJECT_NOT_FOUND) is a real answer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isRouteUnavailable(err: unknown): boolean;
|
|
15
|
+
export declare function unavailableLine(feature: string): string;
|
|
16
|
+
export interface StoryAssigneeConflict {
|
|
17
|
+
code: typeof STORY_ASSIGNEE_CONFLICT;
|
|
18
|
+
error: string;
|
|
19
|
+
holderUid: string;
|
|
20
|
+
holderDisplayName?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Extract a structured conflict from a 409 meshJson failure, if it is one. */
|
|
23
|
+
export declare function storyConflictFromError(err: unknown): StoryAssigneeConflict | null;
|
|
24
|
+
export declare function formatStoryConflictLine(storyId: string, conflict: StoryAssigneeConflict, names?: NameDirectory): string;
|
|
25
|
+
export type NameDirectory = Map<string, string>;
|
|
26
|
+
export interface DirectoryPerson {
|
|
27
|
+
uid: string;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function peopleFromPayload(payload: unknown): DirectoryPerson[];
|
|
32
|
+
/** People known from the local contacts/directory cache. Never throws. */
|
|
33
|
+
export declare function loadCachedPeople(cacheRoot?: string): DirectoryPerson[];
|
|
34
|
+
export declare function loadNameDirectory(cacheRoot?: string): NameDirectory;
|
|
35
|
+
export declare function displayNameFor(uid: string, serverName: string | undefined, names: NameDirectory): string;
|
|
36
|
+
export type PersonResolution = {
|
|
37
|
+
ok: true;
|
|
38
|
+
uid: string;
|
|
39
|
+
displayName?: string;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
reason: "none" | "ambiguous";
|
|
43
|
+
candidates: DirectoryPerson[];
|
|
44
|
+
};
|
|
45
|
+
/** Resolve a uid, email, or display name against a list of people. */
|
|
46
|
+
export declare function resolvePerson(query: string, people: DirectoryPerson[]): PersonResolution;
|
|
47
|
+
export interface MeshOverlap {
|
|
48
|
+
actorUid: string;
|
|
49
|
+
displayName?: string;
|
|
50
|
+
actorType?: string;
|
|
51
|
+
sessionId: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
branch: string;
|
|
54
|
+
projectId?: string;
|
|
55
|
+
storyId?: string;
|
|
56
|
+
reasons: string[];
|
|
57
|
+
sharedFiles: string[];
|
|
58
|
+
lastSeenAt?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface OverlapRequest {
|
|
61
|
+
companyUid: string;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
repo: string;
|
|
64
|
+
branch: string;
|
|
65
|
+
projectId?: string;
|
|
66
|
+
storyId?: string;
|
|
67
|
+
files?: string[];
|
|
68
|
+
}
|
|
69
|
+
export declare const OVERLAP_MAX_FILES = 200;
|
|
70
|
+
export declare function postOverlap(token: string, req: OverlapRequest): Promise<MeshOverlap[]>;
|
|
71
|
+
export declare function formatOverlapLines(overlaps: MeshOverlap[], names: NameDirectory): string[];
|
|
72
|
+
export interface ReassignResult {
|
|
73
|
+
projectId: string;
|
|
74
|
+
ownerUid: string;
|
|
75
|
+
previousOwnerUid: string | null;
|
|
76
|
+
reassignedStories: string[];
|
|
77
|
+
}
|
|
78
|
+
export declare function postProjectReassign(token: string, projectId: string, body: {
|
|
79
|
+
companyUid: string;
|
|
80
|
+
toUid: string;
|
|
81
|
+
includeOpenStories?: boolean;
|
|
82
|
+
note?: string;
|
|
83
|
+
}): Promise<ReassignResult>;
|
|
84
|
+
export interface SuggestionCandidate {
|
|
85
|
+
actorUid: string;
|
|
86
|
+
displayName?: string;
|
|
87
|
+
actorType?: string;
|
|
88
|
+
score: number;
|
|
89
|
+
idle: boolean;
|
|
90
|
+
reasons: string[];
|
|
91
|
+
}
|
|
92
|
+
export declare function fetchStorySuggestions(token: string, companyUid: string, projectId: string, storyId: string): Promise<SuggestionCandidate[]>;
|
|
93
|
+
export declare function formatSuggestionLines(storyId: string, candidates: SuggestionCandidate[], names: NameDirectory): string[];
|
|
94
|
+
export interface RosterMember {
|
|
95
|
+
actorUid: string;
|
|
96
|
+
displayName?: string;
|
|
97
|
+
actorType?: string;
|
|
98
|
+
daemonLastSeenAt: string | null;
|
|
99
|
+
lastSessionAt: string | null;
|
|
100
|
+
boundSessions: number;
|
|
101
|
+
unboundSessions: number;
|
|
102
|
+
}
|
|
103
|
+
export declare function fetchRoster(token: string, companyUid: string): Promise<RosterMember[]>;
|
|
104
|
+
export declare function rosterFlags(member: RosterMember): string[];
|
|
105
|
+
export declare function formatRosterLines(members: RosterMember[], companyLabel: string, names: NameDirectory): string[];
|
|
106
|
+
//# sourceMappingURL=team-coordination.d.ts.map
|