@harness-engineering/cli 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +7 -2
  2. package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +10 -1
  3. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +2 -2
  4. package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +7 -2
  5. package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +10 -1
  6. package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +2 -2
  7. package/dist/agents-md-EMRFLNBC.js +8 -0
  8. package/dist/architecture-5JNN5L3M.js +13 -0
  9. package/dist/bin/harness-mcp.d.ts +1 -0
  10. package/dist/bin/harness-mcp.js +28 -0
  11. package/dist/bin/harness.js +42 -8
  12. package/dist/check-phase-gate-WOKIYGAM.js +12 -0
  13. package/dist/chunk-46YA6FI3.js +293 -0
  14. package/dist/chunk-4PFMY3H7.js +248 -0
  15. package/dist/{chunk-6JIT7CEM.js → chunk-72GHBOL2.js} +1 -1
  16. package/dist/chunk-7X7ZAYMY.js +373 -0
  17. package/dist/chunk-B7HFEHWP.js +35 -0
  18. package/dist/chunk-BM3PWGXQ.js +14 -0
  19. package/dist/chunk-C2ERUR3L.js +255 -0
  20. package/dist/chunk-CWZ4Y2PO.js +189 -0
  21. package/dist/{chunk-ULSRSP53.js → chunk-ECUJQS3B.js} +11 -112
  22. package/dist/chunk-EOLRW32Q.js +72 -0
  23. package/dist/chunk-F3YDAJFQ.js +125 -0
  24. package/dist/chunk-F4PTVZWA.js +116 -0
  25. package/dist/chunk-FPIPT36X.js +187 -0
  26. package/dist/chunk-FX7SQHGD.js +103 -0
  27. package/dist/chunk-HIOXKZYF.js +15 -0
  28. package/dist/chunk-IDZNPTYD.js +16 -0
  29. package/dist/chunk-JSTQ3AWB.js +31 -0
  30. package/dist/chunk-K6XAPGML.js +27 -0
  31. package/dist/chunk-KET4QQZB.js +8 -0
  32. package/dist/chunk-LXU5M77O.js +4028 -0
  33. package/dist/chunk-MDUK2J2O.js +67 -0
  34. package/dist/chunk-MHBMTPW7.js +29 -0
  35. package/dist/chunk-MO4YQOMB.js +85 -0
  36. package/dist/chunk-NKDM3FMH.js +52 -0
  37. package/dist/{chunk-CGSHUJES.js → chunk-NX6DSZSM.js} +7 -26
  38. package/dist/chunk-OPXH4CQN.js +62 -0
  39. package/dist/{chunk-RTPHUDZS.js → chunk-PAHHT2IK.js} +466 -2714
  40. package/dist/chunk-PMTFPOCT.js +122 -0
  41. package/dist/chunk-PSXF277V.js +89 -0
  42. package/dist/chunk-Q6AB7W5Z.js +135 -0
  43. package/dist/chunk-QPEH2QPG.js +347 -0
  44. package/dist/chunk-TEFCFC4H.js +15 -0
  45. package/dist/chunk-TRAPF4IX.js +185 -0
  46. package/dist/chunk-VUCPTQ6G.js +67 -0
  47. package/dist/chunk-W6Y7ZW3Y.js +13 -0
  48. package/dist/chunk-ZOAWBDWU.js +72 -0
  49. package/dist/ci-workflow-ZBBUNTHQ.js +8 -0
  50. package/dist/constants-5JGUXPEK.js +6 -0
  51. package/dist/create-skill-LUWO46WF.js +11 -0
  52. package/dist/dist-D4RYGUZE.js +14 -0
  53. package/dist/dist-L7LAAQAS.js +18 -0
  54. package/dist/{dist-C5PYIQPF.js → dist-PBTNVK6K.js} +8 -6
  55. package/dist/docs-PTJGD6XI.js +12 -0
  56. package/dist/engine-SCMZ3G3E.js +8 -0
  57. package/dist/entropy-YIUBGKY7.js +12 -0
  58. package/dist/feedback-WEVQSLAA.js +18 -0
  59. package/dist/generate-agent-definitions-BU5LOJTI.js +15 -0
  60. package/dist/glob-helper-5OHBUQAI.js +52 -0
  61. package/dist/graph-loader-RLO3KRIX.js +8 -0
  62. package/dist/index.d.ts +11 -1
  63. package/dist/index.js +84 -33
  64. package/dist/loader-6S6PVGSF.js +10 -0
  65. package/dist/mcp-BNLBTCXZ.js +34 -0
  66. package/dist/performance-5TVW6SA6.js +24 -0
  67. package/dist/review-pipeline-4JTQAWKW.js +9 -0
  68. package/dist/runner-VMYLHWOC.js +6 -0
  69. package/dist/runtime-PXIM7UV6.js +9 -0
  70. package/dist/security-URYTKLGK.js +9 -0
  71. package/dist/skill-executor-KVS47DAU.js +8 -0
  72. package/dist/validate-KSDUUK2M.js +12 -0
  73. package/dist/validate-cross-check-WZAX357V.js +8 -0
  74. package/dist/version-KFFPOQAX.js +6 -0
  75. package/package.json +6 -4
  76. package/dist/create-skill-UZOHMXRU.js +0 -8
  77. package/dist/validate-cross-check-VG573VZO.js +0 -7
