@harness-engineering/cli 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{agents-md-ZFV6RR5J.js → agents-md-KIS2RSMG.js} +1 -1
  2. package/dist/{architecture-EXNUMH5R.js → architecture-AJAUDRQQ.js} +2 -2
  3. package/dist/bin/harness-mcp.js +10 -10
  4. package/dist/bin/harness.js +12 -12
  5. package/dist/{check-phase-gate-VZFOY2PO.js → check-phase-gate-K7QCSYRJ.js} +2 -2
  6. package/dist/{chunk-GSIVNYVJ.js → chunk-2SWJ4VO7.js} +4 -4
  7. package/dist/{chunk-I6JZYEGT.js → chunk-747VBPA4.js} +41 -41
  8. package/dist/{chunk-2YSQOUHO.js → chunk-AE2OWWDH.js} +72 -30
  9. package/dist/{chunk-ZWC3MN5E.js → chunk-B5SBNH4S.js} +705 -203
  10. package/dist/{chunk-NC6PXVWT.js → chunk-CTTFXXKJ.js} +3 -3
  11. package/dist/{chunk-X3MN5UQJ.js → chunk-EAURF4LH.js} +1 -1
  12. package/dist/{chunk-PA2XHK75.js → chunk-FLOEMHDF.js} +3 -3
  13. package/dist/{chunk-Z75JC6I2.js → chunk-JLXOEO5C.js} +2 -2
  14. package/dist/{chunk-TI4TGEX6.js → chunk-OIGVQF5V.js} +1 -1
  15. package/dist/{chunk-WUJTCNOU.js → chunk-TJVVU3HB.js} +1 -1
  16. package/dist/{chunk-WJZDO6OY.js → chunk-YXOG2277.js} +2 -2
  17. package/dist/{chunk-2NCIKJES.js → chunk-ZU2UBYBY.js} +1 -1
  18. package/dist/{ci-workflow-K5RCRNYR.js → ci-workflow-NBL4OT4A.js} +1 -1
  19. package/dist/{dist-JVZ2MKBC.js → dist-IJ4J4C5G.js} +3 -1
  20. package/dist/{docs-PWCUVYWU.js → docs-CPTMH3VY.js} +2 -2
  21. package/dist/{engine-6XUP6GAK.js → engine-BUWPAAGD.js} +1 -1
  22. package/dist/{entropy-4I6JEYAC.js → entropy-Z4FYVQ7L.js} +2 -2
  23. package/dist/{feedback-TNIW534S.js → feedback-TT6WF5YX.js} +1 -1
  24. package/dist/{generate-agent-definitions-MWKEA5NU.js → generate-agent-definitions-J5HANRNR.js} +1 -1
  25. package/dist/index.d.ts +39 -2
  26. package/dist/index.js +17 -13
  27. package/dist/{loader-4FIPIFII.js → loader-PCU5YWRH.js} +1 -1
  28. package/dist/{mcp-MOKLYNZL.js → mcp-YM6QLHLZ.js} +10 -10
  29. package/dist/{performance-BTOJCPXU.js → performance-YJVXOKIB.js} +2 -2
  30. package/dist/{review-pipeline-3YTW3463.js → review-pipeline-KGMIMLIE.js} +1 -1
  31. package/dist/{runtime-GO7K2PJE.js → runtime-F6R27LD6.js} +1 -1
  32. package/dist/{security-4P2GGFF6.js → security-MX5VVXBC.js} +1 -1
  33. package/dist/{validate-JN44D2Q7.js → validate-EFNMSFKD.js} +2 -2
  34. package/dist/{validate-cross-check-DB7RIFFF.js → validate-cross-check-LJX65SBS.js} +1 -1
  35. package/package.json +3 -3
@@ -5,9 +5,10 @@ import {
5
5
  OutputFormatter,
6
6
  OutputMode,
7
7
  createCheckPhaseGateCommand,
8
+ findConfigFile,
8
9
  findFiles,
9
10
  resolveConfig
10
- } from "./chunk-2NCIKJES.js";
11
+ } from "./chunk-ZU2UBYBY.js";
11
12
  import {
12
13
  createGenerateAgentDefinitionsCommand,
13
14
  generateAgentDefinitions
@@ -49,7 +50,7 @@ import {
49
50
  generateSlashCommands,
50
51
  handleGetImpact,
51
52
  handleOrphanDeletion
52
- } from "./chunk-I6JZYEGT.js";
53
+ } from "./chunk-747VBPA4.js";
53
54
  import {
54
55
  VALID_PLATFORMS
55
56
  } from "./chunk-ZOAWBDWU.js";
