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/scripts/test.js
CHANGED
|
@@ -4,15 +4,27 @@
|
|
|
4
4
|
*
|
|
5
5
|
* It runs only the tests affected by known changes when possible and falls back
|
|
6
6
|
* to the full suite for package-level or unknown-file changes.
|
|
7
|
+
*
|
|
8
|
+
* Quick lane: `forge push --quick` sets FORGE_PUSH_LANE=quick on the `git push`
|
|
9
|
+
* it spawns, declaring the lint-only review-cycle lane. The pre-push entry point
|
|
10
|
+
* honors that declaration and skips the test run with a loud notice, so --quick
|
|
11
|
+
* is quick end to end instead of only in the step push.js controls. This is not
|
|
12
|
+
* a hook bypass: branch protection and lint still run, local validation
|
|
13
|
+
* (`--validate`) ignores the variable entirely, and CI runs the full matrix.
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
|
-
const {
|
|
16
|
+
const { EventEmitter } = require('node:events');
|
|
17
|
+
const {
|
|
18
|
+
execFileSync: defaultExecFileSync,
|
|
19
|
+
spawn: defaultSpawn,
|
|
20
|
+
} = require('node:child_process');
|
|
10
21
|
const fs = require('node:fs');
|
|
11
22
|
|
|
12
23
|
const {
|
|
13
24
|
getAffectedTestFiles,
|
|
14
25
|
getChangedFiles,
|
|
15
26
|
} = require('../lib/commands/test');
|
|
27
|
+
const { createProcessTree, signalExitCode } = require('./process-tree');
|
|
16
28
|
|
|
17
29
|
const PACKAGE_LEVEL_PATHS = new Set([
|
|
18
30
|
'package.json',
|
|
@@ -64,6 +76,21 @@ const DEFAULT_FULL_SUITE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
64
76
|
// Conventional shell exit code for a command terminated by a timeout.
|
|
65
77
|
const TIMEOUT_EXIT_CODE = 124;
|
|
66
78
|
|
|
79
|
+
// Lane declaration set by `forge push --quick` on the spawned `git push`.
|
|
80
|
+
// Only this exact value opts into the lint-only lane.
|
|
81
|
+
const QUICK_LANE_ENV_VAR = 'FORGE_PUSH_LANE';
|
|
82
|
+
const QUICK_LANE_VALUE = 'quick';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reports whether the environment declares the lint-only quick push lane.
|
|
86
|
+
*
|
|
87
|
+
* @param {NodeJS.ProcessEnv} [env=process.env] Environment to inspect.
|
|
88
|
+
* @returns {boolean} True when the quick lane is explicitly declared.
|
|
89
|
+
*/
|
|
90
|
+
function isQuickPushLane(env = process.env) {
|
|
91
|
+
return env[QUICK_LANE_ENV_VAR] === QUICK_LANE_VALUE;
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
/**
|
|
68
95
|
* Reads and validates the FORGE_TEST_TIMEOUT_MS override, if any.
|
|
69
96
|
*
|
|
@@ -134,9 +161,11 @@ function detectPackageManager() {
|
|
|
134
161
|
function stripGitHookEnv(sourceEnv = process.env) {
|
|
135
162
|
const env = { ...sourceEnv };
|
|
136
163
|
for (const key of Object.keys(env)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
||
|
|
164
|
+
const normalizedKey = key.toUpperCase();
|
|
165
|
+
if (normalizedKey === 'GIT_DIR' || normalizedKey === 'GIT_WORK_TREE'
|
|
166
|
+
|| normalizedKey === 'GIT_INDEX_FILE' || normalizedKey === 'GIT_OBJECT_DIRECTORY'
|
|
167
|
+
|| normalizedKey === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
|
|
168
|
+
|| normalizedKey === 'GIT_QUARANTINE_PATH') {
|
|
140
169
|
delete env[key];
|
|
141
170
|
}
|
|
142
171
|
}
|
|
@@ -317,31 +346,78 @@ function classifyPushTests(projectRoot, execFileSync = defaultExecFileSync) {
|
|
|
317
346
|
*
|
|
318
347
|
* @param {string} command Executable name.
|
|
319
348
|
* @param {string[]} args Command arguments.
|
|
320
|
-
* @param {import('node:child_process').
|
|
321
|
-
* @param {typeof
|
|
322
|
-
* @returns {number} Process exit status, or 1 when no status is reported.
|
|
349
|
+
* @param {import('node:child_process').SpawnOptions} [options={}] Spawn options.
|
|
350
|
+
* @param {typeof defaultSpawn} [spawn=defaultSpawn] Process runner.
|
|
351
|
+
* @returns {Promise<number>} Process exit status, or 1 when no status is reported.
|
|
323
352
|
*/
|
|
324
|
-
function runCommand(command, args, options = {},
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return
|
|
353
|
+
function runCommand(command, args, options = {}, spawn = defaultSpawn) {
|
|
354
|
+
const { processTree, timeout, killSignal: _killSignal, ...spawnOptions } = options;
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
let child;
|
|
357
|
+
let settled = false;
|
|
358
|
+
let timer = null;
|
|
359
|
+
const reservation = processTree?.reserveChild?.({ kind: 'test-lane', label: command }) || null;
|
|
360
|
+
let registered = false;
|
|
361
|
+
const release = () => {
|
|
362
|
+
if (registered) {
|
|
363
|
+
processTree?.unregisterChild?.(reservation);
|
|
364
|
+
registered = false;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const finish = (status) => {
|
|
368
|
+
if (settled) return;
|
|
369
|
+
settled = true;
|
|
370
|
+
release();
|
|
371
|
+
resolve(status ?? 1);
|
|
372
|
+
};
|
|
373
|
+
const fail = (error) => {
|
|
374
|
+
if (settled) return;
|
|
375
|
+
if (timer) clearTimeout(timer);
|
|
376
|
+
if (error?.code === 'ETIMEDOUT') {
|
|
377
|
+
processTree?.cleanup?.('SIGKILL');
|
|
378
|
+
console.error('');
|
|
379
|
+
console.error('Test lane exceeded its wall-clock ceiling and was terminated.');
|
|
380
|
+
console.error('A single test likely hung (e.g. a spawned git/bash call that never returns).');
|
|
381
|
+
console.error('Adjust the ceiling with FORGE_TEST_TIMEOUT_MS. Failing the run instead of');
|
|
382
|
+
console.error('blocking the push. See issue 8aef79e8.');
|
|
383
|
+
console.error('');
|
|
384
|
+
finish(TIMEOUT_EXIT_CODE);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
settled = true;
|
|
388
|
+
release();
|
|
389
|
+
reject(error);
|
|
390
|
+
};
|
|
391
|
+
try {
|
|
392
|
+
child = spawn(command, args, { stdio: 'inherit', shell: isWindows, ...spawnOptions });
|
|
393
|
+
} catch (error) {
|
|
394
|
+
fail(error);
|
|
395
|
+
return;
|
|
340
396
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
397
|
+
child.once?.('error', fail);
|
|
398
|
+
child.once?.('close', (status, signal) => {
|
|
399
|
+
if (timer) clearTimeout(timer);
|
|
400
|
+
finish(status ?? signalExitCode(signal));
|
|
401
|
+
});
|
|
402
|
+
if (reservation && typeof processTree?.registerChild === 'function') {
|
|
403
|
+
registered = true;
|
|
404
|
+
if (!processTree.registerChild(reservation, child)) {
|
|
405
|
+
processTree.abortChild?.(reservation, child);
|
|
406
|
+
release();
|
|
407
|
+
fail(new Error('test lane process could not be registered'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
timer = timeout > 0 ? setTimeout(() => {
|
|
412
|
+
processTree?.cleanup?.('SIGKILL');
|
|
413
|
+
try { child.kill?.('SIGKILL'); } catch { /* best effort */ }
|
|
414
|
+
finish(TIMEOUT_EXIT_CODE);
|
|
415
|
+
}, timeout) : null;
|
|
416
|
+
if (!child.once) {
|
|
417
|
+
if (timer) clearTimeout(timer);
|
|
418
|
+
finish(child.status ?? 1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
345
421
|
}
|
|
346
422
|
|
|
347
423
|
/**
|
|
@@ -351,51 +427,92 @@ function runCommand(command, args, options = {}, spawnSync = defaultSpawnSync) {
|
|
|
351
427
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
352
428
|
* @returns {number} Exit status for the executed plan.
|
|
353
429
|
*/
|
|
354
|
-
function runTestExecutionPlan(plan, deps = {}) {
|
|
355
|
-
const
|
|
430
|
+
async function runTestExecutionPlan(plan, deps = {}) {
|
|
431
|
+
const usesSpawnSyncAdapter = !deps.spawn && typeof deps.spawnSync === 'function';
|
|
432
|
+
const spawn = deps.spawn || (usesSpawnSyncAdapter ? (...args) => {
|
|
433
|
+
const child = new EventEmitter();
|
|
434
|
+
let result;
|
|
435
|
+
try {
|
|
436
|
+
result = deps.spawnSync(...args);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
process.nextTick(() => child.emit('error', error));
|
|
439
|
+
return child;
|
|
440
|
+
}
|
|
441
|
+
child.pid = result?.pid || process.pid;
|
|
442
|
+
process.nextTick(() => {
|
|
443
|
+
if (result?.error) child.emit('error', result.error);
|
|
444
|
+
else child.emit('close', result?.status ?? 1, result?.signal);
|
|
445
|
+
});
|
|
446
|
+
return child;
|
|
447
|
+
} : defaultSpawn);
|
|
356
448
|
const pkgManager = deps.pkgManager || detectPackageManager();
|
|
357
449
|
const env = deps.env || stripGitHookEnv(process.env);
|
|
450
|
+
// The injected synchronous runner has already exited when its adapter returns,
|
|
451
|
+
// so there is no live child to register or reap through host process probes.
|
|
452
|
+
const processTree = deps.processTree || (usesSpawnSyncAdapter ? {} : createProcessTree({
|
|
453
|
+
env,
|
|
454
|
+
platform: deps.platform,
|
|
455
|
+
}));
|
|
456
|
+
let signal = null;
|
|
457
|
+
const removeSignalHandlers = typeof processTree.installSignalHandlers === 'function'
|
|
458
|
+
? processTree.installSignalHandlers((received) => {
|
|
459
|
+
signal = received;
|
|
460
|
+
})
|
|
461
|
+
: () => {};
|
|
462
|
+
const childEnv = typeof processTree.envFor === 'function' ? processTree.envFor(env) : env;
|
|
358
463
|
const bunCommand = deps.bunCommand || env.BUN_EXE || process.env.BUN_EXE || 'bun';
|
|
359
464
|
const label = deps.label || 'tests';
|
|
360
465
|
const timeout = resolveCommandTimeoutMs(env);
|
|
361
|
-
const laneOptions = { env, killSignal: 'SIGKILL', timeout };
|
|
466
|
+
const laneOptions = { env: childEnv, killSignal: 'SIGKILL', timeout, processTree };
|
|
362
467
|
// The full-suite fallback gets a larger, validation-aligned budget so a
|
|
363
468
|
// healthy-but-slow full run is not failed fast by the targeted-lane ceiling.
|
|
364
|
-
const fullSuiteOptions = {
|
|
469
|
+
const fullSuiteOptions = {
|
|
470
|
+
env: childEnv,
|
|
471
|
+
killSignal: 'SIGKILL',
|
|
472
|
+
timeout: resolveFullSuiteTimeoutMs(env),
|
|
473
|
+
processTree,
|
|
474
|
+
};
|
|
365
475
|
|
|
366
476
|
console.log(`Running ${label} (${pkgManager})...`);
|
|
367
477
|
|
|
368
478
|
try {
|
|
369
479
|
if (plan.runFullSuite) {
|
|
370
480
|
console.log(` Mode: full suite (${plan.reason})`);
|
|
371
|
-
|
|
481
|
+
const status = await runCommand('node', ['scripts/test-full-suite.js'], fullSuiteOptions, spawn);
|
|
482
|
+
if (signal) return signalExitCode(signal);
|
|
372
483
|
if (status !== 0) return status;
|
|
373
484
|
} else if (plan.testTargets.length > 0) {
|
|
374
485
|
console.log(` Mode: targeted (${plan.testTargets.length} test file${plan.testTargets.length === 1 ? '' : 's'})`);
|
|
375
486
|
const command = pkgManager === 'bun' ? bunCommand : pkgManager;
|
|
376
|
-
|
|
487
|
+
const status = await runCommand(command, ['run', 'test', ...plan.testTargets], laneOptions, spawn);
|
|
488
|
+
if (signal) return signalExitCode(signal);
|
|
377
489
|
if (status !== 0) return status;
|
|
378
490
|
}
|
|
379
491
|
|
|
380
492
|
if (plan.runE2E) {
|
|
381
493
|
console.log(' Extra: running affected e2e tests');
|
|
382
|
-
|
|
494
|
+
const status = await runCommand(bunCommand, ['test', '--timeout', '15000', 'test/e2e/'], laneOptions, spawn);
|
|
495
|
+
if (signal) return signalExitCode(signal);
|
|
383
496
|
if (status !== 0) return status;
|
|
384
497
|
}
|
|
385
498
|
|
|
386
499
|
if (!plan.runFullSuite && plan.runTestEnv) {
|
|
387
500
|
console.log(' Extra: running affected edge-case tests');
|
|
388
|
-
|
|
501
|
+
const status = await runCommand(bunCommand, ['test', '--timeout', '15000', 'test-env/'], laneOptions, spawn);
|
|
502
|
+
if (signal) return signalExitCode(signal);
|
|
389
503
|
if (status !== 0) return status;
|
|
390
504
|
}
|
|
391
505
|
|
|
392
506
|
console.log('Relevant tests passed');
|
|
393
|
-
return 0;
|
|
507
|
+
return signal ? signalExitCode(signal) : 0;
|
|
394
508
|
} catch (error) {
|
|
395
509
|
console.error('');
|
|
396
510
|
console.error(`Failed to run ${label}: ${error.message}`);
|
|
397
511
|
console.error('');
|
|
398
|
-
return 1;
|
|
512
|
+
return signal ? signalExitCode(signal) : 1;
|
|
513
|
+
} finally {
|
|
514
|
+
removeSignalHandlers();
|
|
515
|
+
processTree.cleanup?.(signal ? 'SIGKILL' : 'SIGTERM');
|
|
399
516
|
}
|
|
400
517
|
}
|
|
401
518
|
|
|
@@ -406,7 +523,15 @@ function runTestExecutionPlan(plan, deps = {}) {
|
|
|
406
523
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
407
524
|
* @returns {number} Exit status for pre-push tests.
|
|
408
525
|
*/
|
|
409
|
-
function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
526
|
+
async function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
527
|
+
if (isQuickPushLane(deps.env || process.env)) {
|
|
528
|
+
console.log('');
|
|
529
|
+
console.log(' quick lane: tests skipped locally — CI runs the full matrix');
|
|
530
|
+
console.log(' Run `forge push` (no --quick) to test before merge.');
|
|
531
|
+
console.log('');
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
410
535
|
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
411
536
|
const plan = classifyPushTests(projectRoot, execFileSync);
|
|
412
537
|
return runTestExecutionPlan(plan, { ...deps, label: 'pre-push tests' });
|
|
@@ -419,26 +544,34 @@ function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
|
419
544
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
420
545
|
* @returns {number} Exit status for local validation tests.
|
|
421
546
|
*/
|
|
422
|
-
function runLocalValidationTests(projectRoot = process.cwd(), deps = {}) {
|
|
547
|
+
async function runLocalValidationTests(projectRoot = process.cwd(), deps = {}) {
|
|
423
548
|
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
424
549
|
const plan = buildTestExecutionPlan(projectRoot, execFileSync, { sinceUpstream: true });
|
|
425
550
|
return runTestExecutionPlan(plan, { ...deps, label: 'local validation tests' });
|
|
426
551
|
}
|
|
427
552
|
|
|
428
553
|
if (require.main === module) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
554
|
+
(async () => {
|
|
555
|
+
const exitCode = process.argv.includes('--validate')
|
|
556
|
+
? await runLocalValidationTests()
|
|
557
|
+
: await runPrePushTests();
|
|
558
|
+
process.exit(exitCode);
|
|
559
|
+
})().catch((error) => {
|
|
560
|
+
console.error(error);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
});
|
|
433
563
|
}
|
|
434
564
|
|
|
435
565
|
module.exports = {
|
|
436
566
|
ALWAYS_RUN_RISK_TEST_TARGETS,
|
|
437
567
|
DEFAULT_FULL_SUITE_TIMEOUT_MS,
|
|
438
568
|
DEFAULT_TEST_COMMAND_TIMEOUT_MS,
|
|
569
|
+
QUICK_LANE_ENV_VAR,
|
|
570
|
+
QUICK_LANE_VALUE,
|
|
439
571
|
buildTestExecutionPlan,
|
|
440
572
|
classifyPushTests,
|
|
441
573
|
detectPackageManager,
|
|
574
|
+
isQuickPushLane,
|
|
442
575
|
resolveCommandTimeoutMs,
|
|
443
576
|
resolveFullSuiteTimeoutMs,
|
|
444
577
|
runLocalValidationTests,
|
package/skills/review/SKILL.md
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review
|
|
3
|
+
invocation: user
|
|
3
4
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
marks it resolved until checks pass, then runs the pre-merge doc gate and hands the PR off
|
|
8
|
-
for MANUAL merge — never runs `gh pr merge`. Use whenever a PR has review feedback or red
|
|
9
|
-
checks: "address the review comments on PR #123", "the bots flagged issues / Greptile score
|
|
10
|
-
is low", "fix the failing checks and resolve the threads", "CodeRabbit and SonarCloud left
|
|
11
|
-
comments", "get my PR merge-ready". This MUTATING feedback-resolution stage is distinct from
|
|
12
|
-
shepherd (only WATCHES an open PR, changes nothing), verify (POST-merge CI health check +
|
|
13
|
-
closing issues), ship (pushes the branch + opens the PR), validate (PRE-PR local
|
|
14
|
-
type/lint/test/security), and sonarcloud / sonarcloud-analysis (only QUERY SonarCloud data).
|
|
5
|
+
Drive an already-open PR's feedback and failing checks to all-green, resolve every thread,
|
|
6
|
+
run the pre-merge doc gate, then stop for manual merge. Use only when the user asks to
|
|
7
|
+
address PR feedback or checks; never merges or substitutes for Shepherd monitoring.
|
|
15
8
|
allowed-tools: Bash, Read, Edit, Grep, Glob
|
|
16
9
|
next: verify
|
|
17
10
|
terminal: false
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"fixtures": "present",
|
|
4
4
|
"static": {
|
|
5
5
|
"token_cost": {
|
|
6
|
-
"desc_chars":
|
|
6
|
+
"desc_chars": 261,
|
|
7
7
|
"body_lines": 476,
|
|
8
|
-
"score":
|
|
8
|
+
"score": 40
|
|
9
9
|
},
|
|
10
10
|
"caps": {
|
|
11
11
|
"desc_within": true,
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"variance": null,
|
|
38
38
|
"note": "behavioral — W5 (LLM judge): semantic recall/precision/chain/outcome/variance, not deterministic"
|
|
39
39
|
},
|
|
40
|
-
"composite":
|
|
40
|
+
"composite": 82
|
|
41
41
|
}
|
package/skills/rollback/SKILL.md
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rollback
|
|
3
|
+
invocation: user
|
|
3
4
|
description: >
|
|
4
|
-
Safely
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
PR (git revert -m 1), restore individual files to a prior version, revert a commit range, or
|
|
8
|
-
dry-run/preview first. Use when the user says roll back, revert, undo, back out, or restore
|
|
9
|
-
— e.g. "undo the last commit", "undo that change, the approach was wrong", "back out PR
|
|
10
|
-
#123", "revert the merge commit", "restore src/auth.js to before my last commit", or when a
|
|
11
|
-
shipped/merged change broke something and must be pulled back. Do NOT use for: replying to
|
|
12
|
-
or fixing PR review feedback (review), monitoring an open PR's checks toward merge
|
|
13
|
-
(shepherd), the post-merge CI health check on master (verify), pushing a branch or opening a
|
|
14
|
-
PR (ship), or ordinary issue status/field edits (issue-basics).
|
|
5
|
+
Safely undo committed or shipped changes with non-destructive git revert or restore. Use
|
|
6
|
+
only when the user explicitly asks to roll back, revert, undo, back out, or restore; never
|
|
7
|
+
reset hard, force-push, or substitute for PR review, monitoring, or issue status work.
|
|
15
8
|
allowed-tools: Bash, Read, Edit, Grep, Glob
|
|
16
9
|
terminal: true
|
|
17
10
|
---
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"fixtures": "present",
|
|
4
4
|
"static": {
|
|
5
5
|
"token_cost": {
|
|
6
|
-
"desc_chars":
|
|
6
|
+
"desc_chars": 266,
|
|
7
7
|
"body_lines": 95,
|
|
8
|
-
"score":
|
|
8
|
+
"score": 78
|
|
9
9
|
},
|
|
10
10
|
"caps": {
|
|
11
11
|
"desc_within": true,
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"variance": null,
|
|
38
38
|
"note": "behavioral — W5 (LLM judge): semantic recall/precision/chain/outcome/variance, not deterministic"
|
|
39
39
|
},
|
|
40
|
-
"composite":
|
|
40
|
+
"composite": 93
|
|
41
41
|
}
|
package/skills/shepherd/SKILL.md
CHANGED
|
@@ -6,8 +6,8 @@ description: >
|
|
|
6
6
|
open PR, converges CI check state into kernel verdicts, re-runs flaky required
|
|
7
7
|
checks, reaps orphan watchers, and self-retires when no PRs remain; one-shot
|
|
8
8
|
passes (forge shepherd <pr>) exist for a single bounded check. Use when a PR was
|
|
9
|
-
just opened or shipped; when a session starts with open PRs (
|
|
10
|
-
|
|
9
|
+
just opened or shipped; when a session starts with open PRs (Forge automatically
|
|
10
|
+
wakes the singleton; the lease makes a duplicate start a safe no-op);
|
|
11
11
|
when the user asks "why isn't my PR merging", "what's
|
|
12
12
|
blocking the PR", "is the PR ready", "did a check fail / go red"; when asked to
|
|
13
13
|
"keep watching / keep an eye on / babysit my PRs"; or to read PR verdicts
|
|
@@ -34,15 +34,18 @@ Forge owns your open PRs. A singleton daemon converges every open PR toward merg
|
|
|
34
34
|
- User asks about **one PR, right now** ("is #212 ready?", "why is this blocked?") → **one-shot** (`forge shepherd <pr> --pull`).
|
|
35
35
|
- Anything **ongoing** — "keep", "until it merges", "watch", "babysit", multiple PRs, or a session that starts with PRs already open → **ensure the daemon is running**.
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Automatic attachment
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Forge wakes the singleton after a successful supported session start, every
|
|
40
|
+
successful push, and every successful non-dry-run ship. Push does not require a
|
|
41
|
+
locally resolved PR number; the daemon enumerates all open and draft PRs itself.
|
|
42
|
+
Ordinary commands do not launch it.
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
The daemon is a **repo singleton** guarded by an O_EXCL lease, so no liveness
|
|
45
|
+
check is needed first. Forge prefers a supplied harness background shell; its
|
|
46
|
+
bare-CLI detached fallback launches from the stable common repository root, not
|
|
47
|
+
the current disposable worktree. Agents never launch a detached spawn directly.
|
|
48
|
+
(A `forge prime` daemon-liveness line remains the separate W-S5 follow-up.)
|
|
46
49
|
|
|
47
50
|
## Reading verdicts (the common case)
|
|
48
51
|
|
|
@@ -69,8 +72,8 @@ forge shepherd events <pr> --since <seq> # only the new events since sequence
|
|
|
69
72
|
|
|
70
73
|
| Situation | Command |
|
|
71
74
|
| --- | --- |
|
|
72
|
-
| PR just opened / shipped |
|
|
73
|
-
| Session starts
|
|
75
|
+
| PR just opened / shipped | automatic singleton wake; read with `forge shepherd <pr> --pull` |
|
|
76
|
+
| Session starts with open PRs | automatic singleton wake |
|
|
74
77
|
| "Why isn't my PR merging / what's blocking it" | `forge shepherd <pr> --pull` |
|
|
75
78
|
| "Is the PR ready?" | `forge shepherd <pr> --pull` (read `MERGE_READY`) |
|
|
76
79
|
| "A check failed / went red" | `forge shepherd <pr> --pull --json` (read `failures[]`) |
|
|
@@ -95,11 +98,14 @@ forge shepherd events <pr> --since <seq> # only the new events since sequence
|
|
|
95
98
|
## Kill-switches
|
|
96
99
|
|
|
97
100
|
```bash
|
|
98
|
-
FORGE_SHEPHERD_DISABLE=1
|
|
99
|
-
forge gate disable rail.auto_shepherd
|
|
101
|
+
FORGE_SHEPHERD_DISABLE=1 # env: makes automatic firing inert
|
|
102
|
+
forge gate disable rail.auto_shepherd # config gate honored by every automatic seam
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
CI/test (`NODE_ENV=test`, `BUN_ENV=test`, `CI`, `GITHUB_ACTIONS`, or
|
|
106
|
+
`GITLAB_CI`), dry-run, uninitialized repositories, and disabled paths return
|
|
107
|
+
before lease, Kernel-state, or process work. All leave the manual
|
|
108
|
+
`forge shepherd` surface usable; they only stop automatic daemon fire.
|
|
103
109
|
|
|
104
110
|
## State
|
|
105
111
|
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ship
|
|
3
|
+
invocation: user
|
|
3
4
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
want a PR on the board. Triggers: "ship it", "ship this branch", "open the PR", "push and
|
|
8
|
-
open a PR", "gh pr create", "checks passed, now cut the PR". Runs branch-freshness +
|
|
9
|
-
parallel-PR merge-sim checks, force-with-lease push, records the ship->review handoff, then
|
|
10
|
-
stops. One stage only; not for: the whole plan->dev->validate->ship->review pipeline or
|
|
11
|
-
drive-to-done (smith); type-check/lint/tests/security first (validate); addressing PR
|
|
12
|
-
comments or resolving Greptile/SonarCloud/CodeRabbit threads on an existing PR (review);
|
|
13
|
-
babysitting an open PR toward merge (shepherd); post-merge CI health check + closing issues
|
|
14
|
-
(verify); reverting an already-shipped change (rollback). If the PR already exists, this is
|
|
15
|
-
not the skill.
|
|
5
|
+
Push a validated feature branch and open its PR from the project's template, then stop for
|
|
6
|
+
manual merge. Use only when the user asks to ship or open the PR after validation; never
|
|
7
|
+
merges, auto-merges, reviews an existing PR, or replaces end-to-end Smith.
|
|
16
8
|
allowed-tools: Bash, Read, Edit, Grep, Glob
|
|
17
9
|
next: review
|
|
18
10
|
terminal: false
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"fixtures": "present",
|
|
4
4
|
"static": {
|
|
5
5
|
"token_cost": {
|
|
6
|
-
"desc_chars":
|
|
6
|
+
"desc_chars": 254,
|
|
7
7
|
"body_lines": 237,
|
|
8
|
-
"score":
|
|
8
|
+
"score": 64
|
|
9
9
|
},
|
|
10
10
|
"caps": {
|
|
11
11
|
"desc_within": true,
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"variance": null,
|
|
38
38
|
"note": "behavioral — W5 (LLM judge): semantic recall/precision/chain/outcome/variance, not deterministic"
|
|
39
39
|
},
|
|
40
|
-
"composite":
|
|
40
|
+
"composite": 89
|
|
41
41
|
}
|
package/skills/worktree/SKILL.md
CHANGED
|
@@ -71,7 +71,12 @@ forge clean # remove worktrees whose branches merged; FF th
|
|
|
71
71
|
forge clean --dry-run # show what would be removed, change nothing
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
`forge clean` is **squash-aware** — it detects branches that merged via squash, not just fast-forward merges.
|
|
74
|
+
`forge clean` is **squash-aware** — it detects branches that merged via squash, not just fast-forward merges. Removing a merged worktree also closes the kernel issue the branch was linked to, citing the merge as evidence.
|
|
75
|
+
|
|
76
|
+
Two things it will never touch, because closing an issue is irreversible (`done` is terminal):
|
|
77
|
+
|
|
78
|
+
- **A branch with no commits of its own.** A freshly created worktree points at a default-branch commit, which git alone reads as "merged". That is unstarted work — it is kept, and its issue stays open.
|
|
79
|
+
- **A worktree it skipped.** Uncommitted changes hold a worktree back, and a held-back worktree never has its issue closed.
|
|
75
80
|
|
|
76
81
|
### `forge clean` footguns
|
|
77
82
|
|