@indigoai-us/hq-cli 5.312.2-beta.0 → 5.313.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 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
- package/assets/scaffold/core/scripts/register-project.sh +262 -0
- package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
- package/dist/command-catalog.generated.d.ts +80 -0
- package/dist/command-catalog.generated.js +99 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/company-rename-flag.d.ts +4 -0
- package/dist/commands/company-rename-flag.js +7 -0
- package/dist/commands/company-rename.d.ts +4 -0
- package/dist/commands/company-rename.js +90 -0
- package/dist/commands/company.js +2 -0
- package/dist/commands/core-eval-trigger-native.d.ts +4 -0
- package/dist/commands/core-eval-trigger-native.js +12 -0
- package/dist/commands/core-knowledge-prefs.d.ts +0 -1
- package/dist/commands/core-knowledge-prefs.js +1 -1
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
- package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
- package/dist/commands/core-native-hook-helper-commands.js +56 -0
- package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
- package/dist/commands/core-native-hook-helpers-flag.js +7 -0
- package/dist/commands/core-register-project-native.d.ts +30 -0
- package/dist/commands/core-register-project-native.js +347 -0
- package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
- package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
- package/dist/commands/core-worker-share-native.d.ts +0 -1
- package/dist/commands/core-worker-share-native.js +1 -1
- package/dist/commands/core.js +7 -0
- package/dist/commands/lanes-message.js +3 -0
- package/dist/commands/lanes-questions.js +2 -1
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +151 -21
- package/dist/fast-core.js +10 -3
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +11 -0
- package/dist/lib/doctor/checks/work-context.js +9 -1
- package/dist/lib/lanes/config.d.ts +3 -0
- package/dist/lib/lanes/config.js +14 -0
- package/dist/lib/lanes/disk-guard.d.ts +66 -0
- package/dist/lib/lanes/disk-guard.js +172 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/grants.d.ts +4 -3
- package/dist/lib/lanes/grants.js +12 -7
- package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +142 -11
- package/dist/lib/lanes/registry-flag.d.ts +6 -0
- package/dist/lib/lanes/registry-flag.js +25 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +26 -1
- package/dist/lib/lanes/store.js +11 -0
- package/dist/lib/lanes/types.d.ts +11 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/client.js +23 -5
- package/dist/lib/mesh/denial-codes.d.ts +5 -0
- package/dist/lib/mesh/denial-codes.js +14 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +11 -1
- package/dist/lib/search-index/embed-lock.d.ts +2 -0
- package/dist/lib/search-index/embed-lock.js +10 -1
- package/dist/lib/work-context/contract.d.ts +3 -0
- package/dist/lib/work-context/contract.schema.json +3 -1
- package/dist/lib/work-context/outbox.d.ts +14 -5
- package/dist/lib/work-context/outbox.js +48 -5
- package/dist/lib/work-context/reconcile.js +26 -5
- package/dist/main.d.ts +20 -0
- package/dist/main.js +8 -1
- package/dist/utils/core-eval-trigger.d.ts +4 -0
- package/dist/utils/core-eval-trigger.js +88 -0
- package/dist/utils/pack-contributions.d.ts +0 -5
- package/dist/utils/pack-contributions.js +1 -1
- package/dist/utils/redact-error-text.d.ts +0 -2
- package/dist/utils/redact-error-text.js +1 -1
- package/dist/utils/sandbox-runner-client.d.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/register-project.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
+
import { resolveBundledAsset } from "../utils/hq-roots.js";
|
|
8
|
+
export const REGISTER_PROJECT_ASSET = "core/scripts/register-project.sh";
|
|
9
|
+
const USAGE = "usage: register-project.sh <company> <project> | --audit <company> | --backfill <company> --only-active [--yes] | --retry-pending <company>";
|
|
10
|
+
const MIN_HQ_CLI = "5.139.0";
|
|
11
|
+
export function createRegisterProjectHqRunner(spawn = spawnSync) {
|
|
12
|
+
return (args, options) => {
|
|
13
|
+
const result = spawn("hq", args, {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: options.env,
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
// Shell hq children inherit fd 0 even when their output is captured or discarded.
|
|
18
|
+
stdio: options.capture ? ["inherit", "pipe", "pipe"] : ["inherit", "ignore", "ignore"],
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
code: result.status ?? 127,
|
|
22
|
+
stdout: options.capture ? String(result.stdout ?? "") : "",
|
|
23
|
+
stderr: options.capture ? String(result.stderr ?? "") : "",
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const defaultRunHq = createRegisterProjectHqRunner();
|
|
28
|
+
function errorResult(message, code = 1) { return { code, stdout: "", stderr: `${message}\n` }; }
|
|
29
|
+
function asRecord(value) {
|
|
30
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function jqOr(value, fallback) { return value === null || value === undefined || value === false ? fallback : value; }
|
|
33
|
+
function jqString(value) {
|
|
34
|
+
if (typeof value === "string")
|
|
35
|
+
return value;
|
|
36
|
+
if (value === null || value === undefined || value === false)
|
|
37
|
+
return "";
|
|
38
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
39
|
+
}
|
|
40
|
+
function readJson(fileSystem, file) {
|
|
41
|
+
return JSON.parse(fileSystem.readFileSync(file, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
function timestamp(deps) { return deps.now().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
44
|
+
function companyUid(company, deps) {
|
|
45
|
+
const direct = deps.env.HQ_COMPANY_UID ?? "";
|
|
46
|
+
if (direct)
|
|
47
|
+
return direct;
|
|
48
|
+
const file = path.join(deps.root, "companies", company, ".company-uid");
|
|
49
|
+
try {
|
|
50
|
+
return deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function boardPath(company, deps) { return path.join(deps.root, "companies", company, "board.json"); }
|
|
57
|
+
function runHelpProbe(deps) {
|
|
58
|
+
return deps.runHq(["mesh", "project", "ensure", "--help"], { cwd: deps.cwd, env: deps.env, capture: false }).code === 0;
|
|
59
|
+
}
|
|
60
|
+
function prdName(prd, deps) {
|
|
61
|
+
const value = readJson(deps.fs, prd);
|
|
62
|
+
return jqString(jqOr(asRecord(value)?.name, ""));
|
|
63
|
+
}
|
|
64
|
+
function boardData(company, board, deps) {
|
|
65
|
+
if (!deps.fs.existsSync(board))
|
|
66
|
+
return { company, projects: [] };
|
|
67
|
+
const value = asRecord(readJson(deps.fs, board));
|
|
68
|
+
if (!value)
|
|
69
|
+
throw new Error("board.json must contain an object");
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function projectRows(board) {
|
|
73
|
+
const projects = jqOr(board.projects, []);
|
|
74
|
+
if (!Array.isArray(projects))
|
|
75
|
+
throw new Error("board.json projects must be an array");
|
|
76
|
+
return projects.map((item) => {
|
|
77
|
+
const record = asRecord(item);
|
|
78
|
+
if (!record)
|
|
79
|
+
throw new Error("board.json project must be an object");
|
|
80
|
+
return record;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function matchesProject(row, id, rel) {
|
|
84
|
+
return row.id === id || row.prd_path === rel || row.mesh_project_id === id;
|
|
85
|
+
}
|
|
86
|
+
function atomicJqWrite(target, value, deps, initialize = false) {
|
|
87
|
+
deps.fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
88
|
+
if (initialize && !deps.fs.existsSync(target))
|
|
89
|
+
deps.fs.writeFileSync(target, serializeJqJson(value, "pretty"), { encoding: "utf8", mode: 0o666 });
|
|
90
|
+
const temporary = path.join(path.dirname(target), `.register-project.${process.pid}.${Math.random().toString(16).slice(2)}`);
|
|
91
|
+
deps.fs.writeFileSync(temporary, serializeJqJson(value, "pretty"), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
92
|
+
deps.fs.chmodSync(temporary, 0o600);
|
|
93
|
+
deps.fs.renameSync(temporary, target);
|
|
94
|
+
}
|
|
95
|
+
function markPending(company, project, deps) {
|
|
96
|
+
const board = boardPath(company, deps);
|
|
97
|
+
const prd = path.join(deps.root, "companies", company, "projects", project, "prd.json");
|
|
98
|
+
const rel = `companies/${company}/projects/${project}/prd.json`;
|
|
99
|
+
const now = timestamp(deps);
|
|
100
|
+
let title = "";
|
|
101
|
+
try {
|
|
102
|
+
title = prdName(prd, deps);
|
|
103
|
+
}
|
|
104
|
+
catch { /* shell suppresses title jq errors */ }
|
|
105
|
+
if (!title)
|
|
106
|
+
title = project;
|
|
107
|
+
const document = boardData(company, board, deps);
|
|
108
|
+
const rows = projectRows(document);
|
|
109
|
+
if (rows.some((row) => matchesProject(row, project, rel))) {
|
|
110
|
+
document.projects = rows.map((row) => matchesProject(row, project, rel)
|
|
111
|
+
? { ...row, pending_registration: true, updated_at: now, prd_path: jqOr(row.prd_path, rel) }
|
|
112
|
+
: row);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
document.projects = [...rows, {
|
|
116
|
+
id: project, title, status: "prd_created", scope: "company", prd_path: rel,
|
|
117
|
+
pending_registration: true, created_at: now, updated_at: now,
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
atomicJqWrite(board, document, deps, true);
|
|
121
|
+
}
|
|
122
|
+
function ensureOne(company, project, deps) {
|
|
123
|
+
const prd = path.join(deps.root, "companies", company, "projects", project, "prd.json");
|
|
124
|
+
if (!deps.fs.existsSync(prd))
|
|
125
|
+
return errorResult(`registration incomplete: missing ${prd}`);
|
|
126
|
+
if (!runHelpProbe(deps)) {
|
|
127
|
+
try {
|
|
128
|
+
markPending(company, project, deps);
|
|
129
|
+
}
|
|
130
|
+
catch { /* legacy jq/write errors remain a failed registration */ }
|
|
131
|
+
return errorResult(`hq-cli ${MIN_HQ_CLI} or newer is required; ${company}/${project} stays local until then`);
|
|
132
|
+
}
|
|
133
|
+
const ensure = deps.runHq(["mesh", "project", "ensure", project, "--company", company, "--stories-file", prd, "--json"], {
|
|
134
|
+
cwd: deps.cwd, env: deps.env, capture: true,
|
|
135
|
+
});
|
|
136
|
+
if (ensure.code !== 0)
|
|
137
|
+
return errorResult(`registration incomplete: hq mesh project ensure failed for ${company}/${project}`);
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
result = asRecord(JSON.parse(ensure.stdout));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return errorResult("registration incomplete: ensure output missing threadId");
|
|
144
|
+
}
|
|
145
|
+
if (result?.threadId === null || result?.threadId === undefined || result?.threadId === false)
|
|
146
|
+
return errorResult("registration incomplete: ensure output missing threadId");
|
|
147
|
+
if (result?.channelId === null || result?.channelId === undefined || result?.channelId === false)
|
|
148
|
+
return errorResult("registration incomplete: ensure output missing channelId");
|
|
149
|
+
const thread = jqString(result.threadId);
|
|
150
|
+
const channel = jqString(result.channelId);
|
|
151
|
+
if (!thread)
|
|
152
|
+
return errorResult("registration incomplete: empty threadId");
|
|
153
|
+
if (!channel)
|
|
154
|
+
return errorResult("registration incomplete: empty channelId");
|
|
155
|
+
const board = boardPath(company, deps);
|
|
156
|
+
const rel = `companies/${company}/projects/${project}/prd.json`;
|
|
157
|
+
const now = timestamp(deps);
|
|
158
|
+
let title = prdName(prd, deps);
|
|
159
|
+
if (!title)
|
|
160
|
+
title = project;
|
|
161
|
+
const document = boardData(company, board, deps);
|
|
162
|
+
const rows = projectRows(document);
|
|
163
|
+
if (rows.some((row) => matchesProject(row, project, rel))) {
|
|
164
|
+
document.projects = rows.map((row) => {
|
|
165
|
+
if (!matchesProject(row, project, rel))
|
|
166
|
+
return row;
|
|
167
|
+
const updated = { ...row, threadId: thread, channelId: channel, updated_at: now, prd_path: jqOr(row.prd_path, rel), mesh_project_id: project };
|
|
168
|
+
delete updated.pending_registration;
|
|
169
|
+
return updated;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
document.projects = [...rows, {
|
|
174
|
+
id: project, title, status: "prd_created", scope: "company", prd_path: rel,
|
|
175
|
+
threadId: thread, channelId: channel, mesh_project_id: project, created_at: now, updated_at: now,
|
|
176
|
+
}];
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
atomicJqWrite(board, document, deps, true);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return errorResult("registration incomplete: board.json write failed");
|
|
183
|
+
}
|
|
184
|
+
const written = boardData(company, board, deps);
|
|
185
|
+
const verified = projectRows(written).some((row) => matchesProject(row, project, rel) && row.threadId === thread && row.channelId === channel);
|
|
186
|
+
if (!verified)
|
|
187
|
+
return errorResult(`registration incomplete: board.json entry for ${project} did not verify`);
|
|
188
|
+
return { code: 0, stdout: `registered ${company}/${project} thread=${thread} channel=${channel}\n`, stderr: "" };
|
|
189
|
+
}
|
|
190
|
+
function audit(company, deps) {
|
|
191
|
+
if (!company)
|
|
192
|
+
return errorResult("usage: register-project.sh --audit <company>");
|
|
193
|
+
const directory = path.join(deps.root, "companies", company, "projects");
|
|
194
|
+
let projects;
|
|
195
|
+
try {
|
|
196
|
+
projects = deps.fs.readdirSync(directory).sort();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
200
|
+
}
|
|
201
|
+
let stderr = "";
|
|
202
|
+
const unregistered = projects.filter((project) => {
|
|
203
|
+
if (project.startsWith("."))
|
|
204
|
+
return false;
|
|
205
|
+
const prd = path.join(directory, project, "prd.json");
|
|
206
|
+
try {
|
|
207
|
+
if (!deps.fs.statSync(prd).isFile())
|
|
208
|
+
return false;
|
|
209
|
+
const uid = companyUid(company, deps);
|
|
210
|
+
if (uid === null) {
|
|
211
|
+
stderr += `no company uid for ${company} (set HQ_COMPANY_UID or companies/${company}/.company-uid)\n`;
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return !deps.fs.existsSync(path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/cache/projects", uid, `${project}.json`));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return { code: 0, stdout: unregistered.length ? `${unregistered.join("\n")}\n` : "", stderr };
|
|
221
|
+
}
|
|
222
|
+
function storyNotDone(prd, deps, diagnostics) {
|
|
223
|
+
try {
|
|
224
|
+
const stories = asRecord(readJson(deps.fs, prd))?.userStories;
|
|
225
|
+
if (!Array.isArray(stories))
|
|
226
|
+
return false;
|
|
227
|
+
return stories.some((story) => jqOr(asRecord(story)?.status, "") !== "done");
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
const jq = spawnSync("jq", ["-e", '[.userStories[]? | select((.status // "") != "done")] | length > 0', prd], {
|
|
231
|
+
cwd: deps.cwd, env: deps.env, encoding: "utf8",
|
|
232
|
+
});
|
|
233
|
+
if (jq.stderr)
|
|
234
|
+
diagnostics.push(jq.stderr);
|
|
235
|
+
else if (jq.error?.code === "ENOENT") {
|
|
236
|
+
const script = resolveBundledAsset(REGISTER_PROJECT_ASSET);
|
|
237
|
+
diagnostics.push(`${script}: line 34: jq: command not found\n`);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function backfill(args, deps) {
|
|
243
|
+
let company = "", only = false, yes = false;
|
|
244
|
+
for (const arg of args) {
|
|
245
|
+
if (arg === "--only-active")
|
|
246
|
+
only = true;
|
|
247
|
+
else if (arg === "--yes")
|
|
248
|
+
yes = true;
|
|
249
|
+
else if (arg.startsWith("--"))
|
|
250
|
+
return errorResult(`unknown flag ${arg}`);
|
|
251
|
+
else
|
|
252
|
+
company = arg;
|
|
253
|
+
}
|
|
254
|
+
if (!company)
|
|
255
|
+
return errorResult("usage: register-project.sh --backfill <company> --only-active [--yes]");
|
|
256
|
+
if (!only)
|
|
257
|
+
return errorResult("--backfill requires --only-active");
|
|
258
|
+
const directory = path.join(deps.root, "companies", company, "projects");
|
|
259
|
+
let projects;
|
|
260
|
+
try {
|
|
261
|
+
projects = deps.fs.readdirSync(directory).sort();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
265
|
+
}
|
|
266
|
+
projects = projects.filter((project) => !project.startsWith("."));
|
|
267
|
+
const diagnostics = [];
|
|
268
|
+
const targets = projects.filter((project) => {
|
|
269
|
+
const pdir = path.join(directory, project);
|
|
270
|
+
const prd = path.join(pdir, "prd.json");
|
|
271
|
+
try {
|
|
272
|
+
if (!deps.fs.statSync(prd).isFile() || !storyNotDone(prd, deps, diagnostics))
|
|
273
|
+
return false;
|
|
274
|
+
return Math.floor(deps.now().getTime() / 1000) - Math.floor(deps.fs.statSync(pdir).mtimeMs / 1000) < 30 * 86400;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
let stderr = diagnostics.join("");
|
|
281
|
+
if (!yes)
|
|
282
|
+
return { code: 0, stdout: targets.map((project) => `would register ${company}/${project}\n`).join(""), stderr };
|
|
283
|
+
let stdout = "";
|
|
284
|
+
for (const project of targets) {
|
|
285
|
+
const output = ensureOne(company, project, deps);
|
|
286
|
+
stdout += output.stdout;
|
|
287
|
+
stderr += output.stderr;
|
|
288
|
+
if (output.code !== 0)
|
|
289
|
+
return { ...output, stdout, stderr };
|
|
290
|
+
}
|
|
291
|
+
return { code: 0, stdout, stderr };
|
|
292
|
+
}
|
|
293
|
+
function retryPending(company, deps) {
|
|
294
|
+
if (!company)
|
|
295
|
+
return errorResult("usage: register-project.sh --retry-pending <company>");
|
|
296
|
+
if (!runHelpProbe(deps))
|
|
297
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
298
|
+
const board = boardPath(company, deps);
|
|
299
|
+
if (!deps.fs.existsSync(board))
|
|
300
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
301
|
+
let rows;
|
|
302
|
+
try {
|
|
303
|
+
rows = projectRows(boardData(company, board, deps));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
307
|
+
}
|
|
308
|
+
let stdout = "", stderr = "";
|
|
309
|
+
const ids = rows.filter((item) => item.pending_registration === true)
|
|
310
|
+
.map((row) => jqString(jqOr(row.id, ""))).filter(Boolean).slice(0, 3);
|
|
311
|
+
for (const id of ids) {
|
|
312
|
+
const result = ensureOne(company, id, deps);
|
|
313
|
+
stdout += result.stdout;
|
|
314
|
+
stderr += result.stderr;
|
|
315
|
+
}
|
|
316
|
+
return { code: 0, stdout, stderr };
|
|
317
|
+
}
|
|
318
|
+
export function runRegisterProject(args, deps) {
|
|
319
|
+
try {
|
|
320
|
+
const [mode, arg1, ...rest] = args;
|
|
321
|
+
if (mode === "--audit")
|
|
322
|
+
return audit(arg1 ?? "", deps);
|
|
323
|
+
if (mode === "--backfill")
|
|
324
|
+
return backfill([arg1 ?? "", ...rest].filter((value, index) => index > 0 || value !== ""), deps);
|
|
325
|
+
if (mode === "--retry-pending")
|
|
326
|
+
return retryPending(arg1 ?? "", deps);
|
|
327
|
+
if (args.length === 2)
|
|
328
|
+
return ensureOne(args[0], args[1], deps);
|
|
329
|
+
return errorResult(USAGE);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
export async function runRegisterProjectFast(args, _stdin, cwd, root) {
|
|
336
|
+
const chosenRoot = root || process.env.HQ_ROOT || cwd;
|
|
337
|
+
const result = runRegisterProject(args, {
|
|
338
|
+
root: chosenRoot, cwd, env: process.env, fs, now: () => new Date(),
|
|
339
|
+
runHq: (argv, opts) => defaultRunHq(argv, opts),
|
|
340
|
+
});
|
|
341
|
+
if (result.stdout)
|
|
342
|
+
process.stdout.write(result.stdout);
|
|
343
|
+
if (result.stderr)
|
|
344
|
+
process.stderr.write(result.stderr);
|
|
345
|
+
return result.code;
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=core-register-project-native.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/work-mesh-live-rebind.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
export declare const WORK_MESH_LIVE_REBIND_ASSET = "core/scripts/work-mesh-live-rebind.sh";
|
|
4
|
+
export type WorkMeshLiveRebindDeps = {
|
|
5
|
+
cwd: string;
|
|
6
|
+
root: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
fs: typeof fs;
|
|
9
|
+
now: () => Date;
|
|
10
|
+
ulid: (date: Date) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare function runWorkMeshLiveRebind(args: readonly string[], deps: WorkMeshLiveRebindDeps): {
|
|
13
|
+
code: number;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function runWorkMeshLiveRebindFast(args: readonly string[], _stdin: string, cwd: string, root?: string): Promise<number>;
|
|
18
|
+
//# sourceMappingURL=core-work-mesh-live-rebind-native.d.ts.map
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/work-mesh-live-rebind.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { generateUlid } from "../lib/mesh/live/ulid.js";
|
|
6
|
+
import { formatSpoolLine } from "../lib/mesh/live/format-spool-line.js";
|
|
7
|
+
export const WORK_MESH_LIVE_REBIND_ASSET = "core/scripts/work-mesh-live-rebind.sh";
|
|
8
|
+
const WORK_MESH_LIVE_REBIND_LIBRARY_ASSET = "core/scripts/lib/work-mesh-live-rebind.sh";
|
|
9
|
+
const USAGE = "usage: work-mesh-live-rebind.sh --session <sid> ...\n";
|
|
10
|
+
function defaultDeps(root, cwd, env) {
|
|
11
|
+
return { root, cwd, env, fs, now: () => new Date(), ulid: (date) => generateUlid(date.getTime()) };
|
|
12
|
+
}
|
|
13
|
+
function stateRoot(deps) { return deps.env.WORK_MESH_HOME || deps.env.HOME || os.homedir(); }
|
|
14
|
+
function markerPath(sid, deps) { return path.join(stateRoot(deps), ".hq/work-context/sessions", `${sid}.live-binding`); }
|
|
15
|
+
class MarkerWriteError extends Error {
|
|
16
|
+
target;
|
|
17
|
+
constructor(target, cause) {
|
|
18
|
+
super("live binding marker write failed", { cause });
|
|
19
|
+
this.target = target;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function readMarker(sid, deps) {
|
|
23
|
+
try {
|
|
24
|
+
const body = deps.fs.readFileSync(markerPath(sid, deps), "utf8").replace(/\r/g, "");
|
|
25
|
+
const value = (key) => {
|
|
26
|
+
const line = body.split("\n").find((row) => row.startsWith(`${key}=`));
|
|
27
|
+
return line === undefined ? "" : line.slice(key.length + 1).split("=", 1)[0];
|
|
28
|
+
};
|
|
29
|
+
return [value("companySlug"), value("project"), value("task")];
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return ["", "", ""];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeMarker(sid, company, project, task, deps) {
|
|
36
|
+
const target = markerPath(sid, deps);
|
|
37
|
+
const directory = path.dirname(target);
|
|
38
|
+
try {
|
|
39
|
+
deps.fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
catch { /* shell mkdir is best effort */ }
|
|
42
|
+
try {
|
|
43
|
+
deps.fs.chmodSync(directory, 0o700);
|
|
44
|
+
}
|
|
45
|
+
catch { /* shell chmod is best effort */ }
|
|
46
|
+
try {
|
|
47
|
+
deps.fs.writeFileSync(target, `companySlug=${company}\nproject=${project}\ntask=${task}\n`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new MarkerWriteError(target, error);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
deps.fs.chmodSync(target, 0o600);
|
|
54
|
+
}
|
|
55
|
+
catch { /* shell chmod is best effort */ }
|
|
56
|
+
}
|
|
57
|
+
function readState(sid, deps) {
|
|
58
|
+
const file = path.join(stateRoot(deps), ".hq/work-context/sessions", `${sid}.json`);
|
|
59
|
+
if (!deps.fs.existsSync(file))
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(deps.fs.readFileSync(file, "utf8"));
|
|
63
|
+
const raw = (key) => {
|
|
64
|
+
const value = parsed?.[key];
|
|
65
|
+
if (value === null || value === undefined || value === false)
|
|
66
|
+
return "";
|
|
67
|
+
return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
68
|
+
};
|
|
69
|
+
return { company: raw("companySlug"), project: raw("projectId"), task: raw("taskId") };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function nextSeq(sid, deps) {
|
|
76
|
+
const directory = deps.env.WORK_MESH_SEQ_DIR || path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/seq");
|
|
77
|
+
try {
|
|
78
|
+
if (!deps.fs.existsSync(directory)) {
|
|
79
|
+
deps.fs.mkdirSync(directory, { recursive: true });
|
|
80
|
+
try {
|
|
81
|
+
deps.fs.chmodSync(directory, 0o700);
|
|
82
|
+
}
|
|
83
|
+
catch { /* best effort */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* next write will fail */ }
|
|
87
|
+
const file = path.join(directory, sid);
|
|
88
|
+
let current = "0";
|
|
89
|
+
try {
|
|
90
|
+
current = deps.fs.readFileSync(file, "utf8").replace(/\s/g, "");
|
|
91
|
+
}
|
|
92
|
+
catch { /* first event */ }
|
|
93
|
+
if (!/^\d+$/.test(current))
|
|
94
|
+
current = "0";
|
|
95
|
+
const value = Number(current) + 1;
|
|
96
|
+
if (!Number.isSafeInteger(value))
|
|
97
|
+
return null;
|
|
98
|
+
try {
|
|
99
|
+
deps.fs.writeFileSync(file, `${value}\n`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function enqueueLifecycle(kind, sid, harness, adapter, company, project, task, deps) {
|
|
107
|
+
const seq = nextSeq(sid, deps);
|
|
108
|
+
if (seq === null)
|
|
109
|
+
return false;
|
|
110
|
+
const now = deps.now();
|
|
111
|
+
const event = {
|
|
112
|
+
v: 1,
|
|
113
|
+
eventId: deps.ulid(now),
|
|
114
|
+
kind,
|
|
115
|
+
sessionId: sid,
|
|
116
|
+
harness: harness,
|
|
117
|
+
adapterVersion: adapter,
|
|
118
|
+
at: `${now.toISOString().slice(0, 19)}.000Z`,
|
|
119
|
+
seq,
|
|
120
|
+
cwd: deps.env.PWD ?? "",
|
|
121
|
+
hqRoot: deps.env.HQ_ROOT ?? deps.root,
|
|
122
|
+
};
|
|
123
|
+
if (company)
|
|
124
|
+
event.companySlug = company;
|
|
125
|
+
if (project)
|
|
126
|
+
event.project = project;
|
|
127
|
+
if (task)
|
|
128
|
+
event.task = task;
|
|
129
|
+
const spool = deps.env.WORK_MESH_SPOOL || path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/spool.jsonl");
|
|
130
|
+
try {
|
|
131
|
+
if (!deps.fs.existsSync(spool)) {
|
|
132
|
+
try {
|
|
133
|
+
deps.fs.mkdirSync(path.dirname(spool), { recursive: true, mode: 0o700 });
|
|
134
|
+
}
|
|
135
|
+
catch { /* legacy mkdir is best effort */ }
|
|
136
|
+
try {
|
|
137
|
+
deps.fs.chmodSync(path.dirname(spool), 0o700);
|
|
138
|
+
}
|
|
139
|
+
catch { /* best effort */ }
|
|
140
|
+
deps.fs.writeFileSync(spool, "", { encoding: "utf8", mode: 0o600 });
|
|
141
|
+
try {
|
|
142
|
+
deps.fs.chmodSync(spool, 0o600);
|
|
143
|
+
}
|
|
144
|
+
catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
const fd = deps.fs.openSync(spool, fs.constants.O_APPEND | fs.constants.O_WRONLY);
|
|
147
|
+
try {
|
|
148
|
+
deps.fs.writeSync(fd, `${formatSpoolLine(event)}\n`);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
deps.fs.closeSync(fd);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch { /* work_mesh_enqueue is called with `|| true` by the shell library */ }
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
function normalizedHarness(env) {
|
|
158
|
+
let value = env.HQ_HARNESS || env.HQ_WORK_MESH_HARNESS || env.HQ_CHECKPOINT_RUNTIME || "claude-code";
|
|
159
|
+
if (["claude", "Claude", "ClaudeCode"].includes(value))
|
|
160
|
+
value = "claude-code";
|
|
161
|
+
else if (value === "Codex")
|
|
162
|
+
value = "codex";
|
|
163
|
+
else if (value === "Grok")
|
|
164
|
+
value = "grok";
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
function runRebind(sid, oldCompany, oldProject, oldTask, company, project, task, deps) {
|
|
168
|
+
if (!sid || !project)
|
|
169
|
+
return 1;
|
|
170
|
+
if (oldProject === project) {
|
|
171
|
+
writeMarker(sid, company, project, task, deps);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
const harness = normalizedHarness(deps.env);
|
|
175
|
+
const adapter = deps.env.HQ_ADAPTER_CONTRACT_VERSION || "1.0.0";
|
|
176
|
+
if (oldProject && !enqueueLifecycle("session_end", sid, harness, adapter, oldCompany, oldProject, oldTask, deps))
|
|
177
|
+
return 1;
|
|
178
|
+
if (!enqueueLifecycle("session_start", sid, harness, adapter, company, project, task, deps))
|
|
179
|
+
return 1;
|
|
180
|
+
writeMarker(sid, company, project, task, deps);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
function runWorkMeshLiveRebindUnsafe(args, deps) {
|
|
184
|
+
let sid = "", oldCompany = "", oldProject = "", oldTask = "", company = "", project = "", task = "", fromState = false;
|
|
185
|
+
const takesValue = {
|
|
186
|
+
"--session": "sid", "--session-id": "sid", "--old-company": "oldCompany", "--old-project": "oldProject", "--old-task": "oldTask",
|
|
187
|
+
"--company": "company", "--company-slug": "company", "--project": "project", "--task": "task",
|
|
188
|
+
};
|
|
189
|
+
for (let i = 0; i < args.length;) {
|
|
190
|
+
const arg = args[i];
|
|
191
|
+
const key = takesValue[arg];
|
|
192
|
+
if (key) {
|
|
193
|
+
if (i + 1 >= args.length)
|
|
194
|
+
return { code: 1, stdout: "", stderr: `assets/scaffold/${WORK_MESH_LIVE_REBIND_ASSET}: line 27: shift: shift count out of range\n` };
|
|
195
|
+
const value = args[i + 1] ?? "";
|
|
196
|
+
if (key === "sid")
|
|
197
|
+
sid = value;
|
|
198
|
+
else if (key === "oldCompany")
|
|
199
|
+
oldCompany = value;
|
|
200
|
+
else if (key === "oldProject")
|
|
201
|
+
oldProject = value;
|
|
202
|
+
else if (key === "oldTask")
|
|
203
|
+
oldTask = value;
|
|
204
|
+
else if (key === "company")
|
|
205
|
+
company = value;
|
|
206
|
+
else if (key === "project")
|
|
207
|
+
project = value;
|
|
208
|
+
else
|
|
209
|
+
task = value;
|
|
210
|
+
i += 2;
|
|
211
|
+
}
|
|
212
|
+
else if (arg === "--from-state") {
|
|
213
|
+
fromState = true;
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
i += 1;
|
|
218
|
+
}
|
|
219
|
+
if (!sid)
|
|
220
|
+
return { code: 1, stdout: "", stderr: USAGE };
|
|
221
|
+
if (fromState || !project) {
|
|
222
|
+
const current = readState(sid, deps);
|
|
223
|
+
if (!current?.project)
|
|
224
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
225
|
+
const [previousCompany, previousProject, previousTask] = readMarker(sid, deps);
|
|
226
|
+
if (!previousProject || previousProject === current.project) {
|
|
227
|
+
writeMarker(sid, current.company, current.project, current.task, deps);
|
|
228
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
229
|
+
}
|
|
230
|
+
return { code: runRebind(sid, previousCompany, previousProject, previousTask, current.company, current.project, current.task, deps), stdout: "", stderr: "" };
|
|
231
|
+
}
|
|
232
|
+
if (!oldProject)
|
|
233
|
+
[oldCompany, oldProject, oldTask] = readMarker(sid, deps);
|
|
234
|
+
return { code: runRebind(sid, oldCompany, oldProject, oldTask, company, project, task, deps), stdout: "", stderr: "" };
|
|
235
|
+
}
|
|
236
|
+
export function runWorkMeshLiveRebind(args, deps) {
|
|
237
|
+
try {
|
|
238
|
+
return runWorkMeshLiveRebindUnsafe(args, deps);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (!(error instanceof MarkerWriteError))
|
|
242
|
+
throw error;
|
|
243
|
+
const cause = error.cause;
|
|
244
|
+
const message = cause?.code === "EISDIR" ? "Is a directory"
|
|
245
|
+
: cause?.code === "EACCES" || cause?.code === "EPERM" ? "Permission denied"
|
|
246
|
+
: cause?.code === "EROFS" ? "Read-only file system"
|
|
247
|
+
: cause?.code === "ENOSPC" ? "No space left on device"
|
|
248
|
+
: "Input/output error";
|
|
249
|
+
const script = path.join(deps.root, WORK_MESH_LIVE_REBIND_LIBRARY_ASSET);
|
|
250
|
+
return { code: 1, stdout: "", stderr: `${script}: line 34: ${error.target}: ${message}\n` };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export async function runWorkMeshLiveRebindFast(args, _stdin, cwd, root) {
|
|
254
|
+
const chosenRoot = process.env.HQ_ROOT || root || cwd;
|
|
255
|
+
const result = runWorkMeshLiveRebind(args, defaultDeps(chosenRoot, cwd, process.env));
|
|
256
|
+
if (result.stdout)
|
|
257
|
+
process.stdout.write(result.stdout);
|
|
258
|
+
if (result.stderr)
|
|
259
|
+
process.stderr.write(result.stderr);
|
|
260
|
+
return result.code;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=core-work-mesh-live-rebind-native.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type WorkerSkillDependencies, type WorkerSkillResult } from "./core-worker-skills-runtime.js";
|
|
2
2
|
export declare const WORKER_SKILL_SHARE_ASSET = "core/scripts/share-worker-skill.sh";
|
|
3
|
-
export declare const WORKER_SKILL_SHARE_USAGE = "usage: $0 [--apply] [--force] <scope-dir> <skill-name> <worker-skill-path>...\n";
|
|
4
3
|
export declare function runWorkerSkillShare(argv: string[], dependencies: WorkerSkillDependencies): WorkerSkillResult;
|
|
5
4
|
//# sourceMappingURL=core-worker-share-native.d.ts.map
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { resolveBundledAsset } from "../utils/hq-roots.js";
|
|
4
4
|
import { existsFollowingLinks, isDirectory, isSymlink, resolveWorkerCallerRoot, runGit, errorCode, } from "./core-worker-skills-runtime.js";
|
|
5
5
|
export const WORKER_SKILL_SHARE_ASSET = "core/scripts/share-worker-skill.sh";
|
|
6
|
-
|
|
6
|
+
const WORKER_SKILL_SHARE_USAGE = "usage: $0 [--apply] [--force] <scope-dir> <skill-name> <worker-skill-path>...\n";
|
|
7
7
|
function error(message, code = 2) {
|
|
8
8
|
return { code, stdout: "", stderr: `${message}\n` };
|
|
9
9
|
}
|
package/dist/commands/core.js
CHANGED
|
@@ -45,6 +45,7 @@ import { registerCoreGitPackExtensionCheckNative } from "./core-git-pack-extensi
|
|
|
45
45
|
import { registerCorePinNative } from "./core-pin-native.js";
|
|
46
46
|
import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-access-native.js";
|
|
47
47
|
import { registerCoreWorkMeshBindTrustedNative } from "./core-work-mesh-bind-trusted-native.js";
|
|
48
|
+
import { registerCoreHookHelperNativeCommands } from "./core-native-hook-helper-commands.js";
|
|
48
49
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
49
50
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
50
51
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -240,6 +241,8 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
240
241
|
{ asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
|
|
241
242
|
{ asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
|
|
242
243
|
{ asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
|
|
244
|
+
{ asset: "core/scripts/lib/work-mesh-live-hook.sh", root: "live", summary: "Work Mesh Live hook helpers sourced by rebind" },
|
|
245
|
+
{ asset: "core/scripts/lib/work-mesh-live-rebind.sh", root: "live", summary: "Work Mesh Live session rebind library sourced by hooks and the native rollback command" },
|
|
243
246
|
// Wave 2 handoff/worktree commands retain their old scripts for one release,
|
|
244
247
|
// behind the shared default-off rollback flag.
|
|
245
248
|
{ asset: WORKTREE_GC_ASSET, root: "live", summary: "Rollback companion for native worktree-gc" },
|
|
@@ -276,6 +279,9 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
276
279
|
{ asset: "core/scripts/policy-retire.sh", root: "live", summary: "Rollback companion for native policy retire" },
|
|
277
280
|
{ asset: "core/scripts/read-policy-frontmatter.sh", root: "cwd", summary: "Rollback companion for native policy frontmatter" },
|
|
278
281
|
{ asset: "core/scripts/eval-trigger.sh", root: "live", summary: "Trigger grammar helper used by the policy validator companion" },
|
|
282
|
+
{ asset: "core/scripts/migrate-policy-triggers.sh", root: "live", summary: "Rollback shell for native migrate-policy-triggers" },
|
|
283
|
+
{ asset: "core/scripts/register-project.sh", root: "live", summary: "Rollback shell for native register-project" },
|
|
284
|
+
{ asset: "core/scripts/work-mesh-live-rebind.sh", root: "live", summary: "Rollback shell for native work-mesh-live-rebind" },
|
|
279
285
|
{ asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Policy validator companion used by policy-retire rollback" },
|
|
280
286
|
// Wave 2a handoff shells remain bundled as the default-off rollback path.
|
|
281
287
|
{ asset: "core/scripts/handoff-finalize.sh", root: "live", summary: "Rollback shell for native handoff finalize" },
|
|
@@ -380,6 +386,7 @@ export function registerCoreCommands(program) {
|
|
|
380
386
|
registerCoreRefreshVaultAccessNative(core);
|
|
381
387
|
registerCoreDmBindNative(core);
|
|
382
388
|
registerCoreWorkMeshBindTrustedNative(core);
|
|
389
|
+
registerCoreHookHelperNativeCommands(core);
|
|
383
390
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
384
391
|
// as a default-off rollback path for one release. Keep the gate helper small
|
|
385
392
|
// so sibling Wave 2 work can reuse it without editing this command table.
|