@codewalla_india/openspec 1.3.1 → 1.3.2

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 (333) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +272 -191
  3. package/bin/openspec.js +5 -5
  4. package/dist/cli/index.js +99 -62
  5. package/dist/commands/change.d.ts +0 -2
  6. package/dist/commands/change.js +107 -86
  7. package/dist/commands/config.js +14 -16
  8. package/dist/commands/doctor.js +9 -4
  9. package/dist/commands/feedback.js +79 -30
  10. package/dist/commands/modify.d.ts +26 -0
  11. package/dist/commands/modify.js +147 -0
  12. package/dist/commands/schema.js +170 -91
  13. package/dist/commands/show.js +5 -2
  14. package/dist/commands/spec.js +49 -29
  15. package/dist/commands/store.js +15 -21
  16. package/dist/commands/validate.d.ts +8 -0
  17. package/dist/commands/validate.js +43 -20
  18. package/dist/commands/workflow/index.d.ts +2 -2
  19. package/dist/commands/workflow/index.js +1 -1
  20. package/dist/commands/workflow/instructions.d.ts +6 -26
  21. package/dist/commands/workflow/instructions.js +183 -376
  22. package/dist/commands/workflow/new-change.d.ts +0 -4
  23. package/dist/commands/workflow/new-change.js +10 -28
  24. package/dist/commands/workflow/shared.d.ts +34 -19
  25. package/dist/commands/workflow/shared.js +34 -4
  26. package/dist/commands/workflow/status.js +10 -32
  27. package/dist/commands/workflow/templates.js +15 -5
  28. package/dist/commands/workset.d.ts +2 -2
  29. package/dist/commands/workset.js +19 -22
  30. package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
  31. package/dist/comprehension-quiz/fingerprint.js +84 -0
  32. package/dist/comprehension-quiz/index.d.ts +42 -0
  33. package/dist/comprehension-quiz/index.js +47 -0
  34. package/dist/comprehension-quiz/pass-record.d.ts +44 -0
  35. package/dist/comprehension-quiz/pass-record.js +98 -0
  36. package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +53 -0
  38. package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
  39. package/dist/comprehension-quiz/providers/factory.js +174 -0
  40. package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
  41. package/dist/comprehension-quiz/providers/interface.js +63 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +34 -0
  43. package/dist/comprehension-quiz/providers/local.js +53 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +53 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
  47. package/dist/comprehension-quiz/providers/openai.js +53 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +69 -0
  49. package/dist/comprehension-quiz/question-generator.js +141 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +165 -0
  52. package/dist/comprehension-quiz/types.d.ts +115 -0
  53. package/dist/comprehension-quiz/types.js +11 -0
  54. package/dist/core/archive.d.ts +14 -1
  55. package/dist/core/archive.js +1350 -190
  56. package/dist/core/artifact-graph/graph.d.ts +28 -9
  57. package/dist/core/artifact-graph/graph.js +94 -43
  58. package/dist/core/artifact-graph/index.d.ts +1 -1
  59. package/dist/core/artifact-graph/index.js +1 -1
  60. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  61. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  62. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  63. package/dist/core/artifact-graph/outputs.js +67 -5
  64. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  65. package/dist/core/artifact-graph/resolver.js +69 -15
  66. package/dist/core/artifact-graph/types.js +21 -3
  67. package/dist/core/available-tools.d.ts +2 -2
  68. package/dist/core/available-tools.js +14 -3
  69. package/dist/core/change-metadata/schema.d.ts +2 -0
  70. package/dist/core/change-metadata/schema.js +13 -0
  71. package/dist/core/change-status-policy.d.ts +10 -1
  72. package/dist/core/change-status-policy.js +30 -2
  73. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  74. package/dist/core/command-generation/adapters/amazon-q.js +11 -5
  75. package/dist/core/command-generation/adapters/antigravity.js +6 -5
  76. package/dist/core/command-generation/adapters/auggie.js +7 -6
  77. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  78. package/dist/core/command-generation/adapters/bob.js +11 -10
  79. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  80. package/dist/core/command-generation/adapters/claude.js +12 -17
  81. package/dist/core/command-generation/adapters/cline.js +5 -5
  82. package/dist/core/command-generation/adapters/codebuddy.js +8 -7
  83. package/dist/core/command-generation/adapters/continue.js +8 -7
  84. package/dist/core/command-generation/adapters/costrict.js +7 -6
  85. package/dist/core/command-generation/adapters/crush.js +9 -9
  86. package/dist/core/command-generation/adapters/cursor.js +8 -8
  87. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  88. package/dist/core/command-generation/adapters/devin.js +36 -0
  89. package/dist/core/command-generation/adapters/factory.js +7 -6
  90. package/dist/core/command-generation/adapters/gemini.js +40 -5
  91. package/dist/core/command-generation/adapters/github-copilot.js +6 -5
  92. package/dist/core/command-generation/adapters/iflow.js +9 -8
  93. package/dist/core/command-generation/adapters/index.d.ts +4 -2
  94. package/dist/core/command-generation/adapters/index.js +4 -2
  95. package/dist/core/command-generation/adapters/junie.js +6 -5
  96. package/dist/core/command-generation/adapters/kilocode.js +1 -1
  97. package/dist/core/command-generation/adapters/kiro.js +6 -5
  98. package/dist/core/command-generation/adapters/lingma.js +9 -9
  99. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  101. package/dist/core/command-generation/adapters/opencode.js +6 -8
  102. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  103. package/dist/core/command-generation/adapters/pi.js +7 -10
  104. package/dist/core/command-generation/adapters/qoder.js +9 -9
  105. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  106. package/dist/core/command-generation/adapters/qwen.js +14 -9
  107. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  108. package/dist/core/command-generation/adapters/roocode.js +9 -9
  109. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  110. package/dist/core/command-generation/adapters/trae.js +28 -0
  111. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  112. package/dist/core/command-generation/adapters/zcode.js +33 -0
  113. package/dist/core/command-generation/generator.d.ts +8 -0
  114. package/dist/core/command-generation/generator.js +15 -1
  115. package/dist/core/command-generation/index.d.ts +1 -1
  116. package/dist/core/command-generation/index.js +1 -1
  117. package/dist/core/command-generation/invocation.d.ts +73 -0
  118. package/dist/core/command-generation/invocation.js +77 -0
  119. package/dist/core/command-generation/registry.js +8 -4
  120. package/dist/core/command-generation/types.d.ts +9 -2
  121. package/dist/core/command-generation/yaml.d.ts +20 -6
  122. package/dist/core/command-generation/yaml.js +31 -21
  123. package/dist/core/command-surface.d.ts +15 -0
  124. package/dist/core/command-surface.js +33 -0
  125. package/dist/core/completions/command-registry.js +16 -61
  126. package/dist/core/completions/generators/bash-generator.js +41 -41
  127. package/dist/core/completions/generators/fish-generator.js +7 -7
  128. package/dist/core/completions/generators/powershell-generator.js +44 -37
  129. package/dist/core/completions/generators/zsh-generator.js +33 -33
  130. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  131. package/dist/core/completions/installers/zsh-installer.js +20 -4
  132. package/dist/core/completions/templates/bash-templates.js +24 -24
  133. package/dist/core/completions/templates/fish-templates.js +38 -38
  134. package/dist/core/completions/templates/powershell-templates.js +28 -28
  135. package/dist/core/completions/templates/zsh-templates.js +39 -39
  136. package/dist/core/config-prompts.d.ts +1 -1
  137. package/dist/core/config-prompts.js +14 -1
  138. package/dist/core/config-schema.d.ts +10 -1
  139. package/dist/core/config-schema.js +67 -1
  140. package/dist/core/config.d.ts +15 -0
  141. package/dist/core/config.js +46 -5
  142. package/dist/core/file-state.js +56 -24
  143. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  144. package/dist/core/github-copilot/cloud-agent.js +497 -0
  145. package/dist/core/global-config.d.ts +16 -0
  146. package/dist/core/init.d.ts +58 -0
  147. package/dist/core/init.js +474 -73
  148. package/dist/core/legacy-cleanup.d.ts +59 -0
  149. package/dist/core/legacy-cleanup.js +253 -24
  150. package/dist/core/list.js +23 -16
  151. package/dist/core/migration.d.ts +73 -1
  152. package/dist/core/migration.js +360 -11
  153. package/dist/core/modify/conflict-detector.d.ts +26 -0
  154. package/dist/core/modify/conflict-detector.js +62 -0
  155. package/dist/core/modify/dependency-propagator.d.ts +38 -0
  156. package/dist/core/modify/dependency-propagator.js +103 -0
  157. package/dist/core/modify/history-tracker.d.ts +34 -0
  158. package/dist/core/modify/history-tracker.js +114 -0
  159. package/dist/core/modify/index.d.ts +7 -0
  160. package/dist/core/modify/index.js +7 -0
  161. package/dist/core/onboarding-commands.d.ts +30 -0
  162. package/dist/core/onboarding-commands.js +38 -0
  163. package/dist/core/openspec-root.js +18 -15
  164. package/dist/core/parsers/change-parser.d.ts +17 -2
  165. package/dist/core/parsers/change-parser.js +35 -21
  166. package/dist/core/parsers/code-fence.d.ts +15 -0
  167. package/dist/core/parsers/code-fence.js +49 -0
  168. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  169. package/dist/core/parsers/markdown-parser.js +7 -60
  170. package/dist/core/parsers/requirement-blocks.d.ts +29 -0
  171. package/dist/core/parsers/requirement-blocks.js +150 -31
  172. package/dist/core/parsers/requirement-text.d.ts +48 -0
  173. package/dist/core/parsers/requirement-text.js +98 -0
  174. package/dist/core/parsers/spec-structure.d.ts +1 -1
  175. package/dist/core/parsers/spec-structure.js +20 -30
  176. package/dist/core/profile-sync-drift.d.ts +0 -8
  177. package/dist/core/profile-sync-drift.js +49 -50
  178. package/dist/core/profiles.d.ts +2 -2
  179. package/dist/core/profiles.js +2 -2
  180. package/dist/core/project-config.d.ts +27 -18
  181. package/dist/core/project-config.js +101 -34
  182. package/dist/core/references.js +42 -4
  183. package/dist/core/relationship-health.d.ts +8 -0
  184. package/dist/core/relationship-health.js +13 -0
  185. package/dist/core/root-selection.d.ts +9 -6
  186. package/dist/core/root-selection.js +42 -5
  187. package/dist/core/schemas/base.schema.js +7 -2
  188. package/dist/core/shared/allowed-tools.d.ts +12 -0
  189. package/dist/core/shared/allowed-tools.js +12 -0
  190. package/dist/core/shared/index.d.ts +1 -0
  191. package/dist/core/shared/index.js +1 -0
  192. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  193. package/dist/core/shared/skill-content-equivalence.js +41 -0
  194. package/dist/core/shared/skill-generation.js +17 -13
  195. package/dist/core/shared/skill-paths.d.ts +13 -0
  196. package/dist/core/shared/skill-paths.js +23 -0
  197. package/dist/core/shared/tool-detection.d.ts +27 -7
  198. package/dist/core/shared/tool-detection.js +167 -37
  199. package/dist/core/shared-skill-target.d.ts +16 -0
  200. package/dist/core/shared-skill-target.js +156 -0
  201. package/dist/core/specs-apply.d.ts +80 -33
  202. package/dist/core/specs-apply.js +667 -145
  203. package/dist/core/store/git.d.ts +13 -0
  204. package/dist/core/store/git.js +24 -0
  205. package/dist/core/store/operations.js +20 -0
  206. package/dist/core/templates/skill-templates.d.ts +3 -2
  207. package/dist/core/templates/skill-templates.js +3 -2
  208. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  209. package/dist/core/templates/workflows/apply-change.js +186 -224
  210. package/dist/core/templates/workflows/archive-change.js +387 -260
  211. package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
  212. package/dist/core/templates/workflows/continue-change.js +210 -219
  213. package/dist/core/templates/workflows/explore.js +479 -443
  214. package/dist/core/templates/workflows/feedback.js +97 -97
  215. package/dist/core/templates/workflows/ff-change.js +200 -191
  216. package/dist/core/templates/workflows/modify-change.d.ts +4 -1
  217. package/dist/core/templates/workflows/modify-change.js +237 -115
  218. package/dist/core/templates/workflows/new-change.js +127 -134
  219. package/dist/core/templates/workflows/onboard.js +548 -542
  220. package/dist/core/templates/workflows/propose.js +273 -217
  221. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  222. package/dist/core/templates/workflows/store-selection.js +1 -1
  223. package/dist/core/templates/workflows/sync-specs.js +500 -273
  224. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  225. package/dist/core/templates/workflows/update-change.js +177 -0
  226. package/dist/core/templates/workflows/verify-change.js +324 -326
  227. package/dist/core/update.d.ts +40 -5
  228. package/dist/core/update.js +411 -96
  229. package/dist/core/validation/constants.d.ts +4 -1
  230. package/dist/core/validation/constants.js +4 -1
  231. package/dist/core/validation/plan-validator.d.ts +8 -0
  232. package/dist/core/validation/plan-validator.js +45 -0
  233. package/dist/core/validation/task-numbering.d.ts +16 -0
  234. package/dist/core/validation/task-numbering.js +57 -0
  235. package/dist/core/validation/validator.d.ts +27 -3
  236. package/dist/core/validation/validator.js +337 -48
  237. package/dist/core/version-check.d.ts +152 -0
  238. package/dist/core/version-check.js +731 -0
  239. package/dist/core/view.js +13 -18
  240. package/dist/prompts/searchable-multi-select.js +1 -1
  241. package/dist/telemetry/caller-detection.d.ts +27 -0
  242. package/dist/telemetry/caller-detection.js +85 -0
  243. package/dist/telemetry/caller-detection.test.d.ts +5 -0
  244. package/dist/telemetry/config.d.ts +48 -2
  245. package/dist/telemetry/config.js +113 -11
  246. package/dist/telemetry/config.test.d.ts +5 -0
  247. package/dist/telemetry/deduplication.d.ts +35 -0
  248. package/dist/telemetry/deduplication.js +82 -0
  249. package/dist/telemetry/deduplication.test.d.ts +5 -0
  250. package/dist/telemetry/identity.d.ts +62 -22
  251. package/dist/telemetry/identity.js +147 -102
  252. package/dist/telemetry/index.d.ts +126 -15
  253. package/dist/telemetry/index.js +569 -37
  254. package/dist/telemetry/sanitization.d.ts +40 -0
  255. package/dist/telemetry/sanitization.js +114 -0
  256. package/dist/telemetry/sanitization.test.d.ts +5 -0
  257. package/dist/telemetry/throttling.d.ts +20 -0
  258. package/dist/telemetry/throttling.js +70 -0
  259. package/dist/ui/welcome-screen.d.ts +9 -1
  260. package/dist/ui/welcome-screen.js +91 -38
  261. package/dist/utils/change-metadata.d.ts +43 -0
  262. package/dist/utils/change-metadata.js +123 -7
  263. package/dist/utils/change-utils.d.ts +10 -4
  264. package/dist/utils/change-utils.js +20 -13
  265. package/dist/utils/ci.d.ts +12 -0
  266. package/dist/utils/ci.js +16 -0
  267. package/dist/utils/command-references.d.ts +92 -6
  268. package/dist/utils/command-references.js +180 -7
  269. package/dist/utils/date.d.ts +8 -0
  270. package/dist/utils/date.js +13 -0
  271. package/dist/utils/file-system.d.ts +10 -0
  272. package/dist/utils/file-system.js +70 -0
  273. package/dist/utils/index.d.ts +1 -1
  274. package/dist/utils/index.js +1 -1
  275. package/dist/utils/interactive.d.ts +23 -0
  276. package/dist/utils/interactive.js +32 -0
  277. package/dist/utils/item-discovery.d.ts +19 -0
  278. package/dist/utils/item-discovery.js +30 -48
  279. package/dist/utils/shell-detection.d.ts +1 -1
  280. package/dist/utils/shell-detection.js +64 -11
  281. package/dist/utils/spec-discovery.d.ts +40 -0
  282. package/dist/utils/spec-discovery.js +115 -0
  283. package/dist/utils/task-progress.d.ts +29 -1
  284. package/dist/utils/task-progress.js +109 -15
  285. package/package.json +96 -84
  286. package/schemas/spec-driven/schema.yaml +252 -184
  287. package/schemas/spec-driven/templates/design.md +19 -19
  288. package/schemas/spec-driven/templates/plan-template.md +91 -0
  289. package/schemas/spec-driven/templates/proposal.md +29 -24
  290. package/schemas/spec-driven/templates/spec.md +11 -8
  291. package/schemas/spec-driven/templates/tasks.md +9 -9
  292. package/scripts/postinstall.js +83 -83
  293. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  294. package/dist/core/command-generation/adapters/codex.js +0 -39
  295. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  296. package/dist/core/command-generation/adapters/windsurf.js +0 -38
  297. package/dist/core/comprehension/config.d.ts +0 -20
  298. package/dist/core/comprehension/config.js +0 -23
  299. package/dist/core/comprehension/fingerprint.d.ts +0 -14
  300. package/dist/core/comprehension/fingerprint.js +0 -33
  301. package/dist/core/comprehension/index.d.ts +0 -57
  302. package/dist/core/comprehension/index.js +0 -87
  303. package/dist/core/comprehension/pass-record.d.ts +0 -29
  304. package/dist/core/comprehension/pass-record.js +0 -64
  305. package/dist/core/comprehension/stats.d.ts +0 -35
  306. package/dist/core/comprehension/stats.js +0 -133
  307. package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
  308. package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
  309. package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
  310. package/dist/core/templates/workflows/mcp-guidance.js +0 -131
  311. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
  312. package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
  313. package/dist/telemetry/caller.d.ts +0 -5
  314. package/dist/telemetry/caller.js +0 -29
  315. package/dist/telemetry/client.d.ts +0 -27
  316. package/dist/telemetry/client.js +0 -127
  317. package/dist/telemetry/command-context.d.ts +0 -13
  318. package/dist/telemetry/command-context.js +0 -59
  319. package/dist/telemetry/comprehension.d.ts +0 -44
  320. package/dist/telemetry/comprehension.js +0 -105
  321. package/dist/telemetry/content.d.ts +0 -10
  322. package/dist/telemetry/content.js +0 -56
  323. package/dist/telemetry/git-stats.d.ts +0 -12
  324. package/dist/telemetry/git-stats.js +0 -69
  325. package/dist/telemetry/identify-cache.d.ts +0 -7
  326. package/dist/telemetry/identify-cache.js +0 -47
  327. package/dist/telemetry/input.d.ts +0 -17
  328. package/dist/telemetry/input.js +0 -68
  329. package/dist/telemetry/marker.d.ts +0 -37
  330. package/dist/telemetry/marker.js +0 -67
  331. package/dist/telemetry/workflow.d.ts +0 -85
  332. package/dist/telemetry/workflow.js +0 -318
  333. package/schemas/spec-driven/templates/plan.md +0 -19
