forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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 +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- 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/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- 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/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -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/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- 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/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -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 +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- 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-profile.js
CHANGED
|
@@ -159,6 +159,17 @@ function finalizeSignalMetrics(metrics, fileDurations) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function parseJUnitTestcases(content) {
|
|
163
|
+
if (typeof content !== 'string') return [];
|
|
164
|
+
return Array.from(
|
|
165
|
+
content.matchAll(/<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g),
|
|
166
|
+
(match) => ({
|
|
167
|
+
attrs: parseAttributes(match[1] || ''),
|
|
168
|
+
body: match[2] || '',
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
function parseJUnitFiles(files) {
|
|
163
174
|
const fileDurations = new Map();
|
|
164
175
|
const timedOutFiles = new Set();
|
|
@@ -177,9 +188,7 @@ function parseJUnitFiles(files) {
|
|
|
177
188
|
suiteDurationMs += Math.round(Number.parseFloat(attrs.time || '0') * 1000);
|
|
178
189
|
}
|
|
179
190
|
|
|
180
|
-
for (const
|
|
181
|
-
const attrs = parseAttributes(caseMatch[1] || caseMatch[3] || '');
|
|
182
|
-
const body = caseMatch[2] || '';
|
|
191
|
+
for (const { attrs, body } of parseJUnitTestcases(content)) {
|
|
183
192
|
const durationMs = Math.round(Number.parseFloat(attrs.time || '0') * 1000);
|
|
184
193
|
const fallbackFile = path.basename(file);
|
|
185
194
|
const testcase = classifyTestcase({
|
|
@@ -273,6 +282,7 @@ module.exports = {
|
|
|
273
282
|
parseArgs,
|
|
274
283
|
parseAttributes,
|
|
275
284
|
parseJUnitFiles,
|
|
285
|
+
parseJUnitTestcases,
|
|
276
286
|
selectPrimaryBucket,
|
|
277
287
|
walk,
|
|
278
288
|
};
|
package/scripts/test.js
CHANGED
|
@@ -4,18 +4,32 @@
|
|
|
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,
|
|
26
|
+
isFullSuiteRequiredFile,
|
|
15
27
|
} = require('../lib/commands/test');
|
|
28
|
+
const { createProcessTree, signalExitCode } = require('./process-tree');
|
|
16
29
|
|
|
17
30
|
const PACKAGE_LEVEL_PATHS = new Set([
|
|
18
31
|
'package.json',
|
|
32
|
+
'bun.lock',
|
|
19
33
|
'bun.lockb',
|
|
20
34
|
'pnpm-lock.yaml',
|
|
21
35
|
'yarn.lock',
|
|
@@ -29,8 +43,49 @@ const KNOWN_TARGETABLE_PREFIXES = [
|
|
|
29
43
|
'.github/agentic-workflows/',
|
|
30
44
|
'.github/workflows/',
|
|
31
45
|
'test/',
|
|
46
|
+
// Source trees with a dedicated suite; see PREFIX_TEST_TARGETS in
|
|
47
|
+
// lib/commands/test.js for the tests each one resolves to. Without both halves
|
|
48
|
+
// (targetable here + a resolved test there) a change falls to the full-suite lane.
|
|
49
|
+
'validation/',
|
|
50
|
+
'eval/',
|
|
51
|
+
'rules/',
|
|
52
|
+
'plugin/',
|
|
32
53
|
];
|
|
33
54
|
|
|
55
|
+
// Individually mapped root files (see DIRECT_TEST_CANDIDATES in lib/commands/test.js).
|
|
56
|
+
const KNOWN_TARGETABLE_FILES = new Set([
|
|
57
|
+
'lefthook.yml',
|
|
58
|
+
'eslint.config.js',
|
|
59
|
+
'.coderabbit.yaml',
|
|
60
|
+
'CODING_STANDARDS.md',
|
|
61
|
+
'.claude-plugin/marketplace.json',
|
|
62
|
+
'lib/kernel/schema.js',
|
|
63
|
+
'lib/kernel/migrations.js',
|
|
64
|
+
'lib/kernel/sqlite-driver.js',
|
|
65
|
+
'lib/commands/shepherd.js',
|
|
66
|
+
'lib/commands/github.js',
|
|
67
|
+
'lib/github-context.js',
|
|
68
|
+
'lib/commands/_registry.js',
|
|
69
|
+
'lib/commands/pr.js',
|
|
70
|
+
'lib/commands/ship.js',
|
|
71
|
+
'lib/commands/merge.js',
|
|
72
|
+
'lib/commands/team.js',
|
|
73
|
+
'lib/commands/clean.js',
|
|
74
|
+
'scripts/github-context-bridge.sh',
|
|
75
|
+
'lib/commands/push.js',
|
|
76
|
+
'lib/commands/hooks.js',
|
|
77
|
+
'lib/commands/serve.js',
|
|
78
|
+
'lib/commands/skill.js',
|
|
79
|
+
'scripts/lib/behavioral-eval-runtime.js',
|
|
80
|
+
'web/dashboard/generate-snapshot.mjs',
|
|
81
|
+
'lib/pr-monitor/watch-owner.js',
|
|
82
|
+
'lib/pr-monitor/reconcile.js',
|
|
83
|
+
'lib/pr-monitor/reconcile-executor.js',
|
|
84
|
+
'lib/pr-monitor/watch-lifecycle.js',
|
|
85
|
+
'lib/pr-monitor/shepherd-lease.js',
|
|
86
|
+
'lib/pr-monitor/monitor.js',
|
|
87
|
+
]);
|
|
88
|
+
|
|
34
89
|
const ALWAYS_RUN_RISK_TEST_TARGETS = [
|
|
35
90
|
// Windows + concurrent filesystem locking has failed post-merge; keep this
|
|
36
91
|
// in the fast PR lane until enough full-matrix runs prove it stable.
|
|
@@ -52,18 +107,48 @@ const isWindows = process.platform === 'win32';
|
|
|
52
107
|
// wedged). Raise it with FORGE_TEST_TIMEOUT_MS for slow machines.
|
|
53
108
|
const DEFAULT_TEST_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
54
109
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
const
|
|
110
|
+
// Measured wall-clock runtime of a HEALTHY, fully passing full suite on a
|
|
111
|
+
// developer Windows machine with pinned Bun 1.3.12 (2026-08-26):
|
|
112
|
+
//
|
|
113
|
+
// 8005 pass, 32 skip, 1 todo, 0 fail — 8038 tests across 594 files [602.43s]
|
|
114
|
+
//
|
|
115
|
+
// Any full-suite budget must clear this by a wide margin. Treat it as a floor to
|
|
116
|
+
// measure against, never as the budget itself.
|
|
117
|
+
const OBSERVED_FULL_SUITE_RUNTIME_MS = 602_430;
|
|
118
|
+
|
|
119
|
+
// Wall-clock budget for the FULL-SUITE lane (`scripts/test-full-suite.js` and the
|
|
120
|
+
// package-level `test` script run by `forge push`), used on package-level,
|
|
121
|
+
// unmapped, or zero-resolved changes.
|
|
122
|
+
//
|
|
123
|
+
// This ceiling exists to catch an INDEFINITE HANG (issue 8aef79e8: a synchronous
|
|
124
|
+
// git/bash spawn wedged during git mid-push state, observed ~50 min), not to
|
|
125
|
+
// bound normal runtime. So it is sized as ~2x the measured healthy runtime above,
|
|
126
|
+
// not fitted to it. The previous 10-min value was BELOW the 602.43s measurement,
|
|
127
|
+
// so it SIGKILLed passing suites — `forge push` died mid-suite with no summary
|
|
128
|
+
// and never pushed. 25 min is the next round number clearing 2x the measurement
|
|
129
|
+
// (1_204_860 ms). Do not shave this back toward the observed runtime; if a
|
|
130
|
+
// slower machine needs more, raise FORGE_TEST_TIMEOUT_MS (which still overrides)
|
|
131
|
+
// and re-measure before changing this default.
|
|
132
|
+
const DEFAULT_FULL_SUITE_TIMEOUT_MS = 25 * 60 * 1000;
|
|
63
133
|
|
|
64
134
|
// Conventional shell exit code for a command terminated by a timeout.
|
|
65
135
|
const TIMEOUT_EXIT_CODE = 124;
|
|
66
136
|
|
|
137
|
+
// Lane declaration set by `forge push --quick` on the spawned `git push`.
|
|
138
|
+
// Only this exact value opts into the lint-only lane.
|
|
139
|
+
const QUICK_LANE_ENV_VAR = 'FORGE_PUSH_LANE';
|
|
140
|
+
const QUICK_LANE_VALUE = 'quick';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reports whether the environment declares the lint-only quick push lane.
|
|
144
|
+
*
|
|
145
|
+
* @param {NodeJS.ProcessEnv} [env=process.env] Environment to inspect.
|
|
146
|
+
* @returns {boolean} True when the quick lane is explicitly declared.
|
|
147
|
+
*/
|
|
148
|
+
function isQuickPushLane(env = process.env) {
|
|
149
|
+
return env[QUICK_LANE_ENV_VAR] === QUICK_LANE_VALUE;
|
|
150
|
+
}
|
|
151
|
+
|
|
67
152
|
/**
|
|
68
153
|
* Reads and validates the FORGE_TEST_TIMEOUT_MS override, if any.
|
|
69
154
|
*
|
|
@@ -87,9 +172,10 @@ function resolveCommandTimeoutMs(env = process.env) {
|
|
|
87
172
|
}
|
|
88
173
|
|
|
89
174
|
/**
|
|
90
|
-
* Resolves the wall-clock budget for the full-suite
|
|
175
|
+
* Resolves the wall-clock budget for the full-suite lane. An explicit
|
|
91
176
|
* FORGE_TEST_TIMEOUT_MS override wins; otherwise it uses the larger,
|
|
92
|
-
*
|
|
177
|
+
* measurement-derived budget (~2x OBSERVED_FULL_SUITE_RUNTIME_MS) so a healthy
|
|
178
|
+
* full run is never failed fast.
|
|
93
179
|
*
|
|
94
180
|
* @param {NodeJS.ProcessEnv} [env=process.env] Environment to read the override from.
|
|
95
181
|
* @returns {number} Timeout in milliseconds (defaults to DEFAULT_FULL_SUITE_TIMEOUT_MS).
|
|
@@ -134,9 +220,11 @@ function detectPackageManager() {
|
|
|
134
220
|
function stripGitHookEnv(sourceEnv = process.env) {
|
|
135
221
|
const env = { ...sourceEnv };
|
|
136
222
|
for (const key of Object.keys(env)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
||
|
|
223
|
+
const normalizedKey = key.toUpperCase();
|
|
224
|
+
if (normalizedKey === 'GIT_DIR' || normalizedKey === 'GIT_WORK_TREE'
|
|
225
|
+
|| normalizedKey === 'GIT_INDEX_FILE' || normalizedKey === 'GIT_OBJECT_DIRECTORY'
|
|
226
|
+
|| normalizedKey === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
|
|
227
|
+
|| normalizedKey === 'GIT_QUARANTINE_PATH') {
|
|
140
228
|
delete env[key];
|
|
141
229
|
}
|
|
142
230
|
}
|
|
@@ -154,6 +242,10 @@ function isKnownTargetablePath(file) {
|
|
|
154
242
|
return true;
|
|
155
243
|
}
|
|
156
244
|
|
|
245
|
+
if (KNOWN_TARGETABLE_FILES.has(file)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
157
249
|
if (file === 'README.md'
|
|
158
250
|
|| file === 'bin/forge.js'
|
|
159
251
|
|| file === 'bin/forge-cmd.js'
|
|
@@ -173,7 +265,11 @@ function isKnownTargetablePath(file) {
|
|
|
173
265
|
// SKILL_TEST_TARGETS in lib/commands/test.js); a skills-only PR stays on the
|
|
174
266
|
// targeted lane instead of the full suite.
|
|
175
267
|
|| file.startsWith('skills/')
|
|
176
|
-
|| file.startsWith('.agents/skills/')
|
|
268
|
+
|| file.startsWith('.agents/skills/')
|
|
269
|
+
// Maintainer-only contributor skills (tracked, never published). They map to
|
|
270
|
+
// the AGENTS.md docs-bleed gate in lib/commands/test.js, so a contributor-docs
|
|
271
|
+
// PR stays on the targeted lane instead of the full suite.
|
|
272
|
+
|| file.startsWith('.forge/contributor-skills/')) {
|
|
177
273
|
return true;
|
|
178
274
|
}
|
|
179
275
|
|
|
@@ -219,17 +315,28 @@ function buildTestExecutionPlan(projectRoot, execFileSync = defaultExecFileSync,
|
|
|
219
315
|
sinceUpstream: options.sinceUpstream !== false,
|
|
220
316
|
};
|
|
221
317
|
const changedFiles = getChangedFiles(execFileSync, diffOptions);
|
|
222
|
-
const affectedTestTargets = getAffectedTestFiles(projectRoot, execFileSync, fs,
|
|
318
|
+
const affectedTestTargets = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
319
|
+
...diffOptions,
|
|
320
|
+
changedFiles,
|
|
321
|
+
});
|
|
223
322
|
|
|
224
323
|
let runFullSuite = false;
|
|
225
324
|
let runTestEnv = false;
|
|
226
325
|
let runE2E = false;
|
|
227
326
|
let runWorkflowTests = includesWorkflowTarget(affectedTestTargets);
|
|
228
327
|
let hasUnmappedFiles = false;
|
|
328
|
+
let hasFullSuiteRequiredFiles = false;
|
|
329
|
+
let hasPackageLevelChanges = false;
|
|
229
330
|
const hasUnknownChangedFiles = changedFiles.length === 0 && affectedTestTargets.length === 0;
|
|
230
331
|
|
|
231
332
|
for (const file of changedFiles) {
|
|
333
|
+
if (isFullSuiteRequiredFile(file)) {
|
|
334
|
+
hasFullSuiteRequiredFiles = true;
|
|
335
|
+
runFullSuite = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
232
338
|
if (PACKAGE_LEVEL_PATHS.has(file) || file.startsWith('packages/')) {
|
|
339
|
+
hasPackageLevelChanges = true;
|
|
233
340
|
runFullSuite = true;
|
|
234
341
|
runTestEnv = true;
|
|
235
342
|
runE2E = true;
|
|
@@ -280,8 +387,10 @@ function buildTestExecutionPlan(projectRoot, execFileSync = defaultExecFileSync,
|
|
|
280
387
|
? 'changed files could not be resolved safely'
|
|
281
388
|
: hasZeroResolvedTests
|
|
282
389
|
? 'known changes did not resolve runnable tests'
|
|
283
|
-
:
|
|
390
|
+
: hasPackageLevelChanges
|
|
284
391
|
? 'package-level changes detected'
|
|
392
|
+
: hasFullSuiteRequiredFiles
|
|
393
|
+
? 'shared authority changes require full unit coverage'
|
|
285
394
|
: 'known changes mapped to targeted tests';
|
|
286
395
|
|
|
287
396
|
return {
|
|
@@ -317,31 +426,78 @@ function classifyPushTests(projectRoot, execFileSync = defaultExecFileSync) {
|
|
|
317
426
|
*
|
|
318
427
|
* @param {string} command Executable name.
|
|
319
428
|
* @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.
|
|
429
|
+
* @param {import('node:child_process').SpawnOptions} [options={}] Spawn options.
|
|
430
|
+
* @param {typeof defaultSpawn} [spawn=defaultSpawn] Process runner.
|
|
431
|
+
* @returns {Promise<number>} Process exit status, or 1 when no status is reported.
|
|
323
432
|
*/
|
|
324
|
-
function runCommand(command, args, options = {},
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return
|
|
433
|
+
function runCommand(command, args, options = {}, spawn = defaultSpawn) {
|
|
434
|
+
const { processTree, timeout, killSignal: _killSignal, ...spawnOptions } = options;
|
|
435
|
+
return new Promise((resolve, reject) => {
|
|
436
|
+
let child;
|
|
437
|
+
let settled = false;
|
|
438
|
+
let timer = null;
|
|
439
|
+
const reservation = processTree?.reserveChild?.({ kind: 'test-lane', label: command }) || null;
|
|
440
|
+
let registered = false;
|
|
441
|
+
const release = () => {
|
|
442
|
+
if (registered) {
|
|
443
|
+
processTree?.unregisterChild?.(reservation);
|
|
444
|
+
registered = false;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const finish = (status) => {
|
|
448
|
+
if (settled) return;
|
|
449
|
+
settled = true;
|
|
450
|
+
release();
|
|
451
|
+
resolve(status ?? 1);
|
|
452
|
+
};
|
|
453
|
+
const fail = (error) => {
|
|
454
|
+
if (settled) return;
|
|
455
|
+
if (timer) clearTimeout(timer);
|
|
456
|
+
if (error?.code === 'ETIMEDOUT') {
|
|
457
|
+
processTree?.cleanup?.('SIGKILL');
|
|
458
|
+
console.error('');
|
|
459
|
+
console.error('Test lane exceeded its wall-clock ceiling and was terminated.');
|
|
460
|
+
console.error('A single test likely hung (e.g. a spawned git/bash call that never returns).');
|
|
461
|
+
console.error('Adjust the ceiling with FORGE_TEST_TIMEOUT_MS. Failing the run instead of');
|
|
462
|
+
console.error('blocking the push. See issue 8aef79e8.');
|
|
463
|
+
console.error('');
|
|
464
|
+
finish(TIMEOUT_EXIT_CODE);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
settled = true;
|
|
468
|
+
release();
|
|
469
|
+
reject(error);
|
|
470
|
+
};
|
|
471
|
+
try {
|
|
472
|
+
child = spawn(command, args, { stdio: 'inherit', shell: isWindows, ...spawnOptions });
|
|
473
|
+
} catch (error) {
|
|
474
|
+
fail(error);
|
|
475
|
+
return;
|
|
340
476
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
477
|
+
child.once?.('error', fail);
|
|
478
|
+
child.once?.('close', (status, signal) => {
|
|
479
|
+
if (timer) clearTimeout(timer);
|
|
480
|
+
finish(status ?? signalExitCode(signal));
|
|
481
|
+
});
|
|
482
|
+
if (reservation && typeof processTree?.registerChild === 'function') {
|
|
483
|
+
registered = true;
|
|
484
|
+
if (!processTree.registerChild(reservation, child)) {
|
|
485
|
+
processTree.abortChild?.(reservation, child);
|
|
486
|
+
release();
|
|
487
|
+
fail(new Error('test lane process could not be registered'));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
timer = timeout > 0 ? setTimeout(() => {
|
|
492
|
+
processTree?.cleanup?.('SIGKILL');
|
|
493
|
+
try { child.kill?.('SIGKILL'); } catch { /* best effort */ }
|
|
494
|
+
finish(TIMEOUT_EXIT_CODE);
|
|
495
|
+
}, timeout) : null;
|
|
496
|
+
if (!child.once) {
|
|
497
|
+
if (timer) clearTimeout(timer);
|
|
498
|
+
finish(child.status ?? 1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
345
501
|
}
|
|
346
502
|
|
|
347
503
|
/**
|
|
@@ -351,51 +507,92 @@ function runCommand(command, args, options = {}, spawnSync = defaultSpawnSync) {
|
|
|
351
507
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
352
508
|
* @returns {number} Exit status for the executed plan.
|
|
353
509
|
*/
|
|
354
|
-
function runTestExecutionPlan(plan, deps = {}) {
|
|
355
|
-
const
|
|
510
|
+
async function runTestExecutionPlan(plan, deps = {}) {
|
|
511
|
+
const usesSpawnSyncAdapter = !deps.spawn && typeof deps.spawnSync === 'function';
|
|
512
|
+
const spawn = deps.spawn || (usesSpawnSyncAdapter ? (...args) => {
|
|
513
|
+
const child = new EventEmitter();
|
|
514
|
+
let result;
|
|
515
|
+
try {
|
|
516
|
+
result = deps.spawnSync(...args);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
process.nextTick(() => child.emit('error', error));
|
|
519
|
+
return child;
|
|
520
|
+
}
|
|
521
|
+
child.pid = result?.pid || process.pid;
|
|
522
|
+
process.nextTick(() => {
|
|
523
|
+
if (result?.error) child.emit('error', result.error);
|
|
524
|
+
else child.emit('close', result?.status ?? 1, result?.signal);
|
|
525
|
+
});
|
|
526
|
+
return child;
|
|
527
|
+
} : defaultSpawn);
|
|
356
528
|
const pkgManager = deps.pkgManager || detectPackageManager();
|
|
357
529
|
const env = deps.env || stripGitHookEnv(process.env);
|
|
530
|
+
// The injected synchronous runner has already exited when its adapter returns,
|
|
531
|
+
// so there is no live child to register or reap through host process probes.
|
|
532
|
+
const processTree = deps.processTree || (usesSpawnSyncAdapter ? {} : createProcessTree({
|
|
533
|
+
env,
|
|
534
|
+
platform: deps.platform,
|
|
535
|
+
}));
|
|
536
|
+
let signal = null;
|
|
537
|
+
const removeSignalHandlers = typeof processTree.installSignalHandlers === 'function'
|
|
538
|
+
? processTree.installSignalHandlers((received) => {
|
|
539
|
+
signal = received;
|
|
540
|
+
})
|
|
541
|
+
: () => {};
|
|
542
|
+
const childEnv = typeof processTree.envFor === 'function' ? processTree.envFor(env) : env;
|
|
358
543
|
const bunCommand = deps.bunCommand || env.BUN_EXE || process.env.BUN_EXE || 'bun';
|
|
359
544
|
const label = deps.label || 'tests';
|
|
360
545
|
const timeout = resolveCommandTimeoutMs(env);
|
|
361
|
-
const laneOptions = { env, killSignal: 'SIGKILL', timeout };
|
|
362
|
-
// The full-suite fallback gets
|
|
546
|
+
const laneOptions = { env: childEnv, killSignal: 'SIGKILL', timeout, processTree };
|
|
547
|
+
// The full-suite fallback gets the larger, measurement-derived budget so a
|
|
363
548
|
// healthy-but-slow full run is not failed fast by the targeted-lane ceiling.
|
|
364
|
-
const fullSuiteOptions = {
|
|
549
|
+
const fullSuiteOptions = {
|
|
550
|
+
env: childEnv,
|
|
551
|
+
killSignal: 'SIGKILL',
|
|
552
|
+
timeout: resolveFullSuiteTimeoutMs(env),
|
|
553
|
+
processTree,
|
|
554
|
+
};
|
|
365
555
|
|
|
366
556
|
console.log(`Running ${label} (${pkgManager})...`);
|
|
367
557
|
|
|
368
558
|
try {
|
|
369
559
|
if (plan.runFullSuite) {
|
|
370
560
|
console.log(` Mode: full suite (${plan.reason})`);
|
|
371
|
-
|
|
561
|
+
const status = await runCommand('node', ['scripts/test-full-suite.js'], fullSuiteOptions, spawn);
|
|
562
|
+
if (signal) return signalExitCode(signal);
|
|
372
563
|
if (status !== 0) return status;
|
|
373
564
|
} else if (plan.testTargets.length > 0) {
|
|
374
565
|
console.log(` Mode: targeted (${plan.testTargets.length} test file${plan.testTargets.length === 1 ? '' : 's'})`);
|
|
375
566
|
const command = pkgManager === 'bun' ? bunCommand : pkgManager;
|
|
376
|
-
|
|
567
|
+
const status = await runCommand(command, ['run', 'test', ...plan.testTargets], laneOptions, spawn);
|
|
568
|
+
if (signal) return signalExitCode(signal);
|
|
377
569
|
if (status !== 0) return status;
|
|
378
570
|
}
|
|
379
571
|
|
|
380
572
|
if (plan.runE2E) {
|
|
381
573
|
console.log(' Extra: running affected e2e tests');
|
|
382
|
-
|
|
574
|
+
const status = await runCommand(bunCommand, ['test', '--timeout', '15000', 'test/e2e/'], laneOptions, spawn);
|
|
575
|
+
if (signal) return signalExitCode(signal);
|
|
383
576
|
if (status !== 0) return status;
|
|
384
577
|
}
|
|
385
578
|
|
|
386
579
|
if (!plan.runFullSuite && plan.runTestEnv) {
|
|
387
580
|
console.log(' Extra: running affected edge-case tests');
|
|
388
|
-
|
|
581
|
+
const status = await runCommand(bunCommand, ['test', '--timeout', '15000', 'test-env/'], laneOptions, spawn);
|
|
582
|
+
if (signal) return signalExitCode(signal);
|
|
389
583
|
if (status !== 0) return status;
|
|
390
584
|
}
|
|
391
585
|
|
|
392
586
|
console.log('Relevant tests passed');
|
|
393
|
-
return 0;
|
|
587
|
+
return signal ? signalExitCode(signal) : 0;
|
|
394
588
|
} catch (error) {
|
|
395
589
|
console.error('');
|
|
396
590
|
console.error(`Failed to run ${label}: ${error.message}`);
|
|
397
591
|
console.error('');
|
|
398
|
-
return 1;
|
|
592
|
+
return signal ? signalExitCode(signal) : 1;
|
|
593
|
+
} finally {
|
|
594
|
+
removeSignalHandlers();
|
|
595
|
+
processTree.cleanup?.(signal ? 'SIGKILL' : 'SIGTERM');
|
|
399
596
|
}
|
|
400
597
|
}
|
|
401
598
|
|
|
@@ -406,7 +603,15 @@ function runTestExecutionPlan(plan, deps = {}) {
|
|
|
406
603
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
407
604
|
* @returns {number} Exit status for pre-push tests.
|
|
408
605
|
*/
|
|
409
|
-
function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
606
|
+
async function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
607
|
+
if (isQuickPushLane(deps.env || process.env)) {
|
|
608
|
+
console.log('');
|
|
609
|
+
console.log(' quick lane: tests skipped locally — CI runs the full matrix');
|
|
610
|
+
console.log(' Run `forge push` (no --quick) to test before merge.');
|
|
611
|
+
console.log('');
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
410
615
|
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
411
616
|
const plan = classifyPushTests(projectRoot, execFileSync);
|
|
412
617
|
return runTestExecutionPlan(plan, { ...deps, label: 'pre-push tests' });
|
|
@@ -419,26 +624,35 @@ function runPrePushTests(projectRoot = process.cwd(), deps = {}) {
|
|
|
419
624
|
* @param {Object} [deps={}] Runtime dependencies for tests.
|
|
420
625
|
* @returns {number} Exit status for local validation tests.
|
|
421
626
|
*/
|
|
422
|
-
function runLocalValidationTests(projectRoot = process.cwd(), deps = {}) {
|
|
627
|
+
async function runLocalValidationTests(projectRoot = process.cwd(), deps = {}) {
|
|
423
628
|
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
424
629
|
const plan = buildTestExecutionPlan(projectRoot, execFileSync, { sinceUpstream: true });
|
|
425
630
|
return runTestExecutionPlan(plan, { ...deps, label: 'local validation tests' });
|
|
426
631
|
}
|
|
427
632
|
|
|
428
633
|
if (require.main === module) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
634
|
+
(async () => {
|
|
635
|
+
const exitCode = process.argv.includes('--validate')
|
|
636
|
+
? await runLocalValidationTests()
|
|
637
|
+
: await runPrePushTests();
|
|
638
|
+
process.exit(exitCode);
|
|
639
|
+
})().catch((error) => {
|
|
640
|
+
console.error(error);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
});
|
|
433
643
|
}
|
|
434
644
|
|
|
435
645
|
module.exports = {
|
|
436
646
|
ALWAYS_RUN_RISK_TEST_TARGETS,
|
|
437
647
|
DEFAULT_FULL_SUITE_TIMEOUT_MS,
|
|
438
648
|
DEFAULT_TEST_COMMAND_TIMEOUT_MS,
|
|
649
|
+
OBSERVED_FULL_SUITE_RUNTIME_MS,
|
|
650
|
+
QUICK_LANE_ENV_VAR,
|
|
651
|
+
QUICK_LANE_VALUE,
|
|
439
652
|
buildTestExecutionPlan,
|
|
440
653
|
classifyPushTests,
|
|
441
654
|
detectPackageManager,
|
|
655
|
+
isQuickPushLane,
|
|
442
656
|
resolveCommandTimeoutMs,
|
|
443
657
|
resolveFullSuiteTimeoutMs,
|
|
444
658
|
runLocalValidationTests,
|
package/skills/coverage.json
CHANGED
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
|
|
@@ -35,6 +28,8 @@ This skill handles ALL issues that arise after creating a pull request.
|
|
|
35
28
|
|
|
36
29
|
## What This Skill Does
|
|
37
30
|
|
|
31
|
+
Before reviewing a diff, read `CODING_STANDARDS.md` from the repository root and apply every relevant rule.
|
|
32
|
+
|
|
38
33
|
### Step 1: Fetch Complete PR Status
|
|
39
34
|
```bash
|
|
40
35
|
# Get full PR details including all checks
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"fixtures": "present",
|
|
4
4
|
"static": {
|
|
5
5
|
"token_cost": {
|
|
6
|
-
"desc_chars":
|
|
7
|
-
"body_lines":
|
|
8
|
-
"score":
|
|
6
|
+
"desc_chars": 261,
|
|
7
|
+
"body_lines": 478,
|
|
8
|
+
"score": 39
|
|
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/setup/SKILL.md
CHANGED
|
@@ -30,6 +30,24 @@ forge setup --path <dir> --dry-run # preview against another d
|
|
|
30
30
|
|
|
31
31
|
`forge setup` is **interactive by default**. In any automated/agent context pass `--yes` (or `--non-interactive`) so it never blocks on a prompt.
|
|
32
32
|
|
|
33
|
+
# Optional GitHub accounts per clone
|
|
34
|
+
|
|
35
|
+
Only when the user opts into multiple accounts: sign in once per account with
|
|
36
|
+
native `gh auth login`, then select inside each intended clone:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
forge github use <login> # verify stored account/access before binding this clone
|
|
40
|
+
forge github status # safe identity and transport diagnostics; --json supported
|
|
41
|
+
forge github run -- <harness> # explicitly give a trusted child the selected GitHub identity
|
|
42
|
+
forge github unset # remove this binding, not stored logins
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Ordinary setup stays unbound. Never paste or persist tokens, automatically log in
|
|
46
|
+
or switch accounts, or change Git author/remotes/SSH/helper configuration.
|
|
47
|
+
Git transport is separate; HTTPS using `gh auth git-credential` can inherit the
|
|
48
|
+
explicit child session's account. See the repository reference guide
|
|
49
|
+
`docs/reference/github-accounts.md` for concurrent sessions and recovery.
|
|
50
|
+
|
|
33
51
|
# Initialize a fresh repo
|
|
34
52
|
|
|
35
53
|
```bash
|