forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4
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/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
package/lib/commands/plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan Command -
|
|
2
|
+
* Plan Command - Kernel Issue Integration
|
|
3
3
|
* Creates implementation plan after research is complete
|
|
4
4
|
*
|
|
5
5
|
* Security: Uses execFileSync instead of exec/execSync to prevent command injection
|
|
@@ -26,15 +26,14 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Human-readable label for the issue backend that created an issue.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* The kernel is the only backend; anything unresolved prints a neutral label
|
|
30
|
+
* rather than naming a store the issue did not come from.
|
|
31
31
|
*
|
|
32
|
-
* @param {string} [backend] - Resolved issue backend ('kernel'
|
|
33
|
-
* @returns {string} Display label ('Kernel'
|
|
32
|
+
* @param {string} [backend] - Resolved issue backend ('kernel').
|
|
33
|
+
* @returns {string} Display label ('Kernel' or a neutral 'Issue').
|
|
34
34
|
* @private
|
|
35
35
|
*/
|
|
36
36
|
function issueBackendLabel(backend) {
|
|
37
|
-
if (backend === 'beads') return 'Beads';
|
|
38
37
|
if (backend === 'kernel') return 'Kernel';
|
|
39
38
|
return 'Issue';
|
|
40
39
|
}
|
|
@@ -76,7 +75,7 @@ function validateFeatureSlug(slug) {
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
|
-
* Build the issue description
|
|
78
|
+
* Build the issue description used when creating the kernel tracking issue.
|
|
80
79
|
* Strategic scope appends a design-doc pointer derived from a sanitized slug.
|
|
81
80
|
*
|
|
82
81
|
* @param {string} featureName
|
|
@@ -247,98 +246,12 @@ function detectScope(researchContent) {
|
|
|
247
246
|
};
|
|
248
247
|
}
|
|
249
248
|
|
|
250
|
-
/**
|
|
251
|
-
* Create Beads issue for the feature
|
|
252
|
-
* Executes `bd create` command with appropriate description based on scope
|
|
253
|
-
*
|
|
254
|
-
* Security: Uses execFileSync (not exec) to prevent command injection
|
|
255
|
-
*
|
|
256
|
-
* @param {string} featureName - Feature name (human-readable)
|
|
257
|
-
* @param {string} researchPath - Research document path (e.g., "docs/research/feature.md")
|
|
258
|
-
* @param {'tactical'|'strategic'} scope - Scope type
|
|
259
|
-
* @returns {{success: boolean, issueId?: string, description?: string, error?: string}} Beads creation result
|
|
260
|
-
* @example
|
|
261
|
-
* const result = createBeadsIssue('Payment Integration', 'docs/research/payment.md', 'strategic');
|
|
262
|
-
* if (result.success) {
|
|
263
|
-
* console.log('Created issue:', result.issueId);
|
|
264
|
-
* }
|
|
265
|
-
*/
|
|
266
|
-
function createBeadsIssue(featureName, researchPath, scope) {
|
|
267
|
-
if (!featureName || !researchPath) {
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
error: 'Feature name and research path are required',
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (scope !== 'tactical' && scope !== 'strategic') {
|
|
275
|
-
return {
|
|
276
|
-
success: false,
|
|
277
|
-
error: `Invalid scope '${scope}'. Must be 'tactical' or 'strategic'`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
const built = buildFeatureIssueDescription(featureName, researchPath, scope);
|
|
283
|
-
if (built.error) {
|
|
284
|
-
return { success: false, error: built.error };
|
|
285
|
-
}
|
|
286
|
-
const description = built.description;
|
|
287
|
-
|
|
288
|
-
// Execute bd create command using execFileSync for safety (OWASP A03)
|
|
289
|
-
const result = execFileSync( // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
290
|
-
'bd', // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
291
|
-
['create', `--title=${featureName}`, `--description=${description}`, '--type=feature', '--priority=2'],
|
|
292
|
-
getExecOptions()
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// Extract issue ID from output (format: "Created issue: forge-xxx" or "forge-xxx.N" for dotted sub-IDs).
|
|
296
|
-
// Character class is [a-z0-9] (not [a-zA-Z0-9]) because the /i flag makes A-Z redundant.
|
|
297
|
-
const createPattern = /Created issue:\s*(forge-[a-z0-9]+(?:\.[a-z0-9]+)*)/i;
|
|
298
|
-
const fallbackPattern = /(forge-[a-z0-9]+(?:\.[a-z0-9]+)*)/i;
|
|
299
|
-
const match = createPattern.exec(result) || fallbackPattern.exec(result);
|
|
300
|
-
|
|
301
|
-
if (!match) {
|
|
302
|
-
return {
|
|
303
|
-
success: false,
|
|
304
|
-
error: 'Failed to extract issue ID from bd create output\n\nEnsure beads is installed: bunx beads init',
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
success: true,
|
|
310
|
-
issueId: match[1],
|
|
311
|
-
description,
|
|
312
|
-
};
|
|
313
|
-
} catch (error) {
|
|
314
|
-
// Check for timeout
|
|
315
|
-
if (error.killed && error.signal === 'SIGTERM') {
|
|
316
|
-
return {
|
|
317
|
-
success: false,
|
|
318
|
-
error: 'Beads command timed out after 2 minutes.',
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Provide actionable error message
|
|
323
|
-
const bdNotFound = error.message.includes('ENOENT') || error.message.includes('not found');
|
|
324
|
-
const errorMsg = bdNotFound
|
|
325
|
-
? 'beads (bd) command not found. Install with: bunx beads init'
|
|
326
|
-
: `Failed to create Beads issue: ${error.message}`;
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
success: false,
|
|
330
|
-
error: errorMsg,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
249
|
/**
|
|
336
250
|
* Create an issue via the Forge Kernel backend (bd-free).
|
|
337
251
|
*
|
|
338
252
|
* Mirrors `forge issue create` on the kernel: routes through runIssueOperation with
|
|
339
253
|
* the kernel broker instead of shelling out to `bd create`. Used by `forge plan` when
|
|
340
|
-
* the
|
|
341
|
-
* install. The beads path (createBeadsIssue) is preserved for backend=beads.
|
|
254
|
+
* the kernel is the only issue backend, so planning needs no bd binary installed.
|
|
342
255
|
*
|
|
343
256
|
* @param {string} featureName
|
|
344
257
|
* @param {string} researchPath
|
|
@@ -413,7 +326,7 @@ async function linkExistingIssue(issueId, options = {}) {
|
|
|
413
326
|
projectRoot,
|
|
414
327
|
{
|
|
415
328
|
issueBackend,
|
|
416
|
-
useKernelBroker:
|
|
329
|
+
useKernelBroker: true,
|
|
417
330
|
kernelBroker: options.kernelBroker,
|
|
418
331
|
},
|
|
419
332
|
);
|
|
@@ -517,7 +430,12 @@ function currentBranchIssueFromDriver(driver, currentBranch) {
|
|
|
517
430
|
if (driver && typeof driver.listWorktrees === 'function') {
|
|
518
431
|
try {
|
|
519
432
|
const rows = driver.listWorktrees() || [];
|
|
520
|
-
|
|
433
|
+
// Only an ACTIVE (live) linkage row binds the branch: a superseded/stale
|
|
434
|
+
// registration for a reused branch name must not trigger a false split-state
|
|
435
|
+
// conflict against the OLD issue (R4/be18881c). Tolerate a null state for
|
|
436
|
+
// rows written before the state column was populated.
|
|
437
|
+
const match = rows.find(row => row && row.branch === currentBranch && row.issue_id
|
|
438
|
+
&& (row.state === 'active' || row.state == null));
|
|
521
439
|
if (match) return match.issue_id;
|
|
522
440
|
} catch {
|
|
523
441
|
// fall through to branch-name parsing
|
|
@@ -553,6 +471,9 @@ async function detectBranchIssueConflict(options, explicitIssueId) {
|
|
|
553
471
|
async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
554
472
|
if (!branch || !issueId) return;
|
|
555
473
|
const cwd = options.projectRoot || process.cwd();
|
|
474
|
+
// F6 defaultStageWarn pattern: write to stderr so a dropped linkage never
|
|
475
|
+
// pollutes machine-readable stdout, yet leaves a trace even under FORGE_JSON=1.
|
|
476
|
+
const warn = options.warn || (message => process.stderr.write(`${message}\n`));
|
|
556
477
|
await withPlanDriver(options, driver => {
|
|
557
478
|
if (!driver || typeof driver.registerWorktree !== 'function') return;
|
|
558
479
|
try {
|
|
@@ -566,16 +487,19 @@ async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
|
566
487
|
registered_at: new Date().toISOString(),
|
|
567
488
|
state: 'active',
|
|
568
489
|
});
|
|
569
|
-
} catch {
|
|
570
|
-
//
|
|
490
|
+
} catch (error) {
|
|
491
|
+
// Best-effort: linkage failure must not fail plan, but it must NOT be
|
|
492
|
+
// silent (R3) — otherwise ship later fail-closes with no signal at plan
|
|
493
|
+
// time about the dropped branch->issue linkage.
|
|
494
|
+
warn(`[forge] could not register branch->issue linkage for ${branch} -> ${issueId}: ${error.message}`);
|
|
571
495
|
}
|
|
572
496
|
});
|
|
573
497
|
}
|
|
574
498
|
|
|
575
499
|
/**
|
|
576
|
-
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE
|
|
577
|
-
* the
|
|
578
|
-
* nested ternary in executePlan.
|
|
500
|
+
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE in
|
|
501
|
+
* the kernel (the only issue backend — planning needs no bd binary). Extracted to
|
|
502
|
+
* avoid a nested ternary in executePlan.
|
|
579
503
|
*
|
|
580
504
|
* @returns {Promise<{success: boolean, issueId?: string, error?: string}>}
|
|
581
505
|
* @private
|
|
@@ -584,10 +508,7 @@ async function resolveTrackingIssue({ explicitIssueId, issueBackend, featureName
|
|
|
584
508
|
if (explicitIssueId) {
|
|
585
509
|
return linkExistingIssue(explicitIssueId, { ...options, issueBackend });
|
|
586
510
|
}
|
|
587
|
-
|
|
588
|
-
return createKernelIssue(featureName, researchPath, scope, options);
|
|
589
|
-
}
|
|
590
|
-
return createBeadsIssue(featureName, researchPath, scope);
|
|
511
|
+
return createKernelIssue(featureName, researchPath, scope, options);
|
|
591
512
|
}
|
|
592
513
|
|
|
593
514
|
async function createKernelIssue(featureName, researchPath, scope, options = {}) {
|
|
@@ -648,7 +569,10 @@ async function createKernelIssue(featureName, researchPath, scope, options = {})
|
|
|
648
569
|
|
|
649
570
|
/**
|
|
650
571
|
* Create feature branch
|
|
651
|
-
* Creates
|
|
572
|
+
* Creates a new git branch following feat/<slug> convention WITHOUT switching
|
|
573
|
+
* the shared checkout's HEAD (uses `git branch`, not `git checkout -b`).
|
|
574
|
+
* Switching HEAD in the shared working tree corrupts concurrent agents
|
|
575
|
+
* (kernel issue aa14966c); isolated work happens in a dedicated worktree.
|
|
652
576
|
*
|
|
653
577
|
* Security: Uses execFileSync with array args to prevent command injection
|
|
654
578
|
*
|
|
@@ -678,14 +602,19 @@ function createFeatureBranch(featureSlug) {
|
|
|
678
602
|
execFileSync('git', ['rev-parse', '--verify', branchName], { ...getExecOptions(), stdio: 'pipe' }); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
679
603
|
return {
|
|
680
604
|
success: false,
|
|
681
|
-
error: `Branch ${branchName} already exists\n\
|
|
605
|
+
error: `Branch ${branchName} already exists\n\nWork on it in an isolated checkout: forge worktree create ${featureSlug}\n(or, working solo: git switch ${branchName})`,
|
|
682
606
|
};
|
|
683
607
|
} catch {
|
|
684
608
|
// Branch doesn't exist, continue (expected case)
|
|
685
609
|
}
|
|
686
610
|
|
|
687
|
-
// Create
|
|
688
|
-
|
|
611
|
+
// Create the branch WITHOUT switching the shared checkout's HEAD.
|
|
612
|
+
// Historically this used `git checkout -b`, which flipped the shared
|
|
613
|
+
// working tree onto the new branch and corrupted concurrent agents
|
|
614
|
+
// (kernel issue aa14966c). `git branch` creates the ref at the current
|
|
615
|
+
// HEAD without touching the working tree; isolated work happens in a
|
|
616
|
+
// dedicated worktree (`forge worktree create`), never the shared tree.
|
|
617
|
+
execFileSync('git', ['branch', branchName], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
689
618
|
|
|
690
619
|
return {
|
|
691
620
|
success: true,
|
|
@@ -933,14 +862,14 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
933
862
|
* Tactical workflow (quick fixes, <1 day):
|
|
934
863
|
* 1. Read research document
|
|
935
864
|
* 2. Detect scope (tactical)
|
|
936
|
-
* 3. Create
|
|
865
|
+
* 3. Create kernel issue
|
|
937
866
|
* 4. Create feature branch
|
|
938
867
|
* → Next: /dev command
|
|
939
868
|
*
|
|
940
869
|
* Strategic workflow (architecture changes, >1 day):
|
|
941
870
|
* 1. Read research document
|
|
942
871
|
* 2. Detect scope (strategic)
|
|
943
|
-
* 3. Create
|
|
872
|
+
* 3. Create kernel issue with design doc link
|
|
944
873
|
* 4. Create feature branch
|
|
945
874
|
* → Next: Create design doc, then /dev command
|
|
946
875
|
*
|
|
@@ -948,7 +877,7 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
948
877
|
* @returns {Promise<{
|
|
949
878
|
* success: boolean,
|
|
950
879
|
* scope?: 'tactical'|'strategic',
|
|
951
|
-
* issueBackend?: 'kernel'
|
|
880
|
+
* issueBackend?: 'kernel',
|
|
952
881
|
* issueId?: string,
|
|
953
882
|
* beadsIssueId?: string,
|
|
954
883
|
* branchName?: string,
|
|
@@ -956,10 +885,10 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
956
885
|
* summary?: string,
|
|
957
886
|
* nextCommand?: string,
|
|
958
887
|
* error?: string
|
|
959
|
-
* }>} Execution result. `issueId` is the created issue id
|
|
960
|
-
*
|
|
961
|
-
* alias of `issueId` kept for
|
|
962
|
-
*
|
|
888
|
+
* }>} Execution result. `issueId` is the created kernel issue id, and
|
|
889
|
+
* `issueBackend` names the backend ('kernel'). `beadsIssueId` is a deprecated
|
|
890
|
+
* alias of `issueId` kept for output-shape compatibility — it never implied a
|
|
891
|
+
* Beads store and now always holds the kernel issue id.
|
|
963
892
|
* @example
|
|
964
893
|
* const result = await executePlan('Payment Integration');
|
|
965
894
|
* if (result.success) {
|
|
@@ -976,7 +905,7 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
976
905
|
}
|
|
977
906
|
|
|
978
907
|
// Resolve the active issue backend (explicit opts > env > .forge/config.yaml >
|
|
979
|
-
// default 'kernel').
|
|
908
|
+
// default 'kernel'). The kernel is bd-free, so planning needs no bd binary.
|
|
980
909
|
const issueBackend = resolveIssueBackend({
|
|
981
910
|
deps: options,
|
|
982
911
|
env: options.env || process.env,
|
|
@@ -1012,8 +941,7 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
1012
941
|
|
|
1013
942
|
// Step 3: Resolve the tracking issue. `--issue <id>` LINKS an existing issue
|
|
1014
943
|
// (claim-first flow) instead of creating a duplicate (B4). Otherwise create
|
|
1015
|
-
//
|
|
1016
|
-
// (no bd); beads preserves the bd path.
|
|
944
|
+
// through the kernel broker (no bd binary involved).
|
|
1017
945
|
const explicitIssueId = options.issue || options.issueId || null;
|
|
1018
946
|
|
|
1019
947
|
// F4c: never link issue B onto a branch already bound to issue A.
|
|
@@ -1061,11 +989,17 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
1061
989
|
scope: scope.type,
|
|
1062
990
|
issueBackend,
|
|
1063
991
|
issueId: issue.issueId,
|
|
1064
|
-
// Deprecated alias of issueId, retained for
|
|
1065
|
-
//
|
|
992
|
+
// Deprecated alias of issueId, retained for output-shape compatibility.
|
|
993
|
+
// It never implied a Beads store and now always holds the kernel id.
|
|
1066
994
|
beadsIssueId: issue.issueId,
|
|
1067
995
|
branchName: branch.branchName,
|
|
1068
996
|
linked: Boolean(explicitIssueId),
|
|
997
|
+
// A FRESH branch was created (HEAD did NOT move — aa14966c). Stage
|
|
998
|
+
// commands resolve the CHECKED-OUT branch, so the user must enter an
|
|
999
|
+
// isolated checkout on this branch before /dev, or stage state resolves
|
|
1000
|
+
// against the default branch (no linkage → ship dead-ends). When the
|
|
1001
|
+
// branch was reused, HEAD is already on it and /dev works directly.
|
|
1002
|
+
branchCreated: !branch.reused,
|
|
1069
1003
|
summary: explicitIssueId
|
|
1070
1004
|
? `Plan linked to existing issue ${issue.issueId} (${scope.type} scope)`
|
|
1071
1005
|
: `Plan created for ${featureName} (${scope.type} scope)`,
|
|
@@ -1104,7 +1038,20 @@ module.exports = {
|
|
|
1104
1038
|
const lines = [`${header}: ${result.summary || result.branchName || featureName}`];
|
|
1105
1039
|
if (result.issueId) lines.push(`${issueBackendLabel(result.issueBackend)}: ${result.issueId}`);
|
|
1106
1040
|
if (result.branchName) lines.push(`Branch: ${result.branchName}`);
|
|
1107
|
-
if (result.
|
|
1041
|
+
if (result.branchCreated) {
|
|
1042
|
+
// A fresh branch was created but HEAD was NOT switched (aa14966c). Stage
|
|
1043
|
+
// commands (/dev, /validate, /ship) resolve the CHECKED-OUT branch — from
|
|
1044
|
+
// the shared tree that is still the default branch, which has no
|
|
1045
|
+
// branch->issue linkage, so ship would dead-end. Direct the user into an
|
|
1046
|
+
// isolated checkout on the new branch first.
|
|
1047
|
+
const slug = String(result.branchName).replace(/^feat\//, '');
|
|
1048
|
+
lines.push('Next: work on this branch in an isolated checkout (HEAD stays put in the shared tree):');
|
|
1049
|
+
lines.push(` forge worktree create ${slug} # concurrent-safe; checks out the existing ${result.branchName}`);
|
|
1050
|
+
lines.push(` # or, working solo: git switch ${result.branchName}`);
|
|
1051
|
+
lines.push(`Then run ${result.nextCommand || '/dev'} from that checkout.`);
|
|
1052
|
+
} else if (result.nextCommand) {
|
|
1053
|
+
lines.push(`Next: ${result.nextCommand}`);
|
|
1054
|
+
}
|
|
1108
1055
|
|
|
1109
1056
|
return {
|
|
1110
1057
|
...result,
|
|
@@ -1113,7 +1060,6 @@ module.exports = {
|
|
|
1113
1060
|
},
|
|
1114
1061
|
readResearchDoc,
|
|
1115
1062
|
detectScope,
|
|
1116
|
-
createBeadsIssue,
|
|
1117
1063
|
createKernelIssue,
|
|
1118
1064
|
createFeatureBranch,
|
|
1119
1065
|
extractDesignDecisions,
|
|
@@ -1122,4 +1068,6 @@ module.exports = {
|
|
|
1122
1068
|
applyYAGNIFilter,
|
|
1123
1069
|
executePlan,
|
|
1124
1070
|
issueBackendLabel,
|
|
1071
|
+
registerBranchIssueLinkage,
|
|
1072
|
+
currentBranchIssueFromDriver,
|
|
1125
1073
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ship = require('./ship');
|
|
4
|
+
const preflight = require('./preflight');
|
|
5
|
+
const shepherd = require('./shepherd');
|
|
6
|
+
const merge = require('./merge');
|
|
7
|
+
const { stripGlobalFlags } = require('../global-flags');
|
|
8
|
+
|
|
9
|
+
// One memorable surface over the EXISTING pull-request commands (kernel issue
|
|
10
|
+
// 6ab3f30c): every subcommand delegates to the standalone ship/preflight/shepherd/
|
|
11
|
+
// merge handlers — the same code, not a reimplementation. The standalone
|
|
12
|
+
// `forge ship`/`preflight`/`shepherd`/`merge` commands remain registered as
|
|
13
|
+
// back-compat aliases (see lib/commands/_aliases.js), so nothing that already
|
|
14
|
+
// calls them breaks. `pr ship` is the canonical PR-creation form; bare `ship`
|
|
15
|
+
// stays a visible shortcut.
|
|
16
|
+
//
|
|
17
|
+
// Delegates are referenced by MODULE (not a pre-bound `.handler`) so the routed
|
|
18
|
+
// handler is resolved at dispatch time — dispatch always reaches whatever the
|
|
19
|
+
// command module currently exports, keeping the standalone command the single
|
|
20
|
+
// source of truth for its own behaviour.
|
|
21
|
+
const SUBCOMMANDS = {
|
|
22
|
+
ship: {
|
|
23
|
+
module: ship,
|
|
24
|
+
summary: 'Create a pull request from validated feature work (= forge ship)',
|
|
25
|
+
},
|
|
26
|
+
preflight: {
|
|
27
|
+
module: preflight,
|
|
28
|
+
summary: 'Fast deterministic-gate parity with CI (= forge preflight; supports --all)',
|
|
29
|
+
},
|
|
30
|
+
shepherd: {
|
|
31
|
+
module: shepherd,
|
|
32
|
+
summary: 'Run one bounded monitor pass over a PR (= forge shepherd; --bundle/--pull/--json, events, watch)',
|
|
33
|
+
},
|
|
34
|
+
merge: {
|
|
35
|
+
module: merge,
|
|
36
|
+
summary: 'Opt-in conditional auto-merge, OFF by default (= forge merge --auto <pr>)',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const usage = 'Usage: forge pr <ship|preflight|shepherd|merge> [args]';
|
|
41
|
+
|
|
42
|
+
function renderHelp() {
|
|
43
|
+
const width = Math.max(...Object.keys(SUBCOMMANDS).map(name => name.length));
|
|
44
|
+
const lines = [
|
|
45
|
+
usage,
|
|
46
|
+
'',
|
|
47
|
+
'Subcommands:',
|
|
48
|
+
...Object.entries(SUBCOMMANDS).map(
|
|
49
|
+
([name, { summary }]) => ` ${name.padEnd(width)} ${summary}`
|
|
50
|
+
),
|
|
51
|
+
'',
|
|
52
|
+
'Back-compat: forge ship / forge preflight / forge shepherd / forge merge remain available as aliases.',
|
|
53
|
+
];
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handler(args, flags, projectRoot, opts) {
|
|
58
|
+
// The subcommand is the first positional token; global flags (e.g. `-p <dir>`) are stripped
|
|
59
|
+
// first so they never masquerade as the subcommand.
|
|
60
|
+
const positional = stripGlobalFlags(args).find(arg => !arg.startsWith('-'));
|
|
61
|
+
|
|
62
|
+
if (!positional || positional === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
63
|
+
return { success: true, output: renderHelp() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sub = SUBCOMMANDS[positional];
|
|
67
|
+
if (!sub) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: `Unknown pr subcommand: ${positional}\n\n${renderHelp()}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Forward everything EXCEPT the consumed subcommand token to the delegate, preserving
|
|
75
|
+
// every remaining token (including flags like `--pull`/`--json`/`--bundle`, and the
|
|
76
|
+
// `events`/`watch` shepherd sub-shapes) so passthrough stays byte-identical.
|
|
77
|
+
const idx = args.indexOf(positional);
|
|
78
|
+
const childArgs = idx >= 0 ? [...args.slice(0, idx), ...args.slice(idx + 1)] : args;
|
|
79
|
+
return sub.module.handler(childArgs, flags, projectRoot, opts);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
name: 'pr',
|
|
84
|
+
description:
|
|
85
|
+
'Unified pull-request surface: forge pr ship|preflight|shepherd|merge (wraps ship/preflight/shepherd/merge)',
|
|
86
|
+
usage,
|
|
87
|
+
handler,
|
|
88
|
+
};
|
package/lib/commands/prime.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
buildPrime,
|
|
5
|
+
collectPrimeLiveState,
|
|
5
6
|
runOrientationCommand,
|
|
6
7
|
} = require('../orientation');
|
|
7
8
|
|
|
@@ -9,5 +10,11 @@ module.exports = {
|
|
|
9
10
|
name: 'prime',
|
|
10
11
|
description: 'Emit session-entry bounded orientation for agents',
|
|
11
12
|
usage: 'Usage: forge prime [--budget N] [--json]',
|
|
12
|
-
|
|
13
|
+
// Async: prime leads with LIVE state (stage / claims / ready / gates / one adoption nudge),
|
|
14
|
+
// which needs a best-effort (non-throwing) kernel read before the synchronous build assembles
|
|
15
|
+
// it into the bounded orientation. All existing prime output/flags are unchanged.
|
|
16
|
+
handler: async (args, _flags, projectRoot) => {
|
|
17
|
+
const liveState = await collectPrimeLiveState(projectRoot);
|
|
18
|
+
return runOrientationCommand(buildPrime, args, projectRoot, { liveState });
|
|
19
|
+
},
|
|
13
20
|
};
|
package/lib/commands/push.js
CHANGED
|
@@ -4,6 +4,8 @@ const { execFileSync, spawnSync } = require('node:child_process');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const forgeToken = require('../../scripts/check-forge-token');
|
|
7
|
+
const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
|
|
8
|
+
const { autoShepherdRailEnabled } = require('./ship');
|
|
7
9
|
|
|
8
10
|
const isWindows = process.platform === 'win32';
|
|
9
11
|
|
|
@@ -110,6 +112,57 @@ async function autoFileBackingIssueForPush(projectRoot, execFn, deps = {}) {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the OPEN PR number for the current branch via `gh pr view`. Returns
|
|
117
|
+
* null when there is no PR, gh is unavailable, or anything errors (fail-open).
|
|
118
|
+
* NEVER throws.
|
|
119
|
+
*
|
|
120
|
+
* @param {function} execFn - execFileSync or mock
|
|
121
|
+
* @returns {number|null}
|
|
122
|
+
*/
|
|
123
|
+
function resolveOpenPrNumber(execFn) {
|
|
124
|
+
try {
|
|
125
|
+
const out = execFn('gh', ['pr', 'view', '--json', 'number', '-q', '.number'], {
|
|
126
|
+
encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
});
|
|
128
|
+
const n = Number.parseInt(String(out).trim(), 10);
|
|
129
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
130
|
+
} catch (_err) { /* intentional: no open PR / gh missing → arm nothing */ // NOSONAR S2486
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort, NON-BLOCKING arm of the constant PR watcher after a successful
|
|
137
|
+
* push, when an OPEN PR exists for the current branch. This closes the gap where
|
|
138
|
+
* PRs not born from `forge ship` (gh pr create, the GitHub UI, an earlier push)
|
|
139
|
+
* never got a watcher. Gated by the default-ON `rail.auto_shepherd`, idempotent
|
|
140
|
+
* via the watch loop's own PID/journal lock, and reusing the same
|
|
141
|
+
* `startPrWatcherDetached` as ship. MUST NEVER throw into or fail the push: a
|
|
142
|
+
* disabled rail, no PR, a gh error, or a spawn error all degrade to
|
|
143
|
+
* `{ armed: false }`.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} params
|
|
146
|
+
* @returns {{ armed: boolean, reason?: string, prNumber?: number }}
|
|
147
|
+
*/
|
|
148
|
+
function maybeArmWatcherAfterPush({
|
|
149
|
+
projectRoot,
|
|
150
|
+
execFn,
|
|
151
|
+
startWatcher = startPrWatcherDetached,
|
|
152
|
+
railEnabled = autoShepherdRailEnabled,
|
|
153
|
+
prLookup = resolveOpenPrNumber,
|
|
154
|
+
}) {
|
|
155
|
+
try {
|
|
156
|
+
if (!railEnabled(projectRoot)) return { armed: false, reason: 'rail.auto_shepherd disabled' };
|
|
157
|
+
const prNumber = prLookup(execFn);
|
|
158
|
+
if (!prNumber) return { armed: false, reason: 'no-open-pr' };
|
|
159
|
+
const res = startWatcher({ prNumber, cwd: projectRoot });
|
|
160
|
+
return { armed: !!(res && res.started), reason: res && res.reason, prNumber };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { armed: false, reason: err.message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
113
166
|
/**
|
|
114
167
|
* Run branch protection check as a subprocess.
|
|
115
168
|
* @param {function} execFn - execFileSync or mock
|
|
@@ -247,6 +300,17 @@ module.exports = {
|
|
|
247
300
|
};
|
|
248
301
|
}
|
|
249
302
|
|
|
303
|
+
// Arm the constant PR watcher for this branch's open PR (best-effort,
|
|
304
|
+
// gated by rail.auto_shepherd, never fails the push). Covers PRs not born
|
|
305
|
+
// from `forge ship`.
|
|
306
|
+
maybeArmWatcherAfterPush({
|
|
307
|
+
projectRoot,
|
|
308
|
+
execFn,
|
|
309
|
+
startWatcher: deps?.startWatcher,
|
|
310
|
+
railEnabled: deps?.railEnabled,
|
|
311
|
+
prLookup: deps?.prLookup,
|
|
312
|
+
});
|
|
313
|
+
|
|
250
314
|
return {
|
|
251
315
|
success: true,
|
|
252
316
|
quickMode,
|
|
@@ -259,5 +323,7 @@ module.exports = {
|
|
|
259
323
|
// Exposed for unit tests; not part of the CLI surface.
|
|
260
324
|
_internal: {
|
|
261
325
|
autoFileBackingIssueForPush,
|
|
326
|
+
maybeArmWatcherAfterPush,
|
|
327
|
+
resolveOpenPrNumber,
|
|
262
328
|
},
|
|
263
329
|
};
|