@@ -6,7 +6,23 @@ import type { Artifact, SchemaYaml, CompletedSet, BlockedArtifacts } from './typ
6
6
  export declare class ArtifactGraph {
7
7
  private artifacts;
8
8
  private schema;
9
+ /** Artifact id -> its position in the schema's `artifacts:` list. */
10
+ private declarationOrder;
9
11
  private constructor();
12
+ /**
13
+ * Orders artifact ids by where the schema declares them.
14
+ *
15
+ * The dependency graph leaves siblings tied - spec-driven's `specs` and
16
+ * `design` both require only `proposal`, so both become ready at the same
17
+ * time. Ties used to be broken alphabetically, which put `design` ahead of
18
+ * `specs` and made the CLI recommend the artifacts in an order that
19
+ * contradicted the schema's own documented sequence
20
+ * (proposal -> specs -> design -> tasks). Breaking ties by declaration order
21
+ * follows the sequence the schema author wrote, for built-in and custom
22
+ * schemas alike, and stays just as deterministic. Ids not in the schema sort
23
+ * last so the comparator stays total.
24
+ */
25
+ private compareByDeclarationOrder;
10
26
  /**
11
27
  * Creates an ArtifactGraph from a YAML file path.
12
28
  */
@@ -35,15 +51,6 @@ export declare class ArtifactGraph {
35
51
  * Gets the schema version.
36
52
  */
37
53
  getVersion(): number;
38
- /**
39
- * Builds reverse adjacency: artifact ID -> IDs of artifacts that depend on it.
40
- */
41
- private buildDependentsMap;
42
- /**
43
- * Returns all transitive downstream artifact IDs that depend on the given artifact,
44
- * sorted by build order (excludes the source artifact itself).
45
- */
46
- getTransitiveDependents(artifactId: string): string[];
47
54
  /**
48
55
  * Computes the topological build order using Kahn's algorithm.
49
56
  * Returns artifact IDs in the order they should be built.
@@ -61,5 +68,17 @@ export declare class ArtifactGraph {
61
68
  * Gets blocked artifacts and their unmet dependencies.
62
69
  */
63
70
  getBlocked(completed: CompletedSet): BlockedArtifacts;
71
+ /**
72
+ * Gets all artifacts that depend on the given artifact (direct and transitive).
73
+ * Returns artifact IDs in reverse topological order (deepest dependents first).
74
+ * Throws an error if a circular dependency is detected.
75
+ */
76
+ getDependents(artifactId: string): string[];
77
+ /**
78
+ * Gets reverse topological order for updating artifacts.
79
+ * Returns artifact IDs in the order they should be updated (dependents before dependees).
80
+ * Throws an error if a circular dependency is detected.
81
+ */
82
+ getReverseUpdateOrder(): string[];
64
83
  }
65
84
  //# sourceMappingURL=graph.d.ts.map
@@ -6,9 +6,29 @@ import { loadSchema, parseSchema } from './schema.js';
6
6
  export class ArtifactGraph {
7
7
  artifacts;
8
8
  schema;
9
+ /** Artifact id -> its position in the schema's `artifacts:` list. */
10
+ declarationOrder;
9
11
  constructor(schema) {
10
12
  this.schema = schema;
11
13
  this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
14
+ this.declarationOrder = new Map(schema.artifacts.map((a, index) => [a.id, index]));
15
+ }
16
+ /**
17
+ * Orders artifact ids by where the schema declares them.
18
+ *
19
+ * The dependency graph leaves siblings tied - spec-driven's `specs` and
20
+ * `design` both require only `proposal`, so both become ready at the same
21
+ * time. Ties used to be broken alphabetically, which put `design` ahead of
22
+ * `specs` and made the CLI recommend the artifacts in an order that
23
+ * contradicted the schema's own documented sequence
24
+ * (proposal -> specs -> design -> tasks). Breaking ties by declaration order
25
+ * follows the sequence the schema author wrote, for built-in and custom
26
+ * schemas alike, and stays just as deterministic. Ids not in the schema sort
27
+ * last so the comparator stays total.
28
+ */
29
+ compareByDeclarationOrder(a, b) {
30
+ return ((this.declarationOrder.get(a) ?? Number.MAX_SAFE_INTEGER) -
31
+ (this.declarationOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
12
32
  }
13
33
  /**
14
34
  * Creates an ArtifactGraph from a YAML file path.
@@ -55,57 +75,27 @@ export class ArtifactGraph {
55
75
  return this.schema.version;
56
76
  }
57
77
  /**
58
- * Builds reverse adjacency: artifact ID -> IDs of artifacts that depend on it.
78
+ * Computes the topological build order using Kahn's algorithm.
79
+ * Returns artifact IDs in the order they should be built.
59
80
  */
60
- buildDependentsMap() {
81
+ getBuildOrder() {
82
+ const inDegree = new Map();
61
83
  const dependents = new Map();
84
+ // Initialize all artifacts
62
85
  for (const artifact of this.artifacts.values()) {
86
+ inDegree.set(artifact.id, artifact.requires.length);
63
87
  dependents.set(artifact.id, []);
64
88
  }
89
+ // Build reverse adjacency (who depends on whom)
65
90
  for (const artifact of this.artifacts.values()) {
66
91
  for (const req of artifact.requires) {
67
92
  dependents.get(req).push(artifact.id);
68
93
  }
69
94
  }
70
- return dependents;
71
- }
72
- /**
73
- * Returns all transitive downstream artifact IDs that depend on the given artifact,
74
- * sorted by build order (excludes the source artifact itself).
75
- */
76
- getTransitiveDependents(artifactId) {
77
- if (!this.artifacts.has(artifactId)) {
78
- return [];
79
- }
80
- const dependents = this.buildDependentsMap();
81
- const collected = new Set();
82
- const queue = [artifactId];
83
- while (queue.length > 0) {
84
- const current = queue.shift();
85
- for (const dependentId of dependents.get(current) ?? []) {
86
- if (!collected.has(dependentId)) {
87
- collected.add(dependentId);
88
- queue.push(dependentId);
89
- }
90
- }
91
- }
92
- return this.getBuildOrder().filter((id) => collected.has(id));
93
- }
94
- /**
95
- * Computes the topological build order using Kahn's algorithm.
96
- * Returns artifact IDs in the order they should be built.
97
- */
98
- getBuildOrder() {
99
- const inDegree = new Map();
100
- const dependents = this.buildDependentsMap();
101
- // Initialize in-degrees
102
- for (const artifact of this.artifacts.values()) {
103
- inDegree.set(artifact.id, artifact.requires.length);
104
- }
105
- // Start with roots (in-degree 0), sorted for determinism
95
+ // Start with roots (in-degree 0), in declaration order for determinism
106
96
  const queue = [...this.artifacts.keys()]
107
97
  .filter(id => inDegree.get(id) === 0)
108
- .sort();
98
+ .sort((a, b) => this.compareByDeclarationOrder(a, b));
109
99
  const result = [];
110
100
  while (queue.length > 0) {
111
101
  const current = queue.shift();
@@ -119,7 +109,10 @@ export class ArtifactGraph {
119
109
  newlyReady.push(dep);
120
110
  }
121
111
  }
122
- queue.push(...newlyReady.sort());
112
+ // Re-sort the whole queue, not just the new arrivals: an artifact that
113
+ // has been waiting can be declared after one that just became ready.
114
+ queue.push(...newlyReady);
115
+ queue.sort((a, b) => this.compareByDeclarationOrder(a, b));
123
116
  }
124
117
  return result;
125
118
  }
@@ -137,8 +130,9 @@ export class ArtifactGraph {
137
130
  ready.push(artifact.id);
138
131
  }
139
132
  }
140
- // Sort for deterministic ordering
141
- return ready.sort();
133
+ // Declaration order: deterministic, and the first entry is the artifact the
134
+ // schema wants written next.
135
+ return ready.sort((a, b) => this.compareByDeclarationOrder(a, b));
142
136
  }
143
137
  /**
144
138
  * Checks if all artifacts in the graph are completed.
@@ -162,10 +156,67 @@ export class ArtifactGraph {
162
156
  }
163
157
  const unmetDeps = artifact.requires.filter(req => !completed.has(req));
164
158
  if (unmetDeps.length > 0) {
165
- blocked[artifact.id] = unmetDeps.sort();
159
+ blocked[artifact.id] = unmetDeps.sort((a, b) => this.compareByDeclarationOrder(a, b));
166
160
  }
167
161
  }
168
162
  return blocked;
169
163
  }
164
+ /**
165
+ * Gets all artifacts that depend on the given artifact (direct and transitive).
166
+ * Returns artifact IDs in reverse topological order (deepest dependents first).
167
+ * Throws an error if a circular dependency is detected.
168
+ */
169
+ getDependents(artifactId) {
170
+ if (!this.artifacts.has(artifactId)) {
171
+ throw new Error(`Artifact not found: ${artifactId}`);
172
+ }
173
+ const dependents = new Set();
174
+ const visited = new Set();
175
+ const visiting = new Set();
176
+ const dfs = (currentId) => {
177
+ if (visiting.has(currentId)) {
178
+ // Cycle detected
179
+ const cycle = Array.from(visiting).concat(currentId).join(' -> ');
180
+ throw new Error(`Circular dependency detected: ${cycle}`);
181
+ }
182
+ if (visited.has(currentId)) {
183
+ return;
184
+ }
185
+ visiting.add(currentId);
186
+ // Find all artifacts that depend on currentId
187
+ for (const artifact of this.artifacts.values()) {
188
+ if (artifact.requires.includes(currentId)) {
189
+ dfs(artifact.id);
190
+ dependents.add(artifact.id);
191
+ }
192
+ }
193
+ visiting.delete(currentId);
194
+ visited.add(currentId);
195
+ };
196
+ dfs(artifactId);
197
+ // Convert to array and sort by reverse build order (deepest first)
198
+ const buildOrder = this.getBuildOrder();
199
+ const dependentArray = Array.from(dependents);
200
+ // Sort by reverse position in build order (deepest dependents first)
201
+ // Use declaration order as tiebreaker for artifacts at same depth
202
+ return dependentArray.sort((a, b) => {
203
+ const posA = buildOrder.indexOf(a);
204
+ const posB = buildOrder.indexOf(b);
205
+ if (posA !== posB) {
206
+ return posB - posA; // Reverse order by depth
207
+ }
208
+ // Same depth: use declaration order (already in build order)
209
+ return this.compareByDeclarationOrder(a, b);
210
+ });
211
+ }
212
+ /**
213
+ * Gets reverse topological order for updating artifacts.
214
+ * Returns artifact IDs in the order they should be updated (dependents before dependees).
215
+ * Throws an error if a circular dependency is detected.
216
+ */
217
+ getReverseUpdateOrder() {
218
+ const buildOrder = this.getBuildOrder();
219
+ return buildOrder.reverse();
220
+ }
170
221
  }
171
222
  //# sourceMappingURL=graph.js.map
@@ -2,7 +2,7 @@ export { ArtifactSchema, SchemaYamlSchema, type Artifact, type SchemaYaml, type
2
2
  export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
3
3
  export { ArtifactGraph } from './graph.js';
4
4
  export { detectCompleted } from './state.js';
5
- export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
5
+ export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
6
6
  export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
7
7
  export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
8
8
  export type { PlanningHomeSummary, ActionContext, } from '../change-status-policy.js';
@@ -6,7 +6,7 @@ export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
6
6
  export { ArtifactGraph } from './graph.js';
7
7
  // State detection
8
8
  export { detectCompleted } from './state.js';
9
- export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
9
+ export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
10
10
  // Schema resolution
11
11
  export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, } from './resolver.js';
12
12
  // Instruction loading
@@ -32,10 +32,18 @@ export interface ChangeContext {
32
32
  planningHome?: PlanningHome;
33
33
  /** Parsed change metadata, when present */
34
34
  metadata?: ChangeMetadata;
35
+ /**
36
+ * Artifact IDs counted as complete only because the change declares
37
+ * skip_specs, not because their files exist. Kept separate so status can
38
+ * render them as skipped rather than done.
39
+ */
40
+ skippedArtifacts?: Set<string>;
35
41
  }
36
42
  export interface LoadChangeContextOptions {
37
43
  changeDir?: string;
38
44
  planningHome?: PlanningHome;
45
+ /** Pre-read project config; suppresses schema resolution's fallback config read. */
46
+ projectConfig?: ProjectConfig | null;
39
47
  }
40
48
  /**
41
49
  * Enriched instructions for creating an artifact.
@@ -73,7 +81,17 @@ export interface ArtifactInstructions {
73
81
  dependencies: DependencyInfo[];
74
82
  /** Artifacts that become available after completing this one */
75
83
  unlocks: string[];
84
+ /** True when the change declares skip_specs and this artifact is skipped */
85
+ skipped?: boolean;
86
+ /** Present only when skipped: tells the consumer not to create the artifact */
87
+ warning?: string;
76
88
  }
