@fission-ai/openspec 1.13.2 → 1.14.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.
Files changed (99) hide show
  1. package/dist/cli/commands/config.d.ts +8 -0
  2. package/dist/cli/commands/config.js +88 -0
  3. package/dist/cli/commands/context.d.ts +3 -0
  4. package/dist/cli/commands/context.js +20 -0
  5. package/dist/cli/commands/doctor.d.ts +3 -0
  6. package/dist/cli/commands/doctor.js +18 -0
  7. package/dist/cli/commands/schema.d.ts +6 -0
  8. package/dist/cli/commands/schema.js +57 -0
  9. package/dist/cli/commands/spec.d.ts +3 -0
  10. package/dist/cli/commands/spec.js +42 -0
  11. package/dist/cli/commands/store.d.ts +3 -0
  12. package/dist/cli/commands/store.js +130 -0
  13. package/dist/cli/commands/workset.d.ts +3 -0
  14. package/dist/cli/commands/workset.js +96 -0
  15. package/dist/cli/index.js +106 -43
  16. package/dist/commands/config.d.ts +16 -7
  17. package/dist/commands/config.js +342 -395
  18. package/dist/commands/context.d.ts +8 -2
  19. package/dist/commands/context.js +22 -37
  20. package/dist/commands/doctor.d.ts +6 -7
  21. package/dist/commands/doctor.js +14 -27
  22. package/dist/commands/schema.d.ts +19 -5
  23. package/dist/commands/schema.js +662 -702
  24. package/dist/commands/spec.d.ts +13 -4
  25. package/dist/commands/spec.js +92 -120
  26. package/dist/commands/store.d.ts +27 -2
  27. package/dist/commands/store.js +1 -120
  28. package/dist/commands/workflow/default-schema.d.ts +2 -0
  29. package/dist/commands/workflow/default-schema.js +4 -0
  30. package/dist/commands/workflow/instructions.js +32 -12
  31. package/dist/commands/workflow/shared.d.ts +3 -1
  32. package/dist/commands/workflow/shared.js +1 -1
  33. package/dist/commands/workflow/status.js +15 -2
  34. package/dist/commands/workset.d.ts +22 -2
  35. package/dist/commands/workset.js +1 -88
  36. package/dist/core/archive.js +28 -7
  37. package/dist/core/artifact-graph/instruction-loader.d.ts +7 -0
  38. package/dist/core/artifact-graph/instruction-loader.js +18 -1
  39. package/dist/core/change-metadata/schema.d.ts +3 -0
  40. package/dist/core/change-metadata/schema.js +10 -0
  41. package/dist/core/change-status-policy.d.ts +8 -0
  42. package/dist/core/change-status-policy.js +33 -2
  43. package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
  44. package/dist/core/command-generation/adapters/atomcode.js +46 -0
  45. package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
  46. package/dist/core/command-generation/adapters/codestudio.js +27 -0
  47. package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
  48. package/dist/core/command-generation/adapters/easycode.js +28 -0
  49. package/dist/core/command-generation/adapters/gemini.js +1 -35
  50. package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
  51. package/dist/core/command-generation/adapters/gigacode.js +31 -0
  52. package/dist/core/command-generation/adapters/index.d.ts +4 -0
  53. package/dist/core/command-generation/adapters/index.js +4 -0
  54. package/dist/core/command-generation/registry.js +8 -0
  55. package/dist/core/command-generation/toml.d.ts +19 -0
  56. package/dist/core/command-generation/toml.js +39 -0
  57. package/dist/core/command-surface.js +1 -1
  58. package/dist/core/completion-tip.js +3 -1
  59. package/dist/core/completions/command-registry.js +19 -0
  60. package/dist/core/completions/installers/zsh-installer.js +4 -2
  61. package/dist/core/config-prompts.js +6 -6
  62. package/dist/core/config-schema.d.ts +1 -1
  63. package/dist/core/config.js +11 -1
  64. package/dist/core/init.js +35 -11
  65. package/dist/core/legacy-cleanup.js +11 -7
  66. package/dist/core/list.d.ts +2 -0
  67. package/dist/core/list.js +49 -23
  68. package/dist/core/parsers/markdown-parser.js +5 -0
  69. package/dist/core/parsers/requirement-blocks.d.ts +6 -0
  70. package/dist/core/parsers/requirement-blocks.js +9 -2
  71. package/dist/core/project-config.js +45 -1
  72. package/dist/core/references.js +3 -1
  73. package/dist/core/root-selection.d.ts +6 -0
  74. package/dist/core/root-selection.js +16 -0
  75. package/dist/core/schemas/base.schema.d.ts +3 -0
  76. package/dist/core/schemas/base.schema.js +6 -0
  77. package/dist/core/schemas/change.schema.d.ts +8 -0
  78. package/dist/core/schemas/spec.schema.d.ts +2 -0
  79. package/dist/core/templates/workflows/apply-change.js +9 -5
  80. package/dist/core/templates/workflows/archive-change.js +26 -4
  81. package/dist/core/templates/workflows/bulk-archive-change.js +6 -2
  82. package/dist/core/templates/workflows/continue-change.js +17 -0
  83. package/dist/core/templates/workflows/verify-change.js +12 -12
  84. package/dist/core/validation/constants.d.ts +1 -1
  85. package/dist/core/validation/constants.js +1 -1
  86. package/dist/core/validation/validator.js +21 -2
  87. package/dist/core/version-check.d.ts +23 -1
  88. package/dist/core/version-check.js +112 -26
  89. package/dist/core/view.d.ts +2 -0
  90. package/dist/core/view.js +36 -2
  91. package/dist/utils/change-metadata.d.ts +20 -1
  92. package/dist/utils/change-metadata.js +57 -1
  93. package/dist/utils/command-references.d.ts +1 -1
  94. package/dist/utils/command-references.js +2 -2
  95. package/dist/utils/index.d.ts +1 -1
  96. package/dist/utils/index.js +1 -1
  97. package/package.json +1 -1
  98. package/schemas/spec-driven/schema.yaml +29 -4
  99. package/schemas/spec-driven/templates/proposal.md +6 -3
