@indigoai-us/hq-cli 5.144.0 → 5.146.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 +19 -0
- package/dist/command-catalog.generated.d.ts +24 -6
- package/dist/command-catalog.generated.js +29 -6
- package/dist/commands/_patterns.d.ts +16 -0
- package/dist/commands/_patterns.js +20 -3
- package/dist/commands/files-browse.d.ts +21 -10
- package/dist/commands/files-browse.js +95 -32
- package/dist/commands/files.js +44 -13
- package/dist/commands/mesh.js +115 -1
- package/dist/commands/report.d.ts +13 -0
- package/dist/commands/report.js +35 -13
- package/dist/commands/sync-narrow.js +36 -5
- package/dist/commands/workers.js +5 -2
- package/dist/lib/mesh/live/daemon/credentials.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/credentials.js +141 -10
- package/dist/lib/mesh/live/daemon/doctor.js +4 -3
- package/dist/lib/mesh/live/daemon/presence.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/presence.js +69 -1
- package/dist/lib/mesh/live/daemon/run.js +3 -1
- package/dist/lib/mesh/project-board.d.ts +14 -0
- package/dist/lib/mesh/project-board.js +24 -1
- package/dist/lib/work-context/bind-best.d.ts +2 -0
- package/dist/lib/work-context/bind-best.js +34 -1
- package/dist/lib/work-context/bind-calling-session.d.ts +26 -0
- package/dist/lib/work-context/bind-calling-session.js +63 -0
- package/dist/lib/work-context/prd-sync.d.ts +34 -0
- package/dist/lib/work-context/prd-sync.js +120 -0
- package/dist/lib/work-context/prompt-rebind.d.ts +21 -0
- package/dist/lib/work-context/prompt-rebind.js +76 -0
- package/dist/lib/work-context/reconcile.d.ts +2 -0
- package/dist/lib/work-context/reconcile.js +31 -0
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-040: diff a bound project's prd.json and describe task_status events.
|
|
3
|
+
* Planning never touches the network. The snapshot is written only after
|
|
4
|
+
* the caller accepts the plan (commit), so a failed emit can retry.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { readSessionState } from "./state.js";
|
|
9
|
+
export function prdSnapshotPath(sessionId, root) {
|
|
10
|
+
return path.join(root, "sessions", `${sessionId}.prd-snapshot.json`);
|
|
11
|
+
}
|
|
12
|
+
export function readPrdSnapshot(sessionId, root) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(prdSnapshotPath(sessionId, root), "utf8"));
|
|
15
|
+
if (!parsed || typeof parsed !== "object" || !parsed.stories)
|
|
16
|
+
return null;
|
|
17
|
+
return { stories: parsed.stories, seq: typeof parsed.seq === "number" ? parsed.seq : 0 };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function writeSnapshot(sessionId, root, snapshot) {
|
|
24
|
+
const dest = prdSnapshotPath(sessionId, root);
|
|
25
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
26
|
+
fs.writeFileSync(dest, `${JSON.stringify(snapshot)}\n`, { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
export function prdBelongsToProject(file, projectId, companySlug) {
|
|
29
|
+
if (!companySlug)
|
|
30
|
+
return false;
|
|
31
|
+
const parts = path.resolve(file).split(path.sep);
|
|
32
|
+
if (parts[parts.length - 1] !== "prd.json")
|
|
33
|
+
return false;
|
|
34
|
+
const projectsAt = parts.lastIndexOf("projects");
|
|
35
|
+
return (projectsAt >= 2 &&
|
|
36
|
+
parts[projectsAt - 2] === "companies" &&
|
|
37
|
+
parts[projectsAt - 1] === companySlug &&
|
|
38
|
+
parts[projectsAt + 1] === projectId &&
|
|
39
|
+
parts.length === projectsAt + 3);
|
|
40
|
+
}
|
|
41
|
+
function storiesFromPrd(file) {
|
|
42
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
43
|
+
const rows = Array.isArray(parsed.userStories) ? parsed.userStories
|
|
44
|
+
: Array.isArray(parsed.stories) ? parsed.stories
|
|
45
|
+
: [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (!row || typeof row !== "object")
|
|
49
|
+
continue;
|
|
50
|
+
const id = row.id;
|
|
51
|
+
if (typeof id !== "string" || !id.trim())
|
|
52
|
+
continue;
|
|
53
|
+
const status = row.status;
|
|
54
|
+
out.push({
|
|
55
|
+
id: id.trim(),
|
|
56
|
+
passes: row.passes === true,
|
|
57
|
+
...(typeof status === "string" && status.trim() ? { status: status.trim() } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
export function planPrdSync(input) {
|
|
63
|
+
const empty = (skip) => ({
|
|
64
|
+
changed: false,
|
|
65
|
+
skip,
|
|
66
|
+
events: [],
|
|
67
|
+
seqStart: 0,
|
|
68
|
+
commit: () => { },
|
|
69
|
+
});
|
|
70
|
+
let state;
|
|
71
|
+
try {
|
|
72
|
+
state = readSessionState(input.sessionId, input.root);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return empty("unreadable");
|
|
76
|
+
}
|
|
77
|
+
if (!state)
|
|
78
|
+
return empty("no_state");
|
|
79
|
+
if (!state.projectId || (state.contextStatus !== "bound" && state.contextStatus !== "needs_task")) {
|
|
80
|
+
return empty("not_bound");
|
|
81
|
+
}
|
|
82
|
+
if (!prdBelongsToProject(input.file, state.projectId, state.companySlug))
|
|
83
|
+
return empty("different_project");
|
|
84
|
+
let stories;
|
|
85
|
+
try {
|
|
86
|
+
stories = storiesFromPrd(input.file);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return empty("unreadable");
|
|
90
|
+
}
|
|
91
|
+
const previous = readPrdSnapshot(input.sessionId, input.root);
|
|
92
|
+
const before = previous?.stories ?? {};
|
|
93
|
+
const events = [];
|
|
94
|
+
let taskId;
|
|
95
|
+
const nextStories = {};
|
|
96
|
+
for (const story of stories) {
|
|
97
|
+
nextStories[story.id] = {
|
|
98
|
+
passes: story.passes,
|
|
99
|
+
...(story.status ? { status: story.status } : {}),
|
|
100
|
+
};
|
|
101
|
+
const prior = previous ? before[story.id] : undefined;
|
|
102
|
+
if (story.passes && !prior?.passes) {
|
|
103
|
+
events.push({ taskId: story.id, status: "done" });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (story.status === "in_progress" && prior?.status !== "in_progress") {
|
|
107
|
+
events.push({ taskId: story.id, status: "in_progress" });
|
|
108
|
+
taskId = story.id;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const snapshot = { stories: nextStories, seq: (previous?.seq ?? 0) + events.length };
|
|
112
|
+
return {
|
|
113
|
+
changed: events.length > 0,
|
|
114
|
+
events,
|
|
115
|
+
...(taskId ? { taskId } : {}),
|
|
116
|
+
seqStart: previous?.seq ?? 0,
|
|
117
|
+
commit: () => writeSnapshot(input.sessionId, input.root, snapshot),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=prd-sync.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-040: UserPromptSubmit re-runs bind-best for a session still in
|
|
3
|
+
* needs_project. Catalog reads are cached. Failures stay local.
|
|
4
|
+
*/
|
|
5
|
+
export type PromptRebindResult = {
|
|
6
|
+
action: "bound" | "skipped" | "unchanged";
|
|
7
|
+
projectId?: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function rebindNeedsProjectOnPrompt(input: {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
root: string;
|
|
13
|
+
hqRoot?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
companySlug?: string;
|
|
16
|
+
companyUid?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
nowMs?: number;
|
|
19
|
+
now?: () => Date;
|
|
20
|
+
}): PromptRebindResult;
|
|
21
|
+
//# sourceMappingURL=prompt-rebind.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-040: UserPromptSubmit re-runs bind-best for a session still in
|
|
3
|
+
* needs_project. Catalog reads are cached. Failures stay local.
|
|
4
|
+
*/
|
|
5
|
+
import { projectIdFromCwd } from "./company.js";
|
|
6
|
+
import { readMetaProjectTask } from "./project.js";
|
|
7
|
+
import { refineProjectBinding } from "./bind-best.js";
|
|
8
|
+
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
9
|
+
import { readSessionState, updateSessionState } from "./state.js";
|
|
10
|
+
export function rebindNeedsProjectOnPrompt(input) {
|
|
11
|
+
let state;
|
|
12
|
+
try {
|
|
13
|
+
state = readSessionState(input.sessionId, input.root);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { action: "skipped", reason: "state_unreadable" };
|
|
17
|
+
}
|
|
18
|
+
if (!state)
|
|
19
|
+
return { action: "skipped", reason: "no_state" };
|
|
20
|
+
if (state.contextStatus !== "needs_project") {
|
|
21
|
+
return { action: "skipped", reason: "not_needs_project" };
|
|
22
|
+
}
|
|
23
|
+
if (state.projectId)
|
|
24
|
+
return { action: "skipped", reason: "different_project" };
|
|
25
|
+
const env = input.env ?? process.env;
|
|
26
|
+
const hint = state.projectHint?.trim() ||
|
|
27
|
+
env.HQ_SPAWN_PROJECT?.trim() ||
|
|
28
|
+
readMetaProjectTask(input.sessionId, input.hqRoot).projectId ||
|
|
29
|
+
projectIdFromCwd(input.cwd, input.hqRoot) ||
|
|
30
|
+
undefined;
|
|
31
|
+
if (!hint)
|
|
32
|
+
return { action: "unchanged", reason: "no_hint" };
|
|
33
|
+
let refined;
|
|
34
|
+
try {
|
|
35
|
+
refined = refineProjectBinding({
|
|
36
|
+
hint,
|
|
37
|
+
hqRoot: input.hqRoot,
|
|
38
|
+
companySlug: input.companySlug ?? state.companySlug,
|
|
39
|
+
companyUid: input.companyUid ?? state.companyUid,
|
|
40
|
+
repoPath: input.cwd,
|
|
41
|
+
env,
|
|
42
|
+
nowMs: input.nowMs ?? Date.now(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { action: "unchanged", reason: err instanceof Error ? err.message : "bind_failed" };
|
|
47
|
+
}
|
|
48
|
+
if (refined.action !== "bind" && refined.action !== "create") {
|
|
49
|
+
return { action: "unchanged", reason: refined.action === "ask" ? refined.reason : "no_match" };
|
|
50
|
+
}
|
|
51
|
+
const projectId = refined.projectId;
|
|
52
|
+
const reason = refined.reason;
|
|
53
|
+
const nowIso = (input.now ?? (() => new Date()))().toISOString();
|
|
54
|
+
const updated = updateSessionState(input.sessionId, input.root, (current) => {
|
|
55
|
+
if (!current || current.contextStatus !== "needs_project")
|
|
56
|
+
return current;
|
|
57
|
+
if (current.projectId && current.projectId !== projectId)
|
|
58
|
+
return current;
|
|
59
|
+
return {
|
|
60
|
+
...current,
|
|
61
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
62
|
+
contextStatus: current.taskId ? "bound" : "needs_task",
|
|
63
|
+
projectId,
|
|
64
|
+
projectGate: undefined,
|
|
65
|
+
projectCandidates: undefined,
|
|
66
|
+
projectHint: undefined,
|
|
67
|
+
projectBindReason: reason,
|
|
68
|
+
updatedAt: nowIso,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
if (!updated || updated.projectId !== projectId) {
|
|
72
|
+
return { action: "skipped", reason: "not_applied" };
|
|
73
|
+
}
|
|
74
|
+
return { action: "bound", projectId, reason };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=prompt-rebind.js.map
|
|
@@ -39,6 +39,8 @@ export interface ReconcileObservation {
|
|
|
39
39
|
source?: "hooks" | "transcript";
|
|
40
40
|
/** Optional turn signal for the 10-minute ask threshold (US-007B). */
|
|
41
41
|
hasTurn?: boolean;
|
|
42
|
+
/** Hook name when the caller is a prompt submit. Session start omits this. */
|
|
43
|
+
hookEvent?: string;
|
|
42
44
|
toolWrites?: number;
|
|
43
45
|
/** Prohibited content may appear on the wire-in observation; must be stripped. */
|
|
44
46
|
prompt?: unknown;
|
|
@@ -10,6 +10,7 @@ import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
|
|
|
10
10
|
import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
|
|
11
11
|
import { decisionFromCandidates, } from "./organize.js";
|
|
12
12
|
import { refineProjectBinding } from "./bind-best.js";
|
|
13
|
+
import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
|
|
13
14
|
import { resolveProjectTask, shouldAskAfter } from "./project.js";
|
|
14
15
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
|
|
15
16
|
import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
|
|
@@ -509,6 +510,35 @@ export async function reconcileObservation(obs, deps) {
|
|
|
509
510
|
diagnostics.push(`project ask: ${refined.reason}`);
|
|
510
511
|
}
|
|
511
512
|
}
|
|
513
|
+
const promptSubmit = obs.hookEvent === "UserPromptSubmit" || obs.hookEvent === "user_prompt_submit";
|
|
514
|
+
if (promptSubmit && prior?.contextStatus === "needs_project" && !projectId && prior.companyUid) {
|
|
515
|
+
try {
|
|
516
|
+
const rebound = rebindNeedsProjectOnPrompt({
|
|
517
|
+
sessionId,
|
|
518
|
+
root: deps.root,
|
|
519
|
+
hqRoot,
|
|
520
|
+
cwd,
|
|
521
|
+
companySlug: resolvedCompany.slug ?? prior.companySlug,
|
|
522
|
+
companyUid: resolvedCompany.uid ?? prior.companyUid,
|
|
523
|
+
env,
|
|
524
|
+
nowMs: nowFn().getTime(),
|
|
525
|
+
now: nowFn,
|
|
526
|
+
});
|
|
527
|
+
if (rebound.action === "bound" && rebound.projectId) {
|
|
528
|
+
projectId = rebound.projectId;
|
|
529
|
+
projectBindReason = rebound.reason;
|
|
530
|
+
projectCandidates = undefined;
|
|
531
|
+
projectGate = undefined;
|
|
532
|
+
diagnostics.push(`prompt rebind: ${rebound.reason}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
diagnostics.push(`prompt rebind failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const projectHint = !projectId
|
|
540
|
+
? (prior?.projectHint || projectResolved?.projectId)
|
|
541
|
+
: undefined;
|
|
512
542
|
const bindResultFields = {
|
|
513
543
|
...(projectBindReason ? { projectBindReason } : {}),
|
|
514
544
|
...(projectCandidates ? { projectCandidates } : {}),
|
|
@@ -516,6 +546,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
516
546
|
const bindStateFields = {
|
|
517
547
|
...bindResultFields,
|
|
518
548
|
...(projectGate ? { projectGate } : {}),
|
|
549
|
+
...(projectHint ? { projectHint } : {}),
|
|
519
550
|
};
|
|
520
551
|
// Trusted / deterministic auto-bind never asks.
|
|
521
552
|
// Transcript-sourced sessions never ask (US-018).
|
|
@@ -104,6 +104,8 @@ export interface SessionStateFile {
|
|
|
104
104
|
* `choose` — the match was unclear. `missing_board` — the server 404'd the project.
|
|
105
105
|
*/
|
|
106
106
|
projectGate?: "choose" | "missing_board";
|
|
107
|
+
/** Slug the session was trying to bind. Re-checked on the next prompt. */
|
|
108
|
+
projectHint?: string;
|
|
107
109
|
updatedAt: string;
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
@@ -32,6 +32,7 @@ export const SESSION_STATE_ALLOWLIST = new Set([
|
|
|
32
32
|
"projectBindReason",
|
|
33
33
|
"projectCandidates",
|
|
34
34
|
"projectGate",
|
|
35
|
+
"projectHint",
|
|
35
36
|
"updatedAt",
|
|
36
37
|
]);
|
|
37
38
|
const SESSION_STATE_LOCK_TIMEOUT_MS = 5_000;
|
|
@@ -237,6 +238,8 @@ export function projectSessionState(state) {
|
|
|
237
238
|
out.projectCandidates = state.projectCandidates;
|
|
238
239
|
if (state.projectGate)
|
|
239
240
|
out.projectGate = state.projectGate;
|
|
241
|
+
if (state.projectHint && !state.projectId)
|
|
242
|
+
out.projectHint = state.projectHint;
|
|
240
243
|
return out;
|
|
241
244
|
}
|
|
242
245
|
/**
|
|
@@ -295,6 +298,11 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
|
|
|
295
298
|
if (!out.companyConfidence && prior.companyConfidence && sameCompanyBinding) {
|
|
296
299
|
out.companyConfidence = prior.companyConfidence;
|
|
297
300
|
}
|
|
301
|
+
if (!out.projectId && !out.projectHint && prior.projectHint) {
|
|
302
|
+
out.projectHint = prior.projectHint;
|
|
303
|
+
}
|
|
304
|
+
if (out.projectId)
|
|
305
|
+
out.projectHint = undefined;
|
|
298
306
|
const sameBinding = prior.companyUid === out.companyUid &&
|
|
299
307
|
prior.companySlug === out.companySlug &&
|
|
300
308
|
prior.projectId === out.projectId &&
|