@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/scaffold/core/scripts/pipeline-conductor.sh +2671 -0
  3. package/assets/scaffold/core/scripts/pipeline-driver.sh +820 -0
  4. package/assets/scaffold/core/scripts/pipeline-envelope.sh +211 -0
  5. package/dist/command-catalog.generated.d.ts +34 -0
  6. package/dist/command-catalog.generated.js +40 -0
  7. package/dist/commands/core-checkpoint-native.d.ts +17 -3
  8. package/dist/commands/core-checkpoint-native.js +48 -8
  9. package/dist/commands/core-derive-trigger-facts-native.js +15 -7
  10. package/dist/commands/core-migrate-policy-triggers-native.js +3 -1
  11. package/dist/commands/core-pipeline-conductor-command.d.ts +5 -0
  12. package/dist/commands/core-pipeline-conductor-command.js +138 -0
  13. package/dist/commands/core-pipeline-conductor-native.d.ts +7 -0
  14. package/dist/commands/core-pipeline-conductor-native.js +2950 -0
  15. package/dist/commands/core-pipeline-driver-native.d.ts +24 -0
  16. package/dist/commands/core-pipeline-driver-native.js +843 -0
  17. package/dist/commands/core-pipeline-driver-process.d.ts +4 -0
  18. package/dist/commands/core-pipeline-driver-process.js +68 -0
  19. package/dist/commands/core-pipeline-envelope-native.d.ts +4 -0
  20. package/dist/commands/core-pipeline-envelope-native.js +87 -0
  21. package/dist/commands/core-policy.js +3 -0
  22. package/dist/commands/core-worker-lint-native.d.ts +0 -1
  23. package/dist/commands/core-worker-lint-native.js +0 -1
  24. package/dist/commands/core-worker-share-native.js +2 -2
  25. package/dist/commands/core-worker-skills-command.d.ts +1 -1
  26. package/dist/commands/core-worker-skills-command.js +3 -20
  27. package/dist/commands/core.d.ts +5 -1
  28. package/dist/commands/core.js +8 -9
  29. package/dist/commands/storage-gate.d.ts +4 -3
  30. package/dist/commands/storage-gate.js +4 -3
  31. package/dist/fast-scaffold.js +0 -2
  32. package/dist/lib/storage-feature-flag.d.ts +7 -7
  33. package/package.json +1 -1
  34. package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +0 -197
  35. package/assets/scaffold/core/scripts/share-worker-skill.sh +0 -178
  36. package/dist/commands/core-native-worker-skills-flag.d.ts +0 -4
  37. 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,4 @@
1
+ type EnvelopeKind = "envelope" | "handoff";
2
+ export declare function validatePipelinePhaseFile(filePath: string, kind: EnvelopeKind): [boolean, string];
3
+ export {};
4
+ //# sourceMappingURL=core-pipeline-envelope-native.d.ts.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 { resolveBundledAsset } from "../utils/hq-roots.js";
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 ?? resolveBundledAsset(WORKER_SKILL_SHARE_ASSET);
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
- /** Keep the shell rollback route byte-transparent while the family flag is off. */
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 { runBundledScript } from "../utils/run-bundled-script.js";
2
- import { coreNativeWorkerSkillsEnabled } from "./core-native-worker-skills-flag.js";
3
- import { runWorkerSkillLint, WORKER_SKILL_LINT_ASSET } from "./core-worker-lint-native.js";
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 });
@@ -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 = ScaffoldAsset & {
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
@@ -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 wrapped script owns its own argument grammar.
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
- * Allowlist gate, fails closed (owner decision 2026-10-11, Indigo-only until
5
- * the owner widens it; see the storage-feature-flag module in src/lib). This
6
- * is the deliberate exception to the default-on kill-switch rule: any error,
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
- * Allowlist gate, fails closed (owner decision 2026-10-11, Indigo-only until
5
- * the owner widens it; see the storage-feature-flag module in src/lib). This
6
- * is the deliberate exception to the default-on kill-switch rule: any error,
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
  *
@@ -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 allowlist gate (hq-flags key `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." That is an explicit
6
- * hold on the default-on kill-switch rule
7
- * (`indigo-feature-flags-default-on-as-fail-safes`): the registry row is
8
- * `defaultValue: false` with a company override for Indigo, so this gate is
9
- * an ALLOWLIST, not a kill switch. It therefore FAILS CLOSED: a missing,
10
- * stale, discovered, archived or unreadable value means Storage is off.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.118",
3
+ "version": "5.345.119",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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[*]})"