@@ -136,4 +136,10 @@ export declare function findMissingCurrentScenarios(current: RequirementBlock, i
136
136
  * the counts a reader sees cannot differ between the two commands.
137
137
  */
138
138
  export declare function describeScenarioBalance(diff: ScenarioNameDiff): string;
139
+ /**
140
+ * scenarioNameAt for header text whose leading `####` is already gone, as the
141
+ * section parser (MarkdownParser) holds it, so `show --json` names a scenario
142
+ * exactly as the MODIFIED loss check does.
143
+ */
144
+ export declare function scenarioNameFromHeaderText(headerText: string): string;
139
145
  //# sourceMappingURL=requirement-blocks.d.ts.map
@@ -471,8 +471,15 @@ function scenarioHeaderAt(lines, mask, index) {
471
471
  * ATX-closed, one not) are not mistaken for a dropped scenario.
472
472
  */
473
473
  function scenarioNameAt(line) {
474
- return line
475
- .replace(SCENARIO_HEADER, '')
474
+ return scenarioNameFromHeaderText(line.replace(SCENARIO_HEADER, ''));
475
+ }
476
+ /**
477
+ * scenarioNameAt for header text whose leading `####` is already gone, as the
478
+ * section parser (MarkdownParser) holds it, so `show --json` names a scenario
479
+ * exactly as the MODIFIED loss check does.
480
+ */
481
+ export function scenarioNameFromHeaderText(headerText) {
482
+ return headerText
476
483
  // Optional ATX closing sequence. CommonMark only treats a trailing `#` run
477
484
  // as a close when it is preceded by a space or tab — not any Unicode space —
478
485
  // so this uses `[ \t]`, not `\s`. A looser `\s` could strip a `#` run after
@@ -181,6 +181,50 @@ function parseDeclarationList(raw) {
181
181
  return byId.size > 0 ? [...byId.values()] : undefined;
182
182
  }
183
183
  export const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit, shared with the references index
184
+ /**
185
+ * Build the warning for an artifact whose `rules:` list is not an array of
186
+ * strings. Names the offending index and what YAML actually produced there, so
187
+ * a config that silently loses an entire rule set can be fixed without
188
+ * bisecting the list by hand. A bare `-` item containing an unquoted ": " is the
189
+ * common cause: YAML reads it as a mapping, so the hint points at quoting.
190
+ */
191
+ function describeRulesShapeError(artifactId, rules) {
192
+ const base = `Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`;
193
+ if (!Array.isArray(rules)) {
194
+ return `${base}. rules.${artifactId} is ${describeYamlType(rules)}`;
195
+ }
196
+ const bad = rules
197
+ .map((rule, index) => ({ rule, index }))
198
+ .filter(({ rule }) => typeof rule !== 'string');
199
+ if (bad.length === 0) {
200
+ return base;
201
+ }
202
+ // Name every offending index with its own shape, so a mixed list does not
203
+ // have to be re-bisected one item at a time.
204
+ const details = bad
205
+ .map(({ rule, index }) => `rules.${artifactId}[${index}] is ${describeYamlType(rule)}`)
206
+ .join('; ');
207
+ // A bare `-` item with an unquoted ": " is the common cause: YAML reads it as
208
+ // a mapping. Say so rather than leaving the reader to work out the quoting.
209
+ const hasMapping = bad.some(({ rule }) => rule !== null && typeof rule === 'object' && !Array.isArray(rule));
210
+ const hint = hasMapping
211
+ ? ' — an unquoted ": " makes YAML read the item as a key/value pair; quote the whole scalar to keep it a string.'
212
+ : '';
213
+ return `${base}. ${details}${hint}`;
214
+ }
215
+ /** Name a YAML value's shape in a warning, e.g. "a mapping" or "a number". */
216
+ function describeYamlType(value) {
217
+ if (value === null)
218
+ return 'null';
219
+ if (Array.isArray(value))
220
+ return 'a nested list';
221
+ const type = typeof value;
222
+ if (type === 'object')
223
+ return 'a mapping';
224
+ if (type === 'number' || type === 'boolean')
225
+ return `a ${type}`;
226
+ return `a ${type}`;
227
+ }
184
228
  /**
185
229
  * Read and parse openspec/config.yaml from project root.
186
230
  * Uses resilient parsing - validates each field independently using Zod safeParse.
@@ -265,7 +309,7 @@ export function readProjectConfig(projectRoot) {
265
309
  }
266
310
  }
267
311
  else {
268
- console.warn(`Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`);
312
+ console.warn(describeRulesShapeError(artifactId, rules));
269
313
  }
270
314
  }
271
315
  if (hasValidRules) {
@@ -191,7 +191,9 @@ export function renderReferencedStoresSection(entries) {
191
191
  * let hostile content forge instruction lines (slice 6.1 hardening).
192
192
  */
193
193
  export function sanitizeInline(value, maxLength = 300) {
194
- const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
194
+ const flattened = value
195
+ .replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u206f]+/g, ' ')
196
+ .trim();
195
197
  return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
196
198
  }
