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
|
@@ -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
|
-
};
|