@harness-engineering/cli 1.9.0 → 1.11.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 (86) hide show
  1. package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +4 -0
  2. package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +7 -2
  3. package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +10 -1
  4. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +2 -2
  5. package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +105 -20
  6. package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +37 -0
  7. package/dist/agents/skills/gemini-cli/enforce-architecture/SKILL.md +4 -0
  8. package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +7 -2
  9. package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +10 -1
  10. package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +2 -2
  11. package/dist/agents/skills/gemini-cli/harness-parallel-agents/SKILL.md +105 -20
  12. package/dist/agents/skills/gemini-cli/harness-pre-commit-review/SKILL.md +37 -0
  13. package/dist/agents-md-ZFV6RR5J.js +8 -0
  14. package/dist/architecture-EXNUMH5R.js +13 -0
  15. package/dist/bin/harness-mcp.d.ts +1 -0
  16. package/dist/bin/harness-mcp.js +28 -0
  17. package/dist/bin/harness.js +42 -8
  18. package/dist/check-phase-gate-VZFOY2PO.js +12 -0
  19. package/dist/chunk-2NCIKJES.js +470 -0
  20. package/dist/chunk-2YPZKGAG.js +62 -0
  21. package/dist/{chunk-CGSHUJES.js → chunk-2YSQOUHO.js} +4484 -2688
  22. package/dist/chunk-3WGJMBKH.js +45 -0
  23. package/dist/{chunk-ULSRSP53.js → chunk-6N4R6FVX.js} +11 -112
  24. package/dist/{chunk-6JIT7CEM.js → chunk-72GHBOL2.js} +1 -1
  25. package/dist/chunk-BM3PWGXQ.js +14 -0
  26. package/dist/chunk-C2ERUR3L.js +255 -0
  27. package/dist/chunk-EBJQ6N4M.js +39 -0
  28. package/dist/chunk-GNGELAXY.js +293 -0
  29. package/dist/chunk-GSIVNYVJ.js +187 -0
  30. package/dist/chunk-HD4IBGLA.js +80 -0
  31. package/dist/chunk-I6JZYEGT.js +4361 -0
  32. package/dist/chunk-IDZNPTYD.js +16 -0
  33. package/dist/chunk-JSTQ3AWB.js +31 -0
  34. package/dist/chunk-K6XAPGML.js +27 -0
  35. package/dist/chunk-KET4QQZB.js +8 -0
  36. package/dist/chunk-L2KLU56K.js +125 -0
  37. package/dist/chunk-MHBMTPW7.js +29 -0
  38. package/dist/chunk-NC6PXVWT.js +116 -0
  39. package/dist/chunk-NKDM3FMH.js +52 -0
  40. package/dist/chunk-PA2XHK75.js +248 -0
  41. package/dist/chunk-Q6AB7W5Z.js +135 -0
  42. package/dist/chunk-QPEH2QPG.js +347 -0
  43. package/dist/chunk-TEFCFC4H.js +15 -0
  44. package/dist/chunk-TI4TGEX6.js +85 -0
  45. package/dist/chunk-TRAPF4IX.js +185 -0
  46. package/dist/chunk-VRFZWGMS.js +68 -0
  47. package/dist/chunk-VUCPTQ6G.js +67 -0
  48. package/dist/chunk-W6Y7ZW3Y.js +13 -0
  49. package/dist/chunk-WJZDO6OY.js +103 -0
  50. package/dist/chunk-WUJTCNOU.js +122 -0
  51. package/dist/chunk-X3MN5UQJ.js +89 -0
  52. package/dist/chunk-Z75JC6I2.js +189 -0
  53. package/dist/chunk-ZOAWBDWU.js +72 -0
  54. package/dist/{chunk-RTPHUDZS.js → chunk-ZWC3MN5E.js} +1944 -2779
  55. package/dist/ci-workflow-K5RCRNYR.js +8 -0
  56. package/dist/constants-5JGUXPEK.js +6 -0
  57. package/dist/create-skill-WPXHSLX2.js +11 -0
  58. package/dist/dist-D4RYGUZE.js +14 -0
  59. package/dist/{dist-C5PYIQPF.js → dist-JVZ2MKBC.js} +108 -6
  60. package/dist/dist-L7LAAQAS.js +18 -0
  61. package/dist/{dist-I7DB5VKB.js → dist-M6BQODWC.js} +1145 -0
  62. package/dist/docs-PWCUVYWU.js +12 -0
  63. package/dist/engine-6XUP6GAK.js +8 -0
  64. package/dist/entropy-4I6JEYAC.js +12 -0
  65. package/dist/feedback-TNIW534S.js +18 -0
  66. package/dist/generate-agent-definitions-MWKEA5NU.js +15 -0
  67. package/dist/glob-helper-5OHBUQAI.js +52 -0
  68. package/dist/graph-loader-KO4GJ5N2.js +8 -0
  69. package/dist/index.d.ts +328 -12
  70. package/dist/index.js +93 -34
  71. package/dist/loader-4FIPIFII.js +10 -0
  72. package/dist/mcp-MOKLYNZL.js +34 -0
  73. package/dist/performance-BTOJCPXU.js +24 -0
  74. package/dist/review-pipeline-3YTW3463.js +9 -0
  75. package/dist/runner-VMYLHWOC.js +6 -0
  76. package/dist/runtime-GO7K2PJE.js +9 -0
  77. package/dist/security-4P2GGFF6.js +9 -0
  78. package/dist/skill-executor-RG45LUO5.js +8 -0
  79. package/dist/templates/orchestrator/WORKFLOW.md +48 -0
  80. package/dist/templates/orchestrator/template.json +6 -0
  81. package/dist/validate-JN44D2Q7.js +12 -0
  82. package/dist/validate-cross-check-DB7RIFFF.js +8 -0
  83. package/dist/version-KFFPOQAX.js +6 -0
  84. package/package.json +13 -7
  85. package/dist/create-skill-UZOHMXRU.js +0 -8
  86. package/dist/validate-cross-check-VG573VZO.js +0 -7
@@ -81,6 +81,26 @@ If a knowledge graph exists at `.harness/graph/` and code files have changed sin
81
81
 
82
82
  If no graph exists, skip this step — the tools fall back to non-graph behavior.
83
83
 
84
+ ### Impact Preview
85
+
86
+ After mechanical checks pass, run `harness impact-preview` to surface the blast radius of staged changes. This is informational only — it never blocks the commit.
87
+
88
+ ```bash
89
+ harness impact-preview
90
+ ```
91
+
92
+ Include the output in the report between the mechanical checks section and the AI review section:
93
+
94
+ ```
95
+ Impact Preview (3 staged files)
96
+ Code: 12 files (routes/login.ts, middleware/verify.ts, +10)
97
+ Tests: 3 tests (auth.test.ts, integration.test.ts, +1)
98
+ Docs: 2 docs (auth-guide.md, api-reference.md)
99
+ Total: 17 affected
100
+ ```
101
+
102
+ If no graph exists, the command prints a nudge message and returns — no action needed. If no files are staged, it says so. Neither case blocks the workflow.
103
+
84
104
  ### Phase 2: Classify Changes
85
105
 
86
106
  Determine whether AI review is needed based on what changed.
@@ -192,6 +212,12 @@ Mechanical Checks:
192
212
  - Tests: PASS (12/12)
193
213
  - Security Scan: PASS (0 errors, 0 warnings)
194
214
 
215
+ Impact Preview (3 staged files)
216
+ Code: 12 files (routes/login.ts, middleware/verify.ts, +10)
217
+ Tests: 3 tests (auth.test.ts, integration.test.ts, +1)
218
+ Docs: 2 docs (auth-guide.md, api-reference.md)
219
+ Total: 17 affected
220
+
195
221
  AI Review: PASS (no issues found)
196
222
  ```
197
223
 
@@ -207,6 +233,11 @@ Mechanical Checks:
207
233
  - Security Scan: WARN (0 errors, 1 warning)
208
234
  - [SEC-NET-001] src/cors.ts:5 — CORS wildcard origin
209
235
 
236
+ Impact Preview (2 staged files)
237
+ Code: 8 files (cors.ts, server.ts, +6)
238
+ Tests: 2 tests (cors.test.ts, server.test.ts)
239
+ Total: 10 affected
240
+
210
241
  AI Review: 2 observations
211
242
  1. [file:line] Possible null dereference — `user.email` accessed without null check after `findUser()` which can return null.
212
243
  2. [file:line] Debug artifact — `console.log('debug:', payload)` appears to be left from debugging.
@@ -244,6 +275,7 @@ fi
244
275
  - Complements harness-code-review (full review) — use pre-commit for quick checks, code-review for thorough analysis
245
276
  - **`assess_project`** — Used in Phase 1 for harness-specific health checks (validate + deps) in a single call.
246
277
  - **`review_changes`** — Used in Phase 4 with `depth: 'quick'` for fast pre-commit diff analysis.
278
+ - **`harness impact-preview`** — Run after mechanical checks pass to show blast radius of staged changes. Informational only — never blocks.
247
279
 
248
280
  ## Success Criteria
249
281
 
@@ -264,6 +296,11 @@ Mechanical Checks:
264
296
  - Types: PASS
265
297
  - Tests: PASS (12/12)
266
298
 
299
+ Impact Preview (2 staged files)
300
+ Code: 5 files (auth.ts, login.ts, +3)
301
+ Tests: 2 tests (auth.test.ts, login.test.ts)
302
+ Total: 7 affected
303
+
267
304
  AI Review: PASS (no issues found)
268
305
  ```
269
306
 
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateAgentsMd
3
+ } from "./chunk-NKDM3FMH.js";
4
+ import "./chunk-2YSQOUHO.js";
5
+ import "./chunk-MHBMTPW7.js";
6
+ export {
7
+ generateAgentsMd
8
+ };
@@ -0,0 +1,13 @@
1
+ import {
2
+ checkDependenciesDefinition,
3
+ handleCheckDependencies
4
+ } from "./chunk-TI4TGEX6.js";
5
+ import "./chunk-K6XAPGML.js";
6
+ import "./chunk-IDZNPTYD.js";
7
+ import "./chunk-W6Y7ZW3Y.js";
8
+ import "./chunk-2YSQOUHO.js";
9
+ import "./chunk-MHBMTPW7.js";
10
+ export {
11
+ checkDependenciesDefinition,
12
+ handleCheckDependencies
13
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ startServer
4
+ } from "../chunk-I6JZYEGT.js";
5
+ import "../chunk-Z75JC6I2.js";
6
+ import "../chunk-GSIVNYVJ.js";
7
+ import "../chunk-PA2XHK75.js";
8
+ import "../chunk-X3MN5UQJ.js";
9
+ import "../chunk-WUJTCNOU.js";
10
+ import "../chunk-ZOAWBDWU.js";
11
+ import "../chunk-WJZDO6OY.js";
12
+ import "../chunk-2YPZKGAG.js";
13
+ import "../chunk-TI4TGEX6.js";
14
+ import "../chunk-K6XAPGML.js";
15
+ import "../chunk-NC6PXVWT.js";
16
+ import "../chunk-IDZNPTYD.js";
17
+ import "../chunk-W6Y7ZW3Y.js";
18
+ import "../chunk-HD4IBGLA.js";
19
+ import "../chunk-3WGJMBKH.js";
20
+ import "../chunk-VRFZWGMS.js";
21
+ import "../chunk-2YSQOUHO.js";
22
+ import "../chunk-MHBMTPW7.js";
23
+
24
+ // src/bin/harness-mcp.ts
25
+ startServer().catch((error) => {
26
+ console.error("Failed to start MCP server:", error);
27
+ process.exit(1);
28
+ });
@@ -1,21 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CLI_VERSION,
4
- createProgram,
3
+ createProgram
4
+ } from "../chunk-ZWC3MN5E.js";
5
+ import "../chunk-VUCPTQ6G.js";
6
+ import {
5
7
  findConfigFile,
6
8
  loadConfig
7
- } from "../chunk-RTPHUDZS.js";
8
- import "../chunk-6JIT7CEM.js";
9
+ } from "../chunk-2NCIKJES.js";
10
+ import "../chunk-GNGELAXY.js";
11
+ import "../chunk-Q6AB7W5Z.js";
12
+ import "../chunk-TRAPF4IX.js";
13
+ import "../chunk-L2KLU56K.js";
14
+ import "../chunk-TEFCFC4H.js";
15
+ import "../chunk-6N4R6FVX.js";
16
+ import "../chunk-EBJQ6N4M.js";
17
+ import "../chunk-QPEH2QPG.js";
18
+ import "../chunk-JSTQ3AWB.js";
19
+ import "../chunk-KET4QQZB.js";
20
+ import "../chunk-NKDM3FMH.js";
21
+ import "../chunk-I6JZYEGT.js";
22
+ import "../chunk-Z75JC6I2.js";
23
+ import "../chunk-GSIVNYVJ.js";
24
+ import "../chunk-PA2XHK75.js";
25
+ import "../chunk-X3MN5UQJ.js";
26
+ import "../chunk-WUJTCNOU.js";
27
+ import "../chunk-ZOAWBDWU.js";
28
+ import "../chunk-WJZDO6OY.js";
29
+ import "../chunk-2YPZKGAG.js";
30
+ import "../chunk-TI4TGEX6.js";
31
+ import "../chunk-K6XAPGML.js";
32
+ import "../chunk-NC6PXVWT.js";
33
+ import "../chunk-IDZNPTYD.js";
34
+ import "../chunk-W6Y7ZW3Y.js";
35
+ import "../chunk-HD4IBGLA.js";
36
+ import {
37
+ handleError
38
+ } from "../chunk-3WGJMBKH.js";
39
+ import "../chunk-VRFZWGMS.js";
40
+ import {
41
+ CLI_VERSION
42
+ } from "../chunk-BM3PWGXQ.js";
43
+ import "../chunk-72GHBOL2.js";
44
+ import "../chunk-C2ERUR3L.js";
9
45
  import {
10
46
  getUpdateNotification,
11
47
  isUpdateCheckEnabled,
12
48
  readCheckState,
13
49
  shouldRunCheck,
14
50
  spawnBackgroundCheck
15
- } from "../chunk-CGSHUJES.js";
16
- import {
17
- handleError
18
- } from "../chunk-ULSRSP53.js";
51
+ } from "../chunk-2YSQOUHO.js";
52
+ import "../chunk-MHBMTPW7.js";
19
53
 
20
54
  // src/bin/update-check-hooks.ts
21
55
  var DEFAULT_INTERVAL_MS = 864e5;
@@ -0,0 +1,12 @@
1
+ import {
2
+ createCheckPhaseGateCommand,
3
+ runCheckPhaseGate
4
+ } from "./chunk-2NCIKJES.js";
5
+ import "./chunk-EBJQ6N4M.js";
6
+ import "./chunk-3WGJMBKH.js";
7
+ import "./chunk-2YSQOUHO.js";
8
+ import "./chunk-MHBMTPW7.js";
9
+ export {
10
+ createCheckPhaseGateCommand,
11
+ runCheckPhaseGate
12
+ };
@@ -0,0 +1,470 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-EBJQ6N4M.js";
4
+ import {
5
+ CLIError,
6
+ ExitCode
7
+ } from "./chunk-3WGJMBKH.js";
8
+ import {
9
+ ArchConfigSchema
10
+ } from "./chunk-2YSQOUHO.js";
11
+ import {
12
+ Err,
13
+ Ok
14
+ } from "./chunk-MHBMTPW7.js";
15
+
16
+ // src/commands/check-phase-gate.ts
17
+ import { Command } from "commander";
18
+ import * as path2 from "path";
19
+ import * as fs2 from "fs";
20
+
21
+ // src/config/loader.ts
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+
25
+ // src/config/schema.ts
26
+ import { z } from "zod";
27
+ var LayerSchema = z.object({
28
+ /** Human-readable name of the layer */
29
+ name: z.string(),
30
+ /** Glob pattern matching files in this layer */
31
+ pattern: z.string(),
32
+ /** Names of other layers this layer is allowed to import from */
33
+ allowedDependencies: z.array(z.string())
34
+ });
35
+ var ForbiddenImportSchema = z.object({
36
+ /** Glob pattern matching source files this rule applies to */
37
+ from: z.string(),
38
+ /** List of modules or patterns that are not allowed to be imported */
39
+ disallow: z.array(z.string()),
40
+ /** Optional custom message to display on violation */
41
+ message: z.string().optional()
42
+ });
43
+ var BoundaryConfigSchema = z.object({
44
+ /** List of globs where files MUST have a corresponding schema/definition */
45
+ requireSchema: z.array(z.string())
46
+ });
47
+ var AgentConfigSchema = z.object({
48
+ /** The execution environment for agents */
49
+ executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
50
+ /** Maximum execution time in milliseconds */
51
+ timeout: z.number().default(3e5),
52
+ /** Optional list of skill IDs pre-authorized for the agent */
53
+ skills: z.array(z.string()).optional()
54
+ });
55
+ var EntropyConfigSchema = z.object({
56
+ /** Patterns to exclude from entropy analysis */
57
+ excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
58
+ /** Whether to automatically attempt to fix simple entropy issues */
59
+ autoFix: z.boolean().default(false)
60
+ });
61
+ var PhaseGateMappingSchema = z.object({
62
+ /** Pattern for implementation files */
63
+ implPattern: z.string(),
64
+ /** Pattern for corresponding specification files */
65
+ specPattern: z.string()
66
+ });
67
+ var PhaseGatesConfigSchema = z.object({
68
+ /** Whether phase gate checks are enabled */
69
+ enabled: z.boolean().default(false),
70
+ /** Severity level when a phase gate check fails */
71
+ severity: z.enum(["error", "warning"]).default("error"),
72
+ /** List of implementation-to-spec mappings */
73
+ mappings: z.array(PhaseGateMappingSchema).default([{ implPattern: "src/**/*.ts", specPattern: "docs/changes/{feature}/proposal.md" }])
74
+ });
75
+ var SecurityConfigSchema = z.object({
76
+ /** Whether security scanning is enabled */
77
+ enabled: z.boolean().default(true),
78
+ /** Whether to fail on any security warning */
79
+ strict: z.boolean().default(false),
80
+ /** Rule-specific severity overrides */
81
+ rules: z.record(z.string(), z.enum(["off", "error", "warning", "info"])).optional(),
82
+ /** Patterns to exclude from security scans */
83
+ exclude: z.array(z.string()).optional()
84
+ }).passthrough();
85
+ var PerformanceConfigSchema = z.object({
86
+ /** Complexity thresholds per module or pattern */
87
+ complexity: z.record(z.unknown()).optional(),
88
+ /** Coupling limits between modules */
89
+ coupling: z.record(z.unknown()).optional(),
90
+ /** Size budget for bundles or directories */
91
+ sizeBudget: z.record(z.unknown()).optional()
92
+ }).passthrough();
93
+ var DesignConfigSchema = z.object({
94
+ /** Strictness of design system enforcement */
95
+ strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
96
+ /** Supported target platforms */
97
+ platforms: z.array(z.enum(["web", "mobile"])).default([]),
98
+ /** Path to design tokens (e.g. JSON or CSS) */
99
+ tokenPath: z.string().optional(),
100
+ /** Brief description of the intended aesthetic direction */
101
+ aestheticIntent: z.string().optional()
102
+ });
103
+ var I18nCoverageConfigSchema = z.object({
104
+ /** Minimum required translation percentage */
105
+ minimumPercent: z.number().min(0).max(100).default(100),
106
+ /** Whether plural forms are required for all keys */
107
+ requirePlurals: z.boolean().default(true),
108
+ /** Whether to detect untranslated strings in source code */
109
+ detectUntranslated: z.boolean().default(true)
110
+ });
111
+ var I18nMcpConfigSchema = z.object({
112
+ /** Name or URL of the MCP server */
113
+ server: z.string(),
114
+ /** Project ID on the remote i18n platform */
115
+ projectId: z.string().optional()
116
+ });
117
+ var I18nConfigSchema = z.object({
118
+ /** Whether i18n management is enabled */
119
+ enabled: z.boolean().default(false),
120
+ /** Strictness of i18n rule enforcement */
121
+ strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
122
+ /** The primary language used for development */
123
+ sourceLocale: z.string().default("en"),
124
+ /** List of locales that translations are required for */
125
+ targetLocales: z.array(z.string()).default([]),
126
+ /** The i18n framework in use */
127
+ framework: z.enum([
128
+ "auto",
129
+ "i18next",
130
+ "react-intl",
131
+ "vue-i18n",
132
+ "flutter-intl",
133
+ "apple",
134
+ "android",
135
+ "custom"
136
+ ]).default("auto"),
137
+ /** Storage format for translation files */
138
+ format: z.string().default("json"),
139
+ /** Syntax used for message formatting */
140
+ messageFormat: z.enum(["icu", "i18next", "custom"]).default("icu"),
141
+ /** Convention for translation keys */
142
+ keyConvention: z.enum(["dot-notation", "snake_case", "camelCase", "custom"]).default("dot-notation"),
143
+ /** Mapping of locales to their file paths */
144
+ translationPaths: z.record(z.string(), z.string()).optional(),
145
+ /** Platforms targeted by this configuration */
146
+ platforms: z.array(z.enum(["web", "mobile", "backend"])).default([]),
147
+ /** Industry vertical (for contextual translations) */
148
+ industry: z.string().optional(),
149
+ /** Translation coverage requirements */
150
+ coverage: I18nCoverageConfigSchema.optional(),
151
+ /** Locale used for pseudo-localization testing */
152
+ pseudoLocale: z.string().optional(),
153
+ /** MCP server for AI-assisted translation */
154
+ mcp: I18nMcpConfigSchema.optional()
155
+ });
156
+ var ModelTierConfigSchema = z.object({
157
+ /** Model ID to use for fast/cheap operations */
158
+ fast: z.string().optional(),
159
+ /** Model ID to use for standard reasoning tasks */
160
+ standard: z.string().optional(),
161
+ /** Model ID to use for complex/critical analysis */
162
+ strong: z.string().optional()
163
+ });
164
+ var ReviewConfigSchema = z.object({
165
+ /** Custom model tier mappings for reviewers */
166
+ model_tiers: ModelTierConfigSchema.optional()
167
+ });
168
+ var HarnessConfigSchema = z.object({
169
+ /** Configuration schema version */
170
+ version: z.literal(1),
171
+ /** Human-readable name of the project */
172
+ name: z.string().optional(),
173
+ /** Root directory of the project, relative to the config file */
174
+ rootDir: z.string().default("."),
175
+ /** Layered architecture definitions */
176
+ layers: z.array(LayerSchema).optional(),
177
+ /** Rules for forbidden cross-module imports */
178
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
179
+ /** Boundary enforcement settings */
180
+ boundaries: BoundaryConfigSchema.optional(),
181
+ /** Path to the project's knowledge map (AGENTS.md) */
182
+ agentsMapPath: z.string().default("./AGENTS.md"),
183
+ /** Directory containing project documentation */
184
+ docsDir: z.string().default("./docs"),
185
+ /** Agent orchestration settings */
186
+ agent: AgentConfigSchema.optional(),
187
+ /** Drift and stale code management settings */
188
+ entropy: EntropyConfigSchema.optional(),
189
+ /** Security scanning configuration */
190
+ security: SecurityConfigSchema.optional(),
191
+ /** Performance and complexity budget settings */
192
+ performance: PerformanceConfigSchema.optional(),
193
+ /** Project template settings (used by 'harness init') */
194
+ template: z.object({
195
+ /** Complexity level of the template */
196
+ level: z.enum(["basic", "intermediate", "advanced"]),
197
+ /** Primary technology framework */
198
+ framework: z.string().optional(),
199
+ /** Template version */
200
+ version: z.number()
201
+ }).optional(),
202
+ /** Phase gate and readiness check configuration */
203
+ phaseGates: PhaseGatesConfigSchema.optional(),
204
+ /** Design system consistency settings */
205
+ design: DesignConfigSchema.optional(),
206
+ /** Internationalization (i18n) settings */
207
+ i18n: I18nConfigSchema.optional(),
208
+ /** Code review settings */
209
+ review: ReviewConfigSchema.optional(),
210
+ /** General architectural enforcement settings */
211
+ architecture: ArchConfigSchema.optional(),
212
+ /** How often (in ms) to check for CLI updates */
213
+ updateCheckInterval: z.number().int().min(0).optional()
214
+ });
215
+
216
+ // src/config/loader.ts
217
+ var CONFIG_FILENAMES = ["harness.config.json"];
218
+ function findConfigFile(startDir = process.cwd()) {
219
+ let currentDir = path.resolve(startDir);
220
+ const root = path.parse(currentDir).root;
221
+ while (currentDir !== root) {
222
+ for (const filename of CONFIG_FILENAMES) {
223
+ const configPath = path.join(currentDir, filename);
224
+ if (fs.existsSync(configPath)) {
225
+ return Ok(configPath);
226
+ }
227
+ }
228
+ currentDir = path.dirname(currentDir);
229
+ }
230
+ return Err(
231
+ new CLIError('No harness.config.json found. Run "harness init" to create one.', ExitCode.ERROR)
232
+ );
233
+ }
234
+ function loadConfig(configPath) {
235
+ if (!fs.existsSync(configPath)) {
236
+ return Err(new CLIError(`Config file not found: ${configPath}`, ExitCode.ERROR));
237
+ }
238
+ let rawConfig;
239
+ try {
240
+ const content = fs.readFileSync(configPath, "utf-8");
241
+ rawConfig = JSON.parse(content);
242
+ } catch (error) {
243
+ return Err(
244
+ new CLIError(
245
+ `Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
246
+ ExitCode.ERROR
247
+ )
248
+ );
249
+ }
250
+ const parsed = HarnessConfigSchema.safeParse(rawConfig);
251
+ if (!parsed.success) {
252
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
253
+ return Err(new CLIError(`Invalid config:
254
+ ${issues}`, ExitCode.ERROR));
255
+ }
256
+ return Ok(parsed.data);
257
+ }
258
+ function resolveConfig(configPath) {
259
+ if (configPath) {
260
+ return loadConfig(configPath);
261
+ }
262
+ const findResult = findConfigFile();
263
+ if (!findResult.ok) {
264
+ return findResult;
265
+ }
266
+ return loadConfig(findResult.value);
267
+ }
268
+
269
+ // src/utils/files.ts
270
+ import { glob } from "glob";
271
+ async function findFiles(pattern, cwd = process.cwd()) {
272
+ return glob(pattern, { cwd, absolute: true });
273
+ }
274
+
275
+ // src/output/formatter.ts
276
+ import chalk from "chalk";
277
+ var OutputMode = {
278
+ /** Output as formatted JSON */
279
+ JSON: "json",
280
+ /** Output as human-readable text */
281
+ TEXT: "text",
282
+ /** Minimal output, only errors and successes */
283
+ QUIET: "quiet",
284
+ /** Full output with detailed context and suggestions */
285
+ VERBOSE: "verbose"
286
+ };
287
+ var OutputFormatter = class {
288
+ /**
289
+ * Creates a new OutputFormatter.
290
+ *
291
+ * @param mode - The output mode to use. Defaults to TEXT.
292
+ */
293
+ constructor(mode = OutputMode.TEXT) {
294
+ this.mode = mode;
295
+ }
296
+ /**
297
+ * Formats raw data for output.
298
+ *
299
+ * @param data - The data to format.
300
+ * @returns A string representation of the data based on the current mode.
301
+ */
302
+ format(data) {
303
+ if (this.mode === OutputMode.JSON) {
304
+ return JSON.stringify(data, null, 2);
305
+ }
306
+ return String(data);
307
+ }
308
+ /**
309
+ * Formats a validation result into a user-friendly string.
310
+ *
311
+ * @param result - The validation result to format.
312
+ * @returns A formatted string containing the validation status and any issues.
313
+ */
314
+ formatValidation(result) {
315
+ if (this.mode === OutputMode.JSON) {
316
+ return JSON.stringify(result, null, 2);
317
+ }
318
+ if (this.mode === OutputMode.QUIET) {
319
+ if (result.valid) return "";
320
+ return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
321
+ }
322
+ const lines = [];
323
+ if (result.valid) {
324
+ lines.push(chalk.green("v validation passed"));
325
+ } else {
326
+ lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
327
+ lines.push("");
328
+ for (const issue of result.issues) {
329
+ const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
330
+ lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
331
+ lines.push(` ${issue.message}`);
332
+ if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
333
+ lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
334
+ }
335
+ }
336
+ }
337
+ return lines.join("\n");
338
+ }
339
+ /**
340
+ * Formats a summary line with a success/failure icon and label.
341
+ *
342
+ * @param label - The name of the field to summarize.
343
+ * @param value - The value to display.
344
+ * @param success - Whether the summary represents a success or failure state.
345
+ * @returns A formatted summary string, or an empty string in JSON or QUIET modes.
346
+ */
347
+ formatSummary(label, value, success) {
348
+ if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
349
+ return "";
350
+ }
351
+ const icon = success ? chalk.green("v") : chalk.red("x");
352
+ return `${icon} ${label}: ${value}`;
353
+ }
354
+ };
355
+
356
+ // src/commands/check-phase-gate.ts
357
+ function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
358
+ const relImpl = path2.relative(cwd, implFile).replace(/\\/g, "/");
359
+ const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
360
+ const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
361
+ const segments = afterBase.split("/");
362
+ const firstSegment = segments[0] ?? "";
363
+ const feature = segments.length > 1 ? firstSegment : path2.basename(firstSegment, path2.extname(firstSegment));
364
+ const specRelative = specPattern.replace("{feature}", feature);
365
+ return path2.resolve(cwd, specRelative);
366
+ }
367
+ async function runCheckPhaseGate(options) {
368
+ const configResult = resolveConfig(options.configPath);
369
+ if (!configResult.ok) {
370
+ return configResult;
371
+ }
372
+ const config = configResult.value;
373
+ const cwd = options.cwd ?? (options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd());
374
+ if (!config.phaseGates?.enabled) {
375
+ return Ok({
376
+ pass: true,
377
+ skipped: true,
378
+ missingSpecs: [],
379
+ checkedFiles: 0
380
+ });
381
+ }
382
+ const phaseGates = config.phaseGates;
383
+ const missingSpecs = [];
384
+ let checkedFiles = 0;
385
+ for (const mapping of phaseGates.mappings) {
386
+ const implFiles = await findFiles(mapping.implPattern, cwd);
387
+ for (const implFile of implFiles) {
388
+ checkedFiles++;
389
+ const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
390
+ if (!fs2.existsSync(expectedSpec)) {
391
+ missingSpecs.push({
392
+ implFile: path2.relative(cwd, implFile).replace(/\\/g, "/"),
393
+ expectedSpec: path2.relative(cwd, expectedSpec).replace(/\\/g, "/")
394
+ });
395
+ }
396
+ }
397
+ }
398
+ const pass = missingSpecs.length === 0;
399
+ return Ok({
400
+ pass,
401
+ skipped: false,
402
+ severity: phaseGates.severity,
403
+ missingSpecs,
404
+ checkedFiles
405
+ });
406
+ }
407
+ function createCheckPhaseGateCommand() {
408
+ const command = new Command("check-phase-gate").description("Verify that implementation files have matching spec documents").action(async (_opts, cmd) => {
409
+ const globalOpts = cmd.optsWithGlobals();
410
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
411
+ const formatter = new OutputFormatter(mode);
412
+ const result = await runCheckPhaseGate({
413
+ configPath: globalOpts.config,
414
+ json: globalOpts.json,
415
+ verbose: globalOpts.verbose,
416
+ quiet: globalOpts.quiet
417
+ });
418
+ if (!result.ok) {
419
+ if (mode === OutputMode.JSON) {
420
+ console.log(JSON.stringify({ error: result.error.message }));
421
+ } else {
422
+ logger.error(result.error.message);
423
+ }
424
+ process.exit(result.error.exitCode);
425
+ }
426
+ const value = result.value;
427
+ if (value.skipped) {
428
+ if (mode === OutputMode.JSON) {
429
+ console.log(formatter.format(value));
430
+ } else if (mode !== OutputMode.QUIET) {
431
+ logger.dim("Phase gates not enabled, skipping.");
432
+ }
433
+ process.exit(ExitCode.SUCCESS);
434
+ }
435
+ const output = formatter.formatValidation({
436
+ valid: value.pass,
437
+ issues: value.missingSpecs.map((m) => ({
438
+ file: m.implFile,
439
+ message: `Missing spec: ${m.expectedSpec}`
440
+ }))
441
+ });
442
+ if (output) {
443
+ console.log(output);
444
+ }
445
+ const summary = formatter.formatSummary(
446
+ "Phase gate check",
447
+ `${value.checkedFiles} files checked, ${value.missingSpecs.length} missing specs`,
448
+ value.pass
449
+ );
450
+ if (summary) {
451
+ console.log(summary);
452
+ }
453
+ if (!value.pass && value.severity === "error") {
454
+ process.exit(ExitCode.VALIDATION_FAILED);
455
+ }
456
+ process.exit(ExitCode.SUCCESS);
457
+ });
458
+ return command;
459
+ }
460
+
461
+ export {
462
+ findConfigFile,
463
+ loadConfig,
464
+ resolveConfig,
465
+ OutputMode,
466
+ OutputFormatter,
467
+ findFiles,
468
+ runCheckPhaseGate,
469
+ createCheckPhaseGateCommand
470
+ };