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.
- package/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- 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
|
+
};
|
package/lib/commands/_issue.js
CHANGED
|
@@ -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
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// issue
|
|
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
|
|
68
|
-
//
|
|
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)
|
|
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
|
|
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.
|
|
401
|
-
//
|
|
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
|
-
//
|
|
455
|
-
// shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
|
|
456
|
-
// when no explicit signal is present (env/config/explicit),
|
|
457
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
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
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
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
|
|
744
|
-
//
|
|
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
|
|
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
|