@indigoai-us/hq-cli 5.345.109 → 5.345.111

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 (35) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/command-catalog.generated.d.ts +17 -5
  3. package/dist/command-catalog.generated.js +20 -5
  4. package/dist/commands/capacity.d.ts +26 -0
  5. package/dist/commands/capacity.js +64 -2
  6. package/dist/commands/core-policy.d.ts +4 -1
  7. package/dist/commands/core-policy.js +21 -43
  8. package/dist/commands/core.js +2 -7
  9. package/dist/commands/db-provision.js +31 -10
  10. package/dist/commands/db-remote-deps.js +12 -0
  11. package/dist/commands/db-sql.js +42 -9
  12. package/dist/commands/db-status.d.ts +2 -1
  13. package/dist/commands/db-status.js +33 -7
  14. package/dist/fast-scaffold.js +0 -12
  15. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +1414 -923
  16. package/dist/hq-cloud-sync-runner/package.json +1 -1
  17. package/dist/lib/bot/brief-job.d.ts +22 -0
  18. package/dist/lib/bot/brief-job.js +38 -0
  19. package/dist/lib/bot/run.d.ts +10 -0
  20. package/dist/lib/bot/run.js +54 -9
  21. package/dist/lib/capacity/collectors/claude-oauth.d.ts +225 -2
  22. package/dist/lib/capacity/collectors/claude-oauth.js +1053 -43
  23. package/dist/lib/capacity/ledger.d.ts +10 -1
  24. package/dist/lib/capacity/ledger.js +8 -1
  25. package/dist/lib/capacity/project.js +3 -0
  26. package/dist/lib/db/control-plane.d.ts +22 -0
  27. package/dist/lib/db/control-plane.js +50 -0
  28. package/dist/lib/db/remote-engine.d.ts +9 -1
  29. package/dist/lib/db/remote-engine.js +32 -4
  30. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  31. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  32. package/package.json +2 -2
  33. package/assets/scaffold/core/scripts/policy-age-report.sh +0 -199
  34. package/assets/scaffold/core/scripts/policy-retire.sh +0 -199
  35. package/assets/scaffold/core/scripts/read-policy-frontmatter.sh +0 -39
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.111] — 2026-10-13
6
+ - The local bot runs a Create brief job from the desktop app in a fresh model session of its own instead of resuming the owner's whole DM conversation, and schedules it like a DM rather than behind the job slot. A resumed session that fails twice in a row with the model CLI's "An unknown error occurred (Unexpected)" is cleared so the next turn starts fresh; a single failure never clears it.
7
+
8
+ - Add `--personal` support to `hq db provision`, `status`, and `sql` for a person's remote database.
9
+ - `hq capacity` now keeps its Claude OAuth sessions awake on its own: when a
10
+ token is about to expire it refreshes in the background, with per-home
11
+ backoff so busy lanes do not thrash the auth endpoint. If a refresh is
12
+ rejected (revoked or invalid grant), the account is reported as "signed
13
+ out: sign-in revoked" and the printed fix is `hq capacity login claude
14
+ --account <id> --repair` for a managed non-default home, or `claude auth
15
+ login` for the default home.
16
+ - Refreshes fail closed when the live-lane or running-process gates cannot
17
+ be verified (unexpected `hq lanes list --json` shape, `ps` failure, or
18
+ darwin credential pre-read failure with a prior refresh token), so a
19
+ rotation never races a running Claude Code.
20
+ - Doctor's refresh error record stores only the HTTP status and a fixed
21
+ classification (`invalid_grant`, `revoked`, `server_error`, `timeout`),
22
+ never a response body.
23
+ - An expired Claude account whose newest usage reading predates the token
24
+ expiry is now flagged as stale, so the router does not trust an old
25
+ reading as live headroom.
26
+ - `hq capacity doctor` adds a per-account Claude table showing token age,
27
+ time to expiry, last refresh attempt and outcome, and any backoff
28
+ window. All strings in the report are passed through a token-redaction
29
+ scrubber before display.
30
+
31
+ ## [5.345.110] — 2026-10-13
32
+ - The bundled sync runner now uses hq-cloud 6.18.69, which compacts the sync-state write-ahead log into a snapshot once the log grows past a size-scaled ratio of the snapshot, so large companies stop accumulating oversized sync-state files.
33
+ - Remove the three policy-family shell rollback copies after the native route's clean release soak. `hq core policy age-report`, `policy retire`, and `policy frontmatter` now use the native route regardless of the retained `core-native-policy` flag, which still gates `knowledge-prefs`.
34
+
5
35
  ## [5.345.109] — 2026-10-13
6
36
  - `hq` commands no longer fail with "The HQ CLI package is being updated" while
7
37
  the desktop app updates the CLI. They wait for the update (up to 4 minutes,
@@ -1916,7 +1916,7 @@ export declare const COMMAND_CATALOG: readonly [{
1916
1916
  readonly options: readonly [];
1917
1917
  readonly subcommands: readonly [{
1918
1918
  readonly name: "status";
1919
- readonly description: "Show local (and remote when bound) vault DB status; auto-creates local SQLite if missing";
1919
+ readonly description: "Show company local/remote status or your personal database status";
1920
1920
  readonly aliases: readonly [];
1921
1921
  readonly hidden: false;
1922
1922
  readonly helpOption: true;
@@ -1926,7 +1926,11 @@ export declare const COMMAND_CATALOG: readonly [{
1926
1926
  readonly arguments: readonly [];
1927
1927
  readonly options: readonly [{
1928
1928
  readonly flags: "--company <slug>";
1929
- readonly description: "Company slug (tenant scope; required)";
1929
+ readonly description: "Company slug (tenant scope)";
1930
+ readonly hidden: false;
1931
+ }, {
1932
+ readonly flags: "--personal";
1933
+ readonly description: "Show your personal database status";
1930
1934
  readonly hidden: false;
1931
1935
  }, {
1932
1936
  readonly flags: "--home <path>";
@@ -1945,7 +1949,7 @@ export declare const COMMAND_CATALOG: readonly [{
1945
1949
  readonly subcommands: readonly [];
1946
1950
  }, {
1947
1951
  readonly name: "sql";
1948
- readonly description: "Run SQL against the company vault DB (local) or an app's remote database (--app); read-only by default";
1952
+ readonly description: "Run read-only SQL against a company or personal database; use --write for changes";
1949
1953
  readonly aliases: readonly [];
1950
1954
  readonly hidden: false;
1951
1955
  readonly helpOption: true;
@@ -1960,7 +1964,11 @@ export declare const COMMAND_CATALOG: readonly [{
1960
1964
  }];
1961
1965
  readonly options: readonly [{
1962
1966
  readonly flags: "--company <slug>";
1963
- readonly description: "Company slug (tenant scope; required)";
1967
+ readonly description: "Company slug (tenant scope)";
1968
+ readonly hidden: false;
1969
+ }, {
1970
+ readonly flags: "--personal";
1971
+ readonly description: "Run against your personal remote database";
1964
1972
  readonly hidden: false;
1965
1973
  }, {
1966
1974
  readonly flags: "--app <app>";
@@ -2027,7 +2035,7 @@ export declare const COMMAND_CATALOG: readonly [{
2027
2035
  readonly subcommands: readonly [];
2028
2036
  }, {
2029
2037
  readonly name: "provision";
2030
- readonly description: "Create the company remote database (Team plan)";
2038
+ readonly description: "Create a company remote database or your personal database";
2031
2039
  readonly aliases: readonly [];
2032
2040
  readonly hidden: false;
2033
2041
  readonly helpOption: true;
@@ -2039,6 +2047,10 @@ export declare const COMMAND_CATALOG: readonly [{
2039
2047
  readonly flags: "--company <slug>";
2040
2048
  readonly description: "Company slug";
2041
2049
  readonly hidden: false;
2050
+ }, {
2051
+ readonly flags: "--personal";
2052
+ readonly description: "Provision your personal database";
2053
+ readonly hidden: false;
2042
2054
  }, {
2043
2055
  readonly flags: "--region <region>";
2044
2056
  readonly description: "AWS region";
@@ -2298,7 +2298,7 @@ export const COMMAND_CATALOG = [
2298
2298
  "subcommands": [
2299
2299
  {
2300
2300
  "name": "status",
2301
- "description": "Show local (and remote when bound) vault DB status; auto-creates local SQLite if missing",
2301
+ "description": "Show company local/remote status or your personal database status",
2302
2302
  "aliases": [],
2303
2303
  "hidden": false,
2304
2304
  "helpOption": true,
@@ -2309,7 +2309,12 @@ export const COMMAND_CATALOG = [
2309
2309
  "options": [
2310
2310
  {
2311
2311
  "flags": "--company <slug>",
2312
- "description": "Company slug (tenant scope; required)",
2312
+ "description": "Company slug (tenant scope)",
2313
+ "hidden": false
2314
+ },
2315
+ {
2316
+ "flags": "--personal",
2317
+ "description": "Show your personal database status",
2313
2318
  "hidden": false
2314
2319
  },
2315
2320
  {
@@ -2333,7 +2338,7 @@ export const COMMAND_CATALOG = [
2333
2338
  },
2334
2339
  {
2335
2340
  "name": "sql",
2336
- "description": "Run SQL against the company vault DB (local) or an app's remote database (--app); read-only by default",
2341
+ "description": "Run read-only SQL against a company or personal database; use --write for changes",
2337
2342
  "aliases": [],
2338
2343
  "hidden": false,
2339
2344
  "helpOption": true,
@@ -2351,7 +2356,12 @@ export const COMMAND_CATALOG = [
2351
2356
  "options": [
2352
2357
  {
2353
2358
  "flags": "--company <slug>",
2354
- "description": "Company slug (tenant scope; required)",
2359
+ "description": "Company slug (tenant scope)",
2360
+ "hidden": false
2361
+ },
2362
+ {
2363
+ "flags": "--personal",
2364
+ "description": "Run against your personal remote database",
2355
2365
  "hidden": false
2356
2366
  },
2357
2367
  {
@@ -2433,7 +2443,7 @@ export const COMMAND_CATALOG = [
2433
2443
  },
2434
2444
  {
2435
2445
  "name": "provision",
2436
- "description": "Create the company remote database (Team plan)",
2446
+ "description": "Create a company remote database or your personal database",
2437
2447
  "aliases": [],
2438
2448
  "hidden": false,
2439
2449
  "helpOption": true,
@@ -2447,6 +2457,11 @@ export const COMMAND_CATALOG = [
2447
2457
  "description": "Company slug",
2448
2458
  "hidden": false
2449
2459
  },
2460
+ {
2461
+ "flags": "--personal",
2462
+ "description": "Provision your personal database",
2463
+ "hidden": false
2464
+ },
2450
2465
  {
2451
2466
  "flags": "--region <region>",
2452
2467
  "description": "AWS region",
@@ -11,6 +11,7 @@
11
11
  import { Command } from "commander";
12
12
  import { type CapacityState, type LedgerEvent, type LedgerOptions, type NowView, type ParserFailureEvent, type Reading, type ReadingSource } from "../lib/capacity/ledger.js";
13
13
  import { type CapacityAccount } from "../lib/capacity/accounts.js";
14
+ import { type ClaudeRefreshLastEntry } from "../lib/capacity/collectors/claude-oauth.js";
14
15
  export declare const CAPACITY_SCHEMA_VERSION: "hq-capacity-now-view.v1";
15
16
  export declare const DOCTOR_SCHEMA_VERSION = "hq-capacity-doctor.v1";
16
17
  export declare const PARSER_FAILURE_WINDOW_MS: number;
@@ -40,6 +41,30 @@ export interface DoctorVersion {
40
41
  installed: string | null;
41
42
  match: boolean;
42
43
  }
44
+ export interface DoctorClaudeAccount {
45
+ /** Account label from the capacity registry (eg "default"). */
46
+ label: string;
47
+ /** Resolved credential home directory. */
48
+ home: string;
49
+ /** ISO timestamp of the last refresh attempt, redacted. Null when nothing was tried for this home. */
50
+ lastRefreshAttempt: string | null;
51
+ /** Outcome of the last refresh attempt. Null when nothing was tried. */
52
+ lastRefreshOutcome: ClaudeRefreshLastEntry["outcome"] | null;
53
+ /** ms since the last refresh attempt. Null when nothing was tried. */
54
+ tokenAgeMs: number | null;
55
+ /** ms until the token's recorded expiry. Negative when past expiry. Null when the collector has not recorded one. */
56
+ timeToExpiryMs: number | null;
57
+ /** ISO expiry of the token the entry describes. Null when unknown. */
58
+ tokenExpiresAt: string | null;
59
+ /** ISO backoff window end, when the last outcome set one. Null otherwise. */
60
+ backoffUntil: string | null;
61
+ /** Short, token-free diagnostic from the last attempt. Null otherwise. Never contains a response body. */
62
+ errorMessage: string | null;
63
+ /** Fixed classification string for the last error outcome. Null otherwise. */
64
+ errorClass: ClaudeRefreshLastEntry["errorClass"] | null;
65
+ /** HTTP status from the last attempt that reached the token endpoint. Null otherwise. */
66
+ httpStatus: number | null;
67
+ }
43
68
  export interface DoctorReport {
44
69
  schemaVersion: typeof DOCTOR_SCHEMA_VERSION;
45
70
  collector: "on" | "off";
@@ -50,6 +75,7 @@ export interface DoctorReport {
50
75
  provider: string;
51
76
  state: CapacityState;
52
77
  }[];
78
+ claudeAccounts: DoctorClaudeAccount[];
53
79
  ok: boolean;
54
80
  stale: boolean;
55
81
  ageMs: number;
@@ -3,9 +3,9 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import * as readline from "node:readline";
6
- import { LedgerCorrupt, LedgerMissing, READING_SOURCES, capacityOptionsFromEnv, capacityRoot, nowViewJsonSchema, readCollectorMark, readLedgerEvents, readNow, rebuildNow, setCollectorEnabled, } from "../lib/capacity/ledger.js";
6
+ import { LedgerCorrupt, LedgerMissing, READING_SOURCES, capacityOptionsFromEnv, capacityRoot, nowViewJsonSchema, readCollectorMark, readLedgerEvents, readNow, rebuildNow, redactProviderTokens, setCollectorEnabled, } from "../lib/capacity/ledger.js";
7
7
  import { listCapacityAccounts, registryAccountIds } from "../lib/capacity/accounts.js";
8
- import { claudeIdentityKeys } from "../lib/capacity/collectors/claude-oauth.js";
8
+ import { claudeIdentityKeys, readClaudeRefreshLast } from "../lib/capacity/collectors/claude-oauth.js";
9
9
  import { projectCapacityView } from "../lib/capacity/project.js";
10
10
  import { capacityWidgetTimeZone, renderCapacityWidget } from "../lib/capacity/widget.js";
11
11
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
@@ -185,6 +185,7 @@ export function buildDoctorReport(options) {
185
185
  provider: provider.provider,
186
186
  state: provider.state,
187
187
  }));
188
+ const claudeAccounts = buildClaudeAccountDoctor(resolved.options, resolved.nowMs);
188
189
  const freshness = capacityFreshness(view.generatedAt, resolved.nowMs);
189
190
  return {
190
191
  schemaVersion: DOCTOR_SCHEMA_VERSION,
@@ -193,11 +194,56 @@ export function buildDoctorReport(options) {
193
194
  versions,
194
195
  parserFailures,
195
196
  providers,
197
+ claudeAccounts,
196
198
  ok: providers.every((provider) => provider.state !== "unknown"),
197
199
  stale: freshness.stale,
198
200
  ageMs: freshness.ageMs,
199
201
  };
200
202
  }
203
+ function buildClaudeAccountDoctor(options, nowMs) {
204
+ const env = options.env ?? process.env;
205
+ const home = options.home ?? env.HOME;
206
+ const root = options.root ?? capacityRoot(env, home);
207
+ const accounts = listCapacityAccounts({ env, hqRoot: env.HQ_ROOT, home })
208
+ .filter((account) => account.provider === "claude");
209
+ return accounts.map((account) => {
210
+ const entry = readClaudeRefreshLast(root, account.home);
211
+ if (!entry) {
212
+ return {
213
+ label: account.label,
214
+ home: account.home,
215
+ lastRefreshAttempt: null,
216
+ lastRefreshOutcome: null,
217
+ tokenAgeMs: null,
218
+ timeToExpiryMs: null,
219
+ tokenExpiresAt: null,
220
+ backoffUntil: null,
221
+ errorMessage: null,
222
+ errorClass: null,
223
+ httpStatus: null,
224
+ };
225
+ }
226
+ const attemptedAtMs = Date.parse(entry.attemptedAt);
227
+ const expiresAtMs = entry.tokenExpiresAt ? Date.parse(entry.tokenExpiresAt) : NaN;
228
+ const tokenAgeMs = entry.outcome === "refreshed" && Number.isFinite(attemptedAtMs)
229
+ ? nowMs - attemptedAtMs
230
+ : null;
231
+ const timeToExpiryMs = Number.isFinite(expiresAtMs) ? expiresAtMs - nowMs : null;
232
+ return {
233
+ label: account.label,
234
+ home: account.home,
235
+ lastRefreshAttempt: redactProviderTokens(entry.attemptedAt),
236
+ lastRefreshOutcome: entry.outcome,
237
+ tokenAgeMs,
238
+ timeToExpiryMs,
239
+ tokenExpiresAt: entry.tokenExpiresAt ?? null,
240
+ backoffUntil: entry.backoffUntil ?? null,
241
+ errorMessage: entry.errorMessage ? redactProviderTokens(entry.errorMessage) : null,
242
+ errorClass: entry.errorClass ?? null,
243
+ httpStatus: typeof entry.httpStatus === "number" ? entry.httpStatus : null,
244
+ };
245
+ });
246
+ }
201
247
  export function formatDoctor(report) {
202
248
  const sources = formatTable(["SOURCE", "LAST SUCCESS"], report.sources.map((source) => [source.source, source.lastSuccess ?? "—"]));
203
249
  const versions = formatTable(["PROVIDER", "PINNED", "INSTALLED", "MATCH"], report.versions.map((version) => [
@@ -212,6 +258,16 @@ export function formatDoctor(report) {
212
258
  const states = report.providers.length === 0
213
259
  ? "none"
214
260
  : report.providers.map((provider) => `${provider.provider} ${provider.state}`).join("\n");
261
+ const claudeAccounts = report.claudeAccounts.length === 0
262
+ ? "none"
263
+ : formatTable(["ACCOUNT", "LAST ATTEMPT", "OUTCOME", "TOKEN AGE", "EXPIRES IN", "BACKOFF UNTIL"], report.claudeAccounts.map((account) => [
264
+ account.label,
265
+ account.lastRefreshAttempt ?? "—",
266
+ account.lastRefreshOutcome ?? "—",
267
+ account.tokenAgeMs === null ? "—" : formatSignedSpan(account.tokenAgeMs),
268
+ account.timeToExpiryMs === null ? "—" : formatSignedSpan(account.timeToExpiryMs),
269
+ account.backoffUntil ?? "—",
270
+ ]));
215
271
  const warning = report.stale
216
272
  ? report.ageMs === 0
217
273
  ? "collector not running, numbers may be stale; last update unknown"
@@ -226,8 +282,14 @@ export function formatDoctor(report) {
226
282
  failures,
227
283
  "provider state",
228
284
  states,
285
+ "claude accounts",
286
+ claudeAccounts,
229
287
  ].join("\n");
230
288
  }
289
+ function formatSignedSpan(ms) {
290
+ const span = formatSpan(Math.floor(Math.abs(ms) / 1000));
291
+ return ms < 0 ? `-${span}` : span;
292
+ }
231
293
  export function registerCapacityCommand(program) {
232
294
  const capacity = program
233
295
  .command("capacity")
@@ -1,4 +1,5 @@
1
1
  import type { Command } from 'commander';
2
+ import { policyValidatorDiagnostic } from '../lib/policy-validator.js';
2
3
  import { type FrontmatterResult } from './core-policy-frontmatter.js';
3
4
  export type CorePolicyResult = {
4
5
  code: number;
@@ -7,7 +8,9 @@ export type CorePolicyResult = {
7
8
  };
8
9
  export declare function runPolicyAgeReport(args: readonly string[], root: string, env?: NodeJS.ProcessEnv, nowEpoch?: number, platform?: NodeJS.Platform): CorePolicyResult;
9
10
  export declare function runPolicyFrontmatter(args: readonly string[], cwd: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): FrontmatterResult;
10
- export declare function runPolicyRetire(args: readonly string[], root: string, cwd: string, env?: NodeJS.ProcessEnv, now?: Date): CorePolicyResult;
11
+ export declare function runPolicyRetire(args: readonly string[], root: string, cwd: string, env?: NodeJS.ProcessEnv, now?: Date, dependencies?: {
12
+ validatePolicy?: typeof policyValidatorDiagnostic;
13
+ }): CorePolicyResult;
11
14
  /** Fast-entry adapter; family gating and shell fallback happen before import. */
12
15
  export declare function runPolicyFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, commandName: string): number;
13
16
  export declare function registerCorePolicyCommand(core: Command): void;
@@ -4,14 +4,15 @@
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import { policyValidatorDiagnostic } from '../lib/policy-validator.js';
7
- import { resolveLiveRoot, resolveBundledAsset } from '../utils/hq-roots.js';
8
- import { runBundledScript } from '../utils/run-bundled-script.js';
7
+ import { packageRoot, resolveLiveRoot } from '../utils/hq-roots.js';
9
8
  import { describeCoreCommand } from './core-command-catalog.js';
10
9
  import { runPolicyFrontmatter as runPolicyFrontmatterImpl } from './core-policy-frontmatter.js';
11
10
  import { awkCannotOpenFileDiagnostic } from '../lib/core-utils/awk-diagnostics.js';
12
- const POLICY_AGE_REPORT_ASSET = 'core/scripts/policy-age-report.sh';
13
- const POLICY_RETIRE_ASSET = 'core/scripts/policy-retire.sh';
14
- const POLICY_FRONTMATTER_ASSET = 'core/scripts/read-policy-frontmatter.sh';
11
+ const POLICY_AGE_REPORT_SCRIPT = 'core/scripts/policy-age-report.sh';
12
+ const POLICY_RETIRE_SCRIPT = 'core/scripts/policy-retire.sh';
13
+ function formerScriptPath(relativePath) {
14
+ return path.join(packageRoot(), 'assets/scaffold', relativePath);
15
+ }
15
16
  const AGE_HELP = [
16
17
  '# hq-core: public',
17
18
  '# policy-age-report.sh — measure policy usage and flag retirement candidates',
@@ -389,7 +390,7 @@ function bashTestGreaterOrEqual(left, right, scriptPath, line, platform) {
389
390
  return { value: BigInt(left) >= parsed, stderr: '' };
390
391
  }
391
392
  export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Math.floor(Date.now() / 1000), platform = process.platform) {
392
- const parsed = parseAgeOptions(args, resolveBundledAsset(POLICY_AGE_REPORT_ASSET));
393
+ const parsed = parseAgeOptions(args, formerScriptPath(POLICY_AGE_REPORT_SCRIPT));
393
394
  if (parsed.kind === 'help')
394
395
  return policyResult(0, AGE_HELP);
395
396
  if (parsed.kind === 'error')
@@ -448,13 +449,13 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
448
449
  const singleIncident = ['user-correction', 'session-learning', 'back-pressure-failure'].includes(source) && (version || '1') === '1';
449
450
  const classes = [];
450
451
  if (fired === 0) {
451
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 173, platform);
452
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 173, platform);
452
453
  stderr += comparison.stderr;
453
454
  if (comparison.value && usage.anyFiles)
454
455
  classes.push('never-fired');
455
456
  }
456
457
  if (fired > 0) {
457
- const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 174, platform);
458
+ const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 174, platform);
458
459
  stderr += comparison.stderr;
459
460
  if (comparison.value)
460
461
  classes.push('dormant');
@@ -464,7 +465,7 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
464
465
  if (refs.renamed.length > 0)
465
466
  classes.push('renamed-refs');
466
467
  if (singleIncident) {
467
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 178, platform);
468
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 178, platform);
468
469
  stderr += comparison.stderr;
469
470
  if (comparison.value && fired <= 1)
470
471
  classes.push('single-incident-old');
@@ -625,7 +626,7 @@ function rewriteStatus(content, status, reason, by, now) {
625
626
  }
626
627
  return Buffer.concat(output);
627
628
  }
628
- function setPolicyStatus(filePath, status, reason, by, env, now, cwd = process.cwd()) {
629
+ function setPolicyStatus(filePath, status, reason, by, env, now, cwd = process.cwd(), validatePolicy = policyValidatorDiagnostic) {
629
630
  const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
630
631
  let content;
631
632
  try {
@@ -639,7 +640,7 @@ function setPolicyStatus(filePath, status, reason, by, env, now, cwd = process.c
639
640
  if (!firstLine(content)?.toString('latin1').startsWith('---')) {
640
641
  return { ok: false, error: `policy-retire: ${filePath} has no frontmatter\n` };
641
642
  }
642
- const diagnostic = policyValidatorDiagnostic(content.toString('utf8'), resolvedPath, env);
643
+ const diagnostic = validatePolicy(content.toString('utf8'), resolvedPath, env);
643
644
  if (diagnostic !== undefined) {
644
645
  // Bash command substitution in policy-retire.sh strips all trailing newlines
645
646
  // from the validator output before printf adds its single final newline.
@@ -929,8 +930,8 @@ function fromReport(options, root, cwd, env, now) {
929
930
  stdout += `policy-retire: ${selected} would be retired [${options.className}] (add --yes)\n`;
930
931
  return policyResult(0, stdout, stderr);
931
932
  }
932
- export function runPolicyRetire(args, root, cwd, env = process.env, now = new Date()) {
933
- const parsed = parseRetireOptions(args, resolveBundledAsset(POLICY_RETIRE_ASSET), env);
933
+ export function runPolicyRetire(args, root, cwd, env = process.env, now = new Date(), dependencies = {}) {
934
+ const parsed = parseRetireOptions(args, formerScriptPath(POLICY_RETIRE_SCRIPT), env);
934
935
  if (parsed.kind === 'help')
935
936
  return policyResult(0, RETIRE_HELP);
936
937
  if (parsed.kind === 'error')
@@ -946,14 +947,14 @@ export function runPolicyRetire(args, root, cwd, env = process.env, now = new Da
946
947
  if (file === undefined)
947
948
  return policyResult(3, '', `policy-retire: no policy found for '${options.target}'\n`);
948
949
  if (options.restore) {
949
- const updated = setPolicyStatus(file, 'active', '', options.by, env, now, cwd);
950
+ const updated = setPolicyStatus(file, 'active', '', options.by, env, now, cwd, dependencies.validatePolicy);
950
951
  if (!updated.ok)
951
952
  return policyResult(1, '', updated.error);
952
953
  return policyResult(0, `policy-retire: restored ${displayPolicyPath(file, root)}\n`);
953
954
  }
954
955
  if (!options.reason)
955
956
  return policyResult(2, '', 'policy-retire: --reason is required\n');
956
- const updated = setPolicyStatus(file, 'retired', options.reason, options.by, env, now, cwd);
957
+ const updated = setPolicyStatus(file, 'retired', options.reason, options.by, env, now, cwd, dependencies.validatePolicy);
957
958
  if (!updated.ok)
958
959
  return policyResult(1, '', updated.error);
959
960
  return policyResult(0, `policy-retire: retired ${displayPolicyPath(file, root)} — ${options.reason}\n`);
@@ -983,7 +984,7 @@ export function runPolicyFast(args, _stdin, cwd, hqRoot, commandName) {
983
984
  }
984
985
  export function registerCorePolicyCommand(core) {
985
986
  const group = core.command('policy').description('Policy maintenance commands');
986
- const register = (name, asset, summary, rootKind) => {
987
+ const register = (name, summary, rootKind) => {
987
988
  const command = group
988
989
  .command(name)
989
990
  .description(summary)
@@ -991,47 +992,24 @@ export function registerCorePolicyCommand(core) {
991
992
  .allowExcessArguments()
992
993
  .helpOption(false)
993
994
  .argument('[args...]', 'arguments passed through to the scaffold command');
994
- describeCoreCommand(command, { execution: 'native', asset, root: rootKind });
995
+ describeCoreCommand(command, { execution: 'native', root: rootKind });
995
996
  return command;
996
997
  };
997
- const ageReport = register('age-report', POLICY_AGE_REPORT_ASSET, 'Report policy usage and retirement candidates', 'live');
998
+ const ageReport = register('age-report', 'Report policy usage and retirement candidates', 'live');
998
999
  ageReport.action(async (args = [], _options, command) => {
999
1000
  const operands = command.args.length > 0 ? command.args : args;
1000
- const { coreNativePolicyEnabled } = await import('./core-native-policy-flag.js');
1001
- if (!await coreNativePolicyEnabled()) {
1002
- const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
1003
- const result = runBundledScript({ asset: POLICY_AGE_REPORT_ASSET, args: operands, cwd: process.cwd(), hqRoot: root });
1004
- if (result.code !== 0)
1005
- process.exitCode = result.code;
1006
- return;
1007
- }
1008
1001
  const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
1009
1002
  writeResult(runPolicyAgeReport(operands, root));
1010
1003
  });
1011
- const retire = register('retire', POLICY_RETIRE_ASSET, 'Retire or restore a policy', 'live');
1004
+ const retire = register('retire', 'Retire or restore a policy', 'live');
1012
1005
  retire.action(async (args = [], _options, command) => {
1013
1006
  const operands = command.args.length > 0 ? command.args : args;
1014
- const { coreNativePolicyEnabled } = await import('./core-native-policy-flag.js');
1015
- if (!await coreNativePolicyEnabled()) {
1016
- const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
1017
- const result = runBundledScript({ asset: POLICY_RETIRE_ASSET, args: operands, cwd: process.cwd(), hqRoot: root });
1018
- if (result.code !== 0)
1019
- process.exitCode = result.code;
1020
- return;
1021
- }
1022
1007
  const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
1023
1008
  writeResult(runPolicyRetire(operands, root, process.cwd()));
1024
1009
  });
1025
- const frontmatter = register('frontmatter', POLICY_FRONTMATTER_ASSET, 'Extract policy frontmatter', 'cwd');
1010
+ const frontmatter = register('frontmatter', 'Extract policy frontmatter', 'cwd');
1026
1011
  frontmatter.action(async (args = [], _options, command) => {
1027
1012
  const operands = command.args.length > 0 ? command.args : args;
1028
- const { coreNativePolicyEnabled } = await import('./core-native-policy-flag.js');
1029
- if (!await coreNativePolicyEnabled()) {
1030
- const result = runBundledScript({ asset: POLICY_FRONTMATTER_ASSET, args: operands, cwd: process.cwd() });
1031
- if (result.code !== 0)
1032
- process.exitCode = result.code;
1033
- return;
1034
- }
1035
1013
  writeResult(runPolicyFrontmatter(operands, process.cwd()));
1036
1014
  });
1037
1015
  }
@@ -306,17 +306,12 @@ export const SCAFFOLD_ONLY_ASSETS = [
306
306
  { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
307
307
  // Native knowledge-prefs retains its own shell companion until its family PR.
308
308
  { asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Rollback companion for native knowledge-prefs" },
309
- // Rollback companions for the default-off native knowledge and policy routes.
310
- // policy-retire also runs the scaffold validator in-process when native, but
311
- // its bundled shell fallback needs both helper assets at these exact paths.
312
- { asset: "core/scripts/policy-age-report.sh", root: "live", summary: "Rollback companion for native policy age-report" },
313
- { asset: "core/scripts/policy-retire.sh", root: "live", summary: "Rollback companion for native policy retire" },
314
- { asset: "core/scripts/read-policy-frontmatter.sh", root: "cwd", summary: "Rollback companion for native policy frontmatter" },
309
+ // Retained policy helpers used by the policy validator and trigger tooling.
315
310
  { asset: "core/scripts/eval-trigger.sh", root: "live", summary: "Trigger grammar helper used by the policy validator companion" },
316
311
  { asset: "core/scripts/migrate-policy-triggers.sh", root: "live", summary: "Rollback shell for native migrate-policy-triggers" },
317
312
  { asset: "core/scripts/register-project.sh", root: "live", summary: "Rollback shell for native register-project" },
318
313
  { asset: "core/scripts/work-mesh-live-rebind.sh", root: "live", summary: "Rollback shell for native work-mesh-live-rebind" },
319
- { asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Policy validator companion used by policy-retire rollback" },
314
+ { asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Hook-time policy frontmatter validator" },
320
315
  // Wave 2a handoff shells remain bundled as the default-off rollback path.
321
316
  { asset: "core/scripts/handoff-finalize.sh", root: "live", summary: "Rollback shell for native handoff finalize" },
322
317
  { asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
@@ -9,24 +9,43 @@ import { defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
9
9
  const defaultDeps = defaultRemoteDeps;
10
10
  export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
11
11
  db.command("provision")
12
- .description("Create the company remote database (Team plan)")
13
- .requiredOption("--company <slug>", "Company slug")
12
+ .description("Create a company remote database or your personal database")
13
+ .option("--company <slug>", "Company slug")
14
+ .option("--personal", "Provision your personal database")
14
15
  .option("--region <region>", "AWS region", "us-east-1")
15
16
  .action(async (opts) => {
16
17
  try {
17
18
  const deps = depsFactory();
18
19
  const company = String(opts.company ?? "").trim();
19
- if (!company) {
20
- console.error(chalk.red("Error: --company is required"));
20
+ if (opts.personal && opts.company !== undefined) {
21
+ console.error(chalk.red("Error: --personal cannot be used with --company"));
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ if (!opts.personal && !company) {
26
+ console.error(chalk.red("error: required option '--company <slug>' not specified"));
21
27
  process.exitCode = 1;
22
28
  return;
23
29
  }
24
- const resolved = await deps.resolveCompany(company);
25
30
  const client = new ControlPlaneDbClient({
26
31
  baseUrl: deps.apiBaseUrl,
27
32
  getAccessToken: deps.getAccessToken,
28
33
  fetchImpl: deps.fetchImpl,
29
34
  });
35
+ if (opts.personal) {
36
+ const result = await client.personalProvision({ region: opts.region });
37
+ if (result.status === "provisioning") {
38
+ console.log("Your personal database is still being set up. Run this again in a minute.");
39
+ }
40
+ else if (result.idempotent || result.status === "existing") {
41
+ console.log(`Your personal database already exists and is ready (${result.region}).`);
42
+ }
43
+ else {
44
+ console.log(`Your personal database is ready (${result.region}).`);
45
+ }
46
+ return;
47
+ }
48
+ const resolved = await deps.resolveCompany(company);
30
49
  const result = await client.provision({
31
50
  companyUid: resolved.companyUid,
32
51
  companySlug: resolved.companySlug,
@@ -50,11 +69,13 @@ export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
50
69
  const status = error.status;
51
70
  if (described) {
52
71
  console.error(chalk.red("Error:"), described);
53
- if (status === 501 || error.code === "REMOTE_DB_NOT_AVAILABLE") {
54
- console.error(chalk.dim("Local hq db status|sql|migrate still work offline."));
55
- }
56
- else {
57
- console.error(chalk.dim("Local databases still work: hq db status|sql|migrate."));
72
+ if (!opts.personal) {
73
+ if (status === 501 || error.code === "REMOTE_DB_NOT_AVAILABLE") {
74
+ console.error(chalk.dim("Local hq db status|sql|migrate still work offline."));
75
+ }
76
+ else {
77
+ console.error(chalk.dim("Local databases still work: hq db status|sql|migrate."));
78
+ }
58
79
  }
59
80
  }
60
81
  else if (status === 404) {
@@ -54,6 +54,18 @@ export function describeRemoteError(error) {
54
54
  return error.message;
55
55
  const status = error.status;
56
56
  const code = error.code;
57
+ if (code === "PERSONAL_DB_NOT_ENABLED") {
58
+ return "Personal databases are not turned on for your account.";
59
+ }
60
+ if (code === "PERSONAL_DB_NOT_PROVISIONED") {
61
+ return "You don't have a personal database yet. Run: hq db provision --personal";
62
+ }
63
+ if (code === "PERSONAL_DB_APP_LIMIT") {
64
+ return "Your personal database already holds 8 apps, which is the limit.";
65
+ }
66
+ if (status === 404 && error.personalRoute) {
67
+ return "The HQ API does not support personal databases yet. Update the CLI or try again later.";
68
+ }
57
69
  if (code === "REMOTE_DB_NOT_AVAILABLE" || status === 501) {
58
70
  const reason = error.reason;
59
71
  const message = error.message;