@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.
- package/dist/agents/personas/architecture-enforcer.yaml +1 -0
- package/dist/agents/personas/code-reviewer.yaml +41 -0
- package/dist/agents/personas/codebase-health-analyst.yaml +27 -0
- package/dist/agents/personas/documentation-maintainer.yaml +2 -0
- package/dist/agents/personas/entropy-cleaner.yaml +3 -0
- package/dist/agents/personas/graph-maintainer.yaml +27 -0
- package/dist/agents/personas/parallel-coordinator.yaml +29 -0
- package/dist/agents/personas/task-executor.yaml +41 -0
- package/dist/agents/skills/README.md +8 -0
- package/dist/agents/skills/claude-code/add-harness-component/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/align-documentation/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/detect-doc-drift/SKILL.md +8 -0
- package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-debugging/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-dependency-health/SKILL.md +150 -0
- package/dist/agents/skills/claude-code/harness-dependency-health/skill.yaml +41 -0
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-hotspot-detector/SKILL.md +135 -0
- package/dist/agents/skills/claude-code/harness-hotspot-detector/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/harness-impact-analysis/SKILL.md +139 -0
- package/dist/agents/skills/claude-code/harness-impact-analysis/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/harness-knowledge-mapper/SKILL.md +154 -0
- package/dist/agents/skills/claude-code/harness-knowledge-mapper/skill.yaml +49 -0
- package/dist/agents/skills/claude-code/harness-onboarding/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +9 -0
- package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +6 -0
- package/dist/agents/skills/claude-code/harness-refactoring/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-tdd/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/harness-test-advisor/SKILL.md +131 -0
- package/dist/agents/skills/claude-code/harness-test-advisor/skill.yaml +44 -0
- package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +10 -0
- package/dist/agents/skills/claude-code/validate-context-engineering/SKILL.md +9 -0
- package/dist/agents/skills/gemini-cli/harness-dependency-health/SKILL.md +150 -0
- package/dist/agents/skills/gemini-cli/harness-dependency-health/skill.yaml +41 -0
- package/dist/agents/skills/gemini-cli/harness-hotspot-detector/SKILL.md +135 -0
- package/dist/agents/skills/gemini-cli/harness-hotspot-detector/skill.yaml +44 -0
- package/dist/agents/skills/gemini-cli/harness-impact-analysis/SKILL.md +139 -0
- package/dist/agents/skills/gemini-cli/harness-impact-analysis/skill.yaml +44 -0
- package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/SKILL.md +154 -0
- package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/skill.yaml +49 -0
- package/dist/agents/skills/gemini-cli/harness-test-advisor/SKILL.md +131 -0
- package/dist/agents/skills/gemini-cli/harness-test-advisor/skill.yaml +44 -0
- package/dist/agents/skills/tests/platform-parity.test.ts +131 -0
- package/dist/agents/skills/tests/schema.ts +2 -0
- package/dist/bin/harness.js +2 -2
- package/dist/{chunk-EFZOLZFB.js → chunk-ACMDUQJG.js} +4 -2
- package/dist/{chunk-C3J2HW4Y.js → chunk-VS4OTOKZ.js} +1354 -329
- package/dist/{create-skill-4GKJZB5R.js → create-skill-NZDLMMR6.js} +1 -1
- package/dist/index.d.ts +265 -143
- package/dist/index.js +30 -4
- package/package.json +3 -2
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
createCreateSkillCommand,
|
|
6
6
|
handleError,
|
|
7
7
|
logger
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ACMDUQJG.js";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
|
-
import { Command as
|
|
11
|
+
import { Command as 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
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1483
|
+
steps: [],
|
|
1370
1484
|
totalDurationMs: 0
|
|
1371
1485
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
|
1503
|
+
const stepStart = Date.now();
|
|
1386
1504
|
const remainingTime = timeout - (Date.now() - startTime);
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
() =>
|
|
1392
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
durationMs
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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 =
|
|
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
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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,
|
|
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
|
|
1507
|
-
const icon =
|
|
1508
|
-
|
|
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
|
|
1626
|
-
import * as
|
|
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 =
|
|
1672
|
-
if (!
|
|
1673
|
-
|
|
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 =
|
|
1677
|
-
if (!
|
|
1678
|
-
|
|
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 =
|
|
1685
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
1694
|
-
if (!
|
|
1695
|
-
|
|
2008
|
+
const docsDir = path15.join(cwd, "docs");
|
|
2009
|
+
if (!fs9.existsSync(docsDir)) {
|
|
2010
|
+
fs9.mkdirSync(docsDir, { recursive: true });
|
|
1696
2011
|
}
|
|
1697
|
-
const docPath =
|
|
1698
|
-
if (
|
|
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
|
-
|
|
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-
|
|
2021
|
+
const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-NZDLMMR6.js");
|
|
1707
2022
|
generateSkillFiles2({
|
|
1708
2023
|
name,
|
|
1709
2024
|
description: `${name} skill`,
|
|
1710
|
-
outputDir:
|
|
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 =
|
|
1718
|
-
if (!
|
|
1719
|
-
|
|
2032
|
+
const personasDir = path15.join(cwd, "agents", "personas");
|
|
2033
|
+
if (!fs9.existsSync(personasDir)) {
|
|
2034
|
+
fs9.mkdirSync(personasDir, { recursive: true });
|
|
1720
2035
|
}
|
|
1721
|
-
const personaPath =
|
|
1722
|
-
if (
|
|
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
|
-
|
|
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
|
|
1914
|
-
import * as
|
|
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
|
-
|
|
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.
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2064
|
-
|
|
2065
|
-
|
|
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 =
|
|
2073
|
-
|
|
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 =
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
|
2108
|
-
import * as
|
|
2109
|
-
import { parse as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
2123
|
-
if (!
|
|
2442
|
+
const yamlPath = path17.join(skillsDir, name, "skill.yaml");
|
|
2443
|
+
if (!fs11.existsSync(yamlPath)) continue;
|
|
2124
2444
|
try {
|
|
2125
|
-
const raw =
|
|
2126
|
-
const parsed =
|
|
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
|
|
2157
|
-
import * as
|
|
2158
|
-
import { parse as
|
|
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 =
|
|
2248
|
-
if (!
|
|
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 =
|
|
2573
|
+
const yamlPath = path18.join(skillDir, "skill.yaml");
|
|
2254
2574
|
let metadata = null;
|
|
2255
|
-
if (
|
|
2575
|
+
if (fs12.existsSync(yamlPath)) {
|
|
2256
2576
|
try {
|
|
2257
|
-
const raw =
|
|
2258
|
-
const parsed =
|
|
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 ?
|
|
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 ?
|
|
2276
|
-
const principlesPath =
|
|
2277
|
-
if (
|
|
2278
|
-
principles =
|
|
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 =
|
|
2294
|
-
if (
|
|
2295
|
-
const stat =
|
|
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 =
|
|
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 =
|
|
2619
|
+
priorState = fs12.readFileSync(path18.join(fullPath, files[0].name), "utf-8");
|
|
2300
2620
|
}
|
|
2301
2621
|
} else {
|
|
2302
|
-
priorState =
|
|
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 =
|
|
2322
|
-
if (!
|
|
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 =
|
|
2647
|
+
let content = fs12.readFileSync(skillMdPath, "utf-8");
|
|
2328
2648
|
if (metadata?.state.persistent && opts.path) {
|
|
2329
|
-
const stateFile =
|
|
2330
|
-
if (
|
|
2331
|
-
const stateContent =
|
|
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
|
|
2350
|
-
import * as
|
|
2351
|
-
import { parse as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
2373
|
-
const yamlPath =
|
|
2374
|
-
const skillMdPath =
|
|
2375
|
-
if (!
|
|
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 =
|
|
2381
|
-
const parsed =
|
|
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 (
|
|
2388
|
-
const mdContent =
|
|
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
|
|
2431
|
-
import * as
|
|
2432
|
-
import { parse as
|
|
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 =
|
|
2438
|
-
if (!
|
|
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 =
|
|
2444
|
-
if (!
|
|
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 =
|
|
2451
|
-
const parsed =
|
|
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
|
|
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 =
|
|
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
|
|
2550
|
-
import * as
|
|
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 =
|
|
2555
|
-
const statePath =
|
|
2556
|
-
if (!
|
|
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((
|
|
2564
|
-
rl.question("Reset project state? This cannot be undone. [y/N] ",
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2615
|
-
import * as
|
|
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 =
|
|
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 :
|
|
2943
|
+
const feature = segments.length > 1 ? firstSegment : path24.basename(firstSegment, path24.extname(firstSegment));
|
|
2624
2944
|
const specRelative = specPattern.replace("{feature}", feature);
|
|
2625
|
-
return
|
|
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 ?
|
|
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 (!
|
|
2970
|
+
if (!fs16.existsSync(expectedSpec)) {
|
|
2651
2971
|
missingSpecs.push({
|
|
2652
|
-
implFile:
|
|
2653
|
-
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
|
|
2724
|
-
import
|
|
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
|
|
2730
|
-
import
|
|
2731
|
-
import { parse as
|
|
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(
|
|
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
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const args = (meta.cli?.args ?? []).map((a) => ({
|
|
2787
|
-
name: a.name,
|
|
2788
|
-
description: a.description ?? "",
|
|
2789
|
-
required: a.required ?? false
|
|
2790
|
-
}));
|
|
2791
|
-
const tools = [...meta.tools ?? []];
|
|
2792
|
-
if (!tools.includes("Read")) {
|
|
2793
|
-
tools.push("Read");
|
|
2794
|
-
}
|
|
2795
|
-
const contextLines = [];
|
|
2796
|
-
if (meta.cognitive_mode) {
|
|
2797
|
-
contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
|
|
2798
|
-
}
|
|
2799
|
-
if (meta.type) {
|
|
2800
|
-
contextLines.push(`Type: ${meta.type}`);
|
|
2801
|
-
}
|
|
2802
|
-
if (meta.state?.persistent) {
|
|
2803
|
-
const files = meta.state.files?.join(", ") ?? "";
|
|
2804
|
-
contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
|
|
2805
|
-
}
|
|
2806
|
-
const objectiveLines = [meta.description];
|
|
2807
|
-
if (meta.phases && meta.phases.length > 0) {
|
|
2808
|
-
objectiveLines.push("");
|
|
2809
|
-
objectiveLines.push("Phases:");
|
|
2810
|
-
for (const phase of meta.phases) {
|
|
2811
|
-
const req = phase.required !== false ? "" : " (optional)";
|
|
2812
|
-
objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
executionContextLines.push(`@${skillMdRelative}`);
|
|
2818
|
-
executionContextLines.push(`@${skillYamlRelative}`);
|
|
2819
|
-
}
|
|
2820
|
-
const processLines = [];
|
|
2821
|
-
if (meta.mcp?.tool) {
|
|
2822
|
-
processLines.push(`1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`);
|
|
2823
|
-
processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
|
|
2824
|
-
processLines.push(`3. Pass through any arguments provided by the user`);
|
|
2825
|
-
} else {
|
|
2826
|
-
processLines.push(`1. Read SKILL.md and follow its workflow directly`);
|
|
2827
|
-
processLines.push(`2. Pass through any arguments provided by the user`);
|
|
2828
|
-
}
|
|
2829
|
-
specs.push({
|
|
2830
|
-
name: normalized,
|
|
2831
|
-
namespace: "harness",
|
|
2832
|
-
fullName: `harness:${normalized}`,
|
|
2833
|
-
description: meta.description,
|
|
2834
|
-
version: meta.version,
|
|
2835
|
-
...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
|
|
2836
|
-
tools,
|
|
2837
|
-
args,
|
|
2838
|
-
skillYamlName: meta.name,
|
|
2839
|
-
sourceDir: entry.name,
|
|
2840
|
-
prompt: {
|
|
2841
|
-
context: contextLines.join("\n"),
|
|
2842
|
-
objective: objectiveLines.join("\n"),
|
|
2843
|
-
executionContext: executionContextLines.join("\n"),
|
|
2844
|
-
process: processLines.join("\n")
|
|
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
|
|
2949
|
-
import
|
|
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 =
|
|
2957
|
-
if (!
|
|
3292
|
+
const filePath = path26.join(outputDir, filename);
|
|
3293
|
+
if (!fs18.existsSync(filePath)) {
|
|
2958
3294
|
added.push(filename);
|
|
2959
3295
|
} else {
|
|
2960
|
-
const existing =
|
|
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 (
|
|
2969
|
-
const existing =
|
|
2970
|
-
const stat =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2994
|
-
if (
|
|
2995
|
-
|
|
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
|
|
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" ?
|
|
3344
|
+
return platform === "claude-code" ? path27.join(home, ".claude", "commands", "harness") : path27.join(home, ".gemini", "commands", "harness");
|
|
3009
3345
|
}
|
|
3010
|
-
return platform === "claude-code" ?
|
|
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((
|
|
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
|
-
|
|
3357
|
+
resolve18(answer.toLowerCase() === "y");
|
|
3022
3358
|
});
|
|
3023
3359
|
});
|
|
3024
3360
|
}
|
|
3025
3361
|
function generateSlashCommands(opts) {
|
|
3026
|
-
const
|
|
3027
|
-
|
|
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 `@${
|
|
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 =
|
|
3053
|
-
const yamlPath =
|
|
3054
|
-
const mdContent =
|
|
3055
|
-
const yamlContent =
|
|
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 =
|
|
3082
|
-
if (
|
|
3083
|
-
|
|
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
|
|
3225
|
-
import * as
|
|
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 (
|
|
3319
|
-
if (
|
|
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 =
|
|
3336
|
-
const dir =
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
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((
|
|
3779
|
+
return new Promise((resolve18) => {
|
|
3419
3780
|
rl.question(question, (answer) => {
|
|
3420
3781
|
rl.close();
|
|
3421
|
-
|
|
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
|
|
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
|
};
|