@indigoai-us/hq-cli 5.108.1 → 5.108.3
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/core.js +5 -4
- package/dist/commands/index-cmd.js +13 -0
- 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 +98 -0
- package/dist/lib/search-index/index.js +174 -2
- 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 +93 -4
- 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/qmd-workdir-missing-error.d.ts +18 -0
- package/dist/utils/qmd-workdir-missing-error.js +78 -0
- package/dist/utils/sentry-fingerprint.js +7 -0
- package/package.json +5 -1
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Work Context errors (US-007A / resolver error map).
|
|
3
|
+
*/
|
|
4
|
+
export class WorkContextError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "WorkContextError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class NotTrackingError extends WorkContextError {
|
|
13
|
+
constructor(message, code = "NOT_TRACKING") {
|
|
14
|
+
super(code, message);
|
|
15
|
+
this.name = "NotTrackingError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class DefaultCompanyLockedError extends WorkContextError {
|
|
19
|
+
constructor(message = "Default-company mode is locked until the server advertises migrationCapability") {
|
|
20
|
+
super("DEFAULT_COMPANY_LOCKED", message);
|
|
21
|
+
this.name = "DefaultCompanyLockedError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class DefaultCompanyUnavailableError extends WorkContextError {
|
|
25
|
+
constructor(message = "Cannot set default company: membership unavailable or caller is not a current member") {
|
|
26
|
+
super("DEFAULT_COMPANY_UNAVAILABLE", message);
|
|
27
|
+
this.name = "DefaultCompanyUnavailableError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class InvalidDecisionOriginError extends WorkContextError {
|
|
31
|
+
constructor(message = "Untracked origin must be user; model/agent origins are rejected") {
|
|
32
|
+
super("InvalidDecisionOriginError", message);
|
|
33
|
+
this.name = "InvalidDecisionOriginError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class UnsafeConfigPathError extends WorkContextError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super("UnsafeConfigPathError", message);
|
|
39
|
+
this.name = "UnsafeConfigPathError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Exit codes for `hq mesh context reconcile` machine mode. */
|
|
43
|
+
export const EXIT_OK = 0;
|
|
44
|
+
export const EXIT_INVALID_IDENTITY = 2;
|
|
45
|
+
export const EXIT_NOT_TRACKING = 3;
|
|
46
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Context reconciliation package (consumable by hq-cli).
|
|
3
|
+
* Contract (US-001) + reconcile / outbox / device default (US-007A)
|
|
4
|
+
* + organize / project resolution (US-007B)
|
|
5
|
+
* + cross-company correct / migrate (US-017B).
|
|
6
|
+
*/
|
|
7
|
+
export * from "./contract.js";
|
|
8
|
+
export * from "./errors.js";
|
|
9
|
+
export * from "./paths.js";
|
|
10
|
+
export * from "./atomic.js";
|
|
11
|
+
export * from "./config.js";
|
|
12
|
+
export * from "./state.js";
|
|
13
|
+
export * from "./outbox.js";
|
|
14
|
+
export * from "./company.js";
|
|
15
|
+
export * from "./project.js";
|
|
16
|
+
export { decisionFromCandidates, digestOrganizePayload, formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, stableDecisionId, stableOrganizeOperationId, submitOrganizeDecision, } from "./organize.js";
|
|
17
|
+
export type { CandidatesResponse, FetchCandidatesFn, OrganizeCandidate, OrganizeListResult, OrganizeReceipt, OrganizeSubmitDecision, OrganizeSubmitResult, PostOrganizeFn, } from "./organize.js";
|
|
18
|
+
export { digestMigratePayload, formatMigrateConfirmation, stableMigrateOperationId, submitSessionMigration, } from "./migrate.js";
|
|
19
|
+
export type { MigrateDestination, MigrateReceipt, MigrateSubmitResult, PostMigrateFn, } from "./migrate.js";
|
|
20
|
+
export { deriveRemoteOwnerSlug, matchCompanySlugForRepo, normalizeRemoteOwnerName, } from "./repo-remote.js";
|
|
21
|
+
export { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "./reconcile.js";
|
|
22
|
+
export type { ReconcileDeps, ReconcileObservation, ReconcileOutcome, } from "./reconcile.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Context reconciliation package (consumable by hq-cli).
|
|
3
|
+
* Contract (US-001) + reconcile / outbox / device default (US-007A)
|
|
4
|
+
* + organize / project resolution (US-007B)
|
|
5
|
+
* + cross-company correct / migrate (US-017B).
|
|
6
|
+
*/
|
|
7
|
+
export * from "./contract.js";
|
|
8
|
+
export * from "./errors.js";
|
|
9
|
+
export * from "./paths.js";
|
|
10
|
+
export * from "./atomic.js";
|
|
11
|
+
export * from "./config.js";
|
|
12
|
+
export * from "./state.js";
|
|
13
|
+
export * from "./outbox.js";
|
|
14
|
+
export * from "./company.js";
|
|
15
|
+
export * from "./project.js";
|
|
16
|
+
export { decisionFromCandidates, digestOrganizePayload, formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, stableDecisionId, stableOrganizeOperationId, submitOrganizeDecision, } from "./organize.js";
|
|
17
|
+
export { digestMigratePayload, formatMigrateConfirmation, stableMigrateOperationId, submitSessionMigration, } from "./migrate.js";
|
|
18
|
+
export { deriveRemoteOwnerSlug, matchCompanySlugForRepo, normalizeRemoteOwnerName, } from "./repo-remote.js";
|
|
19
|
+
export { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "./reconcile.js";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-company session correction (US-017B).
|
|
3
|
+
*
|
|
4
|
+
* Submits only a structured migrate request (operationId + digest), tracks the
|
|
5
|
+
* receipt via the outbox, and keeps the local state file at migration_pending
|
|
6
|
+
* until the destination receipt arrives. Never rebinds by any other path.
|
|
7
|
+
*/
|
|
8
|
+
import { type OutboxOperation } from "./outbox.js";
|
|
9
|
+
import { type SessionStateFile } from "./state.js";
|
|
10
|
+
export interface MigrateDestination {
|
|
11
|
+
projectId?: string;
|
|
12
|
+
taskId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MigrateReceipt {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
sourceCompanyUid: string;
|
|
17
|
+
destinationCompanyUid: string;
|
|
18
|
+
destination: MigrateDestination;
|
|
19
|
+
eventCount: number;
|
|
20
|
+
bindingCount: number;
|
|
21
|
+
receiptCount: number;
|
|
22
|
+
version: number;
|
|
23
|
+
migratedAt: string;
|
|
24
|
+
receipt: {
|
|
25
|
+
operationId: string;
|
|
26
|
+
digest: string;
|
|
27
|
+
status: "committed" | "replayed";
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface MigrateSubmitResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** True when confirmation was declined — nothing was sent. */
|
|
33
|
+
cancelled?: boolean;
|
|
34
|
+
pending?: boolean;
|
|
35
|
+
receipt?: MigrateReceipt;
|
|
36
|
+
outbox?: OutboxOperation;
|
|
37
|
+
state?: SessionStateFile;
|
|
38
|
+
confirmationSummary?: string;
|
|
39
|
+
errorCode?: string;
|
|
40
|
+
message?: string;
|
|
41
|
+
}
|
|
42
|
+
export type PostMigrateFn = (input: {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
operationId: string;
|
|
45
|
+
digest: string;
|
|
46
|
+
sourceCompanyUid: string;
|
|
47
|
+
destinationCompanyUid: string;
|
|
48
|
+
destination: MigrateDestination;
|
|
49
|
+
expectedVersion: number;
|
|
50
|
+
}) => Promise<MigrateReceipt>;
|
|
51
|
+
/** Canonical digest material — mirrors hq-pro migrateDigestPayload + sha256. */
|
|
52
|
+
export declare function digestMigratePayload(parts: {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
sourceCompanyUid: string;
|
|
55
|
+
destinationCompanyUid: string;
|
|
56
|
+
destination: MigrateDestination;
|
|
57
|
+
expectedVersion: number;
|
|
58
|
+
}): string;
|
|
59
|
+
export declare function stableMigrateOperationId(sessionId: string, sourceCompanyUid: string, destinationCompanyUid: string): string;
|
|
60
|
+
/** Human-readable summary printed before the confirmation gate. */
|
|
61
|
+
export declare function formatMigrateConfirmation(input: {
|
|
62
|
+
sessionId: string;
|
|
63
|
+
sourceCompanySlug?: string;
|
|
64
|
+
sourceCompanyUid: string;
|
|
65
|
+
destinationCompanySlug: string;
|
|
66
|
+
destinationCompanyUid: string;
|
|
67
|
+
destination: MigrateDestination;
|
|
68
|
+
}): string;
|
|
69
|
+
/**
|
|
70
|
+
* Submit a cross-company correction after the caller has already confirmed.
|
|
71
|
+
* Writes migration_pending + outbox before any network I/O; on destination
|
|
72
|
+
* receipt, rewrites the state file to the destination company.
|
|
73
|
+
*/
|
|
74
|
+
export declare function submitSessionMigration(input: {
|
|
75
|
+
sessionId: string;
|
|
76
|
+
root: string;
|
|
77
|
+
destinationCompanyUid: string;
|
|
78
|
+
destinationCompanySlug: string;
|
|
79
|
+
destination?: MigrateDestination;
|
|
80
|
+
expectedVersion?: number;
|
|
81
|
+
now?: () => Date;
|
|
82
|
+
postMigrate: PostMigrateFn;
|
|
83
|
+
}): Promise<MigrateSubmitResult>;
|
|
84
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-company session correction (US-017B).
|
|
3
|
+
*
|
|
4
|
+
* Submits only a structured migrate request (operationId + digest), tracks the
|
|
5
|
+
* receipt via the outbox, and keeps the local state file at migration_pending
|
|
6
|
+
* until the destination receipt arrives. Never rebinds by any other path.
|
|
7
|
+
*/
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
9
|
+
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
10
|
+
import { NotTrackingError } from "./errors.js";
|
|
11
|
+
import { enqueueOutbox, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, } from "./outbox.js";
|
|
12
|
+
import { mergeLocalSessionFields, readSessionState, writeSessionState, } from "./state.js";
|
|
13
|
+
/** Canonical digest material — mirrors hq-pro migrateDigestPayload + sha256. */
|
|
14
|
+
export function digestMigratePayload(parts) {
|
|
15
|
+
const canonical = JSON.stringify({
|
|
16
|
+
sessionId: parts.sessionId,
|
|
17
|
+
sourceCompanyUid: parts.sourceCompanyUid,
|
|
18
|
+
destinationCompanyUid: parts.destinationCompanyUid,
|
|
19
|
+
destination: {
|
|
20
|
+
...(parts.destination.projectId
|
|
21
|
+
? { projectId: parts.destination.projectId }
|
|
22
|
+
: {}),
|
|
23
|
+
...(parts.destination.taskId ? { taskId: parts.destination.taskId } : {}),
|
|
24
|
+
},
|
|
25
|
+
expectedVersion: parts.expectedVersion,
|
|
26
|
+
});
|
|
27
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
export function stableMigrateOperationId(sessionId, sourceCompanyUid, destinationCompanyUid) {
|
|
30
|
+
const h = crypto
|
|
31
|
+
.createHash("sha256")
|
|
32
|
+
.update(`migrate:v1:${sessionId}:${sourceCompanyUid}:${destinationCompanyUid}`)
|
|
33
|
+
.digest("hex")
|
|
34
|
+
.slice(0, 32);
|
|
35
|
+
return `op_${h}`;
|
|
36
|
+
}
|
|
37
|
+
/** Human-readable summary printed before the confirmation gate. */
|
|
38
|
+
export function formatMigrateConfirmation(input) {
|
|
39
|
+
const from = input.sourceCompanySlug && input.sourceCompanySlug !== input.sourceCompanyUid
|
|
40
|
+
? `${input.sourceCompanySlug} (${input.sourceCompanyUid})`
|
|
41
|
+
: input.sourceCompanyUid;
|
|
42
|
+
const to = input.destinationCompanySlug !== input.destinationCompanyUid
|
|
43
|
+
? `${input.destinationCompanySlug} (${input.destinationCompanyUid})`
|
|
44
|
+
: input.destinationCompanyUid;
|
|
45
|
+
const destBits = [];
|
|
46
|
+
if (input.destination.projectId) {
|
|
47
|
+
destBits.push(`project=${input.destination.projectId}`);
|
|
48
|
+
}
|
|
49
|
+
if (input.destination.taskId) {
|
|
50
|
+
destBits.push(`task=${input.destination.taskId}`);
|
|
51
|
+
}
|
|
52
|
+
const destExtra = destBits.length ? ` (${destBits.join(", ")})` : "";
|
|
53
|
+
return [
|
|
54
|
+
`Move session ${input.sessionId}`,
|
|
55
|
+
` from company: ${from}`,
|
|
56
|
+
` to company: ${to}${destExtra}`,
|
|
57
|
+
"This freezes the source, copies history to the destination, and leaves a tombstone. It does not move any project or task.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Submit a cross-company correction after the caller has already confirmed.
|
|
62
|
+
* Writes migration_pending + outbox before any network I/O; on destination
|
|
63
|
+
* receipt, rewrites the state file to the destination company.
|
|
64
|
+
*/
|
|
65
|
+
export async function submitSessionMigration(input) {
|
|
66
|
+
const state = readSessionState(input.sessionId, input.root);
|
|
67
|
+
if (!state) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
errorCode: "NO_STATE",
|
|
71
|
+
message: `No work-context state for session ${input.sessionId}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (!state.companyUid) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
errorCode: "SOURCE_COMPANY_REQUIRED",
|
|
78
|
+
message: "Session has no verified source companyUid; cannot migrate across companies",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (state.companyUid === input.destinationCompanyUid) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
errorCode: "SAME_COMPANY",
|
|
85
|
+
message: "Destination company must differ from the session's current company",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (state.contextStatus === "untracked") {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
errorCode: "UNTRACKED",
|
|
92
|
+
message: "Untracked sessions have no server history to migrate",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const destination = {};
|
|
96
|
+
if (input.destination?.projectId?.trim()) {
|
|
97
|
+
destination.projectId = input.destination.projectId.trim();
|
|
98
|
+
}
|
|
99
|
+
if (input.destination?.taskId?.trim()) {
|
|
100
|
+
destination.taskId = input.destination.taskId.trim();
|
|
101
|
+
}
|
|
102
|
+
if (destination.taskId && !destination.projectId) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
errorCode: "PROJECT_REQUIRED",
|
|
106
|
+
message: "destination projectId is required when taskId is set",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const expectedVersion = typeof input.expectedVersion === "number" &&
|
|
110
|
+
Number.isInteger(input.expectedVersion) &&
|
|
111
|
+
input.expectedVersion >= 0
|
|
112
|
+
? input.expectedVersion
|
|
113
|
+
: typeof state.version === "number"
|
|
114
|
+
? state.version
|
|
115
|
+
: 0;
|
|
116
|
+
const operationId = stableMigrateOperationId(input.sessionId, state.companyUid, input.destinationCompanyUid);
|
|
117
|
+
const digest = digestMigratePayload({
|
|
118
|
+
sessionId: input.sessionId,
|
|
119
|
+
sourceCompanyUid: state.companyUid,
|
|
120
|
+
destinationCompanyUid: input.destinationCompanyUid,
|
|
121
|
+
destination,
|
|
122
|
+
expectedVersion,
|
|
123
|
+
});
|
|
124
|
+
const confirmationSummary = formatMigrateConfirmation({
|
|
125
|
+
sessionId: input.sessionId,
|
|
126
|
+
sourceCompanySlug: state.companySlug,
|
|
127
|
+
sourceCompanyUid: state.companyUid,
|
|
128
|
+
destinationCompanySlug: input.destinationCompanySlug,
|
|
129
|
+
destinationCompanyUid: input.destinationCompanyUid,
|
|
130
|
+
destination,
|
|
131
|
+
});
|
|
132
|
+
const nowFn = input.now ?? (() => new Date());
|
|
133
|
+
const nowIso = nowFn().toISOString();
|
|
134
|
+
let outbox;
|
|
135
|
+
try {
|
|
136
|
+
outbox = enqueueOutbox({
|
|
137
|
+
clientOperationId: operationId,
|
|
138
|
+
sessionId: input.sessionId,
|
|
139
|
+
companyUid: state.companyUid,
|
|
140
|
+
companySlug: state.companySlug,
|
|
141
|
+
projectId: destination.projectId,
|
|
142
|
+
taskId: destination.taskId,
|
|
143
|
+
kind: "migrate",
|
|
144
|
+
destinationCompanyUid: input.destinationCompanyUid,
|
|
145
|
+
destinationCompanySlug: input.destinationCompanySlug,
|
|
146
|
+
now: nowFn,
|
|
147
|
+
}, input.root);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err instanceof NotTrackingError) {
|
|
151
|
+
return { ok: false, errorCode: "NOT_TRACKING", message: err.message };
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
// Freeze local view before network I/O.
|
|
156
|
+
const pendingState = mergeLocalSessionFields({
|
|
157
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
158
|
+
sessionId: input.sessionId,
|
|
159
|
+
companyUid: state.companyUid,
|
|
160
|
+
companySlug: state.companySlug,
|
|
161
|
+
contextStatus: "migration_pending",
|
|
162
|
+
...(state.projectId ? { projectId: state.projectId } : {}),
|
|
163
|
+
...(state.taskId ? { taskId: state.taskId } : {}),
|
|
164
|
+
...(state.bindingEpisodeId
|
|
165
|
+
? { bindingEpisodeId: state.bindingEpisodeId }
|
|
166
|
+
: {}),
|
|
167
|
+
...(typeof state.version === "number" ? { version: state.version } : {}),
|
|
168
|
+
migration: {
|
|
169
|
+
operationId: outbox.operationId,
|
|
170
|
+
digest,
|
|
171
|
+
sourceCompanyUid: state.companyUid,
|
|
172
|
+
destinationCompanyUid: input.destinationCompanyUid,
|
|
173
|
+
destinationCompanySlug: input.destinationCompanySlug,
|
|
174
|
+
...(Object.keys(destination).length ? { destination } : {}),
|
|
175
|
+
submittedAt: nowIso,
|
|
176
|
+
},
|
|
177
|
+
updatedAt: nowIso,
|
|
178
|
+
}, state);
|
|
179
|
+
try {
|
|
180
|
+
writeSessionState(pendingState, input.root);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err instanceof NotTrackingError) {
|
|
184
|
+
return { ok: false, errorCode: "NOT_TRACKING", message: err.message };
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
let receipt;
|
|
189
|
+
try {
|
|
190
|
+
receipt = await input.postMigrate({
|
|
191
|
+
sessionId: input.sessionId,
|
|
192
|
+
operationId: outbox.operationId,
|
|
193
|
+
digest,
|
|
194
|
+
sourceCompanyUid: state.companyUid,
|
|
195
|
+
destinationCompanyUid: input.destinationCompanyUid,
|
|
196
|
+
destination,
|
|
197
|
+
expectedVersion,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
const retryable = /ECONN|ENOTFOUND|ETIMEDOUT|network|HTTP_5|429/i.test(message);
|
|
203
|
+
if (retryable) {
|
|
204
|
+
markOutboxQueued(outbox.operationId, input.root, "MIGRATE_RETRYABLE", nowFn);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
markOutboxQuarantined(outbox.operationId, input.root, "MIGRATE_FAILED", nowFn);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
pending: true,
|
|
212
|
+
outbox: outbox,
|
|
213
|
+
state: readSessionState(input.sessionId, input.root) ?? pendingState,
|
|
214
|
+
confirmationSummary,
|
|
215
|
+
errorCode: "MIGRATE_FAILED",
|
|
216
|
+
message,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (!receipt?.receipt?.operationId ||
|
|
220
|
+
!receipt.destinationCompanyUid ||
|
|
221
|
+
!receipt.migratedAt) {
|
|
222
|
+
markOutboxQuarantined(outbox.operationId, input.root, "RECEIPT_INCOMPLETE", nowFn);
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
pending: true,
|
|
226
|
+
outbox,
|
|
227
|
+
state: readSessionState(input.sessionId, input.root) ?? pendingState,
|
|
228
|
+
confirmationSummary,
|
|
229
|
+
errorCode: "RECEIPT_INCOMPLETE",
|
|
230
|
+
message: "Server response missing destination migrate receipt",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const acked = markOutboxAcked(outbox.operationId, input.root, receipt.receipt.operationId, nowFn) ?? outbox;
|
|
234
|
+
const appliedAt = nowFn().toISOString();
|
|
235
|
+
const destStatus = receipt.destination.projectId && receipt.destination.taskId
|
|
236
|
+
? "bound"
|
|
237
|
+
: receipt.destination.projectId
|
|
238
|
+
? "needs_task"
|
|
239
|
+
: "unresolved";
|
|
240
|
+
const applied = {
|
|
241
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
242
|
+
sessionId: input.sessionId,
|
|
243
|
+
companyUid: receipt.destinationCompanyUid,
|
|
244
|
+
companySlug: input.destinationCompanySlug,
|
|
245
|
+
contextStatus: destStatus,
|
|
246
|
+
...(receipt.destination.projectId
|
|
247
|
+
? { projectId: receipt.destination.projectId }
|
|
248
|
+
: {}),
|
|
249
|
+
...(receipt.destination.taskId
|
|
250
|
+
? { taskId: receipt.destination.taskId }
|
|
251
|
+
: {}),
|
|
252
|
+
version: receipt.version,
|
|
253
|
+
migration: {
|
|
254
|
+
operationId: receipt.receipt.operationId,
|
|
255
|
+
digest: receipt.receipt.digest || digest,
|
|
256
|
+
sourceCompanyUid: receipt.sourceCompanyUid,
|
|
257
|
+
destinationCompanyUid: receipt.destinationCompanyUid,
|
|
258
|
+
destinationCompanySlug: input.destinationCompanySlug,
|
|
259
|
+
...(Object.keys(receipt.destination).length
|
|
260
|
+
? { destination: receipt.destination }
|
|
261
|
+
: {}),
|
|
262
|
+
submittedAt: nowIso,
|
|
263
|
+
receiptId: receipt.receipt.operationId,
|
|
264
|
+
completedAt: receipt.migratedAt,
|
|
265
|
+
},
|
|
266
|
+
updatedAt: appliedAt,
|
|
267
|
+
};
|
|
268
|
+
// Preserve local-only counters; drop source project/task when destination
|
|
269
|
+
// did not name replacements (migration never moves a project/task).
|
|
270
|
+
const merged = mergeLocalSessionFields(applied, {
|
|
271
|
+
...state,
|
|
272
|
+
projectId: undefined,
|
|
273
|
+
taskId: undefined,
|
|
274
|
+
bindingEpisodeId: undefined,
|
|
275
|
+
decision: undefined,
|
|
276
|
+
});
|
|
277
|
+
writeSessionState(merged, input.root);
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
pending: false,
|
|
281
|
+
receipt,
|
|
282
|
+
outbox: acked,
|
|
283
|
+
state: merged,
|
|
284
|
+
confirmationSummary,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq mesh context organize — one-time decisions and US-005B submit (US-007B).
|
|
3
|
+
*
|
|
4
|
+
* CLI only presents server candidates and submits structured decisions.
|
|
5
|
+
* Ranking is server-owned. Success is reported only from the server receipt.
|
|
6
|
+
* Cancel / timeout / incapable surface leaves the session unresolved; the CLI
|
|
7
|
+
* never invents a project or adhoc target and never repeats the ask.
|
|
8
|
+
*/
|
|
9
|
+
import { type SessionDecision, type SessionStateFile } from "./state.js";
|
|
10
|
+
export interface OrganizeCandidate {
|
|
11
|
+
candidateId: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
taskId?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
projectName: string;
|
|
16
|
+
updatedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CandidatesResponse {
|
|
19
|
+
contractVersion: 1;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
companyUid: string;
|
|
22
|
+
decisionExpiresAt: string;
|
|
23
|
+
candidates: OrganizeCandidate[];
|
|
24
|
+
}
|
|
25
|
+
export type OrganizeDecisionKind = "bind_existing" | "create_project" | "create_task";
|
|
26
|
+
export interface OrganizeSubmitDecision {
|
|
27
|
+
kind: OrganizeDecisionKind;
|
|
28
|
+
projectId?: string;
|
|
29
|
+
taskId?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface OrganizeReceipt {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
companyUid: string;
|
|
35
|
+
projectId: string;
|
|
36
|
+
taskId?: string;
|
|
37
|
+
episodeId?: string;
|
|
38
|
+
bindingEpisodeId: string;
|
|
39
|
+
contextStatus: "bound";
|
|
40
|
+
version?: number;
|
|
41
|
+
openedAt?: string;
|
|
42
|
+
kind?: OrganizeDecisionKind;
|
|
43
|
+
receipt: {
|
|
44
|
+
operationId: string;
|
|
45
|
+
digest: string;
|
|
46
|
+
status: "committed" | "replayed";
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface OrganizeListResult {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
contextStatus: string;
|
|
52
|
+
pending: boolean;
|
|
53
|
+
decision: SessionDecision | null;
|
|
54
|
+
/** Why no ask is surfaced (threshold / already asked / bound / etc.). */
|
|
55
|
+
reason?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface OrganizeSubmitResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
receipt?: OrganizeReceipt;
|
|
60
|
+
localUntracked?: boolean;
|
|
61
|
+
errorCode?: string;
|
|
62
|
+
message?: string;
|
|
63
|
+
}
|
|
64
|
+
export type FetchCandidatesFn = (input: {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
companyUid: string;
|
|
67
|
+
}) => Promise<CandidatesResponse>;
|
|
68
|
+
export type PostOrganizeFn = (input: {
|
|
69
|
+
sessionId: string;
|
|
70
|
+
companyUid: string;
|
|
71
|
+
operationId: string;
|
|
72
|
+
digest: string;
|
|
73
|
+
decision: OrganizeSubmitDecision;
|
|
74
|
+
expectedVersion: number;
|
|
75
|
+
decisionExpiresAt?: string;
|
|
76
|
+
}) => Promise<OrganizeReceipt>;
|
|
77
|
+
/** Stable decision id for a session + expiry window. */
|
|
78
|
+
export declare function stableDecisionId(sessionId: string, decisionExpiresAt: string): string;
|
|
79
|
+
export declare function digestOrganizePayload(parts: {
|
|
80
|
+
sessionId: string;
|
|
81
|
+
companyUid: string;
|
|
82
|
+
decision: OrganizeSubmitDecision;
|
|
83
|
+
expectedVersion: number;
|
|
84
|
+
}): string;
|
|
85
|
+
export declare function stableOrganizeOperationId(sessionId: string, decision: OrganizeSubmitDecision): string;
|
|
86
|
+
/**
|
|
87
|
+
* Build a pending decision from server candidates. Soft evidence ranks on the
|
|
88
|
+
* server; the CLI never auto-binds from this list.
|
|
89
|
+
*/
|
|
90
|
+
export declare function decisionFromCandidates(sessionId: string, response: CandidatesResponse): SessionDecision;
|
|
91
|
+
/**
|
|
92
|
+
* Evaluate work-signal threshold and, when crossed for the first time, attach
|
|
93
|
+
* a one-time decision (optionally from server candidates). Never invents a
|
|
94
|
+
* project. If a decision already exists, it is left untouched (ask-once).
|
|
95
|
+
*/
|
|
96
|
+
export declare function prepareOrganizeDecision(input: {
|
|
97
|
+
sessionId: string;
|
|
98
|
+
root: string;
|
|
99
|
+
now?: () => Date;
|
|
100
|
+
fetchCandidates?: FetchCandidatesFn;
|
|
101
|
+
/** Skip network; leave unresolved without candidates. */
|
|
102
|
+
offline?: boolean;
|
|
103
|
+
}): Promise<OrganizeListResult>;
|
|
104
|
+
export declare function formatOrganizeList(result: OrganizeListResult): string[];
|
|
105
|
+
/**
|
|
106
|
+
* Mark that the ask was cancelled / timed out / surface incapable.
|
|
107
|
+
* Leaves the session unresolved and visible; clears askAfter so the CLI
|
|
108
|
+
* never repeats the ask; never invents a project.
|
|
109
|
+
*/
|
|
110
|
+
export declare function settleOrganizeAskWithoutBind(input: {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
root: string;
|
|
113
|
+
now?: () => Date;
|
|
114
|
+
reason?: "cancelled" | "timeout" | "incapable";
|
|
115
|
+
}): SessionStateFile | null;
|
|
116
|
+
/**
|
|
117
|
+
* Submit an organize decision through US-005B. Success only from receipt.
|
|
118
|
+
*/
|
|
119
|
+
export declare function submitOrganizeDecision(input: {
|
|
120
|
+
sessionId: string;
|
|
121
|
+
root: string;
|
|
122
|
+
decisionId?: string;
|
|
123
|
+
optionId?: string;
|
|
124
|
+
createProjectTitle?: string;
|
|
125
|
+
createTaskTitle?: string;
|
|
126
|
+
untracked?: boolean;
|
|
127
|
+
now?: () => Date;
|
|
128
|
+
postOrganize?: PostOrganizeFn;
|
|
129
|
+
}): Promise<OrganizeSubmitResult>;
|
|
130
|
+
//# sourceMappingURL=organize.d.ts.map
|