89
+ /**
90
+ * Warning attached to instructions for an artifact skipped via skip_specs.
91
+ * Carried in the JSON payload too, so agents driving the CLI with --json see
92
+ * the same do-not-create signal as the text output.
93
+ */
94
+ export declare const SKIP_SPECS_INSTRUCTIONS_WARNING: string;
77
95
  /**
78
96
  * Dependency information including path and description.
79
97
  */
@@ -86,6 +104,8 @@ export interface DependencyInfo {
86
104
  path: string;
87
105
  /** Description of the dependency artifact */
88
106
  description: string;
107
+ /** True when the dependency is satisfied via skip_specs - no files exist to read */
108
+ skipped?: boolean;
89
109
  }
90
110
  /**
91
111
  * Status of a single artifact in the workflow.
@@ -95,8 +115,13 @@ export interface ArtifactStatus {
95
115
  id: string;
96
116
  /** Output path pattern */
97
117
  outputPath: string;
98
- /** Status: done, ready, or blocked */
99
- status: 'done' | 'ready' | 'blocked';
118
+ /** Status: done, skipped (via skip_specs), ready, or blocked */
119
+ status: 'done' | 'skipped' | 'ready' | 'blocked';
120
+ /** Artifact IDs this artifact directly requires (its `requires` edges).
121
+ * Present for every status so callers can compute the transitive required
122
+ * set even when the artifact is already `done` (file-existence status does
123
+ * not imply its dependencies exist). */
124
+ requires: string[];
100
125
  /** Missing dependencies (only for blocked) */
