@izkac/forgekit 0.3.11 → 0.3.13

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 (116) hide show
  1. package/bin/forge.mjs +107 -107
  2. package/bin/forgekit.mjs +83 -83
  3. package/bin/review.mjs +81 -81
  4. package/package.json +1 -1
  5. package/scripts/prepack.mjs +78 -78
  6. package/scripts/run-tests.mjs +50 -50
  7. package/src/adr.mjs +236 -236
  8. package/src/adr.test.mjs +170 -170
  9. package/src/change.mjs +327 -234
  10. package/src/change.test.mjs +145 -83
  11. package/src/cleanup-sessions.mjs +84 -84
  12. package/src/config.mjs +103 -103
  13. package/src/defer.mjs +75 -75
  14. package/src/doctor.mjs +350 -341
  15. package/src/doctor.test.mjs +114 -114
  16. package/src/init.mjs +680 -621
  17. package/src/install.mjs +815 -815
  18. package/src/install.test.mjs +180 -180
  19. package/src/integrity-check.mjs +60 -60
  20. package/src/integrity.mjs +682 -682
  21. package/src/integrity.test.mjs +566 -566
  22. package/src/lib.mjs +143 -143
  23. package/src/models.defaults.json +41 -41
  24. package/src/new-session.mjs +99 -99
  25. package/src/openspec-overlays/README.md +19 -19
  26. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
  27. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
  28. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
  29. package/src/paths.mjs +92 -92
  30. package/src/plan-engine.mjs +321 -278
  31. package/src/plan-engine.test.mjs +447 -283
  32. package/src/preferences.defaults.json +78 -78
  33. package/src/preferences.mjs +438 -438
  34. package/src/preferences.test.mjs +174 -174
  35. package/src/record-evidence.mjs +204 -204
  36. package/src/resolve-model.mjs +312 -312
  37. package/src/resolve-model.test.mjs +194 -194
  38. package/src/review/cli.test.mjs +117 -117
  39. package/src/review/export.mjs +172 -172
  40. package/src/review/export.test.mjs +197 -197
  41. package/src/review/fixtures/valid-review.json +42 -42
  42. package/src/review/lib.mjs +894 -894
  43. package/src/review/lib.test.mjs +266 -266
  44. package/src/review/schema.json +196 -196
  45. package/src/review/signals.test.mjs +62 -62
  46. package/src/score-cli.mjs +68 -68
  47. package/src/score.mjs +568 -566
  48. package/src/score.test.mjs +366 -340
  49. package/src/session-reminder.mjs +207 -207
  50. package/src/session-status.mjs +70 -70
  51. package/src/set-models.mjs +186 -186
  52. package/src/set-phase.mjs +205 -205
  53. package/src/set-prefs.mjs +294 -294
  54. package/src/specs-sync.mjs +234 -0
  55. package/src/specs-sync.test.mjs +114 -0
  56. package/src/spine.mjs +93 -93
  57. package/src/triage-prompt.mjs +175 -175
  58. package/src/triage-prompt.test.mjs +50 -50
  59. package/src/vendor-openspec-overlays.mjs +176 -176
  60. package/src/vendor-openspec-overlays.test.mjs +62 -62
  61. package/vendor/skills/archive-to-adr/SKILL.md +149 -149
  62. package/vendor/skills/forge/SKILL.md +136 -136
  63. package/vendor/skills/forge/docs/forge.md +650 -647
  64. package/vendor/skills/forge/phases/brainstorm.md +23 -23
  65. package/vendor/skills/forge/phases/finish.md +90 -87
  66. package/vendor/skills/forge/phases/implement.md +77 -77
  67. package/vendor/skills/forge/phases/plan-openspec.md +60 -60
  68. package/vendor/skills/forge/phases/plan-specs.md +163 -117
  69. package/vendor/skills/forge/phases/review.md +25 -25
  70. package/vendor/skills/forge/phases/verify.md +124 -124
  71. package/vendor/skills/forge/references/forge-layout.md +85 -85
  72. package/vendor/skills/forge/references/pace.md +115 -115
  73. package/vendor/skills/forge/references/plan-routing.md +52 -51
  74. package/vendor/skills/forge/references/runtime-integrity.md +232 -225
  75. package/vendor/skills/forge/references/substantial-work.md +37 -37
  76. package/vendor/skills/forge/references/test-evidence.md +30 -30
  77. package/vendor/skills/forge/references/test-strategy.md +68 -68
  78. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
  79. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
  80. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
  81. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
  82. package/vendor/skills/thorough-code-review/SKILL.md +290 -290
  83. package/vendor/skills/thorough-code-review/examples.md +133 -133
  84. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
  85. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
  86. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
  87. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
  88. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
  89. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
  90. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
  91. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
  92. package/vendor/templates/adr/README.md +7 -7
  93. package/vendor/templates/adr/decisions.md +141 -141
  94. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
  95. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
  96. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
  97. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
  98. package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
  99. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
  100. package/vendor/templates/project/claude/commands/forge-build.md +17 -17
  101. package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
  102. package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
  103. package/vendor/templates/project/claude/commands/forge-status.md +16 -16
  104. package/vendor/templates/project/claude/commands/forge.md +16 -16
  105. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
  106. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
  107. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
  108. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
  109. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
  110. package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
  111. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
  112. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
  113. package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
  114. package/vendor/templates/project/cursor/commands/forge.md +16 -16
  115. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
  116. package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -3
