@harness-engineering/cli 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/agents/personas/architecture-enforcer.yaml +1 -0
  2. package/dist/agents/personas/code-reviewer.yaml +41 -0
  3. package/dist/agents/personas/codebase-health-analyst.yaml +27 -0
  4. package/dist/agents/personas/documentation-maintainer.yaml +2 -0
  5. package/dist/agents/personas/entropy-cleaner.yaml +3 -0
  6. package/dist/agents/personas/graph-maintainer.yaml +27 -0
  7. package/dist/agents/personas/parallel-coordinator.yaml +29 -0
  8. package/dist/agents/personas/task-executor.yaml +41 -0
  9. package/dist/agents/skills/README.md +8 -0
  10. package/dist/agents/skills/claude-code/add-harness-component/SKILL.md +10 -0
  11. package/dist/agents/skills/claude-code/align-documentation/SKILL.md +19 -0
  12. package/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +19 -0
  13. package/dist/agents/skills/claude-code/detect-doc-drift/SKILL.md +8 -0
  14. package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +9 -0
  15. package/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +9 -0
  16. package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +10 -0
  17. package/dist/agents/skills/claude-code/harness-debugging/SKILL.md +10 -0
  18. package/dist/agents/skills/claude-code/harness-dependency-health/SKILL.md +150 -0
  19. package/dist/agents/skills/claude-code/harness-dependency-health/skill.yaml +41 -0
  20. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +19 -0
  21. package/dist/agents/skills/claude-code/harness-hotspot-detector/SKILL.md +135 -0
  22. package/dist/agents/skills/claude-code/harness-hotspot-detector/skill.yaml +44 -0
  23. package/dist/agents/skills/claude-code/harness-impact-analysis/SKILL.md +139 -0
  24. package/dist/agents/skills/claude-code/harness-impact-analysis/skill.yaml +44 -0
  25. package/dist/agents/skills/claude-code/harness-knowledge-mapper/SKILL.md +154 -0
  26. package/dist/agents/skills/claude-code/harness-knowledge-mapper/skill.yaml +49 -0
  27. package/dist/agents/skills/claude-code/harness-onboarding/SKILL.md +10 -0
  28. package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +9 -0
  29. package/dist/agents/skills/claude-code/harness-planning/SKILL.md +9 -0
  30. package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +6 -0
  31. package/dist/agents/skills/claude-code/harness-refactoring/SKILL.md +19 -0
  32. package/dist/agents/skills/claude-code/harness-tdd/SKILL.md +10 -0
  33. package/dist/agents/skills/claude-code/harness-test-advisor/SKILL.md +131 -0
  34. package/dist/agents/skills/claude-code/harness-test-advisor/skill.yaml +44 -0
  35. package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +10 -0
  36. package/dist/agents/skills/claude-code/validate-context-engineering/SKILL.md +9 -0
  37. package/dist/agents/skills/gemini-cli/harness-dependency-health/SKILL.md +150 -0
  38. package/dist/agents/skills/gemini-cli/harness-dependency-health/skill.yaml +41 -0
  39. package/dist/agents/skills/gemini-cli/harness-hotspot-detector/SKILL.md +135 -0
  40. package/dist/agents/skills/gemini-cli/harness-hotspot-detector/skill.yaml +44 -0
  41. package/dist/agents/skills/gemini-cli/harness-impact-analysis/SKILL.md +139 -0
  42. package/dist/agents/skills/gemini-cli/harness-impact-analysis/skill.yaml +44 -0
  43. package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/SKILL.md +154 -0
  44. package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/skill.yaml +49 -0
  45. package/dist/agents/skills/gemini-cli/harness-test-advisor/SKILL.md +131 -0
  46. package/dist/agents/skills/gemini-cli/harness-test-advisor/skill.yaml +44 -0
  47. package/dist/agents/skills/tests/platform-parity.test.ts +131 -0
  48. package/dist/agents/skills/tests/schema.ts +2 -0
  49. package/dist/bin/harness.js +2 -2
  50. package/dist/{chunk-EFZOLZFB.js → chunk-ACMDUQJG.js} +4 -2
  51. package/dist/{chunk-C3J2HW4Y.js → chunk-VS4OTOKZ.js} +1354 -329
  52. package/dist/{create-skill-4GKJZB5R.js → create-skill-NZDLMMR6.js} +1 -1
  53. package/dist/index.d.ts +265 -143
  54. package/dist/index.js +30 -4
  55. package/package.json +3 -2
@@ -5,10 +5,10 @@ import {
5
5
  createCreateSkillCommand,
6
6
  handleError,
7
7
  logger
8
- } from "./chunk-EFZOLZFB.js";
8
+ } from "./chunk-ACMDUQJG.js";
9
9
 
10
10
  // src/index.ts
11
- import { Command as Command33 } from "commander";
11
+ import { Command as Command39 } from "commander";
12
12
  import { VERSION } from "@harness-engineering/core";
13
13
 
14
14
  // src/commands/validate.ts
@@ -788,6 +788,29 @@ function resolveSkillsDir() {
788
788
  }
789
789
  return path6.join(__dirname, "agents", "skills", "claude-code");
790
790
  }
791
+ function resolveProjectSkillsDir(cwd) {
792
+ let dir = cwd ?? process.cwd();
793
+ for (let i = 0; i < 8; i++) {
794
+ const candidate = path6.join(dir, "agents", "skills", "claude-code");
795
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
796
+ const agentsDir = path6.join(dir, "agents");
797
+ if (fs3.existsSync(path6.join(agentsDir, "skills"))) {
798
+ return candidate;
799
+ }
800
+ }
801
+ const parent = path6.dirname(dir);
802
+ if (parent === dir) break;
803
+ dir = parent;
804
+ }
805
+ return null;
806
+ }
807
+ function resolveGlobalSkillsDir() {
808
+ const agentsDir = findUpDir("agents", "skills");
809
+ if (agentsDir) {
810
+ return path6.join(agentsDir, "skills", "claude-code");
811
+ }
812
+ return path6.join(__dirname, "agents", "skills", "claude-code");
813
+ }
791
814
 
792
815
  // src/commands/setup-mcp.ts
793
816
  import { Command as Command4 } from "commander";
@@ -1268,7 +1291,7 @@ import { Command as Command10 } from "commander";
1268
1291
 
1269
1292
  // src/commands/agent/run.ts
1270
1293
  import { Command as Command8 } from "commander";
1271
- import * as path12 from "path";
1294
+ import * as path14 from "path";
1272
1295
  import * as childProcess from "child_process";
1273
1296
  import { Ok as Ok10, Err as Err8 } from "@harness-engineering/core";
1274
1297
  import { requestPeerReview } from "@harness-engineering/core";
@@ -1281,10 +1304,24 @@ import { Ok as Ok9, Err as Err7 } from "@harness-engineering/core";
1281
1304
 
1282
1305
  // src/persona/schema.ts
1283
1306
  import { z as z3 } from "zod";
