@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.
- package/dist/agents/personas/architecture-enforcer.yaml +1 -0
- package/dist/agents/personas/code-reviewer.yaml +43 -0
- package/dist/agents/personas/codebase-health-analyst.yaml +32 -0
- package/dist/agents/personas/documentation-maintainer.yaml +2 -0
- package/dist/agents/personas/entropy-cleaner.yaml +3 -0
- package/dist/agents/personas/graph-maintainer.yaml +27 -0
- package/dist/agents/personas/parallel-coordinator.yaml +29 -0
- package/dist/agents/personas/performance-guardian.yaml +26 -0
- package/dist/agents/personas/security-reviewer.yaml +35 -0
- package/dist/agents/personas/task-executor.yaml +41 -0
- package/dist/agents/skills/README.md +8 -0
- package/dist/agents/skills/claude-code/add-harness-component/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/align-documentation/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/detect-doc-drift/SKILL.md +8 -0
- package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +494 -0
- package/dist/agents/skills/claude-code/harness-autopilot/skill.yaml +52 -0
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +25 -0
- package/dist/agents/skills/claude-code/harness-debugging/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-dependency-health/SKILL.md +150 -0
- package/dist/agents/skills/claude-code/harness-dependency-health/skill.yaml +41 -0
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-hotspot-detector/SKILL.md +135 -0
- package/dist/agents/skills/claude-code/harness-hotspot-detector/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/harness-impact-analysis/SKILL.md +139 -0
- package/dist/agents/skills/claude-code/harness-impact-analysis/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/harness-integrity/SKILL.md +20 -6
- package/dist/agents/skills/claude-code/harness-knowledge-mapper/SKILL.md +154 -0
- package/dist/agents/skills/claude-code/harness-knowledge-mapper/skill.yaml +49 -0
- package/dist/agents/skills/claude-code/harness-onboarding/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-perf/SKILL.md +231 -0
- package/dist/agents/skills/claude-code/harness-perf/skill.yaml +47 -0
- package/dist/agents/skills/claude-code/harness-perf-tdd/SKILL.md +236 -0
- package/dist/agents/skills/claude-code/harness-perf-tdd/skill.yaml +47 -0
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +33 -2
- package/dist/agents/skills/claude-code/harness-refactoring/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +657 -0
- package/dist/agents/skills/claude-code/harness-release-readiness/skill.yaml +57 -0
- package/dist/agents/skills/claude-code/harness-security-review/SKILL.md +206 -0
- package/dist/agents/skills/claude-code/harness-security-review/skill.yaml +50 -0
- package/dist/agents/skills/claude-code/harness-security-scan/SKILL.md +102 -0
- package/dist/agents/skills/claude-code/harness-security-scan/skill.yaml +41 -0
- package/dist/agents/skills/claude-code/harness-state-management/SKILL.md +22 -8
- package/dist/agents/skills/claude-code/harness-tdd/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-test-advisor/SKILL.md +131 -0
- package/dist/agents/skills/claude-code/harness-test-advisor/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/validate-context-engineering/SKILL.md +9 -0
- package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +494 -0
- package/dist/agents/skills/gemini-cli/harness-autopilot/skill.yaml +52 -0
- package/dist/agents/skills/gemini-cli/harness-dependency-health/SKILL.md +150 -0
- package/dist/agents/skills/gemini-cli/harness-dependency-health/skill.yaml +41 -0
- package/dist/agents/skills/gemini-cli/harness-hotspot-detector/SKILL.md +135 -0
- package/dist/agents/skills/gemini-cli/harness-hotspot-detector/skill.yaml +44 -0
- package/dist/agents/skills/gemini-cli/harness-impact-analysis/SKILL.md +139 -0
- package/dist/agents/skills/gemini-cli/harness-impact-analysis/skill.yaml +44 -0
- package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/SKILL.md +154 -0
- package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/skill.yaml +49 -0
- package/dist/agents/skills/gemini-cli/harness-perf/SKILL.md +231 -0
- package/dist/agents/skills/gemini-cli/harness-perf/skill.yaml +47 -0
- package/dist/agents/skills/gemini-cli/harness-perf-tdd/SKILL.md +236 -0
- package/dist/agents/skills/gemini-cli/harness-perf-tdd/skill.yaml +47 -0
- package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +657 -0
- package/dist/agents/skills/gemini-cli/harness-release-readiness/skill.yaml +57 -0
- package/dist/agents/skills/gemini-cli/harness-security-review/skill.yaml +50 -0
- package/dist/agents/skills/gemini-cli/harness-security-scan/SKILL.md +102 -0
- package/dist/agents/skills/gemini-cli/harness-security-scan/skill.yaml +41 -0
- package/dist/agents/skills/gemini-cli/harness-test-advisor/SKILL.md +131 -0
- package/dist/agents/skills/gemini-cli/harness-test-advisor/skill.yaml +44 -0
- package/dist/agents/skills/tests/platform-parity.test.ts +131 -0
- package/dist/agents/skills/tests/schema.ts +2 -0
- package/dist/bin/harness.js +2 -2
- package/dist/{chunk-EFZOLZFB.js → chunk-ACMDUQJG.js} +4 -2
- package/dist/{chunk-C3J2HW4Y.js → chunk-O6NEKDYP.js} +2002 -487
- package/dist/{create-skill-4GKJZB5R.js → create-skill-NZDLMMR6.js} +1 -1
- package/dist/index.d.ts +265 -143
- package/dist/index.js +30 -4
- package/package.json +3 -2
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
createCreateSkillCommand,
|
|
6
6
|
handleError,
|
|
7
7
|
logger
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ACMDUQJG.js";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
|
-
import { Command as
|
|
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-
|
|
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,
|
|
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 =
|
|
417
|
-
const sourceDir =
|
|
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
|
|
840
|
+
return Ok6(result);
|
|
447
841
|
}
|
|
448
842
|
function createCheckDocsCommand() {
|
|
449
|
-
const command = new
|
|
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
|
|
895
|
+
import { Command as Command8 } from "commander";
|
|
502
896
|
import chalk3 from "chalk";
|
|
503
897
|
import * as fs5 from "fs";
|
|
504
|
-
import * as
|
|
505
|
-
import { Ok as
|
|
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
|
|
903
|
+
import * as path8 from "path";
|
|
510
904
|
import Handlebars from "handlebars";
|
|
511
|
-
import { Ok as
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
683
|
-
const dir =
|
|
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
|
|
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 =
|
|
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
|
|
1102
|
+
return path8.join(this.templatesDir, entry.name);
|
|
709
1103
|
if (type === "framework" && parsed.data.framework === name)
|
|
710
|
-
return
|
|
711
|
-
if (parsed.data.name === name) return
|
|
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 =
|
|
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:
|
|
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
|
|
1151
|
+
import * as path9 from "path";
|
|
758
1152
|
import { fileURLToPath } from "url";
|
|
759
1153
|
var __filename = fileURLToPath(import.meta.url);
|
|
760
|
-
var __dirname =
|
|
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 =
|
|
1158
|
+
const candidate = path9.join(dir, targetName);
|
|
765
1159
|
if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
|
|
766
|
-
if (fs3.existsSync(
|
|
1160
|
+
if (fs3.existsSync(path9.join(candidate, marker))) {
|
|
767
1161
|
return candidate;
|
|
768
1162
|
}
|
|
769
1163
|
}
|
|
770
|
-
dir =
|
|
1164
|
+
dir = path9.dirname(dir);
|
|
771
1165
|
}
|
|
772
1166
|
return null;
|
|
773
1167
|
}
|
|
774
1168
|
function resolveTemplatesDir() {
|
|
775
|
-
return findUpDir("templates", "base") ??
|
|
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
|
|
1174
|
+
return path9.join(agentsDir, "personas");
|
|
781
1175
|
}
|
|
782
|
-
return
|
|
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
|
|
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
|
|
1206
|
+
return path9.join(__dirname, "agents", "skills", "claude-code");
|
|
790
1207
|
}
|
|
791
1208
|
|
|
792
1209
|
// src/commands/setup-mcp.ts
|
|
793
|
-
import { Command as
|
|
1210
|
+
import { Command as Command7 } from "commander";
|
|
794
1211
|
import * as fs4 from "fs";
|
|
795
|
-
import * as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
891
|
-
|
|
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 ??
|
|
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 =
|
|
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
|
|
1351
|
+
return Ok8({ filesCreated: writeResult.value });
|
|
931
1352
|
}
|
|
932
1353
|
function createInitCommand() {
|
|
933
|
-
const command = new
|
|
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
|
|
976
|
-
import * as
|
|
977
|
-
import { Ok as
|
|
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 =
|
|
993
|
-
const 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: [
|
|
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
|
|
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
|
|
1454
|
+
return Ok9(result);
|
|
1034
1455
|
}
|
|
1035
1456
|
function createCleanupCommand() {
|
|
1036
|
-
const command = new
|
|
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
|
|
1097
|
-
import * as
|
|
1517
|
+
import { Command as Command10 } from "commander";
|
|
1518
|
+
import * as path13 from "path";
|
|
1098
1519
|
import {
|
|
1099
|
-
Ok as
|
|
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 =
|
|
1117
|
-
const 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: [
|
|
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
|
|
1625
|
+
return Ok10(result);
|
|
1205
1626
|
}
|
|
1206
1627
|
function createFixDriftCommand() {
|
|
1207
|
-
const command = new
|
|
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
|
|
1688
|
+
import { Command as Command13 } from "commander";
|
|
1268
1689
|
|
|
1269
1690
|
// src/commands/agent/run.ts
|
|
1270
|
-
import { Command as
|
|
1271
|
-
import * as
|
|
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
|
|
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
|
|
1699
|
+
import * as path14 from "path";
|
|
1279
1700
|
import YAML from "yaml";
|
|
1280
|
-
import { Ok as
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1881
|
+
steps: [],
|
|
1370
1882
|
totalDurationMs: 0
|
|
1371
1883
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
|
1901
|
+
const stepStart = Date.now();
|
|
1386
1902
|
const remainingTime = timeout - (Date.now() - startTime);
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
() =>
|
|
1392
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
durationMs
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
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,
|
|
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
|
|
1507
|
-
const icon =
|
|
1508
|
-
|
|
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
|
|
1532
|
-
import { execSync } from "child_process";
|
|
1533
|
-
import { Ok as
|
|
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 =
|
|
2258
|
+
diff = execSync2("git diff --cached", { encoding: "utf-8" });
|
|
1544
2259
|
if (!diff) {
|
|
1545
|
-
diff =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1625
|
-
import * as
|
|
1626
|
-
import * as
|
|
1627
|
-
import { Ok as
|
|
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 =
|
|
1672
|
-
if (!
|
|
1673
|
-
|
|
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 =
|
|
1677
|
-
if (!
|
|
1678
|
-
|
|
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 =
|
|
1685
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
1694
|
-
if (!
|
|
1695
|
-
|
|
2408
|
+
const docsDir = path18.join(cwd, "docs");
|
|
2409
|
+
if (!fs9.existsSync(docsDir)) {
|
|
2410
|
+
fs9.mkdirSync(docsDir, { recursive: true });
|
|
1696
2411
|
}
|
|
1697
|
-
const docPath =
|
|
1698
|
-
if (
|
|
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
|
-
|
|
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-
|
|
2421
|
+
const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-NZDLMMR6.js");
|
|
1707
2422
|
generateSkillFiles2({
|
|
1708
2423
|
name,
|
|
1709
2424
|
description: `${name} skill`,
|
|
1710
|
-
outputDir:
|
|
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 =
|
|
1718
|
-
if (!
|
|
1719
|
-
|
|
2432
|
+
const personasDir = path18.join(cwd, "agents", "personas");
|
|
2433
|
+
if (!fs9.existsSync(personasDir)) {
|
|
2434
|
+
fs9.mkdirSync(personasDir, { recursive: true });
|
|
1720
2435
|
}
|
|
1721
|
-
const personaPath =
|
|
1722
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2494
|
+
import { Command as Command17 } from "commander";
|
|
1780
2495
|
|
|
1781
2496
|
// src/commands/linter/generate.ts
|
|
1782
|
-
import { Command as
|
|
2497
|
+
import { Command as Command15 } from "commander";
|
|
1783
2498
|
import { generate } from "@harness-engineering/linter-gen";
|
|
1784
2499
|
function createGenerateCommand() {
|
|
1785
|
-
return new
|
|
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
|
|
2562
|
+
import { Command as Command16 } from "commander";
|
|
1848
2563
|
import { validate } from "@harness-engineering/linter-gen";
|
|
1849
2564
|
function createValidateCommand2() {
|
|
1850
|
-
return new
|
|
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
|
|
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
|
|
2593
|
+
import { Command as Command20 } from "commander";
|
|
1879
2594
|
|
|
1880
2595
|
// src/commands/persona/list.ts
|
|
1881
|
-
import { Command as
|
|
2596
|
+
import { Command as Command18 } from "commander";
|
|
1882
2597
|
function createListCommand() {
|
|
1883
|
-
return new
|
|
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
|
|
1913
|
-
import * as
|
|
1914
|
-
import * as
|
|
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
|
|
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
|
-
|
|
2645
|
+
steps: persona.steps,
|
|
1931
2646
|
timeout: persona.config.timeout,
|
|
1932
2647
|
severity: persona.config.severity
|
|
1933
2648
|
};
|
|
1934
|
-
return
|
|
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
|
|
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.
|
|
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 ${
|
|
2692
|
+
**When this agent flags an issue:** Fix violations before merging. Run ${allCommands} locally to validate.
|
|
1974
2693
|
`;
|
|
1975
|
-
return
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2064
|
-
|
|
2065
|
-
|
|
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 =
|
|
2073
|
-
|
|
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 =
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
|
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
|
|
2823
|
+
import { Command as Command25 } from "commander";
|
|
2104
2824
|
|
|
2105
2825
|
// src/commands/skill/list.ts
|
|
2106
|
-
import { Command as
|
|
2107
|
-
import * as
|
|
2108
|
-
import * as
|
|
2109
|
-
import { parse as
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
2123
|
-
if (!
|
|
2842
|
+
const yamlPath = path20.join(skillsDir, name, "skill.yaml");
|
|
2843
|
+
if (!fs11.existsSync(yamlPath)) continue;
|
|
2124
2844
|
try {
|
|
2125
|
-
const raw =
|
|
2126
|
-
const parsed =
|
|
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
|
|
2156
|
-
import * as
|
|
2157
|
-
import * as
|
|
2158
|
-
import { parse as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
2248
|
-
if (!
|
|
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 =
|
|
2973
|
+
const yamlPath = path21.join(skillDir, "skill.yaml");
|
|
2254
2974
|
let metadata = null;
|
|
2255
|
-
if (
|
|
2975
|
+
if (fs12.existsSync(yamlPath)) {
|
|
2256
2976
|
try {
|
|
2257
|
-
const raw =
|
|
2258
|
-
const parsed =
|
|
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 ?
|
|
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 ?
|
|
2276
|
-
const principlesPath =
|
|
2277
|
-
if (
|
|
2278
|
-
principles =
|
|
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 =
|
|
2294
|
-
if (
|
|
2295
|
-
const stat =
|
|
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 =
|
|
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 =
|
|
3019
|
+
priorState = fs12.readFileSync(path21.join(fullPath, files[0].name), "utf-8");
|
|
2300
3020
|
}
|
|
2301
3021
|
} else {
|
|
2302
|
-
priorState =
|
|
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 =
|
|
2322
|
-
if (!
|
|
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 =
|
|
3047
|
+
let content = fs12.readFileSync(skillMdPath, "utf-8");
|
|
2328
3048
|
if (metadata?.state.persistent && opts.path) {
|
|
2329
|
-
const stateFile =
|
|
2330
|
-
if (
|
|
2331
|
-
const stateContent =
|
|
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
|
|
2349
|
-
import * as
|
|
2350
|
-
import * as
|
|
2351
|
-
import { parse as
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
2373
|
-
const yamlPath =
|
|
2374
|
-
const skillMdPath =
|
|
2375
|
-
if (!
|
|
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 =
|
|
2381
|
-
const parsed =
|
|
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 (
|
|
2388
|
-
const mdContent =
|
|
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
|
|
2430
|
-
import * as
|
|
2431
|
-
import * as
|
|
2432
|
-
import { parse as
|
|
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
|
|
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 =
|
|
2438
|
-
if (!
|
|
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 =
|
|
2444
|
-
if (!
|
|
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 =
|
|
2451
|
-
const parsed =
|
|
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
|
|
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
|
|
3220
|
+
import { Command as Command30 } from "commander";
|
|
2501
3221
|
|
|
2502
3222
|
// src/commands/state/show.ts
|
|
2503
|
-
import { Command as
|
|
2504
|
-
import * as
|
|
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
|
|
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 =
|
|
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
|
|
2549
|
-
import * as
|
|
2550
|
-
import * as
|
|
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
|
|
2554
|
-
const projectPath =
|
|
2555
|
-
|
|
2556
|
-
if (
|
|
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((
|
|
2564
|
-
rl.question("Reset project state? This cannot be undone. [y/N] ",
|
|
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
|
-
|
|
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
|
|
2587
|
-
import * as
|
|
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
|
|
2591
|
-
const projectPath =
|
|
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
|
|
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
|
|
2614
|
-
import * as
|
|
2615
|
-
import * as
|
|
2616
|
-
import { Ok as
|
|
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 =
|
|
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 :
|
|
3430
|
+
const feature = segments.length > 1 ? firstSegment : path28.basename(firstSegment, path28.extname(firstSegment));
|
|
2624
3431
|
const specRelative = specPattern.replace("{feature}", feature);
|
|
2625
|
-
return
|
|
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 ?
|
|
3440
|
+
const cwd = options.cwd ?? (options.configPath ? path28.dirname(path28.resolve(options.configPath)) : process.cwd());
|
|
2634
3441
|
if (!config.phaseGates?.enabled) {
|
|
2635
|
-
return
|
|
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 (!
|
|
3457
|
+
if (!fs16.existsSync(expectedSpec)) {
|
|
2651
3458
|
missingSpecs.push({
|
|
2652
|
-
implFile:
|
|
2653
|
-
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
|
|
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
|
|
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
|
|
2723
|
-
import
|
|
2724
|
-
import
|
|
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
|
|
2730
|
-
import
|
|
2731
|
-
import { parse as
|
|
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(
|
|
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
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
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
|
|
2949
|
-
import
|
|
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 =
|
|
2957
|
-
if (!
|
|
3779
|
+
const filePath = path30.join(outputDir, filename);
|
|
3780
|
+
if (!fs18.existsSync(filePath)) {
|
|
2958
3781
|
added.push(filename);
|
|
2959
3782
|
} else {
|
|
2960
|
-
const existing =
|
|
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 (
|
|
2969
|
-
const existing =
|
|
2970
|
-
const stat =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2994
|
-
if (
|
|
2995
|
-
|
|
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
|
|
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" ?
|
|
3831
|
+
return platform === "claude-code" ? path31.join(home, ".claude", "commands", "harness") : path31.join(home, ".gemini", "commands", "harness");
|
|
3009
3832
|
}
|
|
3010
|
-
return platform === "claude-code" ?
|
|
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((
|
|
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
|
-
|
|
3844
|
+
resolve22(answer.toLowerCase() === "y");
|
|
3022
3845
|
});
|
|
3023
3846
|
});
|
|
3024
3847
|
}
|
|
3025
3848
|
function generateSlashCommands(opts) {
|
|
3026
|
-
const
|
|
3027
|
-
|
|
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 `@${
|
|
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 =
|
|
3053
|
-
const yamlPath =
|
|
3054
|
-
const mdContent =
|
|
3055
|
-
const yamlContent =
|
|
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 =
|
|
3082
|
-
if (
|
|
3083
|
-
|
|
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
|
|
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
|
|
3992
|
+
import { Command as Command35 } from "commander";
|
|
3145
3993
|
|
|
3146
3994
|
// src/commands/ci/check.ts
|
|
3147
|
-
import { Command as
|
|
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
|
|
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
|
|
3224
|
-
import * as
|
|
3225
|
-
import * as
|
|
3226
|
-
import { Ok as
|
|
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
|
|
4160
|
+
return Ok19({
|
|
3313
4161
|
filename: entry.filename,
|
|
3314
4162
|
content: entry.generate(skipFlag)
|
|
3315
4163
|
});
|
|
3316
4164
|
}
|
|
3317
4165
|
function detectPlatform() {
|
|
3318
|
-
if (
|
|
3319
|
-
if (
|
|
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
|
|
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 =
|
|
3336
|
-
const dir =
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3361
|
-
import { execSync as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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((
|
|
4266
|
+
return new Promise((resolve22) => {
|
|
3419
4267
|
rl.question(question, (answer) => {
|
|
3420
4268
|
rl.close();
|
|
3421
|
-
|
|
4269
|
+
resolve22(answer.trim().toLowerCase());
|
|
3422
4270
|
});
|
|
3423
4271
|
});
|
|
3424
4272
|
}
|
|
3425
4273
|
function createUpdateCommand() {
|
|
3426
|
-
return new
|
|
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
|
-
|
|
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
|
-
|
|
4332
|
+
execSync4(`harness generate${globalFlag}`, { stdio: "inherit" });
|
|
3485
4333
|
} catch {
|
|
3486
|
-
logger.warn("
|
|
3487
|
-
console.log(` ${chalk4.cyan(`harness generate
|
|
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
|
|
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
|
};
|