@popoverai/dotrequirements 0.26.1 → 0.27.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 (84) hide show
  1. package/README.md +13 -69
  2. package/dist/cli.js +19 -7
  3. package/dist/codebase-to-spec/present.js +4 -5
  4. package/dist/codebase-to-spec/validate.js +3 -2
  5. package/dist/commands/acceptance-test.js +4 -2
  6. package/dist/commands/ai-setup.d.ts +8 -2
  7. package/dist/commands/ai-setup.js +154 -310
  8. package/dist/commands/get.js +6 -2
  9. package/dist/commands/init.js +8 -6
  10. package/dist/commands/link-resolution.d.ts +3 -1
  11. package/dist/commands/link-resolution.js +4 -2
  12. package/dist/commands/mcp.d.ts +8 -2
  13. package/dist/commands/mcp.js +17 -6
  14. package/dist/commands/pull.js +36 -3
  15. package/dist/commands/push.js +54 -16
  16. package/dist/commands/report.js +18 -3
  17. package/dist/commands/review-test.d.ts +5 -1
  18. package/dist/commands/review-test.js +117 -15
  19. package/dist/commands/style-check.d.ts +1 -0
  20. package/dist/commands/style-check.js +137 -13
  21. package/dist/commands/tests-for.js +13 -13
  22. package/dist/commands/validate.js +14 -14
  23. package/dist/convex.d.ts +1 -3
  24. package/dist/convex.js +3 -3
  25. package/dist/harness/cache.d.ts +19 -3
  26. package/dist/harness/cache.js +38 -12
  27. package/dist/harness/finalize.js +33 -1
  28. package/dist/harness/index.js +16 -9
  29. package/dist/harness/requirementsLoader.js +12 -0
  30. package/dist/harness/tracking.d.ts +17 -2
  31. package/dist/harness/tracking.js +83 -9
  32. package/dist/push/core.d.ts +50 -0
  33. package/dist/push/core.js +149 -11
  34. package/dist/push/index.d.ts +1 -1
  35. package/dist/push/index.js +1 -1
  36. package/dist/requirements/cloud-ai.d.ts +21 -8
  37. package/dist/requirements/cloud-ai.js +10 -8
  38. package/dist/requirements/cloud-coverage.d.ts +12 -2
  39. package/dist/requirements/cloud-coverage.js +30 -3
  40. package/dist/requirements/grep.d.ts +7 -2
  41. package/dist/requirements/grep.js +75 -47
  42. package/dist/schema/builder.d.ts +1 -1
  43. package/dist/schema/builder.js +13 -0
  44. package/dist/schema/conversions.d.ts +7 -2
  45. package/dist/schema/conversions.js +13 -4
  46. package/dist/schema/parser-core.d.ts +41 -0
  47. package/dist/schema/parser-core.js +113 -18
  48. package/dist/schema/parser.d.ts +8 -26
  49. package/dist/schema/parser.js +23 -251
  50. package/dist/schema/resolver.js +18 -8
  51. package/dist/templates/context-file-section.md +25 -22
  52. package/dist/utils/context-file.d.ts +7 -3
  53. package/dist/utils/context-file.js +10 -7
  54. package/dist/utils/env.js +17 -1
  55. package/dist/utils/oauth-flow.js +8 -0
  56. package/dist/utils/project-settings.d.ts +5 -0
  57. package/dist/utils/project-settings.js +36 -1
  58. package/package.json +3 -5
  59. package/dist/mcp/convexClient.d.ts +0 -19
  60. package/dist/mcp/convexClient.js +0 -24
  61. package/dist/mcp/handlers/authoring.d.ts +0 -41
  62. package/dist/mcp/handlers/authoring.js +0 -104
  63. package/dist/mcp/handlers/debug.d.ts +0 -16
  64. package/dist/mcp/handlers/debug.js +0 -37
  65. package/dist/mcp/handlers/get.d.ts +0 -24
  66. package/dist/mcp/handlers/get.js +0 -65
  67. package/dist/mcp/handlers/index.d.ts +0 -28
  68. package/dist/mcp/handlers/index.js +0 -19
  69. package/dist/mcp/handlers/list.d.ts +0 -7
  70. package/dist/mcp/handlers/list.js +0 -43
  71. package/dist/mcp/handlers/push.d.ts +0 -26
  72. package/dist/mcp/handlers/push.js +0 -186
  73. package/dist/mcp/handlers/report.d.ts +0 -16
  74. package/dist/mcp/handlers/report.js +0 -134
  75. package/dist/mcp/handlers/review.d.ts +0 -51
  76. package/dist/mcp/handlers/review.js +0 -200
  77. package/dist/mcp/handlers/search.d.ts +0 -30
  78. package/dist/mcp/handlers/search.js +0 -58
  79. package/dist/mcp/handlers/test-mapping.d.ts +0 -39
  80. package/dist/mcp/handlers/test-mapping.js +0 -133
  81. package/dist/mcp/handlers/types.d.ts +0 -75
  82. package/dist/mcp/handlers/types.js +0 -25
  83. package/dist/mcp/index.d.ts +0 -45
  84. package/dist/mcp/index.js +0 -634
