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/lib/commands/ship.js
CHANGED
|
@@ -12,8 +12,9 @@ const { execFileSync } = require('node:child_process');
|
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
16
16
|
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
17
|
+
const { resolveBaseRemote, resolveBaseBranch } = require('../base-remote');
|
|
17
18
|
|
|
18
19
|
const AUTO_SHEPHERD_RAIL = 'rail.auto_shepherd';
|
|
19
20
|
|
|
@@ -65,72 +66,6 @@ function getQuietGitExecOptions(cwd = process.cwd()) {
|
|
|
65
66
|
return { ...getGitExecOptions(cwd), stdio: 'pipe' };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function resolveRemoteHeadTarget(exec = execFileSync, cwd = process.cwd(), remoteName) {
|
|
69
|
-
try {
|
|
70
|
-
const symbolicRef = exec('git', ['symbolic-ref', `refs/remotes/${remoteName}/HEAD`], getQuietGitExecOptions(cwd)).trim();
|
|
71
|
-
if (!symbolicRef) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
exec('git', ['rev-parse', '--verify', symbolicRef], getQuietGitExecOptions(cwd));
|
|
75
|
-
return symbolicRef;
|
|
76
|
-
} catch (_error) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function remoteHasTrackingBase(exec = execFileSync, cwd = process.cwd(), remoteName) {
|
|
82
|
-
if (resolveRemoteHeadTarget(exec, cwd, remoteName)) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const candidate of ['main', 'master']) {
|
|
87
|
-
try {
|
|
88
|
-
exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietGitExecOptions(cwd));
|
|
89
|
-
return true;
|
|
90
|
-
} catch (_error) {
|
|
91
|
-
// Probe next candidate.
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resolveBaseRemote(exec = execFileSync, cwd = process.cwd()) {
|
|
99
|
-
for (const candidate of ['upstream', 'origin']) {
|
|
100
|
-
try {
|
|
101
|
-
exec('git', ['remote', 'get-url', candidate], getQuietGitExecOptions(cwd));
|
|
102
|
-
if (remoteHasTrackingBase(exec, cwd, candidate)) {
|
|
103
|
-
return candidate;
|
|
104
|
-
}
|
|
105
|
-
} catch (_error) {
|
|
106
|
-
// Probe next candidate.
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return 'origin';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function resolveBaseBranch(exec = execFileSync, cwd = process.cwd(), remoteName = resolveBaseRemote(exec, cwd)) {
|
|
114
|
-
const symbolicRef = resolveRemoteHeadTarget(exec, cwd, remoteName);
|
|
115
|
-
if (symbolicRef) {
|
|
116
|
-
const match = new RegExp(`^refs/remotes/${remoteName}/(.+)$`).exec(symbolicRef);
|
|
117
|
-
if (match && match[1]) {
|
|
118
|
-
return match[1];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
for (const candidate of ['main', 'master']) {
|
|
123
|
-
try {
|
|
124
|
-
exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietGitExecOptions(cwd));
|
|
125
|
-
return candidate;
|
|
126
|
-
} catch (_error) {
|
|
127
|
-
// Probe next candidate.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return 'master';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
69
|
function refreshBaseReference(exec = execFileSync, cwd = process.cwd(), remoteName, baseBranch) {
|
|
135
70
|
exec(
|
|
136
71
|
'git',
|
|
@@ -457,10 +392,13 @@ function validatePRTitle(title) {
|
|
|
457
392
|
|
|
458
393
|
async function createPR(options) { // NOSONAR S3776
|
|
459
394
|
const { title, body, dryRun = false, exec = execFileSync, cwd = process.cwd() } = options;
|
|
395
|
+
const gh = options.githubContext?.bound
|
|
396
|
+
? (args, runOptions) => options.githubContext.runGh(args, runOptions)
|
|
397
|
+
: (args, runOptions) => exec('gh', args, runOptions);
|
|
460
398
|
const titleValidation = validatePRTitle(title);
|
|
461
399
|
if (!titleValidation.valid) return { success: false, error: titleValidation.error };
|
|
462
400
|
try {
|
|
463
|
-
|
|
401
|
+
gh(['--version'], { ...getGhCheckOptions(), cwd });
|
|
464
402
|
} catch (error) {
|
|
465
403
|
if (isCommandNotFound(error)) {
|
|
466
404
|
return { success: false, error: 'GitHub CLI (gh) not found. Install from: https://cli.github.com/' };
|
|
@@ -495,7 +433,7 @@ async function createPR(options) { // NOSONAR S3776
|
|
|
495
433
|
return { success: true, message: '[DRY RUN] Would create PR with title: ' + title, prUrl: 'https://github.com/owner/repo/pull/1' };
|
|
496
434
|
}
|
|
497
435
|
try {
|
|
498
|
-
const result =
|
|
436
|
+
const result = gh(['pr', 'create', '--title', title, '--body', body], getGitExecOptions(cwd));
|
|
499
437
|
const urlMatch = /https:\/\/github\.com\/[^\s]+/.exec(result);
|
|
500
438
|
const prUrl = urlMatch ? urlMatch[0] : null;
|
|
501
439
|
const numberMatch = /\/pull\/(\d+)/.exec(result);
|
|
@@ -511,34 +449,31 @@ async function createPR(options) { // NOSONAR S3776
|
|
|
511
449
|
}
|
|
512
450
|
|
|
513
451
|
/**
|
|
514
|
-
* Best-effort
|
|
515
|
-
*
|
|
516
|
-
* ship: `startWatcher` (startPrWatcherDetached) already never throws, and this
|
|
517
|
-
* guard keeps even a surprise error from surfacing to the ship caller.
|
|
452
|
+
* Best-effort wake of the repository-wide singleton after a successful real ship.
|
|
453
|
+
* The shared trigger owns containment and gate checks; dry-run skips before it.
|
|
518
454
|
*
|
|
519
|
-
*
|
|
520
|
-
* disabled it (`forge gate disable rail.auto_shepherd`), the watcher is skipped
|
|
521
|
-
* so the auto-start is honestly toggleable. The rail check is fail-open and
|
|
522
|
-
* wrapped in the same try/catch, so neither a disabled rail nor a config-read
|
|
523
|
-
* error ever fails ship.
|
|
524
|
-
*
|
|
525
|
-
* @param {{ dryRun: boolean, prNumber?: string|number, startWatcher: Function, railEnabled?: Function }} params
|
|
455
|
+
* @param {{ dryRun: boolean, projectRoot?: string, fireAndForget?: Function }} params
|
|
526
456
|
* @returns {{ started: boolean, reason?: string }}
|
|
527
457
|
*/
|
|
528
|
-
function
|
|
529
|
-
if (dryRun
|
|
458
|
+
function maybeTriggerShepherdAfterShip({ dryRun, projectRoot = process.cwd(), fireAndForget: trigger = fireAndForget }) {
|
|
459
|
+
if (dryRun) return { started: false, reason: 'skipped' };
|
|
530
460
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
return startWatcher({ prNumber, cwd: process.cwd() });
|
|
461
|
+
trigger({ projectRoot, dryRun });
|
|
462
|
+
return { started: true };
|
|
535
463
|
} catch (err) {
|
|
536
464
|
return { started: false, reason: err.message };
|
|
537
465
|
}
|
|
538
466
|
}
|
|
539
467
|
|
|
540
468
|
async function executeShip(options) {
|
|
541
|
-
const {
|
|
469
|
+
const {
|
|
470
|
+
featureSlug,
|
|
471
|
+
title,
|
|
472
|
+
dryRun = false,
|
|
473
|
+
projectRoot = process.cwd(),
|
|
474
|
+
fireAndForget: trigger = fireAndForget,
|
|
475
|
+
createPr = createPR,
|
|
476
|
+
} = options || {};
|
|
542
477
|
|
|
543
478
|
// Validate feature slug
|
|
544
479
|
if (!featureSlug || typeof featureSlug !== 'string' || featureSlug.trim() === '') {
|
|
@@ -588,9 +523,12 @@ async function executeShip(options) {
|
|
|
588
523
|
testScenarios,
|
|
589
524
|
coverage,
|
|
590
525
|
});
|
|
591
|
-
const result = await
|
|
526
|
+
const result = await createPr({
|
|
527
|
+
title, body: prBody, dryRun,
|
|
528
|
+
exec: options.exec, cwd: options.cwd || projectRoot, githubContext: options.githubContext,
|
|
529
|
+
});
|
|
592
530
|
if (!result.success) return result;
|
|
593
|
-
|
|
531
|
+
maybeTriggerShepherdAfterShip({ dryRun, projectRoot, fireAndForget: trigger });
|
|
594
532
|
return {
|
|
595
533
|
success: true,
|
|
596
534
|
prUrl: result.prUrl,
|
|
@@ -604,12 +542,19 @@ async function executeShip(options) {
|
|
|
604
542
|
|
|
605
543
|
module.exports = {
|
|
606
544
|
name: 'ship',
|
|
545
|
+
githubAuth: true,
|
|
607
546
|
description: 'Create a pull request from validated feature work',
|
|
608
|
-
handler: async (args, flags = {}) => {
|
|
547
|
+
handler: async (args, flags = {}, projectRoot = process.cwd(), deps = {}) => {
|
|
609
548
|
const result = await executeShip({
|
|
610
549
|
featureSlug: args[0],
|
|
611
550
|
title: args[1],
|
|
612
551
|
dryRun: Boolean(flags.dryRun || flags['--dry-run']),
|
|
552
|
+
projectRoot,
|
|
553
|
+
fireAndForget: deps.fireAndForget,
|
|
554
|
+
exec: deps.exec,
|
|
555
|
+
cwd: deps.cwd || projectRoot,
|
|
556
|
+
githubContext: deps.githubContext,
|
|
557
|
+
createPr: deps.createPr,
|
|
613
558
|
});
|
|
614
559
|
if (!result.success) {
|
|
615
560
|
return result;
|
|
@@ -623,7 +568,7 @@ module.exports = {
|
|
|
623
568
|
output: lines.join('\n'),
|
|
624
569
|
};
|
|
625
570
|
},
|
|
626
|
-
|
|
571
|
+
maybeTriggerShepherdAfterShip,
|
|
627
572
|
autoShepherdRailEnabled,
|
|
628
573
|
extractKeyDecisions,
|
|
629
574
|
extractTestScenarios,
|
package/lib/commands/skill.js
CHANGED
|
@@ -19,9 +19,19 @@ const fs = require('node:fs');
|
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const { routeSkill, loadSkillCatalog } = require('../using-forge');
|
|
21
21
|
const skillEval = require('../skill-eval');
|
|
22
|
+
const { runBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-runner');
|
|
23
|
+
const { resolveBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-runtime');
|
|
24
|
+
const { loadPromotionEvidence } = require('../../scripts/lib/promotion-evidence-loader');
|
|
25
|
+
const { scorePromotion } = require('../../scripts/lib/promotion-scorecard');
|
|
26
|
+
const { createGithubContext } = require('../github-context');
|
|
27
|
+
|
|
28
|
+
const BEHAVIORAL_TIERS = Object.freeze([30, 100, 300]);
|
|
29
|
+
const BEHAVIORAL_TIER_VALUES = new Set(BEHAVIORAL_TIERS.map(String));
|
|
30
|
+
const BEHAVIORAL_TIER_USAGE = BEHAVIORAL_TIERS.join('|');
|
|
22
31
|
|
|
23
32
|
const USAGE = 'Usage: forge skill for "<situation>" [--json]\n' +
|
|
24
33
|
' forge skill eval [name] --static [--json]\n' +
|
|
34
|
+
` forge skill eval <name> --full --tier ${BEHAVIORAL_TIER_USAGE} [--json]\n` +
|
|
25
35
|
' forge skill scores [--json]\n' +
|
|
26
36
|
' forge skill coverage [--json]';
|
|
27
37
|
|
|
@@ -77,12 +87,115 @@ function writeScorecard(skillsDir, name, card) {
|
|
|
77
87
|
fs.writeFileSync(path.join(dir, 'scorecard.json'), JSON.stringify(card, null, 2) + '\n');
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
function readFlagValue(rest, flags, name) {
|
|
91
|
+
const direct = flags[name] ?? flags[`--${name}`];
|
|
92
|
+
if (direct !== undefined && direct !== true) return direct;
|
|
93
|
+
const exact = rest.indexOf(`--${name}`);
|
|
94
|
+
if (exact >= 0) return rest[exact + 1];
|
|
95
|
+
const prefix = `--${name}=`;
|
|
96
|
+
const joined = rest.find(arg => typeof arg === 'string' && arg.startsWith(prefix));
|
|
97
|
+
return joined ? joined.slice(prefix.length) : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasFlag(rest, flags, name) {
|
|
101
|
+
return flags[name] === true || flags[`--${name}`] === true ||
|
|
102
|
+
rest.includes(`--${name}`) || rest.some(arg => typeof arg === 'string' && arg.startsWith(`--${name}=`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function positionalArgs(rest, valueFlags = []) {
|
|
106
|
+
const positional = [];
|
|
107
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
108
|
+
const arg = rest[index];
|
|
109
|
+
if (typeof arg !== 'string') continue;
|
|
110
|
+
if (!arg.startsWith('--')) {
|
|
111
|
+
positional.push(arg);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const flagName = arg.slice(2).split('=', 1)[0];
|
|
115
|
+
if (valueFlags.includes(flagName) && !arg.includes('=')) index += 1;
|
|
116
|
+
}
|
|
117
|
+
return positional;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleFullEval(rest, flags, projectRoot, opts) {
|
|
121
|
+
const json = hasFlag(rest, flags, 'json');
|
|
122
|
+
const tierValue = readFlagValue(rest, flags, 'tier');
|
|
123
|
+
const name = positionalArgs(rest, ['tier'])[0];
|
|
124
|
+
if (!name) return { success: false, error: 'Behavioral evaluation requires a skill name.\n' + USAGE };
|
|
125
|
+
const tier = Number(tierValue);
|
|
126
|
+
if (!BEHAVIORAL_TIER_VALUES.has(String(tierValue))) {
|
|
127
|
+
return { success: false, error: `Behavioral evaluation requires --tier ${BEHAVIORAL_TIER_USAGE}.\n` + USAGE };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ctx = skillEval.resolveSkillsContext(projectRoot);
|
|
131
|
+
if (!ctx || !fs.existsSync(path.join(ctx.skillsDir, name, 'SKILL.md'))) {
|
|
132
|
+
return { success: false, error: `Skill '${name}' not found.` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const scoreResult = (result) => {
|
|
136
|
+
const loaded = loadPromotionEvidence({ tier, findings: result.findings });
|
|
137
|
+
const scorecard = scorePromotion({ tier, pairs: loaded.ok ? loaded.pairs : [] });
|
|
138
|
+
if (!loaded.ok) scorecard.reasons = [loaded.reason];
|
|
139
|
+
return { ...result, scorecard };
|
|
140
|
+
};
|
|
141
|
+
const formatResult = (result) => {
|
|
142
|
+
const successfulScore = tier === BEHAVIORAL_TIERS[0] || result.scorecard.status === 'PASS';
|
|
143
|
+
const success = result.status === 'PASS' && successfulScore;
|
|
144
|
+
let error;
|
|
145
|
+
if (!success) {
|
|
146
|
+
error = result.status !== 'PASS'
|
|
147
|
+
? `Behavioral evaluation ${result.status}.`
|
|
148
|
+
: `Promotion ${result.scorecard.phase || 'evaluation'} ${result.scorecard.status}.`;
|
|
149
|
+
}
|
|
150
|
+
const output = json
|
|
151
|
+
? JSON.stringify(result, null, 2) + '\n'
|
|
152
|
+
: `Behavioral evaluation: ${result.status}\nTier: ${tier}\n` +
|
|
153
|
+
`Completed: ${result.completedRuns}/${result.expectedRuns}\n` +
|
|
154
|
+
`Promotion: ${result.scorecard.phase || 'unavailable'} / ${result.scorecard.status}`;
|
|
155
|
+
return { success, error, behavioral: result, output };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const runner = opts.runBehavioralEvaluation || runBehavioralEvaluation;
|
|
159
|
+
let behavioralOptions = opts.behavioralOptions || {};
|
|
160
|
+
if (!opts.runBehavioralEvaluation || opts.resolveBehavioralEvaluation) {
|
|
161
|
+
const resolver = opts.resolveBehavioralEvaluation || resolveBehavioralEvaluation;
|
|
162
|
+
let githubContext;
|
|
163
|
+
try {
|
|
164
|
+
githubContext = (opts.prepareGithubContext || createGithubContext)(projectRoot);
|
|
165
|
+
} catch {
|
|
166
|
+
return { success: false, error: 'Unable to prepare GitHub account context. Run forge github status for guidance.' };
|
|
167
|
+
}
|
|
168
|
+
const resolved = await resolver({
|
|
169
|
+
projectRoot,
|
|
170
|
+
skillName: name,
|
|
171
|
+
skillPath: path.join(ctx.skillsDir, name, 'SKILL.md'),
|
|
172
|
+
tier,
|
|
173
|
+
env: opts.env || process.env,
|
|
174
|
+
...(githubContext.bound ? { deps: { runGh: githubContext.runGh } } : {}),
|
|
175
|
+
});
|
|
176
|
+
if (!resolved.ok) return formatResult(scoreResult(resolved.result));
|
|
177
|
+
behavioralOptions = { ...resolved.options, ...behavioralOptions };
|
|
178
|
+
}
|
|
179
|
+
const result = scoreResult(await runner({
|
|
180
|
+
...behavioralOptions,
|
|
181
|
+
projectRoot,
|
|
182
|
+
skillName: name,
|
|
183
|
+
tier,
|
|
184
|
+
}));
|
|
185
|
+
return formatResult(result);
|
|
186
|
+
}
|
|
187
|
+
|
|
80
188
|
/**
|
|
81
189
|
* "forge skill eval [name] --static [--json]" -- compute + persist the DETERMINISTIC scorecard(s).
|
|
82
|
-
* All skills when no name. --static is
|
|
83
|
-
*
|
|
190
|
+
* All skills when no name. --static is accepted and implied; behavioral execution is isolated
|
|
191
|
+
* behind the explicit --full branch above so existing static scoring remains unchanged.
|
|
84
192
|
*/
|
|
85
|
-
function handleEval(rest, flags, projectRoot) {
|
|
193
|
+
function handleEval(rest, flags, projectRoot, opts = {}) {
|
|
194
|
+
const full = hasFlag(rest, flags, 'full');
|
|
195
|
+
if (full) return handleFullEval(rest, flags, projectRoot, opts);
|
|
196
|
+
if (readFlagValue(rest, flags, 'tier') !== undefined) {
|
|
197
|
+
return { success: false, error: '--tier is valid only with --full.\n' + USAGE };
|
|
198
|
+
}
|
|
86
199
|
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
87
200
|
const positional = rest.filter(a => typeof a === 'string' && !a.startsWith('--'));
|
|
88
201
|
const name = positional[0];
|
|
@@ -116,7 +229,7 @@ function handleEval(rest, flags, projectRoot) {
|
|
|
116
229
|
}
|
|
117
230
|
const lines = ['Static scorecards written (deterministic tier):'];
|
|
118
231
|
for (const t of written) lines.push(' ' + t + ' composite=' + cards[t].composite);
|
|
119
|
-
lines.push('', '
|
|
232
|
+
lines.push('', 'Use --full --tier 30|100|300 for controlled behavioral evidence.');
|
|
120
233
|
return { success: true, cards, output: lines.join('\n') };
|
|
121
234
|
}
|
|
122
235
|
|
|
@@ -243,18 +356,21 @@ module.exports = {
|
|
|
243
356
|
usage: USAGE,
|
|
244
357
|
flags: {
|
|
245
358
|
'--json': 'Emit the machine-readable result',
|
|
246
|
-
'--static': 'Score only the deterministic static tier
|
|
359
|
+
'--static': 'Score only the deterministic static tier',
|
|
360
|
+
'--full': 'Run the controlled behavioral evaluation tier',
|
|
361
|
+
'--tier': `Behavioral corpus tier: ${BEHAVIORAL_TIER_USAGE}`,
|
|
247
362
|
},
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// from the package root; eval/scores read the canonical skills
|
|
251
|
-
handler: (args,
|
|
363
|
+
// The registry passes (args, flags, projectRoot, opts). Only the trailing `opts` carries a
|
|
364
|
+
// default so SonarCloud S1788 stays satisfied; `flags` is normalized in the body. The router
|
|
365
|
+
// reads the canonical catalog from the package root; eval/scores read the canonical skills dir.
|
|
366
|
+
handler: (args, rawFlags, projectRoot, opts = {}) => {
|
|
367
|
+
const flags = rawFlags || {};
|
|
252
368
|
const verb = args[0];
|
|
253
369
|
if (verb === 'for') {
|
|
254
370
|
return handleFor(args.slice(1), flags);
|
|
255
371
|
}
|
|
256
372
|
if (verb === 'eval') {
|
|
257
|
-
return handleEval(args.slice(1), flags, projectRoot);
|
|
373
|
+
return handleEval(args.slice(1), flags, projectRoot, opts);
|
|
258
374
|
}
|
|
259
375
|
if (verb === 'scores') {
|
|
260
376
|
return handleScores(args.slice(1), flags, projectRoot);
|
|
@@ -271,5 +387,5 @@ module.exports = {
|
|
|
271
387
|
};
|
|
272
388
|
},
|
|
273
389
|
// Exposed for unit tests; not part of the CLI surface.
|
|
274
|
-
_internal: { parseForArgs, formatRouting, handleFor, handleEval, handleScores, handleCoverage, formatScores, formatCoverage },
|
|
390
|
+
_internal: { parseForArgs, formatRouting, handleFor, handleEval, handleFullEval, handleScores, handleCoverage, formatScores, formatCoverage },
|
|
275
391
|
};
|
package/lib/commands/status.js
CHANGED
|
@@ -751,12 +751,28 @@ function formatStatus(result) {
|
|
|
751
751
|
module.exports = {
|
|
752
752
|
name: 'status',
|
|
753
753
|
description: 'One-glance orientation: where you are, what to run next, and your work',
|
|
754
|
-
usage: 'forge status [--full] [--json]',
|
|
754
|
+
usage: 'forge status [-v|--verbose] [--full] [--json]',
|
|
755
755
|
flags: {
|
|
756
|
+
'-v, --verbose': 'Print the full session-entry briefing (replaces the deprecated `forge prime`)',
|
|
756
757
|
'--full': 'Also show blocked, stale, and recently completed issues',
|
|
757
758
|
'--json': 'Emit the machine-readable status envelope',
|
|
758
759
|
},
|
|
759
760
|
handler: async (args, flags, projectRoot) => {
|
|
761
|
+
// git-status-v semantics: bare status is the short pulse, -v is the long form.
|
|
762
|
+
// `--verbose` also arrives as flags.verbose from the global CLI flag parser.
|
|
763
|
+
const statusArgs = Array.isArray(args) ? args : [];
|
|
764
|
+
if (flags?.verbose === true || statusArgs.includes('-v') || statusArgs.includes('--verbose')) {
|
|
765
|
+
// Required lazily so the short pulse never loads the briefing's module graph.
|
|
766
|
+
const { renderBriefing } = require('./prime');
|
|
767
|
+
// The briefing reads --json from args (same as prime), so fold in the flag
|
|
768
|
+
// forms the short pulse accepts or `-v` would silently drop them.
|
|
769
|
+
const wantsJson = flags?.json === true || flags?.['--json'] === true;
|
|
770
|
+
const briefingArgs = wantsJson && !statusArgs.includes('--json')
|
|
771
|
+
? [...statusArgs, '--json']
|
|
772
|
+
: statusArgs;
|
|
773
|
+
return renderBriefing(briefingArgs, projectRoot || process.cwd());
|
|
774
|
+
}
|
|
775
|
+
|
|
760
776
|
// Injectable snapshot read, same convention as setup.js's `options.commandRunner`
|
|
761
777
|
// (lib/commands/setup.js:223): a real default with a test-supplied override. Tests
|
|
762
778
|
// need a snapshot with controlled ids and orderings, which a seeded kernel cannot
|
package/lib/commands/team.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
const { existsSync } = require('node:fs');
|
|
2
2
|
const { execFileSync } = require('node:child_process');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const { getPackageRoot } = require('../package-root');
|
|
4
|
+
const { getPackageRoot, isCompiledBinary } = require('../package-root');
|
|
5
|
+
const { firstPositionalIndex, stripGlobalFlags } = require('../global-flags');
|
|
6
|
+
|
|
7
|
+
function teamArguments(args) {
|
|
8
|
+
const subcommandIndex = firstPositionalIndex(args);
|
|
9
|
+
if (subcommandIndex === -1) return stripGlobalFlags(args);
|
|
10
|
+
const result = stripGlobalFlags(args.slice(0, subcommandIndex));
|
|
11
|
+
// Path is routing metadata anywhere; other flags after the subcommand belong to Team.
|
|
12
|
+
for (let index = subcommandIndex; index < args.length; index++) {
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
if (arg.startsWith('--path=')) continue;
|
|
15
|
+
if (arg === '-p' || arg === '--path') {
|
|
16
|
+
if (args[index + 1] !== undefined && !args[index + 1].startsWith('-')) index++;
|
|
17
|
+
} else {
|
|
18
|
+
result.push(arg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
// Resolve bash binary from fixed, known locations only (no PATH search).
|
|
7
25
|
// SonarCloud S4036: PATH must not be searched for OS commands.
|
|
@@ -22,28 +40,49 @@ function _resolveBash() {
|
|
|
22
40
|
return '/usr/bin/bash';
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
function handleTeam(args) {
|
|
43
|
+
function handleTeam(args, projectRoot = process.cwd(), deps = {}) {
|
|
26
44
|
// Route through the asset root so `forge team` finds the script in BOTH channels:
|
|
27
45
|
// the on-disk package (npm/npx) and a compiled binary (extracted embed dir).
|
|
28
|
-
const
|
|
46
|
+
const packageRoot = getPackageRoot();
|
|
47
|
+
const scriptPath = path.join(packageRoot, 'scripts', 'forge-team', 'index.sh');
|
|
29
48
|
const bashPath = _resolveBash();
|
|
49
|
+
const runFile = deps.execFileSync || execFileSync;
|
|
50
|
+
const options = { stdio: 'inherit' };
|
|
51
|
+
if (deps.githubContext?.bound) {
|
|
52
|
+
// Bash also runs Git/Forge/helpers. Only the re-entering gh bridge may resolve credentials.
|
|
53
|
+
const compiled = (deps.isCompiledBinary || isCompiledBinary)();
|
|
54
|
+
const env = { ...(deps.env || process.env) };
|
|
55
|
+
for (const key of Object.keys(env)) {
|
|
56
|
+
if (['gh_token', 'github_token', 'gh_host'].includes(key.toLowerCase())) delete env[key];
|
|
57
|
+
}
|
|
58
|
+
options.cwd = projectRoot;
|
|
59
|
+
options.env = {
|
|
60
|
+
...env,
|
|
61
|
+
GH_CMD: path.join(packageRoot, 'scripts', 'github-context-bridge.sh').replaceAll('\\', '/'),
|
|
62
|
+
FORGE_GITHUB_RUNTIME: process.execPath.replaceAll('\\', '/'),
|
|
63
|
+
FORGE_GITHUB_ENTRY: compiled ? '' : path.join(packageRoot, 'bin', 'forge.js').replaceAll('\\', '/'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
30
66
|
|
|
31
67
|
try {
|
|
32
|
-
|
|
33
|
-
stdio: 'inherit',
|
|
34
|
-
});
|
|
68
|
+
runFile(bashPath, [scriptPath, ...teamArguments(args)], options);
|
|
35
69
|
} catch (err) {
|
|
36
70
|
process.exit(err.status || 1);
|
|
37
71
|
}
|
|
38
72
|
}
|
|
39
73
|
|
|
40
|
-
async function handler(args =
|
|
41
|
-
handleTeam(args);
|
|
74
|
+
async function handler(args, _flags, projectRoot, deps = {}) {
|
|
75
|
+
handleTeam(args || [], projectRoot, deps);
|
|
42
76
|
return { success: true };
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
module.exports = {
|
|
46
80
|
name: 'team',
|
|
81
|
+
githubAuth: (args = []) => {
|
|
82
|
+
const teamArgs = teamArguments(args);
|
|
83
|
+
return ['add', 'verify', 'sync', 'claim'].includes(teamArgs[0])
|
|
84
|
+
|| (teamArgs[0] === 'workload' && teamArgs.slice(1).some(arg => arg.replace(/\r$/, '') === '--me'));
|
|
85
|
+
},
|
|
47
86
|
description: 'Team coordination workflows and sync utilities',
|
|
48
87
|
handler,
|
|
49
88
|
handleTeam
|