forge-workflow 0.1.0-beta.4 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +16 -374
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +54 -25
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +344 -142
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +223 -25
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +17 -2
- package/lib/commands/setup.js +191 -94
- package/lib/commands/shepherd.js +13 -1
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +119 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/test.js +24 -34
- package/lib/commands/worktree.js +220 -42
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +56 -32
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +93 -4
- package/lib/insights.js +96 -80
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +16 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/sqlite-driver.js +153 -29
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +68 -43
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +224 -41
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +10 -1
- package/lib/pr-monitor/watch-lifecycle.js +13 -1
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +33 -1
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/package.json +6 -7
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +176 -43
- package/skills/review/SKILL.md +4 -11
- package/skills/review/evals/scorecard.json +3 -3
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +20 -14
- package/skills/shepherd/evals/scorecard.json +2 -2
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/commands/pr.js
CHANGED
|
@@ -33,7 +33,7 @@ const SUBCOMMANDS = {
|
|
|
33
33
|
},
|
|
34
34
|
merge: {
|
|
35
35
|
module: merge,
|
|
36
|
-
summary: 'Opt-in
|
|
36
|
+
summary: 'Opt-in guarded merge, OFF by default (= forge merge --auto <pr> --expect-head <sha> --issue <id>)',
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -103,7 +103,16 @@ function resolveChangeSet(exec = execFileSync, { runAll = false } = {}) {
|
|
|
103
103
|
// Diff against the SAME base we just resolved. getChangedFiles() re-resolves
|
|
104
104
|
// its own diff ref (and can fall back to a different default branch), so use
|
|
105
105
|
// baseRef directly to keep the file list consistent with the resolved base.
|
|
106
|
-
|
|
106
|
+
const changedFiles = changedFilesForBase(exec, baseRef);
|
|
107
|
+
if (changedFiles == null) {
|
|
108
|
+
return {
|
|
109
|
+
resolved: false,
|
|
110
|
+
baseRef,
|
|
111
|
+
changedFiles: [],
|
|
112
|
+
reason: `git diff failed or timed out for ${baseRef}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { resolved: true, baseRef, changedFiles };
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/** Files changed between the merge-base of HEAD and `baseRef` and HEAD. */
|
|
@@ -111,7 +120,7 @@ function changedFilesForBase(exec, baseRef) {
|
|
|
111
120
|
const mergeBase = gitTryOut(exec, ['merge-base', 'HEAD', baseRef]);
|
|
112
121
|
const range = mergeBase ? `${mergeBase}...HEAD` : `${baseRef}...HEAD`;
|
|
113
122
|
const out = gitTryOut(exec, ['diff', '--name-only', range]);
|
|
114
|
-
if (out == null) return
|
|
123
|
+
if (out == null) return null;
|
|
115
124
|
return out.split(/\r?\n/).filter(Boolean);
|
|
116
125
|
}
|
|
117
126
|
|
package/lib/commands/prime.js
CHANGED
|
@@ -6,15 +6,28 @@ const {
|
|
|
6
6
|
runOrientationCommand,
|
|
7
7
|
} = require('../orientation');
|
|
8
8
|
|
|
9
|
+
const DEPRECATION_NOTICE =
|
|
10
|
+
'forge prime is deprecated — run `forge status -v` for the same briefing.\n';
|
|
11
|
+
|
|
12
|
+
// The single briefing renderer. `forge status -v` calls this so the full
|
|
13
|
+
// session-entry briefing has exactly one implementation.
|
|
14
|
+
// Async: the briefing leads with LIVE state (stage / claims / ready / gates / one
|
|
15
|
+
// adoption nudge), which needs a best-effort (non-throwing) kernel read before the
|
|
16
|
+
// synchronous build assembles it into the bounded orientation.
|
|
17
|
+
async function renderBriefing(args, projectRoot) {
|
|
18
|
+
const liveState = await collectPrimeLiveState(projectRoot);
|
|
19
|
+
return runOrientationCommand(buildPrime, args, projectRoot, { liveState });
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
module.exports = {
|
|
10
23
|
name: 'prime',
|
|
11
|
-
description: '
|
|
12
|
-
usage: 'Usage: forge prime [--budget N] [--json]',
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return runOrientationCommand(buildPrime, args, projectRoot, { liveState });
|
|
24
|
+
description: 'Deprecated alias for `forge status -v`: session-entry bounded orientation',
|
|
25
|
+
usage: 'Usage: forge prime [--budget N] [--json] (deprecated — use `forge status -v`)',
|
|
26
|
+
// Session-start hooks in consumer repos call this and consume stdout as context,
|
|
27
|
+
// so the notice goes to stderr and stdout stays byte-identical to `status -v`.
|
|
28
|
+
handler: async (args, _flags, projectRoot, options = {}) => {
|
|
29
|
+
(options.stderr || process.stderr).write(DEPRECATION_NOTICE);
|
|
30
|
+
return renderBriefing(args, projectRoot);
|
|
19
31
|
},
|
|
32
|
+
renderBriefing,
|
|
20
33
|
};
|
package/lib/commands/push.js
CHANGED
|
@@ -4,8 +4,8 @@ const { execFileSync, spawnSync } = require('node:child_process');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const forgeToken = require('../../scripts/check-forge-token');
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
7
|
+
const { QUICK_LANE_ENV_VAR, QUICK_LANE_VALUE } = require('../../scripts/test');
|
|
8
|
+
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
9
9
|
|
|
10
10
|
const isWindows = process.platform === 'win32';
|
|
11
11
|
|
|
@@ -113,54 +113,49 @@ async function autoFileBackingIssueForPush(projectRoot, execFn, deps = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
116
|
+
* Best-effort wake of the repository-wide singleton after a successful push.
|
|
117
|
+
* The shared trigger owns all containment and gate checks and never selects a
|
|
118
|
+
* per-PR watcher here.
|
|
119
119
|
*
|
|
120
|
-
* @param {
|
|
121
|
-
* @returns {
|
|
120
|
+
* @param {object} params
|
|
121
|
+
* @returns {{ armed: boolean, reason?: string }}
|
|
122
122
|
*/
|
|
123
|
-
function
|
|
123
|
+
function maybeTriggerShepherdAfterPush({
|
|
124
|
+
projectRoot,
|
|
125
|
+
fireAndForget: trigger = fireAndForget,
|
|
126
|
+
}) {
|
|
124
127
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return Number.isInteger(n) && n > 0 ? n : null;
|
|
130
|
-
} catch (_err) { /* intentional: no open PR / gh missing → arm nothing */ // NOSONAR S2486
|
|
131
|
-
return null;
|
|
128
|
+
trigger({ projectRoot });
|
|
129
|
+
return { armed: true };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { armed: false, reason: err.message };
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
137
|
-
* push, when an OPEN PR exists for the current branch. This closes the gap where
|
|
138
|
-
* PRs not born from `forge ship` (gh pr create, the GitHub UI, an earlier push)
|
|
139
|
-
* never got a watcher. Gated by the default-ON `rail.auto_shepherd`, idempotent
|
|
140
|
-
* via the watch loop's own PID/journal lock, and reusing the same
|
|
141
|
-
* `startPrWatcherDetached` as ship. MUST NEVER throw into or fail the push: a
|
|
142
|
-
* disabled rail, no PR, a gh error, or a spawn error all degrade to
|
|
143
|
-
* `{ armed: false }`.
|
|
136
|
+
* Build the environment for the spawned `git push`, declaring the push lane.
|
|
144
137
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
138
|
+
* `--quick` skips the test step push.js controls, but the `git push` it spawns
|
|
139
|
+
* still fires the lefthook pre-push hook, whose tests job runs scripts/test.js —
|
|
140
|
+
* so --quick was quick only up to the push. This declares the lane to that hook
|
|
141
|
+
* so the quick lane is lint-only end to end.
|
|
142
|
+
*
|
|
143
|
+
* The declaration is set on the child env only, and is explicitly REMOVED for a
|
|
144
|
+
* full push so an inherited value from an outer shell can never silently drop
|
|
145
|
+
* the tests from a `forge push`.
|
|
146
|
+
*
|
|
147
|
+
* @param {boolean} quickMode - Whether this is a --quick push
|
|
148
|
+
* @param {NodeJS.ProcessEnv} [baseEnv=process.env] - Environment to derive from
|
|
149
|
+
* @returns {NodeJS.ProcessEnv} Environment for the git push child process
|
|
147
150
|
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}) {
|
|
155
|
-
try {
|
|
156
|
-
if (!railEnabled(projectRoot)) return { armed: false, reason: 'rail.auto_shepherd disabled' };
|
|
157
|
-
const prNumber = prLookup(execFn);
|
|
158
|
-
if (!prNumber) return { armed: false, reason: 'no-open-pr' };
|
|
159
|
-
const res = startWatcher({ prNumber, cwd: projectRoot });
|
|
160
|
-
return { armed: !!(res && res.started), reason: res && res.reason, prNumber };
|
|
161
|
-
} catch (err) {
|
|
162
|
-
return { armed: false, reason: err.message };
|
|
151
|
+
function buildPushEnv(quickMode, baseEnv = process.env) {
|
|
152
|
+
const env = { ...baseEnv };
|
|
153
|
+
if (quickMode) {
|
|
154
|
+
env[QUICK_LANE_ENV_VAR] = QUICK_LANE_VALUE;
|
|
155
|
+
} else {
|
|
156
|
+
delete env[QUICK_LANE_ENV_VAR];
|
|
163
157
|
}
|
|
158
|
+
return env;
|
|
164
159
|
}
|
|
165
160
|
|
|
166
161
|
/**
|
|
@@ -288,7 +283,7 @@ module.exports = {
|
|
|
288
283
|
// Step 5: git push with passthrough args
|
|
289
284
|
const gitArgs = args.filter(a => a !== '--quick');
|
|
290
285
|
try {
|
|
291
|
-
execFn('git', ['push', ...gitArgs], { stdio: 'inherit' });
|
|
286
|
+
execFn('git', ['push', ...gitArgs], { stdio: 'inherit', env: buildPushEnv(quickMode) });
|
|
292
287
|
} catch (_pushErr) { // NOSONAR S2486
|
|
293
288
|
log('git push failed.');
|
|
294
289
|
return {
|
|
@@ -300,15 +295,10 @@ module.exports = {
|
|
|
300
295
|
};
|
|
301
296
|
}
|
|
302
297
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
// from `forge ship`.
|
|
306
|
-
maybeArmWatcherAfterPush({
|
|
298
|
+
// Wake the repository-wide singleton after a successful push.
|
|
299
|
+
maybeTriggerShepherdAfterPush({
|
|
307
300
|
projectRoot,
|
|
308
|
-
|
|
309
|
-
startWatcher: deps?.startWatcher,
|
|
310
|
-
railEnabled: deps?.railEnabled,
|
|
311
|
-
prLookup: deps?.prLookup,
|
|
301
|
+
fireAndForget: deps?.fireAndForget,
|
|
312
302
|
});
|
|
313
303
|
|
|
314
304
|
return {
|
|
@@ -323,7 +313,7 @@ module.exports = {
|
|
|
323
313
|
// Exposed for unit tests; not part of the CLI surface.
|
|
324
314
|
_internal: {
|
|
325
315
|
autoFileBackingIssueForPush,
|
|
326
|
-
|
|
327
|
-
|
|
316
|
+
buildPushEnv,
|
|
317
|
+
maybeTriggerShepherdAfterPush,
|
|
328
318
|
},
|
|
329
319
|
};
|
package/lib/commands/recall.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
const memoryRouter = require('../memory/router');
|
|
4
4
|
const { stripGlobalFlags } = require('../global-flags');
|
|
5
5
|
const { fenceUntrusted } = require('../untrusted-content');
|
|
6
|
+
const { applyBudget, buildSection, estimateTokens } = require('../orientation');
|
|
7
|
+
const { memoryTrustStatus } = require('../memory-recall');
|
|
6
8
|
|
|
7
9
|
const usage = 'Usage: forge recall [query] [--kind <type>] [--limit N] [--all] [--json]';
|
|
10
|
+
const RECALL_CONTENT_BUDGET = 1100;
|
|
8
11
|
|
|
9
12
|
// Reserved tag prefix that `remember --kind` writes (kernel issue 8cc1db4d). A `--kind`
|
|
10
13
|
// filter keeps only notes carrying this tag; the prefix is stripped when surfacing the
|
|
@@ -79,21 +82,28 @@ function withType(entry) {
|
|
|
79
82
|
|
|
80
83
|
function formatEntry(entry) {
|
|
81
84
|
const date = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
|
|
82
|
-
const
|
|
85
|
+
const trust = memoryTrustStatus({
|
|
86
|
+
tags: entry.tags,
|
|
87
|
+
sourceAgent: entry.sourceAgent,
|
|
88
|
+
value: entry.machine ? {} : entry.note,
|
|
89
|
+
});
|
|
90
|
+
const sourceAgent = entry.sourceAgent || 'unknown';
|
|
91
|
+
const label = `[source=${sourceAgent} trust=${trust} updated=${date || 'unknown'}] `;
|
|
83
92
|
// The reserved `type:` tag renders as a leading `(kind)` marker, not as a raw tag, so the
|
|
84
93
|
// displayed tags stay the user's own labels.
|
|
85
94
|
const type = typeOf(entry);
|
|
86
|
-
const userTags = (entry.tags || []).filter(
|
|
95
|
+
const userTags = (entry.tags || []).filter(
|
|
96
|
+
t => !t.startsWith(TYPE_TAG_PREFIX) && !t.startsWith('trust:')
|
|
97
|
+
);
|
|
87
98
|
const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
|
|
88
99
|
const typeMarker = type ? `(${type}) ` : '';
|
|
89
100
|
// Machine/insights records are LABELED with their source so they are never mistaken for a
|
|
90
101
|
// plain human note; human `remember` notes render clean.
|
|
91
102
|
const marker = entry.machine && entry.sourceAgent ? `(${entry.sourceAgent}) ` : '';
|
|
92
|
-
// Stored note text is
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return `- ${prefix}${marker}${typeMarker}${note}${tagSuffix}`;
|
|
103
|
+
// Stored note text is untrusted. Fence it after budgeting so a truncation cannot
|
|
104
|
+
// sever the close marker; the `--json` path above keeps the raw note unchanged.
|
|
105
|
+
const note = String(entry.note == null ? '' : entry.note);
|
|
106
|
+
return `- ${label}${marker}${typeMarker}${note}${tagSuffix}`;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
async function handler(args, flags, projectRoot) {
|
|
@@ -137,23 +147,57 @@ async function handler(args, flags, projectRoot) {
|
|
|
137
147
|
return { success: true, output: reason };
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
const sections = notes
|
|
151
|
+
.map((entry, index) => {
|
|
152
|
+
const content = formatEntry(entry);
|
|
153
|
+
if (estimateTokens(content) > RECALL_CONTENT_BUDGET) return null;
|
|
154
|
+
const trust = memoryTrustStatus({
|
|
155
|
+
tags: entry.tags,
|
|
156
|
+
sourceAgent: entry.sourceAgent,
|
|
157
|
+
value: entry.machine ? {} : entry.note,
|
|
158
|
+
});
|
|
159
|
+
return buildSection({
|
|
160
|
+
id: `recall_${index}`,
|
|
161
|
+
title: '',
|
|
162
|
+
content,
|
|
163
|
+
priority: index,
|
|
164
|
+
preserve: false,
|
|
165
|
+
data: { trust },
|
|
166
|
+
});
|
|
167
|
+
})
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
const budgeted = applyBudget(sections, RECALL_CONTENT_BUDGET).sections
|
|
170
|
+
.filter(section => section.content);
|
|
171
|
+
for (const section of budgeted) {
|
|
172
|
+
section.content = fenceUntrusted(section.content, { source: 'memory' });
|
|
173
|
+
}
|
|
174
|
+
const rendered = budgeted.length;
|
|
140
175
|
const noun = scope === 'all' && !query ? 'stored memory record(s)' : 'remembered note(s)';
|
|
141
176
|
let header;
|
|
142
177
|
if (query) {
|
|
143
178
|
// BM25 returns at most `limit`; when full, signal it is the TOP-N, not the whole set.
|
|
144
|
-
header = capped
|
|
145
|
-
? `Top ${
|
|
146
|
-
: `${
|
|
147
|
-
} else if (capped) {
|
|
148
|
-
// Never a bare full dump: show the
|
|
149
|
-
header = `Showing ${
|
|
179
|
+
header = capped || rendered < notes.length
|
|
180
|
+
? `Top ${rendered} note(s) matching "${query}" (raise --limit for more):`
|
|
181
|
+
: `${rendered} note(s) matching "${query}":`;
|
|
182
|
+
} else if (capped || rendered < notes.length) {
|
|
183
|
+
// Never a bare full dump: show the rendered count and the true total.
|
|
184
|
+
header = `Showing ${rendered} of ${total} ${noun} (newest first):`;
|
|
150
185
|
} else {
|
|
151
|
-
header = `${
|
|
186
|
+
header = `${rendered} ${noun}:`;
|
|
152
187
|
}
|
|
153
|
-
const
|
|
188
|
+
const confirmed = budgeted.filter(section => section.data.trust === 'confirmed');
|
|
189
|
+
const suggested = budgeted.filter(section => section.data.trust === 'suggested');
|
|
190
|
+
const groups = [
|
|
191
|
+
confirmed.length ? ['Confirmed memory', confirmed] : null,
|
|
192
|
+
suggested.length ? ['Suggested memory — verify before relying', suggested] : null,
|
|
193
|
+
].filter(Boolean);
|
|
194
|
+
const body = groups.flatMap(([title, entries]) => [
|
|
195
|
+
title,
|
|
196
|
+
...entries.map(entry => entry.content),
|
|
197
|
+
]);
|
|
154
198
|
return {
|
|
155
199
|
success: true,
|
|
156
|
-
output: [header, ...
|
|
200
|
+
output: [header, ...body].join('\n'),
|
|
157
201
|
};
|
|
158
202
|
}
|
|
159
203
|
|
package/lib/commands/recap.js
CHANGED
|
@@ -46,7 +46,12 @@ async function handler(args, _flags, projectRoot, opts = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const budget = readOption(args, '--budget', undefined);
|
|
49
|
-
|
|
49
|
+
// `runIssueOperation` is the injectable kernel-read seam (Slice C2) — the same
|
|
50
|
+
// pass-through opts already carry for the grounding write below.
|
|
51
|
+
const recap = await buildIssueRecap(projectRoot, issueId, {
|
|
52
|
+
budgetTokens: budget,
|
|
53
|
+
runIssueOperation: opts.runIssueOperation,
|
|
54
|
+
});
|
|
50
55
|
|
|
51
56
|
// Grounding (gate.read_first): a successful recap IS the load-the-doc action,
|
|
52
57
|
// so append a `context.loaded` event that unblocks a later `forge claim <id>`.
|
package/lib/commands/release.js
CHANGED
|
@@ -8,12 +8,13 @@ const {
|
|
|
8
8
|
} = require('../release-readiness');
|
|
9
9
|
const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
|
|
10
10
|
const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = require('./_issue');
|
|
11
|
+
const { generateNpmPublishWorkflow } = require('../npm-publish-workflow');
|
|
11
12
|
|
|
12
13
|
// `forge release <id>` releases a claimed issue; `forge release check` runs the
|
|
13
14
|
// release-readiness gate. The two share the top-level verb, so this command
|
|
14
15
|
// dispatches `check` to the gate and routes everything else through the shared
|
|
15
16
|
// issue dispatch (resolve backend → runIssueOperation('release') → normalize).
|
|
16
|
-
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit';
|
|
17
|
+
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow';
|
|
17
18
|
|
|
18
19
|
async function runReleaseIssue(args, projectRoot, opts = {}) {
|
|
19
20
|
const resolved = withResolvedIssueBackend(projectRoot, opts);
|
|
@@ -55,7 +56,21 @@ function parseReleaseArgs(args = []) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
async function handler(args, _flags, projectRoot, opts = {}) {
|
|
58
|
-
|
|
59
|
+
const parsed = parseReleaseArgs(args);
|
|
60
|
+
|
|
61
|
+
if (parsed.subcommand === 'generate-npm-workflow') {
|
|
62
|
+
const generated = await generateNpmPublishWorkflow(projectRoot, {
|
|
63
|
+
env: opts.env,
|
|
64
|
+
kernelDeps: opts.kernelDeps,
|
|
65
|
+
});
|
|
66
|
+
return generated.success
|
|
67
|
+
? {
|
|
68
|
+
success: true,
|
|
69
|
+
generated,
|
|
70
|
+
output: `Generated ${generated.path} (${generated.contentHash}).\n`,
|
|
71
|
+
}
|
|
72
|
+
: generated;
|
|
73
|
+
}
|
|
59
74
|
|
|
60
75
|
if (parsed.subcommand === 'regen-audit') {
|
|
61
76
|
// forge release regen-audit — rewrite the D20 kill-list from a live re-scan.
|