@popoverai/dotrequirements 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,10 +44,10 @@ Create `*.requirements.md` files in `.requirements/` or colocate them with your
44
44
  ```markdown
45
45
  ---
46
46
  document:
47
- title: "Authentication Requirements"
47
+ defaultPrefix: AUTH-LOGIN
48
48
  ---
49
49
 
50
- ## User Authentication
50
+ # Authentication Requirements
51
51
 
52
52
  ```dotrequirements
53
53
  AUTH-LOGIN-1: A registered user, Jamie, can log in to their account
@@ -118,12 +118,28 @@ This shows the team name, prompts you to sign in, adds you to the team, and lets
118
118
 
119
119
  ### `dotreq link`
120
120
 
121
- Link your local environment to an existing project (when you already have a project in dot•requirements cloud).
121
+ Connect your local project to the cloud — selecting an existing project, or creating one when the team has room. Opens a browser for authentication, then prints the account it signed in as (`Signed in as you@example.com`).
122
122
 
123
123
  ```bash
124
124
  dotreq link
125
+ dotreq link --yes --json # non-interactive (for AI assistants/scripts)
126
+ dotreq link --connect my-project # connect to a specific project
127
+ dotreq link --create -n my-api # create a new project with this name
125
128
  ```
126
129
 
130
+ **Options:**
131
+
132
+ | Option | Description |
133
+ |--------|-------------|
134
+ | `-y, --yes` | Non-interactive: resolve every decision from defaults or flags; never prompt |
135
+ | `--json` | Machine-readable output (one JSON object on stdout) |
136
+ | `--team <nameOrId>` | Use this team (implies `--yes`) |
137
+ | `--connect <slug>` | Connect to this existing project (implies `--yes`) |
138
+ | `--create` | Create a new project (implies `--yes`) |
139
+ | `-n, --name <name>` | Project name when creating |
140
+
141
+ In non-interactive mode the only human step is completing the browser login. When a genuine choice exists — multiple teams, existing projects, a plan at its project limit — link exits with code `2` and (with `--json`) a `decision_needed` object listing the options, each with the exact flag to retry with where a flag can express it (remedies that happen outside the CLI, like upgrading the team, carry no retry flag). Non-interactive link never syncs the cloud down, so freshly generated local requirements are never overwritten.
142
+
127
143
  ### `dotreq diff`
128
144
 
129
145
  Show how the repo and the cloud differ, read-only. Each document gets a verdict (in sync, additions in repo, additions in cloud, conflict, only in repo, only in cloud, invalid file).
@@ -371,9 +387,9 @@ The following features work fully offline—no account required:
371
387
 
372
388
  The following features require a dot•requirements cloud account:
373
389
 
374
- - Sync requirements (`pull` / `push`)
390
+ - Sync requirements (`dotreq sync`, `dotreq diff`)
375
391
  - Cloud coverage queries (`dotreq report --source cloud`)
376
- - AI-powered style checking and test review (`dotreq style-check`, `dotreq review-test`)
392
+ - Hosted style checking and test review (`dotreq style-check --source cloud`, `dotreq review-test --source cloud`)
377
393
  - Team collaboration
378
394
 
379
395
  To enable cloud features, run `dotreq link` to connect your project to the cloud.
