@izkac/forgekit 0.3.12 → 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.
- package/bin/forge.mjs +107 -107
- package/bin/forgekit.mjs +83 -83
- package/bin/review.mjs +81 -81
- package/package.json +1 -1
- package/scripts/prepack.mjs +78 -78
- package/scripts/run-tests.mjs +50 -50
- package/src/adr.mjs +236 -236
- package/src/adr.test.mjs +170 -170
- package/src/change.mjs +327 -234
- package/src/change.test.mjs +145 -83
- package/src/cleanup-sessions.mjs +84 -84
- package/src/config.mjs +103 -103
- package/src/defer.mjs +75 -75
- package/src/doctor.mjs +350 -341
- package/src/doctor.test.mjs +114 -114
- package/src/init.mjs +680 -621
- package/src/install.mjs +815 -815
- package/src/install.test.mjs +180 -180
- package/src/integrity-check.mjs +60 -60
- package/src/integrity.mjs +682 -682
- package/src/integrity.test.mjs +566 -566
- package/src/lib.mjs +143 -143
- package/src/models.defaults.json +41 -41
- package/src/new-session.mjs +99 -99
- package/src/openspec-overlays/README.md +19 -19
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
- package/src/paths.mjs +92 -92
- package/src/plan-engine.mjs +321 -278
- package/src/plan-engine.test.mjs +447 -283
- package/src/preferences.defaults.json +78 -78
- package/src/preferences.mjs +438 -438
- package/src/preferences.test.mjs +174 -174
- package/src/record-evidence.mjs +204 -204
- package/src/resolve-model.mjs +312 -312
- package/src/resolve-model.test.mjs +194 -194
- package/src/review/cli.test.mjs +117 -117
- package/src/review/export.mjs +172 -172
- package/src/review/export.test.mjs +197 -197
- package/src/review/fixtures/valid-review.json +42 -42
- package/src/review/lib.mjs +894 -894
- package/src/review/lib.test.mjs +266 -266
- package/src/review/schema.json +196 -196
- package/src/review/signals.test.mjs +62 -62
- package/src/score-cli.mjs +68 -68
- package/src/score.mjs +568 -568
- package/src/score.test.mjs +366 -366
- package/src/session-reminder.mjs +207 -207
- package/src/session-status.mjs +70 -70
- package/src/set-models.mjs +186 -186
- package/src/set-phase.mjs +205 -205
- package/src/set-prefs.mjs +294 -294
- package/src/specs-sync.mjs +234 -0
- package/src/specs-sync.test.mjs +114 -0
- package/src/spine.mjs +93 -93
- package/src/triage-prompt.mjs +175 -175
- package/src/triage-prompt.test.mjs +50 -50
- package/src/vendor-openspec-overlays.mjs +176 -176
- package/src/vendor-openspec-overlays.test.mjs +62 -62
- package/vendor/skills/archive-to-adr/SKILL.md +149 -149
- package/vendor/skills/forge/SKILL.md +136 -136
- package/vendor/skills/forge/docs/forge.md +650 -647
- package/vendor/skills/forge/phases/brainstorm.md +23 -23
- package/vendor/skills/forge/phases/finish.md +90 -87
- package/vendor/skills/forge/phases/implement.md +77 -77
- package/vendor/skills/forge/phases/plan-openspec.md +60 -60
- package/vendor/skills/forge/phases/plan-specs.md +163 -117
- package/vendor/skills/forge/phases/review.md +25 -25
- package/vendor/skills/forge/phases/verify.md +124 -124
- package/vendor/skills/forge/references/forge-layout.md +85 -85
- package/vendor/skills/forge/references/pace.md +115 -115
- package/vendor/skills/forge/references/plan-routing.md +52 -51
- package/vendor/skills/forge/references/runtime-integrity.md +232 -232
- package/vendor/skills/forge/references/substantial-work.md +37 -37
- package/vendor/skills/forge/references/test-evidence.md +30 -30
- package/vendor/skills/forge/references/test-strategy.md +68 -68
- package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
- package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
- package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
- package/vendor/skills/thorough-code-review/SKILL.md +290 -290
- package/vendor/skills/thorough-code-review/examples.md +133 -133
- package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
- package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
- package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
- package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
- package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
- package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
- package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
- package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
- package/vendor/templates/adr/README.md +7 -7
- package/vendor/templates/adr/decisions.md +141 -141
- package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
- package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
- package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
- package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
- package/vendor/templates/project/claude/commands/forge-build.md +17 -17
- package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
- package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
- package/vendor/templates/project/claude/commands/forge-status.md +16 -16
- package/vendor/templates/project/claude/commands/forge.md +16 -16
- package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
- package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
- package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
- package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
- package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
- package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
- package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
- package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
- package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
- package/vendor/templates/project/cursor/commands/forge.md +16 -16
- package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
- 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
|
+
});
|