@ikarmic/forge-core 5.1.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The six stages of a guided journey.
3
+ *
4
+ * A stage is a prompt suffix plus a tool set, which is the point: gating reuses
5
+ * the sandbox the agent already applies, so "implementation cannot start yet"
6
+ * is the write tools being absent from the list rather than an instruction the
7
+ * model may decline. That is the one enforcement mechanism in this design that
8
+ * a model cannot talk past.
9
+ *
10
+ * `allowTools` takes its write-tool names from `FILE_WRITE_TOOL_NAMES` rather
11
+ * than retyping them: a tool added to that list and missed here would be a
12
+ * write the Build gate does not cover (RB-13).
13
+ */
14
+ import { FILE_WRITE_TOOL_NAMES } from '../tools/write-tools.js';
15
+ const READ_TOOLS = ['read_file', 'list_files', 'search_files', 'find_symbol', 'list_symbols', 'find_references'];
16
+ /**
17
+ * "This stage runs no tools."
18
+ *
19
+ * It cannot be `[]`: `applySandbox` treats an empty `allowTools` as *no
20
+ * whitelist at all*, so an empty list would hand a host-driven stage every
21
+ * tool, writes included. `vscode/src/chat/modes.ts` already learned this and
22
+ * carries its own copy of the same trick; this one lives in core so a surface
23
+ * wiring stages into a sandbox gets the safe value without rediscovering it.
24
+ */
25
+ export const NO_TOOLS_SENTINEL = '__forge_no_tools__';
26
+ export const STAGES = [
27
+ {
28
+ id: 'survey',
29
+ usesModel: false,
30
+ promptSuffix: '',
31
+ allowTools: [NO_TOOLS_SENTINEL],
32
+ artefact: 'survey facts',
33
+ gateOptions: [],
34
+ },
35
+ {
36
+ id: 'interview',
37
+ usesModel: false,
38
+ promptSuffix: '',
39
+ allowTools: [NO_TOOLS_SENTINEL],
40
+ artefact: 'interview answers',
41
+ gateOptions: [],
42
+ },
43
+ {
44
+ id: 'plan',
45
+ usesModel: true,
46
+ promptSuffix: [
47
+ '',
48
+ 'PLAN STAGE — state the plan and stop.',
49
+ '- Produce a short numbered plan of the work, grounded in what you can read.',
50
+ '- You cannot ask questions at this stage: the interview is over. Where something',
51
+ ' is genuinely undecided, state it as an assumption in the plan.',
52
+ '- Do not write or change any file.',
53
+ ].join('\n'),
54
+ allowTools: [...READ_TOOLS],
55
+ artefact: 'a plan, in chat',
56
+ gateOptions: ['Approve the plan', 'Revise it', 'Skip to the spec'],
57
+ },
58
+ {
59
+ id: 'spec',
60
+ usesModel: true,
61
+ promptSuffix: [
62
+ '',
63
+ 'SPEC STAGE — produce exactly two sections and stop.',
64
+ '- `## requirements.md` then `## design.md`, each starting with its own heading line.',
65
+ '- Inside a section use `###` or deeper for headings: any `##` line ends the section.',
66
+ '- Requirements: user stories with EARS-syntax acceptance criteria, e.g. "WHEN <trigger> THE SYSTEM SHALL <response>".',
67
+ ' Number each requirement (1, 2, 3, ...) and its criteria (1.1, 1.2, ...) so the implementation plan can cite them.',
68
+ '- Design: ground it in what you read: data flow, schemas, interfaces, API endpoints, and the existing patterns to follow.',
69
+ '- The implementation plan is a separate stage: do not write tasks here.',
70
+ '- You cannot write files at this stage. Reply with the sections as text, with no preamble and no closing summary.',
71
+ ].join('\n'),
72
+ allowTools: [...READ_TOOLS],
73
+ artefact: 'requirements.md, design.md',
74
+ gateOptions: ['Approve the spec', 'Revise it'],
75
+ },
76
+ {
77
+ id: 'implPlan',
78
+ usesModel: true,
79
+ promptSuffix: [
80
+ '',
81
+ 'IMPLEMENTATION PLAN STAGE — produce `## tasks.md` and stop.',
82
+ '- A markdown checklist: "- [ ] 1. Title", subtasks indented as " - [ ] 1.1 Title".',
83
+ '- Under each task add a line like "_Requirements: 2.1, 3.2_", citing the requirement numbers you were given.',
84
+ '- One checkbox per line, never two on a line.',
85
+ '- You cannot write files at this stage. Reply with the document as text, with no preamble and no closing summary.',
86
+ ].join('\n'),
87
+ allowTools: [...READ_TOOLS],
88
+ artefact: 'tasks.md',
89
+ gateOptions: ['Approve the tasks', 'Revise them'],
90
+ },
91
+ {
92
+ id: 'build',
93
+ usesModel: true,
94
+ promptSuffix: [
95
+ '',
96
+ 'BUILD STAGE — do the task you were given and stop at it.',
97
+ '- Work only on the current task; the next one is gated on the person.',
98
+ '- Run the project\'s own check when the change could break something.',
99
+ ].join('\n'),
100
+ allowTools: [...READ_TOOLS, ...FILE_WRITE_TOOL_NAMES, 'run_command'],
101
+ artefact: 'ticked tasks',
102
+ gateOptions: ['Continue to the next task', 'Retry this one', 'Skip it', 'Stop', 'Run the rest without asking'],
103
+ },
104
+ ];
105
+ export function stageById(id) {
106
+ return STAGES.find((s) => s.id === id);
107
+ }
@@ -0,0 +1,10 @@
1
+ import type { SurveyFacts } from './types.js';
2
+ /** Ceiling on the tree walk, so a monorepo costs the same as a small repo. */
3
+ export declare const MAX_SURVEY_FILES = 400;
4
+ /** Second ceiling: a tree of mostly empty or non-source directories is bounded too. */
5
+ export declare const MAX_SURVEY_DIRS = 2000;
6
+ /**
7
+ * Read what this workspace already says about itself. Never throws: a fact that
8
+ * cannot be read is absent.
9
+ */
10
+ export declare function surveyWorkspace(cwd: string, request: string): Promise<SurveyFacts>;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The Survey: what the workspace already answers, read before anyone is asked
3
+ * anything.
4
+ *
5
+ * There is no model here, by design (spec decision 18). A survey fact that a
6
+ * model produced could be hallucinated, and the asymmetry is brutal: a missing
7
+ * fact costs one extra interview question, while a wrong fact is a confident
8
+ * silent guess. Everything below is a file read, each one independently
9
+ * guarded, so an unreadable or hostile workspace yields absent facts rather
10
+ * than wrong ones or an exception.
11
+ *
12
+ * Nothing here decides "greenfield" or "brownfield". It reports facts; the
13
+ * question set is derived from which of them came back empty.
14
+ */
15
+ import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { isOutsideRel } from '../tools/shared.js';
18
+ import { detectProjectChecks } from '../verify/detect.js';
19
+ /** Ceiling on the tree walk, so a monorepo costs the same as a small repo. */
20
+ export const MAX_SURVEY_FILES = 400;
21
+ /** Second ceiling: a tree of mostly empty or non-source directories is bounded too. */
22
+ export const MAX_SURVEY_DIRS = 2000;
23
+ const MANIFESTS = ['package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod', 'pom.xml', 'Gemfile', 'composer.json'];
24
+ const CONVENTION_FILES = ['AGENTS.md', 'CLAUDE.md'];
25
+ const SOURCE_EXT = new Set([
26
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.kt', '.swift',
27
+ ]);
28
+ const IGNORE_DIRS = new Set([
29
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next', 'coverage', 'target', 'vendor', '__pycache__', '.venv',
30
+ ]);
31
+ const STOPWORDS = new Set([
32
+ 'add', 'the', 'and', 'this', 'our', 'for', 'with', 'from', 'into', 'build', 'create', 'make', 'need', 'want',
33
+ 'support', 'existing', 'that', 'them', 'app', 'repo', 'project', 'please', 'also', 'new',
34
+ ]);
35
+ const MAX_RELATED = 20;
36
+ /**
37
+ * True when `name` inside `root` resolves to something still inside `root`.
38
+ * A repository can ship a symlink or junction, so the check resolves links on
39
+ * both sides and tests the relative path with `isOutsideRel` (RB-20).
40
+ */
41
+ function containedIn(root, name) {
42
+ try {
43
+ const realRoot = realpathSync(root);
44
+ const realTarget = realpathSync(path.join(root, name));
45
+ return !isOutsideRel(path.relative(realRoot, realTarget));
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function firstContainedManifest(cwd) {
52
+ return MANIFESTS.find((m) => containedIn(cwd, m));
53
+ }
54
+ /**
55
+ * History, read rather than run: a ref anywhere under `refs/heads`, or a
56
+ * packed-refs file. Branch names nest (`refs/heads/user/feature/x`), so each
57
+ * directory's children are joined to *that* directory, and a single unreadable
58
+ * directory skips rather than ending the scan.
59
+ */
60
+ function hasCommits(cwd) {
61
+ const git = path.join(cwd, '.git');
62
+ try {
63
+ // A worktree or submodule has `.git` as a file holding `gitdir: …`. Following
64
+ // it would mean parsing a path out of another repo's layout; a worktree only
65
+ // exists for a repo that already has history, so take that as the answer.
66
+ if (statSync(git).isFile())
67
+ return true;
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ if (existsSync(path.join(git, 'packed-refs')))
73
+ return true;
74
+ const stack = [path.join(git, 'refs', 'heads')];
75
+ while (stack.length > 0) {
76
+ const dir = stack.pop();
77
+ let entries;
78
+ try {
79
+ entries = readdirSync(dir, { withFileTypes: true });
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ for (const e of entries) {
85
+ if (e.isDirectory())
86
+ stack.push(path.join(dir, e.name));
87
+ else
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * Repo-relative source paths, breadth-first, bounded, never entering an ignored
95
+ * directory.
96
+ *
97
+ * The directory is tracked explicitly rather than read from the `Dirent`
98
+ * parent-directory property: that one arrived in Node 20.12 / 18.20, and this
99
+ * also runs in the VS Code extension host, which ships older Node across the
100
+ * supported `engines.vscode` range. A fallback there would build paths that do
101
+ * not exist, and the walk would quietly see only the top level — reporting
102
+ * *wrong* facts, which is the one thing this file promises never to do.
103
+ * `journey-survey.test.ts` forbids that identifier here, prose included, since
104
+ * the mistake cannot fail on a developer's newer Node.
105
+ *
106
+ * Directories visited are capped as well as files, so a huge non-source tree
107
+ * outside `IGNORE_DIRS` cannot make this walk unbounded on the editor's thread.
108
+ */
109
+ function sourceFiles(cwd) {
110
+ const out = [];
111
+ const queue = [cwd];
112
+ let visited = 0;
113
+ while (queue.length > 0 && out.length < MAX_SURVEY_FILES && visited < MAX_SURVEY_DIRS) {
114
+ const dir = queue.shift();
115
+ visited += 1;
116
+ let entries;
117
+ try {
118
+ entries = readdirSync(dir, { withFileTypes: true });
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ for (const e of entries) {
124
+ if (out.length >= MAX_SURVEY_FILES)
125
+ break;
126
+ const abs = path.join(dir, e.name);
127
+ if (e.isDirectory()) {
128
+ if (!IGNORE_DIRS.has(e.name))
129
+ queue.push(abs);
130
+ }
131
+ else if (SOURCE_EXT.has(path.extname(e.name))) {
132
+ out.push(path.relative(cwd, abs));
133
+ }
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+ /** Source paths whose name mentions a noun from the request. Deterministic, no index needed. */
139
+ function relatedTo(request, files) {
140
+ const tokens = [...new Set((request.toLowerCase().match(/[a-z][a-z0-9-]{2,}/g) ?? []).filter((w) => !STOPWORDS.has(w)))];
141
+ if (tokens.length === 0)
142
+ return [];
143
+ const hits = files.filter((f) => {
144
+ const lower = f.toLowerCase();
145
+ return tokens.some((tok) => lower.includes(tok));
146
+ });
147
+ return hits.slice(0, MAX_RELATED);
148
+ }
149
+ function presentFiles(cwd, names) {
150
+ return names.filter((n) => containedIn(cwd, n));
151
+ }
152
+ function priorSpecIds(cwd) {
153
+ try {
154
+ return readdirSync(path.join(cwd, '.forge', 'specs'), { withFileTypes: true })
155
+ .filter((e) => e.isDirectory())
156
+ .map((e) => e.name);
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ }
162
+ /**
163
+ * Read what this workspace already says about itself. Never throws: a fact that
164
+ * cannot be read is absent.
165
+ */
166
+ export async function surveyWorkspace(cwd, request) {
167
+ const facts = {
168
+ checks: [],
169
+ isGitRepo: false,
170
+ hasCommits: false,
171
+ sourceFileCount: 0,
172
+ conventionFiles: [],
173
+ relatedSymbols: [],
174
+ priorSpecIds: [],
175
+ };
176
+ const manifest = firstContainedManifest(cwd);
177
+ if (manifest) {
178
+ // Only ask detect.ts about a manifest we have established is inside the
179
+ // workspace, so a symlinked one is never read for its scripts either.
180
+ try {
181
+ const detected = await detectProjectChecks(cwd);
182
+ facts.checks = detected.checks;
183
+ facts.manifest = { path: manifest, ...(detected.packageManager ? { packageManager: detected.packageManager } : {}) };
184
+ }
185
+ catch {
186
+ facts.manifest = { path: manifest };
187
+ }
188
+ }
189
+ try {
190
+ facts.isGitRepo = existsSync(path.join(cwd, '.git'));
191
+ }
192
+ catch {
193
+ facts.isGitRepo = false;
194
+ }
195
+ if (facts.isGitRepo)
196
+ facts.hasCommits = hasCommits(cwd);
197
+ const files = sourceFiles(cwd);
198
+ facts.sourceFileCount = files.length;
199
+ facts.relatedSymbols = relatedTo(request, files);
200
+ facts.conventionFiles = presentFiles(cwd, CONVENTION_FILES);
201
+ facts.priorSpecIds = priorSpecIds(cwd);
202
+ return facts;
203
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tier 1 of the journey trigger: does this message deserve a read-only Survey?
3
+ *
4
+ * It decides whether to *look*, not whether to show a card. A false positive
5
+ * costs a few auto-approved, token-capped reads; a false positive on the card
6
+ * is the nagging that kills the feature (spec anti-goal 1). So this is
7
+ * deliberately generous and the vetoes carry the weight.
8
+ *
9
+ * Signals are noun-driven, not verb-driven: a change verb on its own trips on
10
+ * everything ("add a button"), so it only counts beside a noun that implies
11
+ * many files.
12
+ */
13
+ import type { TriggerVerdict } from './types.js';
14
+ /**
15
+ * `typed` is what the person wrote, `sent` what goes to the model. They differ
16
+ * when a slash command, skill or `@agent` mention expanded into a long body;
17
+ * judging that text would offer a journey for a one-line request (RB-16).
18
+ */
19
+ export declare function shouldSurvey(typed: string, sent: string): TriggerVerdict;
@@ -0,0 +1,79 @@
1
+ const BUILD_VERBS = /\b(?:build|create|make|develop|scaffold|set up|generate|write me)\b/i;
2
+ const CHANGE_VERBS = /\b(?:add|extend|integrate|migrate|refactor|support|introduce|replace|rework|wire up|hook up|implement)\b/i;
3
+ const SYSTEM_NOUNS = /\b(?:app|application|system|platform|crm|erp|cms|web ?site|api|backend|frontend|dashboard|portal|saas|marketplace|game)\b/i;
4
+ /** Nouns that imply many files rather than one element. */
5
+ const SCALE_NOUNS = /\b(?:module|subsystem|service|integration|pipeline|workflow|migration|multi-?tenancy|auth|authentication|authorisation|authorization|sso|oauth|billing|payments|notifications|search|reporting|analytics|audit log|admin panel|permissions|rbac|onboarding|checkout)\b/i;
6
+ /** Hard vetoes: the person has already told us the scope is small. */
7
+ const SMALL_VERBS = /\b(?:fix|typo|rename|bump|tweak|format|indent|lint|spelling|revert)\b/i;
8
+ const UI_ATOM = /\b(?:button|label|icon|tooltip|placeholder|colour|color|margin|padding|border|font)\b/i;
9
+ const CROSS_CUTTING = /\b(?:across|everywhere|all our|end-to-end|throughout|every (?:page|screen|endpoint))\b/i;
10
+ const PLURAL_DELIVERABLE = /\b(?:pages|endpoints|models|screens|tables|services|roles|reports)\b/i;
11
+ const REQUIREMENT_INTENT = /\b(?:need|want|should|must|would like|let (?:people|users|them|me))\b/i;
12
+ const LONGISH_WORDS = 25;
13
+ const MIN_LIST_SEPARATORS = 3;
14
+ /** Cap before any regex runs, so a pathological message cannot stall the editor. */
15
+ const MAX_INPUT = 4000;
16
+ /**
17
+ * Technology names that are shaped exactly like filenames. Without this,
18
+ * "Build me a Next.js SaaS platform" — close to the canonical phrasing of the
19
+ * flagship scenario — is vetoed as naming a file and silently gets nothing.
20
+ */
21
+ const TECH_TOKENS = /\b(?:next|node|nuxt|vue|three|d3|express|react|nest|ember|backbone|socket|chart|moment|day)\.(?:js|io|ts)\b/gi;
22
+ /**
23
+ * A file path or extension means a scoped change, however big the noun beside it.
24
+ *
25
+ * Two narrowings against the version in `vscode/src/chat/large-request.ts`:
26
+ * technology names are removed first, and the extension must be letters, so a
27
+ * version number ("the 2.0 release") is not a filename.
28
+ */
29
+ function namesAFile(text) {
30
+ const withoutTech = text.replace(TECH_TOKENS, ' ');
31
+ return /[\\/]|\.[a-z]{1,5}\b/i.test(withoutTech.replace(/[.!?]+(?=\s|$)/g, ''));
32
+ }
33
+ function listSeparators(text) {
34
+ const commasAndAnds = (text.match(/,|\band\b/gi) ?? []).length;
35
+ const bullets = (text.match(/^\s*(?:[-*•]|\d+[.)])\s/gm) ?? []).length;
36
+ return commasAndAnds + bullets;
37
+ }
38
+ /**
39
+ * `typed` is what the person wrote, `sent` what goes to the model. They differ
40
+ * when a slash command, skill or `@agent` mention expanded into a long body;
41
+ * judging that text would offer a journey for a one-line request (RB-16).
42
+ */
43
+ export function shouldSurvey(typed, sent) {
44
+ const full = typed.trim();
45
+ if (!full)
46
+ return { survey: false, reasons: ['empty'] };
47
+ if (full !== sent.trim())
48
+ return { survey: false, reasons: ['expanded command'] };
49
+ if (/^\/\w/.test(full))
50
+ return { survey: false, reasons: ['slash command'] };
51
+ if (/(?:^|\s)@\S/.test(full))
52
+ return { survey: false, reasons: ['@agent mention'] };
53
+ const t = full.slice(0, MAX_INPUT);
54
+ if (namesAFile(t))
55
+ return { survey: false, reasons: ['veto: names a file or path'] };
56
+ if (SMALL_VERBS.test(t))
57
+ return { survey: false, reasons: ['veto: small-change verb'] };
58
+ const reasons = [];
59
+ const change = CHANGE_VERBS.test(t);
60
+ if (BUILD_VERBS.test(t) && SYSTEM_NOUNS.test(t))
61
+ reasons.push('build verb + system noun');
62
+ if (change && SCALE_NOUNS.test(t))
63
+ reasons.push('change verb + scale noun');
64
+ if (change && SYSTEM_NOUNS.test(t))
65
+ reasons.push('change verb + system noun');
66
+ if (CROSS_CUTTING.test(t))
67
+ reasons.push('cross-cutting scope');
68
+ if (change && PLURAL_DELIVERABLE.test(t))
69
+ reasons.push('plural deliverables');
70
+ if (t.split(/\s+/).length >= LONGISH_WORDS && REQUIREMENT_INTENT.test(t) && listSeparators(t) >= MIN_LIST_SEPARATORS) {
71
+ reasons.push('long multi-requirement message');
72
+ }
73
+ if (reasons.length === 0)
74
+ return { survey: false, reasons: ['no scope signal'] };
75
+ // One UI element with a single weak signal is a scoped edit, not a subsystem.
76
+ if (UI_ATOM.test(t) && reasons.length < 2)
77
+ return { survey: false, reasons: ['veto: single UI element'] };
78
+ return { survey: true, reasons };
79
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared types for the guided journey. Types only, no logic, so every other
3
+ * file in `journey/` can import this one without a cycle.
4
+ *
5
+ * Design: docs/superpowers/specs/2026-10-23-guided-journey-design.md (rev 3)
6
+ */
7
+ import type { DetectedCheck } from '../verify/detect.js';
8
+ /** Tier-1 verdict: whether a read-only Survey is worth running, and why. */
9
+ export interface TriggerVerdict {
10
+ survey: boolean;
11
+ /** Human-readable signals or vetoes; the offer card shows these. */
12
+ reasons: string[];
13
+ }
14
+ /**
15
+ * What the Survey read. Every field is absent or empty when it was not read —
16
+ * never guessed. There is deliberately no greenfield/brownfield flag: a
17
+ * workspace that is half-scaffolded is neither, and a label would have to lie
18
+ * about it. `journey-questions.test.ts` forbids that identifier anywhere under
19
+ * `src/`, prose included, which is why this note spells it out in words.
20
+ */
21
+ export interface SurveyFacts {
22
+ manifest?: {
23
+ path: string;
24
+ packageManager?: string;
25
+ };
26
+ checks: DetectedCheck[];
27
+ isGitRepo: boolean;
28
+ hasCommits: boolean;
29
+ sourceFileCount: number;
30
+ conventionFiles: string[];
31
+ relatedSymbols: string[];
32
+ priorSpecIds: string[];
33
+ }
34
+ /** Tier-2 decision, with the evidence that earns the card. */
35
+ export interface JourneyOffer {
36
+ offer: boolean;
37
+ evidence: string[];
38
+ }
39
+ /**
40
+ * One interview question. `answeredBy` is the precondition: when it returns a
41
+ * value, the Survey already answered this and the question is never asked.
42
+ */
43
+ export interface InterviewQuestion {
44
+ id: string;
45
+ question: string;
46
+ options?: string[];
47
+ multiSelect?: boolean;
48
+ answeredBy: (facts: SurveyFacts) => string | undefined;
49
+ }
50
+ /** A question the person answered, carried into the system prompt. */
51
+ export interface Answer {
52
+ id: string;
53
+ question: string;
54
+ answer: string;
55
+ }
56
+ export type StageId = 'survey' | 'interview' | 'plan' | 'spec' | 'implPlan' | 'build';
57
+ export interface Stage {
58
+ id: StageId;
59
+ /** Survey and Interview are host-driven and run no model at all. */
60
+ usesModel: boolean;
61
+ promptSuffix: string;
62
+ allowTools: readonly string[];
63
+ artefact: string;
64
+ gateOptions: readonly string[];
65
+ }
66
+ export interface JourneyState {
67
+ /** `'done'` once the journey is over, including when a saved stage id is unknown. */
68
+ current: StageId | 'done';
69
+ answers: Answer[];
70
+ /** Index into the approved `tasks.md` during the Build stage. */
71
+ taskCursor: number;
72
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -51,7 +51,7 @@ ${keys}
51
51
  1. Read the file first. If it does not exist, create it with only the keys you are setting.
52
52
  2. Change only what was asked. Keep every other key and keep the file valid JSON (double quotes, no trailing commas, no comments).
53
53
  3. A secret never goes in a project file. Name an environment variable (\`apiKeyEnv\`) instead of pasting a key.
54
- 4. Do not widen permissions (\`autoApproveTools\`, \`permission\` allow rules, \`autoApproveCommands\`) unless the user asked for exactly that, and say what the change allows.
54
+ 4. Do not widen permissions (\`autoApproveTools\`, \`permission\` allow rules, \`autoApproveCommands\`) unless the user named what to allow. A general request such as "make the prompts go away" or "allow everything" names nothing: do not write it, and never grant \`run_command\`, \`write_file\`, \`edit_file\`, \`apply_patch\` or \`sub_agent\` because of it. Say in one sentence why, then offer the narrow forms: allow one named tool, or choose "Always allow" on a specific command when its card appears. When you do widen, say what the change allows.
55
55
  5. After saving, say what changed and where. Most changes apply on the next message; \`caCertificates\` and the language need a window reload, MCP servers and skills need \`/reload\`.
56
56
  6. Do not run Forge commands that write to the real \`~/.forge\` to test a change. Edit the file and let the user check it.
57
57
  `;
@@ -12,15 +12,24 @@
12
12
  import type { SpecSections } from './types.js';
13
13
  /** Read-only tools exposed to the model during spec-mode generation. */
14
14
  export declare const SPEC_READONLY_TOOLS: readonly string[];
15
+ export declare function splitSpecSections(raw: string): SpecSections;
16
+ /**
17
+ * The Spec stage's two documents. Returns **only** those two keys: a model that
18
+ * volunteers a `## tasks.md` section despite being told not to must not be able
19
+ * to smuggle it into a draft, because the person has not reached the
20
+ * implementation-plan gate yet.
21
+ */
22
+ export declare function splitRequirementsDesign(raw: string): {
23
+ requirements: string;
24
+ design: string;
25
+ };
15
26
  /**
16
- * Split a spec-mode run's final text into its three sections. Each
17
- * section is introduced by a `## <file>.md` markdown heading (case-
18
- * insensitive, arbitrary surrounding whitespace); the body runs until
19
- * the next matching heading or end of text. Section order in `raw`
20
- * does not matter.
27
+ * The ImplPlan stage's single document.
21
28
  *
22
- * Throws when any of the three expected headings is missing: callers
23
- * should surface this as a generation failure rather than write partial
24
- * or malformed files.
29
+ * Throws when the reply holds no task at all: an empty body or a prose-only
30
+ * "Plan is complete." would otherwise be written by `writeDraftTasks` and then
31
+ * approved, because `approveDraft` only checks that the file exists — leaving a
32
+ * spec with nothing to run, which is the very outcome that guard exists to
33
+ * prevent. Failing generation here sends the journey back to "Revise".
25
34
  */
26
- export declare function splitSpecSections(raw: string): SpecSections;
35
+ export declare function splitTasksSection(raw: string): string;