@indigoai-us/hq-cli 5.108.1 → 5.108.2
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 +77 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- package/package.json +5 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company precedence for work-context reconcile (US-007A).
|
|
3
|
+
*
|
|
4
|
+
* Order:
|
|
5
|
+
* 1. trusted explicit (HQ_SPAWN_COMPANY env or observation.trustedContext/explicit)
|
|
6
|
+
* 2. existing authoritative session scope
|
|
7
|
+
* 3. session meta.yaml company_slug (/startwork)
|
|
8
|
+
* 4. agent-box identity file (HQ_AGENT_IDENTITY_FILE → companyUid; source
|
|
9
|
+
* trusted_explicit — never "default company mode"; below HQ_SPAWN_COMPANY,
|
|
10
|
+
* above device default)
|
|
11
|
+
* 5. enabled device default
|
|
12
|
+
* 6. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
|
|
13
|
+
*
|
|
14
|
+
* Prompt-text matching is never used.
|
|
15
|
+
* Device default + conflicting deterministic evidence → company_conflict.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { getDefaultCompany } from "./config.js";
|
|
20
|
+
import { authoritativeCompanyFromState, readSessionState, } from "./state.js";
|
|
21
|
+
/** Pointer to the US-017B correction client (never auto-switches). */
|
|
22
|
+
export const COMPANY_CORRECTION_PATH_PREFIX = "hq mesh context correct --session";
|
|
23
|
+
export function companyCorrectionPath(sessionId) {
|
|
24
|
+
return `${COMPANY_CORRECTION_PATH_PREFIX} ${sessionId}`;
|
|
25
|
+
}
|
|
26
|
+
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
/** Env naming the on-box identity.json (fleet agent boxes). */
|
|
28
|
+
export const HQ_AGENT_IDENTITY_FILE_ENV = "HQ_AGENT_IDENTITY_FILE";
|
|
29
|
+
function normalizeSlug(value) {
|
|
30
|
+
if (!value)
|
|
31
|
+
return undefined;
|
|
32
|
+
const s = value.trim();
|
|
33
|
+
if (!s || !SLUG_RE.test(s))
|
|
34
|
+
return undefined;
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read `companyUid` from the agent identity file when
|
|
39
|
+
* `HQ_AGENT_IDENTITY_FILE` is set. Missing/unreadable/malformed → undefined
|
|
40
|
+
* (never throws; never logs file contents).
|
|
41
|
+
*/
|
|
42
|
+
export function readAgentIdentityCompanyUid(env = process.env) {
|
|
43
|
+
const file = env[HQ_AGENT_IDENTITY_FILE_ENV]?.trim();
|
|
44
|
+
if (!file)
|
|
45
|
+
return undefined;
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(file))
|
|
48
|
+
return undefined;
|
|
49
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
50
|
+
const uid = raw.companyUid;
|
|
51
|
+
return typeof uid === "string" && uid.trim().length > 0
|
|
52
|
+
? uid.trim()
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Compare companies: UIDs when both sides have them; otherwise normalized
|
|
61
|
+
* slugs when both have slugs. Never treat uid+slug vs slug-only as a
|
|
62
|
+
* conflict when the slugs match (verified scope under companies/{slug}/).
|
|
63
|
+
*/
|
|
64
|
+
export function sameCompany(a, b) {
|
|
65
|
+
const uidA = a.uid?.trim();
|
|
66
|
+
const uidB = b.uid?.trim();
|
|
67
|
+
if (uidA && uidB)
|
|
68
|
+
return uidA === uidB;
|
|
69
|
+
const slugA = a.slug?.trim().toLowerCase();
|
|
70
|
+
const slugB = b.slug?.trim().toLowerCase();
|
|
71
|
+
if (slugA && slugB)
|
|
72
|
+
return slugA === slugB;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract company slug from a cwd path containing `companies/{slug}/`.
|
|
77
|
+
*/
|
|
78
|
+
export function companySlugFromCwd(cwd, hqRoot) {
|
|
79
|
+
if (!cwd)
|
|
80
|
+
return undefined;
|
|
81
|
+
const normalized = path.resolve(cwd);
|
|
82
|
+
const parts = normalized.split(path.sep);
|
|
83
|
+
const idx = parts.lastIndexOf("companies");
|
|
84
|
+
if (idx >= 0 && idx + 1 < parts.length) {
|
|
85
|
+
const slug = normalizeSlug(parts[idx + 1]);
|
|
86
|
+
if (slug)
|
|
87
|
+
return slug;
|
|
88
|
+
}
|
|
89
|
+
if (hqRoot) {
|
|
90
|
+
const root = path.resolve(hqRoot);
|
|
91
|
+
const rel = path.relative(root, normalized);
|
|
92
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
93
|
+
const segs = rel.split(path.sep);
|
|
94
|
+
if (segs[0] === "companies" && segs[1]) {
|
|
95
|
+
return normalizeSlug(segs[1]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extract project id from cwd under `companies/{slug}/projects/{project}/`
|
|
103
|
+
* (US-007B deterministic mapping). Never invents a project.
|
|
104
|
+
*/
|
|
105
|
+
export function projectIdFromCwd(cwd, hqRoot) {
|
|
106
|
+
if (!cwd)
|
|
107
|
+
return undefined;
|
|
108
|
+
const normalized = path.resolve(cwd);
|
|
109
|
+
const parts = normalized.split(path.sep);
|
|
110
|
+
const idx = parts.lastIndexOf("companies");
|
|
111
|
+
if (idx >= 0 &&
|
|
112
|
+
parts[idx + 2] === "projects" &&
|
|
113
|
+
typeof parts[idx + 3] === "string" &&
|
|
114
|
+
parts[idx + 3].length > 0) {
|
|
115
|
+
const project = normalizeSlug(parts[idx + 3]);
|
|
116
|
+
if (project)
|
|
117
|
+
return project;
|
|
118
|
+
}
|
|
119
|
+
if (hqRoot) {
|
|
120
|
+
const root = path.resolve(hqRoot);
|
|
121
|
+
const rel = path.relative(root, normalized);
|
|
122
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
123
|
+
const segs = rel.split(path.sep);
|
|
124
|
+
if (segs[0] === "companies" && segs[2] === "projects" && segs[3]) {
|
|
125
|
+
return normalizeSlug(segs[3]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read company_slug from workspace/sessions/<sid>/meta.yaml (best-effort, no yaml dep).
|
|
133
|
+
*/
|
|
134
|
+
export function readMetaCompanySlug(sessionId, hqRoot) {
|
|
135
|
+
if (!hqRoot || !sessionId)
|
|
136
|
+
return undefined;
|
|
137
|
+
const metaPath = path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
|
|
138
|
+
if (!fs.existsSync(metaPath))
|
|
139
|
+
return undefined;
|
|
140
|
+
try {
|
|
141
|
+
const text = fs.readFileSync(metaPath, "utf8");
|
|
142
|
+
// Minimal line parse: company_slug: value (no prompt-text matching)
|
|
143
|
+
for (const line of text.split(/\r?\n/)) {
|
|
144
|
+
const m = /^\s*company_slug\s*:\s*["']?([A-Za-z0-9._-]+)["']?\s*$/.exec(line);
|
|
145
|
+
if (m?.[1])
|
|
146
|
+
return normalizeSlug(m[1]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Deterministic mapping: exactly one of cwd companies/{slug} or remote owner.
|
|
156
|
+
* Multiple distinct signals that disagree → no deterministic mapping (needs_company
|
|
157
|
+
* unless a higher-precedence source exists). Matching signals collapse to one.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveDeterministicCompany(input) {
|
|
160
|
+
const fromCwd = companySlugFromCwd(input.cwd, input.hqRoot);
|
|
161
|
+
const fromRemote = normalizeSlug(input.remoteOwnerSlug ?? undefined);
|
|
162
|
+
if (fromCwd && fromRemote && fromCwd.toLowerCase() !== fromRemote.toLowerCase()) {
|
|
163
|
+
// Ambiguous deterministic evidence — do not guess.
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (fromCwd) {
|
|
167
|
+
return { slug: fromCwd, source: "deterministic_cwd" };
|
|
168
|
+
}
|
|
169
|
+
if (fromRemote) {
|
|
170
|
+
return { slug: fromRemote, source: "deterministic_remote" };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
export function resolveCompany(input) {
|
|
175
|
+
const env = input.env ?? process.env;
|
|
176
|
+
// 1. Trusted explicit: HQ_SPAWN_COMPANY or observation trusted/explicit.
|
|
177
|
+
const spawn = normalizeSlug(env.HQ_SPAWN_COMPANY);
|
|
178
|
+
const trustedSlug = normalizeSlug(input.trusted?.companySlug);
|
|
179
|
+
const trustedUid = input.trusted?.companyUid?.trim() || undefined;
|
|
180
|
+
if (spawn || trustedSlug || trustedUid) {
|
|
181
|
+
return {
|
|
182
|
+
status: "resolved",
|
|
183
|
+
company: {
|
|
184
|
+
slug: spawn ?? trustedSlug,
|
|
185
|
+
uid: trustedUid,
|
|
186
|
+
source: "trusted_explicit",
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// 2. Existing authoritative session scope.
|
|
191
|
+
const state = input.existingState !== undefined
|
|
192
|
+
? input.existingState
|
|
193
|
+
: readSessionState(input.sessionId, input.root);
|
|
194
|
+
const prior = authoritativeCompanyFromState(state);
|
|
195
|
+
if (prior) {
|
|
196
|
+
const scoped = {
|
|
197
|
+
slug: prior.slug,
|
|
198
|
+
uid: prior.uid,
|
|
199
|
+
source: "existing_scope",
|
|
200
|
+
};
|
|
201
|
+
// After a binding exists, a suspected other-company mismatch cannot
|
|
202
|
+
// auto-switch — return company_conflict with the US-017B correction path.
|
|
203
|
+
const hadBinding = Boolean(state &&
|
|
204
|
+
(state.contextStatus === "bound" ||
|
|
205
|
+
state.contextStatus === "needs_task" ||
|
|
206
|
+
state.contextStatus === "needs_project") &&
|
|
207
|
+
(state.projectId || state.bindingEpisodeId));
|
|
208
|
+
if (hadBinding) {
|
|
209
|
+
const deterministic = resolveDeterministicCompany({
|
|
210
|
+
cwd: input.cwd,
|
|
211
|
+
hqRoot: input.hqRoot,
|
|
212
|
+
remoteOwnerSlug: input.remoteOwnerSlug,
|
|
213
|
+
});
|
|
214
|
+
if (deterministic && !sameCompany(scoped, deterministic)) {
|
|
215
|
+
return {
|
|
216
|
+
status: "company_conflict",
|
|
217
|
+
defaultCompany: scoped,
|
|
218
|
+
evidenceCompany: deterministic,
|
|
219
|
+
afterBinding: true,
|
|
220
|
+
correctionPath: companyCorrectionPath(input.sessionId),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { status: "resolved", company: scoped };
|
|
225
|
+
}
|
|
226
|
+
// 3. session meta.yaml company_slug
|
|
227
|
+
const metaSlug = input.metaCompanySlug !== undefined
|
|
228
|
+
? normalizeSlug(input.metaCompanySlug)
|
|
229
|
+
: readMetaCompanySlug(input.sessionId, input.hqRoot);
|
|
230
|
+
if (metaSlug) {
|
|
231
|
+
return {
|
|
232
|
+
status: "resolved",
|
|
233
|
+
company: { slug: metaSlug, source: "session_meta" },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// 4. Agent-box identity file (HQ_AGENT_IDENTITY_FILE). Prefer as the
|
|
237
|
+
// device-default role with source trusted_explicit — never device_default /
|
|
238
|
+
// "default company mode". Below HQ_SPAWN_COMPANY; above device default.
|
|
239
|
+
const identityUid = readAgentIdentityCompanyUid(env);
|
|
240
|
+
if (identityUid) {
|
|
241
|
+
return {
|
|
242
|
+
status: "resolved",
|
|
243
|
+
company: { uid: identityUid, source: "trusted_explicit" },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// 5 + 6. Device default vs deterministic evidence.
|
|
247
|
+
const deviceDefault = getDefaultCompany({ root: input.root });
|
|
248
|
+
const deterministic = resolveDeterministicCompany({
|
|
249
|
+
cwd: input.cwd,
|
|
250
|
+
hqRoot: input.hqRoot,
|
|
251
|
+
remoteOwnerSlug: input.remoteOwnerSlug,
|
|
252
|
+
});
|
|
253
|
+
if (deviceDefault?.enabled && deviceDefault.slug) {
|
|
254
|
+
const def = {
|
|
255
|
+
slug: deviceDefault.slug,
|
|
256
|
+
uid: deviceDefault.uid,
|
|
257
|
+
source: "device_default",
|
|
258
|
+
};
|
|
259
|
+
if (deterministic &&
|
|
260
|
+
!sameCompany(def, deterministic)) {
|
|
261
|
+
return {
|
|
262
|
+
status: "company_conflict",
|
|
263
|
+
defaultCompany: def,
|
|
264
|
+
evidenceCompany: deterministic,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return { status: "resolved", company: def };
|
|
268
|
+
}
|
|
269
|
+
if (deterministic) {
|
|
270
|
+
return { status: "resolved", company: deterministic };
|
|
271
|
+
}
|
|
272
|
+
return { status: "needs_company" };
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=company.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-local default company preference (~/.hq/work-context/config.json).
|
|
3
|
+
* Preference only — never authority. activeCompany is never treated as default.
|
|
4
|
+
* Default-company mode ships dark until migrationCapability (US-017A).
|
|
5
|
+
*/
|
|
6
|
+
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
7
|
+
export declare const DEVICE_CONFIG_SCHEMA_VERSION: 1;
|
|
8
|
+
export interface DeviceDefaultCompany {
|
|
9
|
+
slug: string;
|
|
10
|
+
uid?: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Last probe of GET /v1/work-mesh/capabilities across memberships (US-017B). */
|
|
14
|
+
export interface MigrationCapabilitySnapshot {
|
|
15
|
+
unlocked: boolean;
|
|
16
|
+
offline: boolean;
|
|
17
|
+
checkedAt: string;
|
|
18
|
+
companies: Array<{
|
|
19
|
+
companyUid: string;
|
|
20
|
+
migration: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkContextDeviceConfig {
|
|
24
|
+
schemaVersion: typeof DEVICE_CONFIG_SCHEMA_VERSION;
|
|
25
|
+
/** Explicit device default. Never copied from activeCompany. */
|
|
26
|
+
defaultCompany?: DeviceDefaultCompany | null;
|
|
27
|
+
/** Last-known migration capability across memberships (doctor reports this). */
|
|
28
|
+
migrationCapability?: MigrationCapabilitySnapshot | null;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}
|
|
31
|
+
/** true / { uid } = member; false = not a member. */
|
|
32
|
+
export type MembershipValidationResult = boolean | {
|
|
33
|
+
uid: string;
|
|
34
|
+
};
|
|
35
|
+
export interface DeviceConfigDeps {
|
|
36
|
+
root: string;
|
|
37
|
+
/** When true, server advertises migrationCapability (unlocks set). */
|
|
38
|
+
migrationCapability?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Current-membership check for set <slug>. Required for a successful set —
|
|
41
|
+
* when omitted, set rejects with DEFAULT_COMPANY_UNAVAILABLE (never assume member).
|
|
42
|
+
* Injectable for tests.
|
|
43
|
+
*/
|
|
44
|
+
validateMembership?: (slug: string) => Promise<MembershipValidationResult> | MembershipValidationResult;
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
}
|
|
47
|
+
export declare function readDeviceConfig(deps: Pick<DeviceConfigDeps, "root">): WorkContextDeviceConfig;
|
|
48
|
+
export declare function getDefaultCompany(deps: Pick<DeviceConfigDeps, "root">): DeviceDefaultCompany | null;
|
|
49
|
+
export declare function setDefaultCompany(slug: string, deps: DeviceConfigDeps & {
|
|
50
|
+
allowWithoutMigration?: boolean;
|
|
51
|
+
}): Promise<WorkContextDeviceConfig>;
|
|
52
|
+
export declare function clearDefaultCompany(deps: DeviceConfigDeps): WorkContextDeviceConfig;
|
|
53
|
+
/** Persist the last memberships-wide migration capability probe (US-017B). */
|
|
54
|
+
export declare function recordMigrationCapabilitySnapshot(snapshot: MigrationCapabilitySnapshot, deps: Pick<DeviceConfigDeps, "root" | "now">): WorkContextDeviceConfig;
|
|
55
|
+
/** Convenience: build deps from HOME / injectable root. */
|
|
56
|
+
export declare function deviceConfigDepsFromHome(home?: string, extras?: Partial<DeviceConfigDeps>): DeviceConfigDeps;
|
|
57
|
+
/** Re-export for callers that stamp contractVersion on related state. */
|
|
58
|
+
export { WORK_CONTEXT_CONTRACT_VERSION };
|
|
59
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-local default company preference (~/.hq/work-context/config.json).
|
|
3
|
+
* Preference only — never authority. activeCompany is never treated as default.
|
|
4
|
+
* Default-company mode ships dark until migrationCapability (US-017A).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { atomicWriteJson, ensureOwnerDir, resolveRealTarget } from "./atomic.js";
|
|
9
|
+
import { DefaultCompanyLockedError, DefaultCompanyUnavailableError, UnsafeConfigPathError, WorkContextError, } from "./errors.js";
|
|
10
|
+
import { workContextConfigPath, workContextRoot } from "./paths.js";
|
|
11
|
+
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
12
|
+
export const DEVICE_CONFIG_SCHEMA_VERSION = 1;
|
|
13
|
+
function emptyConfig(now) {
|
|
14
|
+
return {
|
|
15
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
16
|
+
defaultCompany: null,
|
|
17
|
+
migrationCapability: null,
|
|
18
|
+
updatedAt: now().toISOString(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function assertSafeConfigPath(configPath) {
|
|
22
|
+
const dir = path.dirname(configPath);
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(dir)) {
|
|
25
|
+
const lst = fs.lstatSync(dir);
|
|
26
|
+
if (lst.isSymbolicLink()) {
|
|
27
|
+
throw new UnsafeConfigPathError(`Config directory is a symlink: ${dir}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(configPath)) {
|
|
31
|
+
const lst = fs.lstatSync(configPath);
|
|
32
|
+
if (lst.isSymbolicLink()) {
|
|
33
|
+
throw new UnsafeConfigPathError(`Config file is a symlink: ${configPath}`);
|
|
34
|
+
}
|
|
35
|
+
const mode = lst.mode & 0o777;
|
|
36
|
+
// Warn-level only for world-writable; refuse group/other write bits on read for safety.
|
|
37
|
+
if (mode & 0o022) {
|
|
38
|
+
throw new UnsafeConfigPathError(`Config file has unsafe permissions ${mode.toString(8)} (expected owner-only)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof UnsafeConfigPathError)
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function readDeviceConfig(deps) {
|
|
48
|
+
const configPath = workContextConfigPath(deps.root);
|
|
49
|
+
assertSafeConfigPath(configPath);
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
return emptyConfig(() => new Date(0));
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
55
|
+
if (raw.schemaVersion !== DEVICE_CONFIG_SCHEMA_VERSION) {
|
|
56
|
+
throw new WorkContextError("ConfigCorruptionError", `Unsupported config schemaVersion ${String(raw.schemaVersion)}`);
|
|
57
|
+
}
|
|
58
|
+
// Never treat a stray activeCompany field as the default.
|
|
59
|
+
const cleaned = {
|
|
60
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
61
|
+
defaultCompany: raw.defaultCompany ?? null,
|
|
62
|
+
migrationCapability: raw.migrationCapability ?? null,
|
|
63
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date(0).toISOString(),
|
|
64
|
+
};
|
|
65
|
+
return cleaned;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err instanceof WorkContextError)
|
|
69
|
+
throw err;
|
|
70
|
+
throw new WorkContextError("ConfigCorruptionError", `Invalid work-context config: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeDeviceConfig(deps, config) {
|
|
74
|
+
ensureOwnerDir(deps.root);
|
|
75
|
+
const configPath = resolveRealTarget(workContextConfigPath(deps.root));
|
|
76
|
+
assertSafeConfigPath(configPath);
|
|
77
|
+
atomicWriteJson(configPath, config);
|
|
78
|
+
}
|
|
79
|
+
export function getDefaultCompany(deps) {
|
|
80
|
+
const cfg = readDeviceConfig(deps);
|
|
81
|
+
const d = cfg.defaultCompany;
|
|
82
|
+
if (!d || !d.enabled || !d.slug?.trim())
|
|
83
|
+
return null;
|
|
84
|
+
return d;
|
|
85
|
+
}
|
|
86
|
+
export async function setDefaultCompany(slug, deps) {
|
|
87
|
+
const trimmed = slug.trim();
|
|
88
|
+
if (!trimmed || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed)) {
|
|
89
|
+
throw new WorkContextError("InvalidCompanySlug", `Invalid company slug: ${slug}`);
|
|
90
|
+
}
|
|
91
|
+
if (!deps.migrationCapability && !deps.allowWithoutMigration) {
|
|
92
|
+
throw new DefaultCompanyLockedError();
|
|
93
|
+
}
|
|
94
|
+
if (!deps.validateMembership) {
|
|
95
|
+
throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${trimmed}" (no membership validator)`);
|
|
96
|
+
}
|
|
97
|
+
let resolvedUid;
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = await deps.validateMembership(trimmed);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (err instanceof DefaultCompanyUnavailableError)
|
|
104
|
+
throw err;
|
|
105
|
+
throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${trimmed}": ${err instanceof Error ? err.message : String(err)}`);
|
|
106
|
+
}
|
|
107
|
+
if (result === false) {
|
|
108
|
+
throw new DefaultCompanyUnavailableError(`Not a current member of company "${trimmed}"`);
|
|
109
|
+
}
|
|
110
|
+
if (result && typeof result === "object" && typeof result.uid === "string") {
|
|
111
|
+
resolvedUid = result.uid.trim() || undefined;
|
|
112
|
+
}
|
|
113
|
+
const now = deps.now ?? (() => new Date());
|
|
114
|
+
const prior = readDeviceConfig(deps);
|
|
115
|
+
const defaultCompany = {
|
|
116
|
+
slug: trimmed,
|
|
117
|
+
enabled: true,
|
|
118
|
+
};
|
|
119
|
+
if (resolvedUid)
|
|
120
|
+
defaultCompany.uid = resolvedUid;
|
|
121
|
+
const next = {
|
|
122
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
123
|
+
defaultCompany,
|
|
124
|
+
migrationCapability: prior.migrationCapability ?? null,
|
|
125
|
+
updatedAt: now().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
writeDeviceConfig(deps, next);
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
export function clearDefaultCompany(deps) {
|
|
131
|
+
const now = deps.now ?? (() => new Date());
|
|
132
|
+
const prior = readDeviceConfig(deps);
|
|
133
|
+
const next = {
|
|
134
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
135
|
+
defaultCompany: null,
|
|
136
|
+
migrationCapability: prior.migrationCapability ?? null,
|
|
137
|
+
updatedAt: now().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
writeDeviceConfig(deps, next);
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
/** Persist the last memberships-wide migration capability probe (US-017B). */
|
|
143
|
+
export function recordMigrationCapabilitySnapshot(snapshot, deps) {
|
|
144
|
+
const prior = readDeviceConfig(deps);
|
|
145
|
+
const now = deps.now ?? (() => new Date());
|
|
146
|
+
const next = {
|
|
147
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
148
|
+
defaultCompany: prior.defaultCompany ?? null,
|
|
149
|
+
migrationCapability: {
|
|
150
|
+
unlocked: snapshot.unlocked,
|
|
151
|
+
offline: snapshot.offline,
|
|
152
|
+
checkedAt: snapshot.checkedAt,
|
|
153
|
+
companies: snapshot.companies.map((c) => ({
|
|
154
|
+
companyUid: c.companyUid,
|
|
155
|
+
migration: c.migration,
|
|
156
|
+
})),
|
|
157
|
+
},
|
|
158
|
+
updatedAt: now().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
writeDeviceConfig({ root: deps.root }, next);
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
/** Convenience: build deps from HOME / injectable root. */
|
|
164
|
+
export function deviceConfigDepsFromHome(home, extras = {}) {
|
|
165
|
+
return {
|
|
166
|
+
root: extras.root ?? workContextRoot(home),
|
|
167
|
+
...extras,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** Re-export for callers that stamp contractVersion on related state. */
|
|
171
|
+
export { WORK_CONTEXT_CONTRACT_VERSION };
|
|
172
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Context reconciliation contract (US-001A recreation for work-mesh-live).
|
|
3
|
+
*
|
|
4
|
+
* Recreated 2026-09-03 from
|
|
5
|
+
* companies/indigo/projects/work-context-resolver/architectural-decisions.md
|
|
6
|
+
* because feature/work-context-resolver-cli was not on origin. When Stefan's
|
|
7
|
+
* package lands, reconcile differences in the Work Mesh Live contract changelog.
|
|
8
|
+
*
|
|
9
|
+
* Shared domain model with docs/work-mesh-live-contract.md (hq-pro):
|
|
10
|
+
* Project, Task (taskId canonical, storyId ingress alias), Session, Work Binding,
|
|
11
|
+
* and the classification states below.
|
|
12
|
+
*/
|
|
13
|
+
export declare const WORK_CONTEXT_CONTRACT_VERSION: 1;
|
|
14
|
+
/** Classification: where the Session belongs. */
|
|
15
|
+
export type ClassificationState = "unresolved" | "needs_company" | "company_conflict" | "needs_project" | "needs_task" | "bound" | "untracked" | "migration_pending";
|
|
16
|
+
/** Delivery: durable operation delivery / acknowledgement only. */
|
|
17
|
+
export type DeliveryState = "clean" | "queued" | "acked" | "quarantined";
|
|
18
|
+
/** Lifecycle: whether the Session may produce ordinary work events. */
|
|
19
|
+
export type LifecycleState = "open" | "terminal";
|
|
20
|
+
/** ContextResult kind vocabulary from US-001A. */
|
|
21
|
+
export type ContextResultKind = "bound" | "needs_company" | "company_conflict" | "needs_project" | "needs_task" | "queued" | "untracked" | "migration_pending" | "incompatible" | "error";
|
|
22
|
+
export declare const CLASSIFICATION_STATES: readonly ClassificationState[];
|
|
23
|
+
export declare const CONTEXT_RESULT_KINDS: readonly ContextResultKind[];
|
|
24
|
+
/** Local-only classification: no tenant read or write. */
|
|
25
|
+
export declare const LOCAL_ONLY_CLASSIFICATION: ReadonlySet<ClassificationState>;
|
|
26
|
+
export interface SessionIdentity {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
providerSessionId?: string;
|
|
29
|
+
harness?: string;
|
|
30
|
+
adapterVersion?: string;
|
|
31
|
+
runtimeVersion?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TrustedExplicitContext {
|
|
34
|
+
companyUid?: string;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
/** Canonical. Prefer over storyId. */
|
|
37
|
+
taskId?: string;
|
|
38
|
+
/** Ingress alias; normalize to taskId. */
|
|
39
|
+
storyId?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SessionObservation {
|
|
42
|
+
contractVersion: typeof WORK_CONTEXT_CONTRACT_VERSION;
|
|
43
|
+
identity: SessionIdentity;
|
|
44
|
+
explicit?: TrustedExplicitContext;
|
|
45
|
+
clientOperationId: string;
|
|
46
|
+
/** User-origin only; model/agent origins for untracked are rejected. */
|
|
47
|
+
untrackedOrigin?: "user";
|
|
48
|
+
}
|
|
49
|
+
export interface ContextResult {
|
|
50
|
+
contractVersion: typeof WORK_CONTEXT_CONTRACT_VERSION;
|
|
51
|
+
kind: ContextResultKind;
|
|
52
|
+
classification: ClassificationState;
|
|
53
|
+
delivery: DeliveryState;
|
|
54
|
+
lifecycle: LifecycleState;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
companyUid?: string;
|
|
57
|
+
projectId?: string;
|
|
58
|
+
taskId?: string;
|
|
59
|
+
bindingEpisodeId?: string;
|
|
60
|
+
clientOperationId: string;
|
|
61
|
+
errorCode?: string;
|
|
62
|
+
}
|
|
63
|
+
export type NormalizeTaskIdResult = {
|
|
64
|
+
ok: true;
|
|
65
|
+
taskId: string;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
code: "TASK_ALIAS_CONFLICT" | "TASK_ID_MISSING";
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Normalize taskId / storyId ingress. taskId is canonical; storyId is alias.
|
|
72
|
+
* Conflicting dual aliases are rejected.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeTaskId(input: {
|
|
75
|
+
taskId?: string;
|
|
76
|
+
storyId?: string;
|
|
77
|
+
}): NormalizeTaskIdResult;
|
|
78
|
+
export declare function isLocalOnlyClassification(state: ClassificationState): boolean;
|
|
79
|
+
/** Content classes prohibited in durable work-context state. */
|
|
80
|
+
export declare const WORK_CONTEXT_PROHIBITED_CONTENT_CLASSES: readonly ["prompts", "model_output", "transcripts", "message_bodies", "tokens", "credentials"];
|
|
81
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Context reconciliation contract (US-001A recreation for work-mesh-live).
|
|
3
|
+
*
|
|
4
|
+
* Recreated 2026-09-03 from
|
|
5
|
+
* companies/indigo/projects/work-context-resolver/architectural-decisions.md
|
|
6
|
+
* because feature/work-context-resolver-cli was not on origin. When Stefan's
|
|
7
|
+
* package lands, reconcile differences in the Work Mesh Live contract changelog.
|
|
8
|
+
*
|
|
9
|
+
* Shared domain model with docs/work-mesh-live-contract.md (hq-pro):
|
|
10
|
+
* Project, Task (taskId canonical, storyId ingress alias), Session, Work Binding,
|
|
11
|
+
* and the classification states below.
|
|
12
|
+
*/
|
|
13
|
+
export const WORK_CONTEXT_CONTRACT_VERSION = 1;
|
|
14
|
+
export const CLASSIFICATION_STATES = [
|
|
15
|
+
"unresolved",
|
|
16
|
+
"needs_company",
|
|
17
|
+
"company_conflict",
|
|
18
|
+
"needs_project",
|
|
19
|
+
"needs_task",
|
|
20
|
+
"bound",
|
|
21
|
+
"untracked",
|
|
22
|
+
"migration_pending",
|
|
23
|
+
];
|
|
24
|
+
export const CONTEXT_RESULT_KINDS = [
|
|
25
|
+
"bound",
|
|
26
|
+
"needs_company",
|
|
27
|
+
"company_conflict",
|
|
28
|
+
"needs_project",
|
|
29
|
+
"needs_task",
|
|
30
|
+
"queued",
|
|
31
|
+
"untracked",
|
|
32
|
+
"migration_pending",
|
|
33
|
+
"incompatible",
|
|
34
|
+
"error",
|
|
35
|
+
];
|
|
36
|
+
/** Local-only classification: no tenant read or write. */
|
|
37
|
+
export const LOCAL_ONLY_CLASSIFICATION = new Set(["needs_company", "company_conflict", "untracked"]);
|
|
38
|
+
/**
|
|
39
|
+
* Normalize taskId / storyId ingress. taskId is canonical; storyId is alias.
|
|
40
|
+
* Conflicting dual aliases are rejected.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeTaskId(input) {
|
|
43
|
+
const taskId = input.taskId?.trim() || undefined;
|
|
44
|
+
const storyId = input.storyId?.trim() || undefined;
|
|
45
|
+
if (taskId && storyId && taskId !== storyId) {
|
|
46
|
+
return { ok: false, code: "TASK_ALIAS_CONFLICT" };
|
|
47
|
+
}
|
|
48
|
+
const canonical = taskId ?? storyId;
|
|
49
|
+
if (!canonical)
|
|
50
|
+
return { ok: false, code: "TASK_ID_MISSING" };
|
|
51
|
+
return { ok: true, taskId: canonical };
|
|
52
|
+
}
|
|
53
|
+
export function isLocalOnlyClassification(state) {
|
|
54
|
+
return LOCAL_ONLY_CLASSIFICATION.has(state);
|
|
55
|
+
}
|
|
56
|
+
/** Content classes prohibited in durable work-context state. */
|
|
57
|
+
export const WORK_CONTEXT_PROHIBITED_CONTENT_CLASSES = [
|
|
58
|
+
"prompts",
|
|
59
|
+
"model_output",
|
|
60
|
+
"transcripts",
|
|
61
|
+
"message_bodies",
|
|
62
|
+
"tokens",
|
|
63
|
+
"credentials",
|
|
64
|
+
];
|
|
65
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Work Context errors (US-007A / resolver error map).
|
|
3
|
+
*/
|
|
4
|
+
export declare class WorkContextError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
constructor(code: string, message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class NotTrackingError extends WorkContextError {
|
|
9
|
+
constructor(message: string, code?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class DefaultCompanyLockedError extends WorkContextError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class DefaultCompanyUnavailableError extends WorkContextError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class InvalidDecisionOriginError extends WorkContextError {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class UnsafeConfigPathError extends WorkContextError {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/** Exit codes for `hq mesh context reconcile` machine mode. */
|
|
24
|
+
export declare const EXIT_OK = 0;
|
|
25
|
+
export declare const EXIT_INVALID_IDENTITY = 2;
|
|
26
|
+
export declare const EXIT_NOT_TRACKING = 3;
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|