@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,194 +1,194 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { Writable } from 'node:stream';
|
|
7
|
-
import {
|
|
8
|
-
DEFAULTS_PATH,
|
|
9
|
-
deepMerge,
|
|
10
|
-
detectAgent,
|
|
11
|
-
parseArgs,
|
|
12
|
-
resolveFromConfig,
|
|
13
|
-
resolveModel,
|
|
14
|
-
runResolveModel,
|
|
15
|
-
} from './resolve-model.mjs';
|
|
16
|
-
import { getEffectiveBilling, runSetModels, writeLocalBilling } from './set-models.mjs';
|
|
17
|
-
|
|
18
|
-
function tmp(prefix) {
|
|
19
|
-
return fs.mkdtempSync(path.join(tmpdir(), prefix));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function capture() {
|
|
23
|
-
let text = '';
|
|
24
|
-
const stream = new Writable({
|
|
25
|
-
write(chunk, _enc, cb) {
|
|
26
|
-
text += String(chunk);
|
|
27
|
-
cb();
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
return {
|
|
31
|
-
stream,
|
|
32
|
-
text: () => text,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
test('parseArgs requires known flags', () => {
|
|
37
|
-
const opts = parseArgs(['--tier', 'fast', '--billing', 'included', '--agent', 'cursor']);
|
|
38
|
-
assert.equal(opts.tier, 'fast');
|
|
39
|
-
assert.equal(opts.billing, 'included');
|
|
40
|
-
assert.equal(opts.agent, 'cursor');
|
|
41
|
-
assert.throws(() => parseArgs(['--nope']), /Unknown argument/);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('detectAgent prefers explicit host env', () => {
|
|
45
|
-
assert.equal(detectAgent({ CURSOR_AGENT: '1' }).agent, 'cursor');
|
|
46
|
-
assert.equal(detectAgent({ CLAUDE_CODE: '1' }).agent, 'claude-code');
|
|
47
|
-
assert.equal(detectAgent({ CODEX_HOME: '/tmp/codex' }).agent, 'codex');
|
|
48
|
-
assert.equal(detectAgent({}).detected, false);
|
|
49
|
-
assert.equal(detectAgent({}).agent, 'cursor');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('deepMerge overlays nested agent maps', () => {
|
|
53
|
-
const merged = deepMerge(
|
|
54
|
-
{ billing: 'included', agents: { cursor: { included: { fast: 'inherit' } } } },
|
|
55
|
-
{ billing: 'metered', agents: { cursor: { included: { standard: 'inherit' } } } },
|
|
56
|
-
);
|
|
57
|
-
assert.equal(merged.billing, 'metered');
|
|
58
|
-
assert.deepEqual(merged.agents.cursor.included, {
|
|
59
|
-
fast: 'inherit',
|
|
60
|
-
standard: 'inherit',
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('cursor included resolves to omitModel', () => {
|
|
65
|
-
const result = resolveModel({
|
|
66
|
-
tier: 'standard',
|
|
67
|
-
agent: 'cursor',
|
|
68
|
-
billing: 'included',
|
|
69
|
-
defaultsPath: DEFAULTS_PATH,
|
|
70
|
-
env: {},
|
|
71
|
-
});
|
|
72
|
-
assert.equal(result.omitModel, true);
|
|
73
|
-
assert.equal(result.model, null);
|
|
74
|
-
assert.equal(result.billing, 'included');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('cursor metered capable returns explicit slug', () => {
|
|
78
|
-
const result = resolveModel({
|
|
79
|
-
tier: 'capable',
|
|
80
|
-
agent: 'cursor',
|
|
81
|
-
billing: 'metered',
|
|
82
|
-
defaultsPath: DEFAULTS_PATH,
|
|
83
|
-
env: {},
|
|
84
|
-
});
|
|
85
|
-
assert.equal(result.omitModel, false);
|
|
86
|
-
assert.equal(result.model, 'claude-opus-4-8-thinking-high');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('claude-code included fast uses haiku', () => {
|
|
90
|
-
const result = resolveModel({
|
|
91
|
-
tier: 'fast',
|
|
92
|
-
agent: 'claude-code',
|
|
93
|
-
billing: 'included',
|
|
94
|
-
defaultsPath: DEFAULTS_PATH,
|
|
95
|
-
env: {},
|
|
96
|
-
});
|
|
97
|
-
assert.equal(result.model, 'haiku');
|
|
98
|
-
assert.equal(result.omitModel, false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('local billing override changes default lane', () => {
|
|
102
|
-
const dir = tmp('forge-models-');
|
|
103
|
-
const forgeDir = path.join(dir, '.forge');
|
|
104
|
-
writeLocalBilling({ forgeDir, billing: 'metered' });
|
|
105
|
-
|
|
106
|
-
const result = resolveModel({
|
|
107
|
-
tier: 'standard',
|
|
108
|
-
agent: 'cursor',
|
|
109
|
-
defaultsPath: DEFAULTS_PATH,
|
|
110
|
-
forgeDir,
|
|
111
|
-
cwd: dir,
|
|
112
|
-
env: {},
|
|
113
|
-
});
|
|
114
|
-
assert.equal(result.billing, 'metered');
|
|
115
|
-
assert.equal(result.omitModel, false);
|
|
116
|
-
assert.equal(result.model, 'claude-sonnet-5-thinking-high');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('missing cell fails clearly', () => {
|
|
120
|
-
assert.throws(
|
|
121
|
-
() =>
|
|
122
|
-
resolveFromConfig(
|
|
123
|
-
{ agents: { cursor: { included: {} } } },
|
|
124
|
-
{ agent: 'cursor', billing: 'included', tier: 'fast' },
|
|
125
|
-
),
|
|
126
|
-
/Missing cell for \(cursor, included, fast\)/,
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('runResolveModel prints JSON', () => {
|
|
131
|
-
const out = capture();
|
|
132
|
-
const err = capture();
|
|
133
|
-
const code = runResolveModel(
|
|
134
|
-
['--tier', 'fast', '--agent', 'codex', '--billing', 'included'],
|
|
135
|
-
{ stdout: out.stream, stderr: err.stream },
|
|
136
|
-
);
|
|
137
|
-
assert.equal(code, 0);
|
|
138
|
-
const parsed = JSON.parse(out.text());
|
|
139
|
-
assert.equal(parsed.agent, 'codex');
|
|
140
|
-
assert.equal(parsed.omitModel, true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('set-models writes and getEffectiveBilling reads', () => {
|
|
144
|
-
const dir = tmp('forge-set-models-');
|
|
145
|
-
const forgeDir = path.join(dir, '.forge');
|
|
146
|
-
const out = capture();
|
|
147
|
-
const err = capture();
|
|
148
|
-
const code = runSetModels(['metered', '--forge-dir', forgeDir], {
|
|
149
|
-
cwd: dir,
|
|
150
|
-
stdout: out.stream,
|
|
151
|
-
stderr: err.stream,
|
|
152
|
-
});
|
|
153
|
-
assert.equal(code, 0);
|
|
154
|
-
assert.match(out.text(), /billing=metered/);
|
|
155
|
-
|
|
156
|
-
const effective = getEffectiveBilling({
|
|
157
|
-
forgeDir,
|
|
158
|
-
defaultsPath: DEFAULTS_PATH,
|
|
159
|
-
cwd: dir,
|
|
160
|
-
});
|
|
161
|
-
assert.equal(effective.billing, 'metered');
|
|
162
|
-
assert.equal(effective.localExists, true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('getEffectiveBilling defaults to included without local file', () => {
|
|
166
|
-
const dir = tmp('forge-get-billing-');
|
|
167
|
-
const forgeDir = path.join(dir, '.forge');
|
|
168
|
-
fs.mkdirSync(forgeDir, { recursive: true });
|
|
169
|
-
const effective = getEffectiveBilling({
|
|
170
|
-
forgeDir,
|
|
171
|
-
defaultsPath: DEFAULTS_PATH,
|
|
172
|
-
cwd: dir,
|
|
173
|
-
});
|
|
174
|
-
assert.equal(effective.billing, 'included');
|
|
175
|
-
assert.equal(effective.localExists, false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('forge:models get without local file explains no write', () => {
|
|
179
|
-
const dir = tmp('forge-models-get-');
|
|
180
|
-
const forgeDir = path.join(dir, '.forge');
|
|
181
|
-
fs.mkdirSync(forgeDir, { recursive: true });
|
|
182
|
-
const out = capture();
|
|
183
|
-
const err = capture();
|
|
184
|
-
const code = runSetModels(['--forge-dir', forgeDir], {
|
|
185
|
-
cwd: dir,
|
|
186
|
-
stdout: out.stream,
|
|
187
|
-
stderr: err.stream,
|
|
188
|
-
});
|
|
189
|
-
assert.equal(code, 0);
|
|
190
|
-
assert.match(out.text(), /billing=included/);
|
|
191
|
-
assert.match(out.text(), /local=\(none/);
|
|
192
|
-
assert.match(out.text(), /writes \.forge\/models\.local\.json/);
|
|
193
|
-
assert.equal(fs.existsSync(path.join(forgeDir, 'models.local.json')), false);
|
|
194
|
-
});
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { Writable } from 'node:stream';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULTS_PATH,
|
|
9
|
+
deepMerge,
|
|
10
|
+
detectAgent,
|
|
11
|
+
parseArgs,
|
|
12
|
+
resolveFromConfig,
|
|
13
|
+
resolveModel,
|
|
14
|
+
runResolveModel,
|
|
15
|
+
} from './resolve-model.mjs';
|
|
16
|
+
import { getEffectiveBilling, runSetModels, writeLocalBilling } from './set-models.mjs';
|
|
17
|
+
|
|
18
|
+
function tmp(prefix) {
|
|
19
|
+
return fs.mkdtempSync(path.join(tmpdir(), prefix));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function capture() {
|
|
23
|
+
let text = '';
|
|
24
|
+
const stream = new Writable({
|
|
25
|
+
write(chunk, _enc, cb) {
|
|
26
|
+
text += String(chunk);
|
|
27
|
+
cb();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
stream,
|
|
32
|
+
text: () => text,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('parseArgs requires known flags', () => {
|
|
37
|
+
const opts = parseArgs(['--tier', 'fast', '--billing', 'included', '--agent', 'cursor']);
|
|
38
|
+
assert.equal(opts.tier, 'fast');
|
|
39
|
+
assert.equal(opts.billing, 'included');
|
|
40
|
+
assert.equal(opts.agent, 'cursor');
|
|
41
|
+
assert.throws(() => parseArgs(['--nope']), /Unknown argument/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('detectAgent prefers explicit host env', () => {
|
|
45
|
+
assert.equal(detectAgent({ CURSOR_AGENT: '1' }).agent, 'cursor');
|
|
46
|
+
assert.equal(detectAgent({ CLAUDE_CODE: '1' }).agent, 'claude-code');
|
|
47
|
+
assert.equal(detectAgent({ CODEX_HOME: '/tmp/codex' }).agent, 'codex');
|
|
48
|
+
assert.equal(detectAgent({}).detected, false);
|
|
49
|
+
assert.equal(detectAgent({}).agent, 'cursor');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('deepMerge overlays nested agent maps', () => {
|
|
53
|
+
const merged = deepMerge(
|
|
54
|
+
{ billing: 'included', agents: { cursor: { included: { fast: 'inherit' } } } },
|
|
55
|
+
{ billing: 'metered', agents: { cursor: { included: { standard: 'inherit' } } } },
|
|
56
|
+
);
|
|
57
|
+
assert.equal(merged.billing, 'metered');
|
|
58
|
+
assert.deepEqual(merged.agents.cursor.included, {
|
|
59
|
+
fast: 'inherit',
|
|
60
|
+
standard: 'inherit',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('cursor included resolves to omitModel', () => {
|
|
65
|
+
const result = resolveModel({
|
|
66
|
+
tier: 'standard',
|
|
67
|
+
agent: 'cursor',
|
|
68
|
+
billing: 'included',
|
|
69
|
+
defaultsPath: DEFAULTS_PATH,
|
|
70
|
+
env: {},
|
|
71
|
+
});
|
|
72
|
+
assert.equal(result.omitModel, true);
|
|
73
|
+
assert.equal(result.model, null);
|
|
74
|
+
assert.equal(result.billing, 'included');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('cursor metered capable returns explicit slug', () => {
|
|
78
|
+
const result = resolveModel({
|
|
79
|
+
tier: 'capable',
|
|
80
|
+
agent: 'cursor',
|
|
81
|
+
billing: 'metered',
|
|
82
|
+
defaultsPath: DEFAULTS_PATH,
|
|
83
|
+
env: {},
|
|
84
|
+
});
|
|
85
|
+
assert.equal(result.omitModel, false);
|
|
86
|
+
assert.equal(result.model, 'claude-opus-4-8-thinking-high');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('claude-code included fast uses haiku', () => {
|
|
90
|
+
const result = resolveModel({
|
|
91
|
+
tier: 'fast',
|
|
92
|
+
agent: 'claude-code',
|
|
93
|
+
billing: 'included',
|
|
94
|
+
defaultsPath: DEFAULTS_PATH,
|
|
95
|
+
env: {},
|
|
96
|
+
});
|
|
97
|
+
assert.equal(result.model, 'haiku');
|
|
98
|
+
assert.equal(result.omitModel, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('local billing override changes default lane', () => {
|
|
102
|
+
const dir = tmp('forge-models-');
|
|
103
|
+
const forgeDir = path.join(dir, '.forge');
|
|
104
|
+
writeLocalBilling({ forgeDir, billing: 'metered' });
|
|
105
|
+
|
|
106
|
+
const result = resolveModel({
|
|
107
|
+
tier: 'standard',
|
|
108
|
+
agent: 'cursor',
|
|
109
|
+
defaultsPath: DEFAULTS_PATH,
|
|
110
|
+
forgeDir,
|
|
111
|
+
cwd: dir,
|
|
112
|
+
env: {},
|
|
113
|
+
});
|
|
114
|
+
assert.equal(result.billing, 'metered');
|
|
115
|
+
assert.equal(result.omitModel, false);
|
|
116
|
+
assert.equal(result.model, 'claude-sonnet-5-thinking-high');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('missing cell fails clearly', () => {
|
|
120
|
+
assert.throws(
|
|
121
|
+
() =>
|
|
122
|
+
resolveFromConfig(
|
|
123
|
+
{ agents: { cursor: { included: {} } } },
|
|
124
|
+
{ agent: 'cursor', billing: 'included', tier: 'fast' },
|
|
125
|
+
),
|
|
126
|
+
/Missing cell for \(cursor, included, fast\)/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('runResolveModel prints JSON', () => {
|
|
131
|
+
const out = capture();
|
|
132
|
+
const err = capture();
|
|
133
|
+
const code = runResolveModel(
|
|
134
|
+
['--tier', 'fast', '--agent', 'codex', '--billing', 'included'],
|
|
135
|
+
{ stdout: out.stream, stderr: err.stream },
|
|
136
|
+
);
|
|
137
|
+
assert.equal(code, 0);
|
|
138
|
+
const parsed = JSON.parse(out.text());
|
|
139
|
+
assert.equal(parsed.agent, 'codex');
|
|
140
|
+
assert.equal(parsed.omitModel, true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('set-models writes and getEffectiveBilling reads', () => {
|
|
144
|
+
const dir = tmp('forge-set-models-');
|
|
145
|
+
const forgeDir = path.join(dir, '.forge');
|
|
146
|
+
const out = capture();
|
|
147
|
+
const err = capture();
|
|
148
|
+
const code = runSetModels(['metered', '--forge-dir', forgeDir], {
|
|
149
|
+
cwd: dir,
|
|
150
|
+
stdout: out.stream,
|
|
151
|
+
stderr: err.stream,
|
|
152
|
+
});
|
|
153
|
+
assert.equal(code, 0);
|
|
154
|
+
assert.match(out.text(), /billing=metered/);
|
|
155
|
+
|
|
156
|
+
const effective = getEffectiveBilling({
|
|
157
|
+
forgeDir,
|
|
158
|
+
defaultsPath: DEFAULTS_PATH,
|
|
159
|
+
cwd: dir,
|
|
160
|
+
});
|
|
161
|
+
assert.equal(effective.billing, 'metered');
|
|
162
|
+
assert.equal(effective.localExists, true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('getEffectiveBilling defaults to included without local file', () => {
|
|
166
|
+
const dir = tmp('forge-get-billing-');
|
|
167
|
+
const forgeDir = path.join(dir, '.forge');
|
|
168
|
+
fs.mkdirSync(forgeDir, { recursive: true });
|
|
169
|
+
const effective = getEffectiveBilling({
|
|
170
|
+
forgeDir,
|
|
171
|
+
defaultsPath: DEFAULTS_PATH,
|
|
172
|
+
cwd: dir,
|
|
173
|
+
});
|
|
174
|
+
assert.equal(effective.billing, 'included');
|
|
175
|
+
assert.equal(effective.localExists, false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('forge:models get without local file explains no write', () => {
|
|
179
|
+
const dir = tmp('forge-models-get-');
|
|
180
|
+
const forgeDir = path.join(dir, '.forge');
|
|
181
|
+
fs.mkdirSync(forgeDir, { recursive: true });
|
|
182
|
+
const out = capture();
|
|
183
|
+
const err = capture();
|
|
184
|
+
const code = runSetModels(['--forge-dir', forgeDir], {
|
|
185
|
+
cwd: dir,
|
|
186
|
+
stdout: out.stream,
|
|
187
|
+
stderr: err.stream,
|
|
188
|
+
});
|
|
189
|
+
assert.equal(code, 0);
|
|
190
|
+
assert.match(out.text(), /billing=included/);
|
|
191
|
+
assert.match(out.text(), /local=\(none/);
|
|
192
|
+
assert.match(out.text(), /writes \.forge\/models\.local\.json/);
|
|
193
|
+
assert.equal(fs.existsSync(path.join(forgeDir, 'models.local.json')), false);
|
|
194
|
+
});
|
package/src/review/cli.test.mjs
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { parseArgs as parseNewArgs, runNew } from './new-review.mjs';
|
|
7
|
-
import { parseArgs as parseRenderArgs, runRender } from './render.mjs';
|
|
8
|
-
import { validateReport } from './lib.mjs';
|
|
9
|
-
|
|
10
|
-
const FIXED_NOW = new Date('2026-06-05T16:00:00.000Z');
|
|
11
|
-
const stubGit = () => ({ baseSha: 'base000', headSha: 'head111' });
|
|
12
|
-
|
|
13
|
-
function tmp(prefix) {
|
|
14
|
-
return fs.mkdtempSync(path.join(tmpdir(), prefix));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// --- new-review ---
|
|
18
|
-
|
|
19
|
-
test('runNew scaffolds a schema-valid skeleton with git SHAs', () => {
|
|
20
|
-
const dir = tmp('tmp-new-');
|
|
21
|
-
try {
|
|
22
|
-
const opts = parseNewArgs(['mercury-vat', '--type', 'branch']);
|
|
23
|
-
const result = runNew(opts, { now: FIXED_NOW, cwd: dir, gitImpl: stubGit });
|
|
24
|
-
assert.equal(result.exitCode, 0, result.message);
|
|
25
|
-
assert.ok(result.jsonPath.endsWith('20260605T160000Z-mercury-vat-review.json'));
|
|
26
|
-
const report = JSON.parse(fs.readFileSync(result.jsonPath, 'utf8'));
|
|
27
|
-
assert.equal(report.review_id, '20260605T160000Z-mercury-vat');
|
|
28
|
-
assert.equal(report.scope.base_sha, 'base000');
|
|
29
|
-
assert.equal(report.scope.head_sha, 'head111');
|
|
30
|
-
assert.equal(report.lenses.length, 9);
|
|
31
|
-
assert.equal(validateReport(report).ok, true);
|
|
32
|
-
} finally {
|
|
33
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('runNew refuses to overwrite an existing report', () => {
|
|
38
|
-
const dir = tmp('tmp-new-');
|
|
39
|
-
try {
|
|
40
|
-
const opts = parseNewArgs(['dup-scope', '--no-git']);
|
|
41
|
-
assert.equal(runNew(opts, { now: FIXED_NOW, cwd: dir }).exitCode, 0);
|
|
42
|
-
const second = runNew(opts, { now: FIXED_NOW, cwd: dir });
|
|
43
|
-
assert.equal(second.exitCode, 1);
|
|
44
|
-
assert.ok(second.message.includes('refusing to overwrite'));
|
|
45
|
-
} finally {
|
|
46
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('runNew rejects reverify without --parent', () => {
|
|
51
|
-
const dir = tmp('tmp-new-');
|
|
52
|
-
try {
|
|
53
|
-
const opts = parseNewArgs(['x', '--kind', 'reverify', '--no-git']);
|
|
54
|
-
const result = runNew(opts, { now: FIXED_NOW, cwd: dir });
|
|
55
|
-
assert.equal(result.exitCode, 1);
|
|
56
|
-
assert.ok(result.message.includes('reverify'));
|
|
57
|
-
} finally {
|
|
58
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('new-review parseArgs splits comma lists and flags', () => {
|
|
63
|
-
const opts = parseNewArgs(['scope', '--lenses', 'security,correctness', '--paths', 'a.ts,b.ts', '--no-git']);
|
|
64
|
-
assert.deepEqual(opts.lenses, ['security', 'correctness']);
|
|
65
|
-
assert.deepEqual(opts.paths, ['a.ts', 'b.ts']);
|
|
66
|
-
assert.equal(opts.git, false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// --- render ---
|
|
70
|
-
|
|
71
|
-
test('runRender writes paired .md from JSON', () => {
|
|
72
|
-
const dir = tmp('tmp-render-');
|
|
73
|
-
try {
|
|
74
|
-
const report = {
|
|
75
|
-
review_id: '20260605T160000Z-x',
|
|
76
|
-
kind: 'review',
|
|
77
|
-
created_at: '2026-06-05T16:00:00.000Z',
|
|
78
|
-
scope: { type: 'file', description: 'x.ts' },
|
|
79
|
-
lenses: ['security'],
|
|
80
|
-
summary: { tentative_count: 1, confirmed: 1, headline: 'one confirmed' },
|
|
81
|
-
findings: [
|
|
82
|
-
{
|
|
83
|
-
id: 'F-001',
|
|
84
|
-
lens: 'security',
|
|
85
|
-
location: 'x.ts:1',
|
|
86
|
-
claim: 'auth bypass',
|
|
87
|
-
severity: 'critical',
|
|
88
|
-
verdict: 'confirmed',
|
|
89
|
-
verdict_reason: 'real',
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
const jsonPath = path.join(dir, '20260605T160000Z-x-review.json');
|
|
94
|
-
fs.writeFileSync(jsonPath, JSON.stringify(report));
|
|
95
|
-
const opts = parseRenderArgs(['--file', jsonPath]);
|
|
96
|
-
const result = runRender(opts, dir);
|
|
97
|
-
assert.equal(result.exitCode, 0, result.message);
|
|
98
|
-
const md = fs.readFileSync(jsonPath.replace(/\.json$/, '.md'), 'utf8');
|
|
99
|
-
assert.ok(md.includes('## Critical'));
|
|
100
|
-
assert.ok(md.includes('one confirmed'));
|
|
101
|
-
} finally {
|
|
102
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('runRender fails on an invalid report', () => {
|
|
107
|
-
const dir = tmp('tmp-render-');
|
|
108
|
-
try {
|
|
109
|
-
const jsonPath = path.join(dir, '20260605T160000Z-bad-review.json');
|
|
110
|
-
fs.writeFileSync(jsonPath, JSON.stringify({ kind: 'review' }));
|
|
111
|
-
const result = runRender(parseRenderArgs(['--file', jsonPath]), dir);
|
|
112
|
-
assert.equal(result.exitCode, 1);
|
|
113
|
-
assert.ok(result.message.includes('validation failed'));
|
|
114
|
-
} finally {
|
|
115
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
116
|
-
}
|
|
117
|
-
});
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { parseArgs as parseNewArgs, runNew } from './new-review.mjs';
|
|
7
|
+
import { parseArgs as parseRenderArgs, runRender } from './render.mjs';
|
|
8
|
+
import { validateReport } from './lib.mjs';
|
|
9
|
+
|
|
10
|
+
const FIXED_NOW = new Date('2026-06-05T16:00:00.000Z');
|
|
11
|
+
const stubGit = () => ({ baseSha: 'base000', headSha: 'head111' });
|
|
12
|
+
|
|
13
|
+
function tmp(prefix) {
|
|
14
|
+
return fs.mkdtempSync(path.join(tmpdir(), prefix));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- new-review ---
|
|
18
|
+
|
|
19
|
+
test('runNew scaffolds a schema-valid skeleton with git SHAs', () => {
|
|
20
|
+
const dir = tmp('tmp-new-');
|
|
21
|
+
try {
|
|
22
|
+
const opts = parseNewArgs(['mercury-vat', '--type', 'branch']);
|
|
23
|
+
const result = runNew(opts, { now: FIXED_NOW, cwd: dir, gitImpl: stubGit });
|
|
24
|
+
assert.equal(result.exitCode, 0, result.message);
|
|
25
|
+
assert.ok(result.jsonPath.endsWith('20260605T160000Z-mercury-vat-review.json'));
|
|
26
|
+
const report = JSON.parse(fs.readFileSync(result.jsonPath, 'utf8'));
|
|
27
|
+
assert.equal(report.review_id, '20260605T160000Z-mercury-vat');
|
|
28
|
+
assert.equal(report.scope.base_sha, 'base000');
|
|
29
|
+
assert.equal(report.scope.head_sha, 'head111');
|
|
30
|
+
assert.equal(report.lenses.length, 9);
|
|
31
|
+
assert.equal(validateReport(report).ok, true);
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('runNew refuses to overwrite an existing report', () => {
|
|
38
|
+
const dir = tmp('tmp-new-');
|
|
39
|
+
try {
|
|
40
|
+
const opts = parseNewArgs(['dup-scope', '--no-git']);
|
|
41
|
+
assert.equal(runNew(opts, { now: FIXED_NOW, cwd: dir }).exitCode, 0);
|
|
42
|
+
const second = runNew(opts, { now: FIXED_NOW, cwd: dir });
|
|
43
|
+
assert.equal(second.exitCode, 1);
|
|
44
|
+
assert.ok(second.message.includes('refusing to overwrite'));
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('runNew rejects reverify without --parent', () => {
|
|
51
|
+
const dir = tmp('tmp-new-');
|
|
52
|
+
try {
|
|
53
|
+
const opts = parseNewArgs(['x', '--kind', 'reverify', '--no-git']);
|
|
54
|
+
const result = runNew(opts, { now: FIXED_NOW, cwd: dir });
|
|
55
|
+
assert.equal(result.exitCode, 1);
|
|
56
|
+
assert.ok(result.message.includes('reverify'));
|
|
57
|
+
} finally {
|
|
58
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('new-review parseArgs splits comma lists and flags', () => {
|
|
63
|
+
const opts = parseNewArgs(['scope', '--lenses', 'security,correctness', '--paths', 'a.ts,b.ts', '--no-git']);
|
|
64
|
+
assert.deepEqual(opts.lenses, ['security', 'correctness']);
|
|
65
|
+
assert.deepEqual(opts.paths, ['a.ts', 'b.ts']);
|
|
66
|
+
assert.equal(opts.git, false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- render ---
|
|
70
|
+
|
|
71
|
+
test('runRender writes paired .md from JSON', () => {
|
|
72
|
+
const dir = tmp('tmp-render-');
|
|
73
|
+
try {
|
|
74
|
+
const report = {
|
|
75
|
+
review_id: '20260605T160000Z-x',
|
|
76
|
+
kind: 'review',
|
|
77
|
+
created_at: '2026-06-05T16:00:00.000Z',
|
|
78
|
+
scope: { type: 'file', description: 'x.ts' },
|
|
79
|
+
lenses: ['security'],
|
|
80
|
+
summary: { tentative_count: 1, confirmed: 1, headline: 'one confirmed' },
|
|
81
|
+
findings: [
|
|
82
|
+
{
|
|
83
|
+
id: 'F-001',
|
|
84
|
+
lens: 'security',
|
|
85
|
+
location: 'x.ts:1',
|
|
86
|
+
claim: 'auth bypass',
|
|
87
|
+
severity: 'critical',
|
|
88
|
+
verdict: 'confirmed',
|
|
89
|
+
verdict_reason: 'real',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const jsonPath = path.join(dir, '20260605T160000Z-x-review.json');
|
|
94
|
+
fs.writeFileSync(jsonPath, JSON.stringify(report));
|
|
95
|
+
const opts = parseRenderArgs(['--file', jsonPath]);
|
|
96
|
+
const result = runRender(opts, dir);
|
|
97
|
+
assert.equal(result.exitCode, 0, result.message);
|
|
98
|
+
const md = fs.readFileSync(jsonPath.replace(/\.json$/, '.md'), 'utf8');
|
|
99
|
+
assert.ok(md.includes('## Critical'));
|
|
100
|
+
assert.ok(md.includes('one confirmed'));
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('runRender fails on an invalid report', () => {
|
|
107
|
+
const dir = tmp('tmp-render-');
|
|
108
|
+
try {
|
|
109
|
+
const jsonPath = path.join(dir, '20260605T160000Z-bad-review.json');
|
|
110
|
+
fs.writeFileSync(jsonPath, JSON.stringify({ kind: 'review' }));
|
|
111
|
+
const result = runRender(parseRenderArgs(['--file', jsonPath]), dir);
|
|
112
|
+
assert.equal(result.exitCode, 1);
|
|
113
|
+
assert.ok(result.message.includes('validation failed'));
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|