forge-workflow 0.1.0-beta.4 → 0.1.0-beta.5
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/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +16 -374
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +54 -25
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +344 -142
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +223 -25
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +17 -2
- package/lib/commands/setup.js +191 -94
- package/lib/commands/shepherd.js +13 -1
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +119 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/test.js +24 -34
- package/lib/commands/worktree.js +220 -42
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +56 -32
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +93 -4
- package/lib/insights.js +96 -80
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +16 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/sqlite-driver.js +153 -29
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +68 -43
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +224 -41
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +10 -1
- package/lib/pr-monitor/watch-lifecycle.js +13 -1
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +33 -1
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/package.json +6 -7
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +176 -43
- package/skills/review/SKILL.md +4 -11
- package/skills/review/evals/scorecard.json +3 -3
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +20 -14
- package/skills/shepherd/evals/scorecard.json +2 -2
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/commands/ship.js
CHANGED
|
@@ -12,7 +12,7 @@ const { execFileSync } = require('node:child_process');
|
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
16
16
|
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
17
17
|
|
|
18
18
|
const AUTO_SHEPHERD_RAIL = 'rail.auto_shepherd';
|
|
@@ -511,34 +511,31 @@ async function createPR(options) { // NOSONAR S3776
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
/**
|
|
514
|
-
* Best-effort
|
|
515
|
-
*
|
|
516
|
-
* ship: `startWatcher` (startPrWatcherDetached) already never throws, and this
|
|
517
|
-
* guard keeps even a surprise error from surfacing to the ship caller.
|
|
514
|
+
* Best-effort wake of the repository-wide singleton after a successful real ship.
|
|
515
|
+
* The shared trigger owns containment and gate checks; dry-run skips before it.
|
|
518
516
|
*
|
|
519
|
-
*
|
|
520
|
-
* disabled it (`forge gate disable rail.auto_shepherd`), the watcher is skipped
|
|
521
|
-
* so the auto-start is honestly toggleable. The rail check is fail-open and
|
|
522
|
-
* wrapped in the same try/catch, so neither a disabled rail nor a config-read
|
|
523
|
-
* error ever fails ship.
|
|
524
|
-
*
|
|
525
|
-
* @param {{ dryRun: boolean, prNumber?: string|number, startWatcher: Function, railEnabled?: Function }} params
|
|
517
|
+
* @param {{ dryRun: boolean, projectRoot?: string, fireAndForget?: Function }} params
|
|
526
518
|
* @returns {{ started: boolean, reason?: string }}
|
|
527
519
|
*/
|
|
528
|
-
function
|
|
529
|
-
if (dryRun
|
|
520
|
+
function maybeTriggerShepherdAfterShip({ dryRun, projectRoot = process.cwd(), fireAndForget: trigger = fireAndForget }) {
|
|
521
|
+
if (dryRun) return { started: false, reason: 'skipped' };
|
|
530
522
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
return startWatcher({ prNumber, cwd: process.cwd() });
|
|
523
|
+
trigger({ projectRoot, dryRun });
|
|
524
|
+
return { started: true };
|
|
535
525
|
} catch (err) {
|
|
536
526
|
return { started: false, reason: err.message };
|
|
537
527
|
}
|
|
538
528
|
}
|
|
539
529
|
|
|
540
530
|
async function executeShip(options) {
|
|
541
|
-
const {
|
|
531
|
+
const {
|
|
532
|
+
featureSlug,
|
|
533
|
+
title,
|
|
534
|
+
dryRun = false,
|
|
535
|
+
projectRoot = process.cwd(),
|
|
536
|
+
fireAndForget: trigger = fireAndForget,
|
|
537
|
+
createPr = createPR,
|
|
538
|
+
} = options || {};
|
|
542
539
|
|
|
543
540
|
// Validate feature slug
|
|
544
541
|
if (!featureSlug || typeof featureSlug !== 'string' || featureSlug.trim() === '') {
|
|
@@ -588,9 +585,9 @@ async function executeShip(options) {
|
|
|
588
585
|
testScenarios,
|
|
589
586
|
coverage,
|
|
590
587
|
});
|
|
591
|
-
const result = await
|
|
588
|
+
const result = await createPr({ title, body: prBody, dryRun });
|
|
592
589
|
if (!result.success) return result;
|
|
593
|
-
|
|
590
|
+
maybeTriggerShepherdAfterShip({ dryRun, projectRoot, fireAndForget: trigger });
|
|
594
591
|
return {
|
|
595
592
|
success: true,
|
|
596
593
|
prUrl: result.prUrl,
|
|
@@ -605,11 +602,13 @@ async function executeShip(options) {
|
|
|
605
602
|
module.exports = {
|
|
606
603
|
name: 'ship',
|
|
607
604
|
description: 'Create a pull request from validated feature work',
|
|
608
|
-
handler: async (args, flags = {}) => {
|
|
605
|
+
handler: async (args, flags = {}, projectRoot = process.cwd(), deps = {}) => {
|
|
609
606
|
const result = await executeShip({
|
|
610
607
|
featureSlug: args[0],
|
|
611
608
|
title: args[1],
|
|
612
609
|
dryRun: Boolean(flags.dryRun || flags['--dry-run']),
|
|
610
|
+
projectRoot,
|
|
611
|
+
fireAndForget: deps.fireAndForget,
|
|
613
612
|
});
|
|
614
613
|
if (!result.success) {
|
|
615
614
|
return result;
|
|
@@ -623,7 +622,7 @@ module.exports = {
|
|
|
623
622
|
output: lines.join('\n'),
|
|
624
623
|
};
|
|
625
624
|
},
|
|
626
|
-
|
|
625
|
+
maybeTriggerShepherdAfterShip,
|
|
627
626
|
autoShepherdRailEnabled,
|
|
628
627
|
extractKeyDecisions,
|
|
629
628
|
extractTestScenarios,
|
package/lib/commands/skill.js
CHANGED
|
@@ -19,9 +19,18 @@ const fs = require('node:fs');
|
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const { routeSkill, loadSkillCatalog } = require('../using-forge');
|
|
21
21
|
const skillEval = require('../skill-eval');
|
|
22
|
+
const { runBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-runner');
|
|
23
|
+
const { resolveBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-runtime');
|
|
24
|
+
const { loadPromotionEvidence } = require('../../scripts/lib/promotion-evidence-loader');
|
|
25
|
+
const { scorePromotion } = require('../../scripts/lib/promotion-scorecard');
|
|
26
|
+
|
|
27
|
+
const BEHAVIORAL_TIERS = Object.freeze([30, 100, 300]);
|
|
28
|
+
const BEHAVIORAL_TIER_VALUES = new Set(BEHAVIORAL_TIERS.map(String));
|
|
29
|
+
const BEHAVIORAL_TIER_USAGE = BEHAVIORAL_TIERS.join('|');
|
|
22
30
|
|
|
23
31
|
const USAGE = 'Usage: forge skill for "<situation>" [--json]\n' +
|
|
24
32
|
' forge skill eval [name] --static [--json]\n' +
|
|
33
|
+
` forge skill eval <name> --full --tier ${BEHAVIORAL_TIER_USAGE} [--json]\n` +
|
|
25
34
|
' forge skill scores [--json]\n' +
|
|
26
35
|
' forge skill coverage [--json]';
|
|
27
36
|
|
|
@@ -77,12 +86,108 @@ function writeScorecard(skillsDir, name, card) {
|
|
|
77
86
|
fs.writeFileSync(path.join(dir, 'scorecard.json'), JSON.stringify(card, null, 2) + '\n');
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
function readFlagValue(rest, flags, name) {
|
|
90
|
+
const direct = flags[name] ?? flags[`--${name}`];
|
|
91
|
+
if (direct !== undefined && direct !== true) return direct;
|
|
92
|
+
const exact = rest.indexOf(`--${name}`);
|
|
93
|
+
if (exact >= 0) return rest[exact + 1];
|
|
94
|
+
const prefix = `--${name}=`;
|
|
95
|
+
const joined = rest.find(arg => typeof arg === 'string' && arg.startsWith(prefix));
|
|
96
|
+
return joined ? joined.slice(prefix.length) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasFlag(rest, flags, name) {
|
|
100
|
+
return flags[name] === true || flags[`--${name}`] === true ||
|
|
101
|
+
rest.includes(`--${name}`) || rest.some(arg => typeof arg === 'string' && arg.startsWith(`--${name}=`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function positionalArgs(rest, valueFlags = []) {
|
|
105
|
+
const positional = [];
|
|
106
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
107
|
+
const arg = rest[index];
|
|
108
|
+
if (typeof arg !== 'string') continue;
|
|
109
|
+
if (!arg.startsWith('--')) {
|
|
110
|
+
positional.push(arg);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const flagName = arg.slice(2).split('=', 1)[0];
|
|
114
|
+
if (valueFlags.includes(flagName) && !arg.includes('=')) index += 1;
|
|
115
|
+
}
|
|
116
|
+
return positional;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleFullEval(rest, flags, projectRoot, opts) {
|
|
120
|
+
const json = hasFlag(rest, flags, 'json');
|
|
121
|
+
const tierValue = readFlagValue(rest, flags, 'tier');
|
|
122
|
+
const name = positionalArgs(rest, ['tier'])[0];
|
|
123
|
+
if (!name) return { success: false, error: 'Behavioral evaluation requires a skill name.\n' + USAGE };
|
|
124
|
+
const tier = Number(tierValue);
|
|
125
|
+
if (!BEHAVIORAL_TIER_VALUES.has(String(tierValue))) {
|
|
126
|
+
return { success: false, error: `Behavioral evaluation requires --tier ${BEHAVIORAL_TIER_USAGE}.\n` + USAGE };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ctx = skillEval.resolveSkillsContext(projectRoot);
|
|
130
|
+
if (!ctx || !fs.existsSync(path.join(ctx.skillsDir, name, 'SKILL.md'))) {
|
|
131
|
+
return { success: false, error: `Skill '${name}' not found.` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scoreResult = (result) => {
|
|
135
|
+
const loaded = loadPromotionEvidence({ tier, findings: result.findings });
|
|
136
|
+
const scorecard = scorePromotion({ tier, pairs: loaded.ok ? loaded.pairs : [] });
|
|
137
|
+
if (!loaded.ok) scorecard.reasons = [loaded.reason];
|
|
138
|
+
return { ...result, scorecard };
|
|
139
|
+
};
|
|
140
|
+
const formatResult = (result) => {
|
|
141
|
+
const successfulScore = tier === BEHAVIORAL_TIERS[0] || result.scorecard.status === 'PASS';
|
|
142
|
+
const success = result.status === 'PASS' && successfulScore;
|
|
143
|
+
let error;
|
|
144
|
+
if (!success) {
|
|
145
|
+
error = result.status !== 'PASS'
|
|
146
|
+
? `Behavioral evaluation ${result.status}.`
|
|
147
|
+
: `Promotion ${result.scorecard.phase || 'evaluation'} ${result.scorecard.status}.`;
|
|
148
|
+
}
|
|
149
|
+
const output = json
|
|
150
|
+
? JSON.stringify(result, null, 2) + '\n'
|
|
151
|
+
: `Behavioral evaluation: ${result.status}\nTier: ${tier}\n` +
|
|
152
|
+
`Completed: ${result.completedRuns}/${result.expectedRuns}\n` +
|
|
153
|
+
`Promotion: ${result.scorecard.phase || 'unavailable'} / ${result.scorecard.status}`;
|
|
154
|
+
return { success, error, behavioral: result, output };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const runner = opts.runBehavioralEvaluation || runBehavioralEvaluation;
|
|
158
|
+
let behavioralOptions = opts.behavioralOptions || {};
|
|
159
|
+
if (!opts.runBehavioralEvaluation || opts.resolveBehavioralEvaluation) {
|
|
160
|
+
const resolver = opts.resolveBehavioralEvaluation || resolveBehavioralEvaluation;
|
|
161
|
+
const resolved = await resolver({
|
|
162
|
+
projectRoot,
|
|
163
|
+
skillName: name,
|
|
164
|
+
skillPath: path.join(ctx.skillsDir, name, 'SKILL.md'),
|
|
165
|
+
tier,
|
|
166
|
+
env: opts.env || process.env,
|
|
167
|
+
});
|
|
168
|
+
if (!resolved.ok) return formatResult(scoreResult(resolved.result));
|
|
169
|
+
behavioralOptions = { ...resolved.options, ...behavioralOptions };
|
|
170
|
+
}
|
|
171
|
+
const result = scoreResult(await runner({
|
|
172
|
+
...behavioralOptions,
|
|
173
|
+
projectRoot,
|
|
174
|
+
skillName: name,
|
|
175
|
+
tier,
|
|
176
|
+
}));
|
|
177
|
+
return formatResult(result);
|
|
178
|
+
}
|
|
179
|
+
|
|
80
180
|
/**
|
|
81
181
|
* "forge skill eval [name] --static [--json]" -- compute + persist the DETERMINISTIC scorecard(s).
|
|
82
|
-
* All skills when no name. --static is
|
|
83
|
-
*
|
|
182
|
+
* All skills when no name. --static is accepted and implied; behavioral execution is isolated
|
|
183
|
+
* behind the explicit --full branch above so existing static scoring remains unchanged.
|
|
84
184
|
*/
|
|
85
|
-
function handleEval(rest, flags, projectRoot) {
|
|
185
|
+
function handleEval(rest, flags, projectRoot, opts = {}) {
|
|
186
|
+
const full = hasFlag(rest, flags, 'full');
|
|
187
|
+
if (full) return handleFullEval(rest, flags, projectRoot, opts);
|
|
188
|
+
if (readFlagValue(rest, flags, 'tier') !== undefined) {
|
|
189
|
+
return { success: false, error: '--tier is valid only with --full.\n' + USAGE };
|
|
190
|
+
}
|
|
86
191
|
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
87
192
|
const positional = rest.filter(a => typeof a === 'string' && !a.startsWith('--'));
|
|
88
193
|
const name = positional[0];
|
|
@@ -116,7 +221,7 @@ function handleEval(rest, flags, projectRoot) {
|
|
|
116
221
|
}
|
|
117
222
|
const lines = ['Static scorecards written (deterministic tier):'];
|
|
118
223
|
for (const t of written) lines.push(' ' + t + ' composite=' + cards[t].composite);
|
|
119
|
-
lines.push('', '
|
|
224
|
+
lines.push('', 'Use --full --tier 30|100|300 for controlled behavioral evidence.');
|
|
120
225
|
return { success: true, cards, output: lines.join('\n') };
|
|
121
226
|
}
|
|
122
227
|
|
|
@@ -243,18 +348,21 @@ module.exports = {
|
|
|
243
348
|
usage: USAGE,
|
|
244
349
|
flags: {
|
|
245
350
|
'--json': 'Emit the machine-readable result',
|
|
246
|
-
'--static': 'Score only the deterministic static tier
|
|
351
|
+
'--static': 'Score only the deterministic static tier',
|
|
352
|
+
'--full': 'Run the controlled behavioral evaluation tier',
|
|
353
|
+
'--tier': `Behavioral corpus tier: ${BEHAVIORAL_TIER_USAGE}`,
|
|
247
354
|
},
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// from the package root; eval/scores read the canonical skills
|
|
251
|
-
handler: (args,
|
|
355
|
+
// The registry passes (args, flags, projectRoot, opts). Only the trailing `opts` carries a
|
|
356
|
+
// default so SonarCloud S1788 stays satisfied; `flags` is normalized in the body. The router
|
|
357
|
+
// reads the canonical catalog from the package root; eval/scores read the canonical skills dir.
|
|
358
|
+
handler: (args, rawFlags, projectRoot, opts = {}) => {
|
|
359
|
+
const flags = rawFlags || {};
|
|
252
360
|
const verb = args[0];
|
|
253
361
|
if (verb === 'for') {
|
|
254
362
|
return handleFor(args.slice(1), flags);
|
|
255
363
|
}
|
|
256
364
|
if (verb === 'eval') {
|
|
257
|
-
return handleEval(args.slice(1), flags, projectRoot);
|
|
365
|
+
return handleEval(args.slice(1), flags, projectRoot, opts);
|
|
258
366
|
}
|
|
259
367
|
if (verb === 'scores') {
|
|
260
368
|
return handleScores(args.slice(1), flags, projectRoot);
|
|
@@ -271,5 +379,5 @@ module.exports = {
|
|
|
271
379
|
};
|
|
272
380
|
},
|
|
273
381
|
// Exposed for unit tests; not part of the CLI surface.
|
|
274
|
-
_internal: { parseForArgs, formatRouting, handleFor, handleEval, handleScores, handleCoverage, formatScores, formatCoverage },
|
|
382
|
+
_internal: { parseForArgs, formatRouting, handleFor, handleEval, handleFullEval, handleScores, handleCoverage, formatScores, formatCoverage },
|
|
275
383
|
};
|
package/lib/commands/status.js
CHANGED
|
@@ -751,12 +751,28 @@ function formatStatus(result) {
|
|
|
751
751
|
module.exports = {
|
|
752
752
|
name: 'status',
|
|
753
753
|
description: 'One-glance orientation: where you are, what to run next, and your work',
|
|
754
|
-
usage: 'forge status [--full] [--json]',
|
|
754
|
+
usage: 'forge status [-v|--verbose] [--full] [--json]',
|
|
755
755
|
flags: {
|
|
756
|
+
'-v, --verbose': 'Print the full session-entry briefing (replaces the deprecated `forge prime`)',
|
|
756
757
|
'--full': 'Also show blocked, stale, and recently completed issues',
|
|
757
758
|
'--json': 'Emit the machine-readable status envelope',
|
|
758
759
|
},
|
|
759
760
|
handler: async (args, flags, projectRoot) => {
|
|
761
|
+
// git-status-v semantics: bare status is the short pulse, -v is the long form.
|
|
762
|
+
// `--verbose` also arrives as flags.verbose from the global CLI flag parser.
|
|
763
|
+
const statusArgs = Array.isArray(args) ? args : [];
|
|
764
|
+
if (flags?.verbose === true || statusArgs.includes('-v') || statusArgs.includes('--verbose')) {
|
|
765
|
+
// Required lazily so the short pulse never loads the briefing's module graph.
|
|
766
|
+
const { renderBriefing } = require('./prime');
|
|
767
|
+
// The briefing reads --json from args (same as prime), so fold in the flag
|
|
768
|
+
// forms the short pulse accepts or `-v` would silently drop them.
|
|
769
|
+
const wantsJson = flags?.json === true || flags?.['--json'] === true;
|
|
770
|
+
const briefingArgs = wantsJson && !statusArgs.includes('--json')
|
|
771
|
+
? [...statusArgs, '--json']
|
|
772
|
+
: statusArgs;
|
|
773
|
+
return renderBriefing(briefingArgs, projectRoot || process.cwd());
|
|
774
|
+
}
|
|
775
|
+
|
|
760
776
|
// Injectable snapshot read, same convention as setup.js's `options.commandRunner`
|
|
761
777
|
// (lib/commands/setup.js:223): a real default with a test-supplied override. Tests
|
|
762
778
|
// need a snapshot with controlled ids and orderings, which a seeded kernel cannot
|
package/lib/commands/test.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Wraps test execution with smart defaults:
|
|
7
7
|
* - Auto-detects package manager from lockfiles
|
|
8
|
-
* - Checks Beads (Dolt) connectivity, sets BEADS_SKIP_TESTS if unavailable
|
|
9
8
|
* - Supports --affected flag to run only tests for changed files
|
|
10
9
|
*
|
|
11
10
|
* Security: Uses execFileSync for subprocess calls (OWASP A03)
|
|
@@ -17,6 +16,8 @@ const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = requi
|
|
|
17
16
|
const defaultFs = require('node:fs');
|
|
18
17
|
const path = require('node:path');
|
|
19
18
|
|
|
19
|
+
const { selectDocAssertingTests } = require('../doc-assertions');
|
|
20
|
+
|
|
20
21
|
/** @type {Array<[string, string]>} Lockfile → package manager mapping (order matters) */
|
|
21
22
|
const LOCKFILE_MAP = [
|
|
22
23
|
['bun.lockb', 'bun'],
|
|
@@ -26,12 +27,10 @@ const LOCKFILE_MAP = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
const DEFAULT_TIMEOUT = 120000;
|
|
29
|
-
const BEADS_CHECK_TIMEOUT = 3000;
|
|
30
30
|
const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
31
31
|
'bin/forge.js': [
|
|
32
32
|
'test/cli-flags.test.js',
|
|
33
33
|
'test/forge-cli-registry.test.js',
|
|
34
|
-
'test/setup-github-sync.test.js',
|
|
35
34
|
'test/setup-runtime-flags.test.js',
|
|
36
35
|
],
|
|
37
36
|
// The second CLI surface (command descriptions + shepherd wiring). Without an
|
|
@@ -41,6 +40,13 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
41
40
|
'test/forge-cmd-shepherd.test.js',
|
|
42
41
|
],
|
|
43
42
|
'bin/forge-preflight.js': ['test/bin/forge-preflight.test.js'],
|
|
43
|
+
// The CI entry point is covered by the doc-assertion regression suite; without
|
|
44
|
+
// this entry it resolves to zero tests and forces the full suite.
|
|
45
|
+
// (lib/doc-assertions.js resolves via the lib/ -> test/ convention below.)
|
|
46
|
+
'scripts/doc-asserting-tests.js': [
|
|
47
|
+
'test/doc-assertions.test.js',
|
|
48
|
+
'test/scripts/doc-asserting-tests.test.js',
|
|
49
|
+
],
|
|
44
50
|
'lib/lefthook-check.js': ['test/lefthook-check.test.js', 'test/runtime-health.test.js'],
|
|
45
51
|
'lib/runtime-health.js': ['test/runtime-health.test.js'],
|
|
46
52
|
// skill-eval hosts the accuracy-lint detectors (auditCommandDocumentation /
|
|
@@ -84,22 +90,6 @@ function detectPackageManager(projectRoot, fs) {
|
|
|
84
90
|
return 'npm';
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
/**
|
|
88
|
-
* Check if Beads (bd CLI) is reachable.
|
|
89
|
-
*
|
|
90
|
-
* @param {Function} execFileSync - Injected execFileSync
|
|
91
|
-
* @returns {boolean} true if bd is available
|
|
92
|
-
*/
|
|
93
|
-
function checkBeadsConnectivity(execFileSync) {
|
|
94
|
-
try {
|
|
95
|
-
execFileSync('bd', ['list', '--limit=1'], { timeout: BEADS_CHECK_TIMEOUT });
|
|
96
|
-
return true;
|
|
97
|
-
} catch (_e) { // NOSONAR S2486
|
|
98
|
-
/* intentional: bd not installed or unreachable */
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
93
|
function resolveBaseBranch(execFileSync) {
|
|
104
94
|
let baseBranch = 'main';
|
|
105
95
|
try {
|
|
@@ -196,6 +186,16 @@ function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options
|
|
|
196
186
|
}
|
|
197
187
|
}
|
|
198
188
|
|
|
189
|
+
// Markdown changes must also run the suites that assert on that markdown's
|
|
190
|
+
// CONTENT (README size badge, AGENTS.md convention, docs consistency). The
|
|
191
|
+
// static mapping above only knows the docs-consistency suite, so a docs PR
|
|
192
|
+
// used to merge green and break master for the next code PR (issue 63556816).
|
|
193
|
+
// The set is derived from the test sources, so new markdown-reading suites are
|
|
194
|
+
// picked up without editing a list here.
|
|
195
|
+
for (const candidate of selectDocAssertingTests(changedFiles, projectRoot, fs)) {
|
|
196
|
+
testFiles.add(candidate);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
199
|
return Array.from(testFiles).sort((left, right) => left.localeCompare(right));
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -291,7 +291,7 @@ module.exports = {
|
|
|
291
291
|
* @param {Object} [deps.fs] - fs module
|
|
292
292
|
* @param {Function} [deps.execFileSync] - child_process.execFileSync
|
|
293
293
|
* @param {Function} [deps.spawnSync] - child_process.spawnSync
|
|
294
|
-
* @returns {Promise<{ success: boolean, exitCode: number
|
|
294
|
+
* @returns {Promise<{ success: boolean, exitCode: number }>}
|
|
295
295
|
*/
|
|
296
296
|
async handler(_args, flags, projectRoot, deps = {}) {
|
|
297
297
|
const fs = deps.fs || defaultFs;
|
|
@@ -313,19 +313,10 @@ module.exports = {
|
|
|
313
313
|
}
|
|
314
314
|
} catch (_e) { /* intentional: package.json missing or unreadable, use default timeout */ } // NOSONAR S2486
|
|
315
315
|
|
|
316
|
-
// 3.
|
|
317
|
-
const beadsAvailable = checkBeadsConnectivity(execFileSync);
|
|
318
|
-
const beadsSkipped = !beadsAvailable;
|
|
319
|
-
|
|
320
|
-
const extraEnv = {};
|
|
321
|
-
if (beadsSkipped) {
|
|
322
|
-
extraEnv.BEADS_SKIP_TESTS = '1';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 4. Build test command args
|
|
316
|
+
// 3. Build test command args
|
|
326
317
|
let testArgs = ['run', 'test'];
|
|
327
318
|
|
|
328
|
-
//
|
|
319
|
+
// 4. --affected flag: find changed test files
|
|
329
320
|
if (flags['--affected'] || flags.affected) {
|
|
330
321
|
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
331
322
|
sinceUpstream: flags.sinceUpstream || flags['--since-upstream'],
|
|
@@ -336,9 +327,9 @@ module.exports = {
|
|
|
336
327
|
// If no affected tests found, fall back to running all tests
|
|
337
328
|
}
|
|
338
329
|
|
|
339
|
-
//
|
|
330
|
+
// 5. Run tests
|
|
340
331
|
const result = spawnSync(pkgManager, testArgs, {
|
|
341
|
-
env: { ...process.env
|
|
332
|
+
env: { ...process.env },
|
|
342
333
|
timeout,
|
|
343
334
|
stdio: 'inherit',
|
|
344
335
|
shell: process.platform === 'win32',
|
|
@@ -349,7 +340,6 @@ module.exports = {
|
|
|
349
340
|
return {
|
|
350
341
|
success: exitCode === 0,
|
|
351
342
|
exitCode,
|
|
352
|
-
beadsSkipped,
|
|
353
343
|
};
|
|
354
344
|
},
|
|
355
345
|
};
|