@indigoai-us/hq-cli 5.119.9 → 5.119.10

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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.119.10] — 2026-09-18
6
+
7
+ ### Added
8
+
9
+ - `hq skill register` reserves a company skill UID from a proposal-lane
10
+ source and writes the server-stamped bytes only to `--stamped-output`.
11
+ It does not write `companies/<company>/skills/`, create a discovery
12
+ wrapper, or sync. `hq skill promote` copies those exact bytes into the
13
+ canonical skill path only after `--expected-sha256` matches and the
14
+ stamped `skill_uid` matches the server registration. A differing
15
+ canonical file requires `--reviewed-update`. Use `--no-sync` to leave
16
+ upload to a later steward command.
17
+
5
18
  ## [5.119.9] — 2026-09-17
6
19
 
7
20
  ### Added
@@ -1877,7 +1877,7 @@ export declare const COMMAND_CATALOG: readonly [{
1877
1877
  readonly subcommands: readonly [];
1878
1878
  }, {
1879
1879
  readonly name: "skill";
1880
- readonly description: "Create company skills and discuss improvements";
1880
+ readonly description: "Create, stage, promote, and discuss company skills";
1881
1881
  readonly aliases: readonly [];
1882
1882
  readonly hidden: false;
1883
1883
  readonly usage: "[options] [command]";
@@ -1914,6 +1914,59 @@ export declare const COMMAND_CATALOG: readonly [{
1914
1914
  readonly description: "Refresh local skill discovery without registration or sync";
1915
1915
  }];
1916
1916
  readonly subcommands: readonly [];
1917
+ }, {
1918
+ readonly name: "register";
1919
+ readonly description: "Register a company skill to a proposal-lane stamped file without writing the canonical tree";
1920
+ readonly aliases: readonly [];
1921
+ readonly hidden: false;
1922
+ readonly usage: "[options] <slug>";
1923
+ readonly arguments: readonly [{
1924
+ readonly name: "slug";
1925
+ readonly required: true;
1926
+ readonly variadic: false;
1927
+ }];
1928
+ readonly options: readonly [{
1929
+ readonly flags: "--source <path>";
1930
+ readonly description: "Caller-authored SKILL.md (or its directory) to send to the server validator";
1931
+ }, {
1932
+ readonly flags: "--stamped-output <path>";
1933
+ readonly description: "Write the complete server-stamped bytes here, outside companies/<company>/skills/";
1934
+ }, {
1935
+ readonly flags: "--no-canonical-write";
1936
+ readonly description: "Refuse to write companies/<company>/skills/ (always the register behavior)";
1937
+ }, {
1938
+ readonly flags: "--no-surface";
1939
+ readonly description: "Do not create a discovery wrapper (always the register behavior)";
1940
+ }, {
1941
+ readonly flags: "--no-sync";
1942
+ readonly description: "Do not upload the skill file (always the register behavior)";
1943
+ }];
1944
+ readonly subcommands: readonly [];
1945
+ }, {
1946
+ readonly name: "promote";
1947
+ readonly description: "Copy independently-cleared stamped bytes into the canonical skill path after SHA-256 and skill_uid checks";
1948
+ readonly aliases: readonly [];
1949
+ readonly hidden: false;
1950
+ readonly usage: "[options] <slug>";
1951
+ readonly arguments: readonly [{
1952
+ readonly name: "slug";
1953
+ readonly required: true;
1954
+ readonly variadic: false;
1955
+ }];
1956
+ readonly options: readonly [{
1957
+ readonly flags: "--from <path>";
1958
+ readonly description: "Independently-cleared stamped SKILL.md (or its directory)";
1959
+ }, {
1960
+ readonly flags: "--expected-sha256 <hex>";
1961
+ readonly description: "SHA-256 of the exact stamped file bytes that were cleared";
1962
+ }, {
1963
+ readonly flags: "--reviewed-update";
1964
+ readonly description: "Replace an existing canonical SKILL.md whose bytes differ from the stamped file";
1965
+ }, {
1966
+ readonly flags: "--no-sync";
1967
+ readonly description: "Promote locally without uploading the canonical file";
1968
+ }];
1969
+ readonly subcommands: readonly [];
1917
1970
  }, {
1918
1971
  readonly name: "propose";
1919
1972
  readonly description: "Post a comment-only improvement for a skill";
@@ -2403,7 +2403,7 @@ export const COMMAND_CATALOG = [
2403
2403
  },
2404
2404
  {
2405
2405
  "name": "skill",
2406
- "description": "Create company skills and discuss improvements",
2406
+ "description": "Create, stage, promote, and discuss company skills",
2407
2407
  "aliases": [],
2408
2408
  "hidden": false,
2409
2409
  "usage": "[options] [command]",
@@ -2452,6 +2452,76 @@ export const COMMAND_CATALOG = [
2452
2452
  ],
2453
2453
  "subcommands": []
2454
2454
  },
2455
+ {
2456
+ "name": "register",
2457
+ "description": "Register a company skill to a proposal-lane stamped file without writing the canonical tree",
2458
+ "aliases": [],
2459
+ "hidden": false,
2460
+ "usage": "[options] <slug>",
2461
+ "arguments": [
2462
+ {
2463
+ "name": "slug",
2464
+ "required": true,
2465
+ "variadic": false
2466
+ }
2467
+ ],
2468
+ "options": [
2469
+ {
2470
+ "flags": "--source <path>",
2471
+ "description": "Caller-authored SKILL.md (or its directory) to send to the server validator"
2472
+ },
2473
+ {
2474
+ "flags": "--stamped-output <path>",
2475
+ "description": "Write the complete server-stamped bytes here, outside companies/<company>/skills/"
2476
+ },
2477
+ {
2478
+ "flags": "--no-canonical-write",
2479
+ "description": "Refuse to write companies/<company>/skills/ (always the register behavior)"
2480
+ },
2481
+ {
2482
+ "flags": "--no-surface",
2483
+ "description": "Do not create a discovery wrapper (always the register behavior)"
2484
+ },
2485
+ {
2486
+ "flags": "--no-sync",
2487
+ "description": "Do not upload the skill file (always the register behavior)"
2488
+ }
2489
+ ],
2490
+ "subcommands": []
2491
+ },
2492
+ {
2493
+ "name": "promote",
2494
+ "description": "Copy independently-cleared stamped bytes into the canonical skill path after SHA-256 and skill_uid checks",
2495
+ "aliases": [],
2496
+ "hidden": false,
2497
+ "usage": "[options] <slug>",
2498
+ "arguments": [
2499
+ {
2500
+ "name": "slug",
2501
+ "required": true,
2502
+ "variadic": false
2503
+ }
2504
+ ],
2505
+ "options": [
2506
+ {
2507
+ "flags": "--from <path>",
2508
+ "description": "Independently-cleared stamped SKILL.md (or its directory)"
2509
+ },
2510
+ {
2511
+ "flags": "--expected-sha256 <hex>",
2512
+ "description": "SHA-256 of the exact stamped file bytes that were cleared"
2513
+ },
2514
+ {
2515
+ "flags": "--reviewed-update",
2516
+ "description": "Replace an existing canonical SKILL.md whose bytes differ from the stamped file"
2517
+ },
2518
+ {
2519
+ "flags": "--no-sync",
2520
+ "description": "Promote locally without uploading the canonical file"
2521
+ }
2522
+ ],
2523
+ "subcommands": []
2524
+ },
2455
2525
  {
2456
2526
  "name": "propose",
2457
2527
  "description": "Post a comment-only improvement for a skill",
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * `hq skill create <slug>` registers a canonical company skill, stamps its
5
5
  * immutable UID, surfaces its generated runtime wrapper, and syncs it.
6
+ * `hq skill register <slug>` reserves that UID from a proposal-lane source and
7
+ * writes server-stamped bytes only to `--stamped-output` — never under
8
+ * `companies/<company>/skills/`, and never as a discovery wrapper or sync.
9
+ * `hq skill promote <slug>` copies independently-cleared stamped bytes into the
10
+ * canonical tree only after the expected SHA-256 and registered skill_uid match.
6
11
  * `hq skill propose <uid|path> --message "…"` posts a whole-skill comment to
7
12
  * the same improvement thread shown in HQ Console. It never uploads a modified
8
13
  * SKILL.md and cannot overwrite live content. `hq skill delete <target>` removes
@@ -39,7 +44,21 @@ export declare function makeSkillTemplate(input: {
39
44
  description?: string;
40
45
  }): string;
41
46
  /** Replace SKILL.md without exposing a partially-written identity to agents. */
42
- export declare function writeSkillFileAtomically(filePath: string, content: string): void;
47
+ export declare function writeSkillFileAtomically(filePath: string, content: string | Buffer): void;
48
+ export declare function sha256Hex(data: string | Buffer): string;
49
+ export declare function skillVaultPath(skillSlug: string): string;
50
+ export declare function isInsideCompanySkillsTree(hqRoot: string, candidatePath: string): boolean;
51
+ export declare function resolveSkillMarkdownPath(target: string, cwd: string): string;
52
+ export declare function assertStagedOutputPath(hqRoot: string, outputPath: string): string;
53
+ export declare function assertSha256HexMatch(actualHex: string, expectedHex: string): void;
54
+ export declare function assertRegisteredSkillResponse(registered: {
55
+ skillUid?: unknown;
56
+ content?: unknown;
57
+ }): {
58
+ skillUid: string;
59
+ content: string;
60
+ };
61
+ export declare function normalizeLiveSkillVaultPath(rawPath: string): string;
43
62
  export declare function mapSkillError(status: number, body: Record<string, unknown>): string;
44
63
  /**
45
64
  * Type a failed skills-API response by its HTTP status so the top-level error
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * `hq skill create <slug>` registers a canonical company skill, stamps its
5
5
  * immutable UID, surfaces its generated runtime wrapper, and syncs it.
6
+ * `hq skill register <slug>` reserves that UID from a proposal-lane source and
7
+ * writes server-stamped bytes only to `--stamped-output` — never under
8
+ * `companies/<company>/skills/`, and never as a discovery wrapper or sync.
9
+ * `hq skill promote <slug>` copies independently-cleared stamped bytes into the
10
+ * canonical tree only after the expected SHA-256 and registered skill_uid match.
6
11
  * `hq skill propose <uid|path> --message "…"` posts a whole-skill comment to
7
12
  * the same improvement thread shown in HQ Console. It never uploads a modified
8
13
  * SKILL.md and cannot overwrite live content. `hq skill delete <target>` removes
@@ -11,6 +16,7 @@
11
16
  */
12
17
  import * as fs from "node:fs";
13
18
  import * as path from "node:path";
19
+ import { createHash, timingSafeEqual } from "node:crypto";
14
20
  import chalk from "chalk";
15
21
  import yaml from "js-yaml";
16
22
  import { share } from "@indigoai-us/hq-cloud";
@@ -25,6 +31,7 @@ export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
25
31
  export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
26
32
  const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
27
33
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
34
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
28
35
  async function defaultSyncFile(input) {
29
36
  const result = await share({
30
37
  paths: [input.filePath],
@@ -185,11 +192,19 @@ export function writeSkillFileAtomically(filePath, content) {
185
192
  : 0o644;
186
193
  const tempPath = path.join(dir, `.SKILL.md.${process.pid}.${Date.now()}.tmp`);
187
194
  try {
188
- fs.writeFileSync(tempPath, content, {
189
- encoding: "utf8",
190
- flag: "wx",
191
- mode: existingMode,
192
- });
195
+ if (typeof content === "string") {
196
+ fs.writeFileSync(tempPath, content, {
197
+ encoding: "utf8",
198
+ flag: "wx",
199
+ mode: existingMode,
200
+ });
201
+ }
202
+ else {
203
+ fs.writeFileSync(tempPath, content, {
204
+ flag: "wx",
205
+ mode: existingMode,
206
+ });
207
+ }
193
208
  fs.renameSync(tempPath, filePath);
194
209
  }
195
210
  finally {
@@ -197,6 +212,69 @@ export function writeSkillFileAtomically(filePath, content) {
197
212
  fs.rmSync(tempPath, { force: true });
198
213
  }
199
214
  }
215
+ export function sha256Hex(data) {
216
+ return createHash("sha256").update(data).digest("hex");
217
+ }
218
+ export function skillVaultPath(skillSlug) {
219
+ return `skills/${skillSlug}/SKILL.md`;
220
+ }
221
+ export function isInsideCompanySkillsTree(hqRoot, candidatePath) {
222
+ const companiesRoot = path.resolve(hqRoot, "companies");
223
+ const resolved = path.resolve(candidatePath);
224
+ const rel = path.relative(companiesRoot, resolved);
225
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
226
+ return false;
227
+ const parts = rel.split(path.sep);
228
+ return parts.length >= 2 && parts[1] === "skills";
229
+ }
230
+ export function resolveSkillMarkdownPath(target, cwd) {
231
+ let filePath = path.resolve(cwd, target);
232
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
233
+ filePath = path.join(filePath, "SKILL.md");
234
+ }
235
+ return filePath;
236
+ }
237
+ export function assertStagedOutputPath(hqRoot, outputPath) {
238
+ const resolved = path.resolve(outputPath);
239
+ if (isInsideCompanySkillsTree(hqRoot, resolved)) {
240
+ throw localSkillError(`Stamped output '${resolved}' is inside companies/<company>/skills/. Pass a proposal-lane path outside the canonical skill tree.`);
241
+ }
242
+ return resolved;
243
+ }
244
+ export function assertSha256HexMatch(actualHex, expectedHex) {
245
+ const want = expectedHex.trim().toLowerCase();
246
+ if (!SHA256_HEX_PATTERN.test(want)) {
247
+ throw localSkillError("--expected-sha256 must be a 64-character hex SHA-256 digest.");
248
+ }
249
+ const got = actualHex.trim().toLowerCase();
250
+ if (!SHA256_HEX_PATTERN.test(got)) {
251
+ throw localSkillError(`Stamped file SHA-256 ${got} does not match --expected-sha256. Canonical skill was not written.`);
252
+ }
253
+ if (!timingSafeEqual(Buffer.from(got, "hex"), Buffer.from(want, "hex"))) {
254
+ throw localSkillError(`Stamped file SHA-256 ${got} does not match --expected-sha256. Canonical skill was not written.`);
255
+ }
256
+ }
257
+ export function assertRegisteredSkillResponse(registered) {
258
+ if (typeof registered.skillUid !== "string" ||
259
+ !SKILL_UID_PATTERN.test(registered.skillUid) ||
260
+ typeof registered.content !== "string" ||
261
+ parseSkillUid(registered.content) !== registered.skillUid) {
262
+ throw new Error("The server returned an invalid skill registration response; the local file was not changed.");
263
+ }
264
+ return { skillUid: registered.skillUid, content: registered.content };
265
+ }
266
+ function skillRecordPath(record) {
267
+ for (const value of [record?.vaultPath, record?.sourcePath, record?.path]) {
268
+ if (typeof value === "string" && value.length > 0)
269
+ return value;
270
+ }
271
+ return undefined;
272
+ }
273
+ export function normalizeLiveSkillVaultPath(rawPath) {
274
+ const normalized = rawPath.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
275
+ const match = normalized.match(/(?:^|\/)skills\/([a-z0-9][a-z0-9-]*)(?:\/SKILL\.md)?$/);
276
+ return match ? skillVaultPath(match[1]) : normalized;
277
+ }
200
278
  export function mapSkillError(status, body) {
201
279
  const server = (typeof body.error === "string" && body.error) ||
202
280
  (typeof body.message === "string" && body.message) ||
@@ -360,7 +438,7 @@ export function registerSkillCommand(program, deps = {}) {
360
438
  const resolveCompanyUid = deps.resolveCompanyUid ?? getCompanyUid;
361
439
  const skill = program
362
440
  .command("skill")
363
- .description("Create company skills and discuss improvements")
441
+ .description("Create, stage, promote, and discuss company skills")
364
442
  .option("--company <slug>", "Company slug (defaults to the active company)")
365
443
  .option("--hq-root <path>", "Local HQ root", hqRoot);
366
444
  skill
@@ -420,7 +498,7 @@ export function registerSkillCommand(program, deps = {}) {
420
498
  path: `/v1/files/skills/company/${encodeURIComponent(companySlug)}/register`,
421
499
  method: "POST",
422
500
  body: {
423
- path: `skills/${slug}/SKILL.md`,
501
+ path: skillVaultPath(slug),
424
502
  content: localContent,
425
503
  },
426
504
  });
@@ -430,13 +508,8 @@ export function registerSkillCommand(program, deps = {}) {
430
508
  machineIdentity: isMachineIdentity(),
431
509
  });
432
510
  }
433
- const registered = (await response.json());
434
- if (typeof registered.skillUid !== "string" ||
435
- !SKILL_UID_PATTERN.test(registered.skillUid) ||
436
- typeof registered.content !== "string" ||
437
- parseSkillUid(registered.content) !== registered.skillUid) {
438
- throw new Error("The server returned an invalid skill registration response; the local file was not changed.");
439
- }
511
+ const payload = (await response.json());
512
+ const registered = assertRegisteredSkillResponse(payload);
440
513
  writeSkillFileAtomically(filePath, registered.content);
441
514
  let discoveryStatus = null;
442
515
  try {
@@ -506,11 +579,173 @@ export function registerSkillCommand(program, deps = {}) {
506
579
  console.log(chalk.green(`Skill ready: ${registered.skillUid}`));
507
580
  console.log(` File: ${filePath}`);
508
581
  console.log(` Discovery: ${discoveryStatus === 0 ? "ready" : "needs attention"}`);
509
- console.log(registered.accessPolicy === "open"
582
+ console.log(payload.accessPolicy === "open"
510
583
  ? ` Access: Open — every active ${companySlug} member can edit`
511
584
  : " Access: preserved existing policy");
512
585
  console.log(` Sync: ${opts.sync === false ? "not requested" : "complete"}`);
513
586
  });
587
+ skill
588
+ .command("register <slug>")
589
+ .description("Register a company skill to a proposal-lane stamped file without writing the canonical tree")
590
+ .requiredOption("--source <path>", "Caller-authored SKILL.md (or its directory) to send to the server validator")
591
+ .requiredOption("--stamped-output <path>", "Write the complete server-stamped bytes here, outside companies/<company>/skills/")
592
+ .option("--no-canonical-write", "Refuse to write companies/<company>/skills/ (always the register behavior)")
593
+ .option("--no-surface", "Do not create a discovery wrapper (always the register behavior)")
594
+ .option("--no-sync", "Do not upload the skill file (always the register behavior)")
595
+ .action(async (slug, opts) => {
596
+ if (!SKILL_SLUG_PATTERN.test(slug)) {
597
+ throw localSkillError("Skill slug must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens.");
598
+ }
599
+ const parentOpts = skill.opts();
600
+ const resolvedRoot = path.resolve(parentOpts.hqRoot ?? hqRoot);
601
+ const companySlug = resolveCompanySlug(parentOpts.company, resolvedRoot);
602
+ canonicalCompanySkillPath(resolvedRoot, companySlug, slug);
603
+ const sourcePath = resolveSkillMarkdownPath(opts.source, cwd());
604
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
605
+ throw localSkillError(`No SKILL.md found at '${opts.source}'. Pass --source <file> or a directory that contains SKILL.md.`);
606
+ }
607
+ const stampedOutputPath = assertStagedOutputPath(resolvedRoot, path.resolve(cwd(), opts.stampedOutput));
608
+ const localContent = fs.readFileSync(sourcePath, "utf8");
609
+ const sourceHash = sha256Hex(localContent);
610
+ const token = await ensureToken();
611
+ const response = await apiFetch({
612
+ token,
613
+ path: `/v1/files/skills/company/${encodeURIComponent(companySlug)}/register`,
614
+ method: "POST",
615
+ body: {
616
+ path: skillVaultPath(slug),
617
+ content: localContent,
618
+ },
619
+ });
620
+ if (!response.ok) {
621
+ const body = (await response.json().catch(() => ({})));
622
+ throw skillApiError(response.status, body, {
623
+ machineIdentity: isMachineIdentity(),
624
+ });
625
+ }
626
+ const registered = assertRegisteredSkillResponse((await response.json()));
627
+ writeSkillFileAtomically(stampedOutputPath, registered.content);
628
+ const stampedHash = sha256Hex(registered.content);
629
+ console.log(chalk.green(`Skill registered (staged): ${registered.skillUid}`));
630
+ console.log(` Source: ${sourcePath}`);
631
+ console.log(` Source SHA-256: ${sourceHash}`);
632
+ console.log(` Stamped output: ${stampedOutputPath}`);
633
+ console.log(` Stamped SHA-256: ${stampedHash}`);
634
+ console.log(" Canonical write: not requested");
635
+ console.log(" Discovery: not requested");
636
+ console.log(" Sync: not requested");
637
+ });
638
+ skill
639
+ .command("promote <slug>")
640
+ .description("Copy independently-cleared stamped bytes into the canonical skill path after SHA-256 and skill_uid checks")
641
+ .requiredOption("--from <path>", "Independently-cleared stamped SKILL.md (or its directory)")
642
+ .requiredOption("--expected-sha256 <hex>", "SHA-256 of the exact stamped file bytes that were cleared")
643
+ .option("--reviewed-update", "Replace an existing canonical SKILL.md whose bytes differ from the stamped file")
644
+ .option("--no-sync", "Promote locally without uploading the canonical file")
645
+ .action(async (slug, opts) => {
646
+ if (!SKILL_SLUG_PATTERN.test(slug)) {
647
+ throw localSkillError("Skill slug must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens.");
648
+ }
649
+ const parentOpts = skill.opts();
650
+ const resolvedRoot = path.resolve(parentOpts.hqRoot ?? hqRoot);
651
+ const companySlug = resolveCompanySlug(parentOpts.company, resolvedRoot);
652
+ const canonicalPath = canonicalCompanySkillPath(resolvedRoot, companySlug, slug);
653
+ const fromPath = resolveSkillMarkdownPath(opts.from, cwd());
654
+ if (!fs.existsSync(fromPath) || !fs.statSync(fromPath).isFile()) {
655
+ throw localSkillError(`No stamped SKILL.md found at '${opts.from}'. Pass --from <file> or a directory that contains SKILL.md.`);
656
+ }
657
+ const stampedBytes = fs.readFileSync(fromPath);
658
+ assertSha256HexMatch(sha256Hex(stampedBytes), opts.expectedSha256);
659
+ const stampedContent = stampedBytes.toString("utf8");
660
+ const skillUid = parseSkillUid(stampedContent);
661
+ if (!skillUid) {
662
+ throw localSkillError(`The stamped SKILL.md at '${fromPath}' has no registered skill_uid. Canonical skill was not written.`);
663
+ }
664
+ const token = await ensureToken();
665
+ const companyUid = await resolveCompanyUid(token, companySlug);
666
+ const detail = await apiFetch({
667
+ token,
668
+ path: `/v1/skills/${encodeURIComponent(companyUid)}/${encodeURIComponent(skillUid)}`,
669
+ method: "GET",
670
+ });
671
+ if (!detail.ok) {
672
+ const body = (await detail.json().catch(() => ({})));
673
+ throw skillApiError(detail.status, body, {
674
+ machineIdentity: isMachineIdentity(),
675
+ });
676
+ }
677
+ const { skill: record } = (await detail.json());
678
+ if (typeof record?.skillUid === "string" &&
679
+ record.skillUid !== skillUid) {
680
+ throw localSkillError(`Stamped skill_uid ${skillUid} does not match the server registration. Canonical skill was not written.`);
681
+ }
682
+ const recordPath = skillRecordPath(record);
683
+ if (recordPath &&
684
+ normalizeLiveSkillVaultPath(recordPath) !== skillVaultPath(slug)) {
685
+ throw localSkillError(`Stamped skill_uid ${skillUid} is registered at '${recordPath}', not '${skillVaultPath(slug)}'. Canonical skill was not written.`);
686
+ }
687
+ if (fs.existsSync(canonicalPath) && !fs.statSync(canonicalPath).isFile()) {
688
+ throw localSkillError(`Expected a SKILL.md file at '${canonicalPath}'.`);
689
+ }
690
+ const existing = fs.existsSync(canonicalPath)
691
+ ? fs.readFileSync(canonicalPath)
692
+ : null;
693
+ if (existing && !existing.equals(stampedBytes)) {
694
+ if (opts.reviewedUpdate !== true) {
695
+ throw localSkillError(`Canonical skill '${canonicalPath}' already exists and differs from the stamped file. Re-run with --reviewed-update after that exact change is cleared.`);
696
+ }
697
+ }
698
+ if (!existing || !existing.equals(stampedBytes)) {
699
+ writeSkillFileAtomically(canonicalPath, stampedBytes);
700
+ }
701
+ const legacyPrefix = readCompanyPrefix(resolvedRoot, companySlug);
702
+ let discoveryStatus = null;
703
+ try {
704
+ discoveryStatus = surfaceSkillFn({
705
+ hqRoot: resolvedRoot,
706
+ companySlug,
707
+ skillSlug: slug,
708
+ ...(legacyPrefix ? { legacyPrefix } : {}),
709
+ }).status;
710
+ }
711
+ catch (err) {
712
+ console.warn(chalk.yellow(`⚠ Skill promoted, but local discovery failed: ${err instanceof Error ? err.message : String(err)}`));
713
+ }
714
+ if (discoveryStatus !== null && discoveryStatus !== 0) {
715
+ console.warn(chalk.yellow(`⚠ Skill promoted, but local discovery exited ${discoveryStatus}. Run 'hq skill --company ${companySlug} create ${slug} --surface-only' to retry without another registration request.`));
716
+ }
717
+ if (opts.sync !== false) {
718
+ let syncResult;
719
+ try {
720
+ syncResult = await syncFile({
721
+ filePath: canonicalPath,
722
+ companySlug,
723
+ hqRoot: resolvedRoot,
724
+ token,
725
+ });
726
+ }
727
+ catch (err) {
728
+ console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${canonicalPath}', but sync failed.`));
729
+ if (err && typeof err === "object") {
730
+ const diagnostics = vaultAccessDeniedDiagnostics(err, {
731
+ companySlug,
732
+ objectKey: skillVaultPath(slug),
733
+ });
734
+ if (diagnostics)
735
+ stampVaultAccessDenied(err, diagnostics);
736
+ }
737
+ throw err;
738
+ }
739
+ if (syncResult.aborted) {
740
+ throw localSkillError(`Skill ${skillUid} is stamped locally at '${canonicalPath}', but sync aborted because the remote file conflicts.`);
741
+ }
742
+ }
743
+ console.log(chalk.green(`Skill promoted: ${skillUid}`));
744
+ console.log(` File: ${canonicalPath}`);
745
+ console.log(` SHA-256: ${sha256Hex(stampedBytes)}`);
746
+ console.log(` Discovery: ${discoveryStatus === 0 ? "ready" : "needs attention"}`);
747
+ console.log(` Sync: ${opts.sync === false ? "not requested" : "complete"}`);
748
+ });
514
749
  skill
515
750
  .command("propose <target>")
516
751
  .description("Post a comment-only improvement for a skill")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.119.9",
3
+ "version": "5.119.10",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {