@indigoai-us/hq-cli 5.125.0 → 5.127.0
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 +329 -0
- package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
- package/assets/bot-workers/setup/skills/your-business.md +270 -0
- package/assets/bot-workers/setup/worker.yaml +189 -58
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +643 -6
- package/dist/command-catalog.generated.js +836 -6
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +11 -1
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +114 -0
- package/dist/commands/lanes.js +2969 -0
- package/dist/commands/mesh.js +287 -24
- package/dist/commands/onboard-create-identity.d.ts +26 -0
- package/dist/commands/onboard-create-identity.js +23 -0
- package/dist/commands/onboard.js +13 -4
- package/dist/lib/bot/progress.d.ts +6 -0
- package/dist/lib/bot/progress.js +16 -0
- package/dist/lib/bot/runtime/grok.d.ts +30 -7
- package/dist/lib/bot/runtime/grok.js +86 -8
- package/dist/lib/bot/runtime/index.js +15 -8
- package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
- package/dist/lib/bot/runtime/messages-stream.js +49 -3
- package/dist/lib/lanes/accounts.d.ts +103 -0
- package/dist/lib/lanes/accounts.js +318 -0
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +71 -0
- package/dist/lib/lanes/config.js +408 -0
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +49 -0
- package/dist/lib/lanes/index.js +27 -0
- package/dist/lib/lanes/liveness.d.ts +95 -0
- package/dist/lib/lanes/liveness.js +332 -0
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +116 -0
- package/dist/lib/lanes/registry.js +521 -0
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +68 -0
- package/dist/lib/lanes/store.js +745 -0
- package/dist/lib/lanes/throttle.d.ts +89 -0
- package/dist/lib/lanes/throttle.js +297 -0
- package/dist/lib/lanes/types.d.ts +535 -0
- package/dist/lib/lanes/types.js +350 -0
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/config.js +75 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
- package/dist/lib/mesh/live/daemon/index.js +3 -3
- package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
- package/dist/lib/mesh/live/daemon/install.js +444 -2
- package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/paths.js +2 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +18 -13
- package/dist/lib/work-context/config.d.ts +20 -0
- package/dist/lib/work-context/config.js +26 -5
- package/dist/lib/work-context/reconcile.js +28 -7
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/dist/lib/work-context/windows-acl.d.ts +123 -0
- package/dist/lib/work-context/windows-acl.js +425 -0
- package/dist/utils/pack-contributions.js +5 -4
- package/package.json +3 -1
package/dist/commands/mesh.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* Distinct from `hq doctor` (hook guardrails).
|
|
7
7
|
*/
|
|
8
8
|
import * as os from "node:os";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
9
10
|
import { findHqRoot } from "../utils/manifest.js";
|
|
10
11
|
import chalk from "chalk";
|
|
11
12
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
12
13
|
import * as readline from "node:readline/promises";
|
|
13
|
-
import { STORY_STATUSES, appendThreadEvent,
|
|
14
|
+
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
14
15
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
15
16
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
16
17
|
import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
|
|
@@ -29,7 +30,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
|
|
|
29
30
|
import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
|
|
30
31
|
import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
|
|
31
32
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
32
|
-
import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
|
|
33
|
+
import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
|
|
33
34
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
34
35
|
import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
|
|
35
36
|
import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
|
|
@@ -88,12 +89,20 @@ async function runCheck(opts) {
|
|
|
88
89
|
async function runEvent(verb, opts) {
|
|
89
90
|
const { token, company } = await withCompany(opts);
|
|
90
91
|
const projectId = requireProject(opts);
|
|
92
|
+
const eventKind = verb === "start" ? "claim" : verb;
|
|
93
|
+
let claimant = "";
|
|
94
|
+
if (eventKind === "claim") {
|
|
95
|
+
const resolvedClaimant = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
96
|
+
if (!resolvedClaimant) {
|
|
97
|
+
fail("Could not resolve the authenticated claimant. Sign in again and retry `hq mesh start`.");
|
|
98
|
+
}
|
|
99
|
+
claimant = resolvedClaimant;
|
|
100
|
+
}
|
|
91
101
|
const ensured = await ensureProjectThread(token, company, projectId, {
|
|
92
102
|
threadId: opts.threadId,
|
|
93
103
|
summary: opts.summary,
|
|
94
104
|
});
|
|
95
|
-
const
|
|
96
|
-
const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, callerLabelFromToken(token)));
|
|
105
|
+
const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, claimant));
|
|
97
106
|
if (opts.json) {
|
|
98
107
|
console.log(JSON.stringify({
|
|
99
108
|
ok: true,
|
|
@@ -112,17 +121,215 @@ async function runEvent(verb, opts) {
|
|
|
112
121
|
async function runStory(opts) {
|
|
113
122
|
const storyId = opts.story?.trim();
|
|
114
123
|
const status = opts.status?.trim();
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
const assignee = opts.assignee?.trim();
|
|
125
|
+
if (!storyId) {
|
|
126
|
+
fail("story requires --story <id>");
|
|
127
|
+
}
|
|
128
|
+
if (!status && !assignee) {
|
|
129
|
+
fail("story requires --status queued|in_progress|review|done or --assignee <uid|me|none>");
|
|
130
|
+
}
|
|
131
|
+
if (status && !STORY_STATUSES.has(status)) {
|
|
132
|
+
fail("story --status must be queued|in_progress|review|done");
|
|
117
133
|
}
|
|
118
134
|
const { token, company } = await withCompany(opts);
|
|
119
135
|
const projectId = requireProject(opts);
|
|
120
|
-
|
|
136
|
+
let assigneeUid;
|
|
137
|
+
let clearAssignee = false;
|
|
138
|
+
if (assignee === "none") {
|
|
139
|
+
clearAssignee = true;
|
|
140
|
+
}
|
|
141
|
+
else if (assignee === "me") {
|
|
142
|
+
const resolved = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
143
|
+
if (!resolved || resolved === "session") {
|
|
144
|
+
fail("`--assignee me` needs the signed-in person or agent. The synthetic session identity cannot be assigned. Sign in again and retry `hq mesh story`.");
|
|
145
|
+
}
|
|
146
|
+
assigneeUid = resolved;
|
|
147
|
+
}
|
|
148
|
+
else if (assignee) {
|
|
149
|
+
if (!/^(prs|agt)_/.test(assignee)) {
|
|
150
|
+
fail("`--assignee` must be a person or agent uid, `me`, or `none`.");
|
|
151
|
+
}
|
|
152
|
+
assigneeUid = assignee;
|
|
153
|
+
}
|
|
154
|
+
const patched = await patchStory(token, company.companyUid, projectId, storyId, {
|
|
155
|
+
...(status ? { status } : {}),
|
|
156
|
+
...(assigneeUid ? { assigneeUid } : {}),
|
|
157
|
+
...(clearAssignee ? { clearAssignee: true } : {}),
|
|
158
|
+
});
|
|
121
159
|
if (opts.json) {
|
|
122
160
|
console.log(JSON.stringify({ ok: true, action: "story", company, ...patched, storyId }, null, 2));
|
|
123
161
|
return;
|
|
124
162
|
}
|
|
125
|
-
|
|
163
|
+
const assignment = assignee === "none"
|
|
164
|
+
? "assignee cleared"
|
|
165
|
+
: assignee
|
|
166
|
+
? `assignee ${patched.assigneeUid ?? assigneeUid}`
|
|
167
|
+
: "";
|
|
168
|
+
const statusPart = status ? `${storyId} → ${patched.status}` : storyId;
|
|
169
|
+
console.log(`Work mesh: story ${statusPart}${assignment ? ` ${assignment}` : ""} (v${patched.version ?? "?"})`);
|
|
170
|
+
}
|
|
171
|
+
function record(value) {
|
|
172
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
173
|
+
? value
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
function parseProjectRepoSpec(value, source) {
|
|
177
|
+
const raw = value.trim();
|
|
178
|
+
const separator = raw.lastIndexOf("@");
|
|
179
|
+
const path = (separator > 0 ? raw.slice(0, separator) : raw).trim();
|
|
180
|
+
const branch = (separator > 0 ? raw.slice(separator + 1) : "").trim();
|
|
181
|
+
if (!path)
|
|
182
|
+
fail(`${source} requires a non-empty repository path.`);
|
|
183
|
+
if (separator > 0 && !branch)
|
|
184
|
+
fail(`${source} has an empty branch after @.`);
|
|
185
|
+
return { path, branch };
|
|
186
|
+
}
|
|
187
|
+
function parseProjectRepos(value, source) {
|
|
188
|
+
if (!Array.isArray(value))
|
|
189
|
+
fail(`${source} must be a JSON repository array.`);
|
|
190
|
+
return value.map((raw, index) => {
|
|
191
|
+
if (typeof raw === "string")
|
|
192
|
+
return parseProjectRepoSpec(raw, `${source}[${index}]`);
|
|
193
|
+
const repo = record(raw);
|
|
194
|
+
const path = typeof repo?.path === "string"
|
|
195
|
+
? repo.path.trim()
|
|
196
|
+
: typeof repo?.repoPath === "string"
|
|
197
|
+
? repo.repoPath.trim()
|
|
198
|
+
: typeof repo?.repo === "string"
|
|
199
|
+
? repo.repo.trim()
|
|
200
|
+
: "";
|
|
201
|
+
const branchValue = repo?.branch ?? repo?.branchName;
|
|
202
|
+
const branch = branchValue === undefined ? "" : typeof branchValue === "string" ? branchValue.trim() : null;
|
|
203
|
+
if (!repo || !path)
|
|
204
|
+
fail(`${source}[${index}].path requires a string.`);
|
|
205
|
+
if (branch === null)
|
|
206
|
+
fail(`${source}[${index}].branch requires a string.`);
|
|
207
|
+
return { path, branch };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function parseProjectReposJson(raw, source) {
|
|
211
|
+
try {
|
|
212
|
+
return parseProjectRepos(JSON.parse(raw), source);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err instanceof SyntaxError)
|
|
216
|
+
fail(`${source} must contain valid JSON.`);
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function projectStoriesFromJson(value, source) {
|
|
221
|
+
const plan = Array.isArray(value) ? null : record(value);
|
|
222
|
+
const rawStories = Array.isArray(value)
|
|
223
|
+
? value
|
|
224
|
+
: Array.isArray(plan?.userStories)
|
|
225
|
+
? plan.userStories
|
|
226
|
+
: Array.isArray(plan?.stories)
|
|
227
|
+
? plan.stories
|
|
228
|
+
: null;
|
|
229
|
+
if (!rawStories) {
|
|
230
|
+
fail(`${source} must be a JSON story array or an object with userStories.`);
|
|
231
|
+
}
|
|
232
|
+
const stories = rawStories.map((raw, index) => {
|
|
233
|
+
const story = record(raw);
|
|
234
|
+
const id = typeof story?.id === "string" ? story.id.trim() : "";
|
|
235
|
+
const title = typeof story?.title === "string" ? story.title.trim() : "";
|
|
236
|
+
if (!story || !id || !title) {
|
|
237
|
+
fail(`${source} userStories[${index}] requires string id and title.`);
|
|
238
|
+
}
|
|
239
|
+
const status = typeof story.status === "string" ? story.status.trim() : undefined;
|
|
240
|
+
if (status && !STORY_STATUSES.has(status)) {
|
|
241
|
+
fail(`${source} userStories[${index}].status must be queued|in_progress|review|done.`);
|
|
242
|
+
}
|
|
243
|
+
const resolvedStatus = status || (story.passes === true ? "done" : undefined);
|
|
244
|
+
return {
|
|
245
|
+
id,
|
|
246
|
+
title,
|
|
247
|
+
...(typeof story.description === "string" ? { description: story.description } : {}),
|
|
248
|
+
...(Array.isArray(story.acceptanceCriteria)
|
|
249
|
+
? {
|
|
250
|
+
acceptanceCriteria: story.acceptanceCriteria.filter((item) => typeof item === "string"),
|
|
251
|
+
}
|
|
252
|
+
: {}),
|
|
253
|
+
...(resolvedStatus ? { status: resolvedStatus } : {}),
|
|
254
|
+
// hq-pro makes passes mirror the Board status. A completed PRD story
|
|
255
|
+
// without an explicit status must therefore become done.
|
|
256
|
+
passes: resolvedStatus === "done",
|
|
257
|
+
...(typeof story.priority === "number" && Number.isFinite(story.priority)
|
|
258
|
+
? { priority: story.priority }
|
|
259
|
+
: {}),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
const metadata = record(plan?.metadata);
|
|
263
|
+
const repos = metadata && Object.hasOwn(metadata, "repos")
|
|
264
|
+
? parseProjectRepos(metadata.repos, `${source} metadata.repos`)
|
|
265
|
+
: undefined;
|
|
266
|
+
return {
|
|
267
|
+
...(typeof plan?.name === "string" ? { name: plan.name } : {}),
|
|
268
|
+
...(typeof plan?.description === "string" ? { description: plan.description } : {}),
|
|
269
|
+
stories,
|
|
270
|
+
...(repos ? { repos } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function parseProjectPlan(raw, source) {
|
|
274
|
+
try {
|
|
275
|
+
return projectStoriesFromJson(JSON.parse(raw), source);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (err instanceof SyntaxError)
|
|
279
|
+
fail(`${source} must contain valid JSON.`);
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function runProjectSet(projectId, opts) {
|
|
284
|
+
if (opts.storiesFile && opts.storiesJson) {
|
|
285
|
+
fail("Pass only one of --stories-file or --stories-json.");
|
|
286
|
+
}
|
|
287
|
+
if (opts.reposJson && opts.repo?.length) {
|
|
288
|
+
fail("Pass only one of --repos-json or --repo.");
|
|
289
|
+
}
|
|
290
|
+
const source = opts.storiesFile
|
|
291
|
+
? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile)
|
|
292
|
+
: opts.storiesJson
|
|
293
|
+
? parseProjectPlan(opts.storiesJson, "--stories-json")
|
|
294
|
+
: undefined;
|
|
295
|
+
const explicitRepos = opts.reposJson
|
|
296
|
+
? parseProjectReposJson(opts.reposJson, "--repos-json")
|
|
297
|
+
: opts.repo?.length
|
|
298
|
+
? opts.repo.map((repo, index) => parseProjectRepoSpec(repo, `--repo[${index}]`))
|
|
299
|
+
: undefined;
|
|
300
|
+
const { token, company } = await withCompany(opts);
|
|
301
|
+
const current = await getProjectView(token, company.companyUid, projectId);
|
|
302
|
+
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
303
|
+
const view = await putProjectView(token, company.companyUid, projectId, {
|
|
304
|
+
name: opts.name?.trim() || source?.name || current.name,
|
|
305
|
+
...(opts.description !== undefined
|
|
306
|
+
? { description: opts.description }
|
|
307
|
+
: source?.description !== undefined
|
|
308
|
+
? { description: source.description }
|
|
309
|
+
: { description: current.description }),
|
|
310
|
+
stories: source?.stories ?? current.stories,
|
|
311
|
+
repos: explicitRepos ?? source?.repos ?? current.repos,
|
|
312
|
+
...(current.version !== undefined ? { version: current.version } : {}),
|
|
313
|
+
});
|
|
314
|
+
if (opts.json) {
|
|
315
|
+
console.log(JSON.stringify({ ok: true, action: "project.set", principalUid, ...view }, null, 2));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
console.log(`Work mesh: project ${view.projectId} set (${view.name}; ${view.stories.length} stories)`);
|
|
319
|
+
}
|
|
320
|
+
async function runProjectRegister(projectId, opts) {
|
|
321
|
+
if (opts.channel?.trim()) {
|
|
322
|
+
fail("--channel is not supported by the current Work Mesh registration API; omit it to let the server create or bind the project channel.");
|
|
323
|
+
}
|
|
324
|
+
const { token, company } = await withCompany(opts);
|
|
325
|
+
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
326
|
+
const registration = await registerMeshProject(token, company.companyUid, projectId);
|
|
327
|
+
const boardState = "registered";
|
|
328
|
+
if (opts.json) {
|
|
329
|
+
console.log(JSON.stringify({ ok: true, action: "project.register", principalUid, boardState, ...registration }, null, 2));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.log(`Work mesh: project ${registration.projectId} registered (${boardState}; channel ${registration.channelId} ${registration.channelCreated ? "created" : "bound"})`);
|
|
126
333
|
}
|
|
127
334
|
async function runDoctor(opts) {
|
|
128
335
|
if (opts.apply) {
|
|
@@ -131,6 +338,9 @@ async function runDoctor(opts) {
|
|
|
131
338
|
}
|
|
132
339
|
const token = await requireToken();
|
|
133
340
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
341
|
+
if (!principalUid) {
|
|
342
|
+
fail("Could not resolve the authenticated principal. Sign in again and retry `hq mesh doctor`.");
|
|
343
|
+
}
|
|
134
344
|
const warmed = await warmMeshConversationCache(token, principalUid);
|
|
135
345
|
if (opts.json) {
|
|
136
346
|
console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed }, null, 2));
|
|
@@ -346,19 +556,20 @@ async function runContextBackfillHeld(opts) {
|
|
|
346
556
|
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 0;
|
|
347
557
|
const companyHint = opts.company?.trim() || undefined;
|
|
348
558
|
const useDeviceDefault = opts.useDeviceDefault === true || readDeviceConfig({ root }).repairHeldWithDefault === true;
|
|
349
|
-
// Build the
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
// narrowing, unlike the inline reconcile handler).
|
|
559
|
+
// Build the membership seam for both modes; dry-run is read-only but must
|
|
560
|
+
// validate a locally resolved slug just as live reconciliation does.
|
|
561
|
+
// The remaining seams are captured in the reconcile closure below.
|
|
353
562
|
let deliver;
|
|
354
563
|
let fetchCandidates;
|
|
355
564
|
let validateMembership;
|
|
356
565
|
let offline = Boolean(opts.offline);
|
|
357
|
-
if (!
|
|
566
|
+
if (!offline) {
|
|
358
567
|
try {
|
|
359
568
|
const token = await requireToken();
|
|
360
|
-
|
|
361
|
-
|
|
569
|
+
if (!dryRun) {
|
|
570
|
+
deliver = createWorkSessionDeliverer({ token });
|
|
571
|
+
fetchCandidates = createCandidatesFetcher({ token });
|
|
572
|
+
}
|
|
362
573
|
validateMembership = async (candidate) => {
|
|
363
574
|
const membership = await resolveActiveMembershipCompany(token, candidate);
|
|
364
575
|
if (!membership)
|
|
@@ -383,6 +594,7 @@ async function runContextBackfillHeld(opts) {
|
|
|
383
594
|
// so `--dry-run` and the real run report the same counts.
|
|
384
595
|
remoteOwnerHint: companyHint || undefined,
|
|
385
596
|
useDeviceDefault,
|
|
597
|
+
validateMembership,
|
|
386
598
|
reconcile: (obs) => reconcileObservation(
|
|
387
599
|
// Pass --company as a non-forcing hint (remoteOwnerSlug feeds
|
|
388
600
|
// deterministic resolution BELOW the identity-file / device default,
|
|
@@ -410,6 +622,9 @@ async function runContextBackfillHeld(opts) {
|
|
|
410
622
|
`${result.unresolved} unresolved, ` +
|
|
411
623
|
`${result.errors} error(s).` +
|
|
412
624
|
(dryRun ? " (dry-run: nothing written)" : ""));
|
|
625
|
+
for (const { sessionId, reason } of result.unresolvedReasons) {
|
|
626
|
+
console.log(`Unresolved ${sessionId}: ${reason}`);
|
|
627
|
+
}
|
|
413
628
|
if (!dryRun && result.reconciled > 0) {
|
|
414
629
|
console.log("Reconciled session state files were rewritten with a resolved company; " +
|
|
415
630
|
"the mesh daemon's held retry will re-attribute and post those events.");
|
|
@@ -1075,9 +1290,32 @@ export function registerMeshCommand(program) {
|
|
|
1075
1290
|
.option("--summary <text>", "Note text")).action((opts) => wrap(() => runEvent("note", opts))());
|
|
1076
1291
|
addSharedFlags(mesh
|
|
1077
1292
|
.command("story")
|
|
1078
|
-
.description("PATCH one Board story status")
|
|
1293
|
+
.description("PATCH one Board story status and/or assignee")
|
|
1079
1294
|
.requiredOption("--story <id>", "Story id, e.g. US-001")
|
|
1080
|
-
.
|
|
1295
|
+
.option("--status <status>", "queued|in_progress|review|done")
|
|
1296
|
+
.option("--assignee <uid|me|none>", "Assign a person or agent uid, yourself, or clear")).action((opts) => wrap(() => runStory(opts))());
|
|
1297
|
+
const project = mesh
|
|
1298
|
+
.command("project")
|
|
1299
|
+
.description("Set a project Board view or register its project channel");
|
|
1300
|
+
project
|
|
1301
|
+
.command("set <projectId>")
|
|
1302
|
+
.description("PUT the project Board view from flags or a PRD JSON file")
|
|
1303
|
+
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1304
|
+
.option("--name <name>", "Project name")
|
|
1305
|
+
.option("--description <text>", "Project description")
|
|
1306
|
+
.option("--stories-file <path>", "PRD JSON file with userStories")
|
|
1307
|
+
.option("--stories-json <json>", "Story array or JSON object with userStories")
|
|
1308
|
+
.option("--repos-json <json>", "Repository array, such as [{path,branch}]")
|
|
1309
|
+
.option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
|
|
1310
|
+
.option("--json", "Print machine-readable JSON")
|
|
1311
|
+
.action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
|
|
1312
|
+
project
|
|
1313
|
+
.command("register <projectId>")
|
|
1314
|
+
.description("POST project registration to create or bind its project channel and Board")
|
|
1315
|
+
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1316
|
+
.option("--channel <channelId>", "Reserved: the current server chooses the project channel")
|
|
1317
|
+
.option("--json", "Print machine-readable JSON")
|
|
1318
|
+
.action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
|
|
1081
1319
|
mesh
|
|
1082
1320
|
.command("doctor")
|
|
1083
1321
|
.description("Warm ~/.hq/work-mesh/cache from hq-pro (directory, inbox, pair DMs). Not `hq doctor`.")
|
|
@@ -1107,7 +1345,7 @@ export function registerMeshCommand(program) {
|
|
|
1107
1345
|
context
|
|
1108
1346
|
.command("backfill-held")
|
|
1109
1347
|
.description("Reconcile ENDED sessions whose held events lack a company so the daemon re-attributes the backlog (explicit, opt-in; no fleet fan-out)")
|
|
1110
|
-
.option("--dry-run", "Report what would be reconciled; write nothing
|
|
1348
|
+
.option("--dry-run", "Report what would be reconciled; write nothing or deliver events")
|
|
1111
1349
|
.option("--offline", "Reconcile outbox-only (no candidates/register network)")
|
|
1112
1350
|
.option("--limit <n>", "Cap sessions processed (default 0 = no cap)")
|
|
1113
1351
|
.option("--company <slug>", "Optional non-forcing company hint")
|
|
@@ -1227,11 +1465,13 @@ export function registerMeshCommand(program) {
|
|
|
1227
1465
|
daemon
|
|
1228
1466
|
.command("run")
|
|
1229
1467
|
.description("Start the single-instance daemon (pid lock, spool watch, presence)")
|
|
1230
|
-
.
|
|
1468
|
+
.option("--hq-root <path>", "HQ tree root (else HQ_ROOT / persisted trusted roots); the Windows task passes this explicitly")
|
|
1469
|
+
.action((opts) => wrap(() => runDaemonRun(opts))());
|
|
1231
1470
|
daemon
|
|
1232
1471
|
.command("install")
|
|
1233
|
-
.description("Install LaunchAgent (macOS)
|
|
1472
|
+
.description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
|
|
1234
1473
|
.option("--system-user <user>", "Write a root-owned unit template for agent boxes (e.g. ec2-user)")
|
|
1474
|
+
.option("--hq-root <path>", "HQ root to record for the service (else HQ_ROOT, else the nearest HQ root above cwd)")
|
|
1235
1475
|
.option("--json", "Print machine-readable JSON")
|
|
1236
1476
|
.action((opts) => wrap(() => runDaemonInstall(opts))());
|
|
1237
1477
|
daemon
|
|
@@ -1250,19 +1490,24 @@ export function registerMeshCommand(program) {
|
|
|
1250
1490
|
.option("--json", "Print machine-readable JSON")
|
|
1251
1491
|
.action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
|
|
1252
1492
|
}
|
|
1253
|
-
async function runDaemonRun() {
|
|
1254
|
-
const
|
|
1493
|
+
async function runDaemonRun(opts = {}) {
|
|
1494
|
+
const hqRoot = opts.hqRoot?.trim() || undefined;
|
|
1495
|
+
const handle = await runMeshDaemon(hqRoot ? { hqRoot } : {});
|
|
1255
1496
|
// Stay alive until signal handlers call stop + exit.
|
|
1256
1497
|
await new Promise(() => {
|
|
1257
1498
|
void handle;
|
|
1258
1499
|
});
|
|
1259
1500
|
}
|
|
1260
1501
|
async function runDaemonInstall(opts) {
|
|
1502
|
+
// Verified root only: a repo's own .claude/ must never become the daemon's
|
|
1503
|
+
// HQ root (that path is persisted as trusted and used for attribution).
|
|
1504
|
+
const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
|
|
1261
1505
|
const result = installDaemonService({
|
|
1262
1506
|
platform: detectPlatform(),
|
|
1263
1507
|
paths: buildInstallPaths({
|
|
1264
1508
|
systemUser: opts.systemUser,
|
|
1265
|
-
hqRoot:
|
|
1509
|
+
hqRoot: hqRoot.root,
|
|
1510
|
+
hqRootSource: hqRoot.source,
|
|
1266
1511
|
}),
|
|
1267
1512
|
systemUser: opts.systemUser,
|
|
1268
1513
|
});
|
|
@@ -1273,10 +1518,15 @@ async function runDaemonInstall(opts) {
|
|
|
1273
1518
|
platform: result.platform,
|
|
1274
1519
|
dest: result.dest,
|
|
1275
1520
|
message: result.message,
|
|
1521
|
+
hqRoot: hqRoot.root,
|
|
1522
|
+
hqRootSource: hqRoot.source,
|
|
1523
|
+
...(result.notice ? { notice: result.notice } : {}),
|
|
1276
1524
|
}, null, 2));
|
|
1277
1525
|
return;
|
|
1278
1526
|
}
|
|
1279
1527
|
console.log(result.message);
|
|
1528
|
+
if (result.notice)
|
|
1529
|
+
console.log(result.notice);
|
|
1280
1530
|
}
|
|
1281
1531
|
async function runDaemonUninstall(opts) {
|
|
1282
1532
|
const result = uninstallDaemonService({
|
|
@@ -1310,12 +1560,21 @@ async function runDaemonStatus(opts) {
|
|
|
1310
1560
|
running: result.running ?? false,
|
|
1311
1561
|
dest: result.dest,
|
|
1312
1562
|
message: result.message,
|
|
1563
|
+
...(result.lastResult !== undefined ? { lastResult: result.lastResult } : {}),
|
|
1564
|
+
...(result.lastRunFailed !== undefined ? { lastRunFailed: result.lastRunFailed } : {}),
|
|
1565
|
+
...(result.recordedHqRoot !== undefined ? { hqRoot: result.recordedHqRoot } : {}),
|
|
1566
|
+
...(result.rootValid !== undefined ? { rootValid: result.rootValid } : {}),
|
|
1567
|
+
...(result.notice ? { notice: result.notice } : {}),
|
|
1313
1568
|
mqttState: state?.mqttState,
|
|
1314
1569
|
presenceRefusal,
|
|
1315
1570
|
}, null, 2));
|
|
1316
1571
|
return;
|
|
1317
1572
|
}
|
|
1318
1573
|
console.log(result.message);
|
|
1574
|
+
if (result.recordedHqRoot)
|
|
1575
|
+
console.log(`hq root: ${result.recordedHqRoot}`);
|
|
1576
|
+
if (result.notice)
|
|
1577
|
+
console.log(result.notice);
|
|
1319
1578
|
if (presenceRefusal) {
|
|
1320
1579
|
console.log(`presence refusal: ${presenceRefusal.code} (HTTP ${presenceRefusal.status}); next retry at ${presenceRefusal.nextRetryAt}`);
|
|
1321
1580
|
}
|
|
@@ -1324,7 +1583,11 @@ async function runDaemonStatus(opts) {
|
|
|
1324
1583
|
}
|
|
1325
1584
|
}
|
|
1326
1585
|
async function runDaemonDoctorCmd(opts) {
|
|
1327
|
-
|
|
1586
|
+
// Same predicate as install: a repo's .claude/ is not an HQ root. Doctor
|
|
1587
|
+
// only reads, so an unresolvable root is reported as absent, not fatal.
|
|
1588
|
+
const report = collectDaemonDoctor({
|
|
1589
|
+
hqRoot: process.env.HQ_ROOT?.trim() || findAncestorHqRoot(process.cwd()) || undefined,
|
|
1590
|
+
});
|
|
1328
1591
|
if (opts.json) {
|
|
1329
1592
|
console.log(JSON.stringify({ ok: !report.unhealthy, ...report }, null, 2));
|
|
1330
1593
|
return;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a new company is created for, when `hq onboard create-company` is run
|
|
3
|
+
* without `--email` / `--person-name`.
|
|
4
|
+
*
|
|
5
|
+
* The signed-in account already carries both on its ID token. Asking the
|
|
6
|
+
* person to type their own name again — which is what the setup bot had to do,
|
|
7
|
+
* because `hq whoami` shows only the email — is a question HQ can answer
|
|
8
|
+
* itself. Explicit flags still win.
|
|
9
|
+
*/
|
|
10
|
+
export interface CreateCompanyIdentity {
|
|
11
|
+
email: string;
|
|
12
|
+
personName: string;
|
|
13
|
+
}
|
|
14
|
+
export type CreateCompanyIdentityResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
identity: CreateCompanyIdentity;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
/** Pure: flags first, then the ID token's claims. */
|
|
22
|
+
export declare function resolveCreateCompanyIdentity(flags: {
|
|
23
|
+
email?: string;
|
|
24
|
+
personName?: string;
|
|
25
|
+
}, claims: Record<string, unknown>): CreateCompanyIdentityResult;
|
|
26
|
+
//# sourceMappingURL=onboard-create-identity.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function text(value) {
|
|
2
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3
|
+
}
|
|
4
|
+
/** Pure: flags first, then the ID token's claims. */
|
|
5
|
+
export function resolveCreateCompanyIdentity(flags, claims) {
|
|
6
|
+
const email = text(flags.email) || text(claims.email);
|
|
7
|
+
const personName = text(flags.personName) ||
|
|
8
|
+
text(claims.name) ||
|
|
9
|
+
[text(claims.given_name), text(claims.family_name)].filter(Boolean).join(" ");
|
|
10
|
+
const missing = [];
|
|
11
|
+
if (!email)
|
|
12
|
+
missing.push("--email");
|
|
13
|
+
if (!personName)
|
|
14
|
+
missing.push("--person-name");
|
|
15
|
+
if (missing.length > 0) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
message: `Your HQ sign-in does not say ${missing.length === 2 ? "your email or name" : missing[0] === "--email" ? "your email" : "your name"}. Pass ${missing.join(" and ")}.`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, identity: { email, personName } };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=onboard-create-identity.js.map
|
package/dist/commands/onboard.js
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import chalk from "chalk";
|
|
22
22
|
import { runOnboardCli, readCheckpoint } from "../lib/onboarding/index.js";
|
|
23
|
-
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
23
|
+
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoIdToken, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
24
24
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
25
25
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
|
26
26
|
import { planOnboardJoin } from "./onboard-join.js";
|
|
27
27
|
import { buildCreateCompanyWarning } from "./onboard-warning.js";
|
|
28
|
+
import { resolveCreateCompanyIdentity } from "./onboard-create-identity.js";
|
|
29
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
28
30
|
import { addBotToNewCompany, botToAddAfterCreateCompany } from "./onboard-bot-membership.js";
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// Command registration
|
|
@@ -40,11 +42,18 @@ export function registerOnboardCommand(program) {
|
|
|
40
42
|
"a same-named company makes a separate one you'd be alone in.")
|
|
41
43
|
.requiredOption("--slug <slug>", "Company slug (used as bucket name suffix)")
|
|
42
44
|
.requiredOption("--name <name>", "Company display name")
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
+
.option("--email <email>", "Your email (default: your HQ sign-in's email; must match it)")
|
|
46
|
+
.option("--person-name <name>", "Your display name (default: the name on your HQ sign-in)")
|
|
45
47
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
46
|
-
.action(async (
|
|
48
|
+
.action(async (flags) => {
|
|
47
49
|
try {
|
|
50
|
+
const idToken = await ensureCognitoIdToken();
|
|
51
|
+
const resolved = resolveCreateCompanyIdentity(flags, peekIdToken(idToken));
|
|
52
|
+
if (!resolved.ok) {
|
|
53
|
+
console.error(chalk.red(`\n✗ ${resolved.message}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const options = { ...flags, ...resolved.identity };
|
|
48
57
|
console.log(chalk.bold(`\nHQ Onboard — Create Company`));
|
|
49
58
|
console.log(` Company: ${options.name} (${options.slug})`);
|
|
50
59
|
console.log(` Person: ${options.personName} <${options.email}>`);
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* - the final answer is posted only when it adds something: text already
|
|
16
16
|
* posted (or about to be) is never repeated, including Grok's final result
|
|
17
17
|
* that is just every message concatenated
|
|
18
|
+
* - an in-between message that asks the person something (it has a "?") is
|
|
19
|
+
* held, not posted: the model often asks, keeps working, then asks the same
|
|
20
|
+
* thing again in its final answer, and the person read the question twice.
|
|
21
|
+
* It goes out only if the turn ends without a final answer of its own
|
|
18
22
|
*
|
|
19
23
|
* Posts are sent strictly in order. A failed post is logged by the caller's
|
|
20
24
|
* `send` and does not stop the turn.
|
|
@@ -46,6 +50,8 @@ export declare class ProgressPoster {
|
|
|
46
50
|
private readonly workingNoticeMs;
|
|
47
51
|
private readonly workingNoticeText;
|
|
48
52
|
private pending;
|
|
53
|
+
/** In-between questions, held until the turn ends (see the header). */
|
|
54
|
+
private heldQuestions;
|
|
49
55
|
/** Every message the model has said this turn (posted or pending), in order. */
|
|
50
56
|
private readonly said;
|
|
51
57
|
private flushTimer;
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* - the final answer is posted only when it adds something: text already
|
|
16
16
|
* posted (or about to be) is never repeated, including Grok's final result
|
|
17
17
|
* that is just every message concatenated
|
|
18
|
+
* - an in-between message that asks the person something (it has a "?") is
|
|
19
|
+
* held, not posted: the model often asks, keeps working, then asks the same
|
|
20
|
+
* thing again in its final answer, and the person read the question twice.
|
|
21
|
+
* It goes out only if the turn ends without a final answer of its own
|
|
18
22
|
*
|
|
19
23
|
* Posts are sent strictly in order. A failed post is logged by the caller's
|
|
20
24
|
* `send` and does not stop the turn.
|
|
@@ -37,6 +41,8 @@ export class ProgressPoster {
|
|
|
37
41
|
workingNoticeMs;
|
|
38
42
|
workingNoticeText;
|
|
39
43
|
pending = [];
|
|
44
|
+
/** In-between questions, held until the turn ends (see the header). */
|
|
45
|
+
heldQuestions = [];
|
|
40
46
|
/** Every message the model has said this turn (posted or pending), in order. */
|
|
41
47
|
said = [];
|
|
42
48
|
flushTimer = null;
|
|
@@ -75,6 +81,10 @@ export class ProgressPoster {
|
|
|
75
81
|
return;
|
|
76
82
|
this.cancelNotice();
|
|
77
83
|
this.said.push(t);
|
|
84
|
+
if (t.includes("?")) {
|
|
85
|
+
this.heldQuestions.push(t);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
78
88
|
this.pending.push(t);
|
|
79
89
|
this.scheduleFlush();
|
|
80
90
|
}
|
|
@@ -97,8 +107,12 @@ export class ProgressPoster {
|
|
|
97
107
|
this.cancelNotice();
|
|
98
108
|
this.cancelFlush();
|
|
99
109
|
const final = finalText.trim();
|
|
110
|
+
const held = this.heldQuestions;
|
|
111
|
+
this.heldQuestions = [];
|
|
100
112
|
if (final && !this.repeatsSaid(final))
|
|
101
113
|
this.pending.push(final);
|
|
114
|
+
else if (held.length > 0)
|
|
115
|
+
this.pending.push(...held);
|
|
102
116
|
if (this.pending.length === 0 && this.delivered === 0 && !final)
|
|
103
117
|
this.pending.push("(no reply)");
|
|
104
118
|
this.flushNow();
|
|
@@ -114,6 +128,8 @@ export class ProgressPoster {
|
|
|
114
128
|
this.finished = true;
|
|
115
129
|
this.cancelNotice();
|
|
116
130
|
this.cancelFlush();
|
|
131
|
+
this.pending.push(...this.heldQuestions);
|
|
132
|
+
this.heldQuestions = [];
|
|
117
133
|
this.flushNow();
|
|
118
134
|
return this.chain;
|
|
119
135
|
}
|