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,461 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { randomUUID } = require('node:crypto');
|
|
6
|
+
const { secureExecFileSync } = require('./shell-utils');
|
|
7
|
+
const {
|
|
8
|
+
createProtectedStateAuditRecord,
|
|
9
|
+
recordProtectedStateAuditEvent,
|
|
10
|
+
removeProtectedFile,
|
|
11
|
+
writeProtectedFile,
|
|
12
|
+
} = require('./protected-state-surfaces');
|
|
13
|
+
|
|
14
|
+
const EXACT_BUN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
15
|
+
const BUN_WORKFLOW_SPECS = Object.freeze([
|
|
16
|
+
{ path: '.github/workflows/build-binary.yml', key: 'BUN_VERSION', count: 1 },
|
|
17
|
+
{ path: '.github/workflows/docs-validation.yml', key: 'bun-version', count: 2 },
|
|
18
|
+
{ path: '.github/workflows/eslint.yml', key: 'bun-version', count: 1 },
|
|
19
|
+
{ path: '.github/workflows/pr-monitor.yml', key: 'bun-version', count: 1 },
|
|
20
|
+
{ path: '.github/workflows/required-checks-bypass.yml', key: 'bun-version', count: 1 },
|
|
21
|
+
{ path: '.github/workflows/size-check.yml', key: 'bun-version', count: 1 },
|
|
22
|
+
{ path: '.github/workflows/test.yml', key: 'bun-version', count: 8 },
|
|
23
|
+
{ path: '.github/workflows/yaml-lint.yml', key: 'bun-version', count: 1 },
|
|
24
|
+
].map(Object.freeze));
|
|
25
|
+
|
|
26
|
+
function workflowSpec(workflowPath) {
|
|
27
|
+
return BUN_WORKFLOW_SPECS.find(spec => spec.path === workflowPath) || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readPinnedBunVersion(packageContent) {
|
|
31
|
+
let manifest;
|
|
32
|
+
try {
|
|
33
|
+
manifest = JSON.parse(Buffer.from(packageContent).toString('utf8'));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`Could not parse package.json: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
const match = /^bun@(\d+\.\d+\.\d+)$/.exec(manifest.packageManager || '');
|
|
38
|
+
if (!match) throw new Error('package.json packageManager must pin one exact stable Bun version.');
|
|
39
|
+
return match[1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderBunWorkflowPin(workflowPath, sourceContent, version) {
|
|
43
|
+
const spec = workflowSpec(workflowPath);
|
|
44
|
+
if (!spec) throw new Error(`${workflowPath} is not an allowlisted Bun workflow.`);
|
|
45
|
+
if (!EXACT_BUN_VERSION.test(version)) throw new Error('Bun workflow pins require an exact stable version.');
|
|
46
|
+
|
|
47
|
+
let replacements = 0;
|
|
48
|
+
const source = Buffer.from(sourceContent).toString('utf8');
|
|
49
|
+
const key = spec.key === 'BUN_VERSION' ? 'BUN_VERSION' : 'bun-version';
|
|
50
|
+
const pattern = new RegExp(`^([ \\t]*${key}:[ \\t]*)(?:latest|\\d+\\.\\d+\\.\\d+)([ \\t]*\\r?)$`, 'gm');
|
|
51
|
+
const rendered = source.replace(pattern, (_match, prefix, suffix) => {
|
|
52
|
+
replacements += 1;
|
|
53
|
+
return `${prefix}${version}${suffix}`;
|
|
54
|
+
});
|
|
55
|
+
if (replacements !== spec.count) {
|
|
56
|
+
throw new Error(`${workflowPath} must contain exactly ${spec.count} literal ${key} pin${spec.count === 1 ? '' : 's'}; found ${replacements}.`);
|
|
57
|
+
}
|
|
58
|
+
return Buffer.isBuffer(sourceContent) ? Buffer.from(rendered, 'utf8') : rendered;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readSourceWorkflow(projectRoot, sourceHead, workflowPath, execGit = secureExecFileSync) {
|
|
62
|
+
if (!workflowSpec(workflowPath)) throw new Error(`${workflowPath} is not an allowlisted Bun workflow.`);
|
|
63
|
+
const gitOptions = {
|
|
64
|
+
cwd: projectRoot,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
};
|
|
69
|
+
const treeEntry = execGit('git', ['ls-tree', sourceHead, '--', workflowPath], gitOptions).trim();
|
|
70
|
+
if (!/^100(?:644|755) blob [0-9a-f]+\t/.test(treeEntry)) {
|
|
71
|
+
throw new Error(`${workflowPath} is not a regular workflow file at ${sourceHead}.`);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.from(execGit('git', ['show', `${sourceHead}:${workflowPath}`], {
|
|
74
|
+
...gitOptions,
|
|
75
|
+
encoding: 'buffer',
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveCurrentHead(projectRoot, execGit = secureExecFileSync) {
|
|
80
|
+
return execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
81
|
+
cwd: projectRoot,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
}).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readIndexedWorkflow(projectRoot, workflowPath, execGit = secureExecFileSync) {
|
|
89
|
+
if (!workflowSpec(workflowPath)) throw new Error(`${workflowPath} is not an allowlisted Bun workflow.`);
|
|
90
|
+
const options = {
|
|
91
|
+
cwd: projectRoot,
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
};
|
|
96
|
+
const entry = execGit('git', ['ls-files', '--stage', '--', workflowPath], options).trim();
|
|
97
|
+
if (!/^100(?:644|755) [0-9a-f]+ 0\t/.test(entry)) {
|
|
98
|
+
throw new Error(`${workflowPath} is not one regular file in the Git index.`);
|
|
99
|
+
}
|
|
100
|
+
return Buffer.from(execGit('git', ['show', `:${workflowPath}`], { ...options, encoding: 'buffer' }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readIndexedPackageManifest(projectRoot, execGit = secureExecFileSync) {
|
|
104
|
+
const options = {
|
|
105
|
+
cwd: projectRoot,
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
};
|
|
110
|
+
const entry = execGit('git', ['ls-files', '--stage', '--', 'package.json'], options).trim();
|
|
111
|
+
if (!/^100(?:644|755) [0-9a-f]+ 0\tpackage\.json$/.test(entry)) {
|
|
112
|
+
throw new Error('package.json is not one regular file in the Git index.');
|
|
113
|
+
}
|
|
114
|
+
return Buffer.from(execGit('git', ['show', ':package.json'], { ...options, encoding: 'buffer' }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readSourcePackageManifest(projectRoot, sourceHead, execGit = secureExecFileSync) {
|
|
118
|
+
const options = {
|
|
119
|
+
cwd: projectRoot,
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
};
|
|
124
|
+
const entry = execGit('git', ['ls-tree', sourceHead, '--', 'package.json'], options).trim();
|
|
125
|
+
if (!/^100(?:644|755) blob [0-9a-f]+\tpackage\.json$/.test(entry)) {
|
|
126
|
+
throw new Error(`package.json is not a regular file at ${sourceHead}.`);
|
|
127
|
+
}
|
|
128
|
+
return Buffer.from(execGit('git', ['show', `${sourceHead}:package.json`], { ...options, encoding: 'buffer' }));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveBunVersionTarget(projectRoot, options = {}) {
|
|
132
|
+
const indexReader = options.readIndexedPackageManifest || readIndexedPackageManifest;
|
|
133
|
+
const indexed = Buffer.from(indexReader(projectRoot, options.execGit));
|
|
134
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
135
|
+
const stat = fs.lstatSync(packagePath);
|
|
136
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('package.json must be a regular file.');
|
|
137
|
+
const current = fs.readFileSync(packagePath);
|
|
138
|
+
const version = readPinnedBunVersion(indexed);
|
|
139
|
+
if (readPinnedBunVersion(current) !== version) {
|
|
140
|
+
throw new Error('package.json Bun pin differs from the Git index; stage the intended manifest before updating workflow pins.');
|
|
141
|
+
}
|
|
142
|
+
return { version };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function deriveBunWorkflowUpdate(projectRoot, sourceHead, workflowPath, options = {}) {
|
|
146
|
+
const version = options.version || resolveBunVersionTarget(projectRoot, options).version;
|
|
147
|
+
const sourceReader = options.readSourceWorkflow || readSourceWorkflow;
|
|
148
|
+
const baseline = Buffer.from(sourceReader(projectRoot, sourceHead, workflowPath, options.execGit));
|
|
149
|
+
return {
|
|
150
|
+
path: workflowPath,
|
|
151
|
+
version,
|
|
152
|
+
baseline,
|
|
153
|
+
content: Buffer.from(renderBunWorkflowPin(workflowPath, baseline, version)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readRegularWorkflow(projectRoot, workflowPath) {
|
|
158
|
+
const fullPath = path.join(projectRoot, workflowPath);
|
|
159
|
+
const stat = fs.lstatSync(fullPath);
|
|
160
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${workflowPath} must be a regular file.`);
|
|
161
|
+
return fs.readFileSync(fullPath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function snapshotFile(projectRoot, filePath) {
|
|
165
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
166
|
+
try {
|
|
167
|
+
const stat = fs.lstatSync(fullPath);
|
|
168
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${filePath} must be a regular file.`);
|
|
169
|
+
return { existed: true, content: fs.readFileSync(fullPath) };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === 'ENOENT') return { existed: false, content: null };
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sameSnapshot(left, right) {
|
|
177
|
+
return left.existed === right.existed
|
|
178
|
+
&& (!left.existed || left.content.equals(right.content));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function recoverAttempt(update, projectRoot, protectedWriter, protectedRemover) {
|
|
182
|
+
try {
|
|
183
|
+
const current = snapshotFile(projectRoot, update.path);
|
|
184
|
+
if (sameSnapshot(current, update.original)) return { allowed: true, action: 'unchanged' };
|
|
185
|
+
if (!current.existed || !current.content.equals(update.content)) {
|
|
186
|
+
return { allowed: false, reason: 'File changed after the Bun pin write; concurrent content was preserved.' };
|
|
187
|
+
}
|
|
188
|
+
const recoveryOptions = {
|
|
189
|
+
...update.writeOptions,
|
|
190
|
+
operation: update.recoveryOperation,
|
|
191
|
+
expectedContent: update.content,
|
|
192
|
+
};
|
|
193
|
+
return update.original.existed
|
|
194
|
+
? protectedWriter(projectRoot, update.path, update.original.content, recoveryOptions)
|
|
195
|
+
: protectedRemover(projectRoot, update.path, recoveryOptions);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return { allowed: false, reason: error.message };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function recoverAttempts(attempts, projectRoot, protectedWriter, protectedRemover) {
|
|
202
|
+
const files = [...attempts].reverse().map(update => ({
|
|
203
|
+
path: update.path,
|
|
204
|
+
...recoverAttempt(update, projectRoot, protectedWriter, protectedRemover),
|
|
205
|
+
}));
|
|
206
|
+
return { allowed: files.every(file => file.allowed), files };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function failedUpdate(error, results, attempts, authorizations, projectRoot, context, extra = {}) {
|
|
210
|
+
let authority;
|
|
211
|
+
try {
|
|
212
|
+
authority = await context.cancelAuthorizations(projectRoot, authorizations, { deps: context.kernelDeps });
|
|
213
|
+
} catch (cancelError) {
|
|
214
|
+
authority = { success: false, error: cancelError.message, results: [] };
|
|
215
|
+
}
|
|
216
|
+
const files = recoverAttempts(attempts, projectRoot, context.protectedWriter, context.protectedRemover);
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
error,
|
|
220
|
+
results,
|
|
221
|
+
recovery: {
|
|
222
|
+
allowed: authority.success && files.allowed,
|
|
223
|
+
authority,
|
|
224
|
+
files: files.files,
|
|
225
|
+
},
|
|
226
|
+
...extra,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function applyWorkflowUpdate(projectRoot, update, context) {
|
|
231
|
+
const authorityRecord = {
|
|
232
|
+
actor: context.actor,
|
|
233
|
+
path: update.path,
|
|
234
|
+
sourceHead: context.sourceHead,
|
|
235
|
+
capabilityId: context.createCapabilityId(),
|
|
236
|
+
};
|
|
237
|
+
context.authorizations.push(authorityRecord);
|
|
238
|
+
let authorization;
|
|
239
|
+
try {
|
|
240
|
+
authorization = await context.issueAuthorization(projectRoot, {
|
|
241
|
+
actor: context.actor,
|
|
242
|
+
path: update.path,
|
|
243
|
+
sourceHead: context.sourceHead,
|
|
244
|
+
targetBunVersion: context.version,
|
|
245
|
+
}, { deps: context.kernelDeps, capabilityId: authorityRecord.capabilityId });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
authorization = { success: false, error: error.message };
|
|
248
|
+
}
|
|
249
|
+
if (!authorization.success) {
|
|
250
|
+
return { success: false, error: `Could not authorize ${update.path}: ${authorization.error}` };
|
|
251
|
+
}
|
|
252
|
+
if (authorization.capabilityId !== authorityRecord.capabilityId) {
|
|
253
|
+
return { success: false, error: `Could not authorize ${update.path}: authority returned a different capability id.` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const writeOptions = {
|
|
257
|
+
actor: context.actor,
|
|
258
|
+
operation: 'update_bun_workflow_pin',
|
|
259
|
+
viaForgeApi: true,
|
|
260
|
+
surface: 'workflows',
|
|
261
|
+
expectedContent: update.current,
|
|
262
|
+
};
|
|
263
|
+
context.attempts.push({
|
|
264
|
+
...update,
|
|
265
|
+
original: { existed: true, content: update.current },
|
|
266
|
+
writeOptions,
|
|
267
|
+
recoveryOperation: 'recover_bun_workflow_pin',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
let write;
|
|
271
|
+
try {
|
|
272
|
+
write = context.protectedWriter(projectRoot, update.path, update.content, writeOptions);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { success: false, error: error.message };
|
|
275
|
+
}
|
|
276
|
+
if (!write.allowed) return { success: false, error: write.reason };
|
|
277
|
+
|
|
278
|
+
let audit;
|
|
279
|
+
try {
|
|
280
|
+
audit = context.auditWriter(createProtectedStateAuditRecord({
|
|
281
|
+
actor: context.actor,
|
|
282
|
+
surface: 'workflows',
|
|
283
|
+
path: update.path,
|
|
284
|
+
content: update.content,
|
|
285
|
+
operation: 'update_bun_workflow_pin',
|
|
286
|
+
viaForgeApi: true,
|
|
287
|
+
sourceHead: context.sourceHead,
|
|
288
|
+
}), { cwd: projectRoot });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
audit = { success: false, error: error.message };
|
|
291
|
+
}
|
|
292
|
+
if (!audit.success) return { success: false, error: `Could not record ${update.path}: ${audit.error}` };
|
|
293
|
+
|
|
294
|
+
let completion;
|
|
295
|
+
try {
|
|
296
|
+
completion = await context.completeAuthorization(projectRoot, {
|
|
297
|
+
actor: context.actor,
|
|
298
|
+
path: update.path,
|
|
299
|
+
sourceHead: context.sourceHead,
|
|
300
|
+
capabilityId: authorization.capabilityId,
|
|
301
|
+
targetBunVersion: context.version,
|
|
302
|
+
}, { deps: context.kernelDeps });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
completion = { success: false, error: error.message };
|
|
305
|
+
}
|
|
306
|
+
if (!completion.success) return { success: false, error: `Could not complete ${update.path}: ${completion.error}` };
|
|
307
|
+
|
|
308
|
+
return { success: true, result: { path: update.path, write, authorization, completion } };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function updateBunWorkflowPins(projectRoot, options = {}) {
|
|
312
|
+
const npmWorkflow = require('./npm-publish-workflow');
|
|
313
|
+
const { verifyExpectedHead } = npmWorkflow;
|
|
314
|
+
const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
|
|
315
|
+
if (!head.success) return head;
|
|
316
|
+
const sourceHead = head.sourceHead;
|
|
317
|
+
const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'forge-release';
|
|
318
|
+
|
|
319
|
+
let updates;
|
|
320
|
+
let npmUpdate;
|
|
321
|
+
try {
|
|
322
|
+
const target = resolveBunVersionTarget(projectRoot, options);
|
|
323
|
+
updates = BUN_WORKFLOW_SPECS.map(spec => {
|
|
324
|
+
const update = deriveBunWorkflowUpdate(projectRoot, sourceHead, spec.path, { ...options, version: target.version });
|
|
325
|
+
const current = readRegularWorkflow(projectRoot, spec.path);
|
|
326
|
+
const indexReader = options.readIndexedWorkflow || readIndexedWorkflow;
|
|
327
|
+
const indexed = Buffer.from(indexReader(projectRoot, spec.path, options.execGit));
|
|
328
|
+
if (!current.equals(update.baseline) && !current.equals(update.content)) {
|
|
329
|
+
throw new Error(`${spec.path} has unrelated local edits; restore or commit them before updating Bun pins.`);
|
|
330
|
+
}
|
|
331
|
+
if (!indexed.equals(update.baseline) && !indexed.equals(update.content)) {
|
|
332
|
+
throw new Error(`${spec.path} has unrelated staged edits; restore or commit them before updating Bun pins.`);
|
|
333
|
+
}
|
|
334
|
+
return { ...update, current, indexed };
|
|
335
|
+
});
|
|
336
|
+
const npmPath = npmWorkflow.NPM_PUBLISH_WORKFLOW_PATH;
|
|
337
|
+
const sourceVersion = readPinnedBunVersion((options.readSourcePackageManifest || readSourcePackageManifest)(
|
|
338
|
+
projectRoot,
|
|
339
|
+
sourceHead,
|
|
340
|
+
options.execGit,
|
|
341
|
+
));
|
|
342
|
+
const npmBaseline = Buffer.from((options.readSourceNpmPublishWorkflow || npmWorkflow.readSourceNpmPublishWorkflow)(
|
|
343
|
+
projectRoot,
|
|
344
|
+
sourceHead,
|
|
345
|
+
options.execGit,
|
|
346
|
+
));
|
|
347
|
+
if (!npmBaseline.equals(Buffer.from(npmWorkflow.renderNpmPublishWorkflow(sourceVersion)))) {
|
|
348
|
+
throw new Error(`${npmPath} at ${sourceHead} differs from its canonical generator output.`);
|
|
349
|
+
}
|
|
350
|
+
const npmContent = Buffer.from(npmWorkflow.renderNpmPublishWorkflow(updates[0].version));
|
|
351
|
+
const npmOriginal = snapshotFile(projectRoot, npmPath);
|
|
352
|
+
const npmIndexed = Buffer.from((options.readIndexedNpmPublishWorkflow || npmWorkflow.readIndexedNpmPublishWorkflow)(
|
|
353
|
+
projectRoot,
|
|
354
|
+
options.execGit,
|
|
355
|
+
));
|
|
356
|
+
if (!npmOriginal.existed || (!npmOriginal.content.equals(npmBaseline) && !npmOriginal.content.equals(npmContent))) {
|
|
357
|
+
throw new Error(`${npmPath} has unrelated local edits; restore or commit them before updating Bun pins.`);
|
|
358
|
+
}
|
|
359
|
+
if (!npmIndexed.equals(npmBaseline) && !npmIndexed.equals(npmContent)) {
|
|
360
|
+
throw new Error(`${npmPath} has unrelated staged edits; restore or commit them before updating Bun pins.`);
|
|
361
|
+
}
|
|
362
|
+
npmUpdate = {
|
|
363
|
+
path: npmPath,
|
|
364
|
+
original: npmOriginal,
|
|
365
|
+
content: npmContent,
|
|
366
|
+
writeOptions: {
|
|
367
|
+
actor,
|
|
368
|
+
operation: 'generate_npm_workflow',
|
|
369
|
+
viaForgeApi: true,
|
|
370
|
+
surface: 'workflows',
|
|
371
|
+
},
|
|
372
|
+
recoveryOperation: 'recover_npm_workflow',
|
|
373
|
+
};
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return { success: false, error: error.message };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const authority = require('./protected-state-authority');
|
|
379
|
+
const issueAuthorization = options.issueAuthorization || authority.issueBunWorkflowAuthorization;
|
|
380
|
+
const completeAuthorization = options.completeAuthorization || authority.completeBunWorkflowAuthorization;
|
|
381
|
+
const cancelAuthorizations = options.cancelAuthorizations || authority.cancelBunWorkflowBatchAuthorizations;
|
|
382
|
+
const protectedWriter = options.writeProtectedFile || writeProtectedFile;
|
|
383
|
+
const protectedRemover = options.removeProtectedFile || removeProtectedFile;
|
|
384
|
+
const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
|
|
385
|
+
const results = [];
|
|
386
|
+
const attempts = [];
|
|
387
|
+
const authorizations = [];
|
|
388
|
+
const context = {
|
|
389
|
+
actor,
|
|
390
|
+
sourceHead,
|
|
391
|
+
version: updates[0].version,
|
|
392
|
+
kernelDeps: options.kernelDeps,
|
|
393
|
+
createCapabilityId: options.createCapabilityId || randomUUID,
|
|
394
|
+
issueAuthorization,
|
|
395
|
+
completeAuthorization,
|
|
396
|
+
cancelAuthorizations,
|
|
397
|
+
protectedWriter,
|
|
398
|
+
protectedRemover,
|
|
399
|
+
auditWriter,
|
|
400
|
+
attempts,
|
|
401
|
+
authorizations,
|
|
402
|
+
};
|
|
403
|
+
for (const update of updates) {
|
|
404
|
+
const applied = await applyWorkflowUpdate(projectRoot, update, context);
|
|
405
|
+
if (!applied.success) {
|
|
406
|
+
return failedUpdate(applied.error, results, attempts, authorizations, projectRoot, context);
|
|
407
|
+
}
|
|
408
|
+
results.push(applied.result);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const npmGenerator = options.generateNpmPublishWorkflow || npmWorkflow.generateNpmPublishWorkflow;
|
|
412
|
+
attempts.push(npmUpdate);
|
|
413
|
+
const npmAuthorization = {
|
|
414
|
+
actor,
|
|
415
|
+
path: npmUpdate.path,
|
|
416
|
+
sourceHead,
|
|
417
|
+
capabilityId: context.createCapabilityId(),
|
|
418
|
+
};
|
|
419
|
+
authorizations.push(npmAuthorization);
|
|
420
|
+
let npm;
|
|
421
|
+
try {
|
|
422
|
+
npm = await npmGenerator(projectRoot, {
|
|
423
|
+
actor,
|
|
424
|
+
env: options.env,
|
|
425
|
+
kernelDeps: options.kernelDeps,
|
|
426
|
+
expectedHead: sourceHead,
|
|
427
|
+
resolveHead: options.resolveHead,
|
|
428
|
+
bunVersion: updates[0].version,
|
|
429
|
+
targetBunVersion: updates[0].version,
|
|
430
|
+
capabilityId: npmAuthorization.capabilityId,
|
|
431
|
+
expectedSnapshot: npmUpdate.original,
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return failedUpdate(error.message, results, attempts, authorizations, projectRoot, context);
|
|
435
|
+
}
|
|
436
|
+
if (!npm.success) {
|
|
437
|
+
return failedUpdate(npm.error, results, attempts, authorizations, projectRoot, context, { npm });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
success: true,
|
|
442
|
+
version: updates[0].version,
|
|
443
|
+
paths: [...results.map(result => result.path), npm.path],
|
|
444
|
+
results,
|
|
445
|
+
npm,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
BUN_WORKFLOW_SPECS,
|
|
451
|
+
readPinnedBunVersion,
|
|
452
|
+
renderBunWorkflowPin,
|
|
453
|
+
readSourceWorkflow,
|
|
454
|
+
readIndexedWorkflow,
|
|
455
|
+
readIndexedPackageManifest,
|
|
456
|
+
readSourcePackageManifest,
|
|
457
|
+
resolveBunVersionTarget,
|
|
458
|
+
resolveCurrentHead,
|
|
459
|
+
deriveBunWorkflowUpdate,
|
|
460
|
+
updateBunWorkflowPins,
|
|
461
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const RESULT_STATUSES = Object.freeze(['PASS', 'INCOMPLETE']);
|
|
6
|
+
const AVAILABILITY_STATUSES = Object.freeze(['AVAILABLE', 'UNAVAILABLE']);
|
|
7
|
+
const CAPABILITY_STATUSES = Object.freeze(['AVAILABLE', 'UNAVAILABLE', 'INCOMPLETE']);
|
|
8
|
+
|
|
9
|
+
function assertEnum(value, allowed, field) {
|
|
10
|
+
if (!allowed.includes(value)) {
|
|
11
|
+
throw new TypeError(`${field} must be one of: ${allowed.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertOptionalString(value, field) {
|
|
16
|
+
if (value !== null && (typeof value !== 'string' || value.length === 0)) {
|
|
17
|
+
throw new TypeError(`${field} must be null or a non-empty string`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeCapability(capability) {
|
|
22
|
+
if (!capability || typeof capability !== 'object' || Array.isArray(capability)) {
|
|
23
|
+
throw new TypeError('capability must be an object');
|
|
24
|
+
}
|
|
25
|
+
if (typeof capability.id !== 'string' || capability.id.length === 0) {
|
|
26
|
+
throw new TypeError('capability.id must be a non-empty string');
|
|
27
|
+
}
|
|
28
|
+
if (typeof capability.available !== 'boolean') {
|
|
29
|
+
throw new TypeError('capability.available must be a boolean');
|
|
30
|
+
}
|
|
31
|
+
assertEnum(capability.status, CAPABILITY_STATUSES, 'capability.status');
|
|
32
|
+
if (typeof capability.reason !== 'string' || capability.reason.length === 0) {
|
|
33
|
+
throw new TypeError('capability.reason must be a non-empty string');
|
|
34
|
+
}
|
|
35
|
+
if (typeof capability.probe_id !== 'string' || capability.probe_id.length === 0) {
|
|
36
|
+
throw new TypeError('capability.probe_id must be a non-empty string');
|
|
37
|
+
}
|
|
38
|
+
if (capability.available !== (capability.status === 'AVAILABLE')) {
|
|
39
|
+
throw new TypeError('capability.available must agree with capability.status');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: capability.id,
|
|
43
|
+
available: capability.available,
|
|
44
|
+
status: capability.status,
|
|
45
|
+
reason: capability.reason,
|
|
46
|
+
probe_id: capability.probe_id,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compareIds(left, right) {
|
|
51
|
+
if (left.id < right.id) return -1;
|
|
52
|
+
if (left.id > right.id) return 1;
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hashProjection(result) {
|
|
57
|
+
return {
|
|
58
|
+
schema_version: result.schema_version,
|
|
59
|
+
probe_revision: result.probe_revision,
|
|
60
|
+
harness_id: result.harness_id,
|
|
61
|
+
status: result.status,
|
|
62
|
+
availability: result.availability,
|
|
63
|
+
executable: {
|
|
64
|
+
command: result.executable.command,
|
|
65
|
+
identity: result.executable.identity,
|
|
66
|
+
version: result.executable.version,
|
|
67
|
+
},
|
|
68
|
+
capabilities: result.capabilities
|
|
69
|
+
.map(normalizeCapability)
|
|
70
|
+
.sort(compareIds),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function computeProbeResultHash(result) {
|
|
75
|
+
return createHash('sha256')
|
|
76
|
+
.update(JSON.stringify(hashProjection(result)), 'utf8')
|
|
77
|
+
.digest('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function deepFreeze(value) {
|
|
81
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
82
|
+
Object.freeze(value);
|
|
83
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createProbeResult(input) {
|
|
89
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
90
|
+
throw new TypeError('probe result input must be an object');
|
|
91
|
+
}
|
|
92
|
+
if (typeof input.harnessId !== 'string' || input.harnessId.length === 0) {
|
|
93
|
+
throw new TypeError('harnessId must be a non-empty string');
|
|
94
|
+
}
|
|
95
|
+
if (typeof input.probeRevision !== 'string' || input.probeRevision.length === 0) {
|
|
96
|
+
throw new TypeError('probeRevision must be a non-empty string');
|
|
97
|
+
}
|
|
98
|
+
assertEnum(input.status, RESULT_STATUSES, 'status');
|
|
99
|
+
assertEnum(input.availability, AVAILABILITY_STATUSES, 'availability');
|
|
100
|
+
if (!input.executable || typeof input.executable !== 'object' || Array.isArray(input.executable)) {
|
|
101
|
+
throw new TypeError('executable must be an object');
|
|
102
|
+
}
|
|
103
|
+
if (typeof input.executable.command !== 'string' || input.executable.command.length === 0) {
|
|
104
|
+
throw new TypeError('executable.command must be a non-empty string');
|
|
105
|
+
}
|
|
106
|
+
assertOptionalString(input.executable.identity, 'executable.identity');
|
|
107
|
+
assertOptionalString(input.executable.version, 'executable.version');
|
|
108
|
+
if (!Array.isArray(input.capabilities)) {
|
|
109
|
+
throw new TypeError('capabilities must be an array');
|
|
110
|
+
}
|
|
111
|
+
const observedAt = new Date(input.observedAt);
|
|
112
|
+
if (!Number.isFinite(observedAt.getTime())) {
|
|
113
|
+
throw new TypeError('observedAt must be a valid timestamp');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result = {
|
|
117
|
+
schema_version: 1,
|
|
118
|
+
probe_revision: input.probeRevision,
|
|
119
|
+
harness_id: input.harnessId,
|
|
120
|
+
status: input.status,
|
|
121
|
+
availability: input.availability,
|
|
122
|
+
observed_at: observedAt.toISOString(),
|
|
123
|
+
executable: {
|
|
124
|
+
command: input.executable.command,
|
|
125
|
+
identity: input.executable.identity,
|
|
126
|
+
version: input.executable.version,
|
|
127
|
+
},
|
|
128
|
+
capabilities: input.capabilities.map(normalizeCapability)
|
|
129
|
+
.sort(compareIds),
|
|
130
|
+
};
|
|
131
|
+
result.result_hash = computeProbeResultHash(result);
|
|
132
|
+
return deepFreeze(result);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
AVAILABILITY_STATUSES,
|
|
137
|
+
CAPABILITY_STATUSES,
|
|
138
|
+
RESULT_STATUSES,
|
|
139
|
+
computeProbeResultHash,
|
|
140
|
+
createProbeResult,
|
|
141
|
+
};
|