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.
Files changed (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -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 { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
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
- exec('gh', ['--version'], { ...getGhCheckOptions(), cwd }); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
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 = exec('gh', ['pr', 'create', '--title', title, '--body', body], getGitExecOptions(cwd)); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
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, non-blocking auto-start of the constant PR monitor once a real PR
515
- * exists. Skipped on a dry run or when no PR number is known. MUST NEVER fail
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
- * Gated by the default-ON `rail.auto_shepherd` rail: when a maintainer has
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 maybeStartPrWatcher({ dryRun, prNumber, startWatcher, railEnabled = autoShepherdRailEnabled }) {
529
- if (dryRun || !prNumber) return { started: false, reason: 'skipped' };
458
+ function maybeTriggerShepherdAfterShip({ dryRun, projectRoot = process.cwd(), fireAndForget: trigger = fireAndForget }) {
459
+ if (dryRun) return { started: false, reason: 'skipped' };
530
460
  try {
531
- if (!railEnabled(process.cwd())) {
532
- return { started: false, reason: 'rail.auto_shepherd disabled' };
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 { featureSlug, title, dryRun = false, startWatcher = startPrWatcherDetached, railEnabled = autoShepherdRailEnabled } = options || {};
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 createPR({ title, body: prBody, dryRun });
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
- maybeStartPrWatcher({ dryRun, prNumber: result.prNumber, startWatcher, railEnabled });
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
- maybeStartPrWatcher,
571
+ maybeTriggerShepherdAfterShip,
627
572
  autoShepherdRailEnabled,
628
573
  extractKeyDecisions,
629
574
  extractTestScenarios,
@@ -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 the only tier today (behavioral is W5); it is accepted (and
83
- * implied) so the flag reads honestly and future tiers can branch here.
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('', 'Behavioral tier (recall/precision/chains/outcome/variance) is W5.');
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 (the only tier today; behavioral is W5)',
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
- // flags is the LAST declared param so its `= {}` default is trailing (SonarCloud S1788). The
249
- // registry still passes (args, flags, projectRoot, opts) — the router reads the canonical catalog
250
- // from the package root; eval/scores read the canonical skills/ source dir.
251
- handler: (args, flags = {}, projectRoot) => {
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
  };
@@ -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
@@ -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 scriptPath = path.join(getPackageRoot(), 'scripts', 'forge-team', 'index.sh');
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
- execFileSync(bashPath, [scriptPath, ...args], {
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