@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
@@ -1,176 +1,176 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Re-apply Forge overlays to vendor OpenSpec skills and opsx:apply commands.
4
- *
5
- * forge overlay
6
- */
7
-
8
- import fs from 'node:fs';
9
- import path from 'node:path';
10
- import { fileURLToPath, pathToFileURL } from 'node:url';
11
-
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
-
14
- export function resolveOverlayDir() {
15
- const fromEnv = process.env.FORGEKIT_ROOT
16
- ? path.join(process.env.FORGEKIT_ROOT, 'packages', 'cli', 'src', 'openspec-overlays')
17
- : null;
18
- const nextToSrc = path.join(__dirname, 'openspec-overlays');
19
- for (const c of [fromEnv, nextToSrc].filter(Boolean)) {
20
- if (c && fs.existsSync(c)) return c;
21
- }
22
- throw new Error('openspec-overlays directory not found');
23
- }
24
-
25
- const OVERLAY_START = '<!-- forgekit:openspec-overlay:start -->';
26
- const OVERLAY_END = '<!-- forgekit:openspec-overlay:end -->';
27
- const LEGACY_START = '<!-- janus-forge:openspec-overlay:start -->';
28
- const LEGACY_END = '<!-- janus-forge:openspec-overlay:end -->';
29
- const STEP_MARKER = 'REQUIRED (Forge):';
30
- const LEGACY_STEP_MARKER = 'REQUIRED (Janus Forge):';
31
-
32
- const AGENT_PATHS = {
33
- cursor: {
34
- skillRoot: '.cursor/skills/forge',
35
- opsxApply: '.cursor/commands/opsx-apply.md',
36
- applySkill: '.cursor/skills/openspec-apply-change/SKILL.md',
37
- },
38
- claude: {
39
- skillRoot: '.claude/skills/forge',
40
- opsxApply: '.claude/commands/opsx/apply.md',
41
- applySkill: '.claude/skills/openspec-apply-change/SKILL.md',
42
- },
43
- codex: {
44
- skillRoot: '.codex/skills/forge',
45
- opsxApply: null,
46
- applySkill: '.codex/skills/openspec-apply-change/SKILL.md',
47
- },
48
- };
49
-
50
- function readOverlay(overlayDir, name) {
51
- return fs.readFileSync(path.join(overlayDir, name), 'utf8');
52
- }
53
-
54
- export function renderOverlay(template, agentKey) {
55
- const root = AGENT_PATHS[agentKey].skillRoot;
56
- return template
57
- .replaceAll('{{PHASES_IMPLEMENT}}', `${root}/phases/implement.md`)
58
- .replaceAll('{{PHASES_VERIFY}}', `${root}/phases/verify.md`)
59
- .replaceAll('{{PHASES_REVIEW}}', `${root}/phases/review.md`)
60
- .replaceAll('{{SKILL_ROOT}}', root);
61
- }
62
-
63
- export function stripOverlayBlock(content) {
64
- let out = content;
65
- for (const [start, end] of [
66
- [OVERLAY_START, OVERLAY_END],
67
- [LEGACY_START, LEGACY_END],
68
- ]) {
69
- const blockRe = new RegExp(
70
- `(\\r?\\n---\\r?\\n\\r?\\n)?${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\r?\\n?`,
71
- 'g',
72
- );
73
- out = out.replace(blockRe, '\n');
74
- }
75
- return `${out.replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
76
- }
77
-
78
- function escapeRegExp(s) {
79
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
- }
81
-
82
- export function applySkillFooter(content, agentKey, overlayDir = resolveOverlayDir()) {
83
- const footer = renderOverlay(
84
- readOverlay(overlayDir, 'openspec-apply-change-footer.md'),
85
- agentKey,
86
- );
87
- let out = stripOverlayBlock(content);
88
- if (!out.endsWith('\n')) out += '\n';
89
- return out + footer;
90
- }
91
-
92
- const STEP6_RE =
93
- /(6\. \*\*Implement tasks \(loop until done or blocked\)\*\*\r?\n\r?\n)([\s\S]*?)(\r?\n\r?\n \*\*Pause if:\*\*)/;
94
-
95
- export function patchOpsxApplyContent(content, agentKey, overlayDir = resolveOverlayDir()) {
96
- if (!STEP6_RE.test(content)) {
97
- return { content, status: 'no-match' };
98
- }
99
-
100
- const implementStep = renderOverlay(
101
- readOverlay(overlayDir, 'opsx-apply-implement-step.md'),
102
- agentKey,
103
- );
104
- let out = content.replace(STEP6_RE, `$1${implementStep}$3`);
105
-
106
- const completionLine = renderOverlay(
107
- readOverlay(overlayDir, 'opsx-apply-completion-step.md'),
108
- agentKey,
109
- ).trimEnd();
110
-
111
- if (/ - If all done:.*\r?\n/.test(out)) {
112
- out = out.replace(/ - If all done:.*\r?\n/, `${completionLine}\r\n`);
113
- } else {
114
- out = out.replace(
115
- /(7\. \*\*On completion or pause, show status\*\*\r?\n\r?\n Display:\r?\n)/,
116
- `$1${completionLine}\r\n`,
117
- );
118
- }
119
-
120
- const already =
121
- content.includes(STEP_MARKER) || content.includes(LEGACY_STEP_MARKER);
122
- const status = already ? 're-patched' : 'patched';
123
- return { content: out, status };
124
- }
125
-
126
- function patchFile(relPath, transform, label) {
127
- const abs = path.join(process.cwd(), relPath);
128
- if (!fs.existsSync(abs)) {
129
- process.stdout.write(`skip (missing): ${relPath}\n`);
130
- return 'missing';
131
- }
132
- const before = fs.readFileSync(abs, 'utf8');
133
- const after = transform(before);
134
- if (after === before) {
135
- process.stdout.write(`unchanged: ${relPath}\n`);
136
- return 'unchanged';
137
- }
138
- fs.writeFileSync(abs, after, 'utf8');
139
- process.stdout.write(`${label}: ${relPath}\n`);
140
- return label;
141
- }
142
-
143
- function main() {
144
- const overlayDir = resolveOverlayDir();
145
-
146
- for (const [agentKey, paths] of Object.entries(AGENT_PATHS)) {
147
- if (paths.opsxApply) {
148
- patchFile(
149
- paths.opsxApply,
150
- (content) => patchOpsxApplyContent(content, agentKey, overlayDir).content,
151
- 'patched-opsx-apply',
152
- );
153
- }
154
-
155
- patchFile(
156
- paths.applySkill,
157
- (content) => applySkillFooter(content, agentKey, overlayDir),
158
- 'patched-skill-footer',
159
- );
160
- }
161
-
162
- process.stdout.write('\nDone. Forge-owned /forge:apply commands are not modified.\n');
163
- process.stdout.write('After OpenSpec upgrade: forge overlay\n');
164
- }
165
-
166
- const isDirect =
167
- process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
168
-
169
- if (isDirect) {
170
- try {
171
- main();
172
- } catch (err) {
173
- process.stderr.write(`${err.message || err}\n`);
174
- process.exit(1);
175
- }
176
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Re-apply Forge overlays to vendor OpenSpec skills and opsx:apply commands.
4
+ *
5
+ * forge overlay
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath, pathToFileURL } from 'node:url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ export function resolveOverlayDir() {
15
+ const fromEnv = process.env.FORGEKIT_ROOT
16
+ ? path.join(process.env.FORGEKIT_ROOT, 'packages', 'cli', 'src', 'openspec-overlays')
17
+ : null;
18
+ const nextToSrc = path.join(__dirname, 'openspec-overlays');
19
+ for (const c of [fromEnv, nextToSrc].filter(Boolean)) {
20
+ if (c && fs.existsSync(c)) return c;
21
+ }
22
+ throw new Error('openspec-overlays directory not found');
23
+ }
24
+
25
+ const OVERLAY_START = '<!-- forgekit:openspec-overlay:start -->';
26
+ const OVERLAY_END = '<!-- forgekit:openspec-overlay:end -->';
27
+ const LEGACY_START = '<!-- janus-forge:openspec-overlay:start -->';
28
+ const LEGACY_END = '<!-- janus-forge:openspec-overlay:end -->';
29
+ const STEP_MARKER = 'REQUIRED (Forge):';
30
+ const LEGACY_STEP_MARKER = 'REQUIRED (Janus Forge):';
31
+
32
+ const AGENT_PATHS = {
33
+ cursor: {
34
+ skillRoot: '.cursor/skills/forge',
35
+ opsxApply: '.cursor/commands/opsx-apply.md',
36
+ applySkill: '.cursor/skills/openspec-apply-change/SKILL.md',
37
+ },
38
+ claude: {
39
+ skillRoot: '.claude/skills/forge',
40
+ opsxApply: '.claude/commands/opsx/apply.md',
41
+ applySkill: '.claude/skills/openspec-apply-change/SKILL.md',
42
+ },
43
+ codex: {
44
+ skillRoot: '.codex/skills/forge',
45
+ opsxApply: null,
46
+ applySkill: '.codex/skills/openspec-apply-change/SKILL.md',
47
+ },
48
+ };
49
+
50
+ function readOverlay(overlayDir, name) {
51
+ return fs.readFileSync(path.join(overlayDir, name), 'utf8');
52
+ }
53
+
54
+ export function renderOverlay(template, agentKey) {
55
+ const root = AGENT_PATHS[agentKey].skillRoot;
56
+ return template
57
+ .replaceAll('{{PHASES_IMPLEMENT}}', `${root}/phases/implement.md`)
58
+ .replaceAll('{{PHASES_VERIFY}}', `${root}/phases/verify.md`)
59
+ .replaceAll('{{PHASES_REVIEW}}', `${root}/phases/review.md`)
60
+ .replaceAll('{{SKILL_ROOT}}', root);
61
+ }
62
+
63
+ export function stripOverlayBlock(content) {
64
+ let out = content;
65
+ for (const [start, end] of [
66
+ [OVERLAY_START, OVERLAY_END],
67
+ [LEGACY_START, LEGACY_END],
68
+ ]) {
69
+ const blockRe = new RegExp(
70
+ `(\\r?\\n---\\r?\\n\\r?\\n)?${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\r?\\n?`,
71
+ 'g',
72
+ );
73
+ out = out.replace(blockRe, '\n');
74
+ }
75
+ return `${out.replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
76
+ }
77
+
78
+ function escapeRegExp(s) {
79
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
+ }
81
+
82
+ export function applySkillFooter(content, agentKey, overlayDir = resolveOverlayDir()) {
83
+ const footer = renderOverlay(
84
+ readOverlay(overlayDir, 'openspec-apply-change-footer.md'),
85
+ agentKey,
86
+ );
87
+ let out = stripOverlayBlock(content);
88
+ if (!out.endsWith('\n')) out += '\n';
89
+ return out + footer;
90
+ }
91
+
92
+ const STEP6_RE =
93
+ /(6\. \*\*Implement tasks \(loop until done or blocked\)\*\*\r?\n\r?\n)([\s\S]*?)(\r?\n\r?\n \*\*Pause if:\*\*)/;
94
+
95
+ export function patchOpsxApplyContent(content, agentKey, overlayDir = resolveOverlayDir()) {
96
+ if (!STEP6_RE.test(content)) {
97
+ return { content, status: 'no-match' };
98
+ }
99
+
100
+ const implementStep = renderOverlay(
101
+ readOverlay(overlayDir, 'opsx-apply-implement-step.md'),
102
+ agentKey,
103
+ );
104
+ let out = content.replace(STEP6_RE, `$1${implementStep}$3`);
105
+
106
+ const completionLine = renderOverlay(
107
+ readOverlay(overlayDir, 'opsx-apply-completion-step.md'),
108
+ agentKey,
109
+ ).trimEnd();
110
+
111
+ if (/ - If all done:.*\r?\n/.test(out)) {
112
+ out = out.replace(/ - If all done:.*\r?\n/, `${completionLine}\r\n`);
113
+ } else {
114
+ out = out.replace(
115
+ /(7\. \*\*On completion or pause, show status\*\*\r?\n\r?\n Display:\r?\n)/,
116
+ `$1${completionLine}\r\n`,
117
+ );
118
+ }
119
+
120
+ const already =
121
+ content.includes(STEP_MARKER) || content.includes(LEGACY_STEP_MARKER);
122
+ const status = already ? 're-patched' : 'patched';
123
+ return { content: out, status };
124
+ }
125
+
126
+ function patchFile(relPath, transform, label) {
127
+ const abs = path.join(process.cwd(), relPath);
128
+ if (!fs.existsSync(abs)) {
129
+ process.stdout.write(`skip (missing): ${relPath}\n`);
130
+ return 'missing';
131
+ }
132
+ const before = fs.readFileSync(abs, 'utf8');
133
+ const after = transform(before);
134
+ if (after === before) {
135
+ process.stdout.write(`unchanged: ${relPath}\n`);
136
+ return 'unchanged';
137
+ }
138
+ fs.writeFileSync(abs, after, 'utf8');
139
+ process.stdout.write(`${label}: ${relPath}\n`);
140
+ return label;
141
+ }
142
+
143
+ function main() {
144
+ const overlayDir = resolveOverlayDir();
145
+
146
+ for (const [agentKey, paths] of Object.entries(AGENT_PATHS)) {
147
+ if (paths.opsxApply) {
148
+ patchFile(
149
+ paths.opsxApply,
150
+ (content) => patchOpsxApplyContent(content, agentKey, overlayDir).content,
151
+ 'patched-opsx-apply',
152
+ );
153
+ }
154
+
155
+ patchFile(
156
+ paths.applySkill,
157
+ (content) => applySkillFooter(content, agentKey, overlayDir),
158
+ 'patched-skill-footer',
159
+ );
160
+ }
161
+
162
+ process.stdout.write('\nDone. Forge-owned /forge:apply commands are not modified.\n');
163
+ process.stdout.write('After OpenSpec upgrade: forge overlay\n');
164
+ }
165
+
166
+ const isDirect =
167
+ process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
168
+
169
+ if (isDirect) {
170
+ try {
171
+ main();
172
+ } catch (err) {
173
+ process.stderr.write(`${err.message || err}\n`);
174
+ process.exit(1);
175
+ }
176
+ }
@@ -1,62 +1,62 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- applySkillFooter,
5
- patchOpsxApplyContent,
6
- stripOverlayBlock,
7
- } from './vendor-openspec-overlays.mjs';
8
-
9
- const VANILLA = `---
10
- name: test
11
- ---
12
-
13
- 6. **Implement tasks (loop until done or blocked)**
14
-
15
- For each pending task:
16
- - Show which task is being worked on
17
- - Make the code changes required
18
- - Keep changes minimal and focused
19
- - Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
20
- - Continue to next task
21
-
22
- **Pause if:**
23
- - Task is unclear
24
-
25
- 7. **On completion or pause, show status**
26
-
27
- Display:
28
- - Tasks completed this session
29
- - Overall progress: "N/M tasks complete"
30
- - If all done: suggest archive
31
- `;
32
-
33
- test('patchOpsxApplyContent injects Forge implement step', () => {
34
- const { content, status } = patchOpsxApplyContent(VANILLA, 'cursor');
35
- assert.equal(status, 'patched');
36
- assert.match(content, /REQUIRED \(Forge\):/);
37
- assert.match(content, /\.cursor\/skills\/forge\/phases\/implement\.md/);
38
- assert.match(content, /Dispatch \*\*implementer\*\* subagent/);
39
- assert.doesNotMatch(content, /Make the code changes required/);
40
- });
41
-
42
- test('patchOpsxApplyContent is idempotent on re-run', () => {
43
- const first = patchOpsxApplyContent(VANILLA, 'claude');
44
- const second = patchOpsxApplyContent(first.content, 'claude');
45
- assert.equal(second.status, 're-patched');
46
- assert.match(second.content, /\.claude\/skills\/forge\/phases\/verify\.md/);
47
- });
48
-
49
- test('applySkillFooter adds overlay block once', () => {
50
- const skill = '---\nname: openspec-apply-change\n---\n\nBody.\n';
51
- const once = applySkillFooter(skill, 'codex');
52
- assert.match(once, /forgekit:openspec-overlay:start/);
53
- assert.match(once, /\/forge:apply/);
54
- const twice = applySkillFooter(once, 'codex');
55
- assert.equal((twice.match(/## Forge overlay/g) || []).length, 1);
56
- });
57
-
58
- test('stripOverlayBlock removes footer', () => {
59
- const withFooter = applySkillFooter('body\n', 'cursor');
60
- const stripped = stripOverlayBlock(withFooter);
61
- assert.doesNotMatch(stripped, /forgekit:openspec-overlay/);
62
- });
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ applySkillFooter,
5
+ patchOpsxApplyContent,
6
+ stripOverlayBlock,
7
+ } from './vendor-openspec-overlays.mjs';
8
+
9
+ const VANILLA = `---
10
+ name: test
11
+ ---
12
+
13
+ 6. **Implement tasks (loop until done or blocked)**
14
+
15
+ For each pending task:
16
+ - Show which task is being worked on
17
+ - Make the code changes required
18
+ - Keep changes minimal and focused
19
+ - Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
20
+ - Continue to next task
21
+
22
+ **Pause if:**
23
+ - Task is unclear
24
+
25
+ 7. **On completion or pause, show status**
26
+
27
+ Display:
28
+ - Tasks completed this session
29
+ - Overall progress: "N/M tasks complete"
30
+ - If all done: suggest archive
31
+ `;
32
+
33
+ test('patchOpsxApplyContent injects Forge implement step', () => {
34
+ const { content, status } = patchOpsxApplyContent(VANILLA, 'cursor');
35
+ assert.equal(status, 'patched');
36
+ assert.match(content, /REQUIRED \(Forge\):/);
37
+ assert.match(content, /\.cursor\/skills\/forge\/phases\/implement\.md/);
38
+ assert.match(content, /Dispatch \*\*implementer\*\* subagent/);
39
+ assert.doesNotMatch(content, /Make the code changes required/);
40
+ });
41
+
42
+ test('patchOpsxApplyContent is idempotent on re-run', () => {
43
+ const first = patchOpsxApplyContent(VANILLA, 'claude');
44
+ const second = patchOpsxApplyContent(first.content, 'claude');
45
+ assert.equal(second.status, 're-patched');
46
+ assert.match(second.content, /\.claude\/skills\/forge\/phases\/verify\.md/);
47
+ });
48
+
49
+ test('applySkillFooter adds overlay block once', () => {
50
+ const skill = '---\nname: openspec-apply-change\n---\n\nBody.\n';
51
+ const once = applySkillFooter(skill, 'codex');
52
+ assert.match(once, /forgekit:openspec-overlay:start/);
53
+ assert.match(once, /\/forge:apply/);
54
+ const twice = applySkillFooter(once, 'codex');
55
+ assert.equal((twice.match(/## Forge overlay/g) || []).length, 1);
56
+ });
57
+
58
+ test('stripOverlayBlock removes footer', () => {
59
+ const withFooter = applySkillFooter('body\n', 'cursor');
60
+ const stripped = stripOverlayBlock(withFooter);
61
+ assert.doesNotMatch(stripped, /forgekit:openspec-overlay/);
62
+ });