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