forge-workflow 0.1.0-beta.3 → 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/AGENTS.md +1 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +5 -0
- package/docs/reference/COMMANDS.md +3 -1
- package/docs/reference/shepherd.md +72 -1
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/agents-config.js +5 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/hooks.js +155 -8
- package/lib/commands/plan.js +23 -115
- package/lib/commands/prime.js +8 -1
- package/lib/commands/release.js +1 -2
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +0 -1
- package/lib/commands/shepherd.js +36 -3
- 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 +27 -1
- package/lib/core/runtime-graph.js +88 -14
- package/lib/forge-issues.js +13 -464
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +54 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +150 -0
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- 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 +14 -0
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +1 -1
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +70 -15
- package/lib/project-memory.js +8 -0
- 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 +8 -9
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +1 -1
- 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/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/beads-nudge.js +0 -91
- 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
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
* handler (the 4th arg). For issue/alias commands this:
|
|
6
6
|
*
|
|
7
7
|
* 1. Strips the selector tokens (`--kernel`, `--issue-backend <val>`) from the
|
|
8
|
-
* args BEFORE they reach the handler
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* be mistaken for an issue id.
|
|
8
|
+
* args BEFORE they reach the handler. This is load-bearing: bin parseFlags()
|
|
9
|
+
* early-returns for issue passthrough commands, so these tokens otherwise flow
|
|
10
|
+
* untouched into the handler and the positional `kernel` value would be
|
|
11
|
+
* mistaken for an issue id.
|
|
13
12
|
* 2. Resolves the backend (flag > env > config > default) via the backend
|
|
14
13
|
* authority module (lib/issue-backend.js). The CLI flag tokens are mapped to
|
|
15
14
|
* that module's `deps.issueBackend` signal; env/config/default precedence is
|
|
16
|
-
* applied by the resolver itself.
|
|
15
|
+
* applied by the resolver itself. `kernel` is the only accepted value — a
|
|
16
|
+
* retired backend named on the FLAG is a hard error (the user typed it in this
|
|
17
|
+
* invocation, so silently ignoring it would run the wrong store), while the
|
|
18
|
+
* same value in env/config only warns and falls back.
|
|
17
19
|
* 3. When kernel, assembles the kernel deps (driver + flag) via the CLI broker
|
|
18
20
|
* factory so createKernelIssueBackend has a real driver (B1).
|
|
19
21
|
*
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
* @module commands/resolve-command-opts
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
|
-
const { resolveIssueBackend } = require('../issue-backend');
|
|
29
|
+
const { resolveIssueBackend, removedBackendHint } = require('../issue-backend');
|
|
28
30
|
const {
|
|
29
31
|
buildMigratedKernelIssueDeps: defaultBuildKernelIssueDeps,
|
|
30
32
|
} = require('../kernel/cli-broker-factory');
|
|
@@ -45,14 +47,11 @@ const ISSUE_COMMANDS = new Set([
|
|
|
45
47
|
'stale',
|
|
46
48
|
'orphans',
|
|
47
49
|
'lint',
|
|
48
|
-
// Epic grouping read
|
|
49
|
-
// verified Beads equivalent), so it belongs in ISSUE_COMMANDS like the rest.
|
|
50
|
+
// Epic grouping read (issue.children rollup).
|
|
50
51
|
'children',
|
|
51
|
-
// Lease-ownership verification
|
|
52
|
-
// so it is kernel-routed like the rest of the issue surface.
|
|
52
|
+
// Lease-ownership verification (leases live in the Kernel).
|
|
53
53
|
'owns',
|
|
54
|
-
// Active-lease listing
|
|
55
|
-
// is kernel-routed like the rest of the issue surface.
|
|
54
|
+
// Active-lease listing (leases live in the Kernel).
|
|
56
55
|
'claims',
|
|
57
56
|
'search',
|
|
58
57
|
'stats',
|
|
@@ -101,7 +100,7 @@ function stripSelectorTokens(rawArgs = []) {
|
|
|
101
100
|
flags.issueBackend = next;
|
|
102
101
|
i += 1;
|
|
103
102
|
} else {
|
|
104
|
-
throw new Error(`${ISSUE_BACKEND_FLAG} requires a value: kernel
|
|
103
|
+
throw new Error(`${ISSUE_BACKEND_FLAG} requires a value: kernel.`);
|
|
105
104
|
}
|
|
106
105
|
continue;
|
|
107
106
|
}
|
|
@@ -109,7 +108,7 @@ function stripSelectorTokens(rawArgs = []) {
|
|
|
109
108
|
// Equals form: reject an empty value (`--issue-backend=`) for the same reason.
|
|
110
109
|
const value = token.slice(ISSUE_BACKEND_FLAG.length + 1).trim();
|
|
111
110
|
if (!value) {
|
|
112
|
-
throw new Error(`${ISSUE_BACKEND_FLAG} requires a value: kernel
|
|
111
|
+
throw new Error(`${ISSUE_BACKEND_FLAG} requires a value: kernel.`);
|
|
113
112
|
}
|
|
114
113
|
flags.issueBackend = value;
|
|
115
114
|
continue;
|
|
@@ -122,12 +121,14 @@ function stripSelectorTokens(rawArgs = []) {
|
|
|
122
121
|
/**
|
|
123
122
|
* Reduce the stripped selector flags to a single explicit backend value (or null
|
|
124
123
|
* when no flag selects one), honoring `--kernel` / `--issue-backend` equivalence
|
|
125
|
-
* and rejecting a mutually-exclusive conflict.
|
|
126
|
-
* here
|
|
124
|
+
* and rejecting a mutually-exclusive conflict. A RETIRED backend value is rejected
|
|
125
|
+
* here with the migrate pointer; any other value is NOT validated here — the backend
|
|
126
|
+
* authority module normalizes/warns on unknown values.
|
|
127
127
|
*
|
|
128
128
|
* @param {{ kernel?: boolean, issueBackend?: string }} flags
|
|
129
129
|
* @returns {string|null}
|
|
130
|
-
* @throws {Error} when --kernel and --issue-backend select different backends
|
|
130
|
+
* @throws {Error} when --kernel and --issue-backend select different backends, or
|
|
131
|
+
* when --issue-backend names a removed backend.
|
|
131
132
|
*/
|
|
132
133
|
function resolveFlagBackend(flags = {}) {
|
|
133
134
|
const fromBackend = typeof flags.issueBackend === 'string' && flags.issueBackend.trim()
|
|
@@ -142,6 +143,19 @@ function resolveFlagBackend(flags = {}) {
|
|
|
142
143
|
);
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// Hard error (not the resolver's warn+fallback) because the value was typed into
|
|
147
|
+
// THIS invocation: silently running the kernel after the user explicitly asked for
|
|
148
|
+
// a different store is the kind of surprise that loses writes.
|
|
149
|
+
if (fromBackend) {
|
|
150
|
+
const removedHint = removedBackendHint(fromBackend);
|
|
151
|
+
if (removedHint) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`${ISSUE_BACKEND_FLAG} ${fromBackend} is no longer supported: ${removedHint}. `
|
|
154
|
+
+ `Only '${KERNEL}' is accepted.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
return fromKernel || fromBackend || null;
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -188,22 +202,15 @@ async function resolveCommandOpts(command, rawArgs = [], deps = {}) {
|
|
|
188
202
|
|
|
189
203
|
const { args, flags } = stripSelectorTokens(rawArgs);
|
|
190
204
|
const flagBackend = resolveFlagBackend(flags);
|
|
191
|
-
|
|
205
|
+
// Still routed through the authority module (rather than hardcoding 'kernel') so a
|
|
206
|
+
// retired/unknown env or config value produces its warning exactly once, here, on
|
|
207
|
+
// the way to the kernel fallback.
|
|
208
|
+
resolveIssueBackend({
|
|
192
209
|
deps: flagBackend ? { issueBackend: flagBackend } : {},
|
|
193
210
|
env,
|
|
194
211
|
projectRoot: deps.projectRoot,
|
|
195
212
|
});
|
|
196
213
|
|
|
197
|
-
if (issueBackend !== KERNEL) {
|
|
198
|
-
return {
|
|
199
|
-
commandOpts: {
|
|
200
|
-
issueBackend,
|
|
201
|
-
useKernelBroker: false,
|
|
202
|
-
},
|
|
203
|
-
args,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
214
|
// Kernel backend: assemble the driver (B1) + migrated broker (B2). The factory
|
|
208
215
|
// builder constructs the driver, builds the broker, and runs initialize().
|
|
209
216
|
const kernelDeps = await buildKernelIssueDeps({
|
package/lib/commands/claim.js
CHANGED
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
const { runIssueSubcommand } = require('./_issue');
|
|
4
4
|
|
|
5
5
|
// `forge claim <id>` claims an issue through the SHARED issue dispatch
|
|
6
|
-
// (runIssueSubcommand): backend resolution (Kernel
|
|
7
|
-
//
|
|
8
|
-
// Beads backend translates claim to `update <id> --claim`), contract
|
|
9
|
-
// normalization, AND the check-after-write verification loop
|
|
6
|
+
// (runIssueSubcommand): backend resolution (the Kernel is the only backend),
|
|
7
|
+
// contract normalization, AND the check-after-write verification loop
|
|
10
8
|
// (gate.issue_verify). This command previously inlined its own copy of the
|
|
11
9
|
// dispatch, which silently BYPASSED the boundary verify — the exact surface
|
|
12
10
|
// where the d71a824b phantom-claim replay lied to a losing agent — so it now
|
package/lib/commands/hooks.js
CHANGED
|
@@ -27,16 +27,81 @@ const {
|
|
|
27
27
|
renderGlobalHookBlock,
|
|
28
28
|
installGlobalHooks,
|
|
29
29
|
} = require('../hook-global-installer');
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
30
32
|
const { sessionStartCapability, userPromptSubmitCapability, sessionEndCapability } = require('../hook-renderer');
|
|
31
33
|
const { collectDigestData, buildMemoryDigest, defaultFetchIssues, defaultFetchNotes } = require('../memory-digest');
|
|
32
34
|
const { collectInbox, buildInboxNudge } = require('../inbox');
|
|
33
35
|
const { collectDigest } = require('../pr-monitor/digest');
|
|
36
|
+
const { loadDispatchText } = require('../using-forge');
|
|
37
|
+
const projectMemory = require('../project-memory');
|
|
38
|
+
const { parseHookInput, selectInjection, DEFAULT_TOKEN_BUDGET, DEFAULT_SCORE_FLOOR } = require('../memory-recall');
|
|
39
|
+
const { fenceUntrusted } = require('../untrusted-content');
|
|
40
|
+
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
41
|
+
|
|
42
|
+
// Default-ON rail; `forge gate disable rail.memory_recall` turns tier-2 off. Mirrors
|
|
43
|
+
// autoShepherdRailEnabled (lib/commands/ship.js): absent id = enabled, fail-open to true.
|
|
44
|
+
const MEMORY_RECALL_RAIL = 'rail.memory_recall';
|
|
45
|
+
// bm25 candidate pool to rank/floor down from. Wider than what we inject so the floor +
|
|
46
|
+
// dedupe have room (mirrors the research "widen before ranking" guidance).
|
|
47
|
+
const MEMORY_RECALL_CANDIDATES = 25;
|
|
48
|
+
// Cross-turn dedupe memory: how many recently-injected keys to remember per session.
|
|
49
|
+
const SEEN_KEYS_CAP = 40;
|
|
50
|
+
|
|
51
|
+
function memoryRecallRailEnabled(projectRoot, resolveGraph = getResolvedRuntimeGraph) {
|
|
52
|
+
try {
|
|
53
|
+
const graph = resolveGraph({ projectRoot });
|
|
54
|
+
const rail = [...(graph.rails || []), ...(graph.gates || [])].find(entry => entry.id === MEMORY_RECALL_RAIL);
|
|
55
|
+
return !(rail?.enabled === false);
|
|
56
|
+
} catch {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read the hook's stdin payload (Claude delivers UserPromptSubmit JSON on fd 0). Never
|
|
62
|
+
// throws — no stdin / a closed fd yields '' so the hook fails open.
|
|
63
|
+
function readHookStdin() {
|
|
64
|
+
try {
|
|
65
|
+
return fs.readFileSync(0, 'utf8');
|
|
66
|
+
} catch {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function seenPath(projectRoot, sessionId) {
|
|
72
|
+
const safe = String(sessionId || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
73
|
+
return path.join(projectRoot, '.forge', 'memory-recall', `${safe}.json`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Keys injected on recent turns of THIS session (cross-turn dedupe). Best-effort: any read
|
|
77
|
+
// error yields [] so a first turn or a corrupt file simply injects without exclusion.
|
|
78
|
+
function loadSeenKeys(projectRoot, sessionId) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(fs.readFileSync(seenPath(projectRoot, sessionId), 'utf8'));
|
|
81
|
+
return Array.isArray(parsed) ? parsed.filter(k => typeof k === 'string') : [];
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Append the just-injected keys to the session's seen-list (newest last, capped). Best-effort.
|
|
88
|
+
function saveSeenKeys(projectRoot, sessionId, keys) {
|
|
89
|
+
try {
|
|
90
|
+
const merged = [...loadSeenKeys(projectRoot, sessionId), ...keys].slice(-SEEN_KEYS_CAP);
|
|
91
|
+
const file = seenPath(projectRoot, sessionId);
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
93
|
+
fs.writeFileSync(file, JSON.stringify(merged), 'utf8');
|
|
94
|
+
} catch {
|
|
95
|
+
// Dedupe is a nicety, not a correctness gate — a failed write never breaks the prompt.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
34
98
|
|
|
35
99
|
function usage() {
|
|
36
100
|
return 'Usage: forge hooks install --global [--harness codex|hermes|all] [--dry-run]\n'
|
|
37
101
|
+ ' forge hooks session-start --harness <claude> (machine-facing; emits SessionStart context)\n'
|
|
38
102
|
+ ' forge hooks inbox-pickup --harness <claude> (machine-facing; emits UserPromptSubmit context)\n'
|
|
39
103
|
+ ' forge hooks shepherd-events --harness <claude> (machine-facing; emits UserPromptSubmit PR-monitor deltas)\n'
|
|
104
|
+
+ ' forge hooks memory-recall --harness <claude> (machine-facing; emits UserPromptSubmit query-relevant memory)\n'
|
|
40
105
|
+ ' forge hooks capture --harness <claude> --trigger <precompact|stop> (machine-facing; captures a session summary on exit)';
|
|
41
106
|
}
|
|
42
107
|
|
|
@@ -115,17 +180,35 @@ function formatSessionStart(harness, text) {
|
|
|
115
180
|
* @returns {Promise<{ success: boolean, output: string }>}
|
|
116
181
|
*/
|
|
117
182
|
async function handleSessionStart(rest, projectRoot, opts = {}) {
|
|
183
|
+
const harness = parseHarness(rest);
|
|
184
|
+
if (!sessionStartCapability(harness).rendered) return { success: true, output: '' };
|
|
185
|
+
|
|
186
|
+
// NOTE: the autonomous-shepherd session-start trigger is deferred to W-S4c along with the
|
|
187
|
+
// per-command dispatch trigger — both need a firing-policy + containment design (a naive
|
|
188
|
+
// trigger spawns a session-outliving daemon that breaks test isolation).
|
|
189
|
+
|
|
190
|
+
// The using-forge dispatch bootstrap is injected FIRST so Forge skills auto-trigger from turn
|
|
191
|
+
// one (the Superpowers mechanism): a reasoning-driven system, not just harness description
|
|
192
|
+
// matching. It is a deterministic file read that survives a kernel outage. The memory digest
|
|
193
|
+
// (remembered notes + top open issues) is appended when present. Either alone is enough to
|
|
194
|
+
// inject; a total blank yields '' (the harness injects nothing). FAIL-OPEN throughout.
|
|
195
|
+
let dispatch = '';
|
|
196
|
+
try {
|
|
197
|
+
// No projectRoot: the dispatch skill is read from the Forge PACKAGE's canonical skills/
|
|
198
|
+
// (a set-up consumer project has no root skills/, only generated mirrors).
|
|
199
|
+
dispatch = (opts.loadDispatchText || loadDispatchText)() || '';
|
|
200
|
+
} catch { /* keep '' — a missing dispatch skill must not break session start */ }
|
|
201
|
+
|
|
202
|
+
let digestText = '';
|
|
118
203
|
try {
|
|
119
|
-
const harness = parseHarness(rest);
|
|
120
|
-
if (!sessionStartCapability(harness).rendered) return { success: true, output: '' };
|
|
121
204
|
const data = await collectDigestData(projectRoot, opts);
|
|
122
205
|
const digest = buildMemoryDigest(data, opts);
|
|
123
|
-
if (digest.empty)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
206
|
+
if (!digest.empty) digestText = digest.text;
|
|
207
|
+
} catch { /* fail-open: a kernel outage must not suppress the dispatch bootstrap */ }
|
|
208
|
+
|
|
209
|
+
const combined = [dispatch, digestText].filter(Boolean).join('\n\n');
|
|
210
|
+
if (!combined) return { success: true, output: '' };
|
|
211
|
+
return { success: true, output: formatSessionStart(harness, combined) };
|
|
129
212
|
}
|
|
130
213
|
|
|
131
214
|
/** Wrap a digest into a harness-native UserPromptSubmit payload, or '' when unsupported. */
|
|
@@ -205,6 +288,69 @@ function handleShepherdEvents(rest, projectRoot, opts = {}) {
|
|
|
205
288
|
}
|
|
206
289
|
}
|
|
207
290
|
|
|
291
|
+
/**
|
|
292
|
+
* `forge hooks memory-recall --harness <h>` — the QUERY-RELEVANT memory tier (tier-2). On each
|
|
293
|
+
* prompt it reads the submitted prompt from the hook's stdin, ranks stored memories by BM25
|
|
294
|
+
* relevance to it (NOT recency — that is the SessionStart digest's job), applies a relevance
|
|
295
|
+
* floor + anaphora guard + cross-turn dedupe + a hard token budget, and emits the survivors as
|
|
296
|
+
* harness-native UserPromptSubmit context. Complements the always-on recency digest: this one
|
|
297
|
+
* answers "what memory is relevant to THIS turn".
|
|
298
|
+
*
|
|
299
|
+
* Compliance + safety (verified against the Claude Code hooks contract, kernel issue 781f6f65):
|
|
300
|
+
* - It READS its own hook stdin (the supported input channel) — it does NOT inject into a
|
|
301
|
+
* running session's stdin and never drives the agent (Anthropic Usage Policy).
|
|
302
|
+
* - additionalContext APPENDS to history every prompt, so the injection is tiny and gated:
|
|
303
|
+
* below the relevance floor, or on a low-signal (anaphora) prompt, it injects NOTHING.
|
|
304
|
+
* - Injected memory bodies are UNTRUSTED (a planted note could carry directives), so each is
|
|
305
|
+
* provenance-fenced.
|
|
306
|
+
* - UserPromptSubmit has a 30s timeout and blocks the prompt; the read is local BM25 over
|
|
307
|
+
* SQLite (sub-ms) and FAIL-OPEN — any error, a disabled rail, an unsupported harness, or
|
|
308
|
+
* nothing relevant yields '' and the prompt proceeds untouched. NEVER throws.
|
|
309
|
+
* - Kill-switch: `forge gate disable rail.memory_recall`.
|
|
310
|
+
*
|
|
311
|
+
* @param {string[]} rest - args after the `memory-recall` action.
|
|
312
|
+
* @param {string} projectRoot
|
|
313
|
+
* @param {object} [opts] - injectable seams for tests: { railEnabled, readInput, search,
|
|
314
|
+
* loadSeen, saveSeen, scoreFloor, tokenBudget }.
|
|
315
|
+
* @returns {{ success: boolean, output: string }}
|
|
316
|
+
*/
|
|
317
|
+
function handleMemoryRecall(rest, projectRoot, opts = {}) {
|
|
318
|
+
try {
|
|
319
|
+
const harness = parseHarness(rest);
|
|
320
|
+
if (!userPromptSubmitCapability(harness).rendered) return { success: true, output: '' };
|
|
321
|
+
|
|
322
|
+
const railEnabled = opts.railEnabled || memoryRecallRailEnabled;
|
|
323
|
+
if (!railEnabled(projectRoot)) return { success: true, output: '' };
|
|
324
|
+
|
|
325
|
+
const readInput = opts.readInput || readHookStdin;
|
|
326
|
+
const { prompt, sessionId } = parseHookInput(readInput());
|
|
327
|
+
if (!prompt) return { success: true, output: '' };
|
|
328
|
+
|
|
329
|
+
const search = opts.search
|
|
330
|
+
|| ((root, query, limit) => projectMemory.searchRankedScored(root, query, limit));
|
|
331
|
+
const loadSeen = opts.loadSeen || loadSeenKeys;
|
|
332
|
+
const saveSeen = opts.saveSeen || saveSeenKeys;
|
|
333
|
+
|
|
334
|
+
const hits = search(projectRoot, prompt, MEMORY_RECALL_CANDIDATES) || [];
|
|
335
|
+
const excludeKeys = loadSeen(projectRoot, sessionId) || [];
|
|
336
|
+
const { lines, injectedKeys } = selectInjection({
|
|
337
|
+
query: prompt,
|
|
338
|
+
hits,
|
|
339
|
+
scoreFloor: typeof opts.scoreFloor === 'number' ? opts.scoreFloor : DEFAULT_SCORE_FLOOR,
|
|
340
|
+
tokenBudget: opts.tokenBudget || DEFAULT_TOKEN_BUDGET,
|
|
341
|
+
excludeKeys,
|
|
342
|
+
});
|
|
343
|
+
if (!lines.length) return { success: true, output: '' };
|
|
344
|
+
|
|
345
|
+
saveSeen(projectRoot, sessionId, injectedKeys);
|
|
346
|
+
const fenced = lines.map(line => fenceUntrusted(line, { source: 'memory' })).join('\n');
|
|
347
|
+
return { success: true, output: formatUserPromptSubmit(harness, fenced) };
|
|
348
|
+
} catch {
|
|
349
|
+
// Fail-open: a context hook must never break a prompt.
|
|
350
|
+
return { success: true, output: '' };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
208
354
|
/**
|
|
209
355
|
* `forge hooks capture --harness <h> --trigger <precompact|stop>` — the CAPTURE-on-exit hook.
|
|
210
356
|
* PreCompact (before context compaction) and Stop (turn end) fire it; it snapshots a bounded
|
|
@@ -356,6 +502,7 @@ async function handler(args, flags = {}, projectRoot, opts = {}) {
|
|
|
356
502
|
if (action === 'session-start') return handleSessionStart(args.slice(1), projectRoot, opts);
|
|
357
503
|
if (action === 'inbox-pickup') return handleInboxPickup(args.slice(1), projectRoot, opts);
|
|
358
504
|
if (action === 'shepherd-events') return handleShepherdEvents(args.slice(1), projectRoot, opts);
|
|
505
|
+
if (action === 'memory-recall') return handleMemoryRecall(args.slice(1), projectRoot, opts);
|
|
359
506
|
if (action === 'capture') return handleCapture(args.slice(1), projectRoot, opts);
|
|
360
507
|
if (action === 'install') return handleInstall(args, flags, opts);
|
|
361
508
|
return {
|
package/lib/commands/plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan Command -
|
|
2
|
+
* Plan Command - Kernel Issue Integration
|
|
3
3
|
* Creates implementation plan after research is complete
|
|
4
4
|
*
|
|
5
5
|
* Security: Uses execFileSync instead of exec/execSync to prevent command injection
|
|
@@ -26,15 +26,14 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Human-readable label for the issue backend that created an issue.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* The kernel is the only backend; anything unresolved prints a neutral label
|
|
30
|
+
* rather than naming a store the issue did not come from.
|
|
31
31
|
*
|
|
32
|
-
* @param {string} [backend] - Resolved issue backend ('kernel'
|
|
33
|
-
* @returns {string} Display label ('Kernel'
|
|
32
|
+
* @param {string} [backend] - Resolved issue backend ('kernel').
|
|
33
|
+
* @returns {string} Display label ('Kernel' or a neutral 'Issue').
|
|
34
34
|
* @private
|
|
35
35
|
*/
|
|
36
36
|
function issueBackendLabel(backend) {
|
|
37
|
-
if (backend === 'beads') return 'Beads';
|
|
38
37
|
if (backend === 'kernel') return 'Kernel';
|
|
39
38
|
return 'Issue';
|
|
40
39
|
}
|
|
@@ -76,7 +75,7 @@ function validateFeatureSlug(slug) {
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
|
-
* Build the issue description
|
|
78
|
+
* Build the issue description used when creating the kernel tracking issue.
|
|
80
79
|
* Strategic scope appends a design-doc pointer derived from a sanitized slug.
|
|
81
80
|
*
|
|
82
81
|
* @param {string} featureName
|
|
@@ -247,98 +246,12 @@ function detectScope(researchContent) {
|
|
|
247
246
|
};
|
|
248
247
|
}
|
|
249
248
|
|
|
250
|
-
/**
|
|
251
|
-
* Create Beads issue for the feature
|
|
252
|
-
* Executes `bd create` command with appropriate description based on scope
|
|
253
|
-
*
|
|
254
|
-
* Security: Uses execFileSync (not exec) to prevent command injection
|
|
255
|
-
*
|
|
256
|
-
* @param {string} featureName - Feature name (human-readable)
|
|
257
|
-
* @param {string} researchPath - Research document path (e.g., "docs/research/feature.md")
|
|
258
|
-
* @param {'tactical'|'strategic'} scope - Scope type
|
|
259
|
-
* @returns {{success: boolean, issueId?: string, description?: string, error?: string}} Beads creation result
|
|
260
|
-
* @example
|
|
261
|
-
* const result = createBeadsIssue('Payment Integration', 'docs/research/payment.md', 'strategic');
|
|
262
|
-
* if (result.success) {
|
|
263
|
-
* console.log('Created issue:', result.issueId);
|
|
264
|
-
* }
|
|
265
|
-
*/
|
|
266
|
-
function createBeadsIssue(featureName, researchPath, scope) {
|
|
267
|
-
if (!featureName || !researchPath) {
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
error: 'Feature name and research path are required',
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (scope !== 'tactical' && scope !== 'strategic') {
|
|
275
|
-
return {
|
|
276
|
-
success: false,
|
|
277
|
-
error: `Invalid scope '${scope}'. Must be 'tactical' or 'strategic'`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
const built = buildFeatureIssueDescription(featureName, researchPath, scope);
|
|
283
|
-
if (built.error) {
|
|
284
|
-
return { success: false, error: built.error };
|
|
285
|
-
}
|
|
286
|
-
const description = built.description;
|
|
287
|
-
|
|
288
|
-
// Execute bd create command using execFileSync for safety (OWASP A03)
|
|
289
|
-
const result = execFileSync( // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
290
|
-
'bd', // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
291
|
-
['create', `--title=${featureName}`, `--description=${description}`, '--type=feature', '--priority=2'],
|
|
292
|
-
getExecOptions()
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// Extract issue ID from output (format: "Created issue: forge-xxx" or "forge-xxx.N" for dotted sub-IDs).
|
|
296
|
-
// Character class is [a-z0-9] (not [a-zA-Z0-9]) because the /i flag makes A-Z redundant.
|
|
297
|
-
const createPattern = /Created issue:\s*(forge-[a-z0-9]+(?:\.[a-z0-9]+)*)/i;
|
|
298
|
-
const fallbackPattern = /(forge-[a-z0-9]+(?:\.[a-z0-9]+)*)/i;
|
|
299
|
-
const match = createPattern.exec(result) || fallbackPattern.exec(result);
|
|
300
|
-
|
|
301
|
-
if (!match) {
|
|
302
|
-
return {
|
|
303
|
-
success: false,
|
|
304
|
-
error: 'Failed to extract issue ID from bd create output\n\nEnsure beads is installed: bunx beads init',
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
success: true,
|
|
310
|
-
issueId: match[1],
|
|
311
|
-
description,
|
|
312
|
-
};
|
|
313
|
-
} catch (error) {
|
|
314
|
-
// Check for timeout
|
|
315
|
-
if (error.killed && error.signal === 'SIGTERM') {
|
|
316
|
-
return {
|
|
317
|
-
success: false,
|
|
318
|
-
error: 'Beads command timed out after 2 minutes.',
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Provide actionable error message
|
|
323
|
-
const bdNotFound = error.message.includes('ENOENT') || error.message.includes('not found');
|
|
324
|
-
const errorMsg = bdNotFound
|
|
325
|
-
? 'beads (bd) command not found. Install with: bunx beads init'
|
|
326
|
-
: `Failed to create Beads issue: ${error.message}`;
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
success: false,
|
|
330
|
-
error: errorMsg,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
249
|
/**
|
|
336
250
|
* Create an issue via the Forge Kernel backend (bd-free).
|
|
337
251
|
*
|
|
338
252
|
* Mirrors `forge issue create` on the kernel: routes through runIssueOperation with
|
|
339
253
|
* the kernel broker instead of shelling out to `bd create`. Used by `forge plan` when
|
|
340
|
-
* the
|
|
341
|
-
* install. The beads path (createBeadsIssue) is preserved for backend=beads.
|
|
254
|
+
* the kernel is the only issue backend, so planning needs no bd binary installed.
|
|
342
255
|
*
|
|
343
256
|
* @param {string} featureName
|
|
344
257
|
* @param {string} researchPath
|
|
@@ -413,7 +326,7 @@ async function linkExistingIssue(issueId, options = {}) {
|
|
|
413
326
|
projectRoot,
|
|
414
327
|
{
|
|
415
328
|
issueBackend,
|
|
416
|
-
useKernelBroker:
|
|
329
|
+
useKernelBroker: true,
|
|
417
330
|
kernelBroker: options.kernelBroker,
|
|
418
331
|
},
|
|
419
332
|
);
|
|
@@ -584,9 +497,9 @@ async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
|
584
497
|
}
|
|
585
498
|
|
|
586
499
|
/**
|
|
587
|
-
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE
|
|
588
|
-
* the
|
|
589
|
-
* nested ternary in executePlan.
|
|
500
|
+
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE in
|
|
501
|
+
* the kernel (the only issue backend — planning needs no bd binary). Extracted to
|
|
502
|
+
* avoid a nested ternary in executePlan.
|
|
590
503
|
*
|
|
591
504
|
* @returns {Promise<{success: boolean, issueId?: string, error?: string}>}
|
|
592
505
|
* @private
|
|
@@ -595,10 +508,7 @@ async function resolveTrackingIssue({ explicitIssueId, issueBackend, featureName
|
|
|
595
508
|
if (explicitIssueId) {
|
|
596
509
|
return linkExistingIssue(explicitIssueId, { ...options, issueBackend });
|
|
597
510
|
}
|
|
598
|
-
|
|
599
|
-
return createKernelIssue(featureName, researchPath, scope, options);
|
|
600
|
-
}
|
|
601
|
-
return createBeadsIssue(featureName, researchPath, scope);
|
|
511
|
+
return createKernelIssue(featureName, researchPath, scope, options);
|
|
602
512
|
}
|
|
603
513
|
|
|
604
514
|
async function createKernelIssue(featureName, researchPath, scope, options = {}) {
|
|
@@ -952,14 +862,14 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
952
862
|
* Tactical workflow (quick fixes, <1 day):
|
|
953
863
|
* 1. Read research document
|
|
954
864
|
* 2. Detect scope (tactical)
|
|
955
|
-
* 3. Create
|
|
865
|
+
* 3. Create kernel issue
|
|
956
866
|
* 4. Create feature branch
|
|
957
867
|
* → Next: /dev command
|
|
958
868
|
*
|
|
959
869
|
* Strategic workflow (architecture changes, >1 day):
|
|
960
870
|
* 1. Read research document
|
|
961
871
|
* 2. Detect scope (strategic)
|
|
962
|
-
* 3. Create
|
|
872
|
+
* 3. Create kernel issue with design doc link
|
|
963
873
|
* 4. Create feature branch
|
|
964
874
|
* → Next: Create design doc, then /dev command
|
|
965
875
|
*
|
|
@@ -967,7 +877,7 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
967
877
|
* @returns {Promise<{
|
|
968
878
|
* success: boolean,
|
|
969
879
|
* scope?: 'tactical'|'strategic',
|
|
970
|
-
* issueBackend?: 'kernel'
|
|
880
|
+
* issueBackend?: 'kernel',
|
|
971
881
|
* issueId?: string,
|
|
972
882
|
* beadsIssueId?: string,
|
|
973
883
|
* branchName?: string,
|
|
@@ -975,10 +885,10 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
975
885
|
* summary?: string,
|
|
976
886
|
* nextCommand?: string,
|
|
977
887
|
* error?: string
|
|
978
|
-
* }>} Execution result. `issueId` is the created issue id
|
|
979
|
-
*
|
|
980
|
-
* alias of `issueId` kept for
|
|
981
|
-
*
|
|
888
|
+
* }>} Execution result. `issueId` is the created kernel issue id, and
|
|
889
|
+
* `issueBackend` names the backend ('kernel'). `beadsIssueId` is a deprecated
|
|
890
|
+
* alias of `issueId` kept for output-shape compatibility — it never implied a
|
|
891
|
+
* Beads store and now always holds the kernel issue id.
|
|
982
892
|
* @example
|
|
983
893
|
* const result = await executePlan('Payment Integration');
|
|
984
894
|
* if (result.success) {
|
|
@@ -995,7 +905,7 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
995
905
|
}
|
|
996
906
|
|
|
997
907
|
// Resolve the active issue backend (explicit opts > env > .forge/config.yaml >
|
|
998
|
-
// default 'kernel').
|
|
908
|
+
// default 'kernel'). The kernel is bd-free, so planning needs no bd binary.
|
|
999
909
|
const issueBackend = resolveIssueBackend({
|
|
1000
910
|
deps: options,
|
|
1001
911
|
env: options.env || process.env,
|
|
@@ -1031,8 +941,7 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
1031
941
|
|
|
1032
942
|
// Step 3: Resolve the tracking issue. `--issue <id>` LINKS an existing issue
|
|
1033
943
|
// (claim-first flow) instead of creating a duplicate (B4). Otherwise create
|
|
1034
|
-
//
|
|
1035
|
-
// (no bd); beads preserves the bd path.
|
|
944
|
+
// through the kernel broker (no bd binary involved).
|
|
1036
945
|
const explicitIssueId = options.issue || options.issueId || null;
|
|
1037
946
|
|
|
1038
947
|
// F4c: never link issue B onto a branch already bound to issue A.
|
|
@@ -1080,8 +989,8 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
1080
989
|
scope: scope.type,
|
|
1081
990
|
issueBackend,
|
|
1082
991
|
issueId: issue.issueId,
|
|
1083
|
-
// Deprecated alias of issueId, retained for
|
|
1084
|
-
//
|
|
992
|
+
// Deprecated alias of issueId, retained for output-shape compatibility.
|
|
993
|
+
// It never implied a Beads store and now always holds the kernel id.
|
|
1085
994
|
beadsIssueId: issue.issueId,
|
|
1086
995
|
branchName: branch.branchName,
|
|
1087
996
|
linked: Boolean(explicitIssueId),
|
|
@@ -1151,7 +1060,6 @@ module.exports = {
|
|
|
1151
1060
|
},
|
|
1152
1061
|
readResearchDoc,
|
|
1153
1062
|
detectScope,
|
|
1154
|
-
createBeadsIssue,
|
|
1155
1063
|
createKernelIssue,
|
|
1156
1064
|
createFeatureBranch,
|
|
1157
1065
|
extractDesignDecisions,
|
package/lib/commands/prime.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
buildPrime,
|
|
5
|
+
collectPrimeLiveState,
|
|
5
6
|
runOrientationCommand,
|
|
6
7
|
} = require('../orientation');
|
|
7
8
|
|
|
@@ -9,5 +10,11 @@ module.exports = {
|
|
|
9
10
|
name: 'prime',
|
|
10
11
|
description: 'Emit session-entry bounded orientation for agents',
|
|
11
12
|
usage: 'Usage: forge prime [--budget N] [--json]',
|
|
12
|
-
|
|
13
|
+
// Async: prime leads with LIVE state (stage / claims / ready / gates / one adoption nudge),
|
|
14
|
+
// which needs a best-effort (non-throwing) kernel read before the synchronous build assembles
|
|
15
|
+
// it into the bounded orientation. All existing prime output/flags are unchanged.
|
|
16
|
+
handler: async (args, _flags, projectRoot) => {
|
|
17
|
+
const liveState = await collectPrimeLiveState(projectRoot);
|
|
18
|
+
return runOrientationCommand(buildPrime, args, projectRoot, { liveState });
|
|
19
|
+
},
|
|
13
20
|
};
|
package/lib/commands/release.js
CHANGED
|
@@ -12,8 +12,7 @@ const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = requir
|
|
|
12
12
|
// `forge release <id>` releases a claimed issue; `forge release check` runs the
|
|
13
13
|
// release-readiness gate. The two share the top-level verb, so this command
|
|
14
14
|
// dispatches `check` to the gate and routes everything else through the shared
|
|
15
|
-
// issue dispatch (resolve backend → runIssueOperation('release') → normalize).
|
|
16
|
-
// Beads backend has no release op and returns the Kernel-only contract error.
|
|
15
|
+
// issue dispatch (resolve backend → runIssueOperation('release') → normalize).
|
|
17
16
|
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit';
|
|
18
17
|
|
|
19
18
|
async function runReleaseIssue(args, projectRoot, opts = {}) {
|
package/lib/commands/serve.js
CHANGED
|
@@ -240,7 +240,7 @@ async function routeMutation(verb, args, projectRoot, deps = {}) {
|
|
|
240
240
|
function defaultGenerate(projectRoot, dashboardDir) {
|
|
241
241
|
return new Promise((resolve, reject) => {
|
|
242
242
|
const script = path.join(dashboardDir, 'generate-snapshot.mjs');
|
|
243
|
-
const child = spawn(process.execPath, [script], { cwd: projectRoot, stdio: 'ignore' });
|
|
243
|
+
const child = spawn(process.execPath, [script], { cwd: projectRoot, stdio: 'ignore', windowsHide: true });
|
|
244
244
|
const timer = setTimeout(() => { child.kill(); reject(new Error('snapshot generation timed out')); }, GEN_TIMEOUT_MS);
|
|
245
245
|
child.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
246
246
|
child.on('exit', (code) => {
|
|
@@ -441,7 +441,10 @@ function browserOpener() {
|
|
|
441
441
|
function openBrowser(url) {
|
|
442
442
|
try {
|
|
443
443
|
const { file, args } = browserOpener();
|
|
444
|
-
|
|
444
|
+
// windowsHide keeps this detached opener from flashing a console window on
|
|
445
|
+
// Windows (Node defaults windowsHide to false); background/detached spawns
|
|
446
|
+
// must stay silent (issue 931e7924).
|
|
447
|
+
const child = spawn(file, [...args, url], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
445
448
|
// spawn() failures (e.g. the opener binary is missing) surface ASYNChronously
|
|
446
449
|
// as an 'error' event, NOT via the try/catch — an unhandled one would crash
|
|
447
450
|
// `forge serve`. Opening a browser is best-effort; swallow it (URL is printed).
|
package/lib/commands/setup.js
CHANGED
|
@@ -188,7 +188,6 @@ const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
|
188
188
|
'scripts/forge-team/index.sh',
|
|
189
189
|
'scripts/forge-team/lib/agent-prompt.sh',
|
|
190
190
|
'scripts/forge-team/lib/claim.sh',
|
|
191
|
-
'scripts/forge-team/lib/dashboard.sh',
|
|
192
191
|
'scripts/forge-team/lib/epic.sh',
|
|
193
192
|
'scripts/forge-team/lib/hooks.sh',
|
|
194
193
|
'scripts/forge-team/lib/identity.sh',
|