forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-shepherd digest — the thin CONSUMER half of the constant monitor (epic
5
+ * c2d398e5, 33e1bbd3). The constant watch loop is the PRODUCER: it writes the
6
+ * per-PR NDJSON journals under `.forge/pr-monitor/<repo>-<pr>/` (the `forge
7
+ * shepherd events` pull surface reads those same records back). Nothing, though,
8
+ * surfaced those events to a working agent. This module is a pure READER: it
9
+ * reads the NEW budget events across all PR journals since a persisted per-PR
10
+ * CONSUMER cursor, renders a COMPACT capped summary, and advances the cursor —
11
+ * the exact payload a harness hook (Claude UserPromptSubmit) injects each turn.
12
+ *
13
+ * The CORE (events/journal/watch/monitor) is untouched: this only READS the
14
+ * journal via `journal.readEventsSince` and keeps its OWN `consumer.cursor`
15
+ * (distinct from the watcher's snapshot), so consumption never disturbs
16
+ * production. Every function is fail-open — a bad journal degrades to skipped,
17
+ * never throws — because it feeds a hook that must never block a prompt.
18
+ *
19
+ * @module pr-monitor/digest
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const journalMod = require('./journal');
26
+
27
+ /**
28
+ * The event types worth surfacing on each turn — the ACTIONABLE transitions
29
+ * (verdict flip, a failed check, a new review thread, terminal merge/close).
30
+ * Everything else (head pushes, green checks, degraded notices) stays in the
31
+ * journal for `forge shepherd events` but is NOT pushed, to keep the injected
32
+ * context tiny.
33
+ */
34
+ const BUDGET_TYPES = Object.freeze(new Set([
35
+ 'verdict.changed', 'check.failed', 'thread.opened', 'pr.merged', 'pr.closed',
36
+ ]));
37
+
38
+ const DEFAULT_CAP = 8;
39
+ const CONSUMER_CURSOR_FILE = 'consumer.cursor';
40
+
41
+ /** Absolute `.forge/pr-monitor` root for a project. */
42
+ function monitorRoot(root) {
43
+ return path.join(root, '.forge', 'pr-monitor');
44
+ }
45
+
46
+ /** The per-PR consumer cursor path (distinct from the watcher's snapshot/pid). */
47
+ function cursorPath(dir) {
48
+ return path.join(dir, CONSUMER_CURSOR_FILE);
49
+ }
50
+
51
+ /**
52
+ * List absolute PR journal dirs (those containing an events.ndjson). Fail-open:
53
+ * a missing monitor root or unreadable dir yields []. Injectable fs for tests.
54
+ *
55
+ * @param {string} root
56
+ * @param {{ readdirSync?: Function, existsSync?: Function }} [deps]
57
+ * @returns {string[]}
58
+ */
59
+ function discoverPrDirs(root, deps = {}) {
60
+ const readdir = deps.readdirSync || fs.readdirSync;
61
+ const exists = deps.existsSync || fs.existsSync;
62
+ try {
63
+ return readdir(monitorRoot(root), { withFileTypes: true })
64
+ .filter((e) => e.isDirectory())
65
+ .map((e) => path.join(monitorRoot(root), e.name))
66
+ .filter((dir) => exists(path.join(dir, 'events.ndjson')));
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Read a PR's consumer cursor (the last consumed seq). Fail-open → 0 (no cursor,
74
+ * unreadable, or malformed all mean "start from the beginning").
75
+ *
76
+ * @param {string} dir
77
+ * @param {{ readFileSync?: Function }} [deps]
78
+ * @returns {number}
79
+ */
80
+ function readConsumerCursor(dir, deps = {}) {
81
+ const readFile = deps.readFileSync || fs.readFileSync;
82
+ try {
83
+ const obj = JSON.parse(readFile(cursorPath(dir), 'utf8'));
84
+ const seq = Number(obj && obj.seq);
85
+ return Number.isFinite(seq) && seq >= 0 ? seq : 0;
86
+ } catch {
87
+ return 0;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Persist a PR's consumer cursor. Fail-open: an unwritable cursor returns false
93
+ * (next turn re-reads the same events — a duplicate nudge, never a crash).
94
+ *
95
+ * @param {string} dir
96
+ * @param {number} seq
97
+ * @param {{ writeFileSync?: Function }} [deps]
98
+ * @returns {boolean}
99
+ */
100
+ function writeConsumerCursor(dir, seq, deps = {}) {
101
+ const writeFile = deps.writeFileSync || fs.writeFileSync;
102
+ try {
103
+ writeFile(cursorPath(dir), `${JSON.stringify({ seq: Number(seq) || 0 })}\n`);
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A compact one-line label for a budget event. PURE. Bounded so injected context
112
+ * stays tiny; the full record is always available via `forge shepherd events`.
113
+ *
114
+ * @param {object} e - a journal event.
115
+ * @returns {string}
116
+ */
117
+ function renderEventLine(e) {
118
+ const pr = e.pr != null ? `#${e.pr}` : '#?';
119
+ const d = e.data || {};
120
+ let detail;
121
+ switch (e.type) {
122
+ case 'verdict.changed': detail = (e.verdict && (e.verdict.verdict || e.verdict.state)) || d.verdict || d.to || ''; break;
123
+ case 'check.failed': detail = d.name || d.check || ''; break;
124
+ case 'thread.opened': detail = d.author ? `by ${d.author}` : (d.path || ''); break;
125
+ case 'pr.merged': detail = 'merged'; break;
126
+ case 'pr.closed': detail = 'closed'; break;
127
+ default: detail = '';
128
+ }
129
+ const suffix = detail ? `: ${String(detail).slice(0, 60)}` : '';
130
+ return `- PR ${pr} ${e.type}${suffix}`;
131
+ }
132
+
133
+ /**
134
+ * PURE: filter events to the budget types, cap the count, and render lines.
135
+ *
136
+ * @param {object[]} events
137
+ * @param {{ cap?: number }} [opts]
138
+ * @returns {{ lines: string[], total: number }}
139
+ */
140
+ function renderDigestLines(events, { cap = DEFAULT_CAP } = {}) {
141
+ const budget = (Array.isArray(events) ? events : []).filter((e) => e && BUDGET_TYPES.has(e.type));
142
+ const lines = budget.map(renderEventLine);
143
+ return { lines: lines.slice(0, cap), total: lines.length };
144
+ }
145
+
146
+ /**
147
+ * Format the compact injected block (a header + capped lines + an overflow
148
+ * pointer), or '' when there is nothing to surface. PURE.
149
+ */
150
+ function formatBlock(lines, total, cap, prs) {
151
+ if (lines.length === 0) return '';
152
+ const on = prs.length ? ` on PR(s) ${prs.join(', ')}` : '';
153
+ const more = total > cap ? `\n(+${total - cap} more — see \`forge shepherd events <pr> --since <seq>\`)` : '';
154
+ return `[forge PR shepherd] ${total} new event(s)${on}:\n${lines.join('\n')}${more}`;
155
+ }
156
+
157
+ /**
158
+ * Collect a compact digest of NEW budget events across every PR journal since the
159
+ * per-PR consumer cursor, advancing each cursor past EVERYTHING read (budget and
160
+ * non-budget alike, so skipped types never re-surface). Fail-open throughout.
161
+ *
162
+ * @param {object} args
163
+ * @param {string} args.root - project root.
164
+ * @param {object} [args.journal] - journal module (test injection).
165
+ * @param {number} [args.cap] - max lines in the block.
166
+ * @param {object} [args.fsDeps] - injectable fs for discovery + cursor I/O.
167
+ * @returns {{ text: string, total: number, prs: string[] }}
168
+ */
169
+ function collectDigest({ root, journal = journalMod, cap = DEFAULT_CAP, fsDeps = {} } = {}) {
170
+ const dirs = discoverPrDirs(root, fsDeps);
171
+ const allLines = [];
172
+ const prs = new Set();
173
+ for (const dir of dirs) {
174
+ const cursor = readConsumerCursor(dir, fsDeps);
175
+ let evs;
176
+ try {
177
+ evs = journal.readEventsSince(dir, cursor);
178
+ } catch {
179
+ evs = [];
180
+ }
181
+ if (!Array.isArray(evs) || evs.length === 0) continue;
182
+ const maxSeq = evs.reduce((m, e) => Math.max(m, Number(e.seq) || 0), cursor);
183
+ for (const e of evs) {
184
+ if (!e || !BUDGET_TYPES.has(e.type)) continue;
185
+ allLines.push(renderEventLine(e));
186
+ if (e.pr != null) prs.add(String(e.pr));
187
+ }
188
+ writeConsumerCursor(dir, maxSeq, fsDeps);
189
+ }
190
+ const capped = allLines.slice(0, cap);
191
+ return { text: formatBlock(capped, allLines.length, cap, [...prs]), total: allLines.length, prs: [...prs] };
192
+ }
193
+
194
+ module.exports = {
195
+ BUDGET_TYPES,
196
+ DEFAULT_CAP,
197
+ monitorRoot,
198
+ cursorPath,
199
+ discoverPrDirs,
200
+ readConsumerCursor,
201
+ writeConsumerCursor,
202
+ renderEventLine,
203
+ renderDigestLines,
204
+ formatBlock,
205
+ collectDigest,
206
+ };