package/src/change.mjs CHANGED
@@ -1,234 +1,327 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Specs-engine change scaffolding.
4
- *
5
- * Usage:
6
- * forge change new <name> [--cwd <path>] [--force]
7
- * forge change archive <name> [--cwd <path>] [--date YYYY-MM-DD]
8
- */
9
-
10
- import fs from 'node:fs';
11
- import path from 'node:path';
12
- import { pathToFileURL } from 'node:url';
13
- import {
14
- DEFAULT_SPECS_DIR,
15
- resolveProjectPlanEngine,
16
- } from './plan-engine.mjs';
17
-
18
- /**
19
- * @param {string[]} argv
20
- */
21
- export function parseArgs(argv) {
22
- const opts = {
23
- help: false,
24
- force: false,
25
- cwd: process.cwd(),
26
- date: /** @type {string | null} */ (null),
27
- action: /** @type {string | null} */ (null),
28
- name: /** @type {string | null} */ (null),
29
- };
30
-
31
- for (let i = 0; i < argv.length; i += 1) {
32
- const arg = argv[i];
33
- if (arg === '--help' || arg === '-h') opts.help = true;
34
- else if (arg === '--force' || arg === '-f') opts.force = true;
35
- else if (arg === '--cwd') opts.cwd = argv[++i];
36
- else if (arg === '--date') opts.date = argv[++i];
37
- else if (!opts.action && (arg === 'new' || arg === 'archive')) opts.action = arg;
38
- else if (!opts.name && !arg.startsWith('-')) opts.name = arg;
39
- else throw new Error(`Unknown argument: ${arg}`);
40
- }
41
- return opts;
42
- }
43
-
44
- function printHelp() {
45
- process.stdout.write(`Usage: forge change <new|archive> <name> [options]
46
-
47
- Scaffold or archive a change for the built-in specs planning engine.
48
-
49
- Commands:
50
- new <name> Create specs/changes/<name>/{proposal.md,tasks.md}
51
- archive <name> Move specs/changes/<name> → changes/archive/YYYY-MM-DD-<name>
52
-
53
- Options:
54
- --cwd <path> Project root (default: cwd)
55
- --date YYYY-MM-DD Archive date prefix (default: today UTC)
56
- --force, -f Overwrite existing proposal/tasks on new
57
- --help
58
-
59
- Requires \`.forge/config.json\` → plan.engine: specs (or run \`forge init --no-openspec\`).
60
- `);
61
- }
62
-
63
- /**
64
- * @param {string} name
65
- */
66
- export function assertChangeName(name) {
67
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
68
- throw new Error(
69
- `Change name must be kebab-case (got: ${name}). Example: add-stripe-refunds`,
70
- );
71
- }
72
- return name;
73
- }
74
-
75
- const PROPOSAL_TMPL = (title) => `# ${title}
76
-
77
- ## Why
78
-
79
- One or two paragraphs: problem / pressure.
80
-
81
- ## What Changes
82
-
83
- -
84
-
85
- ## Impact
86
-
87
- Affected code/areas, risks, migration notes.
88
- `;
89
-
90
- const TASKS_TMPL = `# Tasks
91
-
92
- ## 1. First group
93
- - [ ] 1.1 Bite-sized task — exact files, expected tests
94
-
95
- ## 2. Second group
96
- - [ ] 2.1 …
97
- `;
98
-
99
- /**
100
- * @param {string} cwd
101
- * @param {string} name
102
- * @param {{ force?: boolean }} [opts]
103
- */
104
- export function createSpecsChange(cwd, name, opts = {}) {
105
- assertChangeName(name);
106
- const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
107
- if (engine.engine !== 'specs') {
108
- throw new Error(
109
- `Project plan engine is "${engine.engine}", not "specs". ` +
110
- `Use \`openspec-propose\` / \`/opsx:propose\` for OpenSpec projects, ` +
111
- `or \`forge init --no-openspec\` to switch.`,
112
- );
113
- }
114
- const dir = engine.dir || DEFAULT_SPECS_DIR;
115
- const changeDir = path.join(cwd, dir, 'changes', name);
116
- fs.mkdirSync(changeDir, { recursive: true });
117
-
118
- /** @type {{ file: string, status: string }[]} */
119
- const files = [];
120
- const write = (rel, body) => {
121
- const dest = path.join(changeDir, rel);
122
- if (fs.existsSync(dest) && !opts.force) {
123
- files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'skipped' });
124
- return;
125
- }
126
- fs.writeFileSync(dest, body, 'utf8');
127
- files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'written' });
128
- };
129
-
130
- const title = name
131
- .split('-')
132
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
133
- .join(' ');
134
- write('proposal.md', PROPOSAL_TMPL(title));
135
- write('tasks.md', TASKS_TMPL);
136
-
137
- return { dir, changeDir, name, files };
138
- }
139
-
140
- /**
141
- * @param {string} cwd
142
- * @param {string} name
143
- * @param {{ date?: string | null }} [opts]
144
- */
145
- export function archiveSpecsChange(cwd, name, opts = {}) {
146
- assertChangeName(name);
147
- const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
148
- if (engine.engine !== 'specs') {
149
- throw new Error(
150
- `Project plan engine is "${engine.engine}", not "specs". ` +
151
- `Use \`openspec archive\` / \`/opsx:archive\` for OpenSpec projects.`,
152
- );
153
- }
154
- const dir = engine.dir || DEFAULT_SPECS_DIR;
155
- const src = path.join(cwd, dir, 'changes', name);
156
- if (!fs.existsSync(src)) {
157
- throw new Error(`Change not found: ${dir}/changes/${name}`);
158
- }
159
- const date =
160
- opts.date ||
161
- new Date().toISOString().slice(0, 10);
162
- if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
163
- throw new Error(`Invalid --date (want YYYY-MM-DD): ${date}`);
164
- }
165
- const archiveParent = path.join(cwd, dir, 'changes', 'archive');
166
- fs.mkdirSync(archiveParent, { recursive: true });
167
- const destName = `${date}-${name}`;
168
- const dest = path.join(archiveParent, destName);
169
- if (fs.existsSync(dest)) {
170
- throw new Error(`Archive already exists: ${dir}/changes/archive/${destName}`);
171
- }
172
- fs.renameSync(src, dest);
173
- return {
174
- dir,
175
- from: `${dir}/changes/${name}`,
176
- to: `${dir}/changes/archive/${destName}`,
177
- };
178
- }
179
-
180
- /**
181
- * @param {string[]} argv
182
- */
183
- export function runChange(argv) {
184
- const opts = parseArgs(argv);
185
- if (opts.help || !opts.action) {
186
- printHelp();
187
- return opts.help ? 0 : 1;
188
- }
189
- if (!opts.name) {
190
- process.stderr.write('Missing change name.\n');
191
- printHelp();
192
- return 1;
193
- }
194
-
195
- if (opts.action === 'new') {
196
- const result = createSpecsChange(opts.cwd, opts.name, { force: opts.force });
197
- process.stdout.write(
198
- `Created specs change "${result.name}" under ${result.dir}/changes/${result.name}/\n`,
199
- );
200
- for (const f of result.files) {
201
- process.stdout.write(` ${f.status.padEnd(8)} ${f.file}\n`);
202
- }
203
- process.stdout.write(
204
- `\nNext: edit proposal.md / tasks.md, then:\n` +
205
- ` forge phase plan --plan-type specs --openspec ${result.name}\n`,
206
- );
207
- return 0;
208
- }
209
-
210
- if (opts.action === 'archive') {
211
- const result = archiveSpecsChange(opts.cwd, opts.name, { date: opts.date });
212
- process.stdout.write(`Archived ${result.from} ${result.to}\n`);
213
- process.stdout.write(
214
- `If ADRs are enabled, run archive-to-adr on the archived folder.\n`,
215
- );
216
- return 0;
217
- }
218
-
219
- process.stderr.write(`Unknown action: ${opts.action}\n`);
220
- return 1;
221
- }
222
-
223
- const isMain =
224
- process.argv[1] &&
225
- import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
226
-
227
- if (isMain) {
228
- try {
229
- process.exitCode = runChange(process.argv.slice(2));
230
- } catch (err) {
231
- process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
232
- process.exitCode = 1;
233
- }
234
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Specs-engine change scaffolding.
4
+ *
5
+ * Usage:
6
+ * forge change new <name> [--capability <id>]… [--cwd <path>] [--force]
7
+ * forge change archive <name> [--cwd <path>] [--date YYYY-MM-DD] [--no-sync]
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import {
14
+ DEFAULT_SPECS_DIR,
15
+ resolveProjectPlanEngine,
16
+ } from './plan-engine.mjs';
17
+ import { deltaSpecTemplate, mergeChangeDeltas } from './specs-sync.mjs';
18
+
19
+ /**
20
+ * @param {string[]} argv
21
+ */
22
+ export function parseArgs(argv) {
23
+ const opts = {
24
+ help: false,
25
+ force: false,
26
+ noSync: false,
27
+ cwd: process.cwd(),
28
+ date: /** @type {string | null} */ (null),
29
+ action: /** @type {string | null} */ (null),
30
+ name: /** @type {string | null} */ (null),
31
+ capabilities: /** @type {string[]} */ ([]),
32
+ };
33
+
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const arg = argv[i];
36
+ if (arg === '--help' || arg === '-h') opts.help = true;
37
+ else if (arg === '--force' || arg === '-f') opts.force = true;
38
+ else if (arg === '--no-sync') opts.noSync = true;
39
+ else if (arg === '--cwd') opts.cwd = argv[++i];
40
+ else if (arg === '--date') opts.date = argv[++i];
41
+ else if (arg === '--capability' || arg === '--cap') {
42
+ const cap = argv[++i];
43
+ if (!cap) throw new Error(`${arg} requires a capability id`);
44
+ opts.capabilities.push(cap);
45
+ } else if (!opts.action && (arg === 'new' || arg === 'archive')) opts.action = arg;
46
+ else if (!opts.name && !arg.startsWith('-')) opts.name = arg;
47
+ else throw new Error(`Unknown argument: ${arg}`);
48
+ }
49
+ return opts;
50
+ }
51
+
52
+ function printHelp() {
53
+ process.stdout.write(`Usage: forge change <new|archive> <name> [options]
54
+
55
+ Scaffold or archive a change for the built-in specs planning engine.
56
+ Layout matches OpenSpec: proposal / design / tasks / delta specs under
57
+ \`changes/<name>/specs/<capability>/spec.md\`, main catalog at
58
+ \`<plan.dir>/specs/\`.
59
+
60
+ Commands:
61
+ new <name> Create <plan.dir>/changes/<name>/{proposal,design,tasks}.md
62
+ plus delta stubs for each --capability
63
+ archive <name> Merge deltas → <plan.dir>/specs/, then move change to
64
+ changes/archive/YYYY-MM-DD-<name>
65
+
66
+ Options:
67
+ --capability <id> Delta capability to stub (repeatable). kebab-case id
68
+ matching OpenSpec domains (e.g. auth, payments)
69
+ --cwd <path> Project root (default: cwd)
70
+ --date YYYY-MM-DD Archive date prefix (default: today UTC)
71
+ --no-sync Archive without merging deltas into the main catalog
72
+ --force, -f Overwrite existing scaffold files on new
73
+ --help
74
+
75
+ Requires \`.forge/config.json\` plan.engine: specs (or run \`forge init --no-openspec\`).
76
+ Engine root is \`plan.dir\` (default \`specs\`; use \`openspec\` to reuse an
77
+ OpenSpec tree).
78
+ `);
79
+ }
80
+
81
+ /**
82
+ * @param {string} name
83
+ */
84
+ export function assertChangeName(name) {
85
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
86
+ throw new Error(
87
+ `Change name must be kebab-case (got: ${name}). Example: add-stripe-refunds`,
88
+ );
89
+ }
90
+ return name;
91
+ }
92
+
93
+ /**
94
+ * @param {string} capability
95
+ */
96
+ export function assertCapabilityName(capability) {
97
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(capability)) {
98
+ throw new Error(
99
+ `Capability id must be kebab-case (got: ${capability}). Example: auth`,
100
+ );
101
+ }
102
+ return capability;
103
+ }
104
+
105
+ const PROPOSAL_TMPL = (title, capabilities) => {
106
+ const caps =
107
+ capabilities.length > 0
108
+ ? capabilities.map((c) => `- \`${c}\`: … (delta: \`specs/${c}/spec.md\`)`).join('\n')
109
+ : `- \`<capability>\`: add with \`forge change new … --capability <id>\` or create \`specs/<id>/spec.md\``;
110
+ return `# ${title}
111
+
112
+ ## Why
113
+
114
+ One or two paragraphs: problem / pressure.
115
+
116
+ ## What Changes
117
+
118
+ -
119
+
120
+ ## Capabilities
121
+
122
+ ${caps}
123
+
124
+ ## Impact
125
+
126
+ Affected code/areas, risks, migration notes.
127
+ `;
128
+ };
129
+
130
+ const DESIGN_TMPL = `# Design
131
+
132
+ ## Context
133
+
134
+
135
+
136
+ ## Decisions
137
+
138
+ - Decision: …
139
+ - Alternatives considered: …
140
+ - Rationale: …
141
+
142
+ ## Risks / Trade-offs
143
+
144
+ -
145
+ `;
146
+
147
+ const TASKS_TMPL = `# Tasks
148
+
149
+ ## 1. First group
150
+ - [ ] 1.1 Bite-sized task exact files, expected tests
151
+
152
+ ## 2. Second group
153
+ - [ ] 2.1 …
154
+ `;
155
+
156
+ /**
157
+ * @param {string} cwd
158
+ * @param {string} name
159
+ * @param {{ force?: boolean, capabilities?: string[] }} [opts]
160
+ */
161
+ export function createSpecsChange(cwd, name, opts = {}) {
162
+ assertChangeName(name);
163
+ const capabilities = (opts.capabilities ?? []).map(assertCapabilityName);
164
+ const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
165
+ if (engine.engine !== 'specs') {
166
+ throw new Error(
167
+ `Project plan engine is "${engine.engine}", not "specs". ` +
168
+ `Use \`openspec-propose\` / \`/opsx:propose\` for OpenSpec projects, ` +
169
+ `or \`forge init --no-openspec\` to switch.`,
170
+ );
171
+ }
172
+ const dir = engine.dir || DEFAULT_SPECS_DIR;
173
+ const changeDir = path.join(cwd, dir, 'changes', name);
174
+ fs.mkdirSync(changeDir, { recursive: true });
175
+ fs.mkdirSync(path.join(changeDir, 'specs'), { recursive: true });
176
+
177
+ /** @type {{ file: string, status: string }[]} */
178
+ const files = [];
179
+ const write = (rel, body) => {
180
+ const dest = path.join(changeDir, rel);
181
+ if (fs.existsSync(dest) && !opts.force) {
182
+ files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'skipped' });
183
+ return;
184
+ }
185
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
186
+ fs.writeFileSync(dest, body, 'utf8');
187
+ files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'written' });
188
+ };
189
+
190
+ const title = name
191
+ .split('-')
192
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
193
+ .join(' ');
194
+ write('proposal.md', PROPOSAL_TMPL(title, capabilities));
195
+ write('design.md', DESIGN_TMPL);
196
+ write('tasks.md', TASKS_TMPL);
197
+
198
+ for (const cap of capabilities) {
199
+ write(`specs/${cap}/spec.md`, deltaSpecTemplate(cap));
200
+ }
201
+
202
+ return { dir, changeDir, name, files, capabilities };
203
+ }
204
+
205
+ /**
206
+ * @param {string} cwd
207
+ * @param {string} name
208
+ * @param {{ date?: string | null, noSync?: boolean }} [opts]
209
+ */
210
+ export function archiveSpecsChange(cwd, name, opts = {}) {
211
+ assertChangeName(name);
212
+ const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
213
+ if (engine.engine !== 'specs') {
214
+ throw new Error(
215
+ `Project plan engine is "${engine.engine}", not "specs". ` +
216
+ `Use \`openspec archive\` / \`/opsx:archive\` for OpenSpec projects.`,
217
+ );
218
+ }
219
+ const dir = engine.dir || DEFAULT_SPECS_DIR;
220
+ const src = path.join(cwd, dir, 'changes', name);
221
+ if (!fs.existsSync(src)) {
222
+ throw new Error(`Change not found: ${dir}/changes/${name}`);
223
+ }
224
+ const date = opts.date || new Date().toISOString().slice(0, 10);
225
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
226
+ throw new Error(`Invalid --date (want YYYY-MM-DD): ${date}`);
227
+ }
228
+
229
+ /** @type {{ capability: string, status: string, file: string }[]} */
230
+ let synced = [];
231
+ if (!opts.noSync) {
232
+ const mainSpecsDir = path.join(cwd, dir, 'specs');
233
+ synced = mergeChangeDeltas(src, mainSpecsDir);
234
+ }
235
+
236
+ const archiveParent = path.join(cwd, dir, 'changes', 'archive');
237
+ fs.mkdirSync(archiveParent, { recursive: true });
238
+ const destName = `${date}-${name}`;
239
+ const dest = path.join(archiveParent, destName);
240
+ if (fs.existsSync(dest)) {
241
+ throw new Error(`Archive already exists: ${dir}/changes/archive/${destName}`);
242
+ }
243
+ fs.renameSync(src, dest);
244
+ return {
245
+ dir,
246
+ from: `${dir}/changes/${name}`,
247
+ to: `${dir}/changes/archive/${destName}`,
248
+ synced,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * @param {string[]} argv
254
+ */
255
+ export function runChange(argv) {
256
+ const opts = parseArgs(argv);
257
+ if (opts.help || !opts.action) {
258
+ printHelp();
259
+ return opts.help ? 0 : 1;
260
+ }
261
+ if (!opts.name) {
262
+ process.stderr.write('Missing change name.\n');
263
+ printHelp();
264
+ return 1;
265
+ }
266
+
267
+ if (opts.action === 'new') {
268
+ const result = createSpecsChange(opts.cwd, opts.name, {
269
+ force: opts.force,
270
+ capabilities: opts.capabilities,
271
+ });
272
+ process.stdout.write(
273
+ `Created specs change "${result.name}" under ${result.dir}/changes/${result.name}/\n`,
274
+ );
275
+ for (const f of result.files) {
276
+ process.stdout.write(` ${f.status.padEnd(8)} ${f.file}\n`);
277
+ }
278
+ if (result.capabilities.length === 0) {
279
+ process.stdout.write(
280
+ `\nNo delta stubs yet — add with:\n` +
281
+ ` forge change new ${result.name} --capability <domain> --force\n` +
282
+ `or create ${result.dir}/changes/${result.name}/specs/<domain>/spec.md\n`,
283
+ );
284
+ }
285
+ process.stdout.write(
286
+ `\nNext: edit proposal.md / design.md / tasks.md / specs/, then:\n` +
287
+ ` forge phase plan --plan-type specs --openspec ${result.name}\n`,
288
+ );
289
+ return 0;
290
+ }
291
+
292
+ if (opts.action === 'archive') {
293
+ const result = archiveSpecsChange(opts.cwd, opts.name, {
294
+ date: opts.date,
295
+ noSync: opts.noSync,
296
+ });
297
+ for (const s of result.synced) {
298
+ process.stdout.write(
299
+ ` sync ${s.status.padEnd(8)} ${result.dir}/specs/${s.capability}/spec.md\n`,
300
+ );
301
+ }
302
+ if (!opts.noSync && result.synced.length === 0) {
303
+ process.stdout.write(' sync (no delta specs to merge)\n');
304
+ }
305
+ process.stdout.write(`Archived ${result.from} → ${result.to}\n`);
306
+ process.stdout.write(
307
+ `If ADRs are enabled, run archive-to-adr on the archived folder.\n`,
308
+ );
309
+ return 0;
310
+ }
311
+
312
+ process.stderr.write(`Unknown action: ${opts.action}\n`);
313
+ return 1;
314
+ }
315
+
316
+ const isMain =
317
+ process.argv[1] &&
318
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
319
+
320
+ if (isMain) {
321
+ try {
322
+ process.exitCode = runChange(process.argv.slice(2));
323
+ } catch (err) {
324
+ process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
325
+ process.exitCode = 1;
326
+ }
327
+ }