@@ -568,12 +584,12 @@ src/components/
568
584
  ```markdown
569
585
  ---
570
586
  document:
571
- title: "Document Title"
587
+ defaultPrefix: AUTH-LOGIN
572
588
  ---
573
589
 
574
- # Optional Markdown Content
590
+ # Document Title
575
591
 
576
- You can include any Markdown here for context.
592
+ The leading H1 is the document's title. You can include any Markdown for context.
577
593
 
578
594
  ## Requirement Heading
579
595
 
@@ -598,7 +614,8 @@ AUTH-LOGIN-2: A user with two-factor auth must provide an OTP
598
614
 
599
615
  ### Format Details
600
616
 
601
- - **Frontmatter**: YAML metadata (only `document.title` required for push)
617
+ - **Frontmatter**: YAML metadata (must be present, but no field is required — sync adds `document.id` to link the file to its cloud document)
618
+ - **Title**: The body's leading H1 (a legacy frontmatter `document.title` is tolerated as a fallback)
602
619
  - **Headings**: Optional documentation (not parsed as requirement data)
603
620
  - **Fenced blocks**: `dotrequirements` blocks contain structured requirement data
604
621
  - **First line**: `KEY: content` — the requirement identifier and summary
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { createRequirementDocumentCommand } from "./commands/create-requirement-
11
11
  import { diffCommand } from "./commands/diff.js";
12
12
  import { finalizeCommand } from "./commands/finalize.js";
13
13
  import { getCommand } from "./commands/get.js";
14
+ import { greenfieldDiscoveryCommand } from "./commands/greenfield-discovery.js";
14
15
  import { initCommand } from "./commands/init.js";
15
16
  import { linkCommand } from "./commands/link.js";
16
17
  import { listCommand } from "./commands/list.js";
@@ -206,6 +207,10 @@ program
206
207
  .command("create-requirement-document [file-path]")
207
208
  .description("Print the project style guide; optional [file-path] annotates the output for that target")
208
209
  .action(wrapCommand(createRequirementDocumentCommand));
210
+ program
211
+ .command("greenfield-discovery")
212
+ .description("Print the greenfield interview protocol — for working out what something new should do before writing its spec")
213
+ .action(wrapCommand(greenfieldDiscoveryCommand));
209
214
  registerCodebaseToSpec(program);
210
215
  program.parse();
211
216
  //# sourceMappingURL=cli.js.map
@@ -37,6 +37,7 @@ export async function createRequirementDocumentCommand(filePath) {
37
37
  customStyleGuidance,
38
38
  filePath,
39
39
  localStyleGuide,
40
+ surface: "workspace",
40
41
  }));
41
42
  }
42
43
  //# sourceMappingURL=create-requirement-document.js.map
@@ -28,7 +28,7 @@ export async function diffCommand(scope, options) {
28
28
  // DIFF-2: any scope argument means document-scope → verbose. No scope means
29
29
  // project-scope → verdict list.
30
30
  const verbose = scope.length > 0;
31
- const notInSync = documents.filter((d) => d.verdict !== "in_sync");
31
+ const notInSync = documents.filter((d) => d.verdict !== "in_sync" || d.attachments?.differ);
32
32
  // DIFF-1.2: don't list every document when everything is in sync.
33
33
  if (notInSync.length === 0 && documents.length > 0) {
34
34
  console.log(`✓ Repo and ${brand} cloud are in sync (${documents.length} document(s)).`);
@@ -49,6 +49,11 @@ export async function diffCommand(scope, options) {
49
49
  console.log(formatUnitDetail(detail));
50
50
  }
51
51
  }
52
+ if (verbose && doc.attachments?.differ) {
53
+ for (const detail of doc.attachments.details) {
54
+ console.log(formatUnitDetail(detail));
55
+ }
56
+ }
52
57
  if (doc.verdict === "conflict") {
53
58
  console.log(` ${resolutionHint(doc)}`);
54
59
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CLI-GREENFIELD-1: print the greenfield interview protocol for context
3
+ * priming. The protocol interpolates nothing — unlike the style guide there
4
+ * are no project conventions to discover at interview time (those arrive at
5
+ * the writing step, through create-requirement-document) — so this touches
6
+ * neither the workspace nor the cloud, and works before any project is
7
+ * linked (CLI-GREENFIELD-1.3 falls out of there being nothing to fail).
8
+ */
9
+ export declare function greenfieldDiscoveryCommand(): void;
10
+ //# sourceMappingURL=greenfield-discovery.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { generateGreenfieldProtocol } from "../requirements/greenfield.js";
2
+ /**
3
+ * CLI-GREENFIELD-1: print the greenfield interview protocol for context
4
+ * priming. The protocol interpolates nothing — unlike the style guide there
5
+ * are no project conventions to discover at interview time (those arrive at
6
+ * the writing step, through create-requirement-document) — so this touches
7
+ * neither the workspace nor the cloud, and works before any project is
8
+ * linked (CLI-GREENFIELD-1.3 falls out of there being nothing to fail).
9
+ */
10
+ export function greenfieldDiscoveryCommand() {
11
+ console.log(generateGreenfieldProtocol({ artifact: "document", formatPointer: true }));
12
+ }
13
+ //# sourceMappingURL=greenfield-discovery.js.map
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import * as readline from "node:readline";
9
+ import { attachmentSyncNeeded } from "../sync/attachments.js";
9
10
  import { executePlan } from "../sync/execute.js";
10
11
  import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateIdAbortMessage, filterByScope, resolutionHint, } from "../sync/index.js";
11
12
  import { buildPlan, resolveMode, } from "../sync/plan.js";
@@ -72,22 +73,52 @@ export async function syncCommand(scope, options) {
72
73
  console.log(`No document matched scope "${token}".`);
73
74
  }
74
75
  const plan = buildPlan(documents, mode);
75
- // SYNC-MODE-4: name every deletion before writing anything.
76
+ // SYNC-MODE-4 gates WHOLE-DOCUMENT deletions only. Content-level removals
77
+ // ride authority ungated — a requirement block dropped from a file is
78
+ // deleted from the cloud by the same run without a prompt, and attachments
79
+ // follow that rule, not the document rule (an attached link is also the
80
+ // most recoverable thing here: re-paste the URL). The docs promise
81
+ // "attachments never hold up the rest of the spec"; a consent gate on
82
+ // their removal held up the entire sync (decided 2026-08-20). What
83
+ // authority removed is NAMED after the run instead — see the
84
+ // attachmentRemovals report below.
76
85
  const deletions = plan.filter((p) => p.action === "cloud_delete" || p.action === "local_delete");
77
86
  if (deletions.length > 0) {
87
+ // SYNC-MODE-4.0: every deletion is NAMED before anything else happens —
88
+ // in the non-TTY refusal too, or the operator is told three documents
89
+ // will be destroyed with no way to learn which three.
90
+ printDeletionNotice(deletions);
78
91
  // SYNC-MODE-4.3: a non-interactive run cannot consent — refuse rather than
79
92
  // hanging on a prompt that will never answer (or worse, deleting silently).
80
93
  if (!options.yes && !process.stdin.isTTY) {
81
- throw new Error(`This sync would delete ${deletions.length} document(s), and there is no terminal to confirm on. ` +
82
- `Re-run with --yes to consent to the deletions listed by \`dotreq diff\`.`);
94
+ throw new Error(`This sync would delete ${deletions.length} document(s), and there is no terminal ` +
95
+ `to confirm on. Re-run with --yes to consent to the deletions listed above.`);
83
96
  }