101
126
  missingDeps?: string[];
102
127
  }
@@ -119,7 +144,9 @@ export interface ChangeStatus {
119
144
  nextSteps: string[];
120
145
  /** Machine-readable action constraints for agents */
121
146
  actionContext: ActionContext;
122
- /** Whether all artifacts are complete */
147
+ /** Whether all planning artifacts are complete */
148
+ isPlanningComplete: boolean;
149
+ /** Compatibility alias for isPlanningComplete */
123
150
  isComplete: boolean;
124
151
  /** Artifact IDs required before apply phase (from schema's apply.requires) */
125
152
  applyRequires: string[];
@@ -1,9 +1,9 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { getSchemaDir, resolveSchema } from './resolver.js';
3
+ import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js';
4
4
  import { ArtifactGraph } from './graph.js';
5
5
  import { detectCompleted } from './state.js';
6
- import { resolveArtifactOutputs } from './outputs.js';
6
+ import { resolveArtifactOutputPath, resolveArtifactOutputs } from './outputs.js';
7
7
  import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
9
  import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
@@ -21,6 +21,13 @@ export class TemplateLoadError extends Error {
21
21
  this.name = 'TemplateLoadError';
22
22
  }
23
23
  }
24
+ /**
25
+ * Warning attached to instructions for an artifact skipped via skip_specs.
26
+ * Carried in the JSON payload too, so agents driving the CLI with --json see
27
+ * the same do-not-create signal as the text output.
28
+ */
29
+ export const SKIP_SPECS_INSTRUCTIONS_WARNING = 'This change declares skip_specs: true in .openspec.yaml (no spec-level behavior changes), so this artifact is skipped.\n' +
30
+ 'Do not create spec files - they will conflict with that marker. If requirements now change, remove skip_specs from .openspec.yaml and rerun this command.';
24
31
  /**
25
32
  * Loads a template from a schema's templates directory.
26
33
  *
@@ -35,7 +42,14 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
35
42
  if (!schemaDir) {
36
43
  throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
37
44
  }
38
- const templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
45
+ const templatesDir = path.join(schemaDir, 'templates');
46
+ const templatePathOnDisk = path.join(templatesDir, templatePath);
47
+ try {
48
+ FileSystemUtils.assertPathWithin(templatesDir, templatePathOnDisk);
49
+ }
50
+ catch (error) {
51
+ throw new TemplateLoadError(error instanceof Error ? error.message : String(error), templatePathOnDisk);
52
+ }
39
53
  if (!fs.existsSync(templatePathOnDisk)) {
40
54
  throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
41
55
  }
@@ -66,10 +80,29 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
66
80
  const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
67
81
  const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
68
82
  metadata: metadata ?? null,
83
+ projectConfig: options.projectConfig,
69
84
  });
70
85
  const schema = resolveSchema(resolvedSchemaName, projectRoot);
71
86
  const graph = ArtifactGraph.fromSchema(schema);
72
87
  const completed = detectCompleted(graph, changeDir);
88
+ // A change that declares skip_specs has no spec deltas by design, so
89
+ // artifacts generating into specs/ count as complete; otherwise the graph
90
+ // would block their dependents (e.g. tasks) on files that must not exist.
91
+ // Tracked separately so status renders them as skipped, not done.
92
+ const skippedArtifacts = new Set();
93
+ if (metadata?.skip_specs) {
94
+ for (const artifact of graph.getAllArtifacts()) {
95
+ // A schema may write generates as './specs/...' - the globs treat that
96
+ // identically to 'specs/...', so the skip set must too, or validate
97
+ // would honor the marker while instructions tell the agent to create
98
+ // the very files the conflict gate polices.
99
+ const generates = artifact.generates.replace(/^(?:\.\/)+/, '');
100
+ if (generates.startsWith('specs/') && !completed.has(artifact.id)) {
101
+ completed.add(artifact.id);
102
+ skippedArtifacts.add(artifact.id);
103
+ }
104
+ }
105
+ }
73
106
  return {
74
107
  graph,
75
108
  completed,
@@ -79,6 +112,7 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
79
112
  projectRoot,
80
113
  ...(options.planningHome ? { planningHome: options.planningHome } : {}),
81
114
  ...(metadata ? { metadata } : {}),
115
+ ...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
82
116
  };
83
117
  }
84
118
  export function generateInstructions(context, artifactId, projectRoot, options = {}) {
@@ -87,7 +121,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
87
121
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
88
122
  }
89
123
  const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
90
- const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
124
+ const dependencies = getDependencyInfo(artifact, context.graph, context.completed, context.skippedArtifacts);
91
125
  const unlocks = getUnlockedArtifacts(context.graph, artifactId);
92
126
  // Use projectRoot from context if not explicitly provided
93
127
  const effectiveProjectRoot = projectRoot ?? context.projectRoot;
@@ -101,10 +135,12 @@ export function generateInstructions(context, artifactId, projectRoot, options =
101
135
  // If config read fails, continue without config
102
136
  }
103
137
  }
104
- // Validate rules artifact IDs if config has rules (only once per session)
138
+ // Validate rules artifact IDs if config has rules (only once per session).
139
+ // The rules map is global while each change can use a different schema, so a
140
+ // key is only "unknown" when it matches no artifact in ANY available schema.
105
141
  if (projectConfig?.rules) {
106
- const validArtifactIds = new Set(context.graph.getAllArtifacts().map((a) => a.id));
107
- const warnings = validateConfigRules(projectConfig.rules, validArtifactIds, context.schemaName);
142
+ const validArtifactIds = new Set(listSchemasWithInfo(effectiveProjectRoot ?? undefined).flatMap((s) => s.artifacts));
143
+ const warnings = validateConfigRules(projectConfig.rules, validArtifactIds);
108
144
  // Show each unique warning only once per session
109
145
  for (const warning of warnings) {
110
146
  if (!shownWarnings.has(warning)) {
@@ -115,7 +151,9 @@ export function generateInstructions(context, artifactId, projectRoot, options =
115
151
  }
116
152
  // Extract context and rules as separate fields (not prepended to template)
117
153
  const configContext = projectConfig?.context?.trim() || undefined;
118
- const rulesForArtifact = projectConfig?.rules?.[artifactId];
154
+ const rulesForArtifact = projectConfig?.rules && Object.hasOwn(projectConfig.rules, artifactId)
155
+ ? projectConfig.rules[artifactId]
156
+ : undefined;
119
157
  const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined;
120
158
  return {
121
159
  changeName: context.changeName,
@@ -124,13 +162,16 @@ export function generateInstructions(context, artifactId, projectRoot, options =
124
162
  changeDir: context.changeDir,
125
163
  planningHome: summarizePlanningHome(context.planningHome),
126
164
  outputPath: artifact.generates,
127
- resolvedOutputPath: path.join(context.changeDir, artifact.generates),
165
+ resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
128
166
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
129
167
  description: artifact.description,
130
168
  instruction: artifact.instruction,
131
169
  context: configContext,
132
170
  rules: configRules,
133
171
  ...(options.references !== undefined ? { references: options.references } : {}),
172
+ ...(context.skippedArtifacts?.has(artifact.id)
173
+ ? { skipped: true, warning: SKIP_SPECS_INSTRUCTIONS_WARNING }
174
+ : {}),
134
175
  template: templateContent,
135
176
  dependencies,
136
177
  unlocks,
@@ -139,7 +180,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
139
180
  /**
140
181
  * Gets dependency info including paths and descriptions.
141
182
  */
