@indigoai-us/hq-cli 5.345.118 → 5.345.120
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 +19 -0
- package/assets/scaffold/core/scripts/pipeline-conductor.sh +2671 -0
- package/assets/scaffold/core/scripts/pipeline-driver.sh +820 -0
- package/assets/scaffold/core/scripts/pipeline-envelope.sh +211 -0
- package/assets/scaffold/core/scripts/session-journal.sh +67 -5
- package/dist/command-catalog.generated.d.ts +38 -0
- package/dist/command-catalog.generated.js +45 -0
- package/dist/commands/core-checkpoint-native.d.ts +17 -3
- package/dist/commands/core-checkpoint-native.js +48 -8
- package/dist/commands/core-derive-trigger-facts-native.js +15 -7
- package/dist/commands/core-migrate-policy-triggers-native.js +3 -1
- package/dist/commands/core-pipeline-conductor-command.d.ts +5 -0
- package/dist/commands/core-pipeline-conductor-command.js +138 -0
- package/dist/commands/core-pipeline-conductor-native.d.ts +7 -0
- package/dist/commands/core-pipeline-conductor-native.js +2950 -0
- package/dist/commands/core-pipeline-driver-native.d.ts +24 -0
- package/dist/commands/core-pipeline-driver-native.js +843 -0
- package/dist/commands/core-pipeline-driver-process.d.ts +4 -0
- package/dist/commands/core-pipeline-driver-process.js +68 -0
- package/dist/commands/core-pipeline-envelope-native.d.ts +4 -0
- package/dist/commands/core-pipeline-envelope-native.js +87 -0
- package/dist/commands/core-policy.js +3 -0
- package/dist/commands/core-session-journal-native.js +142 -12
- package/dist/commands/core-worker-lint-native.d.ts +0 -1
- package/dist/commands/core-worker-lint-native.js +0 -1
- package/dist/commands/core-worker-share-native.js +2 -2
- package/dist/commands/core-worker-skills-command.d.ts +1 -1
- package/dist/commands/core-worker-skills-command.js +3 -20
- package/dist/commands/core.d.ts +5 -1
- package/dist/commands/core.js +8 -9
- package/dist/commands/dm.js +23 -3
- package/dist/commands/storage-gate.d.ts +4 -3
- package/dist/commands/storage-gate.js +4 -3
- package/dist/fast-scaffold.js +0 -2
- package/dist/lib/storage-feature-flag.d.ts +7 -7
- package/dist/utils/vault-api.d.ts +10 -0
- package/dist/utils/vault-api.js +27 -0
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +0 -197
- package/assets/scaffold/core/scripts/share-worker-skill.sh +0 -178
- package/dist/commands/core-native-worker-skills-flag.d.ts +0 -4
- package/dist/commands/core-native-worker-skills-flag.js +0 -7
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function extractWindowsDriverIdentityFunction(source: string): string;
|
|
2
|
+
/** Preserve the pinned driver's liveness-then-identity lock check. */
|
|
3
|
+
export declare function isPipelineDriverProcess(pid: number, platform?: NodeJS.Platform): boolean;
|
|
4
|
+
//# sourceMappingURL=core-pipeline-driver-process.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { BUNDLED_ASSET_DIR, packageRoot } from "../utils/hq-roots.js";
|
|
5
|
+
import { resolveWindowsBash } from "../utils/run-bundled-script.js";
|
|
6
|
+
const DRIVER_MARKER = "pipeline-driver";
|
|
7
|
+
const WINDOWS_DRIVER_IDENTITY_SIGNATURE = "is_driver() { # pid -> 0 when that pid is alive and is a pipeline-driver";
|
|
8
|
+
function isAlive(pid) {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function extractWindowsDriverIdentityFunction(source) {
|
|
18
|
+
const escapedSignature = WINDOWS_DRIVER_IDENTITY_SIGNATURE.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
19
|
+
const functionSource = source.match(new RegExp(`^${escapedSignature}\\n[\\s\\S]*?^\\}`, "mu"))?.[0];
|
|
20
|
+
if (!functionSource)
|
|
21
|
+
throw new Error(`Bundled pipeline-driver.sh must define '${WINDOWS_DRIVER_IDENTITY_SIGNATURE}'`);
|
|
22
|
+
return functionSource;
|
|
23
|
+
}
|
|
24
|
+
function isWindowsPipelineDriverProcess(pid) {
|
|
25
|
+
const bash = resolveWindowsBash(process.env).executable;
|
|
26
|
+
if (!bash)
|
|
27
|
+
return false;
|
|
28
|
+
const scriptPath = path.join(packageRoot(), BUNDLED_ASSET_DIR, "core", "scripts", "pipeline-driver.sh");
|
|
29
|
+
const source = fs.readFileSync(scriptPath, "utf8");
|
|
30
|
+
const functionSource = extractWindowsDriverIdentityFunction(source);
|
|
31
|
+
// Windows intentionally runs the pinned Git Bash implementation because its
|
|
32
|
+
// lock format uses both MSYS and Win32 PIDs. This keeps native checks aligned
|
|
33
|
+
// with the shell's liveness-then-identity order and process-table lookup.
|
|
34
|
+
const result = spawnSync(bash, ["-c", `${functionSource}\nis_driver "$1"`, "pipeline-driver-lock-check", String(pid)], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
});
|
|
39
|
+
if (result.error)
|
|
40
|
+
throw result.error;
|
|
41
|
+
return result.status === 0;
|
|
42
|
+
}
|
|
43
|
+
/** Preserve the pinned driver's liveness-then-identity lock check. */
|
|
44
|
+
export function isPipelineDriverProcess(pid, platform = process.platform) {
|
|
45
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
46
|
+
return false;
|
|
47
|
+
if (platform === "win32")
|
|
48
|
+
return isWindowsPipelineDriverProcess(pid);
|
|
49
|
+
if (!isAlive(pid))
|
|
50
|
+
return false;
|
|
51
|
+
if (platform === "linux") {
|
|
52
|
+
try {
|
|
53
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`).toString("utf8").includes(DRIVER_MARKER);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const args = platform === "darwin"
|
|
60
|
+
? ["-ww", "-o", "args=", "-p", String(pid)]
|
|
61
|
+
: ["-p", String(pid), "-o", "args="];
|
|
62
|
+
const result = spawnSync("ps", args, {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
65
|
+
});
|
|
66
|
+
return result.status === 0 && String(result.stdout ?? "").includes(DRIVER_MARKER);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=core-pipeline-driver-process.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
export function validatePipelinePhaseFile(filePath, kind) {
|
|
3
|
+
let text;
|
|
4
|
+
try {
|
|
5
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(filePath));
|
|
6
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
7
|
+
text = text.slice(1);
|
|
8
|
+
text = text.replace(/\r\n?/g, "\n");
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const message = error && typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
12
|
+
return [false, "pipeline-envelope: cannot read input: " + message];
|
|
13
|
+
}
|
|
14
|
+
if (!text.trim())
|
|
15
|
+
return [false, "invalid: empty input"];
|
|
16
|
+
let doc;
|
|
17
|
+
try {
|
|
18
|
+
doc = JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return [false, "invalid: unparseable JSON (" + message + ")"];
|
|
23
|
+
}
|
|
24
|
+
if (!isObject(doc)) {
|
|
25
|
+
const typeName = doc === null ? "NoneType" : Array.isArray(doc) ? "list" : typeof doc === "string" ? "str"
|
|
26
|
+
: typeof doc === "boolean" ? "bool" : Number.isInteger(doc) ? "int" : "float";
|
|
27
|
+
return [false, "invalid: top level must be a JSON object, got " + typeName];
|
|
28
|
+
}
|
|
29
|
+
const problems = [];
|
|
30
|
+
const schemas = { envelope: "hq-phase-envelope/v1", handoff: "hq-phase-handoff/v1" };
|
|
31
|
+
if (Object.hasOwn(doc, "schema") && doc.schema !== schemas[kind])
|
|
32
|
+
problems.push("wrong type: schema (want " + schemas[kind] + ")");
|
|
33
|
+
else if (!Object.hasOwn(doc, "schema"))
|
|
34
|
+
problems.push("missing field: schema");
|
|
35
|
+
const isString = (value) => typeof value === "string";
|
|
36
|
+
const isNonEmpty = (value) => isString(value) && value !== "";
|
|
37
|
+
const stringList = (value) => Array.isArray(value) && value.every(isString);
|
|
38
|
+
const check = (name, predicate, want, required = true) => {
|
|
39
|
+
if (!Object.hasOwn(doc, name)) {
|
|
40
|
+
if (required)
|
|
41
|
+
problems.push("missing field: " + name);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!predicate(doc[name]))
|
|
45
|
+
problems.push("wrong type: " + name + " (want " + want + ")");
|
|
46
|
+
};
|
|
47
|
+
if (kind === "envelope") {
|
|
48
|
+
for (const field of ["story_id", "phase", "worker_id"])
|
|
49
|
+
check(field, isNonEmpty, "non-empty string");
|
|
50
|
+
check("worktree", isNonEmpty, "string path");
|
|
51
|
+
check("incoming_handoff", (value) => value === null || isNonEmpty(value), "string path or null");
|
|
52
|
+
check("acceptance_criteria", stringList, "array of strings");
|
|
53
|
+
check("deadline", (value) => isString(value) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]00:?00)\n?$/.test(value), "ISO8601 UTC string");
|
|
54
|
+
check("fresh_call", (value) => typeof value === "boolean", "boolean");
|
|
55
|
+
for (const field of ["project", "engine", "result_path", "story_title", "story_description", "reopen_note", "repo", "branch", "prior_handoff"])
|
|
56
|
+
check(field, isString, "string", false);
|
|
57
|
+
check("resumed_after_interrupt", (value) => typeof value === "boolean", "boolean", false);
|
|
58
|
+
check("constraints", stringList, "array of strings", false);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
for (const field of ["story_id", "phase", "worker_id"])
|
|
62
|
+
check(field, isNonEmpty, "non-empty string");
|
|
63
|
+
check("status", (value) => value === "passed" || value === "failed" || value === "blocked", "one of passed|failed|blocked");
|
|
64
|
+
check("summary", isString, "string");
|
|
65
|
+
check("files_changed", stringList, "array of strings");
|
|
66
|
+
check("commits", stringList, "array of strings");
|
|
67
|
+
if (!Object.hasOwn(doc, "back_pressure"))
|
|
68
|
+
problems.push("missing field: back_pressure");
|
|
69
|
+
else if (!isObject(doc.back_pressure))
|
|
70
|
+
problems.push("wrong type: back_pressure (want object)");
|
|
71
|
+
else
|
|
72
|
+
for (const group of ["tests", "lint", "typecheck", "build"]) {
|
|
73
|
+
if (!Object.hasOwn(doc.back_pressure, group))
|
|
74
|
+
problems.push("missing field: back_pressure." + group);
|
|
75
|
+
else if (!["pass", "fail", "skip"].includes(String(doc.back_pressure[group])))
|
|
76
|
+
problems.push("wrong type: back_pressure." + group + " (want one of pass|fail|skip)");
|
|
77
|
+
}
|
|
78
|
+
check("context_for_next", isString, "string");
|
|
79
|
+
for (const field of ["engine", "notes"])
|
|
80
|
+
check(field, isString, "string", false);
|
|
81
|
+
}
|
|
82
|
+
return problems.length ? [false, problems.join("\n")] : [true, "valid " + kind];
|
|
83
|
+
}
|
|
84
|
+
function isObject(value) {
|
|
85
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=core-pipeline-envelope-native.js.map
|
|
@@ -772,6 +772,8 @@ function autoRetire(options, root, cwd, env, now) {
|
|
|
772
772
|
reportText += `# Automatic policy retirement — ${today}\n\nMode: human on the loop. Each policy below was retired by \`policy-retire.sh --auto\` with the reason shown.\nReverse any wrong call with \`bash core/scripts/policy-retire.sh <slug> --restore\` and fix the rule that produced it.\n\n`;
|
|
773
773
|
}
|
|
774
774
|
reportText += `## Run ${now.toISOString().slice(11, 19)}Z — dirs: ${dirs.join(' ')}\n\n`;
|
|
775
|
+
// Match the shell's preflight: fail to create the report temp before any
|
|
776
|
+
// policy can be retired, then replace it with the complete run section.
|
|
775
777
|
fs.writeFileSync(tempPath, reportText);
|
|
776
778
|
let retired = 0;
|
|
777
779
|
let failed = 0;
|
|
@@ -847,6 +849,7 @@ function autoRetire(options, root, cwd, env, now) {
|
|
|
847
849
|
stdout += `policy-retire --auto: ${wouldRetire} of ${candidates.length} candidates would be retired (dry run)\n`;
|
|
848
850
|
}
|
|
849
851
|
else if (retired > 0 || failed > 0 || needfix > 0) {
|
|
852
|
+
fs.writeFileSync(tempPath, reportText);
|
|
850
853
|
fs.appendFileSync(outputPath, fs.readFileSync(tempPath));
|
|
851
854
|
fs.rmSync(tempPath, { force: true });
|
|
852
855
|
stdout += `policy-retire --auto: ${retired} retired, ${failed} failed, ${needfix} need a fix; report ${displayPolicyPath(outputPath, root)}\n`;
|
|
@@ -89,6 +89,18 @@ function processIsAlive(pid) {
|
|
|
89
89
|
return error.code !== "ESRCH";
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
function counterLockOwnerIsAlive(lock, pid) {
|
|
93
|
+
if (processIsAlive(pid))
|
|
94
|
+
return true;
|
|
95
|
+
if (process.platform !== "win32")
|
|
96
|
+
return false;
|
|
97
|
+
let windowsPid = 0;
|
|
98
|
+
try {
|
|
99
|
+
windowsPid = Number.parseInt(fs.readFileSync(path.join(lock, "winpid"), "utf8"), 10) || 0;
|
|
100
|
+
}
|
|
101
|
+
catch { /* older shell locks did not record the Win32 PID */ }
|
|
102
|
+
return windowsPid > 1 && processIsAlive(windowsPid);
|
|
103
|
+
}
|
|
92
104
|
function acquireJournalWriteLock(dir) {
|
|
93
105
|
const lock = path.join(dir, ".session-journal-write.lock");
|
|
94
106
|
const ownerFile = path.join(lock, "pid");
|
|
@@ -142,26 +154,144 @@ function releaseJournalWriteLock(lock) {
|
|
|
142
154
|
}
|
|
143
155
|
catch { /* shell lock cleanup is best-effort */ }
|
|
144
156
|
}
|
|
145
|
-
|
|
157
|
+
const COUNTER_LOCK_PIDLESS_STALE_MS = 60_000;
|
|
158
|
+
function isAgedPidlessCounterLock(lock, now) {
|
|
159
|
+
try {
|
|
160
|
+
return now().getTime() - fs.statSync(lock).mtimeMs >= COUNTER_LOCK_PIDLESS_STALE_MS;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function reclaimStaleCounterLock(lock, now) {
|
|
167
|
+
let rawOwner;
|
|
168
|
+
try {
|
|
169
|
+
rawOwner = fs.readFileSync(path.join(lock, "pid"), "utf8").trim();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code !== "ENOENT" || !isAgedPidlessCounterLock(lock, now))
|
|
173
|
+
return false;
|
|
174
|
+
rawOwner = "";
|
|
175
|
+
}
|
|
176
|
+
if (rawOwner !== "" && !/^\d+$/u.test(rawOwner))
|
|
177
|
+
return false;
|
|
178
|
+
let rawWindowsPid = "";
|
|
179
|
+
try {
|
|
180
|
+
rawWindowsPid = fs.readFileSync(path.join(lock, "winpid"), "utf8").trim();
|
|
181
|
+
}
|
|
182
|
+
catch { /* older shell locks did not record the Win32 PID */ }
|
|
183
|
+
if (rawWindowsPid !== "" && !/^\d+$/u.test(rawWindowsPid))
|
|
184
|
+
return false;
|
|
185
|
+
const owner = rawOwner ? Number.parseInt(rawOwner, 10) : 0;
|
|
186
|
+
const windowsOwner = rawWindowsPid ? Number.parseInt(rawWindowsPid, 10) : 0;
|
|
187
|
+
if (rawOwner && (owner <= 1 || counterLockOwnerIsAlive(lock, owner)))
|
|
188
|
+
return false;
|
|
189
|
+
if (!rawOwner && windowsOwner > 1 && processIsAlive(windowsOwner))
|
|
190
|
+
return false;
|
|
191
|
+
const claim = `${lock}.reclaim`;
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(claim);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(path.join(claim, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
try {
|
|
203
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
catch { /* best effort */ }
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
let currentRawOwner;
|
|
209
|
+
try {
|
|
210
|
+
currentRawOwner = fs.readFileSync(path.join(lock, "pid"), "utf8").trim();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
currentRawOwner = "";
|
|
214
|
+
}
|
|
215
|
+
let currentRawWindowsPid;
|
|
216
|
+
try {
|
|
217
|
+
currentRawWindowsPid = fs.readFileSync(path.join(lock, "winpid"), "utf8").trim();
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
currentRawWindowsPid = "";
|
|
221
|
+
}
|
|
222
|
+
const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid;
|
|
223
|
+
const stillStale = rawOwner
|
|
224
|
+
? sameOwner && !counterLockOwnerIsAlive(lock, owner)
|
|
225
|
+
: sameOwner && !(windowsOwner > 1 && processIsAlive(windowsOwner)) && isAgedPidlessCounterLock(lock, now);
|
|
226
|
+
if (stillStale) {
|
|
227
|
+
const stale = `${lock}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
228
|
+
try {
|
|
229
|
+
fs.renameSync(lock, stale);
|
|
230
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
231
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
catch { /* another waiter reclaimed it */ }
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
catch { /* best effort */ }
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
function withCounterLock(lock, env, writers, now, action) {
|
|
146
243
|
let i = 0;
|
|
147
244
|
let acquired = false;
|
|
148
|
-
|
|
245
|
+
let tries = env.HQ_JOURNAL_COUNTER_LOCK_TRIES ?? "250";
|
|
246
|
+
if (!/^\d+$/u.test(tries))
|
|
247
|
+
tries = "250";
|
|
248
|
+
const retryLimit = Number.parseInt(tries, 10);
|
|
249
|
+
while (i < retryLimit) {
|
|
149
250
|
try {
|
|
150
251
|
fs.mkdirSync(lock);
|
|
151
252
|
acquired = true;
|
|
253
|
+
try {
|
|
254
|
+
if (process.platform === "win32")
|
|
255
|
+
fs.writeFileSync(path.join(lock, "winpid"), `${process.pid}\n`, { flag: "wx" });
|
|
256
|
+
fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
try {
|
|
260
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
catch { /* best effort */ }
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
152
266
|
}
|
|
153
267
|
catch {
|
|
268
|
+
if (reclaimStaleCounterLock(lock, now))
|
|
269
|
+
continue;
|
|
154
270
|
i++;
|
|
155
|
-
if (i
|
|
156
|
-
|
|
157
|
-
sleepSync(20);
|
|
271
|
+
if (i < retryLimit)
|
|
272
|
+
sleepSync(20);
|
|
158
273
|
}
|
|
159
274
|
}
|
|
160
|
-
|
|
275
|
+
if (!acquired) {
|
|
276
|
+
writers.stderr("session-journal: tool-counter: lock remained busy; skipped update\n");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
161
279
|
try {
|
|
162
|
-
|
|
280
|
+
action();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
let owner = 0;
|
|
284
|
+
try {
|
|
285
|
+
owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10) || 0;
|
|
286
|
+
}
|
|
287
|
+
catch { /* lock disappeared */ }
|
|
288
|
+
if (owner === process.pid) {
|
|
289
|
+
try {
|
|
290
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
catch { /* best effort */ }
|
|
293
|
+
}
|
|
163
294
|
}
|
|
164
|
-
catch { /* shell rmdir is best-effort */ }
|
|
165
295
|
}
|
|
166
296
|
function nextSequence(dir) {
|
|
167
297
|
let names = [];
|
|
@@ -205,12 +335,12 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
205
335
|
writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
|
|
206
336
|
};
|
|
207
337
|
if (action === "increment") {
|
|
208
|
-
withCounterLock(lock, () => {
|
|
338
|
+
withCounterLock(lock, env, writers, now, () => {
|
|
209
339
|
try {
|
|
210
340
|
fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
|
|
211
341
|
}
|
|
212
342
|
catch (error) {
|
|
213
|
-
reportDirectoryWriteFailure(error,
|
|
343
|
+
reportDirectoryWriteFailure(error, 457);
|
|
214
344
|
}
|
|
215
345
|
});
|
|
216
346
|
}
|
|
@@ -218,12 +348,12 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
218
348
|
writers.stdout(`${readCounter(counter)}\n`);
|
|
219
349
|
}
|
|
220
350
|
else if (action === "reset") {
|
|
221
|
-
withCounterLock(lock, () => {
|
|
351
|
+
withCounterLock(lock, env, writers, now, () => {
|
|
222
352
|
try {
|
|
223
353
|
fs.writeFileSync(counter, "0\n");
|
|
224
354
|
}
|
|
225
355
|
catch (error) {
|
|
226
|
-
reportDirectoryWriteFailure(error,
|
|
356
|
+
reportDirectoryWriteFailure(error, 465);
|
|
227
357
|
}
|
|
228
358
|
});
|
|
229
359
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { type WorkerSkillDependencies, type WorkerSkillResult } from "./core-worker-skills-runtime.js";
|
|
2
|
-
export declare const WORKER_SKILL_LINT_ASSET = "core/scripts/lint-shared-worker-skills.sh";
|
|
3
2
|
export declare function runWorkerSkillLint(args: string[], dependencies: WorkerSkillDependencies): WorkerSkillResult;
|
|
4
3
|
//# sourceMappingURL=core-worker-lint-native.d.ts.map
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import * as crypto from "node:crypto";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { errorCode, isDirectory, missingOrUnreadable, resolveWorkerCallerRoot, } from "./core-worker-skills-runtime.js";
|
|
5
|
-
export const WORKER_SKILL_LINT_ASSET = "core/scripts/lint-shared-worker-skills.sh";
|
|
6
5
|
const DEFAULT_ROOTS = ["core/workers", "core/packages"];
|
|
7
6
|
const USAGE = "Usage: lint-shared-worker-skills.sh [root ...]\nDefault roots: core/workers core/packages\n";
|
|
8
7
|
function joinFindPath(parent, child) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Native port of share-worker-skill.sh. */
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { BUNDLED_ASSET_DIR, packageRoot } 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";
|
|
@@ -88,7 +88,7 @@ function forceSymlink(target, filePath, fileSystem) {
|
|
|
88
88
|
fileSystem.symlinkSync(target, filePath);
|
|
89
89
|
}
|
|
90
90
|
function runtimeScriptPath(dependencies) {
|
|
91
|
-
return dependencies.scriptPath ??
|
|
91
|
+
return dependencies.scriptPath ?? path.join(packageRoot(), BUNDLED_ASSET_DIR, WORKER_SKILL_SHARE_ASSET);
|
|
92
92
|
}
|
|
93
93
|
export function runWorkerSkillShare(argv, dependencies) {
|
|
94
94
|
let apply = false;
|
|
@@ -3,6 +3,6 @@ export type WorkerSkillCommandContext = {
|
|
|
3
3
|
cwd: string;
|
|
4
4
|
hqRoot?: string;
|
|
5
5
|
};
|
|
6
|
-
/**
|
|
6
|
+
/** Run the native implementation for the worker-skills family. */
|
|
7
7
|
export declare function runWorkerSkillCommand(name: WorkerSkillCommandName, args: string[], context: WorkerSkillCommandContext): Promise<number>;
|
|
8
8
|
//# sourceMappingURL=core-worker-skills-command.d.ts.map
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { runWorkerSkillShare, WORKER_SKILL_SHARE_ASSET } from "./core-worker-share-native.js";
|
|
5
|
-
const assets = {
|
|
6
|
-
lint: WORKER_SKILL_LINT_ASSET,
|
|
7
|
-
share: WORKER_SKILL_SHARE_ASSET,
|
|
8
|
-
};
|
|
9
|
-
/** Keep the shell rollback route byte-transparent while the family flag is off. */
|
|
1
|
+
import { runWorkerSkillLint } from "./core-worker-lint-native.js";
|
|
2
|
+
import { runWorkerSkillShare } from "./core-worker-share-native.js";
|
|
3
|
+
/** Run the native implementation for the worker-skills family. */
|
|
10
4
|
export async function runWorkerSkillCommand(name, args, context) {
|
|
11
|
-
if (!(await coreNativeWorkerSkillsEnabled())) {
|
|
12
|
-
const result = runBundledScript({
|
|
13
|
-
asset: assets[name],
|
|
14
|
-
args,
|
|
15
|
-
cwd: context.cwd,
|
|
16
|
-
hqRoot: context.hqRoot,
|
|
17
|
-
});
|
|
18
|
-
if (result.code !== 0)
|
|
19
|
-
process.exit(result.code);
|
|
20
|
-
return result.code;
|
|
21
|
-
}
|
|
22
5
|
const result = name === "lint"
|
|
23
6
|
? runWorkerSkillLint(args, { cwd: context.cwd })
|
|
24
7
|
: runWorkerSkillShare(args, { cwd: context.cwd });
|
package/dist/commands/core.d.ts
CHANGED
|
@@ -75,9 +75,13 @@ export type RebuildIndexTarget = {
|
|
|
75
75
|
/** One-line summary, shown in `hq core --help`. */
|
|
76
76
|
summary: string;
|
|
77
77
|
};
|
|
78
|
-
export type WorkerSubcommand =
|
|
78
|
+
export type WorkerSubcommand = {
|
|
79
79
|
/** Subcommand name under the `worker` group — `hq core worker <name>`. */
|
|
80
80
|
name: string;
|
|
81
|
+
/** Caller-relative root used by the native command. */
|
|
82
|
+
root: "cwd";
|
|
83
|
+
/** One-line summary, shown in `hq core --help`. */
|
|
84
|
+
summary: string;
|
|
81
85
|
};
|
|
82
86
|
/**
|
|
83
87
|
* Native commands that still claim a scaffold forwarder asset. The forwarder
|
package/dist/commands/core.js
CHANGED
|
@@ -51,6 +51,7 @@ import { DERIVE_TRIGGER_FACTS_ASSET } from "./core-derive-trigger-facts-native.j
|
|
|
51
51
|
import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.js";
|
|
52
52
|
import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
|
|
53
53
|
import { registerCorePipelineWorkerTableCommand } from "./core-pipeline-workers-command.js";
|
|
54
|
+
import { registerCorePipelineRuntimeCommands } from "./core-pipeline-conductor-command.js";
|
|
54
55
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
55
56
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
56
57
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -132,13 +133,11 @@ export const NATIVE_SCAFFOLD_COMMANDS = [
|
|
|
132
133
|
export const WORKER_SUBCOMMANDS = [
|
|
133
134
|
{
|
|
134
135
|
name: "lint",
|
|
135
|
-
asset: "core/scripts/lint-shared-worker-skills.sh",
|
|
136
136
|
root: "cwd",
|
|
137
137
|
summary: "Fail on worker-scoped skills duplicated by copy instead of single-sourced",
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
name: "share",
|
|
141
|
-
asset: "core/scripts/share-worker-skill.sh",
|
|
142
141
|
root: "cwd",
|
|
143
142
|
summary: "Migrate duplicated worker skills onto one canonical file + relative symlinks",
|
|
144
143
|
},
|
|
@@ -307,6 +306,10 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
307
306
|
{ asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
|
|
308
307
|
{ asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
|
|
309
308
|
{ asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
|
|
309
|
+
// Pipeline rollback companions remain bundled until the family removal PR.
|
|
310
|
+
{ asset: "core/scripts/pipeline-conductor.sh", root: "live", summary: "Rollback shell for native pipeline conductor" },
|
|
311
|
+
{ asset: "core/scripts/pipeline-driver.sh", root: "live", summary: "Rollback shell for native pipeline driver" },
|
|
312
|
+
{ asset: "core/scripts/pipeline-envelope.sh", root: "live", summary: "Validation helper for the pipeline shell rollback companions" },
|
|
310
313
|
{ asset: "core/scripts/lib/portable.sh", root: "live", summary: "Shared portable helpers sourced by bundled scripts" },
|
|
311
314
|
...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
|
|
312
315
|
asset,
|
|
@@ -324,7 +327,6 @@ export const SCAFFOLD_ASSETS = [
|
|
|
324
327
|
...SCAFFOLD_COMMANDS,
|
|
325
328
|
...NATIVE_SCAFFOLD_COMMANDS,
|
|
326
329
|
...SCAFFOLD_ONLY_ASSETS,
|
|
327
|
-
...WORKER_SUBCOMMANDS,
|
|
328
330
|
];
|
|
329
331
|
/**
|
|
330
332
|
* Resolve the tree an entry runs against, and the cwd to run it in.
|
|
@@ -404,6 +406,7 @@ export function registerCoreCommands(program) {
|
|
|
404
406
|
registerCoreDeriveTriggerFactsCommand(core);
|
|
405
407
|
registerCoreHookStateHelpers(core);
|
|
406
408
|
registerCoreHookBatchCommand(core);
|
|
409
|
+
registerCorePipelineRuntimeCommands(core);
|
|
407
410
|
const sessionHelperCommands = [
|
|
408
411
|
{
|
|
409
412
|
name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
|
|
@@ -555,7 +558,7 @@ export function registerCoreCommands(program) {
|
|
|
555
558
|
const command = worker
|
|
556
559
|
.command(entry.name)
|
|
557
560
|
.description(entry.summary)
|
|
558
|
-
// Pure passthrough: the
|
|
561
|
+
// Pure passthrough: the native handler owns the worker command grammar.
|
|
559
562
|
.allowUnknownOption()
|
|
560
563
|
.allowExcessArguments()
|
|
561
564
|
.helpOption(false)
|
|
@@ -565,11 +568,7 @@ export function registerCoreCommands(program) {
|
|
|
565
568
|
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
566
569
|
await runWorkerSkillCommand(entry.name, operands, resolveEntryRoot(entry, scope));
|
|
567
570
|
});
|
|
568
|
-
describeCoreCommand(command, {
|
|
569
|
-
execution: interpreterFor(entry.asset) === "node" ? "bundled-node" : "bundled-shell",
|
|
570
|
-
asset: entry.asset,
|
|
571
|
-
root: entry.root,
|
|
572
|
-
});
|
|
571
|
+
describeCoreCommand(command, { execution: "native", root: entry.root });
|
|
573
572
|
}
|
|
574
573
|
const runCommand = (entry, args = [], cmd, target) => {
|
|
575
574
|
const scope = core.opts();
|
package/dist/commands/dm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
-
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
4
|
+
import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
@@ -830,12 +830,20 @@ async function runDmForward(eventId, opts) {
|
|
|
830
830
|
console.error(chalk.red("--to accepts one recipient or channel, not a group recipient list."));
|
|
831
831
|
process.exit(1);
|
|
832
832
|
}
|
|
833
|
+
const destinationChannelName = parseChannelName(opts.to);
|
|
834
|
+
if (opts.company !== undefined && destinationChannelName !== null) {
|
|
835
|
+
console.error(chalk.red("--company applies to person or agent destinations; a channel uses its own company."));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
if (opts.company !== undefined && !opts.company.trim()) {
|
|
839
|
+
console.error(chalk.red("--company requires a company slug or cmp_ ID."));
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
833
842
|
const note = opts.noteFile !== undefined
|
|
834
843
|
? readFileSync(opts.noteFile, "utf8")
|
|
835
844
|
: opts.note;
|
|
836
845
|
const token = await ensureCognitoToken();
|
|
837
846
|
const source = await resolveForwardSourceConversation(token, opts.from);
|
|
838
|
-
const destinationChannelName = parseChannelName(opts.to);
|
|
839
847
|
const fileAccess = opts.grantFiles ? "grant" : opts.omitFiles ? "omit" : undefined;
|
|
840
848
|
const shared = {
|
|
841
849
|
...(note !== undefined ? { body: note } : {}),
|
|
@@ -869,8 +877,19 @@ async function runDmForward(eventId, opts) {
|
|
|
869
877
|
console.error(chalk.red("--to must be a person email, person/agent UID, or channel name/ID."));
|
|
870
878
|
process.exit(1);
|
|
871
879
|
}
|
|
880
|
+
// The server runs the destination-side cross-company check only when the
|
|
881
|
+
// request names the destination company; without it the destination is
|
|
882
|
+
// treated as personal. Not defaulted from the source, which would make a
|
|
883
|
+
// forward into another company look same-company.
|
|
884
|
+
const destinationCompanyUid = opts.company !== undefined
|
|
885
|
+
? await resolveDestinationCompanyUid(token, opts.company.trim())
|
|
886
|
+
: undefined;
|
|
872
887
|
path = "/v1/notify/dm";
|
|
873
|
-
payload = {
|
|
888
|
+
payload = {
|
|
889
|
+
...recipient,
|
|
890
|
+
...shared,
|
|
891
|
+
...(destinationCompanyUid ? { companyUid: destinationCompanyUid } : {}),
|
|
892
|
+
};
|
|
874
893
|
destinationLabel = opts.to;
|
|
875
894
|
}
|
|
876
895
|
const res = await vaultApiFetch({ token, path, method: "POST", body: payload });
|
|
@@ -1529,6 +1548,7 @@ export function registerDmCommand(program) {
|
|
|
1529
1548
|
.option("--grant-files", "Grant the recipient access to files they cannot currently open")
|
|
1530
1549
|
.option("--omit-files", "Omit files the recipient cannot currently open")
|
|
1531
1550
|
.option("--cross-company", "Confirm forwarding across company boundaries")
|
|
1551
|
+
.option("--company <slug|cmp_id>", "Company the person or agent destination belongs to, so cross-company rules apply")
|
|
1532
1552
|
.action(async (eventId, opts) => {
|
|
1533
1553
|
await runDmForward(eventId, opts);
|
|
1534
1554
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live HQ Storage gate and refusal surface for `hq storage`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Registry gate, fails closed. The `hq-storage` registry row decides who has
|
|
5
|
+
* Storage; it defaults on for every signed-in account (widened 2026-10-13
|
|
6
|
+
* from the 2026-10-11 Indigo-only start; see the storage-feature-flag module
|
|
7
|
+
* in src/lib). The fail-closed client fallback is the deliberate exception to the default-on kill-switch rule: any error,
|
|
7
8
|
* a signed-out caller, an API-key caller, a membership read failure, or an
|
|
8
9
|
* unreadable registry all mean Storage is OFF.
|
|
9
10
|
*
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live HQ Storage gate and refusal surface for `hq storage`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Registry gate, fails closed. The `hq-storage` registry row decides who has
|
|
5
|
+
* Storage; it defaults on for every signed-in account (widened 2026-10-13
|
|
6
|
+
* from the 2026-10-11 Indigo-only start; see the storage-feature-flag module
|
|
7
|
+
* in src/lib). The fail-closed client fallback is the deliberate exception to the default-on kill-switch rule: any error,
|
|
7
8
|
* a signed-out caller, an API-key caller, a membership read failure, or an
|
|
8
9
|
* unreadable registry all mean Storage is OFF.
|
|
9
10
|
*
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -278,8 +278,6 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
278
278
|
modulePath: "./commands/core-worker-lint-fast.js",
|
|
279
279
|
exportName: "runWorkerSkillLintFast",
|
|
280
280
|
flagKey: "hq-core-native-fast-route",
|
|
281
|
-
familyFlagKey: "core-native-worker-skills",
|
|
282
|
-
asset: "core/scripts/lint-shared-worker-skills.sh",
|
|
283
281
|
root: "cwd",
|
|
284
282
|
shellCwd: "caller",
|
|
285
283
|
shellHqRoot: "none",
|