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
@@ -1,189 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { execFileSync } = require('node:child_process');
6
- const { cleanupDeprecatedSyncFiles } = require('./deprecated-sync-cleanup');
7
-
8
- const DEFAULT_BEADS_VERSION = '1.0.0';
9
-
10
- /**
11
- * Detect the default branch of the repository.
12
- *
13
- * Strategy (in order):
14
- * 1. `git symbolic-ref refs/remotes/origin/HEAD` -> parse branch name
15
- * 2. `git remote show origin` -> parse "HEAD branch:" line
16
- * 3. Fall back to `'main'`
17
- *
18
- * @param {string} projectRoot - Absolute path to the project root.
19
- * @param {object} [options] - Options object.
20
- * @param {Function} [options._exec] - Injected execFileSync for testing.
21
- * @returns {string} The default branch name.
22
- */
23
- function detectDefaultBranch(projectRoot, options = {}) {
24
- const exec = options._exec || execFileSync;
25
-
26
- // Strategy 1: symbolic-ref
27
- try {
28
- const out = exec('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], {
29
- cwd: projectRoot,
30
- stdio: ['pipe', 'pipe', 'pipe'],
31
- });
32
- const ref = out.toString().trim();
33
- // refs/remotes/origin/main -> main
34
- const parts = ref.split('/');
35
- if (parts.length > 0) {
36
- return parts[parts.length - 1];
37
- }
38
- } catch (_e) {
39
- // Expected: symbolic-ref fails when origin/HEAD is not set — fall through to strategy 2
40
- }
41
-
42
- // Strategy 2: remote show origin
43
- try {
44
- const out = exec('git', ['remote', 'show', 'origin'], {
45
- cwd: projectRoot,
46
- stdio: ['pipe', 'pipe', 'pipe'],
47
- });
48
- const text = out.toString();
49
- const match = text.match(/HEAD branch:\s*(.+)/);
50
- if (match) {
51
- return match[1].trim();
52
- }
53
- } catch (_e) {
54
- // Expected: 'git remote show origin' fails when no remote is configured — fall through to fallback
55
- }
56
-
57
- // Strategy 3: fallback
58
- return 'main';
59
- }
60
-
61
- /**
62
- * Detect the installed Beads version.
63
- *
64
- * Strategy (in order):
65
- * 1. `bd --version` -> parse version string (e.g. "beads version 0.52.0" -> "0.52.0")
66
- * 2. Fall back to the current repo baseline release
67
- *
68
- * @param {object} [options] - Options object.
69
- * @param {Function} [options._exec] - Injected execFileSync for testing.
70
- * @returns {string} The Beads version string.
71
- */
72
- function detectBeadsVersion(options = {}) {
73
- const exec = options._exec || execFileSync;
74
-
75
- try {
76
- const out = exec('bd', ['--version'], {
77
- stdio: ['pipe', 'pipe', 'pipe'],
78
- });
79
- const text = out.toString().trim();
80
- // "beads version 0.52.0" -> "0.52.0"
81
- const match = text.match(/(\d{1,4}\.\d{1,4}\.\d{1,4})/);
82
- if (match) {
83
- return match[1];
84
- }
85
- } catch (_e) {
86
- // Expected: 'bd --version' fails when bd CLI is not installed — fall through to default version
87
- }
88
-
89
- return DEFAULT_BEADS_VERSION;
90
- }
91
-
92
- /**
93
- * Template workflow YAML files by replacing the default branch and Beads version.
94
- *
95
- * Templates only forge-created workflow files by replacing the default branch
96
- * and Beads version. Skips user-owned workflows to avoid overwriting legitimate
97
- * branch targets.
98
- *
99
- * @param {string} workflowDir - Absolute path to the directory containing YAML files.
100
- * @param {string} branch - The default branch name to substitute.
101
- * @param {string} beadsVersion - The Beads version to substitute.
102
- * @param {string[]} [createdFiles=[]] - List of file paths created by scaffoldBeadsSync. Only these are templated.
103
- */
104
- function templateWorkflows(workflowDir, branch, beadsVersion, createdFiles = []) {
105
- if (!fs.existsSync(workflowDir)) return;
106
-
107
- const targetNames = new Set(createdFiles.map(f => path.basename(f)));
108
-
109
- const entries = fs.readdirSync(workflowDir);
110
-
111
- for (const entry of entries) {
112
- // Only template forge-created files; skip user-owned workflows
113
- if (targetNames.size > 0 && !targetNames.has(entry)) continue;
114
-
115
- const ext = path.extname(entry).toLowerCase();
116
- if (ext !== '.yml' && ext !== '.yaml') {
117
- continue;
118
- }
119
-
120
- const filePath = path.join(workflowDir, entry);
121
- const stat = fs.statSync(filePath);
122
- if (!stat.isFile()) {
123
- continue;
124
- }
125
-
126
- const original = fs.readFileSync(filePath, 'utf8');
127
- let content = original;
128
- content = content.replaceAll(
129
- /branches:\s*\[master\]/g,
130
- `branches: [${branch}]`
131
- );
132
- for (const version of ['0.49.1', DEFAULT_BEADS_VERSION, '__FORGE_BEADS_VERSION__']) {
133
- content = content.replaceAll(
134
- `BD_VERSION="${version}"`,
135
- `BD_VERSION="${beadsVersion}"`
136
- );
137
- }
138
- // Only write if content actually changed
139
- if (content !== original) {
140
- fs.writeFileSync(filePath, content);
141
- }
142
- }
143
- }
144
-
145
- function cleanupDeprecatedBeadsSync(projectRoot, options = {}) {
146
- return cleanupDeprecatedSyncFiles(projectRoot, options);
147
- }
148
-
149
- /**
150
- * @typedef {Object} ScaffoldResult
151
- * @property {string[]} filesCreated - Always empty; retained for deprecated API compatibility.
152
- * @property {string[]} filesSkipped - Always empty; retained for deprecated API compatibility.
153
- * @property {string[]} filesRemoved - Relative paths of deprecated generated sync files removed.
154
- * @property {boolean} deprecated - Indicates the legacy scaffold path is deprecated.
155
- * @property {string} message - Deprecation guidance for future sync direction.
156
- */
157
-
158
- /**
159
- * Deprecated compatibility shim for removed Beads/GitHub sync scaffolding.
160
- *
161
- * Performs cleanup of generated legacy sync artifacts only. User-owned files
162
- * at matching paths are preserved unless their content matches known generated
163
- * templates.
164
- *
165
- * @param {string} projectRoot - Absolute path to the user's project root
166
- * @param {string} packageDir - Absolute path to the forge package install location
167
- * @param {Object} [_options={}] - Reserved for future options
168
- * @returns {ScaffoldResult} Deprecated cleanup result.
169
- */
170
- function scaffoldBeadsSync(projectRoot, packageDir, _options = {}) {
171
- const cleanup = cleanupDeprecatedBeadsSync(projectRoot, { packageDir });
172
-
173
- return {
174
- filesCreated: [],
175
- filesSkipped: [],
176
- filesRemoved: cleanup.removed,
177
- deprecated: true,
178
- message: 'Beads GitHub sync scaffolding is deprecated; future GitHub issue sync must use Forge Kernel/server authority.'
179
- };
180
- }
181
-
182
- module.exports = {
183
- DEFAULT_BEADS_VERSION,
184
- detectDefaultBranch,
185
- detectBeadsVersion,
186
- templateWorkflows,
187
- cleanupDeprecatedBeadsSync,
188
- scaffoldBeadsSync,
189
- };
package/lib/pat-setup.js DELETED
@@ -1,207 +0,0 @@
1
- /**
2
- * Guided PAT (Personal Access Token) setup for Beads GitHub sync.
3
- *
4
- * Walks the user through creating a fine-grained PAT and saving it
5
- * as a repository secret via the `gh` CLI. Token values are never
6
- * printed to stdout — they are piped to `gh secret set` via stdin.
7
- *
8
- * @module pat-setup
9
- */
10
-
11
- const { execFileSync: defaultExecFileSync } = require('node:child_process');
12
-
13
- // ---------------------------------------------------------------------------
14
- // checkGhAuth
15
- // ---------------------------------------------------------------------------
16
-
17
- /**
18
- * Check whether the user is authenticated to GitHub via `gh auth status`.
19
- *
20
- * @param {object} [options={}]
21
- * @param {Function} [options._exec] - DI override for execFileSync
22
- * @returns {{ authenticated: boolean, user: string|null }}
23
- */
24
- function checkGhAuth(options = {}) {
25
- const exec = options._exec || defaultExecFileSync;
26
- try {
27
- const output = exec('gh', ['auth', 'status'], { encoding: 'utf8' });
28
- const str = typeof output === 'string' ? output : output.toString('utf8');
29
- const match = str.match(/account\s+(\S+)/);
30
- return {
31
- authenticated: true,
32
- user: match ? match[1] : null
33
- };
34
- } catch (_err) {
35
- // Expected: 'gh auth status' fails when user is not authenticated — report as unauthenticated
36
- return { authenticated: false, user: null };
37
- }
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // validateToken
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * Validate that a string looks like a GitHub PAT.
46
- *
47
- * Accepts tokens starting with `ghp_` or `github_pat_` followed by at
48
- * least one additional character.
49
- *
50
- * @param {string|undefined} token
51
- * @returns {{ valid: boolean, reason?: string }}
52
- */
53
- function validateToken(token) {
54
- if (!token || typeof token !== 'string' || token.trim() === '') {
55
- return { valid: false, reason: 'Token is empty or not provided.' };
56
- }
57
-
58
- const trimmed = token.trim();
59
-
60
- // Must start with a known prefix AND have content after the prefix
61
- const ghpValid = trimmed.startsWith('ghp_') && trimmed.length > 'ghp_'.length;
62
- const patValid = trimmed.startsWith('github_pat_') && trimmed.length > 'github_pat_'.length;
63
-
64
- if (!ghpValid && !patValid) {
65
- return {
66
- valid: false,
67
- reason: 'Token must start with "ghp_" or "github_pat_" followed by additional characters.'
68
- };
69
- }
70
-
71
- return { valid: true };
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // saveSecret
76
- // ---------------------------------------------------------------------------
77
-
78
- /**
79
- * Save a token as a GitHub repository secret via `gh secret set`.
80
- *
81
- * The token is piped to stdin (via the `input` option of execFileSync)
82
- * so that it never appears in process arguments or stdout.
83
- *
84
- * @param {string} secretName - Name of the repository secret
85
- * @param {string} token - The PAT value (piped via stdin, never logged)
86
- * @param {object} [options={}]
87
- * @param {Function} [options._exec] - DI override for execFileSync
88
- * @returns {{ success: boolean, error: string|null }}
89
- */
90
- function saveSecret(secretName, token, options = {}) {
91
- const exec = options._exec || defaultExecFileSync;
92
- try {
93
- exec('gh', ['secret', 'set', secretName], {
94
- input: token,
95
- encoding: 'utf8'
96
- });
97
- return { success: true, error: null };
98
- } catch (err) {
99
- let msg;
100
- if (err.stderr) {
101
- msg = typeof err.stderr === 'string' ? err.stderr : err.stderr.toString('utf8');
102
- } else {
103
- msg = err.message;
104
- }
105
- return { success: false, error: msg };
106
- }
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // setupPAT
111
- // ---------------------------------------------------------------------------
112
-
113
- /**
114
- * Orchestrate the full PAT setup flow.
115
- *
116
- * In non-interactive mode the step is skipped entirely. When `gh` is not
117
- * authenticated the user receives manual instructions. Otherwise the
118
- * function prompts for a token, validates it, and saves it as the
119
- * `BEADS_SYNC_TOKEN` repository secret.
120
- *
121
- * @param {string} _projectRoot - Absolute path to the project root
122
- * @param {object} [options={}]
123
- * @param {boolean} [options.interactive=true] - Whether prompts are allowed
124
- * @param {Function} [options._exec] - DI override for execFileSync
125
- * @param {Function} [options._prompt] - DI override for user input prompt
126
- * @returns {{ success: boolean, method: string, error?: string, reminder?: string, instructions?: string }}
127
- */
128
- function setupPAT(_projectRoot, options = {}) {
129
- const { interactive = true, _prompt } = options;
130
-
131
- // Non-interactive: skip entirely
132
- if (!interactive) {
133
- return {
134
- success: false,
135
- method: 'skipped',
136
- reminder: 'Run "forge setup" interactively to configure the Beads sync PAT, or manually set the BEADS_SYNC_TOKEN repository secret.'
137
- };
138
- }
139
-
140
- // Check gh authentication
141
- const auth = checkGhAuth(options);
142
-
143
- if (!auth.authenticated) {
144
- const instructions = [
145
- 'GitHub CLI is not authenticated. To set up the Beads sync token manually:',
146
- '1. Go to https://github.com/settings/tokens?type=beta',
147
- '2. Create a fine-grained PAT with "repo" scope',
148
- '3. Run: gh secret set BEADS_SYNC_TOKEN',
149
- ' (or add it via your repo Settings > Secrets > Actions)'
150
- ].join('\n');
151
- console.log(` ${instructions.replaceAll('\n', '\n ')}`);
152
- return {
153
- success: false,
154
- method: 'manual',
155
- instructions
156
- };
157
- }
158
-
159
- // Prompt for token
160
- if (!_prompt) {
161
- console.log(' No prompt function provided — skipping PAT setup.');
162
- console.log(' Run "gh secret set BEADS_SYNC_TOKEN" manually to configure.');
163
- return {
164
- success: false,
165
- method: 'manual',
166
- error: 'No prompt function available for interactive token input'
167
- };
168
- }
169
-
170
- const token = _prompt();
171
-
172
- // Validate
173
- const validation = validateToken(token);
174
- if (!validation.valid) {
175
- console.log(` Invalid token: ${validation.reason}`);
176
- return {
177
- success: false,
178
- method: 'automated',
179
- error: validation.reason
180
- };
181
- }
182
-
183
- // Save the secret — token is piped via stdin, never logged
184
- const saveResult = saveSecret('BEADS_SYNC_TOKEN', token, options);
185
-
186
- if (!saveResult.success) {
187
- console.log(` Failed to save secret: ${saveResult.error}`);
188
- return {
189
- success: false,
190
- method: 'automated',
191
- error: saveResult.error
192
- };
193
- }
194
-
195
- console.log(' PAT saved as BEADS_SYNC_TOKEN repository secret.');
196
- return {
197
- success: true,
198
- method: 'automated'
199
- };
200
- }
201
-
202
- module.exports = {
203
- checkGhAuth,
204
- validateToken,
205
- saveSecret,
206
- setupPAT
207
- };
@@ -1,206 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * PR-monitor sticky-comment renderer — turn ONE read-only `gatherPrBundle`
5
- * result (lib/pr-bundle.js) into the Markdown body of the single sticky PR
6
- * comment the pr-monitor GitHub workflow keeps up to date.
7
- *
8
- * This is the SURFACE half of the monitor: it leads with the one-line actionable
9
- * verdict (mirroring the `pr-verdict:*` label the workflow lands), then lists the
10
- * unresolved review threads (grouped by author, ANY author) plus the failing and
11
- * pending checks so async review-bot / human feedback in a window nobody is
12
- * watching cannot rot. The verdict LABELS state (check-failed / threads-open /
13
- * mergeable / …); it is NOT a merge action — it never merges, never resolves
14
- * threads, never blocks, and is fail-closed (`unknown` on unreadable signals).
15
- *
16
- * Pure and deterministic: same bundle + same injected clock → same body, which
17
- * is what lets the workflow rewrite the sticky comment in place without churn.
18
- *
19
- * @module pr-monitor/render-sticky
20
- */
21
-
22
- /**
23
- * Presentation-only headline for each canonical merge verdict (lib/pr-pull.js).
24
- * The verdict VALUE is computed once by pr-pull (`forge shepherd --pull --json`)
25
- * and passed in — this map only decides how to DISPLAY it, so there is no second
26
- * verdict ladder to drift.
27
- */
28
- const VERDICT_HEADLINE = {
29
- UNKNOWN: '⚪ **Verdict: `unknown`** — a signal was unreadable; state unconfirmed (fail-closed).',
30
- 'BLOCKED-CONFLICT': '🔀 **Verdict: `blocked-conflict`** — branch conflicts with base; rebase/merge and resolve.',
31
- BEHIND: '⬇️ **Verdict: `behind`** — branch is behind base; update/rebase (protection requires up-to-date).',
32
- 'BLOCKED-CHECKS': '🔴 **Verdict: `blocked-checks`** — a required check is failing/missing; fix it.',
33
- 'BLOCKED-THREADS': '🟠 **Verdict: `blocked-threads`** — unresolved review threads need addressing.',
34
- 'REVIEW-PENDING': '🟡 **Verdict: `review-pending`** — awaiting review / settle window; not ready yet.',
35
- 'CLEAN-MERGEABLE': '🟢 **Verdict: `clean-mergeable`** — green + zero unresolved threads; ready for a human to merge.',
36
- };
37
-
38
- /**
39
- * Render the one-line verdict headline for a canonical verdict string. Unknown or
40
- * missing input falls closed to the `unknown` headline.
41
- *
42
- * @param {string} verdict
43
- * @returns {string}
44
- */
45
- function verdictHeadline(verdict) {
46
- return VERDICT_HEADLINE[String(verdict || '').toUpperCase()] || VERDICT_HEADLINE.UNKNOWN;
47
- }
48
-
49
- /** Hidden HTML marker: the workflow finds its prior comment by this string and
50
- * UPDATES it in place, so the monitor never spams a PR with new comments. */
51
- const STICKY_MARKER = '<!-- forge-pr-monitor -->';
52
-
53
- /** Cap threads listed per author so a noisy PR can't produce an enormous body. */
54
- const MAX_THREADS_PER_AUTHOR = 8;
55
-
56
- /** Group unresolved review-thread comments by author → ordered [author, threads]. */
57
- function groupByAuthor(comments) {
58
- const byAuthor = new Map();
59
- for (const c of (Array.isArray(comments) ? comments : [])) {
60
- const author = String(c.author || 'unknown');
61
- if (!byAuthor.has(author)) byAuthor.set(author, []);
62
- byAuthor.get(author).push(c);
63
- }
64
- // Sort authors by descending thread count, then name — stable + deterministic.
65
- return [...byAuthor.entries()].sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
66
- }
67
-
68
- /** One-line locator for a thread: `path:line` when known, else the threadId. */
69
- function threadLocator(t) {
70
- if (t.path) return t.line != null ? `${t.path}:${t.line}` : t.path;
71
- return t.threadId || '(thread)';
72
- }
73
-
74
- /** Render the unresolved-review-threads section (author-agnostic + fail-closed). */
75
- function renderThreads(bundle, lines) {
76
- // Fail-closed: if the thread read was not available for ANY reason — it threw
77
- // (error set) OR the adapter cannot read comments at all (capability absent,
78
- // error null) — NEVER render "zero / clean". Only a genuine available:true read
79
- // may report "no unresolved threads". Guard on `!== true` (not `=== false`) so a
80
- // producer that omits the flag is also treated as unread, never as clean.
81
- if (bundle.unresolvedCommentsAvailable !== true) {
82
- const why = bundle.unresolvedCommentsError || 'thread read unavailable (capability absent)';
83
- lines.push('### Review threads');
84
- lines.push(`⚠️ Review threads were **unreadable** this pass (\`${why}\`) — not treated as zero. Re-run once the read recovers.`);
85
- lines.push('');
86
- return;
87
- }
88
-
89
- const comments = Array.isArray(bundle.unresolvedComments) ? bundle.unresolvedComments : [];
90
- if (comments.length === 0) {
91
- lines.push('### Review threads');
92
- lines.push('✅ No unresolved review threads.');
93
- lines.push('');
94
- return;
95
- }
96
-
97
- const groups = groupByAuthor(comments);
98
- lines.push(`### Unresolved review threads (${comments.length})`);
99
- lines.push('');
100
- for (const [author, threads] of groups) {
101
- lines.push(`- **${author}** — ${threads.length}`);
102
- for (const t of threads.slice(0, MAX_THREADS_PER_AUTHOR)) {
103
- lines.push(` - \`${threadLocator(t)}\``);
104
- }
105
- if (threads.length > MAX_THREADS_PER_AUTHOR) {
106
- lines.push(` - …and ${threads.length - MAX_THREADS_PER_AUTHOR} more`);
107
- }
108
- }
109
- lines.push('');
110
- }
111
-
112
- /** Render the failing / pending check sections (author-agnostic + fail-closed). */
113
- function renderChecks(bundle, lines) {
114
- // Fail-closed, mirroring renderThreads: empty ci arrays are AMBIGUOUS — they
115
- // mean either "read, genuinely all-clear" or "never read (gather outage)". Only
116
- // an explicit ciAvailable === true lets us render the summary; anything else
117
- // ("!== true": false or missing) surfaces as unreadable, so the monitor never
118
- // prints a false "no failing checks" for CI it did not actually read.
119
- if (bundle.ciAvailable !== true) {
120
- lines.push('### Checks');
121
- lines.push('⚠️ Checks were **unreadable** this pass — not treated as green. Re-run once the read recovers.');
122
- lines.push('');
123
- return;
124
- }
125
-
126
- const ci = bundle.ci || {};
127
- const failing = Array.isArray(ci.failing) ? ci.failing : [];
128
- const pending = Array.isArray(ci.pending) ? ci.pending : [];
129
-
130
- lines.push('### Checks');
131
- if (failing.length === 0 && pending.length === 0) {
132
- lines.push('✅ No failing or pending checks.');
133
- } else {
134
- if (failing.length > 0) {
135
- lines.push(`- ❌ **Failing (${failing.length}):** ${failing.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
136
- }
137
- if (pending.length > 0) {
138
- lines.push(`- ⏳ **Pending (${pending.length}):** ${pending.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
139
- }
140
- }
141
- lines.push('');
142
- }
143
-
144
- /**
145
- * Render the sticky monitor comment for a PR-state bundle.
146
- *
147
- * @param {object} bundle - a `gatherPrBundle` result (lib/pr-bundle.js).
148
- * @param {object} [opts]
149
- * @param {Date} [opts.now] - injected clock for deterministic output.
150
- * @param {string} [opts.verdict] - the canonical `--pull` verdict.
151
- * @param {string[]} [opts.unreadable] - unreadable signal names (from the `--pull`
152
- * evidence). Surfaced when the verdict is UNKNOWN so the sticky always says WHICH
153
- * signal could not be read, not just a bare `unknown`.
154
- * @returns {{ marker: string, body: string }}
155
- */
156
- function renderStickyComment(bundle = {}, opts = {}) {
157
- const now = opts.now instanceof Date ? opts.now : new Date();
158
- const lines = [];
159
-
160
- // The marker MUST be the very first bytes so the workflow's substring match
161
- // finds the prior comment regardless of any rendering below it.
162
- lines.push(STICKY_MARKER);
163
- lines.push('## 🔭 Forge PR Monitor');
164
- lines.push('');
165
- // Lead with the actionable verdict — the SAME value as the pr-verdict:* label
166
- // and `forge shepherd --pull --json` (passed in via opts.verdict, computed once
167
- // by pr-pull). Surface only: it labels state; this monitor **does not merge**
168
- // and never resolves review threads.
169
- lines.push(verdictHeadline(opts.verdict));
170
- // An `unknown` verdict is useless without the WHY — name the unreadable
171
- // signal(s) so a human/agent knows what to fix (e.g. `requiredChecks` when both
172
- // branch-protection AND the rollup fallback could not be read).
173
- const verdictUpper = String(opts.verdict || '').toUpperCase();
174
- const isUnknown = verdictUpper === 'UNKNOWN' || !VERDICT_HEADLINE[verdictUpper];
175
- const unreadable = Array.isArray(opts.unreadable) ? opts.unreadable.filter(Boolean) : [];
176
- if (isUnknown && unreadable.length > 0) {
177
- lines.push('');
178
- lines.push(`> Unreadable signal(s): ${unreadable.map((s) => `\`${s}\``).join(', ')}.`);
179
- }
180
- lines.push('');
181
- lines.push('_Surfaces open review + check state so async feedback never rots. This monitor **does not merge** and never resolves review threads — a human merges in the GitHub UI._');
182
- lines.push('');
183
-
184
- renderThreads(bundle, lines);
185
- renderChecks(bundle, lines);
186
-
187
- const branch = bundle.branch || {};
188
- if ((branch.behind || 0) > 0) {
189
- lines.push(`> Branch is **${branch.behind}** commit(s) behind base.`);
190
- lines.push('');
191
- }
192
-
193
- lines.push('---');
194
- lines.push(`<sub>Updated ${now.toISOString()} · surface-only monitor · labels state, never merges, never resolves threads.</sub>`);
195
-
196
- return { marker: STICKY_MARKER, body: lines.join('\n') };
197
- }
198
-
199
- module.exports = {
200
- renderStickyComment,
201
- verdictHeadline,
202
- groupByAuthor,
203
- threadLocator,
204
- STICKY_MARKER,
205
- MAX_THREADS_PER_AUTHOR,
206
- };