@indigoai-us/hq-cli 5.312.0 → 5.312.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
  3. package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
  4. package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
  5. package/assets/scaffold/core/scripts/register-project.sh +262 -0
  6. package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
  7. package/dist/command-catalog.generated.d.ts +65 -0
  8. package/dist/command-catalog.generated.js +80 -0
  9. package/dist/commands/company-rename-flag.d.ts +4 -0
  10. package/dist/commands/company-rename-flag.js +7 -0
  11. package/dist/commands/company-rename.d.ts +4 -0
  12. package/dist/commands/company-rename.js +90 -0
  13. package/dist/commands/company.js +2 -0
  14. package/dist/commands/core-checkpoint-stop-gate-native.js +12 -0
  15. package/dist/commands/core-eval-trigger-native.d.ts +4 -0
  16. package/dist/commands/core-eval-trigger-native.js +12 -0
  17. package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
  18. package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
  19. package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
  20. package/dist/commands/core-native-hook-helper-commands.js +56 -0
  21. package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
  22. package/dist/commands/core-native-hook-helpers-flag.js +7 -0
  23. package/dist/commands/core-register-project-native.d.ts +30 -0
  24. package/dist/commands/core-register-project-native.js +347 -0
  25. package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
  26. package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
  27. package/dist/commands/core.js +7 -0
  28. package/dist/fast-core.js +10 -3
  29. package/dist/fast-scaffold.d.ts +2 -0
  30. package/dist/fast-scaffold.js +11 -0
  31. package/dist/lib/agents/model-catalog.d.ts +3 -2
  32. package/dist/lib/company-skill-wrapper.d.ts +0 -8
  33. package/dist/lib/core-utils/codex-skill-bridge.d.ts +2 -1
  34. package/dist/lib/deploy-api.d.ts +0 -1
  35. package/dist/lib/deploy-api.js +1 -1
  36. package/dist/lib/doctor/checks/work-context.js +9 -1
  37. package/dist/lib/mesh/client.js +23 -5
  38. package/dist/lib/mesh/denial-codes.d.ts +5 -0
  39. package/dist/lib/mesh/denial-codes.js +14 -0
  40. package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
  41. package/dist/lib/mesh/live/daemon/doctor.js +11 -1
  42. package/dist/lib/report-run.d.ts +3 -2
  43. package/dist/lib/report-templates.d.ts +0 -1
  44. package/dist/lib/report-templates.js +0 -3
  45. package/dist/lib/scaffold-cli-floor.d.ts +0 -1
  46. package/dist/lib/scaffold-cli-floor.js +1 -1
  47. package/dist/lib/search-index/embed-lock.d.ts +2 -0
  48. package/dist/lib/search-index/embed-lock.js +10 -1
  49. package/dist/lib/work-context/contract.d.ts +3 -0
  50. package/dist/lib/work-context/contract.schema.json +3 -1
  51. package/dist/lib/work-context/outbox.d.ts +14 -5
  52. package/dist/lib/work-context/outbox.js +48 -5
  53. package/dist/lib/work-context/reconcile.js +26 -5
  54. package/dist/main.d.ts +20 -0
  55. package/dist/main.js +8 -1
  56. package/dist/run/discover-schemas.d.ts +2 -1
  57. package/dist/utils/core-eval-trigger.d.ts +4 -0
  58. package/dist/utils/core-eval-trigger.js +88 -0
  59. package/dist/utils/hq-roots.d.ts +0 -2
  60. package/dist/utils/hq-roots.js +1 -8
  61. package/dist/utils/incomplete-install-error.d.ts +7 -6
  62. package/dist/utils/large-file-guard.d.ts +0 -10
  63. package/dist/utils/large-file-guard.js +1 -1
  64. package/dist/utils/package-root-diagnostics.d.ts +1 -1
  65. package/dist/utils/self-update.d.ts +2 -1
  66. package/package.json +1 -1
