@fission-ai/openspec 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +11 -2
- package/dist/commands/change.d.ts +24 -0
- package/dist/commands/change.js +223 -0
- package/dist/commands/schema.d.ts +5 -0
- package/dist/commands/schema.js +201 -35
- package/dist/commands/show.js +1 -1
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +76 -12
- package/dist/core/artifact-graph/resolver.js +15 -9
- package/dist/core/available-tools.js +16 -1
- package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -2
- package/dist/core/completions/command-registry.js +15 -0
- package/dist/core/completions/generators/fish-generator.d.ts +12 -0
- package/dist/core/completions/generators/fish-generator.js +79 -47
- package/dist/core/completions/templates/fish-templates.d.ts +1 -1
- package/dist/core/completions/templates/fish-templates.js +107 -10
- package/dist/core/completions/types.d.ts +4 -0
- package/dist/core/config.js +6 -1
- package/dist/core/init.js +50 -31
- package/dist/core/legacy-cleanup.js +6 -0
- package/dist/core/migration.js +42 -12
- package/dist/core/parsers/requirement-blocks.d.ts +1 -0
- package/dist/core/parsers/requirement-blocks.js +2 -0
- package/dist/core/shared/tool-detection.js +9 -3
- package/dist/core/shared-skill-target.d.ts +13 -0
- package/dist/core/shared-skill-target.js +43 -1
- package/dist/core/specs-apply.js +16 -8
- package/dist/core/templates/workflows/explore.js +79 -73
- package/dist/core/update.js +24 -22
- package/dist/core/validation/constants.d.ts +3 -0
- package/dist/core/validation/constants.js +12 -0
- package/dist/core/validation/purpose-placeholder.d.ts +45 -0
- package/dist/core/validation/purpose-placeholder.js +117 -0
- package/dist/core/validation/validator.js +16 -1
- package/dist/utils/requirement-diff.d.ts +42 -0
- package/dist/utils/requirement-diff.js +61 -0
- package/package.json +2 -1
|
@@ -5,7 +5,7 @@ export function getExploreSkillTemplate() {
|
|
|
5
5
|
description: 'Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.',
|
|
6
6
|
instructions: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
|
7
7
|
|
|
8
|
-
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code,
|
|
8
|
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. For a new change, scaffold it first as described below.
|
|
9
9
|
|
|
10
10
|
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
|
11
11
|
|
|
@@ -48,22 +48,25 @@ Depending on what the user brings, you might:
|
|
|
48
48
|
|
|
49
49
|
**Visualize**
|
|
50
50
|
\`\`\`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
+------------------------------------------+
|
|
52
|
+
| Use ASCII diagrams liberally |
|
|
53
|
+
+------------------------------------------+
|
|
54
|
+
| |
|
|
55
|
+
| [State A] -------> [State B] |
|
|
56
|
+
| | |
|
|
57
|
+
| v |
|
|
58
|
+
| [State C] |
|
|
59
|
+
| |
|
|
60
|
+
| System diagrams, state machines, |
|
|
61
|
+
| data flows, architecture sketches, |
|
|
62
|
+
| dependency graphs, comparison tables |
|
|
63
|
+
| |
|
|
64
|
+
+------------------------------------------+
|
|
65
65
|
\`\`\`
|
|
66
66
|
|
|
67
|
+
**Draw with plain ASCII only** — borders \`+\` \`-\` \`|\`, arrows \`-->\` \`<--\` \`^\` \`v\`, markers \`*\` \`x\`.
|
|
68
|
+
Unicode diagram glyphs can render at different widths across terminals, fonts, and locales, so padded boxes and aligned tables can drift. Keep every diagram character ASCII.
|
|
69
|
+
|
|
67
70
|
**Surface risks and unknowns**
|
|
68
71
|
- Identify what could go wrong
|
|
69
72
|
- Find gaps in understanding
|
|
@@ -164,17 +167,17 @@ User: I'm thinking about adding real-time collaboration
|
|
|
164
167
|
You: Real-time collab is a big space. Let me think about this...
|
|
165
168
|
|
|
166
169
|
COLLABORATION SPECTRUM
|
|
167
|
-
|
|
170
|
+
============================================
|
|
168
171
|
|
|
169
172
|
Awareness Coordination Sync
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
| | |
|
|
174
|
+
v v v
|
|
175
|
+
+--------+ +--------+ +--------+
|
|
176
|
+
|Presence| |Cursors | | CRDT |
|
|
177
|
+
| "3 | | Multi | |Conflict|
|
|
178
|
+
|online" | | select | | free |
|
|
179
|
+
+--------+ +--------+ +--------+
|
|
180
|
+
| | |
|
|
178
181
|
trivial moderate complex
|
|
179
182
|
|
|
180
183
|
Where's your head at?
|
|
@@ -186,27 +189,27 @@ User: The auth system is a mess
|
|
|
186
189
|
|
|
187
190
|
You: [reads codebase]
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
192
|
+
+---------------------------------------------+
|
|
193
|
+
| CURRENT AUTH FLOW |
|
|
194
|
+
+---------------------------------------------+
|
|
195
|
+
|
|
|
196
|
+
+-------------+-------------+
|
|
197
|
+
v v v
|
|
198
|
+
+---------+ +---------+ +---------+
|
|
199
|
+
| Google | | GitHub | | Email |
|
|
200
|
+
| OAuth | | OAuth | | Magic |
|
|
201
|
+
+----+----+ +----+----+ +----+----+
|
|
202
|
+
| | |
|
|
203
|
+
+-------------+-------------+
|
|
204
|
+
v
|
|
205
|
+
+-----------+
|
|
206
|
+
| Session |
|
|
207
|
+
+-----+-----+
|
|
208
|
+
|
|
|
209
|
+
v
|
|
210
|
+
+-----------+
|
|
211
|
+
| Perms |
|
|
212
|
+
+-----------+
|
|
210
213
|
|
|
211
214
|
I see three tangles. Which one's burning?
|
|
212
215
|
\`\`\`
|
|
@@ -238,19 +241,19 @@ User: A CLI tool that tracks local dev environments
|
|
|
238
241
|
|
|
239
242
|
You: That changes everything.
|
|
240
243
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
+-------------------------------------------------+
|
|
245
|
+
| CLI TOOL DATA STORAGE |
|
|
246
|
+
+-------------------------------------------------+
|
|
244
247
|
|
|
245
248
|
Key constraints:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
- No daemon running
|
|
250
|
+
- Must work offline
|
|
251
|
+
- Single user
|
|
249
252
|
|
|
250
253
|
SQLite Postgres
|
|
251
|
-
Deployment embedded
|
|
252
|
-
Offline yes
|
|
253
|
-
Single file yes
|
|
254
|
+
Deployment embedded needs server
|
|
255
|
+
Offline yes no
|
|
256
|
+
Single file yes no
|
|
254
257
|
|
|
255
258
|
SQLite. Not even close.
|
|
256
259
|
|
|
@@ -290,11 +293,11 @@ But this summary is optional. Sometimes the thinking IS the value.
|
|
|
290
293
|
|
|
291
294
|
## Guardrails
|
|
292
295
|
|
|
293
|
-
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing
|
|
296
|
+
- **Don't implement** - Never write code or implement features. Workflow configuration counts too: creating or editing schemas, templates, or \`openspec/config.yaml\` is a change, not thinking. Creating or updating OpenSpec change artifacts within the confirmed scope is fine, writing anything else is not.
|
|
294
297
|
- **Don't fake understanding** - If something is unclear, dig deeper
|
|
295
298
|
- **Don't rush** - Discovery is thinking time, not task time
|
|
296
299
|
- **Don't force structure** - Let patterns emerge naturally
|
|
297
|
-
- **Don't auto-capture** - Offer to save insights, don't just do it
|
|
300
|
+
- **Don't auto-capture** - Offer to save insights, don't just do it. Read-only commands and tools need no confirmation. Before the first write-capable action—including \`openspec new change\` or another command that writes files—name the artifacts or files and proposed changes, ask a direct yes/no question, and wait for explicit confirmation in a separate user message. That confirmation covers only the described scope; ask again before expanding it. Answers to design or clarifying questions are never consent to write.
|
|
298
301
|
- **Don't manually scaffold changes** - Never create a new change directory under \`openspec/changes/\` by hand. Always use \`openspec new change "<name>"\` (with \`--store <id>\` when applicable) so required metadata such as \`.openspec.yaml\` is created before writing artifacts.
|
|
299
302
|
- **Do visualize** - A good diagram is worth many paragraphs
|
|
300
303
|
- **Do explore the codebase** - Ground discussions in reality
|
|
@@ -312,7 +315,7 @@ export function getOpsxExploreCommandTemplate() {
|
|
|
312
315
|
tags: ['workflow', 'explore', 'experimental', 'thinking'],
|
|
313
316
|
content: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
|
314
317
|
|
|
315
|
-
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code,
|
|
318
|
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. For a new change, scaffold it first as described below.
|
|
316
319
|
|
|
317
320
|
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
|
318
321
|
|
|
@@ -362,22 +365,25 @@ Depending on what the user brings, you might:
|
|
|
362
365
|
|
|
363
366
|
**Visualize**
|
|
364
367
|
\`\`\`
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
+------------------------------------------+
|
|
369
|
+
| Use ASCII diagrams liberally |
|
|
370
|
+
+------------------------------------------+
|
|
371
|
+
| |
|
|
372
|
+
| [State A] -------> [State B] |
|
|
373
|
+
| | |
|
|
374
|
+
| v |
|
|
375
|
+
| [State C] |
|
|
376
|
+
| |
|
|
377
|
+
| System diagrams, state machines, |
|
|
378
|
+
| data flows, architecture sketches, |
|
|
379
|
+
| dependency graphs, comparison tables |
|
|
380
|
+
| |
|
|
381
|
+
+------------------------------------------+
|
|
379
382
|
\`\`\`
|
|
380
383
|
|
|
384
|
+
**Draw with plain ASCII only** — borders \`+\` \`-\` \`|\`, arrows \`-->\` \`<--\` \`^\` \`v\`, markers \`*\` \`x\`.
|
|
385
|
+
Unicode diagram glyphs can render at different widths across terminals, fonts, and locales, so padded boxes and aligned tables can drift. Keep every diagram character ASCII.
|
|
386
|
+
|
|
381
387
|
**Surface risks and unknowns**
|
|
382
388
|
- Identify what could go wrong
|
|
383
389
|
- Find gaps in understanding
|
|
@@ -486,11 +492,11 @@ When things crystallize, you might offer a summary - but it's optional. Sometime
|
|
|
486
492
|
|
|
487
493
|
## Guardrails
|
|
488
494
|
|
|
489
|
-
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing
|
|
495
|
+
- **Don't implement** - Never write code or implement features. Workflow configuration counts too: creating or editing schemas, templates, or \`openspec/config.yaml\` is a change, not thinking. Creating or updating OpenSpec change artifacts within the confirmed scope is fine, writing anything else is not.
|
|
490
496
|
- **Don't fake understanding** - If something is unclear, dig deeper
|
|
491
497
|
- **Don't rush** - Discovery is thinking time, not task time
|
|
492
498
|
- **Don't force structure** - Let patterns emerge naturally
|
|
493
|
-
- **Don't auto-capture** - Offer to save insights, don't just do it
|
|
499
|
+
- **Don't auto-capture** - Offer to save insights, don't just do it. Read-only commands and tools need no confirmation. Before the first write-capable action—including \`openspec new change\` or another command that writes files—name the artifacts or files and proposed changes, ask a direct yes/no question, and wait for explicit confirmation in a separate user message. That confirmation covers only the described scope; ask again before expanding it. Answers to design or clarifying questions are never consent to write.
|
|
494
500
|
- **Don't manually scaffold changes** - Never create a new change directory under \`openspec/changes/\` by hand. Always use \`openspec new change "<name>"\` (with \`--store <id>\` when applicable) so required metadata such as \`.openspec.yaml\` is created before writing artifacts.
|
|
495
501
|
- **Do visualize** - A good diagram is worth many paragraphs
|
|
496
502
|
- **Do explore the codebase** - Ground discussions in reality
|
package/dist/core/update.js
CHANGED
|
@@ -23,7 +23,7 @@ import { getAvailableTools } from './available-tools.js';
|
|
|
23
23
|
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
24
|
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
25
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
26
|
-
import { writeSharedSkillTarget,
|
|
26
|
+
import { resolveSharedSkillWriters, writeSharedSkillTarget, } from './shared-skill-target.js';
|
|
27
27
|
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
@@ -154,6 +154,9 @@ export class UpdateCommand {
|
|
|
154
154
|
const deliveryIncludesCommands = delivery !== 'skills';
|
|
155
155
|
// 10. Update tools (all if force, otherwise only those needing update)
|
|
156
156
|
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
157
|
+
const sharedSkillWriters = resolveSharedSkillWriters(resolvedProjectPath, configuredAndNewTools
|
|
158
|
+
.map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
|
|
159
|
+
.filter((tool) => tool !== undefined));
|
|
157
160
|
const updatedTools = [];
|
|
158
161
|
const updatedToolIds = [];
|
|
159
162
|
const failedTools = [];
|
|
@@ -173,11 +176,12 @@ export class UpdateCommand {
|
|
|
173
176
|
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
174
177
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
175
178
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
179
|
+
const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
|
|
176
180
|
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
177
181
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
178
182
|
const commandContents = getCommandContents(toolWorkflows);
|
|
179
183
|
// Generate skill files if delivery includes skills
|
|
180
|
-
if (shouldGenerateSkills) {
|
|
184
|
+
if (shouldGenerateSkills && writesSkills) {
|
|
181
185
|
for (const { template, dirName } of skillTemplates) {
|
|
182
186
|
const skillDir = path.join(skillsDir, dirName);
|
|
183
187
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
@@ -190,7 +194,9 @@ export class UpdateCommand {
|
|
|
190
194
|
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
|
|
191
195
|
}
|
|
192
196
|
// Delete skill directories if delivery is commands-only
|
|
193
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
197
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
198
|
+
writesSkills &&
|
|
199
|
+
!hasGlobalSkillTarget(tool)) {
|
|
194
200
|
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
|
|
195
201
|
// Persist the selected owner even when commands-only delivery leaves
|
|
196
202
|
// this target with no generated skills.
|
|
@@ -803,6 +809,10 @@ export class UpdateCommand {
|
|
|
803
809
|
const newlyConfigured = [];
|
|
804
810
|
const skippedSharedSkillTools = [];
|
|
805
811
|
const workflowOverrides = {};
|
|
812
|
+
const arbitrationTools = [...new Set([...configuredTools, ...selectedTools])]
|
|
813
|
+
.map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
|
|
814
|
+
.filter((tool) => tool !== undefined);
|
|
815
|
+
const sharedSkillWriters = resolveSharedSkillWriters(projectPath, arbitrationTools);
|
|
806
816
|
for (const toolId of selectedTools) {
|
|
807
817
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
808
818
|
if (!tool || !toolSupportsSkills(tool))
|
|
@@ -813,6 +823,7 @@ export class UpdateCommand {
|
|
|
813
823
|
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
|
|
814
824
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
815
825
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
826
|
+
const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
|
|
816
827
|
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
817
828
|
? inferredCodexWorkflows
|
|
818
829
|
: desiredWorkflows);
|
|
@@ -821,32 +832,23 @@ export class UpdateCommand {
|
|
|
821
832
|
}
|
|
822
833
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
823
834
|
const commandContents = getCommandContents(toolWorkflows);
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// Skipping here means the tool is never recorded as configured, so a
|
|
833
|
-
// persistent legacy signal re-offers it on later runs. Because no
|
|
834
|
-
// replacement is written, this tool is also exempted from immediate
|
|
835
|
-
// legacy cleanup (see skippedSharedSkillTools) — otherwise a repo-local
|
|
836
|
-
// `.codex/prompts` would be deleted with nothing put in its place. That
|
|
837
|
-
// repeat is idempotent and harmless — the alternative is the silent
|
|
838
|
-
// hijack this prevents.
|
|
839
|
-
const sharedOwner = shouldGenerateSkills
|
|
840
|
-
? sharedSkillRootOwner(projectPath, tool.value)
|
|
835
|
+
// Never overwrite a shared skills root owned by another tool. A tool
|
|
836
|
+
// with its own command surface can still install those commands: this
|
|
837
|
+
// is how a legacy Antigravity install gains `.agents/workflows` beside
|
|
838
|
+
// Codex-owned `.agents/skills`. A skills-only tool has no safe artifact
|
|
839
|
+
// to install, so preserve its legacy files and re-offer it later.
|
|
840
|
+
const sharedOwner = shouldGenerateSkills && !writesSkills
|
|
841
|
+
? arbitrationTools.find((candidate) => candidate.skillsDir === tool.skillsDir &&
|
|
842
|
+
sharedSkillWriters.has(candidate.value))?.value
|
|
841
843
|
: undefined;
|
|
842
|
-
if (sharedOwner) {
|
|
844
|
+
if (sharedOwner && !shouldGenerateCommands) {
|
|
843
845
|
const ownerName = AI_TOOLS.find((candidate) => candidate.value === sharedOwner)?.name ?? sharedOwner;
|
|
844
846
|
spinner.info(`Skipped ${tool.name}: ${tool.skillsDir}/skills is already managed by another tool (${ownerName}).`);
|
|
845
847
|
skippedSharedSkillTools.push(tool.value);
|
|
846
848
|
continue;
|
|
847
849
|
}
|
|
848
850
|
// Create skill files when delivery includes skills
|
|
849
|
-
if (shouldGenerateSkills) {
|
|
851
|
+
if (shouldGenerateSkills && writesSkills) {
|
|
850
852
|
for (const { template, dirName } of skillTemplates) {
|
|
851
853
|
const skillDir = path.join(skillsDir, dirName);
|
|
852
854
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
@@ -6,6 +6,8 @@ export declare const MIN_PURPOSE_LENGTH = 50;
|
|
|
6
6
|
export declare const MAX_WHY_SECTION_LENGTH = 1000;
|
|
7
7
|
export declare const MAX_REQUIREMENT_TEXT_LENGTH = 500;
|
|
8
8
|
export declare const MAX_DELTAS_PER_CHANGE = 10;
|
|
9
|
+
export declare const PURPOSE_PLACEHOLDER_PREFIX = "TBD - created by archiving change ";
|
|
10
|
+
export declare const PURPOSE_PLACEHOLDER_SUFFIX = ". Update Purpose after archive.";
|
|
9
11
|
export declare const VALIDATION_MESSAGES: {
|
|
10
12
|
readonly SCENARIO_EMPTY: "Scenario text cannot be empty";
|
|
11
13
|
readonly REQUIREMENT_EMPTY: "Requirement text cannot be empty";
|
|
@@ -26,6 +28,7 @@ export declare const VALIDATION_MESSAGES: {
|
|
|
26
28
|
readonly DELTA_SPEC_EMPTY: "Spec name cannot be empty";
|
|
27
29
|
readonly DELTA_DESCRIPTION_EMPTY: "Delta description cannot be empty";
|
|
28
30
|
readonly PURPOSE_TOO_BRIEF: "Purpose section is too brief (less than 50 characters)";
|
|
31
|
+
readonly PURPOSE_IS_PLACEHOLDER: string;
|
|
29
32
|
readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
|
|
30
33
|
readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
|
|
31
34
|
readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
|
|
@@ -8,6 +8,14 @@ export const MIN_PURPOSE_LENGTH = 50;
|
|
|
8
8
|
export const MAX_WHY_SECTION_LENGTH = 1000;
|
|
9
9
|
export const MAX_REQUIREMENT_TEXT_LENGTH = 500;
|
|
10
10
|
export const MAX_DELTAS_PER_CHANGE = 10;
|
|
11
|
+
// The Purpose `openspec archive` writes into a main spec it creates when the
|
|
12
|
+
// delta introduced the capability without a usable `## Purpose`. Named here, and
|
|
13
|
+
// composed from these two halves at the write site, so validation recognises the
|
|
14
|
+
// placeholder through the same definition that produces it: a second, hand-copied
|
|
15
|
+
// spelling would stop matching the day the wording changed, and a check that
|
|
16
|
+
// matches nothing looks exactly like a check that found nothing.
|
|
17
|
+
export const PURPOSE_PLACEHOLDER_PREFIX = 'TBD - created by archiving change ';
|
|
18
|
+
export const PURPOSE_PLACEHOLDER_SUFFIX = '. Update Purpose after archive.';
|
|
11
19
|
// Validation messages
|
|
12
20
|
export const VALIDATION_MESSAGES = {
|
|
13
21
|
// Required content
|
|
@@ -31,6 +39,10 @@ export const VALIDATION_MESSAGES = {
|
|
|
31
39
|
DELTA_DESCRIPTION_EMPTY: 'Delta description cannot be empty',
|
|
32
40
|
// Warnings
|
|
33
41
|
PURPOSE_TOO_BRIEF: `Purpose section is too brief (less than ${MIN_PURPOSE_LENGTH} characters)`,
|
|
42
|
+
PURPOSE_IS_PLACEHOLDER: 'Purpose section is still a placeholder rather than a Purpose anyone wrote (the sentence `openspec archive` ' +
|
|
43
|
+
'writes for a new capability, or a `TBD`/`TODO` marker left in its place). Replace it with what this ' +
|
|
44
|
+
'capability is for, editing the main spec directly: a `## Purpose` in a delta is read only when the ' +
|
|
45
|
+
'capability is created, so it cannot replace this one.',
|
|
34
46
|
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
|
|
35
47
|
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
|
36
48
|
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a `## Purpose` that is still a placeholder, rather than one somebody
|
|
3
|
+
* wrote.
|
|
4
|
+
*
|
|
5
|
+
* When a delta introduces a capability with no usable `## Purpose`, archive
|
|
6
|
+
* stamps the placeholder into the new main spec. That text is over
|
|
7
|
+
* `MIN_PURPOSE_LENGTH`, so the brevity check cannot reach it: the one rule that
|
|
8
|
+
* exists to catch a Purpose nobody wrote is satisfied by the exact string
|
|
9
|
+
* meaning nobody wrote one. Nothing else reads it afterwards, so the capability
|
|
10
|
+
* keeps a to-do in it while every command reports success.
|
|
11
|
+
*
|
|
12
|
+
* Two things count, and deliberately nothing else:
|
|
13
|
+
*
|
|
14
|
+
* - the placeholder this tool generates, recognised through the same constants
|
|
15
|
+
* the writer composes it from, wherever it sits in the Purpose - nobody types
|
|
16
|
+
* that sentence by accident;
|
|
17
|
+
* - a `TBD` or `TODO` **opening** the Purpose, which is the marker left behind
|
|
18
|
+
* when someone is told to leave "a brief TBD placeholder" and never comes
|
|
19
|
+
* back. Which of the two words got typed says nothing about whether the
|
|
20
|
+
* Purpose was written, so both are read the same way.
|
|
21
|
+
*
|
|
22
|
+
* A marker inside a sentence is left alone. "The retry budget is TBD pending
|
|
23
|
+
* benchmarks" is a real Purpose with an open question in it, and reporting it
|
|
24
|
+
* would teach people to ignore the warning - which costs more than the findings
|
|
25
|
+
* it would add.
|
|
26
|
+
*
|
|
27
|
+
* Fenced code inside the Purpose is quoted material rather than the Purpose
|
|
28
|
+
* speaking, so it is read out first. Without that, a Purpose documenting the
|
|
29
|
+
* sentence archive writes is reported as being that sentence: a document about
|
|
30
|
+
* the placeholder, failing for carrying one.
|
|
31
|
+
*/
|
|
32
|
+
export interface PurposePlaceholderIssue {
|
|
33
|
+
/** 1-based line of the placeholder text, when it can be located. */
|
|
34
|
+
line?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reports the Purpose of a main spec as an unwritten placeholder, or null when
|
|
38
|
+
* it reads as authored content.
|
|
39
|
+
*
|
|
40
|
+
* An empty Purpose is not reported here - `SPEC_PURPOSE_EMPTY` already covers
|
|
41
|
+
* it, and reporting both would put two findings on one line. That falls out of
|
|
42
|
+
* the two rules rather than needing a case of its own.
|
|
43
|
+
*/
|
|
44
|
+
export declare function findPurposePlaceholderIssue(overview: string, content?: string): PurposePlaceholderIssue | null;
|
|
45
|
+
//# sourceMappingURL=purpose-placeholder.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from '../parsers/code-fence.js';
|
|
2
|
+
import { PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX } from './constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* A `TBD` or `TODO` opening the Purpose. The lookahead keeps it off a longer
|
|
5
|
+
* word that merely begins with those letters, like "TBDs" or "TODOs", while
|
|
6
|
+
* still allowing the punctuation a marker is usually written with: `TODO:`,
|
|
7
|
+
* `TBD -`. It rejects any letter, digit or combining mark rather than only the
|
|
8
|
+
* ASCII ones `\b` knows about, because a Purpose is prose and prose is not
|
|
9
|
+
* always written in Latin script - `TBD` followed by an Arabic-Indic digit is
|
|
10
|
+
* as much a longer word as `TBDs` is.
|
|
11
|
+
*/
|
|
12
|
+
const LEADING_MARKER = /^(?:TBD|TODO)(?![\p{L}\p{N}\p{M}_])/iu;
|
|
13
|
+
const PURPOSE_HEADER = /^ {0,3}##(?!#)[ \t]+Purpose[ \t]*$/i;
|
|
14
|
+
const TOP_LEVEL_HEADER = /^ {0,3}#{1,2}(?!#)[ \t]+/;
|
|
15
|
+
/**
|
|
16
|
+
* The lines of `text` that sit outside a fenced code block, with line endings
|
|
17
|
+
* normalised first.
|
|
18
|
+
*
|
|
19
|
+
* `buildCodeFenceMask` is the masker the requirement and structure parsers
|
|
20
|
+
* already share, and its own reason for existing is that a second, private
|
|
21
|
+
* notion of what a fence is drifts from the first. This check reads Markdown
|
|
22
|
+
* for the same purpose they do, so it reads fences the same way they do.
|
|
23
|
+
*/
|
|
24
|
+
function unfencedLines(text) {
|
|
25
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
26
|
+
const fenced = buildCodeFenceMask(lines);
|
|
27
|
+
return lines.filter((_, index) => !fenced[index]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when the text carries the sentence archive writes. Matched as its two
|
|
31
|
+
* fixed halves in order, because the change name between them varies - so the
|
|
32
|
+
* check follows the writer's own definition instead of a second copy of it.
|
|
33
|
+
*/
|
|
34
|
+
function generatedPlaceholderPrefixIndex(text) {
|
|
35
|
+
let suffixAt = text.indexOf(PURPOSE_PLACEHOLDER_SUFFIX);
|
|
36
|
+
while (suffixAt !== -1) {
|
|
37
|
+
// Use the closest prefix before this suffix. An authored explanation can
|
|
38
|
+
// mention the prefix above the real placeholder; choosing the first prefix
|
|
39
|
+
// would then point the diagnostic at the explanation instead of the text
|
|
40
|
+
// the user needs to replace.
|
|
41
|
+
const prefixAt = text.lastIndexOf(PURPOSE_PLACEHOLDER_PREFIX, suffixAt);
|
|
42
|
+
if (prefixAt !== -1)
|
|
43
|
+
return prefixAt;
|
|
44
|
+
suffixAt = text.indexOf(PURPOSE_PLACEHOLDER_SUFFIX, suffixAt + 1);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reports the Purpose of a main spec as an unwritten placeholder, or null when
|
|
50
|
+
* it reads as authored content.
|
|
51
|
+
*
|
|
52
|
+
* An empty Purpose is not reported here - `SPEC_PURPOSE_EMPTY` already covers
|
|
53
|
+
* it, and reporting both would put two findings on one line. That falls out of
|
|
54
|
+
* the two rules rather than needing a case of its own.
|
|
55
|
+
*/
|
|
56
|
+
export function findPurposePlaceholderIssue(overview, content) {
|
|
57
|
+
// An empty Purpose needs no branch of its own: neither rule matches empty
|
|
58
|
+
// text, so it falls through to null on the line below. An early return for it
|
|
59
|
+
// would be a guard no test could hold, which is worse than none. A Purpose
|
|
60
|
+
// that is nothing but a fenced block reduces to the same empty text here, and
|
|
61
|
+
// is left to the brevity and empty-Purpose rules for the same reason.
|
|
62
|
+
const prose = unfencedLines(overview).join('\n').trim();
|
|
63
|
+
const leading = LEADING_MARKER.test(prose);
|
|
64
|
+
if (!leading && generatedPlaceholderPrefixIndex(prose) === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
// Which rule matched decides where the placeholder is, so the locator is told.
|
|
67
|
+
// When both match the leading marker wins: it sits at or above the generated
|
|
68
|
+
// sentence, and the earliest marker is the one a reader scanning down meets.
|
|
69
|
+
return { line: content === undefined ? undefined : findPlaceholderLine(content, leading) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The line inside the `## Purpose` section carrying the placeholder, so the
|
|
73
|
+
* warning points at the text to replace rather than at the file.
|
|
74
|
+
*
|
|
75
|
+
* Which line that is depends on the rule that matched. A leading marker is the
|
|
76
|
+
* section's first non-blank line by definition. The generated sentence is not:
|
|
77
|
+
* it can follow prose somebody wrote, and naming the first non-blank line then
|
|
78
|
+
* points at that prose - a line the reader can see is fine, which reads as the
|
|
79
|
+
* check being wrong rather than the Purpose being unwritten.
|
|
80
|
+
*
|
|
81
|
+
* Fenced lines are skipped on the way, for the reason detection skips them, and
|
|
82
|
+
* so a `## Requirements` quoted inside a fence cannot end the section early.
|
|
83
|
+
*
|
|
84
|
+
* Undefined when the placeholder cannot be located - no section header, or a
|
|
85
|
+
* generated sentence no single line carries. The caller then reports the finding
|
|
86
|
+
* without a line rather than with a guessed one, since a wrong line number is
|
|
87
|
+
* worse than none.
|
|
88
|
+
*
|
|
89
|
+
* Line endings are normalised first, so the same spec reports the same line
|
|
90
|
+
* whether it was saved on Windows or on macOS/Linux.
|
|
91
|
+
*/
|
|
92
|
+
function findPlaceholderLine(content, leading) {
|
|
93
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n');
|
|
94
|
+
const fenced = buildCodeFenceMask(lines);
|
|
95
|
+
const headerIndex = lines.findIndex((line, index) => !fenced[index] && PURPOSE_HEADER.test(line));
|
|
96
|
+
if (headerIndex === -1)
|
|
97
|
+
return undefined;
|
|
98
|
+
const purposeLines = [];
|
|
99
|
+
for (let i = headerIndex + 1; i < lines.length; i++) {
|
|
100
|
+
if (fenced[i])
|
|
101
|
+
continue;
|
|
102
|
+
if (TOP_LEVEL_HEADER.test(lines[i]))
|
|
103
|
+
break;
|
|
104
|
+
if (leading && lines[i].trim())
|
|
105
|
+
return i + 1;
|
|
106
|
+
purposeLines.push({ line: i + 1, text: lines[i] });
|
|
107
|
+
}
|
|
108
|
+
if (leading)
|
|
109
|
+
return undefined;
|
|
110
|
+
const purpose = purposeLines.map(({ text }) => text).join('\n');
|
|
111
|
+
const prefixAt = generatedPlaceholderPrefixIndex(purpose);
|
|
112
|
+
if (prefixAt === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const lineOffset = purpose.slice(0, prefixAt).split('\n').length - 1;
|
|
115
|
+
return purposeLines[lineOffset]?.line;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=purpose-placeholder.js.map
|
|
@@ -12,6 +12,7 @@ import { discoverSpecFiles, hasAnyFileUnder } from '../../utils/spec-discovery.j
|
|
|
12
12
|
import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../../utils/change-metadata.js';
|
|
13
13
|
import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
|
|
14
14
|
import { findTaskNumberingIssues } from './task-numbering.js';
|
|
15
|
+
import { findPurposePlaceholderIssue } from './purpose-placeholder.js';
|
|
15
16
|
import { getPackageSchemasDir, getSchemaDir } from '../artifact-graph/index.js';
|
|
16
17
|
export class Validator {
|
|
17
18
|
strictMode;
|
|
@@ -557,7 +558,21 @@ export class Validator {
|
|
|
557
558
|
message: structuralIssue.message,
|
|
558
559
|
});
|
|
559
560
|
}
|
|
560
|
-
|
|
561
|
+
// The placeholder is longer than MIN_PURPOSE_LENGTH, so the brevity check
|
|
562
|
+
// below cannot reach it; it is reported on its own terms instead. Checked
|
|
563
|
+
// first because a hand-written "TBD" is both a placeholder and too brief,
|
|
564
|
+
// and only one of those two tells the author what to do. (A "TODO" opening
|
|
565
|
+
// the Purpose reads the same way, so it is the same finding.)
|
|
566
|
+
const placeholder = findPurposePlaceholderIssue(spec.overview, content);
|
|
567
|
+
if (placeholder) {
|
|
568
|
+
issues.push({
|
|
569
|
+
level: 'WARNING',
|
|
570
|
+
path: 'overview',
|
|
571
|
+
line: placeholder.line,
|
|
572
|
+
message: VALIDATION_MESSAGES.PURPOSE_IS_PLACEHOLDER,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
else if (spec.overview.length < MIN_PURPOSE_LENGTH) {
|
|
561
576
|
issues.push({
|
|
562
577
|
level: 'WARNING',
|
|
563
578
|
path: 'overview',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** A main-spec requirement block, and how its header matched the delta's. */
|
|
2
|
+
export interface MatchedRequirementBlock {
|
|
3
|
+
raw: string;
|
|
4
|
+
/** The header name as the main spec spells it. */
|
|
5
|
+
name: string;
|
|
6
|
+
/**
|
|
7
|
+
* False when the two headers differ only in case or interior spacing. Archive
|
|
8
|
+
* matches names exactly, so an inexact match is a real problem to report even
|
|
9
|
+
* though the diff below it is still the one the author meant.
|
|
10
|
+
*/
|
|
11
|
+
exact: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Find the raw markdown block for a requirement by name in a spec file.
|
|
15
|
+
*
|
|
16
|
+
* Delegates to extractRequirementsSection() which already handles code fences,
|
|
17
|
+
* section boundaries, and requirement header parsing. We just look up by name:
|
|
18
|
+
* exactly first, then falling back to the shared case/whitespace fold so a
|
|
19
|
+
* header that differs only in spelling still shows its diff, flagged inexact.
|
|
20
|
+
*
|
|
21
|
+
* Returns null if no requirement header matches either way.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractRequirementBlock(specContent: string, requirementName: string): MatchedRequirementBlock | null;
|
|
24
|
+
/**
|
|
25
|
+
* Compute a unified diff between a main-spec requirement block and the delta
|
|
26
|
+
* block that replaces it. A null main block (new capability) diffs against the
|
|
27
|
+
* empty string, so every line reads as an addition.
|
|
28
|
+
*
|
|
29
|
+
* Uses structuredPatch to retain unified-diff hunk ranges while omitting the
|
|
30
|
+
* synthetic file headers, since the caller provides its own labeling.
|
|
31
|
+
*/
|
|
32
|
+
export declare function diffRequirementBlock(baseBlock: string | null, deltaBlock: string, label: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Build a map from folded RENAMED TO name to the original main-spec name.
|
|
35
|
+
* Renames apply in source order, so a chain such as A -> B -> C maps C back to
|
|
36
|
+
* A. That is the block a later MODIFIED C replaces.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildRenameMap(renames: Array<{
|
|
39
|
+
from: string;
|
|
40
|
+
to: string;
|
|
41
|
+
}>): Map<string, string>;
|
|
42
|
+
//# sourceMappingURL=requirement-diff.d.ts.map
|