142
- function getDependencyInfo(artifact, graph, completed) {
183
+ function getDependencyInfo(artifact, graph, completed, skippedArtifacts) {
143
184
  return artifact.requires.map(id => {
144
185
  const depArtifact = graph.getArtifact(id);
145
186
  return {
@@ -147,11 +188,16 @@ function getDependencyInfo(artifact, graph, completed) {
147
188
  done: completed.has(id),
148
189
  path: depArtifact?.generates ?? id,
149
190
  description: depArtifact?.description ?? '',
191
+ ...(skippedArtifacts?.has(id) ? { skipped: true } : {}),
150
192
  };
151
193
  });
152
194
  }
153
195
  /**
154
196
  * Gets artifacts that become available after completing the given artifact.
197
+ *
198
+ * `getAllArtifacts()` already yields the schema's declaration order, so the list
199
+ * is returned as collected: sorting it alphabetically would have `unlocks` name
200
+ * the artifacts in a different order than `status` recommends them.
155
201
  */
156
202
  function getUnlockedArtifacts(graph, artifactId) {
157
203
  const unlocks = [];
@@ -160,7 +206,7 @@ function getUnlockedArtifacts(graph, artifactId) {
160
206
  unlocks.push(artifact.id);
161
207
  }
162
208
  }
163
- return unlocks.sort();
209
+ return unlocks;
164
210
  }
165
211
  /**
166
212
  * Formats the status of all artifacts in a change.
@@ -179,14 +225,23 @@ export function formatChangeStatus(context, options = {}) {
179
225
  const artifactStatuses = artifacts.map(artifact => {
180
226
  artifactPaths[artifact.id] = {
181
227
  outputPath: artifact.generates,
182
- resolvedOutputPath: path.join(context.changeDir, artifact.generates),
228
+ resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
183
229
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
184
230
  };
231
+ if (context.skippedArtifacts?.has(artifact.id)) {
232
+ return {
233
+ id: artifact.id,
234
+ outputPath: artifact.generates,
235
+ status: 'skipped',
236
+ requires: artifact.requires,
237
+ };
238
+ }
185
239
  if (context.completed.has(artifact.id)) {
186
240
  return {
187
241
  id: artifact.id,
188
242
  outputPath: artifact.generates,
189
243
  status: 'done',
244
+ requires: artifact.requires,
190
245
  };
191
246
  }
192
247
  if (ready.has(artifact.id)) {
@@ -194,12 +249,14 @@ export function formatChangeStatus(context, options = {}) {
194
249
  id: artifact.id,
195
250
  outputPath: artifact.generates,
196
251
  status: 'ready',
252
+ requires: artifact.requires,
197
253
  };
198
254
  }
199
255
  return {
200
256
  id: artifact.id,
201
257
  outputPath: artifact.generates,
202
258
  status: 'blocked',
259
+ requires: artifact.requires,
203
260
  missingDeps: blocked[artifact.id] ?? [],
204
261
  };
205
262
  });
@@ -215,6 +272,7 @@ export function formatChangeStatus(context, options = {}) {
215
272
  planningHome: summarizePlanningHome(context.planningHome),
216
273
  changeRoot: context.changeDir,
217
274
  artifactPaths,
275
+ isPlanningComplete: isComplete,
218
276
  isComplete,
219
277
  applyRequires,
220
278
  nextSteps: buildNextSteps({
@@ -2,6 +2,7 @@
2
2
  * Checks if a path contains glob pattern characters.
3
3
  */
4
4
  export declare function isGlobPattern(pattern: string): boolean;
5
+ export declare function resolveArtifactOutputPath(changeDir: string, generates: string): string;
5
6
  /**
6
7
  * Resolves an artifact's output path(s) to concrete files that currently exist.
7
8
  * Returns absolute file paths. Glob matches are sorted for deterministic output.
@@ -8,16 +8,66 @@ import { FileSystemUtils } from '../../utils/file-system.js';
8
8
  export function isGlobPattern(pattern) {
9
9
  return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
10
10
  }
11
+ export function resolveArtifactOutputPath(changeDir, generates) {
12
+ const outputPath = path.join(changeDir, generates);
13
+ FileSystemUtils.assertPathWithin(changeDir, outputPath);
14
+ return outputPath;
15
+ }
16
+ function assertGlobDirectoryTraversal(changeDir, currentDir, directorySegments, segmentIndex = 0, visited = new Set(), canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir), ancestors = new Set()) {
17
+ if (segmentIndex >= directorySegments.length)
18
+ return;
19
+ const canonicalDir = FileSystemUtils.canonicalizeExistingPath(currentDir);
20
+ FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalDir);
21
+ const visitKey = `${canonicalDir}\0${segmentIndex}`;
22
+ if (ancestors.has(visitKey)) {
23
+ throw new Error(`Cannot resolve artifact outputs through a linked directory cycle: ${currentDir}`);
24
+ }
25
+ if (visited.has(visitKey))
26
+ return;
27
+ visited.add(visitKey);
28
+ ancestors.add(visitKey);
29
+ try {
30
+ const segment = directorySegments[segmentIndex];
31
+ if (segment === '**') {
32
+ // `**` may consume no directory at all.
33
+ assertGlobDirectoryTraversal(changeDir, canonicalDir, directorySegments, segmentIndex + 1, visited, canonicalChangeDir, ancestors);
34
+ }
35
+ const matches = fg.sync(segment === '**' ? '*' : segment, {
36
+ cwd: canonicalDir,
37
+ onlyFiles: false,
38
+ followSymbolicLinks: false,
39
+ deep: 1,
40
+ });
41
+ for (const match of matches) {
42
+ const candidate = path.join(canonicalDir, match);
43
+ try {
44
+ if (!fs.statSync(candidate).isDirectory())
45
+ continue;
46
+ }
47
+ catch (error) {
48
+ if (error.code === 'ENOENT')
49
+ continue;
50
+ throw error;
51
+ }
52
+ const canonicalCandidate = FileSystemUtils.canonicalizeExistingPath(candidate);
53
+ FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalCandidate);
54
+ assertGlobDirectoryTraversal(changeDir, canonicalCandidate, directorySegments, segment === '**' ? segmentIndex : segmentIndex + 1, visited, canonicalChangeDir, ancestors);
55
+ }
56
+ }
57
+ finally {
58
+ ancestors.delete(visitKey);
59
+ }
60
+ }
11
61
  /**
12
62
  * Resolves an artifact's output path(s) to concrete files that currently exist.
13
63
  * Returns absolute file paths. Glob matches are sorted for deterministic output.
14
64
  */
15
65
  export function resolveArtifactOutputs(changeDir, generates) {
66
+ const outputPath = resolveArtifactOutputPath(changeDir, generates);
16
67
  if (!isGlobPattern(generates)) {
17
- const fullPath = path.join(changeDir, generates);
18
68
  try {
19
- return fs.statSync(fullPath).isFile()
20
- ? [FileSystemUtils.canonicalizeExistingPath(fullPath)]
69
+ return fs.statSync(outputPath).isFile()
70
+ ? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
21
71
  : [];
22
72
  }
23
73
  catch {
@@ -25,9 +75,21 @@ export function resolveArtifactOutputs(changeDir, generates) {
25
75
  }
26
76
  }
27
77
  const normalizedPattern = FileSystemUtils.toPosixPath(generates);
78
+ assertGlobDirectoryTraversal(changeDir, changeDir, normalizedPattern.split('/').slice(0, -1));
28
79
  const matches = fg
29
- .sync(normalizedPattern, { cwd: changeDir, onlyFiles: true, absolute: true })
30
- .map((match) => FileSystemUtils.canonicalizeExistingPath(path.normalize(match)));
80
+ .sync(normalizedPattern, {
81
+ cwd: changeDir,
82
+ onlyFiles: true,
83
+ absolute: true,
84
+ // Preserve existing support for linked artifact directories. Every
85
+ // concrete match is canonically confined below before it is returned.
86
+ followSymbolicLinks: true,
87
+ })
88
+ .map((match) => {
89
+ const normalizedMatch = path.normalize(match);
90
+ FileSystemUtils.assertPathWithin(changeDir, normalizedMatch);
91
+ return FileSystemUtils.canonicalizeExistingPath(normalizedMatch);
92
+ });
31
93
  return Array.from(new Set(matches)).sort();
32
94
  }
33
95
  /**