package/dist/fast-core.js CHANGED
@@ -59,12 +59,13 @@ export async function runFastCore(argv) {
59
59
  if (typeof handler !== "function") {
60
60
  throw new Error(`Native fast handler ${entry.exportName} was not exported`);
61
61
  }
62
+ const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
62
63
  const chunks = [];
63
- if (entry.readsStdin) {
64
+ if (readsStdin) {
64
65
  for await (const chunk of process.stdin)
65
66
  chunks.push(chunk);
66
67
  }
67
- const code = await handler(parsed.args, entry.readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
68
+ const code = await handler(parsed.args, readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
68
69
  // Let Node drain handler output before terminating. The timeout guard's
69
70
  // stderr message must survive the native route's process boundary.
70
71
  process.exitCode = code;
@@ -80,12 +81,18 @@ export async function runFastCore(argv) {
80
81
  : process.cwd();
81
82
  const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
82
83
  (entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
84
+ const childEnv = {
85
+ ...(entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : {}),
86
+ ...(entry.name === "migrate-policy-triggers"
87
+ ? { CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR || process.cwd() }
88
+ : {}),
89
+ };
83
90
  const { code } = runBundledScript({
84
91
  asset,
85
92
  args: parsed.args,
86
93
  cwd,
87
94
  ...(injectRoot ? { hqRoot } : {}),
88
- env: entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
95
+ ...(Object.keys(childEnv).length ? { env: childEnv } : {}),
89
96
  });
90
97
  // Propagate verbatim: these scripts use their exit code as their interface.
91
98
  process.exit(code);
@@ -40,6 +40,8 @@ export type FastScaffoldScript = {
40
40
  shellHqRoot?: "none" | "live" | "reconcile-default";
41
41
  /** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
42
42
  readsStdin?: boolean;
43
+ /** Per-invocation stdin policy for handlers whose shell reads conditionally. */
44
+ readsStdinFor?: (args: readonly string[]) => boolean;
43
45
  /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
44
46
  requiresNodeNetworkCompatibility?: boolean;
45
47
  };
@@ -184,6 +184,17 @@ export const FAST_SCAFFOLD_SCRIPTS = [
184
184
  shellCwd: "live",
185
185
  shellHqRoot: "live",
186
186
  },
187
+ ...[
188
+ { name: "eval-trigger", modulePath: "./commands/core-eval-trigger-native.js", exportName: "runEvalTriggerFast", asset: "core/scripts/eval-trigger.sh", root: "cwd", shellCwd: "caller", shellHqRoot: "none", readsStdinFor: (args) => args[0] === "--check" },
189
+ { name: "migrate-policy-triggers", modulePath: "./commands/core-migrate-policy-triggers-native.js", exportName: "runMigratePolicyTriggersFast", asset: "core/scripts/migrate-policy-triggers.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
190
+ { name: "register-project", modulePath: "./commands/core-register-project-native.js", exportName: "runRegisterProjectFast", asset: "core/scripts/register-project.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
191
+ { name: "work-mesh-live-rebind", modulePath: "./commands/core-work-mesh-live-rebind-native.js", exportName: "runWorkMeshLiveRebindFast", asset: "core/scripts/work-mesh-live-rebind.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
192
+ ].map((entry) => ({
193
+ ...entry,
194
+ kind: "native",
195
+ flagKey: "hq-core-native-fast-route",
196
+ familyFlagKey: "core-native-hook-helpers",
197
+ })),
187
198
  ...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
188
199
  name: `delegate ${name}`,
189
200
  kind: "native",
@@ -1,6 +1,6 @@
1
1
  export declare const AGENT_MODEL_PROVIDERS: readonly ["codex", "grok", "claude"];
2
2
  export type AgentModelProvider = (typeof AGENT_MODEL_PROVIDERS)[number];
3
- export interface AgentCatalogModel {
3
+ interface AgentCatalogModel {
4
4
  id: string;
5
5
  label: string;
6
6
  visibility: string;
@@ -10,7 +10,7 @@ export interface AgentCatalogModel {
10
10
  selectable: boolean;
11
11
  unavailableReason?: string | null;
12
12
  }
13
- export interface AgentCatalogProvider {
13
+ interface AgentCatalogProvider {
14
14
  models: AgentCatalogModel[];
15
15
  defaultModel: string;
16
16
  }
@@ -58,4 +58,5 @@ export declare function validateAgentModelSelection(catalog: AgentModelCatalog,
58
58
  export declare function modelSelectionErrorMessage(modelId: string, result: Extract<ModelSelectionResult, {
59
59
  ok: false;
60
60
  }>): string;
61
+ export {};
61
62
  //# sourceMappingURL=model-catalog.d.ts.map
@@ -1,12 +1,4 @@
1
1
  import * as fs from "node:fs";
2
- export interface CompanySkillWrapperOwnershipDiagnostics {
3
- kind: "directory" | "symlink" | "file" | "other";
4
- bridgeState: "all_live" | "partial" | "none" | "empty" | "not_applicable";
5
- markerState: "bound" | "missing" | "invalid" | "unbound" | "unavailable" | "not_applicable";
6
- linkTarget: "company_skills" | "outside_company_skills" | "unavailable" | "not_applicable";
7
- platform: "win32" | "darwin" | "linux" | "other";
8
- nameEncoding: "percent_encoded" | "literal";
9
- }
10
2
  export declare function companySkillWrapperSentryTagsFor(error: unknown): Record<string, string> | undefined;
11
3
  export interface SurfaceCompanySkillInput {
12
4
  hqRoot: string;
@@ -1,4 +1,4 @@
1
- export type BridgeCommand = "status" | "install" | "doctor";
1
+ type BridgeCommand = "status" | "install" | "doctor";
2
2
  export type BridgeOptions = {
3
3
  command: BridgeCommand;
4
4
  hqRoot: string;
@@ -28,4 +28,5 @@ export declare function isHqRootLike(candidate: string): boolean;
28
28
  export declare function outputStyleSlug(name: string): string;
29
29
  /** Runs one bridge command; returns the process exit code. */
30
30
  export declare function runCodexSkillBridge(options: BridgeOptions, io: BridgeIo): number;
31
+ export {};
31
32
  //# sourceMappingURL=codex-skill-bridge.d.ts.map
@@ -1,6 +1,5 @@
1
1
  /** Shared authenticated request path for the hq-deploy control plane. */
2
2
  export declare const DEPLOY_API_URL: string;
3
3
  export declare function deployHeaders(token: string, json?: boolean, extra?: Record<string, string>): Record<string, string>;
4
- export declare function deployApiUrl(route: string): string;
5
4
  export declare function deployApiRequest(token: string, route: string, method: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<Response>;
6
5
  //# sourceMappingURL=deploy-api.d.ts.map
@@ -8,7 +8,7 @@ export function deployHeaders(token, json = true, extra = {}) {
8
8
  ...extra,
9
9
  };
10
10
  }
11
- export function deployApiUrl(route) {
11
+ function deployApiUrl(route) {
12
12
  return `${DEPLOY_API_URL.replace(/\/+$/, "")}${route}`;
13
13
  }
14
14
  export async function deployApiRequest(token, route, method, body, extraHeaders = {}) {
@@ -89,12 +89,20 @@ function runChecks(deps) {
89
89
  message: `Outbox queued depth=${stats.depth} (acked=${stats.acked}).`,
90
90
  });
91
91
  const showQuarantineReason = stats.quarantined > 0 && outboxQuarantineStatusEnabled();
92
+ const quarantineDetails = [
93
+ stats.lastQuarantineHttpStatus !== null
94
+ ? `HTTP ${stats.lastQuarantineHttpStatus}`
95
+ : "",
96
+ stats.lastQuarantineServerCode ?? "",
97
+ ]
98
+ .filter(Boolean)
99
+ .join(" ");
92
100
  results.push({
93
101
  status: stats.quarantined > 0 ? "WARN" : "PASS",
94
102
  checkId: "work-context.outbox-quarantined",
95
103
  target: path.join(root, "outbox"),
96
104
  message: `Outbox quarantined count=${stats.quarantined}${showQuarantineReason
97
- ? ` (last reason=${stats.lastQuarantineReason ?? "unknown"})`
105
+ ? ` (last reason=${stats.lastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`
98
106
  : ""}.`,
99
107
  });
100
108
  // Default-company mode + migration capability (US-017B)
@@ -4,6 +4,7 @@
4
4
  * vault HTTP + receipt-shaped responses (US-007A / US-007B).
5
5
  */
6
6
  import { listActiveMembershipCompanies, meshJson, requireToken, resolveMeshCompany, } from "./api.js";
7
+ import { isWorkSessionDenialCode } from "./denial-codes.js";
7
8
  export { meshJson, requireToken, resolveMeshCompany, listActiveMembershipCompanies };
8
9
  export async function openMeshTransport(opts) {
9
10
  const token = await requireToken();
@@ -50,11 +51,28 @@ export function createWorkSessionDeliverer(opts) {
50
51
  const message = err instanceof Error ? err.message : String(err);
51
52
  // meshJson throws on !ok with error text; classify by leading status when present.
52
53
  const statusMatch = /^(\d{3})\b/.exec(message);
53
- if (statusMatch) {
54
- const status = Number(statusMatch[1]);
55
- if (status === 401 || status === 403) {
56
- return { ok: false, retryable: false, code: "AUTH_DENIED" };
57
- }
54
+ const errorFields = err && typeof err === "object"
55
+ ? err
56
+ : {};
57
+ const status = typeof errorFields.status === "number" &&
58
+ Number.isInteger(errorFields.status)
59
+ ? errorFields.status
60
+ : statusMatch
61
+ ? Number(statusMatch[1])
62
+ : undefined;
63
+ if (status === 401 || status === 403) {
64
+ const serverCode = isWorkSessionDenialCode(errorFields.code)
65
+ ? errorFields.code
66
+ : undefined;
67
+ return {
68
+ ok: false,
69
+ retryable: false,
70
+ code: "AUTH_DENIED",
71
+ httpStatus: status,
72
+ ...(serverCode ? { serverCode } : {}),
73
+ };
74
+ }
75
+ if (status !== undefined) {
58
76
  if (status === 400 || status === 422) {
59
77
  return { ok: false, retryable: false, code: "VALIDATION_FAILED" };
60
78
  }
@@ -0,0 +1,5 @@
1
+ /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
2
+ export declare const WORK_SESSION_DENIAL_CODES: readonly ["AUTH_REQUIRED", "AGENT_IDENTITY_UNVERIFIED", "IMPERSONATION_FORBIDDEN", "NO_PERSON_ENTITY", "INVALID_COMPANY", "WORK_REGISTRY_DISABLED"];
3
+ export type WorkSessionDenialCode = (typeof WORK_SESSION_DENIAL_CODES)[number];
4
+ export declare function isWorkSessionDenialCode(value: unknown): value is WorkSessionDenialCode;
5
+ //# sourceMappingURL=denial-codes.d.ts.map
@@ -0,0 +1,14 @@
1
+ /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
2
+ export const WORK_SESSION_DENIAL_CODES = [
3
+ "AUTH_REQUIRED",
4
+ "AGENT_IDENTITY_UNVERIFIED",
5
+ "IMPERSONATION_FORBIDDEN",
6
+ "NO_PERSON_ENTITY",
7
+ "INVALID_COMPANY",
8
+ "WORK_REGISTRY_DISABLED",
9
+ ];
10
+ export function isWorkSessionDenialCode(value) {
11
+ return typeof value === "string" &&
12
+ WORK_SESSION_DENIAL_CODES.includes(value);
13
+ }
14
+ //# sourceMappingURL=denial-codes.js.map
@@ -27,6 +27,8 @@ export interface DaemonDoctorReport {
27
27
  outboxDepth: number;
28
28
  outboxQuarantined?: number;
29
29
  outboxLastQuarantineReason?: string;
30
+ outboxLastQuarantineHttpStatus?: number;
31
+ outboxLastQuarantineServerCode?: string;
30
32
  lastFlushAt?: string;
31
33
  lastFlushResult?: DaemonStateFile["lastFlushResult"];
32
34
  unhealthy: boolean;
@@ -263,6 +263,8 @@ export function collectDaemonDoctor(deps = {}) {
263
263
  ? {
264
264
  outboxQuarantined: outbox.quarantined,
265
265
  outboxLastQuarantineReason: outbox.lastQuarantineReason ?? undefined,
266
+ outboxLastQuarantineHttpStatus: outbox.lastQuarantineHttpStatus ?? undefined,
267
+ outboxLastQuarantineServerCode: outbox.lastQuarantineServerCode ?? undefined,
266
268
  }
267
269
  : {}),
268
270
  lastFlushAt: state?.lastFlushAt,
@@ -310,7 +312,15 @@ export function formatDaemonDoctor(report) {
310
312
  lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}${report.heldCount > 0 ? " (needs_company; not sent as NONE)" : ""}`, `held with no local evidence: ${report.heldWithNoLocalEvidence}`, `outbox depth: ${report.outboxDepth}`);
311
313
  }
312
314
  if (report.outboxQuarantined !== undefined) {
313
- lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"})`);
315
+ const quarantineDetails = [
316
+ report.outboxLastQuarantineHttpStatus !== undefined
317
+ ? `HTTP ${report.outboxLastQuarantineHttpStatus}`
318
+ : "",
319
+ report.outboxLastQuarantineServerCode ?? "",
320
+ ]
321
+ .filter(Boolean)
322
+ .join(" ");
323
+ lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`);
314
324
  }
315
325
  if (report.emitMode !== "legacy" && report.heldCount > 0) {
316
326
  lines.push(`held: ${report.heldCount} (needs_company; not sent as NONE)`);
@@ -8,12 +8,12 @@ export type DigestSignal = {
8
8
  kind: string;
9
9
  text: string;
10
10
  };
11
- export type FocusItem = {
11
+ type FocusItem = {
12
12
  title: string;
13
13
  project: string;
14
14
  reason: string;
15
15
  };
16
- export type ReportRunRecord = {
16
+ type ReportRunRecord = {
17
17
  generatedAt: string;
18
18
  inputs: {
19
19
  template: string;
@@ -58,4 +58,5 @@ export declare function renderDailyDigest(input: {
58
58
  generatedAt?: string;
59
59
  }): DigestPage;
60
60
  export declare function writeDigestPage(directory: string, page: DigestPage): void;
61
+ export {};
61
62
  //# sourceMappingURL=report-run.d.ts.map
@@ -3,7 +3,6 @@ export type ReportTemplate = {
3
3
  path: string;
4
4
  body: string;
5
5
  };
6
- export declare function reportPackageRoot(): string;
7
6
  export declare function listReportTemplates(root?: string): ReportTemplate[];
8
7
  /** Verbatim runbook. Callers substitute `<reportId>` when the id is known. */
9
8
  export declare function loadUpdateReportRunbook(root?: string): string;
@@ -3,9 +3,6 @@ import { readdirSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
6
- export function reportPackageRoot() {
7
- return PACKAGE_ROOT;
8
- }
9
6
  export function listReportTemplates(root = PACKAGE_ROOT) {
10
7
  const dir = path.join(root, "templates", "reports");
11
8
  return readdirSync(dir)
@@ -1,4 +1,3 @@
1
- export declare const SCAFFOLD_CLI_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
2
1
  export type ScaffoldCliFloorComparison = {
3
2
  status: "pass" | "fail" | "warn" | "skip";
4
3
  message: string;
@@ -1,5 +1,5 @@
1
1
  import semver from "semver";
2
- export const SCAFFOLD_CLI_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
2
+ const SCAFFOLD_CLI_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
3
3
  const MINIMUM_VERSION_RANGE = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
4
  /** Compare an installed CLI version with the scaffold's supported floor. */
5
5
  export function compareScaffoldCliFloor(required, currentVersion) {
@@ -4,6 +4,8 @@ export type EmbedLockDependencies = {
4
4
  pid: number;
5
5
  random: () => string;
6
6
  isProcessAlive: (pid: number) => boolean;
7
+ /** Test seam for a contender arriving after mkdir and before owner publication. */
8
+ beforeOwnerPublish?: (ownerFile: string) => void;
7
9
  /** Test seam for simulating a competing owner replacing the atomic record. */
8
10
  afterOwnerPublish?: (ownerFile: string) => void;
9
11
  };
@@ -184,8 +184,13 @@ function createAndPublishLock(home, dependencies) {
184
184
  // parity: the script's write_owner_record honored the same variable.
185
185
  if (dependencies.env.QMD_FORCE_OWNER_WRITE_FAIL)
186
186
  throw new Error('forced owner write failure');
187
+ dependencies.beforeOwnerPublish?.(ownerFile);
187
188
  fs.writeFileSync(temporary, `pid=${dependencies.pid}\nts=${dependencies.now()}\nnonce=${nonce}\n`);
188
- fs.renameSync(temporary, ownerFile);
189
+ // renameSync replaces an existing owner on POSIX. A stale reclaim can
190
+ // replace this ownerless directory while this publisher is paused, so
191
+ // publish only if this exact generation still has no owner.
192
+ fs.linkSync(temporary, ownerFile);
193
+ fs.unlinkSync(temporary);
189
194
  dependencies.afterOwnerPublish?.(ownerFile);
190
195
  const verified = readOwner(directory);
191
196
  if (!verified || verified.pid !== dependencies.pid || verified.nonce !== nonce)
@@ -196,6 +201,10 @@ function createAndPublishLock(home, dependencies) {
196
201
  catch {
197
202
  // The marker identifies only the directory this process created. Do not
198
203
  // recursively remove the fixed lock path after a failed publication.
204
+ try {
205
+ fs.unlinkSync(temporary);
206
+ }
207
+ catch { /* absent after link or moved with a stale generation */ }
199
208
  fs.rmSync(marker, { recursive: true, force: true });
200
209
  removeDirectoryIfEmpty(directory);
201
210
  return false;
@@ -61,6 +61,9 @@ export interface ContextResult {
61
61
  bindingEpisodeId?: string;
62
62
  clientOperationId: string;
63
63
  errorCode?: string;
64
+ /** Sanitized HTTP denial details; raw response bodies are never included. */
65
+ httpStatus?: number;
66
+ serverCode?: string;
64
67
  /** Why a project was chosen, or why the session is asking. Local consumers only. */
65
68
  projectBindReason?: string;
66
69
  /** Ranked alternatives when classification is needs_project. */
@@ -100,7 +100,9 @@
100
100
  "taskId": { "type": "string", "maxLength": 128 },
101
101
  "bindingEpisodeId": { "type": "string", "maxLength": 128 },
102
102
  "clientOperationId": { "type": "string", "minLength": 1, "maxLength": 128 },
103
- "errorCode": { "type": "string", "maxLength": 64 }
103
+ "errorCode": { "type": "string", "maxLength": 64 },
104
+ "httpStatus": { "type": "number" },
105
+ "serverCode": { "type": "string", "maxLength": 64 }
104
106
  }
105
107
  }
106
108
  }
@@ -5,7 +5,7 @@
5
5
  import type { DeliveryState } from "./contract.js";
6
6
  import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
7
  /** Fields allowed on a durable outbox operation (privacy allowlist). */
8
- export declare const OUTBOX_ALLOWLIST: readonly ["contractVersion", "operationId", "clientOperationId", "sessionId", "harness", "companyUid", "companySlug", "projectId", "taskId", "companyConfidence", "kind", "digest", "delivery", "createdAt", "updatedAt", "attemptCount", "lastErrorCode", "receiptId", "destinationCompanyUid", "destinationCompanySlug", "nextAttemptAt"];
8
+ export declare const OUTBOX_ALLOWLIST: readonly ["contractVersion", "operationId", "clientOperationId", "sessionId", "harness", "companyUid", "companySlug", "projectId", "taskId", "companyConfidence", "kind", "digest", "delivery", "createdAt", "updatedAt", "attemptCount", "lastErrorCode", "lastErrorHttpStatus", "lastServerErrorCode", "receiptId", "destinationCompanyUid", "destinationCompanySlug", "nextAttemptAt"];
9
9
  /** Base delay for outbox retry backoff (attempt 1 → 30s). */
10
10
  export declare const OUTBOX_RETRY_BASE_MS = 30000;
11
11
  /** Cap for outbox retry backoff (6 hours). */
@@ -34,6 +34,8 @@ export interface OutboxOperation {
34
34
  updatedAt: string;
35
35
  attemptCount: number;
36
36
  lastErrorCode?: string;
37
+ lastErrorHttpStatus?: number;
38
+ lastServerErrorCode?: string;
37
39
  receiptId?: string;
38
40
  /** Destination company for kind=migrate (source is companyUid). */
39
41
  destinationCompanyUid?: string;
@@ -88,8 +90,13 @@ export declare function markOutboxAcked(operationId: string, root: string, recei
88
90
  export declare function writeAckedRegisterState(op: OutboxOperation, root: string, receiptId: string, now?: () => Date): void;
89
91
  /** Repair older unresolved session projections from acknowledged registrations. */
90
92
  export declare function repairAckedRegisterStates(root: string, sessionIds?: ReadonlySet<string>, now?: () => Date): number;
91
- export declare function markOutboxQueued(operationId: string, root: string, errorCode: string, now?: () => Date, random?: () => number): OutboxOperation | null;
92
- export declare function markOutboxQuarantined(operationId: string, root: string, errorCode: string, now?: () => Date): OutboxOperation | null;
93
+ export declare function markOutboxQueued(operationId: string, root: string, errorCode: string, now?: () => Date, random?: () => number, details?: OutboxErrorDetails): OutboxOperation | null;
94
+ export declare function markOutboxQuarantined(operationId: string, root: string, errorCode: string, now?: () => Date, details?: OutboxErrorDetails): OutboxOperation | null;
95
+ export interface OutboxErrorDetails {
96
+ httpStatus?: number;
97
+ serverCode?: string;
98
+ }
99
+ export declare function sanitizeOutboxErrorDetails(details?: OutboxErrorDetails): OutboxErrorDetails;
93
100
  /**
94
101
  * Requeue quarantined outbox operations so the daemon's replay picks them up
95
102
  * again. Quarantine is otherwise terminal: after the work-mesh-live gap 10 fix
@@ -119,16 +126,18 @@ export interface OutboxStats {
119
126
  queued: number;
120
127
  acked: number;
121
128
  lastQuarantineReason: string | null;
129
+ lastQuarantineHttpStatus: number | null;
130
+ lastQuarantineServerCode: string | null;
122
131
  }
123
132
  export declare function outboxStats(root: string): OutboxStats;
124
133
  export type DeliverFn = (op: OutboxOperation) => Promise<{
125
134
  ok: true;
126
135
  receiptId: string;
127
- } | {
136
+ } | ({
128
137
  ok: false;
129
138
  retryable: boolean;
130
139
  code: string;
131
- }>;
140
+ } & OutboxErrorDetails)>;
132
141
  /**
133
142
  * Replay queued (and recover lost-receipt) operations.
134
143
  * FIFO per sessionId; bounded parallelism across sessions.
@@ -8,6 +8,7 @@ import * as path from "node:path";
8
8
  import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
9
9
  import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
10
10
  import { NotTrackingError } from "./errors.js";
11
+ import { isWorkSessionDenialCode } from "../mesh/denial-codes.js";
11
12
  import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
12
13
  import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
13
14
  /** Fields allowed on a durable outbox operation (privacy allowlist). */
@@ -29,6 +30,8 @@ export const OUTBOX_ALLOWLIST = [
29
30
  "updatedAt",
30
31
  "attemptCount",
31
32
  "lastErrorCode",
33
+ "lastErrorHttpStatus",
34
+ "lastServerErrorCode",
32
35
  "receiptId",
33
36
  "destinationCompanyUid",
34
37
  "destinationCompanySlug",
@@ -101,6 +104,11 @@ function projectOutbox(op) {
101
104
  out.companyConfidence = op.companyConfidence;
102
105
  if (op.lastErrorCode)
103
106
  out.lastErrorCode = op.lastErrorCode;
107
+ if (op.lastErrorHttpStatus !== undefined) {
108
+ out.lastErrorHttpStatus = op.lastErrorHttpStatus;
109
+ }
110
+ if (op.lastServerErrorCode)
111
+ out.lastServerErrorCode = op.lastServerErrorCode;
104
112
  if (op.receiptId)
105
113
  out.receiptId = op.receiptId;
106
114
  if (op.destinationCompanyUid) {
@@ -337,12 +345,13 @@ export function repairAckedRegisterStates(root, sessionIds, now = () => new Date
337
345
  }
338
346
  return repaired;
339
347
  }
340
- export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random) {
348
+ export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random, details) {
341
349
  const op = readOutboxOperation(operationId, root);
342
350
  if (!op)
343
351
  return null;
344
352
  op.delivery = "queued";
345
353
  op.lastErrorCode = errorCode;
354
+ applyOutboxErrorDetails(op, details);
346
355
  op.attemptCount += 1;
347
356
  const at = now();
348
357
  op.updatedAt = at.toISOString();
@@ -351,17 +360,40 @@ export function markOutboxQueued(operationId, root, errorCode, now = () => new D
351
360
  updateOutboxOperation(op, root);
352
361
  return op;
353
362
  }
354
- export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date()) {
363
+ export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date(), details) {
355
364
  const op = readOutboxOperation(operationId, root);
356
365
  if (!op)
357
366
  return null;
358
367
  op.delivery = "quarantined";
359
368
  op.lastErrorCode = errorCode;
369
+ applyOutboxErrorDetails(op, details);
360
370
  op.attemptCount += 1;
361
371
  op.updatedAt = now().toISOString();
362
372
  updateOutboxOperation(op, root);
363
373
  return op;
364
374
  }
375
+ function applyOutboxErrorDetails(op, details) {
376
+ delete op.lastErrorHttpStatus;
377
+ delete op.lastServerErrorCode;
378
+ const safe = sanitizeOutboxErrorDetails(details);
379
+ if (safe.httpStatus !== undefined)
380
+ op.lastErrorHttpStatus = safe.httpStatus;
381
+ if (safe.serverCode !== undefined)
382
+ op.lastServerErrorCode = safe.serverCode;
383
+ }
384
+ export function sanitizeOutboxErrorDetails(details) {
385
+ const safe = {};
386
+ if (details?.httpStatus !== undefined &&
387
+ Number.isInteger(details.httpStatus) &&
388
+ details.httpStatus >= 100 &&
389
+ details.httpStatus <= 599) {
390
+ safe.httpStatus = details.httpStatus;
391
+ }
392
+ if (isWorkSessionDenialCode(details?.serverCode)) {
393
+ safe.serverCode = details.serverCode;
394
+ }
395
+ return safe;
396
+ }
365
397
  /**
366
398
  * Requeue quarantined outbox operations so the daemon's replay picks them up
367
399
  * again. Quarantine is otherwise terminal: after the work-mesh-live gap 10 fix
@@ -389,6 +421,8 @@ export function requeueQuarantinedOutbox(root, opts = {}) {
389
421
  op.delivery = "queued";
390
422
  op.attemptCount = 0;
391
423
  op.lastErrorCode = undefined;
424
+ op.lastErrorHttpStatus = undefined;
425
+ op.lastServerErrorCode = undefined;
392
426
  op.nextAttemptAt = undefined;
393
427
  op.updatedAt = now().toISOString();
394
428
  updateOutboxOperation(op, root);
@@ -469,6 +503,15 @@ export function outboxStats(root) {
469
503
  queued: ops.filter((o) => o.delivery === "queued").length,
470
504
  acked: ops.filter((o) => o.delivery === "acked").length,
471
505
  lastQuarantineReason: latestQuarantine?.lastErrorCode ?? null,
506
+ lastQuarantineHttpStatus: latestQuarantine &&
507
+ Number.isInteger(latestQuarantine.lastErrorHttpStatus) &&
508
+ latestQuarantine.lastErrorHttpStatus >= 100 &&
509
+ latestQuarantine.lastErrorHttpStatus <= 599
510
+ ? latestQuarantine.lastErrorHttpStatus
511
+ : null,
512
+ lastQuarantineServerCode: isWorkSessionDenialCode(latestQuarantine?.lastServerErrorCode)
513
+ ? latestQuarantine.lastServerErrorCode
514
+ : null,
472
515
  };
473
516
  }
474
517
  function isOutboxDue(op, nowIso) {
@@ -551,18 +594,18 @@ export async function replayOutbox(root, deliver, opts = {}) {
551
594
  }
552
595
  else if (result.retryable) {
553
596
  if (op.attemptCount + 1 >= OUTBOX_MAX_ATTEMPTS) {
554
- markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now);
597
+ markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now, result);
555
598
  quarantined += 1;
556
599
  }
557
600
  else {
558
- markOutboxQueued(op.operationId, root, result.code, now, random);
601
+ markOutboxQueued(op.operationId, root, result.code, now, random, result);
559
602
  queued += 1;
560
603
  }
561
604
  // Stop this session's FIFO on transient failure.
562
605
  break;
563
606
  }
564
607
  else {
565
- markOutboxQuarantined(op.operationId, root, result.code, now);
608
+ markOutboxQuarantined(op.operationId, root, result.code, now, result);
566
609
  quarantined += 1;
567
610
  break;
568
611
  }
@@ -14,7 +14,7 @@ import { refineProjectBinding } from "./bind-best.js";
14
14
  import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
15
15
  import { resolveProjectTask, shouldAskAfter } from "./project.js";
16
16
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
17
- import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
17
+ import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, sanitizeOutboxErrorDetails, writeAckedRegisterState, } from "./outbox.js";
18
18
  import { mergeLocalSessionFields, isRegisterReceiptEpisodeId, readSessionState, stripRegisterReceiptEpisodes, writeSessionState, } from "./state.js";
19
19
  function resultOf(parts) {
20
20
  const out = {
@@ -38,6 +38,10 @@ function resultOf(parts) {
38
38
  out.bindingEpisodeId = parts.bindingEpisodeId;
39
39
  if (parts.errorCode)
40
40
  out.errorCode = parts.errorCode;
41
+ if (parts.httpStatus !== undefined)
42
+ out.httpStatus = parts.httpStatus;
43
+ if (parts.serverCode)
44
+ out.serverCode = parts.serverCode;
41
45
  if (parts.projectBindReason)
42
46
  out.projectBindReason = parts.projectBindReason;
43
47
  if (parts.projectCandidates)
@@ -974,7 +978,8 @@ export async function reconcileObservation(obs, deps) {
974
978
  };
975
979
  }
976
980
  if (delivered.retryable) {
977
- markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn);
981
+ const details = sanitizeOutboxErrorDetails(delivered);
982
+ markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn, undefined, details);
978
983
  return {
979
984
  result: resultOf({
980
985
  kind: "queued",
@@ -986,15 +991,20 @@ export async function reconcileObservation(obs, deps) {
986
991
  projectId,
987
992
  taskId,
988
993
  errorCode: delivered.code,
994
+ ...details,
989
995
  ...bindResultFields,
990
996
  }),
991
997
  exitCode: EXIT_OK,
992
- diagnostics: [...diagnostics, `transient ${delivered.code}; remains queued`],
998
+ diagnostics: [
999
+ ...diagnostics,
1000
+ `transient ${delivered.code}${formatDeliveryDetails(details)}; remains queued`,
1001
+ ],
993
1002
  statePath,
994
1003
  outboxOperationId: outboxOp.operationId,
995
1004
  };
996
1005
  }
997
- markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn);
1006
+ const details = sanitizeOutboxErrorDetails(delivered);
1007
+ markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn, details);
998
1008
  return {
999
1009
  result: resultOf({
1000
1010
  kind: "error",
@@ -1007,13 +1017,24 @@ export async function reconcileObservation(obs, deps) {
1007
1017
  projectId,
1008
1018
  taskId,
1009
1019
  errorCode: delivered.code,
1020
+ ...details,
1010
1021
  ...bindResultFields,
1011
1022
  }),
1012
1023
  exitCode: EXIT_OK,
1013
- diagnostics: [...diagnostics, `quarantined: ${delivered.code}`],
1024
+ diagnostics: [
1025
+ ...diagnostics,
1026
+ `quarantined: ${delivered.code}${formatDeliveryDetails(details)}`,
1027
+ ],
1014
1028
  statePath,
1015
1029
  outboxOperationId: outboxOp.operationId,
1016
1030
  };
1017
1031
  }
1032
+ function formatDeliveryDetails(details) {
1033
+ const parts = [
1034
+ details.httpStatus !== undefined ? `HTTP ${details.httpStatus}` : "",
1035
+ details.serverCode ?? "",
1036
+ ].filter(Boolean);
1037
+ return parts.length ? ` (${parts.join(" ")})` : "";
1038
+ }
1018
1039
  export { replayOutbox };
1019
1040
  //# sourceMappingURL=reconcile.js.map