@harness-engineering/cli 1.0.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.
@@ -0,0 +1,2512 @@
1
+ // src/index.ts
2
+ import { Command as Command26 } from "commander";
3
+ import { VERSION } from "@harness-engineering/core";
4
+
5
+ // src/commands/validate.ts
6
+ import { Command } from "commander";
7
+ import * as path2 from "path";
8
+ import { Ok as Ok2 } from "@harness-engineering/core";
9
+ import { validateAgentsMap, validateKnowledgeMap } from "@harness-engineering/core";
10
+
11
+ // src/config/loader.ts
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { Ok, Err } from "@harness-engineering/core";
15
+
16
+ // src/config/schema.ts
17
+ import { z } from "zod";
18
+ var LayerSchema = z.object({
19
+ name: z.string(),
20
+ pattern: z.string(),
21
+ allowedDependencies: z.array(z.string())
22
+ });
23
+ var ForbiddenImportSchema = z.object({
24
+ from: z.string(),
25
+ disallow: z.array(z.string()),
26
+ message: z.string().optional()
27
+ });
28
+ var BoundaryConfigSchema = z.object({
29
+ requireSchema: z.array(z.string())
30
+ });
31
+ var AgentConfigSchema = z.object({
32
+ executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
33
+ timeout: z.number().default(3e5),
34
+ skills: z.array(z.string()).optional()
35
+ });
36
+ var EntropyConfigSchema = z.object({
37
+ excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
38
+ autoFix: z.boolean().default(false)
39
+ });
40
+ var HarnessConfigSchema = z.object({
41
+ version: z.literal(1),
42
+ name: z.string().optional(),
43
+ rootDir: z.string().default("."),
44
+ layers: z.array(LayerSchema).optional(),
45
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
46
+ boundaries: BoundaryConfigSchema.optional(),
47
+ agentsMapPath: z.string().default("./AGENTS.md"),
48
+ docsDir: z.string().default("./docs"),
49
+ agent: AgentConfigSchema.optional(),
50
+ entropy: EntropyConfigSchema.optional(),
51
+ template: z.object({
52
+ level: z.enum(["basic", "intermediate", "advanced"]),
53
+ framework: z.string().optional(),
54
+ version: z.number()
55
+ }).optional()
56
+ });
57
+
58
+ // src/utils/errors.ts
59
+ var ExitCode = {
60
+ SUCCESS: 0,
61
+ VALIDATION_FAILED: 1,
62
+ ERROR: 2
63
+ };
64
+ var CLIError = class extends Error {
65
+ exitCode;
66
+ constructor(message, exitCode = ExitCode.ERROR) {
67
+ super(message);
68
+ this.name = "CLIError";
69
+ this.exitCode = exitCode;
70
+ }
71
+ };
72
+ function formatError(error) {
73
+ if (error instanceof CLIError) {
74
+ return `Error: ${error.message}`;
75
+ }
76
+ if (error instanceof Error) {
77
+ return `Error: ${error.message}`;
78
+ }
79
+ return `Error: ${String(error)}`;
80
+ }
81
+ function handleError(error) {
82
+ const message = formatError(error);
83
+ console.error(message);
84
+ const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
85
+ process.exit(exitCode);
86
+ }
87
+
88
+ // src/config/loader.ts
89
+ var CONFIG_FILENAMES = ["harness.config.json"];
90
+ function findConfigFile(startDir = process.cwd()) {
91
+ let currentDir = path.resolve(startDir);
92
+ const root = path.parse(currentDir).root;
93
+ while (currentDir !== root) {
94
+ for (const filename of CONFIG_FILENAMES) {
95
+ const configPath = path.join(currentDir, filename);
96
+ if (fs.existsSync(configPath)) {
97
+ return Ok(configPath);
98
+ }
99
+ }
100
+ currentDir = path.dirname(currentDir);
101
+ }
102
+ return Err(new CLIError(
103
+ 'No harness.config.json found. Run "harness init" to create one.',
104
+ ExitCode.ERROR
105
+ ));
106
+ }
107
+ function loadConfig(configPath) {
108
+ if (!fs.existsSync(configPath)) {
109
+ return Err(new CLIError(
110
+ `Config file not found: ${configPath}`,
111
+ ExitCode.ERROR
112
+ ));
113
+ }
114
+ let rawConfig;
115
+ try {
116
+ const content = fs.readFileSync(configPath, "utf-8");
117
+ rawConfig = JSON.parse(content);
118
+ } catch (error) {
119
+ return Err(new CLIError(
120
+ `Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
121
+ ExitCode.ERROR
122
+ ));
123
+ }
124
+ const parsed = HarnessConfigSchema.safeParse(rawConfig);
125
+ if (!parsed.success) {
126
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
127
+ return Err(new CLIError(
128
+ `Invalid config:
129
+ ${issues}`,
130
+ ExitCode.ERROR
131
+ ));
132
+ }
133
+ return Ok(parsed.data);
134
+ }
135
+ function resolveConfig(configPath) {
136
+ if (configPath) {
137
+ return loadConfig(configPath);
138
+ }
139
+ const findResult = findConfigFile();
140
+ if (!findResult.ok) {
141
+ return findResult;
142
+ }
143
+ return loadConfig(findResult.value);
144
+ }
145
+
146
+ // src/output/formatter.ts
147
+ import chalk from "chalk";
148
+ var OutputMode = {
149
+ JSON: "json",
150
+ TEXT: "text",
151
+ QUIET: "quiet",
152
+ VERBOSE: "verbose"
153
+ };
154
+ var OutputFormatter = class {
155
+ constructor(mode = OutputMode.TEXT) {
156
+ this.mode = mode;
157
+ }
158
+ /**
159
+ * Format raw data (for JSON mode)
160
+ */
161
+ format(data) {
162
+ if (this.mode === OutputMode.JSON) {
163
+ return JSON.stringify(data, null, 2);
164
+ }
165
+ return String(data);
166
+ }
167
+ /**
168
+ * Format validation result
169
+ */
170
+ formatValidation(result) {
171
+ if (this.mode === OutputMode.JSON) {
172
+ return JSON.stringify(result, null, 2);
173
+ }
174
+ if (this.mode === OutputMode.QUIET) {
175
+ if (result.valid) return "";
176
+ return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
177
+ }
178
+ const lines = [];
179
+ if (result.valid) {
180
+ lines.push(chalk.green("v validation passed"));
181
+ } else {
182
+ lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
183
+ lines.push("");
184
+ for (const issue of result.issues) {
185
+ const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
186
+ lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
187
+ lines.push(` ${issue.message}`);
188
+ if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
189
+ lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
190
+ }
191
+ }
192
+ }
193
+ return lines.join("\n");
194
+ }
195
+ /**
196
+ * Format a summary line
197
+ */
198
+ formatSummary(label, value, success) {
199
+ if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
200
+ return "";
201
+ }
202
+ const icon = success ? chalk.green("v") : chalk.red("x");
203
+ return `${icon} ${label}: ${value}`;
204
+ }
205
+ };
206
+
207
+ // src/output/logger.ts
208
+ import chalk2 from "chalk";
209
+ var logger = {
210
+ info: (message) => console.log(chalk2.blue("i"), message),
211
+ success: (message) => console.log(chalk2.green("v"), message),
212
+ warn: (message) => console.log(chalk2.yellow("!"), message),
213
+ error: (message) => console.error(chalk2.red("x"), message),
214
+ dim: (message) => console.log(chalk2.dim(message)),
215
+ // For JSON output mode
216
+ raw: (data) => console.log(JSON.stringify(data, null, 2))
217
+ };
218
+
219
+ // src/commands/validate.ts
220
+ async function runValidate(options) {
221
+ const configResult = resolveConfig(options.configPath);
222
+ if (!configResult.ok) {
223
+ return configResult;
224
+ }
225
+ const config = configResult.value;
226
+ const cwd = options.cwd ?? (options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd());
227
+ const result = {
228
+ valid: true,
229
+ checks: {
230
+ agentsMap: false,
231
+ fileStructure: false,
232
+ knowledgeMap: false
233
+ },
234
+ issues: []
235
+ };
236
+ const agentsMapPath = path2.resolve(cwd, config.agentsMapPath);
237
+ const agentsResult = await validateAgentsMap(agentsMapPath);
238
+ if (agentsResult.ok) {
239
+ result.checks.agentsMap = true;
240
+ } else {
241
+ result.valid = false;
242
+ result.issues.push({
243
+ check: "agentsMap",
244
+ file: config.agentsMapPath,
245
+ message: agentsResult.error.message,
246
+ suggestion: agentsResult.error.suggestions?.[0] ?? void 0
247
+ });
248
+ }
249
+ const knowledgeResult = await validateKnowledgeMap(cwd);
250
+ if (knowledgeResult.ok && knowledgeResult.value.brokenLinks.length === 0) {
251
+ result.checks.knowledgeMap = true;
252
+ } else if (knowledgeResult.ok) {
253
+ result.valid = false;
254
+ for (const broken of knowledgeResult.value.brokenLinks) {
255
+ result.issues.push({
256
+ check: "knowledgeMap",
257
+ file: broken.path,
258
+ message: `Broken link: ${broken.path}`,
259
+ suggestion: broken.suggestion || "Remove or fix the broken link"
260
+ });
261
+ }
262
+ } else {
263
+ result.valid = false;
264
+ result.issues.push({
265
+ check: "knowledgeMap",
266
+ message: knowledgeResult.error.message
267
+ });
268
+ }
269
+ result.checks.fileStructure = true;
270
+ return Ok2(result);
271
+ }
272
+ function createValidateCommand() {
273
+ const command = new Command("validate").description("Run all validation checks").option("--cross-check", "Run cross-artifact consistency validation").action(async (opts, cmd) => {
274
+ const globalOpts = cmd.optsWithGlobals();
275
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
276
+ const formatter = new OutputFormatter(mode);
277
+ const result = await runValidate({
278
+ configPath: globalOpts.config,
279
+ json: globalOpts.json,
280
+ verbose: globalOpts.verbose,
281
+ quiet: globalOpts.quiet
282
+ });
283
+ if (!result.ok) {
284
+ if (mode === OutputMode.JSON) {
285
+ console.log(JSON.stringify({ error: result.error.message }));
286
+ } else {
287
+ logger.error(result.error.message);
288
+ }
289
+ process.exit(result.error.exitCode);
290
+ }
291
+ if (opts.crossCheck) {
292
+ const { runCrossCheck } = await import("./validate-cross-check-ZB2OZDOK.js");
293
+ const cwd = process.cwd();
294
+ const specsDir = path2.join(cwd, "docs", "specs");
295
+ const plansDir = path2.join(cwd, "docs", "plans");
296
+ const crossResult = await runCrossCheck({ specsDir, plansDir, projectPath: cwd });
297
+ if (crossResult.ok && crossResult.value.warnings > 0) {
298
+ console.log("\nCross-artifact validation:");
299
+ for (const w of crossResult.value.planToImpl) console.log(` ! ${w}`);
300
+ for (const w of crossResult.value.staleness) console.log(` ! ${w}`);
301
+ console.log(`
302
+ ${crossResult.value.warnings} warnings`);
303
+ }
304
+ }
305
+ const output = formatter.formatValidation({
306
+ valid: result.value.valid,
307
+ issues: result.value.issues
308
+ });
309
+ if (output) {
310
+ console.log(output);
311
+ }
312
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
313
+ });
314
+ return command;
315
+ }
316
+
317
+ // src/commands/check-deps.ts
318
+ import { Command as Command2 } from "commander";
319
+ import * as path3 from "path";
320
+ import { Ok as Ok3 } from "@harness-engineering/core";
321
+ import {
322
+ validateDependencies,
323
+ detectCircularDepsInFiles,
324
+ defineLayer,
325
+ TypeScriptParser
326
+ } from "@harness-engineering/core";
327
+
328
+ // src/utils/files.ts
329
+ import { glob } from "glob";
330
+ async function findFiles(pattern, cwd = process.cwd()) {
331
+ return glob(pattern, { cwd, absolute: true });
332
+ }
333
+
334
+ // src/commands/check-deps.ts
335
+ async function runCheckDeps(options) {
336
+ const cwd = options.cwd ?? process.cwd();
337
+ const configResult = resolveConfig(options.configPath);
338
+ if (!configResult.ok) {
339
+ return configResult;
340
+ }
341
+ const config = configResult.value;
342
+ const result = {
343
+ valid: true,
344
+ layerViolations: [],
345
+ circularDeps: []
346
+ };
347
+ if (!config.layers || config.layers.length === 0) {
348
+ return Ok3(result);
349
+ }
350
+ const rootDir = path3.resolve(cwd, config.rootDir);
351
+ const parser = new TypeScriptParser();
352
+ const layers = config.layers.map(
353
+ (l) => defineLayer(l.name, [l.pattern], l.allowedDependencies)
354
+ );
355
+ const layerConfig = {
356
+ layers,
357
+ rootDir,
358
+ parser,
359
+ fallbackBehavior: "warn"
360
+ };
361
+ const depsResult = await validateDependencies(layerConfig);
362
+ if (depsResult.ok) {
363
+ for (const violation of depsResult.value.violations) {
364
+ result.valid = false;
365
+ result.layerViolations.push({
366
+ file: violation.file,
367
+ imports: violation.imports,
368
+ fromLayer: violation.fromLayer ?? "unknown",
369
+ toLayer: violation.toLayer ?? "unknown",
370
+ message: violation.reason
371
+ });
372
+ }
373
+ }
374
+ const allFiles = [];
375
+ for (const layer of config.layers) {
376
+ const files = await findFiles(layer.pattern, rootDir);
377
+ allFiles.push(...files);
378
+ }
379
+ const uniqueFiles = [...new Set(allFiles)];
380
+ if (uniqueFiles.length > 0) {
381
+ const circularResult = await detectCircularDepsInFiles(uniqueFiles, parser);
382
+ if (circularResult.ok && circularResult.value.hasCycles) {
383
+ result.valid = false;
384
+ for (const cycle of circularResult.value.cycles) {
385
+ result.circularDeps.push({ cycle: cycle.cycle });
386
+ }
387
+ }
388
+ }
389
+ return Ok3(result);
390
+ }
391
+ function createCheckDepsCommand() {
392
+ const command = new Command2("check-deps").description("Validate dependency layers and detect circular dependencies").action(async (_opts, cmd) => {
393
+ const globalOpts = cmd.optsWithGlobals();
394
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
395
+ const formatter = new OutputFormatter(mode);
396
+ const result = await runCheckDeps({
397
+ configPath: globalOpts.config,
398
+ json: globalOpts.json,
399
+ verbose: globalOpts.verbose,
400
+ quiet: globalOpts.quiet
401
+ });
402
+ if (!result.ok) {
403
+ if (mode === OutputMode.JSON) {
404
+ console.log(JSON.stringify({ error: result.error.message }));
405
+ } else {
406
+ logger.error(result.error.message);
407
+ }
408
+ process.exit(result.error.exitCode);
409
+ }
410
+ const issues = [
411
+ ...result.value.layerViolations.map((v) => ({
412
+ file: v.file,
413
+ message: `Layer violation: ${v.fromLayer} -> ${v.toLayer} (${v.message})`
414
+ })),
415
+ ...result.value.circularDeps.map((c) => ({
416
+ message: `Circular dependency: ${c.cycle.join(" -> ")}`
417
+ }))
418
+ ];
419
+ const output = formatter.formatValidation({
420
+ valid: result.value.valid,
421
+ issues
422
+ });
423
+ if (output) {
424
+ console.log(output);
425
+ }
426
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
427
+ });
428
+ return command;
429
+ }
430
+
431
+ // src/commands/check-docs.ts
432
+ import { Command as Command3 } from "commander";
433
+ import * as path4 from "path";
434
+ import { Ok as Ok4, Err as Err2 } from "@harness-engineering/core";
435
+ import { checkDocCoverage, validateKnowledgeMap as validateKnowledgeMap2 } from "@harness-engineering/core";
436
+ async function runCheckDocs(options) {
437
+ const cwd = options.cwd ?? process.cwd();
438
+ const minCoverage = options.minCoverage ?? 80;
439
+ const configResult = resolveConfig(options.configPath);
440
+ if (!configResult.ok) {
441
+ return configResult;
442
+ }
443
+ const config = configResult.value;
444
+ const docsDir = path4.resolve(cwd, config.docsDir);
445
+ const sourceDir = path4.resolve(cwd, config.rootDir);
446
+ const coverageResult = await checkDocCoverage("project", {
447
+ docsDir,
448
+ sourceDir,
449
+ excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
450
+ });
451
+ if (!coverageResult.ok) {
452
+ return Err2(new CLIError(
453
+ `Documentation coverage check failed: ${coverageResult.error.message}`,
454
+ ExitCode.ERROR
455
+ ));
456
+ }
457
+ const knowledgeResult = await validateKnowledgeMap2(cwd);
458
+ let brokenLinks = [];
459
+ if (knowledgeResult.ok) {
460
+ brokenLinks = knowledgeResult.value.brokenLinks.map((b) => b.path);
461
+ } else {
462
+ logger.warn(`Knowledge map validation failed: ${knowledgeResult.error.message}`);
463
+ }
464
+ const coveragePercent = coverageResult.value.coveragePercentage;
465
+ const result = {
466
+ valid: coveragePercent >= minCoverage && brokenLinks.length === 0,
467
+ coveragePercent,
468
+ documented: coverageResult.value.documented,
469
+ undocumented: coverageResult.value.undocumented,
470
+ brokenLinks
471
+ };
472
+ return Ok4(result);
473
+ }
474
+ function createCheckDocsCommand() {
475
+ const command = new Command3("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
476
+ const globalOpts = cmd.optsWithGlobals();
477
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
478
+ const formatter = new OutputFormatter(mode);
479
+ const result = await runCheckDocs({
480
+ configPath: globalOpts.config,
481
+ minCoverage: parseInt(opts.minCoverage, 10),
482
+ json: globalOpts.json,
483
+ verbose: globalOpts.verbose,
484
+ quiet: globalOpts.quiet
485
+ });
486
+ if (!result.ok) {
487
+ if (mode === OutputMode.JSON) {
488
+ console.log(JSON.stringify({ error: result.error.message }));
489
+ } else {
490
+ logger.error(result.error.message);
491
+ }
492
+ process.exit(result.error.exitCode);
493
+ }
494
+ if (mode === OutputMode.JSON) {
495
+ console.log(JSON.stringify(result.value, null, 2));
496
+ } else if (mode !== OutputMode.QUIET) {
497
+ const { value } = result;
498
+ console.log(formatter.formatSummary(
499
+ "Documentation coverage",
500
+ `${value.coveragePercent.toFixed(1)}%`,
501
+ value.valid
502
+ ));
503
+ if (value.undocumented.length > 0 && (mode === OutputMode.VERBOSE || !value.valid)) {
504
+ console.log("\nUndocumented files:");
505
+ for (const file of value.undocumented.slice(0, 10)) {
506
+ console.log(` - ${file}`);
507
+ }
508
+ if (value.undocumented.length > 10) {
509
+ console.log(` ... and ${value.undocumented.length - 10} more`);
510
+ }
511
+ }
512
+ if (value.brokenLinks.length > 0) {
513
+ console.log("\nBroken links:");
514
+ for (const link of value.brokenLinks) {
515
+ console.log(` - ${link}`);
516
+ }
517
+ }
518
+ }
519
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
520
+ });
521
+ return command;
522
+ }
523
+
524
+ // src/commands/init.ts
525
+ import { Command as Command4 } from "commander";
526
+ import * as fs4 from "fs";
527
+ import * as path7 from "path";
528
+ import { Ok as Ok6, Err as Err4 } from "@harness-engineering/core";
529
+
530
+ // src/templates/engine.ts
531
+ import * as fs2 from "fs";
532
+ import * as path5 from "path";
533
+ import Handlebars from "handlebars";
534
+ import { Ok as Ok5, Err as Err3 } from "@harness-engineering/core";
535
+
536
+ // src/templates/schema.ts
537
+ import { z as z2 } from "zod";
538
+ var MergeStrategySchema = z2.object({
539
+ json: z2.enum(["deep-merge", "overlay-wins"]).default("deep-merge"),
540
+ files: z2.literal("overlay-wins").default("overlay-wins")
541
+ });
542
+ var TemplateMetadataSchema = z2.object({
543
+ name: z2.string(),
544
+ description: z2.string(),
545
+ level: z2.enum(["basic", "intermediate", "advanced"]).optional(),
546
+ framework: z2.string().optional(),
547
+ extends: z2.string().optional(),
548
+ mergeStrategy: MergeStrategySchema.default({}),
549
+ version: z2.literal(1)
550
+ });
551
+
552
+ // src/templates/merger.ts
553
+ function isPlainObject(val) {
554
+ return typeof val === "object" && val !== null && !Array.isArray(val);
555
+ }
556
+ function deepMergeJson(base, overlay) {
557
+ const result = { ...base };
558
+ for (const key of Object.keys(overlay)) {
559
+ if (isPlainObject(result[key]) && isPlainObject(overlay[key])) {
560
+ result[key] = deepMergeJson(
561
+ result[key],
562
+ overlay[key]
563
+ );
564
+ } else {
565
+ result[key] = overlay[key];
566
+ }
567
+ }
568
+ return result;
569
+ }
570
+ var CONCAT_KEYS = /* @__PURE__ */ new Set(["dependencies", "devDependencies", "peerDependencies"]);
571
+ function mergePackageJson(base, overlay) {
572
+ const result = { ...base };
573
+ for (const key of Object.keys(overlay)) {
574
+ if (CONCAT_KEYS.has(key) && isPlainObject(result[key]) && isPlainObject(overlay[key])) {
575
+ result[key] = {
576
+ ...result[key],
577
+ ...overlay[key]
578
+ };
579
+ } else if (isPlainObject(result[key]) && isPlainObject(overlay[key])) {
580
+ result[key] = deepMergeJson(
581
+ result[key],
582
+ overlay[key]
583
+ );
584
+ } else {
585
+ result[key] = overlay[key];
586
+ }
587
+ }
588
+ return result;
589
+ }
590
+
591
+ // src/templates/engine.ts
592
+ var TemplateEngine = class {
593
+ constructor(templatesDir) {
594
+ this.templatesDir = templatesDir;
595
+ }
596
+ listTemplates() {
597
+ try {
598
+ const entries = fs2.readdirSync(this.templatesDir, { withFileTypes: true });
599
+ const templates = [];
600
+ for (const entry of entries) {
601
+ if (!entry.isDirectory()) continue;
602
+ const metaPath = path5.join(this.templatesDir, entry.name, "template.json");
603
+ if (!fs2.existsSync(metaPath)) continue;
604
+ const raw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
605
+ const parsed = TemplateMetadataSchema.safeParse(raw);
606
+ if (parsed.success) templates.push(parsed.data);
607
+ }
608
+ return Ok5(templates);
609
+ } catch (error) {
610
+ return Err3(new Error(`Failed to list templates: ${error instanceof Error ? error.message : String(error)}`));
611
+ }
612
+ }
613
+ resolveTemplate(level, framework) {
614
+ const levelDir = this.findTemplateDir(level, "level");
615
+ if (!levelDir) return Err3(new Error(`Template not found for level: ${level}`));
616
+ const metaPath = path5.join(levelDir, "template.json");
617
+ const metaRaw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
618
+ const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
619
+ if (!metaResult.success) return Err3(new Error(`Invalid template.json in ${level}: ${metaResult.error.message}`));
620
+ const metadata = metaResult.data;
621
+ let files = [];
622
+ if (metadata.extends) {
623
+ const baseDir = path5.join(this.templatesDir, metadata.extends);
624
+ if (fs2.existsSync(baseDir)) files = this.collectFiles(baseDir, metadata.extends);
625
+ }
626
+ const levelFiles = this.collectFiles(levelDir, level);
627
+ files = this.mergeFileLists(files, levelFiles);
628
+ let overlayMetadata;
629
+ if (framework) {
630
+ const frameworkDir = this.findTemplateDir(framework, "framework");
631
+ if (!frameworkDir) return Err3(new Error(`Framework template not found: ${framework}`));
632
+ const fMetaPath = path5.join(frameworkDir, "template.json");
633
+ const fMetaRaw = JSON.parse(fs2.readFileSync(fMetaPath, "utf-8"));
634
+ const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
635
+ if (fMetaResult.success) overlayMetadata = fMetaResult.data;
636
+ const frameworkFiles = this.collectFiles(frameworkDir, framework);
637
+ files = this.mergeFileLists(files, frameworkFiles);
638
+ }
639
+ files = files.filter((f) => f.relativePath !== "template.json");
640
+ const resolved = { metadata, files };
641
+ if (overlayMetadata !== void 0) resolved.overlayMetadata = overlayMetadata;
642
+ return Ok5(resolved);
643
+ }
644
+ render(template, context) {
645
+ const rendered = [];
646
+ const jsonBuffers = /* @__PURE__ */ new Map();
647
+ for (const file of template.files) {
648
+ const outputPath = file.relativePath.replace(/\.hbs$/, "");
649
+ if (file.isHandlebars) {
650
+ try {
651
+ const raw = fs2.readFileSync(file.absolutePath, "utf-8");
652
+ const compiled = Handlebars.compile(raw, { strict: true });
653
+ const content = compiled(context);
654
+ if (outputPath.endsWith(".json") && file.relativePath.endsWith(".json.hbs")) {
655
+ if (!jsonBuffers.has(outputPath)) jsonBuffers.set(outputPath, []);
656
+ jsonBuffers.get(outputPath).push(JSON.parse(content));
657
+ } else {
658
+ rendered.push({ relativePath: outputPath, content });
659
+ }
660
+ } catch (error) {
661
+ const msg = error instanceof Error ? error.message : String(error);
662
+ return Err3(new Error(`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`));
663
+ }
664
+ } else {
665
+ try {
666
+ const content = fs2.readFileSync(file.absolutePath, "utf-8");
667
+ rendered.push({ relativePath: file.relativePath, content });
668
+ } catch (error) {
669
+ const msg = error instanceof Error ? error.message : String(error);
670
+ return Err3(new Error(`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`));
671
+ }
672
+ }
673
+ }
674
+ try {
675
+ for (const [outputPath, jsons] of jsonBuffers) {
676
+ let merged = {};
677
+ for (const json of jsons) {
678
+ merged = outputPath === "package.json" ? mergePackageJson(merged, json) : deepMergeJson(merged, json);
679
+ }
680
+ rendered.push({ relativePath: outputPath, content: JSON.stringify(merged, null, 2) });
681
+ }
682
+ } catch (error) {
683
+ const msg = error instanceof Error ? error.message : String(error);
684
+ return Err3(new Error(`JSON merge failed: ${msg}`));
685
+ }
686
+ return Ok5({ files: rendered });
687
+ }
688
+ write(files, targetDir, options) {
689
+ try {
690
+ const written = [];
691
+ for (const file of files.files) {
692
+ const targetPath = path5.join(targetDir, file.relativePath);
693
+ const dir = path5.dirname(targetPath);
694
+ if (!options.overwrite && fs2.existsSync(targetPath)) continue;
695
+ fs2.mkdirSync(dir, { recursive: true });
696
+ fs2.writeFileSync(targetPath, file.content);
697
+ written.push(file.relativePath);
698
+ }
699
+ return Ok5(written);
700
+ } catch (error) {
701
+ return Err3(new Error(`Failed to write files: ${error instanceof Error ? error.message : String(error)}`));
702
+ }
703
+ }
704
+ findTemplateDir(name, type) {
705
+ const entries = fs2.readdirSync(this.templatesDir, { withFileTypes: true });
706
+ for (const entry of entries) {
707
+ if (!entry.isDirectory()) continue;
708
+ const metaPath = path5.join(this.templatesDir, entry.name, "template.json");
709
+ if (!fs2.existsSync(metaPath)) continue;
710
+ const raw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
711
+ const parsed = TemplateMetadataSchema.safeParse(raw);
712
+ if (!parsed.success) continue;
713
+ if (type === "level" && parsed.data.level === name) return path5.join(this.templatesDir, entry.name);
714
+ if (type === "framework" && parsed.data.framework === name) return path5.join(this.templatesDir, entry.name);
715
+ if (parsed.data.name === name) return path5.join(this.templatesDir, entry.name);
716
+ }
717
+ return null;
718
+ }
719
+ collectFiles(dir, sourceName) {
720
+ const files = [];
721
+ const walk = (currentDir) => {
722
+ const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
723
+ for (const entry of entries) {
724
+ const fullPath = path5.join(currentDir, entry.name);
725
+ if (entry.isDirectory()) {
726
+ walk(fullPath);
727
+ } else {
728
+ files.push({
729
+ relativePath: path5.relative(dir, fullPath),
730
+ absolutePath: fullPath,
731
+ isHandlebars: entry.name.endsWith(".hbs"),
732
+ sourceTemplate: sourceName
733
+ });
734
+ }
735
+ }
736
+ };
737
+ walk(dir);
738
+ return files;
739
+ }
740
+ mergeFileLists(base, overlay) {
741
+ const map = /* @__PURE__ */ new Map();
742
+ for (const file of base) map.set(file.relativePath, file);
743
+ for (const file of overlay) {
744
+ if (file.relativePath.endsWith(".json.hbs")) {
745
+ const baseKey = base.find((f) => f.relativePath === file.relativePath);
746
+ if (baseKey) {
747
+ map.set(`__overlay__${file.relativePath}`, file);
748
+ } else {
749
+ map.set(file.relativePath, file);
750
+ }
751
+ } else {
752
+ map.set(file.relativePath, file);
753
+ }
754
+ }
755
+ return Array.from(map.values());
756
+ }
757
+ };
758
+
759
+ // src/utils/paths.ts
760
+ import * as fs3 from "fs";
761
+ import * as path6 from "path";
762
+ import { fileURLToPath } from "url";
763
+ var __filename = fileURLToPath(import.meta.url);
764
+ var __dirname = path6.dirname(__filename);
765
+ function findUpDir(targetName, marker, maxLevels = 8) {
766
+ let dir = __dirname;
767
+ for (let i = 0; i < maxLevels; i++) {
768
+ const candidate = path6.join(dir, targetName);
769
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
770
+ if (fs3.existsSync(path6.join(candidate, marker))) {
771
+ return candidate;
772
+ }
773
+ }
774
+ dir = path6.dirname(dir);
775
+ }
776
+ return null;
777
+ }
778
+ function resolveTemplatesDir() {
779
+ return findUpDir("templates", "base") ?? path6.join(__dirname, "..", "templates");
780
+ }
781
+ function resolvePersonasDir() {
782
+ const agentsDir = findUpDir("agents", "personas");
783
+ if (agentsDir) {
784
+ return path6.join(agentsDir, "personas");
785
+ }
786
+ return path6.join(__dirname, "..", "..", "agents", "personas");
787
+ }
788
+ function resolveSkillsDir() {
789
+ const agentsDir = findUpDir("agents", "skills");
790
+ if (agentsDir) {
791
+ return path6.join(agentsDir, "skills", "claude-code");
792
+ }
793
+ return path6.join(__dirname, "..", "..", "agents", "skills", "claude-code");
794
+ }
795
+
796
+ // src/commands/init.ts
797
+ async function runInit(options) {
798
+ const cwd = options.cwd ?? process.cwd();
799
+ const name = options.name ?? path7.basename(cwd);
800
+ const level = options.level ?? "basic";
801
+ const force = options.force ?? false;
802
+ const configPath = path7.join(cwd, "harness.config.json");
803
+ if (!force && fs4.existsSync(configPath)) {
804
+ return Err4(new CLIError(
805
+ "Project already initialized. Use --force to overwrite.",
806
+ ExitCode.ERROR
807
+ ));
808
+ }
809
+ const templatesDir = resolveTemplatesDir();
810
+ const engine = new TemplateEngine(templatesDir);
811
+ const resolveResult = engine.resolveTemplate(level, options.framework);
812
+ if (!resolveResult.ok) {
813
+ return Err4(new CLIError(resolveResult.error.message, ExitCode.ERROR));
814
+ }
815
+ const renderResult = engine.render(resolveResult.value, {
816
+ projectName: name,
817
+ level,
818
+ framework: options.framework
819
+ });
820
+ if (!renderResult.ok) {
821
+ return Err4(new CLIError(renderResult.error.message, ExitCode.ERROR));
822
+ }
823
+ const writeResult = engine.write(renderResult.value, cwd, { overwrite: force });
824
+ if (!writeResult.ok) {
825
+ return Err4(new CLIError(writeResult.error.message, ExitCode.ERROR));
826
+ }
827
+ return Ok6({ filesCreated: writeResult.value });
828
+ }
829
+ function createInitCommand() {
830
+ const command = new Command4("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("-f, --force", "Overwrite existing files").action(async (opts, cmd) => {
831
+ const globalOpts = cmd.optsWithGlobals();
832
+ const result = await runInit({
833
+ name: opts.name,
834
+ level: opts.level,
835
+ framework: opts.framework,
836
+ force: opts.force
837
+ });
838
+ if (!result.ok) {
839
+ logger.error(result.error.message);
840
+ process.exit(result.error.exitCode);
841
+ }
842
+ if (!globalOpts.quiet) {
843
+ logger.success("Project initialized!");
844
+ logger.info("Created files:");
845
+ for (const file of result.value.filesCreated) {
846
+ console.log(` - ${file}`);
847
+ }
848
+ console.log("\nNext steps:");
849
+ console.log(" 1. Review harness.config.json");
850
+ console.log(" 2. Update AGENTS.md with your project structure");
851
+ console.log(' 3. Run "harness validate" to check your setup');
852
+ }
853
+ process.exit(ExitCode.SUCCESS);
854
+ });
855
+ return command;
856
+ }
857
+
858
+ // src/commands/cleanup.ts
859
+ import { Command as Command5 } from "commander";
860
+ import * as path8 from "path";
861
+ import { Ok as Ok7, Err as Err5, EntropyAnalyzer } from "@harness-engineering/core";
862
+ async function runCleanup(options) {
863
+ const cwd = options.cwd ?? process.cwd();
864
+ const type = options.type ?? "all";
865
+ const configResult = resolveConfig(options.configPath);
866
+ if (!configResult.ok) {
867
+ return Err5(configResult.error);
868
+ }
869
+ const config = configResult.value;
870
+ const result = {
871
+ driftIssues: [],
872
+ deadCode: [],
873
+ patternViolations: [],
874
+ totalIssues: 0
875
+ };
876
+ const rootDir = path8.resolve(cwd, config.rootDir);
877
+ const docsDir = path8.resolve(cwd, config.docsDir);
878
+ const entropyConfig = {
879
+ rootDir,
880
+ entryPoints: [path8.join(rootDir, "src/index.ts")],
881
+ docPaths: [docsDir],
882
+ analyze: {
883
+ drift: type === "all" || type === "drift",
884
+ deadCode: type === "all" || type === "dead-code",
885
+ patterns: type === "all" || type === "patterns" ? { patterns: [] } : false
886
+ },
887
+ exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
888
+ };
889
+ const analyzer = new EntropyAnalyzer(entropyConfig);
890
+ const analysisResult = await analyzer.analyze();
891
+ if (!analysisResult.ok) {
892
+ return Err5(new CLIError(
893
+ `Entropy analysis failed: ${analysisResult.error.message}`,
894
+ ExitCode.ERROR
895
+ ));
896
+ }
897
+ const report = analysisResult.value;
898
+ if (report.drift) {
899
+ result.driftIssues = report.drift.drifts.map((d) => ({
900
+ file: d.docFile,
901
+ issue: `${d.issue}: ${d.details}`
902
+ }));
903
+ }
904
+ if (report.deadCode) {
905
+ result.deadCode = [
906
+ ...report.deadCode.deadFiles.map((f) => ({ file: f.path })),
907
+ ...report.deadCode.deadExports.map((e) => ({ file: e.file, symbol: e.name }))
908
+ ];
909
+ }
910
+ if (report.patterns) {
911
+ result.patternViolations = report.patterns.violations.map((v) => ({
912
+ file: v.file,
913
+ pattern: v.pattern,
914
+ message: v.message
915
+ }));
916
+ }
917
+ result.totalIssues = result.driftIssues.length + result.deadCode.length + result.patternViolations.length;
918
+ return Ok7(result);
919
+ }
920
+ function createCleanupCommand() {
921
+ const command = new Command5("cleanup").description("Detect entropy issues (doc drift, dead code, patterns)").option("-t, --type <type>", "Issue type: drift, dead-code, patterns, all", "all").action(async (opts, cmd) => {
922
+ const globalOpts = cmd.optsWithGlobals();
923
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
924
+ const formatter = new OutputFormatter(mode);
925
+ const result = await runCleanup({
926
+ configPath: globalOpts.config,
927
+ type: opts.type,
928
+ json: globalOpts.json,
929
+ verbose: globalOpts.verbose,
930
+ quiet: globalOpts.quiet
931
+ });
932
+ if (!result.ok) {
933
+ if (mode === OutputMode.JSON) {
934
+ console.log(JSON.stringify({ error: result.error.message }));
935
+ } else {
936
+ logger.error(result.error.message);
937
+ }
938
+ process.exit(result.error.exitCode);
939
+ }
940
+ if (mode === OutputMode.JSON) {
941
+ console.log(JSON.stringify(result.value, null, 2));
942
+ } else if (mode !== OutputMode.QUIET || result.value.totalIssues > 0) {
943
+ console.log(formatter.formatSummary(
944
+ "Entropy issues",
945
+ result.value.totalIssues.toString(),
946
+ result.value.totalIssues === 0
947
+ ));
948
+ if (result.value.driftIssues.length > 0) {
949
+ console.log("\nDocumentation drift:");
950
+ for (const issue of result.value.driftIssues) {
951
+ console.log(` - ${issue.file}: ${issue.issue}`);
952
+ }
953
+ }
954
+ if (result.value.deadCode.length > 0) {
955
+ console.log("\nDead code:");
956
+ for (const item of result.value.deadCode.slice(0, 10)) {
957
+ console.log(` - ${item.file}${item.symbol ? `: ${item.symbol}` : ""}`);
958
+ }
959
+ if (result.value.deadCode.length > 10) {
960
+ console.log(` ... and ${result.value.deadCode.length - 10} more`);
961
+ }
962
+ }
963
+ if (result.value.patternViolations.length > 0) {
964
+ console.log("\nPattern violations:");
965
+ for (const violation of result.value.patternViolations.slice(0, 10)) {
966
+ console.log(` - ${violation.file} [${violation.pattern}]: ${violation.message}`);
967
+ }
968
+ if (result.value.patternViolations.length > 10) {
969
+ console.log(` ... and ${result.value.patternViolations.length - 10} more`);
970
+ }
971
+ }
972
+ }
973
+ process.exit(result.value.totalIssues === 0 ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
974
+ });
975
+ return command;
976
+ }
977
+
978
+ // src/commands/fix-drift.ts
979
+ import { Command as Command6 } from "commander";
980
+ import * as path9 from "path";
981
+ import {
982
+ Ok as Ok8,
983
+ Err as Err6,
984
+ buildSnapshot,
985
+ detectDocDrift,
986
+ detectDeadCode,
987
+ createFixes,
988
+ applyFixes,
989
+ generateSuggestions
990
+ } from "@harness-engineering/core";
991
+ async function runFixDrift(options) {
992
+ const cwd = options.cwd ?? process.cwd();
993
+ const dryRun = options.dryRun !== false;
994
+ const configResult = resolveConfig(options.configPath);
995
+ if (!configResult.ok) {
996
+ return Err6(configResult.error);
997
+ }
998
+ const config = configResult.value;
999
+ const rootDir = path9.resolve(cwd, config.rootDir);
1000
+ const docsDir = path9.resolve(cwd, config.docsDir);
1001
+ const entropyConfig = {
1002
+ rootDir,
1003
+ entryPoints: [path9.join(rootDir, "src/index.ts")],
1004
+ docPaths: [docsDir],
1005
+ analyze: {
1006
+ drift: true,
1007
+ deadCode: true,
1008
+ patterns: false
1009
+ },
1010
+ exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
1011
+ };
1012
+ const snapshotResult = await buildSnapshot(entropyConfig);
1013
+ if (!snapshotResult.ok) {
1014
+ return Err6(new CLIError(
1015
+ `Failed to build snapshot: ${snapshotResult.error.message}`,
1016
+ ExitCode.ERROR
1017
+ ));
1018
+ }
1019
+ const snapshot = snapshotResult.value;
1020
+ const driftResult = await detectDocDrift(snapshot);
1021
+ if (!driftResult.ok) {
1022
+ return Err6(new CLIError(
1023
+ `Failed to detect drift: ${driftResult.error.message}`,
1024
+ ExitCode.ERROR
1025
+ ));
1026
+ }
1027
+ const driftReport = driftResult.value;
1028
+ const deadCodeResult = await detectDeadCode(snapshot);
1029
+ if (!deadCodeResult.ok) {
1030
+ return Err6(new CLIError(
1031
+ `Failed to detect dead code: ${deadCodeResult.error.message}`,
1032
+ ExitCode.ERROR
1033
+ ));
1034
+ }
1035
+ const deadCodeReport = deadCodeResult.value;
1036
+ const fixes = createFixes(deadCodeReport);
1037
+ const appliedFixes = [];
1038
+ if (!dryRun && fixes.length > 0) {
1039
+ const applyResult = await applyFixes(fixes, { dryRun: false });
1040
+ if (!applyResult.ok) {
1041
+ return Err6(new CLIError(
1042
+ `Failed to apply fixes: ${applyResult.error.message}`,
1043
+ ExitCode.ERROR
1044
+ ));
1045
+ }
1046
+ for (const fix of applyResult.value.applied) {
1047
+ appliedFixes.push({
1048
+ file: fix.file,
1049
+ action: fix.action,
1050
+ applied: true
1051
+ });
1052
+ }
1053
+ for (const fix of applyResult.value.skipped) {
1054
+ appliedFixes.push({
1055
+ file: fix.file,
1056
+ action: fix.action,
1057
+ applied: false
1058
+ });
1059
+ }
1060
+ for (const { fix } of applyResult.value.errors) {
1061
+ appliedFixes.push({
1062
+ file: fix.file,
1063
+ action: fix.action,
1064
+ applied: false
1065
+ });
1066
+ }
1067
+ } else {
1068
+ for (const fix of fixes) {
1069
+ appliedFixes.push({
1070
+ file: fix.file,
1071
+ action: fix.action,
1072
+ applied: false
1073
+ });
1074
+ }
1075
+ }
1076
+ const suggestionReport = generateSuggestions(deadCodeReport, driftReport);
1077
+ const suggestions = [];
1078
+ for (const suggestion of suggestionReport.suggestions) {
1079
+ for (const file of suggestion.files) {
1080
+ suggestions.push({
1081
+ file,
1082
+ suggestion: suggestion.title
1083
+ });
1084
+ }
1085
+ }
1086
+ const result = {
1087
+ dryRun,
1088
+ fixes: appliedFixes,
1089
+ suggestions
1090
+ };
1091
+ return Ok8(result);
1092
+ }
1093
+ function createFixDriftCommand() {
1094
+ const command = new Command6("fix-drift").description("Auto-fix entropy issues (doc drift, dead code)").option("--no-dry-run", "Actually apply fixes (default is dry-run mode)").action(async (opts, cmd) => {
1095
+ const globalOpts = cmd.optsWithGlobals();
1096
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
1097
+ const formatter = new OutputFormatter(mode);
1098
+ const result = await runFixDrift({
1099
+ configPath: globalOpts.config,
1100
+ dryRun: opts.dryRun,
1101
+ json: globalOpts.json,
1102
+ verbose: globalOpts.verbose,
1103
+ quiet: globalOpts.quiet
1104
+ });
1105
+ if (!result.ok) {
1106
+ if (mode === OutputMode.JSON) {
1107
+ console.log(JSON.stringify({ error: result.error.message }));
1108
+ } else {
1109
+ logger.error(result.error.message);
1110
+ }
1111
+ process.exit(result.error.exitCode);
1112
+ }
1113
+ if (mode === OutputMode.JSON) {
1114
+ console.log(JSON.stringify(result.value, null, 2));
1115
+ } else if (mode !== OutputMode.QUIET || result.value.fixes.length > 0 || result.value.suggestions.length > 0) {
1116
+ const { value } = result;
1117
+ const statusMessage = value.dryRun ? "(dry-run)" : "";
1118
+ console.log(formatter.formatSummary(
1119
+ `Fix drift ${statusMessage}`,
1120
+ `${value.fixes.length} fixes, ${value.suggestions.length} suggestions`,
1121
+ value.fixes.length === 0 && value.suggestions.length === 0
1122
+ ));
1123
+ if (value.fixes.length > 0) {
1124
+ console.log("\nFixes:");
1125
+ for (const fix of value.fixes.slice(0, 10)) {
1126
+ const status = fix.applied ? "[applied]" : "[pending]";
1127
+ console.log(` ${status} ${fix.action}: ${fix.file}`);
1128
+ }
1129
+ if (value.fixes.length > 10) {
1130
+ console.log(` ... and ${value.fixes.length - 10} more`);
1131
+ }
1132
+ }
1133
+ if (value.suggestions.length > 0 && (mode === OutputMode.VERBOSE || value.fixes.length === 0)) {
1134
+ console.log("\nSuggestions:");
1135
+ for (const suggestion of value.suggestions.slice(0, 10)) {
1136
+ console.log(` - ${suggestion.file}: ${suggestion.suggestion}`);
1137
+ }
1138
+ if (value.suggestions.length > 10) {
1139
+ console.log(` ... and ${value.suggestions.length - 10} more`);
1140
+ }
1141
+ }
1142
+ if (value.dryRun && value.fixes.length > 0) {
1143
+ console.log("\nRun with --no-dry-run to apply fixes.");
1144
+ }
1145
+ }
1146
+ process.exit(ExitCode.SUCCESS);
1147
+ });
1148
+ return command;
1149
+ }
1150
+
1151
+ // src/commands/agent/index.ts
1152
+ import { Command as Command9 } from "commander";
1153
+
1154
+ // src/commands/agent/run.ts
1155
+ import { Command as Command7 } from "commander";
1156
+ import * as path11 from "path";
1157
+ import * as childProcess from "child_process";
1158
+ import { Ok as Ok10, Err as Err8 } from "@harness-engineering/core";
1159
+ import { requestPeerReview } from "@harness-engineering/core";
1160
+
1161
+ // src/persona/loader.ts
1162
+ import * as fs5 from "fs";
1163
+ import * as path10 from "path";
1164
+ import YAML from "yaml";
1165
+ import { Ok as Ok9, Err as Err7 } from "@harness-engineering/core";
1166
+
1167
+ // src/persona/schema.ts
1168
+ import { z as z3 } from "zod";
1169
+ var PersonaTriggerSchema = z3.discriminatedUnion("event", [
1170
+ z3.object({
1171
+ event: z3.literal("on_pr"),
1172
+ conditions: z3.object({ paths: z3.array(z3.string()).optional() }).optional()
1173
+ }),
1174
+ z3.object({
1175
+ event: z3.literal("on_commit"),
1176
+ conditions: z3.object({ branches: z3.array(z3.string()).optional() }).optional()
1177
+ }),
1178
+ z3.object({
1179
+ event: z3.literal("scheduled"),
1180
+ cron: z3.string()
1181
+ })
1182
+ ]);
1183
+ var PersonaConfigSchema = z3.object({
1184
+ severity: z3.enum(["error", "warning"]).default("error"),
1185
+ autoFix: z3.boolean().default(false),
1186
+ timeout: z3.number().default(3e5)
1187
+ });
1188
+ var PersonaOutputsSchema = z3.object({
1189
+ "agents-md": z3.boolean().default(true),
1190
+ "ci-workflow": z3.boolean().default(true),
1191
+ "runtime-config": z3.boolean().default(true)
1192
+ });
1193
+ var PersonaSchema = z3.object({
1194
+ version: z3.literal(1),
1195
+ name: z3.string(),
1196
+ description: z3.string(),
1197
+ role: z3.string(),
1198
+ skills: z3.array(z3.string()),
1199
+ commands: z3.array(z3.string()),
1200
+ triggers: z3.array(PersonaTriggerSchema),
1201
+ config: PersonaConfigSchema.default({}),
1202
+ outputs: PersonaOutputsSchema.default({})
1203
+ });
1204
+
1205
+ // src/persona/loader.ts
1206
+ function loadPersona(filePath) {
1207
+ try {
1208
+ if (!fs5.existsSync(filePath)) {
1209
+ return Err7(new Error(`Persona file not found: ${filePath}`));
1210
+ }
1211
+ const raw = fs5.readFileSync(filePath, "utf-8");
1212
+ const parsed = YAML.parse(raw);
1213
+ const result = PersonaSchema.safeParse(parsed);
1214
+ if (!result.success) {
1215
+ return Err7(new Error(`Invalid persona ${filePath}: ${result.error.message}`));
1216
+ }
1217
+ return Ok9(result.data);
1218
+ } catch (error) {
1219
+ return Err7(new Error(`Failed to load persona: ${error instanceof Error ? error.message : String(error)}`));
1220
+ }
1221
+ }
1222
+ function listPersonas(dir) {
1223
+ try {
1224
+ if (!fs5.existsSync(dir)) return Ok9([]);
1225
+ const entries = fs5.readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
1226
+ const personas = [];
1227
+ for (const entry of entries) {
1228
+ const filePath = path10.join(dir, entry);
1229
+ const result = loadPersona(filePath);
1230
+ if (result.ok) {
1231
+ personas.push({ name: result.value.name, description: result.value.description, filePath });
1232
+ }
1233
+ }
1234
+ return Ok9(personas);
1235
+ } catch (error) {
1236
+ return Err7(new Error(`Failed to list personas: ${error instanceof Error ? error.message : String(error)}`));
1237
+ }
1238
+ }
1239
+
1240
+ // src/persona/runner.ts
1241
+ var TIMEOUT_ERROR_MESSAGE = "__PERSONA_RUNNER_TIMEOUT__";
1242
+ async function runPersona(persona, executor) {
1243
+ const startTime = Date.now();
1244
+ const timeout = persona.config.timeout;
1245
+ const report = {
1246
+ persona: persona.name.toLowerCase().replace(/\s+/g, "-"),
1247
+ status: "pass",
1248
+ commands: [],
1249
+ totalDurationMs: 0
1250
+ };
1251
+ for (let i = 0; i < persona.commands.length; i++) {
1252
+ const command = persona.commands[i];
1253
+ if (command === void 0) continue;
1254
+ if (Date.now() - startTime >= timeout) {
1255
+ for (let j = i; j < persona.commands.length; j++) {
1256
+ const remaining = persona.commands[j];
1257
+ if (remaining !== void 0) {
1258
+ report.commands.push({ name: remaining, status: "skipped", durationMs: 0 });
1259
+ }
1260
+ }
1261
+ report.status = "partial";
1262
+ break;
1263
+ }
1264
+ const cmdStart = Date.now();
1265
+ const remainingTime = timeout - (Date.now() - startTime);
1266
+ const result = await Promise.race([
1267
+ executor(command),
1268
+ new Promise(
1269
+ (resolve12) => setTimeout(() => resolve12({ ok: false, error: new Error(TIMEOUT_ERROR_MESSAGE) }), remainingTime)
1270
+ )
1271
+ ]);
1272
+ const durationMs = Date.now() - cmdStart;
1273
+ if (result.ok) {
1274
+ report.commands.push({ name: command, status: "pass", result: result.value, durationMs });
1275
+ } else if (result.error.message === TIMEOUT_ERROR_MESSAGE) {
1276
+ report.commands.push({ name: command, status: "skipped", error: "timed out", durationMs });
1277
+ report.status = "partial";
1278
+ for (let j = i + 1; j < persona.commands.length; j++) {
1279
+ const skipped = persona.commands[j];
1280
+ if (skipped !== void 0) {
1281
+ report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1282
+ }
1283
+ }
1284
+ break;
1285
+ } else {
1286
+ report.commands.push({ name: command, status: "fail", error: result.error.message, durationMs });
1287
+ report.status = "fail";
1288
+ for (let j = i + 1; j < persona.commands.length; j++) {
1289
+ const skipped = persona.commands[j];
1290
+ if (skipped !== void 0) {
1291
+ report.commands.push({ name: skipped, status: "skipped", durationMs: 0 });
1292
+ }
1293
+ }
1294
+ break;
1295
+ }
1296
+ }
1297
+ report.totalDurationMs = Date.now() - startTime;
1298
+ return report;
1299
+ }
1300
+
1301
+ // src/commands/agent/run.ts
1302
+ async function runAgentTask(task, options) {
1303
+ const configResult = resolveConfig(options.configPath);
1304
+ if (!configResult.ok) {
1305
+ return configResult;
1306
+ }
1307
+ const agentTypeMap = {
1308
+ review: "architecture-enforcer",
1309
+ "doc-review": "documentation-maintainer",
1310
+ "test-review": "test-reviewer"
1311
+ };
1312
+ const agentType = agentTypeMap[task];
1313
+ if (!agentType) {
1314
+ return Err8(
1315
+ new CLIError(
1316
+ `Unknown task: ${task}. Available: ${Object.keys(agentTypeMap).join(", ")}`,
1317
+ ExitCode.ERROR
1318
+ )
1319
+ );
1320
+ }
1321
+ const config = configResult.value;
1322
+ const timeout = options.timeout ?? config.agent?.timeout ?? 3e5;
1323
+ const reviewResult = await requestPeerReview(
1324
+ agentType,
1325
+ {
1326
+ files: [],
1327
+ diff: "",
1328
+ commitMessage: task,
1329
+ metadata: { task, timeout }
1330
+ },
1331
+ { timeout }
1332
+ );
1333
+ if (!reviewResult.ok) {
1334
+ return Err8(
1335
+ new CLIError(`Agent task failed: ${reviewResult.error.message}`, ExitCode.ERROR)
1336
+ );
1337
+ }
1338
+ const review = reviewResult.value;
1339
+ return Ok10({
1340
+ success: review.approved,
1341
+ output: review.approved ? `Agent task '${task}' completed successfully` : `Agent task '${task}' found issues:
1342
+ ${review.comments.map((c) => ` - ${c.message}`).join("\n")}`
1343
+ });
1344
+ }
1345
+ function createRunCommand() {
1346
+ return new Command7("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) => {
1347
+ const globalOpts = cmd.optsWithGlobals();
1348
+ if (opts.persona) {
1349
+ const personasDir = resolvePersonasDir();
1350
+ const filePath = path11.join(personasDir, `${opts.persona}.yaml`);
1351
+ const personaResult = loadPersona(filePath);
1352
+ if (!personaResult.ok) {
1353
+ logger.error(personaResult.error.message);
1354
+ process.exit(ExitCode.ERROR);
1355
+ }
1356
+ const persona = personaResult.value;
1357
+ const ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
1358
+ "validate",
1359
+ "check-deps",
1360
+ "check-docs",
1361
+ "cleanup",
1362
+ "fix-drift",
1363
+ "add"
1364
+ ]);
1365
+ const executor = async (command) => {
1366
+ if (!ALLOWED_COMMANDS.has(command)) {
1367
+ return Err8(new Error(`Unknown harness command: ${command}`));
1368
+ }
1369
+ try {
1370
+ childProcess.execFileSync("npx", ["harness", command], { stdio: "inherit" });
1371
+ return Ok10(null);
1372
+ } catch (error) {
1373
+ return Err8(new Error(error instanceof Error ? error.message : String(error)));
1374
+ }
1375
+ };
1376
+ const report = await runPersona(persona, executor);
1377
+ if (!globalOpts.quiet) {
1378
+ logger.info(`Persona '${report.persona}' status: ${report.status}`);
1379
+ for (const c of report.commands) {
1380
+ const icon = c.status === "pass" ? "v" : c.status === "fail" ? "x" : "-";
1381
+ console.log(` [${icon}] ${c.name} (${c.durationMs}ms)`);
1382
+ }
1383
+ }
1384
+ process.exit(report.status === "fail" ? ExitCode.ERROR : ExitCode.SUCCESS);
1385
+ }
1386
+ if (!task) {
1387
+ logger.error("Either a task argument or --persona flag is required.");
1388
+ process.exit(ExitCode.ERROR);
1389
+ }
1390
+ const result = await runAgentTask(task, {
1391
+ configPath: globalOpts.config,
1392
+ timeout: parseInt(opts.timeout, 10)
1393
+ });
1394
+ if (!result.ok) {
1395
+ logger.error(result.error.message);
1396
+ process.exit(result.error.exitCode);
1397
+ }
1398
+ logger.success(result.value.output);
1399
+ process.exit(ExitCode.SUCCESS);
1400
+ });
1401
+ }
1402
+
1403
+ // src/commands/agent/review.ts
1404
+ import { Command as Command8 } from "commander";
1405
+ import { execSync } from "child_process";
1406
+ import { Ok as Ok11, Err as Err9 } from "@harness-engineering/core";
1407
+ import { createSelfReview, parseDiff } from "@harness-engineering/core";
1408
+ async function runAgentReview(options) {
1409
+ const configResult = resolveConfig(options.configPath);
1410
+ if (!configResult.ok) {
1411
+ return configResult;
1412
+ }
1413
+ const config = configResult.value;
1414
+ let diff;
1415
+ try {
1416
+ diff = execSync("git diff --cached", { encoding: "utf-8" });
1417
+ if (!diff) {
1418
+ diff = execSync("git diff", { encoding: "utf-8" });
1419
+ }
1420
+ } catch {
1421
+ return Err9(new CLIError("Failed to get git diff", ExitCode.ERROR));
1422
+ }
1423
+ if (!diff) {
1424
+ return Ok11({
1425
+ passed: true,
1426
+ checklist: [{ check: "No changes to review", passed: true }]
1427
+ });
1428
+ }
1429
+ const parsedDiffResult = parseDiff(diff);
1430
+ if (!parsedDiffResult.ok) {
1431
+ return Err9(new CLIError(parsedDiffResult.error.message, ExitCode.ERROR));
1432
+ }
1433
+ const codeChanges = parsedDiffResult.value;
1434
+ const review = await createSelfReview(codeChanges, {
1435
+ rootDir: config.rootDir,
1436
+ diffAnalysis: {
1437
+ enabled: true,
1438
+ checkTestCoverage: true
1439
+ }
1440
+ });
1441
+ if (!review.ok) {
1442
+ return Err9(new CLIError(review.error.message, ExitCode.ERROR));
1443
+ }
1444
+ return Ok11({
1445
+ passed: review.value.passed,
1446
+ checklist: review.value.items.map((item) => ({
1447
+ check: item.check,
1448
+ passed: item.passed,
1449
+ details: item.details
1450
+ }))
1451
+ });
1452
+ }
1453
+ function createReviewCommand() {
1454
+ return new Command8("review").description("Run self-review on current changes").action(async (_opts, cmd) => {
1455
+ const globalOpts = cmd.optsWithGlobals();
1456
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : OutputMode.TEXT;
1457
+ const result = await runAgentReview({
1458
+ configPath: globalOpts.config,
1459
+ json: globalOpts.json,
1460
+ verbose: globalOpts.verbose,
1461
+ quiet: globalOpts.quiet
1462
+ });
1463
+ if (!result.ok) {
1464
+ if (mode === OutputMode.JSON) {
1465
+ console.log(JSON.stringify({ error: result.error.message }));
1466
+ } else {
1467
+ logger.error(result.error.message);
1468
+ }
1469
+ process.exit(result.error.exitCode);
1470
+ }
1471
+ if (mode === OutputMode.JSON) {
1472
+ console.log(JSON.stringify(result.value, null, 2));
1473
+ } else if (mode !== OutputMode.QUIET) {
1474
+ console.log(result.value.passed ? "v Self-review passed" : "x Self-review found issues");
1475
+ console.log("");
1476
+ for (const item of result.value.checklist) {
1477
+ const icon = item.passed ? "v" : "x";
1478
+ console.log(` ${icon} ${item.check}`);
1479
+ if (item.details && !item.passed) {
1480
+ console.log(` ${item.details}`);
1481
+ }
1482
+ }
1483
+ }
1484
+ process.exit(result.value.passed ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
1485
+ });
1486
+ }
1487
+
1488
+ // src/commands/agent/index.ts
1489
+ function createAgentCommand() {
1490
+ const command = new Command9("agent").description("Agent orchestration commands");
1491
+ command.addCommand(createRunCommand());
1492
+ command.addCommand(createReviewCommand());
1493
+ return command;
1494
+ }
1495
+
1496
+ // src/commands/add.ts
1497
+ import { Command as Command10 } from "commander";
1498
+ import * as fs6 from "fs";
1499
+ import * as path12 from "path";
1500
+ import { Ok as Ok12, Err as Err10 } from "@harness-engineering/core";
1501
+ var LAYER_INDEX_TEMPLATE = (name) => `// ${name} layer
1502
+ // Add your ${name} exports here
1503
+
1504
+ export {};
1505
+ `;
1506
+ var MODULE_TEMPLATE = (name) => `/**
1507
+ * ${name} module
1508
+ */
1509
+
1510
+ export function ${name}() {
1511
+ // TODO: Implement
1512
+ }
1513
+ `;
1514
+ var DOC_TEMPLATE = (name) => `# ${name}
1515
+
1516
+ ## Overview
1517
+
1518
+ TODO: Add overview
1519
+
1520
+ ## Usage
1521
+
1522
+ TODO: Add usage examples
1523
+ `;
1524
+ async function runAdd(componentType, name, options) {
1525
+ const cwd = options.cwd ?? process.cwd();
1526
+ const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
1527
+ if (!name || !NAME_PATTERN.test(name)) {
1528
+ return Err10(new CLIError(
1529
+ "Invalid name. Must start with a letter and contain only alphanumeric characters, hyphens, and underscores.",
1530
+ ExitCode.ERROR
1531
+ ));
1532
+ }
1533
+ const configResult = resolveConfig(options.configPath);
1534
+ if (!configResult.ok) {
1535
+ }
1536
+ const created = [];
1537
+ try {
1538
+ switch (componentType) {
1539
+ case "layer": {
1540
+ const layerDir = path12.join(cwd, "src", name);
1541
+ if (!fs6.existsSync(layerDir)) {
1542
+ fs6.mkdirSync(layerDir, { recursive: true });
1543
+ created.push(`src/${name}/`);
1544
+ }
1545
+ const indexPath = path12.join(layerDir, "index.ts");
1546
+ if (!fs6.existsSync(indexPath)) {
1547
+ fs6.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
1548
+ created.push(`src/${name}/index.ts`);
1549
+ }
1550
+ break;
1551
+ }
1552
+ case "module": {
1553
+ const modulePath = path12.join(cwd, "src", `${name}.ts`);
1554
+ if (fs6.existsSync(modulePath)) {
1555
+ return Err10(new CLIError(`Module ${name} already exists`, ExitCode.ERROR));
1556
+ }
1557
+ fs6.writeFileSync(modulePath, MODULE_TEMPLATE(name));
1558
+ created.push(`src/${name}.ts`);
1559
+ break;
1560
+ }
1561
+ case "doc": {
1562
+ const docsDir = path12.join(cwd, "docs");
1563
+ if (!fs6.existsSync(docsDir)) {
1564
+ fs6.mkdirSync(docsDir, { recursive: true });
1565
+ }
1566
+ const docPath = path12.join(docsDir, `${name}.md`);
1567
+ if (fs6.existsSync(docPath)) {
1568
+ return Err10(new CLIError(`Doc ${name} already exists`, ExitCode.ERROR));
1569
+ }
1570
+ fs6.writeFileSync(docPath, DOC_TEMPLATE(name));
1571
+ created.push(`docs/${name}.md`);
1572
+ break;
1573
+ }
1574
+ default:
1575
+ return Err10(new CLIError(
1576
+ `Unknown component type: ${componentType}. Use: layer, module, doc`,
1577
+ ExitCode.ERROR
1578
+ ));
1579
+ }
1580
+ return Ok12({ created });
1581
+ } catch (error) {
1582
+ return Err10(new CLIError(
1583
+ `Failed to add ${componentType}: ${error instanceof Error ? error.message : "Unknown error"}`,
1584
+ ExitCode.ERROR
1585
+ ));
1586
+ }
1587
+ }
1588
+ function createAddCommand() {
1589
+ const command = new Command10("add").description("Add a component to the project").argument("<type>", "Component type (layer, module, doc)").argument("<name>", "Component name").action(async (type, name, _opts, cmd) => {
1590
+ const globalOpts = cmd.optsWithGlobals();
1591
+ const result = await runAdd(type, name, {
1592
+ configPath: globalOpts.config
1593
+ });
1594
+ if (!result.ok) {
1595
+ logger.error(result.error.message);
1596
+ process.exit(result.error.exitCode);
1597
+ }
1598
+ if (!globalOpts.quiet) {
1599
+ logger.success(`Added ${type}: ${name}`);
1600
+ for (const file of result.value.created) {
1601
+ console.log(` + ${file}`);
1602
+ }
1603
+ }
1604
+ process.exit(ExitCode.SUCCESS);
1605
+ });
1606
+ return command;
1607
+ }
1608
+
1609
+ // src/commands/linter/index.ts
1610
+ import { Command as Command13 } from "commander";
1611
+
1612
+ // src/commands/linter/generate.ts
1613
+ import { Command as Command11 } from "commander";
1614
+ import { generate } from "@harness-engineering/linter-gen";
1615
+ function createGenerateCommand() {
1616
+ return new Command11("generate").description("Generate ESLint rules from harness-linter.yml").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("-o, --output <dir>", "Override output directory").option("--clean", "Remove existing files before generating").option("--dry-run", "Preview without writing files").option("--json", "Output as JSON").option("--verbose", "Show detailed output").action(async (options) => {
1617
+ const formatter = new OutputFormatter(
1618
+ options.json ? OutputMode.JSON : OutputMode.TEXT
1619
+ );
1620
+ try {
1621
+ if (options.verbose) {
1622
+ logger.info(`Parsing config: ${options.config}`);
1623
+ }
1624
+ const result = await generate({
1625
+ configPath: options.config,
1626
+ outputDir: options.output,
1627
+ clean: options.clean,
1628
+ dryRun: options.dryRun
1629
+ });
1630
+ if (!result.success) {
1631
+ const errorMessages = result.errors.map((e) => {
1632
+ switch (e.type) {
1633
+ case "parse":
1634
+ return `Config error: ${e.error.message}`;
1635
+ case "template":
1636
+ return `Template error for '${e.ruleName}': ${e.error.message}`;
1637
+ case "render":
1638
+ return `Render error for '${e.ruleName}': ${e.error.message}`;
1639
+ case "write":
1640
+ return `Write error for '${e.path}': ${e.error.message}`;
1641
+ }
1642
+ });
1643
+ if (options.json) {
1644
+ console.log(JSON.stringify({ success: false, errors: errorMessages }, null, 2));
1645
+ } else {
1646
+ errorMessages.forEach((msg) => logger.error(msg));
1647
+ }
1648
+ process.exit(ExitCode.VALIDATION_FAILED);
1649
+ }
1650
+ if (options.json) {
1651
+ console.log(JSON.stringify({
1652
+ success: true,
1653
+ outputDir: result.outputDir,
1654
+ rulesGenerated: result.rulesGenerated,
1655
+ dryRun: result.dryRun
1656
+ }, null, 2));
1657
+ } else {
1658
+ if (result.dryRun) {
1659
+ logger.info("Dry run - no files written");
1660
+ }
1661
+ result.rulesGenerated.forEach((name) => {
1662
+ logger.success(`Generated ${name}.ts`);
1663
+ });
1664
+ logger.success(`Generated index.ts`);
1665
+ logger.info(`
1666
+ Generated ${result.rulesGenerated.length} rules to ${result.outputDir}`);
1667
+ }
1668
+ } catch (err) {
1669
+ throw new CLIError(`Generation failed: ${err.message}`, ExitCode.ERROR);
1670
+ }
1671
+ });
1672
+ }
1673
+
1674
+ // src/commands/linter/validate.ts
1675
+ import { Command as Command12 } from "commander";
1676
+ import { validate } from "@harness-engineering/linter-gen";
1677
+ function createValidateCommand2() {
1678
+ return new Command12("validate").description("Validate harness-linter.yml config").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("--json", "Output as JSON").action(async (options) => {
1679
+ try {
1680
+ const result = await validate({ configPath: options.config });
1681
+ if (options.json) {
1682
+ console.log(JSON.stringify(result, null, 2));
1683
+ } else if (result.success) {
1684
+ logger.success(`Config valid: ${result.ruleCount} rules defined`);
1685
+ } else {
1686
+ logger.error(`Config invalid: ${result.error.message}`);
1687
+ process.exit(ExitCode.VALIDATION_FAILED);
1688
+ }
1689
+ } catch (err) {
1690
+ throw new CLIError(`Validation failed: ${err.message}`, ExitCode.ERROR);
1691
+ }
1692
+ });
1693
+ }
1694
+
1695
+ // src/commands/linter/index.ts
1696
+ function createLinterCommand() {
1697
+ const linter = new Command13("linter").description("Generate and validate ESLint rules from YAML config");
1698
+ linter.addCommand(createGenerateCommand());
1699
+ linter.addCommand(createValidateCommand2());
1700
+ return linter;
1701
+ }
1702
+
1703
+ // src/commands/persona/index.ts
1704
+ import { Command as Command16 } from "commander";
1705
+
1706
+ // src/commands/persona/list.ts
1707
+ import { Command as Command14 } from "commander";
1708
+ function createListCommand() {
1709
+ return new Command14("list").description("List available agent personas").action(async (_opts, cmd) => {
1710
+ const globalOpts = cmd.optsWithGlobals();
1711
+ const personasDir = resolvePersonasDir();
1712
+ const result = listPersonas(personasDir);
1713
+ if (!result.ok) {
1714
+ logger.error(result.error.message);
1715
+ process.exit(ExitCode.ERROR);
1716
+ }
1717
+ if (globalOpts.json) {
1718
+ logger.raw(result.value);
1719
+ } else if (globalOpts.quiet) {
1720
+ for (const p of result.value) console.log(p.name);
1721
+ } else {
1722
+ if (result.value.length === 0) {
1723
+ logger.info("No personas found.");
1724
+ } else {
1725
+ console.log("Available personas:\n");
1726
+ for (const p of result.value) {
1727
+ console.log(` ${p.name}`);
1728
+ console.log(` ${p.description}
1729
+ `);
1730
+ }
1731
+ }
1732
+ }
1733
+ process.exit(ExitCode.SUCCESS);
1734
+ });
1735
+ }
1736
+
1737
+ // src/commands/persona/generate.ts
1738
+ import { Command as Command15 } from "commander";
1739
+ import * as fs7 from "fs";
1740
+ import * as path13 from "path";
1741
+
1742
+ // src/persona/generators/runtime.ts
1743
+ import { Ok as Ok13, Err as Err11 } from "@harness-engineering/core";
1744
+
1745
+ // src/utils/string.ts
1746
+ function toKebabCase(name) {
1747
+ return name.toLowerCase().replace(/\s+/g, "-");
1748
+ }
1749
+
1750
+ // src/persona/generators/runtime.ts
1751
+ function generateRuntime(persona) {
1752
+ try {
1753
+ const config = {
1754
+ name: toKebabCase(persona.name),
1755
+ skills: persona.skills,
1756
+ commands: persona.commands,
1757
+ timeout: persona.config.timeout,
1758
+ severity: persona.config.severity
1759
+ };
1760
+ return Ok13(JSON.stringify(config, null, 2));
1761
+ } catch (error) {
1762
+ return Err11(new Error(`Failed to generate runtime config: ${error instanceof Error ? error.message : String(error)}`));
1763
+ }
1764
+ }
1765
+
1766
+ // src/persona/generators/agents-md.ts
1767
+ import { Ok as Ok14, Err as Err12 } from "@harness-engineering/core";
1768
+ function formatTrigger(trigger) {
1769
+ switch (trigger.event) {
1770
+ case "on_pr": {
1771
+ const paths = trigger.conditions?.paths?.join(", ") ?? "all files";
1772
+ return `On PR (${paths})`;
1773
+ }
1774
+ case "on_commit": {
1775
+ const branches = trigger.conditions?.branches?.join(", ") ?? "all branches";
1776
+ return `On commit (${branches})`;
1777
+ }
1778
+ case "scheduled":
1779
+ return `Scheduled (cron: ${trigger.cron})`;
1780
+ }
1781
+ }
1782
+ function generateAgentsMd(persona) {
1783
+ try {
1784
+ const triggers = persona.triggers.map(formatTrigger).join(", ");
1785
+ const skills = persona.skills.join(", ");
1786
+ const commands = persona.commands.map((c) => `\`harness ${c}\``).join(", ");
1787
+ const fragment = `## ${persona.name} Agent
1788
+
1789
+ **Role:** ${persona.role}
1790
+
1791
+ **Triggers:** ${triggers}
1792
+
1793
+ **Skills:** ${skills}
1794
+
1795
+ **When this agent flags an issue:** Fix violations before merging. Run ${commands} locally to validate.
1796
+ `;
1797
+ return Ok14(fragment);
1798
+ } catch (error) {
1799
+ return Err12(new Error(`Failed to generate AGENTS.md fragment: ${error instanceof Error ? error.message : String(error)}`));
1800
+ }
1801
+ }
1802
+
1803
+ // src/persona/generators/ci-workflow.ts
1804
+ import YAML2 from "yaml";
1805
+ import { Ok as Ok15, Err as Err13 } from "@harness-engineering/core";
1806
+ function buildGitHubTriggers(triggers) {
1807
+ const on = {};
1808
+ for (const trigger of triggers) {
1809
+ switch (trigger.event) {
1810
+ case "on_pr": {
1811
+ const prConfig = {};
1812
+ if (trigger.conditions?.paths) prConfig.paths = trigger.conditions.paths;
1813
+ on.pull_request = prConfig;
1814
+ break;
1815
+ }
1816
+ case "on_commit": {
1817
+ const pushConfig = {};
1818
+ if (trigger.conditions?.branches) pushConfig.branches = trigger.conditions.branches;
1819
+ on.push = pushConfig;
1820
+ break;
1821
+ }
1822
+ case "scheduled":
1823
+ on.schedule = [{ cron: trigger.cron }];
1824
+ break;
1825
+ }
1826
+ }
1827
+ return on;
1828
+ }
1829
+ function generateCIWorkflow(persona, platform) {
1830
+ try {
1831
+ if (platform === "gitlab") return Err13(new Error("GitLab CI generation is not yet supported"));
1832
+ const severity = persona.config.severity;
1833
+ const steps = [
1834
+ { uses: "actions/checkout@v4" },
1835
+ { uses: "actions/setup-node@v4", with: { "node-version": "20" } },
1836
+ { uses: "pnpm/action-setup@v4", with: { run_install: "frozen" } }
1837
+ ];
1838
+ for (const cmd of persona.commands) {
1839
+ const severityFlag = severity ? ` --severity ${severity}` : "";
1840
+ steps.push({ run: `npx harness ${cmd}${severityFlag}` });
1841
+ }
1842
+ const workflow = {
1843
+ name: persona.name,
1844
+ on: buildGitHubTriggers(persona.triggers),
1845
+ jobs: {
1846
+ enforce: {
1847
+ "runs-on": "ubuntu-latest",
1848
+ steps
1849
+ }
1850
+ }
1851
+ };
1852
+ return Ok15(YAML2.stringify(workflow, { lineWidth: 0 }));
1853
+ } catch (error) {
1854
+ return Err13(new Error(`Failed to generate CI workflow: ${error instanceof Error ? error.message : String(error)}`));
1855
+ }
1856
+ }
1857
+
1858
+ // src/commands/persona/generate.ts
1859
+ function createGenerateCommand2() {
1860
+ return new Command15("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) => {
1861
+ const globalOpts = cmd.optsWithGlobals();
1862
+ const personasDir = resolvePersonasDir();
1863
+ const filePath = path13.join(personasDir, `${name}.yaml`);
1864
+ const personaResult = loadPersona(filePath);
1865
+ if (!personaResult.ok) {
1866
+ logger.error(personaResult.error.message);
1867
+ process.exit(ExitCode.ERROR);
1868
+ }
1869
+ const persona = personaResult.value;
1870
+ const outputDir = path13.resolve(opts.outputDir);
1871
+ const slug = toKebabCase(persona.name);
1872
+ const only = opts.only;
1873
+ const generated = [];
1874
+ if (!only || only === "runtime") {
1875
+ const result = generateRuntime(persona);
1876
+ if (result.ok) {
1877
+ const outPath = path13.join(outputDir, `${slug}.runtime.json`);
1878
+ fs7.mkdirSync(path13.dirname(outPath), { recursive: true });
1879
+ fs7.writeFileSync(outPath, result.value);
1880
+ generated.push(outPath);
1881
+ }
1882
+ }
1883
+ if (!only || only === "agents-md") {
1884
+ const result = generateAgentsMd(persona);
1885
+ if (result.ok) {
1886
+ const outPath = path13.join(outputDir, `${slug}.agents.md`);
1887
+ fs7.writeFileSync(outPath, result.value);
1888
+ generated.push(outPath);
1889
+ }
1890
+ }
1891
+ if (!only || only === "ci") {
1892
+ const result = generateCIWorkflow(persona, "github");
1893
+ if (result.ok) {
1894
+ const outPath = path13.join(outputDir, ".github", "workflows", `${slug}.yml`);
1895
+ fs7.mkdirSync(path13.dirname(outPath), { recursive: true });
1896
+ fs7.writeFileSync(outPath, result.value);
1897
+ generated.push(outPath);
1898
+ }
1899
+ }
1900
+ if (!globalOpts.quiet) {
1901
+ logger.success(`Generated ${generated.length} artifacts for ${persona.name}:`);
1902
+ for (const f of generated) console.log(` - ${f}`);
1903
+ }
1904
+ process.exit(ExitCode.SUCCESS);
1905
+ });
1906
+ }
1907
+
1908
+ // src/commands/persona/index.ts
1909
+ function createPersonaCommand() {
1910
+ const command = new Command16("persona").description("Agent persona management commands");
1911
+ command.addCommand(createListCommand());
1912
+ command.addCommand(createGenerateCommand2());
1913
+ return command;
1914
+ }
1915
+
1916
+ // src/commands/skill/index.ts
1917
+ import { Command as Command21 } from "commander";
1918
+
1919
+ // src/commands/skill/list.ts
1920
+ import { Command as Command17 } from "commander";
1921
+ import * as fs8 from "fs";
1922
+ import * as path14 from "path";
1923
+ import { parse as parse2 } from "yaml";
1924
+
1925
+ // src/skill/schema.ts
1926
+ import { z as z4 } from "zod";
1927
+ var SkillPhaseSchema = z4.object({
1928
+ name: z4.string(),
1929
+ description: z4.string(),
1930
+ required: z4.boolean().default(true)
1931
+ });
1932
+ var SkillCliSchema = z4.object({
1933
+ command: z4.string(),
1934
+ args: z4.array(z4.object({
1935
+ name: z4.string(),
1936
+ description: z4.string(),
1937
+ required: z4.boolean().default(false)
1938
+ })).default([])
1939
+ });
1940
+ var SkillMcpSchema = z4.object({
1941
+ tool: z4.string(),
1942
+ input: z4.record(z4.string())
1943
+ });
1944
+ var SkillStateSchema = z4.object({
1945
+ persistent: z4.boolean().default(false),
1946
+ files: z4.array(z4.string()).default([])
1947
+ });
1948
+ var ALLOWED_TRIGGERS = [
1949
+ "manual",
1950
+ "on_pr",
1951
+ "on_commit",
1952
+ "on_new_feature",
1953
+ "on_bug_fix",
1954
+ "on_refactor",
1955
+ "on_project_init",
1956
+ "on_review"
1957
+ ];
1958
+ var ALLOWED_PLATFORMS = ["claude-code", "gemini-cli"];
1959
+ var SkillMetadataSchema = z4.object({
1960
+ name: z4.string().regex(/^[a-z][a-z0-9-]*$/, "Name must be lowercase with hyphens"),
1961
+ version: z4.string().regex(/^\d+\.\d+\.\d+$/, "Version must be semver format"),
1962
+ description: z4.string(),
1963
+ triggers: z4.array(z4.enum(ALLOWED_TRIGGERS)),
1964
+ platforms: z4.array(z4.enum(ALLOWED_PLATFORMS)),
1965
+ tools: z4.array(z4.string()),
1966
+ cli: SkillCliSchema.optional(),
1967
+ mcp: SkillMcpSchema.optional(),
1968
+ type: z4.enum(["rigid", "flexible"]),
1969
+ phases: z4.array(SkillPhaseSchema).optional(),
1970
+ state: SkillStateSchema.default({}),
1971
+ depends_on: z4.array(z4.string()).default([])
1972
+ });
1973
+
1974
+ // src/commands/skill/list.ts
1975
+ function createListCommand2() {
1976
+ return new Command17("list").description("List available skills").action(async (_opts, cmd) => {
1977
+ const globalOpts = cmd.optsWithGlobals();
1978
+ const skillsDir = resolveSkillsDir();
1979
+ if (!fs8.existsSync(skillsDir)) {
1980
+ logger.info("No skills directory found.");
1981
+ process.exit(ExitCode.SUCCESS);
1982
+ return;
1983
+ }
1984
+ const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1985
+ const skills = [];
1986
+ for (const name of entries) {
1987
+ const yamlPath = path14.join(skillsDir, name, "skill.yaml");
1988
+ if (!fs8.existsSync(yamlPath)) continue;
1989
+ try {
1990
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
1991
+ const parsed = parse2(raw);
1992
+ const result = SkillMetadataSchema.safeParse(parsed);
1993
+ if (result.success) {
1994
+ skills.push(result.data);
1995
+ }
1996
+ } catch {
1997
+ }
1998
+ }
1999
+ if (globalOpts.json) {
2000
+ logger.raw(skills);
2001
+ } else if (globalOpts.quiet) {
2002
+ for (const s of skills) console.log(s.name);
2003
+ } else {
2004
+ if (skills.length === 0) {
2005
+ logger.info("No skills found.");
2006
+ } else {
2007
+ console.log("Available skills:\n");
2008
+ for (const s of skills) {
2009
+ console.log(` ${s.name} (${s.type})`);
2010
+ console.log(` ${s.description}
2011
+ `);
2012
+ }
2013
+ }
2014
+ }
2015
+ process.exit(ExitCode.SUCCESS);
2016
+ });
2017
+ }
2018
+
2019
+ // src/commands/skill/run.ts
2020
+ import { Command as Command18 } from "commander";
2021
+ import * as fs9 from "fs";
2022
+ import * as path15 from "path";
2023
+ import { parse as parse3 } from "yaml";
2024
+
2025
+ // src/skill/complexity.ts
2026
+ import { execSync as execSync2 } from "child_process";
2027
+ function evaluateSignals(signals) {
2028
+ if (signals.fileCount >= 3) return "full";
2029
+ if (signals.newDir) return "full";
2030
+ if (signals.newDep) return "full";
2031
+ if (signals.fileCount <= 1) return "light";
2032
+ if (signals.testOnly) return "light";
2033
+ if (signals.docsOnly) return "light";
2034
+ return "full";
2035
+ }
2036
+ function detectComplexity(projectPath) {
2037
+ try {
2038
+ const base = execSync2("git merge-base HEAD main", {
2039
+ cwd: projectPath,
2040
+ encoding: "utf-8",
2041
+ stdio: ["pipe", "pipe", "pipe"]
2042
+ }).trim();
2043
+ const diffFiles = execSync2(`git diff --name-only ${base}`, {
2044
+ cwd: projectPath,
2045
+ encoding: "utf-8",
2046
+ stdio: ["pipe", "pipe", "pipe"]
2047
+ }).trim().split("\n").filter(Boolean);
2048
+ const diffStat = execSync2(`git diff --stat ${base}`, {
2049
+ cwd: projectPath,
2050
+ encoding: "utf-8",
2051
+ stdio: ["pipe", "pipe", "pipe"]
2052
+ }).trim();
2053
+ const signals = {
2054
+ fileCount: diffFiles.length,
2055
+ testOnly: diffFiles.every((f) => f.match(/\.(test|spec)\./)),
2056
+ docsOnly: diffFiles.every((f) => f.startsWith("docs/") || f.endsWith(".md")),
2057
+ newDir: diffStat.includes("create mode") || diffFiles.some((f) => {
2058
+ const parts = f.split("/");
2059
+ return parts.length > 1;
2060
+ }),
2061
+ newDep: diffFiles.some((f) => ["package.json", "Cargo.toml", "go.mod", "requirements.txt"].includes(f))
2062
+ };
2063
+ return evaluateSignals(signals);
2064
+ } catch {
2065
+ return "full";
2066
+ }
2067
+ }
2068
+
2069
+ // src/commands/skill/preamble.ts
2070
+ function buildPreamble(options) {
2071
+ const sections = [];
2072
+ if (options.complexity && options.phases && options.phases.length > 0) {
2073
+ const lines = [`## Active Phases (complexity: ${options.complexity})`];
2074
+ for (const phase of options.phases) {
2075
+ if (options.complexity === "light" && !phase.required) {
2076
+ lines.push(`- ~~${phase.name.toUpperCase()}~~ (skipped in light mode)`);
2077
+ } else {
2078
+ lines.push(`- ${phase.name.toUpperCase()} (${phase.required ? "required" : "optional"})`);
2079
+ }
2080
+ }
2081
+ sections.push(lines.join("\n"));
2082
+ }
2083
+ if (options.principles) {
2084
+ sections.push(`## Project Principles (from docs/principles.md)
2085
+ ${options.principles}`);
2086
+ }
2087
+ if (options.phase) {
2088
+ const lines = [`## Resuming at Phase: ${options.phase}`];
2089
+ if (options.priorState) {
2090
+ lines.push(`## Prior state loaded
2091
+ ${options.priorState}`);
2092
+ }
2093
+ if (options.stateWarning) {
2094
+ lines.push(`> ${options.stateWarning}`);
2095
+ }
2096
+ sections.push(lines.join("\n"));
2097
+ }
2098
+ if (options.party) {
2099
+ sections.push("## Party Mode: Active\nEvaluate each approach from multiple contextually relevant perspectives before converging on a recommendation.");
2100
+ }
2101
+ return sections.length > 0 ? sections.join("\n\n---\n\n") + "\n\n---\n\n" : "";
2102
+ }
2103
+
2104
+ // src/commands/skill/run.ts
2105
+ function createRunCommand2() {
2106
+ return new Command18("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) => {
2107
+ const _globalOpts = cmd.optsWithGlobals();
2108
+ const skillsDir = resolveSkillsDir();
2109
+ const skillDir = path15.join(skillsDir, name);
2110
+ if (!fs9.existsSync(skillDir)) {
2111
+ logger.error(`Skill not found: ${name}`);
2112
+ process.exit(ExitCode.ERROR);
2113
+ return;
2114
+ }
2115
+ const yamlPath = path15.join(skillDir, "skill.yaml");
2116
+ let metadata = null;
2117
+ if (fs9.existsSync(yamlPath)) {
2118
+ try {
2119
+ const raw = fs9.readFileSync(yamlPath, "utf-8");
2120
+ const parsed = parse3(raw);
2121
+ const result = SkillMetadataSchema.safeParse(parsed);
2122
+ if (result.success) metadata = result.data;
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ let complexity;
2127
+ if (metadata?.phases && metadata.phases.length > 0) {
2128
+ const requested = opts.complexity ?? "auto";
2129
+ if (requested === "auto") {
2130
+ const projectPath2 = opts.path ? path15.resolve(opts.path) : process.cwd();
2131
+ complexity = detectComplexity(projectPath2);
2132
+ } else {
2133
+ complexity = requested;
2134
+ }
2135
+ }
2136
+ let principles;
2137
+ const projectPath = opts.path ? path15.resolve(opts.path) : process.cwd();
2138
+ const principlesPath = path15.join(projectPath, "docs", "principles.md");
2139
+ if (fs9.existsSync(principlesPath)) {
2140
+ principles = fs9.readFileSync(principlesPath, "utf-8");
2141
+ }
2142
+ let priorState;
2143
+ let stateWarning;
2144
+ if (opts.phase) {
2145
+ if (metadata?.phases) {
2146
+ const validPhases = metadata.phases.map((p) => p.name);
2147
+ if (!validPhases.includes(opts.phase)) {
2148
+ logger.error(`Unknown phase: ${opts.phase}. Valid phases: ${validPhases.join(", ")}`);
2149
+ process.exit(ExitCode.ERROR);
2150
+ return;
2151
+ }
2152
+ }
2153
+ if (metadata?.state.persistent && metadata.state.files.length > 0) {
2154
+ for (const stateFilePath of metadata.state.files) {
2155
+ const fullPath = path15.join(projectPath, stateFilePath);
2156
+ if (fs9.existsSync(fullPath)) {
2157
+ const stat = fs9.statSync(fullPath);
2158
+ if (stat.isDirectory()) {
2159
+ const files = fs9.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs9.statSync(path15.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2160
+ if (files.length > 0) {
2161
+ priorState = fs9.readFileSync(path15.join(fullPath, files[0].name), "utf-8");
2162
+ }
2163
+ } else {
2164
+ priorState = fs9.readFileSync(fullPath, "utf-8");
2165
+ }
2166
+ break;
2167
+ }
2168
+ }
2169
+ if (!priorState) {
2170
+ stateWarning = "No prior phase data found. Earlier phases have not been completed. Proceed with caution.";
2171
+ }
2172
+ }
2173
+ }
2174
+ const preamble = buildPreamble({
2175
+ complexity,
2176
+ phases: metadata?.phases,
2177
+ principles,
2178
+ phase: opts.phase,
2179
+ priorState,
2180
+ stateWarning,
2181
+ party: opts.party
2182
+ });
2183
+ const skillMdPath = path15.join(skillDir, "SKILL.md");
2184
+ if (!fs9.existsSync(skillMdPath)) {
2185
+ logger.error(`SKILL.md not found for skill: ${name}`);
2186
+ process.exit(ExitCode.ERROR);
2187
+ return;
2188
+ }
2189
+ let content = fs9.readFileSync(skillMdPath, "utf-8");
2190
+ if (metadata?.state.persistent && opts.path) {
2191
+ const stateFile = path15.join(projectPath, ".harness", "state.json");
2192
+ if (fs9.existsSync(stateFile)) {
2193
+ const stateContent = fs9.readFileSync(stateFile, "utf-8");
2194
+ content += `
2195
+
2196
+ ---
2197
+ ## Project State
2198
+ \`\`\`json
2199
+ ${stateContent}
2200
+ \`\`\`
2201
+ `;
2202
+ }
2203
+ }
2204
+ process.stdout.write(preamble + content);
2205
+ process.exit(ExitCode.SUCCESS);
2206
+ });
2207
+ }
2208
+
2209
+ // src/commands/skill/validate.ts
2210
+ import { Command as Command19 } from "commander";
2211
+ import * as fs10 from "fs";
2212
+ import * as path16 from "path";
2213
+ import { parse as parse4 } from "yaml";
2214
+ var REQUIRED_SECTIONS = [
2215
+ "## When to Use",
2216
+ "## Process",
2217
+ "## Harness Integration",
2218
+ "## Success Criteria",
2219
+ "## Examples"
2220
+ ];
2221
+ function createValidateCommand3() {
2222
+ return new Command19("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
2223
+ const globalOpts = cmd.optsWithGlobals();
2224
+ const skillsDir = resolveSkillsDir();
2225
+ if (!fs10.existsSync(skillsDir)) {
2226
+ logger.info("No skills directory found.");
2227
+ process.exit(ExitCode.SUCCESS);
2228
+ return;
2229
+ }
2230
+ const entries = fs10.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2231
+ const errors = [];
2232
+ let validated = 0;
2233
+ for (const name of entries) {
2234
+ const skillDir = path16.join(skillsDir, name);
2235
+ const yamlPath = path16.join(skillDir, "skill.yaml");
2236
+ const skillMdPath = path16.join(skillDir, "SKILL.md");
2237
+ if (!fs10.existsSync(yamlPath)) {
2238
+ errors.push(`${name}: missing skill.yaml`);
2239
+ continue;
2240
+ }
2241
+ try {
2242
+ const raw = fs10.readFileSync(yamlPath, "utf-8");
2243
+ const parsed = parse4(raw);
2244
+ const result = SkillMetadataSchema.safeParse(parsed);
2245
+ if (!result.success) {
2246
+ errors.push(`${name}/skill.yaml: ${result.error.message}`);
2247
+ continue;
2248
+ }
2249
+ if (fs10.existsSync(skillMdPath)) {
2250
+ const mdContent = fs10.readFileSync(skillMdPath, "utf-8");
2251
+ for (const section of REQUIRED_SECTIONS) {
2252
+ if (!mdContent.includes(section)) {
2253
+ errors.push(`${name}/SKILL.md: missing section "${section}"`);
2254
+ }
2255
+ }
2256
+ if (!mdContent.trim().startsWith("# ")) {
2257
+ errors.push(`${name}/SKILL.md: must start with an h1 heading`);
2258
+ }
2259
+ if (result.data.type === "rigid") {
2260
+ if (!mdContent.includes("## Gates")) {
2261
+ errors.push(`${name}/SKILL.md: rigid skill missing "## Gates" section`);
2262
+ }
2263
+ if (!mdContent.includes("## Escalation")) {
2264
+ errors.push(`${name}/SKILL.md: rigid skill missing "## Escalation" section`);
2265
+ }
2266
+ }
2267
+ } else {
2268
+ errors.push(`${name}: missing SKILL.md`);
2269
+ }
2270
+ validated++;
2271
+ } catch (e) {
2272
+ errors.push(`${name}: parse error \u2014 ${e instanceof Error ? e.message : String(e)}`);
2273
+ }
2274
+ }
2275
+ if (globalOpts.json) {
2276
+ logger.raw({ validated, errors });
2277
+ } else if (errors.length > 0) {
2278
+ logger.error(`Validation failed with ${errors.length} error(s):`);
2279
+ for (const err of errors) console.error(` - ${err}`);
2280
+ process.exit(ExitCode.ERROR);
2281
+ } else {
2282
+ if (!globalOpts.quiet) {
2283
+ logger.success(`All ${validated} skill(s) validated successfully.`);
2284
+ }
2285
+ }
2286
+ process.exit(ExitCode.SUCCESS);
2287
+ });
2288
+ }
2289
+
2290
+ // src/commands/skill/info.ts
2291
+ import { Command as Command20 } from "commander";
2292
+ import * as fs11 from "fs";
2293
+ import * as path17 from "path";
2294
+ import { parse as parse5 } from "yaml";
2295
+ function createInfoCommand() {
2296
+ return new Command20("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
2297
+ const globalOpts = cmd.optsWithGlobals();
2298
+ const skillsDir = resolveSkillsDir();
2299
+ const skillDir = path17.join(skillsDir, name);
2300
+ if (!fs11.existsSync(skillDir)) {
2301
+ logger.error(`Skill not found: ${name}`);
2302
+ process.exit(ExitCode.ERROR);
2303
+ return;
2304
+ }
2305
+ const yamlPath = path17.join(skillDir, "skill.yaml");
2306
+ if (!fs11.existsSync(yamlPath)) {
2307
+ logger.error(`skill.yaml not found for skill: ${name}`);
2308
+ process.exit(ExitCode.ERROR);
2309
+ return;
2310
+ }
2311
+ try {
2312
+ const raw = fs11.readFileSync(yamlPath, "utf-8");
2313
+ const parsed = parse5(raw);
2314
+ const result = SkillMetadataSchema.safeParse(parsed);
2315
+ if (!result.success) {
2316
+ logger.error(`Invalid skill.yaml: ${result.error.message}`);
2317
+ process.exit(ExitCode.ERROR);
2318
+ return;
2319
+ }
2320
+ const skill = result.data;
2321
+ if (globalOpts.json) {
2322
+ logger.raw(skill);
2323
+ } else {
2324
+ console.log(`Name: ${skill.name}`);
2325
+ console.log(`Version: ${skill.version}`);
2326
+ console.log(`Type: ${skill.type}`);
2327
+ console.log(`Description: ${skill.description}`);
2328
+ console.log(`Triggers: ${skill.triggers.join(", ")}`);
2329
+ console.log(`Platforms: ${skill.platforms.join(", ")}`);
2330
+ console.log(`Tools: ${skill.tools.join(", ")}`);
2331
+ if (skill.phases && skill.phases.length > 0) {
2332
+ console.log(`Phases:`);
2333
+ for (const p of skill.phases) {
2334
+ console.log(` - ${p.name}: ${p.description}`);
2335
+ }
2336
+ }
2337
+ if (skill.depends_on.length > 0) {
2338
+ console.log(`Depends on: ${skill.depends_on.join(", ")}`);
2339
+ }
2340
+ console.log(`Persistent: ${skill.state.persistent}`);
2341
+ }
2342
+ } catch (e) {
2343
+ logger.error(`Failed to read skill: ${e instanceof Error ? e.message : String(e)}`);
2344
+ process.exit(ExitCode.ERROR);
2345
+ return;
2346
+ }
2347
+ process.exit(ExitCode.SUCCESS);
2348
+ });
2349
+ }
2350
+
2351
+ // src/commands/skill/index.ts
2352
+ function createSkillCommand() {
2353
+ const command = new Command21("skill").description("Skill management commands");
2354
+ command.addCommand(createListCommand2());
2355
+ command.addCommand(createRunCommand2());
2356
+ command.addCommand(createValidateCommand3());
2357
+ command.addCommand(createInfoCommand());
2358
+ return command;
2359
+ }
2360
+
2361
+ // src/commands/state/index.ts
2362
+ import { Command as Command25 } from "commander";
2363
+
2364
+ // src/commands/state/show.ts
2365
+ import { Command as Command22 } from "commander";
2366
+ import * as path18 from "path";
2367
+ import { loadState } from "@harness-engineering/core";
2368
+ function createShowCommand() {
2369
+ return new Command22("show").description("Show current project state").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
2370
+ const globalOpts = cmd.optsWithGlobals();
2371
+ const projectPath = path18.resolve(opts.path);
2372
+ const result = await loadState(projectPath);
2373
+ if (!result.ok) {
2374
+ logger.error(result.error.message);
2375
+ process.exit(ExitCode.ERROR);
2376
+ return;
2377
+ }
2378
+ const state = result.value;
2379
+ if (globalOpts.json) {
2380
+ logger.raw(state);
2381
+ } else if (globalOpts.quiet) {
2382
+ console.log(JSON.stringify(state));
2383
+ } else {
2384
+ console.log(`Schema Version: ${state.schemaVersion}`);
2385
+ if (state.position.phase) console.log(`Phase: ${state.position.phase}`);
2386
+ if (state.position.task) console.log(`Task: ${state.position.task}`);
2387
+ if (state.lastSession) {
2388
+ console.log(`Last Session: ${state.lastSession.date} \u2014 ${state.lastSession.summary}`);
2389
+ }
2390
+ if (Object.keys(state.progress).length > 0) {
2391
+ console.log("\nProgress:");
2392
+ for (const [task, status] of Object.entries(state.progress)) {
2393
+ console.log(` ${task}: ${status}`);
2394
+ }
2395
+ }
2396
+ if (state.decisions.length > 0) {
2397
+ console.log(`
2398
+ Decisions: ${state.decisions.length}`);
2399
+ }
2400
+ if (state.blockers.length > 0) {
2401
+ const open = state.blockers.filter((b) => b.status === "open").length;
2402
+ console.log(`Blockers: ${open} open / ${state.blockers.length} total`);
2403
+ }
2404
+ }
2405
+ process.exit(ExitCode.SUCCESS);
2406
+ });
2407
+ }
2408
+
2409
+ // src/commands/state/reset.ts
2410
+ import { Command as Command23 } from "commander";
2411
+ import * as fs12 from "fs";
2412
+ import * as path19 from "path";
2413
+ import * as readline from "readline";
2414
+ function createResetCommand() {
2415
+ return new Command23("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
2416
+ const projectPath = path19.resolve(opts.path);
2417
+ const statePath = path19.join(projectPath, ".harness", "state.json");
2418
+ if (!fs12.existsSync(statePath)) {
2419
+ logger.info("No state file found. Nothing to reset.");
2420
+ process.exit(ExitCode.SUCCESS);
2421
+ return;
2422
+ }
2423
+ if (!opts.yes) {
2424
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2425
+ const answer = await new Promise((resolve12) => {
2426
+ rl.question("Reset project state? This cannot be undone. [y/N] ", resolve12);
2427
+ });
2428
+ rl.close();
2429
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
2430
+ logger.info("Reset cancelled.");
2431
+ process.exit(ExitCode.SUCCESS);
2432
+ return;
2433
+ }
2434
+ }
2435
+ try {
2436
+ fs12.unlinkSync(statePath);
2437
+ logger.success("Project state reset.");
2438
+ } catch (e) {
2439
+ logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
2440
+ process.exit(ExitCode.ERROR);
2441
+ return;
2442
+ }
2443
+ process.exit(ExitCode.SUCCESS);
2444
+ });
2445
+ }
2446
+
2447
+ // src/commands/state/learn.ts
2448
+ import { Command as Command24 } from "commander";
2449
+ import * as path20 from "path";
2450
+ import { appendLearning } from "@harness-engineering/core";
2451
+ function createLearnCommand() {
2452
+ return new Command24("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) => {
2453
+ const projectPath = path20.resolve(opts.path);
2454
+ const result = await appendLearning(projectPath, message);
2455
+ if (!result.ok) {
2456
+ logger.error(result.error.message);
2457
+ process.exit(ExitCode.ERROR);
2458
+ return;
2459
+ }
2460
+ logger.success(`Learning recorded.`);
2461
+ process.exit(ExitCode.SUCCESS);
2462
+ });
2463
+ }
2464
+
2465
+ // src/commands/state/index.ts
2466
+ function createStateCommand() {
2467
+ const command = new Command25("state").description("Project state management commands");
2468
+ command.addCommand(createShowCommand());
2469
+ command.addCommand(createResetCommand());
2470
+ command.addCommand(createLearnCommand());
2471
+ return command;
2472
+ }
2473
+
2474
+ // src/index.ts
2475
+ function createProgram() {
2476
+ const program = new Command26();
2477
+ 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");
2478
+ program.addCommand(createValidateCommand());
2479
+ program.addCommand(createCheckDepsCommand());
2480
+ program.addCommand(createCheckDocsCommand());
2481
+ program.addCommand(createInitCommand());
2482
+ program.addCommand(createCleanupCommand());
2483
+ program.addCommand(createFixDriftCommand());
2484
+ program.addCommand(createAgentCommand());
2485
+ program.addCommand(createAddCommand());
2486
+ program.addCommand(createLinterCommand());
2487
+ program.addCommand(createPersonaCommand());
2488
+ program.addCommand(createSkillCommand());
2489
+ program.addCommand(createStateCommand());
2490
+ return program;
2491
+ }
2492
+
2493
+ export {
2494
+ ExitCode,
2495
+ CLIError,
2496
+ handleError,
2497
+ findConfigFile,
2498
+ loadConfig,
2499
+ resolveConfig,
2500
+ OutputMode,
2501
+ OutputFormatter,
2502
+ logger,
2503
+ TemplateEngine,
2504
+ loadPersona,
2505
+ listPersonas,
2506
+ runPersona,
2507
+ generateRuntime,
2508
+ generateAgentsMd,
2509
+ generateCIWorkflow,
2510
+ buildPreamble,
2511
+ createProgram
2512
+ };