forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5

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 +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +21 -1
  5. package/bin/forge.js +16 -369
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +9 -4
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +117 -17
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/greptile-review-adapter.js +1 -1
  19. package/lib/adapters/pr-state-adapter.js +397 -100
  20. package/lib/agents-config.js +5 -0
  21. package/lib/audit-evidence.js +71 -110
  22. package/lib/capped-jsonl-log.js +236 -0
  23. package/lib/commands/_issue.js +31 -46
  24. package/lib/commands/_manifest.js +1 -1
  25. package/lib/commands/_registry.js +2 -2
  26. package/lib/commands/_resolve-command-opts.js +36 -29
  27. package/lib/commands/claim.js +2 -4
  28. package/lib/commands/clean.js +196 -32
  29. package/lib/commands/dev.js +4 -33
  30. package/lib/commands/hooks.js +358 -13
  31. package/lib/commands/insights.js +8 -3
  32. package/lib/commands/merge.js +600 -40
  33. package/lib/commands/plan.js +23 -115
  34. package/lib/commands/pr.js +1 -1
  35. package/lib/commands/preflight.js +11 -2
  36. package/lib/commands/prime.js +23 -3
  37. package/lib/commands/push.js +41 -51
  38. package/lib/commands/recall.js +60 -16
  39. package/lib/commands/recap.js +6 -1
  40. package/lib/commands/release.js +18 -4
  41. package/lib/commands/serve.js +5 -2
  42. package/lib/commands/setup.js +191 -95
  43. package/lib/commands/shepherd.js +49 -4
  44. package/lib/commands/ship.js +22 -23
  45. package/lib/commands/skill.js +383 -0
  46. package/lib/commands/status.js +54 -33
  47. package/lib/commands/test.js +56 -34
  48. package/lib/commands/worktree.js +247 -43
  49. package/lib/core/runtime-graph.js +89 -15
  50. package/lib/doc-assertions.js +297 -0
  51. package/lib/existing-tdd-gate.js +253 -0
  52. package/lib/forge-context.js +1 -4
  53. package/lib/forge-issues.js +64 -491
  54. package/lib/git-defaults.js +56 -0
  55. package/lib/harness-capability-matrix.js +5 -5
  56. package/lib/hook-renderer.js +147 -16
  57. package/lib/insights.js +96 -80
  58. package/lib/issue-backend.js +42 -3
  59. package/lib/kernel/backing-issue.js +14 -2
  60. package/lib/kernel/broker.js +44 -0
  61. package/lib/kernel/cli-broker-factory.js +12 -1
  62. package/lib/kernel/close-on-merge.js +154 -0
  63. package/lib/kernel/fs-class.js +42 -25
  64. package/lib/kernel/migrations.js +30 -2
  65. package/lib/kernel/schema.js +35 -0
  66. package/lib/kernel/sqlite-driver.js +292 -18
  67. package/lib/lefthook-wiring.js +21 -1
  68. package/lib/memory/router.js +16 -1
  69. package/lib/memory-digest.js +47 -15
  70. package/lib/memory-recall-events.js +145 -0
  71. package/lib/memory-recall.js +212 -0
  72. package/lib/merge-rules.js +8 -4
  73. package/lib/npm-publish-workflow.js +272 -0
  74. package/lib/orientation.js +371 -49
  75. package/lib/plugin-catalog.js +14 -4
  76. package/lib/pr-bundle.js +9 -6
  77. package/lib/pr-monitor/journal.js +18 -2
  78. package/lib/pr-monitor/reconcile-executor.js +842 -0
  79. package/lib/pr-monitor/reconcile-tick.js +138 -0
  80. package/lib/pr-monitor/reconcile.js +0 -0
  81. package/lib/pr-monitor/render-summary.js +196 -0
  82. package/lib/pr-monitor/shepherd-lease.js +252 -0
  83. package/lib/pr-monitor/watch-lifecycle.js +14 -2
  84. package/lib/pr-pull.js +98 -24
  85. package/lib/pr-shepherd.js +34 -8
  86. package/lib/preflight/gates.js +65 -18
  87. package/lib/preflight/runner.js +5 -0
  88. package/lib/project-memory.js +40 -0
  89. package/lib/protected-state-authority.js +305 -0
  90. package/lib/protected-state-surfaces.js +64 -44
  91. package/lib/release-readiness.js +51 -4
  92. package/lib/rules-sync.js +4 -0
  93. package/lib/runtime-health.js +15 -46
  94. package/lib/shell-utils.js +1 -1
  95. package/lib/skill-eval.js +750 -0
  96. package/lib/skills-sync.js +6 -3
  97. package/lib/smart-merge.js +28 -4
  98. package/lib/status/identity.js +46 -0
  99. package/lib/status/presenter.js +0 -35
  100. package/lib/status/snapshot.js +11 -16
  101. package/lib/symlink-utils.js +74 -26
  102. package/lib/upgrade-safety.js +47 -9
  103. package/lib/using-forge.js +328 -0
  104. package/lib/workflow/enforce-stage.js +5 -5
  105. package/lib/workflow/state-manager.js +23 -23
  106. package/package.json +6 -7
  107. package/rules/using-forge.md +24 -0
  108. package/scripts/doc-asserting-tests.js +158 -0
  109. package/scripts/forge-team/index.sh +0 -5
  110. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  111. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  112. package/scripts/lib/behavioral-eval-runner.js +310 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  114. package/scripts/lib/eval-evidence.js +328 -0
  115. package/scripts/lib/eval-runner.js +81 -41
  116. package/scripts/lib/immutable-eval-corpus.js +309 -0
  117. package/scripts/lib/promotion-evidence-loader.js +94 -0
  118. package/scripts/lib/promotion-scorecard.js +314 -0
  119. package/scripts/npm-release-receipt.js +134 -0
  120. package/scripts/process-tree.js +761 -0
  121. package/scripts/protected-state-check.js +47 -22
  122. package/scripts/run-command-eval.js +29 -1
  123. package/scripts/sync-d20-audit.js +172 -0
  124. package/scripts/test-full-suite.js +249 -37
  125. package/scripts/test.js +184 -44
  126. package/skills/claim-safety/SKILL.md +4 -0
  127. package/skills/claim-safety/evals/scorecard.json +41 -0
  128. package/skills/coverage.json +83 -0
  129. package/skills/dev/SKILL.md +4 -0
  130. package/skills/dev/evals/scorecard.json +41 -0
  131. package/skills/gates/SKILL.md +80 -0
  132. package/skills/gates/evals/evals.json +38 -0
  133. package/skills/gates/evals/scorecard.json +41 -0
  134. package/skills/hermes-forge/SKILL.md +1 -0
  135. package/skills/hermes-forge/evals/scorecard.json +41 -0
  136. package/skills/issue-basics/SKILL.md +1 -0
  137. package/skills/issue-basics/evals/scorecard.json +41 -0
  138. package/skills/kernel/SKILL.md +38 -0
  139. package/skills/kernel/evals/scorecard.json +41 -0
  140. package/skills/memory/SKILL.md +16 -1
  141. package/skills/memory/evals/scorecard.json +41 -0
  142. package/skills/parallel-deep-research/SKILL.md +1 -0
  143. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  144. package/skills/plan/SKILL.md +6 -0
  145. package/skills/plan/evals/scorecard.json +41 -0
  146. package/skills/portability/SKILL.md +47 -0
  147. package/skills/portability/evals/evals.json +34 -0
  148. package/skills/portability/evals/scorecard.json +41 -0
  149. package/skills/research/SKILL.md +1 -0
  150. package/skills/research/evals/scorecard.json +41 -0
  151. package/skills/review/SKILL.md +10 -11
  152. package/skills/review/evals/scorecard.json +41 -0
  153. package/skills/rollback/SKILL.md +5 -11
  154. package/skills/rollback/evals/scorecard.json +41 -0
  155. package/skills/setup/SKILL.md +91 -0
  156. package/skills/setup/evals/evals.json +42 -0
  157. package/skills/setup/evals/scorecard.json +41 -0
  158. package/skills/shepherd/SKILL.md +84 -38
  159. package/skills/shepherd/evals/evals.json +21 -9
  160. package/skills/shepherd/evals/scorecard.json +41 -0
  161. package/skills/ship/SKILL.md +10 -12
  162. package/skills/ship/evals/scorecard.json +41 -0
  163. package/skills/smith/SKILL.md +8 -0
  164. package/skills/smith/evals/scorecard.json +41 -0
  165. package/skills/sonarcloud/SKILL.md +1 -0
  166. package/skills/sonarcloud/evals/scorecard.json +41 -0
  167. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  168. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  169. package/skills/status/SKILL.md +3 -0
  170. package/skills/status/evals/scorecard.json +41 -0
  171. package/skills/triage-ready/SKILL.md +2 -0
  172. package/skills/triage-ready/evals/scorecard.json +41 -0
  173. package/skills/using-forge/SKILL.md +104 -0
  174. package/skills/using-forge/evals/scorecard.json +41 -0
  175. package/skills/validate/SKILL.md +4 -0
  176. package/skills/validate/evals/scorecard.json +41 -0
  177. package/skills/verify/SKILL.md +4 -0
  178. package/skills/verify/evals/scorecard.json +41 -0
  179. package/skills/worktree/SKILL.md +92 -0
  180. package/skills/worktree/evals/evals.json +38 -0
  181. package/skills/worktree/evals/scorecard.json +41 -0
  182. package/lib/adapters/beads-issue-adapter.js +0 -127
  183. package/lib/beads-nudge.js +0 -91
  184. package/lib/beads-setup.js +0 -538
  185. package/lib/beads-sync-scaffold.js +0 -189
  186. package/lib/commands/board.js +0 -64
  187. package/lib/pat-setup.js +0 -207
  188. package/lib/pr-monitor/render-sticky.js +0 -192
  189. package/lib/pr-monitor/upsert-sticky.js +0 -169
  190. package/lib/status/beads-snapshot.js +0 -145
  191. package/scripts/beads-context.sh +0 -577
  192. package/scripts/beads-migrate-to-dolt.sh +0 -7
  193. package/scripts/beads-upgrade-smoke.sh +0 -284
  194. package/scripts/forge-team/lib/dashboard.sh +0 -316
  195. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -1,169 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * PR-monitor sticky-comment upsert — race-safe, converges to EXACTLY ONE sticky