197
199
  /**
@@ -94,6 +94,12 @@ export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
94
94
  export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
95
95
  storeId: string;
96
96
  };
97
+ /**
98
+ * The project on the current path whose `store:` pointer names `storeId`,
99
+ * as a canonical path (the root walk resolves aliases). Null when the
100
+ * nearest root is a real planning root or points at a different store.
101
+ */
102
+ export declare function findDeclaringProjectRoot(storeId: string, startPath?: string): string | null;
97
103
  /**
98
104
  * Human-mode verification signal for a selected store. Written to stderr so
99
105
  * raw-Markdown and agent-consumed stdout payloads stay clean.
@@ -309,6 +309,22 @@ export function toRootOutput(root) {
309
309
  export function isStoreSelectedRoot(root) {
310
310
  return root.storeId !== undefined;
311
311
  }
312
+ /**
313
+ * The project on the current path whose `store:` pointer names `storeId`,
314
+ * as a canonical path (the root walk resolves aliases). Null when the
315
+ * nearest root is a real planning root or points at a different store.
316
+ */
317
+ export function findDeclaringProjectRoot(storeId, startPath = process.cwd()) {
318
+ const nearestRoot = findQualifyingRootSync(startPath);
319
+ if (!nearestRoot) {
320
+ return null;
321
+ }
322
+ const { hasPlanningShape, pointer } = classifyOpenSpecDir(nearestRoot);
323
+ if (hasPlanningShape || pointer.value !== storeId) {
324
+ return null;
325
+ }
326
+ return nearestRoot;
327
+ }
312
328
  /**
313
329
  * Human-mode verification signal for a selected store. Written to stderr so
314
330
  * raw-Markdown and agent-consumed stdout payloads stay clean.
@@ -1,10 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  export declare const ScenarioSchema: z.ZodObject<{
3
+ name: z.ZodOptional<z.ZodString>;
3
4
  rawText: z.ZodString;
4
5
  }, z.core.$strip>;
5
6
  export declare const RequirementSchema: z.ZodObject<{
7
+ name: z.ZodOptional<z.ZodString>;
6
8
  text: z.ZodString;
7
9
  scenarios: z.ZodArray<z.ZodObject<{
10
+ name: z.ZodOptional<z.ZodString>;
8
11
  rawText: z.ZodString;
9
12
  }, z.core.$strip>>;
10
13
  }, z.core.$strip>;
@@ -1,9 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { VALIDATION_MESSAGES } from '../validation/constants.js';
3
3
  export const ScenarioSchema = z.object({
4
+ // Header text without `####`, the closing `#` run, and the `Scenario:`
5
+ // prefix. Optional so objects built outside the parser still validate.
6
+ name: z.string().optional(),
4
7
  rawText: z.string().min(1, VALIDATION_MESSAGES.SCENARIO_EMPTY),
5
8
  });
6
9
  export const RequirementSchema = z.object({
10
+ // Header text without `###` and the `Requirement:` prefix: the name archive
11
+ // matches MODIFIED, REMOVED and RENAMED entries against.
12
+ name: z.string().optional(),
7
13
  // SHALL/MUST body-keyword enforcement lives in the imperative validator
8
14
  // (Validator.applySpecRules), not here: the parser collapses the requirement
9
15
  // header into `text`, so a Zod refine on `text` cannot tell "keyword in header
@@ -15,14 +15,18 @@ export declare const DeltaSchema: z.ZodObject<{
15
15
  }>;
16
16
  description: z.ZodString;
17
17
  requirement: z.ZodOptional<z.ZodObject<{
18
+ name: z.ZodOptional<z.ZodString>;
18
19
  text: z.ZodString;
19
20
  scenarios: z.ZodArray<z.ZodObject<{
21
+ name: z.ZodOptional<z.ZodString>;
20
22
  rawText: z.ZodString;
21
23
  }, z.core.$strip>>;
22
24
  }, z.core.$strip>>;
23
25
  requirements: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ name: z.ZodOptional<z.ZodString>;
24
27
  text: z.ZodString;
25
28
  scenarios: z.ZodArray<z.ZodObject<{
29
+ name: z.ZodOptional<z.ZodString>;
26
30
  rawText: z.ZodString;
27
31
  }, z.core.$strip>>;
28
32
  }, z.core.$strip>>>;
@@ -45,14 +49,18 @@ export declare const ChangeSchema: z.ZodObject<{
45
49
  }>;
46
50
  description: z.ZodString;
47
51
  requirement: z.ZodOptional<z.ZodObject<{
52
+ name: z.ZodOptional<z.ZodString>;
48
53
  text: z.ZodString;
49
54
  scenarios: z.ZodArray<z.ZodObject<{
55
+ name: z.ZodOptional<z.ZodString>;
50
56
  rawText: z.ZodString;
51
57
  }, z.core.$strip>>;
52
58
  }, z.core.$strip>>;
53
59
  requirements: z.ZodOptional<z.ZodArray<z.ZodObject<{
60
+ name: z.ZodOptional<z.ZodString>;
54
61
  text: z.ZodString;
55
62
  scenarios: z.ZodArray<z.ZodObject<{
63
+ name: z.ZodOptional<z.ZodString>;
56
64
  rawText: z.ZodString;
57
65
  }, z.core.$strip>>;
58
66
  }, z.core.$strip>>>;
@@ -3,8 +3,10 @@ export declare const SpecSchema: z.ZodObject<{
3
3
  name: z.ZodString;
4
4
  overview: z.ZodString;
5
5
  requirements: z.ZodArray<z.ZodObject<{
6
+ name: z.ZodOptional<z.ZodString>;
6
7
  text: z.ZodString;
7
8
  scenarios: z.ZodArray<z.ZodObject<{
9
+ name: z.ZodOptional<z.ZodString>;
8
10
  rawText: z.ZodString;
9
11
  }, z.core.$strip>>;
10
12
  }, z.core.$strip>>;
@@ -56,7 +56,7 @@ ${PROJECT_ROOT_GUARD}
56
56
  This returns:
57
57
  - \`contextFiles\`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
58
58
  - Progress (total, complete, remaining)
59
- - Task list with status
59
+ - Task list with status, source path, and source line
60
60
  - Dynamic instruction based on current state
61
61
  - Optional \`context\`: current required project instruction input from the selected root
62
62
  - Optional \`operationGuidance\`: current advisory guidance for apply
@@ -66,7 +66,7 @@ ${PROJECT_ROOT_GUARD}
66
66
  - If \`state: "blocked"\`: show the message and pause implementation.
67
67
  - If \`missingArtifacts\` is non-empty: ${BLOCKED_STATE_HANDOFF}
68
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.
69
- - If \`state: "all_done"\`: congratulate, suggest archive
69
+ - If \`state: "all_done"\`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
70
70
  - Otherwise: proceed to implementation
71
71
 
72
72
  Treat \`context\` as a required prompt-level input. Read and consider it, and
@@ -108,7 +108,9 @@ ${PROJECT_ROOT_GUARD}
108
108
  - Show which task is being worked on
109
109
  - Make the code changes required
110
110
  - Keep changes minimal and focused
111
- - Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
111
+ - Before editing, confirm the checkbox at the returned \`sourcePath\` and \`line\` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
112
+ - Mark the task complete at its returned \`sourcePath\` and \`line\`: \`- [ ]\` → \`- [x]\`
113
+ - Rerun the apply instructions and confirm that task is now done and progress changed
112
114
  - Continue to next task
113
115
 
114
116
  **Pause if:**
@@ -123,7 +125,7 @@ ${PROJECT_ROOT_GUARD}
123
125
  Display:
124
126
  - Tasks completed this session
125
127
  - Overall progress: "N/M tasks complete"
126
- - If all done: suggest archive
128
+ - If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
127
129
  - If paused: explain why and wait for guidance
128
130
 
129
131
  **Output During Implementation**
@@ -154,7 +156,8 @@ Working on task 4/7: <task description>
154
156
  - [x] Task 2
155
157
  ...
156
158
 
157
- All tasks complete! ${ARCHIVE_HANDOFF}
159
+ All tracked tasks are complete. Review or verify the change as appropriate
160
+ before archiving. ${ARCHIVE_HANDOFF}
158
161
  \`\`\`
159
162
 
160
163
  **Output On Pause (Issue Encountered)**
@@ -188,6 +191,7 @@ What would you like to do?
188
191
  - When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
189
192
  - Only mark a task \`- [x]\` when its specified behavior is fully implemented, not when it is partially done or deferred
190
193
  - Use contextFiles from CLI output, don't assume specific file names
194
+ - Use each task's sourcePath and line to update its exact checkbox
191
195
  - Do not use context or operation guidance as proof that a task is complete
192
196
  - Apply relevant project context; report conflicts with controlling workflow inputs
193
197
  - Consider every guidance entry; explain any inapplicable or conflicting advice
@@ -140,12 +140,23 @@ ${PROJECT_ROOT_GUARD}
140
140
  form of main specs produced by this merge; do not use them as archive guidance,
141
141
  change CLI behavior, or copy the rule text into any output file.
142
142
 
143
- 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.
143
+ Then ${optionalWorkflow('sync', 'run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge)', 'perform the delta-to-main-spec merge inline yourself (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.
144
+
145
+ If the sync reports any stop or blocking condition, treat the sync as failed.
146
+ Stop the archive immediately. Do not perform the post-sync content comparison and do not move its \`changeRoot\`.
147
+ Nothing has moved, so the user can fix the blocking condition or re-run the sync.
148
+
149
+ After the sync writes each main spec, verify its structure against the canonical sync contract:
150
+ - A new main spec starts with a \`# <capability> Specification\` title. An existing main spec keeps its title exactly as it is.
151
+ - Preserve existing \`## Purpose\` sections completely untouched for established main specs.
152
+ - For a new main spec, copy the delta \`## Purpose\` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable \`## Purpose\` is provided, use the existing TBD Purpose behavior and warning.
153
+ - Verify that no delta-style section headers (\`## ADDED Requirements\`, \`## MODIFIED Requirements\`, \`## REMOVED Requirements\`, \`## RENAMED Requirements\`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
154
+ - Requirement blocks the sync wrote or changed use \`### Requirement:\` headings, and their scenarios use \`#### Scenario:\` headings, under the spec's \`## Requirements\` section. Leave content the delta does not mention exactly as it is.
144
155
 
145
156
  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:
146
157
  - ADDED requirements present
147
158
  - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
148
- - 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
159
+ - 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.
149
160
  - RENAMED requirements present under the new name and absent under the old one
150
161
 
151
162
  If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and \`changeRoot\` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
@@ -195,7 +206,7 @@ ${PROJECT_ROOT_GUARD}
195
206
  - Don't block archive on warnings - just inform and confirm
196
207
  - Preserve .openspec.yaml when moving to archive (it moves with the directory)
197
208
  - Show clear summary of what happened
198
- - If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven)
209
+ - If sync is requested, ${optionalWorkflow('sync', 'run the `openspec-sync-specs` workflow inline (agent-driven)', 'perform the delta-to-main-spec merge inline (agent-driven)')}
199
210
  - Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving \`changeRoot\`
200
211
  - If delta specs exist, always run the sync assessment and show the combined summary before prompting
201
212
  - Apply relevant runtime context and report conflicts; operation guidance remains advisory
@@ -342,10 +353,21 @@ ${PROJECT_ROOT_GUARD}
342
353
 
343
354
  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.
344
355
 
356
+ If the sync reports any stop or blocking condition, treat the sync as failed.
357
+ Stop the archive immediately. Do not perform the post-sync content comparison and do not move its \`changeRoot\`.
358
+ Nothing has moved, so the user can fix the blocking condition or re-run the sync.
359
+
360
+ After the sync writes each main spec, verify its structure against the canonical sync contract:
361
+ - A new main spec starts with a \`# <capability> Specification\` title. An existing main spec keeps its title exactly as it is.
362
+ - Preserve existing \`## Purpose\` sections completely untouched for established main specs.
363
+ - For a new main spec, copy the delta \`## Purpose\` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable \`## Purpose\` is provided, use the existing TBD Purpose behavior and warning.
364
+ - Verify that no delta-style section headers (\`## ADDED Requirements\`, \`## MODIFIED Requirements\`, \`## REMOVED Requirements\`, \`## RENAMED Requirements\`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
365
+ - Requirement blocks the sync wrote or changed use \`### Requirement:\` headings, and their scenarios use \`#### Scenario:\` headings, under the spec's \`## Requirements\` section. Leave content the delta does not mention exactly as it is.
366
+
345
367
  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:
346
368
  - ADDED requirements present
347
369
  - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
348
- - 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
370
+ - 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.
349
371
  - RENAMED requirements present under the new name and absent under the old one
350
372
 
351
373
  If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and \`changeRoot\` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
@@ -202,6 +202,8 @@ ${PROJECT_ROOT_GUARD}
202
202
 
203
203
  a. **Sync included delta specs**:
204
204
  - ${optionalWorkflow('sync', 'Run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge)', 'Perform the delta-to-main-spec merge inline yourself (agent-driven intelligent merge)')} 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.
205
+ - If the sync reports any stop or blocking condition, treat the sync as failed. Stop processing that change immediately. Before continuing to the next change, record this change's outcome as Failed in the batch results, including the sync blocking/error condition.
206
+ - Do not perform the post-sync content comparison and do not move its \`changeRoot\`; leave the change intact.
205
207
  - For conflicts, apply in resolved order.
206
208
  - Pass that change's fetched specs-rule snapshot into inline sync; inline
207
209
  sync must reuse it without fetching instructions again
@@ -216,7 +218,7 @@ ${PROJECT_ROOT_GUARD}
216
218
  - Verify that main specs are updated:
217
219
  - ADDED requirements present
218
220
  - MODIFIED requirements carrying scenario and description changes named in the delta, with their other scenarios intact
219
- - 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
221
+ - 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.
220
222
  - RENAMED requirements present under the new name and absent under the old one
221
223
  - Do not verify delta specs in \`excludedDeltas\`; they are intentionally left unsynced.
222
224
  - If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's \`changeRoot\` — do not archive that change. \`changeRoot\` remains intact.
@@ -567,6 +569,8 @@ ${PROJECT_ROOT_GUARD}
567
569
 
568
570
  a. **Sync included delta specs**:
569
571
  - ${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.
572
+ - If the sync reports any stop or blocking condition, treat the sync as failed. Stop processing that change immediately. Before continuing to the next change, record this change's outcome as Failed in the batch results, including the sync blocking/error condition.
573
+ - Do not perform the post-sync content comparison and do not move its \`changeRoot\`; leave the change intact.
570
574
  - For conflicts, apply in resolved order.
571
575
  - Pass that change's fetched specs-rule snapshot into inline sync; inline
572
576
  sync must reuse it without fetching instructions again
@@ -581,7 +585,7 @@ ${PROJECT_ROOT_GUARD}
581
585
  - Verify that main specs are updated:
582
586
  - ADDED requirements present
583
587
  - MODIFIED requirements carrying scenario and description changes named in the delta, with their other scenarios intact
584
- - 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
588
+ - 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.
585
589
  - RENAMED requirements present under the new name and absent under the old one
586
590
  - Do not verify delta specs in \`excludedDeltas\`; they are intentionally left unsynced.
587
591
  - If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's \`changeRoot\` — do not archive that change. \`changeRoot\` remains intact.
@@ -7,6 +7,15 @@ import { PROJECT_ROOT_GUARD } from './project-root.js';
7
7
  * optional-workflow.ts).
8
8
  */
