@izkac/forgekit 0.3.12 → 0.3.14

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 (120) hide show
  1. package/bin/forge.mjs +119 -107
  2. package/bin/forgekit.mjs +83 -83
  3. package/bin/review.mjs +81 -81
  4. package/package.json +2 -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/fleet.test.mjs +50 -0
  17. package/src/init.mjs +680 -621
  18. package/src/install.mjs +815 -815
  19. package/src/install.test.mjs +180 -180
  20. package/src/integrity-check.mjs +60 -60
  21. package/src/integrity.mjs +688 -682
  22. package/src/integrity.test.mjs +612 -566
  23. package/src/lib/fleet.mjs +61 -4
  24. package/src/lib.mjs +160 -143
  25. package/src/lib.test.mjs +128 -0
  26. package/src/models.defaults.json +41 -41
  27. package/src/new-session.mjs +99 -99
  28. package/src/openspec-overlays/README.md +19 -19
  29. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
  30. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
  31. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
  32. package/src/paths.mjs +92 -92
  33. package/src/plan-engine.mjs +321 -278
  34. package/src/plan-engine.test.mjs +447 -283
  35. package/src/preferences.defaults.json +78 -78
  36. package/src/preferences.mjs +438 -438
  37. package/src/preferences.test.mjs +174 -174
  38. package/src/record-evidence.mjs +204 -204
  39. package/src/repo-root.mjs +33 -0
  40. package/src/resolve-model.mjs +312 -312
  41. package/src/resolve-model.test.mjs +194 -194
  42. package/src/review/cli.test.mjs +117 -117
  43. package/src/review/export.mjs +172 -172
  44. package/src/review/export.test.mjs +197 -197
  45. package/src/review/fixtures/valid-review.json +42 -42
  46. package/src/review/lib.mjs +894 -894
  47. package/src/review/lib.test.mjs +266 -266
  48. package/src/review/schema.json +196 -196
  49. package/src/review/signals.test.mjs +62 -62
  50. package/src/score-cli.mjs +68 -68
  51. package/src/score.mjs +568 -568
  52. package/src/score.test.mjs +366 -366
  53. package/src/session-reminder.mjs +207 -207
  54. package/src/session-status.mjs +70 -70
  55. package/src/set-models.mjs +186 -186
  56. package/src/set-phase.mjs +205 -205
  57. package/src/set-prefs.mjs +294 -294
  58. package/src/specs-sync.mjs +232 -0
  59. package/src/specs-sync.test.mjs +114 -0
  60. package/src/spine.mjs +93 -93
  61. package/src/triage-prompt.mjs +175 -175
  62. package/src/triage-prompt.test.mjs +50 -50
  63. package/src/vendor-openspec-overlays.mjs +176 -176
  64. package/src/vendor-openspec-overlays.test.mjs +62 -62
  65. package/vendor/skills/archive-to-adr/SKILL.md +149 -149
  66. package/vendor/skills/forge/SKILL.md +136 -136
  67. package/vendor/skills/forge/docs/forge.md +650 -647
  68. package/vendor/skills/forge/phases/brainstorm.md +23 -23
  69. package/vendor/skills/forge/phases/finish.md +90 -87
  70. package/vendor/skills/forge/phases/implement.md +77 -77
  71. package/vendor/skills/forge/phases/plan-openspec.md +60 -60
  72. package/vendor/skills/forge/phases/plan-specs.md +163 -117
  73. package/vendor/skills/forge/phases/review.md +25 -25
  74. package/vendor/skills/forge/phases/verify.md +124 -124
  75. package/vendor/skills/forge/references/forge-layout.md +85 -85
  76. package/vendor/skills/forge/references/pace.md +115 -115
  77. package/vendor/skills/forge/references/plan-routing.md +52 -51
  78. package/vendor/skills/forge/references/runtime-integrity.md +232 -232
  79. package/vendor/skills/forge/references/substantial-work.md +37 -37
  80. package/vendor/skills/forge/references/test-evidence.md +30 -30
  81. package/vendor/skills/forge/references/test-strategy.md +68 -68
  82. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
  83. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
  84. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
  85. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
  86. package/vendor/skills/thorough-code-review/SKILL.md +290 -290
  87. package/vendor/skills/thorough-code-review/examples.md +133 -133
  88. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
  89. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
  90. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
  91. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
  92. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
  93. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
  94. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
  95. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
  96. package/vendor/templates/adr/README.md +7 -7
  97. package/vendor/templates/adr/decisions.md +141 -141
  98. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
  99. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
  100. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
  101. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
  102. package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
  103. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
  104. package/vendor/templates/project/claude/commands/forge-build.md +17 -17
  105. package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
  106. package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
  107. package/vendor/templates/project/claude/commands/forge-status.md +16 -16
  108. package/vendor/templates/project/claude/commands/forge.md +16 -16
  109. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
  110. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
  111. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
  112. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
  113. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
  114. package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
  115. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
  116. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
  117. package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
  118. package/vendor/templates/project/cursor/commands/forge.md +16 -16
  119. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
  120. 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
+ });