@indigoai-us/hq-cli 5.108.0 → 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 +79 -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/cloud.d.ts +6 -0
- package/dist/commands/cloud.js +26 -3
- 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
|
@@ -6,6 +6,29 @@ export type SearchOptions = {
|
|
|
6
6
|
count?: number;
|
|
7
7
|
json?: boolean;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Normalise a caller's query into the single argv token qmd is handed.
|
|
11
|
+
*
|
|
12
|
+
* A quoted multi-line query (a fleet agent forwards a whole Slack message as the
|
|
13
|
+
* query) reaches qmd with its newlines intact. qmd's `query` subcommand then
|
|
14
|
+
* reads it as a query DOCUMENT and, unless EVERY non-empty line carries a
|
|
15
|
+
* lex:/vec:/hyde:/intent:/expand: prefix, exits 1 with a syntax error — which
|
|
16
|
+
* hq surfaced as an unclassified crash (Sentry HQ-CLI-1B / indigo-d0 7705711476,
|
|
17
|
+
* 28 of 29 events).
|
|
18
|
+
*
|
|
19
|
+
* The rule is IMPLIED BY qmd's own acceptance rule, so it can never collapse a
|
|
20
|
+
* document qmd would have accepted: qmd requires a prefix on every line of a
|
|
21
|
+
* multi-line document, so a prefixed FIRST non-empty line is a NECESSARY
|
|
22
|
+
* condition of any document qmd accepts. When the caller opened such a document
|
|
23
|
+
* (two or more non-empty lines AND a prefixed first line) the query is returned
|
|
24
|
+
* VERBATIM so structured hybrid search keeps working; otherwise every run of
|
|
25
|
+
* whitespace — the newlines included — collapses to a single space, so plain
|
|
26
|
+
* multi-line free text becomes the single-line query qmd accepts. A half-prefixed
|
|
27
|
+
* document (a user typo whose first line lacks a prefix) is collapsed and still
|
|
28
|
+
* reaches qmd, which returns a real diagnostic rather than a silently-wrong
|
|
29
|
+
* free-text search.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeQueryForQmd(query: string): string;
|
|
9
32
|
export declare function buildSearchArgs(query: string, options?: SearchOptions): string[];
|
|
10
33
|
export declare function buildGetArgs(document: string, options?: Pick<SearchOptions, 'collection'>): string[];
|
|
11
34
|
export declare function registerSearchCommand(program: Command): void;
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,48 @@
|
|
|
1
1
|
import { runQmd } from '../lib/search-index/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* qmd's query-DOCUMENT line prefixes (qmd 2.5.3, dist/cli/qmd.js:2165-2167 and
|
|
4
|
+
* :2210). qmd's `query` subcommand parses a multi-line positional as a query
|
|
5
|
+
* DOCUMENT: with two or more non-empty lines EVERY line must begin with one of
|
|
6
|
+
* these, or qmd throws and exits 1. Mirrored from the EXACT pinned version
|
|
7
|
+
* (package.json pins `@tobilu/qmd` to `2.5.3`, not a range), so the mirror cannot
|
|
8
|
+
* drift without a deliberate dependency bump.
|
|
9
|
+
*/
|
|
10
|
+
const QMD_DOCUMENT_PREFIX = /^(?:lex|vec|hyde|intent|expand):\s*/i;
|
|
11
|
+
/**
|
|
12
|
+
* Normalise a caller's query into the single argv token qmd is handed.
|
|
13
|
+
*
|
|
14
|
+
* A quoted multi-line query (a fleet agent forwards a whole Slack message as the
|
|
15
|
+
* query) reaches qmd with its newlines intact. qmd's `query` subcommand then
|
|
16
|
+
* reads it as a query DOCUMENT and, unless EVERY non-empty line carries a
|
|
17
|
+
* lex:/vec:/hyde:/intent:/expand: prefix, exits 1 with a syntax error — which
|
|
18
|
+
* hq surfaced as an unclassified crash (Sentry HQ-CLI-1B / indigo-d0 7705711476,
|
|
19
|
+
* 28 of 29 events).
|
|
20
|
+
*
|
|
21
|
+
* The rule is IMPLIED BY qmd's own acceptance rule, so it can never collapse a
|
|
22
|
+
* document qmd would have accepted: qmd requires a prefix on every line of a
|
|
23
|
+
* multi-line document, so a prefixed FIRST non-empty line is a NECESSARY
|
|
24
|
+
* condition of any document qmd accepts. When the caller opened such a document
|
|
25
|
+
* (two or more non-empty lines AND a prefixed first line) the query is returned
|
|
26
|
+
* VERBATIM so structured hybrid search keeps working; otherwise every run of
|
|
27
|
+
* whitespace — the newlines included — collapses to a single space, so plain
|
|
28
|
+
* multi-line free text becomes the single-line query qmd accepts. A half-prefixed
|
|
29
|
+
* document (a user typo whose first line lacks a prefix) is collapsed and still
|
|
30
|
+
* reaches qmd, which returns a real diagnostic rather than a silently-wrong
|
|
31
|
+
* free-text search.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeQueryForQmd(query) {
|
|
34
|
+
const nonEmptyLines = query
|
|
35
|
+
.split(/\r?\n/)
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.filter((line) => line.length > 0);
|
|
38
|
+
if (nonEmptyLines.length >= 2 && QMD_DOCUMENT_PREFIX.test(nonEmptyLines[0])) {
|
|
39
|
+
return query;
|
|
40
|
+
}
|
|
41
|
+
return query.replace(/\s+/g, ' ').trim();
|
|
42
|
+
}
|
|
2
43
|
export function buildSearchArgs(query, options = {}) {
|
|
3
44
|
const command = { keyword: 'search', semantic: 'vsearch', hybrid: 'query' }[options.mode ?? 'keyword'];
|
|
4
|
-
const args = [command, query];
|
|
45
|
+
const args = [command, normalizeQueryForQmd(query)];
|
|
5
46
|
if (options.collection)
|
|
6
47
|
args.push('-c', options.collection);
|
|
7
48
|
if (options.count !== undefined)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work-context doctor family (US-007A + US-008): state-file health, outbox
|
|
3
|
+
* depth, quarantined count, default-company mode, and work-mesh spool /
|
|
4
|
+
* held / dead-letter visibility.
|
|
5
|
+
*/
|
|
6
|
+
import { type MigrationCapabilitySnapshot } from "../../work-context/config.js";
|
|
7
|
+
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
8
|
+
export declare const WORK_CONTEXT_FAMILY_ID = "work-context";
|
|
9
|
+
export declare const WORK_CONTEXT_FAMILY_TITLE = "Work context";
|
|
10
|
+
export interface WorkContextDoctorDeps {
|
|
11
|
+
root: (home?: string) => string;
|
|
12
|
+
home: () => string;
|
|
13
|
+
/** Work-mesh root (spool / held / dead-letter). Defaults to ~/.hq/work-mesh. */
|
|
14
|
+
meshRoot?: (home?: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional override for the last migration-capability probe (US-017B).
|
|
17
|
+
* When omitted, doctor reads the snapshot from device config.
|
|
18
|
+
*/
|
|
19
|
+
migrationCapability?: MigrationCapabilitySnapshot | null;
|
|
20
|
+
}
|
|
21
|
+
export declare const workContextFamily: CheckFamily;
|
|
22
|
+
export declare function checkWorkContext(_context: CheckContext, deps?: WorkContextDoctorDeps): CheckResult[];
|
|
23
|
+
//# sourceMappingURL=work-context.d.ts.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work-context doctor family (US-007A + US-008): state-file health, outbox
|
|
3
|
+
* depth, quarantined count, default-company mode, and work-mesh spool /
|
|
4
|
+
* held / dead-letter visibility.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { readDeviceConfig, getDefaultCompany, } from "../../work-context/config.js";
|
|
10
|
+
import { outboxStats, listOutboxOperations } from "../../work-context/outbox.js";
|
|
11
|
+
import { workContextRoot, workContextSessionsDir } from "../../work-context/paths.js";
|
|
12
|
+
import { countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
13
|
+
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
14
|
+
import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
|
|
15
|
+
export const WORK_CONTEXT_FAMILY_ID = "work-context";
|
|
16
|
+
export const WORK_CONTEXT_FAMILY_TITLE = "Work context";
|
|
17
|
+
const DEFAULT_DEPS = {
|
|
18
|
+
root: (home) => workContextRoot(home ?? os.homedir()),
|
|
19
|
+
home: () => os.homedir(),
|
|
20
|
+
meshRoot: (home) => workMeshRoot(home ?? os.homedir()),
|
|
21
|
+
};
|
|
22
|
+
export const workContextFamily = {
|
|
23
|
+
id: WORK_CONTEXT_FAMILY_ID,
|
|
24
|
+
title: WORK_CONTEXT_FAMILY_TITLE,
|
|
25
|
+
run: (context) => Promise.resolve(checkWorkContext(context)),
|
|
26
|
+
};
|
|
27
|
+
export function checkWorkContext(_context, deps = DEFAULT_DEPS) {
|
|
28
|
+
try {
|
|
29
|
+
return runChecks(deps);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
status: "UNKNOWN",
|
|
35
|
+
checkId: "work-context.error",
|
|
36
|
+
message: `work-context checks could not run: ${error.message}`,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function runChecks(deps) {
|
|
42
|
+
const root = deps.root(deps.home());
|
|
43
|
+
const results = [];
|
|
44
|
+
// State-file health
|
|
45
|
+
const sessionsDir = workContextSessionsDir(root);
|
|
46
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
47
|
+
results.push({
|
|
48
|
+
status: "NA",
|
|
49
|
+
checkId: "work-context.state-files",
|
|
50
|
+
target: sessionsDir,
|
|
51
|
+
message: "No work-context sessions directory yet.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const files = fs.readdirSync(sessionsDir).filter((n) => n.endsWith(".json"));
|
|
56
|
+
let corrupt = 0;
|
|
57
|
+
let healthy = 0;
|
|
58
|
+
for (const name of files) {
|
|
59
|
+
const p = path.join(sessionsDir, name);
|
|
60
|
+
try {
|
|
61
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
62
|
+
if (!raw.sessionId || !raw.contextStatus || raw.contractVersion !== 1) {
|
|
63
|
+
corrupt += 1;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
healthy += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
corrupt += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
results.push({
|
|
74
|
+
status: corrupt > 0 ? "WARN" : "PASS",
|
|
75
|
+
checkId: "work-context.state-files",
|
|
76
|
+
target: sessionsDir,
|
|
77
|
+
message: corrupt > 0
|
|
78
|
+
? `${corrupt} corrupt / ${healthy} healthy session state file(s).`
|
|
79
|
+
: `${healthy} healthy session state file(s).`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Outbox depth + quarantined
|
|
83
|
+
const stats = outboxStats(root);
|
|
84
|
+
results.push({
|
|
85
|
+
status: stats.depth > 50 ? "WARN" : "PASS",
|
|
86
|
+
checkId: "work-context.outbox-depth",
|
|
87
|
+
target: path.join(root, "outbox"),
|
|
88
|
+
message: `Outbox queued depth=${stats.depth} (acked=${stats.acked}).`,
|
|
89
|
+
});
|
|
90
|
+
results.push({
|
|
91
|
+
status: stats.quarantined > 0 ? "WARN" : "PASS",
|
|
92
|
+
checkId: "work-context.outbox-quarantined",
|
|
93
|
+
target: path.join(root, "outbox"),
|
|
94
|
+
message: `Outbox quarantined count=${stats.quarantined}.`,
|
|
95
|
+
});
|
|
96
|
+
// Default-company mode + migration capability (US-017B)
|
|
97
|
+
try {
|
|
98
|
+
const cfg = readDeviceConfig({ root });
|
|
99
|
+
const def = getDefaultCompany({ root });
|
|
100
|
+
const mode = def?.enabled
|
|
101
|
+
? `enabled slug=${def.slug}`
|
|
102
|
+
: "disabled (dark / unset)";
|
|
103
|
+
results.push({
|
|
104
|
+
status: "PASS",
|
|
105
|
+
checkId: "work-context.default-company",
|
|
106
|
+
target: path.join(root, "config.json"),
|
|
107
|
+
message: `Default-company mode: ${mode}; schemaVersion=${cfg.schemaVersion}.`,
|
|
108
|
+
});
|
|
109
|
+
const cap = deps.migrationCapability !== undefined
|
|
110
|
+
? deps.migrationCapability
|
|
111
|
+
: (cfg.migrationCapability ?? null);
|
|
112
|
+
results.push(formatMigrationCapabilityCheck(cap, root));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
results.push({
|
|
116
|
+
status: "WARN",
|
|
117
|
+
checkId: "work-context.default-company",
|
|
118
|
+
target: path.join(root, "config.json"),
|
|
119
|
+
message: `Default-company config unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
});
|
|
121
|
+
results.push({
|
|
122
|
+
status: "WARN",
|
|
123
|
+
checkId: "work-context.migration-capability",
|
|
124
|
+
target: path.join(root, "config.json"),
|
|
125
|
+
message: `Migration capability unknown: ${err instanceof Error ? err.message : String(err)}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Touch listOutboxOperations so depth stays consistent with enumeration.
|
|
129
|
+
void listOutboxOperations(root);
|
|
130
|
+
// Work-mesh spool / held / dead-letter (US-008)
|
|
131
|
+
const meshRoot = (deps.meshRoot ?? DEFAULT_DEPS.meshRoot)(deps.home());
|
|
132
|
+
const spoolPath = workMeshSpoolPath(meshRoot);
|
|
133
|
+
const heldPath = workMeshHeldPath(meshRoot);
|
|
134
|
+
const deadLetterPath = workMeshDeadLetterPath(meshRoot);
|
|
135
|
+
const spoolDepth = countJsonlLines(spoolPath);
|
|
136
|
+
const heldDepth = countJsonlLines(heldPath);
|
|
137
|
+
results.push({
|
|
138
|
+
status: "PASS",
|
|
139
|
+
checkId: "work-context.spool-depth",
|
|
140
|
+
target: spoolPath,
|
|
141
|
+
message: `Work-mesh spool depth=${spoolDepth}; held depth=${heldDepth}.`,
|
|
142
|
+
});
|
|
143
|
+
const deadLetterFull = deadLetterNonEmpty(meshRoot);
|
|
144
|
+
results.push({
|
|
145
|
+
status: deadLetterFull ? "WARN" : "PASS",
|
|
146
|
+
checkId: "work-context.dead-letter",
|
|
147
|
+
target: deadLetterPath,
|
|
148
|
+
message: deadLetterFull
|
|
149
|
+
? `Work-mesh dead-letter is non-empty (${countJsonlLines(deadLetterPath)} line(s)); inspect and clear after fixing the cause.`
|
|
150
|
+
: "Work-mesh dead-letter is empty.",
|
|
151
|
+
});
|
|
152
|
+
// Daemon doctor (US-009): running / MQTT / companies / flush / unhealthy age
|
|
153
|
+
const daemon = collectDaemonDoctor({
|
|
154
|
+
home: deps.home(),
|
|
155
|
+
meshRoot,
|
|
156
|
+
workContextRoot: root,
|
|
157
|
+
});
|
|
158
|
+
results.push({
|
|
159
|
+
status: daemon.unhealthy ? "WARN" : "PASS",
|
|
160
|
+
checkId: "work-context.daemon",
|
|
161
|
+
target: meshRoot,
|
|
162
|
+
message: formatDaemonDoctor(daemon).join(" "),
|
|
163
|
+
});
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
function formatMigrationCapabilityCheck(cap, root) {
|
|
167
|
+
const target = path.join(root, "config.json");
|
|
168
|
+
if (!cap) {
|
|
169
|
+
return {
|
|
170
|
+
status: "NA",
|
|
171
|
+
checkId: "work-context.migration-capability",
|
|
172
|
+
target,
|
|
173
|
+
message: "Migration capability: not probed yet (run hq mesh context default set or correct).",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (cap.offline) {
|
|
177
|
+
return {
|
|
178
|
+
status: "WARN",
|
|
179
|
+
checkId: "work-context.migration-capability",
|
|
180
|
+
target,
|
|
181
|
+
message: `Migration capability: offline at ${cap.checkedAt}; default-company stays locked.`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const falseCount = cap.companies.filter((c) => !c.migration).length;
|
|
185
|
+
const detail = cap.companies.length === 0
|
|
186
|
+
? "no active memberships"
|
|
187
|
+
: `${cap.companies.filter((c) => c.migration).length}/${cap.companies.length} companies advertise migration`;
|
|
188
|
+
return {
|
|
189
|
+
status: cap.unlocked ? "PASS" : "WARN",
|
|
190
|
+
checkId: "work-context.migration-capability",
|
|
191
|
+
target,
|
|
192
|
+
message: `Migration capability: unlocked=${cap.unlocked} (${detail}; false=${falseCount}); checkedAt=${cap.checkedAt}.`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=work-context.js.map
|
|
@@ -21,6 +21,7 @@ import { checkRuntimeProbe } from "./checks/runtime-probe.js";
|
|
|
21
21
|
import { runtimeHealthFamily } from "./checks/runtime-health.js";
|
|
22
22
|
import { integrationsFamily } from "./checks/integrations.js";
|
|
23
23
|
import { syncHealthFamily } from "./checks/sync-health.js";
|
|
24
|
+
import { workContextFamily } from "./checks/work-context.js";
|
|
24
25
|
import { fixtureCoverageFamily } from "./fixtures/discover.js";
|
|
25
26
|
import { checkClaudeWiring } from "./checks/claude-wiring.js";
|
|
26
27
|
/**
|
|
@@ -187,6 +188,9 @@ export function createDefaultRegistry() {
|
|
|
187
188
|
// this registry's docblock always anticipated. Local-only reads, so the
|
|
188
189
|
// doctor's offline contract is preserved.
|
|
189
190
|
registry.register(syncHealthFamily);
|
|
191
|
+
// Work context (US-007A): session state files, outbox depth/quarantine,
|
|
192
|
+
// and default-company mode. Local-only reads under ~/.hq/work-context.
|
|
193
|
+
registry.register(workContextFamily);
|
|
190
194
|
return registry;
|
|
191
195
|
}
|
|
192
196
|
//# sourceMappingURL=registry.js.map
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -29,6 +29,24 @@ export declare function meshJson(token: string, path: string, init?: {
|
|
|
29
29
|
query?: Record<string, string>;
|
|
30
30
|
}): Promise<unknown>;
|
|
31
31
|
export declare function resolveMeshCompany(token: string, company?: string): Promise<MeshCompany>;
|
|
32
|
+
/**
|
|
33
|
+
* Active memberships for the caller via GET /membership/me.
|
|
34
|
+
* Does not honour HQ_COMPANY_UID / HQ_WORK_MESH_COMPANY_UID env overrides.
|
|
35
|
+
* Throws on transport / auth failures.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listActiveMembershipCompanies(token: string): Promise<MeshCompany[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Memberships-backed company lookup for default-company set (US-007A).
|
|
40
|
+
*
|
|
41
|
+
* Lists the caller's active companies via GET /membership/me and matches the
|
|
42
|
+
* requested slug (case-insensitive). Does **not** honour HQ_COMPANY_UID /
|
|
43
|
+
* HQ_WORK_MESH_COMPANY_UID env overrides or treat a raw `cmp_`/`co_` ref as
|
|
44
|
+
* proof of membership without an active membership row.
|
|
45
|
+
*
|
|
46
|
+
* Returns null when the caller is not an active member. Throws on transport /
|
|
47
|
+
* auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveActiveMembershipCompany(token: string, slug: string): Promise<MeshCompany | null>;
|
|
32
50
|
export declare function requireToken(): Promise<string>;
|
|
33
51
|
export declare function listActiveThreads(token: string, companyUid: string, projectId?: string): Promise<MeshThread[]>;
|
|
34
52
|
export declare function clamp(value: string, max: number): string;
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -58,6 +58,61 @@ export async function resolveMeshCompany(token, company) {
|
|
|
58
58
|
const uid = await getCompanyUid(token, company);
|
|
59
59
|
return { companyUid: uid, companySlug: company || uid };
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Active memberships for the caller via GET /membership/me.
|
|
63
|
+
* Does not honour HQ_COMPANY_UID / HQ_WORK_MESH_COMPANY_UID env overrides.
|
|
64
|
+
* Throws on transport / auth failures.
|
|
65
|
+
*/
|
|
66
|
+
export async function listActiveMembershipCompanies(token) {
|
|
67
|
+
const data = asRecord(await meshJson(token, "/membership/me"));
|
|
68
|
+
const rows = Array.isArray(data?.memberships) ? data.memberships : [];
|
|
69
|
+
const out = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const rec = asRecord(row);
|
|
73
|
+
if (!rec)
|
|
74
|
+
continue;
|
|
75
|
+
if (String(rec.status || "").toLowerCase() !== "active")
|
|
76
|
+
continue;
|
|
77
|
+
const companyUid = typeof rec.companyUid === "string" ? rec.companyUid : "";
|
|
78
|
+
if (!companyUid || seen.has(companyUid))
|
|
79
|
+
continue;
|
|
80
|
+
const companySlug = typeof rec.companySlug === "string"
|
|
81
|
+
? rec.companySlug
|
|
82
|
+
: typeof rec.companyFolderSlug === "string"
|
|
83
|
+
? rec.companyFolderSlug
|
|
84
|
+
: companyUid;
|
|
85
|
+
seen.add(companyUid);
|
|
86
|
+
out.push({ companyUid, companySlug: companySlug || companyUid });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Memberships-backed company lookup for default-company set (US-007A).
|
|
92
|
+
*
|
|
93
|
+
* Lists the caller's active companies via GET /membership/me and matches the
|
|
94
|
+
* requested slug (case-insensitive). Does **not** honour HQ_COMPANY_UID /
|
|
95
|
+
* HQ_WORK_MESH_COMPANY_UID env overrides or treat a raw `cmp_`/`co_` ref as
|
|
96
|
+
* proof of membership without an active membership row.
|
|
97
|
+
*
|
|
98
|
+
* Returns null when the caller is not an active member. Throws on transport /
|
|
99
|
+
* auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
|
|
100
|
+
*/
|
|
101
|
+
export async function resolveActiveMembershipCompany(token, slug) {
|
|
102
|
+
const trimmed = slug.trim();
|
|
103
|
+
if (!trimmed)
|
|
104
|
+
return null;
|
|
105
|
+
const rows = await listActiveMembershipCompanies(token);
|
|
106
|
+
const needle = trimmed.toLowerCase();
|
|
107
|
+
const matches = rows.filter((c) => c.companySlug.toLowerCase() === needle ||
|
|
108
|
+
c.companyUid.toLowerCase() === needle);
|
|
109
|
+
if (matches.length === 1)
|
|
110
|
+
return matches[0];
|
|
111
|
+
if (matches.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
// Ambiguous slug across memberships — refuse rather than guess.
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
61
116
|
export async function requireToken() {
|
|
62
117
|
return ensureCognitoToken();
|
|
63
118
|
}
|
package/dist/lib/mesh/cache.js
CHANGED
|
@@ -22,10 +22,41 @@ export function writeMeshCacheFile(segments, value, root = meshCacheRoot()) {
|
|
|
22
22
|
if (segments.length === 0 || !segments.every(isSafeCacheSegment))
|
|
23
23
|
return null;
|
|
24
24
|
const dest = path.join(root, ...segments);
|
|
25
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
26
|
-
const tmp = `${dest}.tmp`;
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
26
|
+
const tmp = `${dest}.tmp.${process.pid}.${Date.now()}`;
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
fd = fs.openSync(tmp, "wx", 0o600);
|
|
30
|
+
const buf = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
31
|
+
fs.writeSync(fd, buf);
|
|
32
|
+
fs.fsyncSync(fd);
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
fd = undefined;
|
|
35
|
+
fs.renameSync(tmp, dest);
|
|
36
|
+
try {
|
|
37
|
+
fs.chmodSync(dest, 0o600);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore exotic FS
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (fd !== undefined) {
|
|
45
|
+
try {
|
|
46
|
+
fs.closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* already closed */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
fs.unlinkSync(tmp);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* temp may not exist */
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
29
60
|
return dest;
|
|
30
61
|
}
|
|
31
62
|
/** Drop leftover session.json once we know the real prs_/agt_ cache key. */
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared work-mesh transport client.
|
|
3
|
+
* Existing `hq mesh` verbs and work-context reconcile both use this for
|
|
4
|
+
* vault HTTP + receipt-shaped responses (US-007A / US-007B).
|
|
5
|
+
*/
|
|
6
|
+
import { listActiveMembershipCompanies, meshJson, requireToken, resolveMeshCompany, type MeshCompany } from "./api.js";
|
|
7
|
+
import type { DeliverFn } from "../work-context/outbox.js";
|
|
8
|
+
import type { CandidatesResponse, OrganizeReceipt, OrganizeSubmitDecision, FetchCandidatesFn, PostOrganizeFn } from "../work-context/organize.js";
|
|
9
|
+
import type { MigrateReceipt, PostMigrateFn } from "../work-context/migrate.js";
|
|
10
|
+
export { meshJson, requireToken, resolveMeshCompany, listActiveMembershipCompanies };
|
|
11
|
+
export type { MeshCompany };
|
|
12
|
+
/** Company-wide live read (US-004 / US-007B session status). */
|
|
13
|
+
export interface CompanyLiveSession {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
harness?: string;
|
|
16
|
+
contextStatus?: string;
|
|
17
|
+
projectId?: string;
|
|
18
|
+
taskId?: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
startedAt?: string;
|
|
21
|
+
lastTurnAt?: string;
|
|
22
|
+
turnCount?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CompanyLiveParticipant {
|
|
25
|
+
actorUid: string;
|
|
26
|
+
actorType?: string;
|
|
27
|
+
displayName?: string;
|
|
28
|
+
presence?: string;
|
|
29
|
+
lastSeenAt?: string;
|
|
30
|
+
sessions?: CompanyLiveSession[];
|
|
31
|
+
}
|
|
32
|
+
export interface CompanyLiveReadResponse {
|
|
33
|
+
contractVersion: 1;
|
|
34
|
+
generatedAt: string;
|
|
35
|
+
participants: CompanyLiveParticipant[];
|
|
36
|
+
nextCursor?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface MeshTransport {
|
|
39
|
+
token: string;
|
|
40
|
+
company: MeshCompany;
|
|
41
|
+
}
|
|
42
|
+
export declare function openMeshTransport(opts: {
|
|
43
|
+
company?: string;
|
|
44
|
+
}): Promise<MeshTransport>;
|
|
45
|
+
/**
|
|
46
|
+
* POST work-session registration for a queued outbox operation.
|
|
47
|
+
* Maps HTTP status to retryable vs quarantine.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createWorkSessionDeliverer(opts: {
|
|
50
|
+
token: string;
|
|
51
|
+
/** Override fetch for tests. */
|
|
52
|
+
post?: (path: string, body: Record<string, unknown>) => Promise<{
|
|
53
|
+
status: number;
|
|
54
|
+
body: unknown;
|
|
55
|
+
}>;
|
|
56
|
+
}): DeliverFn;
|
|
57
|
+
/**
|
|
58
|
+
* Probe whether the server advertises migration capability (US-017A).
|
|
59
|
+
* Failures return false (default-company stays locked).
|
|
60
|
+
*/
|
|
61
|
+
export declare function fetchMigrationCapability(token: string, companyUid: string): Promise<boolean>;
|
|
62
|
+
export interface MigrationCapabilityCompanyResult {
|
|
63
|
+
companyUid: string;
|
|
64
|
+
companySlug: string;
|
|
65
|
+
migration: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface MigrationCapabilityProbe {
|
|
68
|
+
/** True only when every active membership reports migration: true. */
|
|
69
|
+
unlocked: boolean;
|
|
70
|
+
/** True when memberships could not be listed (no session / network). */
|
|
71
|
+
offline: boolean;
|
|
72
|
+
checkedAt: string;
|
|
73
|
+
companies: MigrationCapabilityCompanyResult[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Unlock rule for default-company mode (US-017B): migration must be true for
|
|
77
|
+
* EVERY company the caller belongs to. Offline / empty memberships → locked.
|
|
78
|
+
*/
|
|
79
|
+
export declare function probeMigrationCapabilityForMemberships(token: string, opts?: {
|
|
80
|
+
listCompanies?: (token: string) => Promise<MeshCompany[]>;
|
|
81
|
+
fetchOne?: (token: string, companyUid: string) => Promise<boolean>;
|
|
82
|
+
now?: () => Date;
|
|
83
|
+
}): Promise<MigrationCapabilityProbe>;
|
|
84
|
+
/**
|
|
85
|
+
* POST /v1/work-mesh/sessions/{sessionId}/migrate
|
|
86
|
+
*/
|
|
87
|
+
export declare function createMigratePoster(opts: {
|
|
88
|
+
token: string;
|
|
89
|
+
post?: (path: string, body: Record<string, unknown>) => Promise<{
|
|
90
|
+
status: number;
|
|
91
|
+
body: unknown;
|
|
92
|
+
}>;
|
|
93
|
+
}): PostMigrateFn;
|
|
94
|
+
/**
|
|
95
|
+
* GET /v1/work-mesh/sessions/{sessionId}/candidates?companyUid=...
|
|
96
|
+
*/
|
|
97
|
+
export declare function createCandidatesFetcher(opts: {
|
|
98
|
+
token: string;
|
|
99
|
+
get?: (path: string, query: Record<string, string>) => Promise<unknown>;
|
|
100
|
+
}): FetchCandidatesFn;
|
|
101
|
+
/**
|
|
102
|
+
* POST /v1/work-mesh/sessions/{sessionId}/organize
|
|
103
|
+
*/
|
|
104
|
+
export declare function createOrganizePoster(opts: {
|
|
105
|
+
token: string;
|
|
106
|
+
post?: (path: string, body: Record<string, unknown>) => Promise<{
|
|
107
|
+
status: number;
|
|
108
|
+
body: unknown;
|
|
109
|
+
}>;
|
|
110
|
+
}): PostOrganizeFn;
|
|
111
|
+
/** GET /v1/work-mesh/live?companyUid=... */
|
|
112
|
+
export declare function fetchCompanyLive(token: string, companyUid: string, opts?: {
|
|
113
|
+
cursor?: string;
|
|
114
|
+
get?: (path: string, query: Record<string, string>) => Promise<unknown>;
|
|
115
|
+
}): Promise<CompanyLiveReadResponse>;
|
|
116
|
+
export declare function formatCompanyLiveTable(live: CompanyLiveReadResponse, companyLabel: string): string[];
|
|
117
|
+
export type { OrganizeSubmitDecision, OrganizeReceipt, CandidatesResponse, MigrateReceipt, PostMigrateFn, };
|
|
118
|
+
//# sourceMappingURL=client.d.ts.map
|