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/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-workflow",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
|
+
"packageManager": "bun@1.4.2",
|
|
4
5
|
"description": "Local runtime control plane for AI-assisted engineering workflows, gates, evidence, and all AI agents",
|
|
5
6
|
"bin": {
|
|
6
7
|
"forge": "bin/forge.js",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"test:benchmark:full": "node scripts/benchmark.js --output test-results/benchmark-full-results.json --profile-dir test-results/benchmark-full-profiles --group full-suite --group validate",
|
|
35
36
|
"test:full:parallel": "node scripts/test-full-suite.js",
|
|
36
37
|
"test:dashboard": "node scripts/test-dashboard.js",
|
|
37
|
-
"test:profile": "node scripts/test-profile.js --input-dir test-results --output test-results/test-profile.json --label local",
|
|
38
|
+
"test:profile": "node scripts/test-profile.js --input-dir test-results --output test-results/test-profile.profile.json --label local",
|
|
38
39
|
"test:randomized": "bun test --timeout 15000 --randomize",
|
|
39
40
|
"test:rerun": "bun test --timeout 15000 --rerun-each 2",
|
|
40
41
|
"agentic:sync": "node scripts/sync-agentic-workflow.js"
|
|
@@ -53,11 +54,11 @@
|
|
|
53
54
|
"@eslint/js": "^10.0.1",
|
|
54
55
|
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
|
55
56
|
"@stryker-mutator/core": "^9.6.0",
|
|
56
|
-
"c8": "^
|
|
57
|
+
"c8": "^12.0.0",
|
|
57
58
|
"eslint": "^10.0.2",
|
|
58
59
|
"eslint-plugin-sonarjs": "^4.1.0",
|
|
59
60
|
"globals": "^17.3.0",
|
|
60
|
-
"js-yaml": "^5.
|
|
61
|
+
"js-yaml": "^5.2.3",
|
|
61
62
|
"lefthook": "^2.1.4",
|
|
62
63
|
"typescript": "7.0.2"
|
|
63
64
|
},
|
|
@@ -96,13 +97,12 @@
|
|
|
96
97
|
".cursor/",
|
|
97
98
|
"!.cursor/node_modules/",
|
|
98
99
|
"scripts/",
|
|
99
|
-
"!scripts/github-beads-sync/",
|
|
100
|
-
"!scripts/github-beads-sync.config.json",
|
|
101
100
|
"!scripts/**/*.test.js",
|
|
102
101
|
".github/PLUGIN_TEMPLATE.json",
|
|
103
102
|
".forge/hooks/",
|
|
104
103
|
".forge/protected-paths.yaml",
|
|
105
104
|
"CHANGELOG.md",
|
|
105
|
+
"CODING_STANDARDS.md",
|
|
106
106
|
"QUICKSTART.md",
|
|
107
107
|
"docs/*.md",
|
|
108
108
|
"docs/architecture/**/*.md",
|
|
@@ -117,16 +117,19 @@
|
|
|
117
117
|
],
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@babel/parser": "^8.0.0",
|
|
120
|
+
"cross-spawn": "^7.0.6",
|
|
120
121
|
"fastest-levenshtein": "^1.0.16",
|
|
121
122
|
"yaml": "^2.8.3"
|
|
122
123
|
},
|
|
123
124
|
"overrides": {
|
|
124
125
|
"eslint": "^10.0.2",
|
|
125
|
-
"fast-uri": "^3.1.
|
|
126
|
+
"fast-uri": "^3.1.4",
|
|
126
127
|
"flatted": "3.4.2",
|
|
127
128
|
"minimatch": "10.2.4",
|
|
128
|
-
"brace-expansion": "5.0.
|
|
129
|
-
"
|
|
129
|
+
"brace-expansion": "5.0.9",
|
|
130
|
+
"linkify-it": "^5.0.2",
|
|
131
|
+
"lodash": "4.18.1",
|
|
132
|
+
"qs": "6.16.0"
|
|
130
133
|
},
|
|
131
134
|
"c8": {
|
|
132
135
|
"lines": 80,
|
package/scripts/commitlint.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Cross-platform commitlint runner for lefthook commit-msg hook.
|
|
4
|
-
*
|
|
5
|
-
* Works on Windows CMD, PowerShell, macOS, Linux.
|
|
4
|
+
* Executes the installed commitlint CLI directly on Windows, macOS, and Linux.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
const { spawnSync } = require('node:child_process');
|
|
9
8
|
|
|
10
|
-
// On Windows, npx is npx.cmd — shell: true resolves .cmd extensions
|
|
11
|
-
const isWindows = process.platform === 'win32';
|
|
12
|
-
|
|
13
9
|
const commitMsgFile = process.argv[2];
|
|
14
10
|
if (!commitMsgFile) {
|
|
15
11
|
// Write synchronously so the message is flushed before exit: on Windows the
|
|
@@ -18,17 +14,19 @@ if (!commitMsgFile) {
|
|
|
18
14
|
process.exit(1);
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
|
|
17
|
+
let commitlintCli;
|
|
18
|
+
try {
|
|
19
|
+
commitlintCli = require.resolve('@commitlint/cli/cli.js');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`❌ Failed to resolve commitlint: ${error.message}`);
|
|
22
|
+
console.error(' Install project dependencies before committing.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
27
25
|
|
|
28
26
|
const result = spawnSync(
|
|
29
|
-
|
|
30
|
-
[
|
|
31
|
-
{ stdio: 'inherit', shell:
|
|
27
|
+
process.execPath,
|
|
28
|
+
[commitlintCli, '--edit', commitMsgFile],
|
|
29
|
+
{ stdio: 'inherit', shell: false }
|
|
32
30
|
);
|
|
33
31
|
|
|
34
32
|
if (result.error) {
|
|
@@ -40,5 +38,5 @@ if (result.error) {
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
if (result.status !== 0) {
|
|
43
|
-
process.exit(result.status);
|
|
41
|
+
process.exit(result.status ?? 1);
|
|
44
42
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs (or lists) the test suites that assert on changed markdown.
|
|
6
|
+
*
|
|
7
|
+
* A markdown-only PR does not match the `paths:` filter in
|
|
8
|
+
* `.github/workflows/test.yml`, so the full matrix never runs and
|
|
9
|
+
* `required-checks-bypass.yml` reports the required "Test Suite" check green.
|
|
10
|
+
* Several suites nevertheless assert on markdown CONTENT, so such a PR merged
|
|
11
|
+
* green and then broke master for the next unrelated code PR — twice (kernel
|
|
12
|
+
* issue 63556816: the README size badge in #307/#310, and the AGENTS.md
|
|
13
|
+
* convention test after #325).
|
|
14
|
+
*
|
|
15
|
+
* This script closes that gap by running exactly the doc-asserting suites for the
|
|
16
|
+
* changed markdown. It shares `lib/doc-assertions.js` with the local push lane
|
|
17
|
+
* (`lib/commands/test.js`), so CI and local selection cannot drift apart.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node scripts/doc-asserting-tests.js [--base <ref>] [--list]
|
|
21
|
+
*
|
|
22
|
+
* --base <ref> Compare against <ref> (default: origin/<default-branch> or HEAD~1).
|
|
23
|
+
* --list Print the selected suites instead of running them.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const { selectDocAssertingTests } = require('../lib/doc-assertions');
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
33
|
+
|
|
34
|
+
/** Wall-clock ceiling for the doc-asserting lane; it is a small, fast subset. */
|
|
35
|
+
const LANE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses the supported command-line flags.
|
|
39
|
+
*
|
|
40
|
+
* @param {string[]} argv Raw arguments (without node/script).
|
|
41
|
+
* @returns {{base: string|null, list: boolean}} Parsed options.
|
|
42
|
+
*/
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const baseIndex = argv.indexOf('--base');
|
|
45
|
+
return {
|
|
46
|
+
base: baseIndex !== -1 ? argv[baseIndex + 1] || null : null,
|
|
47
|
+
list: argv.includes('--list'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the ref the PR should be compared against.
|
|
53
|
+
*
|
|
54
|
+
* @param {string|null} explicitBase Base supplied with `--base`.
|
|
55
|
+
* @returns {string} A git ref usable in `git diff <ref>...HEAD`.
|
|
56
|
+
*/
|
|
57
|
+
function resolveBase(explicitBase) {
|
|
58
|
+
if (explicitBase) return explicitBase;
|
|
59
|
+
for (const ref of ['origin/master', 'origin/main']) {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync('git', ['rev-parse', '--verify', ref], { cwd: REPO_ROOT, stdio: 'pipe', timeout: 5000 });
|
|
62
|
+
return ref;
|
|
63
|
+
} catch (_e) { /* intentional: ref not present in this checkout, try next */ } // NOSONAR S2486
|
|
64
|
+
}
|
|
65
|
+
return 'HEAD~1';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parses `git diff --name-only` output into repository-relative paths.
|
|
70
|
+
*
|
|
71
|
+
* Split out so the parsing contract (trimmed, no blanks) is testable without a live
|
|
72
|
+
* repository: CI checks out shallow, so a test that reaches for `HEAD~1` passes locally
|
|
73
|
+
* and fails on the runner.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} output Raw `git diff --name-only` stdout.
|
|
76
|
+
* @returns {string[]} Changed file paths.
|
|
77
|
+
*/
|
|
78
|
+
function parseChangedFiles(output) {
|
|
79
|
+
return output.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Lists repository-relative paths changed against the base ref.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} base Base git ref.
|
|
86
|
+
* @returns {string[]} Changed file paths.
|
|
87
|
+
*/
|
|
88
|
+
function changedFilesSince(base) {
|
|
89
|
+
const output = execFileSync('git', ['diff', '--name-only', `${base}...HEAD`], {
|
|
90
|
+
cwd: REPO_ROOT,
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
timeout: 15000,
|
|
93
|
+
});
|
|
94
|
+
return parseChangedFiles(output);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Turns a `spawnSync` result into a process exit status.
|
|
99
|
+
*
|
|
100
|
+
* A spawn that never produced a status (`error`, or killed by a signal so `status`
|
|
101
|
+
* is null) MUST report FAILURE. Reporting 0 there would re-create the bug this lane
|
|
102
|
+
* exists to remove: a required check reporting green without running the tests.
|
|
103
|
+
*
|
|
104
|
+
* @param {{status: number|null, error?: Error}} result Result of `spawnSync`.
|
|
105
|
+
* @returns {number} Exit status; non-zero whenever the run did not demonstrably pass.
|
|
106
|
+
*/
|
|
107
|
+
function resolveSpawnStatus(result) {
|
|
108
|
+
if (!result || result.error) return 1;
|
|
109
|
+
return result.status ?? 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function main() {
|
|
113
|
+
const { base: explicitBase, list } = parseArgs(process.argv.slice(2));
|
|
114
|
+
const base = resolveBase(explicitBase);
|
|
115
|
+
|
|
116
|
+
let changedFiles;
|
|
117
|
+
try {
|
|
118
|
+
changedFiles = changedFilesSince(base);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Fail closed: if the change set cannot be determined we cannot prove the
|
|
121
|
+
// doc-asserting suites are unaffected.
|
|
122
|
+
console.error(`Could not compute changed files against ${base}: ${error.message}`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const suites = selectDocAssertingTests(changedFiles, REPO_ROOT, fs);
|
|
127
|
+
|
|
128
|
+
if (suites.length === 0) {
|
|
129
|
+
console.log('No changed markdown asserts on by any test suite — nothing to run.');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (list) {
|
|
134
|
+
console.log(suites.join('\n'));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`Running ${suites.length} doc-asserting suite${suites.length === 1 ? '' : 's'} for changed markdown:`);
|
|
139
|
+
for (const suite of suites) console.log(` ${suite}`);
|
|
140
|
+
|
|
141
|
+
const result = spawnSync('bun', ['test', ...suites], {
|
|
142
|
+
cwd: REPO_ROOT,
|
|
143
|
+
stdio: 'inherit',
|
|
144
|
+
timeout: LANE_TIMEOUT_MS,
|
|
145
|
+
shell: process.platform === 'win32',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.error) {
|
|
149
|
+
console.error(`Failed to run doc-asserting suites: ${result.error.message}`);
|
|
150
|
+
}
|
|
151
|
+
return resolveSpawnStatus(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (require.main === module) {
|
|
155
|
+
process.exit(main());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { changedFilesSince, parseArgs, parseChangedFiles, resolveBase, resolveSpawnStatus };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { parse } = require('yaml');
|
|
7
|
+
const { stableStringify } = require('../lib/kernel/evaluators');
|
|
8
|
+
const { hashManifest, validateManifest } = require('../lib/validation/risk-manifest');
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
const SOURCE_PATH = path.join(ROOT, 'validation', 'risk-manifest.source.yaml');
|
|
13
|
+
const OUTPUT_PATH = path.join(ROOT, 'validation', 'risk-manifest.v1.json');
|
|
14
|
+
|
|
15
|
+
function sha256(value) {
|
|
16
|
+
return `sha256:${crypto.createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function compareText(left, right) {
|
|
20
|
+
if (left === right) return 0;
|
|
21
|
+
return left < right ? -1 : 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortById(entries) {
|
|
25
|
+
return [...entries].sort((left, right) => compareText(left.id, right.id));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeSource(source) {
|
|
29
|
+
const canonicalSource = source.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
30
|
+
const parsed = parse(canonicalSource);
|
|
31
|
+
const manifest = {
|
|
32
|
+
schema_id: parsed.schema_id,
|
|
33
|
+
revision: parsed.revision,
|
|
34
|
+
source_hash: sha256(canonicalSource),
|
|
35
|
+
generator_version: 'forge-risk-manifest-generator.v1',
|
|
36
|
+
commands: sortById(parsed.commands).map((command) => ({
|
|
37
|
+
...command,
|
|
38
|
+
argv: [...command.argv],
|
|
39
|
+
})),
|
|
40
|
+
gates: sortById(parsed.gates).map((gate) => ({
|
|
41
|
+
...gate,
|
|
42
|
+
command_ids: [...gate.command_ids].sort(compareText),
|
|
43
|
+
})),
|
|
44
|
+
lanes: sortById(parsed.lanes).map((lane) => ({
|
|
45
|
+
...lane,
|
|
46
|
+
command_ids: [...lane.command_ids].sort(compareText),
|
|
47
|
+
})),
|
|
48
|
+
risks: sortById(parsed.risks).map((risk) => ({
|
|
49
|
+
...risk,
|
|
50
|
+
gate_ids: [...risk.gate_ids].sort(),
|
|
51
|
+
})),
|
|
52
|
+
owners: sortById(parsed.owners).map((owner) => ({
|
|
53
|
+
...owner,
|
|
54
|
+
risk_ids: [...owner.risk_ids].sort(),
|
|
55
|
+
canonical_test_ids: [...owner.canonical_test_ids].sort(),
|
|
56
|
+
dependent_routes: [...owner.dependent_routes].sort(),
|
|
57
|
+
platform_runtime_additions: [...owner.platform_runtime_additions].sort(),
|
|
58
|
+
lanes: [...owner.lanes].sort(),
|
|
59
|
+
package_roots: [...owner.package_roots].sort(),
|
|
60
|
+
selectors: [...owner.selectors].sort((left, right) => compareText(left.kind, right.kind)
|
|
61
|
+
|| compareText(left.prefix, right.prefix)),
|
|
62
|
+
})),
|
|
63
|
+
unknown_owner_fallback: {
|
|
64
|
+
...parsed.unknown_owner_fallback,
|
|
65
|
+
required_gates: [...parsed.unknown_owner_fallback.required_gates].sort(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
manifest.manifest_hash = hashManifest(manifest);
|
|
69
|
+
validateManifest(manifest);
|
|
70
|
+
return JSON.parse(stableStringify(manifest));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateRiskManifest(source) {
|
|
74
|
+
return `${JSON.stringify(normalizeSource(source), null, 2)}\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main(args = process.argv.slice(2)) {
|
|
78
|
+
const generated = generateRiskManifest(fs.readFileSync(SOURCE_PATH, 'utf8'));
|
|
79
|
+
if (args.includes('--check')) {
|
|
80
|
+
if (!fs.existsSync(OUTPUT_PATH) || fs.readFileSync(OUTPUT_PATH, 'utf8') !== generated) {
|
|
81
|
+
console.error('Risk manifest is stale. Run: node scripts/generate-risk-manifest.js');
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(OUTPUT_PATH, generated, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (require.main === module) main();
|
|
90
|
+
|
|
91
|
+
module.exports = { generateRiskManifest, normalizeSource };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# GH_CMD bridge: re-enter this Forge runtime without credentials in the Bash parent.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
: "${FORGE_GITHUB_RUNTIME:?Forge GitHub runtime is required}"
|
|
6
|
+
if [[ -n "${FORGE_GITHUB_ENTRY:-}" ]]; then
|
|
7
|
+
exec "$FORGE_GITHUB_RUNTIME" "$FORGE_GITHUB_ENTRY" github run -- gh "$@"
|
|
8
|
+
else
|
|
9
|
+
exec "$FORGE_GITHUB_RUNTIME" github run -- gh "$@"
|
|
10
|
+
fi
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
createBuiltinSQLiteDriver,
|
|
8
|
+
hardenBackupPermissions,
|
|
9
|
+
hardenBackupPermissionsBatch,
|
|
10
|
+
} = require('../lib/kernel/sqlite-driver');
|
|
11
|
+
const {
|
|
12
|
+
ClaimRepairError,
|
|
13
|
+
createVerifiedClaimRepairBackup,
|
|
14
|
+
} = require('../lib/kernel/legacy-claim-repair');
|
|
15
|
+
|
|
16
|
+
const HELP = `Usage:
|
|
17
|
+
bun scripts/legacy-claim-repair.js --dry-run --database <kernel.sqlite> --backup <separate.sqlite> --at <canonical-UTC>
|
|
18
|
+
bun scripts/legacy-claim-repair.js --apply --database <kernel.sqlite> --backup <separate.sqlite> --at <canonical-UTC> --approved-digest <sha256> --actor <identity>
|
|
19
|
+
|
|
20
|
+
The tool never discovers or repairs a database automatically. Apply is rejected
|
|
21
|
+
without a verified restorable backup and the exact human-approved dry-run digest.`;
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv = []) {
|
|
24
|
+
const result = { mode: null };
|
|
25
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
26
|
+
const arg = argv[index];
|
|
27
|
+
if (arg === '--help' || arg === '-h') return { help: true };
|
|
28
|
+
if (arg === '--dry-run' || arg === '--apply') {
|
|
29
|
+
const mode = arg.slice(2);
|
|
30
|
+
if (result.mode && result.mode !== mode) throw new Error('Choose exactly one of --dry-run or --apply');
|
|
31
|
+
result.mode = mode;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const names = {
|
|
35
|
+
'--database': 'databasePath',
|
|
36
|
+
'--backup': 'backupPath',
|
|
37
|
+
'--at': 'observedAt',
|
|
38
|
+
'--approved-digest': 'approvedDigest',
|
|
39
|
+
'--actor': 'actor',
|
|
40
|
+
};
|
|
41
|
+
if (!Object.hasOwn(names, arg)) throw new Error(`Unknown argument: ${arg}`);
|
|
42
|
+
const name = names[arg];
|
|
43
|
+
const value = argv[index + 1];
|
|
44
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
|
|
45
|
+
result[name] = value;
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
if (!result.mode) throw new Error('Choose exactly one of --dry-run or --apply');
|
|
49
|
+
for (const required of ['databasePath', 'backupPath', 'observedAt']) {
|
|
50
|
+
if (!result[required]) throw new Error(`Missing required option: ${required}`);
|
|
51
|
+
}
|
|
52
|
+
for (const requiredPath of ['databasePath', 'backupPath']) {
|
|
53
|
+
if (!path.isAbsolute(result[requiredPath])) {
|
|
54
|
+
throw new Error(`${requiredPath} must be an absolute path`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (result.mode === 'apply' && !result.approvedDigest) {
|
|
58
|
+
throw new Error('--apply requires --approved-digest');
|
|
59
|
+
}
|
|
60
|
+
if (result.mode === 'apply' && !result.actor) {
|
|
61
|
+
throw new Error('--apply requires --actor');
|
|
62
|
+
}
|
|
63
|
+
if (result.mode === 'dry-run' && result.approvedDigest) {
|
|
64
|
+
throw new Error('--approved-digest is valid only with --apply');
|
|
65
|
+
}
|
|
66
|
+
if (result.mode === 'dry-run' && result.actor) {
|
|
67
|
+
throw new Error('--actor is valid only with --apply');
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function run(options, dependencies = {}) {
|
|
73
|
+
const openDriver = dependencies.openDriver
|
|
74
|
+
|| (databasePath => createBuiltinSQLiteDriver({ databasePath }));
|
|
75
|
+
const createBackup = dependencies.createVerifiedClaimRepairBackup
|
|
76
|
+
|| createVerifiedClaimRepairBackup;
|
|
77
|
+
const hardenPath = dependencies.hardenPath || hardenBackupPermissions;
|
|
78
|
+
const hardenPaths = dependencies.hardenPaths
|
|
79
|
+
|| (dependencies.hardenPath ? dependencies.hardenPath.batch : hardenBackupPermissionsBatch);
|
|
80
|
+
const sourceDriver = openDriver(options.databasePath);
|
|
81
|
+
try {
|
|
82
|
+
if (options.mode === 'dry-run') {
|
|
83
|
+
const backup = await createBackup({
|
|
84
|
+
sourceDriver,
|
|
85
|
+
backupPath: options.backupPath,
|
|
86
|
+
observedAt: options.observedAt,
|
|
87
|
+
openDriver,
|
|
88
|
+
hardenPath,
|
|
89
|
+
hardenPaths,
|
|
90
|
+
});
|
|
91
|
+
const preflight = await sourceDriver.preflightLegacyClaimRepair({ observedAt: options.observedAt });
|
|
92
|
+
if (preflight.digest !== backup.plan_digest) {
|
|
93
|
+
throw new ClaimRepairError(
|
|
94
|
+
'CLAIM_REPAIR_BACKUP_DRIFT',
|
|
95
|
+
'Live claim authority changed while binding the reported preflight to its verified backup',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, preflight, backup };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const receipt = await sourceDriver.applyLegacyClaimRepair({
|
|
102
|
+
observedAt: options.observedAt,
|
|
103
|
+
approvedDigest: options.approvedDigest,
|
|
104
|
+
backupPath: options.backupPath,
|
|
105
|
+
actor: options.actor,
|
|
106
|
+
});
|
|
107
|
+
return { ok: true, receipt };
|
|
108
|
+
} finally {
|
|
109
|
+
sourceDriver.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main(argv = process.argv.slice(2)) {
|
|
114
|
+
try {
|
|
115
|
+
const options = parseArgs(argv);
|
|
116
|
+
if (options.help) {
|
|
117
|
+
process.stdout.write(`${HELP}\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`${JSON.stringify(await run(options), null, 2)}\n`);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const recoveryPath = error?.code === 'CLAIM_REPAIR_BACKUP_POSTCOMMIT_DRIFT'
|
|
123
|
+
&& typeof error?.details?.recovery_path === 'string'
|
|
124
|
+
? error.details.recovery_path
|
|
125
|
+
: null;
|
|
126
|
+
process.stderr.write(`${JSON.stringify({
|
|
127
|
+
ok: false,
|
|
128
|
+
error: {
|
|
129
|
+
code: error?.code || 'CLAIM_REPAIR_USAGE',
|
|
130
|
+
message: error?.message || String(error),
|
|
131
|
+
...(recoveryPath ? { recovery_path: recoveryPath } : {}),
|
|
132
|
+
},
|
|
133
|
+
}, null, 2)}\n`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (require.main === module) void main();
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
HELP,
|
|
142
|
+
main,
|
|
143
|
+
parseArgs,
|
|
144
|
+
run,
|
|
145
|
+
};
|