@indigoai-us/hq-cli 5.306.0 → 5.307.0
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 +9 -0
- package/assets/scaffold/core/scripts/audit-log.sh +384 -0
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +164 -0
- package/assets/scaffold/core/scripts/hq-pin.sh +118 -0
- package/assets/scaffold/core/scripts/lib/portable.sh +211 -0
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +188 -0
- package/dist/command-catalog.generated.d.ts +52 -0
- package/dist/command-catalog.generated.js +64 -0
- package/dist/commands/core-audit-log-native.d.ts +9 -0
- package/dist/commands/core-audit-log-native.js +257 -0
- package/dist/commands/core-git-pack-extension-check-native.d.ts +22 -0
- package/dist/commands/core-git-pack-extension-check-native.js +190 -0
- package/dist/commands/core-native-utilities-flag.d.ts +4 -0
- package/dist/commands/core-native-utilities-flag.js +7 -0
- package/dist/commands/core-native-utilities.d.ts +28 -0
- package/dist/commands/core-native-utilities.js +52 -0
- package/dist/commands/core-pin-native.d.ts +14 -0
- package/dist/commands/core-pin-native.js +236 -0
- package/dist/commands/core-refresh-vault-access-native.d.ts +20 -0
- package/dist/commands/core-refresh-vault-access-native.js +229 -0
- package/dist/commands/core.js +15 -0
- package/dist/commands/dm.js +6 -1
- package/dist/commands/lanes.js +43 -1
- package/dist/lib/agent-kit/fallback.d.ts +0 -7
- package/dist/lib/agent-kit/fallback.js +4 -4
- package/dist/lib/agent-kit/inbox-state.d.ts +0 -2
- package/dist/lib/agent-kit/inbox-state.js +2 -2
- package/dist/lib/agent-kit/kit-config.d.ts +0 -5
- package/dist/lib/agent-kit/kit-config.js +5 -5
- package/dist/lib/agent-kit/log.d.ts +0 -1
- package/dist/lib/agent-kit/log.js +1 -1
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +3 -12
- package/dist/lib/agent-kit/mcp/jsonrpc.js +4 -4
- package/dist/lib/agent-kit/mcp/tools.d.ts +0 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -2
- package/dist/lib/agent-kit/services.d.ts +0 -1
- package/dist/lib/agent-kit/services.js +1 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +0 -49
- package/dist/lib/checkpoint-ontology/apply.js +5 -5
- package/dist/lib/checkpoint-ontology/identity.d.ts +2 -1
- package/dist/lib/checkpoint-ontology/identity.js +1 -1
- package/dist/lib/checkpoint-ontology/multi-writer.d.ts +4 -9
- package/dist/lib/checkpoint-ontology/multi-writer.js +6 -6
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -17
- package/dist/lib/checkpoint-ontology/plan.js +5 -5
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +0 -3
- package/dist/lib/checkpoint-ontology/queued-touches.js +3 -3
- package/dist/lib/db/control-plane.js +6 -0
- package/dist/lib/deploy-apps.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/** In-process implementation of core/scripts/audit-log.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
5
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
6
|
+
import { bundledShellUnboundValue, registerNativeUtility, utilityError } from "./core-native-utilities.js";
|
|
7
|
+
export const AUDIT_LOG_ASSET = "core/scripts/audit-log.sh";
|
|
8
|
+
export const AUDIT_LOG_HELP = `audit-log.sh — HQ audit log utility
|
|
9
|
+
|
|
10
|
+
SUBCOMMANDS:
|
|
11
|
+
append Write one JSONL record to the audit log
|
|
12
|
+
query Filter and display records
|
|
13
|
+
summary Show aggregated table (project | worker | status)
|
|
14
|
+
|
|
15
|
+
APPEND FLAGS (--event and --project are required):
|
|
16
|
+
--timestamp ISO8601 (auto-filled if omitted)
|
|
17
|
+
--event task_started | phase_completed | task_completed | task_failed |
|
|
18
|
+
project_started | project_completed |
|
|
19
|
+
story_dispatched | story_completed | story_failed |
|
|
20
|
+
pipeline_started | pipeline_completed | pipeline_paused |
|
|
21
|
+
pipeline_failed | project_pr_created | project_reviewed |
|
|
22
|
+
project_merged | project_deployed | project_canary_pass |
|
|
23
|
+
project_canary_fail | gate_requested | gate_resolved
|
|
24
|
+
--company Company slug (e.g. {company})
|
|
25
|
+
--project Project slug (e.g. hq-observability)
|
|
26
|
+
--story-id Story ID (e.g. US-001)
|
|
27
|
+
--worker Worker name (e.g. backend-dev)
|
|
28
|
+
--model Model name (e.g. sonnet)
|
|
29
|
+
--action Short description of what happened
|
|
30
|
+
--files Comma-separated file paths touched
|
|
31
|
+
--result success | fail | skipped
|
|
32
|
+
--duration-ms Wall-clock time in milliseconds (integer)
|
|
33
|
+
--error Error message if result=fail
|
|
34
|
+
--session-id Claude session/thread ID
|
|
35
|
+
|
|
36
|
+
QUERY FLAGS:
|
|
37
|
+
--project Filter by project slug
|
|
38
|
+
--company Filter by company slug
|
|
39
|
+
--worker Filter by worker name
|
|
40
|
+
--since Filter by timestamp >= YYYY-MM-DD (or full ISO8601)
|
|
41
|
+
--event Filter by event type
|
|
42
|
+
|
|
43
|
+
EXAMPLES:
|
|
44
|
+
# Append a task_started event
|
|
45
|
+
core/scripts/audit-log.sh append \\
|
|
46
|
+
--event task_started \\
|
|
47
|
+
--project hq-observability \\
|
|
48
|
+
--company {company} \\
|
|
49
|
+
--story-id US-001 \\
|
|
50
|
+
--worker backend-dev \\
|
|
51
|
+
--model sonnet
|
|
52
|
+
|
|
53
|
+
# Query all failures for a project
|
|
54
|
+
core/scripts/audit-log.sh query --project hq-observability --event task_failed
|
|
55
|
+
|
|
56
|
+
# Query since a date
|
|
57
|
+
core/scripts/audit-log.sh query --since 2026-03-01
|
|
58
|
+
|
|
59
|
+
# Show summary table
|
|
60
|
+
core/scripts/audit-log.sh summary
|
|
61
|
+
|
|
62
|
+
LOG FILE:
|
|
63
|
+
workspace/metrics/audit-log.jsonl
|
|
64
|
+
`;
|
|
65
|
+
const EVENTS = "task_started phase_completed task_completed task_failed project_started project_completed story_dispatched story_completed story_failed pipeline_started pipeline_completed pipeline_paused pipeline_failed project_pr_created project_reviewed project_merged project_deployed project_canary_pass project_canary_fail gate_requested gate_resolved";
|
|
66
|
+
const RESULTS = "success fail skipped";
|
|
67
|
+
const APPEND_FLAGS = {
|
|
68
|
+
"--timestamp": "timestamp", "--event": "event", "--company": "company", "--project": "project",
|
|
69
|
+
"--story-id": "story_id", "--worker": "worker", "--model": "model", "--action": "action",
|
|
70
|
+
"--files": "files_touched", "--result": "result", "--duration-ms": "duration_ms", "--error": "error", "--session-id": "session_id",
|
|
71
|
+
};
|
|
72
|
+
const QUERY_FLAGS = {
|
|
73
|
+
"--project": "project", "--company": "company", "--worker": "worker", "--since": "timestamp", "--event": "event",
|
|
74
|
+
};
|
|
75
|
+
const APPEND_SHELL_LINES = {
|
|
76
|
+
"--timestamp": 76, "--event": 77, "--company": 78, "--project": 79, "--story-id": 80,
|
|
77
|
+
"--worker": 81, "--model": 82, "--action": 83, "--files": 84, "--result": 85,
|
|
78
|
+
"--duration-ms": 86, "--error": 87, "--session-id": 88,
|
|
79
|
+
};
|
|
80
|
+
const QUERY_SHELL_LINES = {
|
|
81
|
+
"--project": 171, "--company": 172, "--worker": 173, "--since": 174, "--event": 175,
|
|
82
|
+
};
|
|
83
|
+
const OMIT_EMPTY_FIELDS = ["company", "story_id", "worker", "model", "action", "result", "duration_ms", "error", "session_id"];
|
|
84
|
+
function parseFlags(args, allowed) {
|
|
85
|
+
const values = {};
|
|
86
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
87
|
+
const key = args[i];
|
|
88
|
+
if (!Object.hasOwn(allowed, key))
|
|
89
|
+
return { error: `ERROR: Unknown flag: ${key}` };
|
|
90
|
+
if (i + 1 >= args.length)
|
|
91
|
+
return { missingValue: key };
|
|
92
|
+
values[allowed[key]] = args[i + 1];
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
return { values };
|
|
96
|
+
}
|
|
97
|
+
function logPath(root) {
|
|
98
|
+
return path.join(root, "workspace/metrics/audit-log.jsonl");
|
|
99
|
+
}
|
|
100
|
+
function append(args, deps) {
|
|
101
|
+
const parsed = parseFlags(args, APPEND_FLAGS);
|
|
102
|
+
if (!parsed.values)
|
|
103
|
+
return parsed.missingValue
|
|
104
|
+
? bundledShellUnboundValue(AUDIT_LOG_ASSET, APPEND_SHELL_LINES[parsed.missingValue])
|
|
105
|
+
: utilityError(parsed.error ?? "ERROR: invalid arguments");
|
|
106
|
+
const values = parsed.values;
|
|
107
|
+
const event = values.event ?? "";
|
|
108
|
+
const project = values.project ?? "";
|
|
109
|
+
if (!event)
|
|
110
|
+
return utilityError("ERROR: Missing required field: --event");
|
|
111
|
+
if (!project)
|
|
112
|
+
return utilityError("ERROR: Missing required field: --project");
|
|
113
|
+
if (!` ${EVENTS} `.includes(` ${event} `))
|
|
114
|
+
return utilityError(`ERROR: Invalid event '${event}'. Must be one of: ${EVENTS}`);
|
|
115
|
+
if (values.result && !` ${RESULTS} `.includes(` ${values.result} `))
|
|
116
|
+
return utilityError(`ERROR: Invalid result '${values.result}'. Must be one of: ${RESULTS}`);
|
|
117
|
+
if (values.duration_ms && !/^\d+$/.test(values.duration_ms))
|
|
118
|
+
return utilityError("ERROR: --duration-ms must be a non-negative integer");
|
|
119
|
+
const timestamp = values.timestamp || deps.now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
120
|
+
const files = values.files_touched ? values.files_touched.split(",") : [];
|
|
121
|
+
const entry = {
|
|
122
|
+
timestamp,
|
|
123
|
+
event,
|
|
124
|
+
company: values.company || null,
|
|
125
|
+
project,
|
|
126
|
+
story_id: values.story_id || null,
|
|
127
|
+
worker: values.worker || null,
|
|
128
|
+
model: values.model || null,
|
|
129
|
+
action: values.action || null,
|
|
130
|
+
files_touched: files,
|
|
131
|
+
result: values.result || null,
|
|
132
|
+
duration_ms: values.duration_ms ? Number(values.duration_ms) : null,
|
|
133
|
+
error: values.error || null,
|
|
134
|
+
session_id: values.session_id || null,
|
|
135
|
+
};
|
|
136
|
+
for (const key of OMIT_EMPTY_FIELDS) {
|
|
137
|
+
if (entry[key] === null)
|
|
138
|
+
delete entry[key];
|
|
139
|
+
}
|
|
140
|
+
if (files.length === 0)
|
|
141
|
+
delete entry.files_touched;
|
|
142
|
+
const target = logPath(deps.root);
|
|
143
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
144
|
+
fs.appendFileSync(target, serializeJqJson(entry, "compact"), { encoding: "utf8", mode: 0o666 });
|
|
145
|
+
return { code: 0, stdout: `Appended to ${target}\n`, stderr: "" };
|
|
146
|
+
}
|
|
147
|
+
function readEntries(file) {
|
|
148
|
+
return fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.length > 0)
|
|
149
|
+
.map((line) => JSON.parse(line));
|
|
150
|
+
}
|
|
151
|
+
function query(args, deps) {
|
|
152
|
+
const parsed = parseFlags(args, QUERY_FLAGS);
|
|
153
|
+
if (!parsed.values)
|
|
154
|
+
return parsed.missingValue
|
|
155
|
+
? bundledShellUnboundValue(AUDIT_LOG_ASSET, QUERY_SHELL_LINES[parsed.missingValue])
|
|
156
|
+
: utilityError(parsed.error ?? "ERROR: invalid arguments");
|
|
157
|
+
const file = logPath(deps.root);
|
|
158
|
+
if (!fs.existsSync(file))
|
|
159
|
+
return { code: 0, stdout: "", stderr: `No audit log found at ${file}\n` };
|
|
160
|
+
let entries;
|
|
161
|
+
try {
|
|
162
|
+
entries = readEntries(file);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return { code: 4, stdout: "", stderr: "" };
|
|
166
|
+
}
|
|
167
|
+
const filters = Object.fromEntries(Object.entries(parsed.values).filter(([, value]) => value !== ""));
|
|
168
|
+
const rows = entries.filter((entry) => Object.entries(filters).every(([key, value]) => {
|
|
169
|
+
if (key === "timestamp")
|
|
170
|
+
return String(entry.timestamp ?? "") >= value;
|
|
171
|
+
return entry[key] === value;
|
|
172
|
+
}));
|
|
173
|
+
return { code: 0, stdout: serializeJqJson(rows, "pretty"), stderr: `--- ${rows.length} record(s) matched ---\n` };
|
|
174
|
+
}
|
|
175
|
+
function pad(value, width) {
|
|
176
|
+
return String(value ?? "").padEnd(width, " ");
|
|
177
|
+
}
|
|
178
|
+
function right(value, width) {
|
|
179
|
+
return String(value ?? "").padStart(width, " ");
|
|
180
|
+
}
|
|
181
|
+
function printProject(project, stories, phases, failures, skipped, last) {
|
|
182
|
+
return `${pad(project, 40)} ${right(stories, 8)} ${right(phases, 8)} ${right(failures, 8)} ${right(skipped, 8)} ${pad(last, 26)}\n`;
|
|
183
|
+
}
|
|
184
|
+
function printWorker(worker, tasks, failures, rate) {
|
|
185
|
+
return `${pad(worker, 30)} ${right(tasks, 8)} ${right(failures, 8)} ${pad(rate, 10)}\n`;
|
|
186
|
+
}
|
|
187
|
+
function isTask(entry) { return entry.event === "task_completed" || entry.event === "task_failed"; }
|
|
188
|
+
function isFailure(entry) { return entry.result === "fail" || entry.event === "task_failed"; }
|
|
189
|
+
function summary(_args, deps) {
|
|
190
|
+
const file = logPath(deps.root);
|
|
191
|
+
if (!fs.existsSync(file))
|
|
192
|
+
return { code: 0, stdout: "", stderr: `No audit log found at ${file}\n` };
|
|
193
|
+
let contents;
|
|
194
|
+
let entries;
|
|
195
|
+
try {
|
|
196
|
+
contents = fs.readFileSync(file, "utf8");
|
|
197
|
+
entries = readEntries(file);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return { code: 4, stdout: "", stderr: "" };
|
|
201
|
+
}
|
|
202
|
+
const lineCount = (contents.match(/\n/g) ?? []).length;
|
|
203
|
+
if (lineCount === 0)
|
|
204
|
+
return { code: 0, stdout: "Audit log is empty — no records to summarize.\n", stderr: "" };
|
|
205
|
+
const projects = new Map();
|
|
206
|
+
const workers = new Map();
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const project = String(entry.project ?? "null");
|
|
209
|
+
projects.set(project, [...(projects.get(project) ?? []), entry]);
|
|
210
|
+
if (entry.worker !== undefined && entry.worker !== null) {
|
|
211
|
+
const worker = String(entry.worker);
|
|
212
|
+
workers.set(worker, [...(workers.get(worker) ?? []), entry]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let out = "\n=== Audit Log Summary ===\n\n--- By Project ---\n";
|
|
216
|
+
out += printProject("PROJECT", "STORIES", "PHASES", "FAILURES", "SKIPPED", "LAST_ACTIVITY");
|
|
217
|
+
out += printProject("-".repeat(40), "--------", "--------", "--------", "--------", "-".repeat(26));
|
|
218
|
+
for (const project of [...projects.keys()].sort()) {
|
|
219
|
+
const rows = projects.get(project);
|
|
220
|
+
const sorted = [...rows].sort((a, b) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? "")));
|
|
221
|
+
out += printProject(project === "null" ? "unknown" : project, rows.filter(isTask).length, rows.filter((row) => row.event === "phase_completed").length, rows.filter(isFailure).length, rows.filter((row) => row.result === "skipped").length, String(sorted.at(-1)?.timestamp ?? "-"));
|
|
222
|
+
}
|
|
223
|
+
out += "\n--- By Worker ---\n";
|
|
224
|
+
out += printWorker("WORKER", "TASKS", "FAILURES", "SUCCESS_RATE");
|
|
225
|
+
out += printWorker("-".repeat(30), "--------", "--------", "----------");
|
|
226
|
+
for (const worker of [...workers.keys()].sort()) {
|
|
227
|
+
const rows = workers.get(worker);
|
|
228
|
+
const tasks = rows.filter(isTask).length;
|
|
229
|
+
const failures = rows.filter(isFailure).length;
|
|
230
|
+
const rate = tasks > 0 ? `${Math.floor(((tasks - failures) / tasks * 100) + 0.5)}%` : "n/a";
|
|
231
|
+
out += printWorker(worker, tasks, failures, rate);
|
|
232
|
+
}
|
|
233
|
+
const totalTasks = entries.filter(isTask).length;
|
|
234
|
+
const totalFailures = entries.filter(isFailure).length;
|
|
235
|
+
out += `\n--- Totals ---\n Total events: ${lineCount}\n Total tasks: ${totalTasks}\n Total failures: ${totalFailures}\n\n`;
|
|
236
|
+
return { code: 0, stdout: out, stderr: "" };
|
|
237
|
+
}
|
|
238
|
+
export function runAuditLog(args, deps) {
|
|
239
|
+
const [subcommand = "help", ...rest] = args;
|
|
240
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h")
|
|
241
|
+
return { code: 0, stdout: AUDIT_LOG_HELP, stderr: "" };
|
|
242
|
+
if (subcommand === "append")
|
|
243
|
+
return append(rest, deps);
|
|
244
|
+
if (subcommand === "query")
|
|
245
|
+
return query(rest, deps);
|
|
246
|
+
if (subcommand === "summary")
|
|
247
|
+
return summary(rest, deps);
|
|
248
|
+
return utilityError(`ERROR: Unknown subcommand '${subcommand}'. Use: append | query | summary | help`);
|
|
249
|
+
}
|
|
250
|
+
export function registerCoreAuditLogNative(core) {
|
|
251
|
+
registerNativeUtility(core, {
|
|
252
|
+
name: "audit-log", asset: AUDIT_LOG_ASSET, root: "live", summary: "Append, query, or summarize the HQ audit log",
|
|
253
|
+
resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
|
|
254
|
+
run: (args, context) => runAuditLog(args, context),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=core-audit-log-native.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { type UtilityOutput } from "./core-native-utilities.js";
|
|
4
|
+
export declare const GIT_PACK_EXTENSION_CHECK_ASSET = "core/scripts/git-pack-extension-check.sh";
|
|
5
|
+
export declare const GIT_PACK_EXTENSION_CHECK_HELP: string;
|
|
6
|
+
export type GitResult = {
|
|
7
|
+
code: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
};
|
|
11
|
+
export type GitPackDependencies = {
|
|
12
|
+
cwd: string;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
runGit?: (args: string[], cwd: string) => GitResult;
|
|
15
|
+
renameFile?: (source: string, destination: string) => void;
|
|
16
|
+
};
|
|
17
|
+
type PackHeaderReader = Pick<typeof fs, "openSync" | "readSync" | "closeSync">;
|
|
18
|
+
export declare function readPackHeaderHex(file: string, reader?: PackHeaderReader): string;
|
|
19
|
+
export declare function runGitPackExtensionCheck(args: string[], deps: GitPackDependencies): UtilityOutput;
|
|
20
|
+
export declare function registerCoreGitPackExtensionCheckNative(core: Command): void;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=core-git-pack-extension-check-native.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** In-process pack scan/repair for core/scripts/git-pack-extension-check.sh. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { registerNativeUtility, utilityError } from "./core-native-utilities.js";
|
|
6
|
+
export const GIT_PACK_EXTENSION_CHECK_ASSET = "core/scripts/git-pack-extension-check.sh";
|
|
7
|
+
export const GIT_PACK_EXTENSION_CHECK_HELP = `# hq-core: public
|
|
8
|
+
# git-pack-extension-check.sh — find pack files that have lost their extension.
|
|
9
|
+
#
|
|
10
|
+
# A git packfile set is three files sharing one basename:
|
|
11
|
+
#
|
|
12
|
+
# pack-<sha>.pack the objects (magic "PACK")
|
|
13
|
+
# pack-<sha>.idx the index into them (magic "\\377tOc")
|
|
14
|
+
# pack-<sha>.rev reverse index (magic "RIDX")
|
|
15
|
+
#
|
|
16
|
+
# Git pairs them BY EXTENSION. Strip the extension off any one of them and git
|
|
17
|
+
# stops seeing the set entirely: the objects are still on disk, byte for byte,
|
|
18
|
+
# but they become unreachable. The repo then fails in a way that reads like
|
|
19
|
+
# catastrophic history loss rather than a filename problem:
|
|
20
|
+
#
|
|
21
|
+
# fatal: unable to read tree (<sha>)
|
|
22
|
+
# error: bad tree object HEAD
|
|
23
|
+
# error: refs/heads/<branch>: invalid sha1 pointer <sha>
|
|
24
|
+
#
|
|
25
|
+
# \`git fsck\` reports the objects as missing and \`git gc\` cannot help, because
|
|
26
|
+
# nothing is actually corrupt — the bytes are intact and the repair is a
|
|
27
|
+
# rename. That mismatch between how it presents and what it is makes this
|
|
28
|
+
# failure expensive to diagnose from the error text alone, which is why this
|
|
29
|
+
# check exists as a deterministic probe.
|
|
30
|
+
#
|
|
31
|
+
# Git never writes an extensionless \`pack-*\` file, so finding one is
|
|
32
|
+
# unambiguous. Observed in the wild on a macOS HQ root where a folder-level
|
|
33
|
+
# sync/backup agent had been walking the tree; the mechanism that drops the
|
|
34
|
+
# extension is not established.
|
|
35
|
+
#
|
|
36
|
+
# Usage:
|
|
37
|
+
# git-pack-extension-check.sh [--root <path>] [--fix] [--quiet]
|
|
38
|
+
#
|
|
39
|
+
# --root <path> Repository to check (default: current directory).
|
|
40
|
+
# --fix Rename each orphan back to the extension its magic bytes
|
|
41
|
+
# prove it should carry. Without this the script only
|
|
42
|
+
# reports.
|
|
43
|
+
# --quiet Suppress the healthy-case line; findings still print.
|
|
44
|
+
#
|
|
45
|
+
# Exit codes:
|
|
46
|
+
# 0 healthy, or every orphan was repaired under --fix
|
|
47
|
+
# 1 usage error, or the root is not a git repository
|
|
48
|
+
# 2 orphans found and left in place (report-only, or unrepairable)
|
|
49
|
+
#
|
|
50
|
+
# The repair is a rename and nothing else. It never deletes, never rewrites
|
|
51
|
+
# file contents, never touches refs or the index, and refuses to overwrite an
|
|
52
|
+
# existing target. A file whose magic bytes are not a known pack signature is
|
|
53
|
+
# reported and left alone — this script will not guess.
|
|
54
|
+
|
|
55
|
+
set -uo pipefail
|
|
56
|
+
`.replace(/^# ?/gm, "");
|
|
57
|
+
function defaultGit(args, cwd, env) {
|
|
58
|
+
const result = spawnSync("git", args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
59
|
+
if (result.error)
|
|
60
|
+
return { code: 127, stdout: "", stderr: result.error.message };
|
|
61
|
+
return { code: result.status ?? 128, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? "") };
|
|
62
|
+
}
|
|
63
|
+
export function readPackHeaderHex(file, reader = fs) {
|
|
64
|
+
let fd;
|
|
65
|
+
try {
|
|
66
|
+
fd = reader.openSync(file, "r");
|
|
67
|
+
const header = Buffer.alloc(4);
|
|
68
|
+
const bytesRead = reader.readSync(fd, header, 0, header.length, 0);
|
|
69
|
+
return header.subarray(0, bytesRead).toString("hex");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (fd !== undefined) {
|
|
76
|
+
try {
|
|
77
|
+
reader.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
catch { /* preserve the old unreadable-header result */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function runGitPackExtensionCheck(args, deps) {
|
|
84
|
+
let root = ".";
|
|
85
|
+
let emptyRoot = false;
|
|
86
|
+
let fix = false;
|
|
87
|
+
let quiet = false;
|
|
88
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
89
|
+
const arg = args[i];
|
|
90
|
+
if (arg === "--root") {
|
|
91
|
+
if (i + 1 >= args.length)
|
|
92
|
+
return utilityError("git-pack-extension-check: --root needs a path");
|
|
93
|
+
root = args[++i];
|
|
94
|
+
emptyRoot = root.length === 0;
|
|
95
|
+
}
|
|
96
|
+
else if (arg === "--fix")
|
|
97
|
+
fix = true;
|
|
98
|
+
else if (arg === "--quiet")
|
|
99
|
+
quiet = true;
|
|
100
|
+
else if (arg === "-h" || arg === "--help")
|
|
101
|
+
return { code: 0, stdout: GIT_PACK_EXTENSION_CHECK_HELP, stderr: "" };
|
|
102
|
+
else
|
|
103
|
+
return utilityError(`git-pack-extension-check: unknown argument '${arg}'`);
|
|
104
|
+
}
|
|
105
|
+
if (emptyRoot)
|
|
106
|
+
return utilityError("git-pack-extension-check: not a directory: ");
|
|
107
|
+
const rootPath = path.resolve(deps.cwd, root);
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.statSync(rootPath).isDirectory())
|
|
110
|
+
throw new Error("not a directory");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return utilityError(`git-pack-extension-check: not a directory: ${root}`);
|
|
114
|
+
}
|
|
115
|
+
const git = deps.runGit ?? ((gitArgs, cwd) => defaultGit(gitArgs, cwd, deps.env ?? process.env));
|
|
116
|
+
const renameFile = deps.renameFile ?? ((source, destination) => fs.renameSync(source, destination));
|
|
117
|
+
const shellOutput = (value) => value.replace(/(?:\r?\n)+$/, "");
|
|
118
|
+
let common = shellOutput(git(["-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"], deps.cwd).stdout);
|
|
119
|
+
if (!common) {
|
|
120
|
+
common = shellOutput(git(["-C", root, "rev-parse", "--git-common-dir"], deps.cwd).stdout);
|
|
121
|
+
if (!common)
|
|
122
|
+
return utilityError(`git-pack-extension-check: not a git repository: ${root}`);
|
|
123
|
+
if (!path.isAbsolute(common))
|
|
124
|
+
common = path.resolve(rootPath, common);
|
|
125
|
+
}
|
|
126
|
+
const packDir = path.join(common, "objects/pack");
|
|
127
|
+
let names;
|
|
128
|
+
try {
|
|
129
|
+
names = fs.readdirSync(packDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.startsWith("pack-") && !entry.name.includes(".")).map((entry) => entry.name);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTDIR")
|
|
133
|
+
throw error;
|
|
134
|
+
return { code: 0, stdout: quiet ? "" : "git-pack-extension-check: no pack directory, nothing to check\n", stderr: "" };
|
|
135
|
+
}
|
|
136
|
+
let found = 0;
|
|
137
|
+
let repaired = 0;
|
|
138
|
+
let unrepaired = 0;
|
|
139
|
+
let stdout = "";
|
|
140
|
+
let stderr = "";
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
const orphan = path.join(packDir, name);
|
|
143
|
+
const hex = readPackHeaderHex(orphan);
|
|
144
|
+
const ext = hex === "5041434b" ? "pack" : hex === "ff744f63" ? "idx" : hex === "52494458" ? "rev" : "";
|
|
145
|
+
found += 1;
|
|
146
|
+
if (!ext) {
|
|
147
|
+
stderr += `git-pack-extension-check: FOUND ${name} — extensionless, but its magic bytes (${hex || "empty"}) are not a pack/idx/rev signature; leaving it alone\n`;
|
|
148
|
+
unrepaired += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const target = `${orphan}.${ext}`;
|
|
152
|
+
if (fs.existsSync(target)) {
|
|
153
|
+
stderr += `git-pack-extension-check: FOUND ${name} — should be ${name}.${ext}, but that file already exists; resolve by hand\n`;
|
|
154
|
+
unrepaired += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!fix) {
|
|
158
|
+
stderr += `git-pack-extension-check: FOUND ${name} — lost its .${ext} extension; git cannot see this pack set. Re-run with --fix to rename it.\n`;
|
|
159
|
+
unrepaired += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
// Rename works on filesystems that do not support hard links.
|
|
164
|
+
renameFile(orphan, target);
|
|
165
|
+
stdout += `git-pack-extension-check: REPAIRED ${name} -> ${name}.${ext}\n`;
|
|
166
|
+
repaired += 1;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
stderr += `git-pack-extension-check: FAILED to rename ${name} -> ${name}.${ext}\n`;
|
|
170
|
+
unrepaired += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (found === 0)
|
|
174
|
+
return { code: 0, stdout: quiet ? "" : `git-pack-extension-check: ok — no extensionless pack files in ${packDir}\n`, stderr: "" };
|
|
175
|
+
if (unrepaired > 0)
|
|
176
|
+
return { code: 2, stdout, stderr: `${stderr}git-pack-extension-check: ${found} orphan(s), ${repaired} repaired, ${unrepaired} still broken\n` };
|
|
177
|
+
return { code: 0, stdout: `${stdout}git-pack-extension-check: ${repaired} pack file(s) repaired — re-run 'git fsck' to confirm\n`, stderr: "" };
|
|
178
|
+
}
|
|
179
|
+
export function registerCoreGitPackExtensionCheckNative(core) {
|
|
180
|
+
registerNativeUtility(core, {
|
|
181
|
+
name: "git-pack-extension-check", asset: GIT_PACK_EXTENSION_CHECK_ASSET, root: "cwd", summary: "Find or repair extensionless Git pack files",
|
|
182
|
+
resolveRoot: (cwd) => cwd,
|
|
183
|
+
run: (args, context) => runGitPackExtensionCheck(args, {
|
|
184
|
+
cwd: context.cwd,
|
|
185
|
+
env: context.env,
|
|
186
|
+
runGit: (gitArgs, cwd) => defaultGit(gitArgs, cwd, context.env),
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=core-git-pack-extension-check-native.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Default-off rollout gate for native operator utility commands. */
|
|
2
|
+
export declare const CORE_NATIVE_UTILITIES_FLAG = "core-native-utilities";
|
|
3
|
+
export declare function coreNativeUtilitiesEnabled(): Promise<boolean>;
|
|
4
|
+
//# sourceMappingURL=core-native-utilities-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Default-off rollout gate for native operator utility commands. */
|
|
2
|
+
export const CORE_NATIVE_UTILITIES_FLAG = "core-native-utilities";
|
|
3
|
+
export async function coreNativeUtilitiesEnabled() {
|
|
4
|
+
const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
|
|
5
|
+
return resolveProcessFlagGate(CORE_NATIVE_UTILITIES_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=core-native-utilities-flag.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Shared dispatch for the default-off Wave 5 operator utilities. */
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { type CoreCommandRoot } from "./core-command-catalog.js";
|
|
4
|
+
export type UtilityOutput = {
|
|
5
|
+
code: number;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
};
|
|
9
|
+
export type UtilityContext = {
|
|
10
|
+
root: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
now: () => Date;
|
|
14
|
+
};
|
|
15
|
+
export type NativeUtilityDefinition = {
|
|
16
|
+
name: string;
|
|
17
|
+
asset: string;
|
|
18
|
+
root: CoreCommandRoot;
|
|
19
|
+
summary: string;
|
|
20
|
+
resolveRoot: (cwd: string, hqRoot?: string) => string;
|
|
21
|
+
run: (args: string[], context: UtilityContext) => UtilityOutput;
|
|
22
|
+
};
|
|
23
|
+
/** Register one raw-argv command and retain the original shell as rollback. */
|
|
24
|
+
export declare function registerNativeUtility(core: Command, definition: NativeUtilityDefinition): void;
|
|
25
|
+
export declare function utilityError(message: string, code?: number): UtilityOutput;
|
|
26
|
+
/** Preserve Bash's set -u diagnostic for a missing positional option value. */
|
|
27
|
+
export declare function bundledShellUnboundValue(asset: string, line: number): UtilityOutput;
|
|
28
|
+
//# sourceMappingURL=core-native-utilities.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { coreNativeUtilitiesEnabled } from "./core-native-utilities-flag.js";
|
|
3
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
5
|
+
import { packageRoot } from "../utils/hq-roots.js";
|
|
6
|
+
/** Register one raw-argv command and retain the original shell as rollback. */
|
|
7
|
+
export function registerNativeUtility(core, definition) {
|
|
8
|
+
const command = core.command(definition.name)
|
|
9
|
+
.description(definition.summary)
|
|
10
|
+
.allowUnknownOption()
|
|
11
|
+
.allowExcessArguments()
|
|
12
|
+
.helpOption(false)
|
|
13
|
+
.argument("[args...]")
|
|
14
|
+
.action(async (args = [], _options, cmd) => {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const groupRoot = core.opts().hqRoot;
|
|
17
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
18
|
+
const root = definition.resolveRoot(cwd, groupRoot);
|
|
19
|
+
const nativeEnabled = await coreNativeUtilitiesEnabled();
|
|
20
|
+
if (process.env.HQ_CM_W5_EXPECT_NATIVE === "1" && !nativeEnabled) {
|
|
21
|
+
throw new Error(`core-native-utilities dispatch gate is disabled for ${definition.name}`);
|
|
22
|
+
}
|
|
23
|
+
if (!nativeEnabled) {
|
|
24
|
+
const result = runBundledScript({
|
|
25
|
+
asset: definition.asset,
|
|
26
|
+
args: operands,
|
|
27
|
+
cwd: definition.root === "live" ? root : cwd,
|
|
28
|
+
hqRoot: root,
|
|
29
|
+
});
|
|
30
|
+
if (result.code !== 0)
|
|
31
|
+
process.exitCode = result.code;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const result = definition.run(operands, { root, cwd, env: process.env, now: () => new Date() });
|
|
35
|
+
if (result.stdout)
|
|
36
|
+
process.stdout.write(result.stdout);
|
|
37
|
+
if (result.stderr)
|
|
38
|
+
process.stderr.write(result.stderr);
|
|
39
|
+
if (result.code !== 0)
|
|
40
|
+
process.exitCode = result.code;
|
|
41
|
+
});
|
|
42
|
+
describeCoreCommand(command, { execution: "native", asset: definition.asset, root: definition.root });
|
|
43
|
+
}
|
|
44
|
+
export function utilityError(message, code = 1) {
|
|
45
|
+
return { code, stdout: "", stderr: `${message}\n` };
|
|
46
|
+
}
|
|
47
|
+
/** Preserve Bash's set -u diagnostic for a missing positional option value. */
|
|
48
|
+
export function bundledShellUnboundValue(asset, line) {
|
|
49
|
+
const script = path.join(packageRoot(), "assets/scaffold", asset);
|
|
50
|
+
return { code: 1, stdout: "", stderr: `${script}: line ${line}: $2: unbound variable\n` };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=core-native-utilities.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type UtilityOutput } from "./core-native-utilities.js";
|
|
3
|
+
export declare const PIN_ASSET = "core/scripts/hq-pin.sh";
|
|
4
|
+
export declare const PIN_HELP = "# hq-pin.sh \u2014 anchor the current session to one goal (the \"pin\").\n#\n# A pin is the message, goal, or target the session keeps working toward\n# across loops, wakeups, compactions, and hand-offs. It is stored per session\n# and re-read at every checkpoint so the session cannot drift.\n#\n# Usage:\n# core/scripts/hq-pin.sh set \"<goal>\" [--done \"<criterion>\"]... [--owner <name>] [--channel <dm-channel>]\n# core/scripts/hq-pin.sh show # print the pin (goal, done criteria, owner, channel, set-at)\n# core/scripts/hq-pin.sh check # print a one-line reminder for loop ticks; exit 3 if no pin\n# core/scripts/hq-pin.sh note \"<progress>\" # append a dated progress line under the pin\n# core/scripts/hq-pin.sh clear # remove the pin (the goal is met or withdrawn)\n#\n# Storage: workspace/sessions/<sid>/pin.md plus `pin: <goal>` in the session's\n";
|
|
5
|
+
export type PinDependencies = {
|
|
6
|
+
root: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
now: () => Date;
|
|
9
|
+
tmpDir?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolvePinSessionId(root: string, env: NodeJS.ProcessEnv): string;
|
|
12
|
+
export declare function runPin(args: string[], deps: PinDependencies): UtilityOutput;
|
|
13
|
+
export declare function registerCorePinNative(core: Command): void;
|
|
14
|
+
//# sourceMappingURL=core-pin-native.d.ts.map
|