@harness-engineering/cli 1.4.0 → 1.6.1

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 (82) hide show
  1. package/dist/agents/personas/architecture-enforcer.yaml +1 -0
  2. package/dist/agents/personas/code-reviewer.yaml +43 -0
  3. package/dist/agents/personas/codebase-health-analyst.yaml +32 -0
  4. package/dist/agents/personas/documentation-maintainer.yaml +2 -0
  5. package/dist/agents/personas/entropy-cleaner.yaml +3 -0
  6. package/dist/agents/personas/graph-maintainer.yaml +27 -0
  7. package/dist/agents/personas/parallel-coordinator.yaml +29 -0
  8. package/dist/agents/personas/performance-guardian.yaml +26 -0
  9. package/dist/agents/personas/security-reviewer.yaml +35 -0
  10. package/dist/agents/personas/task-executor.yaml +41 -0
  11. package/dist/agents/skills/README.md +8 -0
  12. package/dist/agents/skills/claude-code/add-harness-component/SKILL.md +10 -0
  13. package/dist/agents/skills/claude-code/align-documentation/SKILL.md +19 -0
  14. package/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +19 -0
  15. package/dist/agents/skills/claude-code/detect-doc-drift/SKILL.md +8 -0
  16. package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +9 -0
  17. package/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +9 -0
  18. package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +494 -0
  19. package/dist/agents/skills/claude-code/harness-autopilot/skill.yaml +52 -0
  20. package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +25 -0
  21. package/dist/agents/skills/claude-code/harness-debugging/SKILL.md +10 -0
  22. package/dist/agents/skills/claude-code/harness-dependency-health/SKILL.md +150 -0
  23. package/dist/agents/skills/claude-code/harness-dependency-health/skill.yaml +41 -0
  24. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +19 -0
  25. package/dist/agents/skills/claude-code/harness-hotspot-detector/SKILL.md +135 -0
  26. package/dist/agents/skills/claude-code/harness-hotspot-detector/skill.yaml +44 -0
  27. package/dist/agents/skills/claude-code/harness-impact-analysis/SKILL.md +139 -0
  28. package/dist/agents/skills/claude-code/harness-impact-analysis/skill.yaml +44 -0
  29. package/dist/agents/skills/claude-code/harness-integrity/SKILL.md +20 -6
  30. package/dist/agents/skills/claude-code/harness-knowledge-mapper/SKILL.md +154 -0
  31. package/dist/agents/skills/claude-code/harness-knowledge-mapper/skill.yaml +49 -0
  32. package/dist/agents/skills/claude-code/harness-onboarding/SKILL.md +10 -0
  33. package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +9 -0
  34. package/dist/agents/skills/claude-code/harness-perf/SKILL.md +231 -0
  35. package/dist/agents/skills/claude-code/harness-perf/skill.yaml +47 -0
  36. package/dist/agents/skills/claude-code/harness-perf-tdd/SKILL.md +236 -0
  37. package/dist/agents/skills/claude-code/harness-perf-tdd/skill.yaml +47 -0
  38. package/dist/agents/skills/claude-code/harness-planning/SKILL.md +9 -0
  39. package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +33 -2
  40. package/dist/agents/skills/claude-code/harness-refactoring/SKILL.md +19 -0
  41. package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +657 -0
  42. package/dist/agents/skills/claude-code/harness-release-readiness/skill.yaml +57 -0
  43. package/dist/agents/skills/claude-code/harness-security-review/SKILL.md +206 -0
  44. package/dist/agents/skills/claude-code/harness-security-review/skill.yaml +50 -0
  45. package/dist/agents/skills/claude-code/harness-security-scan/SKILL.md +102 -0
  46. package/dist/agents/skills/claude-code/harness-security-scan/skill.yaml +41 -0
  47. package/dist/agents/skills/claude-code/harness-state-management/SKILL.md +22 -8
  48. package/dist/agents/skills/claude-code/harness-tdd/SKILL.md +10 -0
  49. package/dist/agents/skills/claude-code/harness-test-advisor/SKILL.md +131 -0
  50. package/dist/agents/skills/claude-code/harness-test-advisor/skill.yaml +44 -0
  51. package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +10 -0
  52. package/dist/agents/skills/claude-code/validate-context-engineering/SKILL.md +9 -0
  53. package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +494 -0
  54. package/dist/agents/skills/gemini-cli/harness-autopilot/skill.yaml +52 -0
  55. package/dist/agents/skills/gemini-cli/harness-dependency-health/SKILL.md +150 -0
  56. package/dist/agents/skills/gemini-cli/harness-dependency-health/skill.yaml +41 -0
  57. package/dist/agents/skills/gemini-cli/harness-hotspot-detector/SKILL.md +135 -0
  58. package/dist/agents/skills/gemini-cli/harness-hotspot-detector/skill.yaml +44 -0
  59. package/dist/agents/skills/gemini-cli/harness-impact-analysis/SKILL.md +139 -0
  60. package/dist/agents/skills/gemini-cli/harness-impact-analysis/skill.yaml +44 -0
  61. package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/SKILL.md +154 -0
  62. package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/skill.yaml +49 -0
  63. package/dist/agents/skills/gemini-cli/harness-perf/SKILL.md +231 -0
  64. package/dist/agents/skills/gemini-cli/harness-perf/skill.yaml +47 -0
  65. package/dist/agents/skills/gemini-cli/harness-perf-tdd/SKILL.md +236 -0
  66. package/dist/agents/skills/gemini-cli/harness-perf-tdd/skill.yaml +47 -0
  67. package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +657 -0
  68. package/dist/agents/skills/gemini-cli/harness-release-readiness/skill.yaml +57 -0
  69. package/dist/agents/skills/gemini-cli/harness-security-review/skill.yaml +50 -0
  70. package/dist/agents/skills/gemini-cli/harness-security-scan/SKILL.md +102 -0
  71. package/dist/agents/skills/gemini-cli/harness-security-scan/skill.yaml +41 -0
  72. package/dist/agents/skills/gemini-cli/harness-test-advisor/SKILL.md +131 -0
  73. package/dist/agents/skills/gemini-cli/harness-test-advisor/skill.yaml +44 -0
  74. package/dist/agents/skills/tests/platform-parity.test.ts +131 -0
  75. package/dist/agents/skills/tests/schema.ts +2 -0
  76. package/dist/bin/harness.js +2 -2
  77. package/dist/{chunk-EFZOLZFB.js → chunk-ACMDUQJG.js} +4 -2
  78. package/dist/{chunk-C3J2HW4Y.js → chunk-O6NEKDYP.js} +2002 -487
  79. package/dist/{create-skill-4GKJZB5R.js → create-skill-NZDLMMR6.js} +1 -1
  80. package/dist/index.d.ts +265 -143
  81. package/dist/index.js +30 -4
  82. package/package.json +3 -2
@@ -5,10 +5,10 @@ import {
5
5
  createCreateSkillCommand,
6
6
  handleError,
7
7
  logger
8
- } from "./chunk-EFZOLZFB.js";
8
+ } from "./chunk-ACMDUQJG.js";
9
9
 
10
10
  // src/index.ts
11
- import { Command as Command33 } from "commander";
11
+ import { Command as Command43 } from "commander";
12
12
  import { VERSION } from "@harness-engineering/core";
13
13
 
14
14
  // src/commands/validate.ts
@@ -400,10 +400,404 @@ function createCheckDepsCommand() {
400
400
  return command;
401
401
  }
402
402
 
403
- // src/commands/check-docs.ts
403
+ // src/commands/check-perf.ts
404
404
  import { Command as Command3 } from "commander";
405
405
  import * as path4 from "path";
406
- import { Ok as Ok4, Err as Err2 } from "@harness-engineering/core";
406
+ import { Ok as Ok4, EntropyAnalyzer } from "@harness-engineering/core";
407
+ async function runCheckPerf(cwd, options) {
408
+ const runAll = !options.structural && !options.size && !options.coupling;
409
+ const analyzer = new EntropyAnalyzer({
410
+ rootDir: path4.resolve(cwd),
411
+ analyze: {
412
+ complexity: runAll || !!options.structural,
413
+ coupling: runAll || !!options.coupling,
414
+ sizeBudget: runAll || !!options.size
415
+ }
416
+ });
417
+ const analysisResult = await analyzer.analyze();
418
+ if (!analysisResult.ok) {
419
+ return Ok4({
420
+ valid: false,
421
+ violations: [
422
+ {
423
+ tier: 1,
424
+ severity: "error",
425
+ metric: "analysis-error",
426
+ file: "",
427
+ value: 0,
428
+ threshold: 0,
429
+ message: `Analysis failed: ${analysisResult.error.message}`
430
+ }
431
+ ],
432
+ stats: { filesAnalyzed: 0, violationCount: 1, errorCount: 1, warningCount: 0, infoCount: 0 }
433
+ });
434
+ }
435
+ const report = analysisResult.value;
436
+ const violations = [];
437
+ if (report.complexity) {
438
+ for (const v of report.complexity.violations) {
439
+ violations.push({
440
+ tier: v.tier,
441
+ severity: v.severity,
442
+ metric: v.metric,
443
+ file: v.file,
444
+ value: v.value,
445
+ threshold: v.threshold,
446
+ message: v.message || `[Tier ${v.tier}] ${v.metric}: ${v.function} (${v.value} > ${v.threshold})`
447
+ });
448
+ }
449
+ }
450
+ if (report.coupling) {
451
+ for (const v of report.coupling.violations) {
452
+ violations.push({
453
+ tier: v.tier,
454
+ severity: v.severity,
455
+ metric: v.metric,
456
+ file: v.file,
457
+ value: v.value,
458
+ threshold: v.threshold,
459
+ message: v.message || `[Tier ${v.tier}] ${v.metric}: ${v.file} (${v.value} > ${v.threshold})`
460
+ });
461
+ }
462
+ }
463
+ if (report.sizeBudget) {
464
+ for (const v of report.sizeBudget.violations) {
465
+ violations.push({
466
+ tier: v.tier,
467
+ severity: v.severity,
468
+ metric: "sizeBudget",
469
+ file: v.package,
470
+ value: v.currentSize,
471
+ threshold: v.budgetSize,
472
+ message: `[Tier ${v.tier}] Size: ${v.package} (${v.currentSize}B > ${v.budgetSize}B)`
473
+ });
474
+ }
475
+ }
476
+ const hasErrors = violations.some((v) => v.severity === "error");
477
+ const errorCount = violations.filter((v) => v.severity === "error").length;
478
+ const warningCount = violations.filter((v) => v.severity === "warning").length;
479
+ const infoCount = violations.filter((v) => v.severity === "info").length;
480
+ return Ok4({
481
+ valid: !hasErrors,
482
+ violations,
483
+ stats: {
484
+ filesAnalyzed: report.complexity?.stats.filesAnalyzed ?? 0,
485
+ violationCount: violations.length,
486
+ errorCount,
487
+ warningCount,
488
+ infoCount
489
+ }
490
+ });
491
+ }
492
+ function createCheckPerfCommand() {
493
+ const command = new Command3("check-perf").description("Run performance checks: structural complexity, coupling, and size budgets").option("--structural", "Run structural complexity checks only").option("--coupling", "Run coupling metric checks only").option("--size", "Run size budget checks only").action(async (opts, cmd) => {
494
+ const globalOpts = cmd.optsWithGlobals();
495
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
496
+ const formatter = new OutputFormatter(mode);
497
+ const result = await runCheckPerf(process.cwd(), {
498
+ structural: opts.structural,
499
+ coupling: opts.coupling,
500
+ size: opts.size
501
+ });
502
+ if (!result.ok) {
503
+ if (mode === OutputMode.JSON) {
504
+ console.log(JSON.stringify({ error: result.error.message }));
505
+ } else {
506
+ logger.error(result.error.message);
507
+ }
508
+ process.exit(ExitCode.ERROR);
509
+ }
510
+ const issues = result.value.violations.map((v) => ({
511
+ file: v.file,
512
+ message: v.message
513
+ }));
514
+ const output = formatter.formatValidation({
515
+ valid: result.value.valid,
516
+ issues
517
+ });
518
+ if (output) {
519
+ console.log(output);
520
+ }
521
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
522
+ });
523
+ return command;
524
+ }
525
+
526
+ // src/commands/check-security.ts
527
+ import { Command as Command4 } from "commander";
528
+ import * as path5 from "path";
529
+ import { execSync } from "child_process";
530
+ import { Ok as Ok5, SecurityScanner, parseSecurityConfig } from "@harness-engineering/core";
531
+ var SEVERITY_RANK = {
532
+ error: 3,
533
+ warning: 2,
534
+ info: 1
535
+ };
536
+ function getChangedFiles(cwd) {
537
+ try {
538
+ const output = execSync("git diff --name-only HEAD~1", {
539
+ cwd,
540
+ encoding: "utf-8"
541
+ });
542
+ return output.trim().split("\n").filter((f) => f.length > 0).map((f) => path5.resolve(cwd, f));
543
+ } catch {
544
+ return [];
545
+ }
546
+ }
547
+ async function runCheckSecurity(cwd, options) {
548
+ const projectRoot = path5.resolve(cwd);
549
+ let configData = {};
550
+ try {
551
+ const fs22 = await import("fs");
552
+ const configPath = path5.join(projectRoot, "harness.config.json");
553
+ if (fs22.existsSync(configPath)) {
554
+ const raw = fs22.readFileSync(configPath, "utf-8");
555
+ const parsed = JSON.parse(raw);
556
+ configData = parsed.security ?? {};
557
+ }
558
+ } catch {
559
+ }
560
+ const securityConfig = parseSecurityConfig(configData);
561
+ const scanner = new SecurityScanner(securityConfig);
562
+ scanner.configureForProject(projectRoot);
563
+ let filesToScan;
564
+ if (options.changedOnly) {
565
+ filesToScan = getChangedFiles(projectRoot);
566
+ } else {
567
+ const { glob: glob2 } = await import("glob");
568
+ const pattern = "**/*.{ts,tsx,js,jsx,go,py,java,rb}";
569
+ const ignore = securityConfig.exclude ?? [
570
+ "**/node_modules/**",
571
+ "**/dist/**",
572
+ "**/*.test.ts",
573
+ "**/fixtures/**"
574
+ ];
575
+ filesToScan = await glob2(pattern, { cwd: projectRoot, absolute: true, ignore });
576
+ }
577
+ const result = await scanner.scanFiles(filesToScan);
578
+ const threshold = options.severity ?? "warning";
579
+ const thresholdRank = SEVERITY_RANK[threshold];
580
+ const filtered = result.findings.filter((f) => SEVERITY_RANK[f.severity] >= thresholdRank);
581
+ const hasErrors = filtered.some((f) => f.severity === "error");
582
+ return Ok5({
583
+ valid: !hasErrors,
584
+ findings: filtered,
585
+ stats: {
586
+ filesScanned: result.scannedFiles,
587
+ rulesApplied: result.rulesApplied,
588
+ errorCount: filtered.filter((f) => f.severity === "error").length,
589
+ warningCount: filtered.filter((f) => f.severity === "warning").length,
590
+ infoCount: filtered.filter((f) => f.severity === "info").length
591
+ }
592
+ });
593
+ }
594
+ function createCheckSecurityCommand() {
595
+ const command = new Command4("check-security").description("Run lightweight security scan: secrets, injection, XSS, weak crypto").option("--severity <level>", "Minimum severity threshold", "warning").hook("preAction", (thisCommand) => {
596
+ const severity = thisCommand.opts().severity;
597
+ if (!["error", "warning", "info"].includes(severity)) {
598
+ logger.error(`Invalid severity: "${severity}". Must be one of: error, warning, info`);
599
+ process.exit(ExitCode.ERROR);
600
+ }
601
+ }).option("--changed-only", "Only scan git-changed files").action(async (opts, cmd) => {
602
+ const globalOpts = cmd.optsWithGlobals();
603
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
604
+ const formatter = new OutputFormatter(mode);
605
+ const result = await runCheckSecurity(process.cwd(), {
606
+ severity: opts.severity,
607
+ changedOnly: opts.changedOnly
608
+ });
609
+ if (!result.ok) {
610
+ if (mode === OutputMode.JSON) {
611
+ console.log(JSON.stringify({ error: result.error.message }));
612
+ } else {
613
+ logger.error(result.error.message);
614
+ }
615
+ process.exit(ExitCode.ERROR);
616
+ }
617
+ const issues = result.value.findings.map((f) => ({
618
+ file: `${f.file}:${f.line}`,
619
+ message: `[${f.ruleId}] ${f.severity.toUpperCase()} ${f.message}`
620
+ }));
621
+ const output = formatter.formatValidation({
622
+ valid: result.value.valid,
623
+ issues
624
+ });
625
+ if (output) {
626
+ console.log(output);
627
+ }
628
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
629
+ });
630
+ return command;
631
+ }
632
+
633
+ // src/commands/perf.ts
634
+ import { Command as Command5 } from "commander";
635
+ import * as path6 from "path";
636
+ import { BaselineManager, CriticalPathResolver } from "@harness-engineering/core";
637
+ function createPerfCommand() {
638
+ const perf = new Command5("perf").description("Performance benchmark and baseline management");
639
+ perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob2, _opts, cmd) => {
640
+ const globalOpts = cmd.optsWithGlobals();
641
+ const cwd = process.cwd();
642
+ const { BenchmarkRunner } = await import("@harness-engineering/core");
643
+ const runner = new BenchmarkRunner();
644
+ const benchFiles = runner.discover(cwd, glob2);
645
+ if (benchFiles.length === 0) {
646
+ if (globalOpts.json) {
647
+ console.log(JSON.stringify({ benchFiles: [], message: "No .bench.ts files found" }));
648
+ } else {
649
+ logger.info("No .bench.ts files found. Create *.bench.ts files to add benchmarks.");
650
+ }
651
+ return;
652
+ }
653
+ if (globalOpts.json) {
654
+ logger.info(`Found ${benchFiles.length} benchmark file(s). Running...`);
655
+ } else {
656
+ logger.info(`Found ${benchFiles.length} benchmark file(s):`);
657
+ for (const f of benchFiles) {
658
+ logger.info(` ${f}`);
659
+ }
660
+ logger.info("Running benchmarks...");
661
+ }
662
+ const result = await runner.run(glob2 ? { cwd, glob: glob2 } : { cwd });
663
+ if (globalOpts.json) {
664
+ console.log(JSON.stringify({ results: result.results, success: result.success }));
665
+ } else {
666
+ if (result.success && result.results.length > 0) {
667
+ logger.info(`
668
+ Results (${result.results.length} benchmarks):`);
669
+ for (const r of result.results) {
670
+ logger.info(
671
+ ` ${r.file}::${r.name}: ${r.opsPerSec} ops/s (mean: ${r.meanMs.toFixed(2)}ms)`
672
+ );
673
+ }
674
+ logger.info("\nTo save as baselines: harness perf baselines update");
675
+ } else {
676
+ logger.info("Benchmark run completed. Check output above for details.");
677
+ if (result.rawOutput) {
678
+ console.log(result.rawOutput);
679
+ }
680
+ }
681
+ }
682
+ });
683
+ const baselines = perf.command("baselines").description("Manage performance baselines");
684
+ baselines.command("show").description("Display current baselines").action(async (_opts, cmd) => {
685
+ const globalOpts = cmd.optsWithGlobals();
686
+ const cwd = process.cwd();
687
+ const manager = new BaselineManager(cwd);
688
+ const data = manager.load();
689
+ if (!data) {
690
+ if (globalOpts.json) {
691
+ console.log(JSON.stringify({ baselines: null, message: "No baselines file found" }));
692
+ } else {
693
+ logger.info("No baselines file found at .harness/perf/baselines.json");
694
+ }
695
+ return;
696
+ }
697
+ if (globalOpts.json) {
698
+ console.log(JSON.stringify(data, null, 2));
699
+ } else {
700
+ logger.info(`Baselines (updated: ${data.updatedAt}, from: ${data.updatedFrom})`);
701
+ for (const [name, baseline] of Object.entries(data.benchmarks)) {
702
+ logger.info(
703
+ ` ${name}: ${baseline.opsPerSec} ops/s (mean: ${baseline.meanMs}ms, p99: ${baseline.p99Ms}ms)`
704
+ );
705
+ }
706
+ }
707
+ });
708
+ baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
709
+ const globalOpts = cmd.optsWithGlobals();
710
+ const cwd = process.cwd();
711
+ const { BenchmarkRunner } = await import("@harness-engineering/core");
712
+ const runner = new BenchmarkRunner();
713
+ const manager = new BaselineManager(cwd);
714
+ logger.info("Running benchmarks to update baselines...");
715
+ const benchResult = await runner.run({ cwd });
716
+ if (!benchResult.success || benchResult.results.length === 0) {
717
+ logger.error(
718
+ "No benchmark results to save. Run `harness perf bench` first to verify benchmarks work."
719
+ );
720
+ return;
721
+ }
722
+ let commitHash = "unknown";
723
+ try {
724
+ const { execSync: execSync5 } = await import("child_process");
725
+ commitHash = execSync5("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
726
+ } catch {
727
+ }
728
+ manager.save(benchResult.results, commitHash);
729
+ if (globalOpts.json) {
730
+ console.log(JSON.stringify({ updated: benchResult.results.length, commitHash }));
731
+ } else {
732
+ logger.info(`Updated ${benchResult.results.length} baseline(s) from commit ${commitHash}`);
733
+ logger.info("Baselines saved to .harness/perf/baselines.json");
734
+ }
735
+ });
736
+ perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
737
+ const globalOpts = cmd.optsWithGlobals();
738
+ const cwd = process.cwd();
739
+ const { EntropyAnalyzer: EntropyAnalyzer3 } = await import("@harness-engineering/core");
740
+ const analyzer = new EntropyAnalyzer3({
741
+ rootDir: path6.resolve(cwd),
742
+ analyze: { complexity: true, coupling: true }
743
+ });
744
+ const result = await analyzer.analyze();
745
+ if (!result.ok) {
746
+ logger.error(result.error.message);
747
+ return;
748
+ }
749
+ const report = result.value;
750
+ if (globalOpts.json) {
751
+ console.log(
752
+ JSON.stringify(
753
+ {
754
+ complexity: report.complexity,
755
+ coupling: report.coupling,
756
+ sizeBudget: report.sizeBudget
757
+ },
758
+ null,
759
+ 2
760
+ )
761
+ );
762
+ } else {
763
+ logger.info("=== Performance Report ===");
764
+ if (report.complexity) {
765
+ logger.info(
766
+ `Complexity: ${report.complexity.stats.violationCount} violations (${report.complexity.stats.errorCount} errors, ${report.complexity.stats.warningCount} warnings)`
767
+ );
768
+ }
769
+ if (report.coupling) {
770
+ logger.info(
771
+ `Coupling: ${report.coupling.stats.violationCount} violations (${report.coupling.stats.warningCount} warnings)`
772
+ );
773
+ }
774
+ }
775
+ });
776
+ perf.command("critical-paths").description("Show resolved critical path set (annotations + graph inference)").action(async (_opts, cmd) => {
777
+ const globalOpts = cmd.optsWithGlobals();
778
+ const cwd = process.cwd();
779
+ const resolver = new CriticalPathResolver(cwd);
780
+ const result = await resolver.resolve();
781
+ if (globalOpts.json) {
782
+ console.log(JSON.stringify(result, null, 2));
783
+ } else {
784
+ logger.info(
785
+ `Critical paths: ${result.stats.total} (${result.stats.annotated} annotated, ${result.stats.graphInferred} graph-inferred)`
786
+ );
787
+ for (const entry of result.entries) {
788
+ logger.info(
789
+ ` ${entry.file}::${entry.function} [${entry.source}]${entry.fanIn ? ` (fan-in: ${entry.fanIn})` : ""}`
790
+ );
791
+ }
792
+ }
793
+ });
794
+ return perf;
795
+ }
796
+
797
+ // src/commands/check-docs.ts
798
+ import { Command as Command6 } from "commander";
799
+ import * as path7 from "path";
800
+ import { Ok as Ok6, Err as Err2 } from "@harness-engineering/core";
407
801
  import { checkDocCoverage, validateKnowledgeMap as validateKnowledgeMap2 } from "@harness-engineering/core";