@@ -79,11 +80,13 @@ import {
79
80
  ArchConfigSchema,
80
81
  BaselineManager,
81
82
  BlueprintGenerator,
83
+ BundleSchema,
82
84
  CriticalPathResolver,
83
85
  EntropyAnalyzer,
84
86
  ProjectScanner,
85
87
  SecurityScanner,
86
88
  TypeScriptParser,
89
+ addProvenance,
87
90
  appendLearning,
88
91
  applyFixes,
89
92
  archiveStream,
@@ -91,6 +94,7 @@ import {
91
94
  checkDocCoverage,
92
95
  createFixes,
93
96
  createStream,
97
+ deepMergeConstraints,
94
98
  defineLayer,
95
99
  detectCircularDepsInFiles,
96
100
  detectDeadCode,
@@ -104,6 +108,9 @@ import {
104
108
  parseDiff,
105
109
  parseManifest,
106
110
  parseSecurityConfig,
111
+ readLockfile,
112
+ removeContributions,
113
+ removeProvenance,
107
114
  requestPeerReview,
108
115
  resolveStreamPath,
109
116
  runAll,
@@ -113,15 +120,16 @@ import {
113
120
  validateAgentsMap,
114
121
  validateDependencies,
115
122
  validateKnowledgeMap,
116
- writeConfig
117
- } from "./chunk-2YSQOUHO.js";
123
+ writeConfig,
124
+ writeLockfile
125
+ } from "./chunk-AE2OWWDH.js";
118
126
  import {
119
127
  Err,
120
128
  Ok
121
129
  } from "./chunk-MHBMTPW7.js";
122
130
 
123
131
  // src/index.ts
124
- import { Command as Command51 } from "commander";
132
+ import { Command as Command53 } from "commander";
125
133
 
126
134
  // src/commands/validate.ts
127
135
  import { Command } from "commander";
@@ -200,7 +208,7 @@ function createValidateCommand() {
200
208
  process.exit(result.error.exitCode);
201
209
  }
202
210
  if (opts.crossCheck) {
203
- const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-DB7RIFFF.js");
211
+ const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-LJX65SBS.js");
204
212
  const cwd = process.cwd();
205
213
  const specsDir = path.join(cwd, "docs", "specs");
206
214
  const plansDir = path.join(cwd, "docs", "plans");
@@ -468,10 +476,10 @@ async function runCheckSecurity(cwd, options) {
468
476
  const projectRoot = path4.resolve(cwd);
469
477
  let configData = {};
470
478
  try {
471
- const fs20 = await import("fs");
479
+ const fs23 = await import("fs");
472
480
  const configPath = path4.join(projectRoot, "harness.config.json");
473
- if (fs20.existsSync(configPath)) {
474
- const raw = fs20.readFileSync(configPath, "utf-8");
481
+ if (fs23.existsSync(configPath)) {
482
+ const raw = fs23.readFileSync(configPath, "utf-8");
475
483
  const parsed = JSON.parse(raw);
476
484
  configData = parsed.security ?? {};
477
485
  }
@@ -558,7 +566,7 @@ function createPerfCommand() {
558
566
  perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob, _opts, cmd) => {
559
567
  const globalOpts = cmd.optsWithGlobals();
560
568
  const cwd = process.cwd();
561
- const { BenchmarkRunner } = await import("./dist-JVZ2MKBC.js");
569
+ const { BenchmarkRunner } = await import("./dist-IJ4J4C5G.js");
562
570
  const runner = new BenchmarkRunner();
563
571
  const benchFiles = runner.discover(cwd, glob);
564
572
  if (benchFiles.length === 0) {
@@ -627,7 +635,7 @@ Results (${result.results.length} benchmarks):`);
627
635
  baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
628
636
  const globalOpts = cmd.optsWithGlobals();
629
637
  const cwd = process.cwd();
630
- const { BenchmarkRunner } = await import("./dist-JVZ2MKBC.js");
638
+ const { BenchmarkRunner } = await import("./dist-IJ4J4C5G.js");
631
639
  const runner = new BenchmarkRunner();
632
640
  const manager = new BaselineManager(cwd);
633
641
  logger.info("Running benchmarks to update baselines...");
@@ -655,7 +663,7 @@ Results (${result.results.length} benchmarks):`);
655
663
  perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
656
664
  const globalOpts = cmd.optsWithGlobals();
657
665
  const cwd = process.cwd();
658
- const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-JVZ2MKBC.js");
666
+ const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-IJ4J4C5G.js");
659
667
  const analyzer = new EntropyAnalyzer2({
660
668
  rootDir: path5.resolve(cwd),
661
669
  analyze: { complexity: true, coupling: true }
@@ -1905,7 +1913,7 @@ function sortedStringify(obj) {
1905
1913
  2
1906
1914
  );
1907
1915
  }
1908
- function readLockfile(filePath) {
1916
+ function readLockfile2(filePath) {
1909
1917
  if (!fs5.existsSync(filePath)) {
1910
1918
  return createEmptyLockfile();
1911
1919
  }
@@ -1925,7 +1933,7 @@ function readLockfile(filePath) {
1925
1933
  }
1926
1934
  return parsed;
1927
1935
  }
1928
- function writeLockfile(filePath, lockfile) {
1936
+ function writeLockfile2(filePath, lockfile) {
1929
1937
  const dir = path14.dirname(filePath);
1930
1938
  fs5.mkdirSync(dir, { recursive: true });
1931
1939
  fs5.writeFileSync(filePath, sortedStringify(lockfile) + "\n", "utf-8");
@@ -1996,8 +2004,18 @@ function collectSkills(opts) {
1996
2004
  const globalDir = resolveGlobalSkillsDir();
1997
2005
  const skillsDir = path15.dirname(globalDir);
1998
2006
  const communityBase = path15.join(skillsDir, "community");
2007
+ const communityPlatformDir = path15.join(communityBase, "claude-code");
1999
2008
  const lockfilePath = path15.join(communityBase, "skills-lock.json");
2000
- const lockfile = readLockfile(lockfilePath);
2009
+ const lockfile = readLockfile2(lockfilePath);
2010
+ const communitySkills = scanDirectory(communityPlatformDir, "community");
2011
+ for (const skill of communitySkills) {
2012
+ const pkgName = `@harness-skills/${skill.name}`;
2013
+ const lockEntry = lockfile.skills[pkgName];
2014
+ if (lockEntry) {
2015
+ skill.version = lockEntry.version;
2016
+ }
2017
+ }
2018
+ addUnique(communitySkills);
2001
2019
  for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
2002
2020
  const shortName = pkgName.replace("@harness-skills/", "");
2003
2021
  if (!seen.has(shortName)) {
@@ -2393,6 +2411,9 @@ function createInfoCommand() {
2393
2411
  import { Command as Command25 } from "commander";
2394
2412
 
2395
2413
  // src/registry/npm-client.ts
2414
+ import * as fs10 from "fs";
2415
+ import * as path19 from "path";
2416
+ import * as os2 from "os";
2396
2417
  var NPM_REGISTRY = "https://registry.npmjs.org";
2397
2418
  var FETCH_TIMEOUT_MS = 3e4;
2398
2419
  var HARNESS_SKILLS_SCOPE = "@harness-skills/";
@@ -2411,12 +2432,37 @@ function extractSkillName(packageName) {
2411
2432
  }
2412
2433
  return packageName;
2413
2434
  }
2414
- async function fetchPackageMetadata(packageName) {
2435
+ function readNpmrcToken(registryUrl) {
2436
+ const { hostname, pathname } = new URL(registryUrl);
2437
+ const registryPath = `//${hostname}${pathname.replace(/\/$/, "")}/:_authToken=`;
2438
+ const candidates = [path19.join(process.cwd(), ".npmrc"), path19.join(os2.homedir(), ".npmrc")];
2439
+ for (const npmrcPath of candidates) {
2440
+ try {
2441
+ const content = fs10.readFileSync(npmrcPath, "utf-8");
2442
+ for (const line of content.split("\n")) {
2443
+ const trimmed = line.trim();
2444
+ if (trimmed.startsWith(registryPath)) {
2445
+ return trimmed.slice(registryPath.length).trim();
2446
+ }
2447
+ }
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ return null;
2452
+ }
2453
+ async function fetchPackageMetadata(packageName, registryUrl) {
2454
+ const registry = registryUrl ?? NPM_REGISTRY;
2455
+ const headers = {};
2456
+ if (registryUrl) {
2457
+ const token = readNpmrcToken(registryUrl);
2458
+ if (token) headers["Authorization"] = `Bearer ${token}`;
2459
+ }
2415
2460
  const encodedName = encodeURIComponent(packageName);
2416
- const url = `${NPM_REGISTRY}/${encodedName}`;
2461
+ const url = `${registry}/${encodedName}`;
2417
2462
  let response;
2418
2463
  try {
2419
2464
  response = await fetch(url, {
2465
+ headers,
2420
2466
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2421
2467
  });
2422
2468
  } catch {
@@ -2432,11 +2478,16 @@ async function fetchPackageMetadata(packageName) {
2432
2478
  }
2433
2479
  return await response.json();
2434
2480
  }
2435
- async function downloadTarball(tarballUrl) {
2481
+ async function downloadTarball(tarballUrl, authToken) {
2436
2482
  let lastError;
2483
+ const headers = {};
2484
+ if (authToken) {
2485
+ headers["Authorization"] = `Bearer ${authToken}`;
2486
+ }
2437
2487
  for (let attempt = 0; attempt < 2; attempt++) {
2438
2488
  try {
2439
2489
  const response = await fetch(tarballUrl, {
2490
+ headers,
2440
2491
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2441
2492
  });
2442
2493
  if (!response.ok) {
@@ -2450,12 +2501,19 @@ async function downloadTarball(tarballUrl) {
2450
2501
  }
2451
2502
  throw new Error(`Download failed for ${tarballUrl}. Try again. (${lastError?.message})`);
2452
2503
  }
2453
- async function searchNpmRegistry(query) {
2504
+ async function searchNpmRegistry(query, registryUrl) {
2505
+ const registry = registryUrl ?? NPM_REGISTRY;
2506
+ const headers = {};
2507
+ if (registryUrl) {
2508
+ const token = readNpmrcToken(registryUrl);
2509
+ if (token) headers["Authorization"] = `Bearer ${token}`;
2510
+ }
2454
2511
  const searchText = encodeURIComponent(`scope:harness-skills ${query}`);
2455
- const url = `${NPM_REGISTRY}/-/v1/search?text=${searchText}&size=20`;
2512
+ const url = `${registry}/-/v1/search?text=${searchText}&size=20`;
2456
2513
  let response;
2457
2514
  try {
2458
2515
  response = await fetch(url, {
2516
+ headers,
2459
2517
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2460
2518
  });
2461
2519
  } catch {
@@ -2476,7 +2534,7 @@ async function searchNpmRegistry(query) {
2476
2534
 
2477
2535
  // src/commands/skill/search.ts
2478
2536
  async function runSearch(query, opts) {
2479
- const results = await searchNpmRegistry(query);
2537
+ const results = await searchNpmRegistry(query, opts.registry);
2480
2538
  return results.filter((r) => {
2481
2539
  if (opts.platform && !r.keywords.includes(opts.platform)) {
2482
2540
  return false;
@@ -2488,7 +2546,7 @@ async function runSearch(query, opts) {
2488
2546
  });
2489
2547
  }
2490
2548
  function createSearchCommand() {
2491
- return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").action(async (query, opts, cmd) => {
2549
+ return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").option("--registry <url>", "Use a custom npm registry URL").action(async (query, opts, cmd) => {
2492
2550
  const globalOpts = cmd.optsWithGlobals();
2493
2551
  try {
2494
2552
  const results = await runSearch(query, opts);
@@ -2525,8 +2583,8 @@ Found ${results.length} skill(s):
2525
2583
 
2526
2584
  // src/commands/skill/create.ts
2527
2585
  import { Command as Command26 } from "commander";
2528
- import * as path19 from "path";
2529
- import * as fs10 from "fs";
2586
+ import * as path20 from "path";
2587
+ import * as fs11 from "fs";
2530
2588
  import YAML from "yaml";
2531
2589
  var KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2532
2590
  function buildReadme(name, description) {
@@ -2610,22 +2668,22 @@ function runCreate(name, opts) {
2610
2668
  if (!KEBAB_CASE_RE.test(name)) {
2611
2669
  throw new Error(`Invalid skill name "${name}". Must be kebab-case (e.g., my-skill).`);
2612
2670
  }
2613
- const baseDir = opts.outputDir ?? path19.join(process.cwd(), "agents", "skills", "claude-code");
2614
- const skillDir = path19.join(baseDir, name);
2615
- if (fs10.existsSync(skillDir)) {
2671
+ const baseDir = opts.outputDir ?? path20.join(process.cwd(), "agents", "skills", "claude-code");
2672
+ const skillDir = path20.join(baseDir, name);
2673
+ if (fs11.existsSync(skillDir)) {
2616
2674
  throw new Error(`Skill directory already exists: ${skillDir}`);
2617
2675
  }
2618
- fs10.mkdirSync(skillDir, { recursive: true });
2676
+ fs11.mkdirSync(skillDir, { recursive: true });
2619
2677
  const description = opts.description || `A community skill: ${name}`;
2620
2678
  const skillYaml = buildSkillYaml(name, opts);
2621
- const skillYamlPath = path19.join(skillDir, "skill.yaml");
2622
- fs10.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
2679
+ const skillYamlPath = path20.join(skillDir, "skill.yaml");
2680
+ fs11.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
2623
2681
  const skillMd = buildSkillMd(name, description);
2624
- const skillMdPath = path19.join(skillDir, "SKILL.md");
2625
- fs10.writeFileSync(skillMdPath, skillMd);
2682
+ const skillMdPath = path20.join(skillDir, "SKILL.md");
2683
+ fs11.writeFileSync(skillMdPath, skillMd);
2626
2684
  const readme = buildReadme(name, description);
2627
- const readmePath = path19.join(skillDir, "README.md");
2628
- fs10.writeFileSync(readmePath, readme);
2685
+ const readmePath = path20.join(skillDir, "README.md");
2686
+ fs11.writeFileSync(readmePath, readme);
2629
2687
  return {
2630
2688
  name,
2631
2689
  directory: skillDir,
@@ -2653,7 +2711,7 @@ function createCreateCommand() {
2653
2711
  logger.info(`
2654
2712
  Next steps:`);
2655
2713
  logger.info(
2656
- ` 1. Edit ${path19.join(result.directory, "SKILL.md")} with your skill content`
2714
+ ` 1. Edit ${path20.join(result.directory, "SKILL.md")} with your skill content`
2657
2715
  );
2658
2716
  logger.info(` 2. Run: harness skill validate ${name}`);
2659
2717
  logger.info(` 3. Run: harness skills publish`);
@@ -2667,27 +2725,28 @@ Next steps:`);
2667
2725
 
2668
2726
  // src/commands/skill/publish.ts
2669
2727
  import { Command as Command27 } from "commander";
2670
- import * as fs13 from "fs";
2671
- import * as path21 from "path";
2728
+ import * as fs14 from "fs";
2729
+ import * as path23 from "path";
2672
2730
  import { execFileSync as execFileSync3 } from "child_process";
2673
2731
 
2674
2732
  // src/registry/validator.ts
2675
- import * as fs12 from "fs";
2676
- import * as path20 from "path";
2733
+ import * as fs13 from "fs";
2734
+ import * as path22 from "path";
2677
2735
  import { parse as parse5 } from "yaml";
2678
2736
  import semver from "semver";
2679
2737
 
2680
2738
  // src/registry/bundled-skills.ts
2681
- import * as fs11 from "fs";
2739
+ import * as fs12 from "fs";
2740
+ import * as path21 from "path";
2682
2741
  function getBundledSkillNames(bundledSkillsDir) {
2683
- if (!fs11.existsSync(bundledSkillsDir)) {
2742
+ if (!fs12.existsSync(bundledSkillsDir)) {
2684
2743
  return /* @__PURE__ */ new Set();
2685
2744
  }
2686
- const entries = fs11.readdirSync(bundledSkillsDir);
2745
+ const entries = fs12.readdirSync(bundledSkillsDir);
2687
2746
  const names = /* @__PURE__ */ new Set();
2688
2747
  for (const entry of entries) {
2689
2748
  try {
2690
- const stat = fs11.statSync(`${bundledSkillsDir}/${entry}`);
2749
+ const stat = fs12.statSync(path21.join(bundledSkillsDir, String(entry)));
2691
2750
  if (stat.isDirectory()) {
2692
2751
  names.add(String(entry));
2693
2752
  }
@@ -2698,16 +2757,16 @@ function getBundledSkillNames(bundledSkillsDir) {
2698
2757
  }
2699
2758
 
2700
2759
  // src/registry/validator.ts
2701
- async function validateForPublish(skillDir) {
2760
+ async function validateForPublish(skillDir, registryUrl) {
2702
2761
  const errors = [];
2703
- const skillYamlPath = path20.join(skillDir, "skill.yaml");
2704
- if (!fs12.existsSync(skillYamlPath)) {
2762
+ const skillYamlPath = path22.join(skillDir, "skill.yaml");
2763
+ if (!fs13.existsSync(skillYamlPath)) {
2705
2764
  errors.push("skill.yaml not found. Create one with: harness skill create <name>");
2706
2765
  return { valid: false, errors };
2707
2766
  }
2708
2767
  let skillMeta;
2709
2768
  try {
2710
- const raw = fs12.readFileSync(skillYamlPath, "utf-8");
2769
+ const raw = fs13.readFileSync(skillYamlPath, "utf-8");
2711
2770
  const parsed = parse5(raw);
2712
2771
  const result = SkillMetadataSchema.safeParse(parsed);
2713
2772
  if (!result.success) {
@@ -2729,11 +2788,11 @@ async function validateForPublish(skillDir) {
2729
2788
  if (!skillMeta.triggers || skillMeta.triggers.length === 0) {
2730
2789
  errors.push("At least one trigger is required. Add triggers to skill.yaml.");
2731
2790
  }
2732
- const skillMdPath = path20.join(skillDir, "SKILL.md");
2733
- if (!fs12.existsSync(skillMdPath)) {
2791
+ const skillMdPath = path22.join(skillDir, "SKILL.md");
2792
+ if (!fs13.existsSync(skillMdPath)) {
2734
2793
  errors.push("SKILL.md not found. Create it with content describing your skill.");
2735
2794
  } else {
2736
- const content = fs12.readFileSync(skillMdPath, "utf-8");
2795
+ const content = fs13.readFileSync(skillMdPath, "utf-8");
2737
2796
  if (!content.includes("## When to Use")) {
2738
2797
  errors.push('SKILL.md must contain a "## When to Use" section.');
2739
2798
  }
@@ -2750,21 +2809,25 @@ async function validateForPublish(skillDir) {
2750
2809
  }
2751
2810
  try {
2752
2811
  const packageName = resolvePackageName(skillMeta.name);
2753
- const metadata = await fetchPackageMetadata(packageName);
2812
+ const metadata = await fetchPackageMetadata(packageName, registryUrl);
2754
2813
  const publishedVersion = metadata["dist-tags"]?.latest;
2755
2814
  if (publishedVersion && !semver.gt(skillMeta.version, publishedVersion)) {
2756
2815
  errors.push(
2757
2816
  `Version ${skillMeta.version} must be greater than published version ${publishedVersion}. Bump the version in skill.yaml.`
2758
2817
  );
2759
2818
  }
2760
- } catch {
2819
+ } catch (err) {
2820
+ const msg = err instanceof Error ? err.message : String(err);
2821
+ if (!msg.includes("not found")) {
2822
+ errors.push(`Cannot verify version against npm registry: ${msg}`);
2823
+ }
2761
2824
  }
2762
2825
  if (skillMeta.depends_on && skillMeta.depends_on.length > 0) {
2763
2826
  for (const dep of skillMeta.depends_on) {
2764
2827
  if (bundledNames.has(dep)) continue;
2765
2828
  try {
2766
2829
  const depPkg = resolvePackageName(dep);
2767
- await fetchPackageMetadata(depPkg);
2830
+ await fetchPackageMetadata(depPkg, registryUrl);
2768
2831
  } catch {
2769
2832
  errors.push(
2770
2833
  `Dependency "${dep}" not found on npm or as a bundled skill. Publish it first or remove from depends_on.`
@@ -2801,7 +2864,7 @@ function derivePackageJson(skill) {
2801
2864
 
2802
2865
  // src/commands/skill/publish.ts
2803
2866
  async function runPublish(skillDir, opts) {
2804
- const validation = await validateForPublish(skillDir);
2867
+ const validation = await validateForPublish(skillDir, opts.registry);
2805
2868
  if (!validation.valid) {
2806
2869
  const errorList = validation.errors.map((e) => ` - ${e}`).join("\n");
2807
2870
  throw new Error(`Pre-publish validation failed:
@@ -2809,11 +2872,11 @@ ${errorList}`);
2809
2872
  }
2810
2873
  const meta = validation.skillMeta;
2811
2874
  const pkg = derivePackageJson(meta);
2812
- const pkgPath = path21.join(skillDir, "package.json");
2813
- fs13.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2814
- const readmePath = path21.join(skillDir, "README.md");
2815
- if (!fs13.existsSync(readmePath)) {
2816
- const skillMdContent = fs13.readFileSync(path21.join(skillDir, "SKILL.md"), "utf-8");
2875
+ const pkgPath = path23.join(skillDir, "package.json");
2876
+ fs14.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2877
+ const readmePath = path23.join(skillDir, "README.md");
2878
+ if (!fs14.existsSync(readmePath)) {
2879
+ const skillMdContent = fs14.readFileSync(path23.join(skillDir, "SKILL.md"), "utf-8");
2817
2880
  const readme = `# ${pkg.name}
2818
2881
 
2819
2882
  ${meta.description}
@@ -2827,7 +2890,7 @@ harness install ${meta.name}
2827
2890
  ---
2828
2891
 
2829
2892
  ${skillMdContent}`;
2830
- fs13.writeFileSync(readmePath, readme);
2893
+ fs14.writeFileSync(readmePath, readme);
2831
2894
  }
2832
2895
  if (opts.dryRun) {
2833
2896
  return {
@@ -2837,7 +2900,11 @@ ${skillMdContent}`;
2837
2900
  dryRun: true
2838
2901
  };
2839
2902
  }
2840
- execFileSync3("npm", ["publish", "--access", "public"], {
2903
+ const publishArgs = ["publish", "--access", "public"];
2904
+ if (opts.registry) {
2905
+ publishArgs.push("--registry", opts.registry);
2906
+ }
2907
+ execFileSync3("npm", publishArgs, {
2841
2908
  cwd: skillDir,
2842
2909
  stdio: "pipe",
2843
2910
  timeout: 6e4
@@ -2849,12 +2916,13 @@ ${skillMdContent}`;
2849
2916
  };
2850
2917
  }
2851
2918
  function createPublishCommand() {
2852
- return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").action(async (opts, cmd) => {
2919
+ return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").option("--registry <url>", "Use a custom npm registry URL").action(async (opts, cmd) => {
2853
2920
  const globalOpts = cmd.optsWithGlobals();
2854
2921
  const skillDir = opts.dir || process.cwd();
2855
2922
  try {
2856
2923
  const result = await runPublish(skillDir, {
2857
- dryRun: opts.dryRun
2924
+ dryRun: opts.dryRun,
2925
+ registry: opts.registry
2858
2926
  });
2859
2927
  if (globalOpts.json) {
2860
2928
  logger.raw(result);
@@ -2889,11 +2957,11 @@ import { Command as Command33 } from "commander";
2889
2957
 
2890
2958
  // src/commands/state/show.ts
2891
2959
  import { Command as Command29 } from "commander";
2892
- import * as path22 from "path";
2960
+ import * as path24 from "path";
2893
2961
  function createShowCommand() {
2894
2962
  return new Command29("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
2895
2963
  const globalOpts = cmd.optsWithGlobals();
2896
- const projectPath = path22.resolve(opts.path);
2964
+ const projectPath = path24.resolve(opts.path);
2897
2965
  const result = await loadState(projectPath, opts.stream);
2898
2966
  if (!result.ok) {
2899
2967
  logger.error(result.error.message);
@@ -2934,12 +3002,12 @@ Decisions: ${state.decisions.length}`);
2934
3002
 
2935
3003
  // src/commands/state/reset.ts
2936
3004
  import { Command as Command30 } from "commander";
2937
- import * as fs14 from "fs";
2938
- import * as path23 from "path";
3005
+ import * as fs15 from "fs";
3006
+ import * as path25 from "path";
2939
3007
  import * as readline from "readline";
2940
3008
  function createResetCommand() {
2941
3009
  return new Command30("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
2942
- const projectPath = path23.resolve(opts.path);
3010
+ const projectPath = path25.resolve(opts.path);
2943
3011
  let statePath;
2944
3012
  if (opts.stream) {
2945
3013
  const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
@@ -2948,19 +3016,19 @@ function createResetCommand() {
2948
3016
  process.exit(ExitCode.ERROR);
2949
3017
  return;
2950
3018
  }
2951
- statePath = path23.join(streamResult.value, "state.json");
3019
+ statePath = path25.join(streamResult.value, "state.json");
2952
3020
  } else {
2953
- statePath = path23.join(projectPath, ".harness", "state.json");
3021
+ statePath = path25.join(projectPath, ".harness", "state.json");
2954
3022
  }
2955
- if (!fs14.existsSync(statePath)) {
3023
+ if (!fs15.existsSync(statePath)) {
2956
3024
  logger.info("No state file found. Nothing to reset.");
2957
3025
  process.exit(ExitCode.SUCCESS);
2958
3026
  return;
2959
3027
  }
2960
3028
  if (!opts.yes) {
2961
3029
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2962
- const answer = await new Promise((resolve24) => {
2963
- rl.question("Reset project state? This cannot be undone. [y/N] ", resolve24);
3030
+ const answer = await new Promise((resolve27) => {
3031
+ rl.question("Reset project state? This cannot be undone. [y/N] ", resolve27);
2964
3032
  });
2965
3033
  rl.close();
2966
3034
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -2970,7 +3038,7 @@ function createResetCommand() {
2970
3038
  }
2971
3039
  }
2972
3040
  try {
2973
- fs14.unlinkSync(statePath);
3041
+ fs15.unlinkSync(statePath);
2974
3042
  logger.success("Project state reset.");
2975
3043
  } catch (e) {
2976
3044
  logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
@@ -2983,10 +3051,10 @@ function createResetCommand() {
2983
3051
 
2984
3052
  // src/commands/state/learn.ts
2985
3053
  import { Command as Command31 } from "commander";
2986
- import * as path24 from "path";
3054
+ import * as path26 from "path";
2987
3055
  function createLearnCommand() {
2988
3056
  return new Command31("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
2989
- const projectPath = path24.resolve(opts.path);
3057
+ const projectPath = path26.resolve(opts.path);
2990
3058
  const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
2991
3059
  if (!result.ok) {
2992
3060
  logger.error(result.error.message);
@@ -3000,12 +3068,12 @@ function createLearnCommand() {
3000
3068
 
3001
3069
  // src/commands/state/streams.ts
3002
3070
  import { Command as Command32 } from "commander";
3003
- import * as path25 from "path";
3071
+ import * as path27 from "path";
3004
3072
  function createStreamsCommand() {
3005
3073
  const command = new Command32("streams").description("Manage state streams");
3006
3074
  command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
3007
3075
  const globalOpts = cmd.optsWithGlobals();
3008
- const projectPath = path25.resolve(opts.path);
3076
+ const projectPath = path27.resolve(opts.path);
3009
3077
  const indexResult = await loadStreamIndex(projectPath);
3010
3078
  const result = await listStreams(projectPath);
3011
3079
  if (!result.ok) {
@@ -3029,7 +3097,7 @@ function createStreamsCommand() {
3029
3097
  process.exit(ExitCode.SUCCESS);
3030
3098
  });
3031
3099
  command.command("create <name>").description("Create a new stream").option("--path <path>", "Project root path", ".").option("--branch <branch>", "Associate with a git branch").action(async (name, opts) => {
3032
- const projectPath = path25.resolve(opts.path);
3100
+ const projectPath = path27.resolve(opts.path);
3033
3101
  const result = await createStream(projectPath, name, opts.branch);
3034
3102
  if (!result.ok) {
3035
3103
  logger.error(result.error.message);
@@ -3040,7 +3108,7 @@ function createStreamsCommand() {
3040
3108
  process.exit(ExitCode.SUCCESS);
3041
3109
  });
3042
3110
  command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3043
- const projectPath = path25.resolve(opts.path);
3111
+ const projectPath = path27.resolve(opts.path);
3044
3112
  const result = await archiveStream(projectPath, name);
3045
3113
  if (!result.ok) {
3046
3114
  logger.error(result.error.message);
@@ -3051,7 +3119,7 @@ function createStreamsCommand() {
3051
3119
  process.exit(ExitCode.SUCCESS);
3052
3120
  });
3053
3121
  command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3054
- const projectPath = path25.resolve(opts.path);
3122
+ const projectPath = path27.resolve(opts.path);
3055
3123
  const result = await setActiveStream(projectPath, name);
3056
3124
  if (!result.ok) {
3057
3125
  logger.error(result.error.message);
@@ -3163,8 +3231,8 @@ function createCheckCommand() {
3163
3231
 
3164
3232
  // src/commands/ci/init.ts
3165
3233
  import { Command as Command35 } from "commander";
3166
- import * as fs15 from "fs";
3167
- import * as path26 from "path";
3234
+ import * as fs16 from "fs";
3235
+ import * as path28 from "path";
3168
3236
  var ALL_CHECKS = [
3169
3237
  "validate",
3170
3238
  "deps",
@@ -3265,8 +3333,8 @@ function generateCIConfig(options) {
3265
3333
  });
3266
3334
  }
3267
3335
  function detectPlatform() {
3268
- if (fs15.existsSync(".github")) return "github";
3269
- if (fs15.existsSync(".gitlab-ci.yml")) return "gitlab";
3336
+ if (fs16.existsSync(".github")) return "github";
3337
+ if (fs16.existsSync(".gitlab-ci.yml")) return "gitlab";
3270
3338
  return null;
3271
3339
  }
3272
3340
  function createInitCommand2() {
@@ -3282,12 +3350,12 @@ function createInitCommand2() {
3282
3350
  process.exit(result.error.exitCode);
3283
3351
  }
3284
3352
  const { filename, content } = result.value;
3285
- const targetPath = path26.resolve(filename);
3286
- const dir = path26.dirname(targetPath);
3287
- fs15.mkdirSync(dir, { recursive: true });
3288
- fs15.writeFileSync(targetPath, content);
3353
+ const targetPath = path28.resolve(filename);
3354
+ const dir = path28.dirname(targetPath);
3355
+ fs16.mkdirSync(dir, { recursive: true });
3356
+ fs16.writeFileSync(targetPath, content);
3289
3357
  if (platform === "generic" && process.platform !== "win32") {
3290
- fs15.chmodSync(targetPath, "755");
3358
+ fs16.chmodSync(targetPath, "755");
3291
3359
  }
3292
3360
  if (globalOpts.json) {
3293
3361
  console.log(JSON.stringify({ file: filename, platform }));
@@ -3367,10 +3435,10 @@ function prompt(question) {
3367
3435
  input: process.stdin,
3368
3436
  output: process.stdout
3369
3437
  });
3370
- return new Promise((resolve24) => {
3438
+ return new Promise((resolve27) => {
3371
3439
  rl.question(question, (answer) => {
3372
3440
  rl.close();
3373
- resolve24(answer.trim().toLowerCase());
3441
+ resolve27(answer.trim().toLowerCase());
3374
3442
  });
3375
3443
  });
3376
3444
  }
@@ -3508,7 +3576,7 @@ function createGenerateCommand3() {
3508
3576
 
3509
3577
  // src/commands/graph/scan.ts
3510
3578
  import { Command as Command39 } from "commander";
3511
- import * as path27 from "path";
3579
+ import * as path29 from "path";
3512
3580
  async function runScan(projectPath) {
3513
3581
  const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-M6BQODWC.js");
3514
3582
  const store = new GraphStore();
@@ -3521,13 +3589,13 @@ async function runScan(projectPath) {
3521
3589
  await new GitIngestor(store).ingest(projectPath);
3522
3590
  } catch {
3523
3591
  }
3524
- const graphDir = path27.join(projectPath, ".harness", "graph");
3592
+ const graphDir = path29.join(projectPath, ".harness", "graph");
3525
3593
  await store.save(graphDir);
3526
3594
  return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
3527
3595
  }
3528
3596
  function createScanCommand() {
3529
3597
  return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
3530
- const projectPath = path27.resolve(inputPath);
3598
+ const projectPath = path29.resolve(inputPath);
3531
3599
  const globalOpts = cmd.optsWithGlobals();
3532
3600
  try {
3533
3601
  const result = await runScan(projectPath);
@@ -3547,12 +3615,12 @@ function createScanCommand() {
3547
3615
 
3548
3616
  // src/commands/graph/ingest.ts
3549
3617
  import { Command as Command40 } from "commander";
3550
- import * as path28 from "path";
3618
+ import * as path30 from "path";
3551
3619
  async function loadConnectorConfig(projectPath, source) {
3552
3620
  try {
3553
- const fs20 = await import("fs/promises");
3554
- const configPath = path28.join(projectPath, "harness.config.json");
3555
- const config = JSON.parse(await fs20.readFile(configPath, "utf-8"));
3621
+ const fs23 = await import("fs/promises");
3622
+ const configPath = path30.join(projectPath, "harness.config.json");
3623
+ const config = JSON.parse(await fs23.readFile(configPath, "utf-8"));
3556
3624
  const connector = config.graph?.connectors?.find(
3557
3625
  (c) => c.source === source
3558
3626
  );
@@ -3592,7 +3660,7 @@ async function runIngest(projectPath, source, opts) {
3592
3660
  JiraConnector,
3593
3661
  SlackConnector
3594
3662
  } = await import("./dist-M6BQODWC.js");
3595
- const graphDir = path28.join(projectPath, ".harness", "graph");
3663
+ const graphDir = path30.join(projectPath, ".harness", "graph");
3596
3664
  const store = new GraphStore();
3597
3665
  await store.load(graphDir);
3598
3666
  if (opts?.all) {
@@ -3659,7 +3727,7 @@ function createIngestCommand() {
3659
3727
  process.exit(1);
3660
3728
  }
3661
3729
  const globalOpts = cmd.optsWithGlobals();
3662
- const projectPath = path28.resolve(globalOpts.config ? path28.dirname(globalOpts.config) : ".");
3730
+ const projectPath = path30.resolve(globalOpts.config ? path30.dirname(globalOpts.config) : ".");
3663
3731
  try {
3664
3732
  const result = await runIngest(projectPath, opts.source ?? "", {
3665
3733
  full: opts.full,
@@ -3682,11 +3750,11 @@ function createIngestCommand() {
3682
3750
 
3683
3751
  // src/commands/graph/query.ts
3684
3752
  import { Command as Command41 } from "commander";
3685
- import * as path29 from "path";
3753
+ import * as path31 from "path";
3686
3754
  async function runQuery(projectPath, rootNodeId, opts) {
3687
3755
  const { GraphStore, ContextQL } = await import("./dist-M6BQODWC.js");
3688
3756
  const store = new GraphStore();
3689
- const graphDir = path29.join(projectPath, ".harness", "graph");
3757
+ const graphDir = path31.join(projectPath, ".harness", "graph");
3690
3758
  const loaded = await store.load(graphDir);
3691
3759
  if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
3692
3760
  const params = {
@@ -3702,7 +3770,7 @@ async function runQuery(projectPath, rootNodeId, opts) {
3702
3770
  function createQueryCommand() {
3703
3771
  return new Command41("query").description("Query the knowledge graph").argument("<rootNodeId>", "Starting node ID").option("--depth <n>", "Max traversal depth", "3").option("--types <types>", "Comma-separated node types to include").option("--edges <edges>", "Comma-separated edge types to include").option("--bidirectional", "Traverse both directions").action(async (rootNodeId, opts, cmd) => {
3704
3772
  const globalOpts = cmd.optsWithGlobals();
3705
- const projectPath = path29.resolve(globalOpts.config ? path29.dirname(globalOpts.config) : ".");
3773
+ const projectPath = path31.resolve(globalOpts.config ? path31.dirname(globalOpts.config) : ".");
3706
3774
  try {
3707
3775
  const result = await runQuery(projectPath, rootNodeId, {
3708
3776
  depth: parseInt(opts.depth),
@@ -3731,18 +3799,18 @@ function createQueryCommand() {
3731
3799
  import { Command as Command42 } from "commander";
3732
3800
 
3733
3801
  // src/commands/graph/status.ts
3734
- import * as path30 from "path";
3802
+ import * as path32 from "path";
3735
3803
  async function runGraphStatus(projectPath) {
3736
3804
  const { GraphStore } = await import("./dist-M6BQODWC.js");
3737
- const graphDir = path30.join(projectPath, ".harness", "graph");
3805
+ const graphDir = path32.join(projectPath, ".harness", "graph");
3738
3806
  const store = new GraphStore();
3739
3807
  const loaded = await store.load(graphDir);
3740
3808
  if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
3741
- const fs20 = await import("fs/promises");
3742
- const metaPath = path30.join(graphDir, "metadata.json");
3809
+ const fs23 = await import("fs/promises");
3810
+ const metaPath = path32.join(graphDir, "metadata.json");
3743
3811
  let lastScan = "unknown";
3744
3812
  try {
3745
- const meta = JSON.parse(await fs20.readFile(metaPath, "utf-8"));
3813
+ const meta = JSON.parse(await fs23.readFile(metaPath, "utf-8"));
3746
3814
  lastScan = meta.lastScanTimestamp;
3747
3815
  } catch {
3748
3816
  }
@@ -3753,8 +3821,8 @@ async function runGraphStatus(projectPath) {
3753
3821
  }
3754
3822
  let connectorSyncStatus = {};
3755
3823
  try {
3756
- const syncMetaPath = path30.join(graphDir, "sync-metadata.json");
3757
- const syncMeta = JSON.parse(await fs20.readFile(syncMetaPath, "utf-8"));
3824
+ const syncMetaPath = path32.join(graphDir, "sync-metadata.json");
3825
+ const syncMeta = JSON.parse(await fs23.readFile(syncMetaPath, "utf-8"));
3758
3826
  for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
3759
3827
  connectorSyncStatus[name] = data.lastSyncTimestamp;
3760
3828
  }
@@ -3771,10 +3839,10 @@ async function runGraphStatus(projectPath) {
3771
3839
  }
3772
3840
 
3773
3841
  // src/commands/graph/export.ts
3774
- import * as path31 from "path";
3842
+ import * as path33 from "path";
3775
3843
  async function runGraphExport(projectPath, format) {
3776
3844
  const { GraphStore } = await import("./dist-M6BQODWC.js");
3777
- const graphDir = path31.join(projectPath, ".harness", "graph");
3845
+ const graphDir = path33.join(projectPath, ".harness", "graph");
3778
3846
  const store = new GraphStore();
3779
3847
  const loaded = await store.load(graphDir);
3780
3848
  if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
@@ -3803,13 +3871,13 @@ async function runGraphExport(projectPath, format) {
3803
3871
  }
3804
3872
 
3805
3873
  // src/commands/graph/index.ts
3806
- import * as path32 from "path";
3874
+ import * as path34 from "path";
3807
3875
  function createGraphCommand() {
3808
3876
  const graph = new Command42("graph").description("Knowledge graph management");
3809
3877
  graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
3810
3878
  try {
3811
3879
  const globalOpts = cmd.optsWithGlobals();
3812
- const projectPath = path32.resolve(globalOpts.config ? path32.dirname(globalOpts.config) : ".");
3880
+ const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
3813
3881
  const result = await runGraphStatus(projectPath);
3814
3882
  if (globalOpts.json) {
3815
3883
  console.log(JSON.stringify(result, null, 2));
@@ -3836,7 +3904,7 @@ function createGraphCommand() {
3836
3904
  });
3837
3905
  graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
3838
3906
  const globalOpts = cmd.optsWithGlobals();
3839
- const projectPath = path32.resolve(globalOpts.config ? path32.dirname(globalOpts.config) : ".");
3907
+ const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
3840
3908
  try {
3841
3909
  const output = await runGraphExport(projectPath, opts.format);
3842
3910
  console.log(output);
@@ -3852,7 +3920,7 @@ function createGraphCommand() {
3852
3920
  import { Command as Command43 } from "commander";
3853
3921
  function createMcpCommand() {
3854
3922
  return new Command43("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
3855
- const { startServer: startServer2 } = await import("./mcp-MOKLYNZL.js");
3923
+ const { startServer: startServer2 } = await import("./mcp-YM6QLHLZ.js");
3856
3924
  await startServer2();
3857
3925
  });
3858
3926
  }
@@ -3860,8 +3928,8 @@ function createMcpCommand() {
3860
3928
  // src/commands/impact-preview.ts
3861
3929
  import { Command as Command44 } from "commander";
3862
3930
  import { execSync as execSync3 } from "child_process";
3863
- import * as path33 from "path";
3864
- import * as fs16 from "fs";
3931
+ import * as path35 from "path";
3932
+ import * as fs17 from "fs";
3865
3933
  function getStagedFiles(cwd) {
3866
3934
  try {
3867
3935
  const output = execSync3("git diff --cached --name-only", {
@@ -3875,7 +3943,7 @@ function getStagedFiles(cwd) {
3875
3943
  }
3876
3944
  function graphExists(projectPath) {
3877
3945
  try {
3878
- return fs16.existsSync(path33.join(projectPath, ".harness", "graph", "graph.json"));
3946
+ return fs17.existsSync(path35.join(projectPath, ".harness", "graph", "graph.json"));
3879
3947
  } catch {
3880
3948
  return false;
3881
3949
  }
@@ -3884,7 +3952,7 @@ function extractNodeName(id) {
3884
3952
  const parts = id.split(":");
3885
3953
  if (parts.length > 1) {
3886
3954
  const fullPath = parts.slice(1).join(":");
3887
- return path33.basename(fullPath);
3955
+ return path35.basename(fullPath);
3888
3956
  }
3889
3957
  return id;
3890
3958
  }
@@ -4007,7 +4075,7 @@ function formatPerFile(perFileResults) {
4007
4075
  return lines.join("\n");
4008
4076
  }
4009
4077
  async function runImpactPreview(options) {
4010
- const projectPath = path33.resolve(options.path ?? process.cwd());
4078
+ const projectPath = path35.resolve(options.path ?? process.cwd());
4011
4079
  const stagedFiles = getStagedFiles(projectPath);
4012
4080
  if (stagedFiles.length === 0) {
4013
4081
  return "Impact Preview: no staged changes";
@@ -4229,19 +4297,19 @@ function createCheckArchCommand() {
4229
4297
 
4230
4298
  // src/commands/blueprint.ts
4231
4299
  import { Command as Command46 } from "commander";
4232
- import * as path34 from "path";
4300
+ import * as path36 from "path";
4233
4301
  function createBlueprintCommand() {
4234
4302
  return new Command46("blueprint").description("Generate a self-contained, interactive blueprint of the codebase").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory", "docs/blueprint").action(async (projectPath, options) => {
4235
4303
  try {
4236
- const rootDir = path34.resolve(projectPath);
4237
- const outputDir = path34.resolve(options.output);
4304
+ const rootDir = path36.resolve(projectPath);
4305
+ const outputDir = path36.resolve(options.output);
4238
4306
  logger.info(`Scanning project at ${rootDir}...`);
4239
4307
  const scanner = new ProjectScanner(rootDir);
4240
4308
  const data = await scanner.scan();
4241
4309
  logger.info(`Generating blueprint to ${outputDir}...`);
4242
4310
  const generator = new BlueprintGenerator();
4243
4311
  await generator.generate(data, { outputDir });
4244
- logger.success(`Blueprint generated successfully at ${path34.join(outputDir, "index.html")}`);
4312
+ logger.success(`Blueprint generated successfully at ${path36.join(outputDir, "index.html")}`);
4245
4313
  } catch (error) {
4246
4314
  logger.error(
4247
4315
  `Failed to generate blueprint: ${error instanceof Error ? error.message : String(error)}`
@@ -4253,15 +4321,15 @@ function createBlueprintCommand() {
4253
4321
 
4254
4322
  // src/commands/share.ts
4255
4323
  import { Command as Command47 } from "commander";
4256
- import * as fs17 from "fs";
4257
- import * as path35 from "path";
4324
+ import * as fs18 from "fs";
4325
+ import * as path37 from "path";
4258
4326
  import { parse as parseYaml } from "yaml";
4259
4327
  var MANIFEST_FILENAME = "constraints.yaml";
4260
4328
  function createShareCommand() {
4261
4329
  return new Command47("share").description("Extract and publish a constraints bundle from constraints.yaml").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory for the bundle", ".").action(async (projectPath, options) => {
4262
- const rootDir = path35.resolve(projectPath);
4263
- const manifestPath = path35.join(rootDir, MANIFEST_FILENAME);
4264
- if (!fs17.existsSync(manifestPath)) {
4330
+ const rootDir = path37.resolve(projectPath);
4331
+ const manifestPath = path37.join(rootDir, MANIFEST_FILENAME);
4332
+ if (!fs18.existsSync(manifestPath)) {
4265
4333
  logger.error(
4266
4334
  `No ${MANIFEST_FILENAME} found at ${manifestPath}.
4267
4335
  Create a constraints.yaml in your project root to define what to share.`
@@ -4270,7 +4338,7 @@ Create a constraints.yaml in your project root to define what to share.`
4270
4338
  }
4271
4339
  let parsed;
4272
4340
  try {
4273
- const raw = fs17.readFileSync(manifestPath, "utf-8");
4341
+ const raw = fs18.readFileSync(manifestPath, "utf-8");
4274
4342
  parsed = parseYaml(raw);
4275
4343
  } catch (err) {
4276
4344
  logger.error(
@@ -4284,7 +4352,7 @@ Create a constraints.yaml in your project root to define what to share.`
4284
4352
  process.exit(1);
4285
4353
  }
4286
4354
  const manifest = manifestResult.value;
4287
- const configResult = resolveConfig(path35.join(rootDir, "harness.config.json"));
4355
+ const configResult = resolveConfig(path37.join(rootDir, "harness.config.json"));
4288
4356
  if (!configResult.ok) {
4289
4357
  logger.error(configResult.error.message);
4290
4358
  process.exit(1);
@@ -4302,12 +4370,11 @@ Create a constraints.yaml in your project root to define what to share.`
4302
4370
  );
4303
4371
  process.exit(1);
4304
4372
  }
4305
- const outputDir = path35.resolve(options.output);
4306
- const outputPath = path35.join(outputDir, `${manifest.name}.harness-constraints.json`);
4307
- try {
4308
- await writeConfig(outputPath, bundle);
4309
- } catch (err) {
4310
- logger.error(`Failed to write bundle: ${err instanceof Error ? err.message : String(err)}`);
4373
+ const outputDir = path37.resolve(options.output);
4374
+ const outputPath = path37.join(outputDir, `${manifest.name}.harness-constraints.json`);
4375
+ const writeResult = await writeConfig(outputPath, bundle);
4376
+ if (!writeResult.ok) {
4377
+ logger.error(`Failed to write bundle: ${writeResult.error.message}`);
4311
4378
  process.exit(1);
4312
4379
  }
4313
4380
  logger.success(`Bundle written to ${outputPath}`);
@@ -4315,25 +4382,25 @@ Create a constraints.yaml in your project root to define what to share.`
4315
4382
  }
4316
4383
 
4317
4384
  // src/commands/install.ts
4318
- import * as fs19 from "fs";
4319
- import * as path37 from "path";
4385
+ import * as fs20 from "fs";
4386
+ import * as path39 from "path";
4320
4387
  import { Command as Command48 } from "commander";
4321
4388
  import { parse as yamlParse } from "yaml";
4322
4389
 
4323
4390
  // src/registry/tarball.ts
4324
- import * as fs18 from "fs";
4325
- import * as path36 from "path";
4326
- import * as os2 from "os";
4391
+ import * as fs19 from "fs";
4392
+ import * as path38 from "path";
4393
+ import * as os3 from "os";
4327
4394
  import { execFileSync as execFileSync5 } from "child_process";
4328
4395
  function extractTarball(tarballBuffer) {
4329
- const tmpDir = fs18.mkdtempSync(path36.join(os2.tmpdir(), "harness-skill-install-"));
4330
- const tarballPath = path36.join(tmpDir, "package.tgz");
4396
+ const tmpDir = fs19.mkdtempSync(path38.join(os3.tmpdir(), "harness-skill-install-"));
4397
+ const tarballPath = path38.join(tmpDir, "package.tgz");
4331
4398
  try {
4332
- fs18.writeFileSync(tarballPath, tarballBuffer);
4399
+ fs19.writeFileSync(tarballPath, tarballBuffer);
4333
4400
  execFileSync5("tar", ["-xzf", tarballPath, "-C", tmpDir], {
4334
4401
  timeout: 3e4
4335
4402
  });
4336
- fs18.unlinkSync(tarballPath);
4403
+ fs19.unlinkSync(tarballPath);
4337
4404
  } catch (err) {
4338
4405
  cleanupTempDir(tmpDir);
4339
4406
  throw new Error(
@@ -4344,37 +4411,37 @@ function extractTarball(tarballBuffer) {
4344
4411
  return tmpDir;
4345
4412
  }
4346
4413
  function placeSkillContent(extractedPkgDir, communityBaseDir, skillName, platforms) {
4347
- const files = fs18.readdirSync(extractedPkgDir);
4414
+ const files = fs19.readdirSync(extractedPkgDir);
4348
4415
  for (const platform of platforms) {
4349
- const targetDir = path36.join(communityBaseDir, platform, skillName);
4350
- if (fs18.existsSync(targetDir)) {
4351
- fs18.rmSync(targetDir, { recursive: true, force: true });
4416
+ const targetDir = path38.join(communityBaseDir, platform, skillName);
4417
+ if (fs19.existsSync(targetDir)) {
4418
+ fs19.rmSync(targetDir, { recursive: true, force: true });
4352
4419
  }
4353
- fs18.mkdirSync(targetDir, { recursive: true });
4420
+ fs19.mkdirSync(targetDir, { recursive: true });
4354
4421
  for (const file of files) {
4355
4422
  if (file === "package.json" || file === "node_modules") continue;
4356
- const srcPath = path36.join(extractedPkgDir, file);
4357
- const destPath = path36.join(targetDir, file);
4358
- const stat = fs18.statSync(srcPath);
4423
+ const srcPath = path38.join(extractedPkgDir, file);
4424
+ const destPath = path38.join(targetDir, file);
4425
+ const stat = fs19.statSync(srcPath);
4359
4426
  if (stat.isDirectory()) {
4360
- fs18.cpSync(srcPath, destPath, { recursive: true });
4427
+ fs19.cpSync(srcPath, destPath, { recursive: true });
4361
4428
  } else {
4362
- fs18.copyFileSync(srcPath, destPath);
4429
+ fs19.copyFileSync(srcPath, destPath);
4363
4430
  }
4364
4431
  }
4365
4432
  }
4366
4433
  }
4367
4434
  function removeSkillContent(communityBaseDir, skillName, platforms) {
4368
4435
  for (const platform of platforms) {
4369
- const targetDir = path36.join(communityBaseDir, platform, skillName);
4370
- if (fs18.existsSync(targetDir)) {
4371
- fs18.rmSync(targetDir, { recursive: true, force: true });
4436
+ const targetDir = path38.join(communityBaseDir, platform, skillName);
4437
+ if (fs19.existsSync(targetDir)) {
4438
+ fs19.rmSync(targetDir, { recursive: true, force: true });
4372
4439
  }
4373
4440
  }
4374
4441
  }
4375
4442
  function cleanupTempDir(dirPath) {
4376
4443
  try {
4377
- fs18.rmSync(dirPath, { recursive: true, force: true });
4444
+ fs19.rmSync(dirPath, { recursive: true, force: true });
4378
4445
  } catch {
4379
4446
  }
4380
4447
  }
@@ -4407,44 +4474,111 @@ function resolveVersion(metadata, versionRange) {
4407
4474
  return metadata.versions[matched];
4408
4475
  }
4409
4476
  function findDependentsOf(lockfile, targetPackageName) {
4410
- const entry = lockfile.skills[targetPackageName];
4411
- if (!entry?.dependencyOf) return [];
4412
- return [entry.dependencyOf];
4477
+ const dependents = [];
4478
+ const targetEntry = lockfile.skills[targetPackageName];
4479
+ if (targetEntry?.dependencyOf) {
4480
+ dependents.push(targetEntry.dependencyOf);
4481
+ }
4482
+ return dependents;
4413
4483
  }
4414
4484
 
4415
4485
  // src/commands/install.ts
4416
4486
  function validateSkillYaml(parsed) {
4417
- if (!parsed || typeof parsed !== "object" || !("name" in parsed) || !("version" in parsed) || !("platforms" in parsed)) {
4418
- throw new Error("contains invalid skill.yaml");
4419
- }
4420
- const obj = parsed;
4421
- if (typeof obj["name"] !== "string" || typeof obj["version"] !== "string" || !Array.isArray(obj["platforms"])) {
4422
- throw new Error("contains invalid skill.yaml");
4487
+ const result = SkillMetadataSchema.safeParse(parsed);
4488
+ if (!result.success) {
4489
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
4490
+ throw new Error(`contains invalid skill.yaml: ${issues}`);
4423
4491
  }
4424
4492
  return {
4425
- name: obj["name"],
4426
- version: obj["version"],
4427
- platforms: obj["platforms"],
4428
- depends_on: Array.isArray(obj["depends_on"]) ? obj["depends_on"] : []
4493
+ name: result.data.name,
4494
+ version: result.data.version,
4495
+ platforms: result.data.platforms,
4496
+ depends_on: result.data.depends_on ?? []
4429
4497
  };
4430
4498
  }
4499
+ async function runLocalInstall(fromPath, options) {
4500
+ const resolvedPath = path39.resolve(fromPath);
4501
+ if (!fs20.existsSync(resolvedPath)) {
4502
+ throw new Error(`--from path does not exist: ${resolvedPath}`);
4503
+ }
4504
+ const stat = fs20.statSync(resolvedPath);
4505
+ let extractDir = null;
4506
+ let pkgDir;
4507
+ if (stat.isDirectory()) {
4508
+ pkgDir = resolvedPath;
4509
+ } else if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
4510
+ const tarballBuffer = fs20.readFileSync(resolvedPath);
4511
+ extractDir = extractTarball(tarballBuffer);
4512
+ pkgDir = path39.join(extractDir, "package");
4513
+ } else {
4514
+ throw new Error(`--from path must be a directory or .tgz file. Got: ${resolvedPath}`);
4515
+ }
4516
+ try {
4517
+ const skillYamlPath = path39.join(pkgDir, "skill.yaml");
4518
+ if (!fs20.existsSync(skillYamlPath)) {
4519
+ throw new Error(`No skill.yaml found at ${skillYamlPath}`);
4520
+ }
4521
+ const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
4522
+ const parsed = yamlParse(rawYaml);
4523
+ const skillYaml = validateSkillYaml(parsed);
4524
+ const shortName = skillYaml.name;
4525
+ const globalDir = resolveGlobalSkillsDir();
4526
+ const skillsDir = path39.dirname(globalDir);
4527
+ const communityBase = path39.join(skillsDir, "community");
4528
+ const lockfilePath = path39.join(communityBase, "skills-lock.json");
4529
+ const bundledNames = getBundledSkillNames(globalDir);
4530
+ if (bundledNames.has(shortName)) {
4531
+ throw new Error(
4532
+ `'${shortName}' is a bundled skill and cannot be overridden by community installs.`
4533
+ );
4534
+ }
4535
+ placeSkillContent(pkgDir, communityBase, shortName, skillYaml.platforms);
4536
+ const packageName = `@harness-skills/${shortName}`;
4537
+ const lockfile = readLockfile2(lockfilePath);
4538
+ const entry = {
4539
+ version: skillYaml.version,
4540
+ resolved: `local:${resolvedPath}`,
4541
+ integrity: "",
4542
+ platforms: skillYaml.platforms,
4543
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4544
+ dependencyOf: options._dependencyOf ?? null
4545
+ };
4546
+ const updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
4547
+ writeLockfile2(lockfilePath, updatedLockfile);
4548
+ return {
4549
+ installed: true,
4550
+ name: packageName,
4551
+ version: skillYaml.version
4552
+ };
4553
+ } finally {
4554
+ if (extractDir) {
4555
+ cleanupTempDir(extractDir);
4556
+ }
4557
+ }
4558
+ }
4431
4559
  async function runInstall(skillName, options) {
4560
+ if (options.from && options.registry) {
4561
+ throw new Error("--from and --registry cannot be used together");
4562
+ }
4563
+ if (options.from) {
4564
+ return runLocalInstall(options.from, options);
4565
+ }
4432
4566
  const packageName = resolvePackageName(skillName);
4433
4567
  const shortName = extractSkillName(packageName);
4434
4568
  const globalDir = resolveGlobalSkillsDir();
4435
- const skillsDir = path37.dirname(globalDir);
4436
- const communityBase = path37.join(skillsDir, "community");
4437
- const lockfilePath = path37.join(communityBase, "skills-lock.json");
4569
+ const skillsDir = path39.dirname(globalDir);
4570
+ const communityBase = path39.join(skillsDir, "community");
4571
+ const lockfilePath = path39.join(communityBase, "skills-lock.json");
4438
4572
  const bundledNames = getBundledSkillNames(globalDir);
4439
4573
  if (bundledNames.has(shortName)) {
4440
4574
  throw new Error(
4441
4575
  `'${shortName}' is a bundled skill and cannot be overridden by community installs.`
4442
4576
  );
4443
4577
  }
4444
- const metadata = await fetchPackageMetadata(packageName);
4578
+ const metadata = await fetchPackageMetadata(packageName, options.registry);
4445
4579
  const versionInfo = resolveVersion(metadata, options.version);
4446
4580
  const resolvedVersion = versionInfo.version;
4447
- const lockfile = readLockfile(lockfilePath);
4581
+ const lockfile = readLockfile2(lockfilePath);
4448
4582
  const existingEntry = lockfile.skills[packageName];
4449
4583
  const previousVersion = existingEntry?.version;
4450
4584
  if (existingEntry && existingEntry.version === resolvedVersion && !options.force) {
@@ -4455,16 +4589,17 @@ async function runInstall(skillName, options) {
4455
4589
  version: resolvedVersion
4456
4590
  };
4457
4591
  }
4458
- const tarballBuffer = await downloadTarball(versionInfo.dist.tarball);
4592
+ const authToken = options.registry ? readNpmrcToken(options.registry) ?? void 0 : void 0;
4593
+ const tarballBuffer = await downloadTarball(versionInfo.dist.tarball, authToken);
4459
4594
  const extractDir = extractTarball(tarballBuffer);
4460
4595
  let skillYaml;
4461
4596
  try {
4462
- const extractedPkgDir = path37.join(extractDir, "package");
4463
- const skillYamlPath = path37.join(extractedPkgDir, "skill.yaml");
4464
- if (!fs19.existsSync(skillYamlPath)) {
4597
+ const extractedPkgDir = path39.join(extractDir, "package");
4598
+ const skillYamlPath = path39.join(extractedPkgDir, "skill.yaml");
4599
+ if (!fs20.existsSync(skillYamlPath)) {
4465
4600
  throw new Error(`contains invalid skill.yaml: file not found in package`);
4466
4601
  }
4467
- const rawYaml = fs19.readFileSync(skillYamlPath, "utf-8");
4602
+ const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
4468
4603
  const parsed = yamlParse(rawYaml);
4469
4604
  skillYaml = validateSkillYaml(parsed);
4470
4605
  placeSkillContent(extractedPkgDir, communityBase, shortName, skillYaml.platforms);
@@ -4479,10 +4614,10 @@ async function runInstall(skillName, options) {
4479
4614
  integrity: versionInfo.dist.integrity,
4480
4615
  platforms: skillYaml.platforms,
4481
4616
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4482
- dependencyOf: null
4617
+ dependencyOf: options._dependencyOf ?? null
4483
4618
  };
4484
4619
  let updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
4485
- writeLockfile(lockfilePath, updatedLockfile);
4620
+ writeLockfile2(lockfilePath, updatedLockfile);
4486
4621
  const result = {
4487
4622
  installed: true,
4488
4623
  name: packageName,
@@ -4494,14 +4629,17 @@ async function runInstall(skillName, options) {
4494
4629
  }
4495
4630
  const deps = skillYaml.depends_on ?? [];
4496
4631
  for (const dep of deps) {
4497
- logger.info(`Installing dependency: ${dep}`);
4498
- await runInstall(dep, {});
4632
+ logger.info(`Installing dependency: ${dep} (required by ${shortName})`);
4633
+ await runInstall(dep, {
4634
+ _dependencyOf: packageName,
4635
+ ...options.registry !== void 0 ? { registry: options.registry } : {}
4636
+ });
4499
4637
  }
4500
4638
  return result;
4501
4639
  }
4502
4640
  function createInstallCommand() {
4503
4641
  const cmd = new Command48("install");
4504
- cmd.description("Install a community skill from the @harness-skills registry").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--version <range>", "Semver range or exact version to install").option("--force", "Force reinstall even if same version is already installed").action(async (skill, opts) => {
4642
+ cmd.description("Install a community skill from the @harness-skills registry").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--version <range>", "Semver range or exact version to install").option("--force", "Force reinstall even if same version is already installed").option("--from <path>", "Install from a local directory or .tgz file").option("--registry <url>", "Use a custom npm registry URL").action(async (skill, opts) => {
4505
4643
  try {
4506
4644
  const result = await runInstall(skill, opts);
4507
4645
  if (result.skipped) {
@@ -4523,17 +4661,377 @@ function createInstallCommand() {
4523
4661
  return cmd;
4524
4662
  }
4525
4663
 
4526
- // src/commands/uninstall.ts
4527
- import * as path38 from "path";
4664
+ // src/commands/install-constraints.ts
4665
+ import * as fs21 from "fs/promises";
4666
+ import * as path40 from "path";
4528
4667
  import { Command as Command49 } from "commander";
4668
+ import semver3 from "semver";
4669
+ async function runInstallConstraints(options) {
4670
+ const { source, configPath, lockfilePath } = options;
4671
+ let rawBundle;
4672
+ try {
4673
+ rawBundle = await fs21.readFile(source, "utf-8");
4674
+ } catch (err) {
4675
+ if (isNodeError(err) && err.code === "ENOENT") {
4676
+ return { ok: false, error: `Bundle file not found: ${source}` };
4677
+ }
4678
+ return {
4679
+ ok: false,
4680
+ error: `Failed to read bundle: ${err instanceof Error ? err.message : String(err)}`
4681
+ };
4682
+ }
4683
+ let parsedJson;
4684
+ try {
4685
+ parsedJson = JSON.parse(rawBundle);
4686
+ } catch {
4687
+ return { ok: false, error: `Bundle file contains invalid JSON: ${source}` };
4688
+ }
4689
+ const bundleResult = BundleSchema.safeParse(parsedJson);
4690
+ if (!bundleResult.success) {
4691
+ const issues = bundleResult.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
4692
+ return { ok: false, error: `Bundle schema validation failed: ${issues}` };
4693
+ }
4694
+ const bundle = bundleResult.data;
4695
+ if (bundle.minHarnessVersion) {
4696
+ const installed = semver3.valid(semver3.coerce(CLI_VERSION));
4697
+ const required = semver3.valid(semver3.coerce(bundle.minHarnessVersion));
4698
+ if (installed && required && semver3.lt(installed, required)) {
4699
+ return {
4700
+ ok: false,
4701
+ error: `Bundle requires harness version >= ${bundle.minHarnessVersion}, but installed version is ${CLI_VERSION}. Please upgrade.`
4702
+ };
4703
+ }
4704
+ }
4705
+ const constraintKeys = Object.keys(bundle.constraints).filter(
4706
+ (k) => bundle.constraints[k] !== void 0
4707
+ );
4708
+ if (constraintKeys.length === 0) {
4709
+ return {
4710
+ ok: false,
4711
+ error: "Bundle contains no constraints. Nothing to install."
4712
+ };
4713
+ }
4714
+ let localConfig;
4715
+ try {
4716
+ const raw = await fs21.readFile(configPath, "utf-8");
4717
+ localConfig = JSON.parse(raw);
4718
+ } catch (err) {
4719
+ return {
4720
+ ok: false,
4721
+ error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
4722
+ };
4723
+ }
4724
+ const lockfileResult = await readLockfile(lockfilePath);
4725
+ if (!lockfileResult.ok) {
4726
+ return { ok: false, error: lockfileResult.error };
4727
+ }
4728
+ const existingLockfile = lockfileResult.value ?? {
4729
+ version: 1,
4730
+ packages: {}
4731
+ };
4732
+ const existingEntry = existingLockfile.packages[bundle.name];
4733
+ if (existingEntry && existingEntry.version === bundle.version) {
4734
+ return {
4735
+ ok: true,
4736
+ value: {
4737
+ installed: false,
4738
+ packageName: bundle.name,
4739
+ version: bundle.version,
4740
+ contributionsCount: 0,
4741
+ conflicts: [],
4742
+ alreadyInstalled: true
4743
+ }
4744
+ };
4745
+ }
4746
+ if (existingEntry) {
4747
+ const oldContributions = existingEntry.contributions ?? {};
4748
+ localConfig = removeContributions(localConfig, oldContributions);
4749
+ }
4750
+ const mergeResult = deepMergeConstraints(localConfig, bundle.constraints);
4751
+ if (mergeResult.conflicts.length > 0) {
4752
+ if (options.forceLocal) {
4753
+ } else if (options.forcePackage) {
4754
+ for (const conflict of mergeResult.conflicts) {
4755
+ applyPackageValue(mergeResult.config, conflict);
4756
+ addConflictContribution(mergeResult.contributions, conflict);
4757
+ }
4758
+ } else if (!options.dryRun) {
4759
+ return {
4760
+ ok: false,
4761
+ error: formatConflictsError(mergeResult.conflicts)
4762
+ };
4763
+ }
4764
+ }
4765
+ if (options.dryRun) {
4766
+ return {
4767
+ ok: true,
4768
+ value: {
4769
+ installed: false,
4770
+ packageName: bundle.name,
4771
+ version: bundle.version,
4772
+ contributionsCount: Object.keys(mergeResult.contributions).length,
4773
+ conflicts: mergeResult.conflicts,
4774
+ dryRun: true
4775
+ }
4776
+ };
4777
+ }
4778
+ const writeResult = await writeConfig(configPath, mergeResult.config);
4779
+ if (!writeResult.ok) {
4780
+ return {
4781
+ ok: false,
4782
+ error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
4783
+ };
4784
+ }
4785
+ const lockfileEntry = {
4786
+ version: bundle.version,
4787
+ source,
4788
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4789
+ contributions: mergeResult.contributions
4790
+ };
4791
+ const updatedLockfile = addProvenance(existingLockfile, bundle.name, lockfileEntry);
4792
+ const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
4793
+ if (!lockfileWriteResult.ok) {
4794
+ return {
4795
+ ok: false,
4796
+ error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
4797
+ };
4798
+ }
4799
+ return {
4800
+ ok: true,
4801
+ value: {
4802
+ installed: true,
4803
+ packageName: bundle.name,
4804
+ version: bundle.version,
4805
+ contributionsCount: Object.keys(mergeResult.contributions).length,
4806
+ conflicts: mergeResult.conflicts
4807
+ }
4808
+ };
4809
+ }
4810
+ var sectionAppliers = {
4811
+ layers(config, key, value) {
4812
+ const layers = config.layers;
4813
+ const idx = layers.findIndex((l) => l.name === key);
4814
+ if (idx >= 0) layers[idx] = value;
4815
+ },
4816
+ forbiddenImports(config, key, value) {
4817
+ const rules = config.forbiddenImports;
4818
+ const idx = rules.findIndex((r) => r.from === key);
4819
+ if (idx >= 0) rules[idx] = value;
4820
+ },
4821
+ "architecture.thresholds"(config, key, value) {
4822
+ const arch = config.architecture;
4823
+ if (arch?.thresholds) arch.thresholds[key] = value;
4824
+ },
4825
+ "architecture.modules"(config, key, value) {
4826
+ const arch = config.architecture;
4827
+ const [modulePath, category] = key.split(":");
4828
+ if (arch?.modules && modulePath && category && arch.modules[modulePath]) {
4829
+ arch.modules[modulePath][category] = value;
4830
+ }
4831
+ },
4832
+ "security.rules"(config, key, value) {
4833
+ const security = config.security;
4834
+ if (security?.rules) security.rules[key] = value;
4835
+ }
4836
+ };
4837
+ function applyPackageValue(config, conflict) {
4838
+ const applier = sectionAppliers[conflict.section];
4839
+ if (applier) applier(config, conflict.key, conflict.packageValue);
4840
+ }
4841
+ function addConflictContribution(contributions, conflict) {
4842
+ const section = conflict.section;
4843
+ const existing = contributions[section] ?? [];
4844
+ existing.push(conflict.key);
4845
+ contributions[section] = existing;
4846
+ }
4847
+ function formatConflictsError(conflicts) {
4848
+ const lines = [
4849
+ `${conflicts.length} conflict(s) detected. Resolve with --force-local or --force-package:`,
4850
+ ""
4851
+ ];
4852
+ for (const c of conflicts) {
4853
+ lines.push(` [${c.section}] ${c.key}: ${c.description}`);
4854
+ lines.push(` Local: ${JSON.stringify(c.localValue)}`);
4855
+ lines.push(` Package: ${JSON.stringify(c.packageValue)}`);
4856
+ lines.push("");
4857
+ }
4858
+ return lines.join("\n");
4859
+ }
4860
+ function isNodeError(err) {
4861
+ return err instanceof Error && "code" in err;
4862
+ }
4863
+ function resolveConfigPath(opts) {
4864
+ if (opts.config) return path40.resolve(opts.config);
4865
+ const found = findConfigFile();
4866
+ if (!found.ok) {
4867
+ logger.error(found.error.message);
4868
+ process.exit(1);
4869
+ }
4870
+ return found.value;
4871
+ }
4872
+ function logInstallResult(val, opts) {
4873
+ if (val.dryRun) {
4874
+ logger.info(`[dry-run] Would install ${val.packageName}@${val.version}`);
4875
+ logger.info(`[dry-run] ${val.contributionsCount} section(s) would be added`);
4876
+ if (val.conflicts.length > 0) {
4877
+ logger.warn(`[dry-run] ${val.conflicts.length} conflict(s) detected`);
4878
+ for (const c of val.conflicts) {
4879
+ logger.warn(` [${c.section}] ${c.key}: ${c.description}`);
4880
+ }
4881
+ }
4882
+ return;
4883
+ }
4884
+ if (val.alreadyInstalled) {
4885
+ logger.info(`${val.packageName}@${val.version} is already installed. No changes made.`);
4886
+ return;
4887
+ }
4888
+ logger.success(
4889
+ `Installed ${val.packageName}@${val.version} (${val.contributionsCount} section(s) merged)`
4890
+ );
4891
+ if (val.conflicts.length > 0) {
4892
+ logger.warn(
4893
+ `${val.conflicts.length} conflict(s) resolved with ${opts.forceLocal ? "--force-local" : "--force-package"}`
4894
+ );
4895
+ }
4896
+ }
4897
+ async function handleInstallConstraints(source, opts) {
4898
+ const configPath = resolveConfigPath(opts);
4899
+ const projectRoot = path40.dirname(configPath);
4900
+ const lockfilePath = path40.join(projectRoot, ".harness", "constraints.lock.json");
4901
+ const resolvedSource = path40.resolve(source);
4902
+ if (opts.forceLocal && opts.forcePackage) {
4903
+ logger.error("Cannot use both --force-local and --force-package.");
4904
+ process.exit(1);
4905
+ }
4906
+ const result = await runInstallConstraints({
4907
+ source: resolvedSource,
4908
+ configPath,
4909
+ lockfilePath,
4910
+ ...opts.forceLocal && { forceLocal: true },
4911
+ ...opts.forcePackage && { forcePackage: true },
4912
+ ...opts.dryRun && { dryRun: true }
4913
+ });
4914
+ if (!result.ok) {
4915
+ logger.error(result.error);
4916
+ process.exit(1);
4917
+ }
4918
+ logInstallResult(result.value, opts);
4919
+ }
4920
+ function createInstallConstraintsCommand() {
4921
+ const cmd = new Command49("install-constraints");
4922
+ cmd.description("Install a constraints bundle into the local harness config").argument("<source>", "Path to a .harness-constraints.json bundle file").option("--force-local", "Resolve all conflicts by keeping local values").option("--force-package", "Resolve all conflicts by using package values").option("--dry-run", "Show what would change without writing files").option("-c, --config <path>", "Path to harness.config.json").action(handleInstallConstraints);
4923
+ return cmd;
4924
+ }
4925
+
4926
+ // src/commands/uninstall-constraints.ts
4927
+ import * as fs22 from "fs/promises";
4928
+ import * as path41 from "path";
4929
+ import { Command as Command50 } from "commander";
4930
+ async function runUninstallConstraints(options) {
4931
+ const { packageName, configPath, lockfilePath } = options;
4932
+ const lockfileResult = await readLockfile(lockfilePath);
4933
+ if (!lockfileResult.ok) {
4934
+ return { ok: false, error: lockfileResult.error };
4935
+ }
4936
+ if (lockfileResult.value === null) {
4937
+ return { ok: false, error: "No lockfile found. No constraint packages are installed." };
4938
+ }
4939
+ const lockfile = lockfileResult.value;
4940
+ const entry = lockfile.packages[packageName];
4941
+ if (!entry) {
4942
+ return {
4943
+ ok: false,
4944
+ error: `Package '${packageName}' is not installed.`
4945
+ };
4946
+ }
4947
+ let localConfig;
4948
+ try {
4949
+ const raw = await fs22.readFile(configPath, "utf-8");
4950
+ localConfig = JSON.parse(raw);
4951
+ } catch (err) {
4952
+ return {
4953
+ ok: false,
4954
+ error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
4955
+ };
4956
+ }
4957
+ const contributions = entry.contributions ?? {};
4958
+ const sectionsRemoved = Object.keys(contributions);
4959
+ const updatedConfig = removeContributions(localConfig, contributions);
4960
+ const { lockfile: updatedLockfile } = removeProvenance(lockfile, packageName);
4961
+ const writeResult = await writeConfig(configPath, updatedConfig);
4962
+ if (!writeResult.ok) {
4963
+ return {
4964
+ ok: false,
4965
+ error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
4966
+ };
4967
+ }
4968
+ const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
4969
+ if (!lockfileWriteResult.ok) {
4970
+ return {
4971
+ ok: false,
4972
+ error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
4973
+ };
4974
+ }
4975
+ return {
4976
+ ok: true,
4977
+ value: {
4978
+ removed: true,
4979
+ packageName,
4980
+ version: entry.version,
4981
+ sectionsRemoved
4982
+ }
4983
+ };
4984
+ }
4985
+ function createUninstallConstraintsCommand() {
4986
+ const cmd = new Command50("uninstall-constraints");
4987
+ cmd.description("Remove a previously installed constraints package").argument("<name>", "Name of the constraint package to uninstall").option("-c, --config <path>", "Path to harness.config.json").action(async (name, opts) => {
4988
+ let configPath;
4989
+ if (opts.config) {
4990
+ configPath = path41.resolve(opts.config);
4991
+ } else {
4992
+ const found = findConfigFile();
4993
+ if (!found.ok) {
4994
+ logger.error(found.error.message);
4995
+ process.exit(1);
4996
+ }
4997
+ configPath = found.value;
4998
+ }
4999
+ const projectRoot = path41.dirname(configPath);
5000
+ const lockfilePath = path41.join(projectRoot, ".harness", "constraints.lock.json");
5001
+ const result = await runUninstallConstraints({
5002
+ packageName: name,
5003
+ configPath,
5004
+ lockfilePath
5005
+ });
5006
+ if (!result.ok) {
5007
+ logger.error(result.error);
5008
+ process.exit(1);
5009
+ }
5010
+ const val = result.value;
5011
+ if (val.sectionsRemoved.length === 0) {
5012
+ logger.success(
5013
+ `Removed ${val.packageName}@${val.version} (no contributed rules to remove)`
5014
+ );
5015
+ } else {
5016
+ logger.success(
5017
+ `Removed ${val.packageName}@${val.version} (${val.sectionsRemoved.length} section(s): ${val.sectionsRemoved.join(", ")})`
5018
+ );
5019
+ }
5020
+ });
5021
+ return cmd;
5022
+ }
5023
+
5024
+ // src/commands/uninstall.ts
5025
+ import * as path42 from "path";
5026
+ import { Command as Command51 } from "commander";
4529
5027
  async function runUninstall(skillName, options) {
4530
5028
  const packageName = resolvePackageName(skillName);
4531
5029
  const shortName = extractSkillName(packageName);
4532
5030
  const globalDir = resolveGlobalSkillsDir();
4533
- const skillsDir = path38.dirname(globalDir);
4534
- const communityBase = path38.join(skillsDir, "community");
4535
- const lockfilePath = path38.join(communityBase, "skills-lock.json");
4536
- const lockfile = readLockfile(lockfilePath);
5031
+ const skillsDir = path42.dirname(globalDir);
5032
+ const communityBase = path42.join(skillsDir, "community");
5033
+ const lockfilePath = path42.join(communityBase, "skills-lock.json");
5034
+ const lockfile = readLockfile2(lockfilePath);
4537
5035
  const entry = lockfile.skills[packageName];
4538
5036
  if (!entry) {
4539
5037
  throw new Error(`Skill '${shortName}' is not installed.`);
@@ -4550,7 +5048,7 @@ async function runUninstall(skillName, options) {
4550
5048
  }
4551
5049
  removeSkillContent(communityBase, shortName, entry.platforms);
4552
5050
  const updatedLockfile = removeLockfileEntry(lockfile, packageName);
4553
- writeLockfile(lockfilePath, updatedLockfile);
5051
+ writeLockfile2(lockfilePath, updatedLockfile);
4554
5052
  const result = {
4555
5053
  removed: true,
4556
5054
  name: packageName,
@@ -4562,7 +5060,7 @@ async function runUninstall(skillName, options) {
4562
5060
  return result;
4563
5061
  }
4564
5062
  function createUninstallCommand() {
4565
- const cmd = new Command49("uninstall");
5063
+ const cmd = new Command51("uninstall");
4566
5064
  cmd.description("Uninstall a community skill").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--force", "Remove even if other skills depend on this one").action(async (skill, opts) => {
4567
5065
  try {
4568
5066
  const result = await runUninstall(skill, opts);
@@ -4581,13 +5079,13 @@ function createUninstallCommand() {
4581
5079
  }
4582
5080
 
4583
5081
  // src/commands/orchestrator.ts
4584
- import { Command as Command50 } from "commander";
4585
- import * as path39 from "path";
5082
+ import { Command as Command52 } from "commander";
5083
+ import * as path43 from "path";
4586
5084
  import { Orchestrator, WorkflowLoader, launchTUI } from "@harness-engineering/orchestrator";
4587
5085
  function createOrchestratorCommand() {
4588
- const orchestrator = new Command50("orchestrator");
5086
+ const orchestrator = new Command52("orchestrator");
4589
5087
  orchestrator.command("run").description("Run the orchestrator daemon").option("-w, --workflow <path>", "Path to WORKFLOW.md", "WORKFLOW.md").action(async (opts) => {
4590
- const workflowPath = path39.resolve(process.cwd(), opts.workflow);
5088
+ const workflowPath = path43.resolve(process.cwd(), opts.workflow);
4591
5089
  const loader = new WorkflowLoader();
4592
5090
  const result = await loader.loadWorkflow(workflowPath);
4593
5091
  if (!result.ok) {
@@ -4612,7 +5110,7 @@ function createOrchestratorCommand() {
4612
5110
 
4613
5111
  // src/index.ts
4614
5112
  function createProgram() {
4615
- const program = new Command51();
5113
+ const program = new Command53();
4616
5114
  program.name("harness").description("CLI for Harness Engineering toolkit").version(CLI_VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
4617
5115
  program.addCommand(createValidateCommand());
4618
5116
  program.addCommand(createCheckDepsCommand());
@@ -4647,6 +5145,8 @@ function createProgram() {
4647
5145
  program.addCommand(createBlueprintCommand());
4648
5146
  program.addCommand(createShareCommand());
4649
5147
  program.addCommand(createInstallCommand());
5148
+ program.addCommand(createInstallConstraintsCommand());
5149
+ program.addCommand(createUninstallConstraintsCommand());
4650
5150
  program.addCommand(createUninstallCommand());
4651
5151
  program.addCommand(createOrchestratorCommand());
4652
5152
  return program;
@@ -4662,6 +5162,8 @@ export {
4662
5162
  runImpactPreview,
4663
5163
  runCheckArch,
4664
5164
  runInstall,
5165
+ runInstallConstraints,
5166
+ runUninstallConstraints,
4665
5167
  runUninstall,
4666
5168
  createProgram
4667
5169
  };