1307
+ var TriggerContextSchema = z3.enum(["always", "on_pr", "on_commit", "on_review", "scheduled", "manual", "on_plan_approved"]).default("always");
1308
+ var CommandStepSchema = z3.object({
1309
+ command: z3.string(),
1310
+ when: TriggerContextSchema
1311
+ });
1312
+ var SkillStepSchema = z3.object({
1313
+ skill: z3.string(),
1314
+ when: TriggerContextSchema,
1315
+ output: z3.enum(["inline", "artifact", "auto"]).default("auto")
1316
+ });
1317
+ var StepSchema = z3.union([CommandStepSchema, SkillStepSchema]);
1284
1318
  var PersonaTriggerSchema = z3.discriminatedUnion("event", [
1285
1319
  z3.object({
1286
1320
  event: z3.literal("on_pr"),
1287
- conditions: z3.object({ paths: z3.array(z3.string()).optional() }).optional()
1321
+ conditions: z3.object({
1322
+ paths: z3.array(z3.string()).optional(),
1323
+ min_files: z3.number().optional()
1324
+ }).optional()
1288
1325
  }),
1289
1326
  z3.object({
1290
1327
  event: z3.literal("on_commit"),
@@ -1293,6 +1330,9 @@ var PersonaTriggerSchema = z3.discriminatedUnion("event", [
1293
1330
  z3.object({
1294
1331
  event: z3.literal("scheduled"),
1295
1332
  cron: z3.string()
1333
+ }),
1334
+ z3.object({
1335
+ event: z3.literal("manual")
1296
1336
  })
1297
1337
  ]);
1298
1338
  var PersonaConfigSchema = z3.object({
@@ -1305,7 +1345,7 @@ var PersonaOutputsSchema = z3.object({
1305
1345
  "ci-workflow": z3.boolean().default(true),
1306
1346
  "runtime-config": z3.boolean().default(true)
1307
1347
  });
1308
- var PersonaSchema = z3.object({
1348
+ var PersonaSchemaV1 = z3.object({
1309
1349
  version: z3.literal(1),
1310
1350
  name: z3.string(),
1311
1351
  description: z3.string(),
@@ -1316,8 +1356,33 @@ var PersonaSchema = z3.object({
1316
1356
  config: PersonaConfigSchema.default({}),
1317
1357
  outputs: PersonaOutputsSchema.default({})
1318
1358
  });
1359
+ var PersonaSchemaV2 = z3.object({
1360
+ version: z3.literal(2),
1361
+ name: z3.string(),
1362
+ description: z3.string(),
1363
+ role: z3.string(),
1364
+ skills: z3.array(z3.string()),
1365
+ steps: z3.array(StepSchema),
1366
+ triggers: z3.array(PersonaTriggerSchema),
1367
+ config: PersonaConfigSchema.default({}),
1368
+ outputs: PersonaOutputsSchema.default({})
1369
+ });
1370
+ var PersonaSchema = z3.union([PersonaSchemaV1, PersonaSchemaV2]);
1319
1371
 
1320
1372
  // src/persona/loader.ts
1373
+ function normalizePersona(raw) {
1374
+ if (raw.version === 1 && Array.isArray(raw.commands)) {
1375
+ const { commands, ...rest } = raw;
1376
+ return {
1377
+ ...rest,
1378
+ steps: commands.map((cmd) => ({
1379
+ command: cmd,
1380
+ when: "always"
1381
+ }))
1382
+ };
1383
+ }
1384
+ return raw;
1385
+ }
1321
1386
  function loadPersona(filePath) {
1322
1387
  try {
1323
1388
  if (!fs6.existsSync(filePath)) {
@@ -1329,7 +1394,7 @@ function loadPersona(filePath) {
1329
1394
  if (!result.success) {
1330
1395
  return Err7(new Error(`Invalid persona ${filePath}: ${result.error.message}`));
1331
1396
  }
1332
- return Ok9(result.data);
1397
+ return Ok9(normalizePersona(result.data));
1333
1398
  } catch (error) {
1334
1399
  return Err7(
1335
1400
  new Error(`Failed to load persona: ${error instanceof Error ? error.message : String(error)}`)
@@ -1358,75 +1423,312 @@ function listPersonas(dir) {
1358
1423
  }
1359
1424
  }
1360
1425
 
1426
+ // src/persona/trigger-detector.ts
1427
+ import * as fs7 from "fs";
1428
+ import * as path12 from "path";
1429
+ function detectTrigger(projectPath) {
1430
+ const handoffPath = path12.join(projectPath, ".harness", "handoff.json");
1431
+ if (!fs7.existsSync(handoffPath)) {
1432
+ return { trigger: "manual" };
1433
+ }
1434
+ try {
1435
+ const raw = fs7.readFileSync(handoffPath, "utf-8");
1436
+ const handoff = JSON.parse(raw);
1437
+ if (handoff.fromSkill === "harness-planning" && Array.isArray(handoff.pending) && handoff.pending.length > 0) {
1438
+ return {
1439
+ trigger: "on_plan_approved",
1440
+ handoff: {
1441
+ fromSkill: handoff.fromSkill,
1442
+ summary: handoff.summary ?? "",
1443
+ pending: handoff.pending,
1444
+ planPath: handoff.planPath
1445
+ }
1446
+ };
1447
+ }
1448
+ return { trigger: "manual" };
1449
+ } catch {
1450
+ return { trigger: "manual" };
1451
+ }
1452
+ }
1453
+
1361
1454
  // src/persona/runner.ts
1362
1455
  var TIMEOUT_ERROR_MESSAGE = "__PERSONA_RUNNER_TIMEOUT__";
1363
- async function runPersona(persona, executor) {
1456
+ function stepName(step) {
1457
+ return "command" in step ? step.command : step.skill;
1458
+ }
1459
+ function stepType(step) {
1460
+ return "command" in step ? "command" : "skill";
1461
+ }
1462
+ function matchesTrigger(step, trigger) {
1463
+ const when = step.when ?? "always";
1464
+ return when === "always" || when === trigger;
1465
+ }
1466
+ function skipRemaining(activeSteps, fromIndex, report) {
1467
+ for (let j = fromIndex; j < activeSteps.length; j++) {
1468
+ const remaining = activeSteps[j];
1469
+ report.steps.push({
1470
+ name: stepName(remaining),
1471
+ type: stepType(remaining),
1472
+ status: "skipped",
1473
+ durationMs: 0
1474
+ });
1475
+ }
1476
+ }
1477
+ async function runPersona(persona, context) {
1364
1478
  const startTime = Date.now();
1365
1479
  const timeout = persona.config.timeout;
1366
1480
  const report = {
1367
1481
  persona: persona.name.toLowerCase().replace(/\s+/g, "-"),
1368
1482
  status: "pass",
1369
- commands: [],
1483
+ steps: [],
1370
1484
  totalDurationMs: 0
1371
1485
  };
1372
- for (let i = 0; i < persona.commands.length; i++) {
1373
- const command = persona.commands[i];
1374
- if (command === void 0) continue;
1486
+ let resolvedTrigger;
1487
+ let handoff = context.handoff;
1488
+ if (context.trigger === "auto") {
1489
+ const detection = detectTrigger(context.projectPath);
1490
+ resolvedTrigger = detection.trigger;
1491
+ handoff = detection.handoff ?? handoff;
1492
+ } else {
1493
+ resolvedTrigger = context.trigger;
1494
+ }
1495
+ const activeSteps = persona.steps.filter((s) => matchesTrigger(s, resolvedTrigger));
1496
+ for (let i = 0; i < activeSteps.length; i++) {
1497
+ const step = activeSteps[i];
1375
1498
  if (Date.now() - startTime >= timeout) {
1376
- for (let j = i; j < persona.commands.length; j++) {
1377
- const remaining = persona.commands[j];
1378
- if (remaining !== void 0) {
1379
- report.commands.push({ name: remaining, status: "skipped", durationMs: 0 });
1380
- }
1381
- }
1499
+ skipRemaining(activeSteps, i, report);
1382
1500
  report.status = "partial";
1383
1501
  break;
1384
1502
  }
1385
- const cmdStart = Date.now();
1503
+ const stepStart = Date.now();
1386
1504
  const remainingTime = timeout - (Date.now() - startTime);
1387
- const result = await Promise.race([
1388
- executor(command),
1389
- new Promise(
1390
- (resolve14) => setTimeout(
1391
- () => resolve14({ ok: false, error: new Error(TIMEOUT_ERROR_MESSAGE) }),
1392
- remainingTime
1505
+ if ("command" in step) {
1506
+ const result = await Promise.race([
1507
+ context.commandExecutor(step.command),
1508
+ new Promise(
1509
+ (resolve18) => setTimeout(
1510
+ () => resolve18({
1511
+ ok: false,
1512
+ error: new Error(TIMEOUT_ERROR_MESSAGE)
1513
+ }),
1514
+ remainingTime
1515
+ )
1393
1516
  )
1394
- )
1395
- ]);
1396
- const durationMs = Date.now() - cmdStart;
1397
- if (result.ok) {
1398
- report.commands.push({ name: command, status: "pass", result: result.value, durationMs });
1399
- } else if (result.error.message === TIMEOUT_ERROR_MESSAGE) {
1400
- report.commands.push({ name: command, status: "skipped", error: "timed out", durationMs });
1401
- report.status = "partial";
1402
- for (let j = i + 1; j < persona.commands.length; j++) {
1403
- const skipped = persona.commands[j];
1404
- if (skipped !== void 0) {
1405
- report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1406
- }
1517
+ ]);
1518
+ const durationMs = Date.now() - stepStart;
1519
+ if (result.ok) {
1520
+ report.steps.push({
1521
+ name: step.command,
1522
+ type: "command",
1523
+ status: "pass",
1524
+ result: result.value,
1525
+ durationMs
1526
+ });
1527
+ } else if (result.error.message === TIMEOUT_ERROR_MESSAGE) {
1528
+ report.steps.push({
1529
+ name: step.command,
1530
+ type: "command",
1531
+ status: "skipped",
1532
+ error: "timed out",
1533
+ durationMs
1534
+ });
1535
+ report.status = "partial";
1536
+ skipRemaining(activeSteps, i + 1, report);
1537
+ break;
1538
+ } else {
1539
+ report.steps.push({
1540
+ name: step.command,
1541
+ type: "command",
1542
+ status: "fail",
1543
+ error: result.error.message,
1544
+ durationMs
1545
+ });
1546
+ report.status = "fail";
1547
+ skipRemaining(activeSteps, i + 1, report);
1548
+ break;
1407
1549
  }
1408
- break;
1409
1550
  } else {
1410
- report.commands.push({
1411
- name: command,
1551
+ const skillContext = {
1552
+ trigger: resolvedTrigger,
1553
+ projectPath: context.projectPath,
1554
+ outputMode: step.output ?? "auto",
1555
+ ...handoff ? { handoff } : {}
1556
+ };
1557
+ const SKILL_TIMEOUT_RESULT = {
1412
1558
  status: "fail",
1413
- error: result.error.message,
1414
- durationMs
1415
- });
1416
- report.status = "fail";
1417
- for (let j = i + 1; j < persona.commands.length; j++) {
1418
- const skipped = persona.commands[j];
1419
- if (skipped !== void 0) {
1420
- report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1421
- }
1559
+ output: "timed out",
1560
+ durationMs: 0
1561
+ };
1562
+ const result = await Promise.race([
1563
+ context.skillExecutor(step.skill, skillContext),
1564
+ new Promise(
1565
+ (resolve18) => setTimeout(() => resolve18(SKILL_TIMEOUT_RESULT), remainingTime)
1566
+ )
1567
+ ]);
1568
+ const durationMs = Date.now() - stepStart;
1569
+ if (result === SKILL_TIMEOUT_RESULT) {
1570
+ report.steps.push({
1571
+ name: step.skill,
1572
+ type: "skill",
1573
+ status: "skipped",
1574
+ error: "timed out",
1575
+ durationMs
1576
+ });
1577
+ report.status = "partial";
1578
+ skipRemaining(activeSteps, i + 1, report);
1579
+ break;
1580
+ } else if (result.status === "pass") {
1581
+ report.steps.push({
1582
+ name: step.skill,
1583
+ type: "skill",
1584
+ status: "pass",
1585
+ result: result.output,
1586
+ ...result.artifactPath ? { artifactPath: result.artifactPath } : {},
1587
+ durationMs
1588
+ });
1589
+ } else {
1590
+ report.steps.push({
1591
+ name: step.skill,
1592
+ type: "skill",
1593
+ status: "fail",
1594
+ error: result.output,
1595
+ durationMs
1596
+ });
1597
+ report.status = "fail";
1598
+ skipRemaining(activeSteps, i + 1, report);
1599
+ break;
1422
1600
  }
1423
- break;
1424
1601
  }
1425
1602
  }
1426
1603
  report.totalDurationMs = Date.now() - startTime;
1427
1604
  return report;
1428
1605
  }
1429
1606
 
1607
+ // src/persona/skill-executor.ts
1608
+ import * as fs8 from "fs";
1609
+ import * as path13 from "path";
1610
+ import { parse as parse2 } from "yaml";
1611
+ function resolveOutputMode(mode, trigger) {
1612
+ if (mode !== "auto") return mode;
1613
+ return trigger === "manual" ? "inline" : "artifact";
1614
+ }
1615
+ function buildArtifactPath(projectPath, headSha) {
1616
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1617
+ const sha = headSha?.slice(0, 7) ?? "unknown";
1618
+ return path13.join(projectPath, ".harness", "reviews", `${date}-${sha}.md`);
1619
+ }
1620
+ function buildArtifactContent(skillName, trigger, headSha) {
1621
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1622
+ return [
1623
+ "---",
1624
+ `skill: ${skillName}`,
1625
+ `trigger: ${trigger}`,
1626
+ `sha: ${headSha?.slice(0, 7) ?? "unknown"}`,
1627
+ `date: ${date}`,
1628
+ `assessment: pending`,
1629
+ "---",
1630
+ "",
1631
+ `# Review by ${skillName}`,
1632
+ "",
1633
+ "## Strengths",
1634
+ "",
1635
+ "- (review pending)",
1636
+ "",
1637
+ "## Issues",
1638
+ "",
1639
+ "### Critical",
1640
+ "",
1641
+ "- None identified",
1642
+ "",
1643
+ "### Important",
1644
+ "",
1645
+ "- None identified",
1646
+ "",
1647
+ "### Suggestions",
1648
+ "",
1649
+ "- None identified",
1650
+ "",
1651
+ "## Assessment",
1652
+ "",
1653
+ "Pending \u2014 skill execution scaffolded.",
1654
+ "",
1655
+ "## Harness Checks",
1656
+ "",
1657
+ "- (run harness validate, check-deps, check-docs to populate)",
1658
+ ""
1659
+ ].join("\n");
1660
+ }
1661
+ async function executeSkill(skillName, context) {
1662
+ const startTime = Date.now();
1663
+ const skillsDir = resolveSkillsDir();
1664
+ const skillDir = path13.join(skillsDir, skillName);
1665
+ if (!fs8.existsSync(skillDir)) {
1666
+ return {
1667
+ status: "fail",
1668
+ output: `Skill not found: ${skillName}`,
1669
+ durationMs: Date.now() - startTime
1670
+ };
1671
+ }
1672
+ const yamlPath = path13.join(skillDir, "skill.yaml");
1673
+ if (!fs8.existsSync(yamlPath)) {
1674
+ return {
1675
+ status: "fail",
1676
+ output: `skill.yaml not found for ${skillName}`,
1677
+ durationMs: Date.now() - startTime
1678
+ };
1679
+ }
1680
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
1681
+ const parsed = parse2(raw);
1682
+ const metadataResult = SkillMetadataSchema.safeParse(parsed);
1683
+ if (!metadataResult.success) {
1684
+ return {
1685
+ status: "fail",
1686
+ output: `Invalid skill metadata: ${metadataResult.error.message}`,
1687
+ durationMs: Date.now() - startTime
1688
+ };
1689
+ }
1690
+ const skillMdPath = path13.join(skillDir, "SKILL.md");
1691
+ if (!fs8.existsSync(skillMdPath)) {
1692
+ return {
1693
+ status: "fail",
1694
+ output: `SKILL.md not found for ${skillName}`,
1695
+ durationMs: Date.now() - startTime
1696
+ };
1697
+ }
1698
+ const skillContent = fs8.readFileSync(skillMdPath, "utf-8");
1699
+ const metadata = metadataResult.data;
1700
+ const resolvedMode = resolveOutputMode(context.outputMode, context.trigger);
1701
+ const output = `Skill ${metadata.name} (${metadata.type}) loaded.
1702
+ Cognitive mode: ${metadata.cognitive_mode ?? "default"}
1703
+ Content length: ${skillContent.length} chars
1704
+ Trigger: ${context.trigger}
1705
+ `;
1706
+ let artifactPath;
1707
+ if (resolvedMode === "artifact") {
1708
+ artifactPath = buildArtifactPath(context.projectPath, context.headSha);
1709
+ const artifactContent = buildArtifactContent(skillName, context.trigger, context.headSha);
1710
+ const dir = path13.dirname(artifactPath);
1711
+ fs8.mkdirSync(dir, { recursive: true });
1712
+ fs8.writeFileSync(artifactPath, artifactContent, "utf-8");
1713
+ }
1714
+ return {
1715
+ status: "pass",
1716
+ output,
1717
+ ...artifactPath ? { artifactPath } : {},
1718
+ durationMs: Date.now() - startTime
1719
+ };
1720
+ }
1721
+
1722
+ // src/persona/constants.ts
1723
+ var ALLOWED_PERSONA_COMMANDS = /* @__PURE__ */ new Set([
1724
+ "validate",
1725
+ "check-deps",
1726
+ "check-docs",
1727
+ "cleanup",
1728
+ "fix-drift",
1729
+ "add"
1730
+ ]);
1731
+
1430
1732
  // src/commands/agent/run.ts
1431
1733
  async function runAgentTask(task, options) {
1432
1734
  const configResult = resolveConfig(options.configPath);
@@ -1469,28 +1771,32 @@ async function runAgentTask(task, options) {
1469
1771
  ${review.comments.map((c) => ` - ${c.message}`).join("\n")}`
1470
1772
  });
1471
1773
  }
1774
+ var VALID_TRIGGERS = /* @__PURE__ */ new Set([
1775
+ "always",
1776
+ "on_pr",
1777
+ "on_commit",
1778
+ "on_review",
1779
+ "scheduled",
1780
+ "manual",
1781
+ "on_plan_approved",
1782
+ "auto"
1783
+ ]);
1472
1784
  function createRunCommand() {
1473
- return new Command8("run").description("Run an agent task").argument("[task]", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").option("--persona <name>", "Run a persona by name").action(async (task, opts, cmd) => {
1785
+ return new Command8("run").description("Run an agent task").argument("[task]", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").option("--persona <name>", "Run a persona by name").option("--trigger <context>", "Trigger context (auto, on_pr, on_commit, manual)", "auto").action(async (task, opts, cmd) => {
1474
1786
  const globalOpts = cmd.optsWithGlobals();
1475
1787
  if (opts.persona) {
1476
1788
  const personasDir = resolvePersonasDir();
1477
- const filePath = path12.join(personasDir, `${opts.persona}.yaml`);
1789
+ const filePath = path14.join(personasDir, `${opts.persona}.yaml`);
1478
1790
  const personaResult = loadPersona(filePath);
1479
1791
  if (!personaResult.ok) {
1480
1792
  logger.error(personaResult.error.message);
1481
1793
  process.exit(ExitCode.ERROR);
1482
1794
  }
1483
1795
  const persona = personaResult.value;
1484
- const ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
1485
- "validate",
1486
- "check-deps",
1487
- "check-docs",
1488
- "cleanup",
1489
- "fix-drift",
1490
- "add"
1491
- ]);
1492
- const executor = async (command) => {
1493
- if (!ALLOWED_COMMANDS.has(command)) {
1796
+ const projectPath = process.cwd();
1797
+ const trigger = opts.trigger === "auto" ? "auto" : VALID_TRIGGERS.has(opts.trigger) ? opts.trigger : "manual";
1798
+ const commandExecutor = async (command) => {
1799
+ if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
1494
1800
  return Err8(new Error(`Unknown harness command: ${command}`));
1495
1801
  }
1496
1802
  try {
@@ -1500,12 +1806,21 @@ function createRunCommand() {
1500
1806
  return Err8(new Error(error instanceof Error ? error.message : String(error)));
1501
1807
  }
1502
1808
  };
1503
- const report = await runPersona(persona, executor);
1809
+ const report = await runPersona(persona, {
1810
+ trigger,
1811
+ commandExecutor,
1812
+ skillExecutor: executeSkill,
1813
+ projectPath
1814
+ });
1504
1815
  if (!globalOpts.quiet) {
1505
1816
  logger.info(`Persona '${report.persona}' status: ${report.status}`);
1506
- for (const c of report.commands) {
1507
- const icon = c.status === "pass" ? "v" : c.status === "fail" ? "x" : "-";
1508
- console.log(` [${icon}] ${c.name} (${c.durationMs}ms)`);
1817
+ for (const s of report.steps) {
1818
+ const icon = s.status === "pass" ? "v" : s.status === "fail" ? "x" : "-";
1819
+ const typeTag = s.type === "skill" ? " [skill]" : "";
1820
+ console.log(` [${icon}] ${s.name}${typeTag} (${s.durationMs}ms)`);
1821
+ if (s.artifactPath) {
1822
+ console.log(` artifact: ${s.artifactPath}`);
1823
+ }
1509
1824
  }
1510
1825
  }
1511
1826
  process.exit(report.status === "fail" ? ExitCode.ERROR : ExitCode.SUCCESS);
@@ -1622,8 +1937,8 @@ function createAgentCommand() {
1622
1937
 
1623
1938
  // src/commands/add.ts
1624
1939
  import { Command as Command11 } from "commander";
1625
- import * as fs7 from "fs";
1626
- import * as path13 from "path";
1940
+ import * as fs9 from "fs";
1941
+ import * as path15 from "path";
1627
1942
  import { Ok as Ok12, Err as Err10 } from "@harness-engineering/core";
1628
1943
  var LAYER_INDEX_TEMPLATE = (name) => `// ${name} layer
1629
1944
  // Add your ${name} exports here
@@ -1668,61 +1983,61 @@ async function runAdd(componentType, name, options) {
1668
1983
  try {
1669
1984
  switch (componentType) {
1670
1985
  case "layer": {
1671
- const layerDir = path13.join(cwd, "src", name);
1672
- if (!fs7.existsSync(layerDir)) {
1673
- fs7.mkdirSync(layerDir, { recursive: true });
1986
+ const layerDir = path15.join(cwd, "src", name);
1987
+ if (!fs9.existsSync(layerDir)) {
1988
+ fs9.mkdirSync(layerDir, { recursive: true });
1674
1989
  created.push(`src/${name}/`);
1675
1990
  }
1676
- const indexPath = path13.join(layerDir, "index.ts");
1677
- if (!fs7.existsSync(indexPath)) {
1678
- fs7.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
1991
+ const indexPath = path15.join(layerDir, "index.ts");
1992
+ if (!fs9.existsSync(indexPath)) {
1993
+ fs9.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
1679
1994
  created.push(`src/${name}/index.ts`);
1680
1995
  }
1681
1996
  break;
1682
1997
  }
1683
1998
  case "module": {
1684
- const modulePath = path13.join(cwd, "src", `${name}.ts`);
1685
- if (fs7.existsSync(modulePath)) {
1999
+ const modulePath = path15.join(cwd, "src", `${name}.ts`);
2000
+ if (fs9.existsSync(modulePath)) {
1686
2001
  return Err10(new CLIError(`Module ${name} already exists`, ExitCode.ERROR));
1687
2002
  }
1688
- fs7.writeFileSync(modulePath, MODULE_TEMPLATE(name));
2003
+ fs9.writeFileSync(modulePath, MODULE_TEMPLATE(name));
1689
2004
  created.push(`src/${name}.ts`);
1690
2005
  break;
1691
2006
  }
1692
2007
  case "doc": {
1693
- const docsDir = path13.join(cwd, "docs");
1694
- if (!fs7.existsSync(docsDir)) {
1695
- fs7.mkdirSync(docsDir, { recursive: true });
2008
+ const docsDir = path15.join(cwd, "docs");
2009
+ if (!fs9.existsSync(docsDir)) {
2010
+ fs9.mkdirSync(docsDir, { recursive: true });
1696
2011
  }
1697
- const docPath = path13.join(docsDir, `${name}.md`);
1698
- if (fs7.existsSync(docPath)) {
2012
+ const docPath = path15.join(docsDir, `${name}.md`);
2013
+ if (fs9.existsSync(docPath)) {
1699
2014
  return Err10(new CLIError(`Doc ${name} already exists`, ExitCode.ERROR));
1700
2015
  }
1701
- fs7.writeFileSync(docPath, DOC_TEMPLATE(name));
2016
+ fs9.writeFileSync(docPath, DOC_TEMPLATE(name));
1702
2017
  created.push(`docs/${name}.md`);
1703
2018
  break;
1704
2019
  }
1705
2020
  case "skill": {
1706
- const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-4GKJZB5R.js");
2021
+ const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-NZDLMMR6.js");
1707
2022
  generateSkillFiles2({
1708
2023
  name,
1709
2024
  description: `${name} skill`,
1710
- outputDir: path13.join(cwd, "agents", "skills", "claude-code")
2025
+ outputDir: path15.join(cwd, "agents", "skills", "claude-code")
1711
2026
  });
1712
2027
  created.push(`agents/skills/claude-code/${name}/skill.yaml`);
1713
2028
  created.push(`agents/skills/claude-code/${name}/SKILL.md`);
1714
2029
  break;
1715
2030
  }
1716
2031
  case "persona": {
1717
- const personasDir = path13.join(cwd, "agents", "personas");
1718
- if (!fs7.existsSync(personasDir)) {
1719
- fs7.mkdirSync(personasDir, { recursive: true });
2032
+ const personasDir = path15.join(cwd, "agents", "personas");
2033
+ if (!fs9.existsSync(personasDir)) {
2034
+ fs9.mkdirSync(personasDir, { recursive: true });
1720
2035
  }
1721
- const personaPath = path13.join(personasDir, `${name}.yaml`);
1722
- if (fs7.existsSync(personaPath)) {
2036
+ const personaPath = path15.join(personasDir, `${name}.yaml`);
2037
+ if (fs9.existsSync(personaPath)) {
1723
2038
  return Err10(new CLIError(`Persona ${name} already exists`, ExitCode.ERROR));
1724
2039
  }
1725
- fs7.writeFileSync(
2040
+ fs9.writeFileSync(
1726
2041
  personaPath,
1727
2042
  `name: ${name}
1728
2043
  description: ${name} persona
@@ -1910,8 +2225,8 @@ function createListCommand() {
1910
2225
 
1911
2226
  // src/commands/persona/generate.ts
1912
2227
  import { Command as Command16 } from "commander";
1913
- import * as fs8 from "fs";
1914
- import * as path14 from "path";
2228
+ import * as fs10 from "fs";
2229
+ import * as path16 from "path";
1915
2230
 
1916
2231
  // src/persona/generators/runtime.ts
1917
2232
  import { Ok as Ok13, Err as Err11 } from "@harness-engineering/core";
@@ -1927,7 +2242,7 @@ function generateRuntime(persona) {
1927
2242
  const config = {
1928
2243
  name: toKebabCase(persona.name),
1929
2244
  skills: persona.skills,
1930
- commands: persona.commands,
2245
+ steps: persona.steps,
1931
2246
  timeout: persona.config.timeout,
1932
2247
  severity: persona.config.severity
1933
2248
  };
@@ -1955,13 +2270,17 @@ function formatTrigger(trigger) {
1955
2270
  }
1956
2271
  case "scheduled":
1957
2272
  return `Scheduled (cron: ${trigger.cron})`;
2273
+ case "manual":
2274
+ return "Manual";
1958
2275
  }
1959
2276
  }
1960
2277
  function generateAgentsMd(persona) {
1961
2278
  try {
1962
2279
  const triggers = persona.triggers.map(formatTrigger).join(", ");
1963
2280
  const skills = persona.skills.join(", ");
1964
- const commands = persona.commands.map((c) => `\`harness ${c}\``).join(", ");
2281
+ const commands = persona.steps.filter((s) => "command" in s).map((s) => `\`harness ${s.command}\``).join(", ");
2282
+ const stepSkills = persona.steps.filter((s) => "skill" in s).map((s) => `\`harness skill run ${s.skill}\``).join(", ");
2283
+ const allCommands = [commands, stepSkills].filter(Boolean).join(", ");
1965
2284
  const fragment = `## ${persona.name} Agent
1966
2285
 
1967
2286
  **Role:** ${persona.role}
@@ -1970,7 +2289,7 @@ function generateAgentsMd(persona) {
1970
2289
 
1971
2290
  **Skills:** ${skills}
1972
2291
 
1973
- **When this agent flags an issue:** Fix violations before merging. Run ${commands} locally to validate.
2292
+ **When this agent flags an issue:** Fix violations before merging. Run ${allCommands} locally to validate.
1974
2293
  `;
1975
2294
  return Ok14(fragment);
1976
2295
  } catch (error) {
@@ -2017,9 +2336,10 @@ function generateCIWorkflow(persona, platform) {
2017
2336
  { uses: "actions/setup-node@v4", with: { "node-version": "20" } },
2018
2337
  { uses: "pnpm/action-setup@v4", with: { run_install: "frozen" } }
2019
2338
  ];
2020
- for (const cmd of persona.commands) {
2339
+ const commandSteps = persona.steps.filter((s) => "command" in s);
2340
+ for (const step of commandSteps) {
2021
2341
  const severityFlag = severity ? ` --severity ${severity}` : "";
2022
- steps.push({ run: `npx harness ${cmd}${severityFlag}` });
2342
+ steps.push({ run: `npx harness ${step.command}${severityFlag}` });
2023
2343
  }
2024
2344
  const workflow = {
2025
2345
  name: persona.name,
@@ -2046,40 +2366,40 @@ function createGenerateCommand2() {
2046
2366
  return new Command16("generate").description("Generate artifacts from a persona config").argument("<name>", "Persona name (e.g., architecture-enforcer)").option("--output-dir <dir>", "Output directory", ".").option("--only <type>", "Generate only: ci, agents-md, runtime").action(async (name, opts, cmd) => {
2047
2367
  const globalOpts = cmd.optsWithGlobals();
2048
2368
  const personasDir = resolvePersonasDir();
2049
- const filePath = path14.join(personasDir, `${name}.yaml`);
2369
+ const filePath = path16.join(personasDir, `${name}.yaml`);
2050
2370
  const personaResult = loadPersona(filePath);
2051
2371
  if (!personaResult.ok) {
2052
2372
  logger.error(personaResult.error.message);
2053
2373
  process.exit(ExitCode.ERROR);
2054
2374
  }
2055
2375
  const persona = personaResult.value;
2056
- const outputDir = path14.resolve(opts.outputDir);
2376
+ const outputDir = path16.resolve(opts.outputDir);
2057
2377
  const slug = toKebabCase(persona.name);
2058
2378
  const only = opts.only;
2059
2379
  const generated = [];
2060
2380
  if (!only || only === "runtime") {
2061
2381
  const result = generateRuntime(persona);
2062
2382
  if (result.ok) {
2063
- const outPath = path14.join(outputDir, `${slug}.runtime.json`);
2064
- fs8.mkdirSync(path14.dirname(outPath), { recursive: true });
2065
- fs8.writeFileSync(outPath, result.value);
2383
+ const outPath = path16.join(outputDir, `${slug}.runtime.json`);
2384
+ fs10.mkdirSync(path16.dirname(outPath), { recursive: true });
2385
+ fs10.writeFileSync(outPath, result.value);
2066
2386
  generated.push(outPath);
2067
2387
  }
2068
2388
  }
2069
2389
  if (!only || only === "agents-md") {
2070
2390
  const result = generateAgentsMd(persona);
2071
2391
  if (result.ok) {
2072
- const outPath = path14.join(outputDir, `${slug}.agents.md`);
2073
- fs8.writeFileSync(outPath, result.value);
2392
+ const outPath = path16.join(outputDir, `${slug}.agents.md`);
2393
+ fs10.writeFileSync(outPath, result.value);
2074
2394
  generated.push(outPath);
2075
2395
  }
2076
2396
  }
2077
2397
  if (!only || only === "ci") {
2078
2398
  const result = generateCIWorkflow(persona, "github");
2079
2399
  if (result.ok) {
2080
- const outPath = path14.join(outputDir, ".github", "workflows", `${slug}.yml`);
2081
- fs8.mkdirSync(path14.dirname(outPath), { recursive: true });
2082
- fs8.writeFileSync(outPath, result.value);
2400
+ const outPath = path16.join(outputDir, ".github", "workflows", `${slug}.yml`);
2401
+ fs10.mkdirSync(path16.dirname(outPath), { recursive: true });
2402
+ fs10.writeFileSync(outPath, result.value);
2083
2403
  generated.push(outPath);
2084
2404
  }
2085
2405
  }
@@ -2104,26 +2424,26 @@ import { Command as Command22 } from "commander";
2104
2424
 
2105
2425
  // src/commands/skill/list.ts
2106
2426
  import { Command as Command18 } from "commander";
2107
- import * as fs9 from "fs";
2108
- import * as path15 from "path";
2109
- import { parse as parse2 } from "yaml";
2427
+ import * as fs11 from "fs";
2428
+ import * as path17 from "path";
2429
+ import { parse as parse3 } from "yaml";
2110
2430
  function createListCommand2() {
2111
2431
  return new Command18("list").description("List available skills").action(async (_opts, cmd) => {
2112
2432
  const globalOpts = cmd.optsWithGlobals();
2113
2433
  const skillsDir = resolveSkillsDir();
2114
- if (!fs9.existsSync(skillsDir)) {
2434
+ if (!fs11.existsSync(skillsDir)) {
2115
2435
  logger.info("No skills directory found.");
2116
2436
  process.exit(ExitCode.SUCCESS);
2117
2437
  return;
2118
2438
  }
2119
- const entries = fs9.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2439
+ const entries = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2120
2440
  const skills = [];
2121
2441
  for (const name of entries) {
2122
- const yamlPath = path15.join(skillsDir, name, "skill.yaml");
2123
- if (!fs9.existsSync(yamlPath)) continue;
2442
+ const yamlPath = path17.join(skillsDir, name, "skill.yaml");
2443
+ if (!fs11.existsSync(yamlPath)) continue;
2124
2444
  try {
2125
- const raw = fs9.readFileSync(yamlPath, "utf-8");
2126
- const parsed = parse2(raw);
2445
+ const raw = fs11.readFileSync(yamlPath, "utf-8");
2446
+ const parsed = parse3(raw);
2127
2447
  const result = SkillMetadataSchema.safeParse(parsed);
2128
2448
  if (result.success) {
2129
2449
  skills.push(result.data);
@@ -2153,9 +2473,9 @@ function createListCommand2() {
2153
2473
 
2154
2474
  // src/commands/skill/run.ts
2155
2475
  import { Command as Command19 } from "commander";
2156
- import * as fs10 from "fs";
2157
- import * as path16 from "path";
2158
- import { parse as parse3 } from "yaml";
2476
+ import * as fs12 from "fs";
2477
+ import * as path18 from "path";
2478
+ import { parse as parse4 } from "yaml";
2159
2479
 
2160
2480
  // src/skill/complexity.ts
2161
2481
  import { execSync as execSync2 } from "child_process";
@@ -2244,18 +2564,18 @@ ${options.priorState}`);
2244
2564
  function createRunCommand2() {
2245
2565
  return new Command19("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Complexity: auto, light, full", "auto").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").action(async (name, opts, _cmd) => {
2246
2566
  const skillsDir = resolveSkillsDir();
2247
- const skillDir = path16.join(skillsDir, name);
2248
- if (!fs10.existsSync(skillDir)) {
2567
+ const skillDir = path18.join(skillsDir, name);
2568
+ if (!fs12.existsSync(skillDir)) {
2249
2569
  logger.error(`Skill not found: ${name}`);
2250
2570
  process.exit(ExitCode.ERROR);
2251
2571
  return;
2252
2572
  }
2253
- const yamlPath = path16.join(skillDir, "skill.yaml");
2573
+ const yamlPath = path18.join(skillDir, "skill.yaml");
2254
2574
  let metadata = null;
2255
- if (fs10.existsSync(yamlPath)) {
2575
+ if (fs12.existsSync(yamlPath)) {
2256
2576
  try {
2257
- const raw = fs10.readFileSync(yamlPath, "utf-8");
2258
- const parsed = parse3(raw);
2577
+ const raw = fs12.readFileSync(yamlPath, "utf-8");
2578
+ const parsed = parse4(raw);
2259
2579
  const result = SkillMetadataSchema.safeParse(parsed);
2260
2580
  if (result.success) metadata = result.data;
2261
2581
  } catch {
@@ -2265,17 +2585,17 @@ function createRunCommand2() {
2265
2585
  if (metadata?.phases && metadata.phases.length > 0) {
2266
2586
  const requested = opts.complexity ?? "auto";
2267
2587
  if (requested === "auto") {
2268
- const projectPath2 = opts.path ? path16.resolve(opts.path) : process.cwd();
2588
+ const projectPath2 = opts.path ? path18.resolve(opts.path) : process.cwd();
2269
2589
  complexity = detectComplexity(projectPath2);
2270
2590
  } else {
2271
2591
  complexity = requested;
2272
2592
  }
2273
2593
  }
2274
2594
  let principles;
2275
- const projectPath = opts.path ? path16.resolve(opts.path) : process.cwd();
2276
- const principlesPath = path16.join(projectPath, "docs", "principles.md");
2277
- if (fs10.existsSync(principlesPath)) {
2278
- principles = fs10.readFileSync(principlesPath, "utf-8");
2595
+ const projectPath = opts.path ? path18.resolve(opts.path) : process.cwd();
2596
+ const principlesPath = path18.join(projectPath, "docs", "principles.md");
2597
+ if (fs12.existsSync(principlesPath)) {
2598
+ principles = fs12.readFileSync(principlesPath, "utf-8");
2279
2599
  }
2280
2600
  let priorState;
2281
2601
  let stateWarning;
@@ -2290,16 +2610,16 @@ function createRunCommand2() {
2290
2610
  }
2291
2611
  if (metadata?.state.persistent && metadata.state.files.length > 0) {
2292
2612
  for (const stateFilePath of metadata.state.files) {
2293
- const fullPath = path16.join(projectPath, stateFilePath);
2294
- if (fs10.existsSync(fullPath)) {
2295
- const stat = fs10.statSync(fullPath);
2613
+ const fullPath = path18.join(projectPath, stateFilePath);
2614
+ if (fs12.existsSync(fullPath)) {
2615
+ const stat = fs12.statSync(fullPath);
2296
2616
  if (stat.isDirectory()) {
2297
- const files = fs10.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs10.statSync(path16.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2617
+ const files = fs12.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs12.statSync(path18.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2298
2618
  if (files.length > 0) {
2299
- priorState = fs10.readFileSync(path16.join(fullPath, files[0].name), "utf-8");
2619
+ priorState = fs12.readFileSync(path18.join(fullPath, files[0].name), "utf-8");
2300
2620
  }
2301
2621
  } else {
2302
- priorState = fs10.readFileSync(fullPath, "utf-8");
2622
+ priorState = fs12.readFileSync(fullPath, "utf-8");
2303
2623
  }
2304
2624
  break;
2305
2625
  }
@@ -2318,17 +2638,17 @@ function createRunCommand2() {
2318
2638
  ...stateWarning !== void 0 && { stateWarning },
2319
2639
  party: opts.party
2320
2640
  });
2321
- const skillMdPath = path16.join(skillDir, "SKILL.md");
2322
- if (!fs10.existsSync(skillMdPath)) {
2641
+ const skillMdPath = path18.join(skillDir, "SKILL.md");
2642
+ if (!fs12.existsSync(skillMdPath)) {
2323
2643
  logger.error(`SKILL.md not found for skill: ${name}`);
2324
2644
  process.exit(ExitCode.ERROR);
2325
2645
  return;
2326
2646
  }
2327
- let content = fs10.readFileSync(skillMdPath, "utf-8");
2647
+ let content = fs12.readFileSync(skillMdPath, "utf-8");
2328
2648
  if (metadata?.state.persistent && opts.path) {
2329
- const stateFile = path16.join(projectPath, ".harness", "state.json");
2330
- if (fs10.existsSync(stateFile)) {
2331
- const stateContent = fs10.readFileSync(stateFile, "utf-8");
2649
+ const stateFile = path18.join(projectPath, ".harness", "state.json");
2650
+ if (fs12.existsSync(stateFile)) {
2651
+ const stateContent = fs12.readFileSync(stateFile, "utf-8");
2332
2652
  content += `
2333
2653
 
2334
2654
  ---
@@ -2346,9 +2666,9 @@ ${stateContent}
2346
2666
 
2347
2667
  // src/commands/skill/validate.ts
2348
2668
  import { Command as Command20 } from "commander";
2349
- import * as fs11 from "fs";
2350
- import * as path17 from "path";
2351
- import { parse as parse4 } from "yaml";
2669
+ import * as fs13 from "fs";
2670
+ import * as path19 from "path";
2671
+ import { parse as parse5 } from "yaml";
2352
2672
  var REQUIRED_SECTIONS = [
2353
2673
  "## When to Use",
2354
2674
  "## Process",
@@ -2360,32 +2680,32 @@ function createValidateCommand3() {
2360
2680
  return new Command20("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
2361
2681
  const globalOpts = cmd.optsWithGlobals();
2362
2682
  const skillsDir = resolveSkillsDir();
2363
- if (!fs11.existsSync(skillsDir)) {
2683
+ if (!fs13.existsSync(skillsDir)) {
2364
2684
  logger.info("No skills directory found.");
2365
2685
  process.exit(ExitCode.SUCCESS);
2366
2686
  return;
2367
2687
  }
2368
- const entries = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2688
+ const entries = fs13.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2369
2689
  const errors = [];
2370
2690
  let validated = 0;
2371
2691
  for (const name of entries) {
2372
- const skillDir = path17.join(skillsDir, name);
2373
- const yamlPath = path17.join(skillDir, "skill.yaml");
2374
- const skillMdPath = path17.join(skillDir, "SKILL.md");
2375
- if (!fs11.existsSync(yamlPath)) {
2692
+ const skillDir = path19.join(skillsDir, name);
2693
+ const yamlPath = path19.join(skillDir, "skill.yaml");
2694
+ const skillMdPath = path19.join(skillDir, "SKILL.md");
2695
+ if (!fs13.existsSync(yamlPath)) {
2376
2696
  errors.push(`${name}: missing skill.yaml`);
2377
2697
  continue;
2378
2698
  }
2379
2699
  try {
2380
- const raw = fs11.readFileSync(yamlPath, "utf-8");
2381
- const parsed = parse4(raw);
2700
+ const raw = fs13.readFileSync(yamlPath, "utf-8");
2701
+ const parsed = parse5(raw);
2382
2702
  const result = SkillMetadataSchema.safeParse(parsed);
2383
2703
  if (!result.success) {
2384
2704
  errors.push(`${name}/skill.yaml: ${result.error.message}`);
2385
2705
  continue;
2386
2706
  }
2387
- if (fs11.existsSync(skillMdPath)) {
2388
- const mdContent = fs11.readFileSync(skillMdPath, "utf-8");
2707
+ if (fs13.existsSync(skillMdPath)) {
2708
+ const mdContent = fs13.readFileSync(skillMdPath, "utf-8");
2389
2709
  for (const section of REQUIRED_SECTIONS) {
2390
2710
  if (!mdContent.includes(section)) {
2391
2711
  errors.push(`${name}/SKILL.md: missing section "${section}"`);
@@ -2427,28 +2747,28 @@ function createValidateCommand3() {
2427
2747
 
2428
2748
  // src/commands/skill/info.ts
2429
2749
  import { Command as Command21 } from "commander";
2430
- import * as fs12 from "fs";
2431
- import * as path18 from "path";
2432
- import { parse as parse5 } from "yaml";
2750
+ import * as fs14 from "fs";
2751
+ import * as path20 from "path";
2752
+ import { parse as parse6 } from "yaml";
2433
2753
  function createInfoCommand() {
2434
2754
  return new Command21("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
2435
2755
  const globalOpts = cmd.optsWithGlobals();
2436
2756
  const skillsDir = resolveSkillsDir();
2437
- const skillDir = path18.join(skillsDir, name);
2438
- if (!fs12.existsSync(skillDir)) {
2757
+ const skillDir = path20.join(skillsDir, name);
2758
+ if (!fs14.existsSync(skillDir)) {
2439
2759
  logger.error(`Skill not found: ${name}`);
2440
2760
  process.exit(ExitCode.ERROR);
2441
2761
  return;
2442
2762
  }
2443
- const yamlPath = path18.join(skillDir, "skill.yaml");
2444
- if (!fs12.existsSync(yamlPath)) {
2763
+ const yamlPath = path20.join(skillDir, "skill.yaml");
2764
+ if (!fs14.existsSync(yamlPath)) {
2445
2765
  logger.error(`skill.yaml not found for skill: ${name}`);
2446
2766
  process.exit(ExitCode.ERROR);
2447
2767
  return;
2448
2768
  }
2449
2769
  try {
2450
- const raw = fs12.readFileSync(yamlPath, "utf-8");
2451
- const parsed = parse5(raw);
2770
+ const raw = fs14.readFileSync(yamlPath, "utf-8");
2771
+ const parsed = parse6(raw);
2452
2772
  const result = SkillMetadataSchema.safeParse(parsed);
2453
2773
  if (!result.success) {
2454
2774
  logger.error(`Invalid skill.yaml: ${result.error.message}`);
@@ -2501,12 +2821,12 @@ import { Command as Command26 } from "commander";
2501
2821
 
2502
2822
  // src/commands/state/show.ts
2503
2823
  import { Command as Command23 } from "commander";
2504
- import * as path19 from "path";
2824
+ import * as path21 from "path";
2505
2825
  import { loadState } from "@harness-engineering/core";
2506
2826
  function createShowCommand() {
2507
2827
  return new Command23("show").description("Show current project state").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
2508
2828
  const globalOpts = cmd.optsWithGlobals();
2509
- const projectPath = path19.resolve(opts.path);
2829
+ const projectPath = path21.resolve(opts.path);
2510
2830
  const result = await loadState(projectPath);
2511
2831
  if (!result.ok) {
2512
2832
  logger.error(result.error.message);
@@ -2546,22 +2866,22 @@ Decisions: ${state.decisions.length}`);
2546
2866
 
2547
2867
  // src/commands/state/reset.ts
2548
2868
  import { Command as Command24 } from "commander";
2549
- import * as fs13 from "fs";
2550
- import * as path20 from "path";
2869
+ import * as fs15 from "fs";
2870
+ import * as path22 from "path";
2551
2871
  import * as readline from "readline";
2552
2872
  function createResetCommand() {
2553
2873
  return new Command24("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
2554
- const projectPath = path20.resolve(opts.path);
2555
- const statePath = path20.join(projectPath, ".harness", "state.json");
2556
- if (!fs13.existsSync(statePath)) {
2874
+ const projectPath = path22.resolve(opts.path);
2875
+ const statePath = path22.join(projectPath, ".harness", "state.json");
2876
+ if (!fs15.existsSync(statePath)) {
2557
2877
  logger.info("No state file found. Nothing to reset.");
2558
2878
  process.exit(ExitCode.SUCCESS);
2559
2879
  return;
2560
2880
  }
2561
2881
  if (!opts.yes) {
2562
2882
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2563
- const answer = await new Promise((resolve14) => {
2564
- rl.question("Reset project state? This cannot be undone. [y/N] ", resolve14);
2883
+ const answer = await new Promise((resolve18) => {
2884
+ rl.question("Reset project state? This cannot be undone. [y/N] ", resolve18);
2565
2885
  });
2566
2886
  rl.close();
2567
2887
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -2571,7 +2891,7 @@ function createResetCommand() {
2571
2891
  }
2572
2892
  }
2573
2893
  try {
2574
- fs13.unlinkSync(statePath);
2894
+ fs15.unlinkSync(statePath);
2575
2895
  logger.success("Project state reset.");
2576
2896
  } catch (e) {
2577
2897
  logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
@@ -2584,11 +2904,11 @@ function createResetCommand() {
2584
2904
 
2585
2905
  // src/commands/state/learn.ts
2586
2906
  import { Command as Command25 } from "commander";
2587
- import * as path21 from "path";
2907
+ import * as path23 from "path";
2588
2908
  import { appendLearning } from "@harness-engineering/core";
2589
2909
  function createLearnCommand() {
2590
2910
  return new Command25("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").action(async (message, opts, _cmd) => {
2591
- const projectPath = path21.resolve(opts.path);
2911
+ const projectPath = path23.resolve(opts.path);
2592
2912
  const result = await appendLearning(projectPath, message);
2593
2913
  if (!result.ok) {
2594
2914
  logger.error(result.error.message);
@@ -2611,18 +2931,18 @@ function createStateCommand() {
2611
2931
 
2612
2932
  // src/commands/check-phase-gate.ts
2613
2933
  import { Command as Command27 } from "commander";
2614
- import * as path22 from "path";
2615
- import * as fs14 from "fs";
2934
+ import * as path24 from "path";
2935
+ import * as fs16 from "fs";
2616
2936
  import { Ok as Ok16 } from "@harness-engineering/core";
2617
2937
  function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
2618
- const relImpl = path22.relative(cwd, implFile);
2938
+ const relImpl = path24.relative(cwd, implFile);
2619
2939
  const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
2620
2940
  const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
2621
2941
  const segments = afterBase.split("/");
2622
2942
  const firstSegment = segments[0] ?? "";
2623
- const feature = segments.length > 1 ? firstSegment : path22.basename(firstSegment, path22.extname(firstSegment));
2943
+ const feature = segments.length > 1 ? firstSegment : path24.basename(firstSegment, path24.extname(firstSegment));
2624
2944
  const specRelative = specPattern.replace("{feature}", feature);
2625
- return path22.resolve(cwd, specRelative);
2945
+ return path24.resolve(cwd, specRelative);
2626
2946
  }
2627
2947
  async function runCheckPhaseGate(options) {
2628
2948
  const configResult = resolveConfig(options.configPath);
@@ -2630,7 +2950,7 @@ async function runCheckPhaseGate(options) {
2630
2950
  return configResult;
2631
2951
  }
2632
2952
  const config = configResult.value;
2633
- const cwd = options.cwd ?? (options.configPath ? path22.dirname(path22.resolve(options.configPath)) : process.cwd());
2953
+ const cwd = options.cwd ?? (options.configPath ? path24.dirname(path24.resolve(options.configPath)) : process.cwd());
2634
2954
  if (!config.phaseGates?.enabled) {
2635
2955
  return Ok16({
2636
2956
  pass: true,
@@ -2647,10 +2967,10 @@ async function runCheckPhaseGate(options) {
2647
2967
  for (const implFile of implFiles) {
2648
2968
  checkedFiles++;
2649
2969
  const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
2650
- if (!fs14.existsSync(expectedSpec)) {
2970
+ if (!fs16.existsSync(expectedSpec)) {
2651
2971
  missingSpecs.push({
2652
- implFile: path22.relative(cwd, implFile),
2653
- expectedSpec: path22.relative(cwd, expectedSpec)
2972
+ implFile: path24.relative(cwd, implFile),
2973
+ expectedSpec: path24.relative(cwd, expectedSpec)
2654
2974
  });
2655
2975
  }
2656
2976
  }
@@ -2720,15 +3040,15 @@ function createCheckPhaseGateCommand() {
2720
3040
 
2721
3041
  // src/commands/generate-slash-commands.ts
2722
3042
  import { Command as Command28 } from "commander";
2723
- import fs17 from "fs";
2724
- import path25 from "path";
3043
+ import fs19 from "fs";
3044
+ import path27 from "path";
2725
3045
  import os2 from "os";
2726
3046
  import readline2 from "readline";
2727
3047
 
2728
3048
  // src/slash-commands/normalize.ts
2729
- import fs15 from "fs";
2730
- import path23 from "path";
2731
- import { parse as parse6 } from "yaml";
3049
+ import fs17 from "fs";
3050
+ import path25 from "path";
3051
+ import { parse as parse7 } from "yaml";
2732
3052
 
2733
3053
  // src/slash-commands/normalize-name.ts
2734
3054
  function normalizeName(skillName) {
@@ -2744,106 +3064,117 @@ function normalizeName(skillName) {
2744
3064
  }
2745
3065
 
2746
3066
  // src/slash-commands/normalize.ts
2747
- function normalizeSkills(skillsDir, platforms) {
2748
- const entries = fs15.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3067
+ function normalizeSkills(skillSources, platforms) {
2749
3068
  const specs = [];
2750
3069
  const nameMap = /* @__PURE__ */ new Map();
2751
- for (const entry of entries) {
2752
- const yamlPath = path23.join(skillsDir, entry.name, "skill.yaml");
2753
- if (!fs15.existsSync(yamlPath)) continue;
2754
- let raw;
2755
- try {
2756
- raw = fs15.readFileSync(yamlPath, "utf-8");
2757
- } catch {
2758
- continue;
2759
- }
2760
- const parsed = parse6(raw);
2761
- const result = SkillMetadataSchema.safeParse(parsed);
2762
- if (!result.success) {
2763
- console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
2764
- continue;
2765
- }
2766
- const meta = result.data;
2767
- const matchesPlatform = platforms.some((p) => meta.platforms.includes(p));
2768
- if (!matchesPlatform) continue;
2769
- const normalized = normalizeName(meta.name);
2770
- if (nameMap.has(normalized)) {
2771
- throw new Error(
2772
- `Name collision: skills "${nameMap.get(normalized)}" and "${meta.name}" both normalize to "${normalized}"`
3070
+ for (const { dir: skillsDir, source } of skillSources) {
3071
+ if (!fs17.existsSync(skillsDir)) continue;
3072
+ const entries = fs17.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3073
+ for (const entry of entries) {
3074
+ const yamlPath = path25.join(skillsDir, entry.name, "skill.yaml");
3075
+ if (!fs17.existsSync(yamlPath)) continue;
3076
+ let raw;
3077
+ try {
3078
+ raw = fs17.readFileSync(yamlPath, "utf-8");
3079
+ } catch {
3080
+ continue;
3081
+ }
3082
+ const parsed = parse7(raw);
3083
+ const result = SkillMetadataSchema.safeParse(parsed);
3084
+ if (!result.success) {
3085
+ console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
3086
+ continue;
3087
+ }
3088
+ const meta = result.data;
3089
+ const matchesPlatform = platforms.some((p) => meta.platforms.includes(p));
3090
+ if (!matchesPlatform) continue;
3091
+ const normalized = normalizeName(meta.name);
3092
+ const existing = nameMap.get(normalized);
3093
+ if (existing) {
3094
+ if (existing.source === source) {
3095
+ throw new Error(
3096
+ `Name collision: skills "${existing.skillName}" and "${meta.name}" both normalize to "${normalized}"`
3097
+ );
3098
+ }
3099
+ continue;
3100
+ }
3101
+ nameMap.set(normalized, { skillName: meta.name, source });
3102
+ const skillMdPath = path25.join(skillsDir, entry.name, "SKILL.md");
3103
+ const skillMdContent = fs17.existsSync(skillMdPath) ? fs17.readFileSync(skillMdPath, "utf-8") : "";
3104
+ const skillMdRelative = path25.relative(
3105
+ process.cwd(),
3106
+ path25.join(skillsDir, entry.name, "SKILL.md")
2773
3107
  );
2774
- }
2775
- nameMap.set(normalized, meta.name);
2776
- const skillMdPath = path23.join(skillsDir, entry.name, "SKILL.md");
2777
- const skillMdContent = fs15.existsSync(skillMdPath) ? fs15.readFileSync(skillMdPath, "utf-8") : "";
2778
- const skillMdRelative = path23.relative(
2779
- process.cwd(),
2780
- path23.join(skillsDir, entry.name, "SKILL.md")
2781
- );
2782
- const skillYamlRelative = path23.relative(
2783
- process.cwd(),
2784
- path23.join(skillsDir, entry.name, "skill.yaml")
2785
- );
2786
- const args = (meta.cli?.args ?? []).map((a) => ({
2787
- name: a.name,
2788
- description: a.description ?? "",
2789
- required: a.required ?? false
2790
- }));
2791
- const tools = [...meta.tools ?? []];
2792
- if (!tools.includes("Read")) {
2793
- tools.push("Read");
2794
- }
2795
- const contextLines = [];
2796
- if (meta.cognitive_mode) {
2797
- contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
2798
- }
2799
- if (meta.type) {
2800
- contextLines.push(`Type: ${meta.type}`);
2801
- }
2802
- if (meta.state?.persistent) {
2803
- const files = meta.state.files?.join(", ") ?? "";
2804
- contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
2805
- }
2806
- const objectiveLines = [meta.description];
2807
- if (meta.phases && meta.phases.length > 0) {
2808
- objectiveLines.push("");
2809
- objectiveLines.push("Phases:");
2810
- for (const phase of meta.phases) {
2811
- const req = phase.required !== false ? "" : " (optional)";
2812
- objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
3108
+ const skillYamlRelative = path25.relative(
3109
+ process.cwd(),
3110
+ path25.join(skillsDir, entry.name, "skill.yaml")
3111
+ );
3112
+ const args = (meta.cli?.args ?? []).map((a) => ({
3113
+ name: a.name,
3114
+ description: a.description ?? "",
3115
+ required: a.required ?? false
3116
+ }));
3117
+ const tools = [...meta.tools ?? []];
3118
+ if (!tools.includes("Read")) {
3119
+ tools.push("Read");
2813
3120
  }
2814
- }
2815
- const executionContextLines = [];
2816
- if (skillMdContent) {
2817
- executionContextLines.push(`@${skillMdRelative}`);
2818
- executionContextLines.push(`@${skillYamlRelative}`);
2819
- }
2820
- const processLines = [];
2821
- if (meta.mcp?.tool) {
2822
- processLines.push(`1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`);
2823
- processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
2824
- processLines.push(`3. Pass through any arguments provided by the user`);
2825
- } else {
2826
- processLines.push(`1. Read SKILL.md and follow its workflow directly`);
2827
- processLines.push(`2. Pass through any arguments provided by the user`);
2828
- }
2829
- specs.push({
2830
- name: normalized,
2831
- namespace: "harness",
2832
- fullName: `harness:${normalized}`,
2833
- description: meta.description,
2834
- version: meta.version,
2835
- ...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
2836
- tools,
2837
- args,
2838
- skillYamlName: meta.name,
2839
- sourceDir: entry.name,
2840
- prompt: {
2841
- context: contextLines.join("\n"),
2842
- objective: objectiveLines.join("\n"),
2843
- executionContext: executionContextLines.join("\n"),
2844
- process: processLines.join("\n")
3121
+ const contextLines = [];
3122
+ if (meta.cognitive_mode) {
3123
+ contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
2845
3124
  }
2846
- });
3125
+ if (meta.type) {
3126
+ contextLines.push(`Type: ${meta.type}`);
3127
+ }
3128
+ if (meta.state?.persistent) {
3129
+ const files = meta.state.files?.join(", ") ?? "";
3130
+ contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
3131
+ }
3132
+ const objectiveLines = [meta.description];
3133
+ if (meta.phases && meta.phases.length > 0) {
3134
+ objectiveLines.push("");
3135
+ objectiveLines.push("Phases:");
3136
+ for (const phase of meta.phases) {
3137
+ const req = phase.required !== false ? "" : " (optional)";
3138
+ objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
3139
+ }
3140
+ }
3141
+ const executionContextLines = [];
3142
+ if (skillMdContent) {
3143
+ executionContextLines.push(`@${skillMdRelative}`);
3144
+ executionContextLines.push(`@${skillYamlRelative}`);
3145
+ }
3146
+ const processLines = [];
3147
+ if (meta.mcp?.tool) {
3148
+ processLines.push(
3149
+ `1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`
3150
+ );
3151
+ processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
3152
+ processLines.push(`3. Pass through any arguments provided by the user`);
3153
+ } else {
3154
+ processLines.push(`1. Read SKILL.md and follow its workflow directly`);
3155
+ processLines.push(`2. Pass through any arguments provided by the user`);
3156
+ }
3157
+ specs.push({
3158
+ name: normalized,
3159
+ namespace: "harness",
3160
+ fullName: `harness:${normalized}`,
3161
+ description: meta.description,
3162
+ version: meta.version,
3163
+ ...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
3164
+ tools,
3165
+ args,
3166
+ skillYamlName: meta.name,
3167
+ sourceDir: entry.name,
3168
+ skillsBaseDir: skillsDir,
3169
+ source,
3170
+ prompt: {
3171
+ context: contextLines.join("\n"),
3172
+ objective: objectiveLines.join("\n"),
3173
+ executionContext: executionContextLines.join("\n"),
3174
+ process: processLines.join("\n")
3175
+ }
3176
+ });
3177
+ }
2847
3178
  }
2848
3179
  return specs;
2849
3180
  }
@@ -2945,19 +3276,24 @@ function renderGemini(spec, skillMdContent, skillYamlContent) {
2945
3276
  }
2946
3277
 
2947
3278
  // src/slash-commands/sync.ts
2948
- import fs16 from "fs";
2949
- import path24 from "path";
3279
+ import fs18 from "fs";
3280
+ import path26 from "path";
3281
+
3282
+ // src/agent-definitions/constants.ts
3283
+ var GENERATED_HEADER_AGENT = "<!-- Generated by harness generate-agent-definitions. Do not edit. -->";
3284
+
3285
+ // src/slash-commands/sync.ts
2950
3286
  function computeSyncPlan(outputDir, rendered) {
2951
3287
  const added = [];
2952
3288
  const updated = [];
2953
3289
  const removed = [];
2954
3290
  const unchanged = [];
2955
3291
  for (const [filename, content] of rendered) {
2956
- const filePath = path24.join(outputDir, filename);
2957
- if (!fs16.existsSync(filePath)) {
3292
+ const filePath = path26.join(outputDir, filename);
3293
+ if (!fs18.existsSync(filePath)) {
2958
3294
  added.push(filename);
2959
3295
  } else {
2960
- const existing = fs16.readFileSync(filePath, "utf-8");
3296
+ const existing = fs18.readFileSync(filePath, "utf-8");
2961
3297
  if (existing === content) {
2962
3298
  unchanged.push(filename);
2963
3299
  } else {
@@ -2965,15 +3301,15 @@ function computeSyncPlan(outputDir, rendered) {
2965
3301
  }
2966
3302
  }
2967
3303
  }
2968
- if (fs16.existsSync(outputDir)) {
2969
- const existing = fs16.readdirSync(outputDir).filter((f) => {
2970
- const stat = fs16.statSync(path24.join(outputDir, f));
3304
+ if (fs18.existsSync(outputDir)) {
3305
+ const existing = fs18.readdirSync(outputDir).filter((f) => {
3306
+ const stat = fs18.statSync(path26.join(outputDir, f));
2971
3307
  return stat.isFile();
2972
3308
  });
2973
3309
  for (const filename of existing) {
2974
3310
  if (rendered.has(filename)) continue;
2975
- const content = fs16.readFileSync(path24.join(outputDir, filename), "utf-8");
2976
- if (content.includes(GENERATED_HEADER_CLAUDE) || content.includes(GENERATED_HEADER_GEMINI)) {
3311
+ const content = fs18.readFileSync(path26.join(outputDir, filename), "utf-8");
3312
+ if (content.includes(GENERATED_HEADER_CLAUDE) || content.includes(GENERATED_HEADER_GEMINI) || content.includes(GENERATED_HEADER_AGENT)) {
2977
3313
  removed.push(filename);
2978
3314
  }
2979
3315
  }
@@ -2981,18 +3317,18 @@ function computeSyncPlan(outputDir, rendered) {
2981
3317
  return { added, updated, removed, unchanged };
2982
3318
  }
2983
3319
  function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
2984
- fs16.mkdirSync(outputDir, { recursive: true });
3320
+ fs18.mkdirSync(outputDir, { recursive: true });
2985
3321
  for (const filename of [...plan.added, ...plan.updated]) {
2986
3322
  const content = rendered.get(filename);
2987
3323
  if (content !== void 0) {
2988
- fs16.writeFileSync(path24.join(outputDir, filename), content);
3324
+ fs18.writeFileSync(path26.join(outputDir, filename), content);
2989
3325
  }
2990
3326
  }
2991
3327
  if (deleteOrphans) {
2992
3328
  for (const filename of plan.removed) {
2993
- const filePath = path24.join(outputDir, filename);
2994
- if (fs16.existsSync(filePath)) {
2995
- fs16.unlinkSync(filePath);
3329
+ const filePath = path26.join(outputDir, filename);
3330
+ if (fs18.existsSync(filePath)) {
3331
+ fs18.unlinkSync(filePath);
2996
3332
  }
2997
3333
  }
2998
3334
  }
@@ -3001,30 +3337,44 @@ function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
3001
3337
  // src/commands/generate-slash-commands.ts
3002
3338
  function resolveOutputDir(platform, opts) {
3003
3339
  if (opts.output) {
3004
- return path25.join(opts.output, "harness");
3340
+ return path27.join(opts.output, "harness");
3005
3341
  }
3006
3342
  if (opts.global) {
3007
3343
  const home = os2.homedir();
3008
- return platform === "claude-code" ? path25.join(home, ".claude", "commands", "harness") : path25.join(home, ".gemini", "commands", "harness");
3344
+ return platform === "claude-code" ? path27.join(home, ".claude", "commands", "harness") : path27.join(home, ".gemini", "commands", "harness");
3009
3345
  }
3010
- return platform === "claude-code" ? path25.join("agents", "commands", "claude-code", "harness") : path25.join("agents", "commands", "gemini-cli", "harness");
3346
+ return platform === "claude-code" ? path27.join("agents", "commands", "claude-code", "harness") : path27.join("agents", "commands", "gemini-cli", "harness");
3011
3347
  }
3012
3348
  function fileExtension(platform) {
3013
3349
  return platform === "claude-code" ? ".md" : ".toml";
3014
3350
  }
3015
3351
  async function confirmDeletion(files) {
3016
3352
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3017
- return new Promise((resolve14) => {
3353
+ return new Promise((resolve18) => {
3018
3354
  rl.question(`
3019
3355
  Remove ${files.length} orphaned command(s)? (y/N) `, (answer) => {
3020
3356
  rl.close();
3021
- resolve14(answer.toLowerCase() === "y");
3357
+ resolve18(answer.toLowerCase() === "y");
3022
3358
  });
3023
3359
  });
3024
3360
  }
3025
3361
  function generateSlashCommands(opts) {
3026
- const skillsDir = opts.skillsDir || resolveSkillsDir();
3027
- const specs = normalizeSkills(skillsDir, opts.platforms);
3362
+ const skillSources = [];
3363
+ if (opts.skillsDir) {
3364
+ skillSources.push({ dir: opts.skillsDir, source: "project" });
3365
+ } else {
3366
+ const projectDir = resolveProjectSkillsDir();
3367
+ if (projectDir) {
3368
+ skillSources.push({ dir: projectDir, source: "project" });
3369
+ }
3370
+ if (opts.includeGlobal || skillSources.length === 0) {
3371
+ const globalDir = resolveGlobalSkillsDir();
3372
+ if (!projectDir || path27.resolve(globalDir) !== path27.resolve(projectDir)) {
3373
+ skillSources.push({ dir: globalDir, source: "global" });
3374
+ }
3375
+ }
3376
+ }
3377
+ const specs = normalizeSkills(skillSources, opts.platforms);
3028
3378
  const results = [];
3029
3379
  for (const platform of opts.platforms) {
3030
3380
  const outputDir = resolveOutputDir(platform, opts);
@@ -3041,7 +3391,7 @@ function generateSlashCommands(opts) {
3041
3391
  executionContext: spec.prompt.executionContext.split("\n").map((line) => {
3042
3392
  if (line.startsWith("@")) {
3043
3393
  const relPath = line.slice(1);
3044
- return `@${path25.resolve(relPath)}`;
3394
+ return `@${path27.resolve(relPath)}`;
3045
3395
  }
3046
3396
  return line;
3047
3397
  }).join("\n")
@@ -3049,10 +3399,10 @@ function generateSlashCommands(opts) {
3049
3399
  } : spec;
3050
3400
  rendered.set(filename, renderClaudeCode(renderSpec));
3051
3401
  } else {
3052
- const mdPath = path25.join(skillsDir, spec.sourceDir, "SKILL.md");
3053
- const yamlPath = path25.join(skillsDir, spec.sourceDir, "skill.yaml");
3054
- const mdContent = fs17.existsSync(mdPath) ? fs17.readFileSync(mdPath, "utf-8") : "";
3055
- const yamlContent = fs17.existsSync(yamlPath) ? fs17.readFileSync(yamlPath, "utf-8") : "";
3402
+ const mdPath = path27.join(spec.skillsBaseDir, spec.sourceDir, "SKILL.md");
3403
+ const yamlPath = path27.join(spec.skillsBaseDir, spec.sourceDir, "skill.yaml");
3404
+ const mdContent = fs19.existsSync(mdPath) ? fs19.readFileSync(mdPath, "utf-8") : "";
3405
+ const yamlContent = fs19.existsSync(yamlPath) ? fs19.readFileSync(yamlPath, "utf-8") : "";
3056
3406
  rendered.set(filename, renderGemini(spec, mdContent, yamlContent));
3057
3407
  }
3058
3408
  }
@@ -3078,9 +3428,9 @@ async function handleOrphanDeletion(results, opts) {
3078
3428
  const shouldDelete = opts.yes || await confirmDeletion(result.removed);
3079
3429
  if (shouldDelete) {
3080
3430
  for (const filename of result.removed) {
3081
- const filePath = path25.join(result.outputDir, filename);
3082
- if (fs17.existsSync(filePath)) {
3083
- fs17.unlinkSync(filePath);
3431
+ const filePath = path27.join(result.outputDir, filename);
3432
+ if (fs19.existsSync(filePath)) {
3433
+ fs19.unlinkSync(filePath);
3084
3434
  }
3085
3435
  }
3086
3436
  }
@@ -3089,7 +3439,7 @@ async function handleOrphanDeletion(results, opts) {
3089
3439
  function createGenerateSlashCommandsCommand() {
3090
3440
  return new Command28("generate-slash-commands").description(
3091
3441
  "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) => {
3442
+ ).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
3443
  const globalOpts = cmd.optsWithGlobals();
3094
3444
  const platforms = opts.platforms.split(",").map((p) => p.trim());
3095
3445
  for (const p of platforms) {
@@ -3103,6 +3453,7 @@ function createGenerateSlashCommandsCommand() {
3103
3453
  const generateOpts = {
3104
3454
  platforms,
3105
3455
  global: opts.global,
3456
+ includeGlobal: opts.includeGlobal,
3106
3457
  output: opts.output,
3107
3458
  skillsDir: opts.skillsDir ?? "",
3108
3459
  dryRun: opts.dryRun,
@@ -3114,6 +3465,16 @@ function createGenerateSlashCommandsCommand() {
3114
3465
  console.log(JSON.stringify(results, null, 2));
3115
3466
  return;
3116
3467
  }
3468
+ const totalCommands = results.reduce(
3469
+ (sum, r) => sum + r.added.length + r.updated.length + r.unchanged.length,
3470
+ 0
3471
+ );
3472
+ if (totalCommands === 0) {
3473
+ console.log(
3474
+ "\nNo skills found. Use --include-global to include built-in skills, or create a skill with: harness create-skill"
3475
+ );
3476
+ return;
3477
+ }
3117
3478
  for (const result of results) {
3118
3479
  console.log(`
3119
3480
  ${result.platform} \u2192 ${result.outputDir}`);
@@ -3221,8 +3582,8 @@ function createCheckCommand() {
3221
3582
 
3222
3583
  // src/commands/ci/init.ts
3223
3584
  import { Command as Command30 } from "commander";
3224
- import * as fs18 from "fs";
3225
- import * as path26 from "path";
3585
+ import * as fs20 from "fs";
3586
+ import * as path28 from "path";
3226
3587
  import { Ok as Ok17, Err as Err14 } from "@harness-engineering/core";
3227
3588
  var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
3228
3589
  function buildSkipFlag(checks) {
@@ -3315,8 +3676,8 @@ function generateCIConfig(options) {
3315
3676
  });
3316
3677
  }
3317
3678
  function detectPlatform() {
3318
- if (fs18.existsSync(".github")) return "github";
3319
- if (fs18.existsSync(".gitlab-ci.yml")) return "gitlab";
3679
+ if (fs20.existsSync(".github")) return "github";
3680
+ if (fs20.existsSync(".gitlab-ci.yml")) return "gitlab";
3320
3681
  return null;
3321
3682
  }
3322
3683
  function createInitCommand2() {
@@ -3332,12 +3693,12 @@ function createInitCommand2() {
3332
3693
  process.exit(result.error.exitCode);
3333
3694
  }
3334
3695
  const { filename, content } = result.value;
3335
- const targetPath = path26.resolve(filename);
3336
- const dir = path26.dirname(targetPath);
3337
- fs18.mkdirSync(dir, { recursive: true });
3338
- fs18.writeFileSync(targetPath, content);
3696
+ const targetPath = path28.resolve(filename);
3697
+ const dir = path28.dirname(targetPath);
3698
+ fs20.mkdirSync(dir, { recursive: true });
3699
+ fs20.writeFileSync(targetPath, content);
3339
3700
  if (platform === "generic") {
3340
- fs18.chmodSync(targetPath, "755");
3701
+ fs20.chmodSync(targetPath, "755");
3341
3702
  }
3342
3703
  if (globalOpts.json) {
3343
3704
  console.log(JSON.stringify({ file: filename, platform }));
@@ -3415,10 +3776,10 @@ function prompt(question) {
3415
3776
  input: process.stdin,
3416
3777
  output: process.stdout
3417
3778
  });
3418
- return new Promise((resolve14) => {
3779
+ return new Promise((resolve18) => {
3419
3780
  rl.question(question, (answer) => {
3420
3781
  rl.close();
3421
- resolve14(answer.trim().toLowerCase());
3782
+ resolve18(answer.trim().toLowerCase());
3422
3783
  });
3423
3784
  });
3424
3785
  }
@@ -3491,9 +3852,654 @@ function createUpdateCommand() {
3491
3852
  });
3492
3853
  }
3493
3854
 
3855
+ // src/commands/generate-agent-definitions.ts
3856
+ import { Command as Command33 } from "commander";
3857
+ import * as fs21 from "fs";
3858
+ import * as path29 from "path";
3859
+ import * as os3 from "os";
3860
+
3861
+ // src/agent-definitions/generator.ts
3862
+ var AGENT_DESCRIPTIONS = {
3863
+ "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.",
3864
+ "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.",
3865
+ "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.",
3866
+ "architecture-enforcer": "Validate architectural constraints and dependency rules. Use when checking layer boundaries, detecting circular dependencies, or verifying import direction compliance.",
3867
+ "documentation-maintainer": "Keep documentation in sync with source code. Use when detecting documentation drift, validating doc coverage, or aligning docs with code changes.",
3868
+ "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."
3869
+ };
3870
+ var DEFAULT_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep"];
3871
+ function generateAgentDefinition(persona, skillContents) {
3872
+ const kebabName = toKebabCase(persona.name);
3873
+ const name = `harness-${kebabName}`;
3874
+ const description = AGENT_DESCRIPTIONS[kebabName] ?? persona.description;
3875
+ const methodologyParts = [];
3876
+ for (const skillName of persona.skills) {
3877
+ const content = skillContents.get(skillName);
3878
+ if (content) {
3879
+ methodologyParts.push(content);
3880
+ }
3881
+ }
3882
+ return {
3883
+ name,
3884
+ description,
3885
+ tools: DEFAULT_TOOLS,
3886
+ role: persona.role,
3887
+ skills: persona.skills,
3888
+ steps: persona.steps,
3889
+ methodology: methodologyParts.join("\n\n---\n\n")
3890
+ };
3891
+ }
3892
+
3893
+ // src/agent-definitions/render-claude-code.ts
3894
+ function formatStep(step, index) {
3895
+ if ("command" in step && step.command) {
3896
+ const cmd = step.command;
3897
+ const when = step.when ?? "always";
3898
+ return `${index + 1}. Run \`harness ${cmd}\` (${when})`;
3899
+ }
3900
+ if ("skill" in step && step.skill) {
3901
+ const skill = step.skill;
3902
+ const when = step.when ?? "always";
3903
+ return `${index + 1}. Execute ${skill} skill (${when})`;
3904
+ }
3905
+ return `${index + 1}. Unknown step`;
3906
+ }
3907
+ function renderClaudeCodeAgent(def) {
3908
+ const lines = ["---"];
3909
+ lines.push(`name: ${def.name}`);
3910
+ lines.push(`description: >`);
3911
+ lines.push(` ${def.description}`);
3912
+ lines.push(`tools: ${def.tools.join(", ")}`);
3913
+ lines.push("---");
3914
+ lines.push("");
3915
+ lines.push(GENERATED_HEADER_AGENT);
3916
+ lines.push("");
3917
+ lines.push("## Role");
3918
+ lines.push("");
3919
+ lines.push(def.role);
3920
+ lines.push("");
3921
+ lines.push("## Skills");
3922
+ lines.push("");
3923
+ for (const skill of def.skills) {
3924
+ lines.push(`- ${skill}`);
3925
+ }
3926
+ lines.push("");
3927
+ lines.push("## Steps");
3928
+ lines.push("");
3929
+ def.steps.forEach((step, i) => {
3930
+ lines.push(formatStep(step, i));
3931
+ });
3932
+ lines.push("");
3933
+ if (def.methodology) {
3934
+ lines.push("## Methodology");
3935
+ lines.push("");
3936
+ lines.push(def.methodology);
3937
+ lines.push("");
3938
+ }
3939
+ return lines.join("\n");
3940
+ }
3941
+
3942
+ // src/agent-definitions/render-gemini-cli.ts
3943
+ function formatStep2(step, index) {
3944
+ if ("command" in step && step.command) {
3945
+ const cmd = step.command;
3946
+ const when = step.when ?? "always";
3947
+ return `${index + 1}. Run \`harness ${cmd}\` (${when})`;
3948
+ }
3949
+ if ("skill" in step && step.skill) {
3950
+ const skill = step.skill;
3951
+ const when = step.when ?? "always";
3952
+ return `${index + 1}. Execute ${skill} skill (${when})`;
3953
+ }
3954
+ return `${index + 1}. Unknown step`;
3955
+ }
3956
+ function renderGeminiAgent(def) {
3957
+ const lines = ["---"];
3958
+ lines.push(`name: ${def.name}`);
3959
+ lines.push(`description: >`);
3960
+ lines.push(` ${def.description}`);
3961
+ lines.push(`tools: ${def.tools.join(", ")}`);
3962
+ lines.push("---");
3963
+ lines.push("");
3964
+ lines.push(GENERATED_HEADER_AGENT);
3965
+ lines.push("");
3966
+ lines.push("## Role");
3967
+ lines.push("");
3968
+ lines.push(def.role);
3969
+ lines.push("");
3970
+ lines.push("## Skills");
3971
+ lines.push("");
3972
+ for (const skill of def.skills) {
3973
+ lines.push(`- ${skill}`);
3974
+ }
3975
+ lines.push("");
3976
+ lines.push("## Steps");
3977
+ lines.push("");
3978
+ def.steps.forEach((step, i) => {
3979
+ lines.push(formatStep2(step, i));
3980
+ });
3981
+ lines.push("");
3982
+ if (def.methodology) {
3983
+ lines.push("## Methodology");
3984
+ lines.push("");
3985
+ lines.push(def.methodology);
3986
+ lines.push("");
3987
+ }
3988
+ return lines.join("\n");
3989
+ }
3990
+
3991
+ // src/commands/generate-agent-definitions.ts
3992
+ function resolveOutputDir2(platform, opts) {
3993
+ if (opts.output) {
3994
+ return platform === "claude-code" ? path29.join(opts.output, "claude-code") : path29.join(opts.output, "gemini-cli");
3995
+ }
3996
+ if (opts.global) {
3997
+ const home = os3.homedir();
3998
+ return platform === "claude-code" ? path29.join(home, ".claude", "agents") : path29.join(home, ".gemini", "agents");
3999
+ }
4000
+ return platform === "claude-code" ? path29.join("agents", "agents", "claude-code") : path29.join("agents", "agents", "gemini-cli");
4001
+ }
4002
+ function loadSkillContent(skillName) {
4003
+ const skillsDir = resolveSkillsDir();
4004
+ const skillMdPath = path29.join(skillsDir, skillName, "SKILL.md");
4005
+ if (!fs21.existsSync(skillMdPath)) return null;
4006
+ return fs21.readFileSync(skillMdPath, "utf-8");
4007
+ }
4008
+ function getRenderer(platform) {
4009
+ return platform === "claude-code" ? renderClaudeCodeAgent : renderGeminiAgent;
4010
+ }
4011
+ function generateAgentDefinitions(opts) {
4012
+ const personasDir = resolvePersonasDir();
4013
+ const personaList = listPersonas(personasDir);
4014
+ if (!personaList.ok) return [];
4015
+ const personas = personaList.value.map((meta) => loadPersona(meta.filePath)).filter((r) => r.ok).map((r) => r.value);
4016
+ const allSkillNames = new Set(personas.flatMap((p) => p.skills));
4017
+ const skillContents = /* @__PURE__ */ new Map();
4018
+ for (const skillName of allSkillNames) {
4019
+ const content = loadSkillContent(skillName);
4020
+ if (content) {
4021
+ skillContents.set(skillName, content);
4022
+ }
4023
+ }
4024
+ const definitions = personas.map((p) => generateAgentDefinition(p, skillContents));
4025
+ const results = [];
4026
+ for (const platform of opts.platforms) {
4027
+ const outputDir = resolveOutputDir2(platform, opts);
4028
+ const renderer = getRenderer(platform);
4029
+ const rendered = /* @__PURE__ */ new Map();
4030
+ for (const def of definitions) {
4031
+ const filename = `${def.name}.md`;
4032
+ rendered.set(filename, renderer(def));
4033
+ }
4034
+ const plan = computeSyncPlan(outputDir, rendered);
4035
+ if (!opts.dryRun) {
4036
+ applySyncPlan(outputDir, rendered, plan, false);
4037
+ }
4038
+ results.push({
4039
+ platform,
4040
+ added: plan.added,
4041
+ updated: plan.updated,
4042
+ removed: plan.removed,
4043
+ unchanged: plan.unchanged,
4044
+ outputDir
4045
+ });
4046
+ }
4047
+ return results;
4048
+ }
4049
+ function createGenerateAgentDefinitionsCommand() {
4050
+ return new Command33("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) => {
4051
+ const globalOpts = cmd.optsWithGlobals();
4052
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
4053
+ for (const p of platforms) {
4054
+ if (!VALID_PLATFORMS.includes(p)) {
4055
+ throw new CLIError(
4056
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
4057
+ ExitCode.VALIDATION_FAILED
4058
+ );
4059
+ }
4060
+ }
4061
+ try {
4062
+ const results = generateAgentDefinitions({
4063
+ platforms,
4064
+ global: opts.global,
4065
+ output: opts.output,
4066
+ dryRun: opts.dryRun
4067
+ });
4068
+ if (globalOpts.json) {
4069
+ console.log(JSON.stringify(results, null, 2));
4070
+ return;
4071
+ }
4072
+ for (const result of results) {
4073
+ console.log(`
4074
+ ${result.platform} \u2192 ${result.outputDir}`);
4075
+ if (result.added.length > 0) {
4076
+ console.log(` + ${result.added.length} new: ${result.added.join(", ")}`);
4077
+ }
4078
+ if (result.updated.length > 0) {
4079
+ console.log(` ~ ${result.updated.length} updated: ${result.updated.join(", ")}`);
4080
+ }
4081
+ if (result.removed.length > 0) {
4082
+ console.log(` - ${result.removed.length} removed: ${result.removed.join(", ")}`);
4083
+ }
4084
+ if (result.unchanged.length > 0) {
4085
+ console.log(` = ${result.unchanged.length} unchanged`);
4086
+ }
4087
+ if (opts.dryRun) {
4088
+ console.log(" (dry run \u2014 no files written)");
4089
+ }
4090
+ }
4091
+ } catch (error) {
4092
+ handleError(error);
4093
+ }
4094
+ });
4095
+ }
4096
+
4097
+ // src/commands/generate.ts
4098
+ import { Command as Command34 } from "commander";
4099
+ function createGenerateCommand3() {
4100
+ return new Command34("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) => {
4101
+ const globalOpts = cmd.optsWithGlobals();
4102
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
4103
+ for (const p of platforms) {
4104
+ if (!VALID_PLATFORMS.includes(p)) {
4105
+ throw new CLIError(
4106
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
4107
+ ExitCode.VALIDATION_FAILED
4108
+ );
4109
+ }
4110
+ }
4111
+ try {
4112
+ console.log("Generating slash commands...");
4113
+ const slashResults = generateSlashCommands({
4114
+ platforms,
4115
+ global: opts.global,
4116
+ includeGlobal: opts.includeGlobal,
4117
+ output: opts.output,
4118
+ skillsDir: "",
4119
+ dryRun: opts.dryRun,
4120
+ yes: opts.yes
4121
+ });
4122
+ for (const result of slashResults) {
4123
+ const total = result.added.length + result.updated.length + result.unchanged.length;
4124
+ console.log(
4125
+ ` ${result.platform}: ${total} commands (${result.added.length} new, ${result.updated.length} updated)`
4126
+ );
4127
+ }
4128
+ await handleOrphanDeletion(slashResults, { yes: opts.yes, dryRun: opts.dryRun });
4129
+ console.log("\nGenerating agent definitions...");
4130
+ const agentResults = generateAgentDefinitions({
4131
+ platforms,
4132
+ global: opts.global,
4133
+ output: opts.output,
4134
+ dryRun: opts.dryRun
4135
+ });
4136
+ for (const result of agentResults) {
4137
+ const total = result.added.length + result.updated.length + result.unchanged.length;
4138
+ console.log(
4139
+ ` ${result.platform}: ${total} agents (${result.added.length} new, ${result.updated.length} updated)`
4140
+ );
4141
+ }
4142
+ if (opts.dryRun) {
4143
+ console.log("\n(dry run \u2014 no files written)");
4144
+ } else {
4145
+ console.log("\nDone.");
4146
+ }
4147
+ if (globalOpts.json) {
4148
+ console.log(
4149
+ JSON.stringify({ slashCommands: slashResults, agentDefinitions: agentResults }, null, 2)
4150
+ );
4151
+ }
4152
+ } catch (error) {
4153
+ handleError(error);
4154
+ }
4155
+ });
4156
+ }
4157
+
4158
+ // src/commands/graph/scan.ts
4159
+ import { Command as Command35 } from "commander";
4160
+ import * as path30 from "path";
4161
+ async function runScan(projectPath) {
4162
+ const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("@harness-engineering/graph");
4163
+ const store = new GraphStore();
4164
+ const start = Date.now();
4165
+ await new CodeIngestor(store).ingest(projectPath);
4166
+ new TopologicalLinker(store).link();
4167
+ const knowledgeIngestor = new KnowledgeIngestor(store);
4168
+ await knowledgeIngestor.ingestAll(projectPath);
4169
+ try {
4170
+ await new GitIngestor(store).ingest(projectPath);
4171
+ } catch {
4172
+ }
4173
+ const graphDir = path30.join(projectPath, ".harness", "graph");
4174
+ await store.save(graphDir);
4175
+ return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
4176
+ }
4177
+ function createScanCommand() {
4178
+ return new Command35("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
4179
+ const projectPath = path30.resolve(inputPath);
4180
+ const globalOpts = cmd.optsWithGlobals();
4181
+ try {
4182
+ const result = await runScan(projectPath);
4183
+ if (globalOpts.json) {
4184
+ console.log(JSON.stringify(result));
4185
+ } else {
4186
+ console.log(
4187
+ `Graph built: ${result.nodeCount} nodes, ${result.edgeCount} edges (${result.durationMs}ms)`
4188
+ );
4189
+ }
4190
+ } catch (err) {
4191
+ console.error("Scan failed:", err instanceof Error ? err.message : err);
4192
+ process.exit(2);
4193
+ }
4194
+ });
4195
+ }
4196
+
4197
+ // src/commands/graph/ingest.ts
4198
+ import { Command as Command36 } from "commander";
4199
+ import * as path31 from "path";
4200
+ async function loadConnectorConfig(projectPath, source) {
4201
+ try {
4202
+ const fs22 = await import("fs/promises");
4203
+ const configPath = path31.join(projectPath, "harness.config.json");
4204
+ const config = JSON.parse(await fs22.readFile(configPath, "utf-8"));
4205
+ const connector = config.graph?.connectors?.find(
4206
+ (c) => c.source === source
4207
+ );
4208
+ return connector?.config ?? {};
4209
+ } catch {
4210
+ return {};
4211
+ }
4212
+ }
4213
+ function mergeResults(...results) {
4214
+ return results.reduce(
4215
+ (acc, r) => ({
4216
+ nodesAdded: acc.nodesAdded + r.nodesAdded,
4217
+ nodesUpdated: acc.nodesUpdated + r.nodesUpdated,
4218
+ edgesAdded: acc.edgesAdded + r.edgesAdded,
4219
+ edgesUpdated: acc.edgesUpdated + r.edgesUpdated,
4220
+ errors: [...acc.errors, ...r.errors],
4221
+ durationMs: acc.durationMs + r.durationMs
4222
+ }),
4223
+ {
4224
+ nodesAdded: 0,
4225
+ nodesUpdated: 0,
4226
+ edgesAdded: 0,
4227
+ edgesUpdated: 0,
4228
+ errors: [],
4229
+ durationMs: 0
4230
+ }
4231
+ );
4232
+ }
4233
+ async function runIngest(projectPath, source, opts) {
4234
+ const {
4235
+ GraphStore,
4236
+ CodeIngestor,
4237
+ TopologicalLinker,
4238
+ KnowledgeIngestor,
4239
+ GitIngestor,
4240
+ SyncManager,
4241
+ JiraConnector,
4242
+ SlackConnector
4243
+ } = await import("@harness-engineering/graph");
4244
+ const graphDir = path31.join(projectPath, ".harness", "graph");
4245
+ const store = new GraphStore();
4246
+ await store.load(graphDir);
4247
+ if (opts?.all) {
4248
+ const startMs = Date.now();
4249
+ const codeResult = await new CodeIngestor(store).ingest(projectPath);
4250
+ new TopologicalLinker(store).link();
4251
+ const knowledgeResult = await new KnowledgeIngestor(store).ingestAll(projectPath);
4252
+ const gitResult = await new GitIngestor(store).ingest(projectPath);
4253
+ const syncManager = new SyncManager(store, graphDir);
4254
+ const connectorMap = {
4255
+ jira: () => new JiraConnector(),
4256
+ slack: () => new SlackConnector()
4257
+ };
4258
+ for (const [name, factory] of Object.entries(connectorMap)) {
4259
+ const config = await loadConnectorConfig(projectPath, name);
4260
+ syncManager.registerConnector(factory(), config);
4261
+ }
4262
+ const connectorResult = await syncManager.syncAll();
4263
+ await store.save(graphDir);
4264
+ const merged = mergeResults(codeResult, knowledgeResult, gitResult, connectorResult);
4265
+ return { ...merged, durationMs: Date.now() - startMs };
4266
+ }
4267
+ let result;
4268
+ switch (source) {
4269
+ case "code":
4270
+ result = await new CodeIngestor(store).ingest(projectPath);
4271
+ new TopologicalLinker(store).link();
4272
+ break;
4273
+ case "knowledge":
4274
+ result = await new KnowledgeIngestor(store).ingestAll(projectPath);
4275
+ break;
4276
+ case "git":
4277
+ result = await new GitIngestor(store).ingest(projectPath);
4278
+ break;
4279
+ default: {
4280
+ const knownConnectors = ["jira", "slack"];
4281
+ if (!knownConnectors.includes(source)) {
4282
+ throw new Error(`Unknown source: ${source}. Available: code, knowledge, git, jira, slack`);
4283
+ }
4284
+ if (!SyncManager) {
4285
+ throw new Error(
4286
+ `Connector support not available. Ensure @harness-engineering/graph is built with connector support.`
4287
+ );
4288
+ }
4289
+ const syncManager = new SyncManager(store, graphDir);
4290
+ const extConnectorMap = {
4291
+ jira: () => new JiraConnector(),
4292
+ slack: () => new SlackConnector()
4293
+ };
4294
+ const factory = extConnectorMap[source];
4295
+ const config = await loadConnectorConfig(projectPath, source);
4296
+ syncManager.registerConnector(factory(), config);
4297
+ result = await syncManager.sync(source);
4298
+ break;
4299
+ }
4300
+ }
4301
+ await store.save(graphDir);
4302
+ return result;
4303
+ }
4304
+ function createIngestCommand() {
4305
+ return new Command36("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) => {
4306
+ if (!opts.source && !opts.all) {
4307
+ console.error("Error: --source or --all is required");
4308
+ process.exit(1);
4309
+ }
4310
+ const globalOpts = cmd.optsWithGlobals();
4311
+ const projectPath = path31.resolve(globalOpts.config ? path31.dirname(globalOpts.config) : ".");
4312
+ try {
4313
+ const result = await runIngest(projectPath, opts.source ?? "", {
4314
+ full: opts.full,
4315
+ all: opts.all
4316
+ });
4317
+ if (globalOpts.json) {
4318
+ console.log(JSON.stringify(result));
4319
+ } else {
4320
+ const label = opts.all ? "all" : opts.source;
4321
+ console.log(
4322
+ `Ingested (${label}): +${result.nodesAdded} nodes, +${result.edgesAdded} edges (${result.durationMs}ms)`
4323
+ );
4324
+ }
4325
+ } catch (err) {
4326
+ console.error("Ingest failed:", err instanceof Error ? err.message : err);
4327
+ process.exit(2);
4328
+ }
4329
+ });
4330
+ }
4331
+
4332
+ // src/commands/graph/query.ts
4333
+ import { Command as Command37 } from "commander";
4334
+ import * as path32 from "path";
4335
+ async function runQuery(projectPath, rootNodeId, opts) {
4336
+ const { GraphStore, ContextQL } = await import("@harness-engineering/graph");
4337
+ const store = new GraphStore();
4338
+ const graphDir = path32.join(projectPath, ".harness", "graph");
4339
+ const loaded = await store.load(graphDir);
4340
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
4341
+ const params = {
4342
+ rootNodeIds: [rootNodeId],
4343
+ maxDepth: opts.depth ?? 3,
4344
+ bidirectional: opts.bidirectional ?? false,
4345
+ ...opts.types ? { includeTypes: opts.types.split(",") } : {},
4346
+ ...opts.edges ? { includeEdges: opts.edges.split(",") } : {}
4347
+ };
4348
+ const cql = new ContextQL(store);
4349
+ return cql.execute(params);
4350
+ }
4351
+ function createQueryCommand() {
4352
+ return new Command37("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) => {
4353
+ const globalOpts = cmd.optsWithGlobals();
4354
+ const projectPath = path32.resolve(globalOpts.config ? path32.dirname(globalOpts.config) : ".");
4355
+ try {
4356
+ const result = await runQuery(projectPath, rootNodeId, {
4357
+ depth: parseInt(opts.depth),
4358
+ types: opts.types,
4359
+ edges: opts.edges,
4360
+ bidirectional: opts.bidirectional
4361
+ });
4362
+ if (globalOpts.json) {
4363
+ console.log(JSON.stringify(result, null, 2));
4364
+ } else {
4365
+ console.log(
4366
+ `Found ${result.nodes.length} nodes, ${result.edges.length} edges (depth ${result.stats.depthReached}, pruned ${result.stats.pruned})`
4367
+ );
4368
+ for (const node of result.nodes) {
4369
+ console.log(` ${node.type.padEnd(12)} ${node.id}`);
4370
+ }
4371
+ }
4372
+ } catch (err) {
4373
+ console.error("Query failed:", err instanceof Error ? err.message : err);
4374
+ process.exit(2);
4375
+ }
4376
+ });
4377
+ }
4378
+
4379
+ // src/commands/graph/index.ts
4380
+ import { Command as Command38 } from "commander";
4381
+
4382
+ // src/commands/graph/status.ts
4383
+ import * as path33 from "path";
4384
+ async function runGraphStatus(projectPath) {
4385
+ const { GraphStore } = await import("@harness-engineering/graph");
4386
+ const graphDir = path33.join(projectPath, ".harness", "graph");
4387
+ const store = new GraphStore();
4388
+ const loaded = await store.load(graphDir);
4389
+ if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
4390
+ const fs22 = await import("fs/promises");
4391
+ const metaPath = path33.join(graphDir, "metadata.json");
4392
+ let lastScan = "unknown";
4393
+ try {
4394
+ const meta = JSON.parse(await fs22.readFile(metaPath, "utf-8"));
4395
+ lastScan = meta.lastScanTimestamp;
4396
+ } catch {
4397
+ }
4398
+ const allNodes = store.findNodes({});
4399
+ const nodesByType = {};
4400
+ for (const node of allNodes) {
4401
+ nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
4402
+ }
4403
+ let connectorSyncStatus = {};
4404
+ try {
4405
+ const syncMetaPath = path33.join(graphDir, "sync-metadata.json");
4406
+ const syncMeta = JSON.parse(await fs22.readFile(syncMetaPath, "utf-8"));
4407
+ for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
4408
+ connectorSyncStatus[name] = data.lastSyncTimestamp;
4409
+ }
4410
+ } catch {
4411
+ }
4412
+ return {
4413
+ status: "ok",
4414
+ nodeCount: store.nodeCount,
4415
+ edgeCount: store.edgeCount,
4416
+ nodesByType,
4417
+ lastScanTimestamp: lastScan,
4418
+ ...Object.keys(connectorSyncStatus).length > 0 ? { connectorSyncStatus } : {}
4419
+ };
4420
+ }
4421
+
4422
+ // src/commands/graph/export.ts
4423
+ import * as path34 from "path";
4424
+ async function runGraphExport(projectPath, format) {
4425
+ const { GraphStore } = await import("@harness-engineering/graph");
4426
+ const graphDir = path34.join(projectPath, ".harness", "graph");
4427
+ const store = new GraphStore();
4428
+ const loaded = await store.load(graphDir);
4429
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
4430
+ if (format === "json") {
4431
+ const nodes = store.findNodes({});
4432
+ const edges = store.getEdges({});
4433
+ return JSON.stringify({ nodes, edges }, null, 2);
4434
+ }
4435
+ if (format === "mermaid") {
4436
+ const nodes = store.findNodes({});
4437
+ const edges = store.getEdges({});
4438
+ const lines = ["graph TD"];
4439
+ for (const node of nodes.slice(0, 200)) {
4440
+ const safeId = node.id.replace(/[^a-zA-Z0-9]/g, "_");
4441
+ const safeName = node.name.replace(/"/g, "#quot;");
4442
+ lines.push(` ${safeId}["${safeName}"]`);
4443
+ }
4444
+ for (const edge of edges.slice(0, 500)) {
4445
+ const safeFrom = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
4446
+ const safeTo = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
4447
+ lines.push(` ${safeFrom} -->|${edge.type}| ${safeTo}`);
4448
+ }
4449
+ return lines.join("\n");
4450
+ }
4451
+ throw new Error(`Unknown format: ${format}. Available: json, mermaid`);
4452
+ }
4453
+
4454
+ // src/commands/graph/index.ts
4455
+ import * as path35 from "path";
4456
+ function createGraphCommand() {
4457
+ const graph = new Command38("graph").description("Knowledge graph management");
4458
+ graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
4459
+ try {
4460
+ const globalOpts = cmd.optsWithGlobals();
4461
+ const projectPath = path35.resolve(globalOpts.config ? path35.dirname(globalOpts.config) : ".");
4462
+ const result = await runGraphStatus(projectPath);
4463
+ if (globalOpts.json) {
4464
+ console.log(JSON.stringify(result, null, 2));
4465
+ } else if (result.status === "no_graph") {
4466
+ console.log(result.message);
4467
+ } else {
4468
+ console.log(`Graph: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
4469
+ console.log(`Last scan: ${result.lastScanTimestamp}`);
4470
+ console.log("Nodes by type:");
4471
+ for (const [type, count] of Object.entries(result.nodesByType)) {
4472
+ console.log(` ${type}: ${count}`);
4473
+ }
4474
+ if (result.connectorSyncStatus) {
4475
+ console.log("Connector sync status:");
4476
+ for (const [name, timestamp] of Object.entries(result.connectorSyncStatus)) {
4477
+ console.log(` ${name}: last synced ${timestamp}`);
4478
+ }
4479
+ }
4480
+ }
4481
+ } catch (err) {
4482
+ console.error("Status failed:", err instanceof Error ? err.message : err);
4483
+ process.exit(2);
4484
+ }
4485
+ });
4486
+ graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
4487
+ const globalOpts = cmd.optsWithGlobals();
4488
+ const projectPath = path35.resolve(globalOpts.config ? path35.dirname(globalOpts.config) : ".");
4489
+ try {
4490
+ const output = await runGraphExport(projectPath, opts.format);
4491
+ console.log(output);
4492
+ } catch (err) {
4493
+ console.error("Export failed:", err instanceof Error ? err.message : err);
4494
+ process.exit(2);
4495
+ }
4496
+ });
4497
+ return graph;
4498
+ }
4499
+
3494
4500
  // src/index.ts
3495
4501
  function createProgram() {
3496
- const program = new Command33();
4502
+ const program = new Command39();
3497
4503
  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
4504
  program.addCommand(createValidateCommand());
3499
4505
  program.addCommand(createCheckDepsCommand());
@@ -3511,8 +4517,14 @@ function createProgram() {
3511
4517
  program.addCommand(createCreateSkillCommand());
3512
4518
  program.addCommand(createSetupMcpCommand());
3513
4519
  program.addCommand(createGenerateSlashCommandsCommand());
4520
+ program.addCommand(createGenerateAgentDefinitionsCommand());
4521
+ program.addCommand(createGenerateCommand3());
3514
4522
  program.addCommand(createCICommand());
3515
4523
  program.addCommand(createUpdateCommand());
4524
+ program.addCommand(createScanCommand());
4525
+ program.addCommand(createIngestCommand());
4526
+ program.addCommand(createQueryCommand());
4527
+ program.addCommand(createGraphCommand());
3516
4528
  return program;
3517
4529
  }
3518
4530
 
@@ -3525,12 +4537,25 @@ export {
3525
4537
  TemplateEngine,
3526
4538
  loadPersona,
3527
4539
  listPersonas,
4540
+ detectTrigger,
3528
4541
  runPersona,
4542
+ executeSkill,
4543
+ ALLOWED_PERSONA_COMMANDS,
3529
4544
  generateRuntime,
3530
4545
  generateAgentsMd,
3531
4546
  generateCIWorkflow,
3532
4547
  buildPreamble,
3533
4548
  runCheckPhaseGate,
3534
4549
  generateSlashCommands,
4550
+ AGENT_DESCRIPTIONS,
4551
+ generateAgentDefinition,
4552
+ renderClaudeCodeAgent,
4553
+ renderGeminiAgent,
4554
+ generateAgentDefinitions,
4555
+ runScan,
4556
+ runIngest,
4557
+ runQuery,
4558
+ runGraphStatus,
4559
+ runGraphExport,
3535
4560
  createProgram
3536
4561
  };