84
- if (!(await confirmDeletions(deletions, options.yes))) {
97
+ if (!(await confirmDeletions(options.yes))) {
85
98
  console.log("Sync cancelled.");
86
99
  return "cancelled";
87
100
  }
88
101
  }
89
- if (plan.every((p) => p.action === "skip")) {
102
+ // ATTACH-10.4: a difference this mode deliberately leaves alone (a no-key
103
+ // file under repo authority, a duplicated legacy cloud row under cloud
104
+ // authority) would otherwise leave `dotreq diff --exit-code` red with
105
+ // nothing in this mode ever clearing it — say so and name the gesture
106
+ // that repairs it, on EVERY run that leaves one behind, not only when the
107
+ // sync had nothing else to do (the round-1 release review's finding).
108
+ const leftAlone = plan.filter((p) => p.doc.attachments?.differ &&
109
+ !attachmentSyncNeeded(p.doc.attachments, mode));
110
+ const printLeftAlone = () => {
111
+ if (leftAlone.length > 0) {
112
+ console.log(` (${leftAlone.length} document(s) have attachment differences this mode leaves alone — a repo-writing sync, e.g. plain \`dotreq sync\`, reconciles them.)`);
113
+ }
114
+ };
115
+ // ATTACH-10.2: attachments resolve outside the body's verdict, so "every
116
+ // body action is skip" is not "nothing to do" — the resolver, per mode,
117
+ // is what knows whether attachment work remains. Without this, an
118
+ // attachment-only difference reported by diff would never clear.
119
+ if (plan.every((p) => p.action === "skip" && !attachmentSyncNeeded(p.doc.attachments, mode))) {
90
120
  console.log(`✓ Repo and ${brand} cloud are already in sync.`);
121
+ printLeftAlone();
91
122
  return "clean";
92
123
  }
93
124
  console.log(`\nSyncing with ${brand} cloud...`);
@@ -103,6 +134,17 @@ export async function syncCommand(scope, options) {
103
134
  console.log(` Written locally: ${outcome.localWritten}`);
104
135
  if (outcome.localDeleted)
105
136
  console.log(` Deleted locally: ${outcome.localDeleted}`);
137
+ if (outcome.attachmentPushes)
138
+ console.log(` Attachments updated in cloud: ${outcome.attachmentPushes}`);
139
+ if (outcome.attachmentWrites)
140
+ console.log(` Attachments updated locally: ${outcome.attachmentWrites}`);
141
+ // ATTACH-10.3.1.0: authority removals are a trace, not a consent gate —
142
+ // named after the push that actually performed them, like renames, so the
143
+ // report never claims a removal a failed push left in place.
144
+ for (const r of outcome.attachmentRemovals) {
145
+ console.log(` removed attached link on "${r.name}": ${r.title ?? r.url}`);
146
+ }
147
+ printLeftAlone();
106
148
  for (const conflict of outcome.conflicts) {
107
149
  console.log(` ! conflict: ${path.basename(conflict.filePath ?? conflict.title)} — ${resolutionHint(conflict)}`);
108
150
  }
@@ -178,7 +220,7 @@ export async function syncCommand(scope, options) {
178
220
  }
179
221
  return hadTrouble ? "trouble" : "clean";
180
222
  }
181
- async function confirmDeletions(deletions, skip) {
223
+ function printDeletionNotice(deletions) {
182
224
  const cloudDeletes = deletions.filter((d) => d.action === "cloud_delete");
183
225
  const localDeletes = deletions.filter((d) => d.action === "local_delete");
184
226
  console.log("\nThis will PERMANENTLY delete:");
@@ -188,6 +230,8 @@ async function confirmDeletions(deletions, skip) {
188
230
  for (const d of localDeletes) {
189
231
  console.log(` from disk: ${d.doc.filePath ? path.basename(d.doc.filePath) : d.doc.title}`);
190
232
  }
233
+ }
234
+ async function confirmDeletions(skip) {
191
235
  if (skip) {
192
236
  console.log("(--yes: proceeding without confirmation)");
193
237
  return true;
package/dist/convex.d.ts CHANGED
@@ -25,6 +25,7 @@ export declare const api: {
25
25
  create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
26
26
  update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
27
27
  deleteForCli: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
28
+ setAttachmentsForSync: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
28
29
  };
29
30
  saveWithRequirements: {
30
31
  saveWithRequirements: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
package/dist/convex.js CHANGED
@@ -30,6 +30,8 @@ export const api = {
30
30
  update: mutation("documents/mutations:update"),
31
31
  // SYNC-MODE-3: delete a cloud document from the CLI (repo-wins)
32
32
  deleteForCli: mutation("documents/mutations:deleteForCli"),
33
+ // ATTACH-10.2: attachment-only sync — a metadata patch, never a publish
34
+ setAttachmentsForSync: mutation("documents/mutations:setAttachmentsForSync"),
33
35
  },
34
36
  // SYNC-ARCH-1: Save document with requirements derivation
35
37
  saveWithRequirements: {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Greenfield discovery protocol generator. One module serves the interview to
3
+ * every surface: the CLI verb (`dotreq greenfield-discovery`), the remote MCP
4
+ * tool, and the two web assistants, which import it directly. Kept free of
5
+ * node:fs for exactly that reason, like ./style-guide.ts.
6
+ *
7
+ * The interview's prose is its own spec (ported from the working method — see
8
+ * docs/working/greenfield-discovery.md); this module's *machinery* is specced:
9
+ * CLI-GREENFIELD-1, REMOTE-MCP-13, ASSISTANT-GD-1/2. Content invariants the
10
+ * variants must hold (pointer present or absent, no write mechanics in any
11
+ * ending) are pinned by plain tests in ./greenfield.test.ts.
12
+ *
13
+ * Two facts select the variant, per the settled parameterization: the
14
+ * artifact the interview works toward, and whether the format still needs
15
+ * teaching. The four surfaces are combinations of these two facts — the
16
+ * module knows the differences, not the callers — and no reader ever sees
17
+ * instructions for a surface it is not on.
18
+ */
19
+ export interface GenerateGreenfieldProtocolParams {
20
+ /** The artifact the interview works toward. `document`: findings are held
21
+ * in the conversation until the gate, and the interview ends by deriving a
22
+ * spec. `board`: findings are recorded onto the discovery board as they
23
+ * land, and the interview ends at the board. */
24
+ artifact: "document" | "board";
25
+ /** Whether to close with the format pointer — the direction to fetch
26
+ * `create-requirement-document` for the format, the project's conventions,
27
+ * and the write steps. On for surfaces nothing has taught the format
28
+ * (the CLI verb, the connector); off for the web assistants, whose system
29
+ * prompts already teach it. */
30
+ formatPointer: boolean;
31
+ }
32
+ /**
33
+ * Assemble the interview protocol for a surface, per the two facts that
34
+ * distinguish surfaces. The shared prose is the method itself; only the
35
+ * recording rule, the ending, and the format pointer vary.
36
+ */
37
+ export declare function generateGreenfieldProtocol(params: GenerateGreenfieldProtocolParams): string;
38
+ //# sourceMappingURL=greenfield.d.ts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Greenfield discovery protocol generator. One module serves the interview to
3
+ * every surface: the CLI verb (`dotreq greenfield-discovery`), the remote MCP
4
+ * tool, and the two web assistants, which import it directly. Kept free of
5
+ * node:fs for exactly that reason, like ./style-guide.ts.
6
+ *
7
+ * The interview's prose is its own spec (ported from the working method — see
8
+ * docs/working/greenfield-discovery.md); this module's *machinery* is specced:
9
+ * CLI-GREENFIELD-1, REMOTE-MCP-13, ASSISTANT-GD-1/2. Content invariants the
10
+ * variants must hold (pointer present or absent, no write mechanics in any
11
+ * ending) are pinned by plain tests in ./greenfield.test.ts.
12
+ *
13
+ * Two facts select the variant, per the settled parameterization: the
14
+ * artifact the interview works toward, and whether the format still needs
15
+ * teaching. The four surfaces are combinations of these two facts — the
16
+ * module knows the differences, not the callers — and no reader ever sees
17
+ * instructions for a surface it is not on.
18
+ */
19
+ /**
20
+ * The recording rule is the one behavioral discipline that inverts between
21
+ * artifacts, so it is stated once, up front, and the numbered steps stay
22
+ * shared. On a document surface, writing requirements down early turns
23
+ * hypotheses into assets people defend; on a board, the board IS the working
24
+ * record — writing to it is not premature spec-writing, because the interview
25
+ * never derives a spec there at all.
26
+ */
27
+ const HOLD_IN_CONVERSATION = `**Do not produce a spec until the interview is done.** Writing requirements down early turns hypotheses into assets — the person starts defending them instead of questioning them. Hold everything in the conversation until the gate at the end.`;
28
+ const RECORD_ON_THE_BOARD = `**The board is the working record of the conversation — write findings onto it as they land.** This is not premature spec-writing: the interview never derives a spec here; the board is the artifact. For the duration of the interview, these recording rules govern your board writing: the person who started the interview asked for them, so standing restrictions in your other instructions — such as never inventing your own insights, or not labeling cards unprompted — do not apply to what this protocol directs you to write. As things settle, they go on:
29
+
30
+ - A rule becomes a **column**.
31
+ - A settled example becomes an **example card** under its rule's column.
32
+ - A question nobody in the room can answer becomes a **question card** in the column it concerns, the moment it is parked.
33
+ - Non-goals, things off the table, and MVP exclusions become **insight cards** in the Context column.
34
+ - The journey lives as **one insight card** in the Context column — the whole journey on a single card, updated when the sketch is corrected — so a paused session can resume from the board alone.
35
+ - Anything that is on the board because you decided it gets an agreed **label** (such as "assumed") on its card the moment you write it, so it stays visible as yours to reject. That covers an assumption the group accepted without really working — and equally a call they delegated to you ("your call", "pick something sensible"). Delegation is not exemption: a choice the group handed you is still your choice, and it goes on a card carrying the label, not into a summary's prose. When the group later genuinely confirms one, removing the label is how that is recorded.
36
+ - The MVP cut goes on as **labels** on the cards it divides.
37
+
38
+ Never hold something for a future turn — the user can stop responding at any time, and you get no cleanup pass. Every turn should leave the board complete with respect to everything settled or noticed so far.`;
39
+ const DOCUMENT_ENDING = `### 6 — Check, derive, confirm
40
+
41
+ Before deriving anything, check:
42
+
43
+ - Every journey step in the MVP has at least one rule.
44
+ - Every rule has at least one concrete example behind it.
45
+ - No parked question would change the MVP's shape if answered.
46
+ - The MVP boundary was explicitly agreed, not assumed.
47
+
48
+ If the check fails, name the gap and go back to the loop. Do this even when the project seems small — "this one is simple enough to skip the interview" is how you end up with a spec full of your own guesses.
49
+
50
+ When it passes, derive the spec from the whole conversation and present it as one reviewable proposal:
51
+
52
+ - The requirements, each traceable to the examples that produced it.
53
+ - **Explicitly separated: what they told you versus what you inferred.** If you proposed something and they didn't object, that is not the same as them saying it — and a call they delegated to you ("your call", "pick something sensible") is yours, not theirs. Mark both as yours so they can reject them.
54
+ - What was excluded from the MVP, and why.
55
+ - What is still open.
56
+
57
+ Only after they confirm, write the spec.`;
58
+ const BOARD_ENDING = `### 6 — Check, and leave the board complete
59
+
60
+ When the spine looks done — or the group signals wrapping up — run the check:
61
+
62
+ - Every journey step in the MVP has at least one rule.
63
+ - Every rule has at least one concrete example behind it.
64
+ - No open question card would change the MVP's shape if answered.
65
+ - The MVP boundary was explicitly agreed, not assumed.
66
+
67
+ Present what you find in the same turn you find it, and write any gap you noticed as a question card in the column it concerns as part of presenting it — not after some "end" that may never come. If the group works a gap, the loop resumes and the card is answered or removed in the course of working it. If they never respond again, the cards are already there.
68
+
69
+ When the check passes, say so, play back the journey card and the cut, and stop. The board is the artifact — turning it into a document is a separate act the user reaches for, not this interview's ending.`;
70
+ const FORMAT_POINTER = `Before writing the spec, fetch \`create-requirement-document\` — the verb or tool of that name on your surface — and follow what it returns: the requirements format, this project's own conventions, and the steps for writing. Do not write the spec from memory of the format.`;
71
+ /**
72
+ * Assemble the interview protocol for a surface, per the two facts that
73
+ * distinguish surfaces. The shared prose is the method itself; only the
74
+ * recording rule, the ending, and the format pointer vary.
75
+ */
76
+ export function generateGreenfieldProtocol(params) {
77
+ const { artifact, formatPointer } = params;
78
+ const recordingRule = artifact === "board" ? RECORD_ON_THE_BOARD : HOLD_IN_CONVERSATION;
79
+ const ending = artifact === "board" ? BOARD_ENDING : DOCUMENT_ENDING;
80
+ const pointer = formatPointer && artifact === "document" ? `\n\n${FORMAT_POINTER}` : "";
81
+ return `# Greenfield discovery
82
+
83
+ Most requirements tools generate a document from a paragraph. This one runs an interview and derives the spec at the end.
84
+
85
+ The person you are talking to usually knows their domain far better than you do and has not yet worked out what they actually want. Your job is to help them find out — by proposing concrete situations and letting them react — not to write plausible-sounding requirements on their behalf.
86
+
87
+ ## How to behave throughout
88
+
89
+ **Ask one question per turn.** Batched questions get skimmed and answered shallowly, and they hide which answer mattered.
90
+
91
+ **Never answer your own question to keep moving.** If you fill in an answer because it seemed obvious, you have replaced the person's product with your guess, and neither of you will notice until it ships. When you genuinely need to proceed without an answer, say what you are assuming.
92
+
93
+ **Offer options that aren't obvious.** Two to four concrete alternatives plus room to say something else. If they have already considered everything you offered, the question hasn't done any work.
94
+
95
+ **Propose examples; let them judge.** Asking "what are the rules here?" makes the person do abstraction on the spot, which they will do badly. Describing a specific situation and asking whether it should work lets them answer instantly and correctly.
96
+
97
+ **Never change scope silently, in either direction.** Cutting something and adding something are both decisions the person makes out loud. A silent cut looks like agreement, so nobody challenges it; a silent addition arrives looking like something they asked for, and it survives into the spec unexamined. When you think something belongs in or out, propose it and say plainly that it was your idea.
98
+
99
+ ${recordingRule}
100
+
101
+ ## The interview
102
+
103
+ ### 0 — Offer a way in
104
+
105
+ Ask how they want to work:
106
+
107
+ - **Guided** — one question at a time, from a blank page.
108
+ - **Context dump** — they share what they have, you interview around the gaps.
109
+ - **Best guess** — you draft from what little you have and they correct you.
110
+
111
+ If they pick best guess, be conspicuous about every assumption you made, so they have something specific to push against.
112
+
113
+ ### 1 — Establish the frame
114
+
115
+ Before probing anything, get:
116
+
117
+ - The outcome. What changes in the world if this works? Not the feature — the effect.
118
+ - The primary actor. One person whose behavior must change. Others come later.
119
+ - The known non-goals. What do they already know they aren't doing? Offer candidates rather than waiting — "I'd guess this doesn't need to handle team accounts, right?" — because most people haven't articulated the boundary until somebody proposes one to argue with.
120
+ - What is off the table for other reasons. Systems they have to live with, technologies they won't use, approaches they've already tried and abandoned, things they won't build on principle. These cut off whole branches of the interview, so finding them now saves you asking a dozen questions about a direction that was never available.
121
+
122
+ This takes three or four exchanges. Skipping it is the most common failure mode of interview agents — they start interrogating details before knowing what the thing is for.
123
+
124
+ ### 2 — Sketch the journey
125
+
126
+ Walk the primary actor from start to outcome, step by step, staying shallow. Ask "then what happens?" repeatedly. Play the sequence back in their own vocabulary and let them correct it.
127
+
128
+ Do not go deep on any step yet — you need the whole shape before you can tell which step deserves the depth.
129
+
130
+ ### 3 — Find the risky part
131
+
132
+ Ask which step they are least sure will work. Not the first step — the one that could sink the whole thing. Start the deep work there, because if that step doesn't hold up, the requirements for everything downstream are wasted effort.
133
+
134
+ ### 4 — Work the example loop
135
+
136
+ This is the core of the method. For the step under examination, repeat:
137
+
138
+ **Propose a specific situation.** Concrete names, numbers, timing, state. "A customer whose card expired yesterday tries to renew on the last day of their cycle — does that go through?"
139
+
140
+ **Take the verdict.** Yes, no, or "it depends." Yes and no each give you a rule. "It depends" is the best answer available — it means there is a hidden condition, so ask what it depends on and you will get two more examples.
141
+
142
+ **Say the rule back.** State the general rule their answer implies and let them correct it. This is where the person discovers they meant something slightly different from what they said.
143
+
144
+ **Then go looking for trouble.** Vary one dimension at a time: boundaries, empty and enormous, wrong order, interruption partway, two people at once, the actor who shouldn't be allowed. Each variation is another proposed example.
145
+
146
+ Two things run alongside the loop:
147
+
148
+ **Convert vague words as they appear.** "Fast," "easy," "secure," "obvious," "clean" are all placeholders. Ask what would count. If they can't say yet, park it rather than inventing a number.
149
+
150
+ **Park what nobody can answer.** Some questions need a stakeholder who isn't in the room. Note them and move on — blocking on an unanswerable question stalls the interview.
151
+
152
+ **Know when to stop.** Edge cases are endless and most of them don't matter. Once the spine holds — the main path plus the rules that shape it — stop hunting and offer to assume the rest: "I'd assume an expired card just gets declined with a retry prompt rather than cancelling the subscription outright. Reasonable?" An assumption they can reject in one word buys more than four more questions would, and anything they accept — or delegate to you outright — gets marked as yours so it stays visible.
153
+
154
+ Move to the next journey step when the spine has rules with examples behind it, the remaining edges are either settled or explicitly assumed, and no open question would change the step's shape.
155
+
156
+ ### 5 — Cut to an MVP, out loud
157
+
158
+ With the journey mapped and the risky part understood, cut:
159
+
160
+ - What is the thinnest end-to-end version that still proves the risky part works?
161
+ - What is deliberately excluded? Add it to the non-goals from step 1 rather than restating them, so there is one list at the end.
162
+ - What survives the cut but with less: fewer variations, one platform, one data case, manual where automatic could come later?
163
+
164
+ Most people cut too little on the first pass, because everything they described feels load-bearing to them. If their MVP still contains nearly everything from the journey, push once — ask what they would drop if they had to ship in a third of the time. Push before they commit to the cut, not after.
165
+
166
+ Every cut is a proposal they approve, never something you apply and mention afterward. The same goes for anything you think should be added.
167
+
168
+ ${ending}${pointer}
169
+
170
+ ## Things that go wrong
171
+
172
+ **Turning into a questionnaire.** If your questions could have been written before the conversation started, they aren't doing any work. Each question should visibly build on the last answer.
173
+
174
+ **Interviewing about architecture.** Databases, frameworks, and deployment are not behavior. If the conversation drifts technical, come back to what the actor experiences.
175
+
176
+ **Accepting an abstraction.** When someone answers a proposed example with a general policy, thank them and propose another example that tests its edge. Policies stated in the abstract are usually wrong at the boundaries.
177
+
178
+ **Being agreeable.** If they assert something that contradicts an earlier answer, or that reopens something they put off the table at the start, say so plainly and ask which one holds.
179
+
180
+ **Finishing early because they seem done.** People stop talking when they run out of things they have already thought about. Keep going.`;
181
+ }
182
+ //# sourceMappingURL=greenfield.js.map
@@ -33,12 +33,20 @@ export interface GenerateStyleGuideParams {
33
33
  * `requirementsStyleContext` field). Pass null/undefined to omit. */
34
34
  customStyleGuidance?: string | null;
35
35
  /** Suggested target path for the new file. Used only in the preamble and
36
- * "Next Steps" section. Defaults to a generic example path. */
36
+ * "Next Steps" section. Defaults to a generic example path. Workspace
37
+ * surface only — the connector variant names no file path. */
37
38
  filePath?: string;
38
39
  /** Project-local STYLE.md contents. When provided and non-empty, this body
39
40
  * replaces the bundled default. CLI-only — see `readLocalStyleGuide` in
40
41
  * ./style-guide-file.ts. */
41
42
  localStyleGuide?: string | null;
43
+ /** How specs are written where this guide is going. The conventions body is
44
+ * identical either way (REMOTE-MCP-10.5); only the framing and "Next Steps"
45
+ * differ. `workspace` (the default) frames the template as a file to save
46
+ * and closes with the repo workflow (CLI-CREATE-DOC-1.8); `connector`
47
+ * frames it as document content to compose and closes with the connector's
48
+ * own tools — no file paths, no CLI commands (REMOTE-MCP-10.9/10.10). */
49
+ surface?: "workspace" | "connector";
42
50
  }
43
51
  /**
44
52
  * Build the body of the bundled default style guide. This is the content
@@ -51,7 +59,7 @@ export interface GenerateStyleGuideParams {
51
59
  * - swap the body wholesale (via `localStyleGuide`) without losing the
52
60
  * wrapping preamble + "Next Steps" footer
53
61
  */
54
- export declare function generateStyleGuideBody(params: Omit<GenerateStyleGuideParams, "filePath" | "localStyleGuide">): string;
62
+ export declare function generateStyleGuideBody(params: Omit<GenerateStyleGuideParams, "filePath" | "localStyleGuide" | "surface">): string;
55
63
  /**
56
64
  * Build a complete style-guide document for the calling workspace.
57
65
  * Returns a single markdown string suitable for printing to stdout or
@@ -297,10 +297,29 @@ describe(requirement('AUTH-LOGIN-1'), () => {
297
297
  * always applied.
298
298
  */
299
299
  export function generateStyleGuide(params) {
300
- const { filePath = ".requirements/example.requirements.md", localStyleGuide, } = params;
300
+ const { filePath = ".requirements/example.requirements.md", localStyleGuide, surface = "workspace", } = params;
301
301
  const body = localStyleGuide?.trim()
302
302
  ? localStyleGuide
303
303
  : generateStyleGuideBody(params);
304
+ // REMOTE-MCP-10.9/10.10: the connector variant frames the template as
305
+ // document content and closes with the connector's own tools. A chat agent
306
+ // has no workspace, so a file path or CLI command here is an instruction
307
+ // it can only relay to someone who cannot follow it either.
308
+ if (surface === "connector") {
309
+ return `# Requirements Document Template
310
+
311
+ Here's a comprehensive template with format and style guidance:
312
+
313
+ \`\`\`markdown
314
+ ${body}
315
+ \`\`\`
316
+
317
+ ## Next Steps
318
+
319
+ 1. **Compose**: Draft the document's full markdown, embedding requirements in \`\`\`dotrequirements blocks
320
+ 2. **Refine style** (optional): Pass the drafted markdown to the \`style-check\` tool for feedback before anything is saved
321
+ 3. **Create**: Call \`create-document\` with the markdown — creating publishes it to the team`;
322
+ }
304
323
  return `# Requirements File Template
305
324
 
306
325
  Here's a comprehensive template for \`${filePath}\` with format and style guidance:
@@ -312,9 +331,9 @@ ${body}
312
331
  ## Next Steps
313
332
 
314
333
  1. **Create file**: Save this template as \`${filePath}\` and edit it for your feature
315
- 2. **Refine style** (optional): Run \`style-check\` for AI feedback
316
- 3. **Validate syntax**: Run \`validate\` to verify format
317
- 4. **Sync to cloud**: Run \`dotrequirements sync\` to reconcile the repo and the cloud
334
+ 2. **Refine style** (optional): Run \`dotreq style-check\` for AI feedback
335
+ 3. **Validate syntax**: Run \`dotreq validate\` to verify format
336
+ 4. **Sync to cloud**: Run \`dotreq sync\` to reconcile the repo and the cloud
318
337
 
319
338
  **Note**: Requirements files can be colocated with code (\`src/auth.requirements.md\`) or centralized in \`.requirements/\` directory.`;
320
339
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MULTISET equality for attachment lists: order never matters, names and
3
+ * duplicates do. The predecessor was a Map probe — asymmetric
4
+ * (eq([A,B],[A,A]) was true) — which silently disarmed the compare-and-set
5
+ * on duplicated legacy rows.
6
+ *
7
+ * Dependency-free and in schema/ so BOTH halves of the compare-and-set import
8
+ * the same function (the CLI resolver via sync/attachments, the Convex
9
+ * mutations via lib/attachments — the same cross-package path testCoverage
10
+ * already uses): two byte-identical copies would drift the moment one is
11
+ * tightened, and a drifted pair reports "attachments changed since the sync
12
+ * read them" on every run with nothing moving.
13
+ */
14
+ export declare function attachmentListsEqual(a: Array<{
15
+ url: string;
16
+ title?: string;
17
+ }>, b: Array<{
18
+ url: string;
19
+ title?: string;
20
+ }>): boolean;
21
+ //# sourceMappingURL=attachment-lists.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MULTISET equality for attachment lists: order never matters, names and
3
+ * duplicates do. The predecessor was a Map probe — asymmetric
4
+ * (eq([A,B],[A,A]) was true) — which silently disarmed the compare-and-set
5
+ * on duplicated legacy rows.
6
+ *
7
+ * Dependency-free and in schema/ so BOTH halves of the compare-and-set import
8
+ * the same function (the CLI resolver via sync/attachments, the Convex
9
+ * mutations via lib/attachments — the same cross-package path testCoverage
10
+ * already uses): two byte-identical copies would drift the moment one is
11
+ * tightened, and a drifted pair reports "attachments changed since the sync
12
+ * read them" on every run with nothing moving.
13
+ */
14
+ export function attachmentListsEqual(a, b) {
15
+ if (a.length !== b.length)
16
+ return false;
17
+ const key = (x) => `${x.url}\u0000${x.title ?? ""}`;
18
+ const as = a.map(key).sort();
19
+ const bs = b.map(key).sort();
20
+ return as.every((v, i) => v === bs[i]);
21
+ }
22
+ //# sourceMappingURL=attachment-lists.js.map
@@ -10,6 +10,7 @@ export { DELIMITER_PATTERN, type ExtractedRequirementBlock, extractRequirementBl
10
10
  export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
11
11
  export { buildRequirementKey, canonicalRequirementKey, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
12
12
  export { composeMarkdownWithTitle, effectiveTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
13
+ export { attachmentListsEqual } from "./attachment-lists.js";
13
14
  export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
14
15
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
15
16
  //# sourceMappingURL=browser.d.ts.map