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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { secureExecFileSync } = require('./shell-utils');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
createProtectedStateAuditRecord,
|
|
9
|
+
recordProtectedStateAuditEvent,
|
|
10
|
+
removeProtectedFile,
|
|
11
|
+
writeProtectedFile,
|
|
12
|
+
} = require('./protected-state-surfaces');
|
|
13
|
+
const {
|
|
14
|
+
completeNpmPublishWorkflowAuthorization,
|
|
15
|
+
issueNpmPublishWorkflowAuthorization,
|
|
16
|
+
} = require('./protected-state-authority');
|
|
17
|
+
|
|
18
|
+
const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
|
|
19
|
+
const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
20
|
+
const BUN_VERSION = /^bun@(\d+\.\d+\.\d+)$/.exec(require('../package.json').packageManager)?.[1];
|
|
21
|
+
if (!BUN_VERSION) throw new Error('package.json must pin an exact stable Bun version.');
|
|
22
|
+
|
|
23
|
+
function resolveCurrentHead(projectRoot, execGit = secureExecFileSync) {
|
|
24
|
+
return execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
25
|
+
cwd: projectRoot,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readNpmPublishWorkflow(projectRoot, revision, execGit = secureExecFileSync) {
|
|
33
|
+
const options = {
|
|
34
|
+
cwd: projectRoot,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
};
|
|
39
|
+
const indexed = revision === ':';
|
|
40
|
+
const entry = execGit('git', indexed
|
|
41
|
+
? ['ls-files', '--stage', '--', NPM_PUBLISH_WORKFLOW_PATH]
|
|
42
|
+
: ['ls-tree', revision, '--', NPM_PUBLISH_WORKFLOW_PATH], options).trim();
|
|
43
|
+
const expected = indexed
|
|
44
|
+
? /^100(?:644|755) [0-9a-f]+ 0\t\.github\/workflows\/npm-publish\.yml$/
|
|
45
|
+
: /^100(?:644|755) blob [0-9a-f]+\t\.github\/workflows\/npm-publish\.yml$/;
|
|
46
|
+
if (!expected.test(entry)) {
|
|
47
|
+
throw new Error(`${NPM_PUBLISH_WORKFLOW_PATH} is not one regular file ${indexed ? 'in the Git index' : `at ${revision}`}.`);
|
|
48
|
+
}
|
|
49
|
+
return Buffer.from(execGit('git', ['show', `${revision}${indexed ? '' : ':'}${NPM_PUBLISH_WORKFLOW_PATH}`], {
|
|
50
|
+
...options,
|
|
51
|
+
encoding: 'buffer',
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readSourceNpmPublishWorkflow(projectRoot, sourceHead, execGit = secureExecFileSync) {
|
|
56
|
+
return readNpmPublishWorkflow(projectRoot, sourceHead, execGit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readIndexedNpmPublishWorkflow(projectRoot, execGit = secureExecFileSync) {
|
|
60
|
+
return readNpmPublishWorkflow(projectRoot, ':', execGit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function verifyExpectedHead(projectRoot, expectedHead, resolveHead = resolveCurrentHead) {
|
|
64
|
+
if (!expectedHead) {
|
|
65
|
+
return { success: false, error: '--expect-head is required for protected workflow generation.' };
|
|
66
|
+
}
|
|
67
|
+
if (!FULL_COMMIT_SHA.test(expectedHead)) {
|
|
68
|
+
return { success: false, error: '--expect-head must be a full 40-character lowercase commit SHA.' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let currentHead;
|
|
72
|
+
try {
|
|
73
|
+
currentHead = resolveHead(projectRoot);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { success: false, error: `Could not resolve current HEAD: ${error.message}` };
|
|
76
|
+
}
|
|
77
|
+
if (!FULL_COMMIT_SHA.test(currentHead)) {
|
|
78
|
+
return { success: false, error: 'Current HEAD did not resolve to a full 40-character lowercase commit SHA.' };
|
|
79
|
+
}
|
|
80
|
+
if (currentHead !== expectedHead) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `Expected HEAD ${expectedHead} does not match current HEAD ${currentHead}.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { success: true, sourceHead: currentHead };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderNpmDistTagResolverScript() {
|
|
90
|
+
return `CORE_RE='(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
|
|
91
|
+
NUMERIC_ID_RE='(0|[1-9][0-9]*)'
|
|
92
|
+
BUILD_RE='(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?'
|
|
93
|
+
BETA_RE="^\${CORE_RE}-beta(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
|
|
94
|
+
RC_RE="^\${CORE_RE}-rc(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
|
|
95
|
+
STABLE_RE="^\${CORE_RE}\${BUILD_RE}$"
|
|
96
|
+
|
|
97
|
+
if [[ "$VERSION" =~ $BETA_RE ]]; then
|
|
98
|
+
echo "tag=beta" >> "$GITHUB_OUTPUT"
|
|
99
|
+
elif [[ "$VERSION" =~ $RC_RE ]]; then
|
|
100
|
+
echo "tag=rc" >> "$GITHUB_OUTPUT"
|
|
101
|
+
elif [[ "$VERSION" =~ $STABLE_RE ]]; then
|
|
102
|
+
echo "tag=latest" >> "$GITHUB_OUTPUT"
|
|
103
|
+
elif [[ "$VERSION" == *-* ]]; then
|
|
104
|
+
echo "::error::Unsupported npm prerelease: $VERSION"
|
|
105
|
+
exit 1
|
|
106
|
+
else
|
|
107
|
+
echo "::error::Invalid npm version: $VERSION"
|
|
108
|
+
exit 1
|
|
109
|
+
fi`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function snapshotWorkflow(projectRoot) {
|
|
113
|
+
const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
|
|
114
|
+
try {
|
|
115
|
+
const stat = fs.lstatSync(fullPath);
|
|
116
|
+
return {
|
|
117
|
+
fullPath,
|
|
118
|
+
existed: true,
|
|
119
|
+
content: stat.isFile() ? fs.readFileSync(fullPath) : null,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code === 'ENOENT') return { fullPath, existed: false, content: null };
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sameSnapshot(expected, actual) {
|
|
128
|
+
if (!expected || typeof expected !== 'object' || Array.isArray(expected)) return false;
|
|
129
|
+
if (typeof expected.existed !== 'boolean') return false;
|
|
130
|
+
if (expected.existed && !Buffer.isBuffer(expected.content)) return false;
|
|
131
|
+
if (!expected.existed && expected.content !== null) return false;
|
|
132
|
+
return expected.existed === actual.existed
|
|
133
|
+
&& (!expected.existed || expected.content.equals(actual.content));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function recoverWorkflow(snapshot, projectRoot, generatedContent, protectedWriter, protectedRemover, writeOptions) {
|
|
137
|
+
const recoveryOptions = {
|
|
138
|
+
...writeOptions,
|
|
139
|
+
operation: 'recover_npm_workflow',
|
|
140
|
+
expectedContent: generatedContent,
|
|
141
|
+
};
|
|
142
|
+
if (!snapshot.existed) {
|
|
143
|
+
return protectedRemover(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, recoveryOptions);
|
|
144
|
+
}
|
|
145
|
+
if (!Buffer.isBuffer(snapshot.content)) {
|
|
146
|
+
return { allowed: false, reason: 'Previous workflow was not a regular file' };
|
|
147
|
+
}
|
|
148
|
+
return protectedWriter(
|
|
149
|
+
projectRoot,
|
|
150
|
+
NPM_PUBLISH_WORKFLOW_PATH,
|
|
151
|
+
snapshot.content,
|
|
152
|
+
recoveryOptions,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function prepareWorkflowAuthorization(prepareAuthorization, projectRoot, params, authorizationOptions) {
|
|
157
|
+
try {
|
|
158
|
+
return await prepareAuthorization(projectRoot, params, authorizationOptions);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { success: false, error: error.message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function recordWorkflowAudit(auditWriter, auditRecord, projectRoot) {
|
|
165
|
+
try {
|
|
166
|
+
return auditWriter(auditRecord, { cwd: projectRoot });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { success: false, error: error.message };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderNpmPublishWorkflow(bunVersion = BUN_VERSION) {
|
|
173
|
+
if (!/^\d+\.\d+\.\d+$/.test(bunVersion)) throw new Error('npm workflow requires an exact stable Bun version.');
|
|
174
|
+
const distTagResolver = renderNpmDistTagResolverScript()
|
|
175
|
+
.split('\n')
|
|
176
|
+
.map(line => line ? ` ${line}` : '')
|
|
177
|
+
.join('\n');
|
|
178
|
+
return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
|
|
179
|
+
name: Bun Package
|
|
180
|
+
|
|
181
|
+
on:
|
|
182
|
+
release:
|
|
183
|
+
types: [published]
|
|
184
|
+
|
|
185
|
+
permissions:
|
|
186
|
+
contents: read
|
|
187
|
+
|
|
188
|
+
jobs:
|
|
189
|
+
resolve-release:
|
|
190
|
+
name: Resolve immutable release SHA
|
|
191
|
+
runs-on: ubuntu-latest
|
|
192
|
+
timeout-minutes: 10
|
|
193
|
+
permissions:
|
|
194
|
+
contents: read
|
|
195
|
+
outputs:
|
|
196
|
+
commitSha: \${{ steps.resolve.outputs.commitSha }}
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@v7
|
|
199
|
+
with:
|
|
200
|
+
ref: \${{ github.event.release.tag_name }}
|
|
201
|
+
fetch-depth: 0
|
|
202
|
+
persist-credentials: false
|
|
203
|
+
- name: Resolve release tag
|
|
204
|
+
id: resolve
|
|
205
|
+
shell: bash
|
|
206
|
+
env:
|
|
207
|
+
RELEASE_TAG: \${{ github.event.release.tag_name }}
|
|
208
|
+
run: |
|
|
209
|
+
set -euo pipefail
|
|
210
|
+
COMMIT_SHA="$(git rev-parse "\${RELEASE_TAG}^{commit}")"
|
|
211
|
+
if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
|
|
212
|
+
echo "::error::Release tag did not resolve to the checked out commit"
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
echo "commitSha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
|
|
216
|
+
|
|
217
|
+
build:
|
|
218
|
+
needs: resolve-release
|
|
219
|
+
runs-on: ubuntu-latest
|
|
220
|
+
timeout-minutes: 25
|
|
221
|
+
permissions:
|
|
222
|
+
contents: read
|
|
223
|
+
outputs:
|
|
224
|
+
commitSha: \${{ steps.evidence.outputs.commitSha }}
|
|
225
|
+
receipt: \${{ steps.evidence.outputs.receipt }}
|
|
226
|
+
receiptSubject: \${{ steps.evidence.outputs.receiptSubject }}
|
|
227
|
+
steps:
|
|
228
|
+
- uses: actions/checkout@v7
|
|
229
|
+
with:
|
|
230
|
+
ref: \${{ needs.resolve-release.outputs.commitSha }}
|
|
231
|
+
fetch-depth: 1
|
|
232
|
+
persist-credentials: false
|
|
233
|
+
- uses: actions/setup-node@v7
|
|
234
|
+
with:
|
|
235
|
+
node-version: 22
|
|
236
|
+
- uses: oven-sh/setup-bun@v2
|
|
237
|
+
with:
|
|
238
|
+
bun-version: ${bunVersion}
|
|
239
|
+
- run: bun install --frozen-lockfile
|
|
240
|
+
- name: Setup test fixtures
|
|
241
|
+
shell: bash
|
|
242
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
243
|
+
- name: Run complete release suite
|
|
244
|
+
shell: bash
|
|
245
|
+
run: |
|
|
246
|
+
set -euo pipefail
|
|
247
|
+
node scripts/test-full-suite.js --label-prefix release-full
|
|
248
|
+
- name: Emit attributable release suite receipt
|
|
249
|
+
id: evidence
|
|
250
|
+
env:
|
|
251
|
+
EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
|
|
252
|
+
run: node scripts/npm-release-receipt.js emit
|
|
253
|
+
|
|
254
|
+
publish-npm:
|
|
255
|
+
needs: [resolve-release, build]
|
|
256
|
+
runs-on: ubuntu-latest
|
|
257
|
+
timeout-minutes: 20
|
|
258
|
+
permissions:
|
|
259
|
+
contents: read
|
|
260
|
+
id-token: write
|
|
261
|
+
steps:
|
|
262
|
+
- uses: actions/checkout@v7
|
|
263
|
+
with:
|
|
264
|
+
ref: \${{ needs.resolve-release.outputs.commitSha }}
|
|
265
|
+
fetch-depth: 1
|
|
266
|
+
persist-credentials: false
|
|
267
|
+
- uses: actions/setup-node@v7
|
|
268
|
+
with:
|
|
269
|
+
node-version: 22
|
|
270
|
+
registry-url: https://registry.npmjs.org/
|
|
271
|
+
- name: Verify exact release suite receipt
|
|
272
|
+
env:
|
|
273
|
+
EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
|
|
274
|
+
VERIFIED_SHA: \${{ needs.build.outputs.commitSha }}
|
|
275
|
+
RECEIPT: \${{ needs.build.outputs.receipt }}
|
|
276
|
+
RECEIPT_SUBJECT: \${{ needs.build.outputs.receiptSubject }}
|
|
277
|
+
run: node scripts/npm-release-receipt.js verify
|
|
278
|
+
- uses: oven-sh/setup-bun@v2
|
|
279
|
+
with:
|
|
280
|
+
bun-version: ${bunVersion}
|
|
281
|
+
- run: bun install --frozen-lockfile
|
|
282
|
+
- name: Release tag matches package.json version
|
|
283
|
+
shell: bash
|
|
284
|
+
run: |
|
|
285
|
+
set -euo pipefail
|
|
286
|
+
PKG="$(node -p "require('./package.json').version")"
|
|
287
|
+
TAG="\${GITHUB_REF_NAME#v}"
|
|
288
|
+
if [ "$PKG" != "$TAG" ]; then
|
|
289
|
+
echo "::error::Release tag ($TAG) != package.json version ($PKG). Bump package.json before releasing."
|
|
290
|
+
exit 1
|
|
291
|
+
fi
|
|
292
|
+
- name: Release-readiness gate
|
|
293
|
+
run: node bin/forge.js release check --target "$(node -p "require('./package.json').version.split('-')[0]")"
|
|
294
|
+
- name: Verify package contents
|
|
295
|
+
run: npm pack --dry-run
|
|
296
|
+
- name: Ensure OIDC-capable npm
|
|
297
|
+
run: npm install --global npm@11.5.1 && npm --version
|
|
298
|
+
- name: Resolve npm dist-tag
|
|
299
|
+
id: dist-tag
|
|
300
|
+
shell: bash
|
|
301
|
+
run: |
|
|
302
|
+
set -euo pipefail
|
|
303
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
304
|
+
${distTagResolver}
|
|
305
|
+
- name: Publish to npm
|
|
306
|
+
shell: bash
|
|
307
|
+
run: |
|
|
308
|
+
set -euo pipefail
|
|
309
|
+
DIST_TAG="\${{ steps.dist-tag.outputs.tag }}"
|
|
310
|
+
case "$DIST_TAG" in
|
|
311
|
+
beta)
|
|
312
|
+
npm publish --provenance --tag beta
|
|
313
|
+
;;
|
|
314
|
+
rc)
|
|
315
|
+
npm publish --provenance --tag rc
|
|
316
|
+
;;
|
|
317
|
+
latest)
|
|
318
|
+
npm publish --provenance
|
|
319
|
+
;;
|
|
320
|
+
*)
|
|
321
|
+
echo "::error::Unsupported npm dist-tag: $DIST_TAG"
|
|
322
|
+
exit 1
|
|
323
|
+
;;
|
|
324
|
+
esac
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function generateNpmPublishWorkflow(projectRoot, options = {}) {
|
|
329
|
+
const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
|
|
330
|
+
if (!head.success) return head;
|
|
331
|
+
const sourceHead = head.sourceHead;
|
|
332
|
+
let targetBunVersion;
|
|
333
|
+
try {
|
|
334
|
+
targetBunVersion = options.targetBunVersion
|
|
335
|
+
|| require('./bun-workflow-pins').resolveBunVersionTarget(projectRoot, options).version;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { success: false, error: error.message };
|
|
338
|
+
}
|
|
339
|
+
const bunVersion = options.bunVersion || targetBunVersion;
|
|
340
|
+
if (bunVersion !== targetBunVersion) {
|
|
341
|
+
return { success: false, error: 'npm workflow Bun version must match the staged package.json Bun pin.' };
|
|
342
|
+
}
|
|
343
|
+
const content = renderNpmPublishWorkflow(bunVersion);
|
|
344
|
+
const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
|
|
345
|
+
const protectedWriter = options.writeProtectedFile || writeProtectedFile;
|
|
346
|
+
const protectedRemover = options.removeProtectedFile || removeProtectedFile;
|
|
347
|
+
const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
|
|
348
|
+
const prepareAuthorization = options.prepareNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
|
|
349
|
+
const activateAuthorization = options.activateNpmPublishWorkflowAuthorization || completeNpmPublishWorkflowAuthorization;
|
|
350
|
+
const snapshot = snapshotWorkflow(projectRoot);
|
|
351
|
+
if (options.expectedSnapshot && !sameSnapshot(options.expectedSnapshot, snapshot)) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: `${NPM_PUBLISH_WORKFLOW_PATH} changed after Bun pin batch preflight.`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const writeOptions = {
|
|
358
|
+
actor,
|
|
359
|
+
operation: 'generate_npm_workflow',
|
|
360
|
+
viaForgeApi: true,
|
|
361
|
+
surface: 'workflows',
|
|
362
|
+
};
|
|
363
|
+
const trustedAuthorization = await prepareWorkflowAuthorization(
|
|
364
|
+
prepareAuthorization,
|
|
365
|
+
projectRoot,
|
|
366
|
+
{ actor, sourceHead, bunVersion, targetBunVersion },
|
|
367
|
+
{ deps: options.kernelDeps, capabilityId: options.capabilityId },
|
|
368
|
+
);
|
|
369
|
+
if (!trustedAuthorization.success) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
|
|
373
|
+
trustedAuthorization,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let write;
|
|
378
|
+
try {
|
|
379
|
+
write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, {
|
|
380
|
+
...writeOptions,
|
|
381
|
+
expectedContent: snapshot.existed ? snapshot.content : null,
|
|
382
|
+
...(options.beforeAtomicCommit ? { beforeAtomicCommit: options.beforeAtomicCommit } : {}),
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return { success: false, error: error.message };
|
|
386
|
+
}
|
|
387
|
+
if (!write.allowed) {
|
|
388
|
+
return { success: false, error: write.reason, write };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const auditRecord = createProtectedStateAuditRecord({
|
|
392
|
+
actor,
|
|
393
|
+
surface: 'workflows',
|
|
394
|
+
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
395
|
+
content,
|
|
396
|
+
operation: 'generate_npm_workflow',
|
|
397
|
+
viaForgeApi: true,
|
|
398
|
+
sourceHead,
|
|
399
|
+
});
|
|
400
|
+
const audit = recordWorkflowAudit(auditWriter, auditRecord, projectRoot);
|
|
401
|
+
if (!audit.success) {
|
|
402
|
+
let recovery;
|
|
403
|
+
try {
|
|
404
|
+
recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
recovery = { allowed: false, reason: error.message };
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
success: false,
|
|
410
|
+
error: `Could not record protected-state authorization: ${audit.error}`,
|
|
411
|
+
audit,
|
|
412
|
+
recovery,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let writeCompletion;
|
|
417
|
+
try {
|
|
418
|
+
writeCompletion = await activateAuthorization(
|
|
419
|
+
projectRoot,
|
|
420
|
+
{ actor, sourceHead, capabilityId: trustedAuthorization.capabilityId, bunVersion, targetBunVersion },
|
|
421
|
+
{ deps: options.kernelDeps },
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
writeCompletion = { success: false, error: error.message };
|
|
425
|
+
}
|
|
426
|
+
if (!writeCompletion.success) {
|
|
427
|
+
let recovery;
|
|
428
|
+
try {
|
|
429
|
+
recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
recovery = { allowed: false, reason: error.message };
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
error: `Could not complete trusted protected-state authorization: ${writeCompletion.error}`,
|
|
436
|
+
audit,
|
|
437
|
+
trustedAuthorization,
|
|
438
|
+
writeCompletion,
|
|
439
|
+
recovery,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
success: true,
|
|
445
|
+
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
446
|
+
sourceHead,
|
|
447
|
+
bunVersion,
|
|
448
|
+
contentHash: write.contentHash,
|
|
449
|
+
write,
|
|
450
|
+
audit,
|
|
451
|
+
trustedAuthorization,
|
|
452
|
+
writeCompletion,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = {
|
|
457
|
+
NPM_PUBLISH_WORKFLOW_PATH,
|
|
458
|
+
readIndexedNpmPublishWorkflow,
|
|
459
|
+
readSourceNpmPublishWorkflow,
|
|
460
|
+
renderNpmDistTagResolverScript,
|
|
461
|
+
renderNpmPublishWorkflow,
|
|
462
|
+
resolveCurrentHead,
|
|
463
|
+
verifyExpectedHead,
|
|
464
|
+
generateNpmPublishWorkflow,
|
|
465
|
+
};
|
package/lib/orientation.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { fenceUntrusted } = require('./untrusted-content');
|
|
6
|
+
const { runIssueOperation: defaultRunIssueOperation } = require('./forge-issues');
|
|
7
|
+
const { memoryTrustStatus } = require('./memory-recall');
|
|
6
8
|
|
|
7
9
|
const DEFAULT_BUDGET_TOKENS = 2000;
|
|
8
10
|
const MIN_BUDGET_TOKENS = 40;
|
|
@@ -43,25 +45,6 @@ function readJsonFile(projectRoot, relativeFilePath) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function readJsonl(projectRoot, relativeFilePath) {
|
|
47
|
-
const text = readText(projectRoot, relativeFilePath);
|
|
48
|
-
if (!text) return [];
|
|
49
|
-
return text
|
|
50
|
-
.split(/\r?\n/)
|
|
51
|
-
.map(line => line.trim())
|
|
52
|
-
.filter(Boolean)
|
|
53
|
-
.map((line, index) => {
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(line);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return {
|
|
58
|
-
_parseError: true,
|
|
59
|
-
line: index + 1,
|
|
60
|
-
error: error.message,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
48
|
|
|
66
49
|
function firstLine(value) {
|
|
67
50
|
return String(value || '')
|
|
@@ -426,17 +409,43 @@ function buildMemorySection(projectRoot, options = {}) {
|
|
|
426
409
|
const result = memoryRouter.recall(projectRoot, { limit: MEMORY_SECTION_NOTE_LIMIT }, { store });
|
|
427
410
|
const notes = Array.isArray(result && result.notes) ? result.notes : [];
|
|
428
411
|
if (notes.length === 0) return [];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
412
|
+
const groups = [
|
|
413
|
+
{
|
|
414
|
+
id: 'remembered_notes',
|
|
415
|
+
title: 'Confirmed Memory',
|
|
416
|
+
trust: 'confirmed',
|
|
417
|
+
priority: 45,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'suggested_memory',
|
|
421
|
+
title: 'Suggested Memory — Verify Before Relying',
|
|
422
|
+
trust: 'suggested',
|
|
423
|
+
priority: 46,
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
return groups.map(group => {
|
|
427
|
+
const content = notes
|
|
428
|
+
.filter(note => memoryTrustStatus({
|
|
429
|
+
tags: note.tags,
|
|
430
|
+
sourceAgent: note.sourceAgent,
|
|
431
|
+
value: note.machine ? {} : note.note,
|
|
432
|
+
}) === group.trust)
|
|
433
|
+
.map(formatMemoryNote)
|
|
434
|
+
.filter(line => estimateTokens(line) <= DEFAULT_BUDGET_TOKENS)
|
|
435
|
+
.join('\n');
|
|
436
|
+
if (!content) return null;
|
|
437
|
+
return buildSection({
|
|
438
|
+
id: group.id,
|
|
439
|
+
title: group.title,
|
|
440
|
+
content,
|
|
441
|
+
sources: [source('kernel.remembered_notes', 'kernel_memory', 'project_memory', group.id)],
|
|
442
|
+
priority: group.priority,
|
|
443
|
+
preserve: false,
|
|
444
|
+
// Remembered notes are untrusted DATA (a planted note must not read as directives).
|
|
445
|
+
// Raw here; the assembler fences it AFTER applyBudget truncation.
|
|
446
|
+
untrustedSource: 'memory',
|
|
447
|
+
});
|
|
448
|
+
}).filter(Boolean);
|
|
440
449
|
} catch {
|
|
441
450
|
return [];
|
|
442
451
|
} finally {
|
|
@@ -470,8 +479,13 @@ function openMemoryStore(projectRoot) {
|
|
|
470
479
|
/** Render one recall note as a compact `- [date] text` line. */
|
|
471
480
|
function formatMemoryNote(note) {
|
|
472
481
|
const date = typeof note.timestamp === 'string' && note.timestamp ? note.timestamp.slice(0, 10) : '';
|
|
473
|
-
const
|
|
474
|
-
|
|
482
|
+
const trust = memoryTrustStatus({
|
|
483
|
+
tags: note.tags,
|
|
484
|
+
sourceAgent: note.sourceAgent,
|
|
485
|
+
value: note.machine ? {} : note.note,
|
|
486
|
+
});
|
|
487
|
+
const sourceAgent = note.sourceAgent || 'unknown';
|
|
488
|
+
return `- [source=${sourceAgent} trust=${trust} updated=${date || 'unknown'}] ${note.note}`;
|
|
475
489
|
}
|
|
476
490
|
|
|
477
491
|
function buildQueueSections() {
|
|
@@ -671,7 +685,8 @@ const PRIME_KEY_COMMANDS_CONTENT = [
|
|
|
671
685
|
'forge upgrade [--dry-run] — preview/self-heal safe Forge upgrade readiness',
|
|
672
686
|
'forge gate <verb> <gate-id> — toggle a workflow gate, or approve/reject a human gate',
|
|
673
687
|
'forge role <role> --use <skill> — bind a role to a skill/ideology',
|
|
674
|
-
'forge merge --auto <pr> — opt-in
|
|
688
|
+
'forge merge --auto <pr> --expect-head <full-sha> --issue <issue-id> — opt-in guarded merge (off by default)',
|
|
689
|
+
'post-ship loop: forge ship → forge shepherd <pr> --pull --json → guarded forge merge with exact head + owned issue',
|
|
675
690
|
].map(line => `- ${line}`).join('\n');
|
|
676
691
|
|
|
677
692
|
function buildPrimeKeyCommandsSection() {
|
|
@@ -685,14 +700,24 @@ function buildPrimeKeyCommandsSection() {
|
|
|
685
700
|
});
|
|
686
701
|
}
|
|
687
702
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
703
|
+
// Read a single issue from the Kernel (the sole issue-state authority). The `show`
|
|
704
|
+
// contract returns the issue summary DIRECTLY as `result.data` (ISSUE_SUMMARY_SCHEMA),
|
|
705
|
+
// NOT `result.data.issue` — the nested shape belongs to the mutation envelope. Resilient
|
|
706
|
+
// by contract: any read failure degrades to `null` (not-found) so orientation never crashes.
|
|
707
|
+
async function findIssue(projectRoot, issueId, runIssueOperation = defaultRunIssueOperation) {
|
|
708
|
+
try {
|
|
709
|
+
const result = await runIssueOperation('show', [issueId], projectRoot, { issueBackend: 'kernel' });
|
|
710
|
+
if (result && result.ok && result.data && result.data.id) {
|
|
711
|
+
return result.data;
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
// best-effort: a kernel read failure surfaces as "not found", never a thrown error
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
692
717
|
}
|
|
693
718
|
|
|
694
|
-
function buildIssueSection(projectRoot, issueId) {
|
|
695
|
-
const issue = findIssue(projectRoot, issueId);
|
|
719
|
+
async function buildIssueSection(projectRoot, issueId, options = {}) {
|
|
720
|
+
const issue = await findIssue(projectRoot, issueId, options.runIssueOperation);
|
|
696
721
|
const content = issue
|
|
697
722
|
? [
|
|
698
723
|
`id: ${issue.id}`,
|
|
@@ -700,7 +725,7 @@ function buildIssueSection(projectRoot, issueId) {
|
|
|
700
725
|
issue.status ? `status: ${issue.status}` : null,
|
|
701
726
|
issue.description ? `description: ${issue.description}` : null,
|
|
702
727
|
].filter(Boolean).join('\n')
|
|
703
|
-
: `id: ${issueId}\nstatus: unknown\nIssue not found in
|
|
728
|
+
: `id: ${issueId}\nstatus: unknown\nIssue not found in the kernel.`;
|
|
704
729
|
|
|
705
730
|
return {
|
|
706
731
|
issue: issue ? {
|
|
@@ -716,19 +741,19 @@ function buildIssueSection(projectRoot, issueId) {
|
|
|
716
741
|
id: 'issue_summary',
|
|
717
742
|
title: 'Issue Summary',
|
|
718
743
|
content,
|
|
719
|
-
sources: [source('
|
|
744
|
+
sources: [source('kernel', 'kernel', 'issue_read', 'issue_summary')],
|
|
720
745
|
priority: 5,
|
|
721
746
|
preserve: true,
|
|
722
747
|
}),
|
|
723
748
|
};
|
|
724
749
|
}
|
|
725
750
|
|
|
726
|
-
function buildIssueRecap(projectRoot, issueId, options = {}) {
|
|
751
|
+
async function buildIssueRecap(projectRoot, issueId, options = {}) {
|
|
727
752
|
if (!issueId || typeof issueId !== 'string') {
|
|
728
753
|
throw new Error('Issue id is required for issue-scoped recap.');
|
|
729
754
|
}
|
|
730
755
|
|
|
731
|
-
const issueResult = buildIssueSection(projectRoot, issueId);
|
|
756
|
+
const issueResult = await buildIssueSection(projectRoot, issueId, options);
|
|
732
757
|
const sections = [
|
|
733
758
|
issueResult.section,
|
|
734
759
|
...buildProjectDesignSections(projectRoot),
|
package/lib/package-root.js
CHANGED
package/lib/plugin-catalog.js
CHANGED
|
@@ -91,15 +91,25 @@ const CATALOG = Object.freeze({
|
|
|
91
91
|
},
|
|
92
92
|
|
|
93
93
|
// ── Plan ──
|
|
94
|
-
|
|
95
|
-
name: '
|
|
94
|
+
'forge-kernel-issues': {
|
|
95
|
+
name: 'Forge Kernel issues',
|
|
96
96
|
type: 'cli',
|
|
97
97
|
tier: 'free',
|
|
98
98
|
stage: 'plan',
|
|
99
|
-
description: '
|
|
99
|
+
description: 'Built-in issue tracker backing /plan — forge issue create, forge ready, forge claim',
|
|
100
100
|
detectWhen: [],
|
|
101
|
-
install: { method: '
|
|
101
|
+
install: { method: 'config', cmd: 'built-in (forge issue create)' },
|
|
102
102
|
},
|
|
103
|
+
'mermaid-cli': {
|
|
104
|
+
name: 'Mermaid CLI',
|
|
105
|
+
type: 'cli',
|
|
106
|
+
tier: 'free',
|
|
107
|
+
stage: 'plan',
|
|
108
|
+
description: 'Render Mermaid architecture diagrams from design docs to SVG/PNG',
|
|
109
|
+
detectWhen: [],
|
|
110
|
+
install: { method: 'npm', cmd: 'bun add -D @mermaid-js/mermaid-cli', dev: true },
|
|
111
|
+
},
|
|
112
|
+
|
|
103
113
|
// ── Dev ──
|
|
104
114
|
'typescript-lsp': {
|
|
105
115
|
name: 'TypeScript LSP',
|