forge-workflow 0.1.0-beta.4 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +16 -374
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +54 -25
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +344 -142
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +223 -25
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +17 -2
- package/lib/commands/setup.js +191 -94
- package/lib/commands/shepherd.js +13 -1
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +119 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/test.js +24 -34
- package/lib/commands/worktree.js +220 -42
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +56 -32
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +93 -4
- package/lib/insights.js +96 -80
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +16 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/sqlite-driver.js +153 -29
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +68 -43
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +224 -41
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +10 -1
- package/lib/pr-monitor/watch-lifecycle.js +13 -1
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +33 -1
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/package.json +6 -7
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +176 -43
- package/skills/review/SKILL.md +4 -11
- package/skills/review/evals/scorecard.json +3 -3
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +20 -14
- package/skills/shepherd/evals/scorecard.json +2 -2
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PR-monitor sticky-comment upsert — race-safe, converges to EXACTLY ONE sticky
|
|
5
|
-
* comment per PR even under a concurrent burst of workflow runs.
|
|
6
|
-
*
|
|
7
|
-
* Why this exists: the pr-monitor workflow deliberately has NO `concurrency:`
|
|
8
|
-
* group. A per-PR group does not help — GitHub's queue replacement cancels the
|
|
9
|
-
* previously-PENDING run in a group UNCONDITIONALLY (independent of
|
|
10
|
-
* cancel-in-progress), and this workflow's triggers (check_suite:completed fires
|
|
11
|
-
* ~10+ times per push, plus reviews/comments) burst hard, so a group left a trail
|
|
12
|
-
* of CANCELLED runs that render as red/non-SUCCESS checks and tripped merge-gate
|
|
13
|
-
* tooling (kernel issue 97e6a146). Dropping the group removes the cancellations,
|
|
14
|
-
* but then two concurrent first-runs on a PR with no sticky yet would BOTH find
|
|
15
|
-
* nothing and BOTH create one → duplicate sticky comments. This module closes
|
|
16
|
-
* that race deterministically instead.
|
|
17
|
-
*
|
|
18
|
-
* Reconcile-to-one algorithm:
|
|
19
|
-
* 1. List marker comments. If none, create one, then RE-LIST (a concurrent run
|
|
20
|
-
* may have created its own in the same burst).
|
|
21
|
-
* 2. Pick the deterministic survivor: the LOWEST comment id (oldest). Every
|
|
22
|
-
* concurrent run picks the SAME survivor, so they never fight.
|
|
23
|
-
* 3. Update the survivor with the latest body; delete every other marker
|
|
24
|
-
* comment. Deletes are idempotent (a 404 means a peer already removed it).
|
|
25
|
-
*
|
|
26
|
-
* A create that lands AFTER a run's re-list is self-healed by the next event:
|
|
27
|
-
* every run reconciles to one, and events keep arriving, so the PR converges to a
|
|
28
|
-
* single sticky comment.
|
|
29
|
-
*
|
|
30
|
-
* @module pr-monitor/upsert-sticky
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const { execFileSync } = require('node:child_process');
|
|
34
|
-
|
|
35
|
-
/** Ids of comments whose body carries the sticky marker. */
|
|
36
|
-
function markerCommentIds(comments, marker) {
|
|
37
|
-
return (Array.isArray(comments) ? comments : [])
|
|
38
|
-
.filter((comment) => typeof comment.body === 'string' && comment.body.includes(marker))
|
|
39
|
-
.map((comment) => comment.id);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Ascending by numeric id, so the survivor (index 0) is the oldest comment. */
|
|
43
|
-
function sortIdsAscending(ids) {
|
|
44
|
-
return [...ids].sort((left, right) => Number(left) - Number(right));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Drive a client to exactly one sticky comment. `client` abstracts the GitHub
|
|
49
|
-
* calls so the reconcile logic is unit-testable without the network:
|
|
50
|
-
* - list() → array of { id, body }
|
|
51
|
-
* - create() → create a new sticky comment (body supplied by the client)
|
|
52
|
-
* - update(id) → overwrite comment `id` with the latest body
|
|
53
|
-
* - remove(id) → delete comment `id` (must tolerate an already-deleted 404)
|
|
54
|
-
*
|
|
55
|
-
* @returns {Promise<{ survivor: (number|string|null), deleted: Array<number|string> }>}
|
|
56
|
-
*/
|
|
57
|
-
async function upsertStickyComment({ marker }, client) {
|
|
58
|
-
let ids = markerCommentIds(await client.list(), marker);
|
|
59
|
-
|
|
60
|
-
if (ids.length === 0) {
|
|
61
|
-
await client.create();
|
|
62
|
-
// Re-list: a concurrent run may have created its own sticky in this burst.
|
|
63
|
-
ids = markerCommentIds(await client.list(), marker);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (ids.length === 0) {
|
|
67
|
-
// The just-created comment is not visible yet (eventual consistency); its
|
|
68
|
-
// body is already correct, and the next event will reconcile if a peer raced.
|
|
69
|
-
return { survivor: null, deleted: [] };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
ids = sortIdsAscending(ids);
|
|
73
|
-
const survivor = ids[0];
|
|
74
|
-
await client.update(survivor);
|
|
75
|
-
|
|
76
|
-
const deleted = [];
|
|
77
|
-
for (const id of ids.slice(1)) {
|
|
78
|
-
await client.remove(id);
|
|
79
|
-
deleted.push(id);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { survivor, deleted };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Single choke point for the `gh` CLI, matching lib/commands/merge.js. `gh` is a
|
|
86
|
-
// hardcoded literal (never user input) and args are an array (no shell), so the
|
|
87
|
-
// S4036 PATH-search finding is a false positive in this developer-tool context;
|
|
88
|
-
// one annotation here covers every call site. `encoding: 'utf8'` also pipes
|
|
89
|
-
// stderr onto the thrown error, so isAlreadyGone() below can classify failures.
|
|
90
|
-
function runGh(args) {
|
|
91
|
-
return execFileSync('gh', args, { encoding: 'utf8', windowsHide: true }); // NOSONAR S4036 - hardcoded CLI (gh), args array (no shell), developer-tool context. windowsHide: sticky upsert runs in the detached watcher (issue 931e7924).
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* True only when a `gh api` failure means the target comment is already absent
|
|
96
|
-
* (HTTP 404 / 410) — the benign "a peer got there first" case. Auth failures,
|
|
97
|
-
* rate limits (403), and every other error return false so they propagate and
|
|
98
|
-
* surface a diagnostic instead of silently breaking the exactly-one invariant.
|
|
99
|
-
*/
|
|
100
|
-
function isAlreadyGone(error) {
|
|
101
|
-
const text = `${error && error.stderr ? error.stderr : ''} ${error && error.message ? error.message : ''}`;
|
|
102
|
-
return /HTTP 404|HTTP 410|\bNot Found\b|\bGone\b/i.test(text);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* GitHub-backed client (shells to `gh api`, like lib/pr-monitor/gather.js). The
|
|
107
|
-
* reconcile logic above is unit-tested; the error-tolerance in update/remove is
|
|
108
|
-
* too, via an injectable `run`. create/update both send the same pre-rendered
|
|
109
|
-
* payload file the render step wrote, so the body is identical.
|
|
110
|
-
*/
|
|
111
|
-
function ghStickyClient({ repo, pr, payloadFile, run = runGh }) {
|
|
112
|
-
return {
|
|
113
|
-
async list() {
|
|
114
|
-
const out = run(['api', `repos/${repo}/issues/${pr}/comments`, '--paginate']);
|
|
115
|
-
return out && out.trim() ? JSON.parse(out) : [];
|
|
116
|
-
},
|
|
117
|
-
async create() {
|
|
118
|
-
run(['api', '-X', 'POST', `repos/${repo}/issues/${pr}/comments`, '--input', payloadFile]);
|
|
119
|
-
},
|
|
120
|
-
async update(id) {
|
|
121
|
-
try {
|
|
122
|
-
run(['api', '-X', 'PATCH', `repos/${repo}/issues/comments/${id}`, '--input', payloadFile]);
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if (!isAlreadyGone(error)) {
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
// Our chosen survivor was deleted by a peer whose survivor had a lower id:
|
|
128
|
-
// the peer's sticky wins, exactly-one still holds, so we are done.
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
async remove(id) {
|
|
132
|
-
try {
|
|
133
|
-
run(['api', '-X', 'DELETE', `repos/${repo}/issues/comments/${id}`]);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (!isAlreadyGone(error)) {
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
// Already gone (a concurrent run deleted it first) — the goal state holds.
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function main() {
|
|
145
|
-
const repo = process.env.GH_REPO;
|
|
146
|
-
const pr = process.env.PR;
|
|
147
|
-
const payloadFile = process.env.STICKY_PAYLOAD_FILE || 'monitor-payload.json';
|
|
148
|
-
const { STICKY_MARKER } = require('./render-sticky');
|
|
149
|
-
|
|
150
|
-
const client = ghStickyClient({ repo, pr, payloadFile });
|
|
151
|
-
const { survivor, deleted } = await upsertStickyComment({ marker: STICKY_MARKER }, client);
|
|
152
|
-
const survivorLabel = survivor === null ? 'created (not yet visible)' : survivor;
|
|
153
|
-
console.log(`Sticky comment reconciled to one: survivor=${survivorLabel}, deleted=${deleted.length}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (require.main === module) {
|
|
157
|
-
main().catch((error) => {
|
|
158
|
-
console.error(error.message);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
upsertStickyComment,
|
|
165
|
-
markerCommentIds,
|
|
166
|
-
sortIdsAscending,
|
|
167
|
-
isAlreadyGone,
|
|
168
|
-
ghStickyClient,
|
|
169
|
-
};
|