9
9
  const PLANNING_COMPLETE_HANDOFF = optionalWorkflow('apply', 'You can now implement this change with `/opsx:apply`.', 'You can now implement this change - `openspec instructions apply --change "<name>" --json` returns the tasks and how to work them.') + ' ' + optionalWorkflow('archive', 'Once implementation and any tracked work are complete, archive it with `/opsx:archive`.', 'Once implementation and any tracked work are complete, archive it with `openspec archive "<name>"`.');
10
+ /**
11
+ * A glob artifact reads `done` once one matching file exists, so a run
12
+ * interrupted while writing several files leaves it `done` with files missing,
13
+ * and the next run would move on without reading its instruction again (#2034).
14
+ */
15
+ const PARTIAL_ARTIFACT_CHECK = `**Before anything else, finish a partly written artifact**:
16
+ - An artifact whose output is a glob pattern (e.g. \`specs/**/*.md\`) reads \`done\` as soon as one matching file exists, so a run interrupted while writing its files leaves it \`done\` with some still missing
17
+ - For each such \`done\` artifact, first get its instructions and read any completed dependencies needed to determine the expected files. Then compare those files with \`artifactPaths.<id>.existingOutputPaths\`. For spec-driven's \`specs\`, read the proposal and check for one \`specs/<capability-path>/spec.md\` per capability it lists; resolve each expected path against \`changeRoot\` before comparing path identity
18
+ - If any are missing, write only the missing files, and STOP - that counts as this invocation's ONE artifact. If you cannot tell whether a file was left out on purpose, ask the user`;
10
19
  export function getContinueChangeSkillTemplate() {
11
20
  return {
12
21
  name: 'openspec-continue-change',
@@ -51,6 +60,10 @@ ${PROJECT_ROOT_GUARD}
51
60
 
52
61
  ---
53
62
 
63
+ ${PARTIAL_ARTIFACT_CHECK}
64
+
65
+ ---
66
+
54
67
  **If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
55
68
  - Congratulate the user
56
69
  - Show final status including the schema used
@@ -169,6 +182,10 @@ ${PROJECT_ROOT_GUARD}
169
182
 
170
183
  ---
171
184
 
185
+ ${PARTIAL_ARTIFACT_CHECK}
186
+
187
+ ---
188
+
172
189
  **If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
173
190
  - Congratulate the user
174
191
  - Show final status including the schema used
@@ -76,9 +76,9 @@ ${PROJECT_ROOT_GUARD}
76
76
  - Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
77
77
 
78
78
  **Spec Coverage**:
79
- - If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact, report the spec-dependent checks as not applicable.
80
- - Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema. If \`contextFiles.specs\` is absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
81
- - If delta specs exist in \`contextFiles.specs\`:
79
+ - If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact (no artifact whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`), report the spec-dependent checks as not applicable.
80
+ - Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema, so do not assume an id such as \`specs\`. The spec artifacts are those whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`; read their files from \`contextFiles.<id>\`. If those spec files are absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
81
+ - If delta specs exist in those spec files:
82
82
  - Extract all requirements (marked with "### Requirement:", or listed as \`FROM:\`/\`TO:\` pairs under \`## RENAMED Requirements\`) and note the delta section each one sits under: \`## ADDED\`, \`## MODIFIED\`, \`## REMOVED\`, or \`## RENAMED Requirements\`. The section decides what the check looks for.
83
83
  - For each ADDED or MODIFIED requirement (for MODIFIED, check the text in the delta, not the old wording):
84
84
  - Search codebase for keywords related to the requirement
@@ -127,14 +127,14 @@ ${PROJECT_ROOT_GUARD}
127
127
  7. **Verify Coherence**
128
128
 
129
129
  **Design Adherence**:
130
- - If the schema defines no design artifact, report **Design Adherence** as not applicable.
131
- - If \`contextFiles.design\` exists:
130
+ - If the schema defines no design artifact (no artifact with id \`design\`, and none whose \`artifactPaths.<id>.outputPath\` is or ends in \`design.md\`), report **Design Adherence** as not applicable.
131
+ - If the design artifact's \`contextFiles.<id>\` file exists:
132
132
  - Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
133
133
  - Verify implementation follows those decisions
134
134
  - If contradiction detected:
135
135
  - Add WARNING: "Design decision not followed: <decision>"
136
136
  - Recommendation: "Update implementation or revise design.md to match reality"
137
- - Otherwise, if \`contextFiles.design\` is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
137
+ - Otherwise, if the design artifact's \`contextFiles.<id>\` file is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
138
138
 
139
139
  **Code Pattern Consistency**:
140
140
  - If implementation changes cannot be identified, mark **Code Pattern Consistency** as not verified and explain the missing evidence.
@@ -285,9 +285,9 @@ ${PROJECT_ROOT_GUARD}
285
285
  - Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
286
286
 
287
287
  **Spec Coverage**:
288
- - If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact, report the spec-dependent checks as not applicable.
289
- - Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema. If \`contextFiles.specs\` is absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
290
- - If delta specs exist in \`contextFiles.specs\`:
288
+ - If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact (no artifact whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`), report the spec-dependent checks as not applicable.
289
+ - Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema, so do not assume an id such as \`specs\`. The spec artifacts are those whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`; read their files from \`contextFiles.<id>\`. If those spec files are absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
290
+ - If delta specs exist in those spec files:
291
291
  - Extract all requirements (marked with "### Requirement:", or listed as \`FROM:\`/\`TO:\` pairs under \`## RENAMED Requirements\`) and note the delta section each one sits under: \`## ADDED\`, \`## MODIFIED\`, \`## REMOVED\`, or \`## RENAMED Requirements\`. The section decides what the check looks for.
292
292
  - For each ADDED or MODIFIED requirement (for MODIFIED, check the text in the delta, not the old wording):
293
293
  - Search codebase for keywords related to the requirement
@@ -336,14 +336,14 @@ ${PROJECT_ROOT_GUARD}
336
336
  7. **Verify Coherence**
337
337
 
338
338
  **Design Adherence**:
339
- - If the schema defines no design artifact, report **Design Adherence** as not applicable.
340
- - If \`contextFiles.design\` exists:
339
+ - If the schema defines no design artifact (no artifact with id \`design\`, and none whose \`artifactPaths.<id>.outputPath\` is or ends in \`design.md\`), report **Design Adherence** as not applicable.
340
+ - If the design artifact's \`contextFiles.<id>\` file exists:
341
341
  - Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
342
342
  - Verify implementation follows those decisions
343
343
  - If contradiction detected:
344
344
  - Add WARNING: "Design decision not followed: <decision>"
345
345
  - Recommendation: "Update implementation or revise design.md to match reality"
346
- - Otherwise, if \`contextFiles.design\` is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
346
+ - Otherwise, if the design artifact's \`contextFiles.<id>\` file is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
347
347
 
348
348
  **Code Pattern Consistency**:
349
349
  - If implementation changes cannot be identified, mark **Code Pattern Consistency** as not verified and explain the missing evidence.
@@ -29,7 +29,7 @@ export declare const VALIDATION_MESSAGES: {
29
29
  readonly DELTA_DESCRIPTION_EMPTY: "Delta description cannot be empty";
30
30
  readonly PURPOSE_TOO_BRIEF: "Purpose section is too brief (less than 50 characters)";
31
31
  readonly PURPOSE_IS_PLACEHOLDER: string;
32
- readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
32
+ readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Move examples and edge cases into scenarios, or split it into separate requirements that each state one behavior.";
33
33
  readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
34
34
  readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
35
35
  readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. If this change intentionally modifies no specs (pure refactor, tooling, docs), set \"skip_specs: true\" in the change's .openspec.yaml instead. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
@@ -43,7 +43,7 @@ export const VALIDATION_MESSAGES = {
43
43
  'writes for a new capability, or a `TBD`/`TODO` marker left in its place). Replace it with what this ' +
44
44
  'capability is for, editing the main spec directly: a `## Purpose` in a delta is read only when the ' +