5
- * comment per PR even under a concurrent burst of workflow runs.
6
- *
7
- * Why this exists: the pr-monitor workflow deliberately has NO `concurrency:`
8
- * group. A per-PR group does not help — GitHub's queue replacement cancels the
9
- * previously-PENDING run in a group UNCONDITIONALLY (independent of
10
- * cancel-in-progress), and this workflow's triggers (check_suite:completed fires
11
- * ~10+ times per push, plus reviews/comments) burst hard, so a group left a trail
12
- * of CANCELLED runs that render as red/non-SUCCESS checks and tripped merge-gate
13
- * tooling (kernel issue 97e6a146). Dropping the group removes the cancellations,
14
- * but then two concurrent first-runs on a PR with no sticky yet would BOTH find
15
- * nothing and BOTH create one → duplicate sticky comments. This module closes
16
- * that race deterministically instead.
17
- *
18
- * Reconcile-to-one algorithm:
19
- * 1. List marker comments. If none, create one, then RE-LIST (a concurrent run
20
- * may have created its own in the same burst).
21
- * 2. Pick the deterministic survivor: the LOWEST comment id (oldest). Every
22
- * concurrent run picks the SAME survivor, so they never fight.
23
- * 3. Update the survivor with the latest body; delete every other marker
24
- * comment. Deletes are idempotent (a 404 means a peer already removed it).
25
- *
26
- * A create that lands AFTER a run's re-list is self-healed by the next event:
27
- * every run reconciles to one, and events keep arriving, so the PR converges to a
28
- * single sticky comment.
29
- *
30
- * @module pr-monitor/upsert-sticky
31
- */
32
-
33
- const { execFileSync } = require('node:child_process');
34
-
35
- /** Ids of comments whose body carries the sticky marker. */
36
- function markerCommentIds(comments, marker) {
37
- return (Array.isArray(comments) ? comments : [])
38
- .filter((comment) => typeof comment.body === 'string' && comment.body.includes(marker))
39
- .map((comment) => comment.id);
40
- }
41
-
42
- /** Ascending by numeric id, so the survivor (index 0) is the oldest comment. */
43
- function sortIdsAscending(ids) {
44
- return [...ids].sort((left, right) => Number(left) - Number(right));
45
- }
46
-
47
- /**
48
- * Drive a client to exactly one sticky comment. `client` abstracts the GitHub
49
- * calls so the reconcile logic is unit-testable without the network:
50
- * - list() → array of { id, body }
51
- * - create() → create a new sticky comment (body supplied by the client)
52
- * - update(id) → overwrite comment `id` with the latest body
53
- * - remove(id) → delete comment `id` (must tolerate an already-deleted 404)
54
- *
55
- * @returns {Promise<{ survivor: (number|string|null), deleted: Array<number|string> }>}
56
- */
57
- async function upsertStickyComment({ marker }, client) {
58
- let ids = markerCommentIds(await client.list(), marker);
59
-
60
- if (ids.length === 0) {
61
- await client.create();
62
- // Re-list: a concurrent run may have created its own sticky in this burst.
63
- ids = markerCommentIds(await client.list(), marker);
64
- }
65
-
66
- if (ids.length === 0) {
67
- // The just-created comment is not visible yet (eventual consistency); its
68
- // body is already correct, and the next event will reconcile if a peer raced.
69
- return { survivor: null, deleted: [] };
70
- }
71
-
72
- ids = sortIdsAscending(ids);
73
- const survivor = ids[0];
74
- await client.update(survivor);
75
-
76
- const deleted = [];
77
- for (const id of ids.slice(1)) {
78
- await client.remove(id);
79
- deleted.push(id);
80
- }
81
-
82
- return { survivor, deleted };
83
- }
84
-
85
- // Single choke point for the `gh` CLI, matching lib/commands/merge.js. `gh` is a
86
- // hardcoded literal (never user input) and args are an array (no shell), so the
87
- // S4036 PATH-search finding is a false positive in this developer-tool context;
88
- // one annotation here covers every call site. `encoding: 'utf8'` also pipes
89
- // stderr onto the thrown error, so isAlreadyGone() below can classify failures.
90
- function runGh(args) {
91
- return execFileSync('gh', args, { encoding: 'utf8' }); // NOSONAR S4036 - hardcoded CLI (gh), args array (no shell), developer-tool context
92
- }
93
-
94
- /**
95
- * True only when a `gh api` failure means the target comment is already absent
96
- * (HTTP 404 / 410) — the benign "a peer got there first" case. Auth failures,
97
- * rate limits (403), and every other error return false so they propagate and
98
- * surface a diagnostic instead of silently breaking the exactly-one invariant.
99
- */
100
- function isAlreadyGone(error) {
101
- const text = `${error && error.stderr ? error.stderr : ''} ${error && error.message ? error.message : ''}`;
102
- return /HTTP 404|HTTP 410|\bNot Found\b|\bGone\b/i.test(text);
103
- }
104
-
105
- /**
106
- * GitHub-backed client (shells to `gh api`, like lib/pr-monitor/gather.js). The
107
- * reconcile logic above is unit-tested; the error-tolerance in update/remove is
108
- * too, via an injectable `run`. create/update both send the same pre-rendered
109
- * payload file the render step wrote, so the body is identical.
110
- */
111
- function ghStickyClient({ repo, pr, payloadFile, run = runGh }) {
112
- return {
113
- async list() {
114
- const out = run(['api', `repos/${repo}/issues/${pr}/comments`, '--paginate']);
115
- return out && out.trim() ? JSON.parse(out) : [];
116
- },
117
- async create() {
118
- run(['api', '-X', 'POST', `repos/${repo}/issues/${pr}/comments`, '--input', payloadFile]);
119
- },
120
- async update(id) {
121
- try {
122
- run(['api', '-X', 'PATCH', `repos/${repo}/issues/comments/${id}`, '--input', payloadFile]);
123
- } catch (error) {
124
- if (!isAlreadyGone(error)) {
125
- throw error;
126
- }
127
- // Our chosen survivor was deleted by a peer whose survivor had a lower id:
128
- // the peer's sticky wins, exactly-one still holds, so we are done.
129
- }
130
- },
131
- async remove(id) {
132
- try {
133
- run(['api', '-X', 'DELETE', `repos/${repo}/issues/comments/${id}`]);
134
- } catch (error) {
135
- if (!isAlreadyGone(error)) {
136
- throw error;
137
- }
138
- // Already gone (a concurrent run deleted it first) — the goal state holds.
139
- }
140
- },
141
- };
142
- }
143
-
144
- async function main() {
145
- const repo = process.env.GH_REPO;
146
- const pr = process.env.PR;
147
- const payloadFile = process.env.STICKY_PAYLOAD_FILE || 'monitor-payload.json';
148
- const { STICKY_MARKER } = require('./render-sticky');
149
-
150
- const client = ghStickyClient({ repo, pr, payloadFile });
151
- const { survivor, deleted } = await upsertStickyComment({ marker: STICKY_MARKER }, client);
152
- const survivorLabel = survivor === null ? 'created (not yet visible)' : survivor;
153
- console.log(`Sticky comment reconciled to one: survivor=${survivorLabel}, deleted=${deleted.length}`);
154
- }
155
-
156
- if (require.main === module) {
157
- main().catch((error) => {
158
- console.error(error.message);
159
- process.exit(1);
160
- });
161
- }
162
-
163
- module.exports = {
164
- upsertStickyComment,
165
- markerCommentIds,
166
- sortIdsAscending,
167
- isAlreadyGone,
168
- ghStickyClient,
169
- };
@@ -1,145 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { secureExecFileSync } = require('../shell-utils.js');
6
-
7
- function getGitConfig(projectRoot, key) {
8
- try {
9
- return secureExecFileSync('git', ['config', key], {
10
- encoding: 'utf8',
11
- cwd: projectRoot,
12
- stdio: ['pipe', 'pipe', 'pipe'],
13
- }).trim();
14
- } catch (_error) {
15
- return '';
16
- }
17
- }
18
-
19
- function getDeveloperIdentity(projectRoot) {
20
- return {
21
- email: getGitConfig(projectRoot, 'user.email'),
22
- name: getGitConfig(projectRoot, 'user.name'),
23
- };
24
- }
25
-
26
- function normalizeEmail(value) {
27
- return typeof value === 'string' ? value.trim().toLowerCase() : '';
28
- }
29
-
30
- function readIssues(projectRoot) {
31
- const issuesPath = path.join(projectRoot, '.beads', 'issues.jsonl');
32
- if (!fs.existsSync(issuesPath)) {
33
- return [];
34
- }
35
-
36
- const raw = fs.readFileSync(issuesPath, 'utf8');
37
- const latestById = new Map();
38
-
39
- for (const line of raw.split(/\r?\n/)) {
40
- if (!line.trim()) {
41
- continue;
42
- }
43
-
44
- try {
45
- const issue = JSON.parse(line);
46
- if (issue && issue.id) {
47
- latestById.set(issue.id, issue);
48
- }
49
- } catch (_error) {
50
- // Ignore malformed rows so one bad append does not break status.
51
- }
52
- }
53
-
54
- return Array.from(latestById.values());
55
- }
56
-
57
- function isAssignedToDeveloper(issue, developer) {
58
- if (!issue || !developer) {
59
- return false;
60
- }
61
-
62
- if (issue.owner) {
63
- const developerEmail = normalizeEmail(developer.email);
64
- return developerEmail !== '' && normalizeEmail(issue.owner) === developerEmail;
65
- }
66
-
67
- return developer.name !== '' && issue.created_by === developer.name;
68
- }
69
-
70
- const DEFAULT_STALE_AFTER_DAYS = 14;
71
-
72
- function parseTimestampOrZero(value) {
73
- if (!value) {
74
- return 0;
75
- }
76
-
77
- const parsed = Date.parse(value);
78
- return Number.isNaN(parsed) ? 0 : parsed;
79
- }
80
-
81
- function sortByUpdatedAtDesc(left, right) {
82
- const leftTime = parseTimestampOrZero(left.updated_at);
83
- const rightTime = parseTimestampOrZero(right.updated_at);
84
- return rightTime - leftTime;
85
- }
86
-
87
- function hasUnresolvedDependencies(issue) {
88
- return Number(issue.dependency_count || 0) > 0;
89
- }
90
-
91
- function isOpenIssue(issue) {
92
- return issue.status === 'open' || issue.status === 'in_progress';
93
- }
94
-
95
- function isStale(issue, now, staleAfterDays) {
96
- if (!isOpenIssue(issue)) {
97
- return false;
98
- }
99
-
100
- const updatedAt = parseTimestampOrZero(issue.updated_at);
101
- if (updatedAt === 0) {
102
- return false;
103
- }
104
-
105
- const staleMs = staleAfterDays * 24 * 60 * 60 * 1000;
106
- return now.getTime() - updatedAt >= staleMs;
107
- }
108
-
109
- function readBeadsSnapshot(projectRoot, options = {}) {
110
- const developer = getDeveloperIdentity(projectRoot);
111
- const issues = readIssues(projectRoot);
112
- const parsedNow = options.now instanceof Date
113
- ? options.now
114
- : (typeof options.now === 'string' ? new Date(options.now) : null);
115
- const now = parsedNow instanceof Date && !Number.isNaN(parsedNow.getTime())
116
- ? parsedNow
117
- : new Date();
118
- const parsedStaleAfterDays = Number(options.staleAfterDays);
119
- const staleAfterDays = Number.isFinite(parsedStaleAfterDays)
120
- ? parsedStaleAfterDays
121
- : DEFAULT_STALE_AFTER_DAYS;
122
-
123
- return {
124
- developer,
125
- issues,
126
- active: issues.filter(issue => issue.status === 'in_progress').sort(sortByUpdatedAtDesc),
127
- activeAssigned: issues.filter(issue => issue.status === 'in_progress' && isAssignedToDeveloper(issue, developer)),
128
- ready: issues.filter(issue => issue.status === 'open' && !hasUnresolvedDependencies(issue)),
129
- blocked: issues.filter(issue => isOpenIssue(issue) && hasUnresolvedDependencies(issue)).sort(sortByUpdatedAtDesc),
130
- stale: issues.filter(issue => isStale(issue, now, staleAfterDays)).sort(sortByUpdatedAtDesc),
131
- recentCompleted: issues.filter(issue => issue.status === 'closed').sort(sortByUpdatedAtDesc),
132
- limits: [
133
- 'Uses local Beads issues.jsonl only.',
134
- 'Does not read GitHub review, CI, project, or sync freshness state.',
135
- ],
136
- };
137
- }
138
-
139
- module.exports = {
140
- getDeveloperIdentity,
141
- isAssignedToDeveloper,
142
- readBeadsSnapshot,
143
- hasUnresolvedDependencies,
144
- isStale,
145
- };