@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
package/dist/commands/spec.js
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import path, { join } from 'path';
|
|
3
3
|
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
|
4
4
|
import { Validator } from '../core/validation/validator.js';
|
|
5
5
|
import { isInteractive } from '../utils/interactive.js';
|
|
6
6
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
7
7
|
import { discoverSpecFiles } from '../utils/spec-discovery.js';
|
|
8
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
8
9
|
const SPECS_DIR = 'openspec/specs';
|
|
9
|
-
function
|
|
10
|
+
function assertSpecPath(specsDir, specPath) {
|
|
11
|
+
const relativePath = path.relative(path.resolve(specsDir), path.resolve(specPath));
|
|
12
|
+
if (relativePath === '..' ||
|
|
13
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
14
|
+
path.isAbsolute(relativePath)) {
|
|
15
|
+
throw new Error(`Path is outside the allowed directory: ${specPath}`);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
// Preserve confined spec.md links, including links to a sibling capability.
|
|
19
|
+
FileSystemUtils.assertPathWithin(specsDir, specPath);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A capability directory may intentionally be a monorepo symlink. Treat it
|
|
23
|
+
// as the trust root while still rejecting a link outside that capability.
|
|
24
|
+
FileSystemUtils.assertPathWithin(path.dirname(specPath), specPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseSpecFromFile(specsDir, specPath, specId) {
|
|
28
|
+
assertSpecPath(specsDir, specPath);
|
|
10
29
|
const content = readFileSync(specPath, 'utf-8');
|
|
11
30
|
const parser = new MarkdownParser(content);
|
|
12
31
|
return parser.parseSpec(specId);
|
|
@@ -41,7 +60,8 @@ function filterSpec(spec, options) {
|
|
|
41
60
|
* Print the raw markdown content for a spec file without any formatting.
|
|
42
61
|
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
|
|
43
62
|
*/
|
|
44
|
-
function printSpecTextRaw(specPath) {
|
|
63
|
+
function printSpecTextRaw(specsDir, specPath) {
|
|
64
|
+
assertSpecPath(specsDir, specPath);
|
|
45
65
|
const content = readFileSync(specPath, 'utf-8');
|
|
46
66
|
console.log(content);
|
|
47
67
|
}
|
|
@@ -70,6 +90,7 @@ export class SpecCommand {
|
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
92
|
const specPath = join(this.specsDir, specId, 'spec.md');
|
|
93
|
+
assertSpecPath(this.specsDir, specPath);
|
|
73
94
|
if (!existsSync(specPath)) {
|
|
74
95
|
// Root-aware callers get the absolute path; the cwd-based noun form
|
|
75
96
|
// keeps its historical forward-slash relative message on all platforms.
|
|
@@ -80,7 +101,7 @@ export class SpecCommand {
|
|
|
80
101
|
if (options.requirements && options.requirement) {
|
|
81
102
|
throw new Error('Options --requirements and --requirement cannot be used together');
|
|
82
103
|
}
|
|
83
|
-
const parsed = parseSpecFromFile(specPath, specId);
|
|
104
|
+
const parsed = parseSpecFromFile(this.specsDir, specPath, specId);
|
|
84
105
|
const filtered = filterSpec(parsed, options);
|
|
85
106
|
const output = {
|
|
86
107
|
id: specId,
|
|
@@ -94,7 +115,7 @@ export class SpecCommand {
|
|
|
94
115
|
console.log(JSON.stringify(output, null, 2));
|
|
95
116
|
return;
|
|
96
117
|
}
|
|
97
|
-
printSpecTextRaw(specPath);
|
|
118
|
+
printSpecTextRaw(this.specsDir, specPath);
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
export function registerSpecCommand(rootProgram) {
|
|
@@ -138,7 +159,8 @@ export function registerSpecCommand(rootProgram) {
|
|
|
138
159
|
const specs = discovered
|
|
139
160
|
.map(({ id, specFile }) => {
|
|
140
161
|
try {
|
|
141
|
-
|
|
162
|
+
assertSpecPath(SPECS_DIR, specFile);
|
|
163
|
+
const spec = parseSpecFromFile(SPECS_DIR, specFile, id);
|
|
142
164
|
return {
|
|
143
165
|
id,
|
|
144
166
|
title: spec.name,
|
|
@@ -199,10 +221,12 @@ export function registerSpecCommand(rootProgram) {
|
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
const specPath = join(SPECS_DIR, specId, 'spec.md');
|
|
224
|
+
assertSpecPath(SPECS_DIR, specPath);
|
|
202
225
|
if (!existsSync(specPath)) {
|
|
203
226
|
throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
|
|
204
227
|
}
|
|
205
228
|
const validator = new Validator(options.strict);
|
|
229
|
+
assertSpecPath(SPECS_DIR, specPath);
|
|
206
230
|
const report = await validator.validateSpec(specPath);
|
|
207
231
|
if (options.json) {
|
|
208
232
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -2,6 +2,7 @@ interface ExecuteOptions {
|
|
|
2
2
|
all?: boolean;
|
|
3
3
|
changes?: boolean;
|
|
4
4
|
specs?: boolean;
|
|
5
|
+
archived?: boolean;
|
|
5
6
|
type?: string;
|
|
6
7
|
strict?: boolean;
|
|
7
8
|
json?: boolean;
|
|
@@ -29,6 +30,28 @@ export declare class ValidateCommand {
|
|
|
29
30
|
private printReport;
|
|
30
31
|
private printNextSteps;
|
|
31
32
|
private runBulkValidation;
|
|
33
|
+
/**
|
|
34
|
+
* Lists archived change ids from the resolved root's archive directory,
|
|
35
|
+
* mirroring `getArchivedChangeIds` but store-aware (uses `root.archiveDir`
|
|
36
|
+
* rather than a cwd-relative path). Directories only, hidden entries skipped.
|
|
37
|
+
*
|
|
38
|
+
* Only a missing archive directory (ENOENT) is an empty list; a permission
|
|
39
|
+
* error, an I/O error, or an `archive` path that is a file (ENOTDIR) is a real
|
|
40
|
+
* failure and must not read as "no archived changes" — that would let a
|
|
41
|
+
* pre-commit lint pass without inspecting anything (#205).
|
|
42
|
+
*/
|
|
43
|
+
private listArchivedChangeIds;
|
|
44
|
+
/**
|
|
45
|
+
* Validates that every archived change has all of its tasks completed.
|
|
46
|
+
*
|
|
47
|
+
* An archived change is expected to be finished; an archived change with
|
|
48
|
+
* unchecked tasks is a real integrity problem the normal validate flow never
|
|
49
|
+
* surfaces, because active-change discovery excludes the archive directory
|
|
50
|
+
* (#205). Reuses the same task-progress counting `status`, `list`, and
|
|
51
|
+
* `archive` rely on, so what counts as a task never forks. Changes with no
|
|
52
|
+
* tasks pass (nothing to complete).
|
|
53
|
+
*/
|
|
54
|
+
private runArchivedTaskValidation;
|
|
32
55
|
}
|
|
33
56
|
export {};
|
|
34
57
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -7,15 +7,33 @@ import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
|
7
7
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
8
8
|
import { getAvailableChanges } from './workflow/shared.js';
|
|
9
9
|
import { nearestMatches } from '../utils/match.js';
|
|
10
|
+
import { promises as fs } from 'fs';
|
|
11
|
+
import { getTaskProgressDetailForChange } from '../utils/task-progress.js';
|
|
12
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
10
13
|
export class ValidateCommand {
|
|
11
14
|
async execute(itemName, options = {}) {
|
|
12
|
-
const
|
|
15
|
+
const bulk = options.all || options.changes || options.specs;
|
|
16
|
+
const root = await resolveRootForCommand(options, {
|
|
17
|
+
json: options.json,
|
|
18
|
+
...(bulk ? { allowImplicitRoot: false } : {}),
|
|
19
|
+
});
|
|
13
20
|
if (!root) {
|
|
14
21
|
return;
|
|
15
22
|
}
|
|
16
23
|
const interactive = isInteractive(options);
|
|
24
|
+
// Archived-task linting is its own scope: it checks task completion of
|
|
25
|
+
// already-archived changes, not delta specs (whose operations are already
|
|
26
|
+
// applied). Handled before the other bulk flags so `--archived` is explicit
|
|
27
|
+
// and never alters an existing invocation's behavior (#205).
|
|
28
|
+
if (options.archived) {
|
|
29
|
+
await this.runArchivedTaskValidation(root, {
|
|
30
|
+
json: !!options.json,
|
|
31
|
+
noInteractive: resolveNoInteractive(options),
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
17
35
|
// Handle bulk flags first
|
|
18
|
-
if (
|
|
36
|
+
if (bulk) {
|
|
19
37
|
await this.runBulkValidation(root, {
|
|
20
38
|
changes: !!options.all || !!options.changes,
|
|
21
39
|
specs: !!options.all || !!options.specs,
|
|
@@ -145,7 +163,10 @@ export class ValidateCommand {
|
|
|
145
163
|
if (type === 'change') {
|
|
146
164
|
const changeDir = path.join(root.changesDir, id);
|
|
147
165
|
const start = Date.now();
|
|
148
|
-
const report = await validator.validateChangeDeltaSpecs(changeDir
|
|
166
|
+
const report = await validator.validateChangeDeltaSpecs(changeDir, {
|
|
167
|
+
mainSpecsDir: root.specsDir,
|
|
168
|
+
projectRoot: root.path,
|
|
169
|
+
});
|
|
149
170
|
const durationMs = Date.now() - start;
|
|
150
171
|
this.printReport('change', id, report, durationMs, opts.json, root);
|
|
151
172
|
// Non-zero exit if invalid (keeps enriched output test semantics)
|
|
@@ -222,7 +243,10 @@ export class ValidateCommand {
|
|
|
222
243
|
queue.push(async () => {
|
|
223
244
|
const start = Date.now();
|
|
224
245
|
const changeDir = path.join(root.changesDir, id);
|
|
225
|
-
const report = await validator.validateChangeDeltaSpecs(changeDir
|
|
246
|
+
const report = await validator.validateChangeDeltaSpecs(changeDir, {
|
|
247
|
+
mainSpecsDir: root.specsDir,
|
|
248
|
+
projectRoot: root.path,
|
|
249
|
+
});
|
|
226
250
|
const durationMs = Date.now() - start;
|
|
227
251
|
return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
|
|
228
252
|
});
|
|
@@ -322,6 +346,126 @@ export class ValidateCommand {
|
|
|
322
346
|
}
|
|
323
347
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
324
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Lists archived change ids from the resolved root's archive directory,
|
|
351
|
+
* mirroring `getArchivedChangeIds` but store-aware (uses `root.archiveDir`
|
|
352
|
+
* rather than a cwd-relative path). Directories only, hidden entries skipped.
|
|
353
|
+
*
|
|
354
|
+
* Only a missing archive directory (ENOENT) is an empty list; a permission
|
|
355
|
+
* error, an I/O error, or an `archive` path that is a file (ENOTDIR) is a real
|
|
356
|
+
* failure and must not read as "no archived changes" — that would let a
|
|
357
|
+
* pre-commit lint pass without inspecting anything (#205).
|
|
358
|
+
*/
|
|
359
|
+
async listArchivedChangeIds(root) {
|
|
360
|
+
try {
|
|
361
|
+
const entries = await fs.readdir(root.archiveDir, { withFileTypes: true });
|
|
362
|
+
return entries
|
|
363
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
364
|
+
.map((entry) => entry.name)
|
|
365
|
+
.sort();
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (error?.code === 'ENOENT')
|
|
369
|
+
return [];
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Validates that every archived change has all of its tasks completed.
|
|
375
|
+
*
|
|
376
|
+
* An archived change is expected to be finished; an archived change with
|
|
377
|
+
* unchecked tasks is a real integrity problem the normal validate flow never
|
|
378
|
+
* surfaces, because active-change discovery excludes the archive directory
|
|
379
|
+
* (#205). Reuses the same task-progress counting `status`, `list`, and
|
|
380
|
+
* `archive` rely on, so what counts as a task never forks. Changes with no
|
|
381
|
+
* tasks pass (nothing to complete).
|
|
382
|
+
*/
|
|
383
|
+
async runArchivedTaskValidation(root, opts) {
|
|
384
|
+
// List first (may throw on a real archive-read failure), then start the
|
|
385
|
+
// spinner so a thrown error never leaves a spinner spinning.
|
|
386
|
+
const ids = await this.listArchivedChangeIds(root);
|
|
387
|
+
const spinner = !opts.json && !opts.noInteractive ? ora('Validating archived changes...').start() : undefined;
|
|
388
|
+
// The archive is append-only and can hold thousands of changes; a single
|
|
389
|
+
// run resolves them all under one constant projectRoot (root.path), so
|
|
390
|
+
// memoize the schema→glob lookup to avoid re-parsing the same schema.yaml
|
|
391
|
+
// once per change. The loop is intentionally sequential: the per-change work
|
|
392
|
+
// is dominated by synchronous schema/config resolution, which a promise pool
|
|
393
|
+
// cannot overlap on Node's single thread — a pool would add complexity for
|
|
394
|
+
// no real gain here.
|
|
395
|
+
const schemaGlobCache = new Map();
|
|
396
|
+
const results = [];
|
|
397
|
+
let passed = 0;
|
|
398
|
+
let failed = 0;
|
|
399
|
+
for (const id of ids) {
|
|
400
|
+
const start = Date.now();
|
|
401
|
+
const issues = [];
|
|
402
|
+
try {
|
|
403
|
+
// The explicit root.path override is load-bearing: an archived change
|
|
404
|
+
// lives one directory deeper (changes/archive/<id>), so the default
|
|
405
|
+
// "../../.." projectRoot derivation would be wrong without it.
|
|
406
|
+
const progress = await getTaskProgressDetailForChange(root.archiveDir, id, root.path, schemaGlobCache);
|
|
407
|
+
// A tasks file that exists but cannot be read must fail loudly, not be
|
|
408
|
+
// silently counted as "no tasks" and pass. Report one issue per file,
|
|
409
|
+
// pathed like every other validate issue (POSIX, root-relative).
|
|
410
|
+
for (const file of progress.unreadable) {
|
|
411
|
+
issues.push({
|
|
412
|
+
level: 'ERROR',
|
|
413
|
+
path: FileSystemUtils.toPosixPath(path.relative(root.path, file)),
|
|
414
|
+
message: 'could not read task file',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const incomplete = Math.max(progress.total - progress.completed, 0);
|
|
418
|
+
if (incomplete > 0) {
|
|
419
|
+
issues.push({
|
|
420
|
+
level: 'ERROR',
|
|
421
|
+
path: 'tasks.md',
|
|
422
|
+
message: `${incomplete} incomplete task${incomplete === 1 ? '' : 's'} (${progress.completed}/${progress.total} completed)`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
issues.push({ level: 'ERROR', path: 'tasks.md', message: error?.message || 'Unknown error' });
|
|
428
|
+
}
|
|
429
|
+
const valid = issues.length === 0;
|
|
430
|
+
if (valid)
|
|
431
|
+
passed++;
|
|
432
|
+
else
|
|
433
|
+
failed++;
|
|
434
|
+
results.push({ id, type: 'change', valid, issues, durationMs: Date.now() - start });
|
|
435
|
+
}
|
|
436
|
+
spinner?.stop();
|
|
437
|
+
const summary = {
|
|
438
|
+
totals: { items: results.length, passed, failed },
|
|
439
|
+
byType: { change: summarizeType(results, 'change') },
|
|
440
|
+
};
|
|
441
|
+
if (opts.json) {
|
|
442
|
+
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
443
|
+
console.log(JSON.stringify(out, null, 2));
|
|
444
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (results.length === 0) {
|
|
448
|
+
console.log('No archived changes found.');
|
|
449
|
+
process.exitCode = 0;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Use the same `<type>/<id>` prefix bulk validation prints, so the plain
|
|
453
|
+
// output maps to the JSON `type` ('change') and stays greppable the same way.
|
|
454
|
+
for (const res of results) {
|
|
455
|
+
if (res.valid) {
|
|
456
|
+
console.log(`✓ change/${res.id}`);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
console.error(`✗ change/${res.id}`);
|
|
460
|
+
for (const issue of res.issues) {
|
|
461
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
462
|
+
console.error(` ${prefix} ${issue.message}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
467
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
468
|
+
}
|
|
325
469
|
}
|
|
326
470
|
function summarizeType(results, type) {
|
|
327
471
|
const filtered = results.filter(r => r.type === type);
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
-
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
10
|
+
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputPath, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
11
|
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
12
12
|
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
13
13
|
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
14
14
|
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
15
15
|
import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
|
|
16
16
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
17
|
+
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
17
18
|
// -----------------------------------------------------------------------------
|
|
18
19
|
// Artifact Instructions Command
|
|
19
20
|
// -----------------------------------------------------------------------------
|
|
@@ -206,25 +207,26 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
206
207
|
// Apply Instructions Command
|
|
207
208
|
// -----------------------------------------------------------------------------
|
|
208
209
|
/**
|
|
209
|
-
*
|
|
210
|
+
* Turns parsed task lines into the listed task items.
|
|
211
|
+
*
|
|
212
|
+
* A checkbox with no text after it is left out of the list: this is work for an
|
|
213
|
+
* agent to act on and tick off, and a bare `- [ ]` gives it nothing to match.
|
|
214
|
+
* It still counts toward progress, which is taken from every parsed line, so
|
|
215
|
+
* this list can be shorter than the totals beside it but never disagrees with
|
|
216
|
+
* `openspec list` or archive about how much work is left. An empty list is also
|
|
217
|
+
* what puts apply in its "nothing to work on" state, so a file of nothing but
|
|
218
|
+
* text-less checkboxes asks to be rewritten instead of being called done.
|
|
210
219
|
*/
|
|
211
|
-
function
|
|
220
|
+
function toTaskItems(parsed) {
|
|
212
221
|
const tasks = [];
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const description = checkboxMatch[2].trim();
|
|
222
|
-
tasks.push({
|
|
223
|
-
id: `${taskIndex}`,
|
|
224
|
-
description,
|
|
225
|
-
done,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
222
|
+
for (const task of parsed) {
|
|
223
|
+
if (task.description.length === 0)
|
|
224
|
+
continue;
|
|
225
|
+
tasks.push({
|
|
226
|
+
id: `${tasks.length + 1}`,
|
|
227
|
+
description: task.description,
|
|
228
|
+
done: task.done,
|
|
229
|
+
});
|
|
228
230
|
}
|
|
229
231
|
return tasks;
|
|
230
232
|
}
|
|
@@ -274,19 +276,21 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
// Parse tasks if tracking file exists
|
|
277
|
-
let
|
|
279
|
+
let parsedTasks = [];
|
|
278
280
|
let tracksFileExists = false;
|
|
279
281
|
if (tracksFile) {
|
|
280
|
-
const tracksPath =
|
|
282
|
+
const tracksPath = resolveArtifactOutputPath(changeDir, tracksFile);
|
|
281
283
|
tracksFileExists = fs.existsSync(tracksPath);
|
|
282
284
|
if (tracksFileExists) {
|
|
283
285
|
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
|
|
284
|
-
|
|
286
|
+
parsedTasks = parseTaskLines(tasksContent);
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const tasks = toTaskItems(parsedTasks);
|
|
290
|
+
// Calculate progress over every checkbox in the file, listed or not, so these
|
|
291
|
+
// numbers match `openspec list` and archive's incomplete-task check.
|
|
292
|
+
const total = parsedTasks.length;
|
|
293
|
+
const complete = parsedTasks.filter((task) => task.done).length;
|
|
290
294
|
const remaining = total - complete;
|
|
291
295
|
// Determine state and instruction
|
|
292
296
|
let state;
|
|
@@ -301,11 +305,12 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
301
305
|
state = 'blocked';
|
|
302
306
|
instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
|
|
303
307
|
}
|
|
304
|
-
else if (tracksFile && tracksFileExists &&
|
|
305
|
-
// Tracking file exists but
|
|
308
|
+
else if (tracksFile && tracksFileExists && tasks.length === 0) {
|
|
309
|
+
// Tracking file exists but lists nothing an agent can work on: either no
|
|
310
|
+
// checkboxes at all, or only checkboxes with no text after them.
|
|
306
311
|
const tracksFilename = path.basename(tracksFile);
|
|
307
312
|
state = 'blocked';
|
|
308
|
-
instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
|
|
313
|
+
instruction = `The ${tracksFilename} file exists but contains no tasks to work on.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
|
|
309
314
|
}
|
|
310
315
|
else if (tracksFile && remaining === 0 && total > 0) {
|
|
311
316
|
state = 'all_done';
|
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { listSchemasWithInfo } from '../../core/artifact-graph/index.js';
|
|
8
|
+
import { resolveRootForCommand } from '../../core/root-selection.js';
|
|
8
9
|
// -----------------------------------------------------------------------------
|
|
9
10
|
// Command Implementation
|
|
10
11
|
// -----------------------------------------------------------------------------
|
|
11
12
|
export async function schemasCommand(options) {
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const root = await resolveRootForCommand(options, {
|
|
14
|
+
json: options.json,
|
|
15
|
+
failurePayload: { schemas: [], root: null },
|
|
16
|
+
});
|
|
17
|
+
if (!root) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const schemas = listSchemasWithInfo(root.path);
|
|
14
21
|
if (options.json) {
|
|
15
22
|
console.log(JSON.stringify(schemas, null, 2));
|
|
16
23
|
return;
|
|
@@ -89,9 +89,9 @@ export function printStatusText(status) {
|
|
|
89
89
|
}
|
|
90
90
|
console.log(line);
|
|
91
91
|
}
|
|
92
|
-
if (status.
|
|
92
|
+
if (status.isPlanningComplete) {
|
|
93
93
|
console.log();
|
|
94
|
-
console.log(chalk.green('All artifacts complete!'));
|
|
94
|
+
console.log(chalk.green('All planning artifacts complete!'));
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
//# sourceMappingURL=status.js.map
|
|
@@ -39,11 +39,21 @@ export async function templatesCommand(options) {
|
|
|
39
39
|
else {
|
|
40
40
|
source = 'package';
|
|
41
41
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
templatePath
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const templatesDir = path.join(schemaDir, 'templates');
|
|
43
|
+
const templates = graph.getAllArtifacts().map((artifact) => {
|
|
44
|
+
const templatePath = path.join(templatesDir, artifact.template);
|
|
45
|
+
try {
|
|
46
|
+
FileSystemUtils.assertPathWithin(templatesDir, templatePath);
|
|
47
|
+
return {
|
|
48
|
+
artifactId: artifact.id,
|
|
49
|
+
templatePath: FileSystemUtils.canonicalizeExistingPath(templatePath),
|
|
50
|
+
source,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`Template '${artifact.template}' for artifact '${artifact.id}' points outside the schema templates directory`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
47
57
|
spinner?.stop();
|
|
48
58
|
if (options.json) {
|
|
49
59
|
const output = {};
|
package/dist/core/archive.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the ONLY thing wrong with a rebuilt spec is that it has no
|
|
3
|
+
* requirements. That is the exact failure retiring a capability replaces
|
|
4
|
+
* (#1302); anything else means the spec is broken in a way the author still has
|
|
5
|
+
* to fix, so archive must abort exactly as it always did instead of retiring.
|
|
6
|
+
*
|
|
7
|
+
* Asking the validator - rather than counting requirement blocks a second time -
|
|
8
|
+
* is what makes "this spec could not have been written anyway" true by
|
|
9
|
+
* construction. The two counts genuinely disagree: `MarkdownParser` accepts any
|
|
10
|
+
* `###` heading under `## Requirements` as a requirement, while the delta block
|
|
11
|
+
* parser only indexes canonical `### Requirement:` headers and sweeps the rest
|
|
12
|
+
* into the preamble, which survives into the rebuilt spec.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isRetirableSpec(specName: string, rebuilt: string): Promise<boolean>;
|
|
1
15
|
export interface ArchiveOptions {
|
|
2
16
|
yes?: boolean;
|
|
3
17
|
skipSpecs?: boolean;
|