45
45
  'capability is created, so it cannot replace this one.',
46
- REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
46
+ REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Move examples and edge cases into scenarios, or split it into separate requirements that each state one behavior.`,
47
47
  DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
48
48
  DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
49
49
  // Guidance snippets (appended to primary messages for remediation)
@@ -10,7 +10,7 @@ import { extractRequirementBody as extractRequirementBodyShared, containsShallOr
10
10
  import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
11
11
  import { FileSystemUtils } from '../../utils/file-system.js';
12
12
  import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
13
- import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../../utils/change-metadata.js';
13
+ import { METADATA_FILENAME, formatUnknownChangeMetadataKeysMessage, readSkipSpecsMarker, readUnknownChangeMetadataKeys, resolveSchemaForChange, } from '../../utils/change-metadata.js';
14
14
  import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
15
15
  import { findTaskNumberingIssues } from './task-numbering.js';
16
16
  import { findMissingTaskCheckboxIssues } from './task-checkboxes.js';
@@ -252,6 +252,17 @@ export class Validator {
252
252
  message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name, true),
253
253
  });
254
254
  }
255
+ // Same limit the main spec enforces after archive (#1976), so
256
+ // `validate <change> --strict` catches a new overlong requirement
257
+ // before it lands. MODIFIED is left alone: its text is the existing
258
+ // requirement, which the specs instruction says to keep whole.
259
+ if (requirementText && requirementText.length > MAX_REQUIREMENT_TEXT_LENGTH) {
260
+ issues.push({
261
+ level: 'WARNING',
262
+ path: entryPath,
263
+ message: `ADDED "${block.name}": ${VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG}`,
264
+ });
265
+ }
255
266
  const scenarioCount = this.countScenarios(block.raw);
256
267
  if (scenarioCount < 1) {
257
268
  issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario${this.emptyScenarioHint(block.raw)}` });
