@fission-ai/openspec 1.11.0 → 1.13.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 -14
- package/dist/cli/index.js +1 -0
- package/dist/commands/change.js +6 -5
- package/dist/commands/validate.d.ts +57 -0
- package/dist/commands/validate.js +99 -13
- package/dist/commands/workflow/instructions.js +140 -5
- package/dist/commands/workflow/shared.d.ts +8 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -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 +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +41 -34
- package/dist/core/onboarding-commands.d.ts +11 -1
- package/dist/core/onboarding-commands.js +26 -0
- package/dist/core/openspec-root.d.ts +1 -0
- package/dist/core/openspec-root.js +10 -2
- package/dist/core/parsers/requirement-blocks.js +77 -31
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/tool-detection.js +28 -1
- package/dist/core/specs-apply.js +184 -8
- package/dist/core/templates/workflows/explore.js +45 -0
- package/dist/core/templates/workflows/ff-change.js +8 -0
- package/dist/core/templates/workflows/propose.js +40 -12
- package/dist/core/update.d.ts +22 -0
- package/dist/core/update.js +55 -17
- package/dist/core/validation/validator.d.ts +12 -3
- package/dist/core/validation/validator.js +74 -3
- package/dist/utils/command-references.js +7 -8
- package/package.json +3 -3
- package/schemas/spec-driven/schema.yaml +14 -2
package/README.md
CHANGED
|
@@ -172,6 +172,7 @@ Both are in the default profile. If you want the expanded workflow (`/opsx:new`,
|
|
|
172
172
|
→ **[Concepts](docs/concepts.md)**: how it all fits<br>
|
|
173
173
|
→ **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
|
|
174
174
|
→ **[Customization](docs/customization.md)**: make it yours<br>
|
|
175
|
+
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
|
|
175
176
|
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
|
|
176
177
|
|
|
177
178
|
|
|
@@ -223,21 +224,9 @@ openspec update
|
|
|
223
224
|
|
|
224
225
|
## Contributing
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
Open a discussion (for core design changes) or an issue before you open a PR, and link the issue or discussion from the PR. New features, significant refactors, and architectural changes need an OpenSpec change proposal first.
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
When writing proposals, keep the OpenSpec philosophy in mind: we serve a wide variety of users across different coding agents, models, and use cases. Changes should work well for everyone.
|
|
231
|
-
|
|
232
|
-
**AI-generated code is welcome** — as long as it's been tested and verified. PRs containing AI-generated code should mention the coding agent and model used (e.g., "Generated with Claude Code using claude-opus-4-5-20251101").
|
|
233
|
-
|
|
234
|
-
### Development
|
|
235
|
-
|
|
236
|
-
- Install dependencies: `pnpm install`
|
|
237
|
-
- Build: `pnpm run build`
|
|
238
|
-
- Test: `pnpm test`
|
|
239
|
-
- Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
|
|
240
|
-
- Conventional commits (one-line): `type(scope): subject`
|
|
229
|
+
→ **[CONTRIBUTING.md](CONTRIBUTING.md)**: the full process, from first issue to merged PR
|
|
241
230
|
|
|
242
231
|
## Other
|
|
243
232
|
|
package/dist/cli/index.js
CHANGED
|
@@ -449,6 +449,7 @@ program
|
|
|
449
449
|
.option('--changes', 'Validate all changes')
|
|
450
450
|
.option('--specs', 'Validate all specs')
|
|
451
451
|
.option('--archived', 'Validate that archived changes have all tasks completed (for pre-commit linting)')
|
|
452
|
+
.option('--report <full|findings>', 'Select bulk report content: full|findings; combine with --json for JSON')
|
|
452
453
|
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
|
|
453
454
|
.option('--strict', 'Enable strict validation mode')
|
|
454
455
|
.option('--json', 'Output validation results as JSON')
|
package/dist/commands/change.js
CHANGED
|
@@ -480,11 +480,12 @@ export class ChangeCommand {
|
|
|
480
480
|
}
|
|
481
481
|
else {
|
|
482
482
|
console.error(`Change "${changeName}" has issues`);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
483
|
+
}
|
|
484
|
+
report.issues.forEach(issue => {
|
|
485
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
486
|
+
console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
487
|
+
});
|
|
488
|
+
if (!report.valid) {
|
|
488
489
|
// Next steps footer to guide fixing issues
|
|
489
490
|
this.printNextSteps(report.issues);
|
|
490
491
|
if (!options?.json) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { toRootOutput } from '../core/root-selection.js';
|
|
2
|
+
type ItemType = 'change' | 'spec';
|
|
1
3
|
interface ExecuteOptions {
|
|
2
4
|
all?: boolean;
|
|
3
5
|
changes?: boolean;
|
|
4
6
|
specs?: boolean;
|
|
5
7
|
archived?: boolean;
|
|
8
|
+
report?: string;
|
|
6
9
|
type?: string;
|
|
7
10
|
strict?: boolean;
|
|
8
11
|
json?: boolean;
|
|
@@ -12,6 +15,58 @@ interface ExecuteOptions {
|
|
|
12
15
|
store?: string;
|
|
13
16
|
storePath?: string;
|
|
14
17
|
}
|
|
18
|
+
interface BulkItemResult {
|
|
19
|
+
id: string;
|
|
20
|
+
type: ItemType;
|
|
21
|
+
valid: boolean;
|
|
22
|
+
issues: {
|
|
23
|
+
level: 'ERROR' | 'WARNING' | 'INFO';
|
|
24
|
+
path: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}[];
|
|
27
|
+
durationMs: number;
|
|
28
|
+
}
|
|
29
|
+
type BulkScope = 'all' | 'changes' | 'specs' | 'archived';
|
|
30
|
+
interface BulkValidationResult<T extends BulkItemResult = BulkItemResult> {
|
|
31
|
+
items: T[];
|
|
32
|
+
summary: {
|
|
33
|
+
totals: {
|
|
34
|
+
items: number;
|
|
35
|
+
passed: number;
|
|
36
|
+
failed: number;
|
|
37
|
+
};
|
|
38
|
+
byType: Partial<Record<ItemType, {
|
|
39
|
+
items: number;
|
|
40
|
+
passed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
}>>;
|
|
43
|
+
};
|
|
44
|
+
root: ReturnType<typeof toRootOutput>;
|
|
45
|
+
}
|
|
46
|
+
/** Findings are a distinct report, not a partial full-v1 items collection. */
|
|
47
|
+
export declare function projectValidationFindings<T extends BulkItemResult>(full: BulkValidationResult<T>, scope: BulkScope): {
|
|
48
|
+
report: {
|
|
49
|
+
kind: "validation-findings";
|
|
50
|
+
version: "1.0";
|
|
51
|
+
scope: BulkScope;
|
|
52
|
+
returnedItems: number;
|
|
53
|
+
totalItems: number;
|
|
54
|
+
};
|
|
55
|
+
itemFindings: T[];
|
|
56
|
+
summary: {
|
|
57
|
+
totals: {
|
|
58
|
+
items: number;
|
|
59
|
+
passed: number;
|
|
60
|
+
failed: number;
|
|
61
|
+
};
|
|
62
|
+
byType: Partial<Record<ItemType, {
|
|
63
|
+
items: number;
|
|
64
|
+
passed: number;
|
|
65
|
+
failed: number;
|
|
66
|
+
}>>;
|
|
67
|
+
};
|
|
68
|
+
root: import("../core/root-selection.js").RootOutput;
|
|
69
|
+
};
|
|
15
70
|
export declare class ValidateCommand {
|
|
16
71
|
execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
|
|
17
72
|
private normalizeType;
|
|
@@ -29,6 +84,8 @@ export declare class ValidateCommand {
|
|
|
29
84
|
private validateByType;
|
|
30
85
|
private printReport;
|
|
31
86
|
private printNextSteps;
|
|
87
|
+
private printFindingsReport;
|
|
88
|
+
private printBulkDetails;
|
|
32
89
|
private runBulkValidation;
|
|
33
90
|
/**
|
|
34
91
|
* Lists archived change ids from the resolved root's archive directory,
|
|
@@ -10,9 +10,54 @@ import { nearestMatches } from '../utils/match.js';
|
|
|
10
10
|
import { promises as fs } from 'fs';
|
|
11
11
|
import { getTaskProgressDetailForChange } from '../utils/task-progress.js';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
+
/** Findings are a distinct report, not a partial full-v1 items collection. */
|
|
14
|
+
export function projectValidationFindings(full, scope) {
|
|
15
|
+
const itemFindings = full.items.filter(item => item.issues.length > 0);
|
|
16
|
+
return {
|
|
17
|
+
report: {
|
|
18
|
+
kind: 'validation-findings',
|
|
19
|
+
version: '1.0',
|
|
20
|
+
scope,
|
|
21
|
+
returnedItems: itemFindings.length,
|
|
22
|
+
totalItems: full.summary.totals.items,
|
|
23
|
+
},
|
|
24
|
+
itemFindings,
|
|
25
|
+
summary: full.summary,
|
|
26
|
+
root: full.root,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
13
29
|
export class ValidateCommand {
|
|
14
30
|
async execute(itemName, options = {}) {
|
|
15
31
|
const bulk = options.all || options.changes || options.specs;
|
|
32
|
+
let findingsScope;
|
|
33
|
+
if (options.report !== undefined) {
|
|
34
|
+
const message = options.report !== 'full' && options.report !== 'findings'
|
|
35
|
+
? `Unknown validation report '${options.report}'.`
|
|
36
|
+
: itemName !== undefined
|
|
37
|
+
? 'A validation report cannot be combined with an item name.'
|
|
38
|
+
: options.archived && bulk
|
|
39
|
+
? 'A validation report cannot combine archived and active scopes.'
|
|
40
|
+
: !options.archived && !bulk
|
|
41
|
+
? 'A validation report requires an explicit bulk scope.'
|
|
42
|
+
: undefined;
|
|
43
|
+
if (message) {
|
|
44
|
+
const fix = 'Use --report full|findings with --all, --changes, --specs, or --archived, without an item name. Do not combine archived and active scopes.';
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify({ status: [{ severity: 'error', code: 'invalid_validation_report_request', message, fix }] }, null, 2));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error(`Error: ${message}`);
|
|
50
|
+
console.error(`Fix: ${fix}`);
|
|
51
|
+
}
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (options.report === 'findings') {
|
|
56
|
+
findingsScope = options.archived ? 'archived'
|
|
57
|
+
: options.all || (options.changes && options.specs) ? 'all'
|
|
58
|
+
: options.changes ? 'changes' : 'specs';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
16
61
|
const root = await resolveRootForCommand(options, {
|
|
17
62
|
json: options.json,
|
|
18
63
|
...(bulk ? { allowImplicitRoot: false } : {}),
|
|
@@ -29,6 +74,7 @@ export class ValidateCommand {
|
|
|
29
74
|
await this.runArchivedTaskValidation(root, {
|
|
30
75
|
json: !!options.json,
|
|
31
76
|
noInteractive: resolveNoInteractive(options),
|
|
77
|
+
findingsScope,
|
|
32
78
|
});
|
|
33
79
|
return;
|
|
34
80
|
}
|
|
@@ -37,7 +83,7 @@ export class ValidateCommand {
|
|
|
37
83
|
await this.runBulkValidation(root, {
|
|
38
84
|
changes: !!options.all || !!options.changes,
|
|
39
85
|
specs: !!options.all || !!options.specs,
|
|
40
|
-
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
|
|
86
|
+
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options), findingsScope });
|
|
41
87
|
return;
|
|
42
88
|
}
|
|
43
89
|
// No item and no flags
|
|
@@ -191,11 +237,12 @@ export class ValidateCommand {
|
|
|
191
237
|
}
|
|
192
238
|
else {
|
|
193
239
|
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
240
|
+
}
|
|
241
|
+
for (const issue of report.issues) {
|
|
242
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
243
|
+
console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
244
|
+
}
|
|
245
|
+
if (!report.valid) {
|
|
199
246
|
this.printNextSteps(type, id, root, report.issues);
|
|
200
247
|
}
|
|
201
248
|
}
|
|
@@ -228,6 +275,34 @@ export class ValidateCommand {
|
|
|
228
275
|
console.error('Next steps:');
|
|
229
276
|
bullets.forEach(b => console.error(` ${b}`));
|
|
230
277
|
}
|
|
278
|
+
printFindingsReport(full, scope, json, root) {
|
|
279
|
+
const findings = projectValidationFindings(full, scope);
|
|
280
|
+
if (json) {
|
|
281
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(`Scope: ${scope} (${findings.report.totalItems} items)`);
|
|
285
|
+
if (findings.itemFindings.length === 0) {
|
|
286
|
+
console.log('No item findings.');
|
|
287
|
+
}
|
|
288
|
+
for (const item of findings.itemFindings) {
|
|
289
|
+
console.error(`${item.type}/${item.id}`);
|
|
290
|
+
for (const issue of item.issues) {
|
|
291
|
+
console.error(` [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const totals = findings.summary.totals;
|
|
295
|
+
console.log(`Totals: ${totals.passed} passed, ${totals.failed} failed (${totals.items} items)`);
|
|
296
|
+
if (scope !== 'archived')
|
|
297
|
+
this.printBulkDetails(full.items, root);
|
|
298
|
+
}
|
|
299
|
+
printBulkDetails(results, root) {
|
|
300
|
+
const firstFailure = results.find((res) => !res.valid);
|
|
301
|
+
if (firstFailure) {
|
|
302
|
+
const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
|
|
303
|
+
console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
231
306
|
async runBulkValidation(root, scope, opts) {
|
|
232
307
|
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
233
308
|
const [changeIds, specIds] = await Promise.all([
|
|
@@ -269,7 +344,10 @@ export class ValidateCommand {
|
|
|
269
344
|
...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
270
345
|
},
|
|
271
346
|
};
|
|
272
|
-
if (opts.
|
|
347
|
+
if (opts.findingsScope) {
|
|
348
|
+
this.printFindingsReport({ items: [], summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
349
|
+
}
|
|
350
|
+
else if (opts.json) {
|
|
273
351
|
const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
|
|
274
352
|
console.log(JSON.stringify(out, null, 2));
|
|
275
353
|
}
|
|
@@ -326,7 +404,10 @@ export class ValidateCommand {
|
|
|
326
404
|
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
|
327
405
|
},
|
|
328
406
|
};
|
|
329
|
-
if (opts.
|
|
407
|
+
if (opts.findingsScope) {
|
|
408
|
+
this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
409
|
+
}
|
|
410
|
+
else if (opts.json) {
|
|
330
411
|
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
331
412
|
console.log(JSON.stringify(out, null, 2));
|
|
332
413
|
}
|
|
@@ -336,13 +417,13 @@ export class ValidateCommand {
|
|
|
336
417
|
console.log(`✓ ${res.type}/${res.id}`);
|
|
337
418
|
else
|
|
338
419
|
console.error(`✗ ${res.type}/${res.id}`);
|
|
420
|
+
for (const issue of res.issues) {
|
|
421
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
422
|
+
console.error(` ${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
423
|
+
}
|
|
339
424
|
}
|
|
340
425
|
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
341
|
-
|
|
342
|
-
if (firstFailure) {
|
|
343
|
-
const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
|
|
344
|
-
console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
|
|
345
|
-
}
|
|
426
|
+
this.printBulkDetails(results, root);
|
|
346
427
|
}
|
|
347
428
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
348
429
|
}
|
|
@@ -438,6 +519,11 @@ export class ValidateCommand {
|
|
|
438
519
|
totals: { items: results.length, passed, failed },
|
|
439
520
|
byType: { change: summarizeType(results, 'change') },
|
|
440
521
|
};
|
|
522
|
+
if (opts.findingsScope) {
|
|
523
|
+
this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
524
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
441
527
|
if (opts.json) {
|
|
442
528
|
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
443
529
|
console.log(JSON.stringify(out, null, 2));
|
|
@@ -8,6 +8,7 @@ import ora from 'ora';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputPath, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
|
+
import { isSpecsArtifactPath } from '../../core/artifact-graph/outputs.js';
|
|
11
12
|
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
12
13
|
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
13
14
|
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
@@ -15,6 +16,7 @@ import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
|
15
16
|
import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
|
|
16
17
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
17
18
|
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
19
|
+
import { METADATA_FILENAME } from '../../utils/change-metadata.js';
|
|
18
20
|
// -----------------------------------------------------------------------------
|
|
19
21
|
// Artifact Instructions Command
|
|
20
22
|
// -----------------------------------------------------------------------------
|
|
@@ -230,6 +232,99 @@ function toTaskItems(parsed) {
|
|
|
230
232
|
}
|
|
231
233
|
return tasks;
|
|
232
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The command that builds one artifact.
|
|
237
|
+
*
|
|
238
|
+
* Every earlier remedy here named the `openspec-continue-change` skill, which
|
|
239
|
+
* the `core` profile never installs - the advice was a dead end for the default
|
|
240
|
+
* install. The CLI verb exists on every profile and is what the skill runs.
|
|
241
|
+
*/
|
|
242
|
+
function describeArtifactRemedy(changeName, artifactId, options = {}) {
|
|
243
|
+
const target = artifactId ?? '<artifact>';
|
|
244
|
+
const verb = options.many ? 'Create each with' : 'Create it with';
|
|
245
|
+
return (`${verb} \`openspec instructions ${target} --change ${changeName}\`` +
|
|
246
|
+
` (\`openspec status --change ${changeName}\` shows what is left).`);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Finds the artifact a schema path is generated by, so a remedy can name it.
|
|
250
|
+
*/
|
|
251
|
+
function findArtifactIdFor(schema, generates) {
|
|
252
|
+
return schema.artifacts.find((artifact) => artifact.generates === generates)?.id;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Everything still to build before apply can run, in build order.
|
|
256
|
+
*
|
|
257
|
+
* Apply blocks on the schema's `apply.requires` alone, so its own list stops at
|
|
258
|
+
* the first hop: a change with only a proposal is told "Missing artifacts:
|
|
259
|
+
* tasks" while the specs `tasks` depends on are missing too. An agent that
|
|
260
|
+
* takes that literally writes the tracking file straight from the proposal and
|
|
261
|
+
* skips the artifacts in between - the failure reported in #834 and #869.
|
|
262
|
+
* Walking `requires` names the whole chain, the same set and order
|
|
263
|
+
* `openspec status` already prints, without changing what apply blocks on.
|
|
264
|
+
*/
|
|
265
|
+
function collectMissingPrerequisites(input) {
|
|
266
|
+
const { requiredArtifactIds, schema, buildOrder, completed } = input;
|
|
267
|
+
const byId = new Map(schema.artifacts.map((artifact) => [artifact.id, artifact]));
|
|
268
|
+
const missing = new Set();
|
|
269
|
+
const queue = [...requiredArtifactIds];
|
|
270
|
+
const seen = new Set(queue);
|
|
271
|
+
while (queue.length > 0) {
|
|
272
|
+
const id = queue.shift();
|
|
273
|
+
const artifact = byId.get(id);
|
|
274
|
+
if (!artifact)
|
|
275
|
+
continue;
|
|
276
|
+
if (!completed.has(id))
|
|
277
|
+
missing.add(id);
|
|
278
|
+
for (const dependency of artifact.requires) {
|
|
279
|
+
if (seen.has(dependency))
|
|
280
|
+
continue;
|
|
281
|
+
seen.add(dependency);
|
|
282
|
+
queue.push(dependency);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const order = new Map(buildOrder.map((id, index) => [id, index]));
|
|
286
|
+
return [...missing].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Warnings apply reports alongside its instruction.
|
|
290
|
+
*
|
|
291
|
+
* Apply gates on the schema's `apply.requires` only, so a change whose tasks
|
|
292
|
+
* file was written ahead of its specs reads as ready even though no delta spec
|
|
293
|
+
* exists - the state `openspec validate` rejects. Blocking here would be a
|
|
294
|
+
* policy change; naming the gap is not, and it is what keeps apply from being
|
|
295
|
+
* the one surface that green-lights a change every other surface flags.
|
|
296
|
+
*
|
|
297
|
+
* Only reported once apply is past its own gate: for a change that has not
|
|
298
|
+
* reached tasks yet, the missing specs are the next step rather than a warning.
|
|
299
|
+
* Schemas that declare no spec-producing artifact carry `skip_specs` from
|
|
300
|
+
* creation, so this never fires on them.
|
|
301
|
+
*/
|
|
302
|
+
function collectApplyWarnings(input) {
|
|
303
|
+
const { state, schema, changeDir, changeName, skippedArtifacts } = input;
|
|
304
|
+
if (state === 'blocked')
|
|
305
|
+
return [];
|
|
306
|
+
const specArtifacts = schema.artifacts.filter((artifact) => isSpecsArtifactPath(artifact.generates));
|
|
307
|
+
if (specArtifacts.length === 0)
|
|
308
|
+
return [];
|
|
309
|
+
if (specArtifacts.some((artifact) => skippedArtifacts?.has(artifact.id)))
|
|
310
|
+
return [];
|
|
311
|
+
const hasDeltas = specArtifacts.some((artifact) => resolveArtifactOutputs(changeDir, artifact.generates).length > 0);
|
|
312
|
+
if (hasDeltas)
|
|
313
|
+
return [];
|
|
314
|
+
const metadataPath = path.join(changeDir, METADATA_FILENAME);
|
|
315
|
+
// The command names the artifact this schema actually declares, never the
|
|
316
|
+
// literal `specs`. A schema whose spec-producing artifact is `contracts` was
|
|
317
|
+
// told to run `openspec instructions specs`, an artifact it does not have,
|
|
318
|
+
// so the warning dead-ended at the exact step meant to resolve it. With more
|
|
319
|
+
// than one such artifact there is no single right answer, so the id becomes
|
|
320
|
+
// a placeholder rather than a guess.
|
|
321
|
+
const specTarget = specArtifacts.length === 1 ? specArtifacts[0].id : '<artifact-id>';
|
|
322
|
+
return [
|
|
323
|
+
`This change has no delta specs and does not declare \`skip_specs: true\`, so \`openspec validate ${changeName}\` fails on it. ` +
|
|
324
|
+
`Write the delta specs before implementing (\`openspec instructions ${specTarget} --change ${changeName}\`), ` +
|
|
325
|
+
`or add \`skip_specs: true\` to ${metadataPath} if this change really changes no specified behavior.`,
|
|
326
|
+
];
|
|
327
|
+
}
|
|
233
328
|
/**
|
|
234
329
|
* Generates apply instructions for implementing tasks from a change.
|
|
235
330
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
@@ -267,6 +362,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
267
362
|
missingArtifacts.push(artifactId);
|
|
268
363
|
}
|
|
269
364
|
}
|
|
365
|
+
// Everything still to build, not just the first hop apply blocks on.
|
|
366
|
+
const missingPrerequisites = collectMissingPrerequisites({
|
|
367
|
+
requiredArtifactIds: [...requiredArtifactIds],
|
|
368
|
+
schema,
|
|
369
|
+
buildOrder: context.graph.getBuildOrder(),
|
|
370
|
+
completed: context.completed,
|
|
371
|
+
});
|
|
270
372
|
// Build context files from all existing artifacts in schema
|
|
271
373
|
const contextFiles = {};
|
|
272
374
|
for (const artifact of schema.artifacts) {
|
|
@@ -297,20 +399,33 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
297
399
|
let instruction;
|
|
298
400
|
if (missingArtifacts.length > 0) {
|
|
299
401
|
state = 'blocked';
|
|
300
|
-
|
|
402
|
+
const chain = missingPrerequisites.length > missingArtifacts.length
|
|
403
|
+
? `\nNot created yet, in build order: ${missingPrerequisites.join(', ')}.` +
|
|
404
|
+
` Build the ones this change needs before applying - the schema says which are conditional.`
|
|
405
|
+
: '';
|
|
406
|
+
instruction =
|
|
407
|
+
`Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.${chain}` +
|
|
408
|
+
`\n${describeArtifactRemedy(changeName,
|
|
409
|
+
// Only name one when one is left: the first of several would be the
|
|
410
|
+
// schema's conditional artifact as often as not.
|
|
411
|
+
missingPrerequisites.length === 1 ? missingPrerequisites[0] : undefined, { many: missingPrerequisites.length > 1 })}`;
|
|
301
412
|
}
|
|
302
413
|
else if (tracksFile && !tracksFileExists) {
|
|
303
414
|
// Tracking file configured but doesn't exist yet
|
|
304
415
|
const tracksFilename = path.basename(tracksFile);
|
|
305
416
|
state = 'blocked';
|
|
306
|
-
instruction =
|
|
417
|
+
instruction =
|
|
418
|
+
`The ${tracksFilename} file is missing and must be created.` +
|
|
419
|
+
`\n${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
307
420
|
}
|
|
308
421
|
else if (tracksFile && tracksFileExists && tasks.length === 0) {
|
|
309
422
|
// Tracking file exists but lists nothing an agent can work on: either no
|
|
310
423
|
// checkboxes at all, or only checkboxes with no text after them.
|
|
311
424
|
const tracksFilename = path.basename(tracksFile);
|
|
312
425
|
state = 'blocked';
|
|
313
|
-
instruction =
|
|
426
|
+
instruction =
|
|
427
|
+
`The ${tracksFilename} file exists but contains no tasks to work on.` +
|
|
428
|
+
`\nAdd tasks to ${tracksFilename}, or rebuild it: ${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
314
429
|
}
|
|
315
430
|
else if (tracksFile && remaining === 0 && total > 0) {
|
|
316
431
|
state = 'all_done';
|
|
@@ -325,6 +440,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
325
440
|
state = 'ready';
|
|
326
441
|
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
|
|
327
442
|
}
|
|
443
|
+
const warnings = collectApplyWarnings({
|
|
444
|
+
state,
|
|
445
|
+
schema,
|
|
446
|
+
changeDir,
|
|
447
|
+
changeName,
|
|
448
|
+
skippedArtifacts: context.skippedArtifacts,
|
|
449
|
+
});
|
|
328
450
|
return {
|
|
329
451
|
changeName,
|
|
330
452
|
changeDir,
|
|
@@ -334,6 +456,8 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
334
456
|
tasks,
|
|
335
457
|
state,
|
|
336
458
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
459
|
+
...(missingPrerequisites.length > 0 ? { missingPrerequisites } : {}),
|
|
460
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
337
461
|
instruction,
|
|
338
462
|
...(references !== undefined ? { references } : {}),
|
|
339
463
|
...operationInputs,
|
|
@@ -375,7 +499,7 @@ export async function applyInstructionsCommand(options) {
|
|
|
375
499
|
}
|
|
376
500
|
}
|
|
377
501
|
export function printApplyInstructionsText(instructions) {
|
|
378
|
-
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
|
|
502
|
+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, warnings, instruction } = instructions;
|
|
379
503
|
console.log(`## Apply: ${changeName}`);
|
|
380
504
|
console.log(`Schema: ${schemaName}`);
|
|
381
505
|
console.log();
|
|
@@ -388,7 +512,18 @@ export function printApplyInstructionsText(instructions) {
|
|
|
388
512
|
console.log('### ⚠️ Blocked');
|
|
389
513
|
console.log();
|
|
390
514
|
console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
|
|
391
|
-
|
|
515
|
+
if (instructions.missingPrerequisites &&
|
|
516
|
+
instructions.missingPrerequisites.length > missingArtifacts.length) {
|
|
517
|
+
console.log(`Not created yet, in build order: ${instructions.missingPrerequisites.join(', ')}`);
|
|
518
|
+
}
|
|
519
|
+
console.log();
|
|
520
|
+
}
|
|
521
|
+
if (warnings && warnings.length > 0) {
|
|
522
|
+
console.log('### ⚠️ Warnings');
|
|
523
|
+
console.log();
|
|
524
|
+
for (const warning of warnings) {
|
|
525
|
+
console.log(`- ${warning}`);
|
|
526
|
+
}
|
|
392
527
|
console.log();
|
|
393
528
|
}
|
|
394
529
|
// Context files (dynamically from schema)
|
|
@@ -30,6 +30,14 @@ export interface ApplyInstructions {
|
|
|
30
30
|
tasks: TaskItem[];
|
|
31
31
|
state: 'blocked' | 'all_done' | 'ready';
|
|
32
32
|
missingArtifacts?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Everything still to build before apply can run, in build order - the
|
|
35
|
+
* transitive closure of the schema's `apply.requires`, so it can be longer
|
|
36
|
+
* than `missingArtifacts`, which stops at the first hop apply blocks on.
|
|
37
|
+
*/
|
|
38
|
+
missingPrerequisites?: string[];
|
|
39
|
+
/** Non-blocking problems with the change, reported alongside the instruction. */
|
|
40
|
+
warnings?: string[];
|
|
33
41
|
instruction: string;
|
|
34
42
|
/** Referenced-store index (read-only upstream context; omitted when none declared) */
|
|
35
43
|
references?: ReferenceIndexEntry[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceCraft Code Assistant Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for the SourceCraft Code Assistant VS Code extension.
|
|
5
|
+
*
|
|
6
|
+
* @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* SourceCraft Code Assistant adapter for command generation.
|
|
11
|
+
* File path: .codeassistant/commands/opsx-<id>.md
|
|
12
|
+
* Format: YAML frontmatter with description
|
|
13
|
+
*/
|
|
14
|
+
export declare const codeassistantAdapter: ToolCommandAdapter;
|
|
15
|
+
//# sourceMappingURL=codeassistant.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceCraft Code Assistant Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for the SourceCraft Code Assistant VS Code extension.
|
|
5
|
+
*
|
|
6
|
+
* @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { escapeYamlValue } from '../yaml.js';
|
|
10
|
+
/**
|
|
11
|
+
* SourceCraft Code Assistant adapter for command generation.
|
|
12
|
+
* File path: .codeassistant/commands/opsx-<id>.md
|
|
13
|
+
* Format: YAML frontmatter with description
|
|
14
|
+
*/
|
|
15
|
+
export const codeassistantAdapter = {
|
|
16
|
+
toolId: 'codeassistant',
|
|
17
|
+
getFilePath(commandId) {
|
|
18
|
+
return path.join('.codeassistant', 'commands', `opsx-${commandId}.md`);
|
|
19
|
+
},
|
|
20
|
+
formatFile(content) {
|
|
21
|
+
return `---
|
|
22
|
+
description: ${escapeYamlValue(content.description)}
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
${content.body}
|
|
26
|
+
`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=codeassistant.js.map
|
|
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
|
|
|
26
26
|
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
27
27
|
export { opencodeAdapter } from './opencode.js';
|
|
28
28
|
export { piAdapter } from './pi.js';
|
|
29
|
+
export { codeassistantAdapter } from './codeassistant.js';
|
|
29
30
|
export { qoderAdapter } from './qoder.js';
|
|
30
31
|
export { lingmaAdapter } from './lingma.js';
|
|
31
32
|
export { qwenAdapter } from './qwen.js';
|
|
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
|
|
|
26
26
|
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
27
27
|
export { opencodeAdapter } from './opencode.js';
|
|
28
28
|
export { piAdapter } from './pi.js';
|
|
29
|
+
export { codeassistantAdapter } from './codeassistant.js';
|
|
29
30
|
export { qoderAdapter } from './qoder.js';
|
|
30
31
|
export { lingmaAdapter } from './lingma.js';
|
|
31
32
|
export { qwenAdapter } from './qwen.js';
|
|
@@ -27,6 +27,7 @@ import { kiroAdapter } from './adapters/kiro.js';
|
|
|
27
27
|
import { ohMyPiAdapter } from './adapters/oh-my-pi.js';
|
|
28
28
|
import { opencodeAdapter } from './adapters/opencode.js';
|
|
29
29
|
import { piAdapter } from './adapters/pi.js';
|
|
30
|
+
import { codeassistantAdapter } from './adapters/codeassistant.js';
|
|
30
31
|
import { qoderAdapter } from './adapters/qoder.js';
|
|
31
32
|
import { lingmaAdapter } from './adapters/lingma.js';
|
|
32
33
|
import { qwenAdapter } from './adapters/qwen.js';
|
|
@@ -63,6 +64,7 @@ export class CommandAdapterRegistry {
|
|
|
63
64
|
CommandAdapterRegistry.register(ohMyPiAdapter);
|
|
64
65
|
CommandAdapterRegistry.register(opencodeAdapter);
|
|
65
66
|
CommandAdapterRegistry.register(piAdapter);
|
|
67
|
+
CommandAdapterRegistry.register(codeassistantAdapter);
|
|
66
68
|
CommandAdapterRegistry.register(qoderAdapter);
|
|
67
69
|
CommandAdapterRegistry.register(lingmaAdapter);
|
|
68
70
|
CommandAdapterRegistry.register(qwenAdapter);
|
|
@@ -106,6 +106,12 @@ export const COMMAND_REGISTRY = [
|
|
|
106
106
|
name: 'archived',
|
|
107
107
|
description: 'Validate that archived changes have all tasks completed (for pre-commit linting)',
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'report',
|
|
111
|
+
description: 'Select bulk report content',
|
|
112
|
+
takesValue: true,
|
|
113
|
+
values: ['full', 'findings'],
|
|
114
|
+
},
|
|
109
115
|
COMMON_FLAGS.type,
|
|
110
116
|
COMMON_FLAGS.strict,
|
|
111
117
|
COMMON_FLAGS.jsonValidation,
|
package/dist/core/config.js
CHANGED
|
@@ -54,6 +54,7 @@ export const AI_TOOLS = [
|
|
|
54
54
|
{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
|
|
55
55
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
|
56
56
|
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
|
57
|
+
{ name: 'SourceCraft Code Assistant', value: 'codeassistant', available: true, successLabel: 'SourceCraft Code Assistant', skillsDir: '.codeassistant' },
|
|
57
58
|
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true },
|
|
58
59
|
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
|
59
60
|
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
|