@fission-ai/openspec 1.7.0 → 1.9.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 +3 -1
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +39 -5
- package/dist/commands/change.js +16 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +5 -1
- package/dist/commands/schema.js +281 -47
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1272 -151
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +49 -9
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +13 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +40 -17
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +253 -46
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +52 -28
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +93 -1
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.js +66 -35
- package/dist/core/shared-skill-target.d.ts +31 -0
- package/dist/core/shared-skill-target.js +186 -0
- package/dist/core/specs-apply.d.ts +72 -0
- package/dist/core/specs-apply.js +459 -59
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +25 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +136 -30
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +26 -3
- package/dist/core/validation/validator.js +189 -19
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +12 -5
- package/dist/telemetry/index.js +27 -9
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +52 -0
- package/dist/utils/interactive.js +135 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +51 -1
- package/dist/utils/task-progress.js +93 -41
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- package/schemas/spec-driven/templates/proposal.md +6 -4
|
@@ -2,21 +2,49 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolveArtifactOutputs, resolveSchema } from '../core/artifact-graph/index.js';
|
|
4
4
|
import { resolveSchemaForChange } from './change-metadata.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A Markdown task line: a `-`/`*` bullet carrying a `[ ]` or `[x]` checkbox.
|
|
7
|
+
*
|
|
8
|
+
* Leading whitespace is allowed so nested sub-tasks count like their parents.
|
|
9
|
+
* Anchoring at column 0 made ` - [ ] 1.1.1 ...` invisible to progress, to the
|
|
10
|
+
* apply task list, and to archive's incomplete-task check, so a change with
|
|
11
|
+
* unfinished sub-tasks reported "✓ Complete" and archived without a warning.
|
|
12
|
+
*
|
|
13
|
+
* Permissive on purpose, and safe to keep that way: any character class
|
|
14
|
+
* tightened here - the `\s` inside the brackets, which lets a tab or
|
|
15
|
+
* non-breaking space stand for an empty box - drops lines that used to count,
|
|
16
|
+
* and a task this parser drops is a task `openspec archive` stops warning about.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately unanchored at the end: `.` does not match `\r`, so writing the
|
|
19
|
+
* description group as `(.*)$` would reject every line of a CRLF tasks.md.
|
|
20
|
+
*/
|
|
21
|
+
const TASK_LINE_PATTERN = /^\s*[-*]\s*\[([\sxX])\]\s*(.*)/;
|
|
22
|
+
/**
|
|
23
|
+
* Parses every task line in a tasks file, in document order.
|
|
24
|
+
*
|
|
25
|
+
* Every line matching the pattern counts, wherever it sits - inside a code
|
|
26
|
+
* fence, an HTML comment or an indented block, as before. Skipping fenced
|
|
27
|
+
* checkboxes was tried and dropped: every rule for deciding which fence is
|
|
28
|
+
* "real" has an input where a stray or unbalanced ``` swallows genuine tasks.
|
|
29
|
+
* Counting a documented example as work is a loud, bypassable false positive;
|
|
30
|
+
* losing a real task is a silent one.
|
|
31
|
+
*/
|
|
32
|
+
export function parseTaskLines(content) {
|
|
33
|
+
const tasks = [];
|
|
34
|
+
for (const line of content.split('\n')) {
|
|
35
|
+
const match = line.match(TASK_LINE_PATTERN);
|
|
36
|
+
if (match) {
|
|
37
|
+
tasks.push({ done: match[1].toLowerCase() === 'x', description: match[2].trim() });
|
|
17
38
|
}
|
|
18
39
|
}
|
|
19
|
-
return
|
|
40
|
+
return tasks;
|
|
41
|
+
}
|
|
42
|
+
export function countTasksFromContent(content) {
|
|
43
|
+
const tasks = parseTaskLines(content);
|
|
44
|
+
return {
|
|
45
|
+
total: tasks.length,
|
|
46
|
+
completed: tasks.filter((task) => task.done).length,
|
|
47
|
+
};
|
|
20
48
|
}
|
|
21
49
|
/**
|
|
22
50
|
* Identifies the change's tracked-tasks artifact: the artifact whose `generates`
|
|
@@ -36,24 +64,43 @@ function findTrackedTasksArtifact(schema) {
|
|
|
36
64
|
* when the schema cannot be resolved or no tracked-tasks artifact exists.
|
|
37
65
|
* `resolveSchema` throws on an unresolvable/misnamed schema; we swallow that so
|
|
38
66
|
* the caller falls back to a single top-level `tasks.md` and never crashes.
|
|
67
|
+
* A `schemaGlobCache`, when supplied, memoizes the schema-name → glob lookup for
|
|
68
|
+
* the duration of one run.
|
|
39
69
|
*/
|
|
40
|
-
function resolveTrackedTasksGlob(changeDir, projectRoot) {
|
|
70
|
+
function resolveTrackedTasksGlob(changeDir, projectRoot, schemaGlobCache) {
|
|
41
71
|
try {
|
|
42
72
|
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot);
|
|
73
|
+
if (schemaGlobCache?.has(schemaName))
|
|
74
|
+
return schemaGlobCache.get(schemaName);
|
|
43
75
|
const schema = resolveSchema(schemaName, projectRoot);
|
|
44
|
-
|
|
76
|
+
const generates = findTrackedTasksArtifact(schema)?.generates;
|
|
77
|
+
schemaGlobCache?.set(schemaName, generates);
|
|
78
|
+
return generates;
|
|
45
79
|
}
|
|
46
80
|
catch {
|
|
47
81
|
return undefined;
|
|
48
82
|
}
|
|
49
83
|
}
|
|
50
|
-
|
|
51
|
-
|
|
84
|
+
/** Resolves the task files selected by the schema's apply tracking rule. */
|
|
85
|
+
export function resolveTaskFilesForChange(changeDir, projectRoot, schemaGlobCache) {
|
|
86
|
+
const generates = resolveTrackedTasksGlob(changeDir, projectRoot, schemaGlobCache);
|
|
87
|
+
return generates ? resolveArtifactOutputs(changeDir, generates) : [];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reads one task file and counts its checkboxes. ENOENT (a glob file that
|
|
91
|
+
* vanished between resolve and read, or the absent single top-level `tasks.md`)
|
|
92
|
+
* means zero tasks, exactly as before. Any other error (permissions, I/O,
|
|
93
|
+
* ENOTDIR) is recorded in `unreadable` so a caller can surface it; the count
|
|
94
|
+
* still contributes zero, so existing callers see no change.
|
|
95
|
+
*/
|
|
96
|
+
async function countTaskFile(file, unreadable) {
|
|
52
97
|
try {
|
|
53
|
-
const content = await fs.readFile(
|
|
98
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
54
99
|
return countTasksFromContent(content);
|
|
55
100
|
}
|
|
56
|
-
catch {
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error?.code !== 'ENOENT')
|
|
103
|
+
unreadable.push(file);
|
|
57
104
|
return { total: 0, completed: 0 };
|
|
58
105
|
}
|
|
59
106
|
}
|
|
@@ -64,31 +111,36 @@ async function countSingleTopLevelTasksFile(changeDir) {
|
|
|
64
111
|
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
65
112
|
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
66
113
|
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
67
|
-
* or the glob matches no file.
|
|
114
|
+
* or the glob matches no file. Also reports task files that exist but could not
|
|
115
|
+
* be read. Per-file read errors are captured (never thrown); the only throw path
|
|
116
|
+
* is a malformed/unsafe schema whose glob resolution rejects (path traversal or
|
|
117
|
+
* a linked-directory cycle in `resolveArtifactOutputs`). Pass `schemaGlobCache`
|
|
118
|
+
* to memoize schema→glob resolution across many changes in one run.
|
|
68
119
|
*/
|
|
69
|
-
export async function
|
|
120
|
+
export async function getTaskProgressDetailForChange(changesDir, changeName, projectRoot, schemaGlobCache) {
|
|
70
121
|
const changeDir = path.join(changesDir, changeName);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const progress = countTasksFromContent(content);
|
|
81
|
-
total += progress.total;
|
|
82
|
-
completed += progress.completed;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// Swallow files that vanish between glob and read, as before.
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return { total, completed };
|
|
89
|
-
}
|
|
122
|
+
const files = resolveTaskFilesForChange(changeDir, projectRoot, schemaGlobCache);
|
|
123
|
+
const targets = files.length > 0 ? files : [path.join(changeDir, 'tasks.md')];
|
|
124
|
+
const unreadable = [];
|
|
125
|
+
let total = 0;
|
|
126
|
+
let completed = 0;
|
|
127
|
+
for (const file of targets) {
|
|
128
|
+
const progress = await countTaskFile(file, unreadable);
|
|
129
|
+
total += progress.total;
|
|
130
|
+
completed += progress.completed;
|
|
90
131
|
}
|
|
91
|
-
return
|
|
132
|
+
return { total, completed, unreadable };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The task-completion counter `status`, `list`, and `archive` share. Delegates
|
|
136
|
+
* to `getTaskProgressDetailForChange` and drops the `unreadable` detail, so its
|
|
137
|
+
* returned totals are unchanged. Throws only on the same malformed/unsafe-schema
|
|
138
|
+
* glob-resolution path as that function (existing behavior; callers guard it as
|
|
139
|
+
* they did before).
|
|
140
|
+
*/
|
|
141
|
+
export async function getTaskProgressForChange(changesDir, changeName, projectRoot) {
|
|
142
|
+
const { total, completed } = await getTaskProgressDetailForChange(changesDir, changeName, projectRoot);
|
|
143
|
+
return { total, completed };
|
|
92
144
|
}
|
|
93
145
|
export function formatTaskStatus(progress) {
|
|
94
146
|
if (progress.total === 0)
|
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@ artifacts:
|
|
|
13
13
|
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
|
|
14
14
|
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
|
|
15
15
|
- **Capabilities**: Identify which specs will be created or modified:
|
|
16
|
-
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<
|
|
17
|
-
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file.
|
|
16
|
+
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
|
|
17
|
+
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
|
|
18
18
|
- **Impact**: Affected code, APIs, dependencies, or systems.
|
|
19
19
|
|
|
20
20
|
IMPORTANT: The Capabilities section is critical. It creates the contract between
|
|
@@ -60,8 +60,10 @@ artifacts:
|
|
|
60
60
|
visible behavior, it likely does not belong in the spec.
|
|
61
61
|
|
|
62
62
|
Create one spec file per capability listed in the proposal's Capabilities section.
|
|
63
|
-
-
|
|
64
|
-
-
|
|
63
|
+
`<capability-path>` is the spec directory relative to `specs/` (for example,
|
|
64
|
+
`user-auth` or `identity/user-auth`). Preserve the full path:
|
|
65
|
+
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
|
|
66
|
+
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Do not move or rename the capability.
|
|
65
67
|
|
|
66
68
|
There must be at least one spec file unless the change's `.openspec.yaml`
|
|
67
69
|
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
|
|
@@ -89,10 +91,10 @@ artifacts:
|
|
|
89
91
|
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
|
|
90
92
|
that spec already has one and the delta's is ignored. To change an
|
|
91
93
|
existing capability's Purpose - including a leftover `TBD` placeholder -
|
|
92
|
-
edit `openspec/specs/<capability>/spec.md` directly.
|
|
94
|
+
edit `openspec/specs/<capability-path>/spec.md` directly.
|
|
93
95
|
|
|
94
96
|
MODIFIED requirements workflow:
|
|
95
|
-
1. Locate the existing requirement in openspec/specs/<capability>/spec.md
|
|
97
|
+
1. Locate the existing requirement in openspec/specs/<capability-path>/spec.md
|
|
96
98
|
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
|
|
97
99
|
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
|
|
98
100
|
4. Ensure header text matches exactly (whitespace-insensitive)
|
|
@@ -9,18 +9,20 @@
|
|
|
9
9
|
## Capabilities
|
|
10
10
|
|
|
11
11
|
### New Capabilities
|
|
12
|
-
<!-- Capabilities being introduced.
|
|
13
|
-
-
|
|
12
|
+
<!-- Capabilities being introduced. Use kebab-case for path segments you introduce
|
|
13
|
+
(e.g., user-auth or identity/user-auth) that follow the project's existing
|
|
14
|
+
spec organization. Each creates specs/<capability-path>/spec.md. -->
|
|
15
|
+
- `<capability-path>`: <brief description of what this capability covers>
|
|
14
16
|
|
|
15
17
|
### Modified Capabilities
|
|
16
18
|
<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation).
|
|
17
19
|
Only list here if spec-level behavior changes. Each needs a delta spec file.
|
|
18
|
-
Use existing
|
|
20
|
+
Use the exact existing path under openspec/specs/. Leave empty if no requirement
|
|
19
21
|
changes. A change with no capabilities at all (pure refactor, tooling, docs)
|
|
20
22
|
must set `skip_specs: true` in its .openspec.yaml - openspec validate rejects
|
|
21
23
|
a zero-delta change without that marker. Do not invent a requirement just to
|
|
22
24
|
satisfy validation. -->
|
|
23
|
-
- `<existing-
|
|
25
|
+
- `<existing-capability-path>`: <what requirement is changing>
|
|
24
26
|
|
|
25
27
|
## Impact
|
|
26
28
|
|