408
802
  async function runCheckDocs(options) {
409
803
  const cwd = options.cwd ?? process.cwd();
@@ -413,8 +807,8 @@ async function runCheckDocs(options) {
413
807
  return configResult;
414
808
  }
415
809
  const config = configResult.value;
416
- const docsDir = path4.resolve(cwd, config.docsDir);
417
- const sourceDir = path4.resolve(cwd, config.rootDir);
810
+ const docsDir = path7.resolve(cwd, config.docsDir);
811
+ const sourceDir = path7.resolve(cwd, config.rootDir);
418
812
  const coverageResult = await checkDocCoverage("project", {
419
813
  docsDir,
420
814
  sourceDir,
@@ -443,10 +837,10 @@ async function runCheckDocs(options) {
443
837
  undocumented: coverageResult.value.undocumented,
444
838
  brokenLinks
445
839
  };
446
- return Ok4(result);
840
+ return Ok6(result);
447
841
  }
448
842
  function createCheckDocsCommand() {
449
- const command = new Command3("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
843
+ const command = new Command6("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
450
844
  const globalOpts = cmd.optsWithGlobals();
451
845
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
452
846
  const formatter = new OutputFormatter(mode);
@@ -498,17 +892,17 @@ function createCheckDocsCommand() {
498
892
  }
499
893
 
500
894
  // src/commands/init.ts
501
- import { Command as Command5 } from "commander";
895
+ import { Command as Command8 } from "commander";
502
896
  import chalk3 from "chalk";
503
897
  import * as fs5 from "fs";
504
- import * as path8 from "path";
505
- import { Ok as Ok6, Err as Err4 } from "@harness-engineering/core";
898
+ import * as path11 from "path";
899
+ import { Ok as Ok8, Err as Err4 } from "@harness-engineering/core";
506
900
 
507
901
  // src/templates/engine.ts
508
902
  import * as fs2 from "fs";
509
- import * as path5 from "path";
903
+ import * as path8 from "path";
510
904
  import Handlebars from "handlebars";
511
- import { Ok as Ok5, Err as Err3 } from "@harness-engineering/core";
905
+ import { Ok as Ok7, Err as Err3 } from "@harness-engineering/core";
512
906
 
513
907
  // src/templates/schema.ts
514
908
  import { z as z2 } from "zod";
@@ -576,13 +970,13 @@ var TemplateEngine = class {
576
970
  const templates = [];
577
971
  for (const entry of entries) {
578
972
  if (!entry.isDirectory()) continue;
579
- const metaPath = path5.join(this.templatesDir, entry.name, "template.json");
973
+ const metaPath = path8.join(this.templatesDir, entry.name, "template.json");
580
974
  if (!fs2.existsSync(metaPath)) continue;
581
975
  const raw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
582
976
  const parsed = TemplateMetadataSchema.safeParse(raw);
583
977
  if (parsed.success) templates.push(parsed.data);
584
978
  }
585
- return Ok5(templates);
979
+ return Ok7(templates);
586
980
  } catch (error) {
587
981
  return Err3(
588
982
  new Error(
@@ -594,7 +988,7 @@ var TemplateEngine = class {
594
988
  resolveTemplate(level, framework) {
595
989
  const levelDir = this.findTemplateDir(level, "level");
596
990
  if (!levelDir) return Err3(new Error(`Template not found for level: ${level}`));
597
- const metaPath = path5.join(levelDir, "template.json");
991
+ const metaPath = path8.join(levelDir, "template.json");
598
992
  const metaRaw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
599
993
  const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
600
994
  if (!metaResult.success)
@@ -602,7 +996,7 @@ var TemplateEngine = class {
602
996
  const metadata = metaResult.data;
603
997
  let files = [];
604
998
  if (metadata.extends) {
605
- const baseDir = path5.join(this.templatesDir, metadata.extends);
999
+ const baseDir = path8.join(this.templatesDir, metadata.extends);
606
1000
  if (fs2.existsSync(baseDir)) files = this.collectFiles(baseDir, metadata.extends);
607
1001
  }
608
1002
  const levelFiles = this.collectFiles(levelDir, level);
@@ -611,7 +1005,7 @@ var TemplateEngine = class {
611
1005
  if (framework) {
612
1006
  const frameworkDir = this.findTemplateDir(framework, "framework");
613
1007
  if (!frameworkDir) return Err3(new Error(`Framework template not found: ${framework}`));
614
- const fMetaPath = path5.join(frameworkDir, "template.json");
1008
+ const fMetaPath = path8.join(frameworkDir, "template.json");
615
1009
  const fMetaRaw = JSON.parse(fs2.readFileSync(fMetaPath, "utf-8"));
616
1010
  const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
617
1011
  if (fMetaResult.success) overlayMetadata = fMetaResult.data;
@@ -621,7 +1015,7 @@ var TemplateEngine = class {
621
1015
  files = files.filter((f) => f.relativePath !== "template.json");
622
1016
  const resolved = { metadata, files };
623
1017
  if (overlayMetadata !== void 0) resolved.overlayMetadata = overlayMetadata;
624
- return Ok5(resolved);
1018
+ return Ok7(resolved);
625
1019
  }
626
1020
  render(template, context) {
627
1021
  const rendered = [];
@@ -673,20 +1067,20 @@ var TemplateEngine = class {
673
1067
  const msg = error instanceof Error ? error.message : String(error);
674
1068
  return Err3(new Error(`JSON merge failed: ${msg}`));
675
1069
  }
676
- return Ok5({ files: rendered });
1070
+ return Ok7({ files: rendered });
677
1071
  }
678
1072
  write(files, targetDir, options) {
679
1073
  try {
680
1074
  const written = [];
681
1075
  for (const file of files.files) {
682
- const targetPath = path5.join(targetDir, file.relativePath);
683
- const dir = path5.dirname(targetPath);
1076
+ const targetPath = path8.join(targetDir, file.relativePath);
1077
+ const dir = path8.dirname(targetPath);
684
1078
  if (!options.overwrite && fs2.existsSync(targetPath)) continue;
685
1079
  fs2.mkdirSync(dir, { recursive: true });
686
1080
  fs2.writeFileSync(targetPath, file.content);
687
1081
  written.push(file.relativePath);
688
1082
  }
689
- return Ok5(written);
1083
+ return Ok7(written);
690
1084
  } catch (error) {
691
1085
  return Err3(
692
1086
  new Error(
@@ -699,16 +1093,16 @@ var TemplateEngine = class {
699
1093
  const entries = fs2.readdirSync(this.templatesDir, { withFileTypes: true });
700
1094
  for (const entry of entries) {
701
1095
  if (!entry.isDirectory()) continue;
702
- const metaPath = path5.join(this.templatesDir, entry.name, "template.json");
1096
+ const metaPath = path8.join(this.templatesDir, entry.name, "template.json");
703
1097
  if (!fs2.existsSync(metaPath)) continue;
704
1098
  const raw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
705
1099
  const parsed = TemplateMetadataSchema.safeParse(raw);
706
1100
  if (!parsed.success) continue;
707
1101
  if (type === "level" && parsed.data.level === name)
708
- return path5.join(this.templatesDir, entry.name);
1102
+ return path8.join(this.templatesDir, entry.name);
709
1103
  if (type === "framework" && parsed.data.framework === name)
710
- return path5.join(this.templatesDir, entry.name);
711
- if (parsed.data.name === name) return path5.join(this.templatesDir, entry.name);
1104
+ return path8.join(this.templatesDir, entry.name);
1105
+ if (parsed.data.name === name) return path8.join(this.templatesDir, entry.name);
712
1106
  }
713
1107
  return null;
714
1108
  }
@@ -717,12 +1111,12 @@ var TemplateEngine = class {
717
1111
  const walk = (currentDir) => {
718
1112
  const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
719
1113
  for (const entry of entries) {
720
- const fullPath = path5.join(currentDir, entry.name);
1114
+ const fullPath = path8.join(currentDir, entry.name);
721
1115
  if (entry.isDirectory()) {
722
1116
  walk(fullPath);
723
1117
  } else {
724
1118
  files.push({
725
- relativePath: path5.relative(dir, fullPath),
1119
+ relativePath: path8.relative(dir, fullPath),
726
1120
  absolutePath: fullPath,
727
1121
  isHandlebars: entry.name.endsWith(".hbs"),
728
1122
  sourceTemplate: sourceName
@@ -754,45 +1148,68 @@ var TemplateEngine = class {
754
1148
 
755
1149
  // src/utils/paths.ts
756
1150
  import * as fs3 from "fs";
757
- import * as path6 from "path";
1151
+ import * as path9 from "path";
758
1152
  import { fileURLToPath } from "url";
759
1153
  var __filename = fileURLToPath(import.meta.url);
760
- var __dirname = path6.dirname(__filename);
1154
+ var __dirname = path9.dirname(__filename);
761
1155
  function findUpDir(targetName, marker, maxLevels = 8) {
762
1156
  let dir = __dirname;
763
1157
  for (let i = 0; i < maxLevels; i++) {
764
- const candidate = path6.join(dir, targetName);
1158
+ const candidate = path9.join(dir, targetName);
765
1159
  if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
766
- if (fs3.existsSync(path6.join(candidate, marker))) {
1160
+ if (fs3.existsSync(path9.join(candidate, marker))) {
767
1161
  return candidate;
768
1162
  }
769
1163
  }
770
- dir = path6.dirname(dir);
1164
+ dir = path9.dirname(dir);
771
1165
  }
772
1166
  return null;
773
1167
  }
774
1168
  function resolveTemplatesDir() {
775
- return findUpDir("templates", "base") ?? path6.join(__dirname, "templates");
1169
+ return findUpDir("templates", "base") ?? path9.join(__dirname, "templates");
776
1170
  }
777
1171
  function resolvePersonasDir() {
778
1172
  const agentsDir = findUpDir("agents", "personas");
779
1173
  if (agentsDir) {
780
- return path6.join(agentsDir, "personas");
1174
+ return path9.join(agentsDir, "personas");
781
1175
  }
782
- return path6.join(__dirname, "agents", "personas");
1176
+ return path9.join(__dirname, "agents", "personas");
783
1177
  }
784
1178
  function resolveSkillsDir() {
785
1179
  const agentsDir = findUpDir("agents", "skills");
786
1180
  if (agentsDir) {
787
- return path6.join(agentsDir, "skills", "claude-code");
1181
+ return path9.join(agentsDir, "skills", "claude-code");
1182
+ }
1183
+ return path9.join(__dirname, "agents", "skills", "claude-code");
1184
+ }
1185
+ function resolveProjectSkillsDir(cwd) {
1186
+ let dir = cwd ?? process.cwd();
1187
+ for (let i = 0; i < 8; i++) {
1188
+ const candidate = path9.join(dir, "agents", "skills", "claude-code");
1189
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
1190
+ const agentsDir = path9.join(dir, "agents");
1191
+ if (fs3.existsSync(path9.join(agentsDir, "skills"))) {
1192
+ return candidate;
1193
+ }
1194
+ }
1195
+ const parent = path9.dirname(dir);
1196
+ if (parent === dir) break;
1197
+ dir = parent;
1198
+ }
1199
+ return null;
1200
+ }
1201
+ function resolveGlobalSkillsDir() {
1202
+ const agentsDir = findUpDir("agents", "skills");
1203
+ if (agentsDir) {
1204
+ return path9.join(agentsDir, "skills", "claude-code");
788
1205
  }
789
- return path6.join(__dirname, "agents", "skills", "claude-code");
1206
+ return path9.join(__dirname, "agents", "skills", "claude-code");
790
1207
  }
791
1208
 
792
1209
  // src/commands/setup-mcp.ts
793
- import { Command as Command4 } from "commander";
1210
+ import { Command as Command7 } from "commander";
794
1211
  import * as fs4 from "fs";
795
- import * as path7 from "path";
1212
+ import * as path10 from "path";
796
1213
  import * as os from "os";
797
1214
  import chalk2 from "chalk";
798
1215
  var HARNESS_MCP_ENTRY = {
@@ -809,7 +1226,7 @@ function readJsonFile(filePath) {
809
1226
  }
810
1227
  }
811
1228
  function writeJsonFile(filePath, data) {
812
- const dir = path7.dirname(filePath);
1229
+ const dir = path10.dirname(filePath);
813
1230
  if (!fs4.existsSync(dir)) {
814
1231
  fs4.mkdirSync(dir, { recursive: true });
815
1232
  }
@@ -828,7 +1245,7 @@ function configureMcpServer(configPath) {
828
1245
  return true;
829
1246
  }
830
1247
  function addGeminiTrustedFolder(cwd) {
831
- const trustedPath = path7.join(os.homedir(), ".gemini", "trustedFolders.json");
1248
+ const trustedPath = path10.join(os.homedir(), ".gemini", "trustedFolders.json");
832
1249
  const folders = readJsonFile(trustedPath) ?? {};
833
1250
  if (folders[cwd] === "TRUST_FOLDER") {
834
1251
  return false;
@@ -842,7 +1259,7 @@ function setupMcp(cwd, client) {
842
1259
  const skipped = [];
843
1260
  let trustedFolder = false;
844
1261
  if (client === "all" || client === "claude") {
845
- const configPath = path7.join(cwd, ".mcp.json");
1262
+ const configPath = path10.join(cwd, ".mcp.json");
846
1263
  if (configureMcpServer(configPath)) {
847
1264
  configured.push("Claude Code");
848
1265
  } else {
@@ -850,7 +1267,7 @@ function setupMcp(cwd, client) {
850
1267
  }
851
1268
  }
852
1269
  if (client === "all" || client === "gemini") {
853
- const configPath = path7.join(cwd, ".gemini", "settings.json");
1270
+ const configPath = path10.join(cwd, ".gemini", "settings.json");
854
1271
  if (configureMcpServer(configPath)) {
855
1272
  configured.push("Gemini CLI");
856
1273
  } else {
@@ -861,7 +1278,7 @@ function setupMcp(cwd, client) {
861
1278
  return { configured, skipped, trustedFolder };
862
1279
  }
863
1280
  function createSetupMcpCommand() {
864
- return new Command4("setup-mcp").description("Configure MCP server for AI agent integration").option("--client <client>", "Client to configure (claude, gemini, all)", "all").action(async (opts, cmd) => {
1281
+ return new Command7("setup-mcp").description("Configure MCP server for AI agent integration").option("--client <client>", "Client to configure (claude, gemini, all)", "all").action(async (opts, cmd) => {
865
1282
  const globalOpts = cmd.optsWithGlobals();
866
1283
  const cwd = process.cwd();
867
1284
  const { configured, skipped, trustedFolder } = setupMcp(cwd, opts.client);
@@ -887,8 +1304,12 @@ function createSetupMcpCommand() {
887
1304
  }
888
1305
  console.log("");
889
1306
  console.log(chalk2.bold("The harness MCP server provides:"));
890
- console.log(" - 15 tools for validation, entropy detection, and skill execution");
891
- console.log(" - 4 resources for project context, skills, rules, and learnings");
1307
+ console.log(
1308
+ " - 31 tools for validation, entropy detection, skill execution, graph querying, and more"
1309
+ );
1310
+ console.log(
1311
+ " - 8 resources for project context, skills, rules, learnings, state, and graph data"
1312
+ );
892
1313
  console.log("");
893
1314
  console.log(`Run ${chalk2.cyan("harness skill list")} to see available skills.`);
894
1315
  console.log("");
@@ -900,10 +1321,10 @@ function createSetupMcpCommand() {
900
1321
  // src/commands/init.ts
901
1322
  async function runInit(options) {
902
1323
  const cwd = options.cwd ?? process.cwd();
903
- const name = options.name ?? path8.basename(cwd);
1324
+ const name = options.name ?? path11.basename(cwd);
904
1325
  const level = options.level ?? "basic";
905
1326
  const force = options.force ?? false;
906
- const configPath = path8.join(cwd, "harness.config.json");
1327
+ const configPath = path11.join(cwd, "harness.config.json");
907
1328
  if (!force && fs5.existsSync(configPath)) {
908
1329
  return Err4(
909
1330
  new CLIError("Project already initialized. Use --force to overwrite.", ExitCode.ERROR)
@@ -927,10 +1348,10 @@ async function runInit(options) {
927
1348
  if (!writeResult.ok) {
928
1349
  return Err4(new CLIError(writeResult.error.message, ExitCode.ERROR));
929
1350
  }
930
- return Ok6({ filesCreated: writeResult.value });
1351
+ return Ok8({ filesCreated: writeResult.value });
931
1352
  }
932
1353
  function createInitCommand() {
933
- const command = new Command5("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Use defaults without prompting").action(async (opts, cmd) => {
1354
+ const command = new Command8("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Use defaults without prompting").action(async (opts, cmd) => {
934
1355
  const globalOpts = cmd.optsWithGlobals();
935
1356
  const result = await runInit({
936
1357
  name: opts.name,
@@ -972,9 +1393,9 @@ function createInitCommand() {
972
1393
  }
973
1394
 
974
1395
  // src/commands/cleanup.ts
975
- import { Command as Command6 } from "commander";
976
- import * as path9 from "path";
977
- import { Ok as Ok7, Err as Err5, EntropyAnalyzer } from "@harness-engineering/core";
1396
+ import { Command as Command9 } from "commander";
1397
+ import * as path12 from "path";
1398
+ import { Ok as Ok9, Err as Err5, EntropyAnalyzer as EntropyAnalyzer2 } from "@harness-engineering/core";
978
1399
  async function runCleanup(options) {
979
1400
  const cwd = options.cwd ?? process.cwd();
980
1401
  const type = options.type ?? "all";
@@ -989,11 +1410,11 @@ async function runCleanup(options) {
989
1410
  patternViolations: [],
990
1411
  totalIssues: 0
991
1412
  };
992
- const rootDir = path9.resolve(cwd, config.rootDir);
993
- const docsDir = path9.resolve(cwd, config.docsDir);
1413
+ const rootDir = path12.resolve(cwd, config.rootDir);
1414
+ const docsDir = path12.resolve(cwd, config.docsDir);
994
1415
  const entropyConfig = {
995
1416
  rootDir,
996
- entryPoints: [path9.join(rootDir, "src/index.ts")],
1417
+ entryPoints: [path12.join(rootDir, "src/index.ts")],
997
1418
  docPaths: [docsDir],
998
1419
  analyze: {
999
1420
  drift: type === "all" || type === "drift",
@@ -1002,7 +1423,7 @@ async function runCleanup(options) {
1002
1423
  },
1003
1424
  exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
1004
1425
  };
1005
- const analyzer = new EntropyAnalyzer(entropyConfig);
1426
+ const analyzer = new EntropyAnalyzer2(entropyConfig);
1006
1427
  const analysisResult = await analyzer.analyze();
1007
1428
  if (!analysisResult.ok) {
1008
1429
  return Err5(
@@ -1030,10 +1451,10 @@ async function runCleanup(options) {
1030
1451
  }));
1031
1452
  }
1032
1453
  result.totalIssues = result.driftIssues.length + result.deadCode.length + result.patternViolations.length;
1033
- return Ok7(result);
1454
+ return Ok9(result);
1034
1455
  }
1035
1456
  function createCleanupCommand() {
1036
- const command = new Command6("cleanup").description("Detect entropy issues (doc drift, dead code, patterns)").option("-t, --type <type>", "Issue type: drift, dead-code, patterns, all", "all").action(async (opts, cmd) => {
1457
+ const command = new Command9("cleanup").description("Detect entropy issues (doc drift, dead code, patterns)").option("-t, --type <type>", "Issue type: drift, dead-code, patterns, all", "all").action(async (opts, cmd) => {
1037
1458
  const globalOpts = cmd.optsWithGlobals();
1038
1459
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
1039
1460
  const formatter = new OutputFormatter(mode);
@@ -1093,10 +1514,10 @@ function createCleanupCommand() {
1093
1514
  }
1094
1515
 
1095
1516
  // src/commands/fix-drift.ts
1096
- import { Command as Command7 } from "commander";
1097
- import * as path10 from "path";
1517
+ import { Command as Command10 } from "commander";
1518
+ import * as path13 from "path";
1098
1519
  import {
1099
- Ok as Ok8,
1520
+ Ok as Ok10,
1100
1521
  Err as Err6,
1101
1522
  buildSnapshot,
1102
1523
  detectDocDrift,
@@ -1113,11 +1534,11 @@ async function runFixDrift(options) {
1113
1534
  return Err6(configResult.error);
1114
1535
  }
1115
1536
  const config = configResult.value;
1116
- const rootDir = path10.resolve(cwd, config.rootDir);
1117
- const docsDir = path10.resolve(cwd, config.docsDir);
1537
+ const rootDir = path13.resolve(cwd, config.rootDir);
1538
+ const docsDir = path13.resolve(cwd, config.docsDir);
1118
1539
  const entropyConfig = {
1119
1540
  rootDir,
1120
- entryPoints: [path10.join(rootDir, "src/index.ts")],
1541
+ entryPoints: [path13.join(rootDir, "src/index.ts")],
1121
1542
  docPaths: [docsDir],
1122
1543
  analyze: {
1123
1544
  drift: true,
@@ -1201,10 +1622,10 @@ async function runFixDrift(options) {
1201
1622
  fixes: appliedFixes,
1202
1623
  suggestions
1203
1624
  };
1204
- return Ok8(result);
1625
+ return Ok10(result);
1205
1626
  }
1206
1627
  function createFixDriftCommand() {
1207
- const command = new Command7("fix-drift").description("Auto-fix entropy issues (doc drift, dead code)").option("--no-dry-run", "Actually apply fixes (default is dry-run mode)").action(async (opts, cmd) => {
1628
+ const command = new Command10("fix-drift").description("Auto-fix entropy issues (doc drift, dead code)").option("--no-dry-run", "Actually apply fixes (default is dry-run mode)").action(async (opts, cmd) => {
1208
1629
  const globalOpts = cmd.optsWithGlobals();
1209
1630
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
1210
1631
  const formatter = new OutputFormatter(mode);
@@ -1264,27 +1685,41 @@ function createFixDriftCommand() {
1264
1685
  }
1265
1686
 
1266
1687
  // src/commands/agent/index.ts
1267
- import { Command as Command10 } from "commander";
1688
+ import { Command as Command13 } from "commander";
1268
1689
 
1269
1690
  // src/commands/agent/run.ts
1270
- import { Command as Command8 } from "commander";
1271
- import * as path12 from "path";
1691
+ import { Command as Command11 } from "commander";
1692
+ import * as path17 from "path";
1272
1693
  import * as childProcess from "child_process";
1273
- import { Ok as Ok10, Err as Err8 } from "@harness-engineering/core";
1694
+ import { Ok as Ok12, Err as Err8 } from "@harness-engineering/core";
1274
1695
  import { requestPeerReview } from "@harness-engineering/core";
1275
1696
 
1276
1697
  // src/persona/loader.ts
1277
1698
  import * as fs6 from "fs";
1278
- import * as path11 from "path";
1699
+ import * as path14 from "path";
1279
1700
  import YAML from "yaml";
1280
- import { Ok as Ok9, Err as Err7 } from "@harness-engineering/core";
1701
+ import { Ok as Ok11, Err as Err7 } from "@harness-engineering/core";
1281
1702
 
1282
1703
  // src/persona/schema.ts
1283
1704
  import { z as z3 } from "zod";
1705
+ var TriggerContextSchema = z3.enum(["always", "on_pr", "on_commit", "on_review", "scheduled", "manual", "on_plan_approved"]).default("always");
1706
+ var CommandStepSchema = z3.object({
1707
+ command: z3.string(),
1708
+ when: TriggerContextSchema
1709
+ });
1710
+ var SkillStepSchema = z3.object({
1711
+ skill: z3.string(),
1712
+ when: TriggerContextSchema,
1713
+ output: z3.enum(["inline", "artifact", "auto"]).default("auto")
1714
+ });
1715
+ var StepSchema = z3.union([CommandStepSchema, SkillStepSchema]);
1284
1716
  var PersonaTriggerSchema = z3.discriminatedUnion("event", [
1285
1717
  z3.object({
1286
1718
  event: z3.literal("on_pr"),
1287
- conditions: z3.object({ paths: z3.array(z3.string()).optional() }).optional()
1719
+ conditions: z3.object({
1720
+ paths: z3.array(z3.string()).optional(),
1721
+ min_files: z3.number().optional()
1722
+ }).optional()
1288
1723
  }),
1289
1724
  z3.object({
1290
1725
  event: z3.literal("on_commit"),
@@ -1293,6 +1728,9 @@ var PersonaTriggerSchema = z3.discriminatedUnion("event", [
1293
1728
  z3.object({
1294
1729
  event: z3.literal("scheduled"),
1295
1730
  cron: z3.string()
1731
+ }),
1732
+ z3.object({
1733
+ event: z3.literal("manual")
1296
1734
  })
1297
1735
  ]);
1298
1736
  var PersonaConfigSchema = z3.object({
@@ -1305,7 +1743,7 @@ var PersonaOutputsSchema = z3.object({
1305
1743
  "ci-workflow": z3.boolean().default(true),
1306
1744
  "runtime-config": z3.boolean().default(true)
1307
1745
  });
1308
- var PersonaSchema = z3.object({
1746
+ var PersonaSchemaV1 = z3.object({
1309
1747
  version: z3.literal(1),
1310
1748
  name: z3.string(),
1311
1749
  description: z3.string(),
@@ -1316,8 +1754,33 @@ var PersonaSchema = z3.object({
1316
1754
  config: PersonaConfigSchema.default({}),
1317
1755
  outputs: PersonaOutputsSchema.default({})
1318
1756
  });
1757
+ var PersonaSchemaV2 = z3.object({
1758
+ version: z3.literal(2),
1759
+ name: z3.string(),
1760
+ description: z3.string(),
1761
+ role: z3.string(),
1762
+ skills: z3.array(z3.string()),
1763
+ steps: z3.array(StepSchema),
1764
+ triggers: z3.array(PersonaTriggerSchema),
1765
+ config: PersonaConfigSchema.default({}),
1766
+ outputs: PersonaOutputsSchema.default({})
1767
+ });
1768
+ var PersonaSchema = z3.union([PersonaSchemaV1, PersonaSchemaV2]);
1319
1769
 
1320
1770
  // src/persona/loader.ts
1771
+ function normalizePersona(raw) {
1772
+ if (raw.version === 1 && Array.isArray(raw.commands)) {
1773
+ const { commands, ...rest } = raw;
1774
+ return {
1775
+ ...rest,
1776
+ steps: commands.map((cmd) => ({
1777
+ command: cmd,
1778
+ when: "always"
1779
+ }))
1780
+ };
1781
+ }
1782
+ return raw;
1783
+ }
1321
1784
  function loadPersona(filePath) {
1322
1785
  try {
1323
1786
  if (!fs6.existsSync(filePath)) {
@@ -1329,7 +1792,7 @@ function loadPersona(filePath) {
1329
1792
  if (!result.success) {
1330
1793
  return Err7(new Error(`Invalid persona ${filePath}: ${result.error.message}`));
1331
1794
  }
1332
- return Ok9(result.data);
1795
+ return Ok11(normalizePersona(result.data));
1333
1796
  } catch (error) {
1334
1797
  return Err7(
1335
1798
  new Error(`Failed to load persona: ${error instanceof Error ? error.message : String(error)}`)
@@ -1338,17 +1801,17 @@ function loadPersona(filePath) {
1338
1801
  }
1339
1802
  function listPersonas(dir) {
1340
1803
  try {
1341
- if (!fs6.existsSync(dir)) return Ok9([]);
1804
+ if (!fs6.existsSync(dir)) return Ok11([]);
1342
1805
  const entries = fs6.readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
1343
1806
  const personas = [];
1344
1807
  for (const entry of entries) {
1345
- const filePath = path11.join(dir, entry);
1808
+ const filePath = path14.join(dir, entry);
1346
1809
  const result = loadPersona(filePath);
1347
1810
  if (result.ok) {
1348
1811
  personas.push({ name: result.value.name, description: result.value.description, filePath });
1349
1812
  }
1350
1813
  }
1351
- return Ok9(personas);
1814
+ return Ok11(personas);
1352
1815
  } catch (error) {
1353
1816
  return Err7(
1354
1817
  new Error(
@@ -1358,75 +1821,314 @@ function listPersonas(dir) {
1358
1821
  }
1359
1822
  }
1360
1823
 
1824
+ // src/persona/trigger-detector.ts
1825
+ import * as fs7 from "fs";
1826
+ import * as path15 from "path";
1827
+ function detectTrigger(projectPath) {
1828
+ const handoffPath = path15.join(projectPath, ".harness", "handoff.json");
1829
+ if (!fs7.existsSync(handoffPath)) {
1830
+ return { trigger: "manual" };
1831
+ }
1832
+ try {
1833
+ const raw = fs7.readFileSync(handoffPath, "utf-8");
1834
+ const handoff = JSON.parse(raw);
1835
+ if (handoff.fromSkill === "harness-planning" && Array.isArray(handoff.pending) && handoff.pending.length > 0) {
1836
+ return {
1837
+ trigger: "on_plan_approved",
1838
+ handoff: {
1839
+ fromSkill: handoff.fromSkill,
1840
+ summary: handoff.summary ?? "",
1841
+ pending: handoff.pending,
1842
+ planPath: handoff.planPath
1843
+ }
1844
+ };
1845
+ }
1846
+ return { trigger: "manual" };
1847
+ } catch {
1848
+ return { trigger: "manual" };
1849
+ }
1850
+ }
1851
+
1361
1852
  // src/persona/runner.ts
1362
1853
  var TIMEOUT_ERROR_MESSAGE = "__PERSONA_RUNNER_TIMEOUT__";
1363
- async function runPersona(persona, executor) {
1854
+ function stepName(step) {
1855
+ return "command" in step ? step.command : step.skill;
1856
+ }
1857
+ function stepType(step) {
1858
+ return "command" in step ? "command" : "skill";
1859
+ }
1860
+ function matchesTrigger(step, trigger) {
1861
+ const when = step.when ?? "always";
1862
+ return when === "always" || when === trigger;
1863
+ }
1864
+ function skipRemaining(activeSteps, fromIndex, report) {
1865
+ for (let j = fromIndex; j < activeSteps.length; j++) {
1866
+ const remaining = activeSteps[j];
1867
+ report.steps.push({
1868
+ name: stepName(remaining),
1869
+ type: stepType(remaining),
1870
+ status: "skipped",
1871
+ durationMs: 0
1872
+ });
1873
+ }
1874
+ }
1875
+ async function runPersona(persona, context) {
1364
1876
  const startTime = Date.now();
1365
1877
  const timeout = persona.config.timeout;
1366
1878
  const report = {
1367
1879
  persona: persona.name.toLowerCase().replace(/\s+/g, "-"),
1368
1880
  status: "pass",
1369
- commands: [],
1881
+ steps: [],
1370
1882
  totalDurationMs: 0
1371
1883
  };
1372
- for (let i = 0; i < persona.commands.length; i++) {
1373
- const command = persona.commands[i];
1374
- if (command === void 0) continue;
1884
+ let resolvedTrigger;
1885
+ let handoff = context.handoff;
1886
+ if (context.trigger === "auto") {
1887
+ const detection = detectTrigger(context.projectPath);
1888
+ resolvedTrigger = detection.trigger;
1889
+ handoff = detection.handoff ?? handoff;
1890
+ } else {
1891
+ resolvedTrigger = context.trigger;
1892
+ }
1893
+ const activeSteps = persona.steps.filter((s) => matchesTrigger(s, resolvedTrigger));
1894
+ for (let i = 0; i < activeSteps.length; i++) {
1895
+ const step = activeSteps[i];
1375
1896
  if (Date.now() - startTime >= timeout) {
1376
- for (let j = i; j < persona.commands.length; j++) {
1377
- const remaining = persona.commands[j];
1378
- if (remaining !== void 0) {
1379
- report.commands.push({ name: remaining, status: "skipped", durationMs: 0 });
1380
- }
1381
- }
1897
+ skipRemaining(activeSteps, i, report);
1382
1898
  report.status = "partial";
1383
1899
  break;
1384
1900
  }
1385
- const cmdStart = Date.now();
1901
+ const stepStart = Date.now();
1386
1902
  const remainingTime = timeout - (Date.now() - startTime);
1387
- const result = await Promise.race([
1388
- executor(command),
1389
- new Promise(
1390
- (resolve14) => setTimeout(
1391
- () => resolve14({ ok: false, error: new Error(TIMEOUT_ERROR_MESSAGE) }),
1392
- remainingTime
1903
+ if ("command" in step) {
1904
+ const result = await Promise.race([
1905
+ context.commandExecutor(step.command),
1906
+ new Promise(
1907
+ (resolve22) => setTimeout(
1908
+ () => resolve22({
1909
+ ok: false,
1910
+ error: new Error(TIMEOUT_ERROR_MESSAGE)
1911
+ }),
1912
+ remainingTime
1913
+ )
1393
1914
  )
1394
- )
1395
- ]);
1396
- const durationMs = Date.now() - cmdStart;
1397
- if (result.ok) {
1398
- report.commands.push({ name: command, status: "pass", result: result.value, durationMs });
1399
- } else if (result.error.message === TIMEOUT_ERROR_MESSAGE) {
1400
- report.commands.push({ name: command, status: "skipped", error: "timed out", durationMs });
1401
- report.status = "partial";
1402
- for (let j = i + 1; j < persona.commands.length; j++) {
1403
- const skipped = persona.commands[j];
1404
- if (skipped !== void 0) {
1405
- report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1406
- }
1915
+ ]);
1916
+ const durationMs = Date.now() - stepStart;
1917
+ if (result.ok) {
1918
+ report.steps.push({
1919
+ name: step.command,
1920
+ type: "command",
1921
+ status: "pass",
1922
+ result: result.value,
1923
+ durationMs
1924
+ });
1925
+ } else if (result.error.message === TIMEOUT_ERROR_MESSAGE) {
1926
+ report.steps.push({
1927
+ name: step.command,
1928
+ type: "command",
1929
+ status: "skipped",
1930
+ error: "timed out",
1931
+ durationMs
1932
+ });
1933
+ report.status = "partial";
1934
+ skipRemaining(activeSteps, i + 1, report);
1935
+ break;
1936
+ } else {
1937
+ report.steps.push({
1938
+ name: step.command,
1939
+ type: "command",
1940
+ status: "fail",
1941
+ error: result.error.message,
1942
+ durationMs
1943
+ });
1944
+ report.status = "fail";
1945
+ skipRemaining(activeSteps, i + 1, report);
1946
+ break;
1407
1947
  }
1408
- break;
1409
1948
  } else {
1410
- report.commands.push({
1411
- name: command,
1949
+ const skillContext = {
1950
+ trigger: resolvedTrigger,
1951
+ projectPath: context.projectPath,
1952
+ outputMode: step.output ?? "auto",
1953
+ ...handoff ? { handoff } : {}
1954
+ };
1955
+ const SKILL_TIMEOUT_RESULT = {
1412
1956
  status: "fail",
1413
- error: result.error.message,
1414
- durationMs
1415
- });
1416
- report.status = "fail";
1417
- for (let j = i + 1; j < persona.commands.length; j++) {
1418
- const skipped = persona.commands[j];
1419
- if (skipped !== void 0) {
1420
- report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1421
- }
1957
+ output: "timed out",
1958
+ durationMs: 0
1959
+ };
1960
+ const result = await Promise.race([
1961
+ context.skillExecutor(step.skill, skillContext),
1962
+ new Promise(
1963
+ (resolve22) => setTimeout(() => resolve22(SKILL_TIMEOUT_RESULT), remainingTime)
1964
+ )
1965
+ ]);
1966
+ const durationMs = Date.now() - stepStart;
1967
+ if (result === SKILL_TIMEOUT_RESULT) {
1968
+ report.steps.push({
1969
+ name: step.skill,
1970
+ type: "skill",
1971
+ status: "skipped",
1972
+ error: "timed out",
1973
+ durationMs
1974
+ });
1975
+ report.status = "partial";
1976
+ skipRemaining(activeSteps, i + 1, report);
1977
+ break;
1978
+ } else if (result.status === "pass") {
1979
+ report.steps.push({
1980
+ name: step.skill,
1981
+ type: "skill",
1982
+ status: "pass",
1983
+ result: result.output,
1984
+ ...result.artifactPath ? { artifactPath: result.artifactPath } : {},
1985
+ durationMs
1986
+ });
1987
+ } else {
1988
+ report.steps.push({
1989
+ name: step.skill,
1990
+ type: "skill",
1991
+ status: "fail",
1992
+ error: result.output,
1993
+ durationMs
1994
+ });
1995
+ report.status = "fail";
1996
+ skipRemaining(activeSteps, i + 1, report);
1997
+ break;
1422
1998
  }
1423
- break;
1424
1999
  }
1425
2000
  }
1426
2001
  report.totalDurationMs = Date.now() - startTime;
1427
2002
  return report;
1428
2003
  }
1429
2004
 
2005
+ // src/persona/skill-executor.ts
2006
+ import * as fs8 from "fs";
2007
+ import * as path16 from "path";
2008
+ import { parse as parse2 } from "yaml";
2009
+ function resolveOutputMode(mode, trigger) {
2010
+ if (mode !== "auto") return mode;
2011
+ return trigger === "manual" ? "inline" : "artifact";
2012
+ }
2013
+ function buildArtifactPath(projectPath, headSha) {
2014
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2015
+ const sha = headSha?.slice(0, 7) ?? "unknown";
2016
+ return path16.join(projectPath, ".harness", "reviews", `${date}-${sha}.md`);
2017
+ }
2018
+ function buildArtifactContent(skillName, trigger, headSha) {
2019
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2020
+ return [
2021
+ "---",
2022
+ `skill: ${skillName}`,
2023
+ `trigger: ${trigger}`,
2024
+ `sha: ${headSha?.slice(0, 7) ?? "unknown"}`,
2025
+ `date: ${date}`,
2026
+ `assessment: pending`,
2027
+ "---",
2028
+ "",
2029
+ `# Review by ${skillName}`,
2030
+ "",
2031
+ "## Strengths",
2032
+ "",
2033
+ "- (review pending)",
2034
+ "",
2035
+ "## Issues",
2036
+ "",
2037
+ "### Critical",
2038
+ "",
2039
+ "- None identified",
2040
+ "",
2041
+ "### Important",
2042
+ "",
2043
+ "- None identified",
2044
+ "",
2045
+ "### Suggestions",
2046
+ "",
2047
+ "- None identified",
2048
+ "",
2049
+ "## Assessment",
2050
+ "",
2051
+ "Pending \u2014 skill execution scaffolded.",
2052
+ "",
2053
+ "## Harness Checks",
2054
+ "",
2055
+ "- (run harness validate, check-deps, check-docs to populate)",
2056
+ ""
2057
+ ].join("\n");
2058
+ }
2059
+ async function executeSkill(skillName, context) {
2060
+ const startTime = Date.now();
2061
+ const skillsDir = resolveSkillsDir();
2062
+ const skillDir = path16.join(skillsDir, skillName);
2063
+ if (!fs8.existsSync(skillDir)) {
2064
+ return {
2065
+ status: "fail",
2066
+ output: `Skill not found: ${skillName}`,
2067
+ durationMs: Date.now() - startTime
2068
+ };
2069
+ }
2070
+ const yamlPath = path16.join(skillDir, "skill.yaml");
2071
+ if (!fs8.existsSync(yamlPath)) {
2072
+ return {
2073
+ status: "fail",
2074
+ output: `skill.yaml not found for ${skillName}`,
2075
+ durationMs: Date.now() - startTime
2076
+ };
2077
+ }
2078
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
2079
+ const parsed = parse2(raw);
2080
+ const metadataResult = SkillMetadataSchema.safeParse(parsed);
2081
+ if (!metadataResult.success) {
2082
+ return {
2083
+ status: "fail",
2084
+ output: `Invalid skill metadata: ${metadataResult.error.message}`,
2085
+ durationMs: Date.now() - startTime
2086
+ };
2087
+ }
2088
+ const skillMdPath = path16.join(skillDir, "SKILL.md");
2089
+ if (!fs8.existsSync(skillMdPath)) {
2090
+ return {
2091
+ status: "fail",
2092
+ output: `SKILL.md not found for ${skillName}`,
2093
+ durationMs: Date.now() - startTime
2094
+ };
2095
+ }
2096
+ const skillContent = fs8.readFileSync(skillMdPath, "utf-8");
2097
+ const metadata = metadataResult.data;
2098
+ const resolvedMode = resolveOutputMode(context.outputMode, context.trigger);
2099
+ const output = `Skill ${metadata.name} (${metadata.type}) loaded.
2100
+ Cognitive mode: ${metadata.cognitive_mode ?? "default"}
2101
+ Content length: ${skillContent.length} chars
2102
+ Trigger: ${context.trigger}
2103
+ `;
2104
+ let artifactPath;
2105
+ if (resolvedMode === "artifact") {
2106
+ artifactPath = buildArtifactPath(context.projectPath, context.headSha);
2107
+ const artifactContent = buildArtifactContent(skillName, context.trigger, context.headSha);
2108
+ const dir = path16.dirname(artifactPath);
2109
+ fs8.mkdirSync(dir, { recursive: true });
2110
+ fs8.writeFileSync(artifactPath, artifactContent, "utf-8");
2111
+ }
2112
+ return {
2113
+ status: "pass",
2114
+ output,
2115
+ ...artifactPath ? { artifactPath } : {},
2116
+ durationMs: Date.now() - startTime
2117
+ };
2118
+ }
2119
+
2120
+ // src/persona/constants.ts
2121
+ var ALLOWED_PERSONA_COMMANDS = /* @__PURE__ */ new Set([
2122
+ "validate",
2123
+ "check-deps",
2124
+ "check-docs",
2125
+ "check-perf",
2126
+ "check-security",
2127
+ "cleanup",
2128
+ "fix-drift",
2129
+ "add"
2130
+ ]);
2131
+
1430
2132
  // src/commands/agent/run.ts
1431
2133
  async function runAgentTask(task, options) {
1432
2134
  const configResult = resolveConfig(options.configPath);
@@ -1463,49 +2165,62 @@ async function runAgentTask(task, options) {
1463
2165
  return Err8(new CLIError(`Agent task failed: ${reviewResult.error.message}`, ExitCode.ERROR));
1464
2166
  }
1465
2167
  const review = reviewResult.value;
1466
- return Ok10({
2168
+ return Ok12({
1467
2169
  success: review.approved,
1468
2170
  output: review.approved ? `Agent task '${task}' completed successfully` : `Agent task '${task}' found issues:
1469
2171
  ${review.comments.map((c) => ` - ${c.message}`).join("\n")}`
1470
2172
  });
1471
2173
  }
2174
+ var VALID_TRIGGERS = /* @__PURE__ */ new Set([
2175
+ "always",
2176
+ "on_pr",
2177
+ "on_commit",
2178
+ "on_review",
2179
+ "scheduled",
2180
+ "manual",
2181
+ "on_plan_approved",
2182
+ "auto"
2183
+ ]);
1472
2184
  function createRunCommand() {
1473
- return new Command8("run").description("Run an agent task").argument("[task]", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").option("--persona <name>", "Run a persona by name").action(async (task, opts, cmd) => {
2185
+ return new Command11("run").description("Run an agent task").argument("[task]", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").option("--persona <name>", "Run a persona by name").option("--trigger <context>", "Trigger context (auto, on_pr, on_commit, manual)", "auto").action(async (task, opts, cmd) => {
1474
2186
  const globalOpts = cmd.optsWithGlobals();
1475
2187
  if (opts.persona) {
1476
2188
  const personasDir = resolvePersonasDir();
1477
- const filePath = path12.join(personasDir, `${opts.persona}.yaml`);
2189
+ const filePath = path17.join(personasDir, `${opts.persona}.yaml`);
1478
2190
  const personaResult = loadPersona(filePath);
1479
2191
  if (!personaResult.ok) {
1480
2192
  logger.error(personaResult.error.message);
1481
2193
  process.exit(ExitCode.ERROR);
1482
2194
  }
1483
2195
  const persona = personaResult.value;
1484
- const ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
1485
- "validate",
1486
- "check-deps",
1487
- "check-docs",
1488
- "cleanup",
1489
- "fix-drift",
1490
- "add"
1491
- ]);
1492
- const executor = async (command) => {
1493
- if (!ALLOWED_COMMANDS.has(command)) {
2196
+ const projectPath = process.cwd();
2197
+ const trigger = opts.trigger === "auto" ? "auto" : VALID_TRIGGERS.has(opts.trigger) ? opts.trigger : "manual";
2198
+ const commandExecutor = async (command) => {
2199
+ if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
1494
2200
  return Err8(new Error(`Unknown harness command: ${command}`));
1495
2201
  }
1496
2202
  try {
1497
2203
  childProcess.execFileSync("npx", ["harness", command], { stdio: "inherit" });
1498
- return Ok10(null);
2204
+ return Ok12(null);
1499
2205
  } catch (error) {
1500
2206
  return Err8(new Error(error instanceof Error ? error.message : String(error)));
1501
2207
  }
1502
2208
  };
1503
- const report = await runPersona(persona, executor);
2209
+ const report = await runPersona(persona, {
2210
+ trigger,
2211
+ commandExecutor,
2212
+ skillExecutor: executeSkill,
2213
+ projectPath
2214
+ });
1504
2215
  if (!globalOpts.quiet) {
1505
2216
  logger.info(`Persona '${report.persona}' status: ${report.status}`);
1506
- for (const c of report.commands) {
1507
- const icon = c.status === "pass" ? "v" : c.status === "fail" ? "x" : "-";
1508
- console.log(` [${icon}] ${c.name} (${c.durationMs}ms)`);
2217
+ for (const s of report.steps) {
2218
+ const icon = s.status === "pass" ? "v" : s.status === "fail" ? "x" : "-";
2219
+ const typeTag = s.type === "skill" ? " [skill]" : "";
2220
+ console.log(` [${icon}] ${s.name}${typeTag} (${s.durationMs}ms)`);
2221
+ if (s.artifactPath) {
2222
+ console.log(` artifact: ${s.artifactPath}`);
2223
+ }
1509
2224
  }
1510
2225
  }
1511
2226
  process.exit(report.status === "fail" ? ExitCode.ERROR : ExitCode.SUCCESS);
@@ -1528,9 +2243,9 @@ function createRunCommand() {
1528
2243
  }
1529
2244
 
1530
2245
  // src/commands/agent/review.ts
1531
- import { Command as Command9 } from "commander";
1532
- import { execSync } from "child_process";
1533
- import { Ok as Ok11, Err as Err9 } from "@harness-engineering/core";
2246
+ import { Command as Command12 } from "commander";
2247
+ import { execSync as execSync2 } from "child_process";
2248
+ import { Ok as Ok13, Err as Err9 } from "@harness-engineering/core";
1534
2249
  import { createSelfReview, parseDiff } from "@harness-engineering/core";
1535
2250
  async function runAgentReview(options) {
1536
2251
  const configResult = resolveConfig(options.configPath);
@@ -1540,15 +2255,15 @@ async function runAgentReview(options) {
1540
2255
  const config = configResult.value;
1541
2256
  let diff;
1542
2257
  try {
1543
- diff = execSync("git diff --cached", { encoding: "utf-8" });
2258
+ diff = execSync2("git diff --cached", { encoding: "utf-8" });
1544
2259
  if (!diff) {
1545
- diff = execSync("git diff", { encoding: "utf-8" });
2260
+ diff = execSync2("git diff", { encoding: "utf-8" });
1546
2261
  }
1547
2262
  } catch {
1548
2263
  return Err9(new CLIError("Failed to get git diff", ExitCode.ERROR));
1549
2264
  }
1550
2265
  if (!diff) {
1551
- return Ok11({
2266
+ return Ok13({
1552
2267
  passed: true,
1553
2268
  checklist: [{ check: "No changes to review", passed: true }]
1554
2269
  });
@@ -1568,7 +2283,7 @@ async function runAgentReview(options) {
1568
2283
  if (!review.ok) {
1569
2284
  return Err9(new CLIError(review.error.message, ExitCode.ERROR));
1570
2285
  }
1571
- return Ok11({
2286
+ return Ok13({
1572
2287
  passed: review.value.passed,
1573
2288
  checklist: review.value.items.map((item) => ({
1574
2289
  check: item.check,
@@ -1578,7 +2293,7 @@ async function runAgentReview(options) {
1578
2293
  });
1579
2294
  }
1580
2295
  function createReviewCommand() {
1581
- return new Command9("review").description("Run self-review on current changes").action(async (_opts, cmd) => {
2296
+ return new Command12("review").description("Run self-review on current changes").action(async (_opts, cmd) => {
1582
2297
  const globalOpts = cmd.optsWithGlobals();
1583
2298
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : OutputMode.TEXT;
1584
2299
  const result = await runAgentReview({
@@ -1614,17 +2329,17 @@ function createReviewCommand() {
1614
2329
 
1615
2330
  // src/commands/agent/index.ts
1616
2331
  function createAgentCommand() {
1617
- const command = new Command10("agent").description("Agent orchestration commands");
2332
+ const command = new Command13("agent").description("Agent orchestration commands");
1618
2333
  command.addCommand(createRunCommand());
1619
2334
  command.addCommand(createReviewCommand());
1620
2335
  return command;
1621
2336
  }
1622
2337
 
1623
2338
  // src/commands/add.ts
1624
- import { Command as Command11 } from "commander";
1625
- import * as fs7 from "fs";
1626
- import * as path13 from "path";
1627
- import { Ok as Ok12, Err as Err10 } from "@harness-engineering/core";
2339
+ import { Command as Command14 } from "commander";
2340
+ import * as fs9 from "fs";
2341
+ import * as path18 from "path";
2342
+ import { Ok as Ok14, Err as Err10 } from "@harness-engineering/core";
1628
2343
  var LAYER_INDEX_TEMPLATE = (name) => `// ${name} layer
1629
2344
  // Add your ${name} exports here
1630
2345
 
@@ -1668,61 +2383,61 @@ async function runAdd(componentType, name, options) {
1668
2383
  try {
1669
2384
  switch (componentType) {
1670
2385
  case "layer": {
1671
- const layerDir = path13.join(cwd, "src", name);
1672
- if (!fs7.existsSync(layerDir)) {
1673
- fs7.mkdirSync(layerDir, { recursive: true });
2386
+ const layerDir = path18.join(cwd, "src", name);
2387
+ if (!fs9.existsSync(layerDir)) {
2388
+ fs9.mkdirSync(layerDir, { recursive: true });
1674
2389
  created.push(`src/${name}/`);
1675
2390
  }
1676
- const indexPath = path13.join(layerDir, "index.ts");
1677
- if (!fs7.existsSync(indexPath)) {
1678
- fs7.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
2391
+ const indexPath = path18.join(layerDir, "index.ts");
2392
+ if (!fs9.existsSync(indexPath)) {
2393
+ fs9.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
1679
2394
  created.push(`src/${name}/index.ts`);
1680
2395
  }
1681
2396
  break;
1682
2397
  }
1683
2398
  case "module": {
1684
- const modulePath = path13.join(cwd, "src", `${name}.ts`);
1685
- if (fs7.existsSync(modulePath)) {
2399
+ const modulePath = path18.join(cwd, "src", `${name}.ts`);
2400
+ if (fs9.existsSync(modulePath)) {
1686
2401
  return Err10(new CLIError(`Module ${name} already exists`, ExitCode.ERROR));
1687
2402
  }
1688
- fs7.writeFileSync(modulePath, MODULE_TEMPLATE(name));
2403
+ fs9.writeFileSync(modulePath, MODULE_TEMPLATE(name));
1689
2404
  created.push(`src/${name}.ts`);
1690
2405
  break;
1691
2406
  }
1692
2407
  case "doc": {
1693
- const docsDir = path13.join(cwd, "docs");
1694
- if (!fs7.existsSync(docsDir)) {
1695
- fs7.mkdirSync(docsDir, { recursive: true });
2408
+ const docsDir = path18.join(cwd, "docs");
2409
+ if (!fs9.existsSync(docsDir)) {
2410
+ fs9.mkdirSync(docsDir, { recursive: true });
1696
2411
  }
1697
- const docPath = path13.join(docsDir, `${name}.md`);
1698
- if (fs7.existsSync(docPath)) {
2412
+ const docPath = path18.join(docsDir, `${name}.md`);
2413
+ if (fs9.existsSync(docPath)) {
1699
2414
  return Err10(new CLIError(`Doc ${name} already exists`, ExitCode.ERROR));
1700
2415
  }
1701
- fs7.writeFileSync(docPath, DOC_TEMPLATE(name));
2416
+ fs9.writeFileSync(docPath, DOC_TEMPLATE(name));
1702
2417
  created.push(`docs/${name}.md`);
1703
2418
  break;
1704
2419
  }
1705
2420
  case "skill": {
1706
- const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-4GKJZB5R.js");
2421
+ const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-NZDLMMR6.js");
1707
2422
  generateSkillFiles2({
1708
2423
  name,
1709
2424
  description: `${name} skill`,
1710
- outputDir: path13.join(cwd, "agents", "skills", "claude-code")
2425
+ outputDir: path18.join(cwd, "agents", "skills", "claude-code")
1711
2426
  });
1712
2427
  created.push(`agents/skills/claude-code/${name}/skill.yaml`);
1713
2428
  created.push(`agents/skills/claude-code/${name}/SKILL.md`);
1714
2429
  break;
1715
2430
  }
1716
2431
  case "persona": {
1717
- const personasDir = path13.join(cwd, "agents", "personas");
1718
- if (!fs7.existsSync(personasDir)) {
1719
- fs7.mkdirSync(personasDir, { recursive: true });
2432
+ const personasDir = path18.join(cwd, "agents", "personas");
2433
+ if (!fs9.existsSync(personasDir)) {
2434
+ fs9.mkdirSync(personasDir, { recursive: true });
1720
2435
  }
1721
- const personaPath = path13.join(personasDir, `${name}.yaml`);
1722
- if (fs7.existsSync(personaPath)) {
2436
+ const personaPath = path18.join(personasDir, `${name}.yaml`);
2437
+ if (fs9.existsSync(personaPath)) {
1723
2438
  return Err10(new CLIError(`Persona ${name} already exists`, ExitCode.ERROR));
1724
2439
  }
1725
- fs7.writeFileSync(
2440
+ fs9.writeFileSync(
1726
2441
  personaPath,
1727
2442
  `name: ${name}
1728
2443
  description: ${name} persona
@@ -1744,7 +2459,7 @@ focus_areas: []
1744
2459
  );
1745
2460
  }
1746
2461
  }
1747
- return Ok12({ created });
2462
+ return Ok14({ created });
1748
2463
  } catch (error) {
1749
2464
  return Err10(
1750
2465
  new CLIError(
@@ -1755,7 +2470,7 @@ focus_areas: []
1755
2470
  }
1756
2471
  }
1757
2472
  function createAddCommand() {
1758
- const command = new Command11("add").description("Add a component to the project").argument("<type>", "Component type (layer, module, doc, skill, persona)").argument("<name>", "Component name").action(async (type, name, _opts, cmd) => {
2473
+ const command = new Command14("add").description("Add a component to the project").argument("<type>", "Component type (layer, module, doc, skill, persona)").argument("<name>", "Component name").action(async (type, name, _opts, cmd) => {
1759
2474
  const globalOpts = cmd.optsWithGlobals();
1760
2475
  const result = await runAdd(type, name, {
1761
2476
  ...globalOpts.config !== void 0 && { configPath: globalOpts.config }
@@ -1776,13 +2491,13 @@ function createAddCommand() {
1776
2491
  }
1777
2492
 
1778
2493
  // src/commands/linter/index.ts
1779
- import { Command as Command14 } from "commander";
2494
+ import { Command as Command17 } from "commander";
1780
2495
 
1781
2496
  // src/commands/linter/generate.ts
1782
- import { Command as Command12 } from "commander";
2497
+ import { Command as Command15 } from "commander";
1783
2498
  import { generate } from "@harness-engineering/linter-gen";
1784
2499
  function createGenerateCommand() {
1785
- return new Command12("generate").description("Generate ESLint rules from harness-linter.yml").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("-o, --output <dir>", "Override output directory").option("--clean", "Remove existing files before generating").option("--dry-run", "Preview without writing files").option("--json", "Output as JSON").option("--verbose", "Show detailed output").action(async (options) => {
2500
+ return new Command15("generate").description("Generate ESLint rules from harness-linter.yml").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("-o, --output <dir>", "Override output directory").option("--clean", "Remove existing files before generating").option("--dry-run", "Preview without writing files").option("--json", "Output as JSON").option("--verbose", "Show detailed output").action(async (options) => {
1786
2501
  try {
1787
2502
  if (options.verbose) {
1788
2503
  logger.info(`Parsing config: ${options.config}`);
@@ -1844,10 +2559,10 @@ Generated ${result.rulesGenerated.length} rules to ${result.outputDir}`);
1844
2559
  }
1845
2560
 
1846
2561
  // src/commands/linter/validate.ts
1847
- import { Command as Command13 } from "commander";
2562
+ import { Command as Command16 } from "commander";
1848
2563
  import { validate } from "@harness-engineering/linter-gen";
1849
2564
  function createValidateCommand2() {
1850
- return new Command13("validate").description("Validate harness-linter.yml config").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("--json", "Output as JSON").action(async (options) => {
2565
+ return new Command16("validate").description("Validate harness-linter.yml config").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("--json", "Output as JSON").action(async (options) => {
1851
2566
  try {
1852
2567
  const result = await validate({ configPath: options.config });
1853
2568
  if (options.json) {
@@ -1866,7 +2581,7 @@ function createValidateCommand2() {
1866
2581
 
1867
2582
  // src/commands/linter/index.ts
1868
2583
  function createLinterCommand() {
1869
- const linter = new Command14("linter").description(
2584
+ const linter = new Command17("linter").description(
1870
2585
  "Generate and validate ESLint rules from YAML config"
1871
2586
  );
1872
2587
  linter.addCommand(createGenerateCommand());
@@ -1875,12 +2590,12 @@ function createLinterCommand() {
1875
2590
  }
1876
2591
 
1877
2592
  // src/commands/persona/index.ts
1878
- import { Command as Command17 } from "commander";
2593
+ import { Command as Command20 } from "commander";
1879
2594
 
1880
2595
  // src/commands/persona/list.ts
1881
- import { Command as Command15 } from "commander";
2596
+ import { Command as Command18 } from "commander";
1882
2597
  function createListCommand() {
1883
- return new Command15("list").description("List available agent personas").action(async (_opts, cmd) => {
2598
+ return new Command18("list").description("List available agent personas").action(async (_opts, cmd) => {
1884
2599
  const globalOpts = cmd.optsWithGlobals();
1885
2600
  const personasDir = resolvePersonasDir();
1886
2601
  const result = listPersonas(personasDir);
@@ -1909,12 +2624,12 @@ function createListCommand() {
1909
2624
  }
1910
2625
 
1911
2626
  // src/commands/persona/generate.ts
1912
- import { Command as Command16 } from "commander";
1913
- import * as fs8 from "fs";
1914
- import * as path14 from "path";
2627
+ import { Command as Command19 } from "commander";
2628
+ import * as fs10 from "fs";
2629
+ import * as path19 from "path";
1915
2630
 
1916
2631
  // src/persona/generators/runtime.ts
1917
- import { Ok as Ok13, Err as Err11 } from "@harness-engineering/core";
2632
+ import { Ok as Ok15, Err as Err11 } from "@harness-engineering/core";
1918
2633
 
1919
2634
  // src/utils/string.ts
1920
2635
  function toKebabCase(name) {
@@ -1927,11 +2642,11 @@ function generateRuntime(persona) {
1927
2642
  const config = {
1928
2643
  name: toKebabCase(persona.name),
1929
2644
  skills: persona.skills,
1930
- commands: persona.commands,
2645
+ steps: persona.steps,
1931
2646
  timeout: persona.config.timeout,
1932
2647
  severity: persona.config.severity
1933
2648
  };
1934
- return Ok13(JSON.stringify(config, null, 2));
2649
+ return Ok15(JSON.stringify(config, null, 2));
1935
2650
  } catch (error) {
1936
2651
  return Err11(
1937
2652
  new Error(
@@ -1942,7 +2657,7 @@ function generateRuntime(persona) {
1942
2657
  }
1943
2658
 
1944
2659
  // src/persona/generators/agents-md.ts
1945
- import { Ok as Ok14, Err as Err12 } from "@harness-engineering/core";
2660
+ import { Ok as Ok16, Err as Err12 } from "@harness-engineering/core";
1946
2661
  function formatTrigger(trigger) {
1947
2662
  switch (trigger.event) {
1948
2663
  case "on_pr": {
@@ -1955,13 +2670,17 @@ function formatTrigger(trigger) {
1955
2670
  }
1956
2671
  case "scheduled":
1957
2672
  return `Scheduled (cron: ${trigger.cron})`;
2673
+ case "manual":
2674
+ return "Manual";
1958
2675
  }
1959
2676
  }
1960
2677
  function generateAgentsMd(persona) {
1961
2678
  try {
1962
2679
  const triggers = persona.triggers.map(formatTrigger).join(", ");
1963
2680
  const skills = persona.skills.join(", ");
1964
- const commands = persona.commands.map((c) => `\`harness ${c}\``).join(", ");
2681
+ const commands = persona.steps.filter((s) => "command" in s).map((s) => `\`harness ${s.command}\``).join(", ");
2682
+ const stepSkills = persona.steps.filter((s) => "skill" in s).map((s) => `\`harness skill run ${s.skill}\``).join(", ");
2683
+ const allCommands = [commands, stepSkills].filter(Boolean).join(", ");
1965
2684
  const fragment = `## ${persona.name} Agent
1966
2685
 
1967
2686
  **Role:** ${persona.role}
@@ -1970,9 +2689,9 @@ function generateAgentsMd(persona) {
1970
2689
 
1971
2690
  **Skills:** ${skills}
1972
2691
 
1973
- **When this agent flags an issue:** Fix violations before merging. Run ${commands} locally to validate.
2692
+ **When this agent flags an issue:** Fix violations before merging. Run ${allCommands} locally to validate.
1974
2693
  `;
1975
- return Ok14(fragment);
2694
+ return Ok16(fragment);
1976
2695
  } catch (error) {
1977
2696
  return Err12(
1978
2697
  new Error(
@@ -1984,7 +2703,7 @@ function generateAgentsMd(persona) {
1984
2703
 
1985
2704
  // src/persona/generators/ci-workflow.ts
1986
2705
  import YAML2 from "yaml";
1987
- import { Ok as Ok15, Err as Err13 } from "@harness-engineering/core";
2706
+ import { Ok as Ok17, Err as Err13 } from "@harness-engineering/core";
1988
2707
  function buildGitHubTriggers(triggers) {
1989
2708
  const on = {};
1990
2709
  for (const trigger of triggers) {
@@ -2017,9 +2736,10 @@ function generateCIWorkflow(persona, platform) {
2017
2736
  { uses: "actions/setup-node@v4", with: { "node-version": "20" } },
2018
2737
  { uses: "pnpm/action-setup@v4", with: { run_install: "frozen" } }
2019
2738
  ];
2020
- for (const cmd of persona.commands) {
2739
+ const commandSteps = persona.steps.filter((s) => "command" in s);
2740
+ for (const step of commandSteps) {
2021
2741
  const severityFlag = severity ? ` --severity ${severity}` : "";
2022
- steps.push({ run: `npx harness ${cmd}${severityFlag}` });
2742
+ steps.push({ run: `npx harness ${step.command}${severityFlag}` });
2023
2743
  }
2024
2744
  const workflow = {
2025
2745
  name: persona.name,
@@ -2031,7 +2751,7 @@ function generateCIWorkflow(persona, platform) {
2031
2751
  }
2032
2752
  }
2033
2753
  };
2034
- return Ok15(YAML2.stringify(workflow, { lineWidth: 0 }));
2754
+ return Ok17(YAML2.stringify(workflow, { lineWidth: 0 }));
2035
2755
  } catch (error) {
2036
2756
  return Err13(
2037
2757
  new Error(
@@ -2043,43 +2763,43 @@ function generateCIWorkflow(persona, platform) {
2043
2763
 
2044
2764
  // src/commands/persona/generate.ts
2045
2765
  function createGenerateCommand2() {
2046
- return new Command16("generate").description("Generate artifacts from a persona config").argument("<name>", "Persona name (e.g., architecture-enforcer)").option("--output-dir <dir>", "Output directory", ".").option("--only <type>", "Generate only: ci, agents-md, runtime").action(async (name, opts, cmd) => {
2766
+ return new Command19("generate").description("Generate artifacts from a persona config").argument("<name>", "Persona name (e.g., architecture-enforcer)").option("--output-dir <dir>", "Output directory", ".").option("--only <type>", "Generate only: ci, agents-md, runtime").action(async (name, opts, cmd) => {
2047
2767
  const globalOpts = cmd.optsWithGlobals();
2048
2768
  const personasDir = resolvePersonasDir();
2049
- const filePath = path14.join(personasDir, `${name}.yaml`);
2769
+ const filePath = path19.join(personasDir, `${name}.yaml`);
2050
2770
  const personaResult = loadPersona(filePath);
2051
2771
  if (!personaResult.ok) {
2052
2772
  logger.error(personaResult.error.message);
2053
2773
  process.exit(ExitCode.ERROR);
2054
2774
  }
2055
2775
  const persona = personaResult.value;
2056
- const outputDir = path14.resolve(opts.outputDir);
2776
+ const outputDir = path19.resolve(opts.outputDir);
2057
2777
  const slug = toKebabCase(persona.name);
2058
2778
  const only = opts.only;
2059
2779
  const generated = [];
2060
2780
  if (!only || only === "runtime") {
2061
2781
  const result = generateRuntime(persona);
2062
2782
  if (result.ok) {
2063
- const outPath = path14.join(outputDir, `${slug}.runtime.json`);
2064
- fs8.mkdirSync(path14.dirname(outPath), { recursive: true });
2065
- fs8.writeFileSync(outPath, result.value);
2783
+ const outPath = path19.join(outputDir, `${slug}.runtime.json`);
2784
+ fs10.mkdirSync(path19.dirname(outPath), { recursive: true });
2785
+ fs10.writeFileSync(outPath, result.value);
2066
2786
  generated.push(outPath);
2067
2787
  }
2068
2788
  }
2069
2789
  if (!only || only === "agents-md") {
2070
2790
  const result = generateAgentsMd(persona);
2071
2791
  if (result.ok) {
2072
- const outPath = path14.join(outputDir, `${slug}.agents.md`);
2073
- fs8.writeFileSync(outPath, result.value);
2792
+ const outPath = path19.join(outputDir, `${slug}.agents.md`);
2793
+ fs10.writeFileSync(outPath, result.value);
2074
2794
  generated.push(outPath);
2075
2795
  }
2076
2796
  }
2077
2797
  if (!only || only === "ci") {
2078
2798
  const result = generateCIWorkflow(persona, "github");
2079
2799
  if (result.ok) {
2080
- const outPath = path14.join(outputDir, ".github", "workflows", `${slug}.yml`);
2081
- fs8.mkdirSync(path14.dirname(outPath), { recursive: true });
2082
- fs8.writeFileSync(outPath, result.value);
2800
+ const outPath = path19.join(outputDir, ".github", "workflows", `${slug}.yml`);
2801
+ fs10.mkdirSync(path19.dirname(outPath), { recursive: true });
2802
+ fs10.writeFileSync(outPath, result.value);
2083
2803
  generated.push(outPath);
2084
2804
  }
2085
2805
  }
@@ -2093,37 +2813,37 @@ function createGenerateCommand2() {
2093
2813
 
2094
2814
  // src/commands/persona/index.ts
2095
2815
  function createPersonaCommand() {
2096
- const command = new Command17("persona").description("Agent persona management commands");
2816
+ const command = new Command20("persona").description("Agent persona management commands");
2097
2817
  command.addCommand(createListCommand());
2098
2818
  command.addCommand(createGenerateCommand2());
2099
2819
  return command;
2100
2820
  }
2101
2821
 
2102
2822
  // src/commands/skill/index.ts
2103
- import { Command as Command22 } from "commander";
2823
+ import { Command as Command25 } from "commander";
2104
2824
 
2105
2825
  // src/commands/skill/list.ts
2106
- import { Command as Command18 } from "commander";
2107
- import * as fs9 from "fs";
2108
- import * as path15 from "path";
2109
- import { parse as parse2 } from "yaml";
2826
+ import { Command as Command21 } from "commander";
2827
+ import * as fs11 from "fs";
2828
+ import * as path20 from "path";
2829
+ import { parse as parse3 } from "yaml";
2110
2830
  function createListCommand2() {
2111
- return new Command18("list").description("List available skills").action(async (_opts, cmd) => {
2831
+ return new Command21("list").description("List available skills").action(async (_opts, cmd) => {
2112
2832
  const globalOpts = cmd.optsWithGlobals();
2113
2833
  const skillsDir = resolveSkillsDir();
2114
- if (!fs9.existsSync(skillsDir)) {
2834
+ if (!fs11.existsSync(skillsDir)) {
2115
2835
  logger.info("No skills directory found.");
2116
2836
  process.exit(ExitCode.SUCCESS);
2117
2837
  return;
2118
2838
  }
2119
- const entries = fs9.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2839
+ const entries = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2120
2840
  const skills = [];
2121
2841
  for (const name of entries) {
2122
- const yamlPath = path15.join(skillsDir, name, "skill.yaml");
2123
- if (!fs9.existsSync(yamlPath)) continue;
2842
+ const yamlPath = path20.join(skillsDir, name, "skill.yaml");
2843
+ if (!fs11.existsSync(yamlPath)) continue;
2124
2844
  try {
2125
- const raw = fs9.readFileSync(yamlPath, "utf-8");
2126
- const parsed = parse2(raw);
2845
+ const raw = fs11.readFileSync(yamlPath, "utf-8");
2846
+ const parsed = parse3(raw);
2127
2847
  const result = SkillMetadataSchema.safeParse(parsed);
2128
2848
  if (result.success) {
2129
2849
  skills.push(result.data);
@@ -2152,13 +2872,13 @@ function createListCommand2() {
2152
2872
  }
2153
2873
 
2154
2874
  // src/commands/skill/run.ts
2155
- import { Command as Command19 } from "commander";
2156
- import * as fs10 from "fs";
2157
- import * as path16 from "path";
2158
- import { parse as parse3 } from "yaml";
2875
+ import { Command as Command22 } from "commander";
2876
+ import * as fs12 from "fs";
2877
+ import * as path21 from "path";
2878
+ import { parse as parse4 } from "yaml";
2159
2879
 
2160
2880
  // src/skill/complexity.ts
2161
- import { execSync as execSync2 } from "child_process";
2881
+ import { execSync as execSync3 } from "child_process";
2162
2882
  function evaluateSignals(signals) {
2163
2883
  if (signals.fileCount >= 3) return "full";
2164
2884
  if (signals.newDir) return "full";
@@ -2170,17 +2890,17 @@ function evaluateSignals(signals) {
2170
2890
  }
2171
2891
  function detectComplexity(projectPath) {
2172
2892
  try {
2173
- const base = execSync2("git merge-base HEAD main", {
2893
+ const base = execSync3("git merge-base HEAD main", {
2174
2894
  cwd: projectPath,
2175
2895
  encoding: "utf-8",
2176
2896
  stdio: ["pipe", "pipe", "pipe"]
2177
2897
  }).trim();
2178
- const diffFiles = execSync2(`git diff --name-only ${base}`, {
2898
+ const diffFiles = execSync3(`git diff --name-only ${base}`, {
2179
2899
  cwd: projectPath,
2180
2900
  encoding: "utf-8",
2181
2901
  stdio: ["pipe", "pipe", "pipe"]
2182
2902
  }).trim().split("\n").filter(Boolean);
2183
- const diffStat = execSync2(`git diff --stat ${base}`, {
2903
+ const diffStat = execSync3(`git diff --stat ${base}`, {
2184
2904
  cwd: projectPath,
2185
2905
  encoding: "utf-8",
2186
2906
  stdio: ["pipe", "pipe", "pipe"]
@@ -2242,20 +2962,20 @@ ${options.priorState}`);
2242
2962
 
2243
2963
  // src/commands/skill/run.ts
2244
2964
  function createRunCommand2() {
2245
- return new Command19("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Complexity: auto, light, full", "auto").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").action(async (name, opts, _cmd) => {
2965
+ return new Command22("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Complexity: auto, light, full", "auto").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").action(async (name, opts, _cmd) => {
2246
2966
  const skillsDir = resolveSkillsDir();
2247
- const skillDir = path16.join(skillsDir, name);
2248
- if (!fs10.existsSync(skillDir)) {
2967
+ const skillDir = path21.join(skillsDir, name);
2968
+ if (!fs12.existsSync(skillDir)) {
2249
2969
  logger.error(`Skill not found: ${name}`);
2250
2970
  process.exit(ExitCode.ERROR);
2251
2971
  return;
2252
2972
  }
2253
- const yamlPath = path16.join(skillDir, "skill.yaml");
2973
+ const yamlPath = path21.join(skillDir, "skill.yaml");
2254
2974
  let metadata = null;
2255
- if (fs10.existsSync(yamlPath)) {
2975
+ if (fs12.existsSync(yamlPath)) {
2256
2976
  try {
2257
- const raw = fs10.readFileSync(yamlPath, "utf-8");
2258
- const parsed = parse3(raw);
2977
+ const raw = fs12.readFileSync(yamlPath, "utf-8");
2978
+ const parsed = parse4(raw);
2259
2979
  const result = SkillMetadataSchema.safeParse(parsed);
2260
2980
  if (result.success) metadata = result.data;
2261
2981
  } catch {
@@ -2265,17 +2985,17 @@ function createRunCommand2() {
2265
2985
  if (metadata?.phases && metadata.phases.length > 0) {
2266
2986
  const requested = opts.complexity ?? "auto";
2267
2987
  if (requested === "auto") {
2268
- const projectPath2 = opts.path ? path16.resolve(opts.path) : process.cwd();
2988
+ const projectPath2 = opts.path ? path21.resolve(opts.path) : process.cwd();
2269
2989
  complexity = detectComplexity(projectPath2);
2270
2990
  } else {
2271
2991
  complexity = requested;
2272
2992
  }
2273
2993
  }
2274
2994
  let principles;
2275
- const projectPath = opts.path ? path16.resolve(opts.path) : process.cwd();
2276
- const principlesPath = path16.join(projectPath, "docs", "principles.md");
2277
- if (fs10.existsSync(principlesPath)) {
2278
- principles = fs10.readFileSync(principlesPath, "utf-8");
2995
+ const projectPath = opts.path ? path21.resolve(opts.path) : process.cwd();
2996
+ const principlesPath = path21.join(projectPath, "docs", "principles.md");
2997
+ if (fs12.existsSync(principlesPath)) {
2998
+ principles = fs12.readFileSync(principlesPath, "utf-8");
2279
2999
  }
2280
3000
  let priorState;
2281
3001
  let stateWarning;
@@ -2290,16 +3010,16 @@ function createRunCommand2() {
2290
3010
  }
2291
3011
  if (metadata?.state.persistent && metadata.state.files.length > 0) {
2292
3012
  for (const stateFilePath of metadata.state.files) {
2293
- const fullPath = path16.join(projectPath, stateFilePath);
2294
- if (fs10.existsSync(fullPath)) {
2295
- const stat = fs10.statSync(fullPath);
3013
+ const fullPath = path21.join(projectPath, stateFilePath);
3014
+ if (fs12.existsSync(fullPath)) {
3015
+ const stat = fs12.statSync(fullPath);
2296
3016
  if (stat.isDirectory()) {
2297
- const files = fs10.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs10.statSync(path16.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
3017
+ const files = fs12.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs12.statSync(path21.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2298
3018
  if (files.length > 0) {
2299
- priorState = fs10.readFileSync(path16.join(fullPath, files[0].name), "utf-8");
3019
+ priorState = fs12.readFileSync(path21.join(fullPath, files[0].name), "utf-8");
2300
3020
  }
2301
3021
  } else {
2302
- priorState = fs10.readFileSync(fullPath, "utf-8");
3022
+ priorState = fs12.readFileSync(fullPath, "utf-8");
2303
3023
  }
2304
3024
  break;
2305
3025
  }
@@ -2318,17 +3038,17 @@ function createRunCommand2() {
2318
3038
  ...stateWarning !== void 0 && { stateWarning },
2319
3039
  party: opts.party
2320
3040
  });
2321
- const skillMdPath = path16.join(skillDir, "SKILL.md");
2322
- if (!fs10.existsSync(skillMdPath)) {
3041
+ const skillMdPath = path21.join(skillDir, "SKILL.md");
3042
+ if (!fs12.existsSync(skillMdPath)) {
2323
3043
  logger.error(`SKILL.md not found for skill: ${name}`);
2324
3044
  process.exit(ExitCode.ERROR);
2325
3045
  return;
2326
3046
  }
2327
- let content = fs10.readFileSync(skillMdPath, "utf-8");
3047
+ let content = fs12.readFileSync(skillMdPath, "utf-8");
2328
3048
  if (metadata?.state.persistent && opts.path) {
2329
- const stateFile = path16.join(projectPath, ".harness", "state.json");
2330
- if (fs10.existsSync(stateFile)) {
2331
- const stateContent = fs10.readFileSync(stateFile, "utf-8");
3049
+ const stateFile = path21.join(projectPath, ".harness", "state.json");
3050
+ if (fs12.existsSync(stateFile)) {
3051
+ const stateContent = fs12.readFileSync(stateFile, "utf-8");
2332
3052
  content += `
2333
3053
 
2334
3054
  ---
@@ -2345,10 +3065,10 @@ ${stateContent}
2345
3065
  }
2346
3066
 
2347
3067
  // src/commands/skill/validate.ts
2348
- import { Command as Command20 } from "commander";
2349
- import * as fs11 from "fs";
2350
- import * as path17 from "path";
2351
- import { parse as parse4 } from "yaml";
3068
+ import { Command as Command23 } from "commander";
3069
+ import * as fs13 from "fs";
3070
+ import * as path22 from "path";
3071
+ import { parse as parse5 } from "yaml";
2352
3072
  var REQUIRED_SECTIONS = [
2353
3073
  "## When to Use",
2354
3074
  "## Process",
@@ -2357,35 +3077,35 @@ var REQUIRED_SECTIONS = [
2357
3077
  "## Examples"
2358
3078
  ];
2359
3079
  function createValidateCommand3() {
2360
- return new Command20("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
3080
+ return new Command23("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
2361
3081
  const globalOpts = cmd.optsWithGlobals();
2362
3082
  const skillsDir = resolveSkillsDir();
2363
- if (!fs11.existsSync(skillsDir)) {
3083
+ if (!fs13.existsSync(skillsDir)) {
2364
3084
  logger.info("No skills directory found.");
2365
3085
  process.exit(ExitCode.SUCCESS);
2366
3086
  return;
2367
3087
  }
2368
- const entries = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3088
+ const entries = fs13.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2369
3089
  const errors = [];
2370
3090
  let validated = 0;
2371
3091
  for (const name of entries) {
2372
- const skillDir = path17.join(skillsDir, name);
2373
- const yamlPath = path17.join(skillDir, "skill.yaml");
2374
- const skillMdPath = path17.join(skillDir, "SKILL.md");
2375
- if (!fs11.existsSync(yamlPath)) {
3092
+ const skillDir = path22.join(skillsDir, name);
3093
+ const yamlPath = path22.join(skillDir, "skill.yaml");
3094
+ const skillMdPath = path22.join(skillDir, "SKILL.md");
3095
+ if (!fs13.existsSync(yamlPath)) {
2376
3096
  errors.push(`${name}: missing skill.yaml`);
2377
3097
  continue;
2378
3098
  }
2379
3099
  try {
2380
- const raw = fs11.readFileSync(yamlPath, "utf-8");
2381
- const parsed = parse4(raw);
3100
+ const raw = fs13.readFileSync(yamlPath, "utf-8");
3101
+ const parsed = parse5(raw);
2382
3102
  const result = SkillMetadataSchema.safeParse(parsed);
2383
3103
  if (!result.success) {
2384
3104
  errors.push(`${name}/skill.yaml: ${result.error.message}`);
2385
3105
  continue;
2386
3106
  }
2387
- if (fs11.existsSync(skillMdPath)) {
2388
- const mdContent = fs11.readFileSync(skillMdPath, "utf-8");
3107
+ if (fs13.existsSync(skillMdPath)) {
3108
+ const mdContent = fs13.readFileSync(skillMdPath, "utf-8");
2389
3109
  for (const section of REQUIRED_SECTIONS) {
2390
3110
  if (!mdContent.includes(section)) {
2391
3111
  errors.push(`${name}/SKILL.md: missing section "${section}"`);
@@ -2426,29 +3146,29 @@ function createValidateCommand3() {
2426
3146
  }
2427
3147
 
2428
3148
  // src/commands/skill/info.ts
2429
- import { Command as Command21 } from "commander";
2430
- import * as fs12 from "fs";
2431
- import * as path18 from "path";
2432
- import { parse as parse5 } from "yaml";
3149
+ import { Command as Command24 } from "commander";
3150
+ import * as fs14 from "fs";
3151
+ import * as path23 from "path";
3152
+ import { parse as parse6 } from "yaml";
2433
3153
  function createInfoCommand() {
2434
- return new Command21("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
3154
+ return new Command24("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
2435
3155
  const globalOpts = cmd.optsWithGlobals();
2436
3156
  const skillsDir = resolveSkillsDir();
2437
- const skillDir = path18.join(skillsDir, name);
2438
- if (!fs12.existsSync(skillDir)) {
3157
+ const skillDir = path23.join(skillsDir, name);
3158
+ if (!fs14.existsSync(skillDir)) {
2439
3159
  logger.error(`Skill not found: ${name}`);
2440
3160
  process.exit(ExitCode.ERROR);
2441
3161
  return;
2442
3162
  }
2443
- const yamlPath = path18.join(skillDir, "skill.yaml");
2444
- if (!fs12.existsSync(yamlPath)) {
3163
+ const yamlPath = path23.join(skillDir, "skill.yaml");
3164
+ if (!fs14.existsSync(yamlPath)) {
2445
3165
  logger.error(`skill.yaml not found for skill: ${name}`);
2446
3166
  process.exit(ExitCode.ERROR);
2447
3167
  return;
2448
3168
  }
2449
3169
  try {
2450
- const raw = fs12.readFileSync(yamlPath, "utf-8");
2451
- const parsed = parse5(raw);
3170
+ const raw = fs14.readFileSync(yamlPath, "utf-8");
3171
+ const parsed = parse6(raw);
2452
3172
  const result = SkillMetadataSchema.safeParse(parsed);
2453
3173
  if (!result.success) {
2454
3174
  logger.error(`Invalid skill.yaml: ${result.error.message}`);
@@ -2488,7 +3208,7 @@ function createInfoCommand() {
2488
3208
 
2489
3209
  // src/commands/skill/index.ts
2490
3210
  function createSkillCommand() {
2491
- const command = new Command22("skill").description("Skill management commands");
3211
+ const command = new Command25("skill").description("Skill management commands");
2492
3212
  command.addCommand(createListCommand2());
2493
3213
  command.addCommand(createRunCommand2());
2494
3214
  command.addCommand(createValidateCommand3());
@@ -2497,17 +3217,17 @@ function createSkillCommand() {
2497
3217
  }
2498
3218
 
2499
3219
  // src/commands/state/index.ts
2500
- import { Command as Command26 } from "commander";
3220
+ import { Command as Command30 } from "commander";
2501
3221
 
2502
3222
  // src/commands/state/show.ts
2503
- import { Command as Command23 } from "commander";
2504
- import * as path19 from "path";
3223
+ import { Command as Command26 } from "commander";
3224
+ import * as path24 from "path";
2505
3225
  import { loadState } from "@harness-engineering/core";
2506
3226
  function createShowCommand() {
2507
- return new Command23("show").description("Show current project state").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
3227
+ return new Command26("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
2508
3228
  const globalOpts = cmd.optsWithGlobals();
2509
- const projectPath = path19.resolve(opts.path);
2510
- const result = await loadState(projectPath);
3229
+ const projectPath = path24.resolve(opts.path);
3230
+ const result = await loadState(projectPath, opts.stream);
2511
3231
  if (!result.ok) {
2512
3232
  logger.error(result.error.message);
2513
3233
  process.exit(ExitCode.ERROR);
@@ -2519,6 +3239,7 @@ function createShowCommand() {
2519
3239
  } else if (globalOpts.quiet) {
2520
3240
  console.log(JSON.stringify(state));
2521
3241
  } else {
3242
+ if (opts.stream) console.log(`Stream: ${opts.stream}`);
2522
3243
  console.log(`Schema Version: ${state.schemaVersion}`);
2523
3244
  if (state.position.phase) console.log(`Phase: ${state.position.phase}`);
2524
3245
  if (state.position.task) console.log(`Task: ${state.position.task}`);
@@ -2545,23 +3266,35 @@ Decisions: ${state.decisions.length}`);
2545
3266
  }
2546
3267
 
2547
3268
  // src/commands/state/reset.ts
2548
- import { Command as Command24 } from "commander";
2549
- import * as fs13 from "fs";
2550
- import * as path20 from "path";
3269
+ import { Command as Command27 } from "commander";
3270
+ import * as fs15 from "fs";
3271
+ import * as path25 from "path";
2551
3272
  import * as readline from "readline";
3273
+ import { resolveStreamPath } from "@harness-engineering/core";
2552
3274
  function createResetCommand() {
2553
- return new Command24("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
2554
- const projectPath = path20.resolve(opts.path);
2555
- const statePath = path20.join(projectPath, ".harness", "state.json");
2556
- if (!fs13.existsSync(statePath)) {
3275
+ return new Command27("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) => {
3276
+ const projectPath = path25.resolve(opts.path);
3277
+ let statePath;
3278
+ if (opts.stream) {
3279
+ const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
3280
+ if (!streamResult.ok) {
3281
+ logger.error(streamResult.error.message);
3282
+ process.exit(ExitCode.ERROR);
3283
+ return;
3284
+ }
3285
+ statePath = path25.join(streamResult.value, "state.json");
3286
+ } else {
3287
+ statePath = path25.join(projectPath, ".harness", "state.json");
3288
+ }
3289
+ if (!fs15.existsSync(statePath)) {
2557
3290
  logger.info("No state file found. Nothing to reset.");
2558
3291
  process.exit(ExitCode.SUCCESS);
2559
3292
  return;
2560
3293
  }
2561
3294
  if (!opts.yes) {
2562
3295
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2563
- const answer = await new Promise((resolve14) => {
2564
- rl.question("Reset project state? This cannot be undone. [y/N] ", resolve14);
3296
+ const answer = await new Promise((resolve22) => {
3297
+ rl.question("Reset project state? This cannot be undone. [y/N] ", resolve22);
2565
3298
  });
2566
3299
  rl.close();
2567
3300
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -2571,7 +3304,7 @@ function createResetCommand() {
2571
3304
  }
2572
3305
  }
2573
3306
  try {
2574
- fs13.unlinkSync(statePath);
3307
+ fs15.unlinkSync(statePath);
2575
3308
  logger.success("Project state reset.");
2576
3309
  } catch (e) {
2577
3310
  logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
@@ -2583,13 +3316,13 @@ function createResetCommand() {
2583
3316
  }
2584
3317
 
2585
3318
  // src/commands/state/learn.ts
2586
- import { Command as Command25 } from "commander";
2587
- import * as path21 from "path";
3319
+ import { Command as Command28 } from "commander";
3320
+ import * as path26 from "path";
2588
3321
  import { appendLearning } from "@harness-engineering/core";
2589
3322
  function createLearnCommand() {
2590
- return new Command25("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").action(async (message, opts, _cmd) => {
2591
- const projectPath = path21.resolve(opts.path);
2592
- const result = await appendLearning(projectPath, message);
3323
+ return new Command28("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) => {
3324
+ const projectPath = path26.resolve(opts.path);
3325
+ const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
2593
3326
  if (!result.ok) {
2594
3327
  logger.error(result.error.message);
2595
3328
  process.exit(ExitCode.ERROR);
@@ -2600,29 +3333,103 @@ function createLearnCommand() {
2600
3333
  });
2601
3334
  }
2602
3335
 
3336
+ // src/commands/state/streams.ts
3337
+ import { Command as Command29 } from "commander";
3338
+ import * as path27 from "path";
3339
+ import {
3340
+ createStream,
3341
+ listStreams,
3342
+ archiveStream,
3343
+ setActiveStream,
3344
+ loadStreamIndex
3345
+ } from "@harness-engineering/core";
3346
+ function createStreamsCommand() {
3347
+ const command = new Command29("streams").description("Manage state streams");
3348
+ command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
3349
+ const globalOpts = cmd.optsWithGlobals();
3350
+ const projectPath = path27.resolve(opts.path);
3351
+ const indexResult = await loadStreamIndex(projectPath);
3352
+ const result = await listStreams(projectPath);
3353
+ if (!result.ok) {
3354
+ logger.error(result.error.message);
3355
+ process.exit(ExitCode.ERROR);
3356
+ return;
3357
+ }
3358
+ const active = indexResult.ok ? indexResult.value.activeStream : null;
3359
+ if (globalOpts.json) {
3360
+ logger.raw({ activeStream: active, streams: result.value });
3361
+ } else {
3362
+ if (result.value.length === 0) {
3363
+ console.log("No streams found.");
3364
+ }
3365
+ for (const s of result.value) {
3366
+ const marker = s.name === active ? " (active)" : "";
3367
+ const branch = s.branch ? ` [${s.branch}]` : "";
3368
+ console.log(` ${s.name}${marker}${branch} \u2014 last active: ${s.lastActiveAt}`);
3369
+ }
3370
+ }
3371
+ process.exit(ExitCode.SUCCESS);
3372
+ });
3373
+ 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) => {
3374
+ const projectPath = path27.resolve(opts.path);
3375
+ const result = await createStream(projectPath, name, opts.branch);
3376
+ if (!result.ok) {
3377
+ logger.error(result.error.message);
3378
+ process.exit(ExitCode.ERROR);
3379
+ return;
3380
+ }
3381
+ logger.success(`Stream '${name}' created.`);
3382
+ process.exit(ExitCode.SUCCESS);
3383
+ });
3384
+ command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3385
+ const projectPath = path27.resolve(opts.path);
3386
+ const result = await archiveStream(projectPath, name);
3387
+ if (!result.ok) {
3388
+ logger.error(result.error.message);
3389
+ process.exit(ExitCode.ERROR);
3390
+ return;
3391
+ }
3392
+ logger.success(`Stream '${name}' archived.`);
3393
+ process.exit(ExitCode.SUCCESS);
3394
+ });
3395
+ command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3396
+ const projectPath = path27.resolve(opts.path);
3397
+ const result = await setActiveStream(projectPath, name);
3398
+ if (!result.ok) {
3399
+ logger.error(result.error.message);
3400
+ process.exit(ExitCode.ERROR);
3401
+ return;
3402
+ }
3403
+ logger.success(`Active stream set to '${name}'.`);
3404
+ process.exit(ExitCode.SUCCESS);
3405
+ });
3406
+ return command;
3407
+ }
3408
+
2603
3409
  // src/commands/state/index.ts
2604
3410
  function createStateCommand() {
2605
- const command = new Command26("state").description("Project state management commands");
3411
+ const command = new Command30("state").description("Project state management commands");
2606
3412
  command.addCommand(createShowCommand());
2607
3413
  command.addCommand(createResetCommand());
2608
3414
  command.addCommand(createLearnCommand());
3415
+ command.addCommand(createStreamsCommand());
2609
3416
  return command;
2610
3417
  }
2611
3418
 
2612
3419
  // src/commands/check-phase-gate.ts
2613
- import { Command as Command27 } from "commander";
2614
- import * as path22 from "path";
2615
- import * as fs14 from "fs";
2616
- import { Ok as Ok16 } from "@harness-engineering/core";
3420
+ import { Command as Command31 } from "commander";
3421
+ import * as path28 from "path";
3422
+ import * as fs16 from "fs";
3423
+ import { Ok as Ok18 } from "@harness-engineering/core";
2617
3424
  function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
2618
- const relImpl = path22.relative(cwd, implFile);
3425
+ const relImpl = path28.relative(cwd, implFile);
2619
3426
  const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
2620
3427
  const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
2621
3428
  const segments = afterBase.split("/");
2622
3429
  const firstSegment = segments[0] ?? "";
2623
- const feature = segments.length > 1 ? firstSegment : path22.basename(firstSegment, path22.extname(firstSegment));
3430
+ const feature = segments.length > 1 ? firstSegment : path28.basename(firstSegment, path28.extname(firstSegment));
2624
3431
  const specRelative = specPattern.replace("{feature}", feature);
2625
- return path22.resolve(cwd, specRelative);
3432
+ return path28.resolve(cwd, specRelative);
2626
3433
  }
2627
3434
  async function runCheckPhaseGate(options) {
2628
3435
  const configResult = resolveConfig(options.configPath);
@@ -2630,9 +3437,9 @@ async function runCheckPhaseGate(options) {
2630
3437
  return configResult;
2631
3438
  }
2632
3439
  const config = configResult.value;
2633
- const cwd = options.cwd ?? (options.configPath ? path22.dirname(path22.resolve(options.configPath)) : process.cwd());
3440
+ const cwd = options.cwd ?? (options.configPath ? path28.dirname(path28.resolve(options.configPath)) : process.cwd());
2634
3441
  if (!config.phaseGates?.enabled) {
2635
- return Ok16({
3442
+ return Ok18({
2636
3443
  pass: true,
2637
3444
  skipped: true,
2638
3445
  missingSpecs: [],
@@ -2647,16 +3454,16 @@ async function runCheckPhaseGate(options) {
2647
3454
  for (const implFile of implFiles) {
2648
3455
  checkedFiles++;
2649
3456
  const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
2650
- if (!fs14.existsSync(expectedSpec)) {
3457
+ if (!fs16.existsSync(expectedSpec)) {
2651
3458
  missingSpecs.push({
2652
- implFile: path22.relative(cwd, implFile),
2653
- expectedSpec: path22.relative(cwd, expectedSpec)
3459
+ implFile: path28.relative(cwd, implFile),
3460
+ expectedSpec: path28.relative(cwd, expectedSpec)
2654
3461
  });
2655
3462
  }
2656
3463
  }
2657
3464
  }
2658
3465
  const pass = missingSpecs.length === 0;
2659
- return Ok16({
3466
+ return Ok18({
2660
3467
  pass,
2661
3468
  skipped: false,
2662
3469
  severity: phaseGates.severity,
@@ -2665,7 +3472,7 @@ async function runCheckPhaseGate(options) {
2665
3472
  });
2666
3473
  }
2667
3474
  function createCheckPhaseGateCommand() {
2668
- const command = new Command27("check-phase-gate").description("Verify that implementation files have matching spec documents").action(async (_opts, cmd) => {
3475
+ const command = new Command31("check-phase-gate").description("Verify that implementation files have matching spec documents").action(async (_opts, cmd) => {
2669
3476
  const globalOpts = cmd.optsWithGlobals();
2670
3477
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
2671
3478
  const formatter = new OutputFormatter(mode);
@@ -2719,16 +3526,16 @@ function createCheckPhaseGateCommand() {
2719
3526
  }
2720
3527
 
2721
3528
  // src/commands/generate-slash-commands.ts
2722
- import { Command as Command28 } from "commander";
2723
- import fs17 from "fs";
2724
- import path25 from "path";
3529
+ import { Command as Command32 } from "commander";
3530
+ import fs19 from "fs";
3531
+ import path31 from "path";
2725
3532
  import os2 from "os";
2726
3533
  import readline2 from "readline";
2727
3534
 
2728
3535
  // src/slash-commands/normalize.ts
2729
- import fs15 from "fs";
2730
- import path23 from "path";
2731
- import { parse as parse6 } from "yaml";
3536
+ import fs17 from "fs";
3537
+ import path29 from "path";
3538
+ import { parse as parse7 } from "yaml";
2732
3539
 
2733
3540
  // src/slash-commands/normalize-name.ts
2734
3541
  function normalizeName(skillName) {
@@ -2744,106 +3551,117 @@ function normalizeName(skillName) {
2744
3551
  }
2745
3552
 
2746
3553
  // src/slash-commands/normalize.ts
2747
- function normalizeSkills(skillsDir, platforms) {
2748
- const entries = fs15.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3554
+ function normalizeSkills(skillSources, platforms) {
2749
3555
  const specs = [];
2750
3556
  const nameMap = /* @__PURE__ */ new Map();
2751
- for (const entry of entries) {
2752
- const yamlPath = path23.join(skillsDir, entry.name, "skill.yaml");
2753
- if (!fs15.existsSync(yamlPath)) continue;
2754
- let raw;
2755
- try {
2756
- raw = fs15.readFileSync(yamlPath, "utf-8");
2757
- } catch {
2758
- continue;
2759
- }
2760
- const parsed = parse6(raw);
2761
- const result = SkillMetadataSchema.safeParse(parsed);
2762
- if (!result.success) {
2763
- console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
2764
- continue;
2765
- }
2766
- const meta = result.data;
2767
- const matchesPlatform = platforms.some((p) => meta.platforms.includes(p));
2768
- if (!matchesPlatform) continue;
2769
- const normalized = normalizeName(meta.name);
2770
- if (nameMap.has(normalized)) {
2771
- throw new Error(
2772
- `Name collision: skills "${nameMap.get(normalized)}" and "${meta.name}" both normalize to "${normalized}"`
3557
+ for (const { dir: skillsDir, source } of skillSources) {
3558
+ if (!fs17.existsSync(skillsDir)) continue;
3559
+ const entries = fs17.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3560
+ for (const entry of entries) {
3561
+ const yamlPath = path29.join(skillsDir, entry.name, "skill.yaml");
3562
+ if (!fs17.existsSync(yamlPath)) continue;
3563
+ let raw;
3564
+ try {
3565
+ raw = fs17.readFileSync(yamlPath, "utf-8");
3566
+ } catch {
3567
+ continue;
3568
+ }
3569
+ const parsed = parse7(raw);
3570
+ const result = SkillMetadataSchema.safeParse(parsed);
3571
+ if (!result.success) {
3572
+ console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
3573
+ continue;
3574
+ }
3575
+ const meta = result.data;
3576
+ const matchesPlatform = platforms.some((p) => meta.platforms.includes(p));
3577
+ if (!matchesPlatform) continue;
3578
+ const normalized = normalizeName(meta.name);
3579
+ const existing = nameMap.get(normalized);
3580
+ if (existing) {
3581
+ if (existing.source === source) {
3582
+ throw new Error(
3583
+ `Name collision: skills "${existing.skillName}" and "${meta.name}" both normalize to "${normalized}"`
3584
+ );
3585
+ }
3586
+ continue;
3587
+ }
3588
+ nameMap.set(normalized, { skillName: meta.name, source });
3589
+ const skillMdPath = path29.join(skillsDir, entry.name, "SKILL.md");
3590
+ const skillMdContent = fs17.existsSync(skillMdPath) ? fs17.readFileSync(skillMdPath, "utf-8") : "";
3591
+ const skillMdRelative = path29.relative(
3592
+ process.cwd(),
3593
+ path29.join(skillsDir, entry.name, "SKILL.md")
2773
3594
  );
2774
- }
2775
- nameMap.set(normalized, meta.name);
2776
- const skillMdPath = path23.join(skillsDir, entry.name, "SKILL.md");
2777
- const skillMdContent = fs15.existsSync(skillMdPath) ? fs15.readFileSync(skillMdPath, "utf-8") : "";
2778
- const skillMdRelative = path23.relative(
2779
- process.cwd(),
2780
- path23.join(skillsDir, entry.name, "SKILL.md")
2781
- );
2782
- const skillYamlRelative = path23.relative(
2783
- process.cwd(),
2784
- path23.join(skillsDir, entry.name, "skill.yaml")
2785
- );
2786
- const args = (meta.cli?.args ?? []).map((a) => ({
2787
- name: a.name,
2788
- description: a.description ?? "",
2789
- required: a.required ?? false
2790
- }));
2791
- const tools = [...meta.tools ?? []];
2792
- if (!tools.includes("Read")) {
2793
- tools.push("Read");
2794
- }
2795
- const contextLines = [];
2796
- if (meta.cognitive_mode) {
2797
- contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
2798
- }
2799
- if (meta.type) {
2800
- contextLines.push(`Type: ${meta.type}`);
2801
- }
2802
- if (meta.state?.persistent) {
2803
- const files = meta.state.files?.join(", ") ?? "";
2804
- contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
2805
- }
2806
- const objectiveLines = [meta.description];
2807
- if (meta.phases && meta.phases.length > 0) {
2808
- objectiveLines.push("");
2809
- objectiveLines.push("Phases:");
2810
- for (const phase of meta.phases) {
2811
- const req = phase.required !== false ? "" : " (optional)";
2812
- objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
3595
+ const skillYamlRelative = path29.relative(
3596
+ process.cwd(),
3597
+ path29.join(skillsDir, entry.name, "skill.yaml")
3598
+ );
3599
+ const args = (meta.cli?.args ?? []).map((a) => ({
3600
+ name: a.name,
3601
+ description: a.description ?? "",
3602
+ required: a.required ?? false
3603
+ }));
3604
+ const tools = [...meta.tools ?? []];
3605
+ if (!tools.includes("Read")) {
3606
+ tools.push("Read");
2813
3607
  }
2814
- }
2815
- const executionContextLines = [];
2816
- if (skillMdContent) {
2817
- executionContextLines.push(`@${skillMdRelative}`);
2818
- executionContextLines.push(`@${skillYamlRelative}`);
2819
- }
2820
- const processLines = [];
2821
- if (meta.mcp?.tool) {
2822
- processLines.push(`1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`);
2823
- processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
2824
- processLines.push(`3. Pass through any arguments provided by the user`);
2825
- } else {
2826
- processLines.push(`1. Read SKILL.md and follow its workflow directly`);
2827
- processLines.push(`2. Pass through any arguments provided by the user`);
2828
- }
2829
- specs.push({
2830
- name: normalized,
2831
- namespace: "harness",
2832
- fullName: `harness:${normalized}`,
2833
- description: meta.description,
2834
- version: meta.version,
2835
- ...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
2836
- tools,
2837
- args,
2838
- skillYamlName: meta.name,
2839
- sourceDir: entry.name,
2840
- prompt: {
2841
- context: contextLines.join("\n"),
2842
- objective: objectiveLines.join("\n"),
2843
- executionContext: executionContextLines.join("\n"),
2844
- process: processLines.join("\n")
3608
+ const contextLines = [];
3609
+ if (meta.cognitive_mode) {
3610
+ contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
2845
3611
  }
2846
- });
3612
+ if (meta.type) {
3613
+ contextLines.push(`Type: ${meta.type}`);
3614
+ }
3615
+ if (meta.state?.persistent) {
3616
+ const files = meta.state.files?.join(", ") ?? "";
3617
+ contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
3618
+ }
3619
+ const objectiveLines = [meta.description];
3620
+ if (meta.phases && meta.phases.length > 0) {
3621
+ objectiveLines.push("");
3622
+ objectiveLines.push("Phases:");
3623
+ for (const phase of meta.phases) {
3624
+ const req = phase.required !== false ? "" : " (optional)";
3625
+ objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
3626
+ }
3627
+ }
3628
+ const executionContextLines = [];
3629
+ if (skillMdContent) {
3630
+ executionContextLines.push(`@${skillMdRelative}`);
3631
+ executionContextLines.push(`@${skillYamlRelative}`);
3632
+ }
3633
+ const processLines = [];
3634
+ if (meta.mcp?.tool) {
3635
+ processLines.push(
3636
+ `1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`
3637
+ );
3638
+ processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
3639
+ processLines.push(`3. Pass through any arguments provided by the user`);
3640
+ } else {
3641
+ processLines.push(`1. Read SKILL.md and follow its workflow directly`);
3642
+ processLines.push(`2. Pass through any arguments provided by the user`);
3643
+ }
3644
+ specs.push({
3645
+ name: normalized,
3646
+ namespace: "harness",
3647
+ fullName: `harness:${normalized}`,
3648
+ description: meta.description,
3649
+ version: meta.version,
3650
+ ...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
3651
+ tools,
3652
+ args,
3653
+ skillYamlName: meta.name,
3654
+ sourceDir: entry.name,
3655
+ skillsBaseDir: skillsDir,
3656
+ source,
3657
+ prompt: {
3658
+ context: contextLines.join("\n"),
3659
+ objective: objectiveLines.join("\n"),
3660
+ executionContext: executionContextLines.join("\n"),
3661
+ process: processLines.join("\n")
3662
+ }
3663
+ });
3664
+ }
2847
3665
  }
2848
3666
  return specs;
2849
3667
  }
@@ -2945,19 +3763,24 @@ function renderGemini(spec, skillMdContent, skillYamlContent) {
2945
3763
  }
2946
3764
 
2947
3765
  // src/slash-commands/sync.ts
2948
- import fs16 from "fs";
2949
- import path24 from "path";
3766
+ import fs18 from "fs";
3767
+ import path30 from "path";
3768
+
3769
+ // src/agent-definitions/constants.ts
3770
+ var GENERATED_HEADER_AGENT = "<!-- Generated by harness generate-agent-definitions. Do not edit. -->";
3771
+
3772
+ // src/slash-commands/sync.ts
2950
3773
  function computeSyncPlan(outputDir, rendered) {
2951
3774
  const added = [];
2952
3775
  const updated = [];
2953
3776
  const removed = [];
2954
3777
  const unchanged = [];
2955
3778
  for (const [filename, content] of rendered) {
2956
- const filePath = path24.join(outputDir, filename);
2957
- if (!fs16.existsSync(filePath)) {
3779
+ const filePath = path30.join(outputDir, filename);
3780
+ if (!fs18.existsSync(filePath)) {
2958
3781
  added.push(filename);
2959
3782
  } else {
2960
- const existing = fs16.readFileSync(filePath, "utf-8");
3783
+ const existing = fs18.readFileSync(filePath, "utf-8");
2961
3784
  if (existing === content) {
2962
3785
  unchanged.push(filename);
2963
3786
  } else {
@@ -2965,15 +3788,15 @@ function computeSyncPlan(outputDir, rendered) {
2965
3788
  }
2966
3789
  }
2967
3790
  }
2968
- if (fs16.existsSync(outputDir)) {
2969
- const existing = fs16.readdirSync(outputDir).filter((f) => {
2970
- const stat = fs16.statSync(path24.join(outputDir, f));
3791
+ if (fs18.existsSync(outputDir)) {
3792
+ const existing = fs18.readdirSync(outputDir).filter((f) => {
3793
+ const stat = fs18.statSync(path30.join(outputDir, f));
2971
3794
  return stat.isFile();
2972
3795
  });
2973
3796
  for (const filename of existing) {
2974
3797
  if (rendered.has(filename)) continue;
2975
- const content = fs16.readFileSync(path24.join(outputDir, filename), "utf-8");
2976
- if (content.includes(GENERATED_HEADER_CLAUDE) || content.includes(GENERATED_HEADER_GEMINI)) {
3798
+ const content = fs18.readFileSync(path30.join(outputDir, filename), "utf-8");
3799
+ if (content.includes(GENERATED_HEADER_CLAUDE) || content.includes(GENERATED_HEADER_GEMINI) || content.includes(GENERATED_HEADER_AGENT)) {
2977
3800
  removed.push(filename);
2978
3801
  }
2979
3802
  }
@@ -2981,18 +3804,18 @@ function computeSyncPlan(outputDir, rendered) {
2981
3804
  return { added, updated, removed, unchanged };
2982
3805
  }
2983
3806
  function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
2984
- fs16.mkdirSync(outputDir, { recursive: true });
3807
+ fs18.mkdirSync(outputDir, { recursive: true });
2985
3808
  for (const filename of [...plan.added, ...plan.updated]) {
2986
3809
  const content = rendered.get(filename);
2987
3810
  if (content !== void 0) {
2988
- fs16.writeFileSync(path24.join(outputDir, filename), content);
3811
+ fs18.writeFileSync(path30.join(outputDir, filename), content);
2989
3812
  }
2990
3813
  }
2991
3814
  if (deleteOrphans) {
2992
3815
  for (const filename of plan.removed) {
2993
- const filePath = path24.join(outputDir, filename);
2994
- if (fs16.existsSync(filePath)) {
2995
- fs16.unlinkSync(filePath);
3816
+ const filePath = path30.join(outputDir, filename);
3817
+ if (fs18.existsSync(filePath)) {
3818
+ fs18.unlinkSync(filePath);
2996
3819
  }
2997
3820
  }
2998
3821
  }
@@ -3001,30 +3824,44 @@ function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
3001
3824
  // src/commands/generate-slash-commands.ts
3002
3825
  function resolveOutputDir(platform, opts) {
3003
3826
  if (opts.output) {
3004
- return path25.join(opts.output, "harness");
3827
+ return path31.join(opts.output, "harness");
3005
3828
  }
3006
3829
  if (opts.global) {
3007
3830
  const home = os2.homedir();
3008
- return platform === "claude-code" ? path25.join(home, ".claude", "commands", "harness") : path25.join(home, ".gemini", "commands", "harness");
3831
+ return platform === "claude-code" ? path31.join(home, ".claude", "commands", "harness") : path31.join(home, ".gemini", "commands", "harness");
3009
3832
  }
3010
- return platform === "claude-code" ? path25.join("agents", "commands", "claude-code", "harness") : path25.join("agents", "commands", "gemini-cli", "harness");
3833
+ return platform === "claude-code" ? path31.join("agents", "commands", "claude-code", "harness") : path31.join("agents", "commands", "gemini-cli", "harness");
3011
3834
  }
3012
3835
  function fileExtension(platform) {
3013
3836
  return platform === "claude-code" ? ".md" : ".toml";
3014
3837
  }
3015
3838
  async function confirmDeletion(files) {
3016
3839
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3017
- return new Promise((resolve14) => {
3840
+ return new Promise((resolve22) => {
3018
3841
  rl.question(`
3019
3842
  Remove ${files.length} orphaned command(s)? (y/N) `, (answer) => {
3020
3843
  rl.close();
3021
- resolve14(answer.toLowerCase() === "y");
3844
+ resolve22(answer.toLowerCase() === "y");
3022
3845
  });
3023
3846
  });
3024
3847
  }
3025
3848
  function generateSlashCommands(opts) {
3026
- const skillsDir = opts.skillsDir || resolveSkillsDir();
3027
- const specs = normalizeSkills(skillsDir, opts.platforms);
3849
+ const skillSources = [];
3850
+ if (opts.skillsDir) {
3851
+ skillSources.push({ dir: opts.skillsDir, source: "project" });
3852
+ } else {
3853
+ const projectDir = resolveProjectSkillsDir();
3854
+ if (projectDir) {
3855
+ skillSources.push({ dir: projectDir, source: "project" });
3856
+ }
3857
+ if (opts.includeGlobal || skillSources.length === 0) {
3858
+ const globalDir = resolveGlobalSkillsDir();
3859
+ if (!projectDir || path31.resolve(globalDir) !== path31.resolve(projectDir)) {
3860
+ skillSources.push({ dir: globalDir, source: "global" });
3861
+ }
3862
+ }
3863
+ }
3864
+ const specs = normalizeSkills(skillSources, opts.platforms);
3028
3865
  const results = [];
3029
3866
  for (const platform of opts.platforms) {
3030
3867
  const outputDir = resolveOutputDir(platform, opts);
@@ -3041,7 +3878,7 @@ function generateSlashCommands(opts) {
3041
3878
  executionContext: spec.prompt.executionContext.split("\n").map((line) => {
3042
3879
  if (line.startsWith("@")) {
3043
3880
  const relPath = line.slice(1);
3044
- return `@${path25.resolve(relPath)}`;
3881
+ return `@${path31.resolve(relPath)}`;
3045
3882
  }
3046
3883
  return line;
3047
3884
  }).join("\n")
@@ -3049,10 +3886,10 @@ function generateSlashCommands(opts) {
3049
3886
  } : spec;
3050
3887
  rendered.set(filename, renderClaudeCode(renderSpec));
3051
3888
  } else {
3052
- const mdPath = path25.join(skillsDir, spec.sourceDir, "SKILL.md");
3053
- const yamlPath = path25.join(skillsDir, spec.sourceDir, "skill.yaml");
3054
- const mdContent = fs17.existsSync(mdPath) ? fs17.readFileSync(mdPath, "utf-8") : "";
3055
- const yamlContent = fs17.existsSync(yamlPath) ? fs17.readFileSync(yamlPath, "utf-8") : "";
3889
+ const mdPath = path31.join(spec.skillsBaseDir, spec.sourceDir, "SKILL.md");
3890
+ const yamlPath = path31.join(spec.skillsBaseDir, spec.sourceDir, "skill.yaml");
3891
+ const mdContent = fs19.existsSync(mdPath) ? fs19.readFileSync(mdPath, "utf-8") : "";
3892
+ const yamlContent = fs19.existsSync(yamlPath) ? fs19.readFileSync(yamlPath, "utf-8") : "";
3056
3893
  rendered.set(filename, renderGemini(spec, mdContent, yamlContent));
3057
3894
  }
3058
3895
  }
@@ -3078,18 +3915,18 @@ async function handleOrphanDeletion(results, opts) {
3078
3915
  const shouldDelete = opts.yes || await confirmDeletion(result.removed);
3079
3916
  if (shouldDelete) {
3080
3917
  for (const filename of result.removed) {
3081
- const filePath = path25.join(result.outputDir, filename);
3082
- if (fs17.existsSync(filePath)) {
3083
- fs17.unlinkSync(filePath);
3918
+ const filePath = path31.join(result.outputDir, filename);
3919
+ if (fs19.existsSync(filePath)) {
3920
+ fs19.unlinkSync(filePath);
3084
3921
  }
3085
3922
  }
3086
3923
  }
3087
3924
  }
3088
3925
  }
3089
3926
  function createGenerateSlashCommandsCommand() {
3090
- return new Command28("generate-slash-commands").description(
3927
+ return new Command32("generate-slash-commands").description(
3091
3928
  "Generate native slash commands for Claude Code and Gemini CLI from skill metadata"
3092
- ).option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global config directories", false).option("--output <dir>", "Custom output directory").option("--skills-dir <path>", "Skills directory to scan").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
3929
+ ).option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global config directories", false).option("--include-global", "Include built-in global skills alongside project skills", false).option("--output <dir>", "Custom output directory").option("--skills-dir <path>", "Skills directory to scan").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
3093
3930
  const globalOpts = cmd.optsWithGlobals();
3094
3931
  const platforms = opts.platforms.split(",").map((p) => p.trim());
3095
3932
  for (const p of platforms) {
@@ -3103,6 +3940,7 @@ function createGenerateSlashCommandsCommand() {
3103
3940
  const generateOpts = {
3104
3941
  platforms,
3105
3942
  global: opts.global,
3943
+ includeGlobal: opts.includeGlobal,
3106
3944
  output: opts.output,
3107
3945
  skillsDir: opts.skillsDir ?? "",
3108
3946
  dryRun: opts.dryRun,
@@ -3114,6 +3952,16 @@ function createGenerateSlashCommandsCommand() {
3114
3952
  console.log(JSON.stringify(results, null, 2));
3115
3953
  return;
3116
3954
  }
3955
+ const totalCommands = results.reduce(
3956
+ (sum, r) => sum + r.added.length + r.updated.length + r.unchanged.length,
3957
+ 0
3958
+ );
3959
+ if (totalCommands === 0) {
3960
+ console.log(
3961
+ "\nNo skills found. Use --include-global to include built-in skills, or create a skill with: harness create-skill"
3962
+ );
3963
+ return;
3964
+ }
3117
3965
  for (const result of results) {
3118
3966
  console.log(`
3119
3967
  ${result.platform} \u2192 ${result.outputDir}`);
@@ -3141,10 +3989,10 @@ ${result.platform} \u2192 ${result.outputDir}`);
3141
3989
  }
3142
3990
 
3143
3991
  // src/commands/ci/index.ts
3144
- import { Command as Command31 } from "commander";
3992
+ import { Command as Command35 } from "commander";
3145
3993
 
3146
3994
  // src/commands/ci/check.ts
3147
- import { Command as Command29 } from "commander";
3995
+ import { Command as Command33 } from "commander";
3148
3996
  import { runCIChecks } from "@harness-engineering/core";
3149
3997
  var VALID_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
3150
3998
  async function runCICheck(options) {
@@ -3176,7 +4024,7 @@ function parseFailOn(failOn) {
3176
4024
  return "error";
3177
4025
  }
3178
4026
  function createCheckCommand() {
3179
- return new Command29("check").description("Run all harness checks for CI (validate, deps, docs, entropy, phase-gate)").option("--skip <checks>", "Comma-separated checks to skip (e.g., entropy,docs)").option("--fail-on <severity>", "Fail on severity level: error (default) or warning", "error").action(async (opts, cmd) => {
4027
+ return new Command33("check").description("Run all harness checks for CI (validate, deps, docs, entropy, phase-gate)").option("--skip <checks>", "Comma-separated checks to skip (e.g., entropy,docs)").option("--fail-on <severity>", "Fail on severity level: error (default) or warning", "error").action(async (opts, cmd) => {
3180
4028
  const globalOpts = cmd.optsWithGlobals();
3181
4029
  const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
3182
4030
  const skip = parseSkip(opts.skip);
@@ -3220,10 +4068,10 @@ function createCheckCommand() {
3220
4068
  }
3221
4069
 
3222
4070
  // src/commands/ci/init.ts
3223
- import { Command as Command30 } from "commander";
3224
- import * as fs18 from "fs";
3225
- import * as path26 from "path";
3226
- import { Ok as Ok17, Err as Err14 } from "@harness-engineering/core";
4071
+ import { Command as Command34 } from "commander";
4072
+ import * as fs20 from "fs";
4073
+ import * as path32 from "path";
4074
+ import { Ok as Ok19, Err as Err14 } from "@harness-engineering/core";
3227
4075
  var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
3228
4076
  function buildSkipFlag(checks) {
3229
4077
  if (!checks) return "";
@@ -3309,18 +4157,18 @@ function generateCIConfig(options) {
3309
4157
  if (!entry) {
3310
4158
  return Err14(new CLIError(`Unknown platform: ${platform}`, ExitCode.ERROR));
3311
4159
  }
3312
- return Ok17({
4160
+ return Ok19({
3313
4161
  filename: entry.filename,
3314
4162
  content: entry.generate(skipFlag)
3315
4163
  });
3316
4164
  }
3317
4165
  function detectPlatform() {
3318
- if (fs18.existsSync(".github")) return "github";
3319
- if (fs18.existsSync(".gitlab-ci.yml")) return "gitlab";
4166
+ if (fs20.existsSync(".github")) return "github";
4167
+ if (fs20.existsSync(".gitlab-ci.yml")) return "gitlab";
3320
4168
  return null;
3321
4169
  }
3322
4170
  function createInitCommand2() {
3323
- return new Command30("init").description("Generate CI configuration for harness checks").option("--platform <platform>", "CI platform: github, gitlab, or generic").option("--checks <list>", "Comma-separated list of checks to include").action(async (opts, cmd) => {
4171
+ return new Command34("init").description("Generate CI configuration for harness checks").option("--platform <platform>", "CI platform: github, gitlab, or generic").option("--checks <list>", "Comma-separated list of checks to include").action(async (opts, cmd) => {
3324
4172
  const globalOpts = cmd.optsWithGlobals();
3325
4173
  const platform = opts.platform ?? detectPlatform() ?? "generic";
3326
4174
  const checks = opts.checks ? opts.checks.split(",").map((s) => s.trim()) : void 0;
@@ -3332,12 +4180,12 @@ function createInitCommand2() {
3332
4180
  process.exit(result.error.exitCode);
3333
4181
  }
3334
4182
  const { filename, content } = result.value;
3335
- const targetPath = path26.resolve(filename);
3336
- const dir = path26.dirname(targetPath);
3337
- fs18.mkdirSync(dir, { recursive: true });
3338
- fs18.writeFileSync(targetPath, content);
4183
+ const targetPath = path32.resolve(filename);
4184
+ const dir = path32.dirname(targetPath);
4185
+ fs20.mkdirSync(dir, { recursive: true });
4186
+ fs20.writeFileSync(targetPath, content);
3339
4187
  if (platform === "generic") {
3340
- fs18.chmodSync(targetPath, "755");
4188
+ fs20.chmodSync(targetPath, "755");
3341
4189
  }
3342
4190
  if (globalOpts.json) {
3343
4191
  console.log(JSON.stringify({ file: filename, platform }));
@@ -3350,15 +4198,15 @@ function createInitCommand2() {
3350
4198
 
3351
4199
  // src/commands/ci/index.ts
3352
4200
  function createCICommand() {
3353
- const command = new Command31("ci").description("CI/CD integration commands");
4201
+ const command = new Command35("ci").description("CI/CD integration commands");
3354
4202
  command.addCommand(createCheckCommand());
3355
4203
  command.addCommand(createInitCommand2());
3356
4204
  return command;
3357
4205
  }
3358
4206
 
3359
4207
  // src/commands/update.ts
3360
- import { Command as Command32 } from "commander";
3361
- import { execSync as execSync3 } from "child_process";
4208
+ import { Command as Command36 } from "commander";
4209
+ import { execSync as execSync4 } from "child_process";
3362
4210
  import { realpathSync } from "fs";
3363
4211
  import readline3 from "readline";
3364
4212
  import chalk4 from "chalk";
@@ -3378,7 +4226,7 @@ function detectPackageManager() {
3378
4226
  return "npm";
3379
4227
  }
3380
4228
  function getLatestVersion(pkg = "@harness-engineering/cli") {
3381
- const output = execSync3(`npm view ${pkg} dist-tags.latest`, {
4229
+ const output = execSync4(`npm view ${pkg} dist-tags.latest`, {
3382
4230
  encoding: "utf-8",
3383
4231
  timeout: 15e3
3384
4232
  });
@@ -3386,7 +4234,7 @@ function getLatestVersion(pkg = "@harness-engineering/cli") {
3386
4234
  }
3387
4235
  function getInstalledVersion(pm) {
3388
4236
  try {
3389
- const output = execSync3(`${pm} list -g @harness-engineering/cli --json`, {
4237
+ const output = execSync4(`${pm} list -g @harness-engineering/cli --json`, {
3390
4238
  encoding: "utf-8",
3391
4239
  timeout: 15e3
3392
4240
  });
@@ -3399,7 +4247,7 @@ function getInstalledVersion(pm) {
3399
4247
  }
3400
4248
  function getInstalledPackages(pm) {
3401
4249
  try {
3402
- const output = execSync3(`${pm} list -g --json`, {
4250
+ const output = execSync4(`${pm} list -g --json`, {
3403
4251
  encoding: "utf-8",
3404
4252
  timeout: 15e3
3405
4253
  });
@@ -3415,15 +4263,15 @@ function prompt(question) {
3415
4263
  input: process.stdin,
3416
4264
  output: process.stdout
3417
4265
  });
3418
- return new Promise((resolve14) => {
4266
+ return new Promise((resolve22) => {
3419
4267
  rl.question(question, (answer) => {
3420
4268
  rl.close();
3421
- resolve14(answer.trim().toLowerCase());
4269
+ resolve22(answer.trim().toLowerCase());
3422
4270
  });
3423
4271
  });
3424
4272
  }
3425
4273
  function createUpdateCommand() {
3426
- return new Command32("update").description("Update all @harness-engineering packages to the latest version").option("--version <semver>", "Pin @harness-engineering/cli to a specific version").action(async (opts, cmd) => {
4274
+ return new Command36("update").description("Update all @harness-engineering packages to the latest version").option("--version <semver>", "Pin @harness-engineering/cli to a specific version").action(async (opts, cmd) => {
3427
4275
  const globalOpts = cmd.optsWithGlobals();
3428
4276
  const pm = detectPackageManager();
3429
4277
  if (globalOpts.verbose) {
@@ -3466,7 +4314,7 @@ function createUpdateCommand() {
3466
4314
  }
3467
4315
  try {
3468
4316
  logger.info("Updating packages...");
3469
- execSync3(installCmd, { stdio: "inherit", timeout: 12e4 });
4317
+ execSync4(installCmd, { stdio: "inherit", timeout: 12e4 });
3470
4318
  console.log("");
3471
4319
  logger.success("Update complete");
3472
4320
  } catch {
@@ -3476,28 +4324,676 @@ function createUpdateCommand() {
3476
4324
  process.exit(ExitCode.ERROR);
3477
4325
  }
3478
4326
  console.log("");
3479
- const regenAnswer = await prompt("Regenerate slash commands? (y/N) ");
4327
+ const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (y/N) ");
3480
4328
  if (regenAnswer === "y" || regenAnswer === "yes") {
3481
4329
  const scopeAnswer = await prompt("Generate for (g)lobal or (l)ocal project? (g/l) ");
3482
4330
  const globalFlag = scopeAnswer === "g" || scopeAnswer === "global" ? " --global" : "";
3483
4331
  try {
3484
- execSync3(`harness generate-slash-commands${globalFlag}`, { stdio: "inherit" });
4332
+ execSync4(`harness generate${globalFlag}`, { stdio: "inherit" });
3485
4333
  } catch {
3486
- logger.warn("Slash command generation failed. Run manually:");
3487
- console.log(` ${chalk4.cyan(`harness generate-slash-commands${globalFlag}`)}`);
4334
+ logger.warn("Generation failed. Run manually:");
4335
+ console.log(` ${chalk4.cyan(`harness generate${globalFlag}`)}`);
3488
4336
  }
3489
4337
  }
3490
4338
  process.exit(ExitCode.SUCCESS);
3491
4339
  });
3492
4340
  }
3493
4341
 
4342
+ // src/commands/generate-agent-definitions.ts
4343
+ import { Command as Command37 } from "commander";
4344
+ import * as fs21 from "fs";
4345
+ import * as path33 from "path";
4346
+ import * as os3 from "os";
4347
+
4348
+ // src/agent-definitions/generator.ts
4349
+ var AGENT_DESCRIPTIONS = {
4350
+ "code-reviewer": "Perform code review and address review findings using harness methodology. Use when reviewing code, fixing review findings, responding to review feedback, or when a code review has produced issues that need to be addressed.",
4351
+ "task-executor": "Execute implementation plans task-by-task with state tracking, TDD, and verification. Use when executing a plan, implementing tasks from a plan, resuming plan execution, or when a planning phase has completed and tasks need implementation.",
4352
+ "parallel-coordinator": "Dispatch independent tasks across isolated agents for parallel execution. Use when multiple independent tasks need to run concurrently, splitting work across agents, or coordinating parallel implementation.",
4353
+ "architecture-enforcer": "Validate architectural constraints and dependency rules. Use when checking layer boundaries, detecting circular dependencies, or verifying import direction compliance.",
4354
+ "documentation-maintainer": "Keep documentation in sync with source code. Use when detecting documentation drift, validating doc coverage, or aligning docs with code changes.",
4355
+ "entropy-cleaner": "Detect and fix codebase entropy including drift, dead code, and pattern violations. Use when running cleanup, detecting dead code, or fixing pattern violations."
4356
+ };
4357
+ var DEFAULT_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep"];
4358
+ function generateAgentDefinition(persona, skillContents) {
4359
+ const kebabName = toKebabCase(persona.name);
4360
+ const name = `harness-${kebabName}`;
4361
+ const description = AGENT_DESCRIPTIONS[kebabName] ?? persona.description;
4362
+ const methodologyParts = [];
4363
+ for (const skillName of persona.skills) {
4364
+ const content = skillContents.get(skillName);
4365
+ if (content) {
4366
+ methodologyParts.push(content);
4367
+ }
4368
+ }
4369
+ return {
4370
+ name,
4371
+ description,
4372
+ tools: DEFAULT_TOOLS,
4373
+ role: persona.role,
4374
+ skills: persona.skills,
4375
+ steps: persona.steps,
4376
+ methodology: methodologyParts.join("\n\n---\n\n")
4377
+ };
4378
+ }
4379
+
4380
+ // src/agent-definitions/render-claude-code.ts
4381
+ function formatStep(step, index) {
4382
+ if ("command" in step && step.command) {
4383
+ const cmd = step.command;
4384
+ const when = step.when ?? "always";
4385
+ return `${index + 1}. Run \`harness ${cmd}\` (${when})`;
4386
+ }
4387
+ if ("skill" in step && step.skill) {
4388
+ const skill = step.skill;
4389
+ const when = step.when ?? "always";
4390
+ return `${index + 1}. Execute ${skill} skill (${when})`;
4391
+ }
4392
+ return `${index + 1}. Unknown step`;
4393
+ }
4394
+ function renderClaudeCodeAgent(def) {
4395
+ const lines = ["---"];
4396
+ lines.push(`name: ${def.name}`);
4397
+ lines.push(`description: >`);
4398
+ lines.push(` ${def.description}`);
4399
+ lines.push(`tools: ${def.tools.join(", ")}`);
4400
+ lines.push("---");
4401
+ lines.push("");
4402
+ lines.push(GENERATED_HEADER_AGENT);
4403
+ lines.push("");
4404
+ lines.push("## Role");
4405
+ lines.push("");
4406
+ lines.push(def.role);
4407
+ lines.push("");
4408
+ lines.push("## Skills");
4409
+ lines.push("");
4410
+ for (const skill of def.skills) {
4411
+ lines.push(`- ${skill}`);
4412
+ }
4413
+ lines.push("");
4414
+ lines.push("## Steps");
4415
+ lines.push("");
4416
+ def.steps.forEach((step, i) => {
4417
+ lines.push(formatStep(step, i));
4418
+ });
4419
+ lines.push("");
4420
+ if (def.methodology) {
4421
+ lines.push("## Methodology");
4422
+ lines.push("");
4423
+ lines.push(def.methodology);
4424
+ lines.push("");
4425
+ }
4426
+ return lines.join("\n");
4427
+ }
4428
+
4429
+ // src/agent-definitions/render-gemini-cli.ts
4430
+ function formatStep2(step, index) {
4431
+ if ("command" in step && step.command) {
4432
+ const cmd = step.command;
4433
+ const when = step.when ?? "always";
4434
+ return `${index + 1}. Run \`harness ${cmd}\` (${when})`;
4435
+ }
4436
+ if ("skill" in step && step.skill) {
4437
+ const skill = step.skill;
4438
+ const when = step.when ?? "always";
4439
+ return `${index + 1}. Execute ${skill} skill (${when})`;
4440
+ }
4441
+ return `${index + 1}. Unknown step`;
4442
+ }
4443
+ function renderGeminiAgent(def) {
4444
+ const lines = ["---"];
4445
+ lines.push(`name: ${def.name}`);
4446
+ lines.push(`description: >`);
4447
+ lines.push(` ${def.description}`);
4448
+ lines.push(`tools: ${def.tools.join(", ")}`);
4449
+ lines.push("---");
4450
+ lines.push("");
4451
+ lines.push(GENERATED_HEADER_AGENT);
4452
+ lines.push("");
4453
+ lines.push("## Role");
4454
+ lines.push("");
4455
+ lines.push(def.role);
4456
+ lines.push("");
4457
+ lines.push("## Skills");
4458
+ lines.push("");
4459
+ for (const skill of def.skills) {
4460
+ lines.push(`- ${skill}`);
4461
+ }
4462
+ lines.push("");
4463
+ lines.push("## Steps");
4464
+ lines.push("");
4465
+ def.steps.forEach((step, i) => {
4466
+ lines.push(formatStep2(step, i));
4467
+ });
4468
+ lines.push("");
4469
+ if (def.methodology) {
4470
+ lines.push("## Methodology");
4471
+ lines.push("");
4472
+ lines.push(def.methodology);
4473
+ lines.push("");
4474
+ }
4475
+ return lines.join("\n");
4476
+ }
4477
+
4478
+ // src/commands/generate-agent-definitions.ts
4479
+ function resolveOutputDir2(platform, opts) {
4480
+ if (opts.output) {
4481
+ return platform === "claude-code" ? path33.join(opts.output, "claude-code") : path33.join(opts.output, "gemini-cli");
4482
+ }
4483
+ if (opts.global) {
4484
+ const home = os3.homedir();
4485
+ return platform === "claude-code" ? path33.join(home, ".claude", "agents") : path33.join(home, ".gemini", "agents");
4486
+ }
4487
+ return platform === "claude-code" ? path33.join("agents", "agents", "claude-code") : path33.join("agents", "agents", "gemini-cli");
4488
+ }
4489
+ function loadSkillContent(skillName) {
4490
+ const skillsDir = resolveSkillsDir();
4491
+ const skillMdPath = path33.join(skillsDir, skillName, "SKILL.md");
4492
+ if (!fs21.existsSync(skillMdPath)) return null;
4493
+ return fs21.readFileSync(skillMdPath, "utf-8");
4494
+ }
4495
+ function getRenderer(platform) {
4496
+ return platform === "claude-code" ? renderClaudeCodeAgent : renderGeminiAgent;
4497
+ }
4498
+ function generateAgentDefinitions(opts) {
4499
+ const personasDir = resolvePersonasDir();
4500
+ const personaList = listPersonas(personasDir);
4501
+ if (!personaList.ok) return [];
4502
+ const personas = personaList.value.map((meta) => loadPersona(meta.filePath)).filter((r) => r.ok).map((r) => r.value);
4503
+ const allSkillNames = new Set(personas.flatMap((p) => p.skills));
4504
+ const skillContents = /* @__PURE__ */ new Map();
4505
+ for (const skillName of allSkillNames) {
4506
+ const content = loadSkillContent(skillName);
4507
+ if (content) {
4508
+ skillContents.set(skillName, content);
4509
+ }
4510
+ }
4511
+ const definitions = personas.map((p) => generateAgentDefinition(p, skillContents));
4512
+ const results = [];
4513
+ for (const platform of opts.platforms) {
4514
+ const outputDir = resolveOutputDir2(platform, opts);
4515
+ const renderer = getRenderer(platform);
4516
+ const rendered = /* @__PURE__ */ new Map();
4517
+ for (const def of definitions) {
4518
+ const filename = `${def.name}.md`;
4519
+ rendered.set(filename, renderer(def));
4520
+ }
4521
+ const plan = computeSyncPlan(outputDir, rendered);
4522
+ if (!opts.dryRun) {
4523
+ applySyncPlan(outputDir, rendered, plan, false);
4524
+ }
4525
+ results.push({
4526
+ platform,
4527
+ added: plan.added,
4528
+ updated: plan.updated,
4529
+ removed: plan.removed,
4530
+ unchanged: plan.unchanged,
4531
+ outputDir
4532
+ });
4533
+ }
4534
+ return results;
4535
+ }
4536
+ function createGenerateAgentDefinitionsCommand() {
4537
+ return new Command37("generate-agent-definitions").description("Generate agent definition files from personas for Claude Code and Gemini CLI").option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global agent directories", false).option("--output <dir>", "Custom output directory").option("--dry-run", "Show what would change without writing", false).action(async (opts, cmd) => {
4538
+ const globalOpts = cmd.optsWithGlobals();
4539
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
4540
+ for (const p of platforms) {
4541
+ if (!VALID_PLATFORMS.includes(p)) {
4542
+ throw new CLIError(
4543
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
4544
+ ExitCode.VALIDATION_FAILED
4545
+ );
4546
+ }
4547
+ }
4548
+ try {
4549
+ const results = generateAgentDefinitions({
4550
+ platforms,
4551
+ global: opts.global,
4552
+ output: opts.output,
4553
+ dryRun: opts.dryRun
4554
+ });
4555
+ if (globalOpts.json) {
4556
+ console.log(JSON.stringify(results, null, 2));
4557
+ return;
4558
+ }
4559
+ for (const result of results) {
4560
+ console.log(`
4561
+ ${result.platform} \u2192 ${result.outputDir}`);
4562
+ if (result.added.length > 0) {
4563
+ console.log(` + ${result.added.length} new: ${result.added.join(", ")}`);
4564
+ }
4565
+ if (result.updated.length > 0) {
4566
+ console.log(` ~ ${result.updated.length} updated: ${result.updated.join(", ")}`);
4567
+ }
4568
+ if (result.removed.length > 0) {
4569
+ console.log(` - ${result.removed.length} removed: ${result.removed.join(", ")}`);
4570
+ }
4571
+ if (result.unchanged.length > 0) {
4572
+ console.log(` = ${result.unchanged.length} unchanged`);
4573
+ }
4574
+ if (opts.dryRun) {
4575
+ console.log(" (dry run \u2014 no files written)");
4576
+ }
4577
+ }
4578
+ } catch (error) {
4579
+ handleError(error);
4580
+ }
4581
+ });
4582
+ }
4583
+
4584
+ // src/commands/generate.ts
4585
+ import { Command as Command38 } from "commander";
4586
+ function createGenerateCommand3() {
4587
+ return new Command38("generate").description("Generate all platform integrations (slash commands + agent definitions)").option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global directories", false).option("--include-global", "Include built-in global skills", false).option("--output <dir>", "Custom output directory").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
4588
+ const globalOpts = cmd.optsWithGlobals();
4589
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
4590
+ for (const p of platforms) {
4591
+ if (!VALID_PLATFORMS.includes(p)) {
4592
+ throw new CLIError(
4593
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
4594
+ ExitCode.VALIDATION_FAILED
4595
+ );
4596
+ }
4597
+ }
4598
+ try {
4599
+ console.log("Generating slash commands...");
4600
+ const slashResults = generateSlashCommands({
4601
+ platforms,
4602
+ global: opts.global,
4603
+ includeGlobal: opts.includeGlobal,
4604
+ output: opts.output,
4605
+ skillsDir: "",
4606
+ dryRun: opts.dryRun,
4607
+ yes: opts.yes
4608
+ });
4609
+ for (const result of slashResults) {
4610
+ const total = result.added.length + result.updated.length + result.unchanged.length;
4611
+ console.log(
4612
+ ` ${result.platform}: ${total} commands (${result.added.length} new, ${result.updated.length} updated)`
4613
+ );
4614
+ }
4615
+ await handleOrphanDeletion(slashResults, { yes: opts.yes, dryRun: opts.dryRun });
4616
+ console.log("\nGenerating agent definitions...");
4617
+ const agentResults = generateAgentDefinitions({
4618
+ platforms,
4619
+ global: opts.global,
4620
+ output: opts.output,
4621
+ dryRun: opts.dryRun
4622
+ });
4623
+ for (const result of agentResults) {
4624
+ const total = result.added.length + result.updated.length + result.unchanged.length;
4625
+ console.log(
4626
+ ` ${result.platform}: ${total} agents (${result.added.length} new, ${result.updated.length} updated)`
4627
+ );
4628
+ }
4629
+ if (opts.dryRun) {
4630
+ console.log("\n(dry run \u2014 no files written)");
4631
+ } else {
4632
+ console.log("\nDone.");
4633
+ }
4634
+ if (globalOpts.json) {
4635
+ console.log(
4636
+ JSON.stringify({ slashCommands: slashResults, agentDefinitions: agentResults }, null, 2)
4637
+ );
4638
+ }
4639
+ } catch (error) {
4640
+ handleError(error);
4641
+ }
4642
+ });
4643
+ }
4644
+
4645
+ // src/commands/graph/scan.ts
4646
+ import { Command as Command39 } from "commander";
4647
+ import * as path34 from "path";
4648
+ async function runScan(projectPath) {
4649
+ const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("@harness-engineering/graph");
4650
+ const store = new GraphStore();
4651
+ const start = Date.now();
4652
+ await new CodeIngestor(store).ingest(projectPath);
4653
+ new TopologicalLinker(store).link();
4654
+ const knowledgeIngestor = new KnowledgeIngestor(store);
4655
+ await knowledgeIngestor.ingestAll(projectPath);
4656
+ try {
4657
+ await new GitIngestor(store).ingest(projectPath);
4658
+ } catch {
4659
+ }
4660
+ const graphDir = path34.join(projectPath, ".harness", "graph");
4661
+ await store.save(graphDir);
4662
+ return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
4663
+ }
4664
+ function createScanCommand() {
4665
+ return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
4666
+ const projectPath = path34.resolve(inputPath);
4667
+ const globalOpts = cmd.optsWithGlobals();
4668
+ try {
4669
+ const result = await runScan(projectPath);
4670
+ if (globalOpts.json) {
4671
+ console.log(JSON.stringify(result));
4672
+ } else {
4673
+ console.log(
4674
+ `Graph built: ${result.nodeCount} nodes, ${result.edgeCount} edges (${result.durationMs}ms)`
4675
+ );
4676
+ }
4677
+ } catch (err) {
4678
+ console.error("Scan failed:", err instanceof Error ? err.message : err);
4679
+ process.exit(2);
4680
+ }
4681
+ });
4682
+ }
4683
+
4684
+ // src/commands/graph/ingest.ts
4685
+ import { Command as Command40 } from "commander";
4686
+ import * as path35 from "path";
4687
+ async function loadConnectorConfig(projectPath, source) {
4688
+ try {
4689
+ const fs22 = await import("fs/promises");
4690
+ const configPath = path35.join(projectPath, "harness.config.json");
4691
+ const config = JSON.parse(await fs22.readFile(configPath, "utf-8"));
4692
+ const connector = config.graph?.connectors?.find(
4693
+ (c) => c.source === source
4694
+ );
4695
+ return connector?.config ?? {};
4696
+ } catch {
4697
+ return {};
4698
+ }
4699
+ }
4700
+ function mergeResults(...results) {
4701
+ return results.reduce(
4702
+ (acc, r) => ({
4703
+ nodesAdded: acc.nodesAdded + r.nodesAdded,
4704
+ nodesUpdated: acc.nodesUpdated + r.nodesUpdated,
4705
+ edgesAdded: acc.edgesAdded + r.edgesAdded,
4706
+ edgesUpdated: acc.edgesUpdated + r.edgesUpdated,
4707
+ errors: [...acc.errors, ...r.errors],
4708
+ durationMs: acc.durationMs + r.durationMs
4709
+ }),
4710
+ {
4711
+ nodesAdded: 0,
4712
+ nodesUpdated: 0,
4713
+ edgesAdded: 0,
4714
+ edgesUpdated: 0,
4715
+ errors: [],
4716
+ durationMs: 0
4717
+ }
4718
+ );
4719
+ }
4720
+ async function runIngest(projectPath, source, opts) {
4721
+ const {
4722
+ GraphStore,
4723
+ CodeIngestor,
4724
+ TopologicalLinker,
4725
+ KnowledgeIngestor,
4726
+ GitIngestor,
4727
+ SyncManager,
4728
+ JiraConnector,
4729
+ SlackConnector
4730
+ } = await import("@harness-engineering/graph");
4731
+ const graphDir = path35.join(projectPath, ".harness", "graph");
4732
+ const store = new GraphStore();
4733
+ await store.load(graphDir);
4734
+ if (opts?.all) {
4735
+ const startMs = Date.now();
4736
+ const codeResult = await new CodeIngestor(store).ingest(projectPath);
4737
+ new TopologicalLinker(store).link();
4738
+ const knowledgeResult = await new KnowledgeIngestor(store).ingestAll(projectPath);
4739
+ const gitResult = await new GitIngestor(store).ingest(projectPath);
4740
+ const syncManager = new SyncManager(store, graphDir);
4741
+ const connectorMap = {
4742
+ jira: () => new JiraConnector(),
4743
+ slack: () => new SlackConnector()
4744
+ };
4745
+ for (const [name, factory] of Object.entries(connectorMap)) {
4746
+ const config = await loadConnectorConfig(projectPath, name);
4747
+ syncManager.registerConnector(factory(), config);
4748
+ }
4749
+ const connectorResult = await syncManager.syncAll();
4750
+ await store.save(graphDir);
4751
+ const merged = mergeResults(codeResult, knowledgeResult, gitResult, connectorResult);
4752
+ return { ...merged, durationMs: Date.now() - startMs };
4753
+ }
4754
+ let result;
4755
+ switch (source) {
4756
+ case "code":
4757
+ result = await new CodeIngestor(store).ingest(projectPath);
4758
+ new TopologicalLinker(store).link();
4759
+ break;
4760
+ case "knowledge":
4761
+ result = await new KnowledgeIngestor(store).ingestAll(projectPath);
4762
+ break;
4763
+ case "git":
4764
+ result = await new GitIngestor(store).ingest(projectPath);
4765
+ break;
4766
+ default: {
4767
+ const knownConnectors = ["jira", "slack"];
4768
+ if (!knownConnectors.includes(source)) {
4769
+ throw new Error(`Unknown source: ${source}. Available: code, knowledge, git, jira, slack`);
4770
+ }
4771
+ if (!SyncManager) {
4772
+ throw new Error(
4773
+ `Connector support not available. Ensure @harness-engineering/graph is built with connector support.`
4774
+ );
4775
+ }
4776
+ const syncManager = new SyncManager(store, graphDir);
4777
+ const extConnectorMap = {
4778
+ jira: () => new JiraConnector(),
4779
+ slack: () => new SlackConnector()
4780
+ };
4781
+ const factory = extConnectorMap[source];
4782
+ const config = await loadConnectorConfig(projectPath, source);
4783
+ syncManager.registerConnector(factory(), config);
4784
+ result = await syncManager.sync(source);
4785
+ break;
4786
+ }
4787
+ }
4788
+ await store.save(graphDir);
4789
+ return result;
4790
+ }
4791
+ function createIngestCommand() {
4792
+ return new Command40("ingest").description("Ingest data into the knowledge graph").option("--source <name>", "Source to ingest (code, knowledge, git, jira, slack)").option("--all", "Run all sources (code, knowledge, git, and configured connectors)").option("--full", "Force full re-ingestion").action(async (opts, cmd) => {
4793
+ if (!opts.source && !opts.all) {
4794
+ console.error("Error: --source or --all is required");
4795
+ process.exit(1);
4796
+ }
4797
+ const globalOpts = cmd.optsWithGlobals();
4798
+ const projectPath = path35.resolve(globalOpts.config ? path35.dirname(globalOpts.config) : ".");
4799
+ try {
4800
+ const result = await runIngest(projectPath, opts.source ?? "", {
4801
+ full: opts.full,
4802
+ all: opts.all
4803
+ });
4804
+ if (globalOpts.json) {
4805
+ console.log(JSON.stringify(result));
4806
+ } else {
4807
+ const label = opts.all ? "all" : opts.source;
4808
+ console.log(
4809
+ `Ingested (${label}): +${result.nodesAdded} nodes, +${result.edgesAdded} edges (${result.durationMs}ms)`
4810
+ );
4811
+ }
4812
+ } catch (err) {
4813
+ console.error("Ingest failed:", err instanceof Error ? err.message : err);
4814
+ process.exit(2);
4815
+ }
4816
+ });
4817
+ }
4818
+
4819
+ // src/commands/graph/query.ts
4820
+ import { Command as Command41 } from "commander";
4821
+ import * as path36 from "path";
4822
+ async function runQuery(projectPath, rootNodeId, opts) {
4823
+ const { GraphStore, ContextQL } = await import("@harness-engineering/graph");
4824
+ const store = new GraphStore();
4825
+ const graphDir = path36.join(projectPath, ".harness", "graph");
4826
+ const loaded = await store.load(graphDir);
4827
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
4828
+ const params = {
4829
+ rootNodeIds: [rootNodeId],
4830
+ maxDepth: opts.depth ?? 3,
4831
+ bidirectional: opts.bidirectional ?? false,
4832
+ ...opts.types ? { includeTypes: opts.types.split(",") } : {},
4833
+ ...opts.edges ? { includeEdges: opts.edges.split(",") } : {}
4834
+ };
4835
+ const cql = new ContextQL(store);
4836
+ return cql.execute(params);
4837
+ }
4838
+ function createQueryCommand() {
4839
+ 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) => {
4840
+ const globalOpts = cmd.optsWithGlobals();
4841
+ const projectPath = path36.resolve(globalOpts.config ? path36.dirname(globalOpts.config) : ".");
4842
+ try {
4843
+ const result = await runQuery(projectPath, rootNodeId, {
4844
+ depth: parseInt(opts.depth),
4845
+ types: opts.types,
4846
+ edges: opts.edges,
4847
+ bidirectional: opts.bidirectional
4848
+ });
4849
+ if (globalOpts.json) {
4850
+ console.log(JSON.stringify(result, null, 2));
4851
+ } else {
4852
+ console.log(
4853
+ `Found ${result.nodes.length} nodes, ${result.edges.length} edges (depth ${result.stats.depthReached}, pruned ${result.stats.pruned})`
4854
+ );
4855
+ for (const node of result.nodes) {
4856
+ console.log(` ${node.type.padEnd(12)} ${node.id}`);
4857
+ }
4858
+ }
4859
+ } catch (err) {
4860
+ console.error("Query failed:", err instanceof Error ? err.message : err);
4861
+ process.exit(2);
4862
+ }
4863
+ });
4864
+ }
4865
+
4866
+ // src/commands/graph/index.ts
4867
+ import { Command as Command42 } from "commander";
4868
+
4869
+ // src/commands/graph/status.ts
4870
+ import * as path37 from "path";
4871
+ async function runGraphStatus(projectPath) {
4872
+ const { GraphStore } = await import("@harness-engineering/graph");
4873
+ const graphDir = path37.join(projectPath, ".harness", "graph");
4874
+ const store = new GraphStore();
4875
+ const loaded = await store.load(graphDir);
4876
+ if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
4877
+ const fs22 = await import("fs/promises");
4878
+ const metaPath = path37.join(graphDir, "metadata.json");
4879
+ let lastScan = "unknown";
4880
+ try {
4881
+ const meta = JSON.parse(await fs22.readFile(metaPath, "utf-8"));
4882
+ lastScan = meta.lastScanTimestamp;
4883
+ } catch {
4884
+ }
4885
+ const allNodes = store.findNodes({});
4886
+ const nodesByType = {};
4887
+ for (const node of allNodes) {
4888
+ nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
4889
+ }
4890
+ let connectorSyncStatus = {};
4891
+ try {
4892
+ const syncMetaPath = path37.join(graphDir, "sync-metadata.json");
4893
+ const syncMeta = JSON.parse(await fs22.readFile(syncMetaPath, "utf-8"));
4894
+ for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
4895
+ connectorSyncStatus[name] = data.lastSyncTimestamp;
4896
+ }
4897
+ } catch {
4898
+ }
4899
+ return {
4900
+ status: "ok",
4901
+ nodeCount: store.nodeCount,
4902
+ edgeCount: store.edgeCount,
4903
+ nodesByType,
4904
+ lastScanTimestamp: lastScan,
4905
+ ...Object.keys(connectorSyncStatus).length > 0 ? { connectorSyncStatus } : {}
4906
+ };
4907
+ }
4908
+
4909
+ // src/commands/graph/export.ts
4910
+ import * as path38 from "path";
4911
+ async function runGraphExport(projectPath, format) {
4912
+ const { GraphStore } = await import("@harness-engineering/graph");
4913
+ const graphDir = path38.join(projectPath, ".harness", "graph");
4914
+ const store = new GraphStore();
4915
+ const loaded = await store.load(graphDir);
4916
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
4917
+ if (format === "json") {
4918
+ const nodes = store.findNodes({});
4919
+ const edges = store.getEdges({});
4920
+ return JSON.stringify({ nodes, edges }, null, 2);
4921
+ }
4922
+ if (format === "mermaid") {
4923
+ const nodes = store.findNodes({});
4924
+ const edges = store.getEdges({});
4925
+ const lines = ["graph TD"];
4926
+ for (const node of nodes.slice(0, 200)) {
4927
+ const safeId = node.id.replace(/[^a-zA-Z0-9]/g, "_");
4928
+ const safeName = node.name.replace(/"/g, "#quot;");
4929
+ lines.push(` ${safeId}["${safeName}"]`);
4930
+ }
4931
+ for (const edge of edges.slice(0, 500)) {
4932
+ const safeFrom = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
4933
+ const safeTo = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
4934
+ lines.push(` ${safeFrom} -->|${edge.type}| ${safeTo}`);
4935
+ }
4936
+ return lines.join("\n");
4937
+ }
4938
+ throw new Error(`Unknown format: ${format}. Available: json, mermaid`);
4939
+ }
4940
+
4941
+ // src/commands/graph/index.ts
4942
+ import * as path39 from "path";
4943
+ function createGraphCommand() {
4944
+ const graph = new Command42("graph").description("Knowledge graph management");
4945
+ graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
4946
+ try {
4947
+ const globalOpts = cmd.optsWithGlobals();
4948
+ const projectPath = path39.resolve(globalOpts.config ? path39.dirname(globalOpts.config) : ".");
4949
+ const result = await runGraphStatus(projectPath);
4950
+ if (globalOpts.json) {
4951
+ console.log(JSON.stringify(result, null, 2));
4952
+ } else if (result.status === "no_graph") {
4953
+ console.log(result.message);
4954
+ } else {
4955
+ console.log(`Graph: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
4956
+ console.log(`Last scan: ${result.lastScanTimestamp}`);
4957
+ console.log("Nodes by type:");
4958
+ for (const [type, count] of Object.entries(result.nodesByType)) {
4959
+ console.log(` ${type}: ${count}`);
4960
+ }
4961
+ if (result.connectorSyncStatus) {
4962
+ console.log("Connector sync status:");
4963
+ for (const [name, timestamp] of Object.entries(result.connectorSyncStatus)) {
4964
+ console.log(` ${name}: last synced ${timestamp}`);
4965
+ }
4966
+ }
4967
+ }
4968
+ } catch (err) {
4969
+ console.error("Status failed:", err instanceof Error ? err.message : err);
4970
+ process.exit(2);
4971
+ }
4972
+ });
4973
+ graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
4974
+ const globalOpts = cmd.optsWithGlobals();
4975
+ const projectPath = path39.resolve(globalOpts.config ? path39.dirname(globalOpts.config) : ".");
4976
+ try {
4977
+ const output = await runGraphExport(projectPath, opts.format);
4978
+ console.log(output);
4979
+ } catch (err) {
4980
+ console.error("Export failed:", err instanceof Error ? err.message : err);
4981
+ process.exit(2);
4982
+ }
4983
+ });
4984
+ return graph;
4985
+ }
4986
+
3494
4987
  // src/index.ts
3495
4988
  function createProgram() {
3496
- const program = new Command33();
4989
+ const program = new Command43();
3497
4990
  program.name("harness").description("CLI for Harness Engineering toolkit").version(VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
3498
4991
  program.addCommand(createValidateCommand());
3499
4992
  program.addCommand(createCheckDepsCommand());
3500
4993
  program.addCommand(createCheckDocsCommand());
4994
+ program.addCommand(createCheckPerfCommand());
4995
+ program.addCommand(createCheckSecurityCommand());
4996
+ program.addCommand(createPerfCommand());
3501
4997
  program.addCommand(createInitCommand());
3502
4998
  program.addCommand(createCleanupCommand());
3503
4999
  program.addCommand(createFixDriftCommand());
@@ -3511,8 +5007,14 @@ function createProgram() {
3511
5007
  program.addCommand(createCreateSkillCommand());
3512
5008
  program.addCommand(createSetupMcpCommand());
3513
5009
  program.addCommand(createGenerateSlashCommandsCommand());
5010
+ program.addCommand(createGenerateAgentDefinitionsCommand());
5011
+ program.addCommand(createGenerateCommand3());
3514
5012
  program.addCommand(createCICommand());
3515
5013
  program.addCommand(createUpdateCommand());
5014
+ program.addCommand(createScanCommand());
5015
+ program.addCommand(createIngestCommand());
5016
+ program.addCommand(createQueryCommand());
5017
+ program.addCommand(createGraphCommand());
3516
5018
  return program;
3517
5019
  }
3518
5020
 
@@ -3525,12 +5027,25 @@ export {
3525
5027
  TemplateEngine,
3526
5028
  loadPersona,
3527
5029
  listPersonas,
5030
+ detectTrigger,
3528
5031
  runPersona,
5032
+ executeSkill,
5033
+ ALLOWED_PERSONA_COMMANDS,
3529
5034
  generateRuntime,
3530
5035
  generateAgentsMd,
3531
5036
  generateCIWorkflow,
3532
5037
  buildPreamble,
3533
5038
  runCheckPhaseGate,
3534
5039
  generateSlashCommands,
5040
+ AGENT_DESCRIPTIONS,
5041
+ generateAgentDefinition,
5042
+ renderClaudeCodeAgent,
5043
+ renderGeminiAgent,
5044
+ generateAgentDefinitions,
5045
+ runScan,
5046
+ runIngest,
5047
+ runQuery,
5048
+ runGraphStatus,
5049
+ runGraphExport,
3535
5050
  createProgram
3536
5051
  };