forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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 +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- 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/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- 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/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -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/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- 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/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -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 +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- 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/recall.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const memoryRouter = require('../memory/router');
|
|
4
|
+
const projectMemory = require('../project-memory');
|
|
5
|
+
const { types: { isProxy } } = require('node:util');
|
|
4
6
|
const { stripGlobalFlags } = require('../global-flags');
|
|
5
7
|
const { fenceUntrusted } = require('../untrusted-content');
|
|
8
|
+
const { applyBudget, buildSection, estimateTokens } = require('../orientation');
|
|
9
|
+
const { memoryTrustStatus } = require('../memory-recall');
|
|
6
10
|
|
|
7
11
|
const usage = 'Usage: forge recall [query] [--kind <type>] [--limit N] [--all] [--json]';
|
|
12
|
+
const RECALL_CONTENT_BUDGET = 1100;
|
|
8
13
|
|
|
9
14
|
// Reserved tag prefix that `remember --kind` writes (kernel issue 8cc1db4d). A `--kind`
|
|
10
15
|
// filter keeps only notes carrying this tag; the prefix is stripped when surfacing the
|
|
@@ -12,10 +17,8 @@ const usage = 'Usage: forge recall [query] [--kind <type>] [--limit N] [--all] [
|
|
|
12
17
|
// `--type`): `--type` is a reserved GLOBAL flag hard-validated to workflow classifications.
|
|
13
18
|
const TYPE_TAG_PREFIX = 'type:';
|
|
14
19
|
|
|
15
|
-
//
|
|
16
|
-
// not reimplemented), so scan a generous window of recent notes before filtering rather than
|
|
20
|
+
// Kind filtering is pushed into the Kernel read so limits and capped metadata stay truthful.
|
|
17
21
|
// the small default page — otherwise the type match could fall outside the default limit.
|
|
18
|
-
const TYPE_FILTER_SCAN = 1000;
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Separate the optional positional query from `--kind <type>`, `--limit N`, and `--json`.
|
|
@@ -77,26 +80,129 @@ function withType(entry) {
|
|
|
77
80
|
return type ? { ...entry, type } : entry;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function privateCommandOption(options, field) {
|
|
84
|
+
if (!options || typeof options !== 'object' || isProxy(options)) return undefined;
|
|
85
|
+
let descriptor;
|
|
86
|
+
try { descriptor = Object.getOwnPropertyDescriptor(options, field); } catch { return undefined; }
|
|
87
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
function formatEntry(entry) {
|
|
81
91
|
const date = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
|
|
82
|
-
const
|
|
92
|
+
const trust = memoryTrustStatus({
|
|
93
|
+
tags: entry.tags,
|
|
94
|
+
sourceAgent: entry.sourceAgent,
|
|
95
|
+
value: entry.machine ? {} : entry.note,
|
|
96
|
+
});
|
|
97
|
+
const sourceAgent = entry.sourceAgent || 'unknown';
|
|
98
|
+
const label = `[source=${sourceAgent} trust=${trust} updated=${date || 'unknown'}] `;
|
|
83
99
|
// The reserved `type:` tag renders as a leading `(kind)` marker, not as a raw tag, so the
|
|
84
100
|
// displayed tags stay the user's own labels.
|
|
85
101
|
const type = typeOf(entry);
|
|
86
|
-
const userTags = (entry.tags || []).filter(
|
|
102
|
+
const userTags = (entry.tags || []).filter(
|
|
103
|
+
t => !t.startsWith(TYPE_TAG_PREFIX) && !t.startsWith('trust:')
|
|
104
|
+
);
|
|
87
105
|
const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
|
|
88
106
|
const typeMarker = type ? `(${type}) ` : '';
|
|
89
107
|
// Machine/insights records are LABELED with their source so they are never mistaken for a
|
|
90
108
|
// plain human note; human `remember` notes render clean.
|
|
91
109
|
const marker = entry.machine && entry.sourceAgent ? `(${entry.sourceAgent}) ` : '';
|
|
92
|
-
// Stored note text is
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
// Stored note text is untrusted. Fence it after budgeting so a truncation cannot
|
|
111
|
+
// sever the close marker; the `--json` path above keeps the raw note unchanged.
|
|
112
|
+
const note = String(entry.note == null ? '' : entry.note);
|
|
113
|
+
return `- ${label}${marker}${typeMarker}${note}${tagSuffix}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createUsageRecorder(projectRoot, recallStore, commandOpts) {
|
|
117
|
+
const invocationId = privateCommandOption(commandOpts, 'invocationId');
|
|
118
|
+
const usageStore = privateCommandOption(commandOpts, 'usageStore');
|
|
119
|
+
const onUsageEvidence = privateCommandOption(commandOpts, 'onUsageEvidence');
|
|
120
|
+
const invocationStartedAt = privateCommandOption(commandOpts, 'invocationStartedAt')
|
|
121
|
+
|| privateCommandOption(commandOpts, 'now')
|
|
122
|
+
|| new Date().toISOString();
|
|
123
|
+
const evidenceCallback = typeof onUsageEvidence === 'function' && !isProxy(onUsageEvidence)
|
|
124
|
+
? onUsageEvidence
|
|
125
|
+
: undefined;
|
|
126
|
+
return selected => {
|
|
127
|
+
const observation = projectMemory.recordRecallUsage(projectRoot, selected, {
|
|
128
|
+
invocationId,
|
|
129
|
+
usageStore: usageStore || recallStore,
|
|
130
|
+
invocationStartedAt,
|
|
131
|
+
});
|
|
132
|
+
if (evidenceCallback) {
|
|
133
|
+
// Observability is deliberately aggregate-only: callers never receive query, note,
|
|
134
|
+
// path, secret, or raw durable identifiers.
|
|
135
|
+
try { evidenceCallback(observation); } catch { /* advisory test seam */ }
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildRecallSections(notes) {
|
|
141
|
+
return notes
|
|
142
|
+
.map((entry, index) => {
|
|
143
|
+
const content = formatEntry(entry);
|
|
144
|
+
if (estimateTokens(content) > RECALL_CONTENT_BUDGET) return null;
|
|
145
|
+
const trust = memoryTrustStatus({
|
|
146
|
+
tags: entry.tags,
|
|
147
|
+
sourceAgent: entry.sourceAgent,
|
|
148
|
+
value: entry.machine ? {} : entry.note,
|
|
149
|
+
});
|
|
150
|
+
return buildSection({
|
|
151
|
+
id: `recall_${index}`,
|
|
152
|
+
title: '',
|
|
153
|
+
content,
|
|
154
|
+
priority: index,
|
|
155
|
+
preserve: false,
|
|
156
|
+
data: { trust, memoryId: entry.id },
|
|
157
|
+
});
|
|
158
|
+
})
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function humanRecallHeader(query, capped, rendered, noteCount, total, scope) {
|
|
163
|
+
const noun = scope === 'all' && !query ? 'stored memory record(s)' : 'remembered note(s)';
|
|
164
|
+
if (query) {
|
|
165
|
+
return capped || rendered < noteCount
|
|
166
|
+
? `Top ${rendered} note(s) matching "${query}" (raise --limit for more):`
|
|
167
|
+
: `${rendered} note(s) matching "${query}":`;
|
|
168
|
+
}
|
|
169
|
+
if (capped || rendered < noteCount) {
|
|
170
|
+
return `Showing ${rendered} of ${total} ${noun} (newest first):`;
|
|
171
|
+
}
|
|
172
|
+
return `${rendered} ${noun}:`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderHumanRecall(notes, result, recordUsage) {
|
|
176
|
+
const sections = buildRecallSections(notes);
|
|
177
|
+
const budgeted = applyBudget(sections, RECALL_CONTENT_BUDGET).sections
|
|
178
|
+
.filter(section => section.content);
|
|
179
|
+
for (const section of budgeted) {
|
|
180
|
+
section.content = fenceUntrusted(section.content, { source: 'memory' });
|
|
181
|
+
}
|
|
182
|
+
recordUsage(budgeted.map(section => ({ id: section.data.memoryId })));
|
|
183
|
+
const rendered = budgeted.length;
|
|
184
|
+
const header = humanRecallHeader(result.query, result.capped, rendered, notes.length, result.total, result.scope);
|
|
185
|
+
const confirmed = budgeted.filter(section => section.data.trust === 'confirmed');
|
|
186
|
+
const suggested = budgeted.filter(section => section.data.trust === 'suggested');
|
|
187
|
+
const groups = [
|
|
188
|
+
confirmed.length ? ['Confirmed memory', confirmed] : null,
|
|
189
|
+
suggested.length ? ['Suggested memory — verify before relying', suggested] : null,
|
|
190
|
+
].filter(Boolean);
|
|
191
|
+
const body = groups.flatMap(([title, entries]) => [
|
|
192
|
+
title,
|
|
193
|
+
...entries.map(entry => entry.content),
|
|
194
|
+
]);
|
|
195
|
+
return { success: true, output: [header, ...body].join('\n') };
|
|
97
196
|
}
|
|
98
197
|
|
|
99
|
-
|
|
198
|
+
function emptyRecall(query) {
|
|
199
|
+
const reason = query
|
|
200
|
+
? `No notes match "${query}".`
|
|
201
|
+
: 'No notes remembered yet. Use "forge remember <note>" to add one.';
|
|
202
|
+
return { success: true, output: reason };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handler(args, flags, projectRoot, commandOpts = {}) {
|
|
100
206
|
const { query, limit, type, json } = parseArgs(args);
|
|
101
207
|
// `--all` is a GLOBAL boolean flag: in production bin/forge.js strips it from
|
|
102
208
|
// args and sets flags.all; on a direct handler call it may still be in args.
|
|
@@ -105,56 +211,28 @@ async function handler(args, flags, projectRoot) {
|
|
|
105
211
|
// A `--type` filter scans a generous recent window, then keeps only matching notes — the
|
|
106
212
|
// read stays entirely in the existing store (no schema change). `--limit` is re-applied
|
|
107
213
|
// AFTER filtering so it caps the typed result set, not the pre-filter scan.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
notes = notes.filter(entry => entry.type === type);
|
|
115
|
-
total = notes.length;
|
|
116
|
-
if (limit && notes.length > limit) {
|
|
117
|
-
notes = notes.slice(0, limit);
|
|
118
|
-
capped = true;
|
|
119
|
-
} else {
|
|
120
|
-
capped = false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
214
|
+
// Resolve once for the complete recall: selection, count, and advisory evidence share
|
|
215
|
+
// one cached driver/connection rather than each opening or migrating independently.
|
|
216
|
+
const recallStore = projectMemory.resolveStore(projectRoot);
|
|
217
|
+
const result = memoryRouter.recall(projectRoot, { query, limit, all, kind: type }, { store: recallStore });
|
|
218
|
+
const notes = result.notes.map(withType);
|
|
219
|
+
const recordUsage = createUsageRecorder(projectRoot, recallStore, commandOpts);
|
|
123
220
|
|
|
124
221
|
if (json) {
|
|
125
222
|
// Object (not a bare array) so programmatic consumers see the total and whether the
|
|
126
223
|
// result was truncated (raise --limit to page further).
|
|
224
|
+
recordUsage(notes);
|
|
127
225
|
return {
|
|
128
226
|
success: true,
|
|
129
|
-
output: `${JSON.stringify({ notes, total, capped, scope }, null, 2)}\n`,
|
|
227
|
+
output: `${JSON.stringify({ notes, total: result.total, capped: result.capped, scope: result.scope }, null, 2)}\n`,
|
|
130
228
|
};
|
|
131
229
|
}
|
|
132
230
|
|
|
133
231
|
if (notes.length === 0) {
|
|
134
|
-
|
|
135
|
-
? `No notes match "${query}".`
|
|
136
|
-
: 'No notes remembered yet. Use "forge remember <note>" to add one.';
|
|
137
|
-
return { success: true, output: reason };
|
|
232
|
+
return emptyRecall(query);
|
|
138
233
|
}
|
|
139
234
|
|
|
140
|
-
|
|
141
|
-
let header;
|
|
142
|
-
if (query) {
|
|
143
|
-
// BM25 returns at most `limit`; when full, signal it is the TOP-N, not the whole set.
|
|
144
|
-
header = capped
|
|
145
|
-
? `Top ${notes.length} note(s) matching "${query}" (raise --limit for more):`
|
|
146
|
-
: `${notes.length} note(s) matching "${query}":`;
|
|
147
|
-
} else if (capped) {
|
|
148
|
-
// Never a bare full dump: show the newest N and the true total.
|
|
149
|
-
header = `Showing ${notes.length} of ${total} ${noun} (newest first):`;
|
|
150
|
-
} else {
|
|
151
|
-
header = `${total} ${noun}:`;
|
|
152
|
-
}
|
|
153
|
-
const lines = notes.map(formatEntry);
|
|
154
|
-
return {
|
|
155
|
-
success: true,
|
|
156
|
-
output: [header, ...lines].join('\n'),
|
|
157
|
-
};
|
|
235
|
+
return renderHumanRecall(notes, result, recordUsage);
|
|
158
236
|
}
|
|
159
237
|
|
|
160
238
|
module.exports = {
|
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,14 @@ 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');
|
|
12
|
+
const { updateBunWorkflowPins } = require('../bun-workflow-pins');
|
|
11
13
|
|
|
12
14
|
// `forge release <id>` releases a claimed issue; `forge release check` runs the
|
|
13
15
|
// release-readiness gate. The two share the top-level verb, so this command
|
|
14
16
|
// dispatches `check` to the gate and routes everything else through the shared
|
|
15
17
|
// 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';
|
|
18
|
+
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow --expect-head <full-sha> | forge release update-bun-pins --expect-head <full-sha>';
|
|
17
19
|
|
|
18
20
|
async function runReleaseIssue(args, projectRoot, opts = {}) {
|
|
19
21
|
const resolved = withResolvedIssueBackend(projectRoot, opts);
|
|
@@ -44,18 +46,51 @@ function parseReleaseArgs(args = []) {
|
|
|
44
46
|
return false;
|
|
45
47
|
}
|
|
46
48
|
const previous = args[index - 1];
|
|
47
|
-
return previous !== '--target';
|
|
49
|
+
return previous !== '--target' && previous !== '--expect-head';
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
53
|
subcommand,
|
|
52
54
|
target: readOption(args, '--target', SUPPORTED_TARGET),
|
|
55
|
+
expectedHead: readOption(args, '--expect-head', null),
|
|
53
56
|
json: args.includes('--json'),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
async function handler(args, _flags, projectRoot, opts = {}) {
|
|
58
|
-
|
|
61
|
+
const parsed = parseReleaseArgs(args);
|
|
62
|
+
if (parsed.subcommand === 'update-bun-pins') {
|
|
63
|
+
const updater = opts.updateBunWorkflowPins || updateBunWorkflowPins;
|
|
64
|
+
const updated = await updater(projectRoot, {
|
|
65
|
+
env: opts.env,
|
|
66
|
+
kernelDeps: opts.kernelDeps,
|
|
67
|
+
expectedHead: parsed.expectedHead,
|
|
68
|
+
resolveHead: opts.resolveHead,
|
|
69
|
+
});
|
|
70
|
+
return updated.success
|
|
71
|
+
? {
|
|
72
|
+
success: true,
|
|
73
|
+
updated,
|
|
74
|
+
output: `Pinned ${updated.paths.length} workflows to Bun ${updated.version}.\n`,
|
|
75
|
+
}
|
|
76
|
+
: updated;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (parsed.subcommand === 'generate-npm-workflow') {
|
|
80
|
+
const generated = await generateNpmPublishWorkflow(projectRoot, {
|
|
81
|
+
env: opts.env,
|
|
82
|
+
kernelDeps: opts.kernelDeps,
|
|
83
|
+
expectedHead: parsed.expectedHead,
|
|
84
|
+
resolveHead: opts.resolveHead,
|
|
85
|
+
});
|
|
86
|
+
return generated.success
|
|
87
|
+
? {
|
|
88
|
+
success: true,
|
|
89
|
+
generated,
|
|
90
|
+
output: `Generated ${generated.path} (${generated.contentHash}).\n`,
|
|
91
|
+
}
|
|
92
|
+
: generated;
|
|
93
|
+
}
|
|
59
94
|
|
|
60
95
|
if (parsed.subcommand === 'regen-audit') {
|
|
61
96
|
// forge release regen-audit — rewrite the D20 kill-list from a live re-scan.
|
|
@@ -96,6 +131,7 @@ module.exports = {
|
|
|
96
131
|
usage,
|
|
97
132
|
flags: {
|
|
98
133
|
'--target <version>': 'Release target to check',
|
|
134
|
+
'--expect-head <full-sha>': 'Required exact HEAD for protected workflow generation',
|
|
99
135
|
'--json': 'Emit the readiness report as JSON',
|
|
100
136
|
},
|
|
101
137
|
handler,
|
package/lib/commands/remember.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const memoryRouter = require('../memory/router');
|
|
4
5
|
const { stripGlobalFlags } = require('../global-flags');
|
|
5
6
|
|
|
@@ -22,6 +23,7 @@ const STRUCTURED_FIELDS = [
|
|
|
22
23
|
// `--kind` (NOT `--type`): `--type` is a reserved GLOBAL flag that bin/forge.js hard-validates
|
|
23
24
|
// against workflow classifications (critical|standard|…), so it can never carry a note type.
|
|
24
25
|
const TYPE_TAG_PREFIX = 'type:';
|
|
26
|
+
const CONTENT_HASH_TAG_PREFIX = 'content-hash:';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Split positional note words from `--kind`, `--tag <label>`, the structured field flags,
|
|
@@ -105,15 +107,37 @@ async function handler(args, _flags, projectRoot) {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// The type rides as a reserved tag so it is stored and filterable without a schema change.
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
+
const contentHash = type === 'session-summary'
|
|
111
|
+
? crypto.createHash('sha256').update(JSON.stringify({
|
|
112
|
+
body,
|
|
113
|
+
tags: [...tags].sort((left, right) => left.localeCompare(right)),
|
|
114
|
+
type,
|
|
115
|
+
}), 'utf8').digest('hex')
|
|
116
|
+
: null;
|
|
117
|
+
const metadata = contentHash ? { kind: 'session-summary', content_hash: contentHash } : undefined;
|
|
118
|
+
const hashTag = contentHash ? `${CONTENT_HASH_TAG_PREFIX}${contentHash}` : null;
|
|
119
|
+
const allTags = [
|
|
120
|
+
...tags,
|
|
121
|
+
...(type ? [`${TYPE_TAG_PREFIX}${type}`] : []),
|
|
122
|
+
...(hashTag ? [hashTag] : []),
|
|
123
|
+
];
|
|
124
|
+
let entry;
|
|
125
|
+
if (hashTag) {
|
|
126
|
+
const matches = memoryRouter.recall(projectRoot, {
|
|
127
|
+
query: contentHash,
|
|
128
|
+
limit: 2,
|
|
129
|
+
all: true,
|
|
130
|
+
}).notes;
|
|
131
|
+
entry = matches.find(note => Array.isArray(note.tags) && note.tags.includes(hashTag));
|
|
132
|
+
}
|
|
133
|
+
if (!entry) entry = memoryRouter.append(projectRoot, body, { tags: allTags });
|
|
110
134
|
|
|
111
135
|
if (json) {
|
|
112
|
-
const payload = type ? { ...entry, type } : entry;
|
|
136
|
+
const payload = type ? { ...entry, type, metadata } : entry;
|
|
113
137
|
return { success: true, output: `${JSON.stringify(payload, null, 2)}\n` };
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
const userTags = entry.tags.filter(tag => !tag.startsWith(TYPE_TAG_PREFIX));
|
|
140
|
+
const userTags = entry.tags.filter(tag => !tag.startsWith(TYPE_TAG_PREFIX) && !tag.startsWith(CONTENT_HASH_TAG_PREFIX));
|
|
117
141
|
const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
|
|
118
142
|
const typePrefix = type ? `(${type}) ` : '';
|
|
119
143
|
return {
|
package/lib/commands/serve.js
CHANGED
|
@@ -31,6 +31,7 @@ const fs = require('node:fs');
|
|
|
31
31
|
const path = require('node:path');
|
|
32
32
|
const crypto = require('node:crypto');
|
|
33
33
|
const { spawn } = require('node:child_process');
|
|
34
|
+
const { githubWorkerEnvironment } = require('../pr-monitor/watch-lifecycle');
|
|
34
35
|
|
|
35
36
|
// Shared-machine hardening (single-instance lock, owner-only perms + audit,
|
|
36
37
|
// hash-chained mutation journal). No dependency on serve.js, so a top-level
|
|
@@ -237,12 +238,21 @@ async function routeMutation(verb, args, projectRoot, deps = {}) {
|
|
|
237
238
|
// Snapshot (GET /data.json) — regenerate on demand, debounced.
|
|
238
239
|
// ---------------------------------------------------------------------------
|
|
239
240
|
|
|
240
|
-
function defaultGenerate(projectRoot, dashboardDir) {
|
|
241
|
+
function defaultGenerate(projectRoot, dashboardDir, deps = {}) {
|
|
241
242
|
return new Promise((resolve, reject) => {
|
|
242
243
|
const script = path.join(dashboardDir, 'generate-snapshot.mjs');
|
|
243
|
-
const
|
|
244
|
+
const env = githubWorkerEnvironment(projectRoot, deps);
|
|
245
|
+
let child;
|
|
246
|
+
try {
|
|
247
|
+
child = (deps.spawn || spawn)(process.execPath, [script], {
|
|
248
|
+
cwd: projectRoot, stdio: 'ignore', windowsHide: true, ...(env ? { env } : {}),
|
|
249
|
+
});
|
|
250
|
+
} catch {
|
|
251
|
+
reject(new Error('snapshot generation failed'));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
244
254
|
const timer = setTimeout(() => { child.kill(); reject(new Error('snapshot generation timed out')); }, GEN_TIMEOUT_MS);
|
|
245
|
-
child.on('error', (
|
|
255
|
+
child.on('error', () => { clearTimeout(timer); reject(new Error('snapshot generation failed')); });
|
|
246
256
|
child.on('exit', (code) => {
|
|
247
257
|
clearTimeout(timer);
|
|
248
258
|
if (code === 0) resolve();
|
|
@@ -409,7 +419,7 @@ function buildContext(projectRoot, dashboardDir, token, deps = {}) {
|
|
|
409
419
|
buffer: null,
|
|
410
420
|
lastGen: 0,
|
|
411
421
|
inFlight: null,
|
|
412
|
-
generate: deps.generate || defaultGenerate,
|
|
422
|
+
generate: deps.generate || ((root, dir) => defaultGenerate(root, dir, deps)),
|
|
413
423
|
},
|
|
414
424
|
};
|
|
415
425
|
}
|
|
@@ -438,13 +448,16 @@ function browserOpener() {
|
|
|
438
448
|
return { file: '/usr/bin/xdg-open', args: [] };
|
|
439
449
|
}
|
|
440
450
|
|
|
441
|
-
function openBrowser(url) {
|
|
451
|
+
function openBrowser(url, projectRoot = process.cwd(), deps = {}) {
|
|
442
452
|
try {
|
|
443
453
|
const { file, args } = browserOpener();
|
|
454
|
+
const env = githubWorkerEnvironment(projectRoot, deps);
|
|
444
455
|
// windowsHide keeps this detached opener from flashing a console window on
|
|
445
456
|
// Windows (Node defaults windowsHide to false); background/detached spawns
|
|
446
457
|
// must stay silent (issue 931e7924).
|
|
447
|
-
const child = spawn(file, [...args, url], {
|
|
458
|
+
const child = (deps.spawn || spawn)(file, [...args, url], {
|
|
459
|
+
detached: true, stdio: 'ignore', windowsHide: true, ...(env ? { env } : {}),
|
|
460
|
+
});
|
|
448
461
|
// spawn() failures (e.g. the opener binary is missing) surface ASYNChronously
|
|
449
462
|
// as an 'error' event, NOT via the try/catch — an unhandled one would crash
|
|
450
463
|
// `forge serve`. Opening a browser is best-effort; swallow it (URL is printed).
|
|
@@ -480,7 +493,7 @@ function announceServe(server, token, options, resolve) {
|
|
|
480
493
|
`\nforge serve — interactive dashboard (loopback only)\n ${url}\n`
|
|
481
494
|
+ ' Token dies with this process. Press Ctrl-C to stop.\n\n',
|
|
482
495
|
);
|
|
483
|
-
if (options.open) openBrowser(url);
|
|
496
|
+
if (options.open) openBrowser(url, options.projectRoot, options.deps);
|
|
484
497
|
installServeShutdown(server, resolve, options.onClose);
|
|
485
498
|
}
|
|
486
499
|
|
|
@@ -535,7 +548,7 @@ function verifyJournalAtStartup(projectRoot, warn = (m) => process.stderr.write(
|
|
|
535
548
|
return result;
|
|
536
549
|
}
|
|
537
550
|
|
|
538
|
-
async function handler(args, flags = {}, projectRoot = process.cwd(),
|
|
551
|
+
async function handler(args, flags = {}, projectRoot = process.cwd(), opts = {}) {
|
|
539
552
|
const { port, open } = parseServeArgs(args, flags);
|
|
540
553
|
const dashboardDir = path.join(projectRoot, 'web', 'dashboard');
|
|
541
554
|
if (!fs.existsSync(path.join(dashboardDir, 'index.html'))) {
|
|
@@ -558,10 +571,12 @@ async function handler(args, flags = {}, projectRoot = process.cwd(), _opts = {}
|
|
|
558
571
|
verifyJournalAtStartup(projectRoot);
|
|
559
572
|
|
|
560
573
|
const token = mintToken();
|
|
561
|
-
const ctx = buildContext(projectRoot, dashboardDir, token);
|
|
574
|
+
const ctx = buildContext(projectRoot, dashboardDir, token, opts);
|
|
562
575
|
const server = createServeServer(ctx);
|
|
563
576
|
return startListening(server, port, token, {
|
|
564
577
|
open,
|
|
578
|
+
projectRoot,
|
|
579
|
+
deps: opts,
|
|
565
580
|
onClose: () => security.releaseLock(projectRoot),
|
|
566
581
|
});
|
|
567
582
|
}
|
|
@@ -575,6 +590,8 @@ module.exports = {
|
|
|
575
590
|
routeMutation,
|
|
576
591
|
parseServeArgs,
|
|
577
592
|
buildContext,
|
|
593
|
+
defaultGenerate,
|
|
594
|
+
openBrowser,
|
|
578
595
|
createServeServer,
|
|
579
596
|
verifyToken,
|
|
580
597
|
isLoopbackRequest,
|