@indigoai-us/hq-cli 5.345.118 → 5.345.119
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 +10 -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/dist/command-catalog.generated.d.ts +34 -0
- package/dist/command-catalog.generated.js +40 -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-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/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/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`;
|
|
@@ -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();
|
|
@@ -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",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HQ Storage
|
|
2
|
+
* HQ Storage gate (hq-flags key `hq-storage`).
|
|
3
3
|
*
|
|
4
4
|
* Owner decision (Corey, 2026-10-11): "let's fully ship the storage feature
|
|
5
|
-
* but all behind a feature flag for indigo users only."
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* but all behind a feature flag for indigo users only." The registry row
|
|
6
|
+
* started as `defaultValue: false` with a company override for Indigo. It
|
|
7
|
+
* was widened on 2026-10-13: the row now defaults to true, so Storage is on
|
|
8
|
+
* for every signed-in account and the Indigo override is redundant. The
|
|
9
|
+
* client still FAILS CLOSED: a missing, stale, discovered, archived or
|
|
10
|
+
* unreadable value means Storage is off.
|
|
11
11
|
*
|
|
12
12
|
* Who it is on for is decided by hq-pro's shared evaluator (person override,
|
|
13
13
|
* then company override by uid, then slug, then default), resolved
|
package/package.json
CHANGED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# lint-shared-worker-skills.sh — fail if a worker-scoped skill is duplicated by
|
|
3
|
-
# copy instead of being single-sourced through the shared-skills convention.
|
|
4
|
-
#
|
|
5
|
-
# THE DRIFT PROBLEM this guards against: a skill used by several workers used to
|
|
6
|
-
# be physically COPIED into each worker's skills/ directory. Editing one copy
|
|
7
|
-
# left the others stale, so the copies drifted apart over time (e.g. a shared
|
|
8
|
-
# e2e-testing skill splitting into two divergent versions). The fix is to keep
|
|
9
|
-
# ONE canonical file under a scope-appropriate `_shared-skills/` store and point
|
|
10
|
-
# each sharing worker's skill entry at it with a relative symlink — see
|
|
11
|
-
# core/knowledge/public/hq-core/shared-worker-skills.md.
|
|
12
|
-
#
|
|
13
|
-
# This linter makes that convention enforceable. It flags two failure shapes:
|
|
14
|
-
#
|
|
15
|
-
# DUPLICATE — two or more NON-symlink skill files, WITHIN A SINGLE OWNERSHIP
|
|
16
|
-
# DOMAIN, whose byte content is identical. Identical bytes across
|
|
17
|
-
# two real files in the same domain is exactly an un-single-sourced
|
|
18
|
-
# copy: the moment someone edits one, they drift. (Symlinks that
|
|
19
|
-
# resolve to a shared canonical are single-sourced and are NOT
|
|
20
|
-
# flagged, even though their resolved content matches.)
|
|
21
|
-
# BROKEN — a skill entry that is a symlink whose target does not resolve.
|
|
22
|
-
#
|
|
23
|
-
# It deliberately does NOT flag two same-NAMED skills whose content differs
|
|
24
|
-
# (e.g. an API-level vs a browser-level e2e skill): distinct content means they
|
|
25
|
-
# are distinct skills that merely share a filename, not a drifted share.
|
|
26
|
-
#
|
|
27
|
-
# OWNERSHIP DOMAINS — filesystem adjacency does NOT establish duplicate
|
|
28
|
-
# ownership, so comparison is partitioned by the tree that OWNS a skill (policy
|
|
29
|
-
# hq-lint-comparison-domains-must-match-ownership):
|
|
30
|
-
#
|
|
31
|
-
# - `core/workers` — the curated first-party worker library
|
|
32
|
-
# - `core/packages/<pack>` — one installed pack (self-contained; may legally
|
|
33
|
-
# vendor a byte-identical copy of a core worker)
|
|
34
|
-
# - `companies/<co>` — one tenant's workers
|
|
35
|
-
#
|
|
36
|
-
# A byte-identical file in TWO DIFFERENT domains (a pack vendoring a core skill,
|
|
37
|
-
# or two packs shipping the same skill) is intentional distribution, NOT drift,
|
|
38
|
-
# and is never flagged. Excluded from the scan entirely: dot-directories
|
|
39
|
-
# (`.archive/`, `.git/`, …) and `._*` AppleDouble OS-metadata files — byte-
|
|
40
|
-
# identical by design, and not skills.
|
|
41
|
-
#
|
|
42
|
-
# Usage: lint-shared-worker-skills.sh [root ...]
|
|
43
|
-
# Default roots: core/workers core/packages (the SHIPPED hq-core scope —
|
|
44
|
-
# company workers live under companies/<co>/ and are linted per-tenant; pass
|
|
45
|
-
# them explicitly to include them).
|
|
46
|
-
#
|
|
47
|
-
# Exit 0 + "OK:" line when clean; exit 1 + a report naming every offending group
|
|
48
|
-
# when not. Matches the loud-and-specific style of lint-skill-script-refs.sh.
|
|
49
|
-
|
|
50
|
-
set -euo pipefail
|
|
51
|
-
|
|
52
|
-
if [[ ${1-} == "--help" || ${1-} == "-h" ]]; then
|
|
53
|
-
printf '%s\n' \
|
|
54
|
-
'Usage: lint-shared-worker-skills.sh [root ...]' \
|
|
55
|
-
'Default roots: core/workers core/packages'
|
|
56
|
-
exit 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
60
|
-
cd "$repo_root"
|
|
61
|
-
|
|
62
|
-
roots=("$@")
|
|
63
|
-
if [[ ${#roots[@]} -gt 0 ]]; then
|
|
64
|
-
for root in "${roots[@]}"; do
|
|
65
|
-
if [[ ! -d "$root" ]]; then
|
|
66
|
-
printf 'lint-shared-worker-skills: root is not a directory: %s\n' "$root" >&2
|
|
67
|
-
exit 2
|
|
68
|
-
fi
|
|
69
|
-
done
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
if [[ ${#roots[@]} -eq 0 ]]; then
|
|
73
|
-
roots=(core/workers core/packages)
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
# Content hash of a file, portable across Linux and macOS (neither guaranteed to
|
|
77
|
-
# ship the other's tool): md5sum (GNU) → md5 (BSD/macOS) → shasum (Perl, both).
|
|
78
|
-
content_hash() {
|
|
79
|
-
if command -v md5sum >/dev/null 2>&1; then
|
|
80
|
-
md5sum "$1" | cut -d' ' -f1
|
|
81
|
-
elif command -v md5 >/dev/null 2>&1; then
|
|
82
|
-
md5 -q "$1"
|
|
83
|
-
else
|
|
84
|
-
shasum "$1" | cut -d' ' -f1
|
|
85
|
-
fi
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# The ownership domain that OWNS a skill path — the boundary within which two
|
|
89
|
-
# byte-identical files are a real duplicate. Across domains, identical content
|
|
90
|
-
# is intentional (a pack vendoring a core skill, two packs shipping the same
|
|
91
|
-
# skill), so it must never be compared. See policy header above.
|
|
92
|
-
domain_of() {
|
|
93
|
-
local p="${1#./}" rest
|
|
94
|
-
case "$p" in
|
|
95
|
-
core/packages/*) rest="${p#core/packages/}"; printf 'core/packages/%s' "${rest%%/*}" ;;
|
|
96
|
-
companies/*) rest="${p#companies/}"; printf 'companies/%s' "${rest%%/*}" ;;
|
|
97
|
-
core/workers/*|core/workers) printf 'core/workers' ;;
|
|
98
|
-
# Arbitrary root (custom scans, tests): the top path segment is the domain.
|
|
99
|
-
*) printf '%s' "${p%%/*}" ;;
|
|
100
|
-
esac
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# Collect skill entries: regular files AND symlinks named *.md under any
|
|
104
|
-
# .../skills/... path. -type l must be matched explicitly — a symlink is not a
|
|
105
|
-
# -type f, so a skills symlink would otherwise be invisible to the scan.
|
|
106
|
-
# EXCLUDED: any dot-directory component (`*/.*/*` — covers `.archive/`, `.git/`)
|
|
107
|
-
# and `._*` AppleDouble files; both are byte-identical by design and not skills.
|
|
108
|
-
mapfile -t entries < <(
|
|
109
|
-
find "${roots[@]}" \( -type f -o -type l \) -path '*/skills/*.md' \
|
|
110
|
-
-not -path '*/.*/*' -not -name '._*' 2>/dev/null | sort
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Empty is clean — and guards `"${entries[@]}"` under `set -u` on bash 3.2
|
|
114
|
-
# (macOS), which errors on an empty array expansion.
|
|
115
|
-
if [[ ${#entries[@]} -eq 0 ]]; then
|
|
116
|
-
echo "OK: worker-scoped skills are single-sourced (0 skill entries under: ${roots[*]})"
|
|
117
|
-
exit 0
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
broken=()
|
|
121
|
-
# Parallel arrays keyed by "<domain>|<content-hash>": hash_keys[i] is that
|
|
122
|
-
# composite key, hash_regfiles[i] a newline-joined list of the NON-symlink files
|
|
123
|
-
# carrying it. Keying on domain AS WELL AS hash is what confines a duplicate
|
|
124
|
-
# finding to a single ownership tree — two identical files in different domains
|
|
125
|
-
# land in different buckets and are never flagged. Bash 3.2 (macOS) has no
|
|
126
|
-
# portable associative arrays, so a linear scan over parallel arrays keeps this
|
|
127
|
-
# runnable everywhere HQ runs.
|
|
128
|
-
hash_keys=()
|
|
129
|
-
hash_regfiles=()
|
|
130
|
-
|
|
131
|
-
hash_index() { # echo the index of $1 in hash_keys, or -1
|
|
132
|
-
local want="$1" i
|
|
133
|
-
for i in "${!hash_keys[@]}"; do
|
|
134
|
-
if [[ "${hash_keys[$i]}" == "$want" ]]; then
|
|
135
|
-
echo "$i"; return 0
|
|
136
|
-
fi
|
|
137
|
-
done
|
|
138
|
-
echo "-1"
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
for entry in "${entries[@]}"; do
|
|
142
|
-
if [[ -L "$entry" ]]; then
|
|
143
|
-
# Symlink: single-sourced by design. Only a DANGLING one is a problem.
|
|
144
|
-
if [[ ! -e "$entry" ]]; then
|
|
145
|
-
broken+=("$entry")
|
|
146
|
-
fi
|
|
147
|
-
continue
|
|
148
|
-
fi
|
|
149
|
-
# Regular file: bucket it by (ownership domain, byte content). Two regular
|
|
150
|
-
# files sharing a bucket are two copies of the same skill in the same tree —
|
|
151
|
-
# the drift hazard.
|
|
152
|
-
key="$(domain_of "$entry")|$(content_hash "$entry")"
|
|
153
|
-
idx="$(hash_index "$key")"
|
|
154
|
-
if [[ "$idx" == "-1" ]]; then
|
|
155
|
-
hash_keys+=("$key")
|
|
156
|
-
hash_regfiles+=("$entry")
|
|
157
|
-
else
|
|
158
|
-
hash_regfiles[$idx]="${hash_regfiles[$idx]}"$'\n'"$entry"
|
|
159
|
-
fi
|
|
160
|
-
done
|
|
161
|
-
|
|
162
|
-
findings=0
|
|
163
|
-
|
|
164
|
-
for i in "${!hash_keys[@]}"; do
|
|
165
|
-
group="${hash_regfiles[$i]}"
|
|
166
|
-
count="$(printf '%s\n' "$group" | grep -c .)"
|
|
167
|
-
if [[ "$count" -ge 2 ]]; then
|
|
168
|
-
if [[ $findings -eq 0 ]]; then
|
|
169
|
-
echo "lint-shared-worker-skills: FAIL — duplicated worker skills (single-source these via _shared-skills/ + relative symlinks):" >&2
|
|
170
|
-
fi
|
|
171
|
-
findings=$((findings + 1))
|
|
172
|
-
domain="${hash_keys[$i]%%|*}"
|
|
173
|
-
echo " DUPLICATE in domain '${domain}' (identical content, ${count} copies — pick one canonical and symlink the rest):" >&2
|
|
174
|
-
printf ' %s\n' "$group" >&2
|
|
175
|
-
fi
|
|
176
|
-
done
|
|
177
|
-
|
|
178
|
-
if [[ ${#broken[@]} -gt 0 ]]; then
|
|
179
|
-
if [[ $findings -eq 0 ]]; then
|
|
180
|
-
echo "lint-shared-worker-skills: FAIL — broken shared-skill symlink(s):" >&2
|
|
181
|
-
fi
|
|
182
|
-
findings=$((findings + ${#broken[@]}))
|
|
183
|
-
echo " BROKEN (symlink target does not resolve):" >&2
|
|
184
|
-
printf ' %s\n' "${broken[@]}" >&2
|
|
185
|
-
fi
|
|
186
|
-
|
|
187
|
-
if [[ $findings -gt 0 ]]; then
|
|
188
|
-
echo "" >&2
|
|
189
|
-
echo " Fix: keep ONE canonical file in the narrowest-scope _shared-skills/ store" >&2
|
|
190
|
-
echo " (core/workers/_shared-skills, core/packages/<pack>/workers/_shared-skills," >&2
|
|
191
|
-
echo " or companies/<co>/workers/_shared-skills) and replace each duplicate with a" >&2
|
|
192
|
-
echo " relative symlink. Helper: hq core worker share" >&2
|
|
193
|
-
echo " Doc: core/knowledge/public/hq-core/shared-worker-skills.md" >&2
|
|
194
|
-
exit 1
|
|
195
|
-
fi
|
|
196
|
-
|
|
197
|
-
echo "OK: worker-scoped skills are single-sourced (${#entries[@]} skill entries scanned under: ${roots[*]})"
|