@@ -416,6 +427,14 @@ export class Validator {
416
427
  if (marker.invalidReason) {
417
428
  issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
418
429
  }
430
+ const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
431
+ if (unknownMetadataKeys.length > 0) {
432
+ issues.push({
433
+ level: 'WARNING',
434
+ path: METADATA_FILENAME,
435
+ message: formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys),
436
+ });
437
+ }
419
438
  // ANY file under specs/ contradicts the marker - not just parsed deltas.
420
439
  // Headerless or stray files would be silently dropped at archive time (and
421
440
  // some still satisfy the artifact graph's specs/** glob) while the change
@@ -703,7 +722,7 @@ export class Validator {
703
722
  spec.requirements.forEach((req, index) => {
704
723
  if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
705
724
  issues.push({
706
- level: 'INFO',
725
+ level: 'WARNING',
707
726
  path: `requirements[${index}]`,
708
727
  message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
709
728
  });
@@ -14,9 +14,19 @@ export declare function registryUrl(): string | null;
14
14
  * otherwise. Prereleases sort below their release (1.7.0-beta.1 < 1.7.0).
15
15
  */
16
16
  export declare function compareVersions(a: string, b: string): number;
17
+ export type CliUpdateStatus = 'available' | 'current' | 'disabled' | 'offline';
18
+ export interface CliUpdateCheck {
19
+ status: CliUpdateStatus;
20
+ latest: string | null;
21
+ }
22
+ /**
23
+ * Checks the registry while preserving enough detail for callers to explain
24
+ * why no newer version was reported. Never throws.
25
+ */
26
+ export declare function checkForCliUpdate(): Promise<CliUpdateCheck>;
17
27
  /**
18
28
  * Returns the published version when the installed CLI is behind it, otherwise
19
- * null. Never throws and never blocks for longer than the request timeout.
29
+ * null. Kept as the compatibility surface used by `openspec update`.
20
30
  */
21
31
  export declare function getAvailableCliUpdate(): Promise<string | null>;
22
32
  /**
@@ -69,11 +79,23 @@ export declare function isNpmGlobalInstall(installDir: string | null, roots?: st
69
79
  */
70
80
  export declare function isSourceCheckout(installDir: string | null): boolean;
71
81
  export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'yarn' | 'volta';
82
+ export type InstallScope = 'global' | 'project' | 'temporary' | 'source';
83
+ export interface CliInstallInfo {
84
+ location: string | null;
85
+ packageManager: PackageManager | null;
86
+ scope: InstallScope | null;
87
+ }
72
88
  /**
73
89
  * The package manager that owns this copy, so the printed command is one the
74
90
  * user's setup will actually honor.
75
91
  */
76
92
  export declare function detectPackageManager(installDir: string | null): PackageManager;
93
+ /** Describes the running copy without guessing when its owner is ambiguous. */
94
+ export declare function getCliInstallInfo(installDir?: string | null, projectPath?: string): CliInstallInfo;
95
+ /** Returns a safe update command only for an install with known global ownership. */
96
+ export declare function getCliUpdateCommand(install: CliInstallInfo): string | null;
97
+ /** Builds the concise human-readable form of the version report. */
98
+ export declare function buildVersionReportLines(version: string, install: CliInstallInfo, update?: CliUpdateCheck, command?: string | null): string[];
77
99
  /**
78
100
  * Builds the hint, with the upgrade command chosen for how this copy of the CLI
79
101
  * was installed. Pure so every branch is assertable.