@@ -1,245 +1,53 @@
1
1
  /**
2
2
  * Parse Markdown requirements files into typed structures.
3
+ *
4
+ * File-level concerns only: frontmatter isolation, metadata validation, and
5
+ * disk IO. All block and criterion parsing delegates to the
6
+ * environment-agnostic parser-core module, which the web editor and Convex
7
+ * also consume — one parser, so the CLI and the cloud cannot drift on what a
8
+ * requirements file means (SYNC-FORMAT-2.3).
3
9
  */
4
10
  import * as fs from "node:fs";
5
11
  import YAML from "yaml";
6
- import { ValidationError, validateKey, validateMetadata, } from "./schemas.js";
7
- const DELIMITER_PATTERN = "(?:→|->)"; // Non-capturing group for both Unicode and ASCII
8
- /**
9
- * Parse a criterion line in "position. Label → content" format.
10
- * Example: "0. Given → user has valid credentials"
11
- * Also supports optional label: "0. → user has valid credentials"
12
- */
13
- export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
14
- // Match: position + period + space + optional(label + space) + delimiter + space + content
15
- // Position can be: 0, 1.0, 2.3.1, etc.
16
- // Label can contain any characters except the delimiter
17
- const pattern = new RegExp(`^\\s*(\\d+(?:\\.\\d+)*)\\.\\s*(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
18
- const match = line.match(pattern);
19
- if (!match) {
20
- return null;
21
- }
22
- return {
23
- position: match[1],
24
- label: match[2] ? match[2].trim() : "", // Preserve original case, empty if no label
25
- content: match[3].trim(),
26
- };
27
- }
28
- /**
29
- * Parse the first line of a dotrequirements block (the root requirement).
30
- * Format: "KEY: content" or "Label → content" or "→ content"
31
- */
32
- function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
33
- const trimmed = line.trim();
34
- // Try to match: KEY + colon + content (explicit key format)
35
- // KEY must be followed by colon (not arrow) to distinguish from "Label → content"
36
- const keyMatch = trimmed.match(/^([\w-]+):\s*(.+)$/);
37
- if (keyMatch) {
38
- return {
39
- label: "", // Keys use empty string for unlabeled requirements
40
- content: keyMatch[2].trim(),
41
- };
42
- }
43
- // Match: optional(label + space) + delimiter + space + content
44
- const pattern = new RegExp(`^(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
45
- const match = trimmed.match(pattern);
46
- if (!match) {
47
- // If no delimiter found, treat entire line as content with empty label
48
- return {
49
- label: "",
50
- content: trimmed,
51
- };
52
- }
53
- return {
54
- label: match[1] ? match[1].trim() : "",
55
- content: match[2].trim(),
56
- };
57
- }
58
- /**
59
- * Parse a dotrequirements fenced block into a RequirementNode tree.
60
- * First line is the requirement content (with optional arrow format).
61
- * Subsequent lines are criteria with position paths.
62
- */
63
- export function parseRequirementBlock(key, blockContent) {
64
- const lines = blockContent.trim().split("\n");
65
- if (lines.length === 0) {
66
- throw new ValidationError("Empty requirement block", key);
67
- }
68
- // First line is the requirement content
69
- const rootParsed = parseRootLine(lines[0]);
70
- if (!rootParsed.content) {
71
- throw new ValidationError("Requirement content (first line) cannot be empty", key);
72
- }
73
- const root = {
74
- id: key,
75
- // Root requirements always use "requirementHeader" label for indexing/querying
76
- // The markdown format doesn't preserve root labels, but we standardize on this value
77
- label: "requirementHeader",
78
- content: rootParsed.content,
79
- children: [],
80
- };
81
- // Parse criteria lines
82
- const criteriaByPosition = new Map();
83
- for (let i = 1; i < lines.length; i++) {
84
- const line = lines[i];
85
- // Skip empty lines and comments
86
- if (!line.trim() || line.trim().startsWith("#")) {
87
- continue;
88
- }
89
- const criterion = parseCriterionLine(line);
90
- if (!criterion) {
91
- throw new ValidationError(`Invalid criterion format at line ${i + 1}: "${line.trim()}"`, key);
92
- }
93
- criteriaByPosition.set(criterion.position, criterion);
94
- }
95
- // Build tree from flat position paths
96
- buildTreeFromPositions(root, key, criteriaByPosition);
97
- return root;
98
- }
99
- /**
100
- * Build a tree structure from flat position paths.
101
- * Positions like "0", "1", "1.0", "1.1", "1.1.0" define the hierarchy.
102
- */
103
- function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
104
- // Get all top-level children (single digit positions: "0", "1", "2")
105
- const topLevelPositions = Array.from(criteriaByPosition.keys())
106
- .filter((pos) => !pos.includes("."))
107
- .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
108
- for (const position of topLevelPositions) {
109
- const criterion = criteriaByPosition.get(position);
110
- const childNode = {
111
- id: `${rootKey}.${position}`,
112
- label: criterion.label,
113
- content: criterion.content,
114
- children: [],
115
- };
116
- // Recursively build children
117
- buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
118
- root.children.push(childNode);
119
- }
120
- }
121
- /**
122
- * Recursively build children for a given position.
123
- */
124
- function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
125
- // Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
126
- const childPositions = Array.from(criteriaByPosition.keys())
127
- .filter((pos) => {
128
- const parts = pos.split(".");
129
- const parentParts = parentPosition.split(".");
130
- // Must be exactly one level deeper
131
- if (parts.length !== parentParts.length + 1) {
132
- return false;
133
- }
134
- // All parent parts must match
135
- for (let i = 0; i < parentParts.length; i++) {
136
- if (parts[i] !== parentParts[i]) {
137
- return false;
138
- }
139
- }
140
- return true;
141
- })
142
- .sort((a, b) => {
143
- const aLast = parseInt(a.split(".").pop(), 10);
144
- const bLast = parseInt(b.split(".").pop(), 10);
145
- return aLast - bLast;
146
- });
147
- for (const position of childPositions) {
148
- const criterion = criteriaByPosition.get(position);
149
- const childNode = {
150
- id: `${rootKey}.${position}`,
151
- label: criterion.label,
152
- content: criterion.content,
153
- children: [],
154
- };
155
- // Recursively build grandchildren
156
- buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
157
- parent.children.push(childNode);
158
- }
159
- }
12
+ import { parseRequirementBlocksFromMarkdown, splitFrontmatter, } from "./parser-core.js";
13
+ import { ValidationError, validateMetadata, } from "./schemas.js";
14
+ // Block/criterion parsing and tree utilities live in parser-core; re-exported
15
+ // here so the schema module's public surface is unchanged.
16
+ export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, } from "./parser-core.js";
160
17
  /**
161
18
  * Extract YAML frontmatter from Markdown content.
162
19
  * Returns { frontmatter, body } where frontmatter is the parsed YAML object.
163
20
  */