@@ -0,0 +1,248 @@
1
+ import {
2
+ resultToMcpResponse
3
+ } from "./chunk-IDZNPTYD.js";
4
+ import {
5
+ sanitizePath
6
+ } from "./chunk-W6Y7ZW3Y.js";
7
+
8
+ // src/mcp/tools/feedback.ts
9
+ var createSelfReviewDefinition = {
10
+ name: "create_self_review",
11
+ description: "Generate a checklist-based code review from a git diff, checking harness constraints, custom rules, and diff patterns",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ path: { type: "string", description: "Path to project root" },
16
+ diff: { type: "string", description: "Git diff string to review" },
17
+ customRules: {
18
+ type: "array",
19
+ items: { type: "object" },
20
+ description: "Optional custom rules to apply during review"
21
+ },
22
+ maxFileSize: {
23
+ type: "number",
24
+ description: "Maximum number of lines changed per file before flagging"
25
+ },
26
+ maxFileCount: {
27
+ type: "number",
28
+ description: "Maximum number of changed files before flagging"
29
+ }
30
+ },
31
+ required: ["path", "diff"]
32
+ }
33
+ };
34
+ async function handleCreateSelfReview(input) {
35
+ try {
36
+ const { parseDiff, createSelfReview } = await import("./dist-PBTNVK6K.js");
37
+ const parseResult = parseDiff(input.diff);
38
+ if (!parseResult.ok) {
39
+ return resultToMcpResponse(parseResult);
40
+ }
41
+ const projectPath = sanitizePath(input.path);
42
+ const config = {
43
+ rootDir: projectPath,
44
+ harness: {
45
+ context: true,
46
+ constraints: true,
47
+ entropy: true
48
+ },
49
+ ...input.customRules ? { customRules: input.customRules } : {},
50
+ diffAnalysis: {
51
+ enabled: true,
52
+ ...input.maxFileSize !== void 0 ? { maxFileSize: input.maxFileSize } : {},
53
+ ...input.maxFileCount !== void 0 ? { maxChangedFiles: input.maxFileCount } : {}
54
+ }
55
+ };
56
+ const { loadGraphStore } = await import("./graph-loader-RLO3KRIX.js");
57
+ const store = await loadGraphStore(projectPath);
58
+ let graphData;
59
+ if (store) {
60
+ const { GraphFeedbackAdapter } = await import("./dist-I7DB5VKB.js");
61
+ const adapter = new GraphFeedbackAdapter(store);
62
+ const changedFiles = parseResult.value.files.map((f) => f.path);
63
+ const impact = adapter.computeImpactData(changedFiles);
64
+ const harness = adapter.computeHarnessCheckData();
65
+ graphData = {
66
+ impact: {
67
+ affectedTests: [...impact.affectedTests],
68
+ affectedDocs: [...impact.affectedDocs],
69
+ impactScope: impact.impactScope
70
+ },
71
+ harness: { ...harness }
72
+ };
73
+ }
74
+ const result = await createSelfReview(
75
+ parseResult.value,
76
+ config,
77
+ graphData
78
+ );
79
+ return resultToMcpResponse(result);
80
+ } catch (error) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
86
+ }
87
+ ],
88
+ isError: true
89
+ };
90
+ }
91
+ }
92
+ var analyzeDiffDefinition = {
93
+ name: "analyze_diff",
94
+ description: "Parse a git diff and check for forbidden patterns, oversized files, and missing test coverage",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ diff: { type: "string", description: "Git diff string to analyze" },
99
+ path: {
100
+ type: "string",
101
+ description: "Path to project root (enables graph-enhanced analysis)"
102
+ },
103
+ forbiddenPatterns: {
104
+ type: "array",
105
+ items: { type: "string" },
106
+ description: "List of regex patterns that are forbidden in the diff"
107
+ },
108
+ maxFileSize: {
109
+ type: "number",
110
+ description: "Maximum number of lines changed per file before flagging"
111
+ },
112
+ maxFileCount: {
113
+ type: "number",
114
+ description: "Maximum number of changed files before flagging"
115
+ }
116
+ },
117
+ required: ["diff"]
118
+ }
119
+ };
120
+ async function handleAnalyzeDiff(input) {
121
+ try {
122
+ const { parseDiff, analyzeDiff } = await import("./dist-PBTNVK6K.js");
123
+ const parseResult = parseDiff(input.diff);
124
+ if (!parseResult.ok) {
125
+ return resultToMcpResponse(parseResult);
126
+ }
127
+ const options = {
128
+ enabled: true,
129
+ ...input.forbiddenPatterns ? {
130
+ forbiddenPatterns: input.forbiddenPatterns.map((pattern) => ({
131
+ pattern,
132
+ message: `Forbidden pattern matched: ${pattern}`,
133
+ severity: "warning"
134
+ }))
135
+ } : {},
136
+ ...input.maxFileSize !== void 0 ? { maxFileSize: input.maxFileSize } : {},
137
+ ...input.maxFileCount !== void 0 ? { maxChangedFiles: input.maxFileCount } : {}
138
+ };
139
+ let graphImpactData;
140
+ if (input.path) {
141
+ try {
142
+ const { loadGraphStore } = await import("./graph-loader-RLO3KRIX.js");
143
+ const store = await loadGraphStore(sanitizePath(input.path));
144
+ if (store) {
145
+ const { GraphFeedbackAdapter } = await import("./dist-I7DB5VKB.js");
146
+ const adapter = new GraphFeedbackAdapter(store);
147
+ const changedFiles = parseResult.value.files.map((f) => f.path);
148
+ const impact = adapter.computeImpactData(changedFiles);
149
+ graphImpactData = {
150
+ affectedTests: [...impact.affectedTests],
151
+ affectedDocs: [...impact.affectedDocs],
152
+ impactScope: impact.impactScope
153
+ };
154
+ }
155
+ } catch {
156
+ }
157
+ }
158
+ const result = await analyzeDiff(parseResult.value, options, graphImpactData);
159
+ return resultToMcpResponse(result);
160
+ } catch (error) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
166
+ }
167
+ ],
168
+ isError: true
169
+ };
170
+ }
171
+ }
172
+ var requestPeerReviewDefinition = {
173
+ name: "request_peer_review",
174
+ description: "Spawn an agent subprocess to perform code review. Returns structured feedback with approval status. Timeout: 120 seconds.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ path: { type: "string", description: "Path to project root" },
179
+ agentType: {
180
+ type: "string",
181
+ enum: [
182
+ "architecture-enforcer",
183
+ "documentation-maintainer",
184
+ "test-reviewer",
185
+ "entropy-cleaner",
186
+ "custom"
187
+ ],
188
+ description: "Type of agent to use for the peer review"
189
+ },
190
+ diff: { type: "string", description: "Git diff string to review" },
191
+ context: { type: "string", description: "Optional additional context for the reviewer" }
192
+ },
193
+ required: ["path", "agentType", "diff"]
194
+ }
195
+ };
196
+ async function handleRequestPeerReview(input) {
197
+ try {
198
+ const { parseDiff, requestPeerReview } = await import("./dist-PBTNVK6K.js");
199
+ const parseResult = parseDiff(input.diff);
200
+ if (!parseResult.ok) {
201
+ return resultToMcpResponse(parseResult);
202
+ }
203
+ const reviewContext = {
204
+ files: parseResult.value.files.map((f) => f.path),
205
+ diff: input.diff,
206
+ ...input.context ? { metadata: { context: input.context } } : {}
207
+ };
208
+ try {
209
+ const { loadGraphStore } = await import("./graph-loader-RLO3KRIX.js");
210
+ const store = await loadGraphStore(sanitizePath(input.path));
211
+ if (store) {
212
+ const { GraphFeedbackAdapter } = await import("./dist-I7DB5VKB.js");
213
+ const adapter = new GraphFeedbackAdapter(store);
214
+ const changedFiles = parseResult.value.files.map((f) => f.path);
215
+ const impactData = adapter.computeImpactData(changedFiles);
216
+ reviewContext.metadata = {
217
+ ...reviewContext.metadata,
218
+ graphContext: impactData
219
+ };
220
+ }
221
+ } catch {
222
+ }
223
+ const result = await requestPeerReview(input.agentType, reviewContext, {
224
+ timeout: 12e4,
225
+ wait: true
226
+ });
227
+ return resultToMcpResponse(result);
228
+ } catch (error) {
229
+ return {
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
234
+ }
235
+ ],
236
+ isError: true
237
+ };
238
+ }
239
+ }
240
+
241
+ export {
242
+ createSelfReviewDefinition,
243
+ handleCreateSelfReview,
244
+ analyzeDiffDefinition,
245
+ handleAnalyzeDiff,
246
+ requestPeerReviewDefinition,
247
+ handleRequestPeerReview
248
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Ok
3
- } from "./chunk-CGSHUJES.js";
3
+ } from "./chunk-MHBMTPW7.js";
4
4
 
5
5
  // src/commands/validate-cross-check.ts
6
6
  import * as fs from "fs";
@@ -0,0 +1,373 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-HIOXKZYF.js";
4
+ import {
5
+ CLIError,
6
+ ExitCode
7
+ } from "./chunk-B7HFEHWP.js";
8
+ import {
9
+ Err,
10
+ Ok
11
+ } from "./chunk-MHBMTPW7.js";
12
+
13
+ // src/commands/check-phase-gate.ts
14
+ import { Command } from "commander";
15
+ import * as path2 from "path";
16
+ import * as fs2 from "fs";
17
+
18
+ // src/config/loader.ts
19
+ import * as fs from "fs";
20
+ import * as path from "path";
21
+
22
+ // src/config/schema.ts
23
+ import { z } from "zod";
24
+ var LayerSchema = z.object({
25
+ name: z.string(),
26
+ pattern: z.string(),
27
+ allowedDependencies: z.array(z.string())
28
+ });
29
+ var ForbiddenImportSchema = z.object({
30
+ from: z.string(),
31
+ disallow: z.array(z.string()),
32
+ message: z.string().optional()
33
+ });
34
+ var BoundaryConfigSchema = z.object({
35
+ requireSchema: z.array(z.string())
36
+ });
37
+ var AgentConfigSchema = z.object({
38
+ executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
39
+ timeout: z.number().default(3e5),
40
+ skills: z.array(z.string()).optional()
41
+ });
42
+ var EntropyConfigSchema = z.object({
43
+ excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
44
+ autoFix: z.boolean().default(false)
45
+ });
46
+ var PhaseGateMappingSchema = z.object({
47
+ implPattern: z.string(),
48
+ specPattern: z.string()
49
+ });
50
+ var PhaseGatesConfigSchema = z.object({
51
+ enabled: z.boolean().default(false),
52
+ severity: z.enum(["error", "warning"]).default("error"),
53
+ mappings: z.array(PhaseGateMappingSchema).default([{ implPattern: "src/**/*.ts", specPattern: "docs/changes/{feature}/proposal.md" }])
54
+ });
55
+ var SecurityConfigSchema = z.object({
56
+ enabled: z.boolean().default(true),
57
+ strict: z.boolean().default(false),
58
+ rules: z.record(z.string(), z.enum(["off", "error", "warning", "info"])).optional(),
59
+ exclude: z.array(z.string()).optional()
60
+ }).passthrough();
61
+ var PerformanceConfigSchema = z.object({
62
+ complexity: z.record(z.unknown()).optional(),
63
+ coupling: z.record(z.unknown()).optional(),
64
+ sizeBudget: z.record(z.unknown()).optional()
65
+ }).passthrough();
66
+ var DesignConfigSchema = z.object({
67
+ strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
68
+ platforms: z.array(z.enum(["web", "mobile"])).default([]),
69
+ tokenPath: z.string().optional(),
70
+ aestheticIntent: z.string().optional()
71
+ });
72
+ var I18nCoverageConfigSchema = z.object({
73
+ minimumPercent: z.number().min(0).max(100).default(100),
74
+ requirePlurals: z.boolean().default(true),
75
+ detectUntranslated: z.boolean().default(true)
76
+ });
77
+ var I18nMcpConfigSchema = z.object({
78
+ server: z.string(),
79
+ projectId: z.string().optional()
80
+ });
81
+ var I18nConfigSchema = z.object({
82
+ enabled: z.boolean().default(false),
83
+ strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
84
+ sourceLocale: z.string().default("en"),
85
+ targetLocales: z.array(z.string()).default([]),
86
+ framework: z.enum([
87
+ "auto",
88
+ "i18next",
89
+ "react-intl",
90
+ "vue-i18n",
91
+ "flutter-intl",
92
+ "apple",
93
+ "android",
94
+ "custom"
95
+ ]).default("auto"),
96
+ format: z.string().default("json"),
97
+ messageFormat: z.enum(["icu", "i18next", "custom"]).default("icu"),
98
+ keyConvention: z.enum(["dot-notation", "snake_case", "camelCase", "custom"]).default("dot-notation"),
99
+ translationPaths: z.record(z.string(), z.string()).optional(),
100
+ platforms: z.array(z.enum(["web", "mobile", "backend"])).default([]),
101
+ industry: z.string().optional(),
102
+ coverage: I18nCoverageConfigSchema.optional(),
103
+ pseudoLocale: z.string().optional(),
104
+ mcp: I18nMcpConfigSchema.optional()
105
+ });
106
+ var ModelTierConfigSchema = z.object({
107
+ fast: z.string().optional(),
108
+ standard: z.string().optional(),
109
+ strong: z.string().optional()
110
+ });
111
+ var ReviewConfigSchema = z.object({
112
+ model_tiers: ModelTierConfigSchema.optional()
113
+ });
114
+ var HarnessConfigSchema = z.object({
115
+ version: z.literal(1),
116
+ name: z.string().optional(),
117
+ rootDir: z.string().default("."),
118
+ layers: z.array(LayerSchema).optional(),
119
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
120
+ boundaries: BoundaryConfigSchema.optional(),
121
+ agentsMapPath: z.string().default("./AGENTS.md"),
122
+ docsDir: z.string().default("./docs"),
123
+ agent: AgentConfigSchema.optional(),
124
+ entropy: EntropyConfigSchema.optional(),
125
+ security: SecurityConfigSchema.optional(),
126
+ performance: PerformanceConfigSchema.optional(),
127
+ template: z.object({
128
+ level: z.enum(["basic", "intermediate", "advanced"]),
129
+ framework: z.string().optional(),
130
+ version: z.number()
131
+ }).optional(),
132
+ phaseGates: PhaseGatesConfigSchema.optional(),
133
+ design: DesignConfigSchema.optional(),
134
+ i18n: I18nConfigSchema.optional(),
135
+ review: ReviewConfigSchema.optional(),
136
+ updateCheckInterval: z.number().int().min(0).optional()
137
+ });
138
+
139
+ // src/config/loader.ts
140
+ var CONFIG_FILENAMES = ["harness.config.json"];
141
+ function findConfigFile(startDir = process.cwd()) {
142
+ let currentDir = path.resolve(startDir);
143
+ const root = path.parse(currentDir).root;
144
+ while (currentDir !== root) {
145
+ for (const filename of CONFIG_FILENAMES) {
146
+ const configPath = path.join(currentDir, filename);
147
+ if (fs.existsSync(configPath)) {
148
+ return Ok(configPath);
149
+ }
150
+ }
151
+ currentDir = path.dirname(currentDir);
152
+ }
153
+ return Err(
154
+ new CLIError('No harness.config.json found. Run "harness init" to create one.', ExitCode.ERROR)
155
+ );
156
+ }
157
+ function loadConfig(configPath) {
158
+ if (!fs.existsSync(configPath)) {
159
+ return Err(new CLIError(`Config file not found: ${configPath}`, ExitCode.ERROR));
160
+ }
161
+ let rawConfig;
162
+ try {
163
+ const content = fs.readFileSync(configPath, "utf-8");
164
+ rawConfig = JSON.parse(content);
165
+ } catch (error) {
166
+ return Err(
167
+ new CLIError(
168
+ `Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
169
+ ExitCode.ERROR
170
+ )
171
+ );
172
+ }
173
+ const parsed = HarnessConfigSchema.safeParse(rawConfig);
174
+ if (!parsed.success) {
175
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
176
+ return Err(new CLIError(`Invalid config:
177
+ ${issues}`, ExitCode.ERROR));
178
+ }
179
+ return Ok(parsed.data);
180
+ }
181
+ function resolveConfig(configPath) {
182
+ if (configPath) {
183
+ return loadConfig(configPath);
184
+ }
185
+ const findResult = findConfigFile();
186
+ if (!findResult.ok) {
187
+ return findResult;
188
+ }
189
+ return loadConfig(findResult.value);
190
+ }
191
+
192
+ // src/utils/files.ts
193
+ import { glob } from "glob";
194
+ async function findFiles(pattern, cwd = process.cwd()) {
195
+ return glob(pattern, { cwd, absolute: true });
196
+ }
197
+
198
+ // src/output/formatter.ts
199
+ import chalk from "chalk";
200
+ var OutputMode = {
201
+ JSON: "json",
202
+ TEXT: "text",
203
+ QUIET: "quiet",
204
+ VERBOSE: "verbose"
205
+ };
206
+ var OutputFormatter = class {
207
+ constructor(mode = OutputMode.TEXT) {
208
+ this.mode = mode;
209
+ }
210
+ /**
211
+ * Format raw data (for JSON mode)
212
+ */
213
+ format(data) {
214
+ if (this.mode === OutputMode.JSON) {
215
+ return JSON.stringify(data, null, 2);
216
+ }
217
+ return String(data);
218
+ }
219
+ /**
220
+ * Format validation result
221
+ */
222
+ formatValidation(result) {
223
+ if (this.mode === OutputMode.JSON) {
224
+ return JSON.stringify(result, null, 2);
225
+ }
226
+ if (this.mode === OutputMode.QUIET) {
227
+ if (result.valid) return "";
228
+ return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
229
+ }
230
+ const lines = [];
231
+ if (result.valid) {
232
+ lines.push(chalk.green("v validation passed"));
233
+ } else {
234
+ lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
235
+ lines.push("");
236
+ for (const issue of result.issues) {
237
+ const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
238
+ lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
239
+ lines.push(` ${issue.message}`);
240
+ if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
241
+ lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
242
+ }
243
+ }
244
+ }
245
+ return lines.join("\n");
246
+ }
247
+ /**
248
+ * Format a summary line
249
+ */
250
+ formatSummary(label, value, success) {
251
+ if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
252
+ return "";
253
+ }
254
+ const icon = success ? chalk.green("v") : chalk.red("x");
255
+ return `${icon} ${label}: ${value}`;
256
+ }
257
+ };
258
+
259
+ // src/commands/check-phase-gate.ts
260
+ function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
261
+ const relImpl = path2.relative(cwd, implFile).replace(/\\/g, "/");
262
+ const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
263
+ const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
264
+ const segments = afterBase.split("/");
265
+ const firstSegment = segments[0] ?? "";
266
+ const feature = segments.length > 1 ? firstSegment : path2.basename(firstSegment, path2.extname(firstSegment));
267
+ const specRelative = specPattern.replace("{feature}", feature);
268
+ return path2.resolve(cwd, specRelative);
269
+ }
270
+ async function runCheckPhaseGate(options) {
271
+ const configResult = resolveConfig(options.configPath);
272
+ if (!configResult.ok) {
273
+ return configResult;
274
+ }
275
+ const config = configResult.value;
276
+ const cwd = options.cwd ?? (options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd());
277
+ if (!config.phaseGates?.enabled) {
278
+ return Ok({
279
+ pass: true,
280
+ skipped: true,
281
+ missingSpecs: [],
282
+ checkedFiles: 0
283
+ });
284
+ }
285
+ const phaseGates = config.phaseGates;
286
+ const missingSpecs = [];
287
+ let checkedFiles = 0;
288
+ for (const mapping of phaseGates.mappings) {
289
+ const implFiles = await findFiles(mapping.implPattern, cwd);
290
+ for (const implFile of implFiles) {
291
+ checkedFiles++;
292
+ const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
293
+ if (!fs2.existsSync(expectedSpec)) {
294
+ missingSpecs.push({
295
+ implFile: path2.relative(cwd, implFile).replace(/\\/g, "/"),
296
+ expectedSpec: path2.relative(cwd, expectedSpec).replace(/\\/g, "/")
297
+ });
298
+ }
299
+ }
300
+ }
301
+ const pass = missingSpecs.length === 0;
302
+ return Ok({
303
+ pass,
304
+ skipped: false,
305
+ severity: phaseGates.severity,
306
+ missingSpecs,
307
+ checkedFiles
308
+ });
309
+ }
310
+ function createCheckPhaseGateCommand() {
311
+ const command = new Command("check-phase-gate").description("Verify that implementation files have matching spec documents").action(async (_opts, cmd) => {
312
+ const globalOpts = cmd.optsWithGlobals();
313
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
314
+ const formatter = new OutputFormatter(mode);
315
+ const result = await runCheckPhaseGate({
316
+ configPath: globalOpts.config,
317
+ json: globalOpts.json,
318
+ verbose: globalOpts.verbose,
319
+ quiet: globalOpts.quiet
320
+ });
321
+ if (!result.ok) {
322
+ if (mode === OutputMode.JSON) {
323
+ console.log(JSON.stringify({ error: result.error.message }));
324
+ } else {
325
+ logger.error(result.error.message);
326
+ }
327
+ process.exit(result.error.exitCode);
328
+ }
329
+ const value = result.value;
330
+ if (value.skipped) {
331
+ if (mode === OutputMode.JSON) {
332
+ console.log(formatter.format(value));
333
+ } else if (mode !== OutputMode.QUIET) {
334
+ logger.dim("Phase gates not enabled, skipping.");
335
+ }
336
+ process.exit(ExitCode.SUCCESS);
337
+ }
338
+ const output = formatter.formatValidation({
339
+ valid: value.pass,
340
+ issues: value.missingSpecs.map((m) => ({
341
+ file: m.implFile,
342
+ message: `Missing spec: ${m.expectedSpec}`
343
+ }))
344
+ });
345
+ if (output) {
346
+ console.log(output);
347
+ }
348
+ const summary = formatter.formatSummary(
349
+ "Phase gate check",
350
+ `${value.checkedFiles} files checked, ${value.missingSpecs.length} missing specs`,
351
+ value.pass
352
+ );
353
+ if (summary) {
354
+ console.log(summary);
355
+ }
356
+ if (!value.pass && value.severity === "error") {
357
+ process.exit(ExitCode.VALIDATION_FAILED);
358
+ }
359
+ process.exit(ExitCode.SUCCESS);
360
+ });
361
+ return command;
362
+ }
363
+
364
+ export {
365
+ findConfigFile,
366
+ loadConfig,
367
+ resolveConfig,
368
+ OutputMode,
369
+ OutputFormatter,
370
+ findFiles,
371
+ runCheckPhaseGate,
372
+ createCheckPhaseGateCommand
373
+ };
@@ -0,0 +1,35 @@
1
+ // src/utils/errors.ts
2
+ var ExitCode = {
3
+ SUCCESS: 0,
4
+ VALIDATION_FAILED: 1,
5
+ ERROR: 2
6
+ };
7
+ var CLIError = class extends Error {
8
+ exitCode;
9
+ constructor(message, exitCode = ExitCode.ERROR) {
10
+ super(message);
11
+ this.name = "CLIError";
12
+ this.exitCode = exitCode;
13
+ }
14
+ };
15
+ function formatError(error) {
16
+ if (error instanceof CLIError) {
17
+ return `Error: ${error.message}`;
18
+ }
19
+ if (error instanceof Error) {
20
+ return `Error: ${error.message}`;
21
+ }
22
+ return `Error: ${String(error)}`;
23
+ }
24
+ function handleError(error) {
25
+ const message = formatError(error);
26
+ console.error(message);
27
+ const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
28
+ process.exit(exitCode);
29
+ }
30
+
31
+ export {
32
+ ExitCode,
33
+ CLIError,
34
+ handleError
35
+ };
@@ -0,0 +1,14 @@
1
+ // src/version.ts
2
+ import { createRequire } from "module";
3
+ var require_ = createRequire(import.meta.url);
4
+ var resolved;
5
+ try {
6
+ resolved = require_("../package.json").version ?? "0.0.0";
7
+ } catch {
8
+ resolved = "0.0.0";
9
+ }
10
+ var CLI_VERSION = resolved;
11
+
12
+ export {
13
+ CLI_VERSION
14
+ };