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,60 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ // Single-source, side-effect-free Beads-store detector (kernel issue a5399f3d).
7
+ //
8
+ // Both upgrade-safety surfaces reuse this ONE definition: the issue-path
9
+ // unmigrated-beads nudge (lib/commands/_issue.js) and the `forge upgrade`
10
+ // advisory (lib/upgrade-safety.js). It deliberately lives in a neutral module,
11
+ // NOT in lib/commands/migrate.js — re-exporting a `detectBeadsJsonlSource` name
12
+ // from the migrate command would revive the identifier the a7e1443c
13
+ // implicit-auto-migrate tombstone pins gone (test/commands/runtime-no-auto-migrate).
14
+ // This detector only READS the filesystem and never triggers a migration, so it
15
+ // honors that tombstone's spirit while giving both nudges a shared source.
16
+
17
+ function dirHasJsonl(dir) {
18
+ try {
19
+ return fs.readdirSync(dir).some(entry => entry.endsWith('.jsonl'));
20
+ } catch (_err) { // NOSONAR S2486 — line-scoped; must sit on the catch line
21
+ /* intentional: an unreadable directory has no usable jsonl */
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Return the absolute path of the directory holding a returning user's Beads
28
+ * JSONL under `projectRoot`, or null when none is found. Checks the top-level
29
+ * `.beads/` first, then the split-store `.beads/backup/` layout (jsonl there and
30
+ * nowhere else — a layout the migrator itself reads), so neither surface misses
31
+ * it. Never throws.
32
+ *
33
+ * @param {string} [projectRoot]
34
+ * @returns {string|null}
35
+ */
36
+ function detectBeadsJsonlSource(projectRoot) {
37
+ const root = projectRoot || process.cwd();
38
+ const beadsDir = path.join(root, '.beads');
39
+ try {
40
+ if (!fs.existsSync(beadsDir)) {
41
+ return null;
42
+ }
43
+ if (dirHasJsonl(beadsDir)) {
44
+ return beadsDir;
45
+ }
46
+ const backupDir = path.join(beadsDir, 'backup');
47
+ if (fs.existsSync(backupDir) && dirHasJsonl(backupDir)) {
48
+ return backupDir;
49
+ }
50
+ return null;
51
+ } catch (_err) { // NOSONAR S2486 — line-scoped; must sit on the catch line
52
+ /* intentional: an unreadable project root has no detectable beads source */
53
+ return null;
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ detectBeadsJsonlSource,
59
+ dirHasJsonl,
60
+ };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Command Aliases — declarative back-compat alias map.
3
+ *
4
+ * Generalises the former hardcoded `ISSUE_ALIAS_COMMANDS` allowlist (bin/forge.js)
5
+ * into a single declarative source of truth for command-surface unification
6
+ * (kernel issue 33d1a906, epic eea186fa). Each entry maps a bare top-level alias
7
+ * to the canonical `<noun> <sub>` form it stands in for, plus two flags:
8
+ *
9
+ * { canonical: 'issue create', visible: false, deprecated?: true }
10
+ *
11
+ * - `canonical` — the canonical noun+subcommand this alias resolves to.
12
+ * - `visible` — true → routable AND shown in `forge --help`;
13
+ * false → routable but hidden from help (back-compat only).
14
+ * - `deprecated` — optional; when true, using the alias emits a one-line hint
15
+ * to STDERR, but ONLY when `FORGE_DEPRECATION_WARNINGS` is set.
16
+ * Aliases are NEVER removed or broken (docker still ships
17
+ * `docker pull`).
18
+ *
19
+ * P0 SCOPE: seeds ONLY the existing issue aliases migrated verbatim from
20
+ * `ISSUE_ALIAS_COMMANDS` (all hidden, none deprecated) so behaviour is identical.
21
+ * No new noun mappings are added here — those land in later phases. The bare
22
+ * files (create.js, update.js, …) still exist and remain the routed handlers, so
23
+ * `resolveDispatch` is a strict no-op for the seed set (its first clause skips any
24
+ * name that is still a registered command).
25
+ *
26
+ * Files starting with `_` are excluded from command auto-discovery (see
27
+ * `_registry.js`), so this module is never mistaken for a command itself.
28
+ *
29
+ * @module _aliases
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} AliasDescriptor
34
+ * @property {string} canonical - Canonical `<noun> <sub>` form the alias resolves to.
35
+ * @property {boolean} visible - Whether the alias appears in `forge --help`.
36
+ * @property {boolean} [deprecated] - Whether using the alias emits an opt-in hint.
37
+ */
38
+
39
+ /** @type {Object<string, AliasDescriptor>} */
40
+ const ALIASES = {
41
+ create: { canonical: 'issue create', visible: false },
42
+ update: { canonical: 'issue update', visible: false },
43
+ claim: { canonical: 'issue claim', visible: false },
44
+ close: { canonical: 'issue close', visible: false },
45
+ show: { canonical: 'issue show', visible: false },
46
+ list: { canonical: 'issue list', visible: false },
47
+ ready: { canonical: 'issue ready', visible: false },
48
+ blocked: { canonical: 'issue blocked', visible: false },
49
+ stale: { canonical: 'issue stale', visible: false },
50
+ orphans: { canonical: 'issue orphans', visible: false },
51
+ lint: { canonical: 'issue lint', visible: false },
52
+ claims: { canonical: 'issue claims', visible: false },
53
+ // `issues` is the plural convenience form of `issue list`.
54
+ issues: { canonical: 'issue list', visible: false },
55
+
56
+ // P1 (kernel issue febf7690) — memory noun shortcuts. The `memory` noun
57
+ // (add/recall/search/insights) shipped in PR #392 (issue 25362344); P1 wires the
58
+ // bare verbs as VISIBLE back-compat shortcuts of the canonical `memory <sub>`
59
+ // form. The canonical WRITE verb is `memory add` (there is NO `save`). These stay
60
+ // registered command files (remember.js/recall.js/insights.js), so resolveDispatch
61
+ // is a strict no-op for them and bare-verb dispatch stays byte-identical; the
62
+ // entries exist to document the mapping and drive the `forge --help` Shortcuts
63
+ // block. They are deliberately NOT issue-backend flag-passthrough aliases (see
64
+ // passthroughAliasNames) so global flags (`-p`, `--help`, `--all`) still parse
65
+ // exactly as they did for the standalone commands.
66
+ remember: { canonical: 'memory add', visible: true },
67
+ recall: { canonical: 'memory recall', visible: true },
68
+ insights: { canonical: 'memory insights', visible: true },
69
+
70
+ // P2 (kernel issue 6ab3f30c) — the `pr` noun (ship/preflight/shepherd/merge) plus
71
+ // folding doc-gate under the existing `gate` noun. `pr ship` is the canonical
72
+ // PR-creation form, but bare `ship` stays a VISIBLE shortcut (hot workflow verb,
73
+ // zero keystroke loss); `preflight` is likewise VISIBLE. shepherd/merge/doc-gate
74
+ // are less-hot, so they stay HIDDEN back-compat aliases. Every one keeps its own
75
+ // registered command file (ship.js/preflight.js/shepherd.js/merge.js/doc-gate.js),
76
+ // so resolveDispatch is a strict no-op for them and bare-verb dispatch stays
77
+ // byte-identical — the entries document the mapping and drive the `forge --help`
78
+ // Shortcuts block. Their canonical is a `pr `/`gate ` form (NOT `issue `), so
79
+ // passthroughAliasNames() excludes them and their global flags (`--pull`,
80
+ // `--json`, `--bundle`, `--all`, `-h`) parse exactly as the standalone commands'.
81
+ ship: { canonical: 'pr ship', visible: true },
82
+ preflight: { canonical: 'pr preflight', visible: true },
83
+ shepherd: { canonical: 'pr shepherd', visible: false },
84
+ merge: { canonical: 'pr merge', visible: false },
85
+ 'doc-gate': { canonical: 'gate doc', visible: false },
86
+ };
87
+
88
+ /**
89
+ * Look up an alias descriptor by bare name.
90
+ * @param {string} name
91
+ * @returns {AliasDescriptor|undefined}
92
+ */
93
+ function resolveAlias(name) {
94
+ return Object.prototype.hasOwnProperty.call(ALIASES, name) ? ALIASES[name] : undefined;
95
+ }
96
+
97
+ /**
98
+ * @param {string} name
99
+ * @returns {boolean} true if `name` is a registered alias.
100
+ */
101
+ function isAlias(name) {
102
+ return resolveAlias(name) !== undefined;
103
+ }
104
+
105
+ /**
106
+ * All alias names. Replaces the flat `ISSUE_ALIAS_COMMANDS` array — used to skip
107
+ * global flag parsing so passthrough flags reach the handler intact.
108
+ * @returns {string[]}
109
+ */
110
+ function aliasNames() {
111
+ return Object.keys(ALIASES);
112
+ }
113
+
114
+ /**
115
+ * The subset of aliases that delegate ALL flag parsing to the issue backend
116
+ * and therefore must SKIP global flag parsing in bin/forge.js so flags like
117
+ * `--type` / `-p` / `--help` reach the handler intact. This is issue-specific:
118
+ * only issue-canonical aliases passthrough. Non-issue aliases (e.g. the P1 memory
119
+ * shortcuts) parse global flags normally — exactly as their standalone command
120
+ * files did — so their behaviour stays byte-identical after becoming aliases.
121
+ * @returns {string[]}
122
+ */
123
+ function passthroughAliasNames() {
124
+ return Object.keys(ALIASES).filter(name => ALIASES[name].canonical.startsWith('issue '));
125
+ }
126
+
127
+ /**
128
+ * Names of aliases shown in the `forge --help` Shortcuts block — visible
129
+ * back-compat shortcuts for a canonical `<noun> <sub>` form. Hidden (back-compat
130
+ * only) aliases are excluded.
131
+ * @returns {string[]}
132
+ */
133
+ function visibleAliasNames() {
134
+ return Object.keys(ALIASES).filter(name => ALIASES[name].visible === true);
135
+ }
136
+
137
+ /**
138
+ * Whether a descriptor is hidden from help. Pure predicate over the `visible`
139
+ * flag so the visible-vs-hidden distinction can be tested independent of the
140
+ * P0 seed (which is entirely hidden).
141
+ * @param {AliasDescriptor|undefined} descriptor
142
+ * @returns {boolean}
143
+ */
144
+ function isHidden(descriptor) {
145
+ return !!descriptor && descriptor.visible === false;
146
+ }
147
+
148
+ /**
149
+ * Whether the named alias is hidden from `forge --help`. Drives the help filter.
150
+ * @param {string} name
151
+ * @returns {boolean}
152
+ */
153
+ function isHiddenAlias(name) {
154
+ return isHidden(resolveAlias(name));
155
+ }
156
+
157
+ /**
158
+ * Whether the named alias is a visible (help-listed) shortcut.
159
+ * @param {string} name
160
+ * @returns {boolean}
161
+ */
162
+ function isVisibleAlias(name) {
163
+ const descriptor = resolveAlias(name);
164
+ return !!descriptor && descriptor.visible === true;
165
+ }
166
+
167
+ /**
168
+ * Render the one-line deprecation hint for an alias.
169
+ * @param {string} name
170
+ * @param {AliasDescriptor} descriptor
171
+ * @returns {string}
172
+ */
173
+ function renderHint(name, descriptor) {
174
+ return `forge ${name} is a back-compat alias; prefer 'forge ${descriptor.canonical}'`;
175
+ }
176
+
177
+ /**
178
+ * Whether a deprecation hint should be emitted: requires BOTH the opt-in env flag
179
+ * AND a descriptor explicitly marked deprecated. Default (flag unset) is silent,
180
+ * so scripted stdout is never affected.
181
+ * @param {AliasDescriptor|undefined} descriptor
182
+ * @param {Object} env
183
+ * @returns {boolean}
184
+ */
185
+ function shouldWarn(descriptor, env) {
186
+ return !!(env && env.FORGE_DEPRECATION_WARNINGS && descriptor && descriptor.deprecated);
187
+ }
188
+
189
+ /**
190
+ * Emit an opt-in deprecation hint to stderr when both gates pass. Never writes to
191
+ * stdout (would corrupt `--json`). Returns whether a hint was emitted.
192
+ * @param {string} name
193
+ * @param {{env?: Object, stderr?: {write: function}, resolve?: function}} [opts]
194
+ * @returns {boolean}
195
+ */
196
+ function maybeWarnDeprecation(name, opts = {}) {
197
+ const env = opts.env || process.env;
198
+ const stderr = opts.stderr || process.stderr;
199
+ const resolve = opts.resolve || resolveAlias;
200
+ const descriptor = resolve(name);
201
+ if (!shouldWarn(descriptor, env)) return false;
202
+ stderr.write(`${renderHint(name, descriptor)}\n`);
203
+ return true;
204
+ }
205
+
206
+ /**
207
+ * Resolve a bare command to its canonical noun handler for dispatch.
208
+ *
209
+ * A command that is still a registered command file (`isRegistered(command)` is
210
+ * true) is NEVER rewritten — dispatch stays byte-identical. Only a bare alias
211
+ * whose name is NOT a registered command resolves to `<noun> <sub>`; this
212
+ * activates when a later phase folds a bare verb into a noun handler. For the P0
213
+ * seed every alias is still registered, so this always returns `redirected:false`.
214
+ *
215
+ * @param {string} command - The bare command name (args[0]).
216
+ * @param {string[]} argv - The full argv (argv[0] is the command token).
217
+ * @param {function(string): boolean} isRegistered - Predicate: is this a live command?
218
+ * @returns {{command: string, args: string[], redirected: boolean}}
219
+ */
220
+ function resolveDispatch(command, argv, isRegistered) {
221
+ if (typeof isRegistered === 'function' && isRegistered(command)) {
222
+ return { command, args: argv, redirected: false };
223
+ }
224
+ const descriptor = resolveAlias(command);
225
+ if (!descriptor) {
226
+ return { command, args: argv, redirected: false };
227
+ }
228
+ const parts = String(descriptor.canonical).trim().split(/\s+/);
229
+ const noun = parts[0];
230
+ const rest = Array.isArray(argv) ? argv.slice(1) : [];
231
+ return { command: noun, args: [noun, ...parts.slice(1), ...rest], redirected: true };
232
+ }
233
+
234
+ module.exports = {
235
+ ALIASES,
236
+ resolveAlias,
237
+ isAlias,
238
+ aliasNames,
239
+ passthroughAliasNames,
240
+ visibleAliasNames,
241
+ isHidden,
242
+ isHiddenAlias,
243
+ isVisibleAlias,
244
+ renderHint,
245
+ shouldWarn,
246
+ maybeWarnDeprecation,
247
+ resolveDispatch,
248
+ };
@@ -12,13 +12,14 @@ const {
12
12
  const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
13
13
  const { renderIssueEnvelope } = require('../issue-render');
14
14
  const { recordStageTransition } = require('../workflow/stage-transition');
15
+ const { checkReadFirst } = require('../grounding/read-first');
16
+ const { recordContextLoaded } = require('../grounding/context-events');
15
17
 
16
18
  // The Forge issue command surface. Each subcommand routes through the shared
17
- // runIssueOperation, which selects the active backend (Kernel by --kernel /
18
- // --issue-backend kernel / FORGE_ISSUE_BACKEND=kernel; Beads otherwise) and performs
19
- // any backend-specific argument translation. This module therefore carries NO direct
20
- // issue-tracker invocation or argv translation — those live in the backend
21
- // abstraction (lib/forge-issues.js + the issue adapters).
19
+ // runIssueOperation, which dispatches to the Forge Kernel the only issue backend.
20
+ // This module therefore carries NO direct issue-tracker invocation or argv
21
+ // translation those live in the backend abstraction (lib/forge-issues.js + the
22
+ // kernel issue adapter).
22
23
  const SUBCOMMANDS = {
23
24
  create: {
24
25
  description: 'Create an issue via Forge',
@@ -64,10 +65,8 @@ const SUBCOMMANDS = {
64
65
  description: 'Show issue statistics via Forge',
65
66
  usage: 'forge issue stats [flags]',
66
67
  },
67
- // KAP-7: derived read queries. The backend abstraction maps each to its tracker
68
- // equivalent (the Kernel passes the operation name through unchanged; the Beads
69
- // backend maps each to its passthrough subcommand). They are READS, so they are
70
- // intentionally NOT in WRITE_SUBCOMMANDS.
68
+ // KAP-7: derived read queries. The Kernel passes the operation name through
69
+ // unchanged. They are READS, so they are intentionally NOT in WRITE_SUBCOMMANDS.
71
70
  blocked: {
72
71
  description: 'Show blocked issues via Forge',
73
72
  usage: 'forge issue blocked [flags]',
@@ -103,9 +102,7 @@ const SUBCOMMANDS = {
103
102
  usage: 'forge issue owns <id> [--json]',
104
103
  },
105
104
  // Active-lease listing (kernel issue 7dc229d4). A bare passthrough to the Kernel
106
- // lease table (kernel_claims); Beads has no lease table to enumerate, so
107
- // forge-issues.js rejects the Beads path explicitly. A READ, so intentionally NOT
108
- // in WRITE_SUBCOMMANDS.
105
+ // lease table (kernel_claims). A READ, so intentionally NOT in WRITE_SUBCOMMANDS.
109
106
  claims: {
110
107
  description: 'Show active issue leases (claims) via Forge',
111
108
  usage: 'forge issue claims [--json]',
@@ -386,8 +383,7 @@ function formatIssueHelp() {
386
383
  }
387
384
 
388
385
  // Map a CLI subcommand to the backend operation name. `dep` fans out to
389
- // `dep.<action>`; every other subcommand uses its own name (the backend performs
390
- // any tracker-specific translation, e.g. Beads claim -> `update <id> --claim`).
386
+ // `dep.<action>`; every other subcommand uses its own name.
391
387
  function resolveIssueOperation(subcommand, args) {
392
388
  if (subcommand === 'dep') {
393
389
  return `dep.${normalizeArgs(args)[0]}`;
@@ -397,10 +393,8 @@ function resolveIssueOperation(subcommand, args) {
397
393
 
398
394
  // The Kernel create payload (buildCreatePayload) reads only the --title flag, so a
399
395
  // bare leading positional (`forge create "title"`) would be ignored and the title
400
- // would default to the minted UUID. For parity on the KERNEL PATH ONLY, translate a
401
- // single leading bare positional into `--title <value>` when no explicit
402
- // --title/--title= is present. The Beads backend keeps its native positional
403
- // handling (this never runs for the Beads path).
396
+ // would default to the minted UUID. Translate a single leading bare positional into
397
+ // `--title <value>` when no explicit --title/--title= is present.
404
398
  function withKernelCreateTitle(args) {
405
399
  const hasTitle = args.some(
406
400
  arg => arg === '--title' || (typeof arg === 'string' && arg.startsWith('--title=')),
@@ -451,10 +445,12 @@ function validateDepArgs(args) {
451
445
  return null;
452
446
  }
453
447
 
454
- // Resolve the active issue backend (kernel|beads) and thread it into opts so the
455
- // shared runIssueOperation deps see it. OPT-IN ONLY: opts is left byte-identical
456
- // when no explicit signal is present (env/config/explicit), preserving the Beads
457
- // default path. A copy is returned — the caller's opts object is never mutated.
448
+ // Normalize any explicit issue-backend signal and thread the resolved value into
449
+ // opts so the shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
450
+ // byte-identical when no explicit signal is present (env/config/explicit), so the
451
+ // no-signal path stays untouched. A copy is returned — the caller's opts object is
452
+ // never mutated. A retired value (`beads`) warns here with the migrate pointer and
453
+ // resolves to the kernel.
458
454
  function withResolvedIssueBackend(projectRoot, opts = {}) {
459
455
  const env = opts.env || process.env;
460
456
  const signalContext = { deps: opts, env, projectRoot };
@@ -476,13 +472,13 @@ function withResolvedIssueBackend(projectRoot, opts = {}) {
476
472
 
477
473
  // The Kernel broker returns the issue-command contract shape
478
474
  // ({ ok, schema_version, command, data, next_commands } or { ok:false, error })
479
- // rather than the Beads-style { success, output }. The bin/forge.js result printer
475
+ // rather than the legacy { success, output }. The bin/forge.js result printer
480
476
  // keys on `success`/`output`, so a raw kernel contract would render as
481
477
  // "Command failed". Normalize ONLY the contract shape (ok defined, success
482
478
  // undefined) into { success, output } here, at the command boundary — the kernel
483
479
  // contract itself stays untouched. Every other result passes through byte-identical.
484
480
  //
485
- // Response-contract parity (the Beads behavior the Kernel replaced):
481
+ // Response-contract guarantees:
486
482
  // * SUCCESS → the printed envelope carries `ok:true` (consumers gate on it).
487
483
  // * FAILURE → the contract `exit_code` is surfaced as `result.exitCode` so the bin
488
484
  // printer exits with the error class's code (not always 1); and on
@@ -606,8 +602,7 @@ function splitLeadingIds(args = []) {
606
602
  // which broke envelope parity for multi-id close. `ok` is true only when every id
607
603
  // closed; per-id outcomes live in `data.results` and the contract `exit_code` of the
608
604
  // first failure is surfaced as `exitCode` so the bin printer exits with the error
609
- // class's code. KERNEL PATH ONLY: the Beads passthrough keeps its single
610
- // `close id1 id2 ...` invocation.
605
+ // class's code.
611
606
  async function runKernelBatchClose(runner, operation, ids, flags, projectRoot, opts, verifyEnabled = false) {
612
607
  const results = [];
613
608
  let allSucceeded = true;
@@ -697,13 +692,11 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
697
692
  return { success: false, error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
698
693
  }
699
694
 
700
- // Backend-agnostic --help short-circuit: print the subcommand's usage and return
701
- // BEFORE resolving the backend or dispatching any operation. Without this, a help
702
- // request was forwarded to the active backend as an operation arg absorbed
703
- // harmlessly by Beads (which swallowed --help), but broken under the Kernel default:
704
- // the plural path failed with a bare "Command failed" and the singular path
705
- // SILENTLY minted a junk issue (and could queue a GitHub projection). Help must
706
- // never touch a backend.
695
+ // --help short-circuit: print the subcommand's usage and return BEFORE resolving
696
+ // the backend or dispatching any operation. Without this, a help request is
697
+ // forwarded to the Kernel as an operation arg: the plural path fails with a bare
698
+ // "Command failed" and the singular path SILENTLY mints a junk issue (and could
699
+ // queue a GitHub projection). Help must never touch a backend.
707
700
  if (normalizeArgs(args).some(arg => arg === '--help' || arg === '-h')) {
708
701
  return { success: true, output: `${spec.usage}\n\n${spec.description}` };
709
702
  }
@@ -731,17 +724,31 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
731
724
 
732
725
  const opts = withResolvedIssueBackend(projectRoot, rawOpts);
733
726
 
734
- // Both backends are reached through the same runIssueOperation seam. Naming the
735
- // injected local `runIssueOperation` keeps the dispatch a literal call to a binding
736
- // named `runIssueOperation` (the kernel-evidence gate is syntactic) while still
737
- // honoring an injected runner; the `kernelBroker: opts.kernelBroker` passthrough is
738
- // a runtime no-op (undefined under Beads) that documents the Kernel-capable surface.
727
+ // gate.read_first (grounding, epic 6ef96e92): HARD-BLOCK `forge claim <id>`
728
+ // until the issue has been read this session/window (a `context.loaded` kernel
729
+ // event exists appended by `forge recap`/`forge show`). Fail-closed: the
730
+ // cheapest path through the gate is the correct behavior, since the remedy
731
+ // (`forge recap <id>`) also injects the context. Consulted here at the command
732
+ // boundary exactly like gate.issue_verify; rail.grounding/gate.read_first
733
+ // disabled -> allow (logged). Read commands never gate; only `claim` in P1.
734
+ if (subcommand === 'claim') {
735
+ const issueId = normalizeArgs(args).find((arg) => !arg.startsWith('--'));
736
+ if (issueId) {
737
+ const block = await checkReadFirst(projectRoot, issueId, opts);
738
+ if (block) return block;
739
+ }
740
+ }
741
+
742
+ // The Kernel is reached through the runIssueOperation seam. Naming the injected
743
+ // local `runIssueOperation` keeps the dispatch a literal call to a binding named
744
+ // `runIssueOperation` (the kernel-evidence gate is syntactic) while still honoring
745
+ // an injected runner.
739
746
  const runIssueOperation = opts.runIssueOperation || defaultRunIssueOperation;
740
747
  const operation = resolveIssueOperation(subcommand, args);
741
748
  const operationArgs = resolveOperationArgs(subcommand, args, opts);
742
749
 
743
- // Check-after-write (gate.issue_verify): resolved ONCE per invocation, kernel
744
- // path only. Reads and the Beads path never trigger a read-back.
750
+ // Check-after-write (gate.issue_verify): resolved ONCE per invocation. Reads never
751
+ // trigger a read-back.
745
752
  const verifyEnabled = VERIFIED_SUBCOMMANDS.has(subcommand)
746
753
  && shouldUseKernelBroker(opts)
747
754
  && isIssueVerifyEnabled(projectRoot, opts);
@@ -761,7 +768,7 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
761
768
  projectRoot,
762
769
  { ...opts, kernelBroker: opts.kernelBroker },
763
770
  );
764
- // Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a Beads
771
+ // Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a legacy
765
772
  // {success,output} shape). Warn-only: attaches verified/mismatches, never
766
773
  // changes the result's success or exit code.
767
774
  if (verifyEnabled && result && typeof result === 'object' && result.ok === true && result.success === undefined) {
@@ -769,6 +776,23 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
769
776
  }
770
777
  // Best-effort, non-blocking: mirror a stage-transition comment into stage_runs.
771
778
  recordStageTransitionFromComment(subcommand, operationArgs, result, opts);
779
+ // Grounding (gate.read_first): a successful `forge show <id>` counts as reading
780
+ // the issue, so append a `context.loaded` event. Best-effort and awaited (a
781
+ // fire-and-forget append could lose the event when the CLI process exits); a
782
+ // failure here never fails the read.
783
+ if (subcommand === 'show' && result && result.ok === true) {
784
+ const shownId = normalizeArgs(args).find((arg) => !arg.startsWith('--'));
785
+ if (shownId) {
786
+ const deps = (opts.kernelBroker && opts.kernelDriver)
787
+ ? { kernelBroker: opts.kernelBroker, kernelDriver: opts.kernelDriver }
788
+ : undefined;
789
+ try {
790
+ await recordContextLoaded(projectRoot, {
791
+ issueId: shownId, cmd: 'show', session: opts.session, env: opts.env, deps, now: opts.now,
792
+ });
793
+ } catch { /* best-effort: never fail a read on grounding bookkeeping */ }
794
+ }
795
+ }
772
796
  // Contract output is opt-in for the human-first reads: an explicit --json flag
773
797
  // or FORGE_JSON=1 in the environment (for scripts that cannot alter argv).
774
798
  const jsonRequested = normalizeArgs(args).includes('--json')
@@ -26,7 +26,6 @@ const commands = [
26
26
  { file: "add.js", module: require("./add") },
27
27
  { file: "audit.js", module: require("./audit") },
28
28
  { file: "blocked.js", module: require("./blocked") },
29
- { file: "board.js", module: require("./board") },
30
29
  { file: "claim.js", module: require("./claim") },
31
30
  { file: "claims.js", module: require("./claims") },
32
31
  { file: "clean.js", module: require("./clean") },
@@ -48,6 +47,7 @@ const commands = [
48
47
  { file: "issues.js", module: require("./issues") },
49
48
  { file: "lint.js", module: require("./lint") },
50
49
  { file: "list.js", module: require("./list") },
50
+ { file: "memory.js", module: require("./memory") },
51
51
  { file: "merge.js", module: require("./merge") },
52
52
  { file: "migrate.js", module: require("./migrate") },
53
53
  { file: "new.js", module: require("./new") },
@@ -56,6 +56,7 @@ const commands = [
56
56
  { file: "orphans.js", module: require("./orphans") },
57
57
  { file: "patch.js", module: require("./patch") },
58
58
  { file: "plan.js", module: require("./plan") },
59
+ { file: "pr.js", module: require("./pr") },
59
60
  { file: "preflight.js", module: require("./preflight") },
60
61
  { file: "prime.js", module: require("./prime") },
61
62
  { file: "push.js", module: require("./push") },
@@ -71,6 +72,7 @@ const commands = [
71
72
  { file: "shepherd.js", module: require("./shepherd") },
72
73
  { file: "ship.js", module: require("./ship") },
73
74
  { file: "show.js", module: require("./show") },
75
+ { file: "skill.js", module: require("./skill") },
74
76
  { file: "stage.js", module: require("./stage") },
75
77
  { file: "stale.js", module: require("./stale") },
76
78
  { file: "status.js", module: require("./status") },
@@ -12,6 +12,7 @@
12
12
  const { existsSync, readdirSync } = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const { normalizeStageId } = require('../workflow/stages');
15
+ const { ensureForgeHome, isMutatingVerb } = require('../activation/ensure-forge-home');
15
16
 
16
17
  // Static command manifest (bundleable fast path). This is a static require so
17
18
  // `bun build --compile` can bundle the command graph; the file is generated by
@@ -199,6 +200,19 @@ async function executeCommand(commands, commandName, args, flags, projectRoot, o
199
200
  }
200
201
  }
201
202
 
203
+ // Lazy `.forge/` home (activation foundation): the FIRST mutating verb in a
204
+ // bare repo materializes the gates-disabled skeleton on demand. Read-only
205
+ // verbs never enter this branch, so they write nothing; an already-inited
206
+ // repo is a no-op (never clobbered). Failure to create the home must not
207
+ // crash the command — degrade to a warning. Opt out via `skipEnsureHome`.
208
+ if (projectRoot && options.skipEnsureHome !== true && isMutatingVerb(commandName, command)) {
209
+ try {
210
+ (options.ensureForgeHome || ensureForgeHome)(projectRoot);
211
+ } catch (err) {
212
+ console.warn(`[forge] Could not initialize .forge/ home: ${err?.message ?? err}`);
213
+ }
214
+ }
215
+
202
216
  // Forward a resolved per-command opts object as the handler's 4th arg. Issue/
203
217
  // alias handlers read it (shouldUseKernelBroker(opts)); other handlers ignore
204
218
  // it. Backward-compatible: defaults to {} so existing 3-arg handlers are