@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.
- package/CHANGELOG.md +30 -0
- package/dist/command-catalog.generated.d.ts +17 -5
- package/dist/command-catalog.generated.js +20 -5
- package/dist/commands/capacity.d.ts +26 -0
- package/dist/commands/capacity.js +64 -2
- package/dist/commands/core-policy.d.ts +4 -1
- package/dist/commands/core-policy.js +21 -43
- package/dist/commands/core.js +2 -7
- package/dist/commands/db-provision.js +31 -10
- package/dist/commands/db-remote-deps.js +12 -0
- package/dist/commands/db-sql.js +42 -9
- package/dist/commands/db-status.d.ts +2 -1
- package/dist/commands/db-status.js +33 -7
- package/dist/fast-scaffold.js +0 -12
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +1414 -923
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/brief-job.d.ts +22 -0
- package/dist/lib/bot/brief-job.js +38 -0
- package/dist/lib/bot/run.d.ts +10 -0
- package/dist/lib/bot/run.js +54 -9
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +225 -2
- package/dist/lib/capacity/collectors/claude-oauth.js +1053 -43
- package/dist/lib/capacity/ledger.d.ts +10 -1
- package/dist/lib/capacity/ledger.js +8 -1
- package/dist/lib/capacity/project.js +3 -0
- package/dist/lib/db/control-plane.d.ts +22 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/remote-engine.d.ts +9 -1
- package/dist/lib/db/remote-engine.js +32 -4
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/package.json +2 -2
- package/assets/scaffold/core/scripts/policy-age-report.sh +0 -199
- package/assets/scaffold/core/scripts/policy-retire.sh +0 -199
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
13
|
-
const
|
|
14
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
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',
|
|
995
|
+
describeCoreCommand(command, { execution: 'native', root: rootKind });
|
|
995
996
|
return command;
|
|
996
997
|
};
|
|
997
|
-
const ageReport = register('age-report',
|
|
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',
|
|
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',
|
|
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
|
}
|
package/dist/commands/core.js
CHANGED
|
@@ -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
|
-
//
|
|
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: "
|
|
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
|
|
13
|
-
.
|
|
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 (
|
|
20
|
-
console.error(chalk.red("Error: --
|
|
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 (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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;
|