@fission-ai/openspec 1.8.0 → 1.10.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.d.ts +32 -0
- package/dist/cli/index.js +80 -9
- package/dist/commands/change.js +1 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +19 -4
- package/dist/commands/feedback.js +28 -7
- package/dist/commands/schema.js +193 -28
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/core/archive.js +74 -10
- package/dist/core/artifact-graph/instruction-loader.js +2 -7
- package/dist/core/artifact-graph/outputs.d.ts +4 -0
- package/dist/core/artifact-graph/outputs.js +7 -0
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +14 -0
- 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/adapters/opencode.d.ts +2 -1
- package/dist/core/command-generation/adapters/opencode.js +13 -2
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completion-tip.d.ts +15 -0
- package/dist/core/completion-tip.js +133 -0
- package/dist/core/completions/command-registry.js +10 -0
- package/dist/core/completions/factory.d.ts +11 -0
- package/dist/core/completions/installers/bash-installer.d.ts +7 -0
- package/dist/core/completions/installers/bash-installer.js +15 -0
- package/dist/core/completions/installers/fish-installer.d.ts +7 -0
- package/dist/core/completions/installers/fish-installer.js +15 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +7 -0
- package/dist/core/completions/installers/powershell-installer.js +15 -0
- package/dist/core/config-prompts.js +19 -10
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +2 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +17 -15
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +110 -14
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +48 -26
- package/dist/core/parsers/requirement-blocks.js +41 -9
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +11 -2
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared-skill-target.d.ts +15 -0
- package/dist/core/shared-skill-target.js +30 -0
- package/dist/core/specs-apply.js +11 -4
- package/dist/core/templates/workflows/apply-change.js +3 -0
- package/dist/core/templates/workflows/feedback.js +1 -0
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +2 -2
- 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/update-change.js +4 -4
- package/dist/core/update.js +49 -11
- 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 +5 -2
- package/dist/core/validation/validator.js +57 -3
- package/dist/telemetry/index.d.ts +3 -1
- package/dist/telemetry/index.js +10 -3
- package/dist/utils/change-metadata.js +17 -13
- package/dist/utils/change-utils.js +5 -0
- package/dist/utils/interactive.d.ts +30 -1
- package/dist/utils/interactive.js +105 -2
- package/dist/utils/task-progress.d.ts +34 -1
- package/dist/utils/task-progress.js +52 -28
- package/package.json +3 -6
- package/schemas/spec-driven/schema.yaml +17 -7
- package/scripts/postinstall.js +0 -83
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
1
2
|
/**
|
|
2
3
|
* Resolves whether non-interactive mode is requested.
|
|
3
4
|
* Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
|
|
@@ -38,7 +39,12 @@ export function isInteractive(value) {
|
|
|
38
39
|
* somebody was there and chose to quit.
|
|
39
40
|
*
|
|
40
41
|
* Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
|
|
41
|
-
* and `--no-interactive` count even when a runner allocated a pty.
|
|
42
|
+
* and `--no-interactive` count even when a runner allocated a pty. It also
|
|
43
|
+
* counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
|
|
44
|
+
* *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
|
|
45
|
+
* (`openspec archive x > log.txt` from a terminal) that hits EOF must classify
|
|
46
|
+
* the same way the prompt was selected — otherwise it would leak the raw
|
|
47
|
+
* `ExitPromptError` instead of the `--yes` guidance.
|
|
42
48
|
*/
|
|
43
49
|
export function isNonInteractivePromptError(error, value) {
|
|
44
50
|
if (!(error instanceof Error))
|
|
@@ -48,6 +54,103 @@ export function isNonInteractivePromptError(error, value) {
|
|
|
48
54
|
return false;
|
|
49
55
|
if (error.message.includes('SIGINT'))
|
|
50
56
|
return false;
|
|
51
|
-
return !isInteractive(value);
|
|
57
|
+
return !isInteractive(value) || !process.stdout.isTTY;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
|
|
61
|
+
* everything else — a pipe, a file redirect, an agent that captures stdout —
|
|
62
|
+
* reads one plain line instead.
|
|
63
|
+
*
|
|
64
|
+
* @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
|
|
65
|
+
* and it emits them even when stdout is not a TTY. Redirected to a file those
|
|
66
|
+
* sequences are noise, and in some non-TTY hosts the render loop never settles
|
|
67
|
+
* and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
|
|
68
|
+
* the answer ourselves keeps the single piped answer @inquirer ever supported
|
|
69
|
+
* working (`printf 'y\n' | openspec archive ...`) without emitting any escapes.
|
|
70
|
+
*
|
|
71
|
+
* `io` overrides the streams; it exists for tests and mirrors @inquirer's own
|
|
72
|
+
* `{ input, output }` context. Production callers pass only the prompt.
|
|
73
|
+
*/
|
|
74
|
+
export async function confirmPrompt(prompt, io = {}) {
|
|
75
|
+
const input = io.input ?? process.stdin;
|
|
76
|
+
const output = io.output ?? process.stdout;
|
|
77
|
+
const isTerminal = Boolean(input.isTTY) &&
|
|
78
|
+
Boolean(output.isTTY);
|
|
79
|
+
if (isTerminal) {
|
|
80
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
81
|
+
return confirm(prompt);
|
|
82
|
+
}
|
|
83
|
+
return readYesNo(prompt, input, output);
|
|
84
|
+
}
|
|
85
|
+
function readYesNo(prompt, input, output) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
// Detach the input-stream error listener on settle: `input` is the
|
|
89
|
+
// long-lived process.stdin, so a leftover listener would accumulate across
|
|
90
|
+
// archive's successive prompts and swallow a later, unrelated stdin error.
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
input.removeListener('error', onError);
|
|
93
|
+
};
|
|
94
|
+
const blockOnNoAnswer = () => {
|
|
95
|
+
if (settled)
|
|
96
|
+
return;
|
|
97
|
+
settled = true;
|
|
98
|
+
cleanup();
|
|
99
|
+
// No line could be read (stdin closed / EOF). Mirror @inquirer's failure
|
|
100
|
+
// so callers that classify it — isNonInteractivePromptError, the #1479
|
|
101
|
+
// "rerun with --yes" guidance — keep working unchanged.
|
|
102
|
+
const error = new Error('User force closed the prompt');
|
|
103
|
+
error.name = 'ExitPromptError';
|
|
104
|
+
reject(error);
|
|
105
|
+
};
|
|
106
|
+
// A stdin error surfaces on the interface (readline forwards input-stream
|
|
107
|
+
// errors since Node 16). Without a handler the promise would hang and the
|
|
108
|
+
// 'error' would go unhandled; settle it with the real fault instead.
|
|
109
|
+
const onError = (err) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
rl.close();
|
|
115
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
};
|
|
117
|
+
// An earlier prompt may have already drained stdin (only one piped answer
|
|
118
|
+
// was ever supported). A fresh readline over an ended stream never emits
|
|
119
|
+
// 'close', so guard here rather than hang and exit as a no-op.
|
|
120
|
+
if (input.readableEnded) {
|
|
121
|
+
blockOnNoAnswer();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
output.write(`${prompt.message} ${prompt.default ? '(Y/n)' : '(y/N)'} `);
|
|
125
|
+
// terminal:false guarantees readline never emits its own line-editing
|
|
126
|
+
// escapes — an escape-free read is the whole point here.
|
|
127
|
+
const rl = createInterface({ input, terminal: false });
|
|
128
|
+
input.once('error', onError);
|
|
129
|
+
rl.once('error', onError);
|
|
130
|
+
rl.once('line', (line) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
rl.close();
|
|
136
|
+
output.write('\n');
|
|
137
|
+
// Mirror @inquirer/confirm's parser (prefix match on y/yes and n/no,
|
|
138
|
+
// otherwise the default) so a piped answer resolves identically to the
|
|
139
|
+
// interactive prompt it replaces.
|
|
140
|
+
const answer = line.trim();
|
|
141
|
+
if (/^(y|yes)/i.test(answer)) {
|
|
142
|
+
resolve(true);
|
|
143
|
+
}
|
|
144
|
+
else if (/^(n|no)/i.test(answer)) {
|
|
145
|
+
resolve(false);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
resolve(prompt.default);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
rl.once('close', () => {
|
|
152
|
+
blockOnNoAnswer();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
52
155
|
}
|
|
53
156
|
//# sourceMappingURL=interactive.js.map
|
|
@@ -20,6 +20,27 @@ export interface TaskProgress {
|
|
|
20
20
|
completed: number;
|
|
21
21
|
}
|
|
22
22
|
export declare function countTasksFromContent(content: string): TaskProgress;
|
|
23
|
+
/**
|
|
24
|
+
* Run-scoped memo mapping a schema name to its tracked-tasks `generates` glob.
|
|
25
|
+
* When one command resolves progress for many changes under a constant
|
|
26
|
+
* `projectRoot` — e.g. `validate --archived` over an append-only archive — this
|
|
27
|
+
* avoids re-reading and re-parsing (YAML + Zod) the same `schema.yaml` once per
|
|
28
|
+
* change. Keyed by schema name alone, which is safe *only* because a single run
|
|
29
|
+
* holds `projectRoot` constant; never reuse one cache across differing roots.
|
|
30
|
+
*/
|
|
31
|
+
export type SchemaGlobCache = Map<string, string | undefined>;
|
|
32
|
+
/** Resolves the task files selected by the schema's apply tracking rule. */
|
|
33
|
+
export declare function resolveTaskFilesForChange(changeDir: string, projectRoot: string, schemaGlobCache?: SchemaGlobCache): string[];
|
|
34
|
+
export interface TaskProgressDetail extends TaskProgress {
|
|
35
|
+
/**
|
|
36
|
+
* Task files that exist but could not be read (any error other than ENOENT).
|
|
37
|
+
* `getTaskProgressForChange` discards this list to preserve its behavior;
|
|
38
|
+
* callers that must fail loudly on an unreadable tasks file — e.g.
|
|
39
|
+
* `openspec validate --archived` — read it so an unreadable file is never
|
|
40
|
+
* silently counted as "no tasks" (#205).
|
|
41
|
+
*/
|
|
42
|
+
unreadable: string[];
|
|
43
|
+
}
|
|
23
44
|
/**
|
|
24
45
|
* Computes a change's task progress by resolving its tracked-tasks artifact and
|
|
25
46
|
* counting checkboxes across every file matched by that artifact's `generates`
|
|
@@ -27,7 +48,19 @@ export declare function countTasksFromContent(content: string): TaskProgress;
|
|
|
27
48
|
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
28
49
|
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
29
50
|
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
30
|
-
* or the glob matches no file.
|
|
51
|
+
* or the glob matches no file. Also reports task files that exist but could not
|
|
52
|
+
* be read. Per-file read errors are captured (never thrown); the only throw path
|
|
53
|
+
* is a malformed/unsafe schema whose glob resolution rejects (path traversal or
|
|
54
|
+
* a linked-directory cycle in `resolveArtifactOutputs`). Pass `schemaGlobCache`
|
|
55
|
+
* to memoize schema→glob resolution across many changes in one run.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getTaskProgressDetailForChange(changesDir: string, changeName: string, projectRoot: string, schemaGlobCache?: SchemaGlobCache): Promise<TaskProgressDetail>;
|
|
58
|
+
/**
|
|
59
|
+
* The task-completion counter `status`, `list`, and `archive` share. Delegates
|
|
60
|
+
* to `getTaskProgressDetailForChange` and drops the `unreadable` detail, so its
|
|
61
|
+
* returned totals are unchanged. Throws only on the same malformed/unsafe-schema
|
|
62
|
+
* glob-resolution path as that function (existing behavior; callers guard it as
|
|
63
|
+
* they did before).
|
|
31
64
|
*/
|
|
32
65
|
export declare function getTaskProgressForChange(changesDir: string, changeName: string, projectRoot: string): Promise<TaskProgress>;
|
|
33
66
|
export declare function formatTaskStatus(progress: TaskProgress): string;
|
|
@@ -64,24 +64,43 @@ function findTrackedTasksArtifact(schema) {
|
|
|
64
64
|
* when the schema cannot be resolved or no tracked-tasks artifact exists.
|
|
65
65
|
* `resolveSchema` throws on an unresolvable/misnamed schema; we swallow that so
|
|
66
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.
|
|
67
69
|
*/
|
|
68
|
-
function resolveTrackedTasksGlob(changeDir, projectRoot) {
|
|
70
|
+
function resolveTrackedTasksGlob(changeDir, projectRoot, schemaGlobCache) {
|
|
69
71
|
try {
|
|
70
72
|
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot);
|
|
73
|
+
if (schemaGlobCache?.has(schemaName))
|
|
74
|
+
return schemaGlobCache.get(schemaName);
|
|
71
75
|
const schema = resolveSchema(schemaName, projectRoot);
|
|
72
|
-
|
|
76
|
+
const generates = findTrackedTasksArtifact(schema)?.generates;
|
|
77
|
+
schemaGlobCache?.set(schemaName, generates);
|
|
78
|
+
return generates;
|
|
73
79
|
}
|
|
74
80
|
catch {
|
|
75
81
|
return undefined;
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
|
-
|
|
79
|
-
|
|
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) {
|
|
80
97
|
try {
|
|
81
|
-
const content = await fs.readFile(
|
|
98
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
82
99
|
return countTasksFromContent(content);
|
|
83
100
|
}
|
|
84
|
-
catch {
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error?.code !== 'ENOENT')
|
|
103
|
+
unreadable.push(file);
|
|
85
104
|
return { total: 0, completed: 0 };
|
|
86
105
|
}
|
|
87
106
|
}
|
|
@@ -92,31 +111,36 @@ async function countSingleTopLevelTasksFile(changeDir) {
|
|
|
92
111
|
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
93
112
|
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
94
113
|
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
95
|
-
* 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.
|
|
96
119
|
*/
|
|
97
|
-
export async function
|
|
120
|
+
export async function getTaskProgressDetailForChange(changesDir, changeName, projectRoot, schemaGlobCache) {
|
|
98
121
|
const changeDir = path.join(changesDir, changeName);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const progress = countTasksFromContent(content);
|
|
109
|
-
total += progress.total;
|
|
110
|
-
completed += progress.completed;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// Swallow files that vanish between glob and read, as before.
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return { total, completed };
|
|
117
|
-
}
|
|
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;
|
|
118
131
|
}
|
|
119
|
-
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 };
|
|
120
144
|
}
|
|
121
145
|
export function formatTaskStatus(progress) {
|
|
122
146
|
if (progress.total === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"dist",
|
|
34
34
|
"bin",
|
|
35
35
|
"schemas",
|
|
36
|
-
"scripts/postinstall.js",
|
|
37
36
|
"!dist/**/*.test.js",
|
|
38
37
|
"!dist/**/__tests__",
|
|
39
38
|
"!dist/**/*.map"
|
|
@@ -53,8 +52,8 @@
|
|
|
53
52
|
"vitest": "^3.2.6"
|
|
54
53
|
},
|
|
55
54
|
"dependencies": {
|
|
56
|
-
"@inquirer/core": "^
|
|
57
|
-
"@inquirer/prompts": "^
|
|
55
|
+
"@inquirer/core": "^11.2.1",
|
|
56
|
+
"@inquirer/prompts": "^8.5.2",
|
|
58
57
|
"chalk": "^5.6.2",
|
|
59
58
|
"commander": "^14.0.0",
|
|
60
59
|
"cross-spawn": "7.0.6",
|
|
@@ -74,8 +73,6 @@
|
|
|
74
73
|
"test:watch": "vitest",
|
|
75
74
|
"test:ui": "vitest --ui",
|
|
76
75
|
"test:coverage": "vitest --coverage",
|
|
77
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
78
|
-
"postinstall": "node scripts/postinstall.js",
|
|
79
76
|
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
80
77
|
"release": "pnpm run release:ci",
|
|
81
78
|
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
@@ -91,10 +91,16 @@ artifacts:
|
|
|
91
91
|
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
|
|
92
92
|
that spec already has one and the delta's is ignored. To change an
|
|
93
93
|
existing capability's Purpose - including a leftover `TBD` placeholder -
|
|
94
|
-
edit
|
|
94
|
+
edit `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
|
|
95
|
+
directly. `planningHome.root` comes from the `openspec instructions ...
|
|
96
|
+
--json` response. Always use it rather than a repo-relative path: it
|
|
97
|
+
resolves to the store whenever the change lives in one - whether that
|
|
98
|
+
came from `--store`, a project `store:` pointer, or a global default
|
|
99
|
+
store - and to the current repository otherwise. Do not try to work out
|
|
100
|
+
which case applies; the field already has.
|
|
95
101
|
|
|
96
102
|
MODIFIED requirements workflow:
|
|
97
|
-
1. Locate the existing requirement in openspec/specs/<capability-path>/spec.md
|
|
103
|
+
1. Locate the existing requirement in `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (the same store-aware root as above)
|
|
98
104
|
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
|
|
99
105
|
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
|
|
100
106
|
4. Ensure header text matches exactly (whitespace-insensitive)
|
|
@@ -182,22 +188,26 @@ artifacts:
|
|
|
182
188
|
- Each task MUST be a checkbox: `- [ ] X.Y Task description`
|
|
183
189
|
- Tasks should be small enough to complete in one session
|
|
184
190
|
- Order tasks by dependency (what must be done first?)
|
|
191
|
+
- Each task MUST state how to verify completion (a test, command,
|
|
192
|
+
observable behavior, or delivered artifact). Put the verification in
|
|
193
|
+
that task's checkbox description. Use a separate verification task only
|
|
194
|
+
when it checks broader integration or system behavior that spans
|
|
195
|
+
multiple implementation tasks.
|
|
185
196
|
|
|
186
197
|
Example:
|
|
187
198
|
```
|
|
188
199
|
## 1. Setup
|
|
189
200
|
|
|
190
|
-
- [ ] 1.1 Create new module structure
|
|
191
|
-
- [ ] 1.2 Add dependencies to package.json
|
|
201
|
+
- [ ] 1.1 Create new module structure and verify expected files are present
|
|
202
|
+
- [ ] 1.2 Add dependencies to package.json and verify package installation succeeds
|
|
192
203
|
|
|
193
204
|
## 2. Core Implementation
|
|
194
205
|
|
|
195
|
-
- [ ] 2.1 Implement data export function
|
|
196
|
-
- [ ] 2.2 Add CSV formatting utilities
|
|
206
|
+
- [ ] 2.1 Implement data export function and verify the export test passes
|
|
207
|
+
- [ ] 2.2 Add CSV formatting utilities and verify unit tests cover quoting and delimiters
|
|
197
208
|
```
|
|
198
209
|
|
|
199
210
|
Reference specs for what needs to be built, design for how to build it.
|
|
200
|
-
Each task should be verifiable - you know when it's done.
|
|
201
211
|
requires:
|
|
202
212
|
- specs
|
|
203
213
|
- design
|
package/scripts/postinstall.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postinstall script that hints about shell completions
|
|
5
|
-
*
|
|
6
|
-
* Completion installation is opt-in: the user must run
|
|
7
|
-
* `openspec completion install` explicitly. This script only
|
|
8
|
-
* prints a one-line tip after npm install.
|
|
9
|
-
*
|
|
10
|
-
* The tip is suppressed when:
|
|
11
|
-
* - CI=true environment variable is set
|
|
12
|
-
* - OPENSPEC_NO_COMPLETIONS=1 environment variable is set
|
|
13
|
-
* - dist/ directory doesn't exist (dev setup scenario)
|
|
14
|
-
*
|
|
15
|
-
* The script never fails npm install - all errors are caught and handled gracefully.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { promises as fs } from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
|
|
22
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
-
const __dirname = path.dirname(__filename);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if we should skip installation
|
|
27
|
-
*/
|
|
28
|
-
function shouldSkipInstallation() {
|
|
29
|
-
// Skip in CI environments
|
|
30
|
-
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
31
|
-
return { skip: true, reason: 'CI environment detected' };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Skip if user opted out
|
|
35
|
-
if (process.env.OPENSPEC_NO_COMPLETIONS === '1') {
|
|
36
|
-
return { skip: true, reason: 'OPENSPEC_NO_COMPLETIONS=1 set' };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { skip: false };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Check if dist/ directory exists
|
|
44
|
-
*/
|
|
45
|
-
async function distExists() {
|
|
46
|
-
const distPath = path.join(__dirname, '..', 'dist');
|
|
47
|
-
try {
|
|
48
|
-
const stat = await fs.stat(distPath);
|
|
49
|
-
return stat.isDirectory();
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Main function
|
|
57
|
-
*/
|
|
58
|
-
async function main() {
|
|
59
|
-
try {
|
|
60
|
-
// Check if we should skip
|
|
61
|
-
const skipCheck = shouldSkipInstallation();
|
|
62
|
-
if (skipCheck.skip) {
|
|
63
|
-
// Silent skip - no output
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Check if dist/ exists (skip silently if not - expected during dev setup)
|
|
68
|
-
if (!(await distExists())) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Completions are opt-in — just print a hint
|
|
73
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
// Fail gracefully - never break npm install
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Run main and handle any unhandled errors
|
|
80
|
-
main().catch(() => {
|
|
81
|
-
// Silent failure - never break npm install
|
|
82
|
-
process.exit(0);
|
|
83
|
-
});
|