@fission-ai/openspec 1.12.0 → 1.13.1
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 -15
- package/dist/commands/change.js +12 -1
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +161 -22
- package/dist/commands/feedback.js +6 -4
- package/dist/commands/schema.js +30 -9
- package/dist/commands/store.js +5 -2
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +63 -1
- package/dist/commands/workflow/instructions.js +170 -16
- package/dist/commands/workflow/new-change.js +20 -0
- package/dist/commands/workflow/shared.d.ts +8 -0
- package/dist/commands/workflow/shared.js +8 -0
- package/dist/commands/workflow/status.d.ts +5 -1
- package/dist/commands/workflow/status.js +42 -5
- package/dist/core/archive.js +17 -1
- package/dist/core/artifact-graph/schema.d.ts +14 -0
- package/dist/core/artifact-graph/schema.js +52 -0
- package/dist/core/artifact-graph/types.js +12 -1
- package/dist/core/change-status-policy.d.ts +14 -0
- package/dist/core/change-status-policy.js +14 -6
- package/dist/core/command-generation/generator.js +2 -0
- package/dist/core/completion-tip.js +7 -7
- package/dist/core/completions/installers/bash-installer.js +15 -7
- package/dist/core/completions/installers/shell-quote.d.ts +11 -0
- package/dist/core/completions/installers/shell-quote.js +13 -0
- package/dist/core/completions/installers/zsh-installer.js +3 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +27 -1
- package/dist/core/global-config.d.ts +30 -2
- package/dist/core/global-config.js +57 -3
- package/dist/core/init.js +33 -3
- package/dist/core/legacy-cleanup.d.ts +4 -0
- package/dist/core/legacy-cleanup.js +180 -12
- package/dist/core/list.js +55 -11
- package/dist/core/migration.js +6 -1
- package/dist/core/onboarding-commands.d.ts +11 -1
- package/dist/core/onboarding-commands.js +26 -0
- package/dist/core/parsers/change-parser.d.ts +12 -1
- package/dist/core/parsers/change-parser.js +85 -87
- package/dist/core/parsers/markdown-parser.js +4 -3
- package/dist/core/parsers/requirement-blocks.d.ts +44 -3
- package/dist/core/parsers/requirement-blocks.js +164 -43
- package/dist/core/parsers/requirement-text.d.ts +13 -2
- package/dist/core/parsers/requirement-text.js +34 -7
- package/dist/core/parsers/spec-structure.js +4 -1
- package/dist/core/project-config.d.ts +9 -1
- package/dist/core/project-config.js +11 -2
- package/dist/core/references.d.ts +20 -0
- package/dist/core/references.js +56 -0
- package/dist/core/shared/skill-content-equivalence.js +5 -1
- package/dist/core/shared/skill-generation.js +36 -8
- package/dist/core/shared/tool-detection.js +37 -4
- package/dist/core/specs-apply.js +260 -18
- package/dist/core/store/git.d.ts +33 -0
- package/dist/core/store/git.js +60 -6
- package/dist/core/store/operations.d.ts +1 -1
- package/dist/core/store/operations.js +32 -10
- package/dist/core/store/registry.d.ts +2 -1
- package/dist/core/store/registry.js +5 -1
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/workflows/apply-change.js +18 -3
- package/dist/core/templates/workflows/archive-change.js +50 -15
- package/dist/core/templates/workflows/bulk-archive-change.js +63 -13
- package/dist/core/templates/workflows/continue-change.js +14 -2
- package/dist/core/templates/workflows/explore.js +48 -14
- package/dist/core/templates/workflows/ff-change.js +19 -3
- package/dist/core/templates/workflows/new-change.js +15 -3
- package/dist/core/templates/workflows/onboard.js +67 -37
- package/dist/core/templates/workflows/project-root.d.ts +29 -0
- package/dist/core/templates/workflows/project-root.js +38 -0
- package/dist/core/templates/workflows/propose.js +52 -15
- package/dist/core/templates/workflows/sync-specs.js +40 -1
- package/dist/core/templates/workflows/update-change.js +52 -25
- package/dist/core/templates/workflows/verify-change.js +12 -3
- package/dist/core/update.d.ts +34 -0
- package/dist/core/update.js +109 -11
- package/dist/core/validation/task-checkboxes.d.ts +34 -0
- package/dist/core/validation/task-checkboxes.js +181 -0
- package/dist/core/validation/validator.d.ts +53 -1
- package/dist/core/validation/validator.js +154 -40
- package/dist/core/version-check.d.ts +14 -7
- package/dist/core/version-check.js +63 -17
- package/dist/core/worksets.js +6 -4
- package/dist/prompts/searchable-multi-select.d.ts +7 -0
- package/dist/prompts/searchable-multi-select.js +35 -6
- package/dist/telemetry/config.js +14 -2
- package/dist/telemetry/index.d.ts +4 -3
- package/dist/telemetry/index.js +22 -10
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +186 -0
- package/dist/utils/spec-discovery.d.ts +20 -0
- package/dist/utils/spec-discovery.js +59 -0
- package/dist/utils/task-progress.d.ts +1 -1
- package/dist/utils/task-progress.js +38 -6
- package/package.json +4 -4
- package/schemas/spec-driven/schema.yaml +24 -5
- package/schemas/spec-driven/templates/design.md +2 -0
- package/schemas/spec-driven/templates/proposal.md +2 -0
- package/schemas/spec-driven/templates/spec.md +2 -0
- package/schemas/spec-driven/templates/tasks.md +2 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional-Workflow Conditionals
|
|
3
|
+
*
|
|
4
|
+
* Not every workflow is installed. The `core` profile ships six of the twelve
|
|
5
|
+
* (`propose`, `explore`, `apply`, `update`, `sync`, `archive`), and a `custom`
|
|
6
|
+
* profile can ship any subset. A template that names `/opsx:continue` is
|
|
7
|
+
* therefore writing a dead reference for anyone whose profile omits it — the
|
|
8
|
+
* agent is told to hand off to a workflow that was never generated (#1734,
|
|
9
|
+
* umbrella #919).
|
|
10
|
+
*
|
|
11
|
+
* `command-references.ts` rewrites how a reference is spelled; this module
|
|
12
|
+
* decides whether it is emitted at all. Templates author both branches with
|
|
13
|
+
* `optionalWorkflow()`, and `resolveOptionalWorkflows()` picks one at
|
|
14
|
+
* generation time against the resolved workflow set, so the generated file
|
|
15
|
+
* states one path instead of asking the model to check availability at runtime.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Authors a passage whose wording depends on whether `workflowId` is installed.
|
|
19
|
+
*
|
|
20
|
+
* Both branches must read correctly on their own: the generated file contains
|
|
21
|
+
* exactly one of them, with no trace of the other.
|
|
22
|
+
*
|
|
23
|
+
* @param workflowId - Workflow id as it appears in ALL_WORKFLOWS (e.g. 'continue')
|
|
24
|
+
* @param whenInstalled - Text to emit when the workflow is part of the profile
|
|
25
|
+
* @param whenMissing - Text to emit otherwise, typically a CLI fallback
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* optionalWorkflow('continue', 'suggest `/opsx:continue`', 'run `openspec status`')
|
|
29
|
+
*/
|
|
30
|
+
export declare function optionalWorkflow(workflowId: string, whenInstalled: string, whenMissing: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* A passage that is dropped entirely when `workflowId` is not installed.
|
|
33
|
+
*
|
|
34
|
+
* Use for a line that only makes sense alongside the workflow it names — a
|
|
35
|
+
* command-reference table row, a bullet listing one workflow. When the
|
|
36
|
+
* conditional is the whole line, the line goes with it rather than leaving a
|
|
37
|
+
* blank one behind.
|
|
38
|
+
*
|
|
39
|
+
* @param workflowId - Workflow id as it appears in ALL_WORKFLOWS
|
|
40
|
+
* @param whenInstalled - Text to emit when the workflow is part of the profile
|
|
41
|
+
*/
|
|
42
|
+
export declare function onlyWithWorkflow(workflowId: string, whenInstalled: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves every `optionalWorkflow()` passage in `text` against the workflows
|
|
45
|
+
* that will actually be installed.
|
|
46
|
+
*
|
|
47
|
+
* Runs before the command-reference transformers, so a reference in a branch
|
|
48
|
+
* that was dropped never reaches them.
|
|
49
|
+
*
|
|
50
|
+
* @param text - Template body, possibly containing conditionals
|
|
51
|
+
* @param installedWorkflows - The resolved workflow set for this installation
|
|
52
|
+
* @returns The body with one branch of each conditional kept
|
|
53
|
+
* @throws If a malformed conditional leaves a marker in the output
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveOptionalWorkflows(text: string, installedWorkflows: ReadonlySet<string>): string;
|
|
56
|
+
/**
|
|
57
|
+
* Fails loudly if `text` still carries a conditional marker.
|
|
58
|
+
*
|
|
59
|
+
* Called at the end of resolution to catch a malformed block, and again at the
|
|
60
|
+
* points that write a generated file — so a body that skipped resolution
|
|
61
|
+
* altogether (a generation path that bypassed getSkillTemplates /
|
|
62
|
+
* getCommandTemplates) throws instead of shipping literal markers to a user.
|
|
63
|
+
*
|
|
64
|
+
* @param text - Text about to be written, or just resolved
|
|
65
|
+
* @param reason - What went wrong, used as the message prefix
|
|
66
|
+
* @throws If any `[[opsx:...]]` marker remains
|
|
67
|
+
*/
|
|
68
|
+
export declare function assertWorkflowConditionalsResolved(text: string, reason: string): void;
|
|
69
|
+
//# sourceMappingURL=optional-workflow.d.ts.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional-Workflow Conditionals
|
|
3
|
+
*
|
|
4
|
+
* Not every workflow is installed. The `core` profile ships six of the twelve
|
|
5
|
+
* (`propose`, `explore`, `apply`, `update`, `sync`, `archive`), and a `custom`
|
|
6
|
+
* profile can ship any subset. A template that names `/opsx:continue` is
|
|
7
|
+
* therefore writing a dead reference for anyone whose profile omits it — the
|
|
8
|
+
* agent is told to hand off to a workflow that was never generated (#1734,
|
|
9
|
+
* umbrella #919).
|
|
10
|
+
*
|
|
11
|
+
* `command-references.ts` rewrites how a reference is spelled; this module
|
|
12
|
+
* decides whether it is emitted at all. Templates author both branches with
|
|
13
|
+
* `optionalWorkflow()`, and `resolveOptionalWorkflows()` picks one at
|
|
14
|
+
* generation time against the resolved workflow set, so the generated file
|
|
15
|
+
* states one path instead of asking the model to check availability at runtime.
|
|
16
|
+
*/
|
|
17
|
+
const OPEN = '[[opsx:if-workflow ';
|
|
18
|
+
const OPEN_END = ']]';
|
|
19
|
+
const ELSE = '[[opsx:else]]';
|
|
20
|
+
const END = '[[opsx:end]]';
|
|
21
|
+
/**
|
|
22
|
+
* A conditional that occupies a whole line on its own. Matched first so a
|
|
23
|
+
* branch that resolves to empty takes its line with it — otherwise dropping a
|
|
24
|
+
* table row or a bullet would leave a blank line behind, which markdown reads
|
|
25
|
+
* as the end of the table or list.
|
|
26
|
+
*/
|
|
27
|
+
const WHOLE_LINE_PATTERN = /^([ \t]*)\[\[opsx:if-workflow ([a-z-]+)\]\]([^\n]*?)\[\[opsx:else\]\]([^\n]*?)\[\[opsx:end\]\][ \t]*\r?\n/gm;
|
|
28
|
+
const CONDITIONAL_PATTERN = /\[\[opsx:if-workflow ([a-z-]+)\]\]([\s\S]*?)\[\[opsx:else\]\]([\s\S]*?)\[\[opsx:end\]\]/g;
|
|
29
|
+
/** Any leftover marker, used to fail loudly on malformed authoring. */
|
|
30
|
+
const RESIDUAL_MARKER_PATTERN = /\[\[opsx:(if-workflow|else|end)/;
|
|
31
|
+
/** A single well-formed marker, in any position. */
|
|
32
|
+
const MARKER_PATTERN = /\[\[opsx:(?:if-workflow [a-z-]+|else|end)\]\]/g;
|
|
33
|
+
/** Anything that opens like a marker, well-formed or not. */
|
|
34
|
+
const MARKER_LIKE_PATTERN = /\[\[opsx:/;
|
|
35
|
+
/**
|
|
36
|
+
* Rejects a malformed conditional before any branch is chosen.
|
|
37
|
+
*
|
|
38
|
+
* Checking after resolution is not enough: the unselected branch is discarded
|
|
39
|
+
* first, so a truncated block inside it would pass for one profile and throw
|
|
40
|
+
* for another — the exact profile-dependent behavior this module exists to
|
|
41
|
+
* remove. Authoring is either valid for every profile or valid for none.
|
|
42
|
+
*
|
|
43
|
+
* @param text - Template body as authored
|
|
44
|
+
* @throws If a marker is unrecognized, or the blocks are not a flat sequence
|
|
45
|
+
* of if / else / end
|
|
46
|
+
*/
|
|
47
|
+
function assertConditionalsWellFormed(text) {
|
|
48
|
+
const kinds = [];
|
|
49
|
+
const withoutMarkers = text.replace(MARKER_PATTERN, (marker) => {
|
|
50
|
+
kinds.push(marker.startsWith(OPEN) ? 'if' : marker === ELSE ? 'else' : 'end');
|
|
51
|
+
return '';
|
|
52
|
+
});
|
|
53
|
+
const unrecognized = MARKER_LIKE_PATTERN.exec(withoutMarkers);
|
|
54
|
+
if (unrecognized) {
|
|
55
|
+
throw new Error(`Malformed optional-workflow conditional: unrecognized marker at '${withoutMarkers
|
|
56
|
+
.slice(unrecognized.index, unrecognized.index + 40)
|
|
57
|
+
.split('\n')[0]}'. Markers are [[opsx:if-workflow <id>]], [[opsx:else]] and [[opsx:end]].`);
|
|
58
|
+
}
|
|
59
|
+
for (let i = 0; i < kinds.length; i += 3) {
|
|
60
|
+
if (kinds[i] !== 'if' || kinds[i + 1] !== 'else' || kinds[i + 2] !== 'end') {
|
|
61
|
+
throw new Error('Malformed optional-workflow conditional: markers are out of order or a ' +
|
|
62
|
+
'block is incomplete. Each block needs the full [[opsx:if-workflow <id>]] ' +
|
|
63
|
+
'... [[opsx:else]] ... [[opsx:end]] form, and blocks cannot nest.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Authors a passage whose wording depends on whether `workflowId` is installed.
|
|
69
|
+
*
|
|
70
|
+
* Both branches must read correctly on their own: the generated file contains
|
|
71
|
+
* exactly one of them, with no trace of the other.
|
|
72
|
+
*
|
|
73
|
+
* @param workflowId - Workflow id as it appears in ALL_WORKFLOWS (e.g. 'continue')
|
|
74
|
+
* @param whenInstalled - Text to emit when the workflow is part of the profile
|
|
75
|
+
* @param whenMissing - Text to emit otherwise, typically a CLI fallback
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* optionalWorkflow('continue', 'suggest `/opsx:continue`', 'run `openspec status`')
|
|
79
|
+
*/
|
|
80
|
+
export function optionalWorkflow(workflowId, whenInstalled, whenMissing) {
|
|
81
|
+
return `${OPEN}${workflowId}${OPEN_END}${whenInstalled}${ELSE}${whenMissing}${END}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A passage that is dropped entirely when `workflowId` is not installed.
|
|
85
|
+
*
|
|
86
|
+
* Use for a line that only makes sense alongside the workflow it names — a
|
|
87
|
+
* command-reference table row, a bullet listing one workflow. When the
|
|
88
|
+
* conditional is the whole line, the line goes with it rather than leaving a
|
|
89
|
+
* blank one behind.
|
|
90
|
+
*
|
|
91
|
+
* @param workflowId - Workflow id as it appears in ALL_WORKFLOWS
|
|
92
|
+
* @param whenInstalled - Text to emit when the workflow is part of the profile
|
|
93
|
+
*/
|
|
94
|
+
export function onlyWithWorkflow(workflowId, whenInstalled) {
|
|
95
|
+
return optionalWorkflow(workflowId, whenInstalled, '');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolves every `optionalWorkflow()` passage in `text` against the workflows
|
|
99
|
+
* that will actually be installed.
|
|
100
|
+
*
|
|
101
|
+
* Runs before the command-reference transformers, so a reference in a branch
|
|
102
|
+
* that was dropped never reaches them.
|
|
103
|
+
*
|
|
104
|
+
* @param text - Template body, possibly containing conditionals
|
|
105
|
+
* @param installedWorkflows - The resolved workflow set for this installation
|
|
106
|
+
* @returns The body with one branch of each conditional kept
|
|
107
|
+
* @throws If a malformed conditional leaves a marker in the output
|
|
108
|
+
*/
|
|
109
|
+
export function resolveOptionalWorkflows(text, installedWorkflows) {
|
|
110
|
+
assertConditionalsWellFormed(text);
|
|
111
|
+
const wholeLinesResolved = text.replace(WHOLE_LINE_PATTERN, (_match, indent, workflowId, whenInstalled, whenMissing) => {
|
|
112
|
+
const chosen = installedWorkflows.has(workflowId) ? whenInstalled : whenMissing;
|
|
113
|
+
return chosen === '' ? '' : `${indent}${chosen}\n`;
|
|
114
|
+
});
|
|
115
|
+
const resolved = wholeLinesResolved.replace(CONDITIONAL_PATTERN, (_match, workflowId, whenInstalled, whenMissing) => installedWorkflows.has(workflowId) ? whenInstalled : whenMissing);
|
|
116
|
+
assertWorkflowConditionalsResolved(resolved, 'Malformed optional-workflow conditional');
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fails loudly if `text` still carries a conditional marker.
|
|
121
|
+
*
|
|
122
|
+
* Called at the end of resolution to catch a malformed block, and again at the
|
|
123
|
+
* points that write a generated file — so a body that skipped resolution
|
|
124
|
+
* altogether (a generation path that bypassed getSkillTemplates /
|
|
125
|
+
* getCommandTemplates) throws instead of shipping literal markers to a user.
|
|
126
|
+
*
|
|
127
|
+
* @param text - Text about to be written, or just resolved
|
|
128
|
+
* @param reason - What went wrong, used as the message prefix
|
|
129
|
+
* @throws If any `[[opsx:...]]` marker remains
|
|
130
|
+
*/
|
|
131
|
+
export function assertWorkflowConditionalsResolved(text, reason) {
|
|
132
|
+
const residual = RESIDUAL_MARKER_PATTERN.exec(text);
|
|
133
|
+
if (residual) {
|
|
134
|
+
throw new Error(`${reason}: '${residual[0]}' is unresolved. Optional-workflow blocks are ` +
|
|
135
|
+
'resolved by getSkillTemplates()/getCommandTemplates() against the installed ' +
|
|
136
|
+
'workflow set, and each needs the full [[opsx:if-workflow <id>]] ... ' +
|
|
137
|
+
'[[opsx:else]] ... [[opsx:end]] form.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=optional-workflow.js.map
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { optionalWorkflow } from '../optional-workflow.js';
|
|
1
2
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
3
|
+
import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
4
|
+
/**
|
|
5
|
+
* `/opsx:continue` is not in the `core` profile, so the blocked-state handoff
|
|
6
|
+
* is authored with a CLI fallback and resolved at generation time (see
|
|
7
|
+
* optional-workflow.ts).
|
|
8
|
+
*/
|
|
9
|
+
const BLOCKED_STATE_HANDOFF = optionalWorkflow('continue', 'suggest using `/opsx:continue` to create them.', 'suggest completing the missing artifacts. Run `openspec status --change "<name>" --json`, select the next `ready` artifact (not `skipped` or `blocked`), and use `openspec instructions "<artifact-id>" --change "<name>" --json` for its rules and template. Keep the selected `--store <id>` on both commands.');
|
|
10
|
+
/** The archive handoff shown once every task is done. */
|
|
11
|
+
const ARCHIVE_HANDOFF = optionalWorkflow('archive', 'You can archive this change with `/opsx:archive`.', 'You can archive this change by running `openspec archive "<name>"`.');
|
|
2
12
|
/**
|
|
3
13
|
* The apply workflow instructions, authored once and rendered by both the
|
|
4
14
|
* skill and command surfaces. The surfaces are intentionally distinct, but
|
|
@@ -13,6 +23,8 @@ export function getApplyInstructions() {
|
|
|
13
23
|
|
|
14
24
|
${STORE_SELECTION_GUIDANCE}
|
|
15
25
|
|
|
26
|
+
${PROJECT_ROOT_GUARD}
|
|
27
|
+
|
|
16
28
|
**Input**: Optionally specify a change name (e.g., \`/opsx:apply add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
17
29
|
|
|
18
30
|
**Steps**
|
|
@@ -48,9 +60,12 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
48
60
|
- Dynamic instruction based on current state
|
|
49
61
|
- Optional \`context\`: current required project instruction input from the selected root
|
|
50
62
|
- Optional \`operationGuidance\`: current advisory guidance for apply
|
|
63
|
+
- \`missingArtifacts\` (when present): required artifact ids with no output
|
|
51
64
|
|
|
52
65
|
**Handle states:**
|
|
53
|
-
- If \`state: "blocked"
|
|
66
|
+
- If \`state: "blocked"\`: show the message and pause implementation.
|
|
67
|
+
- If \`missingArtifacts\` is non-empty: ${BLOCKED_STATE_HANDOFF}
|
|
68
|
+
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
|
|
54
69
|
- If \`state: "all_done"\`: congratulate, suggest archive
|
|
55
70
|
- Otherwise: proceed to implementation
|
|
56
71
|
|
|
@@ -139,7 +154,7 @@ Working on task 4/7: <task description>
|
|
|
139
154
|
- [x] Task 2
|
|
140
155
|
...
|
|
141
156
|
|
|
142
|
-
All tasks complete!
|
|
157
|
+
All tasks complete! ${ARCHIVE_HANDOFF}
|
|
143
158
|
\`\`\`
|
|
144
159
|
|
|
145
160
|
**Output On Pause (Issue Encountered)**
|
|
@@ -189,7 +204,7 @@ This skill supports the "actions on a change" model:
|
|
|
189
204
|
export function getApplyChangeSkillTemplate() {
|
|
190
205
|
return {
|
|
191
206
|
name: 'openspec-apply-change',
|
|
192
|
-
description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.',
|
|
207
|
+
description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. Also use when the user says "openspec apply", "opsx apply", or "openspec implement".',
|
|
193
208
|
instructions: getApplyInstructions(),
|
|
194
209
|
license: 'MIT',
|
|
195
210
|
compatibility: 'Requires openspec CLI.',
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import { optionalWorkflow } from '../optional-workflow.js';
|
|
1
2
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
3
|
+
import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
4
|
+
/**
|
|
5
|
+
* Archiving must merge delta specs into the main specs; the `sync` workflow is
|
|
6
|
+
* how it normally does that. A profile that selects `archive` gets `sync`
|
|
7
|
+
* injected (see getProfileWorkflows), but an install whose workflow set was
|
|
8
|
+
* read back off disk can still be missing it — in which case the merge has to
|
|
9
|
+
* happen inline rather than be handed to a workflow that is not there.
|
|
10
|
+
*/
|
|
11
|
+
const SYNC_INLINE_HANDOFF = optionalWorkflow('sync', 'run the `/opsx:sync` workflow inline (agent-driven intelligent merge)', 'perform the delta-to-main-spec merge inline yourself (agent-driven intelligent merge)');
|
|
12
|
+
const SYNC_GUARDRAIL = optionalWorkflow('sync', 'run the `/opsx:sync` workflow inline (agent-driven)', 'perform the delta-to-main-spec merge inline (agent-driven)');
|
|
2
13
|
export function getArchiveChangeSkillTemplate() {
|
|
3
14
|
return {
|
|
4
15
|
name: 'openspec-archive-change',
|
|
5
|
-
description: 'Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.',
|
|
16
|
+
description: 'Archive a completed OpenSpec change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. Also use when the user says "openspec archive" or "opsx archive".',
|
|
6
17
|
instructions: `Archive a completed change in the experimental workflow.
|
|
7
18
|
|
|
8
19
|
${STORE_SELECTION_GUIDANCE}
|
|
9
20
|
|
|
21
|
+
${PROJECT_ROOT_GUARD}
|
|
22
|
+
|
|
10
23
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
11
24
|
|
|
12
25
|
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
@@ -70,7 +83,11 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
70
83
|
|
|
71
84
|
Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks.
|
|
72
85
|
|
|
73
|
-
|
|
86
|
+
A checkbox is complete when its only content is \`x\` or \`X\`; spacing inside
|
|
87
|
+
the brackets does not matter, so \`- [ x]\` counts as complete too. Every
|
|
88
|
+
other marker is incomplete - \`- [ ]\`, an empty \`- []\`, and markers OpenSpec
|
|
89
|
+
assigns no meaning to such as \`- [~]\` or \`- [-]\`. Never read an unfamiliar
|
|
90
|
+
marker as complete.
|
|
74
91
|
|
|
75
92
|
**If incomplete tasks found:**
|
|
76
93
|
- Display warning showing count of incomplete tasks
|
|
@@ -88,17 +105,23 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
88
105
|
|
|
89
106
|
**If delta specs exist:**
|
|
90
107
|
- Compare each delta spec with its corresponding main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path)
|
|
108
|
+
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
|
|
109
|
+
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
|
|
110
|
+
- Otherwise, if the delta has only REMOVED requirements and the change's \`.openspec.yaml\` declares \`retire_capabilities: true\`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
|
|
111
|
+
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. \`openspec archive\` refuses the unmarked REMOVED-only case with \`Spec must have at least one requirement\`.
|
|
112
|
+
- Otherwise, count the capability as needing sync and name it in the summary (\`<capability-path>: new main spec will be created\`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as \`openspec archive\` does.
|
|
91
113
|
- Determine what changes would be applied (adds, modifications, removals, renames)
|
|
92
|
-
- Show a combined summary before prompting
|
|
114
|
+
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.
|
|
93
115
|
|
|
94
116
|
**Prompt options:**
|
|
95
|
-
- If
|
|
96
|
-
-
|
|
117
|
+
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
|
|
118
|
+
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
|
|
119
|
+
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"
|
|
97
120
|
|
|
98
121
|
Route on the answer:
|
|
99
122
|
- "Cancel" — stop, do not archive
|
|
100
123
|
- "Archive without syncing" or "Archive now" — proceed to archive
|
|
101
|
-
- "Sync now" or "Sync anyway" — sync, then verify (below)
|
|
124
|
+
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
|
|
102
125
|
- Anything else — ask again rather than archiving
|
|
103
126
|
|
|
104
127
|
Before a selected sync writes any main spec, run
|
|
@@ -112,7 +135,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
112
135
|
|
|
113
136
|
Then run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching \`specs\` instructions again. Do not delegate it to a background task — step 5 would move \`changeRoot\` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
|
|
114
137
|
|
|
115
|
-
Then re-run the comparison from the top of this step against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
|
|
138
|
+
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
|
|
116
139
|
- ADDED requirements present
|
|
117
140
|
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
|
|
118
141
|
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving \`## Requirements\` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match
|
|
@@ -188,6 +211,8 @@ export function getOpsxArchiveCommandTemplate() {
|
|
|
188
211
|
|
|
189
212
|
${STORE_SELECTION_GUIDANCE}
|
|
190
213
|
|
|
214
|
+
${PROJECT_ROOT_GUARD}
|
|
215
|
+
|
|
191
216
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
192
217
|
|
|
193
218
|
**Input**: Optionally specify a change name after \`/opsx:archive\` (e.g., \`/opsx:archive add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
@@ -251,7 +276,11 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
251
276
|
|
|
252
277
|
Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks.
|
|
253
278
|
|
|
254
|
-
|
|
279
|
+
A checkbox is complete when its only content is \`x\` or \`X\`; spacing inside
|
|
280
|
+
the brackets does not matter, so \`- [ x]\` counts as complete too. Every
|
|
281
|
+
other marker is incomplete - \`- [ ]\`, an empty \`- []\`, and markers OpenSpec
|
|
282
|
+
assigns no meaning to such as \`- [~]\` or \`- [-]\`. Never read an unfamiliar
|
|
283
|
+
marker as complete.
|
|
255
284
|
|
|
256
285
|
**If incomplete tasks found:**
|
|
257
286
|
- Display warning showing count of incomplete tasks
|
|
@@ -269,17 +298,23 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
269
298
|
|
|
270
299
|
**If delta specs exist:**
|
|
271
300
|
- Compare each delta spec with its corresponding main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path)
|
|
301
|
+
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
|
|
302
|
+
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
|
|
303
|
+
- Otherwise, if the delta has only REMOVED requirements and the change's \`.openspec.yaml\` declares \`retire_capabilities: true\`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
|
|
304
|
+
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. \`openspec archive\` refuses the unmarked REMOVED-only case with \`Spec must have at least one requirement\`.
|
|
305
|
+
- Otherwise, count the capability as needing sync and name it in the summary (\`<capability-path>: new main spec will be created\`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as \`openspec archive\` does.
|
|
272
306
|
- Determine what changes would be applied (adds, modifications, removals, renames)
|
|
273
|
-
- Show a combined summary before prompting
|
|
307
|
+
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.
|
|
274
308
|
|
|
275
309
|
**Prompt options:**
|
|
276
|
-
- If
|
|
277
|
-
-
|
|
310
|
+
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
|
|
311
|
+
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
|
|
312
|
+
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"
|
|
278
313
|
|
|
279
314
|
Route on the answer:
|
|
280
315
|
- "Cancel" — stop, do not archive
|
|
281
316
|
- "Archive without syncing" or "Archive now" — proceed to archive
|
|
282
|
-
- "Sync now" or "Sync anyway" — sync, then verify (below)
|
|
317
|
+
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
|
|
283
318
|
- Anything else — ask again rather than archiving
|
|
284
319
|
|
|
285
320
|
Before a selected sync writes any main spec, run
|
|
@@ -291,9 +326,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
291
326
|
form of main specs produced by this merge; do not use them as archive guidance,
|
|
292
327
|
change CLI behavior, or copy the rule text into any output file.
|
|
293
328
|
|
|
294
|
-
Then
|
|
329
|
+
Then ${SYNC_INLINE_HANDOFF} for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching \`specs\` instructions again. Do not delegate it to a background task — step 5 would move \`changeRoot\` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
|
|
295
330
|
|
|
296
|
-
Then re-run the comparison from the top of this step against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
|
|
331
|
+
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
|
|
297
332
|
- ADDED requirements present
|
|
298
333
|
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
|
|
299
334
|
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving \`## Requirements\` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match
|
|
@@ -393,7 +428,7 @@ Target archive directory already exists.
|
|
|
393
428
|
- Don't block archive on warnings - just inform and confirm
|
|
394
429
|
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
|
395
430
|
- Show clear summary of what happened
|
|
396
|
-
- If sync is requested,
|
|
431
|
+
- If sync is requested, ${SYNC_GUARDRAIL}
|
|
397
432
|
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving \`changeRoot\`
|
|
398
433
|
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
|
399
434
|
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
import { optionalWorkflow } from '../optional-workflow.js';
|
|
1
2
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
3
|
+
import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
4
|
+
/**
|
|
5
|
+
* Archiving must merge delta specs into the main specs; the `sync` workflow is
|
|
6
|
+
* how it normally does that. A profile that selects `archive` gets `sync`
|
|
7
|
+
* injected (see getProfileWorkflows), but an install whose workflow set was
|
|
8
|
+
* read back off disk can still be missing it — in which case the merge has to
|
|
9
|
+
* happen inline rather than be handed to a workflow that is not there.
|
|
10
|
+
*/
|
|
11
|
+
const SYNC_INLINE_HANDOFF = optionalWorkflow('sync', 'Run the `/opsx:sync` workflow inline (agent-driven intelligent merge)', 'Perform the delta-to-main-spec merge inline yourself (agent-driven intelligent merge)');
|
|
12
|
+
const SYNC_GUARDRAIL = optionalWorkflow('sync', 'run the `/opsx:sync` workflow inline (agent-driven)', 'perform the delta-to-main-spec merge inline (agent-driven)');
|
|
2
13
|
export function getBulkArchiveChangeSkillTemplate() {
|
|
3
14
|
return {
|
|
4
15
|
name: 'openspec-bulk-archive-change',
|
|
5
|
-
description: 'Archive multiple completed changes at once. Use when archiving several parallel changes.',
|
|
16
|
+
description: 'Archive multiple completed OpenSpec changes at once. Use when archiving several parallel changes. Also use for a plural archive request - "openspec bulk-archive", "opsx bulk-archive", "openspec archive all", or "openspec archive these changes".',
|
|
6
17
|
instructions: `Archive multiple completed changes in a single operation.
|
|
7
18
|
|
|
8
19
|
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
|
9
20
|
|
|
10
21
|
${STORE_SELECTION_GUIDANCE}
|
|
11
22
|
|
|
23
|
+
${PROJECT_ROOT_GUARD}
|
|
24
|
+
|
|
12
25
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
13
26
|
|
|
14
27
|
**Input**: None required (prompts for selection)
|
|
@@ -64,7 +77,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
64
77
|
- Note which artifacts are \`done\` vs other states
|
|
65
78
|
|
|
66
79
|
b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON
|
|
67
|
-
-
|
|
80
|
+
- Complete means the checkbox holds only \`x\`/\`X\`, ignoring spacing
|
|
81
|
+
(\`- [ x]\` is complete); every other marker is incomplete (\`- [ ]\`,
|
|
82
|
+
\`- []\`, and unfamiliar ones such as \`- [~]\` or \`- [-]\`)
|
|
68
83
|
- If no tasks file exists, note as "No tasks"
|
|
69
84
|
|
|
70
85
|
c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON
|
|
@@ -75,6 +90,14 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
75
90
|
lookup for that change; do not infer deltas from unrelated artifacts.
|
|
76
91
|
- Evaluate this independently for every change, including mixed-schema
|
|
77
92
|
batches where some schemas have no \`specs\` artifact.
|
|
93
|
+
|
|
94
|
+
d. **Archive target** - Compute each change's target name once and record it as that change's \`<target-name>\`
|
|
95
|
+
- Use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-<name>\` (same rule as \`openspec archive\`)
|
|
96
|
+
- Check whether \`<planningHome.changesDir>/archive/<target-name>\` already exists
|
|
97
|
+
- If it exists, or another selected change resolves to the same target name, mark every such change \`Blocked\` with \`Archive directory already exists\`
|
|
98
|
+
- A blocked change is never synced or moved: show it as \`Blocked\` in the step 6 table, leave it out of conflict resolution (resolve its conflicts using only the other changes), and record it as Failed in step 8d
|
|
99
|
+
- Checking here, before any main spec is written, matches \`openspec archive\`: a collision found after sync would leave main specs rewritten for an archive that never happened
|
|
100
|
+
|
|
78
101
|
4. **Detect spec conflicts**
|
|
79
102
|
|
|
80
103
|
Build a map keyed by \`<capability-path>\`, the exact path relative to \`specs/\`:
|
|
@@ -147,8 +170,8 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
147
170
|
Route on the answer by intent, not by exact label — you wrote these labels,
|
|
148
171
|
so match what the user picked rather than the wording above:
|
|
149
172
|
- "Cancel" — stop, do not archive. Report that nothing was archived and skip the remaining steps.
|
|
150
|
-
- The archive-everything option — proceed with every selected change
|
|
151
|
-
- The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d
|
|
173
|
+
- The archive-everything option — proceed with every selected change that is not \`Blocked\`
|
|
174
|
+
- The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d, except \`Blocked\` changes, which stay Failed with \`Archive directory already exists\`. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived.
|
|
152
175
|
- Anything else — ask again rather than archiving
|
|
153
176
|
|
|
154
177
|
Before step 8 writes the first main spec or moves any change, fetch every
|
|
@@ -193,13 +216,20 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
193
216
|
|
|
194
217
|
c. **Perform the archive**:
|
|
195
218
|
|
|
196
|
-
Target name: use the change
|
|
219
|
+
Target name: use the \`<target-name>\` recorded for this change in step 3d, unchanged. Never recompute it here: a batch that runs past midnight would check one date in step 3 and move to another.
|
|
220
|
+
|
|
221
|
+
**Check if target already exists:**
|
|
222
|
+
- Check again immediately before the move, even though step 3 already checked: the target can appear mid-batch
|
|
223
|
+
- If yes: record this change as Failed with \`Archive directory already exists\`, leave \`changeRoot\` where it is, report any main specs step 8a already synced for it, and continue with the remaining changes
|
|
224
|
+
- If no: move \`changeRoot\` to the archive directory
|
|
197
225
|
|
|
198
226
|
\`\`\`bash
|
|
199
227
|
mkdir -p "<planningHome.changesDir>/archive"
|
|
200
228
|
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
|
|
201
229
|
\`\`\`
|
|
202
230
|
|
|
231
|
+
**Confirm the move did not nest:** \`mv\` exits 0 even when the target appeared after the check, moving the change *inside* it. If \`<planningHome.changesDir>/archive/<target-name>/<change-directory-name>\` now exists (the last path segment of \`changeRoot\`), move that directory back to \`changeRoot\` and record this change as Failed with \`Archive directory already exists\`. Never report it as archived.
|
|
232
|
+
|
|
203
233
|
d. **Track outcome** for each change:
|
|
204
234
|
- Success: archived successfully
|
|
205
235
|
- Failed: error during archive or spec verification (record error)
|
|
@@ -314,8 +344,9 @@ No active changes found. Create a new change to get started.
|
|
|
314
344
|
- Never archive after the user cancels the confirmation — a cancelled batch archives nothing
|
|
315
345
|
- Track and report all outcomes (success/skip/fail)
|
|
316
346
|
- Preserve .openspec.yaml when moving to archive
|
|
317
|
-
- Archive directory target uses current date: YYYY-MM-DD-<name>; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date)
|
|
347
|
+
- Archive directory target uses the current date, computed once in step 3d and reused at the move: YYYY-MM-DD-<name>; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date)
|
|
318
348
|
- If archive target exists, fail that change but continue with others
|
|
349
|
+
- Check every archive target in step 3, before the first main-spec write; a change whose target exists is never synced or moved
|
|
319
350
|
- If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven) for each change with included delta specs
|
|
320
351
|
- Carry the per-delta \`includedDeltas\` and \`excludedDeltas\` decisions into execution; sync and verify only included deltas
|
|
321
352
|
- Report every excluded delta as \`sync skipped\` without treating the archive itself as skipped
|
|
@@ -347,6 +378,8 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|
|
347
378
|
|
|
348
379
|
${STORE_SELECTION_GUIDANCE}
|
|
349
380
|
|
|
381
|
+
${PROJECT_ROOT_GUARD}
|
|
382
|
+
|
|
350
383
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
351
384
|
|
|
352
385
|
**Input**: None required (prompts for selection)
|
|
@@ -402,7 +435,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
402
435
|
- Note which artifacts are \`done\` vs other states
|
|
403
436
|
|
|
404
437
|
b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON
|
|
405
|
-
-
|
|
438
|
+
- Complete means the checkbox holds only \`x\`/\`X\`, ignoring spacing
|
|
439
|
+
(\`- [ x]\` is complete); every other marker is incomplete (\`- [ ]\`,
|
|
440
|
+
\`- []\`, and unfamiliar ones such as \`- [~]\` or \`- [-]\`)
|
|
406
441
|
- If no tasks file exists, note as "No tasks"
|
|
407
442
|
|
|
408
443
|
c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON
|
|
@@ -414,6 +449,13 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
414
449
|
- Evaluate this independently for every change, including mixed-schema
|
|
415
450
|
batches where some schemas have no \`specs\` artifact.
|
|
416
451
|
|
|
452
|
+
d. **Archive target** - Compute each change's target name once and record it as that change's \`<target-name>\`
|
|
453
|
+
- Use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-<name>\` (same rule as \`openspec archive\`)
|
|
454
|
+
- Check whether \`<planningHome.changesDir>/archive/<target-name>\` already exists
|
|
455
|
+
- If it exists, or another selected change resolves to the same target name, mark every such change \`Blocked\` with \`Archive directory already exists\`
|
|
456
|
+
- A blocked change is never synced or moved: show it as \`Blocked\` in the step 6 table, leave it out of conflict resolution (resolve its conflicts using only the other changes), and record it as Failed in step 8d
|
|
457
|
+
- Checking here, before any main spec is written, matches \`openspec archive\`: a collision found after sync would leave main specs rewritten for an archive that never happened
|
|
458
|
+
|
|
417
459
|
4. **Detect spec conflicts**
|
|
418
460
|
|
|
419
461
|
Build a map keyed by \`<capability-path>\`, the exact path relative to \`specs/\`:
|
|
@@ -486,8 +528,8 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
486
528
|
Route on the answer by intent, not by exact label — you wrote these labels,
|
|
487
529
|
so match what the user picked rather than the wording above:
|
|
488
530
|
- "Cancel" — stop, do not archive. Report that nothing was archived and skip the remaining steps.
|
|
489
|
-
- The archive-everything option — proceed with every selected change
|
|
490
|
-
- The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d
|
|
531
|
+
- The archive-everything option — proceed with every selected change that is not \`Blocked\`
|
|
532
|
+
- The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d, except \`Blocked\` changes, which stay Failed with \`Archive directory already exists\`. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived.
|
|
491
533
|
- Anything else — ask again rather than archiving
|
|
492
534
|
|
|
493
535
|
Before step 8 writes the first main spec or moves any change, fetch every
|
|
@@ -510,7 +552,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
510
552
|
Process changes in the determined order (respecting conflict resolution):
|
|
511
553
|
|
|
512
554
|
a. **Sync included delta specs**:
|
|
513
|
-
-
|
|
555
|
+
- ${SYNC_INLINE_HANDOFF} only for changes with entries in \`includedDeltas\`, passing only the included delta paths and explicitly instructing it to ignore that change's \`excludedDeltas\`. Wait for it to finish.
|
|
514
556
|
- For conflicts, apply in resolved order.
|
|
515
557
|
- Pass that change's fetched specs-rule snapshot into inline sync; inline
|
|
516
558
|
sync must reuse it without fetching instructions again
|
|
@@ -532,13 +574,20 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
532
574
|
|
|
533
575
|
c. **Perform the archive**:
|
|
534
576
|
|
|
535
|
-
Target name: use the change
|
|
577
|
+
Target name: use the \`<target-name>\` recorded for this change in step 3d, unchanged. Never recompute it here: a batch that runs past midnight would check one date in step 3 and move to another.
|
|
578
|
+
|
|
579
|
+
**Check if target already exists:**
|
|
580
|
+
- Check again immediately before the move, even though step 3 already checked: the target can appear mid-batch
|
|
581
|
+
- If yes: record this change as Failed with \`Archive directory already exists\`, leave \`changeRoot\` where it is, report any main specs step 8a already synced for it, and continue with the remaining changes
|
|
582
|
+
- If no: move \`changeRoot\` to the archive directory
|
|
536
583
|
|
|
537
584
|
\`\`\`bash
|
|
538
585
|
mkdir -p "<planningHome.changesDir>/archive"
|
|
539
586
|
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
|
|
540
587
|
\`\`\`
|
|
541
588
|
|
|
589
|
+
**Confirm the move did not nest:** \`mv\` exits 0 even when the target appeared after the check, moving the change *inside* it. If \`<planningHome.changesDir>/archive/<target-name>/<change-directory-name>\` now exists (the last path segment of \`changeRoot\`), move that directory back to \`changeRoot\` and record this change as Failed with \`Archive directory already exists\`. Never report it as archived.
|
|
590
|
+
|
|
542
591
|
d. **Track outcome** for each change:
|
|
543
592
|
- Success: archived successfully
|
|
544
593
|
- Failed: error during archive or spec verification (record error)
|
|
@@ -653,9 +702,10 @@ No active changes found. Create a new change to get started.
|
|
|
653
702
|
- Never archive after the user cancels the confirmation — a cancelled batch archives nothing
|
|
654
703
|
- Track and report all outcomes (success/skip/fail)
|
|
655
704
|
- Preserve .openspec.yaml when moving to archive
|
|
656
|
-
- Archive directory target uses current date: YYYY-MM-DD-<name>; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date)
|
|
705
|
+
- Archive directory target uses the current date, computed once in step 3d and reused at the move: YYYY-MM-DD-<name>; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date)
|
|
657
706
|
- If archive target exists, fail that change but continue with others
|
|
658
|
-
-
|
|
707
|
+
- Check every archive target in step 3, before the first main-spec write; a change whose target exists is never synced or moved
|
|
708
|
+
- If sync is requested, ${SYNC_GUARDRAIL} for each change with included delta specs
|
|
659
709
|
- Carry the per-delta \`includedDeltas\` and \`excludedDeltas\` decisions into execution; sync and verify only included deltas
|
|
660
710
|
- Report every excluded delta as \`sync skipped\` without treating the archive itself as skipped
|
|
661
711
|
- Never archive a change while a spec sync is still in flight — run the sync inline and verify main specs at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\` before moving \`changeRoot\`
|