@indigoai-us/hq-cli 5.345.152 → 5.345.154
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 +96 -0
- package/dist/bin/hq-auth-refresh.d.ts +4 -0
- package/dist/bin/hq-auth-refresh.js +9 -1
- package/dist/command-catalog.generated.d.ts +185 -0
- package/dist/command-catalog.generated.js +227 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +12 -0
- package/dist/commands/agent-enroll.js +37 -4
- package/dist/commands/cloud.d.ts +17 -0
- package/dist/commands/cloud.js +48 -1
- package/dist/commands/core-policy.js +4 -4
- package/dist/commands/core-session-project-native.js +19 -4
- package/dist/commands/decide.d.ts +13 -0
- package/dist/commands/decide.js +74 -0
- package/dist/commands/integrations-connect.js +24 -1
- package/dist/commands/integrations-core.d.ts +14 -0
- package/dist/commands/integrations-core.js +19 -1
- package/dist/commands/integrations-import.js +22 -1
- package/dist/commands/integrations.js +43 -2
- package/dist/commands/lanes-message.js +4 -11
- package/dist/commands/lanes-questions.js +3 -0
- package/dist/commands/lanes-resume.js +4 -11
- package/dist/commands/lanes-stories.js +3 -0
- package/dist/commands/lanes.js +7 -1
- package/dist/commands/run.js +14 -5
- package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
- package/dist/lib/agent-kit/adopt-identity.js +18 -3
- package/dist/lib/agent-kit/services.d.ts +3 -1
- package/dist/lib/agent-kit/services.js +5 -1
- package/dist/lib/daemon/ops/policy.d.ts +9 -1
- package/dist/lib/daemon/ops/policy.js +37 -2
- package/dist/lib/decisions/client.d.ts +67 -0
- package/dist/lib/decisions/client.js +120 -0
- package/dist/lib/decisions/expiry.d.ts +2 -0
- package/dist/lib/decisions/expiry.js +12 -0
- package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
- package/dist/lib/decisions/fixtures/us001.js +21 -0
- package/dist/lib/decisions/import-lanes.d.ts +16 -0
- package/dist/lib/decisions/import-lanes.js +152 -0
- package/dist/lib/decisions/principal.d.ts +11 -0
- package/dist/lib/decisions/principal.js +52 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
- package/dist/lib/doctor/checks/claude-wiring.js +21 -10
- package/dist/lib/doctor/checks/work-context.js +7 -2
- package/dist/lib/doctor/fix/apply.d.ts +3 -2
- package/dist/lib/doctor/fix/apply.js +19 -15
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
- package/dist/lib/doctor/fix/remediation.js +4 -2
- package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
- package/dist/lib/doctor/hook-gate-profiles.js +34 -0
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/install/provider-wrappers.js +5 -1
- package/dist/lib/lanes/autopilot.js +4 -1
- package/dist/lib/lanes/imported-question.d.ts +5 -0
- package/dist/lib/lanes/imported-question.js +17 -0
- package/dist/lib/lanes/questions.d.ts +0 -5
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +21 -10
- package/dist/lib/lanes/store.d.ts +0 -8
- package/dist/lib/lanes/store.js +2 -1
- package/dist/lib/lanes/stories.d.ts +15 -0
- package/dist/lib/lanes/stories.js +25 -0
- package/dist/lib/mesh/client.d.ts +8 -0
- package/dist/lib/mesh/client.js +44 -3
- package/dist/lib/mesh/denial-codes.js +1 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
- package/dist/lib/mesh/live/daemon/run.js +30 -1
- package/dist/lib/mesh/live/flush.d.ts +7 -0
- package/dist/lib/mesh/live/flush.js +53 -10
- package/dist/lib/registry/registry.js +6 -0
- package/dist/lib/scan-packages/index.js +7 -3
- package/dist/lib/sync/read-only-local-only.d.ts +45 -0
- package/dist/lib/sync/read-only-local-only.js +122 -0
- package/dist/lib/work-context/organize.d.ts +2 -0
- package/dist/lib/work-context/organize.js +22 -4
- package/dist/lib/workers-registry/index.d.ts +12 -1
- package/dist/lib/workers-registry/index.js +39 -1
- package/dist/utils/cognito-session.d.ts +4 -1
- package/dist/utils/cognito-session.js +11 -1
- package/package.json +7 -7
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Producer-service fixtures verified against hq-pro PR #5060 at 7c234efa (in-memory service output; handler wrappers). */
|
|
2
|
+
import type { Decision } from "../client.js";
|
|
3
|
+
export declare const openDecision: Decision;
|
|
4
|
+
export declare const decisionFixtures: {
|
|
5
|
+
create: {
|
|
6
|
+
decision: Decision;
|
|
7
|
+
};
|
|
8
|
+
list: {
|
|
9
|
+
decisions: Decision[];
|
|
10
|
+
};
|
|
11
|
+
answer: {
|
|
12
|
+
decision: {
|
|
13
|
+
state: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
answer: {
|
|
16
|
+
choiceId: string;
|
|
17
|
+
by: string;
|
|
18
|
+
at: string;
|
|
19
|
+
source: string;
|
|
20
|
+
};
|
|
21
|
+
id: string;
|
|
22
|
+
askerPrincipal: string;
|
|
23
|
+
companyUid: string;
|
|
24
|
+
audience: {
|
|
25
|
+
kind: "person";
|
|
26
|
+
personUid: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "company_admins";
|
|
29
|
+
};
|
|
30
|
+
statement: string;
|
|
31
|
+
options: import("../client.js").DecisionOption[];
|
|
32
|
+
context: {
|
|
33
|
+
links: string[];
|
|
34
|
+
brief: string;
|
|
35
|
+
commitmentId: string | null;
|
|
36
|
+
};
|
|
37
|
+
talkThreadId: string | null;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
expiresAt: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
talk: {
|
|
43
|
+
decision: {
|
|
44
|
+
state: string;
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
talkThreadId: null;
|
|
47
|
+
id: string;
|
|
48
|
+
askerPrincipal: string;
|
|
49
|
+
companyUid: string;
|
|
50
|
+
audience: {
|
|
51
|
+
kind: "person";
|
|
52
|
+
personUid: string;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "company_admins";
|
|
55
|
+
};
|
|
56
|
+
statement: string;
|
|
57
|
+
options: import("../client.js").DecisionOption[];
|
|
58
|
+
context: {
|
|
59
|
+
links: string[];
|
|
60
|
+
brief: string;
|
|
61
|
+
commitmentId: string | null;
|
|
62
|
+
};
|
|
63
|
+
answer: {
|
|
64
|
+
choiceId: string;
|
|
65
|
+
by: string;
|
|
66
|
+
at: string;
|
|
67
|
+
source: "tap" | "thread" | "desktop";
|
|
68
|
+
} | null;
|
|
69
|
+
createdAt: string;
|
|
70
|
+
expiresAt: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
withdraw: {
|
|
74
|
+
decision: {
|
|
75
|
+
state: string;
|
|
76
|
+
updatedAt: string;
|
|
77
|
+
id: string;
|
|
78
|
+
askerPrincipal: string;
|
|
79
|
+
companyUid: string;
|
|
80
|
+
audience: {
|
|
81
|
+
kind: "person";
|
|
82
|
+
personUid: string;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "company_admins";
|
|
85
|
+
};
|
|
86
|
+
statement: string;
|
|
87
|
+
options: import("../client.js").DecisionOption[];
|
|
88
|
+
context: {
|
|
89
|
+
links: string[];
|
|
90
|
+
brief: string;
|
|
91
|
+
commitmentId: string | null;
|
|
92
|
+
};
|
|
93
|
+
answer: {
|
|
94
|
+
choiceId: string;
|
|
95
|
+
by: string;
|
|
96
|
+
at: string;
|
|
97
|
+
source: "tap" | "thread" | "desktop";
|
|
98
|
+
} | null;
|
|
99
|
+
talkThreadId: string | null;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
expiresAt: string;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
expired: {
|
|
105
|
+
decision: {
|
|
106
|
+
state: string;
|
|
107
|
+
updatedAt: string;
|
|
108
|
+
id: string;
|
|
109
|
+
askerPrincipal: string;
|
|
110
|
+
companyUid: string;
|
|
111
|
+
audience: {
|
|
112
|
+
kind: "person";
|
|
113
|
+
personUid: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "company_admins";
|
|
116
|
+
};
|
|
117
|
+
statement: string;
|
|
118
|
+
options: import("../client.js").DecisionOption[];
|
|
119
|
+
context: {
|
|
120
|
+
links: string[];
|
|
121
|
+
brief: string;
|
|
122
|
+
commitmentId: string | null;
|
|
123
|
+
};
|
|
124
|
+
answer: {
|
|
125
|
+
choiceId: string;
|
|
126
|
+
by: string;
|
|
127
|
+
at: string;
|
|
128
|
+
source: "tap" | "thread" | "desktop";
|
|
129
|
+
} | null;
|
|
130
|
+
talkThreadId: string | null;
|
|
131
|
+
createdAt: string;
|
|
132
|
+
expiresAt: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
export declare const decisionRoutes: string[][];
|
|
137
|
+
//# sourceMappingURL=us001.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const openDecision = {
|
|
2
|
+
id: "f3ccdf93-9b7e-4564-a3ad-566067900aee", companyUid: "cmp_indigo", askerPrincipal: "agt_personal", audience: { kind: "person", personUid: "prs_owner" },
|
|
3
|
+
statement: "Release the update?",
|
|
4
|
+
options: [{ id: "A", label: "Release", recommended: true }, { id: "B", label: "Wait", recommended: false }],
|
|
5
|
+
context: { links: ["https://example.com/review"], brief: "Checks passed", commitmentId: "cm_1" },
|
|
6
|
+
state: "open", answer: null, talkThreadId: null,
|
|
7
|
+
createdAt: "2026-10-18T02:00:00.000Z", expiresAt: "2026-10-19T02:00:00.000Z", updatedAt: "2026-10-18T02:00:00.000Z",
|
|
8
|
+
};
|
|
9
|
+
export const decisionFixtures = {
|
|
10
|
+
create: { decision: openDecision },
|
|
11
|
+
list: { decisions: [openDecision] },
|
|
12
|
+
answer: { decision: { ...openDecision, state: "answered", updatedAt: "2026-10-18T03:00:00.000Z", answer: { choiceId: "A", by: "prs_owner", at: "2026-10-18T03:00:00.000Z", source: "desktop" } } },
|
|
13
|
+
talk: { decision: { ...openDecision, state: "talking", updatedAt: "2026-10-18T03:00:00.000Z", talkThreadId: null } },
|
|
14
|
+
withdraw: { decision: { ...openDecision, state: "withdrawn", updatedAt: "2026-10-18T03:00:00.000Z" } },
|
|
15
|
+
expired: { decision: { ...openDecision, state: "expired", updatedAt: "2026-10-19T03:00:00.000Z" } },
|
|
16
|
+
};
|
|
17
|
+
export const decisionRoutes = [
|
|
18
|
+
["POST", "/v1/decisions"], ["GET", "/v1/decisions"],
|
|
19
|
+
["POST", "/v1/decisions/{id}/answer"], ["POST", "/v1/decisions/{id}/talk"], ["POST", "/v1/decisions/{id}/withdraw"],
|
|
20
|
+
];
|
|
21
|
+
//# sourceMappingURL=us001.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type QuestionBody } from "../lanes/questions.js";
|
|
2
|
+
import type { QuestionRecord } from "../lanes/types.js";
|
|
3
|
+
import { DecisionsClient, type AskDecision } from "./client.js";
|
|
4
|
+
export declare function legacyDecisionInput(record: QuestionRecord, body: QuestionBody, audience: string): AskDecision | undefined;
|
|
5
|
+
export interface ImportCounts {
|
|
6
|
+
imported: number;
|
|
7
|
+
alreadyImported: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
skippedQuestions: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** Read legacy inputs only. Receipts live separately; legacy lane commands keep their storage. */
|
|
15
|
+
export declare function importLaneQuestions(root: string, audience: string, client: Pick<DecisionsClient, "ask" | "list">): Promise<ImportCounts>;
|
|
16
|
+
//# sourceMappingURL=import-lanes.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { decisionExpiry } from "./expiry.js";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { listQuestionIds, readQuestion, questionRecordPath, withLaneRecordFileLock } from "../lanes/store.js";
|
|
6
|
+
import { questionBodyAbsPath, sha256Text } from "../lanes/questions.js";
|
|
7
|
+
import { DecisionError } from "./client.js";
|
|
8
|
+
import { importedQuestionMarker } from "../lanes/imported-question.js";
|
|
9
|
+
export function legacyDecisionInput(record, body, audience) {
|
|
10
|
+
// Answered/closed/routed questions require contracts beyond release 1.
|
|
11
|
+
if (!record.company?.uid || !/^cmp_[A-Za-z0-9]+$/.test(record.company.uid) || record.status !== "pending" || record.hop > 1 || record.answer || record.answer_history?.length ||
|
|
12
|
+
record.routing_history.at(-1)?.audience.kind !== "user" ||
|
|
13
|
+
record.routing_history.at(-1)?.audience.id !== audience ||
|
|
14
|
+
record.routing_history.some(entry => entry.reason === "escalated") || body.notes || body.loop_stall ||
|
|
15
|
+
!Array.isArray(body.options) || body.options.length < 2 ||
|
|
16
|
+
body.options.some(option => typeof option !== "string" || !option.trim()) ||
|
|
17
|
+
new Set(body.options).size !== body.options.length ||
|
|
18
|
+
typeof body.question !== "string" || !body.question.trim() || typeof body.context !== "string" ||
|
|
19
|
+
(body.recommended !== undefined && !body.options.includes(body.recommended)))
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
companyUid: record.company.uid, expiresAt: decisionExpiry(),
|
|
23
|
+
audience: { kind: "person", personUid: audience }, statement: body.question,
|
|
24
|
+
options: body.options.map((label, index) => ({ id: String(index + 1), label, recommended: label === body.recommended })),
|
|
25
|
+
context: { links: [], commitmentId: null, brief: `${body.context}\n\nImported lane question ${record.question_id}`.trim() },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Read legacy inputs only. Receipts live separately; legacy lane commands keep their storage. */
|
|
29
|
+
export async function importLaneQuestions(root, audience, client) {
|
|
30
|
+
const journalDir = path.join(root, "workspace", "decisions", "imports", audience);
|
|
31
|
+
if (!/^prs_[A-Za-z0-9_-]+$/.test(audience))
|
|
32
|
+
throw new DecisionError("invalid_audience", "Expected person uid");
|
|
33
|
+
fs.mkdirSync(journalDir, { recursive: true });
|
|
34
|
+
const lockPath = path.join(journalDir, "import.lock");
|
|
35
|
+
let lock;
|
|
36
|
+
try {
|
|
37
|
+
lock = fs.openSync(lockPath, "wx", 0o600);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== "EEXIST")
|
|
41
|
+
throw error;
|
|
42
|
+
throw new DecisionError("import_locked", `An import owns ${lockPath}. Read the PID in that file and verify it is no longer running before deleting only import.lock, then rerun import-lanes. Keep receipts and legacy-read-only markers; they prevent duplicate decisions.`);
|
|
43
|
+
}
|
|
44
|
+
fs.writeFileSync(lock, String(process.pid));
|
|
45
|
+
const counts = { imported: 0, alreadyImported: 0, skipped: 0, skippedQuestions: [] };
|
|
46
|
+
const skip = (id, reason) => { counts.skipped++; counts.skippedQuestions.push({ id, reason }); };
|
|
47
|
+
try {
|
|
48
|
+
for (const id of listQuestionIds(root)) {
|
|
49
|
+
const record = readQuestion(root, id);
|
|
50
|
+
const target = record.routing_history.at(-1)?.audience;
|
|
51
|
+
if (target?.kind === "user" && target.id !== audience) {
|
|
52
|
+
skip(id, "addressed_to_other_user");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const bodyPath = questionBodyAbsPath(root, record);
|
|
56
|
+
let serialized;
|
|
57
|
+
try {
|
|
58
|
+
serialized = fs.readFileSync(bodyPath, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== "ENOENT")
|
|
62
|
+
throw error;
|
|
63
|
+
skip(id, "body_unavailable");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (record.body.digest && sha256Text(serialized) !== record.body.digest) {
|
|
67
|
+
skip(id, "body_digest_mismatch");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let body;
|
|
71
|
+
try {
|
|
72
|
+
body = JSON.parse(serialized);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
skip(id, "body_invalid");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!body || typeof body !== "object") {
|
|
79
|
+
skip(id, "body_invalid");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!record.company?.uid || !/^cmp_[A-Za-z0-9]+$/.test(record.company.uid)) {
|
|
83
|
+
skip(id, "company_unavailable");
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const input = legacyDecisionInput(record, body, audience);
|
|
87
|
+
if (!input) {
|
|
88
|
+
skip(id, "advanced_or_closed_question");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const key = createHash("sha256").update(id).digest("hex");
|
|
92
|
+
const receiptPath = path.join(journalDir, `${key}.json`);
|
|
93
|
+
const digest = sha256Text(JSON.stringify({ ...input, expiresAt: undefined }));
|
|
94
|
+
const receipt = fs.existsSync(receiptPath) ? JSON.parse(fs.readFileSync(receiptPath, "utf8")) : undefined;
|
|
95
|
+
if (receipt) {
|
|
96
|
+
if (receipt.sourceDigest !== digest) {
|
|
97
|
+
skip(id, "source_changed_after_import");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Upgrade receipts written before the read-only marker was introduced.
|
|
101
|
+
withLaneRecordFileLock(questionRecordPath(root, id), () => {
|
|
102
|
+
const markerPath = importedQuestionMarker(root, id);
|
|
103
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
104
|
+
if (!fs.existsSync(markerPath))
|
|
105
|
+
fs.writeFileSync(markerPath, JSON.stringify({ audience, receiptPath }), { flag: "wx", mode: 0o600 });
|
|
106
|
+
});
|
|
107
|
+
if (receipt.state === "imported" && receipt.decisionId) {
|
|
108
|
+
counts.alreadyImported++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// A crash or lost POST response is ambiguous. Reconcile, never blindly POST again.
|
|
112
|
+
const matches = (await client.list()).filter(decision => decision.companyUid === input.companyUid && decision.audience.kind === "person" && decision.audience.personUid === audience && decision.context.brief === input.context.brief && decision.statement === input.statement && JSON.stringify(decision.options) === JSON.stringify(input.options));
|
|
113
|
+
if (matches.length !== 1)
|
|
114
|
+
throw new DecisionError("import_outcome_unknown", `Question ${id} has an unresolved create; inspect the server before retrying. Receipt: ${receiptPath}`);
|
|
115
|
+
fs.writeFileSync(receiptPath, JSON.stringify({ sourceDigest: digest, state: "imported", decisionId: matches[0].id }), { mode: 0o600 });
|
|
116
|
+
counts.alreadyImported++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Persist intent before POST, so even interruption cannot cause an automatic duplicate.
|
|
120
|
+
const markerPath = importedQuestionMarker(root, id);
|
|
121
|
+
const fenced = withLaneRecordFileLock(questionRecordPath(root, id), () => {
|
|
122
|
+
const current = readQuestion(root, id);
|
|
123
|
+
if (current.version !== record.version || fs.readFileSync(bodyPath, "utf8") !== serialized)
|
|
124
|
+
return false;
|
|
125
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
fs.writeFileSync(markerPath, JSON.stringify({ audience, receiptPath }), { flag: "wx", mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code !== "EEXIST")
|
|
131
|
+
throw error;
|
|
132
|
+
throw new DecisionError("import_outcome_unknown", `Question ${id} already has a read-only transfer marker; inspect its receipt before retrying`);
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(receiptPath, JSON.stringify({ sourceDigest: digest, state: "pending" }), { flag: "wx", mode: 0o600 });
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
if (!fenced) {
|
|
138
|
+
skip(id, "source_changed_during_import");
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const decision = await client.ask(input);
|
|
142
|
+
fs.writeFileSync(receiptPath, JSON.stringify({ sourceDigest: digest, state: "imported", decisionId: decision.id }), { mode: 0o600 });
|
|
143
|
+
counts.imported++;
|
|
144
|
+
}
|
|
145
|
+
return counts;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
fs.closeSync(lock);
|
|
149
|
+
fs.unlinkSync(lockPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=import-lanes.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentRecord } from "../bot/api.js";
|
|
2
|
+
export declare const ASK_POLICY = "assistant-bound-conductor-release-1-conductor-only";
|
|
3
|
+
/** Session parsing adapted from hq-cli PR 1993; a session reference grants no authority. */
|
|
4
|
+
export declare function desktopSessionRef(env: NodeJS.ProcessEnv): string | undefined;
|
|
5
|
+
/** US-007 seam. No lease authority exists in release 1; env flags never grant it. */
|
|
6
|
+
export declare function hasConductorLease(_session: string | undefined): Promise<boolean>;
|
|
7
|
+
export declare function assertPersonalLocalBot(uid: string, agent: AgentRecord): string;
|
|
8
|
+
export declare function askAudience(token: string): Promise<string>;
|
|
9
|
+
/** Import creates decisions, so the same server-enforced bot gate applies. */
|
|
10
|
+
export declare const importAudience: typeof askAudience;
|
|
11
|
+
//# sourceMappingURL=principal.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolveCallerUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
2
|
+
import { BotApi, BotApiError } from "../bot/api.js";
|
|
3
|
+
import { normalizeSessionIdentity } from "../mesh/live/session-identity.js";
|
|
4
|
+
import { DecisionError } from "./client.js";
|
|
5
|
+
export const ASK_POLICY = "assistant-bound-conductor-release-1-conductor-only";
|
|
6
|
+
/** Session parsing adapted from hq-cli PR 1993; a session reference grants no authority. */
|
|
7
|
+
export function desktopSessionRef(env) {
|
|
8
|
+
try {
|
|
9
|
+
const identity = normalizeSessionIdentity({ env });
|
|
10
|
+
const provider = identity.source.startsWith("CODEX") ? "codex"
|
|
11
|
+
: identity.source.startsWith("CLAUDE") ? "claude"
|
|
12
|
+
: env.HQ_SESSION_PROVIDER?.trim() || (env.CODEX_THREAD_ID || env.CODEX_SESSION_ID ? "codex" : "claude");
|
|
13
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(provider) || !/^[A-Za-z0-9._-]{1,128}$/.test(identity.sessionId) || [".", ".."].includes(identity.sessionId))
|
|
14
|
+
return undefined;
|
|
15
|
+
return `session:${provider}:${identity.sessionId}`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** US-007 seam. No lease authority exists in release 1; env flags never grant it. */
|
|
22
|
+
export async function hasConductorLease(_session) { return false; }
|
|
23
|
+
export function assertPersonalLocalBot(uid, agent) {
|
|
24
|
+
if (agent.uid !== uid || agent.computeMode !== "local" || agent.botKind !== "personal" || !agent.ownerUid?.startsWith("prs_")) {
|
|
25
|
+
throw new DecisionError("asker_not_allowed", `${ASK_POLICY} requires the owner's personal local bot principal`);
|
|
26
|
+
}
|
|
27
|
+
return agent.ownerUid;
|
|
28
|
+
}
|
|
29
|
+
export async function askAudience(token) {
|
|
30
|
+
const caller = await resolveCallerUid(token);
|
|
31
|
+
if (caller.kind === "agent") {
|
|
32
|
+
try {
|
|
33
|
+
const agent = await new BotApi({
|
|
34
|
+
token: async () => token,
|
|
35
|
+
fetchImpl: options => vaultApiFetch({ ...options, signal: AbortSignal.timeout(15_000) }),
|
|
36
|
+
}).getAgent(caller.uid);
|
|
37
|
+
return assertPersonalLocalBot(caller.uid, agent);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof BotApiError && [403, 404].includes(error.status)) {
|
|
41
|
+
throw new DecisionError("asker_not_allowed", `${ASK_POLICY} requires a verified personal local bot principal`);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (await hasConductorLease(desktopSessionRef(process.env)))
|
|
47
|
+
return caller.uid;
|
|
48
|
+
throw new DecisionError("asker_not_allowed", `${ASK_POLICY} allows only the personal local bot; desktop conductor leases await US-007`);
|
|
49
|
+
}
|
|
50
|
+
/** Import creates decisions, so the same server-enforced bot gate applies. */
|
|
51
|
+
export const importAudience = askAudience;
|
|
52
|
+
//# sourceMappingURL=principal.js.map
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* "Wiring" is everything short of firing the hook: is every registration backed
|
|
5
5
|
* by a script that exists and is executable, is every script on disk actually
|
|
6
|
-
* registered, is every gated hook id a member of
|
|
7
|
-
*
|
|
6
|
+
* registered, is every gated hook id a member of each `hook-gate.sh` profile
|
|
7
|
+
* at or above its declared tier, and does any command word-split its own script path. A hook can be
|
|
8
8
|
* present, correct, and completely dead — this check catches the ways that
|
|
9
9
|
* happens without a single error surfacing to the user.
|
|
10
10
|
*
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* "Wiring" is everything short of firing the hook: is every registration backed
|
|
5
5
|
* by a script that exists and is executable, is every script on disk actually
|
|
6
|
-
* registered, is every gated hook id a member of
|
|
7
|
-
*
|
|
6
|
+
* registered, is every gated hook id a member of each `hook-gate.sh` profile
|
|
7
|
+
* at or above its declared tier, and does any command word-split its own script path. A hook can be
|
|
8
8
|
* present, correct, and completely dead — this check catches the ways that
|
|
9
9
|
* happens without a single error surfacing to the user.
|
|
10
10
|
*
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from "node:fs";
|
|
25
25
|
import * as path from "node:path";
|
|
26
|
-
import { GATE_PROFILES, gateMembership, parseHookGateProfiles, } from "../hook-gate-profiles.js";
|
|
26
|
+
import { GATE_PROFILES, activeGateProfile, gateMembership, parseHookGateProfiles, requiredMissingProfiles, } from "../hook-gate-profiles.js";
|
|
27
27
|
import { modeIsExecutable } from "../shared.js";
|
|
28
28
|
import { countStrayGateEntries } from "../stray-gate-entries.js";
|
|
29
29
|
/** Common id prefix for every result this check family emits. */
|
|
@@ -163,7 +163,11 @@ export function checkClaudeWiring(context) {
|
|
|
163
163
|
remediation: `Add ${file} to .claude/hooks/hook-registry.json with the right event and matcher, or delete it.`,
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
-
// --- AC4: every gated hook id is in
|
|
166
|
+
// --- AC4: every gated hook id is in each profile its tier requires ---------
|
|
167
|
+
// Profiles are nested tiers (minimal within standard within strict). A hook
|
|
168
|
+
// absent from a profile BELOW its lowest listed one is excluded on purpose
|
|
169
|
+
// and passes. A required profile missing the hook fails only when it is the
|
|
170
|
+
// active runtime profile; otherwise the hook still runs, so it warns.
|
|
167
171
|
const gatedIds = new Set();
|
|
168
172
|
for (const { scan } of scans) {
|
|
169
173
|
if (scan.gatedHookId)
|
|
@@ -181,27 +185,34 @@ export function checkClaudeWiring(context) {
|
|
|
181
185
|
}
|
|
182
186
|
else {
|
|
183
187
|
const profiles = parseHookGateProfiles(gateSource);
|
|
188
|
+
const active = activeGateProfile(context.hookProfile ?? process.env.HQ_HOOK_PROFILE);
|
|
184
189
|
for (const id of [...gatedIds].sort()) {
|
|
185
|
-
const { present
|
|
190
|
+
const { present } = gateMembership(id, profiles);
|
|
191
|
+
const missing = requiredMissingProfiles(id, profiles);
|
|
186
192
|
if (missing.length === 0) {
|
|
187
193
|
results.push({
|
|
188
194
|
status: "PASS",
|
|
189
195
|
checkId: `${CLAUDE_WIRING_PREFIX}.gate-profiles`,
|
|
190
196
|
target: id,
|
|
191
|
-
message: `Gated hook ${id} is allowlisted in
|
|
197
|
+
message: `Gated hook ${id} is allowlisted in every hook-gate.sh profile from ${present[0]} up.`,
|
|
192
198
|
});
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
195
201
|
const missingList = formatProfiles(missing);
|
|
196
202
|
const presentDesc = present.length > 0 ? formatProfiles(present) : "no profile";
|
|
203
|
+
const inActive = missing.includes(active);
|
|
204
|
+
const plural = missing.length === 1 ? "" : "s";
|
|
197
205
|
results.push({
|
|
198
|
-
status: "FAIL",
|
|
206
|
+
status: inActive ? "FAIL" : "WARN",
|
|
199
207
|
checkId: `${CLAUDE_WIRING_PREFIX}.gate-profiles`,
|
|
200
208
|
target: id,
|
|
201
209
|
message: `Gated hook ${id} is missing from the ${missingList} hook-gate.sh ` +
|
|
202
|
-
`profile${
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
`profile${plural} (present in ${presentDesc}); ` +
|
|
211
|
+
(inActive
|
|
212
|
+
? `it does not run under the active ${active} profile.`
|
|
213
|
+
: `it runs under the active ${active} profile but would not run under ${missing.length === 1 ? "that profile" : "those profiles"}.`),
|
|
214
|
+
remediation: `Run \`hq doctor --fix\`, or add ${id} to the ${missingList} ` +
|
|
215
|
+
`allowlist${plural} in .claude/hooks/hook-gate.sh.`,
|
|
205
216
|
});
|
|
206
217
|
}
|
|
207
218
|
}
|
|
@@ -13,7 +13,7 @@ import { workContextRoot, workContextSessionsDir } from "../../work-context/path
|
|
|
13
13
|
import { countDistinctHeldEvents, countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
14
14
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
15
15
|
import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
|
|
16
|
-
import { workSessionDenialReason } from "../../mesh/denial-codes.js";
|
|
16
|
+
import { WORK_TRACKING_OFF_DENIAL_CODES, workSessionDenialReason, } from "../../mesh/denial-codes.js";
|
|
17
17
|
export const WORK_CONTEXT_FAMILY_ID = "work-context";
|
|
18
18
|
export const WORK_CONTEXT_FAMILY_TITLE = "Work context";
|
|
19
19
|
const DEFAULT_DEPS = {
|
|
@@ -98,7 +98,12 @@ function runChecks(deps) {
|
|
|
98
98
|
]
|
|
99
99
|
.filter(Boolean)
|
|
100
100
|
.join(" ");
|
|
101
|
-
|
|
101
|
+
// A refusal because work tracking is off for the company is shown whether
|
|
102
|
+
// or not the detail flag is on: without it doctor only shows a count and the
|
|
103
|
+
// user is left guessing at auth (Shepherd issue 2726).
|
|
104
|
+
const workTrackingOff = stats.quarantined > 0 &&
|
|
105
|
+
WORK_TRACKING_OFF_DENIAL_CODES.includes(stats.lastQuarantineServerCode ?? "");
|
|
106
|
+
const quarantineDenialReason = showQuarantineReason || workTrackingOff
|
|
102
107
|
? workSessionDenialReason(stats.lastQuarantineServerCode)
|
|
103
108
|
: undefined;
|
|
104
109
|
results.push({
|
|
@@ -190,8 +190,9 @@ export interface AppliedFixBackup {
|
|
|
190
190
|
export declare const MAX_FIX_PASSES = 3;
|
|
191
191
|
/**
|
|
192
192
|
* Two 90s network repairs leave 120s for startup and check overhead under the
|
|
193
|
-
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget
|
|
194
|
-
*
|
|
193
|
+
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget.
|
|
194
|
+
* When both kinds are pending and no upload has run yet, one slot is held for
|
|
195
|
+
* a manifest upload so stale journals cannot starve every manifest.
|
|
195
196
|
*/
|
|
196
197
|
export declare const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
197
198
|
/** The outcome of an {@link applyFixes} run. */
|
|
@@ -40,7 +40,7 @@ import * as path from "node:path";
|
|
|
40
40
|
import { isPersonalJournalSlug } from "../checks/sync-health.js";
|
|
41
41
|
import { createDefaultRegistry } from "../registry.js";
|
|
42
42
|
import { flattenFamilies } from "../report.js";
|
|
43
|
-
import {
|
|
43
|
+
import { parseHookGateProfiles, requiredMissingProfiles, } from "../hook-gate-profiles.js";
|
|
44
44
|
import { createBackup } from "./backup.js";
|
|
45
45
|
import { removeStrayGateEntries } from "../stray-gate-entries.js";
|
|
46
46
|
import { deriveRemediation } from "./remediation.js";
|
|
@@ -48,7 +48,7 @@ import { botDir } from "../../bot/paths.js";
|
|
|
48
48
|
import { readBotConfig } from "../../bot/config.js";
|
|
49
49
|
import { buildBotDaemonPaths, installedDaemonHqBinary, repairBotDaemonIfStale, rewriteBotDaemonNodePath, } from "../../bot/daemon.js";
|
|
50
50
|
import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
|
|
51
|
-
import { readRefreshSyncProgress, writeRefreshSyncCursor, writeRefreshSyncLastRun, } from "./refresh-sync-progress.js";
|
|
51
|
+
import { readRefreshManifestCursor, readRefreshSyncProgress, writeRefreshManifestCursor, writeRefreshSyncCursor, writeRefreshSyncLastRun, } from "./refresh-sync-progress.js";
|
|
52
52
|
import { clientHealthShowManualOnlyEnabled } from "../client-health-reason-codes-flag.js";
|
|
53
53
|
/** The tree subtrees whose uncommitted changes block a `--fix` run. */
|
|
54
54
|
export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
@@ -58,8 +58,9 @@ export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
|
|
|
58
58
|
export const MAX_FIX_PASSES = 3;
|
|
59
59
|
/**
|
|
60
60
|
* Two 90s network repairs leave 120s for startup and check overhead under the
|
|
61
|
-
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget
|
|
62
|
-
*
|
|
61
|
+
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget.
|
|
62
|
+
* When both kinds are pending and no upload has run yet, one slot is held for
|
|
63
|
+
* a manifest upload so stale journals cannot starve every manifest.
|
|
63
64
|
*/
|
|
64
65
|
export const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
65
66
|
function rotateRefreshSyncPlans(plans, afterSlug) {
|
|
@@ -148,6 +149,7 @@ export async function applyFixes(options) {
|
|
|
148
149
|
let lastAttemptedRefreshSyncSlug = readRefreshSyncProgress(hqRoot)?.lastAttemptedSlug ?? null;
|
|
149
150
|
let refreshSyncAppliedThisRun = 0;
|
|
150
151
|
let refreshManifestAppliedThisRun = 0;
|
|
152
|
+
let lastAttemptedRefreshManifestScope = readRefreshManifestCursor(hqRoot);
|
|
151
153
|
const appliedKeys = new Set();
|
|
152
154
|
const planned = [];
|
|
153
155
|
const backups = [];
|
|
@@ -175,21 +177,20 @@ export async function applyFixes(options) {
|
|
|
175
177
|
keysThisPass.add(key);
|
|
176
178
|
plans.push(plan);
|
|
177
179
|
}
|
|
180
|
+
const networkSlotsLeft = Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN - refreshSyncAppliedThisRun - refreshManifestAppliedThisRun);
|
|
181
|
+
// Both kinds start after the company attempted last, so a scope that
|
|
182
|
+
// never clears cannot hold the front of the line on every run.
|
|
183
|
+
const manifestCandidates = rotateRefreshSyncPlans(plans.filter((plan) => plan.fixClass === "refresh-manifest"), lastAttemptedRefreshManifestScope);
|
|
184
|
+
// Hold one slot for an upload until one has run this invocation.
|
|
185
|
+
const manifestReserve = manifestCandidates.length > 0 && refreshManifestAppliedThisRun === 0 ? 1 : 0;
|
|
178
186
|
const seenRefreshTargets = new Set();
|
|
179
187
|
const refreshPlans = rotateRefreshSyncPlans(plans.filter((plan) => plan.fixClass === "refresh-sync"), lastAttemptedRefreshSyncSlug).filter((plan) => {
|
|
180
188
|
if (seenRefreshTargets.has(plan.target))
|
|
181
189
|
return false;
|
|
182
190
|
seenRefreshTargets.add(plan.target);
|
|
183
191
|
return true;
|
|
184
|
-
}).slice(0, Math.max(0,
|
|
185
|
-
|
|
186
|
-
const manifestPlans = plans
|
|
187
|
-
.filter((plan) => plan.fixClass === "refresh-manifest")
|
|
188
|
-
.sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
|
|
189
|
-
.slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN -
|
|
190
|
-
refreshSyncAppliedThisRun -
|
|
191
|
-
refreshManifestAppliedThisRun -
|
|
192
|
-
refreshPlans.length));
|
|
192
|
+
}).slice(0, Math.max(0, networkSlotsLeft - manifestReserve));
|
|
193
|
+
const manifestPlans = manifestCandidates.slice(0, networkSlotsLeft - refreshPlans.length);
|
|
193
194
|
return [
|
|
194
195
|
...plans.filter((plan) => plan.fixClass !== "refresh-sync" && plan.fixClass !== "refresh-manifest"),
|
|
195
196
|
...refreshPlans,
|
|
@@ -255,8 +256,11 @@ export async function applyFixes(options) {
|
|
|
255
256
|
lastAttemptedRefreshSyncSlug = plan.target;
|
|
256
257
|
refreshSyncAppliedThisRun += 1;
|
|
257
258
|
}
|
|
258
|
-
if (plan.fixClass === "refresh-manifest")
|
|
259
|
+
if (plan.fixClass === "refresh-manifest") {
|
|
260
|
+
writeRefreshManifestCursor(hqRoot, plan.target);
|
|
261
|
+
lastAttemptedRefreshManifestScope = plan.target;
|
|
259
262
|
refreshManifestAppliedThisRun += 1;
|
|
263
|
+
}
|
|
260
264
|
appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
|
|
261
265
|
planned.push({ plan, pass });
|
|
262
266
|
await plan.apply();
|
|
@@ -761,7 +765,7 @@ function planGateProfile(hqRoot, result, rem) {
|
|
|
761
765
|
catch {
|
|
762
766
|
return null;
|
|
763
767
|
}
|
|
764
|
-
const missing =
|
|
768
|
+
const missing = requiredMissingProfiles(hookId, parseHookGateProfiles(source));
|
|
765
769
|
if (missing.length === 0)
|
|
766
770
|
return null;
|
|
767
771
|
const relpath = toRelpath(hqRoot, gatePath);
|
|
@@ -19,6 +19,20 @@ export interface RefreshSyncProgress {
|
|
|
19
19
|
lastRunAtMs?: number;
|
|
20
20
|
}
|
|
21
21
|
export declare function readRefreshSyncProgress(hqRoot: string): RefreshSyncProgress | null;
|
|
22
|
+
/**
|
|
23
|
+
* `--fix` manifest-upload cursor. Kept in its own file so the refresh-sync
|
|
24
|
+
* progress format, which doctor JSON reads, does not change. Written before
|
|
25
|
+
* each upload, like the pull cursor, so the next run starts after the last
|
|
26
|
+
* company attempted and one failing company cannot hold the front of the line.
|
|
27
|
+
*/
|
|
28
|
+
export declare const REFRESH_MANIFEST_PROGRESS_RELATIVE_PATH: string;
|
|
29
|
+
export interface RefreshManifestProgress {
|
|
30
|
+
version: 1;
|
|
31
|
+
lastAttemptedScope: string;
|
|
32
|
+
}
|
|
33
|
+
/** The last manifest scope `--fix` attempted, or null when absent or unreadable. */
|
|
34
|
+
export declare function readRefreshManifestCursor(hqRoot: string): string | null;
|
|
35
|
+
export declare function writeRefreshManifestCursor(hqRoot: string, scope: string): void;
|
|
22
36
|
/** Persist the resume cursor without claiming this run finished. */
|
|
23
37
|
export declare function writeRefreshSyncCursor(hqRoot: string, slug: string): void;
|
|
24
38
|
/** Record the check ids this completed `--fix` pass actually tried to refresh. */
|