164
21
  function extractFrontmatter(content) {
165
- // Match YAML frontmatter: ---\n...\n---
166
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
167
- const match = content.match(frontmatterRegex);
168
- if (!match) {
22
+ // Single fence match: yaml and body come from the same split, so they can
23
+ // never disagree about where the frontmatter closes.
24
+ const split = splitFrontmatter(content);
25
+ if (split === undefined) {
169
26
  throw new ValidationError("Missing YAML frontmatter (should start with ---)");
170
27
  }
171
- const [, frontmatterYaml, body] = match;
172
28
  let frontmatter;
173
29
  try {
174
- frontmatter = YAML.parse(frontmatterYaml);
30
+ frontmatter = YAML.parse(split.yaml);
175
31
  }
176
32
  catch (error) {
177
33
  throw new ValidationError("Failed to parse YAML frontmatter", undefined, error);
178
34
  }
179
- return { frontmatter, body: body.trim() };
180
- }
181
- /**
182
- * Extract requirement blocks from Markdown body.
183
- * Returns a map of requirement key -> { title, blockContent }
184
- *
185
- * The KEY is extracted from the first line inside the code block.
186
- * Headings are optional and only used for human readability.
187
- */
188
- function extractRequirementBlocks(body) {
189
- const blocks = new Map();
190
- // Find all ```dotrequirements code blocks
191
- const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
192
- let match = blockRegex.exec(body);
193
- while (match !== null) {
194
- const blockContent = match[1];
195
- // Extract key from first line of block (format: "KEY: content")
196
- const firstLineMatch = blockContent.match(/^([\w-]+):\s*(.+)/);
197
- if (!firstLineMatch) {
198
- throw new ValidationError(`Invalid requirement block format - first line must be "KEY: content"`, blockContent.substring(0, 50));
199
- }
200
- const key = firstLineMatch[1];
201
- // SYNC-KEY-1: reject malformed keys at parse time so push/validate fail
202
- // locally instead of erroring server-side. Validation only — the original
203
- // key is preserved (normalization happens downstream).
204
- validateKey(key);
205
- // Try to find a heading immediately before this block for the title
206
- // Look backwards from the match position to find the nearest heading
207
- const textBeforeBlock = body.substring(0, match.index);
208
- const headingMatch = textBeforeBlock.match(/^#{2,6}\s+([^\n]+)\s*$/m);
209
- const title = headingMatch ? headingMatch[1].trim() : key;
210
- blocks.set(key, { title, blockContent: blockContent.trim() });
211
- match = blockRegex.exec(body);
212
- }
213
- return blocks;
35
+ return { frontmatter, body: split.body.trim() };
214
36
  }
215
37
  /**
216
38
  * Parse a complete requirements Markdown file.
217
39
  */
218
40
  export function parseRequirementsFile(markdownContent) {
219
- // Extract frontmatter and body
41
+ // CRLF normalization (SYNC-FORMAT-1) happens inside the shared parser-core
42
+ // helpers (splitFrontmatter / parseRequirementBlocksFromMarkdown), so every
43
+ // entry point agrees.
220
44
  const { frontmatter, body } = extractFrontmatter(markdownContent);
221
- // Validate metadata from frontmatter
222
45
  const metadata = validateMetadata(frontmatter);
223
- // Extract requirement blocks
224
- const blocks = extractRequirementBlocks(body);
225
- if (blocks.size === 0) {
46
+ const requirements = parseRequirementBlocksFromMarkdown(body);
47
+ if (requirements.length === 0) {
226
48
  throw new ValidationError("No requirement blocks found in document");
227
49
  }
228
- // Parse each requirement block
229
- const requirements = [];
230
- for (const [key, { blockContent }] of blocks) {
231
- try {
232
- const reqNode = parseRequirementBlock(key, blockContent);
233
- requirements.push(reqNode);
234
- }
235
- catch (error) {
236
- throw new ValidationError(`Failed to parse requirement ${key}`, key, error);
237
- }
238
- }
239
- return {
240
- metadata,
241
- requirements,
242
- };
50
+ return { metadata, requirements };
243
51
  }
244
52
  /**
245
53
  * Parse requirements file from disk.
@@ -256,40 +64,4 @@ export function parseRequirementsFromFile(filePath) {
256
64
  throw new ValidationError(`Failed to read file: ${filePath}`, undefined, error);
257
65
  }
258
66
  }
259
- /**
260
- * Flatten a requirement tree into a list of all nodes.
261
- * Useful for searching or displaying all requirements.
262
- */
263
- export function flattenRequirementTree(node) {
264
- const result = [node];
265
- for (const child of node.children) {
266
- result.push(...flattenRequirementTree(child));
267
- }
268
- return result;
269
- }
270
- /**
271
- * Find a requirement by its ID in a tree.
272
- */
273
- export function findRequirementById(nodes, id) {
274
- for (const node of nodes) {
275
- if (node.id === id) {
276
- return node;
277
- }
278
- const found = findRequirementById(node.children, id);
279
- if (found) {
280
- return found;
281
- }
282
- }
283
- return undefined;
284
- }
285
- /**
286
- * Get all requirements from multiple files as a flat list.
287
- */
288
- export function getAllRequirements(requirements) {
289
- const all = [];
290
- for (const req of requirements) {
291
- all.push(...flattenRequirementTree(req));
292
- }
293
- return all;
294
- }
295
67
  //# sourceMappingURL=parser.js.map
@@ -16,7 +16,9 @@ export function parseRequirementPath(path) {
16
16
  * E.g., "given#2" → { label: "given", index: 2 }
17
17
  */
18
18
  export function parsePathSegment(segment) {
19
- const match = segment.match(/^(\w+)(?:#(\d+))?$/);
19
+ // HARNESS-RESOLVE-2.0: multi-word labels are addressed by their kebab-case
20
+ // form (e.g. "Edge Case" → "edge-case"), so segments may contain hyphens.
21
+ const match = segment.match(/^([\w-]+)(?:#(\d+))?$/);
20
22
  if (!match) {
21
23
  throw new Error(`Invalid path segment: ${segment}`);
22
24
  }
@@ -47,10 +49,18 @@ export function findChildrenByLabel(node, label) {
47
49
  * Returns the child node and its numeric index.
48
50
  */
49
51
  export function resolvePathSegment(node, segment) {
50
- // Check if segment is numeric (direct index)
52
+ // Check if segment is numeric (written position)
53
+ // HARNESS-RESOLVE-1: a numeric segment means the position written in the
54
+ // file, not the array index. Node ids preserve written positions (which may
55
+ // have gaps), so match the child whose id's last position segment equals
56
+ // the requested position. When no child was written at that position,
57
+ // resolution fails rather than binding to a neighboring criterion.
51
58
  if (/^\d+$/.test(segment)) {
52
- const index = parseInt(segment, 10);
53
- if (index < node.children.length) {
59
+ const index = node.children.findIndex((child) => {
60
+ const lastDot = child.id.lastIndexOf(".");
61
+ return child.id.slice(lastDot + 1) === segment;
62
+ });
63
+ if (index !== -1) {
54
64
  return {
55
65
  child: node.children[index],
56
66
  index,
@@ -158,13 +168,13 @@ export function checkPathAmbiguity(requirements, path) {
158
168
  // Check each segment for ambiguity
159
169
  for (let i = 1; i < segments.length; i++) {
160
170
  const segment = segments[i];
161
- // Numeric segments are never ambiguous
171
+ // Numeric segments are never ambiguous (resolved by written position)
162
172
  if (/^\d+$/.test(segment)) {
163
- const index = parseInt(segment, 10);
164
- if (index >= currentNode.children.length) {
173
+ const resolved = resolvePathSegment(currentNode, segment);
174
+ if (!resolved) {
165
175
  return 0; // Invalid path
166
176
  }
167
- currentNode = currentNode.children[index];
177
+ currentNode = resolved.child;
168
178
  continue;
169
179
  }
170
180
  // Check for label disambiguation (#N)
@@ -10,13 +10,14 @@ requirements, so your plan should too.
10
10
 
11
11
  **ALWAYS follow this workflow when changing system behavior** (new features, bug fixes, any behavioral change). Only pure refactoring (same behavior, different code) may skip requirements.
12
12
 
13
- 1. **Find or write requirements** - Check `.requirements/` for existing specs; write new ones if needed
14
- 2. **Style-check** - Run `mcp__dotrequirements__style_check` on the file
15
- 3. **Get approval** - Present requirements, wait for go-ahead
16
- 4. **Implement** - Build the feature
17
- 5. **Write tests** - Reference requirements with `requirement()`
18
- 6. **Run tests** - Verify everything passes
19
- 7. **Review tests** - Run `mcp__dotrequirements__review_test` to validate coverage
13
+ 1. **Find or write requirements** - Check `.requirements/` for existing specs (`dotreq search`, `dotreq get`, `dotreq list`); write new ones if needed (`dotreq create-requirement-document` prints the template and style guide)
14
+ 2. **Style-review** - Dispatch a subagent to run `dotreq style-check <file>` and judge the requirements against the style guide it emits. If this platform cannot dispatch subagents, run the command and perform the review in this conversation.
15
+ 3. **Validate** - Run `dotreq validate` to check syntax
16
+ 4. **Get approval** - Present requirements, wait for go-ahead
17
+ 5. **Implement** - Build the feature
18
+ 6. **Write tests** - Reference requirements with `requirement()`
19
+ 7. **Run tests** - Verify everything passes
20
+ 8. **Test-review** - Dispatch a subagent to run `dotreq review-test <test-file>` and judge the tests against the materials it emits. Same fallback: no subagent support, review in this conversation.
20
21
 
21
22
  ### Requirements Syntax
22
23
 
@@ -31,7 +32,7 @@ DOMAIN-1: Short description of expected behavior
31
32
  - Criteria: `position. -> content` (optional label before the arrow)
32
33
  - Nesting: Indent with 2 spaces, use `x.y` position paths
33
34
  - Delimiter: `->` or `→`
34
- - **Key style**: Use sequential keys with a short domain prefix (`ORCHESTRATOR-1`, `ORCHESTRATOR-2`, ...) rather than semantic keys (`AUTONOMOUS-ADVANCE`). Sequential keys stay stable when a requirement gets reworded, so test references don't break. Call `create_requirement_document` for full key-naming guidance.
35
+ - **Key style**: Use sequential keys with a short domain prefix (`ORCHESTRATOR-1`, `ORCHESTRATOR-2`, ...) rather than semantic keys (`AUTONOMOUS-ADVANCE`). Sequential keys stay stable when a requirement gets reworded, so test references don't break. Run `dotreq create-requirement-document` for full key-naming guidance.
35
36
 
36
37
  ### Test Usage
37
38
 
@@ -44,21 +45,23 @@ test(requirement('REQ-ID'), () => { /* test the requirement */ });
44
45
  test(requirement('REQ-ID.0'), () => { /* test specific criterion */ });
45
46
  ```
46
47
 
47
- ### MCP Tools
48
+ ### CLI Verbs
48
49
 
49
50
  **Exploration:**
50
- - `list_requirements` - Overview of all requirements (set `untested: true` to filter to coverage gaps)
51
- - `get_requirement` - Requirement tree with test coverage
52
- - `search_requirements` - Search by text/regex
51
+ - `dotreq list [--untested]` - Overview of all requirements (`--untested` filters to coverage gaps)
52
+ - `dotreq get <id>` - Requirement tree with test coverage
53
+ - `dotreq search <query> [--regex]` - Search by text/regex
54
+ - `dotreq requirements-for <test-file>` - See requirements a test file covers
55
+ - `dotreq tests-for <req-file>` - See test coverage for a requirements file
53
56
 
54
57
  **Authoring:**
55
- - `create_requirement_document` - Get template with format guidance
56
- - `validate_requirements` - Check syntax (works offline)
57
- - `style_check` - AI feedback on clarity
58
- - `push_requirements` - Sync to cloud
59
-
60
- **Testing:**
61
- - `get_requirements_by_test` - See requirements a test file covers
62
- - `list_requirements` (with `untested: true`) - Find requirements without tests
63
- - `report_coverage` - Get test coverage from local cache or cloud
64
- - `review_test` - Validate tests match requirement intent
58
+ - `dotreq create-requirement-document [path]` - Print the template and style guide
59
+ - `dotreq validate [glob]` - Check syntax (works offline)
60
+ - `dotreq push [file]` - Sync to cloud (shows diff preview, then confirms)
61
+
62
+ **Review (judgment runs in the dispatched subagent):**
63
+ - `dotreq style-check <file>` - Emits the style guide + content for the reviewer to judge (`--source cloud` for hosted review)
64
+ - `dotreq review-test <test-file>` - Emits referenced requirement trees + test content for the reviewer to judge (`--source cloud` for hosted review)
65
+
66
+ **Coverage:**
67
+ - `dotreq report [--source local|cloud]` - Test coverage report
@@ -3,7 +3,11 @@
3
3
  */
4
4
  export declare function getContextFileName(platform: string): string | null;
5
5
  /**
6
- * Find the git root directory
6
+ * Find the git root directory.
7
+ *
8
+ * CONTEXT-FILE-6: in a worktree, .git is a FILE pointing at the main
9
+ * checkout — matching directories only would walk past it and resolve to
10
+ * the main checkout's root, landing the context file in the wrong tree.
7
11
  */
8
12
  export declare function findGitRoot(): Promise<string | null>;
9
13
  /**
@@ -39,8 +43,8 @@ export declare function getContextFilePath(platform: string): Promise<string | n
39
43
  * Build a user-facing message explaining that context file installation
40
44
  * was skipped because the current directory is not inside a git repository.
41
45
  *
42
- * The MCP server itself is configured separately, so this message is only
43
- * about the second step (writing the platform's context file).
46
+ * Writing the platform's context file is the whole of setup now, so a missing
47
+ * git root means nothing was installed.
44
48
  */
45
49
  export declare function buildNoGitRepoMessage(fileName: string): string;
46
50
  //# sourceMappingURL=context-file.d.ts.map
@@ -11,7 +11,8 @@ const PLATFORM_CONTEXT_FILES = {
11
11
  cursor: "AGENTS.md",
12
12
  codex: "AGENTS.md",
13
13
  "github-copilot": "AGENTS.md",
14
- antigravity: "GEMINI.md",
14
+ // Antigravity 2.0 retired the GEMINI.md/.gemini conventions for AGENTS.md
15
+ antigravity: "AGENTS.md",
15
16
  };
16
17
  /**
17
18
  * Get the appropriate context file name for a platform
@@ -20,10 +21,14 @@ export function getContextFileName(platform) {
20
21
  return PLATFORM_CONTEXT_FILES[platform] ?? null;
21
22
  }
22
23
  /**
23
- * Find the git root directory
24
+ * Find the git root directory.
25
+ *
26
+ * CONTEXT-FILE-6: in a worktree, .git is a FILE pointing at the main
27
+ * checkout — matching directories only would walk past it and resolve to
28
+ * the main checkout's root, landing the context file in the wrong tree.
24
29
  */
25
30
  export async function findGitRoot() {
26
- const gitDir = await findUp(".git", { type: "directory" });
31
+ const gitDir = await findUp(".git", { type: "both" });
27
32
  return gitDir ? dirname(gitDir) : null;
28
33
  }
29
34
  /**
@@ -95,8 +100,8 @@ export async function getContextFilePath(platform) {
95
100
  * Build a user-facing message explaining that context file installation
96
101
  * was skipped because the current directory is not inside a git repository.
97
102
  *
98
- * The MCP server itself is configured separately, so this message is only
99
- * about the second step (writing the platform's context file).
103
+ * Writing the platform's context file is the whole of setup now, so a missing
104
+ * git root means nothing was installed.
100
105
  */
101
106
  export function buildNoGitRepoMessage(fileName) {
102
107
  return [
@@ -106,8 +111,6 @@ export function buildNoGitRepoMessage(fileName) {
106
111
  " To finish setup, either:",
107
112
  " • run `git init` here, then re-run `dotreq ai-setup`, or",
108
113
  " • `cd` into an existing project directory and run `dotreq ai-setup` there.",
109
- "",
110
- " Note: the MCP server itself was configured successfully — only the context file step was skipped.",
111
114
  ].join("\n");
112
115
  }
113
116
  //# sourceMappingURL=context-file.js.map
package/dist/utils/env.js CHANGED
@@ -23,7 +23,7 @@ export function loadEnvFile(cwd = process.cwd(), force = false) {
23
23
  const match = line.match(/^([^=]+)=(.*)$/);
24
24
  if (match) {
25
25
  const key = match[1].trim();
26
- const value = match[2].trim();
26
+ const value = stripSurroundingQuotes(match[2].trim());
27
27
  // Set if force=true, or if not already in process.env
28
28
  if (force || !process.env[key]) {
29
29
  process.env[key] = value;
@@ -36,4 +36,20 @@ export function loadEnvFile(cwd = process.cwd(), force = false) {
36
36
  // The commands will error appropriately if env vars are missing
37
37
  }
38
38
  }
39
+ /**
40
+ * Strip a single matching pair of surrounding single or double quotes from a
41
+ * value, matching dotenv behavior. Credentials written in standard
42
+ * dotenv/Next.js style (KEY="value") should not carry the quote characters
43
+ * into process.env. Mismatched or interior quotes are left untouched.
44
+ */
45
+ function stripSurroundingQuotes(value) {
46
+ if (value.length >= 2) {
47
+ const first = value[0];
48
+ const last = value[value.length - 1];
49
+ if ((first === '"' || first === "'") && first === last) {
50
+ return value.slice(1, -1);
51
+ }
52
+ }
53
+ return value;
54
+ }
39
55
  //# sourceMappingURL=env.js.map
@@ -32,6 +32,14 @@ export async function executeOAuthFlow() {
32
32
  port: 3010,
33
33
  timeoutMs: 120000,
34
34
  });
35
+ // Attach a no-op rejection handler immediately. startCallbackServer can
36
+ // reject synchronously-adjacent to creation (e.g. listen EADDRINUSE when the
37
+ // callback port is busy), but the real handler isn't awaited until after the
38
+ // browser launch below. Without this, an early rejection has no handler and
39
+ // Node escalates it to a fatal unhandledRejection, bypassing wrapCommand's
40
+ // clean CLI error formatting. The awaited `callbackPromise` below still sees
41
+ // the rejection and surfaces it as a normal thrown error.
42
+ callbackPromise.catch(() => { });
35
43
  // Build authorization URL
36
44
  const authUrl = buildAuthorizationUrl({
37
45
  clientId: WORKOS_CLIENT_ID,
@@ -41,6 +41,7 @@ export interface ProjectInfo {
41
41
  * Returns undefined if either is missing.
42
42
  */
43
43
  export declare function getCredentialsFromEnv(): ProjectSettings | undefined;
44
+ export declare function setAuthFromEnv(enabled: boolean): void;
44
45
  /**
45
46
  * Find the project root by walking up from startDir looking for .requirements/ folder
46
47
  * Returns the directory containing .requirements/, or undefined if not found
@@ -54,6 +55,10 @@ export declare function readProjectSettings(projectRoot: string): ProjectSetting
54
55
  /**
55
56
  * Write project settings to .requirements/project-settings.json
56
57
  * Creates .requirements/ directory if it doesn't exist
58
+ *
59
+ * SETTINGS-2.3/2.4: fields being written replace the stored ones, but any
60
+ * other existing settings (defaultURL, browserTest, ...) survive — re-linking
61
+ * with fresh credentials must not erase the rest of the file.
57
62
  */
58
63
  export declare function writeProjectSettings(projectRoot: string, settings: ProjectSettings): void;
59
64
  /**
@@ -18,6 +18,19 @@ export function getCredentialsFromEnv() {
18
18
  }
19
19
  return undefined;
20
20
  }
21
+ /**
22
+ * AUTHZ-6: explicit CI/CD credential injection. Set by the global
23
+ * --auth-from-env flag (cli.ts preAction hook); when enabled,
24
+ * getProjectCredentials reads DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET
25
+ * instead of file-based discovery. Without the flag those variables are
26
+ * ignored entirely — the explicit opt-in prevents credential conflicts
27
+ * between local and CI environments. (Previously the retired MCP server's
28
+ * --auth-from-env; ported to the CLI with identical semantics.)
29
+ */
30
+ let authFromEnv = false;
31
+ export function setAuthFromEnv(enabled) {
32
+ authFromEnv = enabled;
33
+ }
21
34
  /**
22
35
  * Find the project root by walking up from startDir looking for .requirements/ folder
23
36
  * Returns the directory containing .requirements/, or undefined if not found
@@ -106,6 +119,10 @@ export function readProjectSettings(projectRoot) {
106
119
  /**
107
120
  * Write project settings to .requirements/project-settings.json
108
121
  * Creates .requirements/ directory if it doesn't exist
122
+ *
123
+ * SETTINGS-2.3/2.4: fields being written replace the stored ones, but any
124
+ * other existing settings (defaultURL, browserTest, ...) survive — re-linking
125
+ * with fresh credentials must not erase the rest of the file.
109
126
  */
110
127
  export function writeProjectSettings(projectRoot, settings) {
111
128
  const requirementsDir = path.join(projectRoot, REQUIREMENTS_DIR);
@@ -114,7 +131,16 @@ export function writeProjectSettings(projectRoot, settings) {
114
131
  if (!fs.existsSync(requirementsDir)) {
115
132
  fs.mkdirSync(requirementsDir, { recursive: true });
116
133
  }
117
- const content = `${JSON.stringify(settings, null, 2)}\n`;
134
+ let existing;
135
+ try {
136
+ existing = readProjectSettings(projectRoot);
137
+ }
138
+ catch {
139
+ // Unreadable or invalid existing file: nothing preservable, write fresh
140
+ existing = undefined;
141
+ }
142
+ const merged = existing ? { ...existing, ...settings } : settings;
143
+ const content = `${JSON.stringify(merged, null, 2)}\n`;
118
144
  fs.writeFileSync(settingsPath, content, "utf-8");
119
145
  }
120
146
  /**
@@ -144,6 +170,15 @@ export function getProjectInfo(startDir = process.cwd()) {
144
170
  * Get project credentials, throwing helpful error if not available
145
171
  */
146
172
  export function getProjectCredentials(startDir = process.cwd()) {
173
+ // AUTHZ-6.0 / 6.1: with the explicit flag, env credentials replace
174
+ // file-based discovery entirely
175
+ if (authFromEnv) {
176
+ const envCredentials = getCredentialsFromEnv();
177
+ if (!envCredentials) {
178
+ throw new Error("--auth-from-env requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set.");
179
+ }
180
+ return envCredentials;
181
+ }
147
182
  const info = getProjectInfo(startDir);
148
183
  if (!info) {
149
184
  throw new Error('No dotrequirements project found. Run "dotrequirements init" to create one.');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.26.1",
4
- "description": "Requirements tracking CLI, test harness, and MCP server",
3
+ "version": "0.27.0",
4
+ "description": "Requirements tracking CLI and test harness",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dotrequirements": "./dist/cli.js",
@@ -11,8 +11,7 @@
11
11
  ".": "./dist/cli.js",
12
12
  "./test": "./dist/harness/index.js",
13
13
  "./schema": "./dist/schema/index.js",
14
- "./schema/browser": "./dist/schema/browser.js",
15
- "./mcp": "./dist/mcp/index.js"
14
+ "./schema/browser": "./dist/schema/browser.js"
16
15
  },
17
16
  "publishConfig": {
18
17
  "access": "public"
@@ -34,7 +33,6 @@
34
33
  "test-coverage",
35
34
  "bdd",
36
35
  "tdd",
37
- "mcp",
38
36
  "ai-assistant"
39
37
  ],
40
38
  